mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-06 05:51:15 +08:00
Config: separate core/plugin baseline entries (#60162)
* Config: separate core/plugin baseline entries * Config: split config baseline by kind * Config: split generated baselines by kind * chore(build): skip generated baseline shards in local tooling * chore(build): forbid generated docs in npm pack
This commit is contained in:
@@ -33,6 +33,8 @@ node_modules
|
||||
**/.next
|
||||
coverage
|
||||
**/coverage
|
||||
docs/.generated
|
||||
**/.generated
|
||||
*.log
|
||||
tmp
|
||||
**/tmp
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
These baseline artifacts are generated from the repo-owned OpenClaw config schema and bundled channel/plugin metadata.
|
||||
|
||||
- Do not edit `config-baseline.json` by hand.
|
||||
- Do not edit `config-baseline.jsonl` by hand.
|
||||
- Do not edit `config-baseline.core.json` by hand.
|
||||
- Do not edit `config-baseline.channel.json` by hand.
|
||||
- Do not edit `config-baseline.plugin.json` by hand.
|
||||
- Do not edit `plugin-sdk-api-baseline.json` by hand.
|
||||
- Do not edit `plugin-sdk-api-baseline.jsonl` by hand.
|
||||
- Regenerate config baseline artifacts with `pnpm config:docs:gen`.
|
||||
|
||||
31220
docs/.generated/config-baseline.channel.json
Normal file
31220
docs/.generated/config-baseline.channel.json
Normal file
File diff suppressed because it is too large
Load Diff
22673
docs/.generated/config-baseline.core.json
Normal file
22673
docs/.generated/config-baseline.core.json
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
14013
docs/.generated/config-baseline.plugin.json
Normal file
14013
docs/.generated/config-baseline.plugin.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
const DEFAULT_SKIPPED_DIR_NAMES = new Set(["node_modules", "dist", "coverage"]);
|
||||
const DEFAULT_SKIPPED_DIR_NAMES = new Set(["node_modules", "dist", "coverage", ".generated"]);
|
||||
|
||||
export function isCodeFile(filePath: string): boolean {
|
||||
if (filePath.endsWith(".d.ts")) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { writeConfigDocBaselineStatefile } from "../src/config/doc-baseline.js";
|
||||
import { writeConfigDocBaselineArtifacts } from "../src/config/doc-baseline.js";
|
||||
|
||||
const args = new Set(process.argv.slice(2));
|
||||
const checkOnly = args.has("--check");
|
||||
@@ -12,7 +12,7 @@ if (checkOnly && args.has("--write")) {
|
||||
}
|
||||
|
||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const result = await writeConfigDocBaselineStatefile({
|
||||
const result = await writeConfigDocBaselineArtifacts({
|
||||
repoRoot,
|
||||
check: checkOnly,
|
||||
});
|
||||
@@ -20,15 +20,22 @@ const result = await writeConfigDocBaselineStatefile({
|
||||
if (checkOnly) {
|
||||
if (!result.changed) {
|
||||
console.log(
|
||||
`OK ${path.relative(repoRoot, result.jsonPath)} ${path.relative(repoRoot, result.statefilePath)}`,
|
||||
[
|
||||
`OK ${path.relative(repoRoot, result.jsonPaths.combined)}`,
|
||||
`OK ${path.relative(repoRoot, result.jsonPaths.core)}`,
|
||||
`OK ${path.relative(repoRoot, result.jsonPaths.channel)}`,
|
||||
`OK ${path.relative(repoRoot, result.jsonPaths.plugin)}`,
|
||||
].join("\n"),
|
||||
);
|
||||
process.exit(0);
|
||||
}
|
||||
console.error(
|
||||
[
|
||||
"Config baseline drift detected.",
|
||||
`Expected current: ${path.relative(repoRoot, result.jsonPath)}`,
|
||||
`Expected current: ${path.relative(repoRoot, result.statefilePath)}`,
|
||||
`Expected current: ${path.relative(repoRoot, result.jsonPaths.combined)}`,
|
||||
`Expected current: ${path.relative(repoRoot, result.jsonPaths.core)}`,
|
||||
`Expected current: ${path.relative(repoRoot, result.jsonPaths.channel)}`,
|
||||
`Expected current: ${path.relative(repoRoot, result.jsonPaths.plugin)}`,
|
||||
"If this config-surface change is intentional, run `pnpm config:docs:gen` and commit the updated baseline files.",
|
||||
"If not intentional, treat this as docs drift or a possible breaking config change and fix the schema/help changes first.",
|
||||
].join("\n"),
|
||||
@@ -38,7 +45,9 @@ if (checkOnly) {
|
||||
|
||||
console.log(
|
||||
[
|
||||
`Wrote ${path.relative(repoRoot, result.jsonPath)}`,
|
||||
`Wrote ${path.relative(repoRoot, result.statefilePath)}`,
|
||||
`Wrote ${path.relative(repoRoot, result.jsonPaths.combined)}`,
|
||||
`Wrote ${path.relative(repoRoot, result.jsonPaths.core)}`,
|
||||
`Wrote ${path.relative(repoRoot, result.jsonPaths.channel)}`,
|
||||
`Wrote ${path.relative(repoRoot, result.jsonPaths.plugin)}`,
|
||||
].join("\n"),
|
||||
);
|
||||
|
||||
@@ -56,6 +56,7 @@ const EXPECTED_REPOSITORY_URL = "https://github.com/openclaw/openclaw";
|
||||
const MAX_CALVER_DISTANCE_DAYS = 2;
|
||||
const REQUIRED_PACKED_PATHS = ["dist/control-ui/index.html"];
|
||||
const CONTROL_UI_ASSET_PREFIX = "dist/control-ui/assets/";
|
||||
const FORBIDDEN_PACKED_PATH_PREFIXES = ["docs/.generated/"] as const;
|
||||
const NPM_PACK_MAX_BUFFER_BYTES = 64 * 1024 * 1024;
|
||||
const skipPackValidationEnv = "OPENCLAW_NPM_RELEASE_SKIP_PACK_CHECK";
|
||||
|
||||
@@ -437,7 +438,21 @@ function collectPackedTarballErrors(): string[] {
|
||||
.filter((path): path is string => typeof path === "string" && path.length > 0),
|
||||
);
|
||||
|
||||
return collectControlUiPackErrors(packedPaths);
|
||||
return [
|
||||
...collectControlUiPackErrors(packedPaths),
|
||||
...collectForbiddenPackedPathErrors(packedPaths),
|
||||
];
|
||||
}
|
||||
|
||||
export function collectForbiddenPackedPathErrors(paths: Iterable<string>): string[] {
|
||||
const errors: string[] = [];
|
||||
for (const packedPath of paths) {
|
||||
if (!FORBIDDEN_PACKED_PATH_PREFIXES.some((prefix) => packedPath.startsWith(prefix))) {
|
||||
continue;
|
||||
}
|
||||
errors.push(`npm package must not include generated docs artifact "${packedPath}".`);
|
||||
}
|
||||
return errors.toSorted((left, right) => left.localeCompare(right));
|
||||
}
|
||||
|
||||
function main(): number {
|
||||
|
||||
@@ -33,7 +33,7 @@ const requiredPathGroups = [
|
||||
"dist/channel-catalog.json",
|
||||
"dist/control-ui/index.html",
|
||||
];
|
||||
const forbiddenPrefixes = ["dist-runtime/", "dist/OpenClaw.app/"];
|
||||
const forbiddenPrefixes = ["dist-runtime/", "dist/OpenClaw.app/", "docs/.generated/"];
|
||||
// 2026.3.12 ballooned to ~213.6 MiB unpacked and correlated with low-memory
|
||||
// startup/doctor OOM reports. Keep enough headroom for the current pack with
|
||||
// restored bundled upgrade surfaces and Control UI assets while still catching
|
||||
|
||||
@@ -4,53 +4,51 @@ import path from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import {
|
||||
type ConfigDocBaseline,
|
||||
renderConfigDocBaselineStatefile,
|
||||
writeConfigDocBaselineStatefile,
|
||||
type ConfigDocBaselineEntry,
|
||||
type ConfigDocBaselineArtifacts,
|
||||
flattenConfigDocBaselineEntries,
|
||||
renderConfigDocBaselineArtifacts,
|
||||
writeConfigDocBaselineArtifacts,
|
||||
} from "./doc-baseline.js";
|
||||
|
||||
describe("config doc baseline integration", () => {
|
||||
const tempRoots: string[] = [];
|
||||
const generatedBaselineJsonPath = path.resolve(
|
||||
process.cwd(),
|
||||
"docs/.generated/config-baseline.json",
|
||||
);
|
||||
const generatedBaselineJsonlPath = path.resolve(
|
||||
process.cwd(),
|
||||
"docs/.generated/config-baseline.jsonl",
|
||||
);
|
||||
const generatedBaselinePaths = {
|
||||
combined: path.resolve(process.cwd(), "docs/.generated/config-baseline.json"),
|
||||
core: path.resolve(process.cwd(), "docs/.generated/config-baseline.core.json"),
|
||||
channel: path.resolve(process.cwd(), "docs/.generated/config-baseline.channel.json"),
|
||||
plugin: path.resolve(process.cwd(), "docs/.generated/config-baseline.plugin.json"),
|
||||
} satisfies Record<keyof ConfigDocBaselineArtifacts, string>;
|
||||
let sharedBaselinePromise: Promise<ConfigDocBaseline> | null = null;
|
||||
let sharedRenderedPromise: Promise<
|
||||
Awaited<ReturnType<typeof renderConfigDocBaselineStatefile>>
|
||||
Awaited<ReturnType<typeof renderConfigDocBaselineArtifacts>>
|
||||
> | null = null;
|
||||
let sharedGeneratedJsonPromise: Promise<string> | null = null;
|
||||
let sharedGeneratedJsonlPromise: Promise<string> | null = null;
|
||||
let sharedByPathPromise: Promise<Map<string, ConfigDocBaseline["entries"][number]>> | null = null;
|
||||
const sharedGeneratedJsonPromises: Partial<
|
||||
Record<keyof ConfigDocBaselineArtifacts, Promise<string>>
|
||||
> = {};
|
||||
let sharedByPathPromise: Promise<Map<string, ConfigDocBaselineEntry>> | null = null;
|
||||
|
||||
function getSharedBaseline() {
|
||||
sharedBaselinePromise ??= fs
|
||||
.readFile(generatedBaselineJsonPath, "utf8")
|
||||
.readFile(generatedBaselinePaths.combined, "utf8")
|
||||
.then((raw) => JSON.parse(raw) as ConfigDocBaseline);
|
||||
return sharedBaselinePromise;
|
||||
}
|
||||
|
||||
function getSharedRendered() {
|
||||
sharedRenderedPromise ??= renderConfigDocBaselineStatefile(getSharedBaseline());
|
||||
sharedRenderedPromise ??= renderConfigDocBaselineArtifacts(getSharedBaseline());
|
||||
return sharedRenderedPromise;
|
||||
}
|
||||
|
||||
function getGeneratedJson() {
|
||||
sharedGeneratedJsonPromise ??= fs.readFile(generatedBaselineJsonPath, "utf8");
|
||||
return sharedGeneratedJsonPromise;
|
||||
}
|
||||
|
||||
function getGeneratedJsonl() {
|
||||
sharedGeneratedJsonlPromise ??= fs.readFile(generatedBaselineJsonlPath, "utf8");
|
||||
return sharedGeneratedJsonlPromise;
|
||||
function getGeneratedJson(kind: keyof ConfigDocBaselineArtifacts) {
|
||||
sharedGeneratedJsonPromises[kind] ??= fs.readFile(generatedBaselinePaths[kind], "utf8");
|
||||
return sharedGeneratedJsonPromises[kind];
|
||||
}
|
||||
|
||||
function getSharedByPath() {
|
||||
sharedByPathPromise ??= getSharedBaseline().then(
|
||||
(baseline) => new Map(baseline.entries.map((entry) => [entry.path, entry])),
|
||||
(baseline) =>
|
||||
new Map(flattenConfigDocBaselineEntries(baseline).map((entry) => [entry.path, entry])),
|
||||
);
|
||||
return sharedByPathPromise;
|
||||
}
|
||||
@@ -65,22 +63,29 @@ describe("config doc baseline integration", () => {
|
||||
|
||||
it("is deterministic across repeated runs", async () => {
|
||||
const baseline = await getSharedBaseline();
|
||||
const first = await renderConfigDocBaselineStatefile(baseline);
|
||||
const second = await renderConfigDocBaselineStatefile(baseline);
|
||||
const first = await renderConfigDocBaselineArtifacts(baseline);
|
||||
const second = await renderConfigDocBaselineArtifacts(baseline);
|
||||
|
||||
expect(second.json).toBe(first.json);
|
||||
expect(second.jsonl).toBe(first.jsonl);
|
||||
expect(second.json.combined).toBe(first.json.combined);
|
||||
expect(second.json.core).toBe(first.json.core);
|
||||
expect(second.json.channel).toBe(first.json.channel);
|
||||
expect(second.json.plugin).toBe(first.json.plugin);
|
||||
});
|
||||
|
||||
it("matches the checked-in generated baseline artifacts", async () => {
|
||||
const [rendered, generatedJson, generatedJsonl] = await Promise.all([
|
||||
getSharedRendered(),
|
||||
getGeneratedJson(),
|
||||
getGeneratedJsonl(),
|
||||
]);
|
||||
const [rendered, generatedCombined, generatedCore, generatedChannel, generatedPlugin] =
|
||||
await Promise.all([
|
||||
getSharedRendered(),
|
||||
getGeneratedJson("combined"),
|
||||
getGeneratedJson("core"),
|
||||
getGeneratedJson("channel"),
|
||||
getGeneratedJson("plugin"),
|
||||
]);
|
||||
|
||||
expect(rendered.json).toBe(generatedJson);
|
||||
expect(rendered.jsonl).toBe(generatedJsonl);
|
||||
expect(rendered.json.combined).toBe(generatedCombined);
|
||||
expect(rendered.json.core).toBe(generatedCore);
|
||||
expect(rendered.json.channel).toBe(generatedChannel);
|
||||
expect(rendered.json.plugin).toBe(generatedPlugin);
|
||||
});
|
||||
|
||||
it("includes core, channel, and plugin config metadata", async () => {
|
||||
@@ -153,18 +158,22 @@ describe("config doc baseline integration", () => {
|
||||
tempRoots.push(tempRoot);
|
||||
const rendered = getSharedRendered();
|
||||
|
||||
const initial = await writeConfigDocBaselineStatefile({
|
||||
const initial = await writeConfigDocBaselineArtifacts({
|
||||
repoRoot: tempRoot,
|
||||
jsonPath: "docs/.generated/config-baseline.json",
|
||||
statefilePath: "docs/.generated/config-baseline.jsonl",
|
||||
combinedPath: "docs/.generated/config-baseline.json",
|
||||
corePath: "docs/.generated/config-baseline.core.json",
|
||||
channelPath: "docs/.generated/config-baseline.channel.json",
|
||||
pluginPath: "docs/.generated/config-baseline.plugin.json",
|
||||
rendered,
|
||||
});
|
||||
expect(initial.wrote).toBe(true);
|
||||
|
||||
const current = await writeConfigDocBaselineStatefile({
|
||||
const current = await writeConfigDocBaselineArtifacts({
|
||||
repoRoot: tempRoot,
|
||||
jsonPath: "docs/.generated/config-baseline.json",
|
||||
statefilePath: "docs/.generated/config-baseline.jsonl",
|
||||
combinedPath: "docs/.generated/config-baseline.json",
|
||||
corePath: "docs/.generated/config-baseline.core.json",
|
||||
channelPath: "docs/.generated/config-baseline.channel.json",
|
||||
pluginPath: "docs/.generated/config-baseline.plugin.json",
|
||||
check: true,
|
||||
rendered,
|
||||
});
|
||||
@@ -175,16 +184,13 @@ describe("config doc baseline integration", () => {
|
||||
'{"generatedBy":"broken","entries":[]}\n',
|
||||
"utf8",
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(tempRoot, "docs/.generated/config-baseline.jsonl"),
|
||||
'{"recordType":"meta","generatedBy":"broken","totalPaths":0}\n',
|
||||
"utf8",
|
||||
);
|
||||
|
||||
const stale = await writeConfigDocBaselineStatefile({
|
||||
const stale = await writeConfigDocBaselineArtifacts({
|
||||
repoRoot: tempRoot,
|
||||
jsonPath: "docs/.generated/config-baseline.json",
|
||||
statefilePath: "docs/.generated/config-baseline.jsonl",
|
||||
combinedPath: "docs/.generated/config-baseline.json",
|
||||
corePath: "docs/.generated/config-baseline.core.json",
|
||||
channelPath: "docs/.generated/config-baseline.channel.json",
|
||||
pluginPath: "docs/.generated/config-baseline.plugin.json",
|
||||
check: true,
|
||||
rendered,
|
||||
});
|
||||
|
||||
@@ -50,25 +50,47 @@ export type ConfigDocBaselineEntry = {
|
||||
|
||||
export type ConfigDocBaseline = {
|
||||
generatedBy: "scripts/generate-config-doc-baseline.ts";
|
||||
coreEntries: ConfigDocBaselineEntry[];
|
||||
channelEntries: ConfigDocBaselineEntry[];
|
||||
pluginEntries: ConfigDocBaselineEntry[];
|
||||
};
|
||||
|
||||
export type ConfigDocBaselineKindBaseline = {
|
||||
generatedBy: "scripts/generate-config-doc-baseline.ts";
|
||||
kind: ConfigDocBaselineKind;
|
||||
entries: ConfigDocBaselineEntry[];
|
||||
};
|
||||
|
||||
export type ConfigDocBaselineStatefileRender = {
|
||||
json: string;
|
||||
jsonl: string;
|
||||
baseline: ConfigDocBaseline;
|
||||
export type ConfigDocBaselineArtifacts = {
|
||||
combined: string;
|
||||
core: string;
|
||||
channel: string;
|
||||
plugin: string;
|
||||
};
|
||||
|
||||
export type ConfigDocBaselineStatefileWriteResult = {
|
||||
export type ConfigDocBaselineArtifactsRender = {
|
||||
baseline: ConfigDocBaseline;
|
||||
json: ConfigDocBaselineArtifacts;
|
||||
};
|
||||
|
||||
export type ConfigDocBaselineArtifactPaths = {
|
||||
combined: string;
|
||||
core: string;
|
||||
channel: string;
|
||||
plugin: string;
|
||||
};
|
||||
|
||||
export type ConfigDocBaselineArtifactsWriteResult = {
|
||||
changed: boolean;
|
||||
wrote: boolean;
|
||||
jsonPath: string;
|
||||
statefilePath: string;
|
||||
jsonPaths: ConfigDocBaselineArtifactPaths;
|
||||
};
|
||||
|
||||
const GENERATED_BY = "scripts/generate-config-doc-baseline.ts" as const;
|
||||
const DEFAULT_JSON_OUTPUT = "docs/.generated/config-baseline.json";
|
||||
const DEFAULT_STATEFILE_OUTPUT = "docs/.generated/config-baseline.jsonl";
|
||||
const DEFAULT_COMBINED_OUTPUT = "docs/.generated/config-baseline.json";
|
||||
const DEFAULT_CORE_OUTPUT = "docs/.generated/config-baseline.core.json";
|
||||
const DEFAULT_CHANNEL_OUTPUT = "docs/.generated/config-baseline.channel.json";
|
||||
const DEFAULT_PLUGIN_OUTPUT = "docs/.generated/config-baseline.plugin.json";
|
||||
let cachedConfigDocBaselinePromise: Promise<ConfigDocBaseline> | null = null;
|
||||
const uiHintIndexCache = new WeakMap<
|
||||
ConfigSchemaResponse["uiHints"],
|
||||
@@ -465,6 +487,36 @@ export function dedupeConfigDocBaselineEntries(
|
||||
return [...byPath.values()].toSorted((left, right) => left.path.localeCompare(right.path));
|
||||
}
|
||||
|
||||
export function splitConfigDocBaselineEntries(entries: ConfigDocBaselineEntry[]): {
|
||||
coreEntries: ConfigDocBaselineEntry[];
|
||||
channelEntries: ConfigDocBaselineEntry[];
|
||||
pluginEntries: ConfigDocBaselineEntry[];
|
||||
} {
|
||||
const coreEntries: ConfigDocBaselineEntry[] = [];
|
||||
const channelEntries: ConfigDocBaselineEntry[] = [];
|
||||
const pluginEntries: ConfigDocBaselineEntry[] = [];
|
||||
|
||||
for (const entry of entries) {
|
||||
if (entry.kind === "channel") {
|
||||
channelEntries.push(entry);
|
||||
continue;
|
||||
}
|
||||
if (entry.kind === "plugin") {
|
||||
pluginEntries.push(entry);
|
||||
continue;
|
||||
}
|
||||
coreEntries.push(entry);
|
||||
}
|
||||
|
||||
return { coreEntries, channelEntries, pluginEntries };
|
||||
}
|
||||
|
||||
export function flattenConfigDocBaselineEntries(
|
||||
baseline: ConfigDocBaseline,
|
||||
): ConfigDocBaselineEntry[] {
|
||||
return [...baseline.coreEntries, ...baseline.channelEntries, ...baseline.pluginEntries];
|
||||
}
|
||||
|
||||
export async function buildConfigDocBaseline(): Promise<ConfigDocBaseline> {
|
||||
if (cachedConfigDocBaselinePromise) {
|
||||
return await cachedConfigDocBaselinePromise;
|
||||
@@ -482,13 +534,16 @@ export async function buildConfigDocBaseline(): Promise<ConfigDocBaseline> {
|
||||
const entries = dedupeConfigDocBaselineEntries(
|
||||
collectConfigDocBaselineEntries(schemaRoot, response.uiHints),
|
||||
);
|
||||
const { coreEntries, channelEntries, pluginEntries } = splitConfigDocBaselineEntries(entries);
|
||||
logConfigDocBaselineDebug(
|
||||
`collect baseline entries done count=${entries.length} elapsedMs=${Date.now() - collectStart}`,
|
||||
);
|
||||
logConfigDocBaselineDebug(`build baseline done elapsedMs=${Date.now() - start}`);
|
||||
return {
|
||||
generatedBy: GENERATED_BY,
|
||||
entries,
|
||||
coreEntries,
|
||||
channelEntries,
|
||||
pluginEntries,
|
||||
};
|
||||
})();
|
||||
try {
|
||||
@@ -499,28 +554,33 @@ export async function buildConfigDocBaseline(): Promise<ConfigDocBaseline> {
|
||||
}
|
||||
}
|
||||
|
||||
export async function renderConfigDocBaselineStatefile(
|
||||
baseline?: ConfigDocBaseline | Promise<ConfigDocBaseline>,
|
||||
): Promise<ConfigDocBaselineStatefileRender> {
|
||||
const start = Date.now();
|
||||
logConfigDocBaselineDebug("render statefile start");
|
||||
const resolvedBaseline = baseline ? await baseline : await buildConfigDocBaseline();
|
||||
const json = `${JSON.stringify(resolvedBaseline, null, 2)}\n`;
|
||||
const metadataLine = JSON.stringify({
|
||||
function renderKindBaseline(
|
||||
kind: ConfigDocBaselineKind,
|
||||
entries: ConfigDocBaselineEntry[],
|
||||
): string {
|
||||
const baseline: ConfigDocBaselineKindBaseline = {
|
||||
generatedBy: GENERATED_BY,
|
||||
recordType: "meta",
|
||||
totalPaths: resolvedBaseline.entries.length,
|
||||
});
|
||||
const entryLines = resolvedBaseline.entries.map((entry) =>
|
||||
JSON.stringify({
|
||||
recordType: "path",
|
||||
...entry,
|
||||
}),
|
||||
);
|
||||
logConfigDocBaselineDebug(`render statefile done elapsedMs=${Date.now() - start}`);
|
||||
kind,
|
||||
entries,
|
||||
};
|
||||
return `${JSON.stringify(baseline, null, 2)}\n`;
|
||||
}
|
||||
|
||||
export async function renderConfigDocBaselineArtifacts(
|
||||
baseline?: ConfigDocBaseline | Promise<ConfigDocBaseline>,
|
||||
): Promise<ConfigDocBaselineArtifactsRender> {
|
||||
const start = Date.now();
|
||||
logConfigDocBaselineDebug("render artifacts start");
|
||||
const resolvedBaseline = baseline ? await baseline : await buildConfigDocBaseline();
|
||||
const json: ConfigDocBaselineArtifacts = {
|
||||
combined: `${JSON.stringify(resolvedBaseline, null, 2)}\n`,
|
||||
core: renderKindBaseline("core", resolvedBaseline.coreEntries),
|
||||
channel: renderKindBaseline("channel", resolvedBaseline.channelEntries),
|
||||
plugin: renderKindBaseline("plugin", resolvedBaseline.pluginEntries),
|
||||
};
|
||||
logConfigDocBaselineDebug(`render artifacts done elapsedMs=${Date.now() - start}`);
|
||||
return {
|
||||
json,
|
||||
jsonl: `${[metadataLine, ...entryLines].join("\n")}\n`,
|
||||
baseline: resolvedBaseline,
|
||||
};
|
||||
}
|
||||
@@ -543,49 +603,72 @@ async function writeIfChanged(filePath: string, next: string): Promise<boolean>
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function writeConfigDocBaselineStatefile(params?: {
|
||||
function resolveBaselineArtifactPaths(
|
||||
repoRoot: string,
|
||||
params?: {
|
||||
combinedPath?: string;
|
||||
corePath?: string;
|
||||
channelPath?: string;
|
||||
pluginPath?: string;
|
||||
},
|
||||
): ConfigDocBaselineArtifactPaths {
|
||||
return {
|
||||
combined: path.resolve(repoRoot, params?.combinedPath ?? DEFAULT_COMBINED_OUTPUT),
|
||||
core: path.resolve(repoRoot, params?.corePath ?? DEFAULT_CORE_OUTPUT),
|
||||
channel: path.resolve(repoRoot, params?.channelPath ?? DEFAULT_CHANNEL_OUTPUT),
|
||||
plugin: path.resolve(repoRoot, params?.pluginPath ?? DEFAULT_PLUGIN_OUTPUT),
|
||||
};
|
||||
}
|
||||
|
||||
export async function writeConfigDocBaselineArtifacts(params?: {
|
||||
repoRoot?: string;
|
||||
check?: boolean;
|
||||
jsonPath?: string;
|
||||
statefilePath?: string;
|
||||
rendered?: ConfigDocBaselineStatefileRender | Promise<ConfigDocBaselineStatefileRender>;
|
||||
}): Promise<ConfigDocBaselineStatefileWriteResult> {
|
||||
combinedPath?: string;
|
||||
corePath?: string;
|
||||
channelPath?: string;
|
||||
pluginPath?: string;
|
||||
rendered?: ConfigDocBaselineArtifactsRender | Promise<ConfigDocBaselineArtifactsRender>;
|
||||
}): Promise<ConfigDocBaselineArtifactsWriteResult> {
|
||||
const start = Date.now();
|
||||
logConfigDocBaselineDebug("write statefile start");
|
||||
logConfigDocBaselineDebug("write artifacts start");
|
||||
const repoRoot = params?.repoRoot ?? resolveRepoRoot();
|
||||
const jsonPath = path.resolve(repoRoot, params?.jsonPath ?? DEFAULT_JSON_OUTPUT);
|
||||
const statefilePath = path.resolve(repoRoot, params?.statefilePath ?? DEFAULT_STATEFILE_OUTPUT);
|
||||
const jsonPaths = resolveBaselineArtifactPaths(repoRoot, params);
|
||||
const rendered = params?.rendered
|
||||
? await params.rendered
|
||||
: await renderConfigDocBaselineStatefile();
|
||||
logConfigDocBaselineDebug(`render statefile done elapsedMs=${Date.now() - start}`);
|
||||
logConfigDocBaselineDebug(`read current json start ${jsonPath}`);
|
||||
const currentJson = await readIfExists(jsonPath);
|
||||
logConfigDocBaselineDebug(`read current json done elapsedMs=${Date.now() - start}`);
|
||||
logConfigDocBaselineDebug(`read current statefile start ${statefilePath}`);
|
||||
const currentStatefile = await readIfExists(statefilePath);
|
||||
logConfigDocBaselineDebug(`read current statefile done elapsedMs=${Date.now() - start}`);
|
||||
const changed = currentJson !== rendered.json || currentStatefile !== rendered.jsonl;
|
||||
: await renderConfigDocBaselineArtifacts();
|
||||
logConfigDocBaselineDebug(`render artifacts done elapsedMs=${Date.now() - start}`);
|
||||
|
||||
const current = await Promise.all(
|
||||
Object.entries(jsonPaths).map(async ([key, filePath]) => [key, await readIfExists(filePath)]),
|
||||
);
|
||||
const currentByKey = Object.fromEntries(current) as Record<
|
||||
keyof ConfigDocBaselineArtifacts,
|
||||
string | null
|
||||
>;
|
||||
const changed = (Object.keys(jsonPaths) as Array<keyof ConfigDocBaselineArtifacts>).some(
|
||||
(key) => currentByKey[key] !== rendered.json[key],
|
||||
);
|
||||
logConfigDocBaselineDebug(
|
||||
`compare statefile done changed=${changed} elapsedMs=${Date.now() - start}`,
|
||||
`compare artifacts done changed=${changed} elapsedMs=${Date.now() - start}`,
|
||||
);
|
||||
|
||||
if (params?.check) {
|
||||
return {
|
||||
changed,
|
||||
wrote: false,
|
||||
jsonPath,
|
||||
statefilePath,
|
||||
jsonPaths,
|
||||
};
|
||||
}
|
||||
|
||||
const wroteJson = await writeIfChanged(jsonPath, rendered.json);
|
||||
const wroteStatefile = await writeIfChanged(statefilePath, rendered.jsonl);
|
||||
const wroteResults = await Promise.all(
|
||||
(Object.keys(jsonPaths) as Array<keyof ConfigDocBaselineArtifacts>).map((key) =>
|
||||
writeIfChanged(jsonPaths[key], rendered.json[key]),
|
||||
),
|
||||
);
|
||||
return {
|
||||
changed,
|
||||
wrote: wroteJson || wroteStatefile,
|
||||
jsonPath,
|
||||
statefilePath,
|
||||
wrote: wroteResults.some(Boolean),
|
||||
jsonPaths,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,11 @@ import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { normalizeConfigDocBaselineHelpPath } from "./doc-baseline.js";
|
||||
import {
|
||||
type ConfigDocBaseline,
|
||||
flattenConfigDocBaselineEntries,
|
||||
normalizeConfigDocBaselineHelpPath,
|
||||
} from "./doc-baseline.js";
|
||||
import { FIELD_HELP } from "./schema.help.js";
|
||||
import {
|
||||
describeTalkSilenceTimeoutDefaults,
|
||||
@@ -18,14 +22,11 @@ function readRepoFile(relativePath: string): string {
|
||||
describe("talk silence timeout defaults", () => {
|
||||
it("keeps help text and docs aligned with the policy", () => {
|
||||
const defaultsDescription = describeTalkSilenceTimeoutDefaults();
|
||||
const baselineLines = readRepoFile("docs/.generated/config-baseline.jsonl")
|
||||
.trim()
|
||||
.split("\n")
|
||||
.map((line) => JSON.parse(line) as { recordType: string; path?: string; help?: string });
|
||||
const talkEntry = baselineLines.find(
|
||||
(entry) =>
|
||||
entry.recordType === "path" &&
|
||||
entry.path === normalizeConfigDocBaselineHelpPath("talk.silenceTimeoutMs"),
|
||||
const baseline = JSON.parse(
|
||||
readRepoFile("docs/.generated/config-baseline.json"),
|
||||
) as ConfigDocBaseline;
|
||||
const talkEntry = flattenConfigDocBaselineEntries(baseline).find(
|
||||
(entry) => entry.path === normalizeConfigDocBaselineHelpPath("talk.silenceTimeoutMs"),
|
||||
);
|
||||
|
||||
expect(FIELD_HELP["talk.silenceTimeoutMs"]).toContain(defaultsDescription);
|
||||
|
||||
@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
compareReleaseVersions,
|
||||
collectControlUiPackErrors,
|
||||
collectForbiddenPackedPathErrors,
|
||||
collectReleasePackageMetadataErrors,
|
||||
collectReleaseTagErrors,
|
||||
parseNpmPackJsonOutput,
|
||||
@@ -294,6 +295,21 @@ describe("collectControlUiPackErrors", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("collectForbiddenPackedPathErrors", () => {
|
||||
it("rejects generated docs artifacts in npm pack output", () => {
|
||||
expect(
|
||||
collectForbiddenPackedPathErrors([
|
||||
"dist/index.js",
|
||||
"docs/.generated/config-baseline.json",
|
||||
"docs/.generated/config-baseline.plugin.json",
|
||||
]),
|
||||
).toEqual([
|
||||
'npm package must not include generated docs artifact "docs/.generated/config-baseline.json".',
|
||||
'npm package must not include generated docs artifact "docs/.generated/config-baseline.plugin.json".',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("collectReleaseTagErrors", () => {
|
||||
it("accepts versions within the two-day CalVer window", () => {
|
||||
expect(
|
||||
|
||||
@@ -243,6 +243,19 @@ describe("collectForbiddenPackPaths", () => {
|
||||
]),
|
||||
).toEqual([bundledPluginFile("tlon", "node_modules/.bin/tlon"), "node_modules/.bin/openclaw"]);
|
||||
});
|
||||
|
||||
it("blocks generated docs artifacts from npm pack output", () => {
|
||||
expect(
|
||||
collectForbiddenPackPaths([
|
||||
"dist/index.js",
|
||||
"docs/.generated/config-baseline.json",
|
||||
"docs/.generated/config-baseline.core.json",
|
||||
]),
|
||||
).toEqual([
|
||||
"docs/.generated/config-baseline.core.json",
|
||||
"docs/.generated/config-baseline.json",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("collectMissingPackPaths", () => {
|
||||
|
||||
@@ -36,9 +36,11 @@ describe("scripts/check-file-utils collectFilesSync", () => {
|
||||
const rootDir = makeTempDir();
|
||||
fs.mkdirSync(path.join(rootDir, "src", "nested"), { recursive: true });
|
||||
fs.mkdirSync(path.join(rootDir, "dist"), { recursive: true });
|
||||
fs.mkdirSync(path.join(rootDir, "docs", ".generated"), { recursive: true });
|
||||
fs.writeFileSync(path.join(rootDir, "src", "keep.ts"), "");
|
||||
fs.writeFileSync(path.join(rootDir, "src", "nested", "keep.test.ts"), "");
|
||||
fs.writeFileSync(path.join(rootDir, "dist", "skip.ts"), "");
|
||||
fs.writeFileSync(path.join(rootDir, "docs", ".generated", "skip.ts"), "");
|
||||
|
||||
const files = collectFilesSync(rootDir, {
|
||||
includeFile: (filePath) => filePath.endsWith(".ts"),
|
||||
|
||||
Reference in New Issue
Block a user