From bc2b92fd66e14df6f8da1fef63ec21129e3f1ee6 Mon Sep 17 00:00:00 2001 From: Sebastian Good <2230835+scagood@users.noreply.github.com> Date: Mon, 4 Sep 2023 16:10:49 +0300 Subject: [PATCH] feat: Allow for automatic ts mapping detection --- docs/rules/file-extension-in-import.md | 36 +++++++ docs/rules/no-missing-import.md | 8 ++ docs/rules/no-missing-require.md | 8 ++ lib/util/get-typescript-extension-map.js | 101 ++++++++++++++---- package.json | 1 + .../no-missing/ts-extends/base.tsconfig.json | 5 + tests/fixtures/no-missing/ts-extends/d.ts | 0 tests/fixtures/no-missing/ts-extends/e.tsx | 0 .../no-missing/ts-extends/tsconfig.json | 3 + tests/fixtures/no-missing/ts-preserve/d.ts | 0 tests/fixtures/no-missing/ts-preserve/e.tsx | 0 .../no-missing/ts-preserve/tsconfig.json | 5 + tests/fixtures/no-missing/ts-react/d.ts | 0 tests/fixtures/no-missing/ts-react/e.tsx | 0 .../no-missing/ts-react/tsconfig.json | 5 + tests/lib/rules/no-missing-import.js | 58 ++++++++++ tests/lib/rules/no-missing-require.js | 58 ++++++++++ 17 files changed, 269 insertions(+), 19 deletions(-) create mode 100644 tests/fixtures/no-missing/ts-extends/base.tsconfig.json create mode 100644 tests/fixtures/no-missing/ts-extends/d.ts create mode 100644 tests/fixtures/no-missing/ts-extends/e.tsx create mode 100644 tests/fixtures/no-missing/ts-extends/tsconfig.json create mode 100644 tests/fixtures/no-missing/ts-preserve/d.ts create mode 100644 tests/fixtures/no-missing/ts-preserve/e.tsx create mode 100644 tests/fixtures/no-missing/ts-preserve/tsconfig.json create mode 100644 tests/fixtures/no-missing/ts-react/d.ts create mode 100644 tests/fixtures/no-missing/ts-react/e.tsx create mode 100644 tests/fixtures/no-missing/ts-react/tsconfig.json diff --git a/docs/rules/file-extension-in-import.md b/docs/rules/file-extension-in-import.md index 031c2777..3339fa55 100644 --- a/docs/rules/file-extension-in-import.md +++ b/docs/rules/file-extension-in-import.md @@ -90,6 +90,42 @@ import styles from "./styles.css" import logo from "./logo.png" ``` +### Shared Settings + +The following options can be set by [shared settings](http://eslint.org/docs/user-guide/configuring.html#adding-shared-settings). +Several rules have the same option, but we can set this option at once. + +#### typescriptExtensionMap + +Adds the ability to change the extension mapping when converting between typescript and javascript + +You can also use the [typescript compiler jsx options](https://www.typescriptlang.org/tsconfig#jsx) to automatically use the correct mapping. + +If this option is left undefined we: + +1. Check your `tsconfig.json` `compilerOptions.jsx` +2. Return the default mapping (jsx = `preserve`) + +```js +// .eslintrc.js +module.exports = { + "settings": { + "node": { + "typescriptExtensionMap": [ + [ "", ".js" ], + [ ".ts", ".js" ], + [ ".cts", ".cjs" ], + [ ".mts", ".mjs" ], + [ ".tsx", ".jsx" ], + ] + } + }, + "rules": { + "n/file-extension-in-import": "error" + } +} +``` + ## 🔎 Implementation - [Rule source](../../lib/rules/file-extension-in-import.js) diff --git a/docs/rules/no-missing-import.md b/docs/rules/no-missing-import.md index 3b4d9e7b..272f61fb 100644 --- a/docs/rules/no-missing-import.md +++ b/docs/rules/no-missing-import.md @@ -73,6 +73,14 @@ Default is `[]` Adds the ability to change the extension mapping when converting between typescript and javascript +You can also use the [typescript compiler jsx options](https://www.typescriptlang.org/tsconfig#jsx) to automatically use the correct mapping. + +If this option is left undefined we: + +1. Check the Shared Settings +2. Check your `tsconfig.json` `compilerOptions.jsx` +3. Return the default mapping (jsx = `preserve`) + Default is: ```json diff --git a/docs/rules/no-missing-require.md b/docs/rules/no-missing-require.md index e45a2fbf..31567f03 100644 --- a/docs/rules/no-missing-require.md +++ b/docs/rules/no-missing-require.md @@ -86,6 +86,14 @@ Default is `[".js", ".json", ".node"]`. Adds the ability to change the extension mapping when converting between typescript and javascript +You can also use the [typescript compiler jsx options](https://www.typescriptlang.org/tsconfig#jsx) to automatically use the correct mapping. + +If this option is left undefined we: + +1. Check the Shared Settings +2. Check your `tsconfig.json` `compilerOptions.jsx` +3. Return the default mapping (jsx = `preserve`) + Default is: ```json diff --git a/lib/util/get-typescript-extension-map.js b/lib/util/get-typescript-extension-map.js index da9fb93f..776d71a2 100644 --- a/lib/util/get-typescript-extension-map.js +++ b/lib/util/get-typescript-extension-map.js @@ -1,6 +1,17 @@ "use strict" +const { getTsconfig } = require("get-tsconfig") +const fsCache = new Map() + const DEFAULT_MAPPING = normalise([ + ["", ".js"], + [".ts", ".js"], + [".cts", ".cjs"], + [".mts", ".mjs"], + [".tsx", ".js"], +]) + +const PRESERVE_MAPPING = normalise([ ["", ".js"], [".ts", ".js"], [".cts", ".cjs"], @@ -8,6 +19,14 @@ const DEFAULT_MAPPING = normalise([ [".tsx", ".jsx"], ]) +const tsConfigMapping = { + react: DEFAULT_MAPPING, // Emit .js files with JSX changed to the equivalent React.createElement calls + "react-jsx": DEFAULT_MAPPING, // Emit .js files with the JSX changed to _jsx calls + "react-jsxdev": DEFAULT_MAPPING, // Emit .js files with the JSX changed to _jsx calls + "react-native": DEFAULT_MAPPING, // Emit .js files with the JSX unchanged + preserve: PRESERVE_MAPPING, // Emit .jsx files with the JSX unchanged +} + /** * @typedef {Object} ExtensionMap * @property {Record} forward Convert from typescript to javascript @@ -35,13 +54,44 @@ function normalise(typescriptExtensionMap) { * @returns {ExtensionMap} The `typescriptExtensionMap` value, or `null`. */ function get(option) { + if (!option || !option.typescriptExtensionMap) { + return null + } + if ( - option && - option.typescriptExtensionMap && - Array.isArray(option.typescriptExtensionMap) + {}.hasOwnProperty.call(tsConfigMapping, option.typescriptExtensionMap) ) { + return tsConfigMapping[option.typescriptExtensionMap] + } + + if (Array.isArray(option.typescriptExtensionMap)) { return normalise(option.typescriptExtensionMap) } +} + +/** + * Attempts to get the ExtensionMap from the tsconfig of a given file. + * + * @param {string} filename - The filename we're getting from + * @returns {ExtensionMap} The `typescriptExtensionMap` value, or `null`. + */ +function getFromTSConfig(filename) { + const tsconfig = getTsconfig(filename, "tsconfig.json", fsCache) + + if ( + !tsconfig || + !tsconfig.config || + !tsconfig.config.compilerOptions || + !tsconfig.config.compilerOptions.jsx + ) { + return null + } + + const jsx = tsconfig.config.compilerOptions.jsx + + if ({}.hasOwnProperty.call(tsConfigMapping, jsx)) { + return tsConfigMapping[jsx] + } return null } @@ -49,11 +99,16 @@ function get(option) { /** * Gets "typescriptExtensionMap" setting. * - * 1. This checks `options` property, then returns it if exists. - * 2. This checks `settings.n` | `settings.node` property, then returns it if exists. - * 3. This returns `DEFAULT_MAPPING`. + * 1. This checks `options.typescriptExtensionMap`, if its an array then it gets returned. + * 2. This checks `options.typescriptExtensionMap`, if its a string, convert to the correct mapping. + * 3. This checks `settings.n.typescriptExtensionMap`, if its an array then it gets returned. + * 4. This checks `settings.node.typescriptExtensionMap`, if its an array then it gets returned. + * 5. This checks `settings.n.typescriptExtensionMap`, if its a string, convert to the correct mapping. + * 6. This checks `settings.node.typescriptExtensionMap`, if its a string, convert to the correct mapping. + * 7. This checks for a `tsconfig.json` `config.compilerOptions.jsx` property, if its a string, convert to the correct mapping. + * 8. This returns `PRESERVE_MAPPING`. * - * @param {import('eslint').Rule.RuleContext} context - The rule context. + * @param {import("eslint").Rule.RuleContext} context - The rule context. * @returns {string[]} A list of extensions. */ module.exports = function getTypescriptExtensionMap(context) { @@ -62,20 +117,28 @@ module.exports = function getTypescriptExtensionMap(context) { get( context.settings && (context.settings.n || context.settings.node) ) || - // TODO: Detect tsconfig.json here - DEFAULT_MAPPING + getFromTSConfig(context.filename) || + PRESERVE_MAPPING ) } module.exports.schema = { - type: "array", - items: { - type: "array", - prefixItems: [ - { type: "string", pattern: "^(?:|\\.\\w+)$" }, - { type: "string", pattern: "^\\.\\w+$" }, - ], - additionalItems: false, - }, - uniqueItems: true, + oneOf: [ + { + type: "array", + items: { + type: "array", + prefixItems: [ + { type: "string", pattern: "^(?:|\\.\\w+)$" }, + { type: "string", pattern: "^\\.\\w+$" }, + ], + additionalItems: false, + }, + uniqueItems: true, + }, + { + type: "string", + enum: Object.keys(tsConfigMapping), + }, + ], } diff --git a/package.json b/package.json index a31616f2..9d3e48d5 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "@eslint-community/eslint-utils": "^4.4.0", "builtins": "^5.0.1", "eslint-plugin-es-x": "^7.1.0", + "get-tsconfig": "^4.7.0", "ignore": "^5.2.4", "is-core-module": "^2.12.1", "minimatch": "^3.1.2", diff --git a/tests/fixtures/no-missing/ts-extends/base.tsconfig.json b/tests/fixtures/no-missing/ts-extends/base.tsconfig.json new file mode 100644 index 00000000..b5fbd521 --- /dev/null +++ b/tests/fixtures/no-missing/ts-extends/base.tsconfig.json @@ -0,0 +1,5 @@ +{ + "compilerOptions": { + "jsx": "react" + } +} diff --git a/tests/fixtures/no-missing/ts-extends/d.ts b/tests/fixtures/no-missing/ts-extends/d.ts new file mode 100644 index 00000000..e69de29b diff --git a/tests/fixtures/no-missing/ts-extends/e.tsx b/tests/fixtures/no-missing/ts-extends/e.tsx new file mode 100644 index 00000000..e69de29b diff --git a/tests/fixtures/no-missing/ts-extends/tsconfig.json b/tests/fixtures/no-missing/ts-extends/tsconfig.json new file mode 100644 index 00000000..383418e6 --- /dev/null +++ b/tests/fixtures/no-missing/ts-extends/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": ["./base.tsconfig.json"] +} diff --git a/tests/fixtures/no-missing/ts-preserve/d.ts b/tests/fixtures/no-missing/ts-preserve/d.ts new file mode 100644 index 00000000..e69de29b diff --git a/tests/fixtures/no-missing/ts-preserve/e.tsx b/tests/fixtures/no-missing/ts-preserve/e.tsx new file mode 100644 index 00000000..e69de29b diff --git a/tests/fixtures/no-missing/ts-preserve/tsconfig.json b/tests/fixtures/no-missing/ts-preserve/tsconfig.json new file mode 100644 index 00000000..94e40481 --- /dev/null +++ b/tests/fixtures/no-missing/ts-preserve/tsconfig.json @@ -0,0 +1,5 @@ +{ + "compilerOptions": { + "jsx": "preserve" + } +} diff --git a/tests/fixtures/no-missing/ts-react/d.ts b/tests/fixtures/no-missing/ts-react/d.ts new file mode 100644 index 00000000..e69de29b diff --git a/tests/fixtures/no-missing/ts-react/e.tsx b/tests/fixtures/no-missing/ts-react/e.tsx new file mode 100644 index 00000000..e69de29b diff --git a/tests/fixtures/no-missing/ts-react/tsconfig.json b/tests/fixtures/no-missing/ts-react/tsconfig.json new file mode 100644 index 00000000..b5fbd521 --- /dev/null +++ b/tests/fixtures/no-missing/ts-react/tsconfig.json @@ -0,0 +1,5 @@ +{ + "compilerOptions": { + "jsx": "react" + } +} diff --git a/tests/lib/rules/no-missing-import.js b/tests/lib/rules/no-missing-import.js index 767b4766..418fe9b3 100644 --- a/tests/lib/rules/no-missing-import.js +++ b/tests/lib/rules/no-missing-import.js @@ -212,6 +212,64 @@ ruleTester.run("no-missing-import", rule, { env: { node: true }, }, + // tsx mapping by name + { + filename: fixture("test.tsx"), + code: "import e from './e.jsx';", + options: [{ typescriptExtensionMap: "preserve" }], + env: { node: true }, + }, + { + filename: fixture("test.tsx"), + code: "import e from './e.js';", + options: [{ typescriptExtensionMap: "react" }], + env: { node: true }, + }, + { + filename: fixture("test.tsx"), + code: "import e from './e.jsx';", + settings: { node: { typescriptExtensionMap: "preserve" } }, + env: { node: true }, + }, + { + filename: fixture("test.tsx"), + code: "import e from './e.js';", + settings: { node: { typescriptExtensionMap: "react" } }, + env: { node: true }, + }, + + // tsx from config + { + filename: fixture("ts-react/test.tsx"), + code: "import e from './e.js';", + env: { node: true }, + }, + { + filename: fixture("ts-react/test.ts"), + code: "import d from './d.js';", + env: { node: true }, + }, + { + filename: fixture("ts-preserve/test.tsx"), + code: "import e from './e.jsx';", + env: { node: true }, + }, + { + filename: fixture("ts-preserve/test.ts"), + code: "import d from './d.js';", + env: { node: true }, + }, + { + filename: fixture("ts-extends/test.tsx"), + code: "import e from './e.js';", + env: { node: true }, + }, + { + filename: fixture("ts-extends/test.ts"), + code: "import d from './d.js';", + env: { node: true }, + }, + // import() ...(DynamicImportSupported ? [ diff --git a/tests/lib/rules/no-missing-require.js b/tests/lib/rules/no-missing-require.js index f2b4aea7..95a3f8c9 100644 --- a/tests/lib/rules/no-missing-require.js +++ b/tests/lib/rules/no-missing-require.js @@ -281,6 +281,64 @@ ruleTester.run("no-missing-require", rule, { env: { node: true }, }, + // tsx mapping by name + { + filename: fixture("test.tsx"), + code: "require('./e.jsx');", + options: [{ typescriptExtensionMap: "preserve" }], + env: { node: true }, + }, + { + filename: fixture("test.tsx"), + code: "require('./e.js');", + options: [{ typescriptExtensionMap: "react" }], + env: { node: true }, + }, + { + filename: fixture("test.tsx"), + code: "require('./e.jsx');", + settings: { node: { typescriptExtensionMap: "preserve" } }, + env: { node: true }, + }, + { + filename: fixture("test.tsx"), + code: "require('./e.js');", + settings: { node: { typescriptExtensionMap: "react" } }, + env: { node: true }, + }, + + // tsx from config + { + filename: fixture("ts-react/test.tsx"), + code: "require('./e.js');", + env: { node: true }, + }, + { + filename: fixture("ts-react/test.ts"), + code: "require('./d.js');", + env: { node: true }, + }, + { + filename: fixture("ts-preserve/test.tsx"), + code: "require('./e.jsx');", + env: { node: true }, + }, + { + filename: fixture("ts-preserve/test.ts"), + code: "require('./d.js');", + env: { node: true }, + }, + { + filename: fixture("ts-extends/test.tsx"), + code: "require('./e.js');", + env: { node: true }, + }, + { + filename: fixture("ts-extends/test.ts"), + code: "require('./d.js');", + env: { node: true }, + }, + // require.resolve { filename: fixture("test.js"),