112 lines
4.9 KiB
TypeScript
112 lines
4.9 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'
|
|
|
|
/**
|
|
* 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)}`)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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 {
|
|
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`)
|
|
}
|
|
const compiler = fileURLToPath(import.meta.resolve('typescript/bin/tsc'))
|
|
const result = spawnSync(process.execPath, [
|
|
...face === 'host' ? ['--max-old-space-size=4096'] : [],
|
|
compiler,
|
|
'-b',
|
|
...mode === 'benchmark-emit' ? ['--noCheck'] : [],
|
|
// Relative arguments keep the complete workspace graph below Windows' command-line limit.
|
|
...references.map(reference => relative(root, reference.path)),
|
|
], { cwd: root, stdio: 'inherit' })
|
|
if (result.error !== undefined) throw result.error
|
|
if (result.status !== 0) {
|
|
throw new Error(`compile-referenced-projects: ${face} compiler exited with ${String(result.status ?? result.signal)}`)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Emit package libraries in Host, Desktop, then Client dependency order.
|
|
* @param root - Repository root containing the compiler and bundle configurations.
|
|
* @param hostOnly - Omit the Client compiler and bundler after the Host artifacts are ready.
|
|
* @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 {
|
|
compileReferencedProjects(root, 'host', mode)
|
|
runPnpmCommand(root, ['exec', 'tsdown', '--config-loader', 'native', '--env.DSH_BUILD_FACE', 'host'])
|
|
runPnpmCommand(root, ['--filter', '@deepseek-ai/dsh-desktop', 'run', 'bundle'])
|
|
if (hostOnly) return
|
|
compileReferencedProjects(root, 'client', mode)
|
|
runPnpmCommand(root, ['exec', 'tsdown', '--config-loader', 'native', '--env.DSH_BUILD_FACE', 'client'])
|
|
}
|
|
|
|
if (import.meta.main) {
|
|
const { positionals } = parseArgs({ allowPositionals: true })
|
|
const [mode] = positionals
|
|
if (positionals.length !== 1) {
|
|
throw new Error('compile-referenced-projects: expected exactly one mode: host, client, host-libraries, or libraries')
|
|
}
|
|
const root = resolve(import.meta.dirname, '..')
|
|
switch (mode) {
|
|
case 'host':
|
|
case 'client':
|
|
compileReferencedProjects(root, mode)
|
|
break
|
|
case 'host-libraries':
|
|
buildLibraryArtifacts(root, true)
|
|
break
|
|
case 'libraries':
|
|
buildLibraryArtifacts(root, false)
|
|
break
|
|
default:
|
|
throw new Error('compile-referenced-projects: expected exactly one mode: host, client, host-libraries, or libraries')
|
|
}
|
|
}
|