Build: prune packaged runtime test cargo

This commit is contained in:
Gustavo Madeira Santana
2026-04-15 11:54:25 -04:00
parent f8705f512b
commit 0618c6933d
4 changed files with 188 additions and 1 deletions

View File

@@ -105,9 +105,41 @@ const FORBIDDEN_PRIVATE_QA_CONTENT_MARKERS = [
"qa-lab/runtime-api.js",
] as const;
const FORBIDDEN_PRIVATE_QA_CONTENT_SCAN_PREFIXES = ["dist/"] as const;
const PACKED_TEST_CARGO_DIRECTORY_SEGMENTS = new Set([
"__snapshots__",
"__tests__",
"test",
"tests",
]);
const PACKED_TEST_CARGO_FILE_RE = /(?:^|\/)[^/]+\.(?:test|spec)\.(?:[cm]?[jt]sx?)$/u;
const NPM_PACK_MAX_BUFFER_BYTES = 64 * 1024 * 1024;
const skipPackValidationEnv = "OPENCLAW_NPM_RELEASE_SKIP_PACK_CHECK";
function normalizePackedPath(packedPath: string): string {
return packedPath.replace(/\\/g, "/");
}
function isNodeModulesPackageRoot(segments: string[], index: number): boolean {
const parent = segments[index - 1];
if (parent === "node_modules") {
return true;
}
return parent?.startsWith("@") && segments[index - 2] === "node_modules";
}
function pathContainsPackedTestCargo(packedPath: string): boolean {
const normalizedPath = normalizePackedPath(packedPath);
if (PACKED_TEST_CARGO_FILE_RE.test(normalizedPath)) {
return true;
}
const segments = normalizedPath.split("/").filter(Boolean);
return segments.some(
(segment, index) =>
PACKED_TEST_CARGO_DIRECTORY_SEGMENTS.has(segment) &&
!isNodeModulesPackageRoot(segments, index),
);
}
function normalizeRepoUrl(value: unknown): string {
if (typeof value !== "string") {
return "";
@@ -489,7 +521,11 @@ function collectPackedTarballErrors(): string[] {
return [
...collectControlUiPackErrors(packedPaths),
...collectForbiddenPackedPathErrors(packedPaths),
<<<<<<< HEAD
...collectForbiddenPackedContentErrors(packedPaths),
=======
...collectPackedTestCargoErrors(packedPaths),
>>>>>>> caafdea0bb (Build: prune packaged runtime test cargo)
];
}
@@ -544,7 +580,22 @@ export function collectForbiddenPackedContentErrors(
return errors.toSorted((left, right) => left.localeCompare(right));
}
export function collectPackedTestCargoErrors(paths: Iterable<string>): string[] {
const errors: string[] = [];
for (const packedPath of paths) {
if (!pathContainsPackedTestCargo(packedPath)) {
continue;
}
errors.push(`npm package must not include test cargo "${packedPath}".`);
}
return errors.toSorted((left, right) => left.localeCompare(right));
}
<<<<<<< HEAD
async function main(): Promise<number> {
=======
function main(): number {
>>>>>>> caafdea0bb (Build: prune packaged runtime test cargo)
const pkg = loadPackageJson();
const now = new Date();
const skipPackValidation = shouldSkipPackedTarballValidation();

View File

@@ -142,6 +142,15 @@ function readInstalledDependencyVersionFromRoot(depRoot) {
}
const defaultStagedRuntimeDepGlobalPruneSuffixes = [".d.ts", ".map"];
const defaultStagedRuntimeDepGlobalPruneDirectories = [
"__snapshots__",
"__tests__",
"test",
"tests",
];
const defaultStagedRuntimeDepGlobalPruneFilePatterns = [
/(?:^|\/)[^/]+\.(?:test|spec)\.(?:[cm]?[jt]sx?)$/u,
];
const defaultStagedRuntimeDepPruneRules = new Map([
// Type declarations only; runtime resolves through lib/es entrypoints.
["@larksuiteoapi/node-sdk", { paths: ["types"] }],
@@ -182,11 +191,17 @@ const defaultStagedRuntimeDepPruneRules = new Map([
["@jimp/plugin-quantize", { paths: ["src/__image_snapshots__"] }],
["@jimp/plugin-threshold", { paths: ["src/__image_snapshots__"] }],
]);
const runtimeDepsStagingVersion = 5;
const runtimeDepsStagingVersion = 6;
const exactVersionSpecRe = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/u;
function resolveRuntimeDepPruneConfig(params = {}) {
return {
globalPruneDirectories:
params.stagedRuntimeDepGlobalPruneDirectories ??
defaultStagedRuntimeDepGlobalPruneDirectories,
globalPruneFilePatterns:
params.stagedRuntimeDepGlobalPruneFilePatterns ??
defaultStagedRuntimeDepGlobalPruneFilePatterns,
globalPruneSuffixes:
params.stagedRuntimeDepGlobalPruneSuffixes ?? defaultStagedRuntimeDepGlobalPruneSuffixes,
pruneRules: params.stagedRuntimeDepPruneRules ?? defaultStagedRuntimeDepPruneRules,
@@ -489,6 +504,40 @@ function pruneDependencyFilesBySuffixes(depRoot, suffixes) {
});
}
function pruneDependencyDirectoriesByBasename(depRoot, basenames) {
if (!basenames || basenames.length === 0 || !fs.existsSync(depRoot)) {
return;
}
const basenameSet = new Set(basenames);
const queue = [depRoot];
while (queue.length > 0) {
const currentDir = queue.shift();
for (const entry of fs.readdirSync(currentDir, { withFileTypes: true })) {
if (!entry.isDirectory()) {
continue;
}
const fullPath = path.join(currentDir, entry.name);
if (basenameSet.has(entry.name)) {
removePathIfExists(fullPath);
continue;
}
queue.push(fullPath);
}
}
}
function pruneDependencyFilesByPatterns(depRoot, patterns) {
if (!patterns || patterns.length === 0 || !fs.existsSync(depRoot)) {
return;
}
walkFiles(depRoot, (fullPath) => {
const relativePath = path.relative(depRoot, fullPath).replace(/\\/g, "/");
if (patterns.some((pattern) => pattern.test(relativePath))) {
removePathIfExists(fullPath);
}
});
}
function pruneStagedInstalledDependencyCargo(nodeModulesDir, depName, pruneConfig) {
const depRoot = dependencyNodeModulesPath(nodeModulesDir, depName);
if (depRoot === null) {
@@ -498,6 +547,8 @@ function pruneStagedInstalledDependencyCargo(nodeModulesDir, depName, pruneConfi
for (const relativePath of pruneRule?.paths ?? []) {
removePathIfExists(path.join(depRoot, relativePath));
}
pruneDependencyDirectoriesByBasename(depRoot, pruneConfig.globalPruneDirectories);
pruneDependencyFilesByPatterns(depRoot, pruneConfig.globalPruneFilePatterns);
pruneDependencyFilesBySuffixes(depRoot, pruneConfig.globalPruneSuffixes);
pruneDependencyFilesBySuffixes(depRoot, pruneRule?.suffixes ?? []);
}
@@ -784,6 +835,10 @@ function createRuntimeDepsFingerprint(packageJson, pruneConfig, params = {}) {
return createHash("sha256")
.update(
JSON.stringify({
globalPruneDirectories: pruneConfig.globalPruneDirectories,
globalPruneFilePatterns: pruneConfig.globalPruneFilePatterns.map((pattern) =>
pattern.toString(),
),
globalPruneSuffixes: pruneConfig.globalPruneSuffixes,
packageJson,
pruneRules: [...pruneConfig.pruneRules.entries()],

View File

@@ -8,6 +8,7 @@ import {
collectControlUiPackErrors,
collectForbiddenPackedContentErrors,
collectForbiddenPackedPathErrors,
collectPackedTestCargoErrors,
collectReleasePackageMetadataErrors,
collectReleaseTagErrors,
parseNpmPackJsonOutput,
@@ -380,6 +381,33 @@ describe("collectForbiddenPackedPathErrors", () => {
});
});
describe("collectPackedTestCargoErrors", () => {
it("rejects packed test files and test directories", () => {
expect(
collectPackedTestCargoErrors([
"dist/extensions/webhooks/node_modules/zod/src/v3/tests/all-errors.test.ts",
"dist/extensions/whatsapp/node_modules/pino/test/basic.test.js",
"dist/extensions/whatsapp/node_modules/@jimp/plugin-crop/src/__snapshots__/crop.test.ts.snap",
"dist/index.js",
]),
).toEqual([
'npm package must not include test cargo "dist/extensions/webhooks/node_modules/zod/src/v3/tests/all-errors.test.ts".',
'npm package must not include test cargo "dist/extensions/whatsapp/node_modules/@jimp/plugin-crop/src/__snapshots__/crop.test.ts.snap".',
'npm package must not include test cargo "dist/extensions/whatsapp/node_modules/pino/test/basic.test.js".',
]);
});
it("allows normal runtime files", () => {
expect(
collectPackedTestCargoErrors([
"dist/index.js",
"dist/extensions/whatsapp/node_modules/pino/lib/proto.js",
"dist/extensions/webhooks/node_modules/zod/v4/core/api.js",
]),
).toEqual([]);
});
});
describe("collectReleaseTagErrors", () => {
it("accepts versions within the two-day CalVer window", () => {
expect(

View File

@@ -362,6 +362,59 @@ describe("stageBundledPluginRuntimeDeps", () => {
expect(fs.existsSync(path.join(pluginDir, ".openclaw-runtime-deps-stamp.json"))).toBe(true);
});
it("prunes staged test cargo from copied runtime dependencies", () => {
const { pluginDir, repoRoot } = createBundledPluginFixture({
packageJson: {
name: "@openclaw/fixture-plugin",
version: "1.0.0",
dependencies: { direct: "1.0.0" },
openclaw: { bundle: { stageRuntimeDependencies: true } },
},
});
const directDir = path.join(repoRoot, "node_modules", "direct");
fs.mkdirSync(path.join(directDir, "test"), { recursive: true });
fs.mkdirSync(path.join(directDir, "__snapshots__"), { recursive: true });
fs.mkdirSync(path.join(directDir, "src"), { recursive: true });
fs.writeFileSync(
path.join(directDir, "package.json"),
'{ "name": "direct", "version": "1.0.0" }\n',
"utf8",
);
fs.writeFileSync(path.join(directDir, "index.js"), "module.exports = 'runtime';\n", "utf8");
fs.writeFileSync(
path.join(directDir, "test", "index.test.js"),
"module.exports = 'remove';\n",
"utf8",
);
fs.writeFileSync(
path.join(directDir, "__snapshots__", "index.test.ts.snap"),
"snapshot\n",
"utf8",
);
fs.writeFileSync(
path.join(directDir, "src", "runtime.spec.js"),
"module.exports = 'remove';\n",
"utf8",
);
stageBundledPluginRuntimeDeps({ cwd: repoRoot });
expect(
fs.readFileSync(path.join(pluginDir, "node_modules", "direct", "index.js"), "utf8"),
).toBe("module.exports = 'runtime';\n");
expect(
fs.existsSync(path.join(pluginDir, "node_modules", "direct", "test", "index.test.js")),
).toBe(false);
expect(
fs.existsSync(
path.join(pluginDir, "node_modules", "direct", "__snapshots__", "index.test.ts.snap"),
),
).toBe(false);
expect(
fs.existsSync(path.join(pluginDir, "node_modules", "direct", "src", "runtime.spec.js")),
).toBe(false);
});
it("stages hoisted transitive runtime deps from the root node_modules", () => {
const { pluginDir, repoRoot } = createBundledPluginFixture({
packageJson: {