Skip to content

Commit 1e39028

Browse files
authored
feat: support .cjs .mjs .cts .mts extensions (#84)
1 parent 4487788 commit 1e39028

12 files changed

+147
-19
lines changed

src/index.ts

+90-19
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,24 @@ import {
99
createMatchPath,
1010
loadConfig,
1111
ConfigLoaderResult,
12+
MatchPath,
1213
} from 'tsconfig-paths'
1314

1415
const IMPORTER_NAME = 'eslint-import-resolver-typescript'
1516

1617
const log = debug(IMPORTER_NAME)
1718

19+
/**
20+
* .mts, .cts, .d.mts, .d.cts, .mjs, .cjs are not included because .cjs and .mjs must be used explicitly.
21+
*/
1822
const defaultExtensions = [
1923
'.ts',
2024
'.tsx',
2125
'.d.ts',
22-
// eslint-disable-next-line node/no-deprecated-api, sonar/deprecation
23-
...Object.keys(require.extensions),
26+
'.js',
2427
'.jsx',
28+
'.json',
29+
'.node',
2530
]
2631

2732
export const interfaceVersion = 2
@@ -69,15 +74,16 @@ export function resolve(
6974
initMappers(options)
7075
const mappedPath = getMappedPath(source)
7176
if (mappedPath) {
72-
log('matched ts path:', mappedPath)
77+
log('matched ts path:', mappedPath.path)
7378
}
7479

7580
// note that even if we map the path, we still need to do a final resolve
7681
let foundNodePath: string | null | undefined
7782
try {
78-
foundNodePath = tsResolve(mappedPath ?? source, {
83+
foundNodePath = tsResolve(mappedPath?.path ?? source, {
7984
...options,
80-
extensions: options.extensions ?? defaultExtensions,
85+
extensions:
86+
mappedPath?.extensions ?? options.extensions ?? defaultExtensions,
8187
basedir: path.dirname(path.resolve(file)),
8288
packageFilter: options.packageFilter ?? packageFilterDefault,
8389
})
@@ -126,17 +132,46 @@ function packageFilterDefault(pkg: Record<string, string>) {
126132
return pkg
127133
}
128134

135+
function resolveExtension(id: string) {
136+
const idWithoutJsExt = removeJsExtension(id)
137+
138+
if (idWithoutJsExt === id) {
139+
return
140+
}
141+
142+
if (id.endsWith('.mjs')) {
143+
return {
144+
path: idWithoutJsExt,
145+
extensions: ['.mts', '.d.mts'],
146+
}
147+
}
148+
149+
if (id.endsWith('.cjs')) {
150+
return {
151+
path: idWithoutJsExt,
152+
extensions: ['.cts', '.d.cts'],
153+
}
154+
}
155+
156+
return {
157+
path: idWithoutJsExt,
158+
}
159+
}
160+
129161
/**
130162
* Like `sync` from `resolve` package, but considers that the module id
131163
* could have a .js or .jsx extension.
132164
*/
133-
function tsResolve(id: string, opts?: SyncOpts): string {
165+
function tsResolve(id: string, opts: SyncOpts): string {
134166
try {
135167
return sync(id, opts)
136168
} catch (error) {
137-
const idWithoutJsExt = removeJsExtension(id)
138-
if (idWithoutJsExt !== id) {
139-
return sync(idWithoutJsExt, opts)
169+
const resolved = resolveExtension(id)
170+
if (resolved) {
171+
return sync(resolved.path, {
172+
...opts,
173+
extensions: resolved.extensions ?? opts.extensions,
174+
})
140175
}
141176
throw error
142177
}
@@ -153,11 +188,20 @@ function removeQuerystring(id: string) {
153188

154189
/** Remove .js or .jsx extension from module id. */
155190
function removeJsExtension(id: string) {
156-
return id.replace(/\.jsx?$/, '')
191+
return id.replace(/\.([cm]js|jsx?)$/, '')
157192
}
158193

159194
let mappersBuildForOptions: TsResolverOptions
160-
let mappers: Array<(source: string) => string | undefined> | undefined
195+
let mappers:
196+
| Array<
197+
(source: string) =>
198+
| {
199+
path: string
200+
extensions?: string[]
201+
}
202+
| undefined
203+
>
204+
| undefined
161205

162206
/**
163207
* @param {string} source the module to resolve; i.e './some-module'
@@ -176,18 +220,45 @@ function getMappedPath(source: string) {
176220

177221
/**
178222
* Like `createMatchPath` from `tsconfig-paths` package, but considers
179-
* that the module id could have a .js or .jsx extension.
223+
* that the module id could have a .mjs, .cjs, .js or .jsx extension.
224+
*
225+
* The default resolved path does not include the extension, so we need to return it for reusing,
226+
* otherwise `.mts`, `.cts`, `.d.mts`, `.d.cts` will not be used by default, see also @link {defaultExtensions}.
180227
*/
181-
const createExtendedMatchPath: typeof createMatchPath = (...createArgs) => {
228+
const createExtendedMatchPath: (
229+
...createArgs: Parameters<typeof createMatchPath>
230+
) => (...matchArgs: Parameters<MatchPath>) =>
231+
| {
232+
path: string
233+
extensions?: string[]
234+
}
235+
| undefined = (...createArgs) => {
182236
const matchPath = createMatchPath(...createArgs)
183237

184-
return (id, ...otherArgs) => {
185-
const match = matchPath(id, ...otherArgs)
186-
if (match != null) return match
238+
return (id, readJson, fileExists, extensions) => {
239+
const match = matchPath(id, readJson, fileExists, extensions)
240+
241+
if (match != null) {
242+
return {
243+
path: match,
244+
}
245+
}
187246

188-
const idWithoutJsExt = removeJsExtension(id)
189-
if (idWithoutJsExt !== id) {
190-
return matchPath(idWithoutJsExt, ...otherArgs)
247+
const resolved = resolveExtension(id)
248+
249+
if (resolved) {
250+
const match = matchPath(
251+
resolved.path,
252+
readJson,
253+
fileExists,
254+
resolved.extensions ?? extensions,
255+
)
256+
if (match) {
257+
return {
258+
path: match,
259+
extensions: resolved.extensions,
260+
}
261+
}
191262
}
192263
}
193264
}

tests/withJsExtension/cjsImportee.cjs

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
/* eslint-env node */
2+
module.exports = 'cjsImportee.cjs'

tests/withJsExtension/ctsImportee.cts

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default 'ctsImportee.cts'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
declare const content: 'yes'
2+
3+
export = content
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
declare const content: 'yes'
2+
3+
export default content
+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default 'foo.cjs'
+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default 'foo.mjs'

tests/withJsExtension/jsImportee.js

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default 'jsImportee.js'

tests/withJsExtension/jsxImportee.jsx

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default 'jsxImportee.jsx'

tests/withJsExtension/mjsImportee.mjs

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default 'mjsImportee.mjs'

tests/withJsExtension/mtsImportee.mts

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default 'mtsImportee.mts'

tests/withJsExtension/test.js

+42
Original file line numberDiff line numberDiff line change
@@ -26,20 +26,38 @@ function assertResolve(id, relativePath) {
2626

2727
// import relative
2828

29+
assertResolve('./jsImportee.js', 'jsImportee.js')
30+
31+
assertResolve('./cjsImportee.cjs', 'cjsImportee.cjs')
32+
33+
assertResolve('./mjsImportee.mjs', 'mjsImportee.mjs')
34+
2935
assertResolve('./tsImportee.js', 'tsImportee.ts')
3036

3137
assertResolve('./tsxImportee.jsx', 'tsxImportee.tsx')
3238

39+
assertResolve('./ctsImportee.cjs', 'ctsImportee.cts')
40+
41+
assertResolve('./mtsImportee.mjs', 'mtsImportee.mts')
42+
3343
assertResolve('./dtsImportee.js', 'dtsImportee.d.ts')
3444

3545
assertResolve('./dtsImportee.jsx', 'dtsImportee.d.ts')
3646

47+
assertResolve('./d-ctsImportee.cjs', 'd-ctsImportee.d.cts')
48+
49+
assertResolve('./d-mtsImportee.mjs', 'd-mtsImportee.d.mts')
50+
3751
assertResolve('./foo', 'foo/index.ts')
3852

3953
assertResolve('./foo.js', 'foo.js/index.ts')
4054

4155
assertResolve('./foo.jsx', 'foo.jsx/index.ts')
4256

57+
assertResolve('./foo.cjs', 'foo.cjs/index.ts')
58+
59+
assertResolve('./foo.mjs', 'foo.mjs/index.ts')
60+
4361
assertResolve('./bar', 'bar/index.tsx')
4462

4563
// import using tsconfig.json path mapping
@@ -48,10 +66,22 @@ assertResolve('#/tsImportee.js', 'tsImportee.ts')
4866

4967
assertResolve('#/tsxImportee.jsx', 'tsxImportee.tsx')
5068

69+
assertResolve('#/cjsImportee.cjs', 'cjsImportee.cjs')
70+
71+
assertResolve('#/mjsImportee.mjs', 'mjsImportee.mjs')
72+
73+
assertResolve('#/ctsImportee.cjs', 'ctsImportee.cts')
74+
75+
assertResolve('#/mtsImportee.mjs', 'mtsImportee.mts')
76+
5177
assertResolve('#/dtsImportee.js', 'dtsImportee.d.ts')
5278

5379
assertResolve('#/dtsImportee.jsx', 'dtsImportee.d.ts')
5480

81+
assertResolve('#/d-ctsImportee.cjs', 'd-ctsImportee.d.cts')
82+
83+
assertResolve('#/d-mtsImportee.mjs', 'd-mtsImportee.d.mts')
84+
5585
assertResolve('#/foo', 'foo/index.ts')
5686

5787
assertResolve('#/foo.js', 'foo.js/index.ts')
@@ -66,10 +96,22 @@ assertResolve('tsImportee.js', 'tsImportee.ts')
6696

6797
assertResolve('tsxImportee.jsx', 'tsxImportee.tsx')
6898

99+
assertResolve('cjsImportee.cjs', 'cjsImportee.cjs')
100+
101+
assertResolve('mjsImportee.mjs', 'mjsImportee.mjs')
102+
103+
assertResolve('ctsImportee.cjs', 'ctsImportee.cts')
104+
105+
assertResolve('mtsImportee.mjs', 'mtsImportee.mts')
106+
69107
assertResolve('dtsImportee.js', 'dtsImportee.d.ts')
70108

71109
assertResolve('dtsImportee.jsx', 'dtsImportee.d.ts')
72110

111+
assertResolve('d-ctsImportee.cjs', 'd-ctsImportee.d.cts')
112+
113+
assertResolve('d-mtsImportee.mjs', 'd-mtsImportee.d.mts')
114+
73115
assertResolve('foo', 'foo/index.ts')
74116

75117
assertResolve('foo.js', 'foo.js/index.ts')

0 commit comments

Comments
 (0)