diff --git a/src/index.ts b/src/index.ts index f141fe3..55ee267 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,19 +9,24 @@ import { createMatchPath, loadConfig, ConfigLoaderResult, + MatchPath, } from 'tsconfig-paths' const IMPORTER_NAME = 'eslint-import-resolver-typescript' const log = debug(IMPORTER_NAME) +/** + * .mts, .cts, .d.mts, .d.cts, .mjs, .cjs are not included because .cjs and .mjs must be used explicitly. + */ const defaultExtensions = [ '.ts', '.tsx', '.d.ts', - // eslint-disable-next-line node/no-deprecated-api, sonar/deprecation - ...Object.keys(require.extensions), + '.js', '.jsx', + '.json', + '.node', ] export const interfaceVersion = 2 @@ -69,15 +74,16 @@ export function resolve( initMappers(options) const mappedPath = getMappedPath(source) if (mappedPath) { - log('matched ts path:', mappedPath) + log('matched ts path:', mappedPath.path) } // note that even if we map the path, we still need to do a final resolve let foundNodePath: string | null | undefined try { - foundNodePath = tsResolve(mappedPath ?? source, { + foundNodePath = tsResolve(mappedPath?.path ?? source, { ...options, - extensions: options.extensions ?? defaultExtensions, + extensions: + mappedPath?.extensions ?? options.extensions ?? defaultExtensions, basedir: path.dirname(path.resolve(file)), packageFilter: options.packageFilter ?? packageFilterDefault, }) @@ -126,17 +132,46 @@ function packageFilterDefault(pkg: Record) { return pkg } +function resolveExtension(id: string) { + const idWithoutJsExt = removeJsExtension(id) + + if (idWithoutJsExt === id) { + return + } + + if (id.endsWith('.mjs')) { + return { + path: idWithoutJsExt, + extensions: ['.mts', '.d.mts'], + } + } + + if (id.endsWith('.cjs')) { + return { + path: idWithoutJsExt, + extensions: ['.cts', '.d.cts'], + } + } + + return { + path: idWithoutJsExt, + } +} + /** * Like `sync` from `resolve` package, but considers that the module id * could have a .js or .jsx extension. */ -function tsResolve(id: string, opts?: SyncOpts): string { +function tsResolve(id: string, opts: SyncOpts): string { try { return sync(id, opts) } catch (error) { - const idWithoutJsExt = removeJsExtension(id) - if (idWithoutJsExt !== id) { - return sync(idWithoutJsExt, opts) + const resolved = resolveExtension(id) + if (resolved) { + return sync(resolved.path, { + ...opts, + extensions: resolved.extensions ?? opts.extensions, + }) } throw error } @@ -153,11 +188,20 @@ function removeQuerystring(id: string) { /** Remove .js or .jsx extension from module id. */ function removeJsExtension(id: string) { - return id.replace(/\.jsx?$/, '') + return id.replace(/\.([cm]js|jsx?)$/, '') } let mappersBuildForOptions: TsResolverOptions -let mappers: Array<(source: string) => string | undefined> | undefined +let mappers: + | Array< + (source: string) => + | { + path: string + extensions?: string[] + } + | undefined + > + | undefined /** * @param {string} source the module to resolve; i.e './some-module' @@ -176,18 +220,45 @@ function getMappedPath(source: string) { /** * Like `createMatchPath` from `tsconfig-paths` package, but considers - * that the module id could have a .js or .jsx extension. + * that the module id could have a .mjs, .cjs, .js or .jsx extension. + * + * The default resolved path does not include the extension, so we need to return it for reusing, + * otherwise `.mts`, `.cts`, `.d.mts`, `.d.cts` will not be used by default, see also @link {defaultExtensions}. */ -const createExtendedMatchPath: typeof createMatchPath = (...createArgs) => { +const createExtendedMatchPath: ( + ...createArgs: Parameters +) => (...matchArgs: Parameters) => + | { + path: string + extensions?: string[] + } + | undefined = (...createArgs) => { const matchPath = createMatchPath(...createArgs) - return (id, ...otherArgs) => { - const match = matchPath(id, ...otherArgs) - if (match != null) return match + return (id, readJson, fileExists, extensions) => { + const match = matchPath(id, readJson, fileExists, extensions) + + if (match != null) { + return { + path: match, + } + } - const idWithoutJsExt = removeJsExtension(id) - if (idWithoutJsExt !== id) { - return matchPath(idWithoutJsExt, ...otherArgs) + const resolved = resolveExtension(id) + + if (resolved) { + const match = matchPath( + resolved.path, + readJson, + fileExists, + resolved.extensions ?? extensions, + ) + if (match) { + return { + path: match, + extensions: resolved.extensions, + } + } } } } diff --git a/tests/withJsExtension/cjsImportee.cjs b/tests/withJsExtension/cjsImportee.cjs new file mode 100644 index 0000000..db0e8bd --- /dev/null +++ b/tests/withJsExtension/cjsImportee.cjs @@ -0,0 +1,2 @@ +/* eslint-env node */ +module.exports = 'cjsImportee.cjs' diff --git a/tests/withJsExtension/ctsImportee.cts b/tests/withJsExtension/ctsImportee.cts new file mode 100644 index 0000000..2c6146a --- /dev/null +++ b/tests/withJsExtension/ctsImportee.cts @@ -0,0 +1 @@ +export default 'ctsImportee.cts' diff --git a/tests/withJsExtension/d-ctsImportee.d.cts b/tests/withJsExtension/d-ctsImportee.d.cts new file mode 100644 index 0000000..b3ac07e --- /dev/null +++ b/tests/withJsExtension/d-ctsImportee.d.cts @@ -0,0 +1,3 @@ +declare const content: 'yes' + +export = content diff --git a/tests/withJsExtension/d-mtsImportee.d.mts b/tests/withJsExtension/d-mtsImportee.d.mts new file mode 100644 index 0000000..e645900 --- /dev/null +++ b/tests/withJsExtension/d-mtsImportee.d.mts @@ -0,0 +1,3 @@ +declare const content: 'yes' + +export default content diff --git a/tests/withJsExtension/foo.cjs/index.ts b/tests/withJsExtension/foo.cjs/index.ts new file mode 100644 index 0000000..f7acfff --- /dev/null +++ b/tests/withJsExtension/foo.cjs/index.ts @@ -0,0 +1 @@ +export default 'foo.cjs' diff --git a/tests/withJsExtension/foo.mjs/index.ts b/tests/withJsExtension/foo.mjs/index.ts new file mode 100644 index 0000000..f68c9b4 --- /dev/null +++ b/tests/withJsExtension/foo.mjs/index.ts @@ -0,0 +1 @@ +export default 'foo.mjs' diff --git a/tests/withJsExtension/jsImportee.js b/tests/withJsExtension/jsImportee.js new file mode 100644 index 0000000..35b2b8a --- /dev/null +++ b/tests/withJsExtension/jsImportee.js @@ -0,0 +1 @@ +export default 'jsImportee.js' diff --git a/tests/withJsExtension/jsxImportee.jsx b/tests/withJsExtension/jsxImportee.jsx new file mode 100644 index 0000000..4ef49ec --- /dev/null +++ b/tests/withJsExtension/jsxImportee.jsx @@ -0,0 +1 @@ +export default 'jsxImportee.jsx' diff --git a/tests/withJsExtension/mjsImportee.mjs b/tests/withJsExtension/mjsImportee.mjs new file mode 100644 index 0000000..7bdc9f9 --- /dev/null +++ b/tests/withJsExtension/mjsImportee.mjs @@ -0,0 +1 @@ +export default 'mjsImportee.mjs' diff --git a/tests/withJsExtension/mtsImportee.mts b/tests/withJsExtension/mtsImportee.mts new file mode 100644 index 0000000..49ac10f --- /dev/null +++ b/tests/withJsExtension/mtsImportee.mts @@ -0,0 +1 @@ +export default 'mtsImportee.mts' diff --git a/tests/withJsExtension/test.js b/tests/withJsExtension/test.js index 7fb8b42..f1b4c77 100644 --- a/tests/withJsExtension/test.js +++ b/tests/withJsExtension/test.js @@ -26,20 +26,38 @@ function assertResolve(id, relativePath) { // import relative +assertResolve('./jsImportee.js', 'jsImportee.js') + +assertResolve('./cjsImportee.cjs', 'cjsImportee.cjs') + +assertResolve('./mjsImportee.mjs', 'mjsImportee.mjs') + assertResolve('./tsImportee.js', 'tsImportee.ts') assertResolve('./tsxImportee.jsx', 'tsxImportee.tsx') +assertResolve('./ctsImportee.cjs', 'ctsImportee.cts') + +assertResolve('./mtsImportee.mjs', 'mtsImportee.mts') + assertResolve('./dtsImportee.js', 'dtsImportee.d.ts') assertResolve('./dtsImportee.jsx', 'dtsImportee.d.ts') +assertResolve('./d-ctsImportee.cjs', 'd-ctsImportee.d.cts') + +assertResolve('./d-mtsImportee.mjs', 'd-mtsImportee.d.mts') + assertResolve('./foo', 'foo/index.ts') assertResolve('./foo.js', 'foo.js/index.ts') assertResolve('./foo.jsx', 'foo.jsx/index.ts') +assertResolve('./foo.cjs', 'foo.cjs/index.ts') + +assertResolve('./foo.mjs', 'foo.mjs/index.ts') + assertResolve('./bar', 'bar/index.tsx') // import using tsconfig.json path mapping @@ -48,10 +66,22 @@ assertResolve('#/tsImportee.js', 'tsImportee.ts') assertResolve('#/tsxImportee.jsx', 'tsxImportee.tsx') +assertResolve('#/cjsImportee.cjs', 'cjsImportee.cjs') + +assertResolve('#/mjsImportee.mjs', 'mjsImportee.mjs') + +assertResolve('#/ctsImportee.cjs', 'ctsImportee.cts') + +assertResolve('#/mtsImportee.mjs', 'mtsImportee.mts') + assertResolve('#/dtsImportee.js', 'dtsImportee.d.ts') assertResolve('#/dtsImportee.jsx', 'dtsImportee.d.ts') +assertResolve('#/d-ctsImportee.cjs', 'd-ctsImportee.d.cts') + +assertResolve('#/d-mtsImportee.mjs', 'd-mtsImportee.d.mts') + assertResolve('#/foo', 'foo/index.ts') assertResolve('#/foo.js', 'foo.js/index.ts') @@ -66,10 +96,22 @@ assertResolve('tsImportee.js', 'tsImportee.ts') assertResolve('tsxImportee.jsx', 'tsxImportee.tsx') +assertResolve('cjsImportee.cjs', 'cjsImportee.cjs') + +assertResolve('mjsImportee.mjs', 'mjsImportee.mjs') + +assertResolve('ctsImportee.cjs', 'ctsImportee.cts') + +assertResolve('mtsImportee.mjs', 'mtsImportee.mts') + assertResolve('dtsImportee.js', 'dtsImportee.d.ts') assertResolve('dtsImportee.jsx', 'dtsImportee.d.ts') +assertResolve('d-ctsImportee.cjs', 'd-ctsImportee.d.cts') + +assertResolve('d-mtsImportee.mjs', 'd-mtsImportee.d.mts') + assertResolve('foo', 'foo/index.ts') assertResolve('foo.js', 'foo.js/index.ts')