fix: guard cli bootstrap imports

This commit is contained in:
Peter Steinberger
2026-04-27 11:24:18 +01:00
parent fa0d81ed13
commit a0aedea63d
12 changed files with 393 additions and 39 deletions

View File

@@ -13,6 +13,11 @@ const BUILD_CACHE_VERSION = 2;
export const BUILD_ALL_STEPS = [
{ label: "canvas:a2ui:bundle", kind: "pnpm", pnpmArgs: ["canvas:a2ui:bundle"] },
{ label: "tsdown", kind: "node", args: ["scripts/tsdown-build.mjs"] },
{
label: "check-cli-bootstrap-imports",
kind: "node",
args: ["scripts/check-cli-bootstrap-imports.mjs"],
},
{ label: "runtime-postbuild", kind: "node", args: ["scripts/runtime-postbuild.mjs"] },
{ label: "build-stamp", kind: "node", args: ["scripts/build-stamp.mjs"] },
{
@@ -91,6 +96,7 @@ export const BUILD_ALL_PROFILES = {
ciArtifacts: [
"canvas:a2ui:bundle",
"tsdown",
"check-cli-bootstrap-imports",
"runtime-postbuild",
"build-stamp",
"build:plugin-sdk:dts",
@@ -103,7 +109,7 @@ export const BUILD_ALL_PROFILES = {
"write-cli-startup-metadata",
"write-cli-compat",
],
gatewayWatch: ["tsdown", "runtime-postbuild", "build-stamp"],
gatewayWatch: ["tsdown", "check-cli-bootstrap-imports", "runtime-postbuild", "build-stamp"],
};
export function resolveBuildAllSteps(profile = "full") {

View File

@@ -0,0 +1,127 @@
#!/usr/bin/env node
import fs from "node:fs";
import module from "node:module";
import path from "node:path";
import { fileURLToPath } from "node:url";
const DEFAULT_ENTRYPOINTS = ["dist/entry.js", "dist/cli/run-main.js"];
const STATIC_IMPORT_RE =
/\b(?:import|export)\s+(?:(?:[^'"()]*?\s+from\s+)|)["'](?<specifier>[^"']+)["']/gu;
function isMainModule() {
return process.argv[1] ? path.resolve(process.argv[1]) === fileURLToPath(import.meta.url) : false;
}
function isBuiltinSpecifier(specifier) {
return specifier.startsWith("node:") || module.isBuiltin(specifier);
}
function isRelativeSpecifier(specifier) {
return specifier.startsWith("./") || specifier.startsWith("../") || specifier.startsWith("/");
}
function resolveRelativeImport(importer, specifier, fsImpl = fs) {
const base = specifier.startsWith("/")
? specifier
: path.resolve(path.dirname(importer), specifier);
const candidates = [
base,
`${base}.js`,
`${base}.mjs`,
`${base}.cjs`,
path.join(base, "index.js"),
path.join(base, "index.mjs"),
path.join(base, "index.cjs"),
];
return candidates.find((candidate) => {
try {
return fsImpl.statSync(candidate).isFile();
} catch {
return false;
}
});
}
export function listStaticImportSpecifiers(source) {
return [...source.matchAll(STATIC_IMPORT_RE)].map((match) => match.groups?.specifier ?? "");
}
export function collectCliBootstrapExternalImportErrors(params = {}) {
const rootDir = params.rootDir ?? process.cwd();
const entrypoints = params.entrypoints ?? DEFAULT_ENTRYPOINTS;
const fsImpl = params.fs ?? fs;
const queue = entrypoints.map((entrypoint) => path.resolve(rootDir, entrypoint));
const visited = new Set();
const errors = [];
for (let index = 0; index < queue.length; index += 1) {
const filePath = queue[index];
if (!filePath || visited.has(filePath)) {
continue;
}
visited.add(filePath);
let source;
try {
source = fsImpl.readFileSync(filePath, "utf8");
} catch {
errors.push(
`CLI bootstrap import guard could not read ${path.relative(rootDir, filePath) || filePath}. Run pnpm build first.`,
);
continue;
}
for (const specifier of listStaticImportSpecifiers(source)) {
if (!specifier || isBuiltinSpecifier(specifier)) {
continue;
}
if (!isRelativeSpecifier(specifier)) {
errors.push(
`CLI bootstrap static graph imports external package "${specifier}" from ${path.relative(
rootDir,
filePath,
)}.`,
);
continue;
}
const resolved = resolveRelativeImport(filePath, specifier, fsImpl);
if (!resolved) {
errors.push(
`CLI bootstrap import guard could not resolve "${specifier}" from ${path.relative(
rootDir,
filePath,
)}.`,
);
continue;
}
if (!visited.has(resolved)) {
queue.push(resolved);
}
}
}
return errors.toSorted((left, right) => left.localeCompare(right));
}
export function checkCliBootstrapExternalImports(params = {}) {
const errors = collectCliBootstrapExternalImportErrors(params);
if (errors.length === 0) {
return;
}
const logger = params.logger ?? console;
logger.error("CLI bootstrap import guard failed:");
for (const error of errors) {
logger.error(` - ${error}`);
}
throw new Error("CLI bootstrap static graph imports external packages.");
}
if (isMainModule()) {
try {
checkCliBootstrapExternalImports();
console.log("CLI bootstrap import guard passed.");
} catch {
process.exit(1);
}
}

View File

@@ -23,6 +23,7 @@ import {
resolveBundledRuntimeDependencyInstallRoot,
resolveBundledRuntimeDependencyPackageInstallRoot,
} from "../src/plugins/bundled-runtime-deps.ts";
import { checkCliBootstrapExternalImports } from "./check-cli-bootstrap-imports.mjs";
import {
collectBundledExtensionManifestErrors,
type BundledExtension,
@@ -110,6 +111,8 @@ const laneFloorAdoptionDateKey = 20260227;
const SAFE_UNIX_SMOKE_PATH = "/usr/bin:/bin";
export const PACKED_CLI_SMOKE_COMMANDS = [
["--help"],
["onboard", "--help"],
["doctor", "--help"],
["status", "--json", "--timeout", "1"],
["config", "schema"],
["models", "list", "--provider", "amazon-bedrock"],
@@ -807,6 +810,11 @@ async function checkPluginSdkExports() {
async function main() {
checkAppcastSparkleVersions();
checkCliBootstrapExternalImports({
logger: {
error: (message: string) => console.error(`release-check: ${message}`),
},
});
await checkPluginSdkExports();
checkBundledExtensionMetadata();
await writePackageDistInventory(process.cwd());