Skip to content

Commit

Permalink
feat: add the ability to configure typescript to javascript file exte…
Browse files Browse the repository at this point in the history
…nsion conversion (#112)
  • Loading branch information
scagood committed Sep 5, 2023
1 parent d7bf4e1 commit 20d2713
Show file tree
Hide file tree
Showing 11 changed files with 319 additions and 27 deletions.
24 changes: 24 additions & 0 deletions docs/rules/no-missing-import.md
Expand Up @@ -69,13 +69,30 @@ If a path is relative, it will be resolved from CWD.

Default is `[]`

#### typescriptExtensionMap

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

Default is:

```json
[
[ "", ".js" ],
[ ".ts", ".js" ],
[ ".cts", ".cjs" ],
[ ".mts", ".mjs" ],
[ ".tsx", ".jsx" ],
]
```

### 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.

- `allowModules`
- `resolvePaths`
- `typescriptExtensionMap`

```js
// .eslintrc.js
Expand All @@ -84,6 +101,13 @@ module.exports = {
"node": {
"allowModules": ["electron"],
"resolvePaths": [__dirname],
"typescriptExtensionMap": [
[ "", ".js" ],
[ ".ts", ".js" ],
[ ".cts", ".cjs" ],
[ ".mts", ".mjs" ],
[ ".tsx", ".js" ],
]
}
},
"rules": {
Expand Down
26 changes: 25 additions & 1 deletion docs/rules/no-missing-require.md
Expand Up @@ -82,6 +82,22 @@ When an import path does not exist, this rule checks whether or not any of `path

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

#### typescriptExtensionMap

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

Default is:

```json
[
[ "", ".js" ],
[ ".ts", ".js" ],
[ ".cts", ".cjs" ],
[ ".mts", ".mjs" ],
[ ".tsx", ".jsx" ],
]
```

### Shared Settings

The following options can be set by [shared settings](http://eslint.org/docs/user-guide/configuring.html#adding-shared-settings).
Expand All @@ -90,6 +106,7 @@ Several rules have the same option, but we can set this option at once.
- `allowModules`
- `resolvePaths`
- `tryExtensions`
- `typescriptExtensionMap`

```js
// .eslintrc.js
Expand All @@ -98,7 +115,14 @@ module.exports = {
"node": {
"allowModules": ["electron"],
"resolvePaths": [__dirname],
"tryExtensions": [".js", ".json", ".node"]
"tryExtensions": [".js", ".json", ".node"],
"typescriptExtensionMap": [
[ "", ".js" ],
[ ".ts", ".js" ],
[ ".cts", ".cjs" ],
[ ".mts", ".mjs" ],
[ ".tsx", ".js" ],
]
}
},
"rules": {
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 getTypescriptExtensionMap = require("../util/get-typescript-extension-map")
const visitImport = require("../util/visit-import")

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

module.exports = {
Expand All @@ -28,6 +29,7 @@ module.exports = {
allowModules: getAllowModules.schema,
tryExtensions: getTryExtensions.schema,
resolvePaths: getResolvePaths.schema,
typescriptExtensionMap: getTypescriptExtensionMap.schema,
},
additionalProperties: false,
},
Expand Down
14 changes: 9 additions & 5 deletions lib/util/check-existence.js
Expand Up @@ -32,17 +32,21 @@ exports.checkExistence = function checkExistence(context, targets) {
let missingFile = target.moduleName == null && !exists(target.filePath)
if (missingFile && isTypescript(context)) {
const parsed = path.parse(target.filePath)
const reversedExt = mapTypescriptExtension(
const reversedExts = mapTypescriptExtension(
context,
target.filePath,
parsed.ext,
true
)
const reversedPath =
path.resolve(parsed.dir, parsed.name) + reversedExt
missingFile = target.moduleName == null && !exists(reversedPath)
const reversedPaths = reversedExts.map(
reversedExt =>
path.resolve(parsed.dir, parsed.name) + reversedExt
)
missingFile = reversedPaths.every(
reversedPath =>
target.moduleName == null && !exists(reversedPath)
)
}

if (missingModule || missingFile) {
context.report({
node: target.node,
Expand Down
81 changes: 81 additions & 0 deletions lib/util/get-typescript-extension-map.js
@@ -0,0 +1,81 @@
"use strict"

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

/**
* @typedef {Object} ExtensionMap
* @property {Record<string, string>} forward Convert from typescript to javascript
* @property {Record<string, string[]>} backward Convert from javascript to typescript
*/

function normalise(typescriptExtensionMap) {
const forward = {}
const backward = {}
for (const [typescript, javascript] of typescriptExtensionMap) {
forward[typescript] = javascript
if (!typescript) {
continue
}
backward[javascript] ??= []

This comment has been minimized.

Copy link
@doronpr

doronpr Sep 21, 2023

@scagood this introduces backward incompatibility on Node v16.
Can you please merge this fix?

This comment has been minimized.

Copy link
@scagood

scagood Sep 21, 2023

Author

This comment has been minimized.

Copy link
@doronpr

doronpr via email Sep 21, 2023

backward[javascript].push(typescript)
}
return { forward, backward }
}

/**
* Gets `typescriptExtensionMap` property from a given option object.
*
* @param {object|undefined} option - An option object to get.
* @returns {ExtensionMap} The `typescriptExtensionMap` value, or `null`.
*/
function get(option) {
if (
option &&
option.typescriptExtensionMap &&
Array.isArray(option.typescriptExtensionMap)
) {
return normalise(option.typescriptExtensionMap)
}

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`.
*
* @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)
) ||
// TODO: Detect tsconfig.json here
DEFAULT_MAPPING
)
}

