Files
deepseek-harness/scripts/compile-referenced-projects.ts
T
imccyu 129c915498 build: split build:src and build:test, and schedule build in parallel
pnpm run build ran every step in one serial chain, including the two
noEmit aggregate programs (tests, scripts, benchmarks, website) that no
artifact step consumes, so the Host aggregate check alone delayed tsdown by
about 66 s.

- build:src (scripts/build.ts --scope src) builds every artifact in the
  existing order and writes the client build record; it replaces the
  removed --artifacts-only flag and the libraries CLI mode.
- build:test (--scope test) type-checks only the Host and Client aggregate
  programs with tsc -p, concurrently, without compiling references or
  checking prerequisites.
- build runs both as one dependency graph through run-gates' new
  runAggregate: each aggregate check starts once its face's packages are
  compiled, and Client compilation stays after the Host bundle that writes
  the Typert /remote declarations. DSH_GATE_CONCURRENCY=1 serializes it;
  DSH_GATE_FAIL_FAST=1 stops the remaining steps at the first failure.
  Passing steps print one PASS line (DSH_GATE_VERBOSE=1 shows their output).
- PR artifact builds (e2e, release, preview, Node 22 compatibility) and the
  Python builder (--build-src) use build:src; dev:web and dev:desktop use
  build:src. test:*, bench, typecheck and the required consumer/Windows
  lanes keep the complete build.
- lint, lint:fix, doc-typecheck and the Typert contracts gate prepare with
  host-libraries (Host package compilation + Host bundle) instead of
  build:lib:host; their own checks never read the aggregate root or the
  Desktop bundle.

Parallel and serial build produce byte-identical lib/, Web dist and client
build record; a test-only type error fails build and build:test while
build:src passes.

Refs #5852
2026-10-11 18:19:23 +08:00

192 lines
8.6 KiB
TypeScript

/** Compile and bundle package projects without repeating aggregate test and script typechecks. */
import { spawnSync } from 'node:child_process'
import { relative, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import { parseArgs } from 'node:util'
import ts from 'typescript'
import { pnpmCommand } from './release/process.ts'
type CompilationMode = 'checked' | 'benchmark-emit'
/** Identifier of one package-library build step. */
export type LibraryStepId = 'host-src' | 'host-bundle' | 'desktop-bundle' | 'client-src' | 'client-bundle'
/** One package-library build step as a shell-free child-process invocation. */
export interface LibraryStep {
readonly id: LibraryStepId
/** Name used in progress and failure reports. */
readonly label: string
/** Short command shown in failure reports; reference lists are summarized. */
readonly displayCommand: string
readonly command: string
readonly args: readonly string[]
/** Steps that must succeed first; together they repeat the serial library order. */
readonly needs: readonly LibraryStepId[]
}
/**
* Run one package-manager command serially with the inherited build environment.
* @param root - Repository root supplying the command's working directory.
* @param args - Package-manager arguments, without shell quoting.
* @throws If the command cannot start or exits unsuccessfully.
*/
export function runPnpmCommand(root: string, args: readonly string[]): void {
const [command, ...prefix] = pnpmCommand()
const result = spawnSync(command, [...prefix, ...args], { cwd: root, stdio: 'inherit' })
if (result.error !== undefined) throw result.error
if (result.status !== 0) {
throw new Error(`compile-referenced-projects: pnpm ${args.join(' ')} exited with ${String(result.status ?? result.signal)}`)
}
}
/**
* Resolve the Node arguments that compile one aggregate's project references with the serial TypeScript builder.
* @param root - Repository root containing the aggregate tsconfig.
* @param face - Independent compiler face whose package projects are emitted.
* @param mode - Check package types by default; benchmark emission defers diagnostics to required full builds.
* @returns Arguments for `process.execPath`, including the heap limit the Host face needs.
* @throws If the aggregate configuration is invalid, has no references, or can emit files.
*/
export function referencedProjectsCompilerArgs(
root: string,
face: 'host' | 'client',
mode: CompilationMode = 'checked',
): string[] {
const configPath = resolve(root, `tsconfig.${face}.json`)
const config = ts.getParsedCommandLineOfConfigFile(configPath, {}, {
...ts.sys,
onUnRecoverableConfigFileDiagnostic(diagnostic) {
throw new Error(ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n'))
},
})
if (config === undefined) throw new Error(`compile-referenced-projects: cannot read ${configPath}`)
const references = config.projectReferences
if (references === undefined || references.length === 0) {
throw new Error(`compile-referenced-projects: ${configPath} requires non-empty project references`)
}
if (config.errors.length > 0) {
throw new Error(config.errors
.map(diagnostic => ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n'))
.join('\n'))
}
if (config.options.noEmit !== true) {
throw new Error(`compile-referenced-projects: ${configPath} requires noEmit: true to exclude its aggregate program`)
}
return [
...hostHeapArgs(face),
fileURLToPath(import.meta.resolve('typescript/bin/tsc')),
'-b',
...mode === 'benchmark-emit' ? ['--noCheck'] : [],
// Relative arguments keep the complete workspace graph below Windows' command-line limit.
...references.map(reference => relative(root, reference.path)),
]
}
/**
* Resolve the Node arguments that type-check one aggregate's own test and script program in project mode.
* Project mode reads the declarations that its references already emitted and never compiles a reference,
* so missing package outputs surface as compiler diagnostics.
* @param face - Compiler face whose aggregate program is checked.
* @returns Arguments for `process.execPath`, run from the repository root.
*/
export function aggregateCheckArgs(face: 'host' | 'client'): string[] {
return [...hostHeapArgs(face), fileURLToPath(import.meta.resolve('typescript/bin/tsc')), '-p', `tsconfig.${face}.json`]
}
function hostHeapArgs(face: 'host' | 'client'): string[] {
return face === 'host' ? ['--max-old-space-size=4096'] : []
}
/**
* Compile one aggregate's project references with the serial TypeScript builder.
* @param root - Repository root containing the aggregate tsconfig.
* @param face - Independent compiler face whose package projects are emitted.
* @param mode - Check package types by default; benchmark emission defers diagnostics to required full builds.
* @throws If the aggregate emits files, its configuration is invalid, or package compilation fails.
*/
export function compileReferencedProjects(
root: string,
face: 'host' | 'client',
mode: CompilationMode = 'checked',
): void {
runStep(root, compilerStep(root, face, mode))
}
function compilerStep(root: string, face: 'host' | 'client', mode: CompilationMode): LibraryStep {
return {
id: face === 'host' ? 'host-src' : 'client-src',
label: `${face} compiler`,
displayCommand: `tsc -b${mode === 'benchmark-emit' ? ' --noCheck' : ''} <tsconfig.${face}.json references>`,
command: process.execPath,
args: referencedProjectsCompilerArgs(root, face, mode),
needs: face === 'host' ? [] : ['desktop-bundle'],
}
}
function pnpmStep(id: LibraryStepId, label: string, args: readonly string[], needs: readonly LibraryStepId[]): LibraryStep {
const [command, ...prefix] = pnpmCommand()
return { id, label, displayCommand: `pnpm ${args.join(' ')}`, command, args: [...prefix, ...args], needs }
}
/**
* List the package-library build steps in Host, Desktop, then Client dependency order.
* Host bundling runs Typert, whose Remote declarations Client compilation imports.
* @param root - Repository root containing the compiler and bundle configurations.
* @param faces - `host` lists Host compilation and bundling, which produce the Typert outputs; `all` adds Desktop and Client.
* @param mode - Check package types by default; benchmark emission defers diagnostics to required full builds.
* @returns The ordered steps; each step needs its predecessor.
* @throws If an aggregate configuration is invalid.
*/
export function libraryBuildSteps(root: string, faces: 'host' | 'all', mode: CompilationMode = 'checked'): LibraryStep[] {
const host: LibraryStep[] = [
compilerStep(root, 'host', mode),
pnpmStep('host-bundle', 'Host bundle', ['exec', 'tsdown', '--config-loader', 'native', '--env.DSH_BUILD_FACE', 'host'], ['host-src']),
]
if (faces === 'host') return host
return [
...host,
pnpmStep('desktop-bundle', 'Desktop bundle', ['--filter', '@deepseek-ai/dsh-desktop', 'run', 'bundle'], ['host-bundle']),
compilerStep(root, 'client', mode),
pnpmStep('client-bundle', 'Client bundle', ['exec', 'tsdown', '--config-loader', 'native', '--env.DSH_BUILD_FACE', 'client'], ['client-src']),
]
}
function runStep(root: string, step: LibraryStep): void {
const result = spawnSync(step.command, step.args, { cwd: root, stdio: 'inherit' })
if (result.error !== undefined) throw result.error
if (result.status !== 0) {
throw new Error(`compile-referenced-projects: ${step.label} exited with ${String(result.status ?? result.signal)}`)
}
}
/**
* Emit package libraries serially in Host, Desktop, then Client dependency order.
* @param root - Repository root containing the compiler and bundle configurations.
* @param hostOnly - Stop after Host compilation and bundling, which produce the Typert outputs.
* @param mode - Check package types by default; benchmark emission defers diagnostics to required full builds.
* @throws If any compiler or bundler fails.
*/
export function buildLibraryArtifacts(root: string, hostOnly: boolean, mode: CompilationMode = 'checked'): void {
for (const step of libraryBuildSteps(root, hostOnly ? 'host' : 'all', mode)) runStep(root, step)
}
if (import.meta.main) {
const usage = 'compile-referenced-projects: expected exactly one mode: host, client, or host-libraries'
const { positionals } = parseArgs({ allowPositionals: true })
const [mode] = positionals
if (positionals.length !== 1) throw new Error(usage)
const root = resolve(import.meta.dirname, '..')
switch (mode) {
case 'host':
case 'client':
compileReferencedProjects(root, mode)
break
case 'host-libraries':
buildLibraryArtifacts(root, true)
break
default:
throw new Error(usage)
}
}