Skip to content

Commit 968e562

Browse files
feat: added prefer-node-builtin-imports rule
1 parent fc361a9 commit 968e562

File tree

5 files changed

+225
-0
lines changed

5 files changed

+225
-0
lines changed

config/recommended.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ module.exports = {
1717
'import/no-named-as-default': 'warn',
1818
'import/no-named-as-default-member': 'warn',
1919
'import/no-duplicates': 'warn',
20+
'import/prefer-node-builtin-imports': 'warn',
2021
},
2122

2223
// need all these for parsing dependencies (even if _your_ code doesn't need
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# import/prefer-node-builtin-imports
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+
Reports when there is no `node:` protocol for builtin modules.
8+
9+
```ts
10+
import path from "node:path";
11+
```
12+
13+
## Rule Details
14+
15+
This rule enforces that builtins node imports are using `node:` protocol. It resolved the conflict of a module (npm-installed) in `node_modules` overriding the built-in module. Besides that, it is also clear that a built-in Node.js module is imported.
16+
17+
## Examples
18+
19+
❌ Invalid
20+
21+
```ts
22+
import fs from "fs";
23+
export { promises } from "fs";
24+
// require
25+
const fs = require("fs/promises");
26+
```
27+
28+
✅ Valid
29+
30+
```ts
31+
import fs from "node:fs";
32+
export { promises } from "node:fs";
33+
// require
34+
const fs = require("node:fs/promises");
35+
```
36+
37+
## When Not To Use It
38+
39+
If you are using browser or Bun or Deno since this rule doesn't do anything with them.

