Skip to content

Commit

Permalink
feat: Allow for automatic ts mapping detection (#114)
Browse files Browse the repository at this point in the history
  • Loading branch information
scagood committed Sep 11, 2023
1 parent 78595c4 commit 2ab30ce
Show file tree
Hide file tree
Showing 20 changed files with 399 additions and 49 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
12 changes: 12 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 All @@ -85,6 +93,10 @@ Default is:
]
```

#### tsconfigPath

Adds the ability to specify the tsconfig used by the typescriptExtensionMap tool.

### Shared Settings

The following options can be set by [shared settings](http://eslint.org/docs/user-guide/configuring.html#adding-shared-settings).
Expand Down
12 changes: 12 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 All @@ -98,6 +106,10 @@ Default is:
]
```

#### tsconfigPath

Adds the ability to specify the tsconfig used by the typescriptExtensionMap tool.

### Shared Settings

The following options can be set by [shared settings](http://eslint.org/docs/user-guide/configuring.html#adding-shared-settings).
Expand Down
2 changes: 2 additions & 0 deletions lib/rules/no-missing-import.js
Expand Up @@ -7,6 +7,7 @@
const { checkExistence, messages } = require("../util/check-existence")
const getAllowModules = require("../util/get-allow-modules")
const getResolvePaths = require("../util/get-resolve-paths")
const getTSConfig = require("../util/get-tsconfig")
const getTypescriptExtensionMap = require("../util/get-typescript-extension-map")
const visitImport = require("../util/visit-import")

Expand All @@ -28,6 +29,7 @@ module.exports = {
allowModules: getAllowModules.schema,
resolvePaths: getResolvePaths.schema,
typescriptExtensionMap: getTypescriptExtensionMap.schema,
tsconfigPath: getTSConfig.schema,
},
additionalProperties: false,
},
Expand Down
2 changes: 2 additions & 0 deletions lib/rules/no-missing-require.js
Expand Up @@ -7,6 +7,7 @@
const { checkExistence, messages } = require("../util/check-existence")
const getAllowModules = require("../util/get-allow-modules")
const getResolvePaths = require("../util/get-resolve-paths")
const getTSConfig = require("../util/get-tsconfig")
const getTryExtensions = require("../util/get-try-extensions")
const getTypescriptExtensionMap = require("../util/get-typescript-extension-map")
const visitRequire = require("../util/visit-require")
Expand All @@ -30,6 +31,7 @@ module.exports = {
tryExtensions: getTryExtensions.schema,
resolvePaths: getResolvePaths.schema,
typescriptExtensionMap: getTypescriptExtensionMap.schema,
tsconfigPath: getTSConfig.schema,
},
additionalProperties: false,
},
Expand Down
31 changes: 31 additions & 0 deletions lib/util/get-tsconfig.js
@@ -0,0 +1,31 @@
"use strict"

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

/**
* Attempts to get the ExtensionMap from the tsconfig given the path to the tsconfig file.
*
* @param {string} filename - The path to the tsconfig.json file
* @returns {import("get-tsconfig").TsConfigJsonResolved}
*/
function getTSConfig(filename) {
return parseTsconfig(filename, fsCache)
}

/**
* Attempts to get the ExtensionMap from the tsconfig of a given file.
*
* @param {string} filename - The path to the file we need to find the tsconfig.json of
* @returns {import("get-tsconfig").TsConfigResult}
*/
function getTSConfigForFile(filename) {
return getTsconfig(filename, "tsconfig.json", fsCache)
}

module.exports = {
getTSConfig,
getTSConfigForFile,
}

module.exports.schema = { type: "string" }
114 changes: 92 additions & 22 deletions lib/util/get-typescript-extension-map.js
@@ -1,13 +1,31 @@
"use strict"

const { getTSConfig, getTSConfigForFile } = require("./get-tsconfig")

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 All @@ -28,6 +46,22 @@ function normalise(typescriptExtensionMap) {
return { forward, backward }
}

/**
* Attempts to get the ExtensionMap from the resolved tsconfig.
*
* @param {import("get-tsconfig").TsConfigJsonResolved} [tsconfig] - The resolved tsconfig
* @returns {ExtensionMap} The `typescriptExtensionMap` value, or `null`.
*/
function getMappingFromTSConfig(tsconfig) {
const jsx = tsconfig?.compilerOptions?.jsx

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

return null
}

/**
* Gets `typescriptExtensionMap` property from a given option object.
*
Expand All @@ -36,46 +70,82 @@ function normalise(typescriptExtensionMap) {
*/
function get(option) {
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)
}

if (option?.tsconfigPath) {
return getMappingFromTSConfig(getTSConfig(option?.tsconfigPath))
}

return null
}

/**
* 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 getFromTSConfigFromFile(filename) {
return getMappingFromTSConfig(getTSConfigForFile(filename)?.config)
}

/**
* 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) {
return (
get(context.options && context.options[0]) ||
get(
context.settings && (context.settings.n || context.settings.node)
get(context.options?.[0]) ||
get(context.settings?.n ?? context.settings?.node) ||
getFromTSConfigFromFile(
// eslint ^8
context.physicalFilename ??
// eslint ^7.28 (deprecated ^8)
context.getPhysicalFilename?.() ??
// eslint ^8 (if physicalFilename undefined)
context.filename ??
// eslint ^7 (deprecated ^8)
context.getFilename?.()
) ||
// TODO: Detect tsconfig.json here
DEFAULT_MAPPING
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"
}
}

0 comments on commit 2ab30ce

Please sign in to comment.