refactor(gui): rebuild the client loading kernel as dsh-client-modules with a two-phase boot
The module system moves out of dsh-client-runtime (./loader retired) into its own package: a lazy CJS table where executing a bundle only registers its factory and materialization happens at first require, memoized, with recursive requires self-ordering. ClientModuleSystem is a class; index.ts keeps the types and a thin factory. Boot is two-phase: phase one prefetches the immediately tier in parallel (registration only, failures deferred to phase two's loud import); phase two mounts the vendored Loader with the module system as internal, creates one entry per graph row plus the app-shell pseudo-row the kernel appends itself, and settles on an all-ACTIVE sweep. The shell kernel is self-sufficient: hand-rolled loader-status stores, no plugin value imports, platform seed list single- sourced in platform.ts.
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
# @deepseek-ai/dsh-client-modules
|
||||
|
||||
Client module system: the browser peer of Node's internal ESM loader, built as a lazy CJS table. The web shell mounts the vendored cordis Loader for entry governance (fiber lifecycle, inject waiting, update/refresh) and injects this package's `ClientModuleLoader` as its `internal` seam — the vendored side's only consumption point is `EntryTree.import`, so replacing `internal` replaces exactly "how plugin code arrives" and nothing else.
|
||||
|
||||
Lazy CJS model (web2): executing a plugin bundle only REGISTERS its factory (`window.__ModuleLoader__.load({id, factory})`); every module body side effect — CSS injection included — lives in the factory closure and runs at materialization (`factory(require)` → export surface, memoized in `loadCache`), not at script execution. A factory that requires another registered-but-unmaterialized module materializes it recursively, so load order needs no external sequencing; require cycles throw (factory-form CJS cannot deliver partial exports). `<id>/client` and the bare id name the same surface (a plugin bundle IS its package's client half).
|
||||
|
||||
Resolution branch order (`import(specifier)`): platform seed word → shell instance; memoized record → surface; shell-own static registry (`registerStatic`, app-shell) → module; registered factory → materialize; graph row (`window.__DSH_BOOT__`) → fetch + execute + materialize; anything else throws — the runtime mirror of the build-time bundle purity gate. The synchronous `require` handed to factories walks the same order minus the fetch branch and records observed edges into the module record. `prefetch` is the stage-one arrival hook (fetch + execute, registration only; concurrent calls share one in-flight task); `invalidate` drops the factory and the materialized record so the next prefetch/import refetches (the HMR hook).
|
||||
|
||||
## Model Experience
|
||||
|
||||
None, as the module loader is browser-side kernel machinery; nothing here reaches a model request.
|
||||
|
||||
#### KV Cache effect
|
||||
|
||||
None; this package neither assembles nor sends a provider request.
|
||||
|
||||
## Known Limitations and Deferred Work
|
||||
|
||||
- **Flat module graph by design** — every bundle is one module node whose edges point only at table leaves; the interface (loadCache/edges/invalidate) is shaped for a general module graph so the externalization granularity can change without an interface change.
|
||||
- **No unload bookkeeping of its own** — style removal and fiber teardown ordering live with the HMR driver (`@deepseek-ai/dsh-client-hmr`); the loader only inventories owned style tag ids per record.
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-client-modules",
|
||||
"description": "Client module loader: the browser peer of Node's internal ESM loader, consumed by the vendored cordis Loader as its internal seam (resolve/import/loadCache/invalidate over seed table, static registry and fetch bundles)",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/types/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./lib/types/index.d.ts",
|
||||
"default": "./lib/index.js"
|
||||
},
|
||||
"./invariant": {
|
||||
"types": "./lib/types/invariant.d.ts",
|
||||
"default": "./lib/invariant.js"
|
||||
},
|
||||
"./src/*": "./src/*",
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"license": "BSD-3-Clause",
|
||||
"devDependencies": {
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"cordis": "^4.0.0-rc.7"
|
||||
},
|
||||
"files": [
|
||||
"lib/index.js",
|
||||
"lib/invariant.js",
|
||||
"lib/types/**/*.d.ts",
|
||||
"lib/types/**/*.d.ts.map",
|
||||
"src"
|
||||
],
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/dsh-invariants": "^0.0.1",
|
||||
"cordis": "^4.0.0-rc.7"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
/**
|
||||
* 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` seam (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 → fetch + execute +
|
||||
* 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 fetch branch: fetching is async,
|
||||
* so only already-executed bundles can be required — and cross-plugin value
|
||||
* imports are a build error anyway.
|
||||
* @module @deepseek-ai/dsh-client-modules
|
||||
*/
|
||||
|
||||
import { ClientModuleLoaderImpl } from './loader.ts'
|
||||
|
||||
export { ClientModuleLoaderImpl }
|
||||
|
||||
declare module 'cordis' {
|
||||
interface Context {
|
||||
/** The client module system the web shell provides at boot (contract C5). */
|
||||
modules: ClientModuleLoader
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* One composed client entry pushed by the host (web2 §0 graph row).
|
||||
* `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).
|
||||
*
|
||||
* Wire contract, held on both sides: the producing peer lives in
|
||||
* `@deepseek-ai/dsh-host-webserver` (host packages keep zero workspace
|
||||
* dependencies, so neither side imports the other's shape — drift between
|
||||
* the two declarations is a bug against the web2 contract).
|
||||
*/
|
||||
export interface WebBootEntry {
|
||||
/** Entry name == package name (or a shell-owned pseudo id, e.g. app-shell). */
|
||||
id: string
|
||||
/**
|
||||
* Bundle endpoint, '/plugins/<id>/client.js?rev=<rev>'. Absent only on
|
||||
* shell-owned pseudo rows (app-shell) whose module is statically registered
|
||||
* — a row that is neither fetchable nor static-registered fails loud.
|
||||
*/
|
||||
url?: string
|
||||
/** Bundle content hash (cache-busting consistency anchor); absent with url. */
|
||||
rev?: string
|
||||
/** Package-name dependency edges, informational (preflight display / HMR diffing). */
|
||||
inject?: string[]
|
||||
/** Stage-one prefetch mark: fetch + execute (factory registration) during module-face boot. */
|
||||
immediately?: boolean
|
||||
}
|
||||
|
||||
/** The composed client entry graph the host injects as `window.__DSH_BOOT__` (dual-held wire contract — see {@link WebBootEntry}). */
|
||||
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 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<string, unknown>
|
||||
}
|
||||
|
||||
/** Window surface this loader owns (bundle side of the handoff protocol) plus the host-injected graph. */
|
||||
export interface DshWindow {
|
||||
/** Host-composed entry graph, injected before the shell bundle runs. */
|
||||
__DSH_BOOT__?: WebBootGraph
|
||||
/** Bundle registration sink; installed once per page by {@link createClientModuleLoader} (contract C6). */
|
||||
__ModuleLoader__?: { load(handoff: ClientPluginHandoff): void }
|
||||
}
|
||||
|
||||
/** Per-module bookkeeping in {@link ClientModuleLoader.loadCache} (module-graph seam, 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 `<style data-plugin>` tag ids (`data-plugin-css` values) injected during materialization. */
|
||||
styles: string[]
|
||||
/** Observed `require()` edges (module-graph seam; only table words can appear today). */
|
||||
edges: Set<string>
|
||||
}
|
||||
|
||||
/**
|
||||
* The internal-seam subset the vendored Loader and the client HMR plugin
|
||||
* consume. Mounted on `ctx.loader.internal` by the shell boot and provided
|
||||
* as `ctx.modules` (contract C5).
|
||||
*/
|
||||
export interface ClientModuleLoader {
|
||||
/** Discriminant against Node's internal loader shapes ('v1'/'v2'). */
|
||||
version: 'client'
|
||||
/** Materialized-module registry: id → record. The governance-side read face for entry export surfaces. */
|
||||
loadCache: Map<string, ClientModuleRecord>
|
||||
/**
|
||||
* Internal seam consumed by the vendored Loader's `tree.import`. Resolves
|
||||
* `specifier` through the branch order documented on the module, fetching
|
||||
* and executing a bundle when needed.
|
||||
* @param specifier - module specifier (entry name or table word).
|
||||
* @param parentURL - importer URL (unused — the client module graph is flat).
|
||||
* @param attrs - import attributes (unused; interface parity with Node's seam).
|
||||
* @returns the module's export surface.
|
||||
*/
|
||||
import(specifier: string, parentURL: string, attrs: Record<string, unknown>): Promise<unknown>
|
||||
/**
|
||||
* Register a shell-own module (app-shell — code that ships inside the shell
|
||||
* bundle and never arrives as a plugin bundle).
|
||||
* @param id - entry name (shell-owned pseudo id).
|
||||
* @param module - the statically imported module namespace.
|
||||
*/
|
||||
registerStatic(id: string, module: unknown): void
|
||||
/**
|
||||
* Stage-one arrival: fetch the entry's bundle and execute it, registering
|
||||
* its factory (no materialization — module side effects wait for import).
|
||||
* No-op for static-registered ids and ids whose factory is already
|
||||
* registered; concurrent calls share one in-flight task. To force a fresh
|
||||
* fetch (HMR), {@link invalidate} first.
|
||||
* @param id - graph entry name.
|
||||
*/
|
||||
prefetch(id: string): Promise<void>
|
||||
/**
|
||||
* Full reset of one module: drop its registered factory, its materialized
|
||||
* record, and any consumed bundle text, so the next prefetch/import
|
||||
* refetches and re-executes (the HMR invalidation hook).
|
||||
* @param id - entry name to invalidate.
|
||||
*/
|
||||
invalidate(id: string): void
|
||||
}
|
||||
|
||||
/** Options for {@link createClientModuleLoader} (assembled by the web shell at boot). */
|
||||
export interface ClientModuleLoaderOptions {
|
||||
/** Host-composed entry graph. */
|
||||
graph: WebBootGraph
|
||||
/** Module-table seed: platform-singleton specifier → shell instance. */
|
||||
staticModules: Record<string, unknown>
|
||||
/** Bundle fetch seam (parallelizable half). Defaults to same-origin fetch().text(). */
|
||||
fetchBundle?: (url: string) => Promise<string>
|
||||
/**
|
||||
* Bundle execution seam (synchronously performs the load() registration).
|
||||
* Defaults to a <script> element carrying the code.
|
||||
*/
|
||||
executeBundle?: (code: string, url: string) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the client module system.
|
||||
* @param options - entry graph, module-table staticModules, fetch/execute seams.
|
||||
* @returns the loader the shell mounts as `ctx.loader.internal` and provides as `ctx.modules`.
|
||||
*/
|
||||
export function createClientModuleLoader(options: ClientModuleLoaderOptions): ClientModuleLoader {
|
||||
return new ClientModuleLoaderImpl(options)
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Package-owned invariant companion for `@deepseek-ai/dsh-client-modules`.
|
||||
* @module @deepseek-ai/dsh-client-modules/invariant
|
||||
*/
|
||||
|
||||
/* jscpd:ignore-start */
|
||||
import type { Context } from 'cordis'
|
||||
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
|
||||
|
||||
const PACKAGE_NAME = '@deepseek-ai/dsh-client-modules'
|
||||
|
||||
/** Cordis companion plugin name. */
|
||||
export const name = 'client-modules-invariant'
|
||||
/** Service required before the companion can reserve package ownership. */
|
||||
export const inject = ['invariants']
|
||||
|
||||
/**
|
||||
* No runtime invariant: the module loader is pre-plugin kernel machinery —
|
||||
* it emits no cordis events (the vendored Loader owns entry lifecycle events)
|
||||
* and its mutable state (loadCache, handoff slot) lives below the plugin
|
||||
* layer where invariant observers cannot mount before it runs; resolve branch
|
||||
* order and handoff discipline are asserted by the web boot specs against the
|
||||
* real execution path.
|
||||
*/
|
||||
const install: InvariantInstaller = () => {}
|
||||
|
||||
/**
|
||||
* Register this package's invariant companion.
|
||||
* @param ctx - Cordis context carrying the invariant service.
|
||||
* @returns the installed registration's disposer after setup succeeds.
|
||||
*/
|
||||
export const apply = (ctx: Context): Promise<() => void> =>
|
||||
Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install))
|
||||
/* jscpd:ignore-end */
|
||||
@@ -0,0 +1,223 @@
|
||||
/**
|
||||
* ClientModuleLoaderImpl — the implementation behind the {@link ClientModuleLoader}
|
||||
* seam. The conceptual contract (lazy CJS model, resolution branch order) is
|
||||
* documented on the package module and the public interfaces in `./index.ts`;
|
||||
* this file owns the state tables and the fetch/execute/materialize machinery.
|
||||
*/
|
||||
import type {
|
||||
ClientModuleLoader, ClientModuleLoaderOptions, ClientModuleRecord,
|
||||
ClientPluginHandoff, DshWindow, WebBootEntry,
|
||||
} from './index.ts'
|
||||
|
||||
/** A registered-but-unmaterialized bundle: the factory plus its source URL (diagnostics). */
|
||||
interface RegisteredFactory {
|
||||
factory: ClientPluginHandoff['factory']
|
||||
url: string
|
||||
}
|
||||
|
||||
/** Default bundle fetch seam: same-origin fetch().text(). */
|
||||
const defaultFetchBundle = async (url: string): Promise<string> => {
|
||||
const res = await fetch(url)
|
||||
if (!res.ok) throw new Error(`client-modules: bundle fetch ${url} answered ${String(res.status)}`)
|
||||
return res.text()
|
||||
}
|
||||
|
||||
/** Default bundle execution seam: a <script> element carrying the code. */
|
||||
const defaultExecuteBundle = (code: string, url: string): void => {
|
||||
const el = document.createElement('script')
|
||||
// Inline execution (not src) so the fetch half stays parallelizable; the
|
||||
// sourceURL comment keeps devtools stack frames attributed to the bundle.
|
||||
el.textContent = `${code}\n//# sourceURL=${url}`
|
||||
document.head.appendChild(el)
|
||||
}
|
||||
|
||||
const urlOf = (row: WebBootEntry): string => {
|
||||
// url is conditional on the wire (shell-own pseudo rows omit it); those
|
||||
// ids resolve through the static registry and never reach a fetch.
|
||||
if (row.url === undefined) throw new Error(`client-modules: entry "${row.id}" has no bundle url and no static registration`)
|
||||
return row.url
|
||||
}
|
||||
|
||||
/**
|
||||
* A plugin bundle IS its package's client half: `<id>/client` (the exports
|
||||
* subpath external bundles emit) and the bare graph id name the same
|
||||
* surface, so table lookups normalize the suffix away.
|
||||
*/
|
||||
const stripClientSuffix = (spec: string): string =>
|
||||
spec.endsWith('/client') ? spec.slice(0, -'/client'.length) : spec
|
||||
|
||||
/**
|
||||
* Claim and inventory the <style> tags a factory injected during
|
||||
* materialization: preset-emitted tags arrive pre-tagged with data-plugin;
|
||||
* any untagged tag is claimed for the materializing plugin (HMR bookkeeping).
|
||||
*/
|
||||
const claimStyles = (id: string): string[] => {
|
||||
if (typeof document === 'undefined') return []
|
||||
for (const el of document.querySelectorAll('style:not([data-plugin])')) {
|
||||
el.setAttribute('data-plugin', id)
|
||||
}
|
||||
const owned: string[] = []
|
||||
for (const el of document.querySelectorAll(`style[data-plugin=${JSON.stringify(id)}]`)) {
|
||||
owned.push(el.getAttribute('data-plugin-css') ?? id)
|
||||
}
|
||||
return owned
|
||||
}
|
||||
|
||||
/**
|
||||
* The client module system: state tables plus the arrival/materialization
|
||||
* machinery implementing {@link ClientModuleLoader} (whose members carry the
|
||||
* seam contract docs). Construction indexes the boot graph and installs the
|
||||
* `window.__ModuleLoader__` registration sink (contract C6) — once per page.
|
||||
*/
|
||||
export class ClientModuleLoaderImpl implements ClientModuleLoader {
|
||||
readonly version = 'client'
|
||||
readonly loadCache = new Map<string, ClientModuleRecord>()
|
||||
|
||||
private readonly seed: Map<string, unknown>
|
||||
private readonly statics = new Map<string, unknown>()
|
||||
private readonly factories = new Map<string, RegisteredFactory>()
|
||||
/** In-flight prefetch (fetch + execute) per id; concurrent callers share it. */
|
||||
private readonly pendingArrival = new Map<string, Promise<void>>()
|
||||
/** Materialization re-entrancy guard: factory-form CJS cannot deliver partial exports, so a cycle is fatal. */
|
||||
private readonly materializing = new Set<string>()
|
||||
private readonly graphRows = new Map<string, WebBootEntry>()
|
||||
// Execution URL of the bundle currently being executed (bound into the
|
||||
// factory registration so diagnostics can name the source).
|
||||
private executingUrl = ''
|
||||
|
||||
private readonly fetchBundle: (url: string) => Promise<string>
|
||||
private readonly executeBundle: (code: string, url: string) => void
|
||||
|
||||
/**
|
||||
* Build the module system over the host graph.
|
||||
* @param options - entry graph, module-table staticModules, fetch/execute seams.
|
||||
*/
|
||||
constructor(options: ClientModuleLoaderOptions) {
|
||||
this.seed = new Map(Object.entries(options.staticModules))
|
||||
this.fetchBundle = options.fetchBundle ?? defaultFetchBundle
|
||||
this.executeBundle = options.executeBundle ?? defaultExecuteBundle
|
||||
|
||||
for (const entry of options.graph.entries) {
|
||||
if (this.graphRows.has(entry.id)) throw new Error(`client-modules: duplicate graph entry "${entry.id}"`)
|
||||
this.graphRows.set(entry.id, entry)
|
||||
}
|
||||
|
||||
const win = globalThis as DshWindow
|
||||
if (win.__ModuleLoader__ !== undefined) throw new Error('client-modules: window.__ModuleLoader__ already installed (double boot?)')
|
||||
win.__ModuleLoader__ = {
|
||||
load: (handoff: ClientPluginHandoff): void => {
|
||||
// Registration is keyed by the handoff id; a duplicate means a bundle
|
||||
// executed twice without an invalidate — always a bug, always loud.
|
||||
if (this.factories.has(handoff.id)) throw new Error(`client-modules: duplicate factory registration for "${handoff.id}" (bundle executed twice without invalidate?)`)
|
||||
this.factories.set(handoff.id, { factory: handoff.factory, url: this.executingUrl })
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/** Fetch + execute one graph row so its factory is registered (idempotent per in-flight arrival). */
|
||||
private arrive(row: WebBootEntry): Promise<void> {
|
||||
const { id } = row
|
||||
const pending = this.pendingArrival.get(id)
|
||||
if (pending !== undefined) return pending
|
||||
if (this.factories.has(id)) return Promise.resolve()
|
||||
const task = (async (): Promise<void> => {
|
||||
const url = urlOf(row)
|
||||
const code = await this.fetchBundle(url)
|
||||
this.executingUrl = url
|
||||
try {
|
||||
this.executeBundle(code, url)
|
||||
} finally {
|
||||
this.executingUrl = ''
|
||||
}
|
||||
if (!this.factories.has(id)) {
|
||||
throw new Error(`client-modules: bundle ${url} executed without registering "${id}" via __ModuleLoader__.load`)
|
||||
}
|
||||
})().finally(() => { this.pendingArrival.delete(id) })
|
||||
this.pendingArrival.set(id, task)
|
||||
return task
|
||||
}
|
||||
|
||||
/** Materialize a registered factory (synchronous; memoized in loadCache). */
|
||||
private materialize(id: string): ClientModuleRecord {
|
||||
const existing = this.loadCache.get(id)
|
||||
if (existing !== undefined) return existing
|
||||
const registered = this.factories.get(id)
|
||||
/* v8 ignore next -- callers check the factory branch before dispatching here. */
|
||||
if (registered === undefined) throw new Error(`client-modules: no registered factory for "${id}"`)
|
||||
if (this.materializing.has(id)) {
|
||||
throw new Error(`client-modules: require cycle through "${id}" (factory-form CJS cannot deliver partial exports)`)
|
||||
}
|
||||
this.materializing.add(id)
|
||||
try {
|
||||
const edges = new Set<string>()
|
||||
const surface = registered.factory(this.makeRequire(edges))
|
||||
const record: ClientModuleRecord = { id, surface, styles: claimStyles(id), edges }
|
||||
this.loadCache.set(id, record)
|
||||
return record
|
||||
} finally {
|
||||
this.materializing.delete(id)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The synchronous require answered to factories: seed → static → memoized
|
||||
* record → registered factory (recursive materialization — this is what
|
||||
* makes load order self-resolving). Fetching is async and therefore
|
||||
* unreachable from here; an unregistered plugin specifier is loud (and a
|
||||
* cross-plugin value import is already a build error upstream).
|
||||
*/
|
||||
private makeRequire(edges: Set<string>): (spec: string) => unknown {
|
||||
return (spec: string): unknown => {
|
||||
edges.add(spec)
|
||||
if (this.seed.has(spec)) return this.seed.get(spec)
|
||||
if (this.statics.has(spec)) return this.statics.get(spec)
|
||||
const id = stripClientSuffix(spec)
|
||||
const record = this.loadCache.get(id)
|
||||
if (record !== undefined) return record.surface
|
||||
if (this.factories.has(id)) return this.materialize(id).surface
|
||||
throw new Error(
|
||||
`client-modules: require("${spec}") missed the module table — not a platform seed word, not a shell-own module, `
|
||||
+ 'and no registered factory (a build-time externals drift, or a forbidden cross-plugin value import)',
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
async import(specifier: string): Promise<unknown> {
|
||||
if (this.seed.has(specifier)) return this.seed.get(specifier)
|
||||
const existing = this.loadCache.get(specifier)
|
||||
if (existing !== undefined) return existing.surface
|
||||
if (this.statics.has(specifier)) {
|
||||
const surface = this.statics.get(specifier)
|
||||
this.loadCache.set(specifier, { id: specifier, surface, styles: [], edges: new Set() })
|
||||
return surface
|
||||
}
|
||||
if (!this.factories.has(specifier)) {
|
||||
const row = this.graphRows.get(specifier)
|
||||
if (row === undefined) {
|
||||
throw new Error(
|
||||
`client-modules: cannot resolve "${specifier}" — not a seed word, not a shell-own module, `
|
||||
+ 'and not a row in the boot graph (the runtime mirror of the bundle purity gate)',
|
||||
)
|
||||
}
|
||||
await this.arrive(row)
|
||||
}
|
||||
return this.materialize(specifier).surface
|
||||
}
|
||||
|
||||
registerStatic(id: string, module: unknown): void {
|
||||
if (this.statics.has(id)) throw new Error(`client-modules: shell-own module "${id}" registered twice`)
|
||||
this.statics.set(id, module)
|
||||
}
|
||||
|
||||
async prefetch(id: string): Promise<void> {
|
||||
if (this.statics.has(id)) return
|
||||
const row = this.graphRows.get(id)
|
||||
if (row === undefined) throw new Error(`client-modules: prefetch("${id}") — not a graph entry`)
|
||||
await this.arrive(row)
|
||||
}
|
||||
|
||||
invalidate(id: string): void {
|
||||
this.factories.delete(id)
|
||||
this.loadCache.delete(id)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"outDir": "lib/types",
|
||||
"lib": [
|
||||
"ES2024",
|
||||
"DOM",
|
||||
"DOM.Iterable"
|
||||
],
|
||||
"types": []
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../../vendor/cordis"
|
||||
},
|
||||
{
|
||||
"path": "../../support/invariants"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user