build(desktop): check every import record and mirror the bundle step in the wine gate
The packaged-imports plugin moves from generateBundle to moduleParsed so external dynamic import() and require() targets, which chunk metadata omits, fail the bundle too. Each bundle now carries an import policy: the main process allows Node builtins, sandboxed preloads only the modules Electron's require polyfill resolves, and the failure names the importing module and the allowed set. The wine Windows gate bundles apps/desktop after the Host tsdown pass like build:lib:host, the ts-build-config note lists that step, and the tsdown docs name the workspace match exactly. Fixes #4993
This commit is contained in:
1 parent
06717aebbb
commit
4e41ccadcc
20 files changed
+176
-79
No files matched your search
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-06-17-ts-build-config.md
|
||||
2026-06-17-ts-build-config.md: 05b70cb836491e8a3e6c66deb38773b5351e69e8
|
||||
2026-06-17-ts-build-config.zh.md: d42d9b7abe6368b3ed8e0f32605e866e214f1e7c
|
||||
2026-06-17-ts-build-config.md: 5b40ba01a0438867cd170f6eb7bd7bd535cefc75
|
||||
2026-06-17-ts-build-config.zh.md: c250cdd831614edf4e4d69b4710a220d8500edf8
|
||||
@@ -48,6 +48,7 @@ The command orchestration shape is:
|
||||
pnpm run build:
|
||||
tsc -b tsconfig.host.json
|
||||
tsdown --env.DSH_BUILD_FACE host
|
||||
pnpm --filter @deepseek-ai/dsh-desktop run bundle
|
||||
tsc -b tsconfig.client.json
|
||||
tsdown --env.DSH_BUILD_FACE client
|
||||
pnpm run build:web
|
||||
|
||||
@@ -48,6 +48,7 @@ Status: implemented
|
||||
pnpm run build:
|
||||
tsc -b tsconfig.host.json
|
||||
tsdown --env.DSH_BUILD_FACE host
|
||||
pnpm --filter @deepseek-ai/dsh-desktop run bundle
|
||||
tsc -b tsconfig.client.json
|
||||
tsdown --env.DSH_BUILD_FACE client
|
||||
pnpm run build:web
|
||||
|
||||
+2
-2
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-09-22-desktop-main-bundle-after-workspace-tsdown.md
|
||||
2026-09-22-desktop-main-bundle-after-workspace-tsdown.md: 4aea81fd11873a53d54b16bc8fd4326d923d714b
|
||||
2026-09-22-desktop-main-bundle-after-workspace-tsdown.zh.md: 0f2fd83835254c241f9ccfffab93d5a0abd2c17b
|
||||
2026-09-22-desktop-main-bundle-after-workspace-tsdown.md: 3c997f0707ddc9b292af2016a8ceb39a511aa063
|
||||
2026-09-22-desktop-main-bundle-after-workspace-tsdown.zh.md: 3f741125f68ee47f93d0e74a65cba7b69c7ff939
|
||||
+4
-4
@@ -14,16 +14,16 @@ The root tsdown config builds every matched workspace member concurrently (`Prom
|
||||
|
||||
`apps/desktop` leaves the root tsdown `workspace` list. The root `build:lib:host` script runs the Host tsdown pass and then `pnpm --filter @deepseek-ai/dsh-desktop run bundle`, so the main bundle resolves workspace `lib/` outputs that already exist. The package's own `build` script keeps the same `tsc -b` then `bundle` order for local use. A standalone run of the Desktop config produces byte-identical output to the former workspace-mode run: the config was already self-contained, and the root plugins contributed nothing to it.
|
||||
|
||||
The Desktop config also asserts the invariant the crash violated. [`desktop-bundle-imports`](../../../../apps/desktop/scripts/desktop-bundle-imports.mjs) is a rolldown plugin on every Desktop bundle whose `generateBundle` hook fails the build when a chunk imports a bare specifier the packaged application cannot resolve: the main bundle may import `electron` and the manifest's `dependencies`, sandboxed preloads only `electron`, and Node builtins are always allowed. The check names the actual requirement rather than the symptom, so it also rejects a workspace package moved out of `dependencies` or a broken `exports` entry, and a rejected bundle writes no output.
|
||||
The Desktop config also asserts the invariant the crash violated. [`desktop-bundle-imports`](../../../../apps/desktop/scripts/desktop-bundle-imports.mjs) is a rolldown plugin on every Desktop bundle whose `moduleParsed` hook fails the build when a module imports a bare specifier the packaged application cannot resolve, whether through a static import, a dynamic `import()`, or a `require()` call: the main bundle may import `electron`, Node builtins, and the manifest's `dependencies`; a sandboxed preload only `electron`, `events`, `timers`, and `url`, the modules Electron's sandbox `require` polyfill resolves. The hook reads each module's import records, where an external import keeps its bare specifier and a bundled one carries an absolute id; chunk metadata (`imports`/`dynamicImports`) omits external `import()` and `require()` targets and cannot serve the check. The check names the actual requirement rather than the symptom, so it also rejects a workspace package moved out of `dependencies` or a broken `exports` entry, and a rejected bundle writes no output.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Declare the three packages as runtime `dependencies` so electron-builder ships them in the asar.** This is how `@deepseek-ai/cordis` and `@deepseek-ai/dsh-api-gateway` reach the asar today, and it removes the ordering problem. `dsh-app-boot` would bring a large dependency tree into `app.asar/node_modules`, duplicating the copy already inside the packed dsh runtime and enlarging the payload the runtime file policy must screen. Inlining the main-process helpers was a deliberate choice; the fix keeps it.
|
||||
**Declare the three packages as runtime `dependencies` so electron-builder ships them in the asar.** This is how `@deepseek-ai/cordis` and `@deepseek-ai/dsh-api-gateway` reach the asar today, and it removes the ordering problem. `dsh-app-boot` would bring a large dependency tree into `app.asar/node_modules`, duplicating the copy already inside the packed dsh runtime and enlarging the payload the runtime file policy must screen.
|
||||
|
||||
**Fail on rolldown's `UNRESOLVED_IMPORT` warning** through `inputOptions.onLog` or `failOnWarn`. It catches only the unresolved case and would not have covered a dependency misdeclared as a devDependency, which produces a resolved, inlined bundle when the output exists and an external import when it does not. The packaged-imports check covers both with one rule; a warning-based check remains an optional addition.
|
||||
**Fail on rolldown's `UNRESOLVED_IMPORT` warning** through `inputOptions.onLog` or `failOnWarn`. It catches only the unresolved case and would not have covered a dependency misdeclared as a devDependency, which produces a resolved, inlined bundle when the output exists and an external import when it does not. The packaged-imports check covers both with one rule.
|
||||
|
||||
**Order the workspace build by dependency graph.** tsdown offers no such ordering, and every other member either inlines nothing from the workspace or externalizes declared `dependencies`, which need no ordering. One extra step for the one package that needs it is smaller than a general scheduler.
|
||||
|
||||
## Consequences
|
||||
|
||||
A clean checkout, including CI and a fresh worktree, produces the same Desktop main bundle as a long-lived one. A missing or misdeclared runtime import now fails `pnpm run build` and every packaging command with the offending file and specifiers, before signing and notarization spend their time. The Host build gains one sequential step of about a second on a development machine. `docs/development.md` lists the desktop bundle step in the root build order, and the [Desktop README](../../../../apps/desktop/README.md#bundled-workspace-dependencies) owns the import rule.
|
||||
A clean checkout, including CI and a fresh worktree, produces the same Desktop main bundle as a long-lived one. A missing or misdeclared runtime import now fails `pnpm run build` and every packaging command with the importing module and the offending specifiers, before signing and notarization spend their time. The Host build gains one sequential step of about a second on a development machine. `docs/development.md` lists the desktop bundle step in the root build order, `scripts/wine-windows-gates.sh` mirrors it, and the [Desktop README](../../../../apps/desktop/README.md#bundled-workspace-dependencies) owns the import rule.
|
||||
+4
-4
@@ -14,16 +14,16 @@ Status: implemented
|
||||
|
||||
`apps/desktop` 退出根 tsdown 的 `workspace` 列表。根 `build:lib:host` 脚本先跑 Host tsdown 阶段,再执行 `pnpm --filter @deepseek-ai/dsh-desktop run bundle`,让主进程 bundle 解析的是已经存在的工作区 `lib/` 产物。该包自己的 `build` 脚本为本地使用保留同样的先 `tsc -b` 再 `bundle` 的顺序。单独运行 Desktop 配置得到的产物与此前 workspace 模式下的产物逐字节一致:该配置本来就是自足的,根配置的插件没有为它贡献任何内容。
|
||||
|
||||
Desktop 配置同时断言这次崩溃所违反的不变量。[`desktop-bundle-imports`](../../../../apps/desktop/scripts/desktop-bundle-imports.mjs) 是挂在每个 Desktop bundle 上的 rolldown 插件,其 `generateBundle` 钩子在某个 chunk 导入了打包应用无法解析的裸说明符时让构建失败:主进程 bundle 可以导入 `electron` 与清单的 `dependencies`,沙箱 preload 只能导入 `electron`,Node 内置模块始终允许。这项检查陈述的是真正的要求而非症状,因此它同样拒绝被移出 `dependencies` 的工作区包或损坏的 `exports` 条目,且被拒绝的 bundle 不写出任何产物。
|
||||
Desktop 配置同时断言这次崩溃所违反的不变量。[`desktop-bundle-imports`](../../../../apps/desktop/scripts/desktop-bundle-imports.mjs) 是挂在每个 Desktop bundle 上的 rolldown 插件,其 `moduleParsed` 钩子在某个模块以静态 import、动态 `import()` 或 `require()` 导入了打包应用无法解析的裸说明符时让构建失败:主进程 bundle 可以导入 `electron`、Node 内置模块与清单的 `dependencies`;沙箱 preload 只能导入 `electron`、`events`、`timers` 与 `url`,即 Electron 沙箱 `require` polyfill 能解析的模块。该钩子读取每个模块的导入记录,其中外部导入保留裸说明符、被打包的导入带有绝对 id;chunk 元数据(`imports`/`dynamicImports`)不包含外部的 `import()` 与 `require()` 目标,无法用于这项检查。这项检查陈述的是真正的要求而非症状,因此它同样拒绝被移出 `dependencies` 的工作区包或损坏的 `exports` 条目,且被拒绝的 bundle 不写出任何产物。
|
||||
|
||||
## 备选方案
|
||||
|
||||
**把这三个包声明为运行时 `dependencies`,让 electron-builder 装进 asar。** `@deepseek-ai/cordis` 与 `@deepseek-ai/dsh-api-gateway` 今天就是这样进入 asar 的,这也消除了顺序问题。但 `dsh-app-boot` 会把一棵很大的依赖树带进 `app.asar/node_modules`,与 packed dsh 运行时内已有的副本重复,并扩大运行时文件策略必须筛查的载荷。内联主进程辅助包是有意的选择;本修复保留它。
|
||||
**把这三个包声明为运行时 `dependencies`,让 electron-builder 装进 asar。** `@deepseek-ai/cordis` 与 `@deepseek-ai/dsh-api-gateway` 今天就是这样进入 asar 的,这也消除了顺序问题。但 `dsh-app-boot` 会把一棵很大的依赖树带进 `app.asar/node_modules`,与 packed dsh 运行时内已有的副本重复,并扩大运行时文件策略必须筛查的载荷。
|
||||
|
||||
**通过 `inputOptions.onLog` 或 `failOnWarn` 让 rolldown 的 `UNRESOLVED_IMPORT` warning 直接失败。** 它只覆盖未解析这一种情况,覆盖不了被误声明为 devDependency 的依赖:产物存在时它会被解析并内联,不存在时才留成外部导入。打包导入检查用一条规则同时覆盖两者;基于 warning 的检查仍可作为可选补充。
|
||||
**通过 `inputOptions.onLog` 或 `failOnWarn` 让 rolldown 的 `UNRESOLVED_IMPORT` warning 直接失败。** 它只覆盖未解析这一种情况,覆盖不了被误声明为 devDependency 的依赖:产物存在时它会被解析并内联,不存在时才留成外部导入。打包导入检查用一条规则同时覆盖两者。
|
||||
|
||||
**按依赖图给 workspace 构建排序。** tsdown 不提供这种排序,而其他每个成员要么不从工作区内联任何内容,要么把已声明的 `dependencies` 留成外部导入,都不需要排序。为唯一需要它的包多加一步,比通用调度器更小。
|
||||
|
||||
## 后果
|
||||
|
||||
干净的检出目录(包括 CI 与全新 worktree)产出的 Desktop 主进程 bundle 与长期使用的检出目录一致。缺失或误声明的运行时导入现在会让 `pnpm run build` 和每条打包命令在签名与公证耗时之前失败,并报出违规文件与说明符。Host 构建多出一个串行步骤,在开发机上约一秒。`docs/development.md` 在根构建顺序中列出了 desktop bundle 步骤,导入规则由 [Desktop README](../../../../apps/desktop/README.zh.md#bundled-workspace-dependencies) 负责。
|
||||
干净的检出目录(包括 CI 与全新 worktree)产出的 Desktop 主进程 bundle 与长期使用的检出目录一致。缺失或误声明的运行时导入现在会让 `pnpm run build` 和每条打包命令在签名与公证耗时之前失败,并报出导入方模块与违规说明符。Host 构建多出一个串行步骤,在开发机上约一秒。`docs/development.md` 在根构建顺序中列出了 desktop bundle 步骤,`scripts/wine-windows-gates.sh` 与之保持一致,导入规则由 [Desktop README](../../../../apps/desktop/README.zh.md#bundled-workspace-dependencies) 负责。
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write apps/desktop/README.md
|
||||
README.md: e534a9443be4fdc576878f39f132fff34973db2f
|
||||
README.zh.md: 8123832ac97185539bff87257cdb8921e332d2d9
|
||||
README.md: 8c70063b812e475cff8b2de2c0d477fe2358dc3a
|
||||
README.zh.md: 2802dcb7516393c32fb0fc5bc128381e85e2b718
|
||||
@@ -26,7 +26,7 @@ The macOS PNG uses an inset rounded background for legacy ICNS packaging, with r
|
||||
|
||||
### Bundled workspace dependencies
|
||||
|
||||
electron-builder copies only the manifest's `dependencies` into `app.asar/node_modules`, so the Electron main bundle `lib/main.js` inlines its workspace devDependencies and leaves `electron` and those `dependencies` as its only bare imports; sandboxed preloads import only `electron`. The main bundle resolves the inlined packages from their `lib/` output, so the root `build:lib:host` bundles `apps/desktop` after the concurrent workspace tsdown pass, and [`desktop-bundle-imports`](scripts/desktop-bundle-imports.mjs) fails any Desktop bundle whose external imports the packaged application cannot resolve. Without that check an import rolldown could not resolve ships as an external specifier and fails at launch with `ERR_MODULE_NOT_FOUND`. The [bundle-order decision](../../.agents/notes/implemented/process/2026-09-22-desktop-main-bundle-after-workspace-tsdown.md) records the alternatives.
|
||||
electron-builder copies only the manifest's `dependencies` into `app.asar/node_modules`, so the Electron main bundle `lib/main.js` inlines its workspace devDependencies and leaves `electron`, Node builtins, and those `dependencies` as its only bare imports; a sandboxed preload may leave only `electron`, `events`, `timers`, and `url`, the modules its `require` polyfill resolves. The main bundle resolves the inlined packages from their `lib/` output, so the root `build:lib:host` bundles `apps/desktop` after the concurrent workspace tsdown pass, and [`desktop-bundle-imports`](scripts/desktop-bundle-imports.mjs) fails any Desktop bundle whose static, dynamic, or `require()` import the packaged application cannot resolve. Without that check an import rolldown could not resolve ships as an external specifier and fails at launch with `ERR_MODULE_NOT_FOUND`. The [bundle-order decision](../../.agents/notes/implemented/process/2026-09-22-desktop-main-bundle-after-workspace-tsdown.md) records the alternatives.
|
||||
|
||||
Signed Windows packaging scans the primary runtime and application production dependencies for PE content, including files without a conventional extension. The final scan covers the entire unpacked application. Directory links, malformed `MZ` files and non-PE `.exe`, `.dll` or `.pyd` files stop packaging; data files beginning with `MZ` are also rejected unless they contain a valid PE header. It preserves valid vendor signatures and signs unsigned code before sealing runtime hashes or executing smoke checks. Public-key inspection uses batches of 32 files with at most four processes; hardware-token signing remains serial, and each new signature must match the configured certificate and carry a timestamp. Hardware signing and verification failures stop the run; isolated timestamp requests follow the bounded policy below. Electron-builder preserves copied runtime executables only after signature and exact-byte verification. A final PE-signature audit and a fresh-cache ASAR payload and Host smoke must pass before the release completion record is written. Development, preparation-only and unsigned builds do not use the hardware token and can be blocked by Windows code-integrity policy; no build mode disables that policy. Passing smoke checks does not establish compatibility with every enterprise policy.
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ macOS PNG 使用带留白的圆角底板,供传统 ICNS 打包使用,包含
|
||||
|
||||
### 内置工作区依赖
|
||||
|
||||
electron-builder 只把清单中的 `dependencies` 复制进 `app.asar/node_modules`,因此 Electron 主进程 bundle `lib/main.js` 内联其工作区 devDependencies,裸导入只剩 `electron` 与这些 `dependencies`;沙箱 preload 只导入 `electron`。主进程 bundle 从被内联包的 `lib/` 产物解析它们,所以根 `build:lib:host` 在并发的工作区 tsdown 阶段之后才为 `apps/desktop` 打 bundle,并由 [`desktop-bundle-imports`](scripts/desktop-bundle-imports.mjs) 让任何外部导入无法在打包应用内解析的 Desktop bundle 直接失败。没有这项检查时,rolldown 无法解析的导入会作为外部说明符进入产物,并在启动时以 `ERR_MODULE_NOT_FOUND` 失败。[bundle 顺序决策](../../.agents/notes/implemented/process/2026-09-22-desktop-main-bundle-after-workspace-tsdown.zh.md)记录了备选方案。
|
||||
electron-builder 只把清单中的 `dependencies` 复制进 `app.asar/node_modules`,因此 Electron 主进程 bundle `lib/main.js` 内联其工作区 devDependencies,裸导入只剩 `electron`、Node 内置模块与这些 `dependencies`;沙箱 preload 只能留下 `electron`、`events`、`timers` 与 `url`,即其 `require` polyfill 能解析的模块。主进程 bundle 从被内联包的 `lib/` 产物解析它们,所以根 `build:lib:host` 在并发的工作区 tsdown 阶段之后才为 `apps/desktop` 打 bundle,并由 [`desktop-bundle-imports`](scripts/desktop-bundle-imports.mjs) 让任何静态、动态或 `require()` 导入无法在打包应用内解析的 Desktop bundle 直接失败。没有这项检查时,rolldown 无法解析的导入会作为外部说明符进入产物,并在启动时以 `ERR_MODULE_NOT_FOUND` 失败。[bundle 顺序决策](../../.agents/notes/implemented/process/2026-09-22-desktop-main-bundle-after-workspace-tsdown.zh.md)记录了备选方案。
|
||||
|
||||
Windows 签名打包按 PE 文件内容扫描第一方运行时和应用生产依赖,包括没有常规扩展名的文件。最终扫描覆盖整个解包应用。目录链接、格式错误的 `MZ` 文件以及非 PE 的 `.exe`、`.dll` 或 `.pyd` 文件会使打包停止;以 `MZ` 开头的数据文件也会被拒绝,除非包含有效 PE 头。它保留有效的上游签名,并在记录运行时哈希或执行冒烟检查前为未签名代码补签。公钥验签每个进程处理最多 32 个文件,同时最多运行四个进程;硬件令牌签名仍串行执行,每个新签名必须匹配配置的证书且带时间戳。硬件签名或验签失败会停止本轮执行;独立的时间戳请求遵循下文的有界重试规则。electron-builder 只有在验签和逐字节比对通过后,才保留复制后运行时可执行文件的签名。写入发布完成记录前,必须通过最终 PE 签名检查,以及使用全新缓存的 ASAR 载荷和 Host 冒烟检查。开发、仅准备和未签名构建不使用硬件令牌,可能被 Windows 代码完整性策略阻止;任何构建模式都不会关闭该策略。冒烟检查通过不代表兼容所有企业策略。
|
||||
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
import type { Rolldown } from 'tsdown'
|
||||
|
||||
/** Bare specifiers one Desktop bundle may leave for the runtime to resolve. */
|
||||
export interface BundleImportPolicy {
|
||||
/** Package names present at runtime, matched by package name so subpaths pass. */
|
||||
readonly packages: ReadonlySet<string>
|
||||
/**
|
||||
* Whether Node builtins resolve at runtime: true for the Electron main
|
||||
* process; false for sandboxed preloads, whose `require` polyfill offers only
|
||||
* the modules listed in `packages`.
|
||||
*/
|
||||
readonly nodeBuiltins: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Package name of a bare import specifier.
|
||||
* @param specifier - Bare specifier as rolldown reports it, such as `@scope/name/subpath`.
|
||||
@@ -7,18 +19,28 @@ import type { Rolldown } from 'tsdown'
|
||||
*/
|
||||
export function importPackageName(specifier: string): string
|
||||
|
||||
/**
|
||||
* Whether a module id rolldown reports is a bare specifier, i.e. an import the
|
||||
* bundle leaves external. Bundled modules carry absolute ids; virtual modules
|
||||
* start with `\0`.
|
||||
* @param id - Module id from `importedIds` or `dynamicallyImportedIds`.
|
||||
* @returns True for a bare specifier.
|
||||
*/
|
||||
export function isBareSpecifier(id: string): boolean
|
||||
|
||||
/**
|
||||
* Bare imports the packaged application cannot resolve.
|
||||
* @param imports - External specifiers a chunk imports, statically or dynamically.
|
||||
* @param packaged - Package names available to the bundle at runtime.
|
||||
* @param imports - External specifiers a module imports statically, dynamically, or through `require()`.
|
||||
* @param policy - What this bundle may leave external.
|
||||
* @returns Offending specifiers in import order, each once.
|
||||
*/
|
||||
export function unpackagedImports(imports: readonly string[], packaged: ReadonlySet<string>): string[]
|
||||
export function unpackagedImports(imports: readonly string[], policy: BundleImportPolicy): string[]
|
||||
|
||||
/**
|
||||
* Rolldown plugin that fails a bundle whose external imports the packaged
|
||||
* application cannot resolve.
|
||||
* @param packaged - Package names available to the bundle at runtime.
|
||||
* application cannot resolve. The failure names the importing module and every
|
||||
* offending specifier, and no output is written.
|
||||
* @param policy - What this bundle may leave external.
|
||||
* @returns The plugin.
|
||||
*/
|
||||
export function packagedImportsPlugin(packaged: ReadonlySet<string>): Rolldown.Plugin
|
||||
export function packagedImportsPlugin(policy: BundleImportPolicy): Rolldown.Plugin
|
||||
@@ -8,11 +8,25 @@
|
||||
* bundle inlines its workspace devDependencies; when one of their `lib/`
|
||||
* outputs is missing at bundle time, rolldown reports `UNRESOLVED_IMPORT` as a
|
||||
* warning and keeps the specifier as an external import, which is exactly the
|
||||
* broken artifact. Sandboxed preloads may `require` only `electron`. This
|
||||
* check fails the bundle instead of the installed application.
|
||||
* broken artifact. This check fails the bundle instead of the installed
|
||||
* application.
|
||||
*
|
||||
* The check runs in `moduleParsed`, where rolldown reports every import record
|
||||
* of a module: static imports, dynamic `import()`, and `require()` calls. A
|
||||
* bundled import resolves to an absolute module id; an external one keeps its
|
||||
* bare specifier. Chunk metadata (`imports`/`dynamicImports`) omits external
|
||||
* `import()` and `require()` targets, so it cannot serve this check.
|
||||
*/
|
||||
|
||||
import { isBuiltin } from 'node:module'
|
||||
import { isAbsolute, relative } from 'node:path'
|
||||
|
||||
/**
|
||||
* Bare specifiers one Desktop bundle may leave for the runtime to resolve.
|
||||
* @typedef {object} BundleImportPolicy
|
||||
* @property {ReadonlySet<string>} packages - Package names present at runtime, matched by package name so subpaths pass.
|
||||
* @property {boolean} nodeBuiltins - Whether Node builtins resolve at runtime (true for the Electron main process; false for sandboxed preloads, whose `require` polyfill offers only the modules listed in `packages`).
|
||||
*/
|
||||
|
||||
/**
|
||||
* Package name of a bare import specifier.
|
||||
@@ -24,17 +38,28 @@ export function importPackageName(specifier) {
|
||||
return specifier.startsWith('@') ? segments.slice(0, 2).join('/') : segments[0]
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a module id rolldown reports is a bare specifier, i.e. an import the
|
||||
* bundle leaves external. Bundled modules carry absolute ids; virtual modules
|
||||
* start with `\0`.
|
||||
* @param {string} id - Module id from `importedIds` or `dynamicallyImportedIds`.
|
||||
* @returns {boolean} True for a bare specifier.
|
||||
*/
|
||||
export function isBareSpecifier(id) {
|
||||
return !id.startsWith('.') && !id.startsWith('\0') && !isAbsolute(id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Bare imports the packaged application cannot resolve.
|
||||
* @param {readonly string[]} imports - External specifiers a chunk imports, statically or dynamically.
|
||||
* @param {ReadonlySet<string>} packaged - Package names available to the bundle at runtime.
|
||||
* @param {readonly string[]} imports - External specifiers a module imports statically, dynamically, or through `require()`.
|
||||
* @param {BundleImportPolicy} policy - What this bundle may leave external.
|
||||
* @returns {string[]} Offending specifiers in import order, each once.
|
||||
*/
|
||||
export function unpackagedImports(imports, packaged) {
|
||||
export function unpackagedImports(imports, policy) {
|
||||
const offending = new Set()
|
||||
for (const specifier of imports) {
|
||||
if (isBuiltin(specifier)) continue
|
||||
if (packaged.has(importPackageName(specifier))) continue
|
||||
if (policy.nodeBuiltins && isBuiltin(specifier)) continue
|
||||
if (policy.packages.has(importPackageName(specifier))) continue
|
||||
offending.add(specifier)
|
||||
}
|
||||
return [...offending]
|
||||
@@ -42,25 +67,24 @@ export function unpackagedImports(imports, packaged) {
|
||||
|
||||
/**
|
||||
* Rolldown plugin that fails a bundle whose external imports the packaged
|
||||
* application cannot resolve.
|
||||
* @param {ReadonlySet<string>} packaged - Package names available to the bundle at runtime.
|
||||
* application cannot resolve. The failure names the importing module and every
|
||||
* offending specifier, and no output is written.
|
||||
* @param {BundleImportPolicy} policy - What this bundle may leave external.
|
||||
* @returns {import('tsdown').Rolldown.Plugin} The plugin.
|
||||
*/
|
||||
export function packagedImportsPlugin(packaged) {
|
||||
export function packagedImportsPlugin(policy) {
|
||||
const allowed = [...policy.packages].join(', ') + (policy.nodeBuiltins ? ', and Node builtins' : '')
|
||||
return {
|
||||
name: 'desktop-packaged-imports',
|
||||
generateBundle(_options, bundle) {
|
||||
for (const output of Object.values(bundle)) {
|
||||
if (output.type !== 'chunk') continue
|
||||
const external = [...output.imports, ...output.dynamicImports].filter(specifier => !(specifier in bundle))
|
||||
const offending = unpackagedImports(external, packaged)
|
||||
if (offending.length === 0) continue
|
||||
this.error(
|
||||
`desktop bundle: ${output.fileName} imports ${offending.join(', ')}, which the packaged application does not ship. `
|
||||
+ `A bare import must name electron or a package in this manifest's dependencies; anything else must be bundled, `
|
||||
+ 'which requires its lib/ output to exist before this bundle runs (pnpm run build:lib:host).',
|
||||
)
|
||||
}
|
||||
moduleParsed(info) {
|
||||
const external = [...info.importedIds, ...info.dynamicallyImportedIds].filter(isBareSpecifier)
|
||||
const offending = unpackagedImports(external, policy)
|
||||
if (offending.length === 0) return
|
||||
this.error(
|
||||
`desktop bundle: ${relative(process.cwd(), info.id)} imports ${offending.join(', ')}, which the packaged application does not ship. `
|
||||
+ `This bundle may leave only ${allowed} as bare imports; anything else must be bundled, `
|
||||
+ 'which requires its lib/ output to exist before this bundle runs (pnpm run build:lib:host).',
|
||||
)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -5,11 +5,17 @@ import { Rolldown } from 'tsdown'
|
||||
import { afterAll, describe, expect, it } from 'vitest'
|
||||
import {
|
||||
importPackageName,
|
||||
isBareSpecifier,
|
||||
packagedImportsPlugin,
|
||||
unpackagedImports,
|
||||
type BundleImportPolicy,
|
||||
} from '../scripts/desktop-bundle-imports.mjs'
|
||||
|
||||
const PACKAGED = new Set(['electron', 'electron-updater', '@deepseek-ai/dsh-api-gateway'])
|
||||
const MAIN: BundleImportPolicy = {
|
||||
packages: new Set(['electron', 'electron-updater', '@deepseek-ai/dsh-api-gateway']),
|
||||
nodeBuiltins: true,
|
||||
}
|
||||
const PRELOAD: BundleImportPolicy = { packages: new Set(['electron', 'events', 'timers', 'url']), nodeBuiltins: false }
|
||||
|
||||
describe('desktop bundle imports', () => {
|
||||
it('names the package behind a bare specifier', () => {
|
||||
@@ -18,14 +24,27 @@ describe('desktop bundle imports', () => {
|
||||
expect(importPackageName('ws')).toBe('ws')
|
||||
})
|
||||
|
||||
it('accepts Node builtins and packaged dependencies, including their subpaths', () => {
|
||||
it('tells external specifiers from bundled and virtual module ids', () => {
|
||||
expect(isBareSpecifier('electron')).toBe(true)
|
||||
expect(isBareSpecifier('@deepseek-ai/dsh-home-paths')).toBe(true)
|
||||
expect(isBareSpecifier('node:fs')).toBe(true)
|
||||
expect(isBareSpecifier('./helper.js')).toBe(false)
|
||||
expect(isBareSpecifier(join(tmpdir(), 'lib', 'index.js'))).toBe(false)
|
||||
expect(isBareSpecifier('\0rolldown/runtime')).toBe(false)
|
||||
})
|
||||
|
||||
it('accepts Node builtins and packaged dependencies, including their subpaths, for the main process', () => {
|
||||
expect(unpackagedImports([
|
||||
'node:fs/promises',
|
||||
'crypto',
|
||||
'electron',
|
||||
'electron-updater/out/electronHttpExecutor.js',
|
||||
'@deepseek-ai/dsh-api-gateway/stream-protocol',
|
||||
], PACKAGED)).toEqual([])
|
||||
], MAIN)).toEqual([])
|
||||
})
|
||||
|
||||
it('rejects Node builtins outside the sandbox polyfill for a preload', () => {
|
||||
expect(unpackagedImports(['electron', 'events', 'url', 'node:fs', 'crypto'], PRELOAD)).toEqual(['node:fs', 'crypto'])
|
||||
})
|
||||
|
||||
it('reports each specifier the packaged application cannot resolve once, in import order', () => {
|
||||
@@ -34,45 +53,69 @@ describe('desktop bundle imports', () => {
|
||||
'ws',
|
||||
'@deepseek-ai/dsh-home-paths',
|
||||
'electron',
|
||||
], PACKAGED)).toEqual(['@deepseek-ai/dsh-home-paths', 'ws'])
|
||||
], MAIN)).toEqual(['@deepseek-ai/dsh-home-paths', 'ws'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('packaged imports plugin', () => {
|
||||
const root = mkdtempSync(join(tmpdir(), 'dsh-desktop-bundle-imports-'))
|
||||
const entry = join(root, 'entry.js')
|
||||
writeFileSync(entry, [
|
||||
'import { app } from "electron";',
|
||||
'import { resolveDshHome } from "@deepseek-ai/dsh-home-paths";',
|
||||
'export const home = resolveDshHome(app);',
|
||||
].join('\n'))
|
||||
const entries = {
|
||||
static: ['import { app } from "electron";', 'import { resolveDshHome } from "@deepseek-ai/dsh-home-paths";', 'export const home = resolveDshHome(app);'],
|
||||
dynamic: ['import { app } from "electron";', 'export const home = () => import("@deepseek-ai/dsh-home-paths").then(m => m.resolveDshHome(app));'],
|
||||
require: ['import { app } from "electron";', 'export const home = () => require("@deepseek-ai/dsh-home-paths").resolveDshHome(app);'],
|
||||
builtin: ['import { contextBridge } from "electron";', 'import { readFileSync } from "node:fs";', 'contextBridge.exposeInMainWorld("x", readFileSync);'],
|
||||
} as const
|
||||
for (const [name, lines] of Object.entries(entries)) writeFileSync(join(root, `${name}.js`), lines.join('\n'))
|
||||
afterAll(() => {
|
||||
rmSync(root, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
async function generate(packaged: ReadonlySet<string>): Promise<Rolldown.RolldownOutput> {
|
||||
async function generate(entry: keyof typeof entries, policy: BundleImportPolicy, format: 'esm' | 'cjs' = 'esm'): Promise<Rolldown.RolldownOutput> {
|
||||
// Every import stays external, which is what an unresolved workspace lib/ output produces.
|
||||
const bundle = await Rolldown.rolldown({
|
||||
input: entry,
|
||||
input: join(root, `${entry}.js`),
|
||||
platform: 'node',
|
||||
external: () => true,
|
||||
logLevel: 'silent',
|
||||
plugins: [packagedImportsPlugin(packaged)],
|
||||
plugins: [packagedImportsPlugin(policy)],
|
||||
})
|
||||
try {
|
||||
return await bundle.generate({ format: 'esm' })
|
||||
return await bundle.generate({ format })
|
||||
} finally {
|
||||
await bundle.close()
|
||||
}
|
||||
}
|
||||
|
||||
it('fails the bundle that leaves an import the packaged application does not ship', async () => {
|
||||
await expect(generate(new Set(['electron']))).rejects.toThrow(
|
||||
/entry\.js imports @deepseek-ai\/dsh-home-paths, which the packaged application does not ship/u,
|
||||
const unshipped = /imports @deepseek-ai\/dsh-home-paths, which the packaged application does not ship/u
|
||||
|
||||
it('fails the bundle whose static import the packaged application does not ship', async () => {
|
||||
await expect(generate('static', { ...MAIN, packages: new Set(['electron']) })).rejects.toThrow(unshipped)
|
||||
})
|
||||
|
||||
it('fails the bundle whose dynamic import() the packaged application does not ship', async () => {
|
||||
await expect(generate('dynamic', { ...MAIN, packages: new Set(['electron']) })).rejects.toThrow(unshipped)
|
||||
})
|
||||
|
||||
it('fails the CommonJS bundle whose require() the packaged application does not ship', async () => {
|
||||
await expect(generate('require', { ...MAIN, packages: new Set(['electron']) }, 'cjs')).rejects.toThrow(unshipped)
|
||||
})
|
||||
|
||||
it('fails a preload bundle that requires a Node builtin the sandbox does not polyfill', async () => {
|
||||
await expect(generate('builtin', PRELOAD, 'cjs')).rejects.toThrow(
|
||||
/builtin\.js imports node:fs, which the packaged application does not ship\. /u,
|
||||
)
|
||||
await expect(generate('builtin', PRELOAD, 'cjs')).rejects.toThrow(
|
||||
/This bundle may leave only electron, events, timers, url as bare imports/u,
|
||||
)
|
||||
})
|
||||
|
||||
it('passes the bundle whose external imports are all packaged', async () => {
|
||||
const output = await generate(new Set(['electron', '@deepseek-ai/dsh-home-paths']))
|
||||
expect(output.output.map(chunk => chunk.fileName)).toEqual(['entry.js'])
|
||||
it('passes the bundles whose external imports are all packaged', async () => {
|
||||
const policy = { ...MAIN, packages: new Set(['electron', '@deepseek-ai/dsh-home-paths']) }
|
||||
for (const entry of ['static', 'dynamic'] as const) {
|
||||
const output = await generate(entry, policy)
|
||||
expect(output.output.map(chunk => chunk.fileName)).toEqual([`${entry}.js`])
|
||||
}
|
||||
const output = await generate('require', policy, 'cjs')
|
||||
expect(output.output.map(chunk => chunk.fileName)).toEqual(['require.js'])
|
||||
})
|
||||
})
|
||||
@@ -11,10 +11,13 @@ import { packagedImportsPlugin } from './scripts/desktop-bundle-imports.mjs'
|
||||
const manifest = JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf8')) as {
|
||||
dependencies: Record<string, string>
|
||||
}
|
||||
/** electron-builder ships these next to the main bundle; Electron itself provides `electron`. */
|
||||
const mainProcessImports = new Set(['electron', ...Object.keys(manifest.dependencies)])
|
||||
/** Sandboxed preloads can require nothing but `electron`. */
|
||||
const preloadImports = new Set(['electron'])
|
||||
/** electron-builder ships the manifest `dependencies` next to the main bundle; Electron provides `electron` and Node. */
|
||||
const mainProcessImports = { packages: new Set(['electron', ...Object.keys(manifest.dependencies)]), nodeBuiltins: true }
|
||||
/**
|
||||
* The `require` polyfill of a sandboxed preload resolves only these modules
|
||||
* (Electron: Process Sandboxing, "Preload scripts").
|
||||
*/
|
||||
const preloadImports = { packages: new Set(['electron', 'events', 'timers', 'url']), nodeBuiltins: false }
|
||||
|
||||
export default defineConfig([
|
||||
{
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write docs/api-gateway.md
|
||||
api-gateway.md: d6a8444c9f18a7c3df9cd64e4a836d1831f8a0d8
|
||||
api-gateway.zh.md: 23c7c18d3d784fc2b6abe4c1df75e08447100b98
|
||||
api-gateway.md: 91f992dc5ee25f3c3edf5ea63bf01ebaf7ba8102
|
||||
api-gateway.zh.md: 4e21bafa9babfba1f6ef36ca1cb6c4e8807d3468
|
||||
+1
-1
@@ -98,7 +98,7 @@ The API Gateway package owns the Host dispatcher and Client Remote endpoint as p
|
||||
|
||||
The root build runs `build:lib:host`, `build:lib:client`, and `build:web` in order. The Host lib phase first runs `tsc -b tsconfig.host.json`, then `tsdown --env.DSH_BUILD_FACE host`; the normal Host Project Reference graph compiles the Typert generator, which runs during this tsdown pass with the Host aggregate as its only `ts.Program` seed. The Client lib phase then runs `tsc -b tsconfig.client.json` and `tsdown --env.DSH_BUILD_FACE client`, consuming the newly generated Remote Client declarations and runtime contributions without starting Typert again.
|
||||
|
||||
Both tsdown passes receive the same workspace match ([build order](development.md#typescript-project-layout)) and bundle only JavaScript emitted to `lib/types` by the corresponding tsc phase. The root config does not scan Client artifacts, classify package names, or pass a maintained filter to tsdown; package-local configs return entries for the current phase based on `DSH_BUILD_FACE`. An ordinary Client plugin produces both its Node loader entry and browser bundle during the Client phase.
|
||||
Both tsdown passes match `vendor/*`, `packages/*/*`, and `apps/cli`, the Host pass also `apps/desktop-host` ([build order](development.md#typescript-project-layout)), and both bundle only JavaScript emitted to `lib/types` by the corresponding tsc phase. The root config does not scan Client artifacts, classify package names, or pass a maintained filter to tsdown; package-local configs return entries for the current phase based on `DSH_BUILD_FACE`. An ordinary Client plugin produces both its Node loader entry and browser bundle during the Client phase.
|
||||
|
||||
`api/remotes`, `api/gateway`, `api/session-controller`, and `api/workspace-controller` (plus `client/connection`) split TypeScript faces. `api/remotes`' Client project depends on `/remote` declarations generated for business packages during Host tsdown; root aggregates and direct consumers must reference each split package's `tsconfig.host.json` or `tsconfig.client.json` respectively. `api-remotes`' `clientBundle(..., { hostPhase: true })` produces its Host entry during Host tsdown and leaves only the browser entry for Client tsdown. The Agent/Session lookup policy lives in `@deepseek-ai/dsh-api-session-controller`, not in `api-remotes`.
|
||||
|
||||
|
||||
@@ -98,7 +98,7 @@ API Gateway 包同时拥有 Host dispatcher 与 Client Remote endpoint 两个对
|
||||
|
||||
根构建依次执行 `build:lib:host`、`build:lib:client` 与 `build:web`。Host lib 阶段先运行 `tsc -b tsconfig.host.json`,再运行 `tsdown --env.DSH_BUILD_FACE host`;Typert generator 由正常 Host Project Reference 图编译,并在这次 tsdown 中以 Host aggregate 为唯一 `ts.Program` 种子运行。Client lib 阶段随后运行 `tsc -b tsconfig.client.json` 与 `tsdown --env.DSH_BUILD_FACE client`,使用刚生成的 Remote Client 声明和运行时贡献,但不再次启动 Typert。
|
||||
|
||||
两次 tsdown 都接收同一组 workspace 匹配([构建顺序](development.zh.md#typescript-project-layout)),且都只打包 `lib/types` 中由对应 tsc 阶段发射的 JavaScript。根配置不扫描 Client 产物、不按包名分类,也不向 tsdown 传维护式 filter;各包的本地配置根据 `DSH_BUILD_FACE` 返回当前阶段的入口。普通 Client 插件在 Client 阶段一起生成 Node loader 入口与 browser bundle。
|
||||
两次 tsdown 都匹配 `vendor/*`、`packages/*/*` 与 `apps/cli`,Host 阶段另外匹配 `apps/desktop-host`([构建顺序](development.zh.md#typescript-project-layout)),且都只打包 `lib/types` 中由对应 tsc 阶段发射的 JavaScript。根配置不扫描 Client 产物、不按包名分类,也不向 tsdown 传维护式 filter;各包的本地配置根据 `DSH_BUILD_FACE` 返回当前阶段的入口。普通 Client 插件在 Client 阶段一起生成 Node loader 入口与 browser bundle。
|
||||
|
||||
`api/remotes`、`api/gateway`、`api/session-controller` 与 `api/workspace-controller`(外加 `client/connection`)都拆分 TypeScript face。`api/remotes` 的 Client project 依赖业务包在 Host tsdown 中生成的 `/remote` 声明;根 aggregate 与直接消费方必须分别引用各拆分包自己的 `tsconfig.host.json` 或 `tsconfig.client.json`。`api-remotes` 的 `clientBundle(..., { hostPhase: true })` 让 Host 入口在 Host tsdown 中生成,让 Client tsdown 只生成 browser 入口。Agent/Session lookup 策略位于 `@deepseek-ai/dsh-api-session-controller`,而非 `api-remotes`。
|
||||
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write docs/development.md
|
||||
development.md: 812cabbe3e2af5965033ef0c73301e5cd28cba28
|
||||
development.zh.md: b45e8e3e7b3c5fc0ee05967273a1c73e407b32fc
|
||||
development.md: 8ecbf9b51772631f22d1aef2763a28ace7a7ae1b
|
||||
development.zh.md: 3408d54479de81e8464b863d15647ae67229f703
|
||||
+1
-1
@@ -80,7 +80,7 @@ tsdown --env.DSH_BUILD_FACE client
|
||||
pnpm run build:web
|
||||
```
|
||||
|
||||
Both tsdown passes use the same workspace match. 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`. 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.
|
||||
|
||||
|
||||
@@ -84,7 +84,7 @@ tsdown --env.DSH_BUILD_FACE client
|
||||
pnpm run build:web
|
||||
```
|
||||
|
||||
两次 tsdown 都使用同一组 workspace 匹配,不扫描构建产物来发现 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 成员,因此主进程 bundle 要从工作区 devDependencies 的 `lib/` 产物内联它们的 `apps/desktop` 在 Host 阶段之后单独一步打 bundle([Desktop README](../apps/desktop/README.zh.md#bundled-workspace-dependencies))。
|
||||
两次 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))。
|
||||
|
||||
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 构建。
|
||||
|
||||
|
||||
@@ -239,12 +239,14 @@ cat "$scratch/logs/smoke.log"
|
||||
grep -q '^smoke: win32 x64' "$scratch/logs/smoke.log" || { echo 'wine-windows-gates: Windows Node smoke did not report win32 x64' >&2; exit 1; }
|
||||
|
||||
# ---- the two blocking surfaces, concurrently ------------------------------
|
||||
# The build preserves the face order from package.json: compile and bundle the
|
||||
# Host face before compiling and bundling the Client face.
|
||||
# The build mirrors the root build:lib:host and build:lib:client scripts:
|
||||
# compile and bundle the Host face, bundle apps/desktop from the Host lib/
|
||||
# outputs, then compile and bundle the Client face.
|
||||
# Both statuses are captured so one failure cannot hide the other's result.
|
||||
build_gate() {
|
||||
wine_node "$scratch/logs/host-tsc.log" --max-old-space-size=4096 "$tsc_js" -b tsconfig.host.json --pretty false || return $?
|
||||
wine_node "$scratch/logs/host-tsdown.log" "$tsdown_js" --env.DSH_BUILD_FACE host || return $?
|
||||
(cd apps/desktop && wine_node "$scratch/logs/desktop-tsdown.log" "../../$tsdown_js") || return $?
|
||||
wine_node "$scratch/logs/client-tsc.log" "$tsc_js" -b tsconfig.client.json --pretty false || return $?
|
||||
wine_node "$scratch/logs/client-tsdown.log" "$tsdown_js" --env.DSH_BUILD_FACE client
|
||||
}
|
||||
@@ -272,9 +274,10 @@ report() {
|
||||
for log in "$@"; do tail -n 200 "$log" >&2 || true; done
|
||||
fi
|
||||
}
|
||||
report 'build (Host tsc/tsdown, Client tsc/tsdown)' "$build_status" \
|
||||
report 'build (Host tsc/tsdown, desktop tsdown, Client tsc/tsdown)' "$build_status" \
|
||||
"$scratch/logs/host-tsc.log" \
|
||||
"$scratch/logs/host-tsdown.log" \
|
||||
"$scratch/logs/desktop-tsdown.log" \
|
||||
"$scratch/logs/client-tsc.log" \
|
||||
"$scratch/logs/client-tsdown.log"
|
||||
report 'production site (vitepress build)' "$site_status" "$scratch/logs/site.log"
|
||||
|
||||
Reference in new issue
Block a user