Skip to content

Commit ad0d6f5

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

File tree

6 files changed

+595
-19
lines changed

6 files changed

+595
-19
lines changed

CHANGELOG.md

Lines changed: 11 additions & 3 deletions
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

Lines changed: 30 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

@@ -495,6 +496,19 @@ 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+
498512
## SublimeLinter-eslint
499513

500514
SublimeLinter-eslint introduced a change to support `.eslintignore` files
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
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+
15+
- the code is more explicitly and clearly referencing a Node.js built-in module
16+
17+
Reasons to prefer omitting the protocol include:
18+
19+
- some tools don't support the `node:` protocol
20+
- the code is more portable, because import maps and automatic polyfilling can be used
21+
22+
## Options
23+
24+
The rule requires a single string option which may be one of:
25+
26+
- `'always'` - enforces that builtins node imports are using the `node:` protocol.
27+
- `'never'` - enforces that builtins node imports are not using the `node:` protocol.
28+
29+
## Examples
30+
31+
### `'always'`
32+
33+
❌ Invalid
34+
35+
```js
36+
import fs from 'fs';
37+
export { promises } from 'fs';
38+
// require
39+
const fs = require('fs/promises');
40+
```
41+
42+
✅ Valid
43+
44+
```js
45+
import fs from 'node:fs';
46+
export { promises } from 'node:fs';
47+
import * as test from 'node:test';
48+
// require
49+
const fs = require('node:fs/promises');
50+
```
51+
52+
### `'never'`
53+
54+
❌ Invalid
55+
56+
```js
57+
import fs from 'node:fs';
58+
export { promises } from 'node:fs';
59+
// require
60+
const fs = require('node:fs/promises');
61+
```
62+
63+
✅ Valid
64+
65+
```js
66+
import fs from 'fs';
67+
export { promises } from 'fs';
68+
import * as test from 'node:test';
69+
// require
70+
const fs = require('fs/promises');
71+
```
72+
73+
## When Not To Use It
74+
75+
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: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
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+
const { value: moduleName } = src;
47+
const { settings } = context;
48+
const nodeVersion = settings && settings['node-version'];
49+
if (
50+
typeof nodeVersion !== 'undefined'
51+
&& (
52+
typeof nodeVersion !== 'string'
53+
|| !(/^[0-9]+\.[0-9]+\.[0-9]+$/).test(nodeVersion)
54+
)
55+
) {
56+
throw new TypeError('`import/node-version` setting must be a string in the format "10.23.45" (a semver version, with no leading zero)');
57+
}
58+
59+
if (context.options[0] === 'never') {
60+
if (!moduleName.startsWith('node:')) { return; }
61+
62+
const actualModuleName = moduleName.slice(5);
63+
if (!isCoreModule(actualModuleName, nodeVersion || undefined)) { return; }
64+
65+
context.report({
66+
node: src,
67+
message: messages[NEVER_PREFER_MESSAGE_ID],
68+
data: { moduleName: actualModuleName },
69+
/** @param {import('eslint').Rule.RuleFixer} fixer */
70+
fix(fixer) {
71+
return replaceStringLiteral(fixer, src, '', 0, 5);
72+
},
73+
});
74+
} else if (context.options[0] === 'always') {
75+
if (
76+
moduleName.startsWith('node:')
77+
|| !isCoreModule(moduleName, nodeVersion || undefined)
78+
|| !isCoreModule(`node:${moduleName}`, nodeVersion || undefined)
79+
) {
80+
return;
81+
}
82+
83+
context.report({
84+
node: src,
85+
message: messages[DO_PREFER_MESSAGE_ID],
86+
data: { moduleName },
87+
/** @param {import('eslint').Rule.RuleFixer} fixer */
88+
fix(fixer) {
89+
return replaceStringLiteral(fixer, src, 'node:', 0, 0);
90+
},
91+
});
92+
} else if (typeof context.options[0] === 'undefined') {
93+
throw new Error('Missing option');
94+
} else {
95+
throw new Error(`Unexpected option: ${context.options[0]}`);
96+
}
97+
}
98+
99+
/** @type {import('eslint').Rule.RuleModule} */
100+
module.exports = {
101+
meta: {
102+
type: 'suggestion',
103+
docs: {
104+
description: 'Enforce either using, or omitting, the `node:` protocol when importing Node.js builtin modules.',
105+
recommended: true,
106+
category: 'Static analysis',
107+
url: docsUrl('enforce-node-protocol-usage'),
108+
},
109+
fixable: 'code',
110+
schema: {
111+
type: 'array',
112+
minItems: 1,
113+
maxItems: 1,
114+
items: [
115+
{
116+
enum: ['always', 'never'],
117+
},
118+
],
119+
},
120+
messages,
121+
},
122+
create(context) {
123+
return {
124+
CallExpression(node) {
125+
if (!isStaticRequireWith1Param(node)) { return; }
126+
127+
const arg = node.arguments[0];
128+
129+
return checkAndReport(arg, context);
130+
},
131+
ExportNamedDeclaration(node) {
132+
return checkAndReport(node.source, context);
133+
},
134+
ImportDeclaration(node) {
135+
return checkAndReport(node.source, context);
136+
},
137+
ImportExpression(node) {
138+
return checkAndReport(node.source, context);
139+
},
140+
};
141+
},
142+
};

0 commit comments

Comments
 (0)