Skip to content

Commit

Permalink
feat: support .cjs .mjs .cts .mts extensions (#84)
Browse files Browse the repository at this point in the history
  • Loading branch information
JounQin committed Mar 23, 2022
1 parent 4487788 commit 1e39028
Show file tree
Hide file tree
Showing 12 changed files with 147 additions and 19 deletions.
109 changes: 90 additions & 19 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
})
Expand Down Expand Up @@ -126,17 +132,46 @@ function packageFilterDefault(pkg: Record<string, string>) {
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
}
Expand All @@ -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'
Expand All @@ -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<typeof createMatchPath>
) => (...matchArgs: Parameters<MatchPath>) =>
| {
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,
}
}
}
}
}
Expand Down
2 changes: 2 additions & 0 deletions tests/withJsExtension/cjsImportee.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/* eslint-env node */
module.exports = 'cjsImportee.cjs'
1 change: 1 addition & 0 deletions tests/withJsExtension/ctsImportee.cts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default 'ctsImportee.cts'
3 changes: 3 additions & 0 deletions tests/withJsExtension/d-ctsImportee.d.cts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
declare const content: 'yes'

export = content
3 changes: 3 additions & 0 deletions tests/withJsExtension/d-mtsImportee.d.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
declare const content: 'yes'

export default content
1 change: 1 addition & 0 deletions tests/withJsExtension/foo.cjs/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default 'foo.cjs'
1 change: 1 addition & 0 deletions tests/withJsExtension/foo.mjs/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default 'foo.mjs'
1 change: 1 addition & 0 deletions tests/withJsExtension/jsImportee.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default 'jsImportee.js'
1 change: 1 addition & 0 deletions tests/withJsExtension/jsxImportee.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default 'jsxImportee.jsx'
1 change: 1 addition & 0 deletions tests/withJsExtension/mjsImportee.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default 'mjsImportee.mjs'
1 change: 1 addition & 0 deletions tests/withJsExtension/mtsImportee.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default 'mtsImportee.mts'
42 changes: 42 additions & 0 deletions tests/withJsExtension/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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')
Expand All @@ -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')
Expand Down

0 comments on commit 1e39028

Please sign in to comment.