Skip to content

Commit 2ab30ce

Browse files
authored
feat: Allow for automatic ts mapping detection (#114)
1 parent 78595c4 commit 2ab30ce

20 files changed

+399
-49
lines changed

docs/rules/file-extension-in-import.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,42 @@ import styles from "./styles.css"
9090
import logo from "./logo.png"
9191
```
9292

93+
### Shared Settings
94+
95+
The following options can be set by [shared settings](http://eslint.org/docs/user-guide/configuring.html#adding-shared-settings).
96+
Several rules have the same option, but we can set this option at once.
97+
98+
#### typescriptExtensionMap
99+
100+
Adds the ability to change the extension mapping when converting between typescript and javascript
101+
102+
You can also use the [typescript compiler jsx options](https://www.typescriptlang.org/tsconfig#jsx) to automatically use the correct mapping.
103+
104+
If this option is left undefined we:
105+
106+
1. Check your `tsconfig.json` `compilerOptions.jsx`
107+
2. Return the default mapping (jsx = `preserve`)
108+
109+
```js
110+
// .eslintrc.js
111+
module.exports = {
112+
"settings": {
113+
"node": {
114+
"typescriptExtensionMap": [
115+
[ "", ".js" ],
116+
[ ".ts", ".js" ],
117+
[ ".cts", ".cjs" ],
118+
[ ".mts", ".mjs" ],
119+
[ ".tsx", ".jsx" ],
120+
]
121+
}
122+
},
123+
"rules": {
124+
"n/file-extension-in-import": "error"
125+
}
126+
}
127+
```
128+
93129
## 🔎 Implementation
94130

95131
- [Rule source](../../lib/rules/file-extension-in-import.js)

docs/rules/no-missing-import.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,14 @@ Default is `[]`
7373

7474
Adds the ability to change the extension mapping when converting between typescript and javascript
7575

76+
You can also use the [typescript compiler jsx options](https://www.typescriptlang.org/tsconfig#jsx) to automatically use the correct mapping.
77+
78+
If this option is left undefined we:
79+
80+
1. Check the Shared Settings
81+
2. Check your `tsconfig.json` `compilerOptions.jsx`
82+
3. Return the default mapping (jsx = `preserve`)
83+
7684
Default is:
7785

7886
```json
@@ -85,6 +93,10 @@ Default is:
8593
]
8694
```
8795

96+
#### tsconfigPath
97+
98+
Adds the ability to specify the tsconfig used by the typescriptExtensionMap tool.
99+
88100
### Shared Settings
89101

90102
The following options can be set by [shared settings](http://eslint.org/docs/user-guide/configuring.html#adding-shared-settings).

docs/rules/no-missing-require.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,14 @@ Default is `[".js", ".json", ".node"]`.
8686

8787
Adds the ability to change the extension mapping when converting between typescript and javascript
8888

89+
You can also use the [typescript compiler jsx options](https://www.typescriptlang.org/tsconfig#jsx) to automatically use the correct mapping.
90+
91+
If this option is left undefined we:
92+
93+
1. Check the Shared Settings
94+
2. Check your `tsconfig.json` `compilerOptions.jsx`
95+
3. Return the default mapping (jsx = `preserve`)
96+
8997
Default is:
9098

9199
```json
@@ -98,6 +106,10 @@ Default is:
98106
]
99107
```
100108

109+
#### tsconfigPath
110+
111+
Adds the ability to specify the tsconfig used by the typescriptExtensionMap tool.
112+
101113
### Shared Settings
102114

103115
The following options can be set by [shared settings](http://eslint.org/docs/user-guide/configuring.html#adding-shared-settings).

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 getTSConfig = require("../util/get-tsconfig")
1011
const getTypescriptExtensionMap = require("../util/get-typescript-extension-map")
1112
const visitImport = require("../util/visit-import")
1213

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

lib/rules/no-missing-require.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 getTSConfig = require("../util/get-tsconfig")
1011
const getTryExtensions = require("../util/get-try-extensions")
1112
const getTypescriptExtensionMap = require("../util/get-typescript-extension-map")
1213
const visitRequire = require("../util/visit-require")
@@ -30,6 +31,7 @@ module.exports = {
3031
tryExtensions: getTryExtensions.schema,
3132
resolvePaths: getResolvePaths.schema,
3233
typescriptExtensionMap: getTypescriptExtensionMap.schema,
34+
tsconfigPath: getTSConfig.schema,
3335
},
3436
additionalProperties: false,
3537
},

lib/util/get-tsconfig.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
"use strict"
2+
3+
const { getTsconfig, parseTsconfig } = require("get-tsconfig")
4+
const fsCache = new Map()
5+
6+
/**
7+
* Attempts to get the ExtensionMap from the tsconfig given the path to the tsconfig file.
8+
*
9+
* @param {string} filename - The path to the tsconfig.json file
10+
* @returns {import("get-tsconfig").TsConfigJsonResolved}
11+
*/
12+
function getTSConfig(filename) {
13+
return parseTsconfig(filename, fsCache)
14+
}
15+
16+
/**
17+
* Attempts to get the ExtensionMap from the tsconfig of a given file.
18+
*
19+
* @param {string} filename - The path to the file we need to find the tsconfig.json of
20+
* @returns {import("get-tsconfig").TsConfigResult}
21+
*/
22+
function getTSConfigForFile(filename) {
23+
return getTsconfig(filename, "tsconfig.json", fsCache)
24+
}
25+
26+
module.exports = {
27+
getTSConfig,
28+
getTSConfigForFile,
29+
}
30+
31+
module.exports.schema = { type: "string" }
Lines changed: 92 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,31 @@
11
"use strict"
22

3+
const { getTSConfig, getTSConfigForFile } = require("./get-tsconfig")
4+
35
const DEFAULT_MAPPING = normalise([
6+
["", ".js"],
7+
[".ts", ".js"],
8+
[".cts", ".cjs"],
9+
[".mts", ".mjs"],
10+
[".tsx", ".js"],
11+
])
12+
13+
const PRESERVE_MAPPING = normalise([
414
["", ".js"],
515
[".ts", ".js"],
616
[".cts", ".cjs"],
717
[".mts", ".mjs"],
818
[".tsx", ".jsx"],
919
])
1020

21+
const tsConfigMapping = {
22+
react: DEFAULT_MAPPING, // Emit .js files with JSX changed to the equivalent React.createElement calls
23+
"react-jsx": DEFAULT_MAPPING, // Emit .js files with the JSX changed to _jsx calls
24+
"react-jsxdev": DEFAULT_MAPPING, // Emit .js files with the JSX changed to _jsx calls
25+
"react-native": DEFAULT_MAPPING, // Emit .js files with the JSX unchanged
26+
preserve: PRESERVE_MAPPING, // Emit .jsx files with the JSX unchanged
27+
}
28+
1129
/**
1230
* @typedef {Object} ExtensionMap
1331
* @property {Record<string, string>} forward Convert from typescript to javascript
@@ -28,6 +46,22 @@ function normalise(typescriptExtensionMap) {
2846
return { forward, backward }
2947
}
3048

49+
/**
50+
* Attempts to get the ExtensionMap from the resolved tsconfig.
51+
*
52+
* @param {import("get-tsconfig").TsConfigJsonResolved} [tsconfig] - The resolved tsconfig
53+
* @returns {ExtensionMap} The `typescriptExtensionMap` value, or `null`.
54+
*/
55+
function getMappingFromTSConfig(tsconfig) {
56+
const jsx = tsconfig?.compilerOptions?.jsx
57+
58+
if ({}.hasOwnProperty.call(tsConfigMapping, jsx)) {
59+
return tsConfigMapping[jsx]
60+
}
61+
62+
return null
63+
}
64+
3165
/**
3266
* Gets `typescriptExtensionMap` property from a given option object.
3367
*
@@ -36,46 +70,82 @@ function normalise(typescriptExtensionMap) {
3670
*/
3771
function get(option) {
3872
if (
39-
option &&
40-
option.typescriptExtensionMap &&
41-
Array.isArray(option.typescriptExtensionMap)
73+
{}.hasOwnProperty.call(tsConfigMapping, option?.typescriptExtensionMap)
4274
) {
75+
return tsConfigMapping[option.typescriptExtensionMap]
76+
}
77+
78+
if (Array.isArray(option?.typescriptExtensionMap)) {
4379
return normalise(option.typescriptExtensionMap)
4480
}
4581

82+
if (option?.tsconfigPath) {
83+
return getMappingFromTSConfig(getTSConfig(option?.tsconfigPath))
84+
}
85+
4686
return null
4787
}
4888

89+
/**
90+
* Attempts to get the ExtensionMap from the tsconfig of a given file.
91+
*
92+
* @param {string} filename - The filename we're getting from
93+
* @returns {ExtensionMap} The `typescriptExtensionMap` value, or `null`.
94+
*/
95+
function getFromTSConfigFromFile(filename) {
96+
return getMappingFromTSConfig(getTSConfigForFile(filename)?.config)
97+
}
98+
4999
/**
50100
* Gets "typescriptExtensionMap" setting.
51101
*
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`.
102+
* 1. This checks `options.typescriptExtensionMap`, if its an array then it gets returned.
103+
* 2. This checks `options.typescriptExtensionMap`, if its a string, convert to the correct mapping.
104+
* 3. This checks `settings.n.typescriptExtensionMap`, if its an array then it gets returned.
105+
* 4. This checks `settings.node.typescriptExtensionMap`, if its an array then it gets returned.
106+
* 5. This checks `settings.n.typescriptExtensionMap`, if its a string, convert to the correct mapping.
107+
* 6. This checks `settings.node.typescriptExtensionMap`, if its a string, convert to the correct mapping.
108+
* 7. This checks for a `tsconfig.json` `config.compilerOptions.jsx` property, if its a string, convert to the correct mapping.
109+
* 8. This returns `PRESERVE_MAPPING`.
55110
*
56-
* @param {import('eslint').Rule.RuleContext} context - The rule context.
111+
* @param {import("eslint").Rule.RuleContext} context - The rule context.
57112
* @returns {string[]} A list of extensions.
58113
*/
59114
module.exports = function getTypescriptExtensionMap(context) {
60115
return (
61-
get(context.options && context.options[0]) ||
62-
get(
63-
context.settings && (context.settings.n || context.settings.node)
116+
get(context.options?.[0]) ||
117+
get(context.settings?.n ?? context.settings?.node) ||
118+
getFromTSConfigFromFile(
119+
// eslint ^8
120+
context.physicalFilename ??
121+
// eslint ^7.28 (deprecated ^8)
122+
context.getPhysicalFilename?.() ??
123+
// eslint ^8 (if physicalFilename undefined)
124+
context.filename ??
125+
// eslint ^7 (deprecated ^8)
126+
context.getFilename?.()
64127
) ||
65-
// TODO: Detect tsconfig.json here
66-
DEFAULT_MAPPING
128+
PRESERVE_MAPPING
67129
)
68130
}
69131

70132
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,
133+
oneOf: [
134+
{
135+
type: "array",
136+
items: {
137+
type: "array",
138+
prefixItems: [
139+
{ type: "string", pattern: "^(?:|\\.\\w+)$" },
140+
{ type: "string", pattern: "^\\.\\w+$" },
141+
],
142+
additionalItems: false,
143+
},
144+
uniqueItems: true,
145+
},
146+
{
147+
type: "string",
148+
enum: Object.keys(tsConfigMapping),
149+
},
150+
],
81151
}

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"@eslint-community/eslint-utils": "^4.4.0",
1818
"builtins": "^5.0.1",
1919
"eslint-plugin-es-x": "^7.1.0",
20+
"get-tsconfig": "^4.7.0",
2021
"ignore": "^5.2.4",
2122
"is-core-module": "^2.12.1",
2223
"minimatch": "^3.1.2",
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"compilerOptions": {
3+
"jsx": "react"
4+
}
5+
}

tests/fixtures/no-missing/ts-extends/d.ts

Whitespace-only changes.

0 commit comments

Comments
 (0)