cloudflare/kumo

Public

mirrored fromhttps://github.com/cloudflare/kumoAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
3db829493b37299c87a22a81bbae04cb5bedda37

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

lint/no-cross-package-imports.js

163lines · modecode

1import { defineRule } from "oxlint";
2
3const RULE_NAME = "no-cross-package-imports";
4
5// Known package directory names in this monorepo
6const PACKAGE_DIRS = new Set(["kumo", "kumo-docs-astro", "kumo-figma"]);
7
8// Pattern to detect relative imports that traverse up to packages/ level
9// and then into a sibling package directory.
10// This looks for paths like:
11// ../../kumo/... (from packages/kumo-docs-astro/src/foo.ts)
12// ../../../kumo/... (from packages/kumo-docs-astro/src/deep/foo.ts)
13//
14// The key insight: we need enough "../" to escape the current package's src/
15// directory and land in packages/, then go into another package.
16//
17// We detect this by looking for patterns where a package name appears
18// right after the "../" traversal sequence, which indicates leaving
19// the current package and entering a sibling.
20const CROSS_PACKAGE_PATTERN = /^((?:\.\.\/)+)([a-z0-9-]+)\//;
21
22/**
23 * Check if an import path is a cross-package relative import.
24 * Returns the package name if it is, null otherwise.
25 *
26 * We consider it a cross-package import if:
27 * 1. The path starts with "../" (going up from current directory)
28 * 2. After the "../" sequence, it immediately enters a known package directory
29 * 3. The number of "../" is >= 2 (minimum to escape src/ and reach packages/)
30 *
31 * This avoids false positives for local directories that happen to be named
32 * like packages (e.g., ./kumo/ or ../kumo/ within the same package).
33 */
34function getCrossPackageImport(importPath) {
35 if (!importPath || !importPath.startsWith("..")) {
36 return null;
37 }
38
39 const match = importPath.match(CROSS_PACKAGE_PATTERN);
40 if (!match) {
41 return null;
42 }
43
44 const traversal = match[1]; // The "../../../" part
45 const packageDir = match[2]; // The directory name after traversal
46
47 // Count how many levels we're going up
48 const levelsUp = (traversal.match(/\.\.\//g) || []).length;
49
50 // If we're only going up 1 level (../package/), it's likely a local
51 // directory within the same package. Cross-package imports typically
52 // need at least 2 levels (../../package/) to escape src/ and reach
53 // the packages/ directory.
54 if (levelsUp < 2) {
55 return null;
56 }
57
58 // Check if the target directory is a known package
59 if (PACKAGE_DIRS.has(packageDir)) {
60 return packageDir;
61 }
62
63 return null;
64}
65
66/**
67 * Get the source value from an import declaration or expression.
68 */
69function getImportSource(node) {
70 // Static import: import x from "../kumo/foo"
71 if (node.source && node.source.type === "Literal") {
72 return node.source.value;
73 }
74 // Dynamic import: import("../kumo/foo")
75 if (
76 node.type === "ImportExpression" &&
77 node.source &&
78 node.source.type === "Literal"
79 ) {
80 return node.source.value;
81 }
82 return null;
83}
84
85export const noCrossPackageImportsRule = defineRule({
86 meta: {
87 type: "problem",
88 docs: {
89 description:
90 "Disallow relative imports that reach into sibling packages in the monorepo",
91 },
92 messages: {
93 [RULE_NAME]:
94 "Cross-package relative import detected. Import from '{{packageName}}' using its package name (e.g., '@cloudflare/{{packageName}}') instead of relative paths ('{{importPath}}').",
95 },
96 schema: [],
97 },
98 defaultOptions: [],
99 createOnce(context) {
100 function checkImport(node, importPath) {
101 const packageName = getCrossPackageImport(importPath);
102 if (packageName) {
103 context.report({
104 node,
105 messageId: RULE_NAME,
106 data: {
107 packageName,
108 importPath,
109 },
110 });
111 }
112 }
113
114 return {
115 // Static imports: import x from "../kumo/foo"
116 ImportDeclaration(node) {
117 const source = getImportSource(node);
118 if (source) {
119 checkImport(node, source);
120 }
121 },
122
123 // Dynamic imports: import("../kumo/foo")
124 ImportExpression(node) {
125 const source = getImportSource(node);
126 if (source) {
127 checkImport(node, source);
128 }
129 },
130
131 // require() calls: require("../kumo/foo")
132 CallExpression(node) {
133 if (
134 node.callee.type === "Identifier" &&
135 node.callee.name === "require" &&
136 node.arguments.length > 0 &&
137 node.arguments[0].type === "Literal" &&
138 typeof node.arguments[0].value === "string"
139 ) {
140 checkImport(node, node.arguments[0].value);
141 }
142 },
143
144 // export from: export { x } from "../kumo/foo"
145 ExportNamedDeclaration(node) {
146 if (node.source) {
147 const source = getImportSource(node);
148 if (source) {
149 checkImport(node, source);
150 }
151 }
152 },
153
154 // export * from "../kumo/foo"
155 ExportAllDeclaration(node) {
156 const source = getImportSource(node);
157 if (source) {
158 checkImport(node, source);
159 }
160 },
161 };
162 },
163});
164