1import { defineRule } from "@oxlint/plugins";
2
3const NODE_MODULE_ALIASES = new Map([
4 ["assert/strict", "Assert"],
5 ["fs/promises", "FSP"],
6]);
7
8const NODE_SEGMENT_ALIASES = new Map([
9 ["fs", "FS"],
10 ["os", "OS"],
11 ["url", "URL"],
12 ["vm", "VM"],
13]);
14
15const toPascalCase = (value: string) =>
16 value
17 .split(/[_-]/u)
18 .filter((segment) => segment.length > 0)
19 .map((segment) => segment[0]?.toUpperCase() + segment.slice(1))
20 .join("");
21
22const expectedNamespaceAlias = (source: string) => {
23 const moduleName = source.slice("node:".length);
24 const knownAlias = NODE_MODULE_ALIASES.get(moduleName);
25 if (knownAlias !== undefined) return `Node${knownAlias}`;
26
27 return `Node${moduleName
28 .split("/")
29 .map((segment) => NODE_SEGMENT_ALIASES.get(segment) ?? toPascalCase(segment))
30 .join("")}`;
31};
32
33const literalStringValue = (node: unknown): string | undefined => {
34 if (typeof node !== "object" || node === null) return undefined;
35 if (!("type" in node) || node.type !== "Literal") return undefined;
36 if (!("value" in node) || typeof node.value !== "string") return undefined;
37 return node.value;
38};
39
40const identifierName = (node: unknown): string | undefined => {
41 if (typeof node !== "object" || node === null) return undefined;
42 if (!("type" in node) || node.type !== "Identifier") return undefined;
43 if (!("name" in node) || typeof node.name !== "string") return undefined;
44 return node.name;
45};
46
47export default defineRule({
48 meta: {
49 type: "problem",
50 docs: {
51 description: "Require canonical namespace imports for Node.js built-in modules.",
52 },
53 },
54 create(context) {
55 return {
56 ImportDeclaration(node) {
57 const source = literalStringValue(node.source);
58 if (source === undefined || !source.startsWith("node:")) return;
59
60 const expectedAlias = expectedNamespaceAlias(source);
61 const namespaceImport =
62 node.specifiers.length === 1 && node.specifiers[0]?.type === "ImportNamespaceSpecifier"
63 ? node.specifiers[0]
64 : undefined;
65 const actualAlias = identifierName(namespaceImport?.local);
66
67 if (actualAlias === expectedAlias) return;
68
69 context.report({
70 node,
71 message: `Import ${source} as a namespace named ${expectedAlias}.`,
72 });
73 },
74 };
75 },
76});