Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ For [Shareable Configs](https://eslint.org/docs/latest/developer-guide/shareable
| [prefer-global/text-encoder](docs/rules/prefer-global/text-encoder.md) | enforce either `TextEncoder` or `require("util").TextEncoder` | | | |
| [prefer-global/url](docs/rules/prefer-global/url.md) | enforce either `URL` or `require("url").URL` | | | |
| [prefer-global/url-search-params](docs/rules/prefer-global/url-search-params.md) | enforce either `URLSearchParams` or `require("url").URLSearchParams` | | | |
| [prefer-node-protocol](docs/rules/prefer-node-protocol.md) | enforce using the `node:` protocol when importing Node.js builtin modules. | | 🔧 | |
| [prefer-promises/dns](docs/rules/prefer-promises/dns.md) | enforce `require("dns").promises` | | | |
| [prefer-promises/fs](docs/rules/prefer-promises/fs.md) | enforce `require("fs").promises` | | | |
| [process-exit-as-throw](docs/rules/process-exit-as-throw.md) | require that `process.exit()` expressions use the same code path as `throw` | ☑️ 🟢 ✅ ☑️ 🟢 ✅ | | |
Expand Down
52 changes: 52 additions & 0 deletions docs/rules/prefer-node-protocol.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Enforce using the `node:` protocol when importing Node.js builtin modules (`n/prefer-node-protocol`)

🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).

<!-- end auto-generated rule header -->

Older built-in Node modules such as fs now can be imported via either their name or `node:` + their name:

```js
import fs from "fs"
import fs from "node:fs"
```

The prefixed versions are nice because they can't be overridden by user modules and are similarly formatted to prefix-only modules such as node:test.

Note that Node.js support for this feature began in:

> v16.0.0, v14.18.0 (`require()`)
> v14.13.1, v12.20.0 (`import`)

## 📖 Rule Details

This rule enforces that `node:` protocol is prepended to built-in Node modules when importing or exporting built-in Node modules.

👍 Examples of **correct** code for this rule:

```js
/*eslint n/prefer-node-protocol: error */

import fs from "node:fs"

export { promises } from "node:fs"

const fs = require("node:fs")
```

👎 Examples of **incorrect** code for this rule:

```js
/*eslint n/prefer-node-protocol: error */

import fs from "fs"

export { promises } from "fs"

const fs = require("fs")
```

## 🔎 Implementation

- [Rule source](../../lib/rules/prefer-node-protocol.js)
- [Test source](../../tests/lib/rules/prefer-node-protocol.js)
1 change: 1 addition & 0 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ const rules = {
"prefer-global/text-encoder": require("./rules/prefer-global/text-encoder"),
"prefer-global/url-search-params": require("./rules/prefer-global/url-search-params"),
"prefer-global/url": require("./rules/prefer-global/url"),
"prefer-node-protocol": require("./rules/prefer-node-protocol"),
"prefer-promises/dns": require("./rules/prefer-promises/dns"),
"prefer-promises/fs": require("./rules/prefer-promises/fs"),
"process-exit-as-throw": require("./rules/process-exit-as-throw"),
Expand Down
108 changes: 108 additions & 0 deletions lib/rules/prefer-node-protocol.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/**
* @author Yusuke Iinuma
* See LICENSE file in root directory for full license.
*/
"use strict"

const isBuiltinModule = require("is-builtin-module")
const messageId = "preferNodeProtocol"

module.exports = {
meta: {
docs: {
description:
"enforce using the `node:` protocol when importing Node.js builtin modules.",
recommended: true,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the recommended config changes would be a breaking change.

Suggested change
recommended: true,
recommended: false,

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for your review 🙏
I fixed it.

url: "https://github.com/eslint-community/eslint-plugin-n/blob/HEAD/docs/rules/prefer-node-protocol.md",
},
fixable: "code",
messages: {
[messageId]: "Prefer `node:{{moduleName}}` over `{{moduleName}}`.",
},
schema: [],
type: "suggestion",
},
create(context) {
function isSourceForImportOrExport(node) {
return (
(node.parent.type === "ImportDeclaration" ||
node.parent.type === "ExportNamedDeclaration" ||
node.parent.type === "ImportExpression") &&
node.parent.source === node
)
}

function isCallExpression(node, { name, argumentsLength }) {
if (node?.type !== "CallExpression") {
return false
}

if (node.optional) {
return false
}

if (node.arguments.length !== argumentsLength) {
return false
}

if (
node.callee.type !== "Identifier" ||
node.callee.name !== name
) {
return false
}

return true
}

function isStringLiteral(node) {
return node?.type === "Literal" && typeof node.type === "string"
}

function isStaticRequire(node) {
return (
isCallExpression(node, {
name: "require",
argumentsLength: 1,
}) && isStringLiteral(node.arguments[0])
)
}

return {
Literal(node) {
if (
!(
isSourceForImportOrExport(node) ||
(isStaticRequire(node.parent) &&
node.parent.arguments[0] === node)
)
) {
return
}

const { value } = node

if (
typeof value !== "string" ||
value.startsWith("node:") ||
!isBuiltinModule(value) ||
!isBuiltinModule(`node:${value}`)
) {
return
}

context.report({
node,
messageId,
fix(fixer) {
const firstCharacterIndex = node.range[0] + 1
return fixer.replaceTextRange(
[firstCharacterIndex, firstCharacterIndex],
"node:"
)
},
})
},
}
},
}
173 changes: 173 additions & 0 deletions tests/lib/rules/prefer-node-protocol.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
/**
* @author Yusuke Iinuma
* See LICENSE file in root directory for full license.
*/
"use strict"

const { RuleTester } = require("#eslint-rule-tester")
const rule = require("../../../lib/rules/prefer-node-protocol.js")

new RuleTester({
languageOptions: {
ecmaVersion: 2020,
sourceType: "module",
},
}).run("prefer-node-protocol", rule, {
valid: [
'import nodePlugin from "eslint-plugin-n";',
'import fs from "./fs";',
'import fs from "unknown-builtin-module";',
'import fs from "node:fs";',
`
async function foo() {
const fs = await import(fs);
}
`,
`
async function foo() {
const fs = await import(0);
}
`,
`
async function foo() {
const fs = await import(\`fs\`);
}
`,
'import "punycode/";',
// https://bun.sh/docs/runtime/bun-apis
'import "bun";',
'import "bun:jsc";',
'import "bun:sqlite";',
'export {promises} from "node:fs";',

// `require`
'const fs = require("node:fs");',
'const fs = require("node:fs/promises");',
"const fs = require(fs);",
'const fs = notRequire("fs");',
'const fs = foo.require("fs");',
'const fs = require.resolve("fs");',
"const fs = require(`fs`);",
'const fs = require?.("fs");',
'const fs = require("fs", extra);',
"const fs = require();",
'const fs = require(...["fs"]);',
'const fs = require("eslint-plugin-n");',
],
invalid: [
{
code: 'import fs from "fs";',
output: 'import fs from "node:fs";',
errors: ["Prefer `node:{{moduleName}}` over `{{moduleName}}`."],
},
{
code: 'export {promises} from "fs";',
output: 'export {promises} from "node:fs";',
errors: ["Prefer `node:{{moduleName}}` over `{{moduleName}}`."],
},
{
code: `
async function foo() {
const fs = await import('fs');
}
`,
output: `
async function foo() {
const fs = await import('node:fs');
}
`,
errors: ["Prefer `node:{{moduleName}}` over `{{moduleName}}`."],
},
{
code: 'import fs from "fs/promises";',
output: 'import fs from "node:fs/promises";',
errors: ["Prefer `node:{{moduleName}}` over `{{moduleName}}`."],
},
{
code: 'export {default} from "fs/promises";',
output: 'export {default} from "node:fs/promises";',
errors: ["Prefer `node:{{moduleName}}` over `{{moduleName}}`."],
},
{
code: `
async function foo() {
const fs = await import('fs/promises');
}
`,
output: `
async function foo() {
const fs = await import('node:fs/promises');
}
`,
errors: ["Prefer `node:{{moduleName}}` over `{{moduleName}}`."],
},
{
code: 'import {promises} from "fs";',
output: 'import {promises} from "node:fs";',
errors: ["Prefer `node:{{moduleName}}` over `{{moduleName}}`."],
},
{
code: 'export {default as promises} from "fs";',
output: 'export {default as promises} from "node:fs";',
errors: ["Prefer `node:{{moduleName}}` over `{{moduleName}}`."],
},
{
code: "import {promises} from 'fs';",
output: "import {promises} from 'node:fs';",
errors: ["Prefer `node:{{moduleName}}` over `{{moduleName}}`."],
},
{
code: `
async function foo() {
const fs = await import("fs/promises");
}
`,
output: `
async function foo() {
const fs = await import("node:fs/promises");
}
`,
errors: ["Prefer `node:{{moduleName}}` over `{{moduleName}}`."],
},
{
code: `
async function foo() {
const fs = await import(/* escaped */"\\u{66}s/promises");
}
`,
output: `
async function foo() {
const fs = await import(/* escaped */"node:\\u{66}s/promises");
}
`,
errors: ["Prefer `node:{{moduleName}}` over `{{moduleName}}`."],
},
{
code: 'import "buffer";',
output: 'import "node:buffer";',
errors: ["Prefer `node:{{moduleName}}` over `{{moduleName}}`."],
},
{
code: 'import "child_process";',
output: 'import "node:child_process";',
errors: ["Prefer `node:{{moduleName}}` over `{{moduleName}}`."],
},
{
code: 'import "timers/promises";',
output: 'import "node:timers/promises";',
errors: ["Prefer `node:{{moduleName}}` over `{{moduleName}}`."],
},

// `require`
{
code: 'const {promises} = require("fs")',
output: 'const {promises} = require("node:fs")',
errors: ["Prefer `node:{{moduleName}}` over `{{moduleName}}`."],
},
{
code: "const fs = require('fs/promises')",
output: "const fs = require('node:fs/promises')",
errors: ["Prefer `node:{{moduleName}}` over `{{moduleName}}`."],
},
],
})