Skip to content

Commit 5340f96

Browse files
authored
feat: import with .js and .jsx file extensions (import-js#56)
1 parent 32b1529 commit 5340f96

13 files changed

+202
-4
lines changed

package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
"test": "run-p test:*",
4141
"test:multipleEslintrcs": "eslint --ext ts,tsx tests/multipleEslintrcs",
4242
"test:multipleTsconfigs": "eslint --ext ts,tsx tests/multipleTsconfigs",
43+
"test:withJsExtension": "node tests/withJsExtension/test.js && eslint --ext ts,tsx tests/withJsExtension",
4344
"test:withPaths": "eslint --ext ts,tsx tests/withPaths",
4445
"test:withoutPaths": "eslint --ext ts,tsx tests/withoutPaths",
4546
"type-coverage": "type-coverage --cache --detail --ignore-catch --strict --update"
@@ -77,6 +78,6 @@
7778
"prettier": "^2.0.5"
7879
},
7980
"typeCoverage": {
80-
"atLeast": 98.73
81+
"atLeast": 99.27
8182
}
8283
}

src/index.ts

+42-3
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
} from 'tsconfig-paths'
99
import { sync as globSync } from 'glob'
1010
import isGlob from 'is-glob'
11-
import { isCore, sync } from 'resolve'
11+
import { isCore, sync, SyncOpts } from 'resolve'
1212
import debug from 'debug'
1313

