Skip to content

Commit 20d2713

Browse files
authored
feat: add the ability to configure typescript to javascript file extension conversion (#112)
1 parent d7bf4e1 commit 20d2713

11 files changed

+319
-27
lines changed

docs/rules/no-missing-import.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,13 +69,30 @@ If a path is relative, it will be resolved from CWD.
6969

7070
Default is `[]`
7171

72+
#### typescriptExtensionMap
73+
74+
Adds the ability to change the extension mapping when converting between typescript and javascript
75+
76+
Default is:
77+
78+
```json
79+
[
80+
[ "", ".js" ],
81+
[ ".ts", ".js" ],
82+
[ ".cts", ".cjs" ],
83+
[ ".mts", ".mjs" ],
84+
[ ".tsx", ".jsx" ],
85+
]
86+
```
87+
7288
### Shared Settings
7389

7490
The following options can be set by [shared settings](http://eslint.org/docs/user-guide/configuring.html#adding-shared-settings).
7591
Several rules have the same option, but we can set this option at once.
7692

7793
- `allowModules`
7894
- `resolvePaths`
95+
- `typescriptExtensionMap`
7996

8097
```js
8198
// .eslintrc.js
@@ -84,6 +101,13 @@ module.exports = {
84101
"node": {
85102
"allowModules": ["electron"],
86103
"resolvePaths": [__dirname],
104+
"typescriptExtensionMap": [
105+
[ "", ".js" ],
106+
[ ".ts", ".js" ],
107+
[ ".cts", ".cjs" ],
108+
[ ".mts", ".mjs" ],
109+
[ ".tsx", ".js" ],
110+
]
87111
}
88112
},
89113
"rules": {

docs/rules/no-missing-require.md

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,22 @@ When an import path does not exist, this rule checks whether or not any of `path
8282

8383
Default is `[".js", ".json", ".node"]`.
8484

85+
#### typescriptExtensionMap
86+
87+
Adds the ability to change the extension mapping when converting between typescript and javascript
88+
89+
Default is:
90+
91+
```json
92+
[
93+
[ "", ".js" ],
94+
[ ".ts", ".js" ],
95+
[ ".cts", ".cjs" ],
96+
[ ".mts", ".mjs" ],
97+
[ ".tsx", ".jsx" ],
98+
]
99+
```
100+
85101
### Shared Settings
86102

87103
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.
90106
- `allowModules`
91107
- `resolvePaths`
92108
- `tryExtensions`
109+
- `typescriptExtensionMap`
93110

94111
```js
95112
// .eslintrc.js
@@ -98,7 +115,14 @@ module.exports = {
98115
"node": {
99116
"allowModules": ["electron"],
100117
"resolvePaths": [__dirname],
101-
"tryExtensions": [".js", ".json", ".node"]
118+
"tryExtensions": [".js", ".json", ".node"],
119+
"typescriptExtensionMap": [
120+
[ "", ".js" ],
121+
[ ".ts", ".js" ],
122+
[ ".cts", ".cjs" ],
123+
[ ".mts", ".mjs" ],
124+
[ ".tsx", ".js" ],
125+
]
102126
}
103127
},
104128
"rules": {

lib/rules/no-missing-import.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
const { checkExistence, messages } = require("../util/check-existence")
88
const getAllowModules = require("../util/get-allow-modules")
99
const getResolvePaths = require("../util/get-resolve-paths")
10+
const getTypescriptExtensionMap = require("../util/get-typescript-extension-map")
1011
const visitImport = require("../util/visit-import")
1112

1213
module.exports = {
@@ -26,6 +27,7 @@ module.exports = {
2627
properties: {
2728
allowModules: getAllowModules.schema,
2829
resolvePaths: getResolvePaths.schema,
30+
typescriptExtensionMap: getTypescriptExtensionMap.schema,
2931
},
3032
additionalProperties: false,
3133
},

lib/rules/no-missing-require.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ const { checkExistence, messages } = require("../util/check-existence")
88
const getAllowModules = require("../util/get-allow-modules")
99
const getResolvePaths = require("../util/get-resolve-paths")
1010
const getTryExtensions = require("../util/get-try-extensions")
11+
const getTypescriptExtensionMap = require("../util/get-typescript-extension-map")
1112
const visitRequire = require("../util/visit-require")
1213

1314
module.exports = {
@@ -28,6 +29,7 @@ module.exports = {
2829
allowModules: getAllowModules.schema,
2930
tryExtensions: getTryExtensions.schema,
3031
resolvePaths: getResolvePaths.schema,
32+
typescriptExtensionMap: getTypescriptExtensionMap.schema,
3133
},
3234
additionalProperties: false,
3335
},

lib/util/check-existence.js

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,17 +32,21 @@ exports.checkExistence = function checkExistence(context, targets) {
3232
let missingFile = target.moduleName == null && !exists(target.filePath)
3333
if (missingFile && isTypescript(context)) {
3434
const parsed = path.parse(target.filePath)
35-
const reversedExt = mapTypescriptExtension(
35+
const reversedExts = mapTypescriptExtension(
3636
context,
3737
target.filePath,
3838
parsed.ext,
3939
true
4040
)
41-
const reversedPath =
42-
path.resolve(parsed.dir, parsed.name) + reversedExt
43-
missingFile = target.moduleName == null && !exists(reversedPath)
41+
const reversedPaths = reversedExts.map(
42+
reversedExt =>
43+
path.resolve(parsed.dir, parsed.name) + reversedExt
44+
)
45+
missingFile = reversedPaths.every(
46+
reversedPath =>
47+
target.moduleName == null && !exists(reversedPath)
48+
)
4449
}
45-
4650
if (missingModule || missingFile) {
4751
context.report({
4852
node: target.node,
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
"use strict"
2+
3+
const DEFAULT_MAPPING = normalise([
4+
["", ".js"],
5+
[".ts", ".js"],
6+
[".cts", ".cjs"],
7+
[".mts", ".mjs"],
8+
[".tsx", ".jsx"],
9+
])
10+
11+
/**
12+
* @typedef {Object} ExtensionMap
13+
* @property {Record<string, string>} forward Convert from typescript to javascript
14+
* @property {Record<string, string[]>} backward Convert from javascript to typescript
15+
*/
16+
17+
function normalise(typescriptExtensionMap) {
18+
const forward = {}
19+
const backward = {}
20+
for (const [typescript, javascript] of typescriptExtensionMap) {
21+
forward[typescript] = javascript
22+
if (!typescript) {
23+
continue
24+
}
25+
backward[javascript] ??= []
26+
backward[javascript].push(typescript)
27+
}
28+
return { forward, backward }
29+
}
30+
31+
/**
32+
* Gets `typescriptExtensionMap` property from a given option object.
33+
*
34+
* @param {object|undefined} option - An option object to get.
35+
* @returns {ExtensionMap} The `typescriptExtensionMap` value, or `null`.
36+
*/
37+
function get(option) {
38+
if (
39+
option &&
40+
option.typescriptExtensionMap &&
41+
Array.isArray(option.typescriptExtensionMap)
42+
) {
43+
return normalise(option.typescriptExtensionMap)
44+
}
45+
46+
return null
47+
}
48+
49+
/**
50+
* Gets "typescriptExtensionMap" setting.
51+
*
52+
* 1. This checks `options` property, then returns it if exists.
53+
* 2. This checks `settings.n` | `settings.node` property, then returns it if exists.
54+
* 3. This returns `DEFAULT_MAPPING`.
55+
*
56+
* @param {import('eslint').Rule.RuleContext} context - The rule context.
57+
* @returns {string[]} A list of extensions.
58+
*/
59+
module.exports = function getTypescriptExtensionMap(context) {
60+
return (
61+
get(context.options && context.options[0]) ||
62+
get(
63+
context.settings && (context.settings.n || context.settings.node)
64+
) ||
65+
// TODO: Detect tsconfig.json here
66+
DEFAULT_MAPPING
67+
)
68+
}
69+
70+
module.exports.schema = {
71+
type: "array",
72+
items: {
73+
type: "array",
74+
prefixItems: [
75+
{ type: "string", pattern: "^(?:|\\.\\w+)$" },
76+
{ type: "string", pattern: "^\\.\\w+$" },
77+
],
78+
additionalItems: false,
79+
},
80+
uniqueItems: true,
81+
}

lib/util/map-typescript-extension.js

Lines changed: 9 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,8 @@
11
"use strict"
22

33
const path = require("path")
4-
const isTypescript = require("../util/is-typescript")
5-
6-
const mapping = {
7-
"": ".js", // default empty extension will map to js
8-
".ts": ".js",
9-
".cts": ".cjs",
10-
".mts": ".mjs",
11-
".tsx": ".jsx",
12-
}
13-
14-
const reverseMapping = {
15-
".js": ".ts",
16-
".cjs": ".cts",
17-
".mjs": ".mts",
18-
".jsx": ".tsx",
19-
}
4+
const isTypescript = require("./is-typescript")
5+
const getTypescriptExtensionMap = require("./get-typescript-extension-map")
206

217
/**
228
* Maps the typescript file extension that should be added in an import statement,
@@ -25,7 +11,7 @@ const reverseMapping = {
2511
* For example, in typescript, when referencing another typescript from a typescript file,
2612
* a .js extension should be used instead of the original .ts extension of the referenced file.
2713
*
28-
* @param {RuleContext} context
14+
* @param {import('eslint').Rule.RuleContext} context
2915
* @param {string} filePath The filePath of the import
3016
* @param {string} fallbackExtension The non-typescript fallback
3117
* @param {boolean} reverse Execute a reverse path mapping
@@ -37,14 +23,16 @@ module.exports = function mapTypescriptExtension(
3723
fallbackExtension,
3824
reverse = false
3925
) {
26+
const { forward, backward } = getTypescriptExtensionMap(context)
4027
const ext = path.extname(filePath)
4128
if (reverse) {
42-
if (isTypescript(context) && ext in reverseMapping) {
43-
return reverseMapping[ext]
29+
if (isTypescript(context) && ext in backward) {
30+
return backward[ext]
4431
}
32+
return [fallbackExtension]
4533
} else {
46-
if (isTypescript(context) && ext in mapping) {
47-
return mapping[ext]
34+
if (isTypescript(context) && ext in forward) {
35+
return forward[ext]
4836
}
4937
}
5038

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
"devDependencies": {
2727
"@eslint/eslintrc": "^2.0.3",
2828
"@eslint/js": "^8.43.0",
29+
"@types/eslint": "^8.44.2",
2930
"@typescript-eslint/parser": "^5.60.0",
3031
"codecov": "^3.8.2",
3132
"esbuild": "^0.18.7",

tests/lib/rules/file-extension-in-import.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,14 @@ if (!DynamicImportSupported) {
2121
)
2222
}
2323

24+
const tsReactExtensionMap = [
25+
["", ".js"],
26+
[".ts", ".js"],
27+
[".cts", ".cjs"],
28+
[".mts", ".mjs"],
29+
[".tsx", ".js"],
30+
]
31+
2432
function fixture(filename) {
2533
return path.resolve(
2634
__dirname,
@@ -146,6 +154,32 @@ new RuleTester({
146154
code: "import './c'",
147155
options: ["never", { ".json": "always" }],
148156
},
157+
158+
// typescriptExtensionMap
159+
{
160+
filename: fixture("test.tsx"),
161+
code: "require('./d.js');",
162+
env: { node: true },
163+
settings: { node: { typescriptExtensionMap: tsReactExtensionMap } },
164+
},
165+
{
166+
filename: fixture("test.tsx"),
167+
code: "require('./e.js');",
168+
env: { node: true },
169+
settings: { node: { typescriptExtensionMap: tsReactExtensionMap } },
170+
},
171+
{
172+
filename: fixture("test.ts"),
173+
code: "require('./d.js');",
174+
env: { node: true },
175+
settings: { node: { typescriptExtensionMap: tsReactExtensionMap } },
176+
},
177+
{
178+
filename: fixture("test.ts"),
179+
code: "require('./e.js');",
180+
env: { node: true },
181+
settings: { node: { typescriptExtensionMap: tsReactExtensionMap } },
182+
},
149183
],
150184
invalid: [
151185
{

0 commit comments

Comments
 (0)