@emkodev/emroute 1.10.0-beta.4 → 1.12.0-beta.1
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.md +1 -1
- package/core/component/widget.component.ts +3 -8
- package/core/util/html.util.ts +1 -1
- package/core/util/js.util.ts +8 -0
- package/core/util/widget-resolve.util.ts +4 -2
- package/dist/core/component/widget.component.js +3 -7
- package/dist/core/component/widget.component.js.map +1 -1
- package/dist/core/util/html.util.js +1 -1
- package/dist/core/util/html.util.js.map +1 -1
- package/dist/core/util/js.util.d.ts +5 -0
- package/dist/core/util/js.util.js +8 -0
- package/dist/core/util/js.util.js.map +1 -0
- package/dist/core/util/widget-resolve.util.js +4 -2
- package/dist/core/util/widget-resolve.util.js.map +1 -1
- package/dist/emroute.js +110 -27
- package/dist/emroute.js.map +10 -9
- package/dist/runtime/abstract.runtime.d.ts +13 -1
- package/dist/runtime/abstract.runtime.js +34 -22
- package/dist/runtime/abstract.runtime.js.map +1 -1
- package/dist/runtime/bun/fs/bun-fs.runtime.d.ts +4 -0
- package/dist/runtime/bun/fs/bun-fs.runtime.js +29 -2
- package/dist/runtime/bun/fs/bun-fs.runtime.js.map +1 -1
- package/dist/server/build.util.d.ts +5 -9
- package/dist/server/build.util.js +17 -54
- package/dist/server/build.util.js.map +1 -1
- package/dist/server/dev.server.d.ts +25 -0
- package/dist/server/dev.server.js +81 -0
- package/dist/server/dev.server.js.map +1 -0
- package/dist/src/element/component.element.d.ts +12 -0
- package/dist/src/element/component.element.js +55 -3
- package/dist/src/element/component.element.js.map +1 -1
- package/dist/src/util/html.util.d.ts +5 -0
- package/dist/src/util/html.util.js +38 -1
- package/dist/src/util/html.util.js.map +1 -1
- package/package.json +2 -2
- package/runtime/abstract.runtime.ts +38 -23
- package/runtime/bun/fs/bun-fs.runtime.ts +31 -2
- package/server/build.util.ts +17 -55
- package/src/element/component.element.ts +63 -3
- package/src/util/html.util.ts +46 -1
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
|
|
12
12
|
import type { Component } from '../../core/component/abstract.component.ts';
|
|
13
13
|
import type { ComponentContext, ContextProvider } from '../../core/type/component.type.ts';
|
|
14
|
-
import { HTMLElementBase } from '../util/html.util.ts';
|
|
15
|
-
import { LAZY_ATTR, RESERVED_ATTRS, SSR_ATTR } from '../../core/util/html.util.ts';
|
|
14
|
+
import { CSSStyleSheetBase, HTMLElementBase } from '../util/html.util.ts';
|
|
15
|
+
import { LAZY_ATTR, RESERVED_ATTRS, SSR_ATTR, scopeWidgetCss } from '../../core/util/html.util.ts';
|
|
16
16
|
|
|
17
17
|
type ComponentState = 'idle' | 'loading' | 'ready' | 'error';
|
|
18
18
|
|
|
@@ -36,6 +36,9 @@ export class ComponentElement<TParams, TData> extends HTMLElementBase {
|
|
|
36
36
|
/** Lazy module loaders keyed by tag name — set by registerLazy(). */
|
|
37
37
|
private static lazyLoaders = new Map<string, () => Promise<unknown>>();
|
|
38
38
|
|
|
39
|
+
/** Shared CSSStyleSheet cache — keyed by widget name for cross-instance sharing. */
|
|
40
|
+
private static sheetCache = new Map<string, CSSStyleSheet>();
|
|
41
|
+
|
|
39
42
|
/** App-level context provider set once during router initialization. */
|
|
40
43
|
private static extendContext: ContextProvider | undefined;
|
|
41
44
|
|
|
@@ -47,6 +50,9 @@ export class ComponentElement<TParams, TData> extends HTMLElementBase {
|
|
|
47
50
|
ComponentElement.extendContext = provider;
|
|
48
51
|
}
|
|
49
52
|
|
|
53
|
+
/** Custom state names exposed via ElementInternals for CSS `:state()` matching. */
|
|
54
|
+
private static readonly CUSTOM_STATES = ['lazy', 'loading', 'hydrating', 'ready', 'error'] as const;
|
|
55
|
+
|
|
50
56
|
private component: Component<TParams, TData>;
|
|
51
57
|
private effectiveFiles?: WidgetFiles | undefined;
|
|
52
58
|
private params: TParams | null = null;
|
|
@@ -57,6 +63,7 @@ export class ComponentElement<TParams, TData> extends HTMLElementBase {
|
|
|
57
63
|
private deferred: PromiseWithResolvers<void> | null = null;
|
|
58
64
|
private abortController: AbortController | null = null;
|
|
59
65
|
private intersectionObserver: IntersectionObserver | null = null;
|
|
66
|
+
private readonly internals: ElementInternals;
|
|
60
67
|
|
|
61
68
|
/** Promise that resolves with fetched data (available after loadData starts) */
|
|
62
69
|
dataPromise: Promise<TData | null> | null = null;
|
|
@@ -65,6 +72,7 @@ export class ComponentElement<TParams, TData> extends HTMLElementBase {
|
|
|
65
72
|
super();
|
|
66
73
|
this.component = component;
|
|
67
74
|
this.effectiveFiles = files;
|
|
75
|
+
this.internals = this.attachInternals();
|
|
68
76
|
// Attach shadow root if not already present (Declarative Shadow DOM creates it from <template shadowrootmode="open">)
|
|
69
77
|
// This enables progressive enhancement: SSR with DSD works without JS, then hydrates when JS loads
|
|
70
78
|
if (!this.shadowRoot) {
|
|
@@ -72,6 +80,14 @@ export class ComponentElement<TParams, TData> extends HTMLElementBase {
|
|
|
72
80
|
}
|
|
73
81
|
}
|
|
74
82
|
|
|
83
|
+
/** Update the CSS-visible custom state via ElementInternals. */
|
|
84
|
+
private setCustomState(next: typeof ComponentElement.CUSTOM_STATES[number]): void {
|
|
85
|
+
for (const s of ComponentElement.CUSTOM_STATES) {
|
|
86
|
+
this.internals.states.delete(s);
|
|
87
|
+
}
|
|
88
|
+
this.internals.states.add(next);
|
|
89
|
+
}
|
|
90
|
+
|
|
75
91
|
/**
|
|
76
92
|
* Register a widget as a custom element: `widget-{name}`.
|
|
77
93
|
* Creates a fresh widget instance per DOM element (per-element state).
|
|
@@ -203,7 +219,6 @@ export class ComponentElement<TParams, TData> extends HTMLElementBase {
|
|
|
203
219
|
}
|
|
204
220
|
|
|
205
221
|
this.component.element = this;
|
|
206
|
-
this.style.contentVisibility = 'auto';
|
|
207
222
|
this.abortController = new AbortController();
|
|
208
223
|
const signal = this.abortController.signal;
|
|
209
224
|
|
|
@@ -244,9 +259,13 @@ export class ComponentElement<TParams, TData> extends HTMLElementBase {
|
|
|
244
259
|
};
|
|
245
260
|
this.context = ComponentElement.extendContext ? ComponentElement.extendContext(base) : base;
|
|
246
261
|
|
|
262
|
+
// Apply CSS via adoptedStyleSheets (shared across instances)
|
|
263
|
+
this.adoptCss();
|
|
264
|
+
|
|
247
265
|
// Hydrate from SSR: adopt content from Declarative Shadow DOM
|
|
248
266
|
if (this.hasAttribute(SSR_ATTR)) {
|
|
249
267
|
this.removeAttribute(SSR_ATTR);
|
|
268
|
+
this.setCustomState('hydrating');
|
|
250
269
|
|
|
251
270
|
// Read SSR data from light DOM (JSON text placed alongside shadow root)
|
|
252
271
|
const lightText = this.textContent?.trim();
|
|
@@ -267,7 +286,10 @@ export class ComponentElement<TParams, TData> extends HTMLElementBase {
|
|
|
267
286
|
const args = { data: this.data, params: this.params!, context: this.context };
|
|
268
287
|
queueMicrotask(() => {
|
|
269
288
|
this.component.hydrate!(args);
|
|
289
|
+
this.setCustomState('ready');
|
|
270
290
|
});
|
|
291
|
+
} else {
|
|
292
|
+
this.setCustomState('ready');
|
|
271
293
|
}
|
|
272
294
|
|
|
273
295
|
this.signalReady();
|
|
@@ -276,6 +298,7 @@ export class ComponentElement<TParams, TData> extends HTMLElementBase {
|
|
|
276
298
|
|
|
277
299
|
// Lazy: defer loadData until element is visible
|
|
278
300
|
if (this.hasAttribute(LAZY_ATTR)) {
|
|
301
|
+
this.setCustomState('lazy');
|
|
279
302
|
this.intersectionObserver = new IntersectionObserver((entries) => {
|
|
280
303
|
const entry = entries[0]!;
|
|
281
304
|
if (entry.isIntersecting) {
|
|
@@ -299,6 +322,9 @@ export class ComponentElement<TParams, TData> extends HTMLElementBase {
|
|
|
299
322
|
this.abortController?.abort();
|
|
300
323
|
this.abortController = null;
|
|
301
324
|
this.state = 'idle';
|
|
325
|
+
for (const s of ComponentElement.CUSTOM_STATES) {
|
|
326
|
+
this.internals.states.delete(s);
|
|
327
|
+
}
|
|
302
328
|
this.data = null;
|
|
303
329
|
this.context = undefined!;
|
|
304
330
|
this.dataPromise = null;
|
|
@@ -328,12 +354,44 @@ export class ComponentElement<TParams, TData> extends HTMLElementBase {
|
|
|
328
354
|
return this.effectiveFiles ?? {};
|
|
329
355
|
}
|
|
330
356
|
|
|
357
|
+
/** Base sheet shared by all widgets — host-level defaults. */
|
|
358
|
+
private static baseSheet: CSSStyleSheet | null = null;
|
|
359
|
+
|
|
360
|
+
private static getBaseSheet(): CSSStyleSheet {
|
|
361
|
+
if (!ComponentElement.baseSheet) {
|
|
362
|
+
ComponentElement.baseSheet = new CSSStyleSheetBase();
|
|
363
|
+
ComponentElement.baseSheet.replaceSync(':host { container-type: inline-size; content-visibility: auto; }');
|
|
364
|
+
}
|
|
365
|
+
return ComponentElement.baseSheet;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/** Apply CSS via adoptedStyleSheets with cross-instance sheet sharing. */
|
|
369
|
+
private adoptCss(): void {
|
|
370
|
+
const css = this.effectiveFiles?.css;
|
|
371
|
+
const base = ComponentElement.getBaseSheet();
|
|
372
|
+
|
|
373
|
+
if (!css) {
|
|
374
|
+
this.shadowRoot!.adoptedStyleSheets = [base];
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const name = this.component.name;
|
|
379
|
+
let sheet = ComponentElement.sheetCache.get(name);
|
|
380
|
+
if (!sheet) {
|
|
381
|
+
sheet = new CSSStyleSheetBase();
|
|
382
|
+
sheet.replaceSync(scopeWidgetCss(css, name));
|
|
383
|
+
ComponentElement.sheetCache.set(name, sheet);
|
|
384
|
+
}
|
|
385
|
+
this.shadowRoot!.adoptedStyleSheets = [base, sheet];
|
|
386
|
+
}
|
|
387
|
+
|
|
331
388
|
private async loadData(): Promise<void> {
|
|
332
389
|
if (this.params === null) return;
|
|
333
390
|
|
|
334
391
|
const signal = this.abortController?.signal;
|
|
335
392
|
|
|
336
393
|
this.state = 'loading';
|
|
394
|
+
this.setCustomState('loading');
|
|
337
395
|
this.render();
|
|
338
396
|
|
|
339
397
|
try {
|
|
@@ -349,6 +407,7 @@ export class ComponentElement<TParams, TData> extends HTMLElementBase {
|
|
|
349
407
|
if (signal?.aborted) return;
|
|
350
408
|
|
|
351
409
|
this.state = 'ready';
|
|
410
|
+
this.setCustomState('ready');
|
|
352
411
|
} catch (e) {
|
|
353
412
|
if (e instanceof DOMException && e.name === 'AbortError') return;
|
|
354
413
|
if (signal?.aborted) return;
|
|
@@ -363,6 +422,7 @@ export class ComponentElement<TParams, TData> extends HTMLElementBase {
|
|
|
363
422
|
|
|
364
423
|
private setError(message: string): void {
|
|
365
424
|
this.state = 'error';
|
|
425
|
+
this.setCustomState('error');
|
|
366
426
|
this.errorMessage = message;
|
|
367
427
|
this.render();
|
|
368
428
|
this.signalReady(); // Ready even on error (completed loading)
|
package/src/util/html.util.ts
CHANGED
|
@@ -16,16 +16,50 @@ export {
|
|
|
16
16
|
STATUS_MESSAGES,
|
|
17
17
|
} from '../../core/util/html.util.ts';
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* SSR-compatible CSSStyleSheet mock.
|
|
21
|
+
* Stores cssText for serialization into <style> tags during SSR.
|
|
22
|
+
*/
|
|
23
|
+
class SsrCSSStyleSheet {
|
|
24
|
+
cssText = '';
|
|
25
|
+
|
|
26
|
+
replaceSync(css: string): void {
|
|
27
|
+
this.cssText = css;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
replace(css: string): Promise<SsrCSSStyleSheet> {
|
|
31
|
+
this.cssText = css;
|
|
32
|
+
return Promise.resolve(this);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Server-safe CSSStyleSheet: real in browser, mock on server. */
|
|
37
|
+
export const CSSStyleSheetBase = globalThis.CSSStyleSheet ??
|
|
38
|
+
(SsrCSSStyleSheet as unknown as typeof CSSStyleSheet);
|
|
39
|
+
|
|
19
40
|
/**
|
|
20
41
|
* SSR-compatible ShadowRoot mock.
|
|
21
42
|
*/
|
|
22
43
|
class SsrShadowRoot {
|
|
23
44
|
private _innerHTML = '';
|
|
45
|
+
private _adoptedStyleSheets: SsrCSSStyleSheet[] = [];
|
|
24
46
|
|
|
25
47
|
constructor(public readonly host: SsrHTMLElement) {}
|
|
26
48
|
|
|
49
|
+
get adoptedStyleSheets(): SsrCSSStyleSheet[] {
|
|
50
|
+
return this._adoptedStyleSheets;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
set adoptedStyleSheets(sheets: SsrCSSStyleSheet[]) {
|
|
54
|
+
this._adoptedStyleSheets = sheets;
|
|
55
|
+
}
|
|
56
|
+
|
|
27
57
|
get innerHTML(): string {
|
|
28
|
-
|
|
58
|
+
const adopted = this._adoptedStyleSheets
|
|
59
|
+
.filter(s => s.cssText)
|
|
60
|
+
.map(s => `<style>${s.cssText}</style>`)
|
|
61
|
+
.join('');
|
|
62
|
+
return adopted + this._innerHTML;
|
|
29
63
|
}
|
|
30
64
|
|
|
31
65
|
set innerHTML(value: string) {
|
|
@@ -55,6 +89,13 @@ class SsrShadowRoot {
|
|
|
55
89
|
}
|
|
56
90
|
}
|
|
57
91
|
|
|
92
|
+
/**
|
|
93
|
+
* SSR-compatible ElementInternals mock.
|
|
94
|
+
*/
|
|
95
|
+
class SsrElementInternals {
|
|
96
|
+
readonly states = new Set<string>();
|
|
97
|
+
}
|
|
98
|
+
|
|
58
99
|
/**
|
|
59
100
|
* SSR-compatible HTMLElement mock.
|
|
60
101
|
*/
|
|
@@ -105,6 +146,10 @@ class SsrHTMLElement {
|
|
|
105
146
|
return this._shadowRoot as unknown as ShadowRoot;
|
|
106
147
|
}
|
|
107
148
|
|
|
149
|
+
attachInternals(): ElementInternals {
|
|
150
|
+
return new SsrElementInternals() as unknown as ElementInternals;
|
|
151
|
+
}
|
|
152
|
+
|
|
108
153
|
getAttribute(name: string): string | null {
|
|
109
154
|
return this._attributes.get(name) ?? null;
|
|
110
155
|
}
|