diff --git a/docs/rules/no-missing-import.md b/docs/rules/no-missing-import.md index 3e9bd7ac..3b4d9e7b 100644 --- a/docs/rules/no-missing-import.md +++ b/docs/rules/no-missing-import.md @@ -69,6 +69,22 @@ If a path is relative, it will be resolved from CWD. Default is `[]` +#### typescriptExtensionMap + +Adds the ability to change the extension mapping when converting between typescript and javascript + +Default is: + +```json +[ + [ "", ".js" ], + [ ".ts", ".js" ], + [ ".cts", ".cjs" ], + [ ".mts", ".mjs" ], + [ ".tsx", ".jsx" ], +] +``` + ### Shared Settings The following options can be set by [shared settings](http://eslint.org/docs/user-guide/configuring.html#adding-shared-settings). @@ -76,6 +92,7 @@ Several rules have the same option, but we can set this option at once. - `allowModules` - `resolvePaths` +- `typescriptExtensionMap` ```js // .eslintrc.js @@ -84,6 +101,13 @@ module.exports = { "node": { "allowModules": ["electron"], "resolvePaths": [__dirname], + "typescriptExtensionMap": [ + [ "", ".js" ], + [ ".ts", ".js" ], + [ ".cts", ".cjs" ], + [ ".mts", ".mjs" ], + [ ".tsx", ".js" ], + ] } }, "rules": { diff --git a/docs/rules/no-missing-require.md b/docs/rules/no-missing-require.md index 96822d5f..e45a2fbf 100644 --- a/docs/rules/no-missing-require.md +++ b/docs/rules/no-missing-require.md @@ -82,6 +82,22 @@ When an import path does not exist, this rule checks whether or not any of `path Default is `[".js", ".json", ".node"]`. +#### typescriptExtensionMap + +Adds the ability to change the extension mapping when converting between typescript and javascript + +Default is: + +```json +[ + [ "", ".js" ], + [ ".ts", ".js" ], + [ ".cts", ".cjs" ], + [ ".mts", ".mjs" ], + [ ".tsx", ".jsx" ], +] +``` + ### Shared Settings The following options can be set by [shared settings](http://eslint.org/docs/user-guide/configuring.html#adding-shared-settings). @@ -90,6 +106,7 @@ Several rules have the same option, but we can set this option at once. - `allowModules` - `resolvePaths` - `tryExtensions` +- `typescriptExtensionMap` ```js // .eslintrc.js @@ -98,7 +115,14 @@ module.exports = { "node": { "allowModules": ["electron"], "resolvePaths": [__dirname], - "tryExtensions": [".js", ".json", ".node"] + "tryExtensions": [".js", ".json", ".node"], + "typescriptExtensionMap": [ + [ "", ".js" ], + [ ".ts", ".js" ], + [ ".cts", ".cjs" ], + [ ".mts", ".mjs" ], + [ ".tsx", ".js" ], + ] } }, "rules": { diff --git a/lib/rules/no-missing-import.js b/lib/rules/no-missing-import.js index 76b3b107..3b780475 100644 --- a/lib/rules/no-missing-import.js +++ b/lib/rules/no-missing-import.js @@ -7,6 +7,7 @@ const { checkExistence, messages } = require("../util/check-existence") const getAllowModules = require("../util/get-allow-modules") const getResolvePaths = require("../util/get-resolve-paths") +const getTypescriptExtensionMap = require("../util/get-typescript-extension-map") const visitImport = require("../util/visit-import") module.exports = { @@ -26,6 +27,7 @@ module.exports = { properties: { allowModules: getAllowModules.schema, resolvePaths: getResolvePaths.schema, + typescriptExtensionMap: getTypescriptExtensionMap.schema, }, additionalProperties: false, }, diff --git a/lib/rules/no-missing-require.js b/lib/rules/no-missing-require.js index c1d1e9d7..7566fa49 100644 --- a/lib/rules/no-missing-require.js +++ b/lib/rules/no-missing-require.js @@ -8,6 +8,7 @@ const { checkExistence, messages } = require("../util/check-existence") const getAllowModules = require("../util/get-allow-modules") const getResolvePaths = require("../util/get-resolve-paths") const getTryExtensions = require("../util/get-try-extensions") +const getTypescriptExtensionMap = require("../util/get-typescript-extension-map") const visitRequire = require("../util/visit-require") module.exports = { @@ -28,6 +29,7 @@ module.exports = { allowModules: getAllowModules.schema, tryExtensions: getTryExtensions.schema, resolvePaths: getResolvePaths.schema, + typescriptExtensionMap: getTypescriptExtensionMap.schema, }, additionalProperties: false, }, diff --git a/lib/util/check-existence.js b/lib/util/check-existence.js index fac3f21a..29ef3551 100644 --- a/lib/util/check-existence.js +++ b/lib/util/check-existence.js @@ -32,17 +32,21 @@ exports.checkExistence = function checkExistence(context, targets) { let missingFile = target.moduleName == null && !exists(target.filePath) if (missingFile && isTypescript(context)) { const parsed = path.parse(target.filePath) - const reversedExt = mapTypescriptExtension( + const reversedExts = mapTypescriptExtension( context, target.filePath, parsed.ext, true ) - const reversedPath = - path.resolve(parsed.dir, parsed.name) + reversedExt - missingFile = target.moduleName == null && !exists(reversedPath) + const reversedPaths = reversedExts.map( + reversedExt => + path.resolve(parsed.dir, parsed.name) + reversedExt + ) + missingFile = reversedPaths.every( + reversedPath => + target.moduleName == null && !exists(reversedPath) + ) } - if (missingModule || missingFile) { context.report({ node: target.node, diff --git a/lib/util/get-typescript-extension-map.js b/lib/util/get-typescript-extension-map.js new file mode 100644 index 00000000..da9fb93f --- /dev/null +++ b/lib/util/get-typescript-extension-map.js @@ -0,0 +1,81 @@ +"use strict" + +const DEFAULT_MAPPING = normalise([ + ["", ".js"], + [".ts", ".js"], + [".cts", ".cjs"], + [".mts", ".mjs"], + [".tsx", ".jsx"], +]) + +/** + * @typedef {Object} ExtensionMap + * @property {Record} forward Convert from typescript to javascript + * @property {Record} backward Convert from javascript to typescript + */ + +function normalise(typescriptExtensionMap) { + const forward = {} + const backward = {} + for (const [typescript, javascript] of typescriptExtensionMap) { + forward[typescript] = javascript + if (!typescript) { + continue + } + backward[javascript] ??= [] + backward[javascript].push(typescript) + } + return { forward, backward } +} + +/** + * Gets `typescriptExtensionMap` property from a given option object. + * + * @param {object|undefined} option - An option object to get. + * @returns {ExtensionMap} The `typescriptExtensionMap` value, or `null`. + */ +function get(option) { + if ( + option && + option.typescriptExtensionMap && + Array.isArray(option.typescriptExtensionMap) + ) { + return normalise(option.typescriptExtensionMap) + } + + return null +} + +/** + * 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`. + * + * @param {import('eslint').Rule.RuleContext} context - The rule context. + * @returns {string[]} A list of extensions. + */ +module.exports = function getTypescriptExtensionMap(context) { + return ( + get(context.options && context.options[0]) || + get( + context.settings && (context.settings.n || context.settings.node) + ) || + // TODO: Detect tsconfig.json here + DEFAULT_MAPPING + ) +} + +module.exports.schema = { + type: "array", + items: { + type: "array", + prefixItems: [ + { type: "string", pattern: "^(?:|\\.\\w+)$" }, + { type: "string", pattern: "^\\.\\w+$" }, + ], + additionalItems: false, + }, + uniqueItems: true, +} diff --git a/lib/util/map-typescript-extension.js b/lib/util/map-typescript-extension.js index fe428004..b4806033 100644 --- a/lib/util/map-typescript-extension.js +++ b/lib/util/map-typescript-extension.js @@ -1,22 +1,8 @@ "use strict" const path = require("path") -const isTypescript = require("../util/is-typescript") - -const mapping = { - "": ".js", // default empty extension will map to js - ".ts": ".js", - ".cts": ".cjs", - ".mts": ".mjs", - ".tsx": ".jsx", -} - -const reverseMapping = { - ".js": ".ts", - ".cjs": ".cts", - ".mjs": ".mts", - ".jsx": ".tsx", -} +const isTypescript = require("./is-typescript") +const getTypescriptExtensionMap = require("./get-typescript-extension-map") /** * Maps the typescript file extension that should be added in an import statement, @@ -25,7 +11,7 @@ const reverseMapping = { * For example, in typescript, when referencing another typescript from a typescript file, * a .js extension should be used instead of the original .ts extension of the referenced file. * - * @param {RuleContext} context + * @param {import('eslint').Rule.RuleContext} context * @param {string} filePath The filePath of the import * @param {string} fallbackExtension The non-typescript fallback * @param {boolean} reverse Execute a reverse path mapping @@ -37,14 +23,16 @@ module.exports = function mapTypescriptExtension( fallbackExtension, reverse = false ) { + const { forward, backward } = getTypescriptExtensionMap(context) const ext = path.extname(filePath) if (reverse) { - if (isTypescript(context) && ext in reverseMapping) { - return reverseMapping[ext] + if (isTypescript(context) && ext in backward) { + return backward[ext] } + return [fallbackExtension] } else { - if (isTypescript(context) && ext in mapping) { - return mapping[ext] + if (isTypescript(context) && ext in forward) { + return forward[ext] } } diff --git a/package.json b/package.json index c5c2e42a..a31616f2 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "devDependencies": { "@eslint/eslintrc": "^2.0.3", "@eslint/js": "^8.43.0", + "@types/eslint": "^8.44.2", "@typescript-eslint/parser": "^5.60.0", "codecov": "^3.8.2", "esbuild": "^0.18.7", diff --git a/tests/lib/rules/file-extension-in-import.js b/tests/lib/rules/file-extension-in-import.js index 30ccb160..6d3661c1 100644 --- a/tests/lib/rules/file-extension-in-import.js +++ b/tests/lib/rules/file-extension-in-import.js @@ -21,6 +21,14 @@ if (!DynamicImportSupported) { ) } +const tsReactExtensionMap = [ + ["", ".js"], + [".ts", ".js"], + [".cts", ".cjs"], + [".mts", ".mjs"], + [".tsx", ".js"], +] + function fixture(filename) { return path.resolve( __dirname, @@ -146,6 +154,32 @@ new RuleTester({ code: "import './c'", options: ["never", { ".json": "always" }], }, + + // typescriptExtensionMap + { + filename: fixture("test.tsx"), + code: "require('./d.js');", + env: { node: true }, + settings: { node: { typescriptExtensionMap: tsReactExtensionMap } }, + }, + { + filename: fixture("test.tsx"), + code: "require('./e.js');", + env: { node: true }, + settings: { node: { typescriptExtensionMap: tsReactExtensionMap } }, + }, + { + filename: fixture("test.ts"), + code: "require('./d.js');", + env: { node: true }, + settings: { node: { typescriptExtensionMap: tsReactExtensionMap } }, + }, + { + filename: fixture("test.ts"), + code: "require('./e.js');", + env: { node: true }, + settings: { node: { typescriptExtensionMap: tsReactExtensionMap } }, + }, ], invalid: [ { diff --git a/tests/lib/rules/no-missing-import.js b/tests/lib/rules/no-missing-import.js index 4a054061..767b4766 100644 --- a/tests/lib/rules/no-missing-import.js +++ b/tests/lib/rules/no-missing-import.js @@ -21,6 +21,14 @@ if (!DynamicImportSupported) { ) } +const tsReactExtensionMap = [ + ["", ".js"], + [".ts", ".js"], + [".cts", ".cjs"], + [".mts", ".mjs"], + [".tsx", ".js"], +] + /** * Makes a file path to a fixture. * @param {string} name - A name. @@ -146,6 +154,64 @@ ruleTester.run("no-missing-import", rule, { env: { node: true }, }, + // typescriptExtensionMap + { + filename: fixture("test.tsx"), + code: "import a from './d.js';", + env: { node: true }, + settings: { + node: { typescriptExtensionMap: tsReactExtensionMap }, + }, + }, + { + filename: fixture("test.ts"), + code: "import a from './d.js';", + env: { node: true }, + settings: { + node: { typescriptExtensionMap: tsReactExtensionMap }, + }, + }, + { + filename: fixture("test.tsx"), + code: "import a from './e.js';", + env: { node: true }, + settings: { + node: { typescriptExtensionMap: tsReactExtensionMap }, + }, + }, + { + filename: fixture("test.ts"), + code: "import a from './e.js';", + env: { node: true }, + settings: { + node: { typescriptExtensionMap: tsReactExtensionMap }, + }, + }, + { + filename: fixture("test.tsx"), + code: "import a from './d.js';", + options: [{ typescriptExtensionMap: tsReactExtensionMap }], + env: { node: true }, + }, + { + filename: fixture("test.ts"), + code: "import a from './d.js';", + options: [{ typescriptExtensionMap: tsReactExtensionMap }], + env: { node: true }, + }, + { + filename: fixture("test.tsx"), + code: "import a from './e.js';", + options: [{ typescriptExtensionMap: tsReactExtensionMap }], + env: { node: true }, + }, + { + filename: fixture("test.ts"), + code: "import a from './e.js';", + options: [{ typescriptExtensionMap: tsReactExtensionMap }], + env: { node: true }, + }, + // import() ...(DynamicImportSupported ? [ diff --git a/tests/lib/rules/no-missing-require.js b/tests/lib/rules/no-missing-require.js index 7ca480e8..f2b4aea7 100644 --- a/tests/lib/rules/no-missing-require.js +++ b/tests/lib/rules/no-missing-require.js @@ -8,6 +8,14 @@ const path = require("path") const RuleTester = require("eslint").RuleTester const rule = require("../../../lib/rules/no-missing-require") +const tsReactExtensionMap = [ + ["", ".js"], + [".ts", ".js"], + [".cts", ".cjs"], + [".mts", ".mjs"], + [".tsx", ".js"], +] + /** * Makes a file path to a fixture. * @param {string} name - A name. @@ -215,6 +223,64 @@ ruleTester.run("no-missing-require", rule, { env: { node: true }, }, + // typescriptExtensionMap + { + filename: fixture("test.tsx"), + code: "require('./d.js');", + env: { node: true }, + settings: { + node: { typescriptExtensionMap: tsReactExtensionMap }, + }, + }, + { + filename: fixture("test.ts"), + code: "require('./d.js');", + env: { node: true }, + settings: { + node: { typescriptExtensionMap: tsReactExtensionMap }, + }, + }, + { + filename: fixture("test.tsx"), + code: "require('./e.js');", + env: { node: true }, + settings: { + node: { typescriptExtensionMap: tsReactExtensionMap }, + }, + }, + { + filename: fixture("test.ts"), + code: "require('./e.js');", + env: { node: true }, + settings: { + node: { typescriptExtensionMap: tsReactExtensionMap }, + }, + }, + { + filename: fixture("test.tsx"), + code: "require('./d.js');", + options: [{ typescriptExtensionMap: tsReactExtensionMap }], + env: { node: true }, + }, + { + filename: fixture("test.ts"), + code: "require('./d.js');", + options: [{ typescriptExtensionMap: tsReactExtensionMap }], + env: { node: true }, + }, + { + filename: fixture("test.tsx"), + code: "require('./e.js');", + options: [{ typescriptExtensionMap: tsReactExtensionMap }], + env: { node: true }, + }, + { + filename: fixture("test.ts"), + code: "require('./e.js');", + options: [{ typescriptExtensionMap: tsReactExtensionMap }], + env: { node: true }, + }, + // require.resolve { filename: fixture("test.js"),