@monotykamary/dsh-client-web 0.1.0-rc.8 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.i18n.yaml +2 -2
- package/README.md +5 -8
- package/README.zh.md +5 -8
- package/lib/base.css +41 -0
- package/lib/boot-page.module.css +104 -0
- package/lib/index.js +154 -332
- package/lib/invariant.js +2 -0
- package/lib/types/boot-page.d.ts +46 -0
- package/lib/types/boot.d.ts +20 -41
- package/lib/types/index.d.ts +5 -11
- package/lib/types/loader-status.d.ts +3 -51
- package/lib/types/platform.d.ts +3 -1
- package/package.json +13 -20
- package/lib/types/AppRoot.d.ts +0 -16
- package/lib/types/DocumentTitle.d.ts +0 -13
- package/lib/types/app-shell.d.ts +0 -28
- package/lib/types/app.d.ts +0 -21
package/README.i18n.yaml
CHANGED
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
|
3
3
|
# after editing either side, bring the other along and re-record with:
|
|
4
4
|
# pnpm run verify-translation-pairing --write packages/client/web/README.md
|
|
5
|
-
README.md:
|
|
6
|
-
README.zh.md:
|
|
5
|
+
README.md: d305de8108d5859c2a45ffb00d0af600eef23c5f
|
|
6
|
+
README.zh.md: d1b5c919fc3d6f5a0948118e81280de901123f5b
|
package/README.md
CHANGED
|
@@ -2,15 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
English | [中文](README.zh.md)
|
|
4
4
|
|
|
5
|
-
Web
|
|
5
|
+
Web boot kernel: `new AppWebEntry(el, seams?).run()` mounts the client through two stages. The module stage calls the Host-installed `window.__ModuleLoader__.create()` with `window.__DSH_BOOT__`, the shell's static modules, and any test transport override; the facade returns the constructed module system and parsed manifest after adopting parser-preloaded registrations. This package then prefetches the `immediately` tier. The plugin stage mounts the vendored Cordis Loader, injects that module system through the Loader's `internal` interface, creates every graph entry uniformly, and waits for every fiber to become ACTIVE. It then hands the marked boot DOM to the dynamic UI renderer's `ctx.uiRenderer.mount(el)` operation; the renderer hydrates that DOM before switching to the complete UI. The Host owns the graph, parser preloads, and facade; AppWebEntry does not know the bootstrap package id or parse the wire format.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
The boot page uses plain DOM and local CSS, so client-bundle and plugin-activation failures remain visible. Its fallback fonts and colors match the theme tokens that arrive during loading. Fiber updates retain one spinner node and grow its CSS arc as entries first become active; hydration preserves that node and its animation phase until the application commit. React mounting, slot rendering, application assembly, and browser-title projection live in [`ui-renderer`](../ui-renderer/README.md). The modules bundle caches its own materialized exports and provides the closed-over system when its ordinary graph entry activates; Cordis service waiting makes graph-row creation order independent from that activation.
|
|
8
8
|
|
|
9
|
-
`PLATFORM_MODULES` (src/platform.ts) is the single source of truth for shared modules
|
|
9
|
+
`PLATFORM_MODULES` (src/platform.ts) is the single source of truth for shell-seeded shared modules. Together with `PRELOADED_CLIENT_EXTERNALS`, it defines the implicit external baseline for every dynamic bundle; `dsh.client.external` adds only exact non-baseline requests.
|
|
10
10
|
|
|
11
|
-
The optional override parameter `seams` forwards the module system's `loadBundle` transport override (`BootSeams`) for environments where external `<script>` execution cannot reach the page context; ordinary browser callers omit it.
|
|
12
|
-
|
|
13
|
-
The shell owns browser-title projection. With a selected session carrying a durable title, it renders `<session title> — <existing HTML title>` and reacts to later title revisions; no selection or a selected untitled session preserves the existing title, and shell unmount restores it. The existing HTML title remains the configurable product suffix. The non-reloadable kernel observes its shell-owned `#root` child list and document visibility: when a visible root loses its required AppRoot child, it reloads the complete document immediately; a root emptied while hidden reloads when the document becomes visible, while a mounted child never triggers recovery.
|
|
11
|
+
The optional override parameter `seams` forwards the module system's `loadBundle` transport override (`BootSeams`) for environments where external `<script>` execution cannot reach the page context; ordinary browser callers omit it. A pre-injected page transport is the default ahead of it: when `globalThis.__DSH_TRANSPORT__` (the connection package's `ClientTransportHooks`) carries `loadBundle`, the module stage adopts it as the bundle transport and skips the immediate-tier HTTP prefetch — explicit `seams` still win.
|
|
14
12
|
|
|
15
13
|
## Model Experience
|
|
16
14
|
|
|
@@ -22,5 +20,4 @@ None; this package neither assembles nor sends a provider request.
|
|
|
22
20
|
|
|
23
21
|
## Known Limitations and Deferred Work
|
|
24
22
|
|
|
25
|
-
- **
|
|
26
|
-
- **Narrow-window shell behavior lacks an assembled walkthrough** — ui-layout implements the concession chain, but this package has no shell-level narrow-viewport acceptance case.
|
|
23
|
+
- **The application waits for the full roster** — one failed entry keeps the framework-free boot page visible with a per-entry report; partial UI availability is not supported.
|
package/README.zh.md
CHANGED
|
@@ -2,15 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
[English](README.md) | 中文
|
|
4
4
|
|
|
5
|
-
Web
|
|
5
|
+
Web 启动内核:`new AppWebEntry(el, seams?).run()` 分两个阶段挂载客户端。模块阶段调用 Host 安装的 `window.__ModuleLoader__.create()`,传入 `window.__DSH_BOOT__`、外壳静态模块以及可选测试传输覆盖;facade 接纳 parser 预载的 registration 后返回构造好的模块系统与已解析 manifest。本包随后预取 `immediately` 层级。插件阶段挂载仓库内置的 Cordis Loader,通过 Loader 的 `internal` 接口注入该模块系统,统一创建全部图 entry,并等待每个 fiber 进入 ACTIVE。随后它把带标记的启动 DOM 交给动态 UI 渲染器的 `ctx.uiRenderer.mount(el)` 操作;渲染器先 hydrate 该 DOM,再切换到完整 UI。Graph、parser preload 与 facade 归 Host 所有;AppWebEntry 不感知 bootstrap package id,也不解析 wire 格式。
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
启动页只使用原生 DOM 与本地 CSS,因此客户端 bundle 或插件激活失败时仍能显示。其回退字体和颜色与加载期间到达的主题 token 一致。fiber 更新会保留同一个 spinner 节点,并在 entry 首次进入 active 时增长其 CSS 圆弧;hydrate 会继续保留该节点及其动画相位,直到应用提交。React 挂载、slot 渲染、应用组装和浏览器标题投影位于 [`ui-renderer`](../ui-renderer/README.zh.md)。Modules bundle 会缓存自身已物化导出,并在其普通图 entry 激活时提供闭包中的系统;Cordis service 等待使图 row 创建顺序不依赖该激活时点。
|
|
8
8
|
|
|
9
|
-
`PLATFORM_MODULES`(src/platform.ts
|
|
9
|
+
`PLATFORM_MODULES`(src/platform.ts)是外壳播种共享模块的唯一事实来源。它与 `PRELOADED_CLIENT_EXTERNALS` 一起定义全部动态 bundle 的隐式 external 基座;`dsh.client.external` 只添加基座之外的精确请求。
|
|
10
10
|
|
|
11
|
-
可选的覆盖参数 `seams` 会为外部 `<script>` 执行无法到达页面上下文的环境转发模块系统的 `loadBundle` 传输覆盖(`BootSeams
|
|
12
|
-
|
|
13
|
-
外壳拥有浏览器标题投影。选中带有持久标题的会话时,它会渲染 `<session title> — <existing HTML title>` 并响应后续标题修订;未选择会话或选中无标题会话时,会保留现有标题;外壳卸载时恢复标题。现有 HTML 标题仍是可配置的产品后缀。不可 reload 的 kernel 会观察外壳自有 `#root` 的子项列表与 document visibility:当可见 root 丢失其必需 AppRoot 子项时,它会立即 reload 完整 document;隐藏时变空的 root 会在 document 变为可见时 reload,仍挂载的子项不会触发恢复。
|
|
11
|
+
可选的覆盖参数 `seams` 会为外部 `<script>` 执行无法到达页面上下文的环境转发模块系统的 `loadBundle` 传输覆盖(`BootSeams`);普通浏览器调用方省略此参数。预注入的页面传输是位于其前的默认值:当 `globalThis.__DSH_TRANSPORT__`(connection 包的 `ClientTransportHooks`)携带 `loadBundle` 时,模块阶段将其采纳为 bundle 传输并跳过 immediately 层级的 HTTP 预取——显式 `seams` 仍然优先。
|
|
14
12
|
|
|
15
13
|
## 模型体验
|
|
16
14
|
|
|
@@ -22,5 +20,4 @@ Web 外壳内核:`new AppWebEntry(el, seams?).run()` 通过两阶段启动(w
|
|
|
22
20
|
|
|
23
21
|
## 已知限制与暂缓事项
|
|
24
22
|
|
|
25
|
-
-
|
|
26
|
-
- **窄窗口外壳行为缺少组装后演练**:ui-layout 已实现让步链,但该包没有外壳级窄视口验收用例。
|
|
23
|
+
- **应用会等待完整名册**:只要一个 entry 失败,不依赖框架的启动页就会保留并逐项报告;不支持部分 UI 可用。
|
package/lib/base.css
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/* Shell-owned mount defaults. Theme tokens arrive with the ui-theme client
|
|
2
|
+
* plugin before the loader roster activates. */
|
|
3
|
+
|
|
4
|
+
html,
|
|
5
|
+
body,
|
|
6
|
+
#root {
|
|
7
|
+
height: 100%;
|
|
8
|
+
margin: 0;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
body {
|
|
12
|
+
font-family: var(
|
|
13
|
+
--dsw-font-family,
|
|
14
|
+
-apple-system,
|
|
15
|
+
BlinkMacSystemFont,
|
|
16
|
+
'Segoe UI',
|
|
17
|
+
'PingFang SC',
|
|
18
|
+
'Hiragino Sans GB',
|
|
19
|
+
'Microsoft YaHei',
|
|
20
|
+
'Helvetica Neue',
|
|
21
|
+
Helvetica,
|
|
22
|
+
Arial,
|
|
23
|
+
sans-serif
|
|
24
|
+
);
|
|
25
|
+
/* Grayscale antialiasing over subpixel rendering: WebKit/Blink and the
|
|
26
|
+
Firefox macOS equivalent; other engines ignore both lines. */
|
|
27
|
+
-webkit-font-smoothing: antialiased;
|
|
28
|
+
-moz-osx-font-smoothing: grayscale;
|
|
29
|
+
color: var(--dsw-alias-label-primary, #0f1115);
|
|
30
|
+
background: var(--dsw-alias-bg-base, #fff);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/* Form controls don't inherit the body font (UA sheets pin their families —
|
|
34
|
+
Chrome buttons fall back to Arial, textareas to monospace), so the app
|
|
35
|
+
stack is re-applied to them explicitly, as upstream's global reset does. */
|
|
36
|
+
button,
|
|
37
|
+
input,
|
|
38
|
+
select,
|
|
39
|
+
textarea {
|
|
40
|
+
font-family: inherit;
|
|
41
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/* The framework-free boot page cannot depend on theme delivery succeeding. */
|
|
2
|
+
|
|
3
|
+
.boot {
|
|
4
|
+
--dsh-boot-bg: #fff;
|
|
5
|
+
--dsh-boot-label-primary: #0f1115;
|
|
6
|
+
--dsh-boot-label-secondary: #61666b;
|
|
7
|
+
--dsh-boot-label-tertiary: #81858c;
|
|
8
|
+
--dsh-boot-border: rgb(0 0 0 / 10%);
|
|
9
|
+
--dsh-boot-brand: #0f1115;
|
|
10
|
+
|
|
11
|
+
height: 100%;
|
|
12
|
+
display: grid;
|
|
13
|
+
place-items: center;
|
|
14
|
+
background: var(--dsw-alias-bg-base, var(--dsh-boot-bg));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
:global(body[data-ds-dark-theme]) .boot {
|
|
18
|
+
--dsh-boot-bg: #151517;
|
|
19
|
+
--dsh-boot-label-primary: #f9fafb;
|
|
20
|
+
--dsh-boot-label-secondary: #cfd3d6;
|
|
21
|
+
--dsh-boot-label-tertiary: #adb2b8;
|
|
22
|
+
--dsh-boot-border: rgb(255 255 255 / 12%);
|
|
23
|
+
--dsh-boot-brand: #f9fafb;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.card {
|
|
27
|
+
display: flex;
|
|
28
|
+
flex-direction: column;
|
|
29
|
+
align-items: center;
|
|
30
|
+
gap: 16px;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.wordmark {
|
|
34
|
+
font-size: 16px;
|
|
35
|
+
line-height: 24px;
|
|
36
|
+
font-weight: 600;
|
|
37
|
+
letter-spacing: 0.08em;
|
|
38
|
+
color: var(--dsw-alias-label-primary, var(--dsh-boot-label-primary));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.hint {
|
|
42
|
+
font-size: 12px;
|
|
43
|
+
line-height: 18px;
|
|
44
|
+
color: var(--dsw-alias-label-tertiary, var(--dsh-boot-label-tertiary));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.spinner {
|
|
48
|
+
position: relative;
|
|
49
|
+
width: 20px;
|
|
50
|
+
height: 20px;
|
|
51
|
+
border-radius: 50%;
|
|
52
|
+
border: 2px solid var(--dsw-alias-border-l2, var(--dsh-boot-border));
|
|
53
|
+
animation: spin 0.8s linear infinite;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.spinner::after {
|
|
57
|
+
content: '';
|
|
58
|
+
position: absolute;
|
|
59
|
+
inset: -2px;
|
|
60
|
+
border-radius: inherit;
|
|
61
|
+
background: conic-gradient(
|
|
62
|
+
var(--dsw-alias-brand-primary, var(--dsh-boot-brand)) var(--dsh-boot-arc, 72deg),
|
|
63
|
+
transparent 0
|
|
64
|
+
);
|
|
65
|
+
-webkit-mask: radial-gradient(farthest-side, transparent calc(100% - 2px), #000 0);
|
|
66
|
+
mask: radial-gradient(farthest-side, transparent calc(100% - 2px), #000 0);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
@keyframes spin {
|
|
70
|
+
to { transform: rotate(360deg); }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.failed {
|
|
74
|
+
max-width: 480px;
|
|
75
|
+
display: flex;
|
|
76
|
+
flex-direction: column;
|
|
77
|
+
gap: 8px;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.failedTitle {
|
|
81
|
+
font-size: 14px;
|
|
82
|
+
line-height: 22px;
|
|
83
|
+
font-weight: 600;
|
|
84
|
+
color: var(--dsw-alias-label-primary, var(--dsh-boot-label-primary));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.failedItem {
|
|
88
|
+
font-family: var(
|
|
89
|
+
--ds-font-family-code,
|
|
90
|
+
'SF Mono',
|
|
91
|
+
'JetBrains Mono',
|
|
92
|
+
'Fira Code',
|
|
93
|
+
Consolas,
|
|
94
|
+
'Liberation Mono',
|
|
95
|
+
Menlo,
|
|
96
|
+
Courier,
|
|
97
|
+
'PingFang SC',
|
|
98
|
+
'Microsoft YaHei'
|
|
99
|
+
);
|
|
100
|
+
font-size: 12px;
|
|
101
|
+
line-height: 18px;
|
|
102
|
+
color: var(--dsw-alias-label-secondary, var(--dsh-boot-label-secondary));
|
|
103
|
+
white-space: pre-wrap;
|
|
104
|
+
}
|
package/lib/index.js
CHANGED
|
@@ -1,153 +1,100 @@
|
|
|
1
|
-
import * as ReactJsxRuntime from "react/jsx-runtime";
|
|
2
|
-
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
1
|
import * as Cordis from "@monotykamary/cordis";
|
|
4
2
|
import { Context } from "@monotykamary/cordis";
|
|
5
3
|
import Loader from "@monotykamary/cordis-plugin-loader";
|
|
6
|
-
import
|
|
7
|
-
import { createRoot } from "react-dom/client";
|
|
8
|
-
import * as ModulesClient from "@monotykamary/dsh-client-modules/client";
|
|
9
|
-
import { ClientModuleSystem, parseBootManifest } from "@monotykamary/dsh-client-modules/client";
|
|
10
|
-
import * as WebReact from "@monotykamary/dsh-client-web-react";
|
|
11
|
-
import { bindSnapshotSelector, createSlotRenderer } from "@monotykamary/dsh-client-web-react";
|
|
4
|
+
import css from "./boot-page.module.css";
|
|
12
5
|
import * as React from "react";
|
|
13
|
-
import
|
|
6
|
+
import * as ReactJsxRuntime from "react/jsx-runtime";
|
|
14
7
|
import * as ReactDom from "react-dom";
|
|
8
|
+
import * as ReactDomClient from "react-dom/client";
|
|
15
9
|
import * as UiSlots from "@monotykamary/dsh-client-ui-slots";
|
|
16
10
|
import * as UiPrimitives from "@monotykamary/dsh-client-ui-primitives";
|
|
17
|
-
import
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
enumerable: true
|
|
26
|
-
});
|
|
27
|
-
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
28
|
-
return target;
|
|
29
|
-
};
|
|
30
|
-
//#endregion
|
|
31
|
-
//#region lib/types/DocumentTitle.js
|
|
32
|
-
/**
|
|
33
|
-
* Project the selected durable session title into the browser title and
|
|
34
|
-
* restore the shell's original product title when unmounted.
|
|
35
|
-
* @param props - selected session title projection.
|
|
36
|
-
* @returns no rendered content.
|
|
37
|
-
*/
|
|
38
|
-
function DocumentTitle({ title }) {
|
|
39
|
-
const original = useRef(document.title);
|
|
40
|
-
useEffect(() => {
|
|
41
|
-
document.title = title === void 0 ? original.current : `${title} — ${original.current}`;
|
|
42
|
-
return () => {
|
|
43
|
-
document.title = original.current;
|
|
44
|
-
};
|
|
45
|
-
}, [title]);
|
|
46
|
-
return null;
|
|
47
|
-
}
|
|
48
|
-
//#endregion
|
|
49
|
-
//#region lib/types/app.js
|
|
50
|
-
/**
|
|
51
|
-
* Build the renderApp factory the app-shell plugin provides to AppRoot.
|
|
52
|
-
* @param deps - assembly inputs.
|
|
53
|
-
* @returns factory producing the real UI tree (called once per AppRoot render after settled).
|
|
54
|
-
*/
|
|
55
|
-
function buildRenderApp(deps) {
|
|
56
|
-
const { ctx } = deps;
|
|
57
|
-
const sessions = ctx.get("sessions");
|
|
58
|
-
if (sessions === void 0) throw new Error("shell assembly: sessions service unavailable");
|
|
59
|
-
const useSessions = bindSnapshotSelector(sessions.list);
|
|
60
|
-
const SessionDocumentTitle = () => {
|
|
61
|
-
const title = useSessions((state) => {
|
|
62
|
-
const id = state.current;
|
|
63
|
-
return id === void 0 ? void 0 : state.byId[id]?.title;
|
|
64
|
-
});
|
|
65
|
-
return jsx(DocumentTitle, { ...title === void 0 ? {} : { title } });
|
|
66
|
-
};
|
|
67
|
-
return () => jsxs(Fragment, { children: [jsx(SessionDocumentTitle, {}), ctx.slots.renderSlot("root", {})] });
|
|
68
|
-
}
|
|
69
|
-
//#endregion
|
|
70
|
-
//#region lib/types/app-shell.js
|
|
71
|
-
var app_shell_exports = /* @__PURE__ */ __exportAll({
|
|
72
|
-
APP_SHELL_ID: () => APP_SHELL_ID,
|
|
73
|
-
apply: () => apply,
|
|
74
|
-
inject: () => inject,
|
|
75
|
-
name: () => name
|
|
76
|
-
});
|
|
77
|
-
/** Shell-owned pseudo entry id under which the host graph mounts this plugin. */
|
|
78
|
-
const APP_SHELL_ID = "@monotykamary/dsh-client-app-shell";
|
|
79
|
-
/** Cordis plugin name. */
|
|
80
|
-
const name = "app-shell";
|
|
81
|
-
/** Services required before shell assembly. */
|
|
82
|
-
const inject = [
|
|
83
|
-
"slots",
|
|
84
|
-
"sessions",
|
|
85
|
-
"layout"
|
|
86
|
-
];
|
|
87
|
-
/** Installs the React renderer and exposes the assembled application.
|
|
88
|
-
* @param ctx - Plugin context.
|
|
89
|
-
*/
|
|
90
|
-
function apply(ctx) {
|
|
91
|
-
ctx.slots.install(createSlotRenderer());
|
|
92
|
-
let renderApp;
|
|
93
|
-
ctx.reflect.provide("appShell", { renderApp: () => {
|
|
94
|
-
renderApp ??= buildRenderApp({ ctx });
|
|
95
|
-
return renderApp();
|
|
96
|
-
} });
|
|
97
|
-
}
|
|
98
|
-
//#endregion
|
|
99
|
-
//#region \0dsh-css-stub:./AppRoot.module.css.mjs
|
|
100
|
-
var AppRoot_module_css_default = {};
|
|
101
|
-
//#endregion
|
|
102
|
-
//#region lib/types/AppRoot.js
|
|
103
|
-
/**
|
|
104
|
-
* Shell root: boot loading page → (boot settled) → real UI in one switch.
|
|
105
|
-
* Pure kernel component with zero plugin dependencies — before settled it may
|
|
106
|
-
* only rely on itself (the fail-loud presentation must not depend on the
|
|
107
|
-
* system whose failure it reports; the status/signal stores are kernel-own,
|
|
108
|
-
* shell self-sufficiency rule); the real UI is produced by the
|
|
109
|
-
* app-shell entry once every entry is active. A failed boot keeps the
|
|
110
|
-
* loading page, lists the per-entry fiber states and the sweep report (fail
|
|
111
|
-
* loud, no partial UI).
|
|
112
|
-
*/
|
|
113
|
-
/** Boot gate: loading page until the boot settles; failures stay here. */
|
|
114
|
-
function AppRoot(props) {
|
|
115
|
-
const settled = useSyncExternalStore(props.settled.subscribe, props.settled.getSnapshot);
|
|
116
|
-
const status = useSyncExternalStore(props.status.subscribe, props.status.getSnapshot);
|
|
117
|
-
const error = useSyncExternalStore(props.error.subscribe, props.error.getSnapshot);
|
|
118
|
-
const failed = Object.entries(status).filter(([, s]) => s === "failed");
|
|
119
|
-
if (settled) return jsx(Fragment, { children: props.renderApp() });
|
|
120
|
-
const loud = error !== void 0 || failed.length > 0;
|
|
121
|
-
return jsx("div", {
|
|
122
|
-
className: AppRoot_module_css_default.boot,
|
|
123
|
-
children: jsxs("div", {
|
|
124
|
-
className: AppRoot_module_css_default.card,
|
|
125
|
-
children: [jsx("div", {
|
|
126
|
-
className: AppRoot_module_css_default.wordmark,
|
|
127
|
-
children: "HARNESS"
|
|
128
|
-
}), !loud ? jsxs(Fragment, { children: [jsx("div", { className: AppRoot_module_css_default.spinner }), jsx("div", {
|
|
129
|
-
className: AppRoot_module_css_default.hint,
|
|
130
|
-
children: "Loading plugins…"
|
|
131
|
-
})] }) : jsxs("div", {
|
|
132
|
-
className: AppRoot_module_css_default.failed,
|
|
133
|
-
children: [
|
|
134
|
-
jsx("div", {
|
|
135
|
-
className: AppRoot_module_css_default.failedTitle,
|
|
136
|
-
children: "Failed to load plugins"
|
|
137
|
-
}),
|
|
138
|
-
failed.map(([id]) => jsx("div", {
|
|
139
|
-
className: AppRoot_module_css_default.failedItem,
|
|
140
|
-
children: id
|
|
141
|
-
}, id)),
|
|
142
|
-
error !== void 0 && jsx("div", {
|
|
143
|
-
className: AppRoot_module_css_default.failedItem,
|
|
144
|
-
children: error
|
|
145
|
-
})
|
|
146
|
-
]
|
|
147
|
-
})]
|
|
148
|
-
})
|
|
149
|
-
});
|
|
11
|
+
import "./base.css";
|
|
12
|
+
//#region lib/types/boot-page.js
|
|
13
|
+
/** Create a div with one module class and optional text. */
|
|
14
|
+
function div(className, text) {
|
|
15
|
+
const el = document.createElement("div");
|
|
16
|
+
el.className = className ?? "";
|
|
17
|
+
if (text !== void 0) el.textContent = text;
|
|
18
|
+
return el;
|
|
150
19
|
}
|
|
20
|
+
/** Kernel-owned page mounted below the application's root element. */
|
|
21
|
+
var BootPage = class {
|
|
22
|
+
root;
|
|
23
|
+
card;
|
|
24
|
+
wordmark;
|
|
25
|
+
spinner;
|
|
26
|
+
hint;
|
|
27
|
+
states = /* @__PURE__ */ new Map();
|
|
28
|
+
active = /* @__PURE__ */ new Set();
|
|
29
|
+
total = 0;
|
|
30
|
+
failure;
|
|
31
|
+
/**
|
|
32
|
+
* Build and attach the boot page.
|
|
33
|
+
* @param container - Application mount point.
|
|
34
|
+
*/
|
|
35
|
+
constructor(container) {
|
|
36
|
+
this.root = div(css.boot);
|
|
37
|
+
this.root.dataset.dshBoot = "";
|
|
38
|
+
this.card = div(css.card);
|
|
39
|
+
this.wordmark = div(css.wordmark, "HARNESS");
|
|
40
|
+
this.spinner = div(css.spinner);
|
|
41
|
+
this.spinner.dataset.dshBootSpinner = "";
|
|
42
|
+
this.hint = div(css.hint, "Loading plugins…");
|
|
43
|
+
this.card.append(this.wordmark, this.spinner, this.hint);
|
|
44
|
+
this.root.append(this.card);
|
|
45
|
+
container.append(this.root);
|
|
46
|
+
this.updateProgress();
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Set the number of loader entries represented by the progress arc.
|
|
50
|
+
* @param total - Complete boot roster size.
|
|
51
|
+
*/
|
|
52
|
+
setTotal(total) {
|
|
53
|
+
this.total = total;
|
|
54
|
+
this.updateProgress();
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Project one loader entry's fiber state.
|
|
58
|
+
* @param id - Loader entry name.
|
|
59
|
+
* @param state - Projected fiber state.
|
|
60
|
+
*/
|
|
61
|
+
setState(id, state) {
|
|
62
|
+
this.states.set(id, state);
|
|
63
|
+
if (state === "active") this.active.add(id);
|
|
64
|
+
this.updateProgress();
|
|
65
|
+
this.render();
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Display the boot failure report.
|
|
69
|
+
* @param message - Failure report text.
|
|
70
|
+
*/
|
|
71
|
+
fail(message) {
|
|
72
|
+
this.failure = message;
|
|
73
|
+
this.render();
|
|
74
|
+
}
|
|
75
|
+
/** Detach the page before or after the UI renderer takes the mount point. */
|
|
76
|
+
dispose() {
|
|
77
|
+
this.root.remove();
|
|
78
|
+
}
|
|
79
|
+
/** Redraw the state-dependent content below the wordmark. */
|
|
80
|
+
render() {
|
|
81
|
+
const failed = [...this.states].filter(([, state]) => state === "failed").map(([id]) => id);
|
|
82
|
+
if (this.failure === void 0 && failed.length === 0) {
|
|
83
|
+
if (this.spinner.parentElement !== this.card) this.card.replaceChildren(this.wordmark, this.spinner, this.hint);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const report = div(css.failed);
|
|
87
|
+
report.append(div(css.failedTitle, "Failed to load plugins"));
|
|
88
|
+
for (const id of failed) report.append(div(css.failedItem, id));
|
|
89
|
+
if (this.failure !== void 0) report.append(div(css.failedItem, this.failure));
|
|
90
|
+
this.card.replaceChildren(this.wordmark, report);
|
|
91
|
+
}
|
|
92
|
+
/** Grow the rotating arc monotonically as loader entries activate. */
|
|
93
|
+
updateProgress() {
|
|
94
|
+
const ratio = this.total === 0 ? 0 : Math.min(this.active.size / this.total, 1);
|
|
95
|
+
this.spinner.style.setProperty("--dsh-boot-arc", `${String(Math.round(72 + ratio * 216))}deg`);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
151
98
|
//#endregion
|
|
152
99
|
//#region lib/types/seed.js
|
|
153
100
|
/**
|
|
@@ -170,10 +117,7 @@ function getStaticModules() {
|
|
|
170
117
|
"react-dom/client": ReactDomClient,
|
|
171
118
|
"@monotykamary/cordis": Cordis,
|
|
172
119
|
"@monotykamary/dsh-client-ui-slots": UiSlots,
|
|
173
|
-
"@monotykamary/dsh-client-
|
|
174
|
-
"@monotykamary/dsh-client-ui-primitives": UiPrimitives,
|
|
175
|
-
"@monotykamary/dsh-client-ui-attachment": UiAttachment,
|
|
176
|
-
"@monotykamary/dsh-client-schema-form": SchemaForm
|
|
120
|
+
"@monotykamary/dsh-client-ui-primitives": UiPrimitives
|
|
177
121
|
};
|
|
178
122
|
}
|
|
179
123
|
//#endregion
|
|
@@ -201,224 +145,101 @@ const STATE_LABELS = {
|
|
|
201
145
|
[FIBER_STATE.DISPOSED]: "disposed",
|
|
202
146
|
[FIBER_STATE.UNLOADING]: "unloading"
|
|
203
147
|
};
|
|
204
|
-
/**
|
|
205
|
-
* Create a writable kernel signal.
|
|
206
|
-
* @param init - initial value.
|
|
207
|
-
* @returns the signal.
|
|
208
|
-
*/
|
|
209
|
-
function createSignal(init) {
|
|
210
|
-
let value = init;
|
|
211
|
-
const listeners = /* @__PURE__ */ new Set();
|
|
212
|
-
return {
|
|
213
|
-
getSnapshot: () => value,
|
|
214
|
-
subscribe: (fn) => {
|
|
215
|
-
listeners.add(fn);
|
|
216
|
-
return () => {
|
|
217
|
-
listeners.delete(fn);
|
|
218
|
-
};
|
|
219
|
-
},
|
|
220
|
-
set: (next) => {
|
|
221
|
-
value = next;
|
|
222
|
-
for (const fn of [...listeners]) fn();
|
|
223
|
-
}
|
|
224
|
-
};
|
|
225
|
-
}
|
|
226
|
-
/**
|
|
227
|
-
* Create the boot status store.
|
|
228
|
-
* @returns the store (empty until the boot chain projects rows).
|
|
229
|
-
*/
|
|
230
|
-
function createLoaderStatusStore() {
|
|
231
|
-
let value = {};
|
|
232
|
-
const listeners = /* @__PURE__ */ new Set();
|
|
233
|
-
return {
|
|
234
|
-
getSnapshot: () => value,
|
|
235
|
-
subscribe: (fn) => {
|
|
236
|
-
listeners.add(fn);
|
|
237
|
-
return () => {
|
|
238
|
-
listeners.delete(fn);
|
|
239
|
-
};
|
|
240
|
-
},
|
|
241
|
-
set: (id, state) => {
|
|
242
|
-
value = {
|
|
243
|
-
...value,
|
|
244
|
-
[id]: state
|
|
245
|
-
};
|
|
246
|
-
for (const fn of [...listeners]) fn();
|
|
247
|
-
}
|
|
248
|
-
};
|
|
249
|
-
}
|
|
250
148
|
//#endregion
|
|
251
149
|
//#region lib/types/boot.js
|
|
252
150
|
/**
|
|
253
|
-
* Web
|
|
254
|
-
*
|
|
255
|
-
*
|
|
256
|
-
*
|
|
257
|
-
* sanctioned exception is the modules package (bootstrap
|
|
258
|
-
* identity): the module system cannot arrive through itself, so its class
|
|
259
|
-
* and its client-half wrapper are shell-bundled and the kernel adopts its
|
|
260
|
-
* plugin entry once cordis is up.
|
|
261
|
-
*
|
|
262
|
-
* AppWebEntry.run(), module face first, then plugin face: parse
|
|
263
|
-
* `window.__DSH_BOOT__` into the two-view BootManifest (wire boundary)
|
|
264
|
-
* → build the module system over the module-view rows → render the loading
|
|
265
|
-
* page → prefetch every `immediately` row in parallel with mounting the
|
|
266
|
-
* vendored cordis Loader (`internal` contract injection BEFORE any entry exists —
|
|
267
|
-
* the bare-import fallback in tree.import must never run in a browser) →
|
|
268
|
-
* await the prefetch tier, THEN adopt the modules entry and create one
|
|
269
|
-
* loader entry per plugin-view row plus the shell-own app-shell assembly
|
|
270
|
-
* entry → loader.await() + a full fiber sweep (all ACTIVE, else fail
|
|
271
|
-
* listing who/what/which service) → flip the settled signal so AppRoot
|
|
272
|
-
* switches to the real UI in one pass.
|
|
273
|
-
*
|
|
274
|
-
* Entry creation waits for the whole immediately tier: materialization runs
|
|
275
|
-
* synchronous cross-package require edges (e.g. locale → runtime/client) that
|
|
276
|
-
* fiber inject waiting cannot protect — a bundle's factory must be
|
|
277
|
-
* registered before any dependent entry materializes. Per-row prefetch
|
|
278
|
-
* failures still resolve silently (the create-side import reloads and
|
|
279
|
-
* owns the loud failure), so the barrier never turns one bad bundle into a
|
|
280
|
-
* boot-wide fail-fast.
|
|
281
|
-
*
|
|
282
|
-
* Composition lives in the host graph; the shell makes zero composition
|
|
283
|
-
* decisions (the app-shell assembly is itself a graph entry, the only
|
|
284
|
-
* shell-own module registered with the module system).
|
|
285
|
-
*/
|
|
286
|
-
/**
|
|
287
|
-
* Reload a visible document whose shell-owned React mount point has become empty.
|
|
288
|
-
* @param element - shell mount point that must retain one AppRoot child after render.
|
|
289
|
-
* @param reload - document reload action, replaceable by browser tests.
|
|
290
|
-
* @returns disposer for the visibility listener.
|
|
291
|
-
*/
|
|
292
|
-
function installBlankRootRecovery(element, reload = () => {
|
|
293
|
-
globalThis.location.reload();
|
|
294
|
-
}) {
|
|
295
|
-
const recover = () => {
|
|
296
|
-
if (document.visibilityState === "visible" && element.childElementCount === 0) reload();
|
|
297
|
-
};
|
|
298
|
-
const observer = new MutationObserver(recover);
|
|
299
|
-
observer.observe(element, { childList: true });
|
|
300
|
-
document.addEventListener("visibilitychange", recover);
|
|
301
|
-
return () => {
|
|
302
|
-
observer.disconnect();
|
|
303
|
-
document.removeEventListener("visibilitychange", recover);
|
|
304
|
-
};
|
|
305
|
-
}
|
|
306
|
-
/**
|
|
307
|
-
* The modules package's own graph row id. The kernel adopts that entry
|
|
308
|
-
* itself (its wrapper is statically registered — shell-bundled code, never
|
|
309
|
-
* fetched), so the plugin-row loop must skip it: the vendored Group.create
|
|
310
|
-
* does not deduplicate by name, and a second fiber would provide 'modules'
|
|
311
|
-
* twice.
|
|
312
|
-
*/
|
|
313
|
-
const MODULES_ID = "@monotykamary/dsh-client-modules";
|
|
314
|
-
/**
|
|
315
|
-
* The web shell kernel: mounts the loading page into a DOM element and runs
|
|
316
|
-
* the two-stage boot over the host graph. Fields hold only what must exist
|
|
317
|
-
* before cordis does — the parsed manifest, the module system, and the
|
|
318
|
-
* loading-page UI handles; everything else lives in plugins.
|
|
151
|
+
* Web boot kernel. It owns only the module system, Cordis loader, and a
|
|
152
|
+
* framework-free boot page. The dynamic UI renderer receives the mount
|
|
153
|
+
* point after every client entry activates.
|
|
154
|
+
* @module @monotykamary/dsh-client-web/src/boot
|
|
319
155
|
*/
|
|
156
|
+
/** Browser boot entry consumed by `apps/web`. */
|
|
320
157
|
var AppWebEntry = class {
|
|
321
|
-
|
|
158
|
+
container;
|
|
322
159
|
seams;
|
|
323
|
-
|
|
324
|
-
settled = createSignal(false);
|
|
325
|
-
error = createSignal(void 0);
|
|
160
|
+
page;
|
|
326
161
|
ctx;
|
|
327
162
|
modules;
|
|
328
163
|
manifest;
|
|
329
|
-
root;
|
|
330
|
-
disposeBlankRootRecovery;
|
|
331
164
|
/**
|
|
332
|
-
*
|
|
333
|
-
* @param
|
|
334
|
-
* @param seams - Optional module transport
|
|
165
|
+
* Draw the boot page; {@link run} starts the loader.
|
|
166
|
+
* @param container - Application mount point.
|
|
167
|
+
* @param seams - Optional module transport replacement.
|
|
335
168
|
*/
|
|
336
|
-
constructor(
|
|
337
|
-
this.
|
|
169
|
+
constructor(container, seams) {
|
|
170
|
+
this.container = container;
|
|
338
171
|
this.seams = seams;
|
|
172
|
+
this.page = new BootPage(container);
|
|
339
173
|
}
|
|
340
174
|
/**
|
|
341
|
-
*
|
|
342
|
-
*
|
|
343
|
-
*
|
|
344
|
-
* is missing or malformed — there is nothing to boot against.
|
|
345
|
-
* @returns resolves once the UI settled or the failure report rendered.
|
|
175
|
+
* Load and activate every client entry, then hand the mount point to the
|
|
176
|
+
* UI renderer. Plugin failures remain visible on the boot page.
|
|
177
|
+
* @returns Resolves after application mount or failure rendering.
|
|
346
178
|
*/
|
|
347
179
|
async run() {
|
|
348
|
-
this.manifest = parseBootManifest(globalThis.__DSH_BOOT__);
|
|
349
|
-
this.modules = new ClientModuleSystem({
|
|
350
|
-
modules: this.manifest.modules,
|
|
351
|
-
staticModules: getStaticModules(),
|
|
352
|
-
...this.seams
|
|
353
|
-
});
|
|
354
|
-
this.modules.registerStatic(APP_SHELL_ID, app_shell_exports);
|
|
355
|
-
this.modules.registerStatic(MODULES_ID, ModulesClient);
|
|
356
|
-
globalThis.__DSH_MODULES__ = this.modules;
|
|
357
|
-
this.root = createRoot(this.el);
|
|
358
|
-
this.root.render(jsx(AppRoot, {
|
|
359
|
-
settled: this.settled,
|
|
360
|
-
status: this.status,
|
|
361
|
-
error: this.error,
|
|
362
|
-
renderApp: () => {
|
|
363
|
-
const shell = this.ctx.get("appShell");
|
|
364
|
-
if (shell === void 0) throw new Error("web boot: appShell service missing after settled");
|
|
365
|
-
return shell.renderApp();
|
|
366
|
-
}
|
|
367
|
-
}));
|
|
368
|
-
this.disposeBlankRootRecovery = installBlankRootRecovery(this.el);
|
|
369
|
-
const prefetching = this.prefetchImmediateTier();
|
|
370
|
-
this.ctx = new Context();
|
|
371
180
|
try {
|
|
372
|
-
|
|
373
|
-
|
|
181
|
+
const win = globalThis;
|
|
182
|
+
const moduleLoader = win.__ModuleLoader__;
|
|
183
|
+
if (moduleLoader === void 0) throw new Error("web boot: window.__ModuleLoader__ bootstrap facade is missing");
|
|
184
|
+
const transport = globalThis.__DSH_TRANSPORT__;
|
|
185
|
+
this.modules = moduleLoader.create({
|
|
186
|
+
boot: win.__DSH_BOOT__,
|
|
187
|
+
staticModules: getStaticModules(),
|
|
188
|
+
...transport?.loadBundle === void 0 ? {} : { loadBundle: transport.loadBundle },
|
|
189
|
+
...this.seams
|
|
190
|
+
});
|
|
191
|
+
this.manifest = this.modules.manifest;
|
|
192
|
+
const prefetching = this.prefetchImmediateTier();
|
|
193
|
+
const ctx = new Context();
|
|
194
|
+
this.ctx = ctx;
|
|
195
|
+
await this.runPluginBoot(ctx, prefetching);
|
|
196
|
+
await this.mountApp(ctx);
|
|
374
197
|
} catch (reason) {
|
|
375
198
|
console.error(reason);
|
|
376
|
-
this.
|
|
199
|
+
this.page.fail(reason instanceof Error ? reason.message : String(reason));
|
|
377
200
|
}
|
|
378
201
|
}
|
|
379
|
-
/**
|
|
380
|
-
dispose() {
|
|
381
|
-
this.
|
|
382
|
-
this.
|
|
383
|
-
|
|
202
|
+
/** Dispose the client plugin tree and whichever page owns the mount point. */
|
|
203
|
+
async dispose() {
|
|
204
|
+
const ctx = this.ctx;
|
|
205
|
+
this.ctx = void 0;
|
|
206
|
+
if (ctx !== void 0) await ctx.fiber.dispose();
|
|
207
|
+
this.page.dispose();
|
|
208
|
+
}
|
|
209
|
+
/** Mount through a dependency fiber so replacing uiRenderer remounts the application. */
|
|
210
|
+
async mountApp(ctx) {
|
|
211
|
+
await ctx.inject(["uiRenderer"], (scope) => {
|
|
212
|
+
scope.effect(() => scope.uiRenderer.mount(this.container), "web boot: application mount");
|
|
213
|
+
});
|
|
384
214
|
}
|
|
385
|
-
/** Prefetch
|
|
215
|
+
/** Prefetch stage-one bundles; their import path owns any eventual failure. */
|
|
386
216
|
async prefetchImmediateTier() {
|
|
387
|
-
|
|
217
|
+
if (globalThis.__DSH_TRANSPORT__?.loadBundle !== void 0) return;
|
|
218
|
+
await Promise.all(this.manifest.plugins.filter((row) => row.immediately).map((row) => this.modules.prefetch(row.id).catch((_prefetchError) => {})));
|
|
388
219
|
}
|
|
389
|
-
/**
|
|
390
|
-
async runPluginBoot(prefetching) {
|
|
391
|
-
const ctx = this.ctx;
|
|
220
|
+
/** Mount the Loader, create all graph entries, await quiescence, and audit activation. */
|
|
221
|
+
async runPluginBoot(ctx, prefetching) {
|
|
392
222
|
await ctx.plugin(Loader);
|
|
393
223
|
const loader = ctx.loader;
|
|
394
224
|
loader.internal = this.modules;
|
|
395
225
|
ctx.on("internal/status", (fiber) => {
|
|
396
226
|
const entry = fiber.entry;
|
|
397
227
|
if (entry === void 0 || entry.fiber === void 0) return;
|
|
398
|
-
this.
|
|
228
|
+
this.page.setState(entry.options.name, STATE_LABELS[entry.fiber.state]);
|
|
399
229
|
});
|
|
230
|
+
const rows = this.manifest.plugins.map((row) => row.id);
|
|
231
|
+
this.page.setTotal(rows.length);
|
|
400
232
|
await prefetching;
|
|
401
|
-
const rows = [
|
|
402
|
-
MODULES_ID,
|
|
403
|
-
...this.manifest.plugins.map((row) => row.id).filter((id) => id !== MODULES_ID),
|
|
404
|
-
APP_SHELL_ID
|
|
405
|
-
];
|
|
406
233
|
await Promise.all(rows.map(async (name) => {
|
|
407
|
-
this.
|
|
234
|
+
this.page.setState(name, "loading");
|
|
408
235
|
const id = await loader.create({ name });
|
|
409
|
-
if (loader.resolve(id).fiber === void 0) this.
|
|
236
|
+
if (loader.resolve(id).fiber === void 0) this.page.setState(name, "failed");
|
|
410
237
|
}));
|
|
411
238
|
await loader.await();
|
|
412
|
-
this.assertEntriesActive();
|
|
239
|
+
this.assertEntriesActive(ctx);
|
|
413
240
|
}
|
|
414
|
-
/**
|
|
415
|
-
|
|
416
|
-
* fiber failed its import; a fiber not ACTIVE is FAILED (apply threw) or
|
|
417
|
-
* PENDING (a required service never arrived — cordis inject waiting has no
|
|
418
|
-
* timeout, so this sweep is the fail-loud compensation).
|
|
419
|
-
*/
|
|
420
|
-
assertEntriesActive() {
|
|
421
|
-
const ctx = this.ctx;
|
|
241
|
+
/** Reject entries that failed import/apply or still wait on missing services. */
|
|
242
|
+
assertEntriesActive(ctx) {
|
|
422
243
|
const failures = [];
|
|
423
244
|
for (const entry of ctx.loader.entries()) {
|
|
424
245
|
const name = entry.options.name;
|
|
@@ -451,10 +272,11 @@ const PLATFORM_MODULES = [
|
|
|
451
272
|
"react-dom/client",
|
|
452
273
|
"@monotykamary/cordis",
|
|
453
274
|
"@monotykamary/dsh-client-ui-slots",
|
|
454
|
-
"@monotykamary/dsh-client-
|
|
455
|
-
"@monotykamary/dsh-client-ui-primitives",
|
|
456
|
-
"@monotykamary/dsh-client-ui-attachment",
|
|
457
|
-
"@monotykamary/dsh-client-schema-form"
|
|
275
|
+
"@monotykamary/dsh-client-ui-primitives"
|
|
458
276
|
];
|
|
277
|
+
/** Client-bundle specifiers whose factories the parser preloads before the shell starts. */
|
|
278
|
+
const PRELOADED_CLIENT_EXTERNALS = ["@monotykamary/dsh-client-runtime/client"];
|
|
459
279
|
//#endregion
|
|
460
|
-
export {
|
|
280
|
+
export { AppWebEntry, PLATFORM_MODULES, PRELOADED_CLIENT_EXTERNALS, getStaticModules };
|
|
281
|
+
|
|
282
|
+
//# sourceMappingURL=index.js.map
|
package/lib/invariant.js
CHANGED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Framework-free boot page and failure report. It remains available when a
|
|
3
|
+
* client plugin fails because React arrives only with the UI renderer.
|
|
4
|
+
* @module @monotykamary/dsh-client-web/src/boot-page
|
|
5
|
+
*/
|
|
6
|
+
import type { LoaderEntryState } from './loader-status.ts';
|
|
7
|
+
/** Kernel-owned page mounted below the application's root element. */
|
|
8
|
+
export declare class BootPage {
|
|
9
|
+
private readonly root;
|
|
10
|
+
private readonly card;
|
|
11
|
+
private readonly wordmark;
|
|
12
|
+
private readonly spinner;
|
|
13
|
+
private readonly hint;
|
|
14
|
+
private readonly states;
|
|
15
|
+
private readonly active;
|
|
16
|
+
private total;
|
|
17
|
+
private failure;
|
|
18
|
+
/**
|
|
19
|
+
* Build and attach the boot page.
|
|
20
|
+
* @param container - Application mount point.
|
|
21
|
+
*/
|
|
22
|
+
constructor(container: HTMLElement);
|
|
23
|
+
/**
|
|
24
|
+
* Set the number of loader entries represented by the progress arc.
|
|
25
|
+
* @param total - Complete boot roster size.
|
|
26
|
+
*/
|
|
27
|
+
setTotal(total: number): void;
|
|
28
|
+
/**
|
|
29
|
+
* Project one loader entry's fiber state.
|
|
30
|
+
* @param id - Loader entry name.
|
|
31
|
+
* @param state - Projected fiber state.
|
|
32
|
+
*/
|
|
33
|
+
setState(id: string, state: LoaderEntryState): void;
|
|
34
|
+
/**
|
|
35
|
+
* Display the boot failure report.
|
|
36
|
+
* @param message - Failure report text.
|
|
37
|
+
*/
|
|
38
|
+
fail(message: string): void;
|
|
39
|
+
/** Detach the page before or after the UI renderer takes the mount point. */
|
|
40
|
+
dispose(): void;
|
|
41
|
+
/** Redraw the state-dependent content below the wordmark. */
|
|
42
|
+
private render;
|
|
43
|
+
/** Grow the rotating arc monotonically as loader entries activate. */
|
|
44
|
+
private updateProgress;
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=boot-page.d.ts.map
|
package/lib/types/boot.d.ts
CHANGED
|
@@ -1,57 +1,36 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { ClientModuleCreateOptions } from '@monotykamary/dsh-client-modules/client';
|
|
2
2
|
import './base.css';
|
|
3
|
-
/** Module transport hook
|
|
4
|
-
export type BootSeams = Pick<
|
|
5
|
-
/**
|
|
6
|
-
* Reload a visible document whose shell-owned React mount point has become empty.
|
|
7
|
-
* @param element - shell mount point that must retain one AppRoot child after render.
|
|
8
|
-
* @param reload - document reload action, replaceable by browser tests.
|
|
9
|
-
* @returns disposer for the visibility listener.
|
|
10
|
-
*/
|
|
11
|
-
export declare function installBlankRootRecovery(element: HTMLElement, reload?: () => void): () => void;
|
|
12
|
-
/**
|
|
13
|
-
* The web shell kernel: mounts the loading page into a DOM element and runs
|
|
14
|
-
* the two-stage boot over the host graph. Fields hold only what must exist
|
|
15
|
-
* before cordis does — the parsed manifest, the module system, and the
|
|
16
|
-
* loading-page UI handles; everything else lives in plugins.
|
|
17
|
-
*/
|
|
3
|
+
/** Module transport hook replaced by jsdom tests. */
|
|
4
|
+
export type BootSeams = Pick<ClientModuleCreateOptions, 'loadBundle'>;
|
|
5
|
+
/** Browser boot entry consumed by `apps/web`. */
|
|
18
6
|
export declare class AppWebEntry {
|
|
19
|
-
private readonly
|
|
7
|
+
private readonly container;
|
|
20
8
|
private readonly seams;
|
|
21
|
-
private readonly
|
|
22
|
-
private readonly settled;
|
|
23
|
-
private readonly error;
|
|
9
|
+
private readonly page;
|
|
24
10
|
private ctx;
|
|
25
11
|
private modules;
|
|
26
12
|
private manifest;
|
|
27
|
-
private root;
|
|
28
|
-
private disposeBlankRootRecovery;
|
|
29
13
|
/**
|
|
30
|
-
*
|
|
31
|
-
* @param
|
|
32
|
-
* @param seams - Optional module transport
|
|
14
|
+
* Draw the boot page; {@link run} starts the loader.
|
|
15
|
+
* @param container - Application mount point.
|
|
16
|
+
* @param seams - Optional module transport replacement.
|
|
33
17
|
*/
|
|
34
|
-
constructor(
|
|
18
|
+
constructor(container: HTMLElement, seams?: BootSeams);
|
|
35
19
|
/**
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
* is missing or malformed — there is nothing to boot against.
|
|
40
|
-
* @returns resolves once the UI settled or the failure report rendered.
|
|
20
|
+
* Load and activate every client entry, then hand the mount point to the
|
|
21
|
+
* UI renderer. Plugin failures remain visible on the boot page.
|
|
22
|
+
* @returns Resolves after application mount or failure rendering.
|
|
41
23
|
*/
|
|
42
24
|
run(): Promise<void>;
|
|
43
|
-
/**
|
|
44
|
-
dispose(): void
|
|
45
|
-
/**
|
|
25
|
+
/** Dispose the client plugin tree and whichever page owns the mount point. */
|
|
26
|
+
dispose(): Promise<void>;
|
|
27
|
+
/** Mount through a dependency fiber so replacing uiRenderer remounts the application. */
|
|
28
|
+
private mountApp;
|
|
29
|
+
/** Prefetch stage-one bundles; their import path owns any eventual failure. */
|
|
46
30
|
private prefetchImmediateTier;
|
|
47
|
-
/**
|
|
31
|
+
/** Mount the Loader, create all graph entries, await quiescence, and audit activation. */
|
|
48
32
|
private runPluginBoot;
|
|
49
|
-
/**
|
|
50
|
-
* Sweep every loader entry after the tree quiesced: an entry without a
|
|
51
|
-
* fiber failed its import; a fiber not ACTIVE is FAILED (apply threw) or
|
|
52
|
-
* PENDING (a required service never arrived — cordis inject waiting has no
|
|
53
|
-
* timeout, so this sweep is the fail-loud compensation).
|
|
54
|
-
*/
|
|
33
|
+
/** Reject entries that failed import/apply or still wait on missing services. */
|
|
55
34
|
private assertEntriesActive;
|
|
56
35
|
}
|
|
57
36
|
//# sourceMappingURL=boot.d.ts.map
|
package/lib/types/index.d.ts
CHANGED
|
@@ -1,17 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Web shell library entry. The shell's product is {@link AppWebEntry} —
|
|
3
|
-
* apps/web's
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* single source of truth for the tsdown client externals projection.
|
|
3
|
+
* apps/web's Vite entry runs it against #root. The boot page and fiber-state
|
|
4
|
+
* projection remain internal; the static module table and its platform words
|
|
5
|
+
* form the package's build-time contract.
|
|
7
6
|
* @module @monotykamary/dsh-client-web
|
|
8
7
|
*/
|
|
9
|
-
export { AppWebEntry, type BootSeams } from './boot.
|
|
10
|
-
export { AppRoot, type AppRootProps } from './AppRoot.tsx';
|
|
11
|
-
export { buildRenderApp, type AssemblyDeps } from './app.tsx';
|
|
12
|
-
export { DocumentTitle, type DocumentTitleProps } from './DocumentTitle.tsx';
|
|
13
|
-
export { APP_SHELL_ID, type AppShellService } from './app-shell.ts';
|
|
8
|
+
export { AppWebEntry, type BootSeams } from './boot.ts';
|
|
14
9
|
export { getStaticModules } from './seed.ts';
|
|
15
|
-
export { PLATFORM_MODULES, type PlatformModule } from './platform.ts';
|
|
16
|
-
export { STATE_LABELS, FIBER_STATE, createSignal, createLoaderStatusStore, type LoaderStatus, type LoaderEntryState, type KernelSignal, type KernelValueSignal, type LoaderStatusStore, } from './loader-status.ts';
|
|
10
|
+
export { PLATFORM_MODULES, PRELOADED_CLIENT_EXTERNALS, type PlatformModule } from './platform.ts';
|
|
17
11
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1,13 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Fiber-state projection vocabulary
|
|
3
|
-
* boot
|
|
4
|
-
*
|
|
5
|
-
* subscribes `internal/status` and recomputes one row per loader entry.
|
|
6
|
-
*
|
|
7
|
-
* The store is hand-rolled here because of the shell self-sufficiency rule:
|
|
8
|
-
* the snapshot-store machinery lives in the runtime PLUGIN
|
|
9
|
-
* package, and the shell kernel must not value-import any plugin package —
|
|
10
|
-
* the loading page has to work while (and especially when) plugins fail.
|
|
2
|
+
* Fiber-state projection vocabulary for the framework-free boot page. The
|
|
3
|
+
* boot chain subscribes to `internal/status` and projects the owning loader
|
|
4
|
+
* entry's current state.
|
|
11
5
|
* @module @monotykamary/dsh-client-web/src/loader-status
|
|
12
6
|
*/
|
|
13
7
|
import type { FiberState } from '@monotykamary/cordis';
|
|
@@ -29,46 +23,4 @@ export declare const FIBER_STATE: {
|
|
|
29
23
|
export type LoaderEntryState = 'pending' | 'loading' | 'active' | 'failed' | 'disposed' | 'unloading';
|
|
30
24
|
/** Label for each fiber state, keyed by member (inlining-safe — no reverse mapping). */
|
|
31
25
|
export declare const STATE_LABELS: Record<FiberState, LoaderEntryState>;
|
|
32
|
-
/** Per-entry state projection (AppRoot's status feed), keyed by entry name. */
|
|
33
|
-
export type LoaderStatus = Record<string, LoaderEntryState>;
|
|
34
|
-
/** Minimal observable snapshot the kernel components consume (useSyncExternalStore shape). */
|
|
35
|
-
export interface KernelSignal<T> {
|
|
36
|
-
/** Current value (stable reference between changes). */
|
|
37
|
-
getSnapshot: () => T;
|
|
38
|
-
/**
|
|
39
|
-
* Subscribe to changes.
|
|
40
|
-
* @param fn - change listener.
|
|
41
|
-
* @returns the unsubscribe disposer.
|
|
42
|
-
*/
|
|
43
|
-
subscribe: (fn: () => void) => () => void;
|
|
44
|
-
}
|
|
45
|
-
/** Writable one-value signal (settled flag, boot failure report). */
|
|
46
|
-
export interface KernelValueSignal<T> extends KernelSignal<T> {
|
|
47
|
-
/**
|
|
48
|
-
* Publish a new value and notify subscribers.
|
|
49
|
-
* @param next - the new value.
|
|
50
|
-
*/
|
|
51
|
-
set: (next: T) => void;
|
|
52
|
-
}
|
|
53
|
-
/**
|
|
54
|
-
* Create a writable kernel signal.
|
|
55
|
-
* @param init - initial value.
|
|
56
|
-
* @returns the signal.
|
|
57
|
-
*/
|
|
58
|
-
export declare function createSignal<T>(init: T): KernelValueSignal<T>;
|
|
59
|
-
/** The boot status store: per-entry rows over a {@link KernelSignal} face. */
|
|
60
|
-
export interface LoaderStatusStore extends KernelSignal<LoaderStatus> {
|
|
61
|
-
/**
|
|
62
|
-
* Project one entry's state (copy-on-write so getSnapshot references only
|
|
63
|
-
* change on writes — useSyncExternalStore contract).
|
|
64
|
-
* @param id - entry name.
|
|
65
|
-
* @param state - projected fiber state.
|
|
66
|
-
*/
|
|
67
|
-
set: (id: string, state: LoaderEntryState) => void;
|
|
68
|
-
}
|
|
69
|
-
/**
|
|
70
|
-
* Create the boot status store.
|
|
71
|
-
* @returns the store (empty until the boot chain projects rows).
|
|
72
|
-
*/
|
|
73
|
-
export declare function createLoaderStatusStore(): LoaderStatusStore;
|
|
74
26
|
//# sourceMappingURL=loader-status.d.ts.map
|
package/lib/types/platform.d.ts
CHANGED
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
* @module @monotykamary/dsh-client-web/src/platform
|
|
5
5
|
*/
|
|
6
6
|
/** The module specifiers the shell shares into the frozen module table. */
|
|
7
|
-
export declare const PLATFORM_MODULES: readonly ["react", "react/jsx-runtime", "react-dom", "react-dom/client", "@monotykamary/cordis", "@monotykamary/dsh-client-ui-slots", "@monotykamary/dsh-client-
|
|
7
|
+
export declare const PLATFORM_MODULES: readonly ["react", "react/jsx-runtime", "react-dom", "react-dom/client", "@monotykamary/cordis", "@monotykamary/dsh-client-ui-slots", "@monotykamary/dsh-client-ui-primitives"];
|
|
8
|
+
/** Client-bundle specifiers whose factories the parser preloads before the shell starts. */
|
|
9
|
+
export declare const PRELOADED_CLIENT_EXTERNALS: readonly ["@monotykamary/dsh-client-runtime/client"];
|
|
8
10
|
/** One platform module specifier (a seed-table key). */
|
|
9
11
|
export type PlatformModule = (typeof PLATFORM_MODULES)[number];
|
|
10
12
|
//# sourceMappingURL=platform.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@monotykamary/dsh-client-web",
|
|
3
|
-
"description": "Web
|
|
4
|
-
"version": "0.1.0
|
|
3
|
+
"description": "Web boot kernel: static module table, Cordis loader, framework-free boot page, and UI-renderer handoff",
|
|
4
|
+
"version": "0.1.0",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -26,35 +26,28 @@
|
|
|
26
26
|
"./package.json": "./package.json"
|
|
27
27
|
},
|
|
28
28
|
"license": "MIT",
|
|
29
|
-
"dependencies": {
|
|
30
|
-
"react": "^18.2.0",
|
|
31
|
-
"react-dom": "^18.2.0",
|
|
32
|
-
"@monotykamary/dsh-client-ui-theme": "^0.1.0-rc.8",
|
|
33
|
-
"@monotykamary/dsh-client-ui-attachment": "^0.1.0-rc.8",
|
|
34
|
-
"@monotykamary/dsh-client-web-react": "^0.1.0-rc.8",
|
|
35
|
-
"@monotykamary/dsh-client-schema-form": "^0.1.0-rc.8",
|
|
36
|
-
"@monotykamary/dsh-client-modules": "^0.1.0-rc.8",
|
|
37
|
-
"@monotykamary/dsh-client-ui-slots": "^0.1.0-rc.8",
|
|
38
|
-
"@monotykamary/dsh-client-ui-primitives": "^0.1.0-rc.8"
|
|
39
|
-
},
|
|
40
29
|
"devDependencies": {
|
|
41
30
|
"@types/react": "~18.3.1",
|
|
42
31
|
"@types/react-dom": "~18.3.0",
|
|
32
|
+
"react": "^18.2.0",
|
|
33
|
+
"react-dom": "^18.2.0",
|
|
43
34
|
"typescript": "^6.0.3",
|
|
44
|
-
"@monotykamary/
|
|
45
|
-
"@monotykamary/dsh-client-
|
|
46
|
-
"@monotykamary/dsh-
|
|
47
|
-
"@monotykamary/
|
|
48
|
-
"@monotykamary/
|
|
35
|
+
"@monotykamary/cordis-plugin-loader": "^1.0.2",
|
|
36
|
+
"@monotykamary/dsh-client-ui-primitives": "^0.1.0",
|
|
37
|
+
"@monotykamary/dsh-client-modules": "^0.1.0",
|
|
38
|
+
"@monotykamary/dsh-invariants": "^0.1.0",
|
|
39
|
+
"@monotykamary/dsh-client-ui-slots": "^0.1.0",
|
|
40
|
+
"@monotykamary/dsh-client-ui-renderer": "^0.1.0",
|
|
41
|
+
"@monotykamary/cordis": "^4.0.1"
|
|
49
42
|
},
|
|
50
43
|
"peerDependencies": {
|
|
51
|
-
"@monotykamary/
|
|
52
|
-
"@monotykamary/dsh-invariants": "^0.1.0-rc.8",
|
|
44
|
+
"@monotykamary/dsh-invariants": "^0.1.0",
|
|
53
45
|
"@monotykamary/cordis": "^4.0.1"
|
|
54
46
|
},
|
|
55
47
|
"files": [
|
|
56
48
|
"lib/index.js",
|
|
57
49
|
"lib/invariant.js",
|
|
50
|
+
"lib/**/*.css",
|
|
58
51
|
"lib/types/**/*.d.ts"
|
|
59
52
|
]
|
|
60
53
|
}
|
package/lib/types/AppRoot.d.ts
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import type { ReactNode } from 'react';
|
|
2
|
-
import type { KernelSignal, LoaderStatus } from './loader-status.ts';
|
|
3
|
-
/** AppRoot props: settled signal, fiber-state projection feed, boot failure report, deferred real-UI factory. */
|
|
4
|
-
export interface AppRootProps {
|
|
5
|
-
/** True once the boot chain settled (loader quiesced + all entries ACTIVE); the boot closure flips it. */
|
|
6
|
-
settled: KernelSignal<boolean>;
|
|
7
|
-
/** Per-entry fiber-state projection store (drives loading/failed rendering). */
|
|
8
|
-
status: KernelSignal<LoaderStatus>;
|
|
9
|
-
/** Boot failure report (the settle rejection message); undefined while loading or after success. */
|
|
10
|
-
error: KernelSignal<string | undefined>;
|
|
11
|
-
/** Builds the real UI; called only after settled. */
|
|
12
|
-
renderApp: () => ReactNode;
|
|
13
|
-
}
|
|
14
|
-
/** Boot gate: loading page until the boot settles; failures stay here. */
|
|
15
|
-
export declare function AppRoot(props: AppRootProps): import("react").JSX.Element;
|
|
16
|
-
//# sourceMappingURL=AppRoot.d.ts.map
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
/** Props for the shell-owned browser title projection. */
|
|
2
|
-
export interface DocumentTitleProps {
|
|
3
|
-
/** Durable title of the selected session, or undefined for the product title. */
|
|
4
|
-
title?: string;
|
|
5
|
-
}
|
|
6
|
-
/**
|
|
7
|
-
* Project the selected durable session title into the browser title and
|
|
8
|
-
* restore the shell's original product title when unmounted.
|
|
9
|
-
* @param props - selected session title projection.
|
|
10
|
-
* @returns no rendered content.
|
|
11
|
-
*/
|
|
12
|
-
export declare function DocumentTitle({ title }: DocumentTitleProps): null;
|
|
13
|
-
//# sourceMappingURL=DocumentTitle.d.ts.map
|
package/lib/types/app-shell.d.ts
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* App-shell assembly plugin. Its pseudo package id exists only in the host
|
|
3
|
-
* graph and shell registry; there is no npm package behind it.
|
|
4
|
-
*/
|
|
5
|
-
import type { ReactNode } from 'react';
|
|
6
|
-
import type { Context } from '@monotykamary/cordis';
|
|
7
|
-
/** Shell-owned pseudo entry id under which the host graph mounts this plugin. */
|
|
8
|
-
export declare const APP_SHELL_ID = "@monotykamary/dsh-client-app-shell";
|
|
9
|
-
/** The assembled-UI face AppRoot renders once the boot settles. */
|
|
10
|
-
export interface AppShellService {
|
|
11
|
-
/** Build (once) and render the real UI tree. */
|
|
12
|
-
renderApp: () => ReactNode;
|
|
13
|
-
}
|
|
14
|
-
declare module '@monotykamary/cordis' {
|
|
15
|
-
interface Context {
|
|
16
|
-
/** The shell assembly face, provided by the app-shell entry once its inject set is active. */
|
|
17
|
-
appShell: AppShellService;
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
/** Cordis plugin name. */
|
|
21
|
-
export declare const name = "app-shell";
|
|
22
|
-
/** Services required before shell assembly. */
|
|
23
|
-
export declare const inject: string[];
|
|
24
|
-
/** Installs the React renderer and exposes the assembled application.
|
|
25
|
-
* @param ctx - Plugin context.
|
|
26
|
-
*/
|
|
27
|
-
export declare function apply(ctx: Context): void;
|
|
28
|
-
//# sourceMappingURL=app-shell.d.ts.map
|
package/lib/types/app.d.ts
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Real-UI assembly closure, invoked by the app-shell plugin once its inject
|
|
3
|
-
* set is active: the whole layout tree hangs off the built-in 'root' slot
|
|
4
|
-
* (ui-layout registers AppFrame there and renders the child slots
|
|
5
|
-
* internally) — the shell's render is the one ctx-level renderSlot call in
|
|
6
|
-
* the program.
|
|
7
|
-
*/
|
|
8
|
-
import type { ReactNode } from 'react';
|
|
9
|
-
import type { Context } from '@monotykamary/cordis';
|
|
10
|
-
/** Assembly inputs: the active app-shell plugin ctx (slots/sessions/layout services provided). */
|
|
11
|
-
export interface AssemblyDeps {
|
|
12
|
-
/** Client context with the assembly's inject set active. */
|
|
13
|
-
ctx: Context;
|
|
14
|
-
}
|
|
15
|
-
/**
|
|
16
|
-
* Build the renderApp factory the app-shell plugin provides to AppRoot.
|
|
17
|
-
* @param deps - assembly inputs.
|
|
18
|
-
* @returns factory producing the real UI tree (called once per AppRoot render after settled).
|
|
19
|
-
*/
|
|
20
|
-
export declare function buildRenderApp(deps: AssemblyDeps): () => ReactNode;
|
|
21
|
-
//# sourceMappingURL=app.d.ts.map
|