@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.
- package/browser/loading-overlay-body.html +13 -0
- package/browser/loading-overlay-styles.html +52 -0
- package/browser/routed-app-conventions.ts +1 -0
- package/browser/src/common-harness/managed-harness.ts +69 -58
- package/browser/src/common-harness/ui-chrome.ts +15 -0
- package/browser/src/index.ts +23 -0
- package/browser/src/routed-app-conventions.ts +292 -0
- package/browser/src/routed-harness.ts +4 -8
- package/package.json +9 -2
- package/scripts/build.sh +28 -4
|
@@ -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 =
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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}: ${
|
|
765
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
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);
|
package/browser/src/index.ts
CHANGED
|
@@ -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, "&")
|
|
228
|
+
.replace(/</g, "<")
|
|
229
|
+
.replace(/>/g, ">")
|
|
230
|
+
.replace(/"/g, """);
|
|
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
|
|
24
|
-
readonly routePath: string;
|
|
24
|
+
export type RoutedHarnessRoute = RoutedHarnessRouteSpec & {
|
|
25
25
|
readonly matchPath?: string;
|
|
26
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
316
|
-
|
|
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
|
-
|
|
322
|
-
|
|
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}"
|