From 0b5671d9563923a0427b1a45ef037569f750ec8d Mon Sep 17 00:00:00 2001 From: Yichen Jiang Date: Fri, 9 Oct 2026 13:46:34 +0800 Subject: [PATCH] fix(build): exclude stale package directories from workspace bundles --- AGENTS.md | 2 +- Makefile | 10 ++++-- docs/development.i18n.yaml | 8 ++--- docs/development.md | 4 +-- docs/development.zh.md | 4 +-- scripts/build-workspace.spec.ts | 56 +++++++++++++++++++++++++++++++++ scripts/build-workspace.ts | 16 ++++++++++ tsdown.config.ts | 5 ++- 8 files changed, 90 insertions(+), 15 deletions(-) create mode 100644 scripts/build-workspace.spec.ts create mode 100644 scripts/build-workspace.ts diff --git a/AGENTS.md b/AGENTS.md index 8c54b1b145..782c6b46e3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -101,7 +101,7 @@ pnpm run website:build # VitePress build (doubles as dead-link check) pnpm dsh --profile headless "task" # run one task from source (needs DEEPSEEK_API_KEY) pnpm run demo:ptc -- "task" # headless PTC mode run (needs key) pnpm run dev:web | dev:desktop # build, then launch; Web also rebuilds client bundles on edits. start:web | start:desktop skip the build -make web|dev-web|desktop|dev-desktop|build # the same commands; ARGS='--no-open' forwards options +make web|dev-web|desktop|dev-desktop|build|clean # the same commands; ARGS='--no-open' forwards options ``` ### Host sandbox failures diff --git a/Makefile b/Makefile index 4b95df6766..11f3de95f2 100644 --- a/Makefile +++ b/Makefile @@ -1,13 +1,14 @@ -# Short names for the Web and Desktop application commands. The package.json -# scripts they call stay the source of truth; docs/development.md documents both. +# Short names for repository builds, cleanup, and Web/Desktop commands. +# package.json owns the scripts; docs/development.md documents these commands. .DEFAULT_GOAL := help -.PHONY: help build web desktop dev-web dev-desktop +.PHONY: help build clean web desktop dev-web dev-desktop PNPM ?= pnpm ARGS ?= help: @echo "make build pnpm run build complete repository build" + @echo "make clean pnpm run clean remove build outputs and safe deleted-package residue" @echo "make web pnpm run start:web serve the built Web artifacts from source" @echo "make desktop pnpm run start:desktop launch the built Desktop artifacts" @echo "make dev-web pnpm run dev:web build, serve, and rebuild Web on source edits" @@ -18,6 +19,9 @@ help: build: $(PNPM) run build +clean: + $(PNPM) run clean + web: $(PNPM) run start:web $(ARGS) diff --git a/docs/development.i18n.yaml b/docs/development.i18n.yaml index 2a08605a85..114e7b8eaa 100644 --- a/docs/development.i18n.yaml +++ b/docs/development.i18n.yaml @@ -21,8 +21,8 @@ en: 3221d1076554001f zh: 29f166be6a5c04d9 /development-guide/contributor-reference/typescript-project-layout: - en: e3327c5492a593cb - zh: d43a5edcb11523ae + en: 637e7c21d468df9a + zh: c1b7081eba036234 /development-guide/contributor-reference/environment-variables: en: dba2c72ee6ffa930 zh: b65c27fbb9129ec8 @@ -39,8 +39,8 @@ en: 7ce6264a6a06dc4a zh: e12b684f2bef942b /development-guide/contributor-reference/application-commands: - en: 28a029c8768f0880 - zh: 67c17256e046624e + en: dc29a9d5f3f10cc0 + zh: 5f1420641be960ae /development-guide/contributor-reference/todo-markers: en: 85b38f78d87fc88f zh: 00d5eb625e7dfb61 diff --git a/docs/development.md b/docs/development.md index 668beec326..cc3449029e 100644 --- a/docs/development.md +++ b/docs/development.md @@ -87,7 +87,7 @@ pnpm run build:web `pnpm run build --artifacts-only` emits the same project and application artifacts while checking and compiling only the existing package references of each compiler face. Required Linux and Windows normal builds and public `typecheck` also check the repository-wide test and script programs. CI benchmark preparation additionally uses `--noCheck` for its disposable library outputs, relying on those required builds for diagnostics; other artifact builders keep package checking ([rationale](../.agents/notes/implemented/process/2026-10-05-pr-artifact-typechecks.md)). -Both tsdown passes match `vendor/*`, `packages/*/*`, and `apps/cli`; the Host pass also matches `apps/desktop-host`. They neither scan build artifacts to discover Client packages nor maintain a Host/Client package filter list. Package-local tsdown configs select entries for the current phase through `DSH_BUILD_FACE`: an ordinary Client plugin produces both its Node loader and browser bundle during the Client phase; `api-remotes` uses `hostPhase: true` to produce its Host entry early and only its browser bundle during the Client phase. Tsdown consumes only the JavaScript emitted to `lib/types` by the preceding tsc phase. Tsdown builds the matched workspace members concurrently, so `apps/desktop`, whose main bundle inlines workspace devDependencies from their `lib/` output, bundles in its own step after the Host pass ([Desktop README](../apps/desktop/README.md#bundled-workspace-dependencies)). +Both tsdown passes match `vendor/*`, `packages/*/*`, and `apps/cli`; the Host pass also matches `apps/desktop-host`. Only directories containing `package.json` are included, so generated residue from moved or removed packages is not bundled. They neither scan build artifacts to discover Client packages nor maintain a Host/Client package filter list. Package-local tsdown configs select entries for the current phase through `DSH_BUILD_FACE`: an ordinary Client plugin produces both its Node loader and browser bundle during the Client phase; `api-remotes` uses `hostPhase: true` to produce its Host entry early and only its browser bundle during the Client phase. Tsdown consumes only the JavaScript emitted to `lib/types` by the preceding tsc phase. Tsdown builds the matched workspace members concurrently, so `apps/desktop`, whose main bundle inlines workspace devDependencies from their `lib/` output, bundles in its own step after the Host pass ([Desktop README](../apps/desktop/README.md#bundled-workspace-dependencies)). Typert runs only during Host tsdown, seeded by `tsconfig.host.json`. It analyzes Host types and generates both Host reflection artifacts and the Host-for-Client Remote projection; Client tsdown does not start Typert. Consequently, `pnpm run typecheck` runs the complete Host lib phase before Client tsc, while `pnpm run build` continues through Client tsdown and the Web build. @@ -173,7 +173,7 @@ pnpm run start:desktop # launch built Desktop artifacts pnpm run dev:desktop # build, then launch Desktop ``` -Arguments after a Web command reach `dsh web`, for example `pnpm run dev:web --no-open --port 3081`; `dev:web` also accepts `--skip-build` to reuse the existing artifact tree and `--no-serve` to run only the rebuild watchers beside a server started elsewhere. Both Web commands use the normal Harness home, while the Desktop commands use the isolated development home described in the [Desktop README](../apps/desktop/README.md). The root `Makefile` names the same commands as `make web`, `make dev-web`, `make desktop`, `make dev-desktop`, and `make build`; `ARGS='--no-open'` forwards options. +Arguments after a Web command reach `dsh web`, for example `pnpm run dev:web --no-open --port 3081`; `dev:web` also accepts `--skip-build` to reuse the existing artifact tree and `--no-serve` to run only the rebuild watchers beside a server started elsewhere. Both Web commands use the normal Harness home, while the Desktop commands use the isolated development home described in the [Desktop README](../apps/desktop/README.md). The root `Makefile` names the same commands as `make web`, `make dev-web`, `make desktop`, `make dev-desktop`, and `make build`; `ARGS='--no-open'` forwards options. `make clean` runs `pnpm run clean` to remove build outputs and safe residue from deleted packages. ### TODO markers diff --git a/docs/development.zh.md b/docs/development.zh.md index 3154c4d988..67fc7a7e3f 100644 --- a/docs/development.zh.md +++ b/docs/development.zh.md @@ -91,7 +91,7 @@ pnpm run build:web `pnpm run build --artifacts-only` 产出相同的 project 和应用产物,但只检查并编译各 compiler face 的现有包 reference。必需的 Linux 与 Windows 普通构建以及公开 `typecheck` 还检查仓库级测试和脚本 program。CI benchmark 准备阶段还对其临时 library 输出使用 `--noCheck`,由这些必需构建负责诊断;其他产物 builder 保留包检查([理由](../.agents/notes/implemented/process/2026-10-05-pr-artifact-typechecks.zh.md))。 -两次 tsdown 都匹配 `vendor/*`、`packages/*/*` 与 `apps/cli`,Host 阶段另外匹配 `apps/desktop-host`;两者都不扫描构建产物来发现 Client 包,也不维护 Host/Client 包过滤表。包内 tsdown 配置根据 `DSH_BUILD_FACE` 决定当前阶段的入口:普通 Client 插件在 Client 阶段同时生成 Node loader 与 browser bundle;`api-remotes` 通过 `hostPhase: true` 提前生成 Host 入口,再在 Client 阶段只生成 browser bundle。tsdown 只消费 `lib/types` 中由前置 tsc 发射的 JavaScript。tsdown 并发构建匹配到的 workspace 成员;`apps/desktop` 的主进程 bundle 需要从工作区 devDependencies 的 `lib/` 产物内联这些包,因此在 Host 阶段之后单独一步打 bundle([Desktop README](../apps/desktop/README.zh.md#bundled-workspace-dependencies))。 +两次 tsdown 都匹配 `vendor/*`、`packages/*/*` 与 `apps/cli`,Host 阶段另外匹配 `apps/desktop-host`。只有含 `package.json` 的目录才会被纳入,因此已迁移或删除包残留的构建产物不会参与打包。两者都不扫描构建产物来发现 Client 包,也不维护 Host/Client 包过滤表。包内 tsdown 配置根据 `DSH_BUILD_FACE` 决定当前阶段的入口:普通 Client 插件在 Client 阶段同时生成 Node loader 与 browser bundle;`api-remotes` 通过 `hostPhase: true` 提前生成 Host 入口,再在 Client 阶段只生成 browser bundle。tsdown 只消费 `lib/types` 中由前置 tsc 发射的 JavaScript。tsdown 并发构建匹配到的 workspace 成员;`apps/desktop` 的主进程 bundle 需要从工作区 devDependencies 的 `lib/` 产物内联这些包,因此在 Host 阶段之后单独一步打 bundle([Desktop README](../apps/desktop/README.zh.md#bundled-workspace-dependencies))。 Typert 只在 Host tsdown 中以 `tsconfig.host.json` 为种子运行。它分析 Host 类型并生成 Host 反射产物及 Host-for-Client Remote 投影;Client tsdown 不启动 Typert。`pnpm run typecheck` 因此先执行完整 Host lib 阶段,再运行 Client tsc;`pnpm run build` 继续执行 Client tsdown 和 Web 构建。 @@ -177,7 +177,7 @@ pnpm run start:desktop # launch built Desktop artifacts pnpm run dev:desktop # build, then launch Desktop ``` -Web 命令后面的参数会传给 `dsh web`,例如 `pnpm run dev:web --no-open --port 3081`;`dev:web` 还接受 `--skip-build` 复用现有产物树,以及 `--no-serve` 只运行重建 watcher、配合别处启动的服务器。两个 Web 命令使用正常的 Harness home,而 Desktop 命令使用 [Desktop README](../apps/desktop/README.zh.md) 描述的隔离开发 home。根目录 `Makefile` 以 `make web`、`make dev-web`、`make desktop`、`make dev-desktop` 和 `make build` 命名同一套命令;`ARGS='--no-open'` 用于转发参数。 +Web 命令后面的参数会传给 `dsh web`,例如 `pnpm run dev:web --no-open --port 3081`;`dev:web` 还接受 `--skip-build` 复用现有产物树,以及 `--no-serve` 只运行重建 watcher、配合别处启动的服务器。两个 Web 命令使用正常的 Harness home,而 Desktop 命令使用 [Desktop README](../apps/desktop/README.zh.md) 描述的隔离开发 home。根目录 `Makefile` 以 `make web`、`make dev-web`、`make desktop`、`make dev-desktop` 和 `make build` 命名同一套命令;`ARGS='--no-open'` 用于转发参数。`make clean` 调用 `pnpm run clean`,删除构建产物和已删除包中可安全清理的残留。 ### TODO 标记 diff --git a/scripts/build-workspace.spec.ts b/scripts/build-workspace.spec.ts new file mode 100644 index 0000000000..2e4c2130a2 --- /dev/null +++ b/scripts/build-workspace.spec.ts @@ -0,0 +1,56 @@ +/** Workspace bundle discovery after a package moves or is removed. */ + +import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { dirname, join } from 'node:path' +import { build, type TsdownBundle } from 'tsdown' +import { describe, expect, it, onTestFinished } from 'vitest' +import { buildWorkspaceDirectories } from './build-workspace.ts' +import { removeFixtureSafely } from './test-fixture-cleanup.ts' + +function write(root: string, path: string, content: string): void { + const target = join(root, path) + mkdirSync(dirname(target), { recursive: true }) + writeFileSync(target, content) +} + +describe('repository bundle workspaces', () => { + it.each(['host', 'client'] as const)('builds live %s packages without bundling a moved package’s residue', async (face) => { + const root = mkdtempSync(join(tmpdir(), 'dsh-build-workspace-')) + onTestFinished(() => { removeFixtureSafely(root) }) + write(root, 'package.json', JSON.stringify({ private: true, type: 'module' })) + const shared = [ + 'apps/cli', + 'packages/experimental/session-title-all-prompts-llm', + 'packages/session/session-title-llm', + 'vendor/cordis', + ] + for (const directory of [...shared, 'apps/desktop-host', 'apps/desktop', 'apps/web', 'native/system']) { + write(root, `${directory}/package.json`, JSON.stringify({ name: directory.replaceAll('/', '-'), type: 'module' })) + write(root, `${directory}/lib/types/index.js`, 'export const executeSessionTitleLlm = "live"\n') + } + const staleEntry = 'packages/session/session-title-all-prompts-llm/lib/types/index.js' + const staleCode = 'export { registerSessionTitleLlmProvider } from "../../../session-title-llm/lib/types/index.js"\n' + write(root, staleEntry, staleCode) + write(root, 'packages/session/session-title-all-prompts-llm/node_modules/.cache/residue', '') + + const workspace = buildWorkspaceDirectories(root, face) + const expected = [...shared, ...face === 'host' ? ['apps/desktop-host'] : []].sort() + expect(workspace).toEqual(expected) + + let bundles: TsdownBundle[] = [] + try { + bundles = await build({ + cwd: root, workspace, config: false, tsconfig: false, + entry: ['lib/types/index.js'], outDir: 'lib', format: 'esm', platform: 'node', + dts: false, clean: false, report: false, logLevel: 'silent', + }) + for (const directory of expected) { + expect(readFileSync(join(root, directory, 'lib/index.mjs'), 'utf8')).toContain('executeSessionTitleLlm') + } + expect(readFileSync(join(root, staleEntry), 'utf8')).toBe(staleCode) + } finally { + for (const bundle of bundles) await bundle[Symbol.asyncDispose]() + } + }) +}) diff --git a/scripts/build-workspace.ts b/scripts/build-workspace.ts new file mode 100644 index 0000000000..263b6f5732 --- /dev/null +++ b/scripts/build-workspace.ts @@ -0,0 +1,16 @@ +/** Discover live workspace packages for the repository's tsdown build phases. */ + +import { globSync } from 'node:fs' +import { dirname, sep } from 'node:path' + +/** + * Select packages with manifests, excluding generated residue from removed packages. + * @param root - Repository root containing the workspace directories. + * @param face - Compiler phase selecting the Host-only desktop host package. + * @returns Sorted repository-relative package directories with forward slashes. + */ +export function buildWorkspaceDirectories(root: string, face: 'host' | 'client'): string[] { + const patterns = ['vendor/*/package.json', 'packages/*/*/package.json', 'apps/cli/package.json'] + if (face === 'host') patterns.push('apps/desktop-host/package.json') + return globSync(patterns, { cwd: root }).map(path => dirname(path).split(sep).join('/')).sort() +} diff --git a/tsdown.config.ts b/tsdown.config.ts index c378de376a..dbe9a3c81b 100644 --- a/tsdown.config.ts +++ b/tsdown.config.ts @@ -1,5 +1,6 @@ import { defineConfig } from 'tsdown' import { typertPlugin } from './packages/typert/generator/lib/types/tsdown-plugin.js' +import { buildWorkspaceDirectories } from './scripts/build-workspace.ts' function isBuildFaceClient(value: unknown): boolean { if (value === undefined || value === 'host') return false @@ -19,9 +20,7 @@ function isBuildFaceClient(value: unknown): boolean { export default defineConfig(({ env }) => { const client = isBuildFaceClient(env?.DSH_BUILD_FACE) return { - workspace: client - ? ['vendor/*', 'packages/*/*', 'apps/cli'] - : ['vendor/*', 'packages/*/*', 'apps/cli', 'apps/desktop-host'], + workspace: buildWorkspaceDirectories(import.meta.dirname, client ? 'client' : 'host'), entry: client ? '' : ['lib/types/{index,startup}.js'], outDir: 'lib', format: ['esm'],