/** * Client module system: the browser peer of Node's internal ESM loader, built * as a lazy CJS table. The vendored cordis Loader consumes this object * through its `internal` contract (the only call site is `EntryTree.import` → * `internal.import`), which keeps entry governance (fiber lifecycle, inject * waiting, update/refresh) entirely on the vendored side while this package * owns code arrival. * * Lazy CJS model (web2 §0): executing a plugin bundle only REGISTERS its * factory (`window.__ModuleLoader__.load({id, factory})`); every module body * side effect — including CSS injection — lives inside the factory closure * and runs at materialization, not at script execution. Materialization * (factory(require) → export surface) happens on first import/require and is * memoized in {@link ClientModuleLoader.loadCache}; a factory that requires * another registered-but-unmaterialized module materializes it recursively, * so load order needs no external sequencing. * * Resolution branch order (import): seed word → shell instance; memoized * record → surface; static registry (shell-own modules, e.g. app-shell) → * module; registered factory → materialize; graph row → load + materialize; * anything else → throw (loud — the runtime mirror of the * build-time bundle purity gate). The synchronous `require` handed to * factories walks the same order minus the load branch: loading is async, * so only already-registered bundles can be required — and cross-plugin value * imports are a build error anyway. * * This file is the browser-safe contract face (zero node imports): the * `__DSH_BOOT__` wire types, the boot-manifest parser, and the boundaries around * {@link ClientModuleSystem}. The package root is the host-side service that * composes the wire. */ import type {} from 'cordis' import type { ClientModuleSystem } from './system.ts' declare module 'cordis' { interface Context { /** The client module system the web shell builds at boot (contract C5; provided by the `./client` wrapper plugin). */ modules: ClientModuleLoader } } /** * One composed client entry pushed by the host (web2 §0 graph row). Wire * single source: the host node half (package root) produces this same shape. * `immediately` marks stage-one prefetch; `inject` is informational graph * metadata (the authoritative edges live in each package's dshClient * declaration and reach fibers through entry creation). */ export interface WebBootEntry { /** Entry name == package name. */ id: string /** Bundle endpoint, '/plugins//client.js?rev='. */ url: string /** Bundle content hash (cache-busting consistency anchor). */ rev: string /** Package-name dependency edges, informational (preflight display / HMR diffing). */ inject?: string[] /** Stage-one prefetch mark: load the script for factory registration during module-face boot. */ immediately?: boolean } /** The composed client entry graph the host injects as `window.__DSH_BOOT__`. */ export interface WebBootGraph { /** Consistency anchor over the whole graph (content + bundle hashes). */ rev: string /** Composed entries; order carries no semantics (activation order is fiber inject waiting). */ entries: WebBootEntry[] } /** The npm-package view of one boot row: what the module table needs to fetch the bundle. */ export interface BootModuleRow { /** Entry name == package name (module-table key). */ id: string /** Bundle endpoint, '/plugins//client.js?rev='. */ url: string /** Bundle content hash. */ rev: string } /** The cordis-plugin view of one boot row: what entry composition needs (optional wire fields normalized). */ export interface BootPluginRow { /** Entry name == package name. */ id: string /** Package-name dependency edges ([] when the wire omits them). */ inject: string[] /** Stage-one prefetch tier (false when the wire omits it). */ immediately: boolean } /** The parsed boot manifest: one wire, two consumer views. */ export interface BootManifest { /** Consistency anchor over the whole graph. */ rev: string /** Rows as the module table consumes them. */ modules: BootModuleRow[] /** Rows as entry composition consumes them. */ plugins: BootPluginRow[] } /** * Parse `window.__DSH_BOOT__` into the two consumer views. Wire boundary: * a missing or malformed graph throws (the shell shows the loud failure — * a page without a valid manifest cannot boot anything). * @param wire - the raw `window.__DSH_BOOT__` value. * @returns the manifest with optional plugin-view fields normalized. */ export function parseBootManifest(wire: unknown): BootManifest { if (typeof wire !== 'object' || wire === null) { throw new Error('client-modules: window.__DSH_BOOT__ is missing or not an object') } const graph = wire as Record if (typeof graph.rev !== 'string') { throw new Error('client-modules: boot manifest rev must be a string') } if (!Array.isArray(graph.entries)) { throw new Error('client-modules: boot manifest entries must be an array') } const modules: BootModuleRow[] = [] const plugins: BootPluginRow[] = [] for (const value of graph.entries as unknown[]) { if (typeof value !== 'object' || value === null) { throw new Error('client-modules: boot manifest entry is not an object') } const row = value as Record const where = typeof row.id === 'string' ? `"${row.id}"` : JSON.stringify(row) if (typeof row.id !== 'string' || typeof row.url !== 'string' || typeof row.rev !== 'string') { throw new Error(`client-modules: boot manifest entry ${where} must carry string id/url/rev`) } if (row.inject !== undefined && (!Array.isArray(row.inject) || row.inject.some(i => typeof i !== 'string'))) { throw new Error(`client-modules: boot manifest entry ${where} inject must be a string array`) } if (row.immediately !== undefined && typeof row.immediately !== 'boolean') { throw new Error(`client-modules: boot manifest entry ${where} immediately must be a boolean`) } modules.push({ id: row.id, url: row.url, rev: row.rev }) plugins.push({ id: row.id, inject: row.inject === undefined ? [] : [...row.inject as string[]], immediately: row.immediately === true, }) } return { rev: graph.rev, modules, plugins } } /** The shape a client bundle hands to `window.__ModuleLoader__.load` (registration handoff, contract C6). */ export interface ClientPluginHandoff { /** Plugin id (package name) — the registration key; must match the graph row being executed. */ id: string /** * Closure factory holding the whole bundle body: receives the synchronous * require bound to the module table and returns the bundle's export * surface. Runs once, at materialization. */ factory: (require: (spec: string) => unknown) => Record } /** Window surface of the web boot protocol: the host-injected graph, the registration sink, and the kernel handoff slot. */ export interface DshWindow { /** Host-composed entry graph, injected before the shell bundle runs; wire-boundary raw until {@link parseBootManifest}. */ __DSH_BOOT__?: unknown /** Bundle registration sink; installed once per page by the {@link ClientModuleSystem} constructor (contract C6). */ __ModuleLoader__?: { load(handoff: ClientPluginHandoff): void } /** * Kernel handoff slot: the shell kernel stores the instance here right * after construction (before cordis exists) so the `./client` wrapper * plugin can provide it as `ctx.modules`. Missing slot at wrapper apply * time = kernel sequencing bug, thrown loud. */ __DSH_MODULES__?: ClientModuleSystem } /** Per-module bookkeeping in {@link ClientModuleLoader.loadCache} (module-graph boundary, flat today). */ export interface ClientModuleRecord { /** Module id (entry name / package name). */ id: string /** The materialized export surface (factory `module.exports`, or the shell module for static registrations). */ surface: unknown /** Owned `