diff --git a/config/knip.config.ts b/config/knip.config.ts index 9b0292d76e4a..4a0a2aaa054e 100644 --- a/config/knip.config.ts +++ b/config/knip.config.ts @@ -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!"], diff --git a/extensions/tsconfig.json b/extensions/tsconfig.json index 04bb70f1b5e0..cfee4533bc83 100644 --- a/extensions/tsconfig.json +++ b/extensions/tsconfig.json @@ -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", diff --git a/packages/acp-core/package.json b/packages/acp-core/package.json index dfdd76559caf..90ec4a0b47b1 100644 --- a/packages/acp-core/package.json +++ b/packages/acp-core/package.json @@ -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" } } diff --git a/packages/gateway-client/package.json b/packages/gateway-client/package.json index 5ead6a72f0f2..5e0b1610fd10 100644 --- a/packages/gateway-client/package.json +++ b/packages/gateway-client/package.json @@ -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:*", diff --git a/packages/gateway-protocol/package.json b/packages/gateway-protocol/package.json index adae137ead88..774b4edf1873 100644 --- a/packages/gateway-protocol/package.json +++ b/packages/gateway-protocol/package.json @@ -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": { diff --git a/packages/media-core/package.json b/packages/media-core/package.json index 395acee3015b..90cbc404edc8 100644 --- a/packages/media-core/package.json +++ b/packages/media-core/package.json @@ -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" } } diff --git a/packages/media-generation-core/package.json b/packages/media-generation-core/package.json index c80fc9f92b10..f3a9ba57abc5 100644 --- a/packages/media-generation-core/package.json +++ b/packages/media-generation-core/package.json @@ -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" } } diff --git a/packages/media-understanding-common/package.json b/packages/media-understanding-common/package.json index e00a0e6743f8..91a43d6e1ff1 100644 --- a/packages/media-understanding-common/package.json +++ b/packages/media-understanding-common/package.json @@ -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" } } diff --git a/packages/net-policy/package.json b/packages/net-policy/package.json index ccea74e21c0a..e9dbeade1e5d 100644 --- a/packages/net-policy/package.json +++ b/packages/net-policy/package.json @@ -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:*", diff --git a/packages/normalization-core/package.json b/packages/normalization-core/package.json index 46d0e26aab87..ff3fee1e5e19 100644 --- a/packages/normalization-core/package.json +++ b/packages/normalization-core/package.json @@ -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", diff --git a/packages/normalization-core/src/package-exports.test.ts b/packages/normalization-core/src/package-exports.test.ts index 02059b71acea..9b4bd9995a30 100644 --- a/packages/normalization-core/src/package-exports.test.ts +++ b/packages/normalization-core/src/package-exports.test.ts @@ -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); } }); }); diff --git a/packages/retry/package.json b/packages/retry/package.json index a184531ea602..1fb4ca6fcf74 100644 --- a/packages/retry/package.json +++ b/packages/retry/package.json @@ -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" } } diff --git a/packages/sdk/package.json b/packages/sdk/package.json index f10cbb3dea5c..75754f627634 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -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:*", diff --git a/scripts/build-workspace-package.mts b/scripts/build-workspace-package.mts new file mode 100644 index 000000000000..e6d14472c829 --- /dev/null +++ b/scripts/build-workspace-package.mts @@ -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); +} diff --git a/scripts/lib/managed-child-process.mts b/scripts/lib/managed-child-process.mts index d2fae0b17782..6dec4115f671 100644 --- a/scripts/lib/managed-child-process.mts +++ b/scripts/lib/managed-child-process.mts @@ -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); } diff --git a/scripts/lib/tsdown-output-roots.mts b/scripts/lib/tsdown-output-roots.mts index 707a04b07386..06a26231f1ba 100644 --- a/scripts/lib/tsdown-output-roots.mts +++ b/scripts/lib/tsdown-output-roots.mts @@ -13,6 +13,7 @@ const TSDOWN_PACKAGE_NAMES = [ "net-policy", "normalization-core", "retry", + "sdk", "terminal-core", "acp-core", ] as const; diff --git a/scripts/lib/workspace-package-entries.mts b/scripts/lib/workspace-package-entries.mts new file mode 100644 index 000000000000..76ff8fd64da2 --- /dev/null +++ b/scripts/lib/workspace-package-entries.mts @@ -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 { + const packageJsonPath = path.join("packages", packageDir, "package.json"); + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")) as { + exports?: Record; + }; + const entries: Record = {}; + 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).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))); +} diff --git a/src/tsconfig.json b/src/tsconfig.json index 56d15b0c4441..360ef9f1986a 100644 --- a/src/tsconfig.json +++ b/src/tsconfig.json @@ -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 }, diff --git a/test/scripts/check-deadcode-exports.test.ts b/test/scripts/check-deadcode-exports.test.ts index 9b59f313d3fe..aed2c13291b9 100644 --- a/test/scripts/check-deadcode-exports.test.ts +++ b/test/scripts/check-deadcode-exports.test.ts @@ -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; + }; + 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 }; @@ -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)) diff --git a/test/scripts/dist-artifact-ownership.test.ts b/test/scripts/dist-artifact-ownership.test.ts index 42e8448f4a4a..7aad253a3630 100644 --- a/test/scripts/dist-artifact-ownership.test.ts +++ b/test/scripts/dist-artifact-ownership.test.ts @@ -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; diff --git a/test/scripts/native-declaration-boundary.test.ts b/test/scripts/native-declaration-boundary.test.ts index a2b2af28b488..483c87b43b4b 100644 --- a/test/scripts/native-declaration-boundary.test.ts +++ b/test/scripts/native-declaration-boundary.test.ts @@ -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) => { diff --git a/test/scripts/oxlint-config.test.ts b/test/scripts/oxlint-config.test.ts index 5b463854f510..089c3ddc8f56 100644 --- a/test/scripts/oxlint-config.test.ts +++ b/test/scripts/oxlint-config.test.ts @@ -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 { 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", diff --git a/test/scripts/tsdown-config.test.ts b/test/scripts/tsdown-config.test.ts index 0a5379a4e70e..ecbf7b064506 100644 --- a/test/scripts/tsdown-config.test.ts +++ b/test/scripts/tsdown-config.test.ts @@ -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 }; + 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( diff --git a/test/scripts/tsdown-declaration-resolution.test.ts b/test/scripts/tsdown-declaration-resolution.test.ts index 1e5261a9a008..7e41720ec8c6 100644 --- a/test/scripts/tsdown-declaration-resolution.test.ts +++ b/test/scripts/tsdown-declaration-resolution.test.ts @@ -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; + dependencies?: Record; + }; + 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", () => { diff --git a/tsconfig.json b/tsconfig.json index 76024fd2916c..af4d0ab4e67d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -318,5 +318,5 @@ } }, "include": ["src/**/*", "ui/**/*", "extensions/**/*", "packages/**/*"], - "exclude": ["node_modules", "dist", "**/dist/**"] + "exclude": ["node_modules", "dist", "**/dist/**", "**/*.test-compat.d.ts"] } diff --git a/tsdown.config.ts b/tsdown.config.ts index b1313c72bd04..23f42eeddcbb 100644 --- a/tsdown.config.ts +++ b/tsdown.config.ts @@ -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, Function>; type InputOptionsArg = InputOptionsFactory extends ( @@ -555,33 +556,6 @@ function buildAgentCoreDistEntries(): Record { }; } -function buildPackageDistEntriesFromExports(packageDir: string): Record { - const packageJsonPath = path.join("packages", packageDir, "package.json"); - const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")) as { - exports?: Record; - }; - const entries: Record = {}; - 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).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 { 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, diff --git a/ui/tsconfig.json b/ui/tsconfig.json index 422aaa5fad9b..fa6e05116bb8 100644 --- a/ui/tsconfig.json +++ b/ui/tsconfig.json @@ -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",