Skip to content

Commit d7bafc3

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 d7bafc3

File tree

7 files changed

+602
-19
lines changed

7 files changed

+602
-19
lines changed

.markdownlint.json

Lines changed: 1 addition & 0 deletions
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

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: 31 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,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
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: 147 additions & 0 deletions
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['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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy