@panyam/tsappkit 0.6.0 → 0.6.2
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/dist/index.d.mts +47 -1
- package/dist/index.d.ts +47 -1
- package/dist/index.js +82 -9
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +82 -10
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +1 -0
- package/src/page/IslandPage.ts +30 -1
- package/src/page/mount.ts +28 -7
- package/src/page/overlay.ts +63 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@panyam/tsappkit",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.2",
|
|
4
4
|
"description": "TypeScript application toolkit with component lifecycle management, event system, UI utilities, and documentation site components",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|
package/src/index.ts
CHANGED
|
@@ -32,6 +32,7 @@ export { IslandPage } from './page/IslandPage';
|
|
|
32
32
|
export { readSpec, SPEC_ELEMENT_ID } from './page/spec';
|
|
33
33
|
export type { IslandSpec, PageSpec, SpecExtension } from './page/spec';
|
|
34
34
|
export { lazy, mountIslands } from './page/mount';
|
|
35
|
+
export { IslandOverlay } from './page/overlay';
|
|
35
36
|
export type { IslandFactory, LazyIsland, MountOptions, Registry } from './page/mount';
|
|
36
37
|
export { parseLoad, scheduleMount } from './page/load';
|
|
37
38
|
export type { LoadEnv, LoadStrategy } from './page/load';
|
package/src/page/IslandPage.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { LCMComponent } from "../LCMComponent";
|
|
|
4
4
|
import { LifecycleController } from "../LifecycleController";
|
|
5
5
|
import { parseLoad, scheduleMount, type LoadStrategy } from "./load";
|
|
6
6
|
import { mountIslands, type Registry } from "./mount";
|
|
7
|
+
import { IslandOverlay } from "./overlay";
|
|
7
8
|
import { readSpec, SPEC_ELEMENT_ID, type PageSpec } from "./spec";
|
|
8
9
|
|
|
9
10
|
/**
|
|
@@ -24,6 +25,10 @@ import { readSpec, SPEC_ELEMENT_ID, type PageSpec } from "./spec";
|
|
|
24
25
|
* setupDependencies and activate. A deferred island mustn't be something
|
|
25
26
|
* another island or the page needs at startup: nothing waits for it.
|
|
26
27
|
*
|
|
28
|
+
* With `?islands` in the URL (or showIslandOverlay overridden), each slot is
|
|
29
|
+
* outlined and labelled with its island's name, load strategy and state; see
|
|
30
|
+
* IslandOverlay.
|
|
31
|
+
*
|
|
27
32
|
* A page with no readable `#page-spec` mounts nothing and warns. Subclasses
|
|
28
33
|
* that override initializeSpecificComponents call super and add to what it
|
|
29
34
|
* returns.
|
|
@@ -46,10 +51,18 @@ export abstract class IslandPage<Ctx, Ext extends object = {}> extends BasePage
|
|
|
46
51
|
console.warn(`page spec: no readable #${SPEC_ELEMENT_ID} on this page, so nothing is mounted`);
|
|
47
52
|
return [];
|
|
48
53
|
}
|
|
54
|
+
const findSlot = (slot: string) => document.querySelector<HTMLElement>(`[data-slot="${slot}"]`);
|
|
55
|
+
const overlay = this.showIslandOverlay() ? new IslandOverlay() : undefined;
|
|
56
|
+
if (overlay) {
|
|
57
|
+
for (const island of spec.islands) {
|
|
58
|
+
const el = findSlot(island.slot);
|
|
59
|
+
if (el) overlay.waiting(island, el);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
49
62
|
return mountIslands(
|
|
50
63
|
spec,
|
|
51
64
|
this.registry(),
|
|
52
|
-
|
|
65
|
+
findSlot,
|
|
53
66
|
() => this.makeContext(spec),
|
|
54
67
|
this.eventBus,
|
|
55
68
|
(message) => console.warn(message),
|
|
@@ -60,10 +73,26 @@ export abstract class IslandPage<Ctx, Ext extends object = {}> extends BasePage
|
|
|
60
73
|
.initializeFromRoot(component)
|
|
61
74
|
.catch((err) => console.warn(`page spec: island "${island.name}" failed to start: ${err instanceof Error ? err.message : String(err)}`));
|
|
62
75
|
},
|
|
76
|
+
onMount: overlay && ((_c, island, el) => overlay.mounted(island, el)),
|
|
77
|
+
onSkip:
|
|
78
|
+
overlay &&
|
|
79
|
+
((island, reason) => {
|
|
80
|
+
const el = findSlot(island.slot);
|
|
81
|
+
if (el) overlay.failed(island, el, reason);
|
|
82
|
+
}),
|
|
63
83
|
},
|
|
64
84
|
);
|
|
65
85
|
}
|
|
66
86
|
|
|
87
|
+
/**
|
|
88
|
+
* Whether to draw the island overlay. The default is true when the URL has
|
|
89
|
+
* an `islands` query parameter (`/game?islands`); a subclass can tie it to
|
|
90
|
+
* its own debug setting instead.
|
|
91
|
+
*/
|
|
92
|
+
protected showIslandOverlay(): boolean {
|
|
93
|
+
return new URLSearchParams(window.location.search).has("islands");
|
|
94
|
+
}
|
|
95
|
+
|
|
67
96
|
/**
|
|
68
97
|
* Waits for `strategy` and then calls `mount`. Defaults to scheduleMount
|
|
69
98
|
* against the window; a subclass or test can replace how the waiting is done.
|
package/src/page/mount.ts
CHANGED
|
@@ -48,6 +48,18 @@ export interface MountOptions<El, C> {
|
|
|
48
48
|
defer?: (island: IslandSpec, el: El, mount: () => void) => void;
|
|
49
49
|
/** Gets what the factory built for each island mounted later: through `defer`, or from a lazy entry. */
|
|
50
50
|
onLateMount?: (component: C, island: IslandSpec) => void;
|
|
51
|
+
/**
|
|
52
|
+
* Gets every island that mounts, eager or late, with its slot, as it
|
|
53
|
+
* mounts (before onLateMount for a late one). IslandPage's debug overlay
|
|
54
|
+
* uses it to show when each island arrived.
|
|
55
|
+
*/
|
|
56
|
+
onMount?: (component: C, island: IslandSpec, el: El) => void;
|
|
57
|
+
/**
|
|
58
|
+
* Gets every island that won't mount, with a short reason ("not in the
|
|
59
|
+
* registry", "no slot", "failed to load", "no factory", "factory threw"),
|
|
60
|
+
* alongside the message `log` gets.
|
|
61
|
+
*/
|
|
62
|
+
onSkip?: (island: IslandSpec, reason: string) => void;
|
|
51
63
|
}
|
|
52
64
|
|
|
53
65
|
/**
|
|
@@ -80,16 +92,20 @@ export function mountIslands<Ctx, El, C, B>(
|
|
|
80
92
|
options: MountOptions<El, C> = {},
|
|
81
93
|
): C[] {
|
|
82
94
|
const out: C[] = [];
|
|
95
|
+
const skip = (island: IslandSpec, reason: string, message: string) => {
|
|
96
|
+
log(message);
|
|
97
|
+
options.onSkip?.(island, reason);
|
|
98
|
+
};
|
|
83
99
|
let ctx: Ctx | undefined;
|
|
84
100
|
for (const island of spec.islands) {
|
|
85
101
|
const entry = Object.prototype.hasOwnProperty.call(registry, island.name) ? registry[island.name] : undefined;
|
|
86
102
|
if (!entry) {
|
|
87
|
-
|
|
103
|
+
skip(island, "not in the registry", `page spec: no island called "${island.name}" in this page's registry`);
|
|
88
104
|
continue;
|
|
89
105
|
}
|
|
90
106
|
const el = findSlot(island.slot);
|
|
91
107
|
if (el === null) {
|
|
92
|
-
|
|
108
|
+
skip(island, "no slot", `page spec: island "${island.name}" wants slot "${island.slot}", which isn't on the page`);
|
|
93
109
|
continue;
|
|
94
110
|
}
|
|
95
111
|
const build = (factory: IslandFactory<Ctx, El, C, B>): C | undefined => {
|
|
@@ -97,14 +113,16 @@ export function mountIslands<Ctx, El, C, B>(
|
|
|
97
113
|
ctx ??= context();
|
|
98
114
|
return factory(el, island, ctx, bus);
|
|
99
115
|
} catch (err) {
|
|
100
|
-
|
|
116
|
+
skip(island, "factory threw", `page spec: island "${island.name}" failed to mount: ${message(err)}`);
|
|
101
117
|
return undefined;
|
|
102
118
|
}
|
|
103
119
|
};
|
|
104
120
|
const mountLate = () => {
|
|
105
121
|
const late = (factory: IslandFactory<Ctx, El, C, B>) => {
|
|
106
122
|
const c = build(factory);
|
|
107
|
-
if (c
|
|
123
|
+
if (c === undefined) return;
|
|
124
|
+
options.onMount?.(c, island, el);
|
|
125
|
+
options.onLateMount?.(c, island);
|
|
108
126
|
};
|
|
109
127
|
if (typeof entry === "function") {
|
|
110
128
|
late(entry);
|
|
@@ -115,9 +133,9 @@ export function mountIslands<Ctx, El, C, B>(
|
|
|
115
133
|
(m) => {
|
|
116
134
|
const factory = typeof m === "function" ? m : m?.default;
|
|
117
135
|
if (typeof factory === "function") late(factory);
|
|
118
|
-
else
|
|
136
|
+
else skip(island, "no factory", `page spec: island "${island.name}" loaded, but its module has no factory (a default export or the function itself)`);
|
|
119
137
|
},
|
|
120
|
-
(err) =>
|
|
138
|
+
(err) => skip(island, "failed to load", `page spec: island "${island.name}" failed to load: ${message(err)}`),
|
|
121
139
|
);
|
|
122
140
|
};
|
|
123
141
|
const strategy = parseLoad(island.load);
|
|
@@ -133,7 +151,10 @@ export function mountIslands<Ctx, El, C, B>(
|
|
|
133
151
|
continue;
|
|
134
152
|
}
|
|
135
153
|
const c = build(entry);
|
|
136
|
-
if (c !== undefined)
|
|
154
|
+
if (c !== undefined) {
|
|
155
|
+
out.push(c);
|
|
156
|
+
options.onMount?.(c, island, el);
|
|
157
|
+
}
|
|
137
158
|
}
|
|
138
159
|
return out;
|
|
139
160
|
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { IslandSpec } from "./spec";
|
|
2
|
+
|
|
3
|
+
const STYLE_ID = "island-overlay-style";
|
|
4
|
+
|
|
5
|
+
// The label is drawn by ::after from an attribute, so the overlay adds no
|
|
6
|
+
// children to a slot the island owns, and an island clearing its slot on
|
|
7
|
+
// mount (SolidIsland does) doesn't take the label with it.
|
|
8
|
+
const CSS = `
|
|
9
|
+
[data-island-debug] { position: relative; outline: 2px dashed #1565c0; outline-offset: -2px; }
|
|
10
|
+
[data-island-debug]::after {
|
|
11
|
+
content: attr(data-island-debug); position: absolute; top: 0; right: 0; z-index: 2147483647;
|
|
12
|
+
font: 11px/1.4 ui-monospace, monospace; padding: 1px 6px; background: #1565c0; color: #fff; pointer-events: none;
|
|
13
|
+
}
|
|
14
|
+
[data-island-state="mounted"] { outline-color: #2e7d32; }
|
|
15
|
+
[data-island-state="mounted"]::after { background: #2e7d32; }
|
|
16
|
+
[data-island-state="failed"] { outline-color: #c62828; }
|
|
17
|
+
[data-island-state="failed"]::after { background: #c62828; }
|
|
18
|
+
`;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Outlines each island's slot and labels it with its name, slot, load
|
|
22
|
+
* strategy and state: `hero · top · eager · mounted 212 ms`,
|
|
23
|
+
* `below · bottom · visible · waiting`, or `ghost · foot · eager · not in the
|
|
24
|
+
* registry`. Times are from navigation start (performance.now()).
|
|
25
|
+
*
|
|
26
|
+
* IslandPage turns it on with `?islands` in the URL (see
|
|
27
|
+
* showIslandOverlay). It's a development aid: while it's on, every labelled
|
|
28
|
+
* slot is `position: relative`, which can move an island's absolutely
|
|
29
|
+
* positioned content.
|
|
30
|
+
*/
|
|
31
|
+
export class IslandOverlay {
|
|
32
|
+
constructor(
|
|
33
|
+
private readonly doc: Document = document,
|
|
34
|
+
private readonly now: () => number = () => performance.now(),
|
|
35
|
+
) {
|
|
36
|
+
if (!doc.getElementById(STYLE_ID)) {
|
|
37
|
+
const style = doc.createElement("style");
|
|
38
|
+
style.id = STYLE_ID;
|
|
39
|
+
style.textContent = CSS;
|
|
40
|
+
doc.head.appendChild(style);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The island has a slot and hasn't mounted yet. */
|
|
45
|
+
waiting(island: IslandSpec, el: HTMLElement): void {
|
|
46
|
+
this.label(island, el, "waiting", "waiting");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** The island has just mounted. */
|
|
50
|
+
mounted(island: IslandSpec, el: HTMLElement): void {
|
|
51
|
+
this.label(island, el, "mounted", `mounted ${Math.round(this.now())} ms`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** The island won't mount, for `reason`. */
|
|
55
|
+
failed(island: IslandSpec, el: HTMLElement, reason: string): void {
|
|
56
|
+
this.label(island, el, "failed", reason);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private label(island: IslandSpec, el: HTMLElement, state: string, text: string): void {
|
|
60
|
+
el.dataset.islandState = state;
|
|
61
|
+
el.dataset.islandDebug = [island.name, island.slot, island.load || "eager", text].join(" · ");
|
|
62
|
+
}
|
|
63
|
+
}
|