| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393 |
- import { createHash } from 'node:crypto'
- import { execFileSync, spawnSync } from 'node:child_process'
- import {
- existsSync,
- globSync,
- mkdirSync,
- readFileSync,
- statSync,
- writeFileSync,
- } from 'node:fs'
- import { dirname, resolve } from 'node:path'
- /** Prefix reserved for build-time values that may be embedded in browser artifacts. */
- const CLIENT_BUILD_ENV_PREFIX = 'DSH_CLIENT_'
- /** Non-public selector used by build orchestration to request a named client profile. */
- export const CLIENT_BUILD_PROFILE_SELECTOR = 'DSH_BUILD_CLIENT_PROFILE'
- /** Public client environment required by official DSH artifacts. */
- const OFFICIAL_CLIENT_BUILD_ENVIRONMENT = {
- DSH_CLIENT_BUILD_PROFILE: 'official',
- DSH_CLIENT_TITLE: 'DeepSeek Harness',
- } as const
- /** Public variable carrying the source commit embedded in client artifacts. */
- const CLIENT_COMMIT_HASH_VARIABLE = 'DSH_CLIENT_COMMIT_HASH'
- /** Public variable carrying the repository package version embedded in client artifacts. */
- const CLIENT_VERSION_VARIABLE = 'DSH_CLIENT_VERSION'
- /** Repository-relative path of the complete client build record. */
- export const CLIENT_BUILD_RECORD_PATH = '.dsh-build/client-build-environment.json'
- const CLIENT_BUILD_RECORD_FORMAT = 1
- const CLIENT_ARTIFACT_PATTERNS = [
- 'apps/web/dist/**/*',
- 'packages/*/*/lib/client.js',
- 'packages/*/*/lib/client.js.map',
- ] as const
- /** Public values embedded in one set of client artifacts. */
- export type ClientBuildEnvironment = Readonly<Record<string, string>>
- /**
- * Resolve the short source commit used by browser build metadata.
- * @param root - repository root used when no explicit value is supplied.
- * @param environment - environment that may already carry a commit value.
- * @returns lowercase 7-character Git commit prefix.
- */
- export function repositoryCommitHash(root: string, environment: NodeJS.ProcessEnv = process.env): string {
- const explicit = environment[CLIENT_COMMIT_HASH_VARIABLE]
- const value = explicit ?? execFileSync('git', ['rev-parse', 'HEAD'], {
- cwd: root,
- encoding: 'utf8',
- stdio: ['ignore', 'pipe', 'ignore'],
- }).trim()
- if (!/^[0-9a-f]{7,40}$/iu.test(value)) {
- throw new Error(`${CLIENT_COMMIT_HASH_VARIABLE} must be a Git commit hash; got ${JSON.stringify(value)}`)
- }
- return value.slice(0, 7).toLowerCase()
- }
- /**
- * Resolve the repository package version used by browser build metadata.
- * @param root - repository root containing the authoritative package.json.
- * @returns the repository's semver-compatible package version.
- */
- export function repositoryVersion(root: string): string {
- const path = resolve(root, 'package.json')
- let manifest: unknown
- try {
- manifest = JSON.parse(readFileSync(path, 'utf8'))
- } catch (error) {
- const detail = error instanceof Error ? error.message : String(error)
- throw new Error(`cannot read repository version from ${path}: ${detail}`)
- }
- if (!isObject(manifest) || typeof manifest.version !== 'string'
- || !/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(manifest.version)) {
- throw new Error(`repository package.json has an invalid version ${JSON.stringify(isObject(manifest) ? manifest.version : undefined)}`)
- }
- return manifest.version
- }
- /**
- * Read whether Git reports any staged, unstaged, untracked, or submodule change.
- * @param root - repository root whose worktree is inspected.
- * @returns true or false inside a Git worktree; undefined without Git metadata.
- */
- export function repositoryGitDirty(root: string): boolean | undefined {
- const probe = spawnSync('git', ['rev-parse', '--is-inside-work-tree'], {
- cwd: root,
- encoding: 'utf8',
- stdio: ['ignore', 'pipe', 'ignore'],
- })
- if (probe.error !== undefined || probe.status !== 0 || probe.stdout.trim() !== 'true') return undefined
- const status = spawnSync('git', ['status', '--porcelain=v1', '--untracked-files=normal'], {
- cwd: root,
- encoding: 'utf8',
- stdio: ['ignore', 'pipe', 'pipe'],
- })
- if (status.error !== undefined) throw status.error
- if (status.status !== 0) {
- throw new Error(`git status failed in ${root}: ${status.stderr.trim() || String(status.status)}`)
- }
- return status.stdout !== ''
- }
- /**
- * Resolve the public environment for a complete default build from one checkout.
- * Repository-owned metadata replaces inherited values; other public values pass through.
- * @param root - repository root supplying version and Git metadata.
- * @param environment - caller environment supplying optional commit and public extensions.
- * @returns complete public client environment for the default build.
- */
- export function repositoryClientBuildEnvironment(
- root: string,
- environment: NodeJS.ProcessEnv = process.env,
- ): ClientBuildEnvironment {
- const inherited = { ...clientBuildEnvironment(environment) }
- delete inherited.DSH_CLIENT_COMMIT_HASH
- delete inherited.DSH_CLIENT_GIT_DIRTY
- delete inherited.DSH_CLIENT_VERSION
- const dirty = repositoryGitDirty(root)
- return {
- ...inherited,
- DSH_CLIENT_COMMIT_HASH: repositoryCommitHash(root, environment),
- ...(dirty === true ? { DSH_CLIENT_GIT_DIRTY: 'true' } : {}),
- DSH_CLIENT_VERSION: repositoryVersion(root),
- }
- }
- /**
- * Resolve the exact public values required by an official build at one commit.
- * @param root - repository root whose HEAD must match the built source.
- * @param environment - optional explicit commit source for non-Git build environments.
- * @returns complete official client environment.
- */
- export function officialClientBuildEnvironment(
- root: string,
- environment: NodeJS.ProcessEnv = process.env,
- ): Readonly<Record<`DSH_CLIENT_${string}`, string>> {
- return {
- DSH_CLIENT_COMMIT_HASH: repositoryCommitHash(root, environment),
- DSH_CLIENT_VERSION: repositoryVersion(root),
- ...OFFICIAL_CLIENT_BUILD_ENVIRONMENT,
- }
- }
- /** Digest of every client artifact produced by the complete root build. */
- interface ClientArtifactDigest {
- /** Number of files covered by the digest. */
- readonly fileCount: number
- /** Lowercase SHA-256 digest of sorted paths and file contents. */
- readonly sha256: string
- }
- /** Durable description of one complete root client build. */
- export interface ClientBuildRecord {
- /** Record schema version. */
- readonly formatVersion: number
- /** Exact public environment embedded by Vite and tsdown. */
- readonly environment: ClientBuildEnvironment
- /** Digest that binds the environment to the current artifacts. */
- readonly artifacts: ClientArtifactDigest
- }
- /**
- * Collect the public client environment in deterministic key order.
- * @param environment - environment inherited by the build process.
- * @returns defined `DSH_CLIENT_*` values only.
- */
- function clientBuildEnvironment(environment: NodeJS.ProcessEnv): ClientBuildEnvironment {
- return Object.fromEntries(Object.entries(environment)
- .filter(([name, value]) => name.startsWith(CLIENT_BUILD_ENV_PREFIX) && value !== undefined)
- .sort(([left], [right]) => left.localeCompare(right))) as Record<string, string>
- }
- /**
- * Resolve the exact public environment selected for a complete client build.
- * @param environment - parent process environment.
- * @param profile - explicit profile, or the non-public selector when omitted.
- * @returns the inherited public values when no profile is selected, otherwise the named profile.
- */
- export function resolveClientBuildEnvironment(
- environment: NodeJS.ProcessEnv,
- profile: string | undefined = environment[CLIENT_BUILD_PROFILE_SELECTOR],
- ): ClientBuildEnvironment {
- if (profile === undefined) return clientBuildEnvironment(environment)
- if (profile === 'official') {
- const commitHash = environment[CLIENT_COMMIT_HASH_VARIABLE]
- const version = environment[CLIENT_VERSION_VARIABLE]
- if (commitHash === undefined) {
- throw new Error(`${CLIENT_COMMIT_HASH_VARIABLE} is required for the official client build profile`)
- }
- if (version === undefined) {
- throw new Error(`${CLIENT_VERSION_VARIABLE} is required for the official client build profile`)
- }
- return {
- DSH_CLIENT_COMMIT_HASH: commitHash,
- DSH_CLIENT_VERSION: version,
- ...OFFICIAL_CLIENT_BUILD_ENVIRONMENT,
- }
- }
- throw new Error(`unknown client build profile ${JSON.stringify(profile)}; expected "official"`)
- }
- /**
- * Construct a subprocess environment containing exactly the selected public values.
- * @param environment - parent process environment.
- * @param clientEnvironment - complete public environment selected for the build.
- * @returns the parent environment with selectors and inherited public values replaced.
- */
- export function clientBuildProcessEnvironment(
- environment: NodeJS.ProcessEnv,
- clientEnvironment: ClientBuildEnvironment,
- ): NodeJS.ProcessEnv {
- const child: NodeJS.ProcessEnv = {}
- for (const [name, value] of Object.entries(environment)) {
- if (name === CLIENT_BUILD_PROFILE_SELECTOR || name.startsWith(CLIENT_BUILD_ENV_PREFIX)) continue
- child[name] = value
- }
- return { ...child, ...clientEnvironment }
- }
- /**
- * Require the public client environment to match an artifact profile exactly.
- *
- * An exact key set matters because every prefixed value is eligible for
- * inlining: an unexpected variable can change published bytes just as surely
- * as a missing or incorrect required value.
- *
- * @param environment - public environment from a build process or build record.
- * @param expected - complete public client environment for the artifact profile.
- */
- export function assertClientBuildEnvironment(
- environment: Readonly<Record<string, string | undefined>>,
- expected: Readonly<Record<`DSH_CLIENT_${string}`, string>>,
- ): void {
- const actual = Object.fromEntries(Object.entries(environment)
- .filter(([name, value]) => name.startsWith(CLIENT_BUILD_ENV_PREFIX) && value !== undefined)
- .sort(([left], [right]) => left.localeCompare(right)))
- const normalizedExpected = Object.fromEntries(Object.entries(expected)
- .sort(([left], [right]) => left.localeCompare(right)))
- if (JSON.stringify(actual) === JSON.stringify(normalizedExpected)) return
- const names = [...new Set([...Object.keys(actual), ...Object.keys(normalizedExpected)])].sort()
- const differences = names.filter(name => actual[name] !== normalizedExpected[name])
- throw new Error(`client build environment differs from the required artifact profile: ${differences.join(', ')}`)
- }
- /**
- * Create bundler substitutions for public client build environment variables.
- *
- * The empty `process.env` fallback makes an unset static property read
- * evaluate to `undefined` without providing a browser `process` global.
- * Exact substitutions remain longer matches than that fallback. Dynamic
- * property reads and enumeration deliberately observe the empty object.
- *
- * @param environment - environment inherited by the build process.
- * @returns deterministic Vite/tsdown `define` expressions.
- */
- export function clientBuildEnvironmentDefines(
- environment: NodeJS.ProcessEnv,
- ): Record<string, string> {
- const defines: Record<string, string> = { 'process.env': '{}' }
- for (const [name, value] of Object.entries(clientBuildEnvironment(environment))) {
- defines[`process.env.${name}`] = JSON.stringify(value)
- }
- return defines
- }
- /**
- * Write the build record after a complete root build succeeds.
- * @param root - repository root containing the generated artifacts.
- * @param environment - exact public environment supplied to both bundlers.
- * @returns the record written to disk.
- */
- export function writeClientBuildRecord(
- root: string,
- environment: ClientBuildEnvironment,
- ): ClientBuildRecord {
- const record: ClientBuildRecord = {
- formatVersion: CLIENT_BUILD_RECORD_FORMAT,
- environment: clientBuildEnvironment(environment),
- artifacts: clientArtifactDigest(root),
- }
- const path = resolve(root, CLIENT_BUILD_RECORD_PATH)
- mkdirSync(dirname(path), { recursive: true })
- writeFileSync(path, `${JSON.stringify(record, null, 2)}\n`)
- return record
- }
- /**
- * Read a complete build record and prove it still describes the current artifacts.
- * @param root - repository root containing the record and generated artifacts.
- * @param expected - optional exact public environment required by a consumer.
- * @returns the parsed and artifact-verified record.
- */
- export function readClientBuildRecord(
- root: string,
- expected?: Readonly<Record<`DSH_CLIENT_${string}`, string>>,
- ): ClientBuildRecord {
- const path = resolve(root, CLIENT_BUILD_RECORD_PATH)
- if (!existsSync(path)) {
- throw new Error(`client build record ${CLIENT_BUILD_RECORD_PATH} is missing; run a complete pnpm run build first`)
- }
- let parsed: unknown
- try {
- parsed = JSON.parse(readFileSync(path, 'utf8'))
- } catch (error) {
- const detail = error instanceof Error ? error.message : String(error)
- throw new Error(`client build record ${CLIENT_BUILD_RECORD_PATH} is invalid JSON: ${detail}`)
- }
- const record = parseClientBuildRecord(parsed)
- if (expected !== undefined) assertClientBuildEnvironment(record.environment, expected)
- const current = clientArtifactDigest(root)
- if (current.fileCount !== record.artifacts.fileCount || current.sha256 !== record.artifacts.sha256) {
- throw new Error(
- `client artifacts differ from ${CLIENT_BUILD_RECORD_PATH}; run a complete pnpm run build before consuming them`,
- )
- }
- return record
- }
- /** Return the deterministic digest of every artifact affected by the public client environment. */
- function clientArtifactDigest(root: string): ClientArtifactDigest {
- const paths = globSync([...CLIENT_ARTIFACT_PATTERNS], { cwd: root })
- .map(path => path.replaceAll('\\', '/'))
- .filter(path => statSync(resolve(root, path)).isFile())
- .sort()
- if (paths.length === 0) throw new Error('complete client build produced no Vite or dynamic client artifacts')
- const digest = createHash('sha256')
- for (const path of paths) {
- const content = readFileSync(resolve(root, path))
- digest.update(`${Buffer.byteLength(path)}:`)
- digest.update(path)
- digest.update(`${content.byteLength}:`)
- digest.update(content)
- }
- return { fileCount: paths.length, sha256: digest.digest('hex') }
- }
- /** Parse and validate the persisted record before any consumer trusts it. */
- function parseClientBuildRecord(value: unknown): ClientBuildRecord {
- if (!isObject(value) || !hasExactKeys(value, ['artifacts', 'environment', 'formatVersion'])) {
- throw new Error(`client build record ${CLIENT_BUILD_RECORD_PATH} has an invalid top-level schema`)
- }
- if (value.formatVersion !== CLIENT_BUILD_RECORD_FORMAT) {
- throw new Error(
- `client build record ${CLIENT_BUILD_RECORD_PATH} uses format ${String(value.formatVersion)}; expected ${String(CLIENT_BUILD_RECORD_FORMAT)}`,
- )
- }
- if (!isObject(value.environment)) {
- throw new Error(`client build record ${CLIENT_BUILD_RECORD_PATH} has an invalid environment`)
- }
- const environment: Record<string, string> = {}
- for (const [name, entry] of Object.entries(value.environment).sort(([left], [right]) => left.localeCompare(right))) {
- if (!name.startsWith(CLIENT_BUILD_ENV_PREFIX) || typeof entry !== 'string') {
- throw new Error(`client build record ${CLIENT_BUILD_RECORD_PATH} has an invalid environment entry ${name}`)
- }
- environment[name] = entry
- }
- if (!isObject(value.artifacts) || !hasExactKeys(value.artifacts, ['fileCount', 'sha256'])) {
- throw new Error(`client build record ${CLIENT_BUILD_RECORD_PATH} has an invalid artifact digest`)
- }
- if (!Number.isSafeInteger(value.artifacts.fileCount) || Number(value.artifacts.fileCount) < 1) {
- throw new Error(`client build record ${CLIENT_BUILD_RECORD_PATH} has an invalid artifact count`)
- }
- if (typeof value.artifacts.sha256 !== 'string' || !/^[0-9a-f]{64}$/.test(value.artifacts.sha256)) {
- throw new Error(`client build record ${CLIENT_BUILD_RECORD_PATH} has an invalid SHA-256 digest`)
- }
- return {
- formatVersion: CLIENT_BUILD_RECORD_FORMAT,
- environment,
- artifacts: {
- fileCount: Number(value.artifacts.fileCount),
- sha256: value.artifacts.sha256,
- },
- }
- }
- function isObject(value: unknown): value is Record<string, unknown> {
- return typeof value === 'object' && value !== null && !Array.isArray(value)
- }
- function hasExactKeys(value: Record<string, unknown>, expected: readonly string[]): boolean {
- const actual = Object.keys(value).sort()
- return actual.length === expected.length && actual.every((key, index) => key === expected[index])
- }
|