mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 14:30:45 +00:00
Build: prune packaged runtime test cargo
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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()],
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user