build: order Host and Client compilation faces
This commit is contained in:
+3
-4
@@ -16,13 +16,12 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "npm run build:lib && npm run build:web",
|
"build": "npm run build:lib && npm run build:web",
|
||||||
"build:lib": "npm run build:lib:host && npm run build:lib:client",
|
"build:lib": "npm run build:lib:host && npm run build:lib:client",
|
||||||
"build:lib:host": "npm run build:lib:contracts && tsc -b tsconfig.host.json",
|
"build:lib:host": "tsc -b tsconfig.host.json && tsdown --env.DSH_BUILD_FACE host",
|
||||||
"build:lib:contracts": "tsc -b packages/typert/generator && tsdown --config tsdown.typert-host.config.ts",
|
"build:lib:client": "tsc -b tsconfig.client.json && tsdown --env.DSH_BUILD_FACE client",
|
||||||
"build:lib:client": "tsc -b tsconfig.client.json && tsdown",
|
|
||||||
"build:web": "pnpm --filter @deepseek-ai/dsh-frontend run build",
|
"build:web": "pnpm --filter @deepseek-ai/dsh-frontend run build",
|
||||||
"clean": "tsx scripts/clean.ts",
|
"clean": "tsx scripts/clean.ts",
|
||||||
"change-scope": "tsx scripts/change-scope.ts",
|
"change-scope": "tsx scripts/change-scope.ts",
|
||||||
"typecheck": "npm run build:lib:contracts && tsc -b",
|
"typecheck": "npm run build:lib:host && tsc -b tsconfig.client.json",
|
||||||
"lint": "tsx scripts/run-oxlint.ts .",
|
"lint": "tsx scripts/run-oxlint.ts .",
|
||||||
"lint:fix": "eslint --config eslint.format.config.mjs --fix . && tsx scripts/run-oxlint.ts . --fix",
|
"lint:fix": "eslint --config eslint.format.config.mjs --fix . && tsx scripts/run-oxlint.ts . --fix",
|
||||||
"duplication": "jscpd --config .jscpd.json packages scripts",
|
"duplication": "jscpd --config .jscpd.json packages scripts",
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../../tsconfig.base.client.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"rootDir": "src",
|
||||||
|
"outDir": "lib/types",
|
||||||
|
"tsBuildInfoFile": "lib/tsconfig.client.tsbuildinfo"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"src/client/index.ts"
|
||||||
|
],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "../../../vendor/cordis"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "../../goal/goal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "../../typert/type-meta"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../../tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"rootDir": "src",
|
||||||
|
"outDir": "lib/types",
|
||||||
|
"tsBuildInfoFile": "lib/tsconfig.host.tsbuildinfo"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"src/agent-lookup.ts",
|
||||||
|
"src/index.ts",
|
||||||
|
"src/invariant.ts"
|
||||||
|
],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "../../../vendor/cordis"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "../../core/agent"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "../../core/session"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "../../session-persistence/session-persistence"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "../../support/invariants"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "../../typert/registry"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "../../typert/type-meta"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,42 +1,11 @@
|
|||||||
{
|
{
|
||||||
"extends": "../../../tsconfig.base.client.json",
|
"files": [],
|
||||||
"compilerOptions": {
|
|
||||||
"rootDir": "src",
|
|
||||||
"outDir": "lib/types"
|
|
||||||
},
|
|
||||||
"include": [
|
|
||||||
"src"
|
|
||||||
],
|
|
||||||
"references": [
|
"references": [
|
||||||
{
|
{
|
||||||
"path": "../../../vendor/cordis"
|
"path": "./tsconfig.host.json"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "../../core/agent"
|
"path": "./tsconfig.client.json"
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "../../core/session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "../../session-persistence/session-persistence"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "../../typert/type-meta"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "../../typert/registry"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "../../ui/commands"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "../../goal/goal"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "../../session-title/session-title"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "../../support/invariants"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -1,3 +1,7 @@
|
|||||||
import { clientBundle } from '../../client/tsdown.client.ts'
|
import { clientBundle } from '../../client/tsdown.client.ts'
|
||||||
|
|
||||||
export default clientBundle('@deepseek-ai/dsh-api-remotes', ['lib/types/index.js', 'lib/types/invariant.js'])
|
export default clientBundle(
|
||||||
|
'@deepseek-ai/dsh-api-remotes',
|
||||||
|
['lib/types/index.js', 'lib/types/invariant.js'],
|
||||||
|
{ hostPhase: true },
|
||||||
|
)
|
||||||
@@ -25,7 +25,6 @@
|
|||||||
"dshClient": {
|
"dshClient": {
|
||||||
"inject": [
|
"inject": [
|
||||||
"@deepseek-ai/dsh-client-connection",
|
"@deepseek-ai/dsh-client-connection",
|
||||||
"@deepseek-ai/dsh-api-remotes",
|
|
||||||
"@deepseek-ai/dsh-typert-registry"
|
"@deepseek-ai/dsh-typert-registry"
|
||||||
],
|
],
|
||||||
"platform": "web",
|
"platform": "web",
|
||||||
@@ -49,14 +48,12 @@
|
|||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@deepseek-ai/dsh-invariants": "^0.0.1",
|
"@deepseek-ai/dsh-invariants": "^0.0.1",
|
||||||
"@deepseek-ai/dsh-api-remotes": "^0.0.1",
|
|
||||||
"@deepseek-ai/dsh-type-meta": "^0.0.1",
|
"@deepseek-ai/dsh-type-meta": "^0.0.1",
|
||||||
"@deepseek-ai/dsh-typert-registry": "^0.0.1",
|
"@deepseek-ai/dsh-typert-registry": "^0.0.1",
|
||||||
"cordis": "^4.0.0-rc.7"
|
"cordis": "^4.0.0-rc.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||||
"@deepseek-ai/dsh-api-remotes": "workspace:^",
|
|
||||||
"@deepseek-ai/dsh-timeout": "workspace:^",
|
"@deepseek-ai/dsh-timeout": "workspace:^",
|
||||||
"@deepseek-ai/dsh-type-meta": "workspace:^",
|
"@deepseek-ai/dsh-type-meta": "workspace:^",
|
||||||
"@deepseek-ai/dsh-typert-registry": "workspace:^",
|
"@deepseek-ai/dsh-typert-registry": "workspace:^",
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
/** Browser runtime services for slots, sessions, workspaces, and connection-stream delivery. */
|
/** Browser runtime services for slots, sessions, workspaces, and connection-stream delivery. */
|
||||||
import type { Context } from 'cordis'
|
import type { Context } from 'cordis'
|
||||||
import type { ConnectionHandle, SessionId } from '@deepseek-ai/dsh-client-connection/client'
|
import type { ConnectionHandle, SessionId } from '@deepseek-ai/dsh-client-connection/client'
|
||||||
import type {} from '@deepseek-ai/dsh-api-remotes/client'
|
|
||||||
import type { TypeRTContext } from '@deepseek-ai/dsh-type-meta'
|
import type { TypeRTContext } from '@deepseek-ai/dsh-type-meta'
|
||||||
import type { MaybeSnapshotSelectorHook, SnapshotSelectorHook } from '@deepseek-ai/dsh-client-ui-slots'
|
import type { MaybeSnapshotSelectorHook, SnapshotSelectorHook } from '@deepseek-ai/dsh-client-ui-slots'
|
||||||
import { SlotsService } from './slots.ts'
|
import { SlotsService } from './slots.ts'
|
||||||
@@ -179,8 +178,8 @@ declare module 'cordis' {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Required services: the Remote root, wire handle, and Client TypeRT registry. */
|
/** Required services: the wire handle and Client TypeRT registry. */
|
||||||
export const inject = ['remote', 'connection', 'typert']
|
export const inject = ['connection', 'typert']
|
||||||
|
|
||||||
/** Mounts the browser runtime services and connection stream.
|
/** Mounts the browser runtime services and connection stream.
|
||||||
* @param ctx - Client Cordis context.
|
* @param ctx - Client Cordis context.
|
||||||
|
|||||||
@@ -20,9 +20,6 @@
|
|||||||
{
|
{
|
||||||
"path": "../connection"
|
"path": "../connection"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"path": "../../api/remotes"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"path": "../../host/apiproxy"
|
"path": "../../host/apiproxy"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { clientLibrary } from '../tsdown.client.ts'
|
||||||
|
|
||||||
|
export default clientLibrary(
|
||||||
|
'@deepseek-ai/dsh-client-schema-form',
|
||||||
|
['lib/types/index.js', 'lib/types/invariant.js'],
|
||||||
|
)
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { clientLibrary } from '../tsdown.client.ts'
|
||||||
|
|
||||||
|
export default clientLibrary(
|
||||||
|
'@deepseek-ai/dsh-client-test-runtime',
|
||||||
|
['lib/types/index.js', 'lib/types/invariant.js'],
|
||||||
|
)
|
||||||
@@ -9,6 +9,7 @@
|
|||||||
* The virtual loader registers each real stylesheet as a watch dependency.
|
* The virtual loader registers each real stylesheet as a watch dependency.
|
||||||
*/
|
*/
|
||||||
import { readFile } from 'node:fs/promises'
|
import { readFile } from 'node:fs/promises'
|
||||||
|
import { existsSync } from 'node:fs'
|
||||||
import { basename, dirname, relative, resolve as resolvePath, sep } from 'node:path'
|
import { basename, dirname, relative, resolve as resolvePath, sep } from 'node:path'
|
||||||
import { fileURLToPath } from 'node:url'
|
import { fileURLToPath } from 'node:url'
|
||||||
import type { UserConfig } from 'tsdown'
|
import type { UserConfig } from 'tsdown'
|
||||||
@@ -34,6 +35,12 @@ export const INLINE_SAFE = /^@deepseek-ai\/dsh-(host-apiproxy|session|llm|tools|
|
|||||||
/** Generated descriptor/codec contribution with no shared runtime identity. */
|
/** Generated descriptor/codec contribution with no shared runtime identity. */
|
||||||
const GENERATED_REMOTE = /^@deepseek-ai\/dsh-[a-z0-9]+(?:-[a-z0-9]+)*\/remote$/
|
const GENERATED_REMOTE = /^@deepseek-ai\/dsh-[a-z0-9]+(?:-[a-z0-9]+)*\/remote$/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Workspace mode replaces an empty config array with the root defaults. A
|
||||||
|
* falsey entry instead removes this package before entry resolution.
|
||||||
|
*/
|
||||||
|
const SKIP_WORKSPACE_BUILD: UserConfig = { entry: '' }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Documented TEMPORARY exemption, not a platform module (hence not in
|
* Documented TEMPORARY exemption, not a platform module (hence not in
|
||||||
* platform.ts): the snapshot-store engine (createSnapshotStore/defineStore/
|
* platform.ts): the snapshot-store engine (createSnapshotStore/defineStore/
|
||||||
@@ -61,19 +68,83 @@ function browserSourcePath(source: string, sourcemapPath: string): string {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Build the tsdown config for one UI plugin package: the node-half lib build
|
* Build the tsdown config for one UI plugin package: the node-half lib build
|
||||||
* plus the browser client bundle. A package-level tsdown.config.ts REPLACES
|
* plus the browser client bundle. Client packages emit both halves during the
|
||||||
* the root workspace shape, so the lib half must be restated here — dropping
|
* Client pass by default; packages needed for Host reflection may opt into the
|
||||||
* it leaves the package without lib/index.js and the host Loader cannot
|
* earlier Host pass. A package-level tsdown.config.ts REPLACES the root
|
||||||
* import its node half.
|
* workspace shape, so the lib half must be restated here — dropping it leaves
|
||||||
|
* the package without lib/index.js and the host Loader cannot import its node
|
||||||
|
* half.
|
||||||
* @param id - plugin id (package name), stamped into the __ModuleLoader__.load
|
* @param id - plugin id (package name), stamped into the __ModuleLoader__.load
|
||||||
* handoff and onto the injected style tags.
|
* handoff and onto the injected style tags.
|
||||||
* @param libEntry - node-half entries, spelled at the call site so the
|
* @param libEntry - node-half entries, spelled at the call site so the
|
||||||
* package-invariants gate can see `lib/types/invariant.js` in each package's
|
* package-invariants gate can see `lib/types/invariant.js` in each package's
|
||||||
* own tsdown.config.ts (a preset-side glob hides it from the mechanical check).
|
* own tsdown.config.ts (a preset-side glob hides it from the mechanical check).
|
||||||
* @returns tsdown user configs emitting lib/*.js and lib/client.js.
|
* @param options - phase placement, lib overrides, and companion Node configs.
|
||||||
|
* @returns ENV-selected tsdown config for the current build face.
|
||||||
*/
|
*/
|
||||||
export function clientBundle(id: string, libEntry: readonly string[]): [UserConfig, UserConfig] {
|
export function clientBundle(
|
||||||
return [{
|
id: string,
|
||||||
|
libEntry: readonly string[],
|
||||||
|
options: ClientBundleOptions = {},
|
||||||
|
): BuildFaceConfig {
|
||||||
|
const lib = clientLibraryConfig(id, libEntry, options.lib)
|
||||||
|
return ({ env }) => {
|
||||||
|
const face = buildFace(env?.DSH_BUILD_FACE)
|
||||||
|
const client = clientConfig(id, face === undefined
|
||||||
|
? 'src/client/index.ts'
|
||||||
|
: 'lib/types/client/index.js')
|
||||||
|
const host = [lib, ...(options.host ?? [])]
|
||||||
|
if (face === 'host') return options.hostPhase === true ? host : [SKIP_WORKSPACE_BUILD]
|
||||||
|
if (face === 'client') return options.hostPhase === true ? [client] : [...host, client]
|
||||||
|
return [...host, client]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a Client-only Node library during the Client pass.
|
||||||
|
* @param id - Package name used in tsdown diagnostics.
|
||||||
|
* @param libEntry - Emitted JavaScript entries consumed from `lib/types`.
|
||||||
|
* @returns ENV-selected tsdown config for the Client build face.
|
||||||
|
*/
|
||||||
|
export function clientLibrary(id: string, libEntry: readonly string[]): BuildFaceConfig {
|
||||||
|
const lib = clientLibraryConfig(id, libEntry)
|
||||||
|
return clientOnly([lib])
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Select arbitrary package-local configs only during the Client pass.
|
||||||
|
* @param configs - Node-side configs emitted after Client tsc.
|
||||||
|
* @returns ENV-selected tsdown config for the Client build face.
|
||||||
|
*/
|
||||||
|
export function clientOnly(configs: readonly UserConfig[]): BuildFaceConfig {
|
||||||
|
return ({ env }) => buildFace(env?.DSH_BUILD_FACE) === 'host'
|
||||||
|
? [SKIP_WORKSPACE_BUILD]
|
||||||
|
: [...configs]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ClientBundleOptions {
|
||||||
|
/** Emit the Node-side artifacts during the Host pass instead of the Client pass. */
|
||||||
|
readonly hostPhase?: boolean
|
||||||
|
readonly host?: readonly UserConfig[]
|
||||||
|
readonly lib?: UserConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
type BuildFace = 'host' | 'client' | undefined
|
||||||
|
|
||||||
|
type BuildFaceConfig = (inlineConfig: Pick<UserConfig, 'env'>) => UserConfig[]
|
||||||
|
|
||||||
|
function buildFace(value: unknown): BuildFace {
|
||||||
|
if (value === undefined || value === 'host' || value === 'client') return value
|
||||||
|
throw new Error(`tsdown: --env.DSH_BUILD_FACE must be host or client, received ${String(value)}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
function clientLibraryConfig(
|
||||||
|
id: string,
|
||||||
|
libEntry: readonly string[],
|
||||||
|
overrides: UserConfig = {},
|
||||||
|
): UserConfig {
|
||||||
|
return {
|
||||||
|
name: id,
|
||||||
entry: [...libEntry],
|
entry: [...libEntry],
|
||||||
outDir: 'lib',
|
outDir: 'lib',
|
||||||
format: ['esm'],
|
format: ['esm'],
|
||||||
@@ -82,8 +153,14 @@ export function clientBundle(id: string, libEntry: readonly string[]): [UserConf
|
|||||||
fixedExtension: false,
|
fixedExtension: false,
|
||||||
dts: false,
|
dts: false,
|
||||||
clean: false,
|
clean: false,
|
||||||
}, {
|
...overrides,
|
||||||
entry: { client: 'src/client/index.ts' },
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clientConfig(id: string, entry: string): UserConfig {
|
||||||
|
return {
|
||||||
|
name: `${id}/client`,
|
||||||
|
entry: { client: entry },
|
||||||
// Browser bundle lands next to the node half (single lib/ artifact dir;
|
// Browser bundle lands next to the node half (single lib/ artifact dir;
|
||||||
// the entryFileNames pin keeps it exactly lib/client.js). clean must stay
|
// the entryFileNames pin keeps it exactly lib/client.js). clean must stay
|
||||||
// off — a default clean would wipe the node-half output emitted above.
|
// off — a default clean would wipe the node-half output emitted above.
|
||||||
@@ -139,7 +216,7 @@ export function clientBundle(id: string, libEntry: readonly string[]): [UserConf
|
|||||||
name: 'dsh-css-modules-inline',
|
name: 'dsh-css-modules-inline',
|
||||||
resolveId(source: string, importer: string | undefined) {
|
resolveId(source: string, importer: string | undefined) {
|
||||||
if (!source.endsWith('.module.css')) return null
|
if (!source.endsWith('.module.css')) return null
|
||||||
const abs = importer !== undefined ? resolvePath(dirname(importer), source) : source
|
const abs = importer !== undefined ? sourceAssetPath(source, importer) : source
|
||||||
return CSS_VIRTUAL_PREFIX + abs + CSS_VIRTUAL_SUFFIX
|
return CSS_VIRTUAL_PREFIX + abs + CSS_VIRTUAL_SUFFIX
|
||||||
},
|
},
|
||||||
async load(virtualId: string) {
|
async load(virtualId: string) {
|
||||||
@@ -182,5 +259,15 @@ export function clientBundle(id: string, libEntry: readonly string[]): [UserConf
|
|||||||
footer: `return module.exports; } });`,
|
footer: `return module.exports; } });`,
|
||||||
intro: 'var module = { exports: {} }; var exports = module.exports;',
|
intro: 'var module = { exports: {} }; var exports = module.exports;',
|
||||||
},
|
},
|
||||||
}]
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Resolve an emitted JS asset import against its source-tree counterpart. */
|
||||||
|
function sourceAssetPath(source: string, importer: string): string {
|
||||||
|
const emitted = resolvePath(dirname(importer), source)
|
||||||
|
if (existsSync(emitted)) return emitted
|
||||||
|
const marker = `${sep}lib${sep}types${sep}`
|
||||||
|
const boundary = emitted.indexOf(marker)
|
||||||
|
if (boundary < 0) return emitted
|
||||||
|
return resolvePath(emitted.slice(0, boundary), 'src', emitted.slice(boundary + marker.length))
|
||||||
}
|
}
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
"path": "../locale"
|
"path": "../locale"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "../../api/remotes"
|
"path": "../../api/remotes/tsconfig.client.json"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "../runtime"
|
"path": "../runtime"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { defineConfig } from 'tsdown'
|
import { clientOnly } from '../tsdown.client.ts'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ui-primitives is browser-only, but its lib bundle IS imported under plain
|
* ui-primitives is browser-only, but its lib bundle IS imported under plain
|
||||||
@@ -8,7 +8,7 @@ import { defineConfig } from 'tsdown'
|
|||||||
* (loader module table / vite source paths), which compile src directly and
|
* (loader module table / vite source paths), which compile src directly and
|
||||||
* never read lib.
|
* never read lib.
|
||||||
*/
|
*/
|
||||||
export default defineConfig({
|
export default clientOnly([{
|
||||||
entry: ['lib/types/index.js', 'lib/types/invariant.js'],
|
entry: ['lib/types/index.js', 'lib/types/invariant.js'],
|
||||||
outDir: 'lib',
|
outDir: 'lib',
|
||||||
format: ['esm'],
|
format: ['esm'],
|
||||||
@@ -28,4 +28,4 @@ export default defineConfig({
|
|||||||
return 'export default {};'
|
return 'export default {};'
|
||||||
},
|
},
|
||||||
}],
|
}],
|
||||||
})
|
}])
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { clientLibrary } from '../tsdown.client.ts'
|
||||||
|
|
||||||
|
export default clientLibrary(
|
||||||
|
'@deepseek-ai/dsh-client-ui-slots',
|
||||||
|
['lib/types/index.js', 'lib/types/invariant.js'],
|
||||||
|
)
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
import { clientBundle } from '../tsdown.client.ts'
|
import { clientBundle } from '../tsdown.client.ts'
|
||||||
|
|
||||||
const [lib, client] = clientBundle(
|
export default clientBundle(
|
||||||
'@deepseek-ai/dsh-client-ui-theme',
|
'@deepseek-ai/dsh-client-ui-theme',
|
||||||
['lib/types/index.js', 'lib/types/invariant.js'],
|
['lib/types/index.js', 'lib/types/invariant.js'],
|
||||||
|
{
|
||||||
|
lib: {
|
||||||
|
copy: [{ from: 'src/styles/*', to: 'lib/styles' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
export default [{
|
|
||||||
...lib,
|
|
||||||
copy: [{ from: 'src/styles/*', to: 'lib/styles' }],
|
|
||||||
}, client]
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { defineConfig } from 'tsdown'
|
import { clientOnly } from '../tsdown.client.ts'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Root and invariant shapes as SEPARATE single-entry bundles: a multi-entry
|
* Root and invariant shapes as SEPARATE single-entry bundles: a multi-entry
|
||||||
@@ -8,7 +8,7 @@ import { defineConfig } from 'tsdown'
|
|||||||
* runtime — browser consumers resolve this package through the loader module
|
* runtime — browser consumers resolve this package through the loader module
|
||||||
* table.
|
* table.
|
||||||
*/
|
*/
|
||||||
export default defineConfig([
|
export default clientOnly([
|
||||||
{
|
{
|
||||||
entry: { index: 'lib/types/index.js' },
|
entry: { index: 'lib/types/index.js' },
|
||||||
outDir: 'lib',
|
outDir: 'lib',
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { defineConfig } from 'tsdown'
|
import { clientOnly } from '../tsdown.client.ts'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Root-shape lib build plus a css stub: the shell's components import
|
* Root-shape lib build plus a css stub: the shell's components import
|
||||||
@@ -8,7 +8,7 @@ import { defineConfig } from 'tsdown'
|
|||||||
* this node lib build stubs every css import to an empty module — importing
|
* this node lib build stubs every css import to an empty module — importing
|
||||||
* the lib under plain node must not crash on an asset specifier.
|
* the lib under plain node must not crash on an asset specifier.
|
||||||
*/
|
*/
|
||||||
export default defineConfig({
|
export default clientOnly([{
|
||||||
entry: ['lib/types/index.js', 'lib/types/invariant.js'],
|
entry: ['lib/types/index.js', 'lib/types/invariant.js'],
|
||||||
outDir: 'lib',
|
outDir: 'lib',
|
||||||
format: ['esm'],
|
format: ['esm'],
|
||||||
@@ -28,4 +28,4 @@ export default defineConfig({
|
|||||||
return 'export default {};'
|
return 'export default {};'
|
||||||
},
|
},
|
||||||
}],
|
}],
|
||||||
})
|
}])
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
"path": "../../../vendor/schemastery"
|
"path": "../../../vendor/schemastery"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "../../api/remotes"
|
"path": "../../api/remotes/tsconfig.host.json"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "../../util/brand"
|
"path": "../../util/brand"
|
||||||
|
|||||||
@@ -3,18 +3,21 @@ import { clientBundle } from '../../client/tsdown.client.ts'
|
|||||||
// The Win32 dialog worker builds as its own CJS entry (mirroring
|
// The Win32 dialog worker builds as its own CJS entry (mirroring
|
||||||
// dsh-workflow-workerthread's worker): path-loaded by the driver, inlining
|
// dsh-workflow-workerthread's worker): path-loaded by the driver, inlining
|
||||||
// the dialog logic while koffi stays an external native require.
|
// the dialog logic while koffi stays an external native require.
|
||||||
export default [
|
export default clientBundle(
|
||||||
...clientBundle('@deepseek-ai/dsh-host-directory-picker-native', ['lib/types/index.js', 'lib/types/invariant.js']),
|
'@deepseek-ai/dsh-host-directory-picker-native',
|
||||||
|
['lib/types/index.js', 'lib/types/invariant.js'],
|
||||||
{
|
{
|
||||||
// The artifact is lib/worker.cjs (the ./worker export the workspace
|
host: [{
|
||||||
// constraint keys on), bundled from the descriptive source entry.
|
// The artifact is lib/worker.cjs (the ./worker export the workspace
|
||||||
entry: { worker: 'lib/types/win32-dialog-worker.js' },
|
// constraint keys on), bundled from the descriptive source entry.
|
||||||
outDir: 'lib',
|
entry: { worker: 'lib/types/win32-dialog-worker.js' },
|
||||||
format: ['cjs'] as ['cjs'],
|
outDir: 'lib',
|
||||||
platform: 'node' as const,
|
format: ['cjs'],
|
||||||
target: 'es2024',
|
platform: 'node',
|
||||||
fixedExtension: false,
|
target: 'es2024',
|
||||||
dts: false,
|
fixedExtension: false,
|
||||||
clean: false,
|
dts: false,
|
||||||
|
clean: false,
|
||||||
|
}],
|
||||||
},
|
},
|
||||||
]
|
)
|
||||||
@@ -476,11 +476,20 @@ export class WorkspaceAnalyzer {
|
|||||||
config: this.caches.config(configPath),
|
config: this.caches.config(configPath),
|
||||||
manifest,
|
manifest,
|
||||||
}
|
}
|
||||||
if (isDualFacePackage(manifest)) {
|
if (!isDualFacePackage(manifest)) {
|
||||||
registrations.push({ ...registration, face: 'host', exportSubpaths: hostExportSubpaths(manifest) })
|
|
||||||
registrations.push({ ...registration, face: 'client', exportSubpaths: clientExportSubpaths(manifest) })
|
|
||||||
} else {
|
|
||||||
registrations.push(registration)
|
registrations.push(registration)
|
||||||
|
} else if (configPath === join(packageRoot, 'tsconfig.json')) {
|
||||||
|
registrations.push(
|
||||||
|
{ ...registration, face: 'host', exportSubpaths: hostExportSubpaths(manifest) },
|
||||||
|
{ ...registration, face: 'client', exportSubpaths: clientExportSubpaths(manifest) },
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
registrations.push({
|
||||||
|
...registration,
|
||||||
|
exportSubpaths: face === 'host'
|
||||||
|
? hostExportSubpaths(manifest)
|
||||||
|
: clientExportSubpaths(manifest),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -864,6 +864,31 @@ describe('WorkspaceAnalyzer', { timeout: 60_000 }, () => {
|
|||||||
.toEqual(['@fixture/host'])
|
.toEqual(['@fixture/host'])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('keeps both runtime faces for an ordinary dshClient project', () => {
|
||||||
|
const root = copyFixture('typert-dual-runtime-')
|
||||||
|
configureDualRuntimeClient(root, false)
|
||||||
|
|
||||||
|
expect(new WorkspaceAnalyzer({ root }).discoverPackages()).toContainEqual({
|
||||||
|
package: '@fixture/client',
|
||||||
|
root: 'packages/client',
|
||||||
|
faces: ['client', 'host'],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('confines explicit face projects to their selected TypeRT face', () => {
|
||||||
|
const root = copyFixture('typert-split-project-')
|
||||||
|
configureDualRuntimeClient(root, true)
|
||||||
|
|
||||||
|
const markers = new WorkspaceAnalyzer({ root }).indexSourceDeclarations()
|
||||||
|
.filter(declaration => declaration.package === '@fixture/client'
|
||||||
|
&& declaration.name.endsWith('OnlyMarker'))
|
||||||
|
.map(declaration => ({ face: declaration.face, name: declaration.name }))
|
||||||
|
expect(markers).toEqual([
|
||||||
|
{ face: 'client', name: 'ClientOnlyMarker' },
|
||||||
|
{ face: 'host', name: 'HostOnlyMarker' },
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
it('accepts package export forms while skipping artifact-only rows and unexported packages', { timeout: 180_000 }, () => {
|
it('accepts package export forms while skipping artifact-only rows and unexported packages', { timeout: 180_000 }, () => {
|
||||||
const root = copyFixture('typert-export-forms-')
|
const root = copyFixture('typert-export-forms-')
|
||||||
const hostRoot = join(root, 'packages/host')
|
const hostRoot = join(root, 'packages/host')
|
||||||
@@ -1193,6 +1218,63 @@ function copyFixture(prefix: string): string {
|
|||||||
return root
|
return root
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function configureDualRuntimeClient(root: string, splitProjects: boolean): void {
|
||||||
|
const packageRoot = join(root, 'packages/client')
|
||||||
|
const manifestPath = join(packageRoot, 'package.json')
|
||||||
|
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8')) as {
|
||||||
|
dshClient?: object
|
||||||
|
exports: Record<string, unknown>
|
||||||
|
}
|
||||||
|
manifest.dshClient = {}
|
||||||
|
manifest.exports['./client'] = {
|
||||||
|
types: './lib/types/client.d.ts',
|
||||||
|
default: './lib/client.js',
|
||||||
|
}
|
||||||
|
writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`)
|
||||||
|
writeFileSync(join(packageRoot, 'src/client.ts'), [
|
||||||
|
"import { Service } from 'cordis'",
|
||||||
|
'export interface ClientOnlyMarker { readonly client: true }',
|
||||||
|
'export class BrowserBridge extends Service {}',
|
||||||
|
"declare module 'cordis' { interface Context { browserBridge: BrowserBridge } }",
|
||||||
|
'',
|
||||||
|
].join('\n'))
|
||||||
|
const indexPath = join(packageRoot, 'src/index.ts')
|
||||||
|
writeFileSync(indexPath, `${readFileSync(indexPath, 'utf8')}\nexport interface HostOnlyMarker { readonly host: true }\n`)
|
||||||
|
if (!splitProjects) return
|
||||||
|
|
||||||
|
const project = JSON.parse(readFileSync(join(packageRoot, 'tsconfig.json'), 'utf8')) as Record<string, unknown>
|
||||||
|
delete project.include
|
||||||
|
writeFileSync(join(packageRoot, 'tsconfig.host.json'), `${JSON.stringify({
|
||||||
|
...project,
|
||||||
|
files: ['src/index.ts'],
|
||||||
|
}, null, 2)}\n`)
|
||||||
|
writeFileSync(join(packageRoot, 'tsconfig.client.json'), `${JSON.stringify({
|
||||||
|
...project,
|
||||||
|
files: ['src/client.ts'],
|
||||||
|
}, null, 2)}\n`)
|
||||||
|
writeFileSync(join(packageRoot, 'tsconfig.json'), `${JSON.stringify({
|
||||||
|
files: [],
|
||||||
|
references: [
|
||||||
|
{ path: './tsconfig.host.json' },
|
||||||
|
{ path: './tsconfig.client.json' },
|
||||||
|
],
|
||||||
|
}, null, 2)}\n`)
|
||||||
|
|
||||||
|
const hostAggregatePath = join(root, 'tsconfig.host.json')
|
||||||
|
const hostAggregate = JSON.parse(readFileSync(hostAggregatePath, 'utf8')) as {
|
||||||
|
references: { path: string }[]
|
||||||
|
}
|
||||||
|
hostAggregate.references.push({ path: './packages/client/tsconfig.host.json' })
|
||||||
|
writeFileSync(hostAggregatePath, `${JSON.stringify(hostAggregate, null, 2)}\n`)
|
||||||
|
|
||||||
|
const clientAggregatePath = join(root, 'tsconfig.client.json')
|
||||||
|
const clientAggregate = JSON.parse(readFileSync(clientAggregatePath, 'utf8')) as {
|
||||||
|
references: { path: string }[]
|
||||||
|
}
|
||||||
|
clientAggregate.references = [{ path: './packages/client/tsconfig.client.json' }]
|
||||||
|
writeFileSync(clientAggregatePath, `${JSON.stringify(clientAggregate, null, 2)}\n`)
|
||||||
|
}
|
||||||
|
|
||||||
function addSameFacePackage(root: string, specifier: string, importedName: string): void {
|
function addSameFacePackage(root: string, specifier: string, importedName: string): void {
|
||||||
const packageRoot = join(root, 'packages/consumer')
|
const packageRoot = join(root, 'packages/consumer')
|
||||||
mkdirSync(join(packageRoot, 'src'), { recursive: true })
|
mkdirSync(join(packageRoot, 'src'), { recursive: true })
|
||||||
|
|||||||
Generated
-3
@@ -1446,9 +1446,6 @@ importers:
|
|||||||
specifier: ~4.4.7
|
specifier: ~4.4.7
|
||||||
version: 4.4.7(@types/react@18.3.31)(immer@10.2.0)(react@18.3.1)
|
version: 4.4.7(@types/react@18.3.31)(immer@10.2.0)(react@18.3.1)
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@deepseek-ai/dsh-api-remotes':
|
|
||||||
specifier: workspace:^
|
|
||||||
version: link:../../api/remotes
|
|
||||||
'@deepseek-ai/dsh-invariants':
|
'@deepseek-ai/dsh-invariants':
|
||||||
specifier: workspace:^
|
specifier: workspace:^
|
||||||
version: link:../../support/invariants
|
version: link:../../support/invariants
|
||||||
|
|||||||
@@ -15,8 +15,13 @@ interface CssPlugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function cssPlugin(): CssPlugin {
|
function cssPlugin(): CssPlugin {
|
||||||
const configs = clientBundle('@deepseek-ai/dsh-client-test', ['lib/types/index.js', 'lib/types/invariant.js'])
|
const configs = clientBundle(
|
||||||
const plugins = (configs[1] as { plugins: CssPlugin[] }).plugins
|
'@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')
|
const plugin = plugins.find(candidate => candidate.name === 'dsh-css-modules-inline')
|
||||||
if (plugin === undefined) throw new Error('CSS Modules plugin missing from client config')
|
if (plugin === undefined) throw new Error('CSS Modules plugin missing from client config')
|
||||||
return plugin
|
return plugin
|
||||||
|
|||||||
@@ -14,6 +14,24 @@ interface CssModulePlugin {
|
|||||||
load?: (this: { addWatchFile: (id: string) => void }, id: string) => Promise<unknown>
|
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 {
|
function clientSourceMapPath(packagePath: string): string {
|
||||||
return fileURLToPath(new URL(`../packages/${packagePath}/lib/client.js.map`, import.meta.url))
|
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 {
|
function purityResolveId(): ResolveId {
|
||||||
// libEntry is spelled at every call site (no default) so the
|
// libEntry is spelled at every call site (no default) so the
|
||||||
// package-invariants text check can see the invariant entry per package.
|
// 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 configs = clientConfigs()
|
||||||
const plugins = (configs[1] as { plugins: { name: string; resolveId?: unknown }[] }).plugins
|
const plugins = (configs[0] as { plugins: { name: string; resolveId?: unknown }[] }).plugins
|
||||||
const gate = plugins.find(p => p.name === 'dsh-client-bundle-purity')
|
const gate = plugins.find(p => p.name === 'dsh-client-bundle-purity')
|
||||||
if (gate?.resolveId === undefined) throw new Error('purity plugin missing from client config')
|
if (gate?.resolveId === undefined) throw new Error('purity plugin missing from client config')
|
||||||
return gate.resolveId as ResolveId
|
return gate.resolveId as ResolveId
|
||||||
}
|
}
|
||||||
|
|
||||||
function cssModulePlugin(): CssModulePlugin {
|
function cssModulePlugin(): CssModulePlugin {
|
||||||
const configs = clientBundle('@deepseek-ai/dsh-client-test', ['lib/types/index.js', 'lib/types/invariant.js'])
|
const configs = clientConfigs()
|
||||||
const plugins = (configs[1] as { plugins: CssModulePlugin[] }).plugins
|
const plugins = (configs[0] as { plugins: CssModulePlugin[] }).plugins
|
||||||
const plugin = plugins.find(candidate => candidate.name === 'dsh-css-modules-inline')
|
const plugin = plugins.find(candidate => candidate.name === 'dsh-css-modules-inline')
|
||||||
if (plugin?.resolveId === undefined || plugin.load === undefined) {
|
if (plugin?.resolveId === undefined || plugin.load === undefined) {
|
||||||
throw new Error('CSS Modules plugin missing from client config')
|
throw new Error('CSS Modules plugin missing from client config')
|
||||||
@@ -87,13 +105,13 @@ describe('client bundle purity gate', () => {
|
|||||||
|
|
||||||
describe('client bundle debug artifacts', () => {
|
describe('client bundle debug artifacts', () => {
|
||||||
it('emits source maps for plugin TS and TSX outside the Vite module graph', () => {
|
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'])
|
const configs = clientConfigs()
|
||||||
expect(configs[1]?.sourcemap).toBe(true)
|
expect(configs[0]?.sourcemap).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('maps first-party sources to their repository package paths', () => {
|
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 configs = clientConfigs('@deepseek-ai/dsh-client-ui-goal')
|
||||||
const outputOptions = configs[1]?.outputOptions
|
const outputOptions = configs[0]?.outputOptions
|
||||||
if (typeof outputOptions !== 'object' || outputOptions === null) throw new Error('client output options missing')
|
if (typeof outputOptions !== 'object' || outputOptions === null) throw new Error('client output options missing')
|
||||||
const transform = outputOptions.sourcemapPathTransform
|
const transform = outputOptions.sourcemapPathTransform
|
||||||
if (transform === undefined) throw new Error('client sourcemap path transform missing')
|
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', () => {
|
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 configs = clientConfigs('@deepseek-ai/dsh-host-directory-picker-native')
|
||||||
const outputOptions = configs[1]?.outputOptions
|
const outputOptions = configs[0]?.outputOptions
|
||||||
if (typeof outputOptions !== 'object' || outputOptions === null) throw new Error('client output options missing')
|
if (typeof outputOptions !== 'object' || outputOptions === null) throw new Error('client output options missing')
|
||||||
const transform = outputOptions.sourcemapPathTransform
|
const transform = outputOptions.sourcemapPathTransform
|
||||||
if (transform === undefined) throw new Error('client sourcemap path transform missing')
|
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', () => {
|
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 configs = clientConfigs('@deepseek-ai/dsh-client-connection')
|
||||||
const outputOptions = configs[1]?.outputOptions
|
const outputOptions = configs[0]?.outputOptions
|
||||||
if (typeof outputOptions !== 'object' || outputOptions === null) throw new Error('client output options missing')
|
if (typeof outputOptions !== 'object' || outputOptions === null) throw new Error('client output options missing')
|
||||||
const transform = outputOptions.sourcemapPathTransform
|
const transform = outputOptions.sourcemapPathTransform
|
||||||
if (transform === undefined) throw new Error('client sourcemap path transform missing')
|
if (transform === undefined) throw new Error('client sourcemap path transform missing')
|
||||||
|
|||||||
+16
-13
@@ -136,22 +136,25 @@ function formatDiagnostics(diagnostics: readonly ts.Diagnostic[], blocks: Block[
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reuse the host-aggregate references from a temp project one directory below
|
* Reuse both aggregate reference sets from a temp project one directory below
|
||||||
* root. Doc fragments speak the host vocabulary, so the standalone project
|
* root. Each referenced package remains its own program, while documentation
|
||||||
* seeds tsconfig.host.json (never the root solution: flattening host+client
|
* examples can import either the Host or Client API.
|
||||||
* into one program collides the cordis Context merges).
|
|
||||||
*/
|
*/
|
||||||
function workspaceReferences(): { path: string }[] {
|
function workspaceReferences(): { path: string }[] {
|
||||||
const file = join(root, 'tsconfig.host.json')
|
const paths = new Set<string>()
|
||||||
// Parse with TypeScript's own JSONC reader: a regex comment stripper corrupts the `/*/` path
|
for (const aggregate of ['tsconfig.host.json', 'tsconfig.client.json']) {
|
||||||
// candidate in the workspace wildcard.
|
const file = join(root, aggregate)
|
||||||
const result = ts.readConfigFile(file, path => readFileSync(path, 'utf8'))
|
// Parse with TypeScript's own JSONC reader: a regex comment stripper corrupts the `/*/` path
|
||||||
if (result.error) {
|
// candidate in the workspace wildcard.
|
||||||
throw new Error(`doc-typecheck: cannot read ${file}: ${ts.flattenDiagnosticMessageText(result.error.messageText, '\n')}`)
|
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.
|
return [...paths].map(path => ({
|
||||||
const { references } = result.config as { references: { path: string }[] }
|
|
||||||
return references.map(({ path }) => ({
|
|
||||||
path: path.startsWith('./') ? `../${path.slice(2)}` : `../${path}`,
|
path: path.startsWith('./') ? `../${path.slice(2)}` : `../${path}`,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,6 +72,20 @@ describe('package invariant gate', () => {
|
|||||||
expect(collectPackageInvariantViolations(fixture())).toEqual([])
|
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', () => {
|
it('rejects missing publication metadata and build output', () => {
|
||||||
const violations = collectPackageInvariantViolations(fixture({
|
const violations = collectPackageInvariantViolations(fixture({
|
||||||
invariantExport: false,
|
invariantExport: false,
|
||||||
|
|||||||
@@ -118,11 +118,8 @@ function checkBuild(
|
|||||||
violations: PackageInvariantViolation[],
|
violations: PackageInvariantViolation[],
|
||||||
): void {
|
): void {
|
||||||
const tsconfigPath = `${owner.dir}/tsconfig.json`
|
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'
|
if (owner.packageName !== '@deepseek-ai/dsh-invariants'
|
||||||
&& !tsconfig.references?.some(reference => reference.path === '../../support/invariants')) {
|
&& !projectReferencesInvariants(root, owner.dir, tsconfigPath)) {
|
||||||
addViolation(
|
addViolation(
|
||||||
violations,
|
violations,
|
||||||
tsconfigPath,
|
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(
|
function checkSource(
|
||||||
owner: PackageInvariantOwner,
|
owner: PackageInvariantOwner,
|
||||||
root: string,
|
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; }
|
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 two blocking surfaces, concurrently ------------------------------
|
||||||
# The build preserves the face order from package.json: generate Host contracts
|
# The build preserves the face order from package.json: compile and bundle the
|
||||||
# before either aggregate typecheck, then bundle the completed workspace.
|
# Host face before compiling and bundling the Client face.
|
||||||
# Both statuses are captured so one failure cannot hide the other's result.
|
# Both statuses are captured so one failure cannot hide the other's result.
|
||||||
build_gate() {
|
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-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/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() {
|
site_gate() {
|
||||||
cd website
|
cd website
|
||||||
@@ -238,12 +237,11 @@ report() {
|
|||||||
for log in "$@"; do tail -n 200 "$log" >&2 || true; done
|
for log in "$@"; do tail -n 200 "$log" >&2 || true; done
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
report 'build (contract prepass, tsc, tsdown)' "$build_status" \
|
report 'build (Host tsc/tsdown, Client tsc/tsdown)' "$build_status" \
|
||||||
"$scratch/logs/contracts-tsc.log" \
|
|
||||||
"$scratch/logs/contracts-tsdown.log" \
|
|
||||||
"$scratch/logs/host-tsc.log" \
|
"$scratch/logs/host-tsc.log" \
|
||||||
|
"$scratch/logs/host-tsdown.log" \
|
||||||
"$scratch/logs/client-tsc.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"
|
report 'production site (vitepress build)' "$site_status" "$scratch/logs/site.log"
|
||||||
if (( build_status != 0 )); then exit "$build_status"; fi
|
if (( build_status != 0 )); then exit "$build_status"; fi
|
||||||
exit "$site_status"
|
exit "$site_status"
|
||||||
@@ -53,7 +53,7 @@
|
|||||||
{ "path": "./packages/client/connection" },
|
{ "path": "./packages/client/connection" },
|
||||||
{ "path": "./packages/typert/registry" },
|
{ "path": "./packages/typert/registry" },
|
||||||
{ "path": "./packages/api/gateway" },
|
{ "path": "./packages/api/gateway" },
|
||||||
{ "path": "./packages/api/remotes" },
|
{ "path": "./packages/api/remotes/tsconfig.client.json" },
|
||||||
{ "path": "./packages/client/runtime" },
|
{ "path": "./packages/client/runtime" },
|
||||||
{ "path": "./packages/client/test-runtime" },
|
{ "path": "./packages/client/test-runtime" },
|
||||||
{ "path": "./packages/client/ui-layout" },
|
{ "path": "./packages/client/ui-layout" },
|
||||||
|
|||||||
@@ -104,6 +104,7 @@
|
|||||||
{ "path": "./packages/typert/type-meta" },
|
{ "path": "./packages/typert/type-meta" },
|
||||||
{ "path": "./packages/typert/registry" },
|
{ "path": "./packages/typert/registry" },
|
||||||
{ "path": "./packages/api/gateway" },
|
{ "path": "./packages/api/gateway" },
|
||||||
|
{ "path": "./packages/api/remotes/tsconfig.host.json" },
|
||||||
{ "path": "./packages/typert/loader" },
|
{ "path": "./packages/typert/loader" },
|
||||||
{ "path": "./packages/session-persistence/session-persistence" },
|
{ "path": "./packages/session-persistence/session-persistence" },
|
||||||
{ "path": "./packages/session-persistence/session-checkpoint-policy" },
|
{ "path": "./packages/session-persistence/session-checkpoint-policy" },
|
||||||
|
|||||||
+24
-28
@@ -1,34 +1,30 @@
|
|||||||
import { defineConfig } from 'tsdown'
|
import { defineConfig } from 'tsdown'
|
||||||
import { typertPlugin } from './packages/typert/generator/lib/types/tsdown-plugin.js'
|
import { typertPlugin } from './packages/typert/generator/lib/types/tsdown-plugin.js'
|
||||||
|
|
||||||
|
function isBuildFaceClient(value: unknown): boolean {
|
||||||
|
if (value === undefined || value === 'host') return false
|
||||||
|
if (value === 'client') return true
|
||||||
|
throw new Error(`tsdown: --env.DSH_BUILD_FACE must be host or client, received ${String(value)}`)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* JS bundling for vendored Cordis and Harness TypeScript packages.
|
* The ordinary workspace build consumes JavaScript emitted by the Host
|
||||||
* TypeScript source is compiled first by `tsc -b` (the root solution); tsdown
|
* TypeScript project and runs TypeRT. The Client pass selects packages that
|
||||||
* reads only the emitted JS under lib/types and writes the package root and
|
* declare a browser bundle and lets their package-local configs emit both
|
||||||
* invariant companion runtime bundles. Declarations are NOT produced here,
|
* their Node loader entry and browser artifact.
|
||||||
* hence `dts: false`.
|
|
||||||
*
|
|
||||||
* Per-package shape overrides live in `<package>/tsdown.config.ts`
|
|
||||||
* (schemastery: dual ESM+CJS; logger-console: extra browser entry).
|
|
||||||
*/
|
*/
|
||||||
export default defineConfig({
|
export default defineConfig(({ env }) => {
|
||||||
// Explicit globs keep bundling to vendored Cordis, the TypeScript package tree, and
|
const client = isBuildFaceClient(env?.DSH_BUILD_FACE)
|
||||||
// the Node CLI assembly. `apps/web` is a Vite application with no lib/types entry;
|
return {
|
||||||
// `workspace: true` or `apps/*` would incorrectly treat it as a package bundle.
|
workspace: ['vendor/*', 'packages/*/*', 'apps/cli'],
|
||||||
workspace: ['vendor/*', 'packages/*/*', 'apps/cli'],
|
entry: client ? '' : ['lib/types/{index,invariant}.js'],
|
||||||
// The brace glob admits the package companion when present while retaining the
|
outDir: 'lib',
|
||||||
// index-only build for vendored Cordis packages outside the Harness package tree.
|
format: ['esm'],
|
||||||
entry: ['lib/types/{index,invariant}.js'],
|
platform: 'node',
|
||||||
outDir: 'lib',
|
target: 'es2024',
|
||||||
format: ['esm'],
|
fixedExtension: false,
|
||||||
platform: 'node',
|
dts: false,
|
||||||
target: 'es2024',
|
clean: false,
|
||||||
// All packages set "type": "module"; fixedExtension false keeps ESM output
|
plugins: client ? [] : [typertPlugin({ mode: 'workspace', faces: ['host'] })],
|
||||||
// at .js (not .mjs), matching the package.json main/exports fields.
|
}
|
||||||
fixedExtension: false,
|
|
||||||
dts: false,
|
|
||||||
clean: false,
|
|
||||||
// The final pass sees both independent TypeScript faces. Workspace mode
|
|
||||||
// writes only packages that explicitly publish a Typert/Remote subpath.
|
|
||||||
plugins: [typertPlugin({ mode: 'workspace' })],
|
|
||||||
})
|
})
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import { defineConfig } from 'tsdown'
|
|
||||||
import { typertPlugin } from './packages/typert/generator/lib/types/tsdown-plugin.js'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Host-only TypeRT contract prepass. The generator and its project references
|
|
||||||
* are compiled first; the plugin then analyzes Host source and emits local and
|
|
||||||
* Host-for-Client artifacts before either aggregate consumes Remote subpaths.
|
|
||||||
*/
|
|
||||||
export default defineConfig({
|
|
||||||
workspace: ['packages/typert/generator'],
|
|
||||||
entry: ['lib/types/{index,invariant}.js'],
|
|
||||||
outDir: 'lib',
|
|
||||||
format: ['esm'],
|
|
||||||
platform: 'node',
|
|
||||||
target: 'es2024',
|
|
||||||
fixedExtension: false,
|
|
||||||
dts: false,
|
|
||||||
clean: false,
|
|
||||||
plugins: [typertPlugin({ mode: 'workspace', faces: ['host'] })],
|
|
||||||
})
|
|
||||||
Reference in New Issue
Block a user