1414
const IMPORTER_NAME = 'eslint-import-resolver-typescript'
@@ -70,7 +70,7 @@ export function resolve(
7070
// note that even if we map the path, we still need to do a final resolve
7171
let foundNodePath: string | null | undefined
7272
try {
73-
foundNodePath = sync(mappedPath || source, {
73+
foundNodePath = tsResolve(mappedPath || source, {
7474
extensions: options.extensions || defaultExtensions,
7575
basedir: path.dirname(path.resolve(file)),
7676
packageFilter: options.packageFilter || packageFilterDefault,
@@ -120,6 +120,27 @@ function packageFilterDefault(pkg: Record<string, string>) {
120120
return pkg
121121
}
122122

123+
/**
124+
* Like `sync` from `resolve` package, but considers that the module id
125+
* could have a .js or .jsx extension.
126+
*/
127+
function tsResolve(id: string, opts?: SyncOpts): string {
128+
try {
129+
return sync(id, opts)
130+
} catch (error) {
131+
const idWithoutJsExt = removeJsExtension(id)
132+
if (idWithoutJsExt !== id) {
133+
return sync(idWithoutJsExt, opts)
134+
}
135+
throw error
136+
}
137+
}
138+
139+
/** Remove .js or .jsx extension from module id. */
140+
function removeJsExtension(id: string) {
141+
return id.replace(/\.jsx?$/, '')
142+
}
143+
123144
let mappersBuildForOptions: TsResolverOptions
124145
let mappers:
125146
| Array<(source: string, file: string) => string | undefined>
@@ -142,6 +163,24 @@ function getMappedPath(source: string, file: string) {
142163
return paths[0]
143164
}
144165

166+
/**
167+
* Like `createMatchPath` from `tsconfig-paths` package, but considers
168+
* that the module id could have a .js or .jsx extension.
169+
*/
170+
const createExtendedMatchPath: typeof createMatchPath = (...createArgs) => {
171+
const matchPath = createMatchPath(...createArgs)
172+
173+
return (id, ...otherArgs) => {
174+
const match = matchPath(id, ...otherArgs)
175+
if (match != null) return match
176+
177+
const idWithoutJsExt = removeJsExtension(id)
178+
if (idWithoutJsExt !== id) {
179+
return matchPath(idWithoutJsExt, ...otherArgs)
180+
}
181+
}
182+
}
183+
145184
function initMappers(options: TsResolverOptions) {
146185
if (mappers && mappersBuildForOptions === options) {
147186
return
@@ -175,7 +214,7 @@ function initMappers(options: TsResolverOptions) {
175214
// eslint-disable-next-line unicorn/no-fn-reference-in-iterator
176215
.filter(isConfigLoaderSuccessResult)
177216
.map(configLoaderResult => {
178-
const matchPath = createMatchPath(
217+
const matchPath = createExtendedMatchPath(
179218
configLoaderResult.absoluteBaseUrl,
180219
configLoaderResult.paths,
181220
)

tests/withJsExtension/.eslintrc.js

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/* eslint-env node */
2+
/* eslint-disable @typescript-eslint/no-var-requires */
3+
const config = require('../baseEslintConfig')(__dirname)
4+
5+
module.exports = {
6+
...config,
7+
rules: {
8+
...config.rules,
9+
'import/extensions': [
10+
2,
11+
'ignorePackages',
12+
{
13+
ts: 'never',
14+
tsx: 'never',
15+
},
16+
],
17+
},
18+
}

tests/withJsExtension/bar/index.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default 'bar'
+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
declare const content: 'yes'
2+
3+
export default content

tests/withJsExtension/foo.js/index.ts

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

tests/withJsExtension/foo/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default 'foo'

tests/withJsExtension/index.ts

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// import relative
2+
import './tsImportee.js'
3+
import './tsxImportee.jsx'
4+
import './dtsImportee.js'
5+
import './dtsImportee.jsx'
6+
import './foo'
7+
import './foo.js'
8+
import './foo.jsx'
9+
import './bar'
10+
11+
// import using tsconfig.json path mapping
12+
import '#/tsImportee.js'
13+
import '#/tsxImportee.jsx'
14+
import '#/dtsImportee.js'
15+
import '#/dtsImportee.jsx'
16+
import '#/foo'
17+
import '#/foo.js'
18+
import '#/foo.jsx'
19+
import '#/bar'
20+
21+
// import using tsconfig.json base url
22+
import 'tsImportee.js'
23+
import 'tsxImportee.jsx'
24+
import 'dtsImportee.js'
25+
import 'dtsImportee.jsx'
26+
import 'foo'
27+
import 'foo.js'
28+
import 'foo.jsx'
29+
import 'bar'
30+
31+
// import from node_module
32+
import 'typescript/lib/typescript.js'
33+
import 'dummy.js'

tests/withJsExtension/test.js

+88
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/* eslint-env node */
2+
/* eslint-disable @typescript-eslint/no-var-requires */
3+
4+
const path = require('path')
5+
const assert = require('assert')
6+
7+
const { resolve } = require('../../')
8+
9+
const config = {
10+
project: path.join(__dirname, 'tsconfig.json'),
11+
}
12+
13+
const file = path.join(__dirname, 'index.ts')
14+
15+
function assertResolve(id, relativePath) {
16+
const filePath = path.join(__dirname, relativePath)
17+
assert.deepStrictEqual(resolve(id, file, config), {
18+
found: true,
19+
path: filePath,
20+
})
21+
assert.deepStrictEqual(
22+
resolve(id, file, { ...config, alwaysTryTypes: true }),
23+
{ found: true, path: filePath },
24+
)
25+
}
26+
27+
// import relative
28+
29+
assertResolve('./tsImportee.js', 'tsImportee.ts')
30+
31+
assertResolve('./tsxImportee.jsx', 'tsxImportee.tsx')
32+
33+
assertResolve('./dtsImportee.js', 'dtsImportee.d.ts')
34+
35+
assertResolve('./dtsImportee.jsx', 'dtsImportee.d.ts')
36+
37+
assertResolve('./foo', 'foo/index.ts')
38+
39+
assertResolve('./foo.js', 'foo.js/index.ts')
40+
41+
assertResolve('./foo.jsx', 'foo.jsx/index.ts')
42+
43+
assertResolve('./bar', 'bar/index.tsx')
44+
45+
// import using tsconfig.json path mapping
46+
47+
assertResolve('#/tsImportee.js', 'tsImportee.ts')
48+
49+
assertResolve('#/tsxImportee.jsx', 'tsxImportee.tsx')
50+
51+
assertResolve('#/dtsImportee.js', 'dtsImportee.d.ts')
52+
53+
assertResolve('#/dtsImportee.jsx', 'dtsImportee.d.ts')
54+
55+
assertResolve('#/foo', 'foo/index.ts')
56+
57+
assertResolve('#/foo.js', 'foo.js/index.ts')
58+
59+
assertResolve('#/foo.jsx', 'foo.jsx/index.ts')
60+
61+
assertResolve('#/bar', 'bar/index.tsx')
62+
63+
// import using tsconfig.json base url
64+
65+
assertResolve('tsImportee.js', 'tsImportee.ts')
66+
67+
assertResolve('tsxImportee.jsx', 'tsxImportee.tsx')
68+
69+
assertResolve('dtsImportee.js', 'dtsImportee.d.ts')
70+
71+
assertResolve('dtsImportee.jsx', 'dtsImportee.d.ts')
72+
73+
assertResolve('foo', 'foo/index.ts')
74+
75+
assertResolve('foo.js', 'foo.js/index.ts')
76+
77+
assertResolve('foo.jsx', 'foo.jsx/index.ts')
78+
79+
assertResolve('bar', 'bar/index.tsx')
80+
81+
// import from node_module
82+
83+
assertResolve(
84+
'typescript/lib/typescript.js',
85+
'../../node_modules/typescript/lib/typescript.js',
86+
)
87+
88+
assertResolve('dummy.js', '../../node_modules/dummy.js/index.js')

tests/withJsExtension/tsImportee.ts

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default 'tsImportee.ts'

tests/withJsExtension/tsconfig.json

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"compilerOptions": {
3+
"jsx": "react",
4+
"baseUrl": "./",
5+
"paths": {
6+
"#/*": ["*"]
7+
}
8+
},
9+
"includes": ["./**/*"]
10+
}

tests/withJsExtension/tsxImportee.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default 'tsxImportee.tsx'

0 commit comments

Comments
 (0)