Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Allow for automatic ts mapping detection #114

Merged
merged 6 commits into from
Sep 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions docs/rules/file-extension-in-import.md
Original file line number Diff line number Diff line change
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": [
scagood marked this conversation as resolved.
Show resolved Hide resolved
[ "", ".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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"compilerOptions": {
"jsx": "react"
}
}
Loading
Loading