build: order Host and Client compilation faces
This commit is contained in:
@@ -15,8 +15,13 @@ interface CssPlugin {
|
||||
}
|
||||
|
||||
function cssPlugin(): CssPlugin {
|
||||
const configs = clientBundle('@deepseek-ai/dsh-client-test', ['lib/types/index.js', 'lib/types/invariant.js'])
|
||||
const plugins = (configs[1] as { plugins: CssPlugin[] }).plugins
|
||||
const configs = clientBundle(
|
||||
'@deepseek-ai/dsh-client-test',
|
||||
['lib/types/index.js', 'lib/types/invariant.js'],
|
||||
)({ env: { DSH_BUILD_FACE: 'client' } })
|
||||
const client = configs.find(config => config.platform === 'browser')
|
||||
if (client === undefined) throw new Error('client config missing')
|
||||
const plugins = (client as { plugins: CssPlugin[] }).plugins
|
||||
const plugin = plugins.find(candidate => candidate.name === 'dsh-css-modules-inline')
|
||||
if (plugin === undefined) throw new Error('CSS Modules plugin missing from client config')
|
||||
return plugin
|
||||
|
||||
@@ -14,6 +14,24 @@ interface CssModulePlugin {
|
||||
load?: (this: { addWatchFile: (id: string) => void }, id: string) => Promise<unknown>
|
||||
}
|
||||
|
||||
function clientConfigs(id = '@deepseek-ai/dsh-client-test') {
|
||||
return clientBundle(id, ['lib/types/index.js', 'lib/types/invariant.js'])(
|
||||
{ env: { DSH_BUILD_FACE: 'client' } },
|
||||
).filter(config => config.platform === 'browser')
|
||||
}
|
||||
|
||||
describe('client bundle build faces', () => {
|
||||
it('watches source in development and consumes emitted JavaScript in the Client build', () => {
|
||||
const bundle = clientBundle('@deepseek-ai/dsh-client-test', ['lib/types/index.js'])
|
||||
const development = bundle({ env: {} }).find(config => config.platform === 'browser')
|
||||
const artifact = bundle({ env: { DSH_BUILD_FACE: 'client' } })
|
||||
.find(config => config.platform === 'browser')
|
||||
|
||||
expect(development?.entry).toEqual({ client: 'src/client/index.ts' })
|
||||
expect(artifact?.entry).toEqual({ client: 'lib/types/client/index.js' })
|
||||
})
|
||||
})
|
||||
|
||||
function clientSourceMapPath(packagePath: string): string {
|
||||
return fileURLToPath(new URL(`../packages/${packagePath}/lib/client.js.map`, import.meta.url))
|
||||
}
|
||||
@@ -21,16 +39,16 @@ function clientSourceMapPath(packagePath: string): string {
|
||||
function purityResolveId(): ResolveId {
|
||||
// libEntry is spelled at every call site (no default) so the
|
||||
// package-invariants text check can see the invariant entry per package.
|
||||
const configs = clientBundle('@deepseek-ai/dsh-client-test', ['lib/types/index.js', 'lib/types/invariant.js'])
|
||||
const plugins = (configs[1] as { plugins: { name: string; resolveId?: unknown }[] }).plugins
|
||||
const configs = clientConfigs()
|
||||
const plugins = (configs[0] as { plugins: { name: string; resolveId?: unknown }[] }).plugins
|
||||
const gate = plugins.find(p => p.name === 'dsh-client-bundle-purity')
|
||||
if (gate?.resolveId === undefined) throw new Error('purity plugin missing from client config')
|
||||
return gate.resolveId as ResolveId
|
||||
}
|
||||
|
||||
function cssModulePlugin(): CssModulePlugin {
|
||||
const configs = clientBundle('@deepseek-ai/dsh-client-test', ['lib/types/index.js', 'lib/types/invariant.js'])
|
||||
const plugins = (configs[1] as { plugins: CssModulePlugin[] }).plugins
|
||||
const configs = clientConfigs()
|
||||
const plugins = (configs[0] as { plugins: CssModulePlugin[] }).plugins
|
||||
const plugin = plugins.find(candidate => candidate.name === 'dsh-css-modules-inline')
|
||||
if (plugin?.resolveId === undefined || plugin.load === undefined) {
|
||||
throw new Error('CSS Modules plugin missing from client config')
|
||||
@@ -87,13 +105,13 @@ describe('client bundle purity gate', () => {
|
||||
|
||||
describe('client bundle debug artifacts', () => {
|
||||
it('emits source maps for plugin TS and TSX outside the Vite module graph', () => {
|
||||
const configs = clientBundle('@deepseek-ai/dsh-client-test', ['lib/types/index.js', 'lib/types/invariant.js'])
|
||||
expect(configs[1]?.sourcemap).toBe(true)
|
||||
const configs = clientConfigs()
|
||||
expect(configs[0]?.sourcemap).toBe(true)
|
||||
})
|
||||
|
||||
it('maps first-party sources to their repository package paths', () => {
|
||||
const configs = clientBundle('@deepseek-ai/dsh-client-ui-goal', ['lib/types/index.js', 'lib/types/invariant.js'])
|
||||
const outputOptions = configs[1]?.outputOptions
|
||||
const configs = clientConfigs('@deepseek-ai/dsh-client-ui-goal')
|
||||
const outputOptions = configs[0]?.outputOptions
|
||||
if (typeof outputOptions !== 'object' || outputOptions === null) throw new Error('client output options missing')
|
||||
const transform = outputOptions.sourcemapPathTransform
|
||||
if (transform === undefined) throw new Error('client sourcemap path transform missing')
|
||||
@@ -105,8 +123,8 @@ describe('client bundle debug artifacts', () => {
|
||||
})
|
||||
|
||||
it('maps dual-face host sources to the host package group', () => {
|
||||
const configs = clientBundle('@deepseek-ai/dsh-host-directory-picker-native', ['lib/types/index.js'])
|
||||
const outputOptions = configs[1]?.outputOptions
|
||||
const configs = clientConfigs('@deepseek-ai/dsh-host-directory-picker-native')
|
||||
const outputOptions = configs[0]?.outputOptions
|
||||
if (typeof outputOptions !== 'object' || outputOptions === null) throw new Error('client output options missing')
|
||||
const transform = outputOptions.sourcemapPathTransform
|
||||
if (transform === undefined) throw new Error('client sourcemap path transform missing')
|
||||
@@ -116,8 +134,8 @@ describe('client bundle debug artifacts', () => {
|
||||
})
|
||||
|
||||
it('maps inlined workspace sources to packages and leaves dependencies outside it unchanged', () => {
|
||||
const configs = clientBundle('@deepseek-ai/dsh-client-connection', ['lib/types/index.js'])
|
||||
const outputOptions = configs[1]?.outputOptions
|
||||
const configs = clientConfigs('@deepseek-ai/dsh-client-connection')
|
||||
const outputOptions = configs[0]?.outputOptions
|
||||
if (typeof outputOptions !== 'object' || outputOptions === null) throw new Error('client output options missing')
|
||||
const transform = outputOptions.sourcemapPathTransform
|
||||
if (transform === undefined) throw new Error('client sourcemap path transform missing')
|
||||
|
||||
@@ -136,22 +136,25 @@ function formatDiagnostics(diagnostics: readonly ts.Diagnostic[], blocks: Block[
|
||||
}
|
||||
|
||||
/**
|
||||
* Reuse the host-aggregate references from a temp project one directory below
|
||||
* root. Doc fragments speak the host vocabulary, so the standalone project
|
||||
* seeds tsconfig.host.json (never the root solution: flattening host+client
|
||||
* into one program collides the cordis Context merges).
|
||||
* Reuse both aggregate reference sets from a temp project one directory below
|
||||
* root. Each referenced package remains its own program, while documentation
|
||||
* examples can import either the Host or Client API.
|
||||
*/
|
||||
function workspaceReferences(): { path: string }[] {
|
||||
const file = join(root, 'tsconfig.host.json')
|
||||
// Parse with TypeScript's own JSONC reader: a regex comment stripper corrupts the `/*/` path
|
||||
// candidate in the workspace wildcard.
|
||||
const result = ts.readConfigFile(file, path => readFileSync(path, 'utf8'))
|
||||
if (result.error) {
|
||||
throw new Error(`doc-typecheck: cannot read ${file}: ${ts.flattenDiagnosticMessageText(result.error.messageText, '\n')}`)
|
||||
const paths = new Set<string>()
|
||||
for (const aggregate of ['tsconfig.host.json', 'tsconfig.client.json']) {
|
||||
const file = join(root, aggregate)
|
||||
// Parse with TypeScript's own JSONC reader: a regex comment stripper corrupts the `/*/` path
|
||||
// candidate in the workspace wildcard.
|
||||
const result = ts.readConfigFile(file, path => readFileSync(path, 'utf8'))
|
||||
if (result.error) {
|
||||
throw new Error(`doc-typecheck: cannot read ${file}: ${ts.flattenDiagnosticMessageText(result.error.messageText, '\n')}`)
|
||||
}
|
||||
// `config` is typed `any` by the TS API; narrow it to the one field read here.
|
||||
const { references } = result.config as { references: { path: string }[] }
|
||||
for (const { path } of references) paths.add(path)
|
||||
}
|
||||
// `config` is typed `any` by the TS API; narrow it to the one field read here.
|
||||
const { references } = result.config as { references: { path: string }[] }
|
||||
return references.map(({ path }) => ({
|
||||
return [...paths].map(path => ({
|
||||
path: path.startsWith('./') ? `../${path.slice(2)}` : `../${path}`,
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -72,6 +72,20 @@ describe('package invariant gate', () => {
|
||||
expect(collectPackageInvariantViolations(fixture())).toEqual([])
|
||||
})
|
||||
|
||||
it('accepts an invariant reference owned by a package-local leaf project', () => {
|
||||
const root = fixture({ invariantReference: false })
|
||||
const dir = join(root, 'packages/core/probe')
|
||||
writeFileSync(join(dir, 'tsconfig.json'), `${JSON.stringify({
|
||||
files: [],
|
||||
references: [{ path: './tsconfig.host.json' }],
|
||||
}, null, 2)}\n`)
|
||||
writeFileSync(join(dir, 'tsconfig.host.json'), `${JSON.stringify({
|
||||
references: [{ path: '../../support/invariants' }],
|
||||
}, null, 2)}\n`)
|
||||
|
||||
expect(collectPackageInvariantViolations(root)).toEqual([])
|
||||
})
|
||||
|
||||
it('rejects missing publication metadata and build output', () => {
|
||||
const violations = collectPackageInvariantViolations(fixture({
|
||||
invariantExport: false,
|
||||
|
||||
@@ -118,11 +118,8 @@ function checkBuild(
|
||||
violations: PackageInvariantViolation[],
|
||||
): void {
|
||||
const tsconfigPath = `${owner.dir}/tsconfig.json`
|
||||
const tsconfig = JSON.parse(readFileSync(resolve(root, tsconfigPath), 'utf8')) as {
|
||||
references?: Array<{ path?: string }>
|
||||
}
|
||||
if (owner.packageName !== '@deepseek-ai/dsh-invariants'
|
||||
&& !tsconfig.references?.some(reference => reference.path === '../../support/invariants')) {
|
||||
&& !projectReferencesInvariants(root, owner.dir, tsconfigPath)) {
|
||||
addViolation(
|
||||
violations,
|
||||
tsconfigPath,
|
||||
@@ -138,6 +135,31 @@ function checkBuild(
|
||||
}
|
||||
}
|
||||
|
||||
function projectReferencesInvariants(root: string, ownerDir: string, entryPath: string): boolean {
|
||||
const ownerRoot = resolve(root, ownerDir)
|
||||
const target = resolve(root, 'packages/support/invariants')
|
||||
const pending = [resolve(root, entryPath)]
|
||||
const visited = new Set<string>()
|
||||
while (pending.length > 0) {
|
||||
const configPath = pending.pop()
|
||||
if (configPath === undefined) break
|
||||
if (visited.has(configPath)) continue
|
||||
visited.add(configPath)
|
||||
const config = JSON.parse(readFileSync(configPath, 'utf8')) as {
|
||||
references?: Array<{ path?: string }>
|
||||
}
|
||||
for (const reference of config.references ?? []) {
|
||||
if (reference.path === undefined) continue
|
||||
const referenced = resolve(dirname(configPath), reference.path)
|
||||
if (referenced === target) return true
|
||||
if (!referenced.startsWith(`${ownerRoot}${sep}`)) continue
|
||||
const childConfig = referenced.endsWith('.json') ? referenced : resolve(referenced, 'tsconfig.json')
|
||||
if (existsSync(childConfig)) pending.push(childConfig)
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function checkSource(
|
||||
owner: PackageInvariantOwner,
|
||||
root: string,
|
||||
|
||||
@@ -204,15 +204,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: generate Host contracts
|
||||
# before either aggregate typecheck, then bundle the completed workspace.
|
||||
# The build preserves the face order from package.json: compile and bundle the
|
||||
# Host face before compiling and bundling the Client face.
|
||||
# Both statuses are captured so one failure cannot hide the other's result.
|
||||
build_gate() {
|
||||
wine_node "$scratch/logs/contracts-tsc.log" "$tsc_js" -b packages/typert/generator --pretty false || return $?
|
||||
wine_node "$scratch/logs/contracts-tsdown.log" "$tsdown_js" --config tsdown.typert-host.config.ts || return $?
|
||||
wine_node "$scratch/logs/host-tsc.log" "$tsc_js" -b tsconfig.host.json --pretty false || return $?
|
||||
wine_node "$scratch/logs/host-tsdown.log" "$tsdown_js" --env.DSH_BUILD_FACE host || return $?
|
||||
wine_node "$scratch/logs/client-tsc.log" "$tsc_js" -b tsconfig.client.json --pretty false || return $?
|
||||
wine_node "$scratch/logs/tsdown.log" "$tsdown_js"
|
||||
wine_node "$scratch/logs/client-tsdown.log" "$tsdown_js" --env.DSH_BUILD_FACE client
|
||||
}
|
||||
site_gate() {
|
||||
cd website
|
||||
@@ -238,12 +237,11 @@ report() {
|
||||
for log in "$@"; do tail -n 200 "$log" >&2 || true; done
|
||||
fi
|
||||
}
|
||||
report 'build (contract prepass, tsc, tsdown)' "$build_status" \
|
||||
"$scratch/logs/contracts-tsc.log" \
|
||||
"$scratch/logs/contracts-tsdown.log" \
|
||||
report 'build (Host tsc/tsdown, Client tsc/tsdown)' "$build_status" \
|
||||
"$scratch/logs/host-tsc.log" \
|
||||
"$scratch/logs/host-tsdown.log" \
|
||||
"$scratch/logs/client-tsc.log" \
|
||||
"$scratch/logs/tsdown.log"
|
||||
"$scratch/logs/client-tsdown.log"
|
||||
report 'production site (vitepress build)' "$site_status" "$scratch/logs/site.log"
|
||||
if (( build_status != 0 )); then exit "$build_status"; fi
|
||||
exit "$site_status"
|
||||
|
||||
Reference in New Issue
Block a user