mirror of
https://github.com/mvschwarz/openrig.git
synced 2026-11-07 05:56:21 -10:00
A clean install of the CLI couldn't start: bare `rig` and `rig --help` exited 1 with `ERR_MODULE_NOT_FOUND` for `@modelcontextprotocol/sdk`. `packages/cli/src/commands/environment.ts` imports the SDK client at module load and `index.ts` loads the environment command statically, but the SDK was declared only in the CLI's `devDependencies`, so an install without development dependencies didn't have it. The SDK (`^1.32.2`) is now a production dependency of `packages/cli`; the lockfile update is metadata only (its dev flags), with no package added, removed or re-versioned. `scripts/cli-runtime-dependencies.test.mjs` walks the import closure from the CLI's entry points and fails if any external package it reaches isn't a production dependency, and the testbed image now runs `rig --help` after `rig --version`. 4 files, +77/-87.
74 lines
3.1 KiB
JavaScript
74 lines
3.1 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { readFileSync } from "node:fs";
|
|
import { isBuiltin } from "node:module";
|
|
import { dirname, resolve } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import test from "node:test";
|
|
import ts from "typescript";
|
|
|
|
const root = fileURLToPath(new URL("../", import.meta.url));
|
|
|
|
function runtimeImports(text, file) {
|
|
const source = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true);
|
|
const imports = [];
|
|
const visit = (node) => {
|
|
let literal;
|
|
if (ts.isImportDeclaration(node)) {
|
|
const clause = node.importClause;
|
|
if (clause?.isTypeOnly) return;
|
|
const bindings = clause?.namedBindings;
|
|
if (!clause?.name && bindings && ts.isNamedImports(bindings)
|
|
&& bindings.elements.length && bindings.elements.every((e) => e.isTypeOnly)) return;
|
|
literal = node.moduleSpecifier;
|
|
} else if (ts.isExportDeclaration(node) && !node.isTypeOnly) {
|
|
if (node.exportClause && ts.isNamedExports(node.exportClause)
|
|
&& node.exportClause.elements.every((e) => e.isTypeOnly)) return;
|
|
literal = node.moduleSpecifier;
|
|
} else if (ts.isCallExpression(node) && (node.expression.kind === ts.SyntaxKind.ImportKeyword
|
|
|| (ts.isIdentifier(node.expression) && node.expression.text === "require"))) {
|
|
literal = node.arguments[0];
|
|
}
|
|
if (literal && ts.isStringLiteralLike(literal)) imports.push(literal.text);
|
|
ts.forEachChild(node, visit);
|
|
};
|
|
visit(source);
|
|
return imports;
|
|
}
|
|
|
|
test("runtime import audit includes lazy loads and re-exports, excludes types and prose", () => {
|
|
assert.deepEqual(runtimeImports(`
|
|
import type { A } from "types-only";
|
|
import { type B } from "inline-types-only";
|
|
export type { C } from "exported-types";
|
|
const prose = 'import("not-an-import")';
|
|
import "side-effect";
|
|
export { value } from "re-export";
|
|
const lazy = () => import("lazy");
|
|
const cjs = require("required");
|
|
`, "control.ts"), ["side-effect", "re-export", "lazy", "required"]);
|
|
});
|
|
|
|
test("the CLI entry points declare every reachable external runtime dependency", () => {
|
|
const manifest = JSON.parse(readFileSync(resolve(root, "packages/cli/package.json"), "utf8"));
|
|
const pending = ["bin-wrapper.ts", "index.ts"].map((f) => resolve(root, "packages/cli/src", f));
|
|
const visited = new Set();
|
|
const missing = new Set();
|
|
while (pending.length) {
|
|
const file = pending.pop();
|
|
if (visited.has(file)) continue;
|
|
visited.add(file);
|
|
for (const specifier of runtimeImports(readFileSync(file, "utf8"), file)) {
|
|
if (specifier.startsWith(".")) {
|
|
pending.push(resolve(dirname(file), specifier.replace(/\.js$/, ".ts")));
|
|
continue;
|
|
}
|
|
if (isBuiltin(specifier)) continue;
|
|
const name = specifier.split("/").slice(0, specifier.startsWith("@") ? 2 : 1).join("/");
|
|
// build-package rewrites these to the daemon files shipped inside the CLI.
|
|
if (name === "@openrig/daemon") continue;
|
|
if (!Object.hasOwn(manifest.dependencies, name)) missing.add(`${name} (${file.slice(root.length)})`);
|
|
}
|
|
}
|
|
assert.deepEqual([...missing].sort(), [], "Runtime imports must survive a production-only install");
|
|
});
|