src/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ export const rules = {
4343
'dynamic-import-chunkname': require('./rules/dynamic-import-chunkname'),
4444
'no-import-module-exports': require('./rules/no-import-module-exports'),
4545
'no-empty-named-blocks': require('./rules/no-empty-named-blocks'),
46+
'prefer-node-builtin-imports': require('./rules/prefer-node-builtin-imports'),
4647

4748
// export
4849
'exports-last': require('./rules/exports-last'),
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
'use strict';
2+
3+
const { builtinModules } = require('module');
4+
const { default: docsUrl } = require('../docsUrl');
5+
6+
const MESSAGE_ID = 'prefer-node-builtin-imports';
7+
const messages = {
8+
[MESSAGE_ID]: 'Prefer `node:{{moduleName}}` over `{{moduleName}}`.',
9+
};
10+
11+
function replaceStringLiteral(
12+
fixer,
13+
node,
14+
text,
15+
relativeRangeStart,
16+
relativeRangeEnd,
17+
) {
18+
const firstCharacterIndex = node.range[0] + 1;
19+
const start = Number.isInteger(relativeRangeEnd)
20+
? relativeRangeStart + firstCharacterIndex
21+
: firstCharacterIndex;
22+
const end = Number.isInteger(relativeRangeEnd)
23+
? relativeRangeEnd + firstCharacterIndex
24+
: node.range[1] - 1;
25+
26+
return fixer.replaceTextRange([start, end], text);
27+
}
28+
29+
const isStringLiteral = (node) => node.type === 'Literal' && typeof node.value === 'string';
30+
31+
const isStaticRequireWith1Param = (node) => !node.optional
32+
&& node.callee.type === 'Identifier'
33+
&& node.callee.name === 'require'
34+
&& node.arguments[0]
35+
// check for only 1 argument
36+
&& !node.arguments[1];
37+
38+
function checkAndReport(src, ctx) {
39+
const { value } = src;
40+
41+
if (!builtinModules.includes(value)) { return; }
42+
43+
if (value.startsWith('node:')) { return; }
44+
45+
ctx.report({
46+
node: src,
47+
messageId: MESSAGE_ID,
48+
data: { moduleName: value },
49+
/** @param {import('eslint').Rule.RuleFixer} fixer */
50+
fix(fixer) {
51+
return replaceStringLiteral(fixer, src, 'node:', 0, 0);
52+
},
53+
});
54+
}
55+
56+
/** @type {import('eslint').Rule.RuleModule} */
57+
module.exports = {
58+
meta: {
59+
type: 'suggestion',
60+
docs: {
61+
description:
62+
'Prefer using the `node:` protocol when importing Node.js builtin modules.',
63+
recommended: true,
64+
category: 'Best Practices',
65+
url: docsUrl('prefer-node-builin-imports'),
66+
},
67+
fixable: 'code',
68+
schema: [],
69+
messages,
70+
},
71+
create(ctx) {
72+
return {
73+
CallExpression(node) {
74+
if (!isStaticRequireWith1Param(node)) {
75+
return;
76+
}
77+
78+
if (!isStringLiteral(node.arguments[0])) {
79+
return;
80+
}
81+
82+
return checkAndReport(node.arguments[0], ctx);
83+
},
84+
ExportNamedDeclaration(node) {
85+
if (!isStringLiteral) { return; }
86+
87+
return checkAndReport(node.source, ctx);
88+
},
89+
ImportDeclaration(node) {
90+
if (!isStringLiteral) { return; }
91+
92+
return checkAndReport(node.source, ctx);
93+
},
94+
ImportExpression(node) {
95+
if (!isStringLiteral) { return; }
96+
97+
return checkAndReport(node.source, ctx);
98+
},
99+
};
100+
},
101+
};
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { test } from '../utils';
2+
3+
import { RuleTester } from 'eslint';
4+
5+
const ruleTester = new RuleTester();
6+
const rule = require('rules/prefer-node-builtin-imports');
7+
8+
ruleTester.run('prefer-node-builtin-imports', rule, {
9+
valid: [
10+
test({ code: 'import unicorn from "unicorn";' }),
11+
test({ code: 'import fs from "./fs";' }),
12+
test({ code: 'import fs from "unknown-builtin-module";' }),
13+
test({ code: 'import fs from "node:fs";' }),
14+
test({
15+
code: `
16+
async function foo() {
17+
const fs = await import(fs);
18+
}`,
19+
}),
20+
test({
21+
code: `
22+
async function foo() {
23+
const fs = await import(0);
24+
}`,
25+
}),
26+
test({
27+
code: `
28+
async function foo() {
29+
const fs = await import(\`fs\`);
30+
}`,
31+
}),
32+
test({ code: 'import "punycode/";' }),
33+
test({ code: 'const fs = require("node:fs");' }),
34+
test({ code: 'const fs = require("node:fs/promises");' }),
35+
test({ code: 'const fs = require(fs);' }),
36+
test({ code: 'const fs = notRequire("fs");' }),
37+
test({ code: 'const fs = foo.require("fs");' }),
38+
test({ code: 'const fs = require.resolve("fs");' }),
39+
test({ code: 'const fs = require(`fs`);' }),
40+
test({ code: 'const fs = require?.("fs");' }),
41+
test({ code: 'const fs = require("fs", extra);' }),
42+
test({ code: 'const fs = require();' }),
43+
test({ code: 'const fs = require(...["fs"]);' }),
44+
test({ code: 'const fs = require("unicorn");' }),
45+
],
46+
invalid: [
47+
test({ code: 'import fs from "fs";' }),
48+
test({ code: 'export {promises} from "fs";' }),
49+
test({
50+
code: `
51+
async function foo() {
52+
const fs = await import('fs');
53+
}`,
54+
}),
55+
test({ code: 'import fs from "fs/promises";' }),
56+
test({ code: 'export {default} from "fs/promises";' }),
57+
test({
58+
code: `
59+
async function foo() {
60+
const fs = await import('fs/promises');
61+
}`,
62+
}),
63+
test({ code: 'import {promises} from "fs";' }),
64+
test({ code: 'export {default as promises} from "fs";' }),
65+
test({
66+
code: `
67+
async function foo() {
68+
const fs = await import("fs/promises");
69+
}`,
70+
}),
71+
test({
72+
code: `
73+
async function foo() {
74+
const fs = await import(/* escaped */"\\u{66}s/promises");
75+
`,
76+
}),
77+
test({ code: 'import "buffer";' }),
78+
test({ code: 'import "child_process";' }),
79+
test({ code: 'import "timers/promises";' }),
80+
test({ code: 'const {promises} = require("fs")' }),
81+
test({ code: 'const fs = require("fs/promises")' }),
82+
],
83+
});

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