Skip to content

Commit

Permalink
feat: Allow for automatic ts mapping detection
Browse files Browse the repository at this point in the history
  • Loading branch information
scagood committed Sep 5, 2023
1 parent 20d2713 commit 889dc2e
Show file tree
Hide file tree
Showing 17 changed files with 269 additions and 19 deletions.
36 changes: 36 additions & 0 deletions docs/rules/file-extension-in-import.md
Expand Up @@ -90,6 +90,42 @@ import styles from "./styles.css"
import logo from "./logo.png"
```

### Shared Settings

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

#### typescriptExtensionMap

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

You can also use the [typescript compiler jsx options](https://www.typescriptlang.org/tsconfig#jsx) to automatically use the correct mapping.

If this option is left undefined we:

1. Check your `tsconfig.json` `compilerOptions.jsx`
2. Return the default mapping (jsx = `preserve`)

```js
// .eslintrc.js
module.exports = {
"settings": {
"node": {
"typescriptExtensionMap": [
[ "", ".js" ],
[ ".ts", ".js" ],
[ ".cts", ".cjs" ],
[ ".mts", ".mjs" ],
[ ".tsx", ".jsx" ],
]
}
},
"rules": {
"n/file-extension-in-import": "error"
}
}
```

## 🔎 Implementation

- [Rule source](../../lib/rules/file-extension-in-import.js)
Expand Down
8 changes: 8 additions & 0 deletions docs/rules/no-missing-import.md
Expand Up @@ -73,6 +73,14 @@ Default is `[]`

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

You can also use the [typescript compiler jsx options](https://www.typescriptlang.org/tsconfig#jsx) to automatically use the correct mapping.

If this option is left undefined we:

1. Check the Shared Settings
2. Check your `tsconfig.json` `compilerOptions.jsx`
3. Return the default mapping (jsx = `preserve`)

Default is:

```json
Expand Down
8 changes: 8 additions & 0 deletions docs/rules/no-missing-require.md
Expand Up @@ -86,6 +86,14 @@ Default is `[".js", ".json", ".node"]`.

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

You can also use the [typescript compiler jsx options](https://www.typescriptlang.org/tsconfig#jsx) to automatically use the correct mapping.

If this option is left undefined we:

1. Check the Shared Settings
2. Check your `tsconfig.json` `compilerOptions.jsx`
3. Return the default mapping (jsx = `preserve`)

Default is:

```json
Expand Down
101 changes: 82 additions & 19 deletions lib/util/get-typescript-extension-map.js
@@ -1,13 +1,32 @@
"use strict"

const { getTsconfig } = require("get-tsconfig")
const fsCache = new Map()

const DEFAULT_MAPPING = normalise([
["", ".js"],
[".ts", ".js"],
[".cts", ".cjs"],
[".mts", ".mjs"],
[".tsx", ".js"],
])

const PRESERVE_MAPPING = normalise([
["", ".js"],
[".ts", ".js"],
[".cts", ".cjs"],
[".mts", ".mjs"],
[".tsx", ".jsx"],
])

const tsConfigMapping = {
react: DEFAULT_MAPPING, // Emit .js files with JSX changed to the equivalent React.createElement calls
"react-jsx": DEFAULT_MAPPING, // Emit .js files with the JSX changed to _jsx calls
"react-jsxdev": DEFAULT_MAPPING, // Emit .js files with the JSX changed to _jsx calls
"react-native": DEFAULT_MAPPING, // Emit .js files with the JSX unchanged
preserve: PRESERVE_MAPPING, // Emit .jsx files with the JSX unchanged
}

/**
* @typedef {Object} ExtensionMap
* @property {Record<string, string>} forward Convert from typescript to javascript
Expand Down Expand Up @@ -35,25 +54,61 @@ function normalise(typescriptExtensionMap) {
* @returns {ExtensionMap} The `typescriptExtensionMap` value, or `null`.
*/
function get(option) {
if (!option || !option.typescriptExtensionMap) {
return null
}

if (
option &&
option.typescriptExtensionMap &&
Array.isArray(option.typescriptExtensionMap)
{}.hasOwnProperty.call(tsConfigMapping, option.typescriptExtensionMap)
) {
return tsConfigMapping[option.typescriptExtensionMap]
}

if (Array.isArray(option.typescriptExtensionMap)) {
return normalise(option.typescriptExtensionMap)
}
}

/**
* Attempts to get the ExtensionMap from the tsconfig of a given file.
*
* @param {string} filename - The filename we're getting from
* @returns {ExtensionMap} The `typescriptExtensionMap` value, or `null`.
*/
function getFromTSConfig(filename) {
const tsconfig = getTsconfig(filename, "tsconfig.json", fsCache)

if (
!tsconfig ||
!tsconfig.config ||
!tsconfig.config.compilerOptions ||
!tsconfig.config.compilerOptions.jsx
) {
return null
}

const jsx = tsconfig.config.compilerOptions.jsx

if ({}.hasOwnProperty.call(tsConfigMapping, jsx)) {
return tsConfigMapping[jsx]
}

return null
}

/**
* Gets "typescriptExtensionMap" setting.
*
* 1. This checks `options` property, then returns it if exists.
* 2. This checks `settings.n` | `settings.node` property, then returns it if exists.
* 3. This returns `DEFAULT_MAPPING`.
* 1. This checks `options.typescriptExtensionMap`, if its an array then it gets returned.
* 2. This checks `options.typescriptExtensionMap`, if its a string, convert to the correct mapping.
* 3. This checks `settings.n.typescriptExtensionMap`, if its an array then it gets returned.
* 4. This checks `settings.node.typescriptExtensionMap`, if its an array then it gets returned.
* 5. This checks `settings.n.typescriptExtensionMap`, if its a string, convert to the correct mapping.
* 6. This checks `settings.node.typescriptExtensionMap`, if its a string, convert to the correct mapping.
* 7. This checks for a `tsconfig.json` `config.compilerOptions.jsx` property, if its a string, convert to the correct mapping.
* 8. This returns `PRESERVE_MAPPING`.
*
* @param {import('eslint').Rule.RuleContext} context - The rule context.
* @param {import("eslint").Rule.RuleContext} context - The rule context.
* @returns {string[]} A list of extensions.
*/
module.exports = function getTypescriptExtensionMap(context) {
Expand All @@ -62,20 +117,28 @@ module.exports = function getTypescriptExtensionMap(context) {
get(
context.settings && (context.settings.n || context.settings.node)
) ||
// TODO: Detect tsconfig.json here
DEFAULT_MAPPING
getFromTSConfig(context.filename) ||
PRESERVE_MAPPING
)
}

module.exports.schema = {
type: "array",
items: {
type: "array",
prefixItems: [
{ type: "string", pattern: "^(?:|\\.\\w+)$" },
{ type: "string", pattern: "^\\.\\w+$" },
],
additionalItems: false,
},
uniqueItems: true,
oneOf: [
{
type: "array",
items: {
type: "array",
prefixItems: [
{ type: "string", pattern: "^(?:|\\.\\w+)$" },
{ type: "string", pattern: "^\\.\\w+$" },
],
additionalItems: false,
},
uniqueItems: true,
},
{
type: "string",
enum: Object.keys(tsConfigMapping),
},
],
}
1 change: 1 addition & 0 deletions package.json
Expand Up @@ -17,6 +17,7 @@
"@eslint-community/eslint-utils": "^4.4.0",
"builtins": "^5.0.1",
"eslint-plugin-es-x": "^7.1.0",
"get-tsconfig": "^4.7.0",
"ignore": "^5.2.4",
"is-core-module": "^2.12.1",
"minimatch": "^3.1.2",
Expand Down
5 changes: 5 additions & 0 deletions tests/fixtures/no-missing/ts-extends/base.tsconfig.json
@@ -0,0 +1,5 @@
{
"compilerOptions": {
"jsx": "react"
}
}
Empty file.
Empty file.
3 changes: 3 additions & 0 deletions tests/fixtures/no-missing/ts-extends/tsconfig.json
@@ -0,0 +1,3 @@
{
"extends": ["./base.tsconfig.json"]
}
Empty file.
Empty file.
5 changes: 5 additions & 0 deletions tests/fixtures/no-missing/ts-preserve/tsconfig.json
@@ -0,0 +1,5 @@
{
"compilerOptions": {
"jsx": "preserve"
}
}
Empty file.
Empty file.
5 changes: 5 additions & 0 deletions tests/fixtures/no-missing/ts-react/tsconfig.json
@@ -0,0 +1,5 @@
{
"compilerOptions": {
"jsx": "react"
}
}
58 changes: 58 additions & 0 deletions tests/lib/rules/no-missing-import.js
Expand Up @@ -212,6 +212,64 @@ ruleTester.run("no-missing-import", rule, {
env: { node: true },
},

// tsx mapping by name
{
filename: fixture("test.tsx"),
code: "import e from './e.jsx';",
options: [{ typescriptExtensionMap: "preserve" }],
env: { node: true },
},
{
filename: fixture("test.tsx"),
code: "import e from './e.js';",
options: [{ typescriptExtensionMap: "react" }],
env: { node: true },
},
{
filename: fixture("test.tsx"),
code: "import e from './e.jsx';",
settings: { node: { typescriptExtensionMap: "preserve" } },
env: { node: true },
},
{
filename: fixture("test.tsx"),
code: "import e from './e.js';",
settings: { node: { typescriptExtensionMap: "react" } },
env: { node: true },
},

// tsx from config
{
filename: fixture("ts-react/test.tsx"),
code: "import e from './e.js';",
env: { node: true },
},
{
filename: fixture("ts-react/test.ts"),
code: "import d from './d.js';",
env: { node: true },
},
{
filename: fixture("ts-preserve/test.tsx"),
code: "import e from './e.jsx';",
env: { node: true },
},
{
filename: fixture("ts-preserve/test.ts"),
code: "import d from './d.js';",
env: { node: true },
},
{
filename: fixture("ts-extends/test.tsx"),
code: "import e from './e.js';",
env: { node: true },
},
{
filename: fixture("ts-extends/test.ts"),
code: "import d from './d.js';",
env: { node: true },
},

// import()
...(DynamicImportSupported
? [
Expand Down
58 changes: 58 additions & 0 deletions tests/lib/rules/no-missing-require.js
Expand Up @@ -281,6 +281,64 @@ ruleTester.run("no-missing-require", rule, {
env: { node: true },
},

// tsx mapping by name
{
filename: fixture("test.tsx"),
code: "require('./e.jsx');",
options: [{ typescriptExtensionMap: "preserve" }],
env: { node: true },
},
{
filename: fixture("test.tsx"),
code: "require('./e.js');",
options: [{ typescriptExtensionMap: "react" }],
env: { node: true },
},
{
filename: fixture("test.tsx"),
code: "require('./e.jsx');",
settings: { node: { typescriptExtensionMap: "preserve" } },
env: { node: true },
},
{
filename: fixture("test.tsx"),
code: "require('./e.js');",
settings: { node: { typescriptExtensionMap: "react" } },
env: { node: true },
},

// tsx from config
{
filename: fixture("ts-react/test.tsx"),
code: "require('./e.js');",
env: { node: true },
},
{
filename: fixture("ts-react/test.ts"),
code: "require('./d.js');",
env: { node: true },
},
{
filename: fixture("ts-preserve/test.tsx"),
code: "require('./e.jsx');",
env: { node: true },
},
{
filename: fixture("ts-preserve/test.ts"),
code: "require('./d.js');",
env: { node: true },
},
{
filename: fixture("ts-extends/test.tsx"),
code: "require('./e.js');",
env: { node: true },
},
{
filename: fixture("ts-extends/test.ts"),
code: "require('./d.js');",
env: { node: true },
},

// require.resolve
{
filename: fixture("test.js"),
Expand Down

0 comments on commit 889dc2e

Please sign in to comment.