|
|
@@ -1,10 +1,10 @@
|
|
|
-# Agent Note: Client plugin loading — plain packages, dsh.client plugins, and the two-phase boot
|
|
|
+# Agent Note: Client plugin loading — lazy factories, Cordis lifecycle, and hot reload
|
|
|
|
|
|
Status: implemented
|
|
|
|
|
|
English | [中文](2026-07-23-client-plugin-loading-model.zh.md)
|
|
|
|
|
|
-> Scope: the browser-side plugin loading machinery — what is a plugin, how code arrives, and how hot reload rides on that model. This note owns the loading chain; the [web client architecture note](2026-07-19-gui-web-client-architecture.md) defers to it for loading and keeps owning slots, the data object layer, and the React face.
|
|
|
+> Scope: the browser-side plugin loading machinery — how code arrives, how Cordis governs it, and how hot reload rides on that model. This note owns the loading chain; the [client shell layering note](2026-08-15-client-shells-and-dynamic-packages.md) owns package categories, build faces, shared-module requests, and npm dependency declarations, while the [web client architecture note](2026-07-19-gui-web-client-architecture.md) owns slots and the data object layer.
|
|
|
|
|
|
## Problem
|
|
|
|
|
|
@@ -24,31 +24,17 @@ The first-generation client loader (`createClientLoader`) hand-wrote both layers
|
|
|
|
|
|
## Decision
|
|
|
|
|
|
-### Two package kinds; `dsh.client` means plugin, period
|
|
|
+### Package membership and module requests
|
|
|
|
|
|
-What makes a package a plugin? One rule: **a package is a plugin package once its consumption is cordis dependency injection; until then it is a plain package.** How code reaches the page is not part of the taxonomy — arrival follows from the kind instead of defining it.
|
|
|
+The [client shell layering note](2026-08-15-client-shells-and-dynamic-packages.md) defines the current static and dynamic package sets and the import rules between them. The loading machinery treats every `dsh.client` package as a host-graph row with one ordinary `lib/client.js` factory bundle. Its declaration carries Cordis `inject` edges, synchronous module-table `external` requests, and the optional `immediately` prefetch mark; the composing app owns only the mounted roster.
|
|
|
|
|
|
-- **Plain packages** are the absolute base the module system itself needs, plus libraries not yet converted to DI: the react family, cordis, `@deepseek-ai/dsh-client-modules` (the module system itself — it can never be a plugin, because modules precede all modules), the web shell kernel, and — for now — ui-slots, web-react, ui-primitives. Plain packages are shell-bundled, seeded into the module table, and invisible to the host graph.
|
|
|
-- **Plugin packages** are everything else. Each one carries a `dsh.client` manifest declaration (`{ platform, inject, immediately? }`) and one uniform shape: the shared tsdown preset emits `lib/client.js`, and `exports["./client"]` points at that bundle. Each is a governed entry of the host-authored graph. The current set is connection, runtime, ui-theme, i18n, hmr (dev graphs only), ui-layout, ui-sidebar, ui-conversation, ui-model-selector, ui-user-questions, and ui-trajectory.
|
|
|
-
|
|
|
-The manifest owns the package's loading contract: its `inject` dependency edges, plus the optional `immediately` prefetch mark (absent means lazy). The composing app owns only the roster.
|
|
|
-
|
|
|
-To add a plugin package: declare `dsh.client`, emit the `./client` bundle through the shared preset, add the name to the composing app's roster. Nothing else changes hands.
|
|
|
-
|
|
|
-When does a plain package become a plugin? The upgrade law, recorded so the migration path stays honest: **a plain package becomes a plugin package when its consumers switch to cordis DI, not before.** Three promotions are queued: ui-slots (the slots machinery now living in runtime — SlotRegistry, the renderer contract, the root slot), web-react (the renderer install moving into its own `apply`), and ui-primitives (once components are served through slots/services). Until then they stay plain, and their symbol exports stay ordinary static imports.
|
|
|
-
|
|
|
-Four edge rules govern imports across the two kinds. None of them depends on any per-package mark:
|
|
|
-
|
|
|
-- **Plugin ↔ plugin value imports are a build error.** This holds regardless of either side's `immediately` declaration — the rule must not depend on a mark someone can flip. Cooperation goes through cordis inject/services. `import type` is exempt; the type chain is untouched. This rule is why `scopeOf` is a `SessionRuntime` method and why `transportError` lives in `dsh-host-apiproxy`'s wire layer (its `RpcResult` home, inline-safe).
|
|
|
-- **Plugin → plain package value imports are externals**, judged against the platform list. That list is one constant in the shell (`platform.ts`: react family, cordis, ui-slots, web-react, ui-primitives), imported by both the tsdown preset (for the external judgement) and `seed.ts` (for the table warm-up). One constant, two consumers — the hand-sync drift class stays dead.
|
|
|
-- **The purity gate covers every plugin package.** Its three branches: platform imports become externals; INLINE_SAFE wire layers are inlined; any other workspace leak is a build error. The uniform bundle shape is what makes this coverage total — every plugin builds through the same preset, so no package can sit outside the gate.
|
|
|
-- **The shell is self-sufficient.** The kernel (boot + loading page) value-imports no plugin package; its status stores are hand-rolled. The fail-loud presentation must not depend on the system whose failure it reports.
|
|
|
+The web kernel remains framework-free and imports no dynamic package value. Modules is itself a dynamic row, but the host parser delivers its ordinary factory before the Vite main module. The HTML-installed `__ModuleLoader__` facade uses that factory to construct the module system when the kernel calls `create()`. Runtime arrives through the same pending queue; static React, Cordis, and UI library identities come from the shell seed.
|
|
|
|
|
|
### One module system, one plugin governor
|
|
|
|
|
|
The browser mirrors the host's division of labor. `dsh-client-modules` (`ClientModuleSystem`) takes the module-system seat that Node's internal ESM loader holds host-side; the same vendored `@cordisjs/plugin-loader` keeps the governance seat on both sides. The line between them in one sentence: **the module system owns module identity and bytes — how code arrives, registers, and becomes an exports; the Loader owns plugin lifecycle — when a plugin mounts, what it waits for, and how it is torn down.**
|
|
|
|
|
|
-`ClientModuleSystem` is a lazy CJS table. Executing a bundle only **registers** its factory — the bundle calls `window.__ModuleLoader__.load({ id, factory })` and nothing else happens. Every module body side effect, CSS injection included, lives inside the factory closure and runs at materialization: the first `require`/import of that id, memoized after that. A factory that requires a registered-but-unmaterialized sibling materializes it recursively, so no sort order exists anywhere. When asked to import an id, the table resolves through a fixed branch order: seed word → memoized record → static registration (shell-own modules, e.g. app-shell) → registered factory → graph-row external classic-script load → loud throw. That final throw is the runtime mirror of the build-time purity gate. The system also keeps per-module bookkeeping — owned `<style data-plugin>` tag ids, observed require edges — and exposes the two verbs HMR needs: `prefetch(id)` (load the script and register its factory; concurrent calls share one in-flight task) and `invalidate(id)` (drop the factory and record so the next arrival reloads it).
|
|
|
+`ClientModuleSystem` is a lazy CJS table. Executing a bundle only **registers** its factory — the bundle calls `window.__ModuleLoader__.load({ id, factory })` and nothing else happens. Every module body side effect, CSS injection included, lives inside the factory closure and runs at materialization: the first `require`/import of that id, memoized after that. Import and prefetch recursively register declared dynamic requests before their consumer; a factory then materializes any registered-but-unmaterialized request synchronously. The table resolves through a fixed branch order: seed word → memoized record → graph-row classic-script registration → registered-factory materialization → loud throw. The modules factory is the bootstrap exception: the HTML facade materializes it first, and construction places those same exports directly in the memoized table. That final throw is the runtime mirror of the build-time purity gate. The system also keeps per-module bookkeeping — owned `<style data-plugin>` tag ids, observed require edges — and exposes the two verbs HMR needs: `prefetch(id)` (register the requested dynamic factories and the row's own factory; concurrent arrivals share one task) and `invalidate(id)` (drop a non-bootstrap factory and record so the next arrival reloads it).
|
|
|
|
|
|
The vendored Loader consumes the module system through its `internal` contract — the only call site is `tree.import` — and owns everything entry-shaped: entry creation, fiber activation through cordis service waiting (PENDING until injected services exist, cascading when a service is provided), update/refresh, teardown. The governance code is byte-identical to the host side, per vendor policy. Browserization is compile-time mapping in the shell's vite config: a `node:module` stub alias plus `process.*` defines make `ModuleLoader.fromInternal()` return undefined — exactly the empty slot the shell fills. The module system mounts as `ctx.modules`.
|
|
|
|
|
|
@@ -58,29 +44,29 @@ Each graph row's `url` goes to a same-origin external classic `<script src>` wit
|
|
|
|
|
|
The shared tsdown preset emits `client.js.map` for every plugin and rewrites first-party source paths into the browser-resolvable repository shape `/packages/<group>/<package>/src/...`. Other workspace sources inlined into a bundle likewise resolve to their `packages/` owner, while dependency paths remain unchanged; `sourcesContent` carries the source, so the host only serves the map at `/plugins/<id>/client.js.map` and exposes no source route. The Vite shell also emits source maps, letting both shell code and out-of-graph plugins map stacks and performance profiles back to TypeScript/TSX.
|
|
|
|
|
|
-`rev` remains the script URL's query parameter and content-consistency anchor, and the bundle and map are both served with `no-cache`. An external script's `error` event exposes neither response status nor body, so failure diagnostics name only the URL; the same-origin host and build-stamped handoff id form the identity boundary, while the post-`load` factory-presence check rejects an artifact that did not register the expected id.
|
|
|
+`rev` remains the script URL's query parameter and content-consistency anchor, and the bundle and map are both served with `no-cache`. An external script's `error` event exposes neither response status nor body, so failure diagnostics name only the URL; the same-origin host and build-stamped registration id form the identity boundary, while the post-`load` factory-presence check rejects an artifact that did not register the expected id.
|
|
|
|
|
|
### The loading flow, end to end
|
|
|
|
|
|
-What happens between `dsh web` starting and the UI appearing? Three stages: the host composes and serves a graph, the shell prefetches, then cordis orchestrates.
|
|
|
+What happens between `dsh web` starting and the UI appearing? Three stages: the host composes a graph and parser-preloads bootstrap factories, the HTML facade creates the module system and the shell prefetches, then Cordis orchestrates.
|
|
|
|
|
|
**Host side — compose the graph.**
|
|
|
|
|
|
1. The composing app (`apps/cli`) ships the roster as ordinary rows in its `cordis.yml` config tree — client plugin packages are entry rows like every host plugin, including the always-mounted `client-hmr` row. A roster row that fails to import is caught by `assertEntriesLoaded`; a row whose fiber rejects is reported with its original stack by `assertEntriesActivated` ([host boot decision](2026-07-24-web-config-tree-boot-and-transport-layering.md)).
|
|
|
-2. The `dsh-client-modules` node half (the package is dual-face: its browser half is the module table) scans loader entries' package.json `dsh.client` declarations and composes `window.__DSH_BOOT__`: `{ rev, entries: [{ id, url, rev, inject?, immediately? }] }`. The `inject` edges and the `immediately` mark come from manifests, never hand-copied. It refuses declared plugins without built `./client` bundles and groups their package/path rows under one required source-build instruction; malformed declaration fields also fail activation, and the host audit reports either error from the FAILED fiber.
|
|
|
+2. The `dsh-client-modules` node half (the package is dual-face: its browser half is the module table) scans loader entries' package.json `dsh.client` declarations and composes `window.__DSH_BOOT__`: `{ rev, entries: [{ id, url, rev, inject?, immediately?, external? }] }`. The three optional fields come from manifests, never hand-copied. Composition orders requested dynamic rows before their consumers and rejects synchronous request cycles. It refuses declared plugins without built `./client` bundles and groups their package/path rows under one required source-build instruction; malformed declaration fields also fail activation, and the host audit reports either error from the FAILED fiber.
|
|
|
3. Scanning is incremental per package — there is no full-rescan code path. Each cordis `internal/plugin` emission marks the fiber's entry name dirty (entry-less fibers drop O(1)); a microtask flush reconciles each dirty name against live loader entries, with package metadata (including the negative "not a client package" verdict) cached per name forever and bundle re-hashing reachable only through `rebuilt(id)`. The activation pass seeds the same dirty set from current entries and flushes synchronously, so first scan and steady state share one implementation. Each bundle's content hash is its `rev` (cache busting + HMR diff anchor), the row set hashes into `graph.rev`, and every row is served as a script resource at `/plugins/<id>/client.js?rev=…`, with its source map at the same path plus `.map`. The graph types are single-sourced in the modules package's `./client` export — the webserver knows nothing about the graph (it is a plain route-registration plugin; modules registers the bundle route and taps the index render itself).
|
|
|
|
|
|
Why is the roster yml rows and not a scan? Because which plugins compose into a deployment is a composition decision, not a package property — a package declaring `dsh.client` in the repo does not mean this deployment mounts it, so discovery-by-scan cannot make that call; the node half scans only what the tree actually mounted.
|
|
|
|
|
|
-**Phase one — the module face.** The shell builds the module system over the graph, then prefetches every `immediately` row in parallel. Prefetch loads the external script and registers its factory only. A single row's prefetch failure is swallowed here: phase two's import retries the load and owns the loud failure, so one bad row cannot mask the others. `immediately` is a prefetch mark — not a barrier, not an identity. The package declares it, the registry carries it into the row. The infrastructure plugins (connection, runtime, ui-theme, i18n, plus hmr) declare it; UI plugins simply arrive on demand.
|
|
|
+**Phase one — the module face.** The injected HTML installs `window.__ModuleLoader__` in queue mode, executes the modules and runtime graph rows as blocking classic scripts, assigns `window.__DSH_BOOT__`, and then starts the Vite main module. The kernel calls the facade's `create()` with the raw graph and shell seeds. The facade removes and materializes the modules registration with a bootstrap `require` that rejects every external, then calls its `createClientModuleSystem` export. The modules bundle parses the graph, constructs the system, memoizes its own exports, and retains the instance in its module closure; construction switches the same facade to live registration before draining runtime's pending factory. The kernel then prefetches every `immediately` row in parallel; prefetch recursively registers declared dynamic requests and the row itself without materializing either. A row's prefetch failure is swallowed here because phase two's import retries and owns the loud failure. `immediately` remains an arrival mark, not a lifecycle barrier or package identity.
|
|
|
|
|
|
**Phase two — the plugin face.**
|
|
|
|
|
|
1. The kernel mounts the vendored Loader and injects the module system as `internal` before any entry exists. Ordering matters: `tree.import`'s bare-import fallback must never run in a browser.
|
|
|
-2. It creates one entry per graph row, plus the app-shell pseudo-row. The assembly entry is shell-own code the kernel appends itself — registered static with the module system, never part of the host graph — so it rides the same entry lifecycle and status coverage as everything else.
|
|
|
-3. Creation order carries no semantics; fibers activate through service waiting.
|
|
|
+2. It creates every graph row uniformly. Importing the modules row returns the memoized bootstrap exports, whose `apply()` provides the closed-over system as `ctx.modules`; rows that require that service remain PENDING until then, so the modules row needs no special creation position. Render assembly is an ordinary host-graph row provided by `dsh-client-ui-renderer`; the kernel appends no assembly pseudo-entry.
|
|
|
+3. Graph order governs synchronous factory availability; Cordis activation remains independent and proceeds through service waiting.
|
|
|
4. `settled` = every entry created + `loader.await()` quiescent + an all-ACTIVE sweep. The sweep lists each import-failed, FAILED, or PENDING fiber with its missing services. It exists because cordis inject waits have no timeout — the sweep is the fail-loud floor.
|
|
|
-5. The loading page's boot status is a projection of real fiber states via `internal/status`. The settled flip switches to the real UI in one pass.
|
|
|
+5. The framework-free loading page projects real fiber states via `internal/status`. After the sweep, the kernel calls `ctx.uiRenderer.mount(container)` and replaces the page with the real UI in one pass.
|
|
|
|
|
|
### Hot reload: one driver plugin, self-watched bundles
|
|
|
|
|
|
@@ -100,33 +86,19 @@ On the browser side, the driver reloads one plugin per frame, serialized:
|
|
|
|
|
|
Every plugin shares this one semantics; an `immediately` row reloads exactly like a lazy one. Dependency cascade costs zero client code: a fiber's activation epoch strings its service providers' uids, so replacing a provider's fiber re-loads every dependent through cordis itself. Reloading connection or runtime cascades the whole UI — correct, if heavy.
|
|
|
|
|
|
-The support boundary, stated honestly. Reload is coarse by design: fresh fiber, fresh components, React state lost, data layer untouched — react-refresh-grade state preservation conflicts with "re-executing the bundle re-runs the factory" and is deliberately out. Plain packages (react family, shell kernel, not-yet-promoted libraries) are not entries: changing them means a shell rebuild and a full page reload. No rollback in v1: an import failure leaves the entry fiberless and the next rebuilt frame retries from scratch; an apply failure leaves a FAILED fiber for the status projection; both log loudly. Self-reload works — the in-flight reload finishes in the old bundle's closure and the new apply opens a fresh SSE channel — but frames arriving in the gap are lost, and the next rebuild renotifies. One known dev-only race: a rebuilt frame overlapping a still-in-flight boot arrival shares that arrival's task and may materialize the pre-rebuild bytes; the next frame self-heals.
|
|
|
-
|
|
|
-## Package inventory (today → long term)
|
|
|
-
|
|
|
-| Package | Role | Today | Long term |
|
|
|
-|---|---|---|---|
|
|
|
-| react family / cordis | platform singletons | shell-bundled, seeded | plain forever (absolute base) |
|
|
|
-| vendored `@cordisjs/plugin-loader` | entry governance (same code both sides) | compile-time browserization, kernel-mounted | untouched (vendor policy) |
|
|
|
-| `dsh-client-modules` | the client module system | lazy CJS table; two-phase boot | plain forever (modules precede modules) |
|
|
|
-| `dsh-client-web` | shell kernel + AppRoot + app-shell assembly | self-sufficient (hand-rolled status stores, no plugin value imports) | keeps shrinking |
|
|
|
-| `dsh-client-ui-slots` | slot registry core | plain, seeded | promote to plugin; receive runtime's slots machinery |
|
|
|
-| `dsh-client-web-react` | ctx↔React glue | plain, seeded | promote to plugin; renderer install moves into its apply |
|
|
|
-| `dsh-client-ui-primitives` | base components | plain, seeded | promote to plugin (components via slots/services) |
|
|
|
-| `dsh-client-connection` | wire layer | plugin (`dsh.client` + bundle), declares `immediately` | transport swap (Electron IPC carrier) |
|
|
|
-| `dsh-client-runtime` | session object layer + slots service + store engine | plugin, declares `immediately` | keeps shrinking toward a pure session object layer |
|
|
|
-| `dsh-client-ui-theme` | theme tokens/service | plugin, declares `immediately`, plus the `./styles/*` source channel | Theme Registry (separate ruling) |
|
|
|
-| `dsh-client-i18n` | I18nService | plugin, declares `immediately` | per-deployment locale composition |
|
|
|
-| `dsh-client-hmr` | hot reload driver | plugin, declares `immediately` | rollback; reconnect handshake |
|
|
|
-| ui-layout / ui-sidebar / ui-conversation / ui-trajectory | UI features | plugins, on-demand | conversation domain split; trajectory real implementation |
|
|
|
+The support boundary, stated honestly. Reload is coarse by design: fresh fiber, fresh components, React state lost, data layer untouched — react-refresh-grade state preservation conflicts with "re-executing the bundle re-runs the factory" and is deliberately out. Static assembly packages and the shell kernel are not entries: changing them means a shell rebuild and a full page reload. Reload has no rollback: an import failure leaves the entry fiberless and the next rebuilt frame retries from scratch; an apply failure leaves a FAILED fiber for the status projection; both log loudly. Self-reload works — the in-flight reload finishes in the old bundle's closure and the new apply opens a fresh SSE channel — but frames arriving in the gap are lost, and the next rebuild renotifies. One known dev-only race: a rebuilt frame overlapping a still-in-flight boot arrival shares that arrival's task and may materialize the pre-rebuild bytes; the next frame self-heals.
|
|
|
+
|
|
|
+## Package ownership
|
|
|
+
|
|
|
+The current package inventory and build forms live in the [client shell layering note](2026-08-15-client-shells-and-dynamic-packages.md). This note retains only the loading properties that apply to every dynamic row: lazy factory registration, Cordis entry governance, external-script arrival, source maps, and HMR.
|
|
|
|
|
|
## Consequences
|
|
|
|
|
|
-One governance implementation runs on both sides of the wire; the browser-specific layer is one module system plus one reload plugin. Plugin packages have one shape, so the purity gate covers them all. Dependency edges and the boot tier live with their owners — the manifests — while the composing app holds only the roster. The drift classes stay structurally closed: share-list hand-sync, load-order coupling, cross-plugin imports, roster/tier double bookkeeping. Browser-native script loading preserves the standard mapping among plugin network resources, generated bundles, and TypeScript/TSX sources, while the module system keeps only one replaceable `loadBundle` hook.
|
|
|
+One governance implementation runs on both sides of the wire; the browser-specific layer is one module system plus one reload plugin. Dynamic packages have one artifact form, so the purity check covers them all. Cordis dependencies, module requests, and the boot tier live with their owners — the manifests — while the composing app holds only the roster. Host graph validation and recursive request arrival keep synchronous factory dependencies explicit. Browser-native script loading preserves the standard mapping among plugin network resources, generated bundles, and TypeScript/TSX sources, while the module system keeps only one replaceable `loadBundle` hook.
|
|
|
|
|
|
-Costs accepted: the vendored Loader carries idle machinery in the browser (EntryTree persistence is a no-op, groups/isolation unused); every plugin edit in dev pays a bundle rebuild plus fiber remount; graph `inject` rows are informational — activation truth is service-level — so a mismatch appears at the settled sweep, not at graph validation; the three not-yet-promoted libraries keep their static-import exports until their DI conversions land; every bundle gains a source-map artifact; and external-script failures provide only coarse URL diagnostics instead of the HTTP status available to an explicit fetch.
|
|
|
+Costs accepted: the vendored Loader carries idle machinery in the browser (EntryTree persistence is a no-op, groups/isolation unused); every plugin edit in dev pays a bundle rebuild plus fiber remount; graph `inject` rows are informational — activation truth is service-level — so a mismatch appears at the settled sweep, not at graph validation; the static UI libraries keep direct value exports; every bundle gains a source-map artifact; and external-script failures provide only coarse URL diagnostics instead of the HTTP status available to an explicit fetch.
|
|
|
|
|
|
-Roster: it lives in the web bundle's config tree (`packages/bundle/web-app/cordis.patch.yml`); `mountWebPlugins` and the `CLIENT_PACKAGES` constant are gone, and recomposing a deployment means swapping the yml/overlay. The graph composer moved from a webserver-side registry into the `dsh-client-modules` node half (the package upgraded to dual-face per this note's promotion rule — its consumer now reaches it through cordis DI), and the transport split landed alongside: the webserver became a plain route-registration plugin, `/api/*` binding moved to the connection node half over the upgraded `api-gateway` plugin (`dsh-host-apiproxy` providing `ctx.apiProxy`), and the dev bundle watch + SSE channel moved to the hmr node half.
|
|
|
+Roster: it lives in the web bundle's config tree (`packages/bundle/web-app/cordis.patch.yml`); `mountWebPlugins` and the `CLIENT_PACKAGES` constant are gone, and recomposing a deployment means swapping the yml/overlay. The graph composer lives in the `dsh-client-modules` node half, while the parser-preloaded client face bootstraps the browser module table. The webserver remains a plain route-registration plugin; `/api/*` binding belongs to the connection node half over `api-gateway` (`dsh-host-apiproxy` providing `ctx.apiProxy`), and the dev bundle watch plus SSE channel belongs to the hmr node half.
|
|
|
|
|
|
## Alternatives considered
|
|
|
|
|
|
@@ -137,7 +109,7 @@ Roster: it lives in the web bundle's config tree (`packages/bundle/web-app/cordi
|
|
|
| Reuse `@cordisjs/plugin-hmr` in the browser | ~80% solves problems the browser doesn't have (fs watching, deep graph coloring, Node's dual caches); the reload skeleton is copied as a shape |
|
|
|
| Module federation | Independently built remote bundles are exactly the form vite federation does not support |
|
|
|
| Import maps | Ruled out earlier; the DI require table is the terminal mechanism |
|
|
|
-| Full ctx-ification now (react and libraries via services, no module table) | The module-axis extreme; parked — the upgrade law walks there one package at a time instead |
|
|
|
-| Eager instantiation with a frozen table | Requires arrival-time ordering; lazy CJS registration makes recursive `require` self-ordering and matches the naive-puller phase split |
|
|
|
+| Full ctx-ification now (React and libraries via services, no module table) | Static UI libraries still expose synchronous values, so removing the table would leave those imports without a shared identity |
|
|
|
+| Eager instantiation with a frozen table | Runs bundle side effects at script arrival; lazy registration keeps execution at Cordis import while recursive `require` materializes registered requests |
|
|
|
| Fetch response text, then inject an inline `<script>` | Makes the module system buffer the complete source and maintain separate fetch/execute paths; dynamic source execution also breaks the browser-native association among the network resource, source map, and profile |
|
|
|
| Builder-push rebuild channel (`POST /plugins/rebuilt` from the orchestrator's `onSuccess`) | Couples reload to one blessed builder process and a second wire protocol; the webserver already holds every bundle path, and stat polling covers the torn-write race (re-hash on every stat change) that once justified pushing |
|