module.exports.schema = {
type: "array",
items: {
type: "array",
prefixItems: [
{ type: "string", pattern: "^(?:|\\.\\w+)$" },
{ type: "string", pattern: "^\\.\\w+$" },
],
additionalItems: false,
},
uniqueItems: true,
}
30 changes: 9 additions & 21 deletions lib/util/map-typescript-extension.js
@@ -1,22 +1,8 @@
"use strict"

const path = require("path")
const isTypescript = require("../util/is-typescript")

const mapping = {
"": ".js", // default empty extension will map to js
".ts": ".js",
".cts": ".cjs",
".mts": ".mjs",
".tsx": ".jsx",
}

const reverseMapping = {
".js": ".ts",
".cjs": ".cts",
".mjs": ".mts",
".jsx": ".tsx",
}
const isTypescript = require("./is-typescript")
const getTypescriptExtensionMap = require("./get-typescript-extension-map")

/**
* Maps the typescript file extension that should be added in an import statement,
Expand All @@ -25,7 +11,7 @@ const reverseMapping = {
* For example, in typescript, when referencing another typescript from a typescript file,
* a .js extension should be used instead of the original .ts extension of the referenced file.
*
* @param {RuleContext} context
* @param {import('eslint').Rule.RuleContext} context
* @param {string} filePath The filePath of the import
* @param {string} fallbackExtension The non-typescript fallback
* @param {boolean} reverse Execute a reverse path mapping
Expand All @@ -37,14 +23,16 @@ module.exports = function mapTypescriptExtension(
fallbackExtension,
reverse = false
) {
const { forward, backward } = getTypescriptExtensionMap(context)
const ext = path.extname(filePath)
if (reverse) {
if (isTypescript(context) && ext in reverseMapping) {
return reverseMapping[ext]
if (isTypescript(context) && ext in backward) {
return backward[ext]
}
return [fallbackExtension]
} else {
if (isTypescript(context) && ext in mapping) {
return mapping[ext]
if (isTypescript(context) && ext in forward) {
return forward[ext]
}
}

Expand Down
1 change: 1 addition & 0 deletions package.json
Expand Up @@ -26,6 +26,7 @@
"devDependencies": {
"@eslint/eslintrc": "^2.0.3",
"@eslint/js": "^8.43.0",
"@types/eslint": "^8.44.2",
"@typescript-eslint/parser": "^5.60.0",
"codecov": "^3.8.2",
"esbuild": "^0.18.7",
Expand Down
34 changes: 34 additions & 0 deletions tests/lib/rules/file-extension-in-import.js
Expand Up @@ -21,6 +21,14 @@ if (!DynamicImportSupported) {
)
}

const tsReactExtensionMap = [
["", ".js"],
[".ts", ".js"],
[".cts", ".cjs"],
[".mts", ".mjs"],
[".tsx", ".js"],
]

function fixture(filename) {
return path.resolve(
__dirname,
Expand Down Expand Up @@ -146,6 +154,32 @@ new RuleTester({
code: "import './c'",
options: ["never", { ".json": "always" }],
},

// typescriptExtensionMap
{
filename: fixture("test.tsx"),
code: "require('./d.js');",
env: { node: true },
settings: { node: { typescriptExtensionMap: tsReactExtensionMap } },
},
{
filename: fixture("test.tsx"),
code: "require('./e.js');",
env: { node: true },
settings: { node: { typescriptExtensionMap: tsReactExtensionMap } },
},
{
filename: fixture("test.ts"),
code: "require('./d.js');",
env: { node: true },
settings: { node: { typescriptExtensionMap: tsReactExtensionMap } },
},
{
filename: fixture("test.ts"),
code: "require('./e.js');",
env: { node: true },
settings: { node: { typescriptExtensionMap: tsReactExtensionMap } },
},
],
invalid: [
{
Expand Down

0 comments on commit 20d2713

Please sign in to comment.