verify-module-closure.mjs
69 lines 2.6 KB
Raw
sha256:bd1eb24434d281fe65142038c1bf217493421f5a86880c22dff7e6037d2d4700 feat(companion): add production macOS distribution path Human minor ⚠ breaking 26 days ago
1 #!/usr/bin/env node
2 import fs from 'node:fs';
3 import module from 'node:module';
4 import path from 'node:path';
5 import vm from 'node:vm';
6
7 const runtimeIndex = process.argv.indexOf('--runtime');
8 if (runtimeIndex < 0 || !process.argv[runtimeIndex + 1]) throw new Error('missing --runtime');
9 const runtime = path.resolve(process.argv[runtimeIndex + 1]);
10 const builtins = new Set([...module.builtinModules, ...module.builtinModules.map((name) => `node:${name}`)]);
11 if (typeof vm.SourceTextModule !== 'function') {
12 throw new Error('verify-module-closure requires Node --experimental-vm-modules');
13 }
14 const queue = [
15 path.join(runtime, 'companion/runtime/main.mjs'),
16 path.join(runtime, 'companion/runtime/app-runtime.mjs'),
17 path.join(runtime, 'cli/index.mjs'),
18 path.join(runtime, 'mcp/server.mjs'),
19 ];
20 const visited = new Set();
21 const barePackages = new Set();
22
23 function packageName(specifier) {
24 const parts = specifier.split('/');
25 return specifier.startsWith('@') ? parts.slice(0, 2).join('/') : parts[0];
26 }
27
28 function resolveRelative(from, specifier) {
29 const candidate = path.resolve(path.dirname(from), specifier);
30 const choices = [
31 candidate,
32 `${candidate}.mjs`,
33 `${candidate}.js`,
34 path.join(candidate, 'index.mjs'),
35 path.join(candidate, 'index.js'),
36 ];
37 const found = choices.find((choice) => fs.existsSync(choice) && fs.statSync(choice).isFile());
38 if (!found || !found.startsWith(`${runtime}${path.sep}`)) {
39 throw new Error(`bundled source import is missing or escapes runtime: ${specifier}`);
40 }
41 return found;
42 }
43
44 while (queue.length > 0) {
45 const file = queue.pop();
46 if (visited.has(file)) continue;
47 visited.add(file);
48 const source = fs.readFileSync(file, 'utf8');
49 const parsed = new vm.SourceTextModule(source, { identifier: file });
50 const specifiers = [...parsed.dependencySpecifiers];
51 const dynamicImports = /\bimport\s*\(\s*(['"])([^'"]+)\1\s*\)/g;
52 for (const match of source.matchAll(dynamicImports)) specifiers.push(match[2]);
53 for (const specifier of specifiers) {
54 if (specifier.startsWith('.') || specifier.startsWith('/')) {
55 queue.push(resolveRelative(file, specifier));
56 } else if (!builtins.has(specifier)) {
57 barePackages.add(packageName(specifier));
58 }
59 }
60 }
61
62 for (const dependency of [...barePackages].sort()) {
63 const packageFile = path.join(runtime, 'node_modules', dependency, 'package.json');
64 if (!fs.existsSync(packageFile)) throw new Error(`bundled dependency is missing: ${dependency}`);
65 }
66
67 process.stdout.write(
68 `module closure verified (${visited.size} source modules, ${barePackages.size} dependency roots)\n`
69 );
File History 1 commit
sha256:bd1eb24434d281fe65142038c1bf217493421f5a86880c22dff7e6037d2d4700 feat(companion): add production macOS distribution path Human minor ⚠ 26 days ago