diff --git a/scripts/bundle-smoke.mjs b/scripts/bundle-smoke.mjs new file mode 100644 index 0000000..7d25ea5 --- /dev/null +++ b/scripts/bundle-smoke.mjs @@ -0,0 +1,135 @@ +#!/usr/bin/env node +/** + * Bundle smoke check — the ONLY test that exercises the built artifacts + * (lib/client.js). The vitest suite tests src/ directly and never builds; + * this script exists because the packaging glue itself (the + * `window.__ModuleLoader__.load({id, factory})` handoff, the CSS channel's + * data-plugin style tag, platform-module requires) only exists after tsdown + * runs. It is opt-in: run `pnpm run build && pnpm run test:bundle`. + * + * Everything here runs against the REAL bundle in jsdom with REAL React and + * the REAL ui-primitives — only the harness services (locale/slots/effects) + * are in-memory implementations of their documented contracts. + * + * Exit code 0 on success, 1 on failure. + */ +import assert from 'node:assert/strict' +import { readFile } from 'node:fs/promises' +import { createRequire } from 'node:module' +import { JSDOM } from 'jsdom' +import React from 'react' +import ReactDOM from 'react-dom' + +const require = createRequire(import.meta.url) + +let bundle +try { + bundle = await readFile(new URL('../lib/client.js', import.meta.url), 'utf8') +} catch { + console.error('lib/client.js missing — run `pnpm run build` first.') + process.exit(1) +} + +const dom = new JSDOM('', { pretendToBeVisual: true }) +globalThis.window = dom.window +globalThis.document = dom.window.document + +// The module table: the shell seeds exactly these specifiers. The bundle must +// require nothing else (the tsdown purity gate pins this too). The primitives +// dist ships CSS modules Node cannot parse — the bundle only references its +// components as React leaves, so inert stand-ins play the shell's role here +// (their real rendering is covered by the vitest jsdom lane). +const inert = name => { + const C = () => null + C.displayName = name + return C +} +const moduleTable = (spec) => { + if (spec === 'react') return React + if (spec === 'react-dom') return ReactDOM + if (spec === '@deepseek-ai/dsh-client-ui-primitives') { + return new Proxy({}, { get: (_t, key) => (typeof key === 'string' ? inert(key) : undefined) }) + } + throw new Error(`bundle required a non-platform module: ${spec}`) +} + +let handoff = null +dom.window.__ModuleLoader__ = { load(h) { handoff = h } } +new Function(bundle)() +assert.ok(handoff !== null, 'bundle must register through __ModuleLoader__.load') +assert.equal(handoff.id, 'dsh-context', 'handoff id is the package name') +assert.equal(typeof handoff.factory, 'function') + +const plugin = handoff.factory(moduleTable) +assert.equal(plugin.name, 'dsh-context') +assert.deepEqual(plugin.inject, ['slots', 'locale']) + +// The CSS channel: one plugin-owned style tag, minified, officially tagged. +const styleTag = () => dom.window.document.head.querySelector('style[data-plugin="dsh-context"]') +assert.ok(styleTag() !== null, 'plugin-owned