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
192 lines
8.6 KiB
TypeScript
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)
|
|
}
|
|
}
|