@effindomv2/fui-as 0.1.20 → 0.1.22

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.
@@ -0,0 +1,13 @@
1
+ <div
2
+ class="effindom-loading-overlay"
3
+ id="effindom-loading-overlay"
4
+ data-state="loading"
5
+ aria-live="polite"
6
+ aria-hidden="true"
7
+ hidden>
8
+ <div class="effindom-loading-card">
9
+ <p class="effindom-loading-kicker">EffinDom backstage</p>
10
+ <h2 class="effindom-loading-title" id="effindom-loading-title">Teaching the pixels their lines...</h2>
11
+ <p class="effindom-loading-detail" id="effindom-loading-detail">The runtime orchestra is tuning up behind the canvas.</p>
12
+ </div>
13
+ </div>
@@ -0,0 +1,52 @@
1
+ .effindom-loading-overlay {
2
+ position: absolute;
3
+ inset: 0;
4
+ display: grid;
5
+ place-items: center;
6
+ padding: 24px;
7
+ box-sizing: border-box;
8
+ background: linear-gradient(180deg, rgba(2, 6, 23, 0.72), rgba(10, 20, 34, 0.82));
9
+ backdrop-filter: blur(10px);
10
+ z-index: 2;
11
+ }
12
+
13
+ .effindom-loading-overlay[hidden] {
14
+ display: none;
15
+ }
16
+
17
+ .effindom-loading-card {
18
+ max-width: 420px;
19
+ padding: 22px 24px;
20
+ border: 1px solid rgba(148, 163, 184, 0.32);
21
+ border-radius: 18px;
22
+ background: rgba(6, 12, 21, 0.80);
23
+ text-align: center;
24
+ box-shadow: 0 18px 48px rgba(2, 6, 23, 0.30);
25
+ }
26
+
27
+ .effindom-loading-overlay[data-state="error"] .effindom-loading-card {
28
+ border-color: rgba(248, 113, 113, 0.50);
29
+ background: rgba(69, 10, 10, 0.78);
30
+ }
31
+
32
+ .effindom-loading-kicker {
33
+ margin: 0 0 10px;
34
+ font-size: 11px;
35
+ font-weight: 700;
36
+ letter-spacing: 0.12em;
37
+ text-transform: uppercase;
38
+ color: #7dd3fc;
39
+ }
40
+
41
+ .effindom-loading-title {
42
+ margin: 0;
43
+ font-size: 28px;
44
+ line-height: 1.15;
45
+ }
46
+
47
+ .effindom-loading-detail {
48
+ margin: 12px 0 0;
49
+ font-size: 14px;
50
+ line-height: 1.55;
51
+ color: #cbd5e1;
52
+ }
@@ -0,0 +1 @@
1
+ export * from './src/routed-app-conventions';
@@ -71,26 +71,10 @@ const decoder = new TextDecoder();
71
71
  const encoder = new TextEncoder();
72
72
  const harnessUiChrome = new HarnessUiChrome();
73
73
 
74
- function setLoadingOverlay(state: 'loading' | 'error', title: string, detail: string): void {
75
- harnessUiChrome.setLoadingOverlay(state, title, detail);
76
- }
77
-
78
- function hideLoadingOverlay(): void {
79
- harnessUiChrome.hideLoadingOverlay();
80
- }
81
-
82
74
  function describeHarnessError(error: unknown): string {
83
75
  return error instanceof Error ? error.message : String(error);
84
76
  }
85
77
 
86
- function reportHarnessErrorOverlay(title: string, detail: string, error: unknown): void {
87
- console.error(error instanceof Error ? error.stack ?? error : error);
88
- setLoadingOverlay('error', title, detail);
89
- const windowWithHarnessError = window as Window & { __fuiAsError?: string; __fuiAsReady?: boolean };
90
- windowWithHarnessError.__fuiAsReady = false;
91
- windowWithHarnessError.__fuiAsError = detail;
92
- }
93
-
94
78
  function defaultRunHarnessApp(exports: HarnessExports): void {
95
79
  const autoExports = exports as AutoHarnessExports;
96
80
  if (typeof autoExports.__runApp !== 'function') {
@@ -118,23 +102,7 @@ function defaultOnReady(): void {
118
102
  function defaultOnError(error: unknown): void {
119
103
  const windowWithHarnessState = window as Window & { __fuiAsReady?: boolean; __fuiAsError?: string };
120
104
  windowWithHarnessState.__fuiAsReady = false;
121
- windowWithHarnessState.__fuiAsError = describeHarnessError(error);
122
- }
123
-
124
- function setUrlPreviewText(text: string): void {
125
- harnessUiChrome.setUrlPreviewText(text);
126
- }
127
-
128
- function detectPlatformFamily(): number {
129
- return harnessUiChrome.detectPlatformFamily();
130
- }
131
-
132
- function detectCoarsePointer(): boolean {
133
- return harnessUiChrome.detectCoarsePointer();
134
- }
135
-
136
- function getCanvasSizeSource(canvas: HTMLCanvasElement): HTMLElement | HTMLCanvasElement {
137
- return harnessUiChrome.getCanvasSizeSource(canvas);
105
+ windowWithHarnessState.__fuiAsError = error instanceof Error ? error.message : String(error);
138
106
  }
139
107
 
140
108
  export function startHarness<Exports extends HarnessExports>(options: HarnessOptions<Exports>): void {
@@ -158,16 +126,59 @@ export function startManagedHarness(options: ManagedHarnessOptions): void {
158
126
  let cleanup = () => {
159
127
  delete window.__fui_debug;
160
128
  };
161
- setLoadingOverlay(
162
- 'loading',
163
- 'Teaching the pixels their lines...',
164
- 'The runtime orchestra is tuning up behind the canvas.',
165
- );
129
+ const loadingOverlayText = harnessUiChrome.getLoadingOverlayText();
130
+ let loadingOverlayTitle = loadingOverlayText.title;
131
+ let loadingOverlayDetail = loadingOverlayText.detail;
132
+ let loadingOverlayVisible = false;
133
+ let loadingOverlayTimer: number | null = null;
134
+
135
+ function clearLoadingOverlayTimer(): void {
136
+ if (loadingOverlayTimer === null) {
137
+ return;
138
+ }
139
+ window.clearTimeout(loadingOverlayTimer);
140
+ loadingOverlayTimer = null;
141
+ }
142
+
143
+ function scheduleLoadingOverlay(): void {
144
+ if (loadingOverlayVisible || loadingOverlayTimer !== null) {
145
+ return;
146
+ }
147
+ loadingOverlayTimer = window.setTimeout(() => {
148
+ loadingOverlayTimer = null;
149
+ loadingOverlayVisible = true;
150
+ harnessUiChrome.setLoadingOverlay('loading', loadingOverlayTitle, loadingOverlayDetail);
151
+ }, 1000);
152
+ }
153
+
154
+ function updateLoadingOverlay(detail: string): void {
155
+ loadingOverlayDetail = detail;
156
+ if (loadingOverlayVisible) {
157
+ harnessUiChrome.setLoadingOverlay('loading', loadingOverlayTitle, loadingOverlayDetail);
158
+ return;
159
+ }
160
+ scheduleLoadingOverlay();
161
+ }
162
+
163
+ function finishLoadingOverlay(): void {
164
+ clearLoadingOverlayTimer();
165
+ loadingOverlayVisible = false;
166
+ harnessUiChrome.hideLoadingOverlay();
167
+ }
168
+
169
+ function failLoadingOverlay(detail: string): void {
170
+ clearLoadingOverlayTimer();
171
+ loadingOverlayVisible = true;
172
+ harnessUiChrome.setLoadingOverlay('error', loadingOverlayTitle, detail);
173
+ }
174
+
166
175
  const bridge = window.EffinDomBrowserBridge;
167
176
  if (bridge === undefined) {
177
+ failLoadingOverlay('EffinDomBrowserBridge is unavailable.');
168
178
  throw new Error('EffinDomBrowserBridge is unavailable.');
169
179
  }
170
180
  if (typeof Worker !== 'function') {
181
+ failLoadingOverlay('FUI-AS requires browser Worker support.');
171
182
  throw new Error('FUI-AS requires browser Worker support.');
172
183
  }
173
184
  const bridgeState = bridge;
@@ -195,7 +206,7 @@ export function startManagedHarness(options: ManagedHarnessOptions): void {
195
206
  offsets: number[];
196
207
  colors: number[];
197
208
  }>();
198
- setUrlPreviewText('');
209
+ harnessUiChrome.setUrlPreviewText('');
199
210
 
200
211
  function getCurrentSession(): HarnessAppSession {
201
212
  if (currentSession === null) {
@@ -761,8 +772,12 @@ export function startManagedHarness(options: ManagedHarnessOptions): void {
761
772
 
762
773
  function handleSameOriginNavigationFailure(target: URL, mode: HarnessNavigationMode, error: unknown): void {
763
774
  const route = toAppRoute(target);
764
- const detail = `Failed to load ${mode === 'pop' ? 'history route' : 'route'} ${route}: ${describeHarnessError(error)}`;
765
- reportHarnessErrorOverlay('The render raccoons chewed through a cable.', detail, error);
775
+ const detail = `Failed to load ${mode === 'pop' ? 'history route' : 'route'} ${route}: ${error instanceof Error ? error.message : String(error)}`;
776
+ console.error(error instanceof Error ? error.stack ?? error : error);
777
+ failLoadingOverlay(detail);
778
+ const windowWithHarnessError = window as Window & { __fuiAsError?: string; __fuiAsReady?: boolean };
779
+ windowWithHarnessError.__fuiAsReady = false;
780
+ windowWithHarnessError.__fuiAsError = detail;
766
781
  options.onError?.(error);
767
782
  }
768
783
 
@@ -1041,7 +1056,7 @@ export function startManagedHarness(options: ManagedHarnessOptions): void {
1041
1056
  session.exports.__fui_hide_active_context_menu();
1042
1057
  }
1043
1058
  runtime.clearPointerHover();
1044
- setUrlPreviewText('');
1059
+ harnessUiChrome.setUrlPreviewText('');
1045
1060
  };
1046
1061
  const handleWindowBlur = () => {
1047
1062
  dismissTransientUi();
@@ -1120,7 +1135,7 @@ export function startManagedHarness(options: ManagedHarnessOptions): void {
1120
1135
  };
1121
1136
  registerExternalDragTarget(runtime.canvas);
1122
1137
  registerExternalDragTarget(runtime.canvas.parentElement);
1123
- registerExternalDragTarget(getCanvasSizeSource(runtime.canvas));
1138
+ registerExternalDragTarget(harnessUiChrome.getCanvasSizeSource(runtime.canvas));
1124
1139
  window.addEventListener('blur', handleWindowBlur);
1125
1140
  runtime.canvas.addEventListener('blur', handleCanvasBlur);
1126
1141
  for (let index = 0; index < externalDragTargets.length; index += 1) {
@@ -1135,7 +1150,7 @@ export function startManagedHarness(options: ManagedHarnessOptions): void {
1135
1150
 
1136
1151
  cleanup = () => {
1137
1152
  workerManager.terminateAll();
1138
- setUrlPreviewText('');
1153
+ harnessUiChrome.setUrlPreviewText('');
1139
1154
  window.removeEventListener('resize', handleViewportChange);
1140
1155
  darkModeQuery.removeEventListener('change', handleDarkModeChange);
1141
1156
  window.removeEventListener('popstate', handlePopState);
@@ -1285,7 +1300,7 @@ export function startManagedHarness(options: ManagedHarnessOptions): void {
1285
1300
  runtime.setCapturedPointerHandle(null);
1286
1301
  runtime.clearPointerHover();
1287
1302
  runtime.canvas.style.cursor = 'default';
1288
- setUrlPreviewText('');
1303
+ harnessUiChrome.setUrlPreviewText('');
1289
1304
  runtime.core._ed_clear_focus_state?.();
1290
1305
  runtime.core._ed_clear_text_input_state?.();
1291
1306
  runtime.core._ed_reset_scene();
@@ -1315,7 +1330,7 @@ export function startManagedHarness(options: ManagedHarnessOptions): void {
1315
1330
  runtime.setAppFrameHandler(null);
1316
1331
  runtime.setCapturedPointerHandle(null);
1317
1332
  runtime.clearPointerHover();
1318
- setUrlPreviewText('');
1333
+ harnessUiChrome.setUrlPreviewText('');
1319
1334
  latestCommandWords = [];
1320
1335
  latestRootHandle = null;
1321
1336
  updateState();
@@ -1330,11 +1345,7 @@ export function startManagedHarness(options: ManagedHarnessOptions): void {
1330
1345
  loadOptions: HarnessAppOptions<Exports>,
1331
1346
  ): Promise<HarnessContext<Exports>> {
1332
1347
  if (loadOptions.showLoadingOverlay !== false) {
1333
- setLoadingOverlay(
1334
- 'loading',
1335
- 'Winding up the tiny widget clockwork...',
1336
- `Loading ${loadOptions.wasmPath}`,
1337
- );
1348
+ updateLoadingOverlay(`Loading ${loadOptions.wasmPath}`);
1338
1349
  }
1339
1350
  await unloadApp();
1340
1351
  const restoredSnapshot = await queuePersistedUiStateWork(() => {
@@ -1404,7 +1415,7 @@ export function startManagedHarness(options: ManagedHarnessOptions): void {
1404
1415
  await queuePersistedUiStateWork(() => ensureCurrentHistoryEntrySnapshot(`loading ${loadOptions.wasmPath}`));
1405
1416
  lastHandledUrlHref = window.location.href;
1406
1417
  updateState();
1407
- hideLoadingOverlay();
1418
+ finishLoadingOverlay();
1408
1419
  return context;
1409
1420
  }
1410
1421
 
@@ -1424,12 +1435,12 @@ export function startManagedHarness(options: ManagedHarnessOptions): void {
1424
1435
  await options.onReady?.(controller);
1425
1436
  }).catch((error: unknown) => {
1426
1437
  cleanup();
1427
- const message = describeHarnessError(error);
1428
- reportHarnessErrorOverlay(
1429
- 'The render raccoons chewed through a cable.',
1430
- message,
1431
- error,
1432
- );
1438
+ const message = error instanceof Error ? error.message : String(error);
1439
+ console.error(error instanceof Error ? error.stack ?? error : error);
1440
+ failLoadingOverlay(message);
1441
+ const windowWithHarnessError = window as Window & { __fuiAsError?: string; __fuiAsReady?: boolean };
1442
+ windowWithHarnessError.__fuiAsReady = false;
1443
+ windowWithHarnessError.__fuiAsError = message;
1433
1444
  options.onError?.(error);
1434
1445
  throw error;
1435
1446
  });
@@ -94,6 +94,21 @@ export function waitForFrame(): Promise<void> {
94
94
  }
95
95
 
96
96
  export class HarnessUiChrome {
97
+ getLoadingOverlayText(): { title: string; detail: string } {
98
+ const overlay = document.getElementById(LOADING_OVERLAY_ID);
99
+ const titleNode = document.getElementById(LOADING_TITLE_ID);
100
+ const detailNode = document.getElementById(LOADING_DETAIL_ID);
101
+ const title = titleNode instanceof HTMLElement ? titleNode.textContent ?? '' : '';
102
+ const detail = detailNode instanceof HTMLElement ? detailNode.textContent ?? '' : '';
103
+ if (overlay instanceof HTMLElement && title.length > 0 && detail.length > 0) {
104
+ return { title, detail };
105
+ }
106
+ return {
107
+ title: 'Loading...',
108
+ detail: 'The runtime is starting up.',
109
+ };
110
+ }
111
+
97
112
  setLoadingOverlay(state: 'loading' | 'error', title: string, detail: string): void {
98
113
  const overlay = document.getElementById(LOADING_OVERLAY_ID);
99
114
  const titleNode = document.getElementById(LOADING_TITLE_ID);
@@ -23,16 +23,39 @@ export type {
23
23
  ManagedHistoryState,
24
24
  } from './common-harness';
25
25
 
26
+ export {
27
+ buildRoutedHarnessRoutes,
28
+ defineRoutedAppManifest,
29
+ renderRoutedPageHead,
30
+ resolveRouteManifest,
31
+ resolveRoutePath,
32
+ routeDef,
33
+ routeHead,
34
+ } from './routed-app-conventions';
35
+
26
36
  export {
27
37
  startRoutedHarness,
28
38
  } from './routed-harness';
29
39
 
40
+ export type {
41
+ RoutedAppHeadTag,
42
+ RoutedAppRoute,
43
+ RoutedAppRouteDefinition,
44
+ RoutedAppRouteManifest,
45
+ ResolvedRoutedAppRoute,
46
+ ResolvedRoutedAppRouteManifest,
47
+ } from './routed-app-conventions';
48
+
30
49
  export type {
31
50
  RoutedHarnessConfig,
32
51
  RoutedHarnessManagerState,
33
52
  RoutedHarnessRoute,
34
53
  } from './routed-harness';
35
54
 
55
+ export type {
56
+ RoutedHarnessRouteSpec,
57
+ } from './routed-app-conventions';
58
+
36
59
  export {
37
60
  defineHostEvents,
38
61
  hostEvent,
@@ -0,0 +1,292 @@
1
+ export class RoutedAppHeadTag {
2
+ kind: string;
3
+ name: string;
4
+ content: string;
5
+
6
+ constructor(kind: string = "", name: string = "", content: string = "") {
7
+ this.kind = kind;
8
+ this.name = name;
9
+ this.content = content;
10
+ }
11
+ }
12
+
13
+ export class RoutedAppRoute {
14
+ key: string;
15
+ title: string;
16
+ headTags: Array<RoutedAppHeadTag>;
17
+ entrypoint: string;
18
+ wasmFile: string;
19
+ shellDir: string;
20
+ sourceRoutePath: string;
21
+ publishedRoutePath: string;
22
+
23
+ constructor(
24
+ key: string = "",
25
+ title: string = "",
26
+ headTags: Array<RoutedAppHeadTag> = new Array<RoutedAppHeadTag>(),
27
+ entrypoint: string = "",
28
+ wasmFile: string = "",
29
+ shellDir: string = "",
30
+ sourceRoutePath: string = "",
31
+ publishedRoutePath: string = "",
32
+ ) {
33
+ this.key = key;
34
+ this.title = title;
35
+ this.headTags = headTags;
36
+ this.entrypoint = entrypoint;
37
+ this.wasmFile = wasmFile;
38
+ this.shellDir = shellDir;
39
+ this.sourceRoutePath = sourceRoutePath;
40
+ this.publishedRoutePath = publishedRoutePath;
41
+ }
42
+ }
43
+
44
+ export class RoutedAppRouteManifest {
45
+ sourceRouteBase: string;
46
+ routes: Array<RoutedAppRoute>;
47
+
48
+ constructor(sourceRouteBase: string = "", routes: Array<RoutedAppRoute> = new Array<RoutedAppRoute>()) {
49
+ this.sourceRouteBase = sourceRouteBase;
50
+ this.routes = routes;
51
+ }
52
+ }
53
+
54
+ export class ResolvedRoutedAppRoute {
55
+ key: string;
56
+ title: string;
57
+ headTags: Array<RoutedAppHeadTag>;
58
+ shellDir: string;
59
+ wasmFile: string;
60
+ entrypoint: string;
61
+ sourceRoutePath: string;
62
+ publishedRoutePath: string;
63
+
64
+ constructor(
65
+ key: string = "",
66
+ title: string = "",
67
+ headTags: Array<RoutedAppHeadTag> = new Array<RoutedAppHeadTag>(),
68
+ shellDir: string = "",
69
+ wasmFile: string = "",
70
+ entrypoint: string = "",
71
+ sourceRoutePath: string = "",
72
+ publishedRoutePath: string = "",
73
+ ) {
74
+ this.key = key;
75
+ this.title = title;
76
+ this.headTags = headTags;
77
+ this.shellDir = shellDir;
78
+ this.wasmFile = wasmFile;
79
+ this.entrypoint = entrypoint;
80
+ this.sourceRoutePath = sourceRoutePath;
81
+ this.publishedRoutePath = publishedRoutePath;
82
+ }
83
+ }
84
+
85
+ export class ResolvedRoutedAppRouteManifest {
86
+ sourceRouteBase: string;
87
+ routes: Array<ResolvedRoutedAppRoute>;
88
+
89
+ constructor(sourceRouteBase: string = "", routes: Array<ResolvedRoutedAppRoute> = new Array<ResolvedRoutedAppRoute>()) {
90
+ this.sourceRouteBase = sourceRouteBase;
91
+ this.routes = routes;
92
+ }
93
+ }
94
+
95
+ export class RoutedHarnessRouteSpec {
96
+ routePath: string;
97
+ wasmPath: string;
98
+ title: string;
99
+
100
+ constructor(routePath: string = "", wasmPath: string = "", title: string = "") {
101
+ this.routePath = routePath;
102
+ this.wasmPath = wasmPath;
103
+ this.title = title;
104
+ }
105
+ }
106
+
107
+ export class RoutedAppRouteDefinition {
108
+ key: string;
109
+ title: string;
110
+ entrypoint: string;
111
+ wasmFile: string;
112
+ shellDir: string;
113
+ sourceRoutePath: string;
114
+ publishedRoutePath: string;
115
+
116
+ constructor(
117
+ key: string = "",
118
+ title: string = "",
119
+ entrypoint: string = "",
120
+ wasmFile: string = "",
121
+ shellDir: string = "",
122
+ sourceRoutePath: string = "",
123
+ publishedRoutePath: string = "",
124
+ ) {
125
+ this.key = key;
126
+ this.title = title;
127
+ this.entrypoint = entrypoint;
128
+ this.wasmFile = wasmFile;
129
+ this.shellDir = shellDir;
130
+ this.sourceRoutePath = sourceRoutePath;
131
+ this.publishedRoutePath = publishedRoutePath;
132
+ }
133
+ }
134
+
135
+ function trimSlashes(path: string): string {
136
+ let normalized = path;
137
+ while (normalized.startsWith("/")) {
138
+ normalized = normalized.slice(1);
139
+ }
140
+ while (normalized.endsWith("/")) {
141
+ normalized = normalized.slice(0, -1);
142
+ }
143
+ return normalized;
144
+ }
145
+
146
+ function normalizeRouteBase(path: string): string {
147
+ const trimmed = trimSlashes(path);
148
+ return trimmed.length === 0 ? "" : `/${trimmed}`;
149
+ }
150
+
151
+ function toPascalCase(value: string): string {
152
+ let result = "";
153
+ let capitalizeNext = true;
154
+ const normalized = trimSlashes(value);
155
+ for (let index = 0; index < normalized.length; index += 1) {
156
+ const char = normalized.charAt(index);
157
+ const code = char.charCodeAt(0);
158
+ const isAlphaNum =
159
+ (code >= 48 && code <= 57) ||
160
+ (code >= 65 && code <= 90) ||
161
+ (code >= 97 && code <= 122);
162
+ if (!isAlphaNum) {
163
+ capitalizeNext = true;
164
+ continue;
165
+ }
166
+ result += capitalizeNext ? char.toUpperCase() : char;
167
+ capitalizeNext = false;
168
+ }
169
+ return result;
170
+ }
171
+
172
+ export function routeDef(
173
+ key: string,
174
+ title: string,
175
+ headTags: Array<RoutedAppHeadTag> = new Array<RoutedAppHeadTag>(),
176
+ entrypoint: string = "",
177
+ wasmFile: string = "",
178
+ shellDir: string = "",
179
+ sourceRoutePath: string = "",
180
+ publishedRoutePath: string = "",
181
+ ): RoutedAppRoute {
182
+ return new RoutedAppRoute(key, title, headTags, entrypoint, wasmFile, shellDir, sourceRoutePath, publishedRoutePath);
183
+ }
184
+
185
+ export function defineRoutedAppManifest(
186
+ sourceRouteBase: string,
187
+ routes: Array<RoutedAppRoute>,
188
+ ): RoutedAppRouteManifest {
189
+ const normalizedBase = normalizeRouteBase(sourceRouteBase);
190
+ const normalizedRoutes = new Array<RoutedAppRoute>();
191
+ for (let index = 0; index < routes.length; index += 1) {
192
+ const route = routes[index];
193
+ const routeKey = trimSlashes(route.key);
194
+ const entrypoint = route.entrypoint == null || route.entrypoint.length == 0 ? `src/routes/${toPascalCase(routeKey)}App.ts` : route.entrypoint;
195
+ const wasmFile = route.wasmFile == null || route.wasmFile.length == 0 ? `${routeKey}.wasm` : route.wasmFile;
196
+ const shellDir = route.shellDir == null || route.shellDir.length == 0 ? routeKey : route.shellDir;
197
+ const sourceRoutePath = route.sourceRoutePath == null || route.sourceRoutePath.length == 0 ? `${normalizedBase}/${routeKey}/` : route.sourceRoutePath;
198
+ const publishedRoutePath = route.publishedRoutePath == null || route.publishedRoutePath.length == 0 ? `/${routeKey}/` : route.publishedRoutePath;
199
+ normalizedRoutes.push(new RoutedAppRoute(routeKey, route.title, route.headTags, entrypoint, wasmFile, shellDir, sourceRoutePath, publishedRoutePath));
200
+ }
201
+ return new RoutedAppRouteManifest(normalizedBase, normalizedRoutes);
202
+ }
203
+
204
+ export function resolveRouteManifest(manifest: RoutedAppRouteManifest): ResolvedRoutedAppRouteManifest {
205
+ const routes = new Array<ResolvedRoutedAppRoute>();
206
+ for (let index = 0; index < manifest.routes.length; index += 1) {
207
+ const route = manifest.routes[index];
208
+ const routeKey = trimSlashes(route.key);
209
+ routes.push(
210
+ new ResolvedRoutedAppRoute(
211
+ routeKey,
212
+ route.title,
213
+ route.headTags,
214
+ routeKey.length == 0 ? "" : routeKey,
215
+ `${routeKey}.wasm`,
216
+ `src/routes/${toPascalCase(routeKey)}App.ts`,
217
+ `${manifest.sourceRouteBase}/${routeKey}/`,
218
+ `/${routeKey}/`,
219
+ ),
220
+ );
221
+ }
222
+ return new ResolvedRoutedAppRouteManifest(manifest.sourceRouteBase, routes);
223
+ }
224
+
225
+ function escapeHtml(value: string): string {
226
+ return value
227
+ .replace(/&/g, "&amp;")
228
+ .replace(/</g, "&lt;")
229
+ .replace(/>/g, "&gt;")
230
+ .replace(/"/g, "&quot;");
231
+ }
232
+
233
+ function pushHeadTag(tags: Array<string>, headTag: RoutedAppHeadTag): void {
234
+ if (headTag.content.length == 0 || headTag.name.length == 0) {
235
+ return;
236
+ }
237
+ const attribute = headTag.kind == "property" ? "property" : "name";
238
+ tags.push(` <meta ${attribute}="${escapeHtml(headTag.name)}" content="${escapeHtml(headTag.content)}" />`);
239
+ }
240
+
241
+ export function routeHead(...entries: Array<string>): Array<RoutedAppHeadTag> {
242
+ const headTags = new Array<RoutedAppHeadTag>();
243
+ for (let index = 0; index + 1 < entries.length; index += 2) {
244
+ const name = entries[index];
245
+ const content = entries[index + 1];
246
+ const kind = name.startsWith("og:") || name.startsWith("fb:") ? "property" : "name";
247
+ headTags.push(new RoutedAppHeadTag(kind, name, content));
248
+ }
249
+ return headTags;
250
+ }
251
+
252
+ export function renderRoutedPageHead(title: string, headTags: Array<RoutedAppHeadTag> = new Array<RoutedAppHeadTag>()): string {
253
+ const tags = new Array<string>();
254
+ const effectiveTitle = title.length == 0 ? "FUI-AS" : title;
255
+ tags.push(` <title>${escapeHtml(effectiveTitle)}</title>`);
256
+ for (let index = 0; index < headTags.length; index += 1) {
257
+ pushHeadTag(tags, headTags[index]);
258
+ }
259
+ return tags.join("\n");
260
+ }
261
+
262
+ export function buildRoutedHarnessRoutes(
263
+ manifest: RoutedAppRouteManifest,
264
+ pathname: string,
265
+ ): Array<RoutedHarnessRouteSpec> {
266
+ const resolvedManifest = resolveRouteManifest(manifest);
267
+ const routePrefix = pathname.startsWith(`${resolvedManifest.sourceRouteBase}/`) ? resolvedManifest.sourceRouteBase : "";
268
+ const routes = new Array<RoutedHarnessRouteSpec>();
269
+ for (let index = 0; index < resolvedManifest.routes.length; index += 1) {
270
+ const route = resolvedManifest.routes[index];
271
+ routes.push(
272
+ new RoutedHarnessRouteSpec(
273
+ `${routePrefix}${route.publishedRoutePath}`,
274
+ `${routePrefix}/${route.wasmFile}`.replace("//", "/"),
275
+ route.title,
276
+ ),
277
+ );
278
+ }
279
+ return routes;
280
+ }
281
+
282
+ export function resolveRoutePath(manifest: RoutedAppRouteManifest, routeKey: string, currentRoutePath: string): string {
283
+ const resolvedManifest = resolveRouteManifest(manifest);
284
+ const isSourceRoute = currentRoutePath.length == 0 || currentRoutePath.startsWith(`${resolvedManifest.sourceRouteBase}/`);
285
+ for (let index = 0; index < resolvedManifest.routes.length; index += 1) {
286
+ const route = resolvedManifest.routes[index];
287
+ if (route.key == routeKey) {
288
+ return isSourceRoute ? route.sourceRoutePath : route.publishedRoutePath;
289
+ }
290
+ }
291
+ return "";
292
+ }
@@ -10,6 +10,7 @@ import {
10
10
  import type { HostEventsDefinition } from './host-events';
11
11
  import type { HostServicesDefinition } from './host-services';
12
12
  import type { WorkerHostServicesBundleConfig } from './worker-types';
13
+ import type { RoutedHarnessRouteSpec } from './routed-app-conventions';
13
14
 
14
15
  type NavigationMode = 'push' | 'replace' | 'pop';
15
16
 
@@ -20,12 +21,9 @@ declare global {
20
21
  }
21
22
  }
22
23
 
23
- export interface RoutedHarnessRoute {
24
- readonly routePath: string;
24
+ export type RoutedHarnessRoute = RoutedHarnessRouteSpec & {
25
25
  readonly matchPath?: string;
26
- readonly wasmPath: string;
27
- readonly title: string;
28
- }
26
+ };
29
27
 
30
28
  export interface RoutedHarnessManagerState {
31
29
  readonly shellId: string;
@@ -169,9 +167,7 @@ export function startRoutedHarness<
169
167
  },
170
168
  };
171
169
  const showLoadingOverlay = config.showLoadingOverlay?.(isWarmRouteSwap, route);
172
- if (showLoadingOverlay !== undefined) {
173
- appOptions.showLoadingOverlay = showLoadingOverlay;
174
- }
170
+ appOptions.showLoadingOverlay = showLoadingOverlay ?? !isWarmRouteSwap;
175
171
 
176
172
  await controller.loadApp(appOptions);
177
173
  routeLoads[route.routePath] = (routeLoads[route.routePath] ?? 0) + 1;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effindomv2/fui-as",
3
- "version": "0.1.20",
3
+ "version": "0.1.22",
4
4
  "private": false,
5
5
  "license": "AGPL-3.0-only OR LicenseRef-EffinDom-Commercial",
6
6
  "description": "EffinDom v2 AssemblyScript frontend framework SDK and browser harness",
@@ -37,6 +37,10 @@
37
37
  "types": "./browser/src/index.ts",
38
38
  "default": "./browser/src/index.ts"
39
39
  },
40
+ "./browser/routed-app-conventions": {
41
+ "types": "./browser/routed-app-conventions.ts",
42
+ "default": "./browser/routed-app-conventions.ts"
43
+ },
40
44
  "./browser/common-harness": {
41
45
  "types": "./browser/src/common-harness.ts",
42
46
  "default": "./browser/src/common-harness.ts"
@@ -60,6 +64,9 @@
60
64
  },
61
65
  "files": [
62
66
  "src",
67
+ "browser/routed-app-conventions.ts",
68
+ "browser/loading-overlay-styles.html",
69
+ "browser/loading-overlay-body.html",
63
70
  "browser/src",
64
71
  "scripts",
65
72
  "LICENSE.md",
@@ -78,7 +85,7 @@
78
85
  },
79
86
  "dependencies": {
80
87
  "@assemblyscript/loader": "^0.28.17",
81
- "@effindomv2/runtime": "0.1.7"
88
+ "@effindomv2/runtime": "0.1.8"
82
89
  },
83
90
  "devDependencies": {
84
91
  "@as-pect/assembly": "8.1.0",
package/scripts/build.sh CHANGED
@@ -20,6 +20,8 @@ HOST_SERVICE_GENERATOR_BUILD="${PACKAGE_DIR}/build/generate-host-services.mjs"
20
20
  HOST_EVENT_GENERATOR_BUILD="${PACKAGE_DIR}/build/generate-host-events.mjs"
21
21
  RUNTIME_CONFIG_FILE="effindom-runtime-config.js"
22
22
  DEFAULT_MANIFEST_PATH="./runtime/dist/effindom.v2.manifest.json"
23
+ LOADING_OVERLAY_STYLES_FILE="${PACKAGE_DIR}/browser/loading-overlay-styles.html"
24
+ LOADING_OVERLAY_BODY_FILE="${PACKAGE_DIR}/browser/loading-overlay-body.html"
23
25
 
24
26
  rm -rf "${OUT_DIR}"
25
27
  mkdir -p "${PACKAGE_DIR}/build" "${OUT_DIR}" "${DEMO_OUT_DIR}" "${WORKER_BUILD_DIR}"
@@ -161,6 +163,28 @@ window.__effindomRuntime = Object.assign({}, window.__effindomRuntime, {
161
163
  EOF
162
164
  }
163
165
 
166
+ render_html_with_loading_overlay() {
167
+ local source="$1"
168
+ local destination="$2"
169
+
170
+ SOURCE_HTML_PATH="${source}" \
171
+ DEST_HTML_PATH="${destination}" \
172
+ LOADING_OVERLAY_STYLES_PATH="${LOADING_OVERLAY_STYLES_FILE}" \
173
+ LOADING_OVERLAY_BODY_PATH="${LOADING_OVERLAY_BODY_FILE}" \
174
+ node --input-type=module <<'NODE'
175
+ import { readFileSync, writeFileSync } from 'node:fs';
176
+ const source = readFileSync(process.env.SOURCE_HTML_PATH, 'utf8');
177
+ const styles = readFileSync(process.env.LOADING_OVERLAY_STYLES_PATH, 'utf8');
178
+ const body = readFileSync(process.env.LOADING_OVERLAY_BODY_PATH, 'utf8');
179
+ writeFileSync(
180
+ process.env.DEST_HTML_PATH,
181
+ source
182
+ .replace('{{LOADING_OVERLAY_STYLES}}', styles)
183
+ .replace('{{LOADING_OVERLAY_BODY}}', body),
184
+ );
185
+ NODE
186
+ }
187
+
164
188
  copy_runtime_assets() {
165
189
  local destination="$1"
166
190
  cp "${RUNTIME_DIST_DIR}/bridge.js" "${destination}/bridge.js"
@@ -312,14 +336,14 @@ npx esbuild "${PACKAGE_DIR}/demo/worker-host-services.ts" \
312
336
  --outfile="${WORKER_HOST_SERVICES_BUILD}" \
313
337
  --sourcemap
314
338
 
315
- cp "${SMOKE_FIXTURE_DIR}/index.html" "${OUT_DIR}/index.html"
316
- cp "${PACKAGE_DIR}/demo/index.html" "${DEMO_OUT_DIR}/index.html"
339
+ render_html_with_loading_overlay "${SMOKE_FIXTURE_DIR}/index.html" "${OUT_DIR}/index.html"
340
+ render_html_with_loading_overlay "${PACKAGE_DIR}/demo/index.html" "${DEMO_OUT_DIR}/index.html"
317
341
  cp "${PACKAGE_DIR}/demo/demo-texture.png" "${DEMO_OUT_DIR}/demo-texture.png"
318
342
  cp "${PACKAGE_DIR}/demo/demo-secondary-texture.png" "${DEMO_OUT_DIR}/demo-secondary-texture.png"
319
343
 
320
344
  mkdir -p "${DEMO_OUT_DIR}/advanced-controls" "${DEMO_OUT_DIR}/templated-controls"
321
- cp "${PACKAGE_DIR}/demo/route-shell.html" "${DEMO_OUT_DIR}/advanced-controls/index.html"
322
- cp "${PACKAGE_DIR}/demo/route-shell.html" "${DEMO_OUT_DIR}/templated-controls/index.html"
345
+ render_html_with_loading_overlay "${PACKAGE_DIR}/demo/route-shell.html" "${DEMO_OUT_DIR}/advanced-controls/index.html"
346
+ render_html_with_loading_overlay "${PACKAGE_DIR}/demo/route-shell.html" "${DEMO_OUT_DIR}/templated-controls/index.html"
323
347
 
324
348
  copy_runtime_assets "${OUT_DIR}"
325
349
  copy_runtime_assets "${DEMO_OUT_DIR}"