|
1 | | -import { fileURLToPath } from "node:url" |
2 | | -import { readdirSync, readFileSync } from "node:fs" |
| 1 | +import { spawnSync } from "node:child_process" |
3 | 2 | import path from "node:path" |
| 3 | +import { fileURLToPath } from "node:url" |
4 | 4 |
|
5 | | -const __dirname = path.dirname(fileURLToPath(import.meta.url)) |
6 | | -const ROOT = path.resolve(__dirname, "..") |
7 | | - |
8 | | -const targetFiles = ["packages/web/src", "packages/web/server", "packages/electron/src"] |
9 | | - |
10 | | -const disallowedPatterns = [/\.\.\/ui\/src\b/, /\.\.\/desktop\/src\b/] |
11 | | - |
12 | | -const allowedOpenChamberUiImports = [ |
13 | | - /^@openchamber\/ui$/, |
14 | | - /^@openchamber\/ui\/main$/, |
15 | | - /^@openchamber\/ui\/index\.css$/, |
16 | | - /^@openchamber\/ui\/styles\/fonts$/, |
17 | | - /^@openchamber\/ui\/terminalApi$/, |
18 | | - /^@openchamber\/ui\/api\/(endpoints|gitApiHttp|types)$/, |
19 | | - /^@openchamber\/ui\/apps\/renderElectronMiniChatApp$/, |
20 | | -] |
21 | | - |
22 | | -const importSpecifiers = [ |
23 | | - /(?:import|export)\s+(?:type\s+)?(?:[^'"]*?\s+from\s+)?['"]([^'"]+)['"]/g, |
24 | | - /import\s*\(\s*['"]([^'"]+)['"]\s*\)/g, |
25 | | - /require\s*\(\s*['"]([^'"]+)['"]\s*\)/g, |
26 | | -] |
27 | | - |
28 | | -const packageDependencyRules = [ |
29 | | - { |
30 | | - packageJsonPath: "packages/ui/package.json", |
31 | | - dependencyNames: ["electron", "electron-updater", "express", "http-proxy-middleware", "node-pty", "simple-git"], |
32 | | - reason: "server or desktop-shell dependency declared by UI package", |
33 | | - }, |
34 | | -] |
35 | | - |
36 | | -function collectFiles(baseDir, extensions) { |
37 | | - const entries = readdirSync(baseDir, { withFileTypes: true }) |
38 | | - const files = [] |
39 | | - |
40 | | - for (const entry of entries) { |
41 | | - const fullPath = path.join(baseDir, entry.name) |
42 | | - if (entry.isDirectory()) { |
43 | | - if (entry.name === "dist" || entry.name === "node_modules" || entry.name.startsWith(".")) { |
44 | | - continue |
45 | | - } |
46 | | - files.push(...collectFiles(fullPath, extensions)) |
47 | | - continue |
48 | | - } |
49 | | - if (!extensions.includes(path.extname(entry.name))) { |
50 | | - continue |
51 | | - } |
52 | | - files.push(fullPath) |
53 | | - } |
54 | | - |
55 | | - return files |
56 | | -} |
57 | | - |
58 | | -const violations = [] |
59 | | - |
60 | | -function addViolation(file, lineNumber, lineText, reason) { |
61 | | - violations.push({ |
62 | | - file, |
63 | | - lineNumber, |
64 | | - lineText: lineText.trim(), |
65 | | - reason, |
66 | | - }) |
67 | | -} |
68 | | - |
69 | | -function isAllowedOpenChamberUiImport(specifier) { |
70 | | - return allowedOpenChamberUiImports.some((pattern) => pattern.test(specifier)) |
71 | | -} |
72 | | - |
73 | | -function dependencyLineNumber(content, dependencyName) { |
74 | | - const lines = content.split(/\r?\n/) |
75 | | - const needle = `"${dependencyName}"` |
76 | | - const index = lines.findIndex((line) => line.includes(needle)) |
77 | | - return index === -1 ? 1 : index + 1 |
78 | | -} |
79 | | - |
80 | | -for (const filePath of targetFiles) { |
81 | | - const absolute = path.join(ROOT, filePath) |
82 | | - const files = collectFiles(absolute, [".ts", ".tsx", ".js", ".jsx"]) |
83 | | - for (const file of files) { |
84 | | - const content = readFileSync(file, "utf8") |
85 | | - const lines = content.split(/\r?\n/) |
86 | | - lines.forEach((line, index) => { |
87 | | - if (disallowedPatterns.some((pattern) => pattern.test(line))) { |
88 | | - addViolation(file, index + 1, line, "sibling-package source import") |
89 | | - } |
90 | | - |
91 | | - for (const pattern of importSpecifiers) { |
92 | | - pattern.lastIndex = 0 |
93 | | - let match |
94 | | - while ((match = pattern.exec(line)) !== null) { |
95 | | - const specifier = match[1] |
96 | | - if (specifier.startsWith("@openchamber/ui/") && !isAllowedOpenChamberUiImport(specifier)) { |
97 | | - addViolation(file, index + 1, line, `private @openchamber/ui import: ${specifier}`) |
98 | | - } |
99 | | - } |
100 | | - } |
101 | | - }) |
102 | | - } |
103 | | -} |
104 | | - |
105 | | -for (const rule of packageDependencyRules) { |
106 | | - const file = path.join(ROOT, rule.packageJsonPath) |
107 | | - const content = readFileSync(file, "utf8") |
108 | | - const manifest = JSON.parse(content) |
109 | | - const dependencyBlocks = [ |
110 | | - manifest.dependencies ?? {}, |
111 | | - manifest.devDependencies ?? {}, |
112 | | - manifest.peerDependencies ?? {}, |
113 | | - manifest.optionalDependencies ?? {}, |
114 | | - ] |
115 | | - |
116 | | - for (const dependencyName of rule.dependencyNames) { |
117 | | - if (dependencyBlocks.some((block) => Object.hasOwn(block, dependencyName))) { |
118 | | - addViolation(file, dependencyLineNumber(content, dependencyName), `"${dependencyName}"`, rule.reason) |
119 | | - } |
120 | | - } |
121 | | -} |
| 5 | +const desktopRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..") |
| 6 | +const repositoryRoot = path.resolve(desktopRoot, "..") |
| 7 | +const checker = path.join(repositoryRoot, "script/check-desktop-boundaries.ts") |
122 | 8 |
|
123 | | -if (violations.length > 0) { |
124 | | - console.error("Boundary import check failed.") |
125 | | - for (const item of violations) { |
126 | | - const relative = path.relative(ROOT, item.file) |
127 | | - console.error(`- ${relative}:${item.lineNumber}: ${item.reason}: ${item.lineText}`) |
128 | | - } |
129 | | - process.exit(1) |
130 | | -} |
| 9 | +// Keep the historical Desktop-local entrypoint, but delegate policy to the |
| 10 | +// repository checker so the two commands cannot drift apart. |
| 11 | +const result = spawnSync(process.execPath, ["--import", "tsx", checker, ...process.argv.slice(2)], { |
| 12 | + cwd: repositoryRoot, |
| 13 | + stdio: "inherit", |
| 14 | +}) |
131 | 15 |
|
132 | | -console.log("Boundary import check passed.") |
| 16 | +if (result.error) throw result.error |
| 17 | +process.exitCode = result.status ?? 1 |
0 commit comments