Skip to content

Commit 8c8058c

Browse files
MikhailPertsev1sevenc-nanashi
authored andcommitted
[New] add enforce-node-protocol-usage rule and import/node-version setting
Co-authored-by: Mikhail Pertsev <[email protected]> Co-authored-by: sevenc-nanashi <[email protected]>
1 parent d5f2950 commit 8c8058c

7 files changed

+617
-19
lines changed

.markdownlint.json

+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
22
"line-length": false,
3+
"ignore_case": true,
34
"no-duplicate-heading": {
45
"siblings_only": true
56
},

CHANGELOG.md

+11-3
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,11 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange
66

77
## [Unreleased]
88

9+
### Added
10+
- add [`enforce-node-protocol-usage`] rule and `import/node-version` setting ([#3024], thanks [@GoldStrikeArch] and [@sevenc-nanashi])
11+
912
### Changed
10-
- [Docs] `extensions`, `order`: improve documentation ([#3106], thanks [@Xunnamius])
13+
- [Docs] [`extensions`], [`order`]: improve documentation ([#3106], thanks [@Xunnamius])
1114

1215
## [2.31.0] - 2024-10-03
1316

@@ -1106,10 +1109,12 @@ for info on changes for earlier releases.
11061109
[`import/core-modules` setting]: ./README.md#importcore-modules
11071110
[`import/external-module-folders` setting]: ./README.md#importexternal-module-folders
11081111
[`internal-regex` setting]: ./README.md#importinternal-regex
1112+
[`import/node-version` setting]: ./README.md#importnode-version
11091113

11101114
[`consistent-type-specifier-style`]: ./docs/rules/consistent-type-specifier-style.md
11111115
[`default`]: ./docs/rules/default.md
11121116
[`dynamic-import-chunkname`]: ./docs/rules/dynamic-import-chunkname.md
1117+
[`enforce-node-protocol-usage`]: ./docs/rules/enforce-node-protocol-usage.md
11131118
[`export`]: ./docs/rules/export.md
11141119
[`exports-last`]: ./docs/rules/exports-last.md
11151120
[`extensions`]: ./docs/rules/extensions.md
@@ -1169,6 +1174,7 @@ for info on changes for earlier releases.
11691174
[#3036]: https://github.com/import-js/eslint-plugin-import/pull/3036
11701175
[#3033]: https://github.com/import-js/eslint-plugin-import/pull/3033
11711176
[#3032]: https://github.com/import-js/eslint-plugin-import/pull/3032
1177+
[#3024]: https://github.com/import-js/eslint-plugin-import/pull/3024
11721178
[#3018]: https://github.com/import-js/eslint-plugin-import/pull/3018
11731179
[#3012]: https://github.com/import-js/eslint-plugin-import/pull/3012
11741180
[#3011]: https://github.com/import-js/eslint-plugin-import/pull/3011
@@ -1788,7 +1794,6 @@ for info on changes for earlier releases.
17881794
[@bicstone]: https://github.com/bicstone
17891795
[@Blasz]: https://github.com/Blasz
17901796
[@bmish]: https://github.com/bmish
1791-
[@developer-bandi]: https://github.com/developer-bandi
17921797
[@borisyankov]: https://github.com/borisyankov
17931798
[@bradennapier]: https://github.com/bradennapier
17941799
[@bradzacher]: https://github.com/bradzacher
@@ -1808,6 +1813,7 @@ for info on changes for earlier releases.
18081813
[@darkartur]: https://github.com/darkartur
18091814
[@davidbonnet]: https://github.com/davidbonnet
18101815
[@dbrewer5]: https://github.com/dbrewer5
1816+
[@developer-bandi]: https://github.com/developer-bandi
18111817
[@devinrhode2]: https://github.com/devinrhode2
18121818
[@devongovett]: https://github.com/devongovett
18131819
[@dmnd]: https://github.com/dmnd
@@ -1842,6 +1848,7 @@ for info on changes for earlier releases.
18421848
[@georeith]: https://github.com/georeith
18431849
[@giodamelio]: https://github.com/giodamelio
18441850
[@gnprice]: https://github.com/gnprice
1851+
[@GoldStrikeArch]: https://github.com/GoldStrikeArch
18451852
[@golergka]: https://github.com/golergka
18461853
[@golopot]: https://github.com/golopot
18471854
[@GoodForOneFare]: https://github.com/GoodForOneFare
@@ -1901,9 +1908,9 @@ for info on changes for earlier releases.
19011908
[@Librazy]: https://github.com/Librazy
19021909
[@liby]: https://github.com/liby
19031910
[@lilling]: https://github.com/lilling
1911+
[@liuxingbaoyu]: https://github.com/liuxingbaoyu
19041912
[@ljharb]: https://github.com/ljharb
19051913
[@ljqx]: https://github.com/ljqx
1906-
[@liuxingbaoyu]: https://github.com/liuxingbaoyu
19071914
[@lo1tuma]: https://github.com/lo1tuma
19081915
[@loganfsmyth]: https://github.com/loganfsmyth
19091916
[@luczsoma]: https://github.com/luczsoma
@@ -1977,6 +1984,7 @@ for info on changes for earlier releases.
19771984
[@Schweinepriester]: https://github.com/Schweinepriester
19781985
[@scottnonnenberg]: https://github.com/scottnonnenberg
19791986
[@sergei-startsev]: https://github.com/sergei-startsev
1987+
[@sevenc-nanashi]: https://github.com/sevenc-nanashi
19801988
[@sharmilajesupaul]: https://github.com/sharmilajesupaul
19811989
[@sheepsteak]: https://github.com/sheepsteak
19821990
[@silverwind]: https://github.com/silverwind

README.md

+31-16
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

@@ -495,6 +496,20 @@ For example, if your packages in a monorepo are all in `@scope`, you can configu
495496
}
496497
```
497498

499+
### `import/node-version`
500+
501+
A string that represents the version of Node.js that you are using.
502+
A falsy value will imply the version of Node.js that you are running ESLint with.
503+
504+
```jsonc
505+
// .eslintrc
506+
{
507+
"settings": {
508+
"import/node-version": "22.3.4",
509+
},
510+
}
511+
```
512+
498513
## SublimeLinter-eslint
499514

500515
SublimeLinter-eslint introduced a change to support `.eslintignore` files
+81
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
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+
Determining whether a specifier is a core module depends on the node version being used to run `eslint`.
14+
This version can be specified in the configuration with the [`import/node-version` setting](../../README.md#importnode-version).
15+
16+
Reasons to prefer using the protocol include:
17+
18+
- the code is more explicitly and clearly referencing a Node.js built-in module
19+
20+
Reasons to prefer omitting the protocol include:
21+
22+
- some tools don't support the `node:` protocol
23+
- the code is more portable, because import maps and automatic polyfilling can be used
24+
25+
## Options
26+
27+
The rule requires a single string option which may be one of:
28+
29+
- `'always'` - enforces that builtins node imports are using the `node:` protocol.
30+
- `'never'` - enforces that builtins node imports are not using the `node:` protocol.
31+
32+
## Examples
33+
34+
### `'always'`
35+
36+
❌ Invalid
37+
38+
```js
39+
import fs from 'fs';
40+
export { promises } from 'fs';
41+
// require
42+
const fs = require('fs/promises');
43+
```
44+
45+
✅ Valid
46+
47+
```js
48+
import fs from 'node:fs';
49+
export { promises } from 'node:fs';
50+
import * as test from 'node:test';
51+
// require
52+
const fs = require('node:fs/promises');
53+
```
54+
55+
### `'never'`
56+
57+
❌ Invalid
58+
59+
```js
60+
import fs from 'node:fs';
61+
export { promises } from 'node:fs';
62+
// require
63+
const fs = require('node:fs/promises');
64+
```
65+
66+
✅ Valid
67+
68+
```js
69+
import fs from 'fs';
70+
export { promises } from 'fs';
71+
72+
// require
73+
const fs = require('fs/promises');
74+
75+
// This rule will not enforce not using `node:` protocol when the module is only available under the `node:` protocol.
76+
import * as test from 'node:test';
77+
```
78+
79+
## When Not To Use It
80+
81+
If you don't want to consistently enforce using, or omitting, the `node:` protocol when importing Node.js builtin modules.

src/index.js

+1
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'),
+147
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
'use strict';
2+
3+
const isCoreModule = require('is-core-module');
4+
const { default: docsUrl } = require('../docsUrl');
5+
6+
const DO_PREFER_MESSAGE_ID = 'requireNodeProtocol';
7+
const NEVER_PREFER_MESSAGE_ID = 'forbidNodeProtocol';
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+
// check for only 1 argument
40+
&& node.arguments.length === 1
41+
&& node.arguments[0]
42+
&& isStringLiteral(node.arguments[0]);
43+
}
44+
45+
function checkAndReport(src, context) {
46+
// TODO use src.quasis[0].value.raw
47+
if (src.type === 'TemplateLiteral') { return; }
48+
const moduleName = 'value' in src ? src.value : src.name;
49+
if (typeof moduleName !== 'string') { console.log(src, moduleName); }
50+
const { settings } = context;
51+
const nodeVersion = settings && settings['import/node-version'];
52+
if (
53+
typeof nodeVersion !== 'undefined'
54+
&& (
55+
typeof nodeVersion !== 'string'
56+
|| !(/^[0-9]+\.[0-9]+\.[0-9]+$/).test(nodeVersion)
57+
)
58+
) {
59+
throw new TypeError('`import/node-version` setting must be a string in the format "10.23.45" (a semver version, with no leading zero)');
60+
}
61+
62+
if (context.options[0] === 'never') {
63+
if (!moduleName.startsWith('node:')) { return; }
64+
65+
const actualModuleName = moduleName.slice(5);
66+
if (!isCoreModule(actualModuleName, nodeVersion || undefined)) { return; }
67+
68+
context.report({
69+
node: src,
70+
message: messages[NEVER_PREFER_MESSAGE_ID],
71+
data: { moduleName: actualModuleName },
72+
/** @param {import('eslint').Rule.RuleFixer} fixer */
73+
fix(fixer) {
74+
return replaceStringLiteral(fixer, src, '', 0, 5);
75+
},
76+
});
77+
} else if (context.options[0] === 'always') {
78+
if (
79+
moduleName.startsWith('node:')
80+
|| !isCoreModule(moduleName, nodeVersion || undefined)
81+
|| !isCoreModule(`node:${moduleName}`, nodeVersion || undefined)
82+
) {
83+
return;
84+
}
85+
86+
context.report({
87+
node: src,
88+
message: messages[DO_PREFER_MESSAGE_ID],
89+
data: { moduleName },
90+
/** @param {import('eslint').Rule.RuleFixer} fixer */
91+
fix(fixer) {
92+
return replaceStringLiteral(fixer, src, 'node:', 0, 0);
93+
},
94+
});
95+
} else if (typeof context.options[0] === 'undefined') {
96+
throw new Error('Missing option');
97+
} else {
98+
throw new Error(`Unexpected option: ${context.options[0]}`);
99+
}
100+
}
101+
102+
/** @type {import('eslint').Rule.RuleModule} */
103+
module.exports = {
104+
meta: {
105+
type: 'suggestion',
106+
docs: {
107+
description: 'Enforce either using, or omitting, the `node:` protocol when importing Node.js builtin modules.',
108+
recommended: true,
109+
category: 'Static analysis',
110+
url: docsUrl('enforce-node-protocol-usage'),
111+
},
112+
fixable: 'code',
113+
schema: {
114+
type: 'array',
115+
minItems: 1,
116+
maxItems: 1,
117+
items: [
118+
{
119+
enum: ['always', 'never'],
120+
},
121+
],
122+
},
123+
messages,
124+
},
125+
create(context) {
126+
return {
127+
CallExpression(node) {
128+
if (!isStaticRequireWith1Param(node)) { return; }
129+
130+
const arg = node.arguments[0];
131+
132+
return checkAndReport(arg, context);
133+
},
134+
ExportNamedDeclaration(node) {
135+
return checkAndReport(node.source, context);
136+
},
137+
ImportDeclaration(node) {
138+
return checkAndReport(node.source, context);
139+
},
140+
ImportExpression(node) {
141+
if (!isStringLiteral(node.source)) { return; }
142+
143+
return checkAndReport(node.source, context);
144+
},
145+
};
146+
},
147+
};

0 commit comments

Comments
 (0)