diff --git a/docs/rules/prefer-node-protocol.md b/docs/rules/prefer-node-protocol.md new file mode 100644 index 0000000000..ac59da41a3 --- /dev/null +++ b/docs/rules/prefer-node-protocol.md @@ -0,0 +1,47 @@ +# Prefer using the `node:` protocol when importing Node.js builtin modules + +When importing builtin modules, it's better to use the [`node:` protocol](https://nodejs.org/api/esm.html#esm_node_imports) as it makes it perfectly clear that the package is a Node.js builtin module. + +And don't forget to [upvote this issue](https://github.com/nodejs/node/issues/38343) if you agree. + +This rule is fixable. + +## Fail + +```js +import dgram from 'dgram'; +``` + +```js +export {strict as default} from 'assert'; +``` + +```js +import fs from 'fs/promises'; +``` + +## Pass + +```js +import dgram from 'node:dgram'; +``` + +```js +export {strict as default} from 'node:assert'; +``` + +```js +import fs from 'node:fs/promises'; +``` + +```js +const fs = require('fs'); +``` + +```js +import _ from 'lodash'; +``` + +```js +import fs from './fs.js'; +``` diff --git a/index.js b/index.js index ed49177bbe..f077293edd 100644 --- a/index.js +++ b/index.js @@ -101,6 +101,7 @@ module.exports = { 'unicorn/prefer-modern-dom-apis': 'error', 'unicorn/prefer-module': 'error', 'unicorn/prefer-negative-index': 'error', + 'unicorn/prefer-node-protocol': 'error', 'unicorn/prefer-number-properties': 'error', 'unicorn/prefer-optional-catch-binding': 'error', 'unicorn/prefer-query-selector': 'error', diff --git a/package.json b/package.json index 7bbbe5e3af..11242e44aa 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "eslint-utils": "^2.1.0", "eslint-visitor-keys": "^2.0.0", "import-modules": "^2.1.0", + "is-builtin-module": "^3.1.0", "lodash": "^4.17.21", "pluralize": "^8.0.0", "read-pkg-up": "^7.0.1", diff --git a/readme.md b/readme.md index cce6845e5d..798f7b4547 100644 --- a/readme.md +++ b/readme.md @@ -92,6 +92,7 @@ Configure it in `package.json`. "unicorn/prefer-modern-dom-apis": "error", "unicorn/prefer-module": "error", "unicorn/prefer-negative-index": "error", + "unicorn/prefer-node-protocol": "error", "unicorn/prefer-number-properties": "error", "unicorn/prefer-optional-catch-binding": "error", "unicorn/prefer-query-selector": "error", @@ -184,6 +185,7 @@ Each rule has emojis denoting: | [prefer-modern-dom-apis](docs/rules/prefer-modern-dom-apis.md) | Prefer `.before()` over `.insertBefore()`, `.replaceWith()` over `.replaceChild()`, prefer one of `.before()`, `.after()`, `.append()` or `.prepend()` over `insertAdjacentText()` and `insertAdjacentElement()`. | ✅ | 🔧 | | [prefer-module](docs/rules/prefer-module.md) | Prefer JavaScript modules (ESM) over CommonJS. | ✅ | 🔧 | | [prefer-negative-index](docs/rules/prefer-negative-index.md) | Prefer negative index over `.length - index` for `{String,Array,TypedArray}#slice()` and `Array#splice()`. | ✅ | 🔧 | +| [prefer-node-protocol](docs/rules/prefer-node-protocol.md) | Prefer using the `node:` protocol when importing Node.js builtin modules. | ✅ | 🔧 | | [prefer-number-properties](docs/rules/prefer-number-properties.md) | Prefer `Number` static properties over global ones. | ✅ | 🔧 | | [prefer-optional-catch-binding](docs/rules/prefer-optional-catch-binding.md) | Prefer omitting the `catch` binding parameter. | ✅ | 🔧 | | [prefer-query-selector](docs/rules/prefer-query-selector.md) | Prefer `.querySelector()` over `.getElementById()`, `.querySelectorAll()` over `.getElementsByClassName()` and `.getElementsByTagName()`. | ✅ | 🔧 | diff --git a/rules/prefer-node-protocol.js b/rules/prefer-node-protocol.js new file mode 100644 index 0000000000..12747bb52b --- /dev/null +++ b/rules/prefer-node-protocol.js @@ -0,0 +1,53 @@ +'use strict'; +const isBuiltinModule = require('is-builtin-module'); +const getDocumentationUrl = require('./utils/get-documentation-url'); + +const MESSAGE_ID = 'prefer-node-protocol'; +const messages = { + [MESSAGE_ID]: 'Prefer `node:{{moduleName}}` over `{{moduleName}}`.' +}; + +const selector = [ + ':matches(ImportDeclaration, ExportNamedDeclaration, ImportExpression)', + ' > ', + 'Literal.source' +].join(''); + +/** @param {import('eslint').Rule.RuleContext} context */ +const create = context => { + return { + [selector](node) { + const {value} = node; + if ( + typeof value !== 'string' || + value.startsWith('node:') || + !isBuiltinModule(value) + ) { + return; + } + + const firstCharacterIndex = node.range[0] + 1; + context.report({ + node, + messageId: MESSAGE_ID, + data: {moduleName: value}, + /** @param {import('eslint').Rule.RuleFixer} fixer */ + fix: fixer => fixer.insertTextBeforeRange([firstCharacterIndex, firstCharacterIndex], 'node:') + }); + } + }; +}; + +module.exports = { + create, + meta: { + type: 'suggestion', + docs: { + description: 'Prefer using the `node:` protocol when importing Node.js builtin modules.', + url: getDocumentationUrl(__filename) + }, + fixable: 'code', + schema: [], + messages + } +}; diff --git a/scripts/generate-rules-table.mjs b/scripts/generate-rules-table.mjs index c2937543a6..a4c12a8900 100644 --- a/scripts/generate-rules-table.mjs +++ b/scripts/generate-rules-table.mjs @@ -1,10 +1,10 @@ // Automatically regenerates the rules table in readme.md. -import {readFileSync, writeFileSync} from 'fs'; -import path from 'path'; -import package_ from '../index.js'; -import {fileURLToPath} from 'url'; +import {readFileSync, writeFileSync} from 'node:fs'; +import path from 'node:path'; +import {fileURLToPath} from 'node:url'; import outdent from 'outdent'; +import package_ from '../index.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); diff --git a/test/better-regex.mjs b/test/better-regex.mjs index 00cdb57e88..79b93771f2 100644 --- a/test/better-regex.mjs +++ b/test/better-regex.mjs @@ -1,4 +1,4 @@ -import {createRequire} from 'module'; +import {createRequire} from 'node:module'; import {getTester} from './utils/test.mjs'; const {test} = getTester(import.meta); diff --git a/test/custom-error-definition.mjs b/test/custom-error-definition.mjs index 933df282ae..bbde4eb1ed 100644 --- a/test/custom-error-definition.mjs +++ b/test/custom-error-definition.mjs @@ -1,4 +1,4 @@ -import {createRequire} from 'module'; +import {createRequire} from 'node:module'; import test from 'ava'; import avaRuleTester from 'eslint-ava-rule-tester'; import outdent from 'outdent'; diff --git a/test/no-hex-escape.mjs b/test/no-hex-escape.mjs index 247888df2c..cf62b9c630 100644 --- a/test/no-hex-escape.mjs +++ b/test/no-hex-escape.mjs @@ -1,4 +1,4 @@ -import {createRequire} from 'module'; +import {createRequire} from 'node:module'; import test from 'ava'; import avaRuleTester from 'eslint-ava-rule-tester'; import {getTester} from './utils/test.mjs'; diff --git a/test/number-literal-case.mjs b/test/number-literal-case.mjs index 7278272ad2..7913e886aa 100644 --- a/test/number-literal-case.mjs +++ b/test/number-literal-case.mjs @@ -1,4 +1,4 @@ -import {createRequire} from 'module'; +import {createRequire} from 'node:module'; import test from 'ava'; import avaRuleTester from 'eslint-ava-rule-tester'; import outdent from 'outdent'; diff --git a/test/package.mjs b/test/package.mjs index 503023538a..cef7fbe6b1 100644 --- a/test/package.mjs +++ b/test/package.mjs @@ -1,5 +1,5 @@ -import fs from 'fs'; -import path from 'path'; +import fs from 'node:fs'; +import path from 'node:path'; import test from 'ava'; import pify from 'pify'; import index from '../index.js'; diff --git a/test/prefer-node-protocol.mjs b/test/prefer-node-protocol.mjs new file mode 100644 index 0000000000..61c569d115 --- /dev/null +++ b/test/prefer-node-protocol.mjs @@ -0,0 +1,79 @@ +import outdent from 'outdent'; +import {getTester} from './utils/test.mjs'; + +const {test} = getTester(import.meta); + +test.snapshot({ + valid: [ + 'import unicorn from "unicorn";', + 'import fs from "./fs";', + 'import fs from "unknown-builtin-module";', + 'const fs = require("fs");', + 'import fs from "node:fs";', + outdent` + async function foo() { + const fs = await import(fs); + } + `, + outdent` + async function foo() { + const fs = await import(0); + } + `, + outdent` + async function foo() { + const fs = await import(\`fs\`); + } + ` + ], + invalid: [ + 'import fs from "fs";', + 'export {promises} from "fs";', + outdent` + async function foo() { + const fs = await import('fs'); + } + `, + 'import fs from "fs/promises";', + 'export {default} from "fs/promises";', + outdent` + async function foo() { + const fs = await import('fs/promises'); + } + `, + 'import {promises} from "fs";', + 'export {default as promises} from "fs";', + 'import {promises} from \'fs\';', + outdent` + async function foo() { + const fs = await import("fs/promises"); + } + `, + outdent` + async function foo() { + const fs = await import(/* escaped */"\\u{66}s/promises"); + } + `, + 'import "buffer";', + 'import "child_process";', + 'import "timers/promises";' + ] +}); + +test.babel({ + valid: [ + 'export fs from "node:fs";' + ], + invalid: [ + { + code: 'export fs from "fs";', + output: 'export fs from "node:fs";', + errors: 1 + }, + { + code: 'await import(\'assert/strict\')', + output: 'await import(\'node:assert/strict\')', + errors: 1 + } + ] +}); diff --git a/test/snapshots/prefer-node-protocol.mjs.md b/test/snapshots/prefer-node-protocol.mjs.md new file mode 100644 index 0000000000..e18a9db264 --- /dev/null +++ b/test/snapshots/prefer-node-protocol.mjs.md @@ -0,0 +1,253 @@ +# Snapshot report for `test/prefer-node-protocol.mjs` + +The actual snapshot is saved in `prefer-node-protocol.mjs.snap`. + +Generated by [AVA](https://avajs.dev). + +## Invalid #1 + 1 | import fs from "fs"; + +> Output + + `␊ + 1 | import fs from "node:fs";␊ + ` + +> Error 1/1 + + `␊ + > 1 | import fs from "fs";␊ + | ^^^^ Prefer \`node:fs\` over \`fs\`.␊ + ` + +## Invalid #2 + 1 | export {promises} from "fs"; + +> Output + + `␊ + 1 | export {promises} from "node:fs";␊ + ` + +> Error 1/1 + + `␊ + > 1 | export {promises} from "fs";␊ + | ^^^^ Prefer \`node:fs\` over \`fs\`.␊ + ` + +## Invalid #3 + 1 | async function foo() { + 2 | const fs = await import('fs'); + 3 | } + +> Output + + `␊ + 1 | async function foo() {␊ + 2 | const fs = await import('node:fs');␊ + 3 | }␊ + ` + +> Error 1/1 + + `␊ + 1 | async function foo() {␊ + > 2 | const fs = await import('fs');␊ + | ^^^^ Prefer \`node:fs\` over \`fs\`.␊ + 3 | }␊ + ` + +## Invalid #4 + 1 | import fs from "fs/promises"; + +> Output + + `␊ + 1 | import fs from "node:fs/promises";␊ + ` + +> Error 1/1 + + `␊ + > 1 | import fs from "fs/promises";␊ + | ^^^^^^^^^^^^^ Prefer \`node:fs/promises\` over \`fs/promises\`.␊ + ` + +## Invalid #5 + 1 | export {default} from "fs/promises"; + +> Output + + `␊ + 1 | export {default} from "node:fs/promises";␊ + ` + +> Error 1/1 + + `␊ + > 1 | export {default} from "fs/promises";␊ + | ^^^^^^^^^^^^^ Prefer \`node:fs/promises\` over \`fs/promises\`.␊ + ` + +## Invalid #6 + 1 | async function foo() { + 2 | const fs = await import('fs/promises'); + 3 | } + +> Output + + `␊ + 1 | async function foo() {␊ + 2 | const fs = await import('node:fs/promises');␊ + 3 | }␊ + ` + +> Error 1/1 + + `␊ + 1 | async function foo() {␊ + > 2 | const fs = await import('fs/promises');␊ + | ^^^^^^^^^^^^^ Prefer \`node:fs/promises\` over \`fs/promises\`.␊ + 3 | }␊ + ` + +## Invalid #7 + 1 | import {promises} from "fs"; + +> Output + + `␊ + 1 | import {promises} from "node:fs";␊ + ` + +> Error 1/1 + + `␊ + > 1 | import {promises} from "fs";␊ + | ^^^^ Prefer \`node:fs\` over \`fs\`.␊ + ` + +## Invalid #8 + 1 | export {default as promises} from "fs"; + +> Output + + `␊ + 1 | export {default as promises} from "node:fs";␊ + ` + +> Error 1/1 + + `␊ + > 1 | export {default as promises} from "fs";␊ + | ^^^^ Prefer \`node:fs\` over \`fs\`.␊ + ` + +## Invalid #9 + 1 | import {promises} from 'fs'; + +> Output + + `␊ + 1 | import {promises} from 'node:fs';␊ + ` + +> Error 1/1 + + `␊ + > 1 | import {promises} from 'fs';␊ + | ^^^^ Prefer \`node:fs\` over \`fs\`.␊ + ` + +## Invalid #10 + 1 | async function foo() { + 2 | const fs = await import("fs/promises"); + 3 | } + +> Output + + `␊ + 1 | async function foo() {␊ + 2 | const fs = await import("node:fs/promises");␊ + 3 | }␊ + ` + +> Error 1/1 + + `␊ + 1 | async function foo() {␊ + > 2 | const fs = await import("fs/promises");␊ + | ^^^^^^^^^^^^^ Prefer \`node:fs/promises\` over \`fs/promises\`.␊ + 3 | }␊ + ` + +## Invalid #11 + 1 | async function foo() { + 2 | const fs = await import(/* escaped */"\u{66}s/promises"); + 3 | } + +> Output + + `␊ + 1 | async function foo() {␊ + 2 | const fs = await import(/* escaped */"node:\\u{66}s/promises");␊ + 3 | }␊ + ` + +> Error 1/1 + + `␊ + 1 | async function foo() {␊ + > 2 | const fs = await import(/* escaped */"\\u{66}s/promises");␊ + | ^^^^^^^^^^^^^^^^^^ Prefer \`node:fs/promises\` over \`fs/promises\`.␊ + 3 | }␊ + ` + +## Invalid #12 + 1 | import "buffer"; + +> Output + + `␊ + 1 | import "node:buffer";␊ + ` + +> Error 1/1 + + `␊ + > 1 | import "buffer";␊ + | ^^^^^^^^ Prefer \`node:buffer\` over \`buffer\`.␊ + ` + +## Invalid #13 + 1 | import "child_process"; + +> Output + + `␊ + 1 | import "node:child_process";␊ + ` + +> Error 1/1 + + `␊ + > 1 | import "child_process";␊ + | ^^^^^^^^^^^^^^^ Prefer \`node:child_process\` over \`child_process\`.␊ + ` + +## Invalid #14 + 1 | import "timers/promises"; + +> Output + + `␊ + 1 | import "node:timers/promises";␊ + ` + +> Error 1/1 + + `␊ + > 1 | import "timers/promises";␊ + | ^^^^^^^^^^^^^^^^^ Prefer \`node:timers/promises\` over \`timers/promises\`.␊ + ` diff --git a/test/snapshots/prefer-node-protocol.mjs.snap b/test/snapshots/prefer-node-protocol.mjs.snap new file mode 100644 index 0000000000..1766a3f305 Binary files /dev/null and b/test/snapshots/prefer-node-protocol.mjs.snap differ diff --git a/test/unit/get-documentation-url.mjs b/test/unit/get-documentation-url.mjs index 02bdb653d6..681bb7efe4 100644 --- a/test/unit/get-documentation-url.mjs +++ b/test/unit/get-documentation-url.mjs @@ -1,6 +1,6 @@ -import {createRequire} from 'module'; +import {createRequire} from 'node:module'; +import url from 'node:url'; import test from 'ava'; -import url from 'url'; import getDocumentationUrl from '../../rules/utils/get-documentation-url.js'; const require = createRequire(import.meta.url); diff --git a/test/utils/test.mjs b/test/utils/test.mjs index db84db8b04..33901d3b83 100644 --- a/test/utils/test.mjs +++ b/test/utils/test.mjs @@ -1,7 +1,7 @@ -import path from 'path'; -import url from 'url'; +import path from 'node:path'; +import url from 'node:url'; +import {createRequire} from 'node:module'; import test from 'ava'; -import {createRequire} from 'module'; import avaRuleTester from 'eslint-ava-rule-tester'; import snapshotRuleTester from './snapshot-rule-tester.mjs'; import defaultParserOptions from './default-parser-options.mjs';