Skip to content

Commit e897498

Browse files
MikhailPertsev1sevenc-nanashi
authored andcommitted
[New] add enforce-node-protocol-usage rule
Co-authored-by: Mikhail Pertsev <mikhail.pertsev@brightpattern.com> Co-authored-by: sevenc-nanashi <sevenc7c@sevenc7c.com>
1 parent d5f2950 commit e897498

File tree

5 files changed

+509
-16
lines changed

5 files changed

+509
-16
lines changed

README.md

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -51,22 +51,23 @@ This plugin intends to support linting of ES2015+ (ES6+) import/export syntax, a
5151

5252
### Static analysis
5353

54-
| Name                       | Description | 💼 | ⚠️ | 🚫 | 🔧 | 💡 ||
55-
| :--------------------------------------------------------------------- | :----------------------------------------------------------------------------------- | :--- | :- | :- | :- | :- | :- |
56-
| [default](docs/rules/default.md) | Ensure a default export is present, given a default import. | ❗ ☑️ | | | | | |
57-
| [named](docs/rules/named.md) | Ensure named imports correspond to a named export in the remote file. | ❗ ☑️ | | ⌨️ | | | |
58-
| [namespace](docs/rules/namespace.md) | Ensure imported namespaces contain dereferenced properties as they are dereferenced. | ❗ ☑️ | | | | | |
59-
| [no-absolute-path](docs/rules/no-absolute-path.md) | Forbid import of modules using absolute paths. | | | | 🔧 | | |
60-
| [no-cycle](docs/rules/no-cycle.md) | Forbid a module from importing a module with a dependency path back to itself. | | | | | | |
61-
| [no-dynamic-require](docs/rules/no-dynamic-require.md) | Forbid `require()` calls with expressions. | | | | | | |
62-
| [no-internal-modules](docs/rules/no-internal-modules.md) | Forbid importing the submodules of other modules. | | | | | | |
63-
| [no-relative-packages](docs/rules/no-relative-packages.md) | Forbid importing packages through relative paths. | | | | 🔧 | | |
64-
| [no-relative-parent-imports](docs/rules/no-relative-parent-imports.md) | Forbid importing modules from parent directories. | | | | | | |
65-
| [no-restricted-paths](docs/rules/no-restricted-paths.md) | Enforce which files can be imported in a given folder. | | | | | | |
66-
| [no-self-import](docs/rules/no-self-import.md) | Forbid a module from importing itself. | | | | | | |
67-
| [no-unresolved](docs/rules/no-unresolved.md) | Ensure imports point to a file/module that can be resolved. | ❗ ☑️ | | | | | |
68-
| [no-useless-path-segments](docs/rules/no-useless-path-segments.md) | Forbid unnecessary path segments in import and require statements. | | | | 🔧 | | |
69-
| [no-webpack-loader-syntax](docs/rules/no-webpack-loader-syntax.md) | Forbid webpack loader syntax in imports. | | | | | | |
54+
| Name                        | Description | 💼 | ⚠️ | 🚫 | 🔧 | 💡 ||
55+
| :----------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------- | :--- | :- | :- | :- | :- | :- |
56+
| [default](docs/rules/default.md) | Ensure a default export is present, given a default import. | ❗ ☑️ | | | | | |
57+
| [enforce-node-protocol-usage](docs/rules/enforce-node-protocol-usage.md) | Enforce either using, or omitting, the `node:` protocol when importing Node.js builtin modules. | | | | 🔧 | | |
58+
| [named](docs/rules/named.md) | Ensure named imports correspond to a named export in the remote file. | ❗ ☑️ | | ⌨️ | | | |
59+
| [namespace](docs/rules/namespace.md) | Ensure imported namespaces contain dereferenced properties as they are dereferenced. | ❗ ☑️ | | | | | |
60+
| [no-absolute-path](docs/rules/no-absolute-path.md) | Forbid import of modules using absolute paths. | | | | 🔧 | | |
61+
| [no-cycle](docs/rules/no-cycle.md) | Forbid a module from importing a module with a dependency path back to itself. | | | | | | |
62+
| [no-dynamic-require](docs/rules/no-dynamic-require.md) | Forbid `require()` calls with expressions. | | | | | | |
63+
| [no-internal-modules](docs/rules/no-internal-modules.md) | Forbid importing the submodules of other modules. | | | | | | |
64+
| [no-relative-packages](docs/rules/no-relative-packages.md) | Forbid importing packages through relative paths. | | | | 🔧 | | |
65+
| [no-relative-parent-imports](docs/rules/no-relative-parent-imports.md) | Forbid importing modules from parent directories. | | | | | | |
66+
| [no-restricted-paths](docs/rules/no-restricted-paths.md) | Enforce which files can be imported in a given folder. | | | | | | |
67+
| [no-self-import](docs/rules/no-self-import.md) | Forbid a module from importing itself. | | | | | | |
68+
| [no-unresolved](docs/rules/no-unresolved.md) | Ensure imports point to a file/module that can be resolved. | ❗ ☑️ | | | | | |
69+
| [no-useless-path-segments](docs/rules/no-useless-path-segments.md) | Forbid unnecessary path segments in import and require statements. | | | | 🔧 | | |
70+
| [no-webpack-loader-syntax](docs/rules/no-webpack-loader-syntax.md) | Forbid webpack loader syntax in imports. | | | | | | |
7071

7172
### Style guide
7273

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# import/enforce-node-protocol-usage
2+
3+
🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).
4+
5+
<!-- end auto-generated rule header -->
6+
7+
Enforce either using, or omitting, the `node:` protocol when importing Node.js builtin modules.
8+
9+
## Rule Details
10+
11+
This rule enforces that builtins node imports are using, or omitting, the `node:` protocol.
12+
13+
Reasons to prefer using the protocol include:
14+
- the code is more explicitly and clearly referencing a Node.js built-in module
15+
16+
Reasons to prefer omitting the protocol include:
17+
- some tools don't support the `node:` protocol
18+
- the code is more portable, because import maps and automatic polyfilling can be used
19+
20+
## Options
21+
22+
The rule requires a single string option which may be one of:
23+
24+
- `'always'` - enforces that builtins node imports are using the `node:` protocol.
25+
- `'never'` - enforces that builtins node imports are not using the `node:` protocol.
26+
27+
## Examples
28+
29+
### `'always'`
30+
31+
❌ Invalid
32+
33+
```js
34+
import fs from 'fs';
35+
export { promises } from 'fs';
36+
// require
37+
const fs = require('fs/promises');
38+
```
39+
40+
✅ Valid
41+
42+
```js
43+
import fs from 'node:fs';
44+
export { promises } from 'node:fs';
45+
import * as test from 'node:test';
46+
// require
47+
const fs = require('node:fs/promises');
48+
```
49+
50+
### `'never'`
51+
52+
❌ Invalid
53+
54+
```js
55+
import fs from 'node:fs';
56+
export { promises } from 'node:fs';
57+
// require
58+
const fs = require('node:fs/promises');
59+
```
60+
61+
✅ Valid
62+
63+
```js
64+
import fs from 'fs';
65+
export { promises } from 'fs';
66+
import * as test from 'node:test';
67+
// require
68+
const fs = require('fs/promises');
69+
```
70+
71+
## When Not To Use It
72+
73+
If you don't want to consistently enforce using, or omitting, the `node:` protocol when importing Node.js builtin modules.

src/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ export const rules = {
4545
'dynamic-import-chunkname': require('./rules/dynamic-import-chunkname'),
4646
'no-import-module-exports': require('./rules/no-import-module-exports'),
4747
'no-empty-named-blocks': require('./rules/no-empty-named-blocks'),
48+
'enforce-node-protocol-usage': require('./rules/enforce-node-protocol-usage'),
4849

4950
// export
5051
'exports-last': require('./rules/exports-last'),
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
'use strict';
2+
3+
const isCoreModule = require('is-core-module');
4+
const { default: docsUrl } = require('../docsUrl');
5+
6+
const DO_PREFER_MESSAGE_ID = 'preferNodeBuiltinImports';
7+
const NEVER_PREFER_MESSAGE_ID = 'neverPreferNodeBuiltinImports';
8+
const messages = {
9+
[DO_PREFER_MESSAGE_ID]: 'Prefer `node:{{moduleName}}` over `{{moduleName}}`.',
10+
[NEVER_PREFER_MESSAGE_ID]: 'Prefer `{{moduleName}}` over `node:{{moduleName}}`.',
11+
};
12+
13+
function replaceStringLiteral(
14+
fixer,
15+
node,
16+
text,
17+
relativeRangeStart,
18+
relativeRangeEnd,
19+
) {
20+
const firstCharacterIndex = node.range[0] + 1;
21+
const start = Number.isInteger(relativeRangeEnd)
22+
? relativeRangeStart + firstCharacterIndex
23+
: firstCharacterIndex;
24+
const end = Number.isInteger(relativeRangeEnd)
25+
? relativeRangeEnd + firstCharacterIndex
26+
: node.range[1] - 1;
27+
28+
return fixer.replaceTextRange([start, end], text);
29+
}
30+
31+
function isStringLiteral(node) {
32+
return node.type === 'Literal' && typeof node.value === 'string';
33+
}
34+
35+
function isStaticRequireWith1Param(node) {
36+
return !node.optional
37+
&& node.callee.type === 'Identifier'
38+
&& node.callee.name === 'require'
39+
&& node.arguments[0]
40+
&& isStringLiteral(node.arguments[0])
41+
// check for only 1 argument
42+
&& !node.arguments[1];
43+
}
44+
45+
function checkAndReport(src, context) {
46+
const { value: moduleName } = src;
47+
if (!isCoreModule(moduleName)) { return; }
48+
49+
if (context.options[0] === 'never') {
50+
if (!moduleName.startsWith('node:')) { return; }
51+
52+
const actualModuleName = moduleName.slice(5);
53+
if (!isCoreModule(actualModuleName)) { return; }
54+
55+
context.report({
56+
node: src,
57+
messageId: NEVER_PREFER_MESSAGE_ID,
58+
data: { moduleName: actualModuleName },
59+
/** @param {import('eslint').Rule.RuleFixer} fixer */
60+
fix(fixer) {
61+
return replaceStringLiteral(fixer, src, '', 0, 5);
62+
},
63+
});
64+
} else if (context.options[0] === 'always') {
65+
if (moduleName.startsWith('node:')) { return; }
66+
67+
context.report({
68+
node: src,
69+
messageId: DO_PREFER_MESSAGE_ID,
70+
data: { moduleName },
71+
/** @param {import('eslint').Rule.RuleFixer} fixer */
72+
fix(fixer) {
73+
return replaceStringLiteral(fixer, src, 'node:', 0, 0);
74+
},
75+
});
76+
} else if (typeof context.options[0] === 'undefined') {
77+
throw new Error('Missing option');
78+
} else {
79+
throw new Error(`Unexpected option: ${context.options[0]}`);
80+
}
81+
}
82+
83+
/** @type {import('eslint').Rule.RuleModule} */
84+
module.exports = {
85+
meta: {
86+
type: 'suggestion',
87+
docs: {
88+
description: 'Enforce either using, or omitting, the `node:` protocol when importing Node.js builtin modules.',
89+
recommended: true,
90+
category: 'Static analysis',
91+
url: docsUrl('enforce-node-protocol-usage'),
92+
},
93+
fixable: 'code',
94+
schema: [
95+
{
96+
enum: ['always', 'never'],
97+
required: true,
98+
},
99+
],
100+
messages,
101+
},
102+
create(context) {
103+
return {
104+
CallExpression(node) {
105+
if (!isStaticRequireWith1Param(node)) { return; }
106+
107+
const arg = node.arguments[0];
108+
109+
if (!isStringLiteral(arg)) { return; }
110+
111+
return checkAndReport(arg, context);
112+
},
113+
ExportNamedDeclaration(node) {
114+
if (!isStringLiteral(node)) { return; }
115+
116+
return checkAndReport(node.source, context);
117+
},
118+
ImportDeclaration(node) {
119+
if (!isStringLiteral(node)) { return; }
120+
121+
return checkAndReport(node.source, context);
122+
},
123+
ImportExpression(node) {
124+
if (!isStringLiteral(node)) { return; }
125+
126+
return checkAndReport(node.source, context);
127+
},
128+
};
129+
},
130+
};

0 commit comments

Comments
 (0)