@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.
Files changed (40) hide show
  1. package/README.md +1 -1
  2. package/core/component/widget.component.ts +3 -8
  3. package/core/util/html.util.ts +1 -1
  4. package/core/util/js.util.ts +8 -0
  5. package/core/util/widget-resolve.util.ts +4 -2
  6. package/dist/core/component/widget.component.js +3 -7
  7. package/dist/core/component/widget.component.js.map +1 -1
  8. package/dist/core/util/html.util.js +1 -1
  9. package/dist/core/util/html.util.js.map +1 -1
  10. package/dist/core/util/js.util.d.ts +5 -0
  11. package/dist/core/util/js.util.js +8 -0
  12. package/dist/core/util/js.util.js.map +1 -0
  13. package/dist/core/util/widget-resolve.util.js +4 -2
  14. package/dist/core/util/widget-resolve.util.js.map +1 -1
  15. package/dist/emroute.js +110 -27
  16. package/dist/emroute.js.map +10 -9
  17. package/dist/runtime/abstract.runtime.d.ts +13 -1
  18. package/dist/runtime/abstract.runtime.js +34 -22
  19. package/dist/runtime/abstract.runtime.js.map +1 -1
  20. package/dist/runtime/bun/fs/bun-fs.runtime.d.ts +4 -0
  21. package/dist/runtime/bun/fs/bun-fs.runtime.js +29 -2
  22. package/dist/runtime/bun/fs/bun-fs.runtime.js.map +1 -1
  23. package/dist/server/build.util.d.ts +5 -9
  24. package/dist/server/build.util.js +17 -54
  25. package/dist/server/build.util.js.map +1 -1
  26. package/dist/server/dev.server.d.ts +25 -0
  27. package/dist/server/dev.server.js +81 -0
  28. package/dist/server/dev.server.js.map +1 -0
  29. package/dist/src/element/component.element.d.ts +12 -0
  30. package/dist/src/element/component.element.js +55 -3
  31. package/dist/src/element/component.element.js.map +1 -1
  32. package/dist/src/util/html.util.d.ts +5 -0
  33. package/dist/src/util/html.util.js +38 -1
  34. package/dist/src/util/html.util.js.map +1 -1
  35. package/package.json +2 -2
  36. package/runtime/abstract.runtime.ts +38 -23
  37. package/runtime/bun/fs/bun-fs.runtime.ts +31 -2
  38. package/server/build.util.ts +17 -55
  39. package/src/element/component.element.ts +63 -3
  40. 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)
@@ -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
- return this._innerHTML;
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
  }