fix(build): package preparation fails on unrelated test types (#157318)

* fix(build): bound standalone package declaration graphs

Co-authored-by: steipete <58493+steipete@users.noreply.github.com>

* fix(build): migrate declaration producer audit and fixture consumers

Derive public audit roots from the existing canonical export mapper and supply the editor augmentation graph to maintained lint fixtures. Keep producer entries, public exports, diagnostics and runtime behavior unchanged.

Co-authored-by: steipete <58493+steipete@users.noreply.github.com>

---------

Co-authored-by: steipete <58493+steipete@users.noreply.github.com>
This commit is contained in:
RoboClaw 2026-09-24 11:25:18 -07:00 • committed by GitHub
parent 67384b481a
commit 5ae011b735
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
27 changed files with 503 additions and 193 deletions

View file

@ -6,6 +6,7 @@ import path from "node:path";
import { collectPluginSourceEntries } from "../scripts/lib/bundled-plugin-build-entries.mjs";
import { createManagedHandoffBuildConfig } from "../scripts/lib/managed-handoff-build-config.mts";
import { runtimeProcessBuildEntries } from "../scripts/lib/runtime-process-build-entries.mts";
import { buildPackageDistEntriesFromExports } from "../scripts/lib/workspace-package-entries.mts";
import { controlUiSource } from "../src/plugins/package-manifest.js";
const BUNDLED_PLUGIN_ROOT_DIR = "extensions";
@ -538,6 +539,20 @@ const rootToolingAndWorkspaceDependencies = [
"string-width",
] as const;
function workspacePackage(packageDir: string, extraEntries: readonly string[] = []) {
const workspace = path.join("packages", packageDir);
return {
// Package exports, not shell arguments, own these public source entrypoints.
entry: [
...Object.values(buildPackageDistEntriesFromExports(packageDir)).map(
(source) => path.relative(workspace, source).replaceAll("\\", "/") + "!",
),
...extraEntries,
],
project: ["src/**/*.ts!"],
} as const;
}
function bundledPluginWorkspace(extraEntries: readonly string[] = []) {
return {
entry: [...bundledPluginEntries, ...extraEntries],
@ -743,10 +758,7 @@ const config = {
],
project: ["src/**/*.ts!"],
},
"packages/sdk": {
entry: ["src/index.ts!"],
project: ["src/**/*.ts!"],
},
"packages/sdk": workspacePackage("sdk"),
"packages/agent-core": {
entry: [
"src/index.ts!",
@ -763,132 +775,21 @@ const config = {
],
project: ["src/**/*.ts!"],
},
"packages/gateway-client": {
// Mirror package.json exports; these subpaths are published surfaces.
entry: ["src/index.ts!", "src/readiness.ts!", "src/timeouts.ts!"],
project: ["src/**/*.ts!"],
},
"packages/gateway-protocol": {
// Mirror package.json exports; these subpaths are published surfaces.
entry: [
"src/index.ts!",
"src/client-info.ts!",
"src/connect-error-details.ts!",
"src/frame-guards.ts!",
"src/schema.ts!",
"src/startup-unavailable.ts!",
"src/version.ts!",
],
project: ["src/**/*.ts!"],
},
"packages/model-catalog-core": {
// Mirror the published export map so package-owned runtime dependencies
// are traced from the TypeScript sources instead of the JS fallback.
entry: [
"src/index.ts!",
"src/configured-model-refs.ts!",
"src/model-catalog-normalize.ts!",
"src/model-catalog-refs.ts!",
"src/model-catalog-types.ts!",
"src/provider-id.ts!",
"src/provider-model-id-normalization.ts!",
"src/provider-model-id-normalize.ts!",
],
project: ["src/**/*.ts!"],
},
"packages/normalization-core": {
// Mirror package.json exports; root and UI builds consume these source subpaths directly.
entry: [
"src/index.ts!",
"src/agent-id.ts!",
"src/boolean-coercion.ts!",
"src/browser-error-runtime.ts!",
"src/error-coercion.ts!",
"src/expect.ts!",
"src/json-coercion.ts!",
"src/number-coercion.ts!",
"src/phone-presentation.ts!",
"src/record-coerce.ts!",
"src/result.ts!",
"src/string-coerce.ts!",
"src/string-normalization.ts!",
"src/utf16-slice.ts!",
],
project: ["src/**/*.ts!"],
},
"packages/net-policy": {
entry: ["src/index.ts!", "src/ip.ts!"],
project: ["src/**/*.ts!"],
},
"packages/markdown-core": {
entry: [
"src/index.ts!",
"src/code-spans.ts!",
"src/fences.ts!",
"src/frontmatter.ts!",
"src/ir.ts!",
"src/render.ts!",
"src/render-aware-chunking.ts!",
"src/tables.ts!",
"src/types.ts!",
],
project: ["src/**/*.ts!"],
},
"packages/media-core": {
entry: [
"src/index.ts!",
"src/attachment-classify.ts!",
"src/base64.ts!",
"src/constants.ts!",
"src/content-length.ts!",
"src/file-name.ts!",
"src/inbound-path-policy.ts!",
"src/inline-image-data-url.ts!",
"src/media-source-url.ts!",
"src/mime.ts!",
"src/read-byte-stream-with-limit.ts!",
],
project: ["src/**/*.ts!"],
},
"packages/acp-core": {
entry: [
"src/index.ts!",
"src/meta.ts!",
"src/session.ts!",
"src/session-interaction-mode.ts!",
"src/session-lineage-meta.ts!",
"src/types.ts!",
"src/runtime/error-text.ts!",
"src/runtime/errors.ts!",
"src/runtime/session-identifiers.ts!",
"src/runtime/session-identity.ts!",
"src/runtime/types.ts!",
],
project: ["src/**/*.ts!"],
},
"packages/terminal-core": {
entry: [
"src/index.ts!",
"src/ansi.ts!",
"src/decorative-emoji.ts!",
"src/health-style.ts!",
"src/links.ts!",
"src/note.ts!",
"src/osc-progress.ts!",
"src/palette.ts!",
"src/progress-line.ts!",
"src/prompt-select-styled.ts!",
"src/prompt-select-styled-params.ts!",
"src/prompt-style.ts!",
"src/restore.ts!",
"src/safe-text.ts!",
"src/stream-writer.ts!",
"src/table.ts!",
"src/terminal-link.ts!",
"src/theme.ts!",
],
project: ["src/**/*.ts!"],
},
"packages/gateway-client": workspacePackage("gateway-client"),
"packages/gateway-protocol": workspacePackage("gateway-protocol"),
"packages/model-catalog-core": workspacePackage("model-catalog-core"),
"packages/normalization-core": workspacePackage("normalization-core", [
// extensions/qa-lab/web/vite.config.ts aliases error-runtime to this private browser implementation.
"src/browser-error-runtime.ts!",
]),
"packages/net-policy": workspacePackage("net-policy"),
"packages/markdown-core": workspacePackage("markdown-core"),
"packages/media-core": workspacePackage("media-core"),
"packages/acp-core": workspacePackage("acp-core"),
"packages/terminal-core": workspacePackage("terminal-core"),
"packages/retry": workspacePackage("retry"),
"packages/media-generation-core": workspacePackage("media-generation-core"),
"packages/media-understanding-common": workspacePackage("media-understanding-common"),
"packages/memory-host-sdk": {
entry: ["src/*.ts!", "src/host/embeddings.types.ts!"],
project: ["src/**/*.ts!"],

View file

@ -1,5 +1,7 @@
{
"extends": "../tsconfig.json",
// This editor/lint graph includes tests; retain their compatibility augmentation.
"files": ["../src/config/sessions/session-entry.test-compat.d.ts"],
"include": [
"../src/**/*.d.ts",
"../ui/**/*.d.ts",

View file

@ -69,6 +69,6 @@
"@openclaw/normalization-core": "workspace:*"
},
"scripts": {
"build": "tsdown src/index.ts src/error-format.ts src/meta.ts src/session.ts src/session-interaction-mode.ts src/session-lineage-meta.ts src/types.ts src/runtime/error-text.ts src/runtime/errors.ts src/runtime/session-identifiers.ts src/runtime/session-identity.ts src/runtime/types.ts --no-config --platform node --format esm --dts --out-dir dist --clean"
"build": "node --import ../../scripts/tsx.mjs ../../scripts/build-workspace-package.mts acp-core"
}
}

View file

@ -62,7 +62,7 @@
}
},
"scripts": {
"build": "tsdown src/index.ts src/browser.ts src/readiness.ts src/scope-upgrade.ts src/timeouts.ts src/websocket-data.ts --no-config --platform node --format esm --dts --out-dir dist --clean"
"build": "node --import ../../scripts/tsx.mjs ../../scripts/build-workspace-package.mts gateway-client"
},
"dependencies": {
"@openclaw/gateway-protocol": "workspace:*",

View file

@ -89,7 +89,7 @@
}
},
"scripts": {
"build": "tsdown src/index.ts src/client-info.ts src/connect-error-details.ts src/frame-guards.ts src/gateway-error-details.ts src/restart-unavailable.ts src/schema.ts src/startup-unavailable.ts src/version.ts src/system-agent-context.ts src/theme.ts --no-config --platform node --format esm --dts --out-dir dist --clean && node --import tsx ../../scripts/check-protocol-registry-types.mts",
"build": "node --import ../../scripts/tsx.mjs ../../scripts/build-workspace-package.mts gateway-protocol && node --import tsx ../../scripts/check-protocol-registry-types.mts",
"prepack": "pnpm run build && node --import tsx ../../scripts/protocol-gen.ts --out ./protocol.schema.json"
},
"dependencies": {

View file

@ -70,6 +70,6 @@
"file-type": "22.1.0"
},
"scripts": {
"build": "tsdown src/index.ts src/attachment-classify.ts src/base64.ts src/constants.ts src/content-length.ts src/file-name.ts src/inbound-path-policy.ts src/inline-image-data-url.ts src/media-source-url.ts src/mime.ts src/read-byte-stream-with-limit.ts --no-config --platform node --format esm --dts --out-dir dist --clean"
"build": "node --import ../../scripts/tsx.mjs ../../scripts/build-workspace-package.mts media-core"
}
}

View file

@ -39,6 +39,6 @@
}
},
"scripts": {
"build": "tsdown src/index.ts src/capability-model-ref.ts src/catalog.ts src/model-ref.ts src/normalization.ts --no-config --platform node --format esm --dts --out-dir dist --clean"
"build": "node --import ../../scripts/tsx.mjs ../../scripts/build-workspace-package.mts media-generation-core"
}
}

View file

@ -59,6 +59,6 @@
}
},
"scripts": {
"build": "tsdown src/active-model.ts src/defaults.ts src/errors.ts src/format.ts src/openai-compatible-video.ts src/output-extract.ts src/provider-id.ts src/provider-supports.ts src/types.ts src/video.ts --no-config --platform node --format esm --dts --out-dir dist --clean"
"build": "node --import ../../scripts/tsx.mjs ../../scripts/build-workspace-package.mts media-understanding-common"
}
}

View file

@ -41,7 +41,7 @@
}
},
"scripts": {
"build": "tsdown src/index.ts src/ip.ts src/ipv4.ts src/redact-sensitive-url.ts src/url-protocol.ts src/url-userinfo.ts --no-config --platform node --format esm --dts --out-dir dist --clean"
"build": "node --import ../../scripts/tsx.mjs ../../scripts/build-workspace-package.mts net-policy"
},
"dependencies": {
"@openclaw/normalization-core": "workspace:*",

View file

@ -136,7 +136,7 @@
}
},
"scripts": {
"build": "tsdown src/index.ts src/agent-id.ts src/agent-run-terminal-outcome.ts src/boolean-coercion.ts src/cjk-chars.ts src/code-points.ts src/error-coercion.ts src/expect.ts src/grapheme.ts src/home-dir.ts src/json-coercion.ts src/json-schema.ts src/markdown-plain-text.ts src/mountinfo-path.ts src/node-crypto.ts src/number-coercion.ts src/phone-presentation.ts src/promise-like.ts src/record-coerce.ts src/result.ts src/stable-node-path.ts src/stable-stringify.ts src/string-coerce.ts src/string-normalization.ts src/utf16-slice.ts --no-config --platform node --format esm --dts --out-dir dist --clean"
"build": "node --import ../../scripts/tsx.mjs ../../scripts/build-workspace-package.mts normalization-core"
},
"dependencies": {
"libphonenumber-js": "1.13.13",

View file

@ -1,6 +1,7 @@
import fs from "node:fs";
import { fileURLToPath } from "node:url";
import { describe, expect, it } from "vitest";
import { buildPackageDistEntriesFromExports } from "../../../scripts/lib/workspace-package-entries.mts";
type PackageManifest = {
exports: Record<
@ -19,6 +20,11 @@ const manifest = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")) as Package
describe("normalization-core package exports", () => {
it("builds every focused export from its matching source entry", () => {
const entries = buildPackageDistEntriesFromExports("normalization-core");
expect(manifest.scripts.build.split(/\s+/u).slice(-2)).toEqual([
"../../scripts/build-workspace-package.mts",
"normalization-core",
]);
for (const [subpath, target] of Object.entries(manifest.exports)) {
const entryName = subpath === "." ? "index" : subpath.slice(2);
expect(target).toEqual({
@ -26,7 +32,9 @@ describe("normalization-core package exports", () => {
import: `./dist/${entryName}.mjs`,
default: `./dist/${entryName}.mjs`,
});
expect(manifest.scripts.build.split(/\s+/u)).toContain(`src/${entryName}.ts`);
const source = `packages/normalization-core/src/${entryName}.ts`;
expect(entries[entryName]).toBe(source);
expect(fs.existsSync(source)).toBe(true);
}
});
});

View file

@ -16,6 +16,6 @@
}
},
"scripts": {
"build": "tsdown src/index.ts --no-config --platform node --format esm --dts --out-dir dist --clean"
"build": "node --import ../../scripts/tsx.mjs ../../scripts/build-workspace-package.mts retry"
}
}

View file

@ -16,7 +16,7 @@
}
},
"scripts": {
"build": "tsdown src/index.ts --no-config --platform node --format esm --dts --out-dir dist --clean"
"build": "node --import ../../scripts/tsx.mjs ../../scripts/build-workspace-package.mts sdk"
},
"dependencies": {
"@openclaw/gateway-client": "workspace:*",

View file

@ -0,0 +1,109 @@
// Standalone package builds share the root build's entry and declaration ownership.
import fs from "node:fs";
import path from "node:path";
import type { InlineConfig, PackageJsonWithPath, UserConfig } from "tsdown";
import { isDirectRunUrl } from "./lib/direct-run.mjs";
import { withDistArtifactOwnership } from "./lib/dist-artifact-ownership.mts";
import { resolveRepoRoot } from "./lib/repo-root.mjs";
import { tsdownPackageOutputRoot } from "./lib/tsdown-output-roots.mts";
import { cleanTsdownOutputRoots, sanitizeTsdownBuildOutputRoots } from "./tsdown-build.mts";
export function selectWorkspacePackageBuildConfig(
configs: UserConfig[],
packageName: string,
target: string | false,
): InlineConfig {
const outDir = tsdownPackageOutputRoot(packageName);
const selected = configs.filter((config) => config.outDir === outDir);
if (selected.length !== 1) {
throw new Error(`Expected one build config for ${packageName}; found ${selected.length}`);
}
return {
...selected[0],
config: false,
clean: false,
dts: true,
platform: "node",
format: "esm",
target,
concurrency: 1,
// Application folding and dependency policy do not belong to standalone libraries.
env: {},
deps: {},
inputOptions: undefined,
};
}
export async function buildWorkspacePackage(packageName: string) {
const root = resolveRepoRoot(import.meta.url);
const output = tsdownPackageOutputRoot(packageName);
const packageJsonPath = path.join(root, "packages", packageName, "package.json");
const manifest: PackageJsonWithPath = {
...JSON.parse(fs.readFileSync(packageJsonPath, "utf8")),
packageJsonPath,
};
const { minVersion } = await import("semver");
const nodeEngine = manifest.engines?.node;
const nodeVersion = nodeEngine ? minVersion(nodeEngine) : null;
if (nodeEngine && !nodeVersion) {
throw new Error(`Invalid Node engine for ${packageName}`);
}
const previousCwd = process.cwd();
process.chdir(root);
try {
// The canonical config reads manifests relative to the repository root.
const { default: configs } = await import("../tsdown.config.ts");
const config = selectWorkspacePackageBuildConfig(
configs,
packageName,
// No engine means no transformation, as in the package-local tsdown CLI.
nodeVersion ? `node${nodeVersion.version}` : false,
);
const canonicalHooks = config.hooks;
config.hooks = async (hooks) => {
if (typeof canonicalHooks === "function") {
await canonicalHooks(hooks);
} else if (canonicalHooks) {
hooks.addHooks(canonicalHooks);
}
// tsdown indexes package completion before this hook, but creates dependency
// plugins afterward. Preserve its coordinator identity while supplying the
// standalone library's metadata to its native dependency-policy owner.
hooks.hook("build:prepare", ({ options }) => {
if (!options.pkg) {
throw new Error("Missing build package metadata");
}
options.pkg.dependencies = manifest.dependencies;
options.pkg.peerDependencies = manifest.peerDependencies;
options.pkg.peerDependenciesMeta = manifest.peerDependenciesMeta;
options.pkg.optionalDependencies = manifest.optionalDependencies;
});
};
await withDistArtifactOwnership(root, async () => {
cleanTsdownOutputRoots({
cwd: root,
roots: [output],
env: { ...process.env, OPENCLAW_RUN_NODE_SKIP_DTS_BUILD: "0" },
});
const { build } = await import("tsdown");
const { bundles } = await build({ ...config, cwd: root });
try {
sanitizeTsdownBuildOutputRoots(["--out-dir", output], root);
} finally {
for (const bundle of bundles) {
await bundle[Symbol.asyncDispose]();
}
}
});
} finally {
process.chdir(previousCwd);
}
}
if (isDirectRunUrl(process.argv[1], import.meta.url)) {
const [packageName, ...extra] = process.argv.slice(2);
if (!packageName || extra.length) {
throw new Error("Expected one workspace package name");
}
await buildWorkspacePackage(packageName);
}

View file

@ -165,7 +165,8 @@ export function hasUnjoinedWork(value: unknown): boolean {
if ("processTreeState" in current && current.processTreeState !== "terminated") {
return true;
}
if (current instanceof AggregateError) {
// Rolldown preserves plugin failures in a plain Error.errors array.
if ("errors" in current && Array.isArray(current.errors)) {
for (const error of current.errors) {
pending.push(error);
}

View file

@ -13,6 +13,7 @@ const TSDOWN_PACKAGE_NAMES = [
"net-policy",
"normalization-core",
"retry",
"sdk",
"terminal-core",
"acp-core",
] as const;

View file

@ -0,0 +1,30 @@
import fs from "node:fs";
import path from "node:path";
/** Canonical source entries for workspace packages with dist ESM exports. */
export function buildPackageDistEntriesFromExports(packageDir: string): Record<string, string> {
const packageJsonPath = path.join("packages", packageDir, "package.json");
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")) as {
exports?: Record<string, unknown>;
};
const entries: Record<string, string> = {};
for (const [exportKey, value] of Object.entries(packageJson.exports ?? {})) {
const entry =
exportKey === "." ? "index" : exportKey.startsWith("./") ? exportKey.slice(2) : "";
if (!entry || entry.includes("..")) {
continue;
}
const importPath =
typeof value === "object" && value !== null && !Array.isArray(value)
? (value as Record<string, unknown>).import
: value;
if (typeof importPath !== "string" || !importPath.startsWith("./dist/")) {
continue;
}
const sourcePath = importPath
.replace(/^\.\/dist\//u, `packages/${packageDir}/src/`)
.replace(/\.mjs$/u, ".ts");
entries[entry] = sourcePath;
}
return Object.fromEntries(Object.entries(entries).toSorted(([a], [b]) => a.localeCompare(b)));
}

View file

@ -1,5 +1,7 @@
{
"extends": "../tsconfig.json",
// This editor/lint graph includes tests; retain their compatibility augmentation.
"files": ["config/sessions/session-entry.test-compat.d.ts"],
"compilerOptions": {
"allowJs": true
},

View file

@ -321,12 +321,56 @@ describe("check-deadcode-exports", () => {
});
it.each([
"packages/agent-core",
"packages/markdown-core",
"packages/media-core",
"packages/acp-core",
"packages/terminal-core",
] as const)("mirrors the published entry map for %s", (workspace) => {
"gateway-client",
"gateway-protocol",
"sdk",
"retry",
"normalization-core",
"net-policy",
"media-understanding-common",
"media-generation-core",
"media-core",
"acp-core",
"markdown-core",
"terminal-core",
"model-catalog-core",
] as const)("derives public audit roots from the %s export map", (packageDir) => {
const workspace = "packages/" + packageDir;
const manifest = JSON.parse(fs.readFileSync(workspace + "/package.json", "utf8")) as {
exports: Record<string, { import: string }>;
};
const expected = Object.values(manifest.exports)
.map(({ import: target }) => target.replace("./dist/", "src/").replace(/\.mjs$/u, ".ts!"))
.toSorted();
if (packageDir === "normalization-core") {
// The QA Lab Vite alias is a distinct source consumer, not a package export.
expect(fs.readFileSync("extensions/qa-lab/web/vite.config.ts", "utf8")).toContain(
"../../../packages/normalization-core/src/browser-error-runtime.ts",
);
expected.push("src/browser-error-runtime.ts!");
expected.sort();
}
const production = Object.entries(knipConfig.workspaces).find(
([name]) => name === workspace,
)?.[1];
const full = allExportsKnipConfig.workspaces[workspace];
expect(production?.entry.toSorted(), workspace).toEqual(expected);
expect(production?.project).toEqual(["src/**/*.ts!"]);
expect(full?.entry).toEqual(expect.arrayContaining(expected));
for (const entry of expected) {
expect(fs.existsSync(workspace + "/" + entry.slice(0, -1)), entry).toBe(true);
}
});
it("does not promote private implementation or build-only artifacts to public audit roots", () => {
expect(knipConfig.workspaces["packages/gateway-client"].entry).not.toContain(
"src/protocol-client.ts!",
);
expect(knipConfig.workspaces["packages/acp-core"].entry).not.toContain("src/error-format.ts!");
});
it("preserves the agent-core relocated source-entry contract", () => {
const workspace = "packages/agent-core";
const packageJson = JSON.parse(
fs.readFileSync(new URL(`../../${workspace}/package.json`, import.meta.url), "utf8"),
) as { exports: Record<string, unknown> };
@ -339,21 +383,18 @@ describe("check-deadcode-exports", () => {
new URL(`../../${workspace}/${entry.slice(0, -"!".length)}`, import.meta.url),
),
);
expect(missingConventionalEntries).toEqual(
workspace === "packages/agent-core"
? ["src/harness/compaction.ts!", "src/harness/branch-summarization.ts!"]
: [],
);
if (workspace === "packages/agent-core") {
for (const sourcePath of [
"src/harness/compaction/compaction.ts",
"src/harness/compaction/branch-summarization.ts",
]) {
expect(
fs.existsSync(new URL(`../../${workspace}/${sourcePath}`, import.meta.url)),
sourcePath,
).toBe(true);
}
expect(missingConventionalEntries).toEqual([
"src/harness/compaction.ts!",
"src/harness/branch-summarization.ts!",
]);
for (const sourcePath of [
"src/harness/compaction/compaction.ts",
"src/harness/compaction/branch-summarization.ts",
]) {
expect(
fs.existsSync(new URL(`../../${workspace}/${sourcePath}`, import.meta.url)),
sourcePath,
).toBe(true);
}
const expected = conventionalEntries
.filter((entry) => !missingConventionalEntries.includes(entry))

View file

@ -573,7 +573,7 @@ describe.skipIf(process.platform === "win32")("dist artifact ownership", () => {
expect(fs.existsSync(path.join(resolveDistArtifactLockPath(root), "unjoined"))).toBe(false);
});
it.for(["cause", "error", "cyclic aggregate"])(
it.for(["cause", "error", "cyclic aggregate", "bundler errors"])(
"retains ownership for unjoined work nested in %s",
async (kind, { signal }) => {
// Retention deliberately keeps lock handles open; a joined child owns
@ -591,6 +591,7 @@ describe.skipIf(process.platform === "win32")("dist artifact ownership", () => {
const aggregate = new AggregateError([], 'sibling cleanup');
aggregate.errors.push(aggregate, new Error('command failed', { cause: uncertainty }));
const error = kind === 'cyclic aggregate' ? aggregate
: kind === 'bundler errors' ? Object.assign(new Error('Build failed'), { errors: [aggregate] })
: new Error('command failed', { cause: kind === 'cause' ? uncertainty : { error: uncertainty } });
const outcome = await withDistArtifactOwnership(process.cwd(), async () => {
throw error;

View file

@ -7,6 +7,7 @@ import { portableRelativePath } from "../../scripts/lib/build-artifact-cache.mts
import { BoundaryInputSnapshot } from "../../scripts/lib/extension-boundary-inputs.mts";
import { createDeclarationInputBoundary } from "../../scripts/lib/local-check-runtime.mts";
import { emitNativeDeclarations } from "../../scripts/lib/native-declaration-emitter.mts";
import { readNativeTypeScriptConfig } from "../../scripts/lib/native-typescript-config.mts";
import { useAutoCleanupTempDirTracker } from "../helpers/temp-dir.js";
import {
installNativeAncestorTypes,
@ -17,6 +18,55 @@ import {
const roots = useAutoCleanupTempDirTracker(afterEach);
it("keeps test-only ambient augmentation out of declaration roots but in test graphs", () => {
const root = fs.realpathSync.native(roots.make("native-declaration-production-roots-"));
// Use the actual production/test selection rules with a tiny semantic-free fixture.
const testConfigs = [
"test/tsconfig/tsconfig.test.json",
"src/tsconfig.json",
"ui/tsconfig.json",
"extensions/tsconfig.json",
];
// Actual emit owners: tsgo:prod UI/plugin configs are noEmit typecheck graphs.
const productionConfigs = [
"tsconfig.json",
"packages/plugin-sdk/tsconfig.json",
"extensions/browser/tsconfig.json",
];
for (const config of [
...productionConfigs,
...testConfigs,
"extensions/tsconfig.package-boundary.paths.json",
"extensions/tsconfig.package-boundary.base.json",
]) {
writeNativeFixtureFile(root, config, fs.readFileSync(config, "utf8"));
}
const productionAmbient = "src/types/production.d.ts";
const testAmbient = "src/config/sessions/session-entry.test-compat.d.ts";
writeNativeFixtureFile(root, productionAmbient, "declare const productionOrigin: string;");
writeNativeFixtureFile(root, testAmbient, 'import "./runtime.js";');
writeNativeFixtureFile(root, "src/config/sessions/runtime.ts", "export const value = 1;");
for (const file of [
"packages/example/src/index.ts",
"src/plugin-sdk/index.ts",
"extensions/browser/src/index.ts",
]) {
writeNativeFixtureFile(root, file, "export const value = 1;");
}
const configuredRoots = (configFileName: string) =>
readNativeTypeScriptConfig({ cwd: root, configFileName }).fileNames.map((file) =>
path.relative(root, file).replaceAll(path.sep, "/"),
);
const production = configuredRoots("tsconfig.json");
expect(production).toContain(productionAmbient);
for (const config of productionConfigs) {
expect(configuredRoots(config), config).not.toContain(testAmbient);
}
for (const config of testConfigs) {
expect(configuredRoots(config), config).toContain(testAmbient);
}
});
it.each([true, false])(
"diagnoses declaration escapes with an ancestor install=%s",
(ancestorInstall) => {

View file

@ -132,6 +132,33 @@ function readJson(filePath: string): unknown {
return JSON5.parse(fs.readFileSync(filePath, "utf8"));
}
function writeSessionCompatibilityFixture(root: string) {
const directory = path.join(root, "src/config/sessions");
fs.mkdirSync(directory, { recursive: true });
// Real editor configs explicitly root this augmentation. Keep its real contents,
// with fixture-owned base modules rather than importing the whole session graph.
fs.copyFileSync(
"src/config/sessions/session-entry.test-compat.d.ts",
path.join(directory, "session-entry.test-compat.d.ts"),
);
for (const [file, interfaces] of [
["types.ts", ["SessionEntry", "InternalSessionEntry"]],
[
"session-accessor.types.ts",
[
"SessionTranscriptRuntimeTarget",
"SessionTranscriptTurnPersistResult",
"SessionTranscriptReadTarget",
],
],
] as const) {
fs.writeFileSync(
path.join(directory, file),
interfaces.map((name) => "export interface " + name + " { id: string; }").join("\n"),
);
}
}
describe("oxlint config", () => {
it("enforces namespace, evaluation, and unused-binding policies with the installed binary", () => {
const tempRoot = fs.realpathSync(createTempDir("openclaw-oxlint-policy-"));
@ -262,6 +289,7 @@ describe("oxlint config", () => {
fs.copyFileSync(file, target);
}
}
writeSessionCompatibilityFixture(tempRoot);
fs.symlinkSync(path.resolve("node_modules"), path.join(tempRoot, "node_modules"), "junction");
const fixtures = {
"src/imported.ts": "export function work(): Promise<void> { return Promise.resolve(); }",
@ -376,6 +404,7 @@ describe("oxlint config", () => {
fs.copyFileSync(file, target);
}
}
writeSessionCompatibilityFixture(tempRoot);
fs.symlinkSync(path.resolve("node_modules"), path.join(tempRoot, "node_modules"), "junction");
const source = [
'import { work } from "../packages/imported.js";',
@ -464,6 +493,7 @@ describe("oxlint config", () => {
fs.copyFileSync(file, target);
}
}
writeSessionCompatibilityFixture(tempRoot);
fs.symlinkSync(path.resolve("node_modules"), path.join(tempRoot, "node_modules"), "junction");
const supportFiles = [
"src/cli/diagnostics.test-support.ts",

View file

@ -1042,6 +1042,37 @@ console.log("relocated Bash parser works without native grammar package");
},
);
it("gives every standalone declaration caller one canonical package config", () => {
for (const packageName of [
"gateway-client",
"gateway-protocol",
"sdk",
"retry",
"normalization-core",
"net-policy",
"media-understanding-common",
"media-generation-core",
"media-core",
"acp-core",
]) {
const manifest = JSON.parse(
fs.readFileSync(`packages/${packageName}/package.json`, "utf8"),
) as { scripts: { build: string }; exports: Record<string, { import: string }> };
expect(manifest.scripts.build).toContain(`build-workspace-package.mts ${packageName}`);
const selected = configs.filter((config) => config.outDir === `packages/${packageName}/dist`);
expect(selected, packageName).toHaveLength(1);
const sources = Object.values(selected[0]!.entry ?? {});
for (const entry of Object.values(manifest.exports)) {
expect(sources).toContain(
entry.import.replace("./dist/", `packages/${packageName}/src/`).replace(/\.mjs$/u, ".ts"),
);
}
if (packageName === "acp-core") {
expect(sources).toContain("packages/acp-core/src/error-format.ts");
}
}
});
it("isolates runtime output from bounded declaration-only graphs", () => {
const packageConfigs = configs.filter((entry) => entry.name === TSDOWN_PACKAGE_CONFIG_GROUP);
const unifiedRuntimeConfig = configs.find(

View file

@ -88,6 +88,118 @@ function nestedFixture(groups: readonly string[] = TSDOWN_PLUGIN_SDK_DTS_CONFIG_
}
describe("tsdown checkout declaration resolution", () => {
it("bounds standalone package builds while preserving public declarations and sibling outputs", () => {
const { root, write } = containedFixture();
fs.symlinkSync(
fs.realpathSync("node_modules/semver"),
path.join(root, "node_modules/semver"),
"junction",
);
write(
"scripts/build-workspace-package.mts",
fs.readFileSync("scripts/build-workspace-package.mts", "utf8"),
);
const manifest = JSON.parse(
fs.readFileSync("packages/gateway-client/package.json", "utf8"),
) as {
exports: Record<string, { import: string; types: string }>;
dependencies?: Record<string, string>;
};
manifest.dependencies = { ...manifest.dependencies, "standalone-dependency": "1.0.0" };
write("packages/gateway-client/package.json", JSON.stringify(manifest));
write(
"node_modules/standalone-dependency/package.json",
JSON.stringify({
name: "standalone-dependency",
version: "1.0.0",
type: "module",
exports: { "./value": { types: "./value.d.ts", import: "./value.js" } },
}),
);
write(
"node_modules/standalone-dependency/value.d.ts",
"export declare const externalValue: string;",
);
write(
"node_modules/standalone-dependency/value.js",
'export const externalValue = "package-owned";',
);
for (const entry of Object.values(manifest.exports)) {
write(
entry.import.replace("./dist/", "packages/gateway-client/src/").replace(/\.mjs$/u, ".ts"),
'export const marker = "bounded";\n',
);
}
write(
"packages/gateway-client/src/index.ts",
`
export const marker = "bounded";
export { externalValue } from "standalone-dependency/value";
declare const process: { env: { NODE_ENV?: string } };
export function runtimeMode(env: { NODE_ENV?: string }) {
return env.NODE_ENV ?? process.env.NODE_ENV;
}
`,
);
write("packages/sdk/src/index.ts", 'export const sdkMarker = "sdk";\n');
// Upstream whole-project declaration emit rejects this unselected test helper (TS4094).
write(
"src/unrelated.test-support.ts",
"export const hidden = new (class { private value = 1; })();\n",
);
write("packages/gateway-client/dist/obsolete.d.mts", "export declare const retired: 1;");
write("dist/keep.txt", "root output");
write("packages/gateway-protocol/dist/keep.txt", "sibling output");
const result = runFixtureModule(
root,
`
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
import { pathToFileURL } from "node:url";
import { buildWorkspacePackage } from "./scripts/build-workspace-package.mts";
import configs from "./tsdown.config.ts";
const root = process.cwd();
for (const [name, target] of [["sdk", undefined], ["gateway-client", ["node22.19.0"]]]) {
const config = configs.find(config => config.outDir === "packages/" + name + "/dist");
const original = config.hooks;
config.hooks = async hooks => {
await original(hooks);
hooks.hook("build:prepare", ({ options }) => assert.deepEqual(options.target, target));
};
}
await buildWorkspacePackage("sdk");
assert.match(fs.readFileSync("packages/sdk/dist/index.d.mts", "utf8"), /sdkMarker.*"sdk"/);
process.env.OPENCLAW_RUN_NODE_SKIP_DTS_BUILD = "1";
const packageDir = path.join(root, "packages/gateway-client");
process.chdir(packageDir);
await buildWorkspacePackage("gateway-client");
assert.equal(process.cwd(), packageDir);
assert.equal(fs.existsSync("dist/obsolete.d.mts"), false);
fs.writeFileSync(path.join(root, "node_modules/standalone-dependency/value.js"), 'export const externalValue = "installed-update";');
process.env.NODE_ENV = "runtime-mode";
const output = await import(pathToFileURL(path.join(packageDir, "dist/index.mjs")).href);
assert.deepEqual(
{ mode: output.runtimeMode({}), external: output.externalValue },
{ mode: "runtime-mode", external: "installed-update" },
);
const manifest = JSON.parse(fs.readFileSync("package.json", "utf8"));
for (const entry of Object.values(manifest.exports)) {
assert.ok(fs.existsSync(entry.import), entry.import);
assert.match(fs.readFileSync(entry.types, "utf8"), /marker.*"bounded"/);
}
assert.equal(fs.readFileSync(path.join(root, "dist/keep.txt"), "utf8"), "root output");
assert.equal(fs.readFileSync(path.join(root, "packages/gateway-protocol/dist/keep.txt"), "utf8"), "sibling output");
fs.writeFileSync("src/index.ts", 'export const invalid: number = "selected";');
await assert.rejects(buildWorkspacePackage("gateway-client"), /Native declaration emit failed/);
assert.equal(process.cwd(), packageDir);
console.log("standalone package boundary verified");
`,
);
expect(result.status, result.stdout + result.stderr).toBe(0);
expect(result.stdout).toContain("standalone package boundary verified");
});
it.runIf(process.platform === "win32")(
"starts the checkout compiler when its Windows executable uses an extended-length path",
() => {

View file

@ -318,5 +318,5 @@
}
},
"include": ["src/**/*", "ui/**/*", "extensions/**/*", "packages/**/*"],
"exclude": ["node_modules", "dist", "**/dist/**"]
"exclude": ["node_modules", "dist", "**/dist/**", "**/*.test-compat.d.ts"]
}

View file

@ -44,6 +44,7 @@ import {
createWorkerDeployBuildPlugin,
WORKER_DEPLOY_OPTIONAL_NATIVE_MODULE_ID,
} from "./scripts/lib/worker-deploy-build-plugin.mts";
import { buildPackageDistEntriesFromExports } from "./scripts/lib/workspace-package-entries.mts";
type InputOptionsFactory = Extract<NonNullable<UserConfig["inputOptions"]>, Function>;
type InputOptionsArg = InputOptionsFactory extends (
@ -555,33 +556,6 @@ function buildAgentCoreDistEntries(): Record<string, string> {
};
}
function buildPackageDistEntriesFromExports(packageDir: string): Record<string, string> {
const packageJsonPath = path.join("packages", packageDir, "package.json");
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")) as {
exports?: Record<string, unknown>;
};
const entries: Record<string, string> = {};
for (const [exportKey, value] of Object.entries(packageJson.exports ?? {})) {
const entry =
exportKey === "." ? "index" : exportKey.startsWith("./") ? exportKey.slice(2) : "";
if (!entry || entry.includes("..")) {
continue;
}
const importPath =
typeof value === "object" && value !== null && !Array.isArray(value)
? (value as Record<string, unknown>).import
: value;
if (typeof importPath !== "string" || !importPath.startsWith("./dist/")) {
continue;
}
const sourcePath = importPath
.replace(/^\.\/dist\//u, `packages/${packageDir}/src/`)
.replace(/\.mjs$/u, ".ts");
entries[entry] = sourcePath;
}
return Object.fromEntries(Object.entries(entries).toSorted(([a], [b]) => a.localeCompare(b)));
}
function buildLlmCoreDistEntries(): Record<string, string> {
return {
index: "packages/llm-core/src/index.ts",
@ -874,8 +848,23 @@ const configs: UserConfig[] = [
}),
nodeWorkspacePackageBuildConfig("normalization-core"),
nodeWorkspacePackageBuildConfig("retry"),
nodeWorkspacePackageBuildConfig("sdk", {
deps: withExternalPackageSubpaths({
neverBundle: [
"@openclaw/gateway-client",
"@openclaw/gateway-protocol",
"@openclaw/normalization-core",
],
}),
}),
nodeWorkspacePackageBuildConfig("media-core"),
nodeWorkspacePackageBuildConfig("acp-core"),
nodeWorkspacePackageBuildConfig("acp-core", {
entry: {
...buildPackageDistEntriesFromExports("acp-core"),
// Preserve the standalone package build's non-exported redactor artifact.
"error-format": "packages/acp-core/src/error-format.ts",
},
}),
nodeWorkspacePackageBuildConfig("terminal-core", {
deps: {
neverBundle: shouldExternalizeTerminalCoreDependency,

View file

@ -1,5 +1,7 @@
{
"extends": "../tsconfig.json",
// This editor/lint graph includes tests; retain their compatibility augmentation.
"files": ["../src/config/sessions/session-entry.test-compat.d.ts"],
"include": [
"**/*.ts",
"**/*.tsx",