Files
dsh-context/scripts/build.mjs
T
Bowen Liang 36ae27765a build: silence commonjs-variable-in-esm warning for the harness-loader export
The client entry's `module.exports = { name, inject, apply }` is the
closure-factory contract every dsh.client package's `./client` export
must use — the harness's __ModuleLoader__ wraps the bundled CJS in a
factory that provides `var module = { exports: {} }` as a closure local
(see scripts/build.mjs:73-86), so the reference is legitimate. esbuild's
parser nonetheless flags it because the .ts source lives in a
"type": "module" package.

`logOverride: { 'commonjs-variable-in-esm': 'silent' }` suppresses that
one specific warning without touching any other esbuild signal — a future
maintainer adding a real CJS leak (e.g. require() inside the entry) would
still see a different warning. No source, test, or runtime behavior
changes; the bundled client.js still ends with the documented factory
shape that the harness's loader consumes.
2026-08-17 23:07:29 +08:00

110 lines
4.8 KiB
JavaScript
Executable File

#!/usr/bin/env node
/**
* dsh-context build — one runtime dependency (zod, for the projection's wire
* schema; every `@deepseek-ai/*` contract is type-only and erased here).
*
* Produces the npm-package artifacts under lib/:
* lib/index.js — host half: src/host/ bundled to plain ESM (zod external).
* lib/client.js — client half: src/client/ bundled to CJS, then
* wrapped in the web boot handoff
* `window.__ModuleLoader__.load({ id, factory })`, the
* closure-factory format every `dsh.client` package's
* `./client` export must use (React and other platform
* modules arrive through the injected `require`, so
* `react` is marked external and stays a bare require).
*
* Both halves bundle their local modules (shared types are type-only and
* vanish at build time). Type checking is a separate step
* (`npm run typecheck`, tsc --noEmit); this script only transpiles (esbuild)
* and runs smoke checks: both outputs must parse, and the host half must
* import with the expected plugin shape.
*/
import { build } from 'esbuild'
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises'
import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..')
const pkg = JSON.parse(await readFile(join(ROOT, 'package.json'), 'utf8'))
await mkdir(join(ROOT, 'lib'), { recursive: true })
// ---- host half: bundled TS -> ESM (zod stays external — the user's harness
// provides the wire schema validator, like every other @deepseek-ai/* runtime
// service; the package ships zod as a regular dependency) ----------------
await build({
entryPoints: [join(ROOT, 'src', 'host', 'index.ts')],
outfile: join(ROOT, 'lib', 'index.js'),
format: 'esm',
platform: 'node',
target: 'es2022',
bundle: true,
external: ['zod'],
sourcemap: false,
})
// ---- client half: bundled TS -> CJS, then wrap in the loader handoff -------
// `react` stays external (the browser module table answers the injected
// require), as do the framework seed words this bundle uses — esbuild turns
// JSX into plain React.createElement calls (classic transform), and the loader
// resolves the shared framework singletons at runtime. Everything else is
// inlined by the bundler.
await build({
entryPoints: [join(ROOT, 'src', 'client', 'index.ts')],
outfile: join(ROOT, 'lib', '_client.js'),
format: 'cjs',
platform: 'browser',
target: 'es2020',
bundle: true,
external: ['react', '@deepseek-ai/dsh-client-ui-primitives'],
// The harness's `__ModuleLoader__` wraps the bundled output in a CJS
// closure factory that provides `var module = { exports: {} }` as a local
// (see scripts/build.mjs:73-86). The source's `module.exports = {...}` is
// therefore legitimate, but esbuild's parser flags it as suspicious in a
// `.ts` source under `"type": "module"`. Silence that one warning here; any
// NEW `module` reference added by a future maintainer (e.g. a real CJS leak
// outside the harness-loader contract) should land in its a separate `.cjs`
// file or `.cts` rename, not get buried here.
logOverride: { 'commonjs-variable-in-esm': 'silent' },
// Plugin self-metadata (shown on the Plugin info card), read from
// package.json so the card always matches the shipped release.
define: {
__DSH_CTX_VERSION__: JSON.stringify(pkg.version),
__DSH_CTX_REPO__: JSON.stringify(
String((pkg.repository && pkg.repository.url) || '').replace(/^git\+/, '').replace(/\.git$/, ''),
),
},
sourcemap: false,
})
const clientSource = await readFile(join(ROOT, 'lib', '_client.js'), 'utf8')
await rm(join(ROOT, 'lib', '_client.js'))
const bundle = [
'/* dsh-context client bundle — generated by scripts/build.mjs from src/client/ */',
'window.__ModuleLoader__.load({',
` id: ${JSON.stringify(pkg.name)},`,
' factory: (require) => {',
' var module = { exports: {} };',
' var exports = module.exports;',
clientSource.replace(/\s+$/, '\n'),
' return module.exports;',
' },',
'});',
'',
].join('\n')
await writeFile(join(ROOT, 'lib', 'client.js'), bundle)
// ---- smoke checks ----
new Function(bundle) // syntax parse only; never executes (window is undefined here)
const host = await import(join(ROOT, 'lib', 'index.js'))
if (host.name !== 'dsh-context' || !Array.isArray(host.inject) || typeof host.apply !== 'function') {
throw new Error('host half does not expose the expected plugin shape (name/inject/apply)')
}
if (!host.inject.includes('sessionProjections')) {
throw new Error('host half must inject the sessionProjections service')
}
console.log(`built ${pkg.name}:`)
console.log(' lib/index.js (host half, ESM, from src/host/)')
console.log(' lib/client.js (client half, __ModuleLoader__ closure bundle, from src/client/)')