返回 DeepSeek-Reasonix
check-desktop-host-boundary.mjs
根目录 / desktop / frontend / scripts / check-desktop-host-boundary.mjs
1 #!/usr/bin/env node
2 import { existsSync, readdirSync, readFileSync } from "node:fs";
3 import { dirname, join, relative, resolve } from "node:path";
4 import { fileURLToPath, pathToFileURL } from "node:url";
5 import ts from "typescript";
6
7 const HOST_GLOBALS = new Set(["go", "runtime", "reasonixDesktop"]);
8 const SHELL_MODULES = [/(?:^|\/)wailsjs(?:\/|$)/, /^@wailsapp(?:\/|$)/];
9 const ALLOWED = new Set(["lib/desktopHost.ts"]);
10
11 function sourceFiles(root) {
12 if (!existsSync(root)) return [];
13 return readdirSync(root, { withFileTypes: true }).flatMap((entry) =>
14 entry.isDirectory() ? sourceFiles(join(root, entry.name))
15 : /\.[cm]?[jt]sx?$/.test(entry.name) ? [join(root, entry.name)] : []);
16 }
17
18 function unwrap(node) {
19 while (ts.isParenthesizedExpression(node) || ts.isAsExpression(node) || ts.isTypeAssertionExpression(node)
20 || ts.isNonNullExpression(node) || ts.isSatisfiesExpression(node)) node = node.expression;
21 return node;
22 }
23
24 function isWindowObject(node) {
25 const inner = unwrap(node);
26 if (ts.isIdentifier(inner)) return inner.text === "window";
27 // globalThis.window / self.window
28 return ts.isPropertyAccessExpression(inner) && inner.name.text === "window" && ts.isIdentifier(unwrap(inner.expression));
29 }
30
31 function line(tree, node) {
32 return tree.getLineAndCharacterOfPosition(node.getStart(tree)).line + 1;
33 }
34
35 export function desktopHostViolations(code, file) {
36 const tree = ts.createSourceFile(file, code, ts.ScriptTarget.Latest, true);
37 const violations = [];
38 const shellModule = (specifier) => SHELL_MODULES.some((pattern) => pattern.test(specifier));
39 function visit(node) {
40 if (ts.isPropertyAccessExpression(node) && HOST_GLOBALS.has(node.name.text) && isWindowObject(node.expression)) {
41 violations.push(`${line(tree, node)}: window.${node.name.text}`);
42 } else if (ts.isElementAccessExpression(node) && ts.isStringLiteral(node.argumentExpression)
43 && HOST_GLOBALS.has(node.argumentExpression.text) && isWindowObject(node.expression)) {
44 violations.push(`${line(tree, node)}: window["${node.argumentExpression.text}"]`);
45 } else if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier) && shellModule(node.moduleSpecifier.text)) {
46 // The Wails shell is retired; no import of its generated bindings or
47 // runtime package is legitimate anymore, type-only included.
48 violations.push(`${line(tree, node)}: import from ${node.moduleSpecifier.text}`);
49 } else if (ts.isExportDeclaration(node) && node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier)
50 && shellModule(node.moduleSpecifier.text) && !node.isTypeOnly) {
51 violations.push(`${line(tree, node)}: export from ${node.moduleSpecifier.text}`);
52 } else if (ts.isCallExpression(node) && (node.expression.kind === ts.SyntaxKind.ImportKeyword
53 || (ts.isIdentifier(node.expression) && node.expression.text === "require"))) {
54 const argument = node.arguments[0];
55 if (argument && ts.isStringLiteral(argument) && shellModule(argument.text)) {
56 violations.push(`${line(tree, node)}: dynamic import of ${argument.text}`);
57 }
58 }
59 ts.forEachChild(node, visit);
60 }
61 visit(tree);
62 return violations;
63 }
64
65 export function checkDesktopHostBoundary(sourceRoot) {
66 const failures = [];
67 for (const file of sourceFiles(sourceRoot)) {
68 const name = relative(sourceRoot, file).replaceAll("\\", "/");
69 if (ALLOWED.has(name) || name.startsWith("__tests__/") || /\.test\.[cm]?[jt]sx?$/.test(name)) continue;
70 for (const violation of desktopHostViolations(readFileSync(file, "utf8"), file)) failures.push(`${name}:${violation}`);
71 }
72 return failures;
73 }
74
75 if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) {
76 const frontend = dirname(dirname(fileURLToPath(import.meta.url)));
77 const failures = checkDesktopHostBoundary(join(frontend, "src"));
78 for (const failure of failures) console.error("check-desktop-host-boundary: " + failure);
79 if (failures.length) process.exitCode = 1;
80 else console.log("check-desktop-host-boundary: only lib/desktopHost.ts reaches the shell globals");
81 }
82
82 lines Plain Text