Skip to content

Commit

Permalink
Add prefer-node-protocol rule (#1203)
Browse files Browse the repository at this point in the history
Co-authored-by: Sindre Sorhus <[email protected]>
  • Loading branch information
fisker and sindresorhus authored Apr 22, 2021
1 parent a7e393c commit b1a5f53
Show file tree
Hide file tree
Showing 16 changed files with 451 additions and 15 deletions.
47 changes: 47 additions & 0 deletions docs/rules/prefer-node-protocol.md
Original file line number Diff line number Diff line change
@@ -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';
```
1 change: 1 addition & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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()`. || 🔧 |
Expand Down
53 changes: 53 additions & 0 deletions rules/prefer-node-protocol.js
Original file line number Diff line number Diff line change
@@ -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
}
};
8 changes: 4 additions & 4 deletions scripts/generate-rules-table.mjs
Original file line number Diff line number Diff line change
@@ -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);
Expand Down
2 changes: 1 addition & 1 deletion test/better-regex.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {createRequire} from 'module';
import {createRequire} from 'node:module';
import {getTester} from './utils/test.mjs';

const {test} = getTester(import.meta);
Expand Down
2 changes: 1 addition & 1 deletion test/custom-error-definition.mjs
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
2 changes: 1 addition & 1 deletion test/no-hex-escape.mjs
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
2 changes: 1 addition & 1 deletion test/number-literal-case.mjs
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
4 changes: 2 additions & 2 deletions test/package.mjs
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
79 changes: 79 additions & 0 deletions test/prefer-node-protocol.mjs
Original file line number Diff line number Diff line change
@@ -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
}
]
});
Loading

0 comments on commit b1a5f53

Please sign in to comment.