zero-query 0.6.3 → 0.8.6

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 (72) hide show
  1. package/README.md +39 -29
  2. package/cli/commands/build.js +113 -4
  3. package/cli/commands/bundle.js +392 -29
  4. package/cli/commands/create.js +1 -1
  5. package/cli/commands/dev/devtools/index.js +56 -0
  6. package/cli/commands/dev/devtools/js/components.js +49 -0
  7. package/cli/commands/dev/devtools/js/core.js +409 -0
  8. package/cli/commands/dev/devtools/js/elements.js +413 -0
  9. package/cli/commands/dev/devtools/js/network.js +166 -0
  10. package/cli/commands/dev/devtools/js/performance.js +73 -0
  11. package/cli/commands/dev/devtools/js/router.js +105 -0
  12. package/cli/commands/dev/devtools/js/source.js +132 -0
  13. package/cli/commands/dev/devtools/js/stats.js +35 -0
  14. package/cli/commands/dev/devtools/js/tabs.js +79 -0
  15. package/cli/commands/dev/devtools/panel.html +95 -0
  16. package/cli/commands/dev/devtools/styles.css +244 -0
  17. package/cli/commands/dev/index.js +29 -4
  18. package/cli/commands/dev/logger.js +6 -1
  19. package/cli/commands/dev/overlay.js +428 -2
  20. package/cli/commands/dev/server.js +42 -5
  21. package/cli/commands/dev/watcher.js +59 -1
  22. package/cli/help.js +8 -5
  23. package/cli/scaffold/{scripts → app}/app.js +16 -23
  24. package/cli/scaffold/{scripts → app}/components/about.js +4 -4
  25. package/cli/scaffold/{scripts → app}/components/api-demo.js +1 -1
  26. package/cli/scaffold/{scripts → app}/components/contacts/contacts.css +0 -7
  27. package/cli/scaffold/{scripts → app}/components/contacts/contacts.html +3 -3
  28. package/cli/scaffold/app/components/home.js +137 -0
  29. package/cli/scaffold/{scripts → app}/routes.js +1 -1
  30. package/cli/scaffold/{scripts → app}/store.js +6 -6
  31. package/cli/scaffold/assets/.gitkeep +0 -0
  32. package/cli/scaffold/{styles/styles.css → global.css} +4 -2
  33. package/cli/scaffold/index.html +12 -11
  34. package/cli/utils.js +111 -6
  35. package/dist/zquery.dist.zip +0 -0
  36. package/dist/zquery.js +1122 -158
  37. package/dist/zquery.min.js +3 -16
  38. package/index.d.ts +129 -1290
  39. package/index.js +15 -10
  40. package/package.json +7 -6
  41. package/src/component.js +172 -49
  42. package/src/core.js +359 -18
  43. package/src/diff.js +256 -58
  44. package/src/expression.js +33 -3
  45. package/src/reactive.js +37 -5
  46. package/src/router.js +243 -7
  47. package/tests/component.test.js +886 -0
  48. package/tests/core.test.js +977 -0
  49. package/tests/diff.test.js +525 -0
  50. package/tests/errors.test.js +162 -0
  51. package/tests/expression.test.js +482 -0
  52. package/tests/http.test.js +289 -0
  53. package/tests/reactive.test.js +339 -0
  54. package/tests/router.test.js +649 -0
  55. package/tests/store.test.js +379 -0
  56. package/tests/utils.test.js +512 -0
  57. package/types/collection.d.ts +383 -0
  58. package/types/component.d.ts +217 -0
  59. package/types/errors.d.ts +103 -0
  60. package/types/http.d.ts +81 -0
  61. package/types/misc.d.ts +179 -0
  62. package/types/reactive.d.ts +76 -0
  63. package/types/router.d.ts +161 -0
  64. package/types/ssr.d.ts +49 -0
  65. package/types/store.d.ts +107 -0
  66. package/types/utils.d.ts +142 -0
  67. package/cli/commands/dev.old.js +0 -520
  68. package/cli/scaffold/scripts/components/home.js +0 -137
  69. /package/cli/scaffold/{scripts → app}/components/contacts/contacts.js +0 -0
  70. /package/cli/scaffold/{scripts → app}/components/counter.js +0 -0
  71. /package/cli/scaffold/{scripts → app}/components/not-found.js +0 -0
  72. /package/cli/scaffold/{scripts → app}/components/todos.js +0 -0
package/index.js CHANGED
@@ -11,11 +11,11 @@
11
11
 
12
12
  import { query, queryAll, ZQueryCollection } from './src/core.js';
13
13
  import { reactive, Signal, signal, computed, effect } from './src/reactive.js';
14
- import { component, mount, mountAll, getInstance, destroy, getRegistry, style } from './src/component.js';
14
+ import { component, mount, mountAll, getInstance, destroy, getRegistry, prefetch, style } from './src/component.js';
15
15
  import { createRouter, getRouter } from './src/router.js';
16
16
  import { createStore, getStore } from './src/store.js';
17
17
  import { http } from './src/http.js';
18
- import { morph } from './src/diff.js';
18
+ import { morph, morphElement } from './src/diff.js';
19
19
  import { safeEval } from './src/expression.js';
20
20
  import {
21
21
  debounce, throttle, pipe, once, sleep,
@@ -23,7 +23,7 @@ import {
23
23
  deepClone, deepMerge, isEqual, param, parseQuery,
24
24
  storage, session, bus,
25
25
  } from './src/utils.js';
26
- import { ZQueryError, ErrorCode, onError, reportError } from './src/errors.js';
26
+ import { ZQueryError, ErrorCode, onError, reportError, guardCallback, validate } from './src/errors.js';
27
27
 
28
28
 
29
29
  // ---------------------------------------------------------------------------
@@ -100,8 +100,10 @@ $.mountAll = mountAll;
100
100
  $.getInstance = getInstance;
101
101
  $.destroy = destroy;
102
102
  $.components = getRegistry;
103
+ $.prefetch = prefetch;
103
104
  $.style = style;
104
- $.morph = morph;
105
+ $.morph = morph;
106
+ $.morphElement = morphElement;
105
107
  $.safeEval = safeEval;
106
108
 
107
109
  // --- Router ----------------------------------------------------------------
@@ -142,12 +144,15 @@ $.session = session;
142
144
  $.bus = bus;
143
145
 
144
146
  // --- Error handling --------------------------------------------------------
145
- $.onError = onError;
146
- $.ZQueryError = ZQueryError;
147
- $.ErrorCode = ErrorCode;
147
+ $.onError = onError;
148
+ $.ZQueryError = ZQueryError;
149
+ $.ErrorCode = ErrorCode;
150
+ $.guardCallback = guardCallback;
151
+ $.validate = validate;
148
152
 
149
153
  // --- Meta ------------------------------------------------------------------
150
154
  $.version = '__VERSION__';
155
+ $.libSize = '__LIB_SIZE__';
151
156
  $.meta = {}; // populated at build time by CLI bundler
152
157
 
153
158
  $.noConflict = () => {
@@ -176,13 +181,13 @@ export {
176
181
  ZQueryCollection,
177
182
  queryAll,
178
183
  reactive, Signal, signal, computed, effect,
179
- component, mount, mountAll, getInstance, destroy, getRegistry, style,
180
- morph,
184
+ component, mount, mountAll, getInstance, destroy, getRegistry, prefetch, style,
185
+ morph, morphElement,
181
186
  safeEval,
182
187
  createRouter, getRouter,
183
188
  createStore, getStore,
184
189
  http,
185
- ZQueryError, ErrorCode, onError, reportError,
190
+ ZQueryError, ErrorCode, onError, reportError, guardCallback, validate,
186
191
  debounce, throttle, pipe, once, sleep,
187
192
  escapeHtml, html, trust, uuid, camelCase, kebabCase,
188
193
  deepClone, deepMerge, isEqual, param, parseQuery,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zero-query",
3
- "version": "0.6.3",
3
+ "version": "0.8.6",
4
4
  "description": "Lightweight modern frontend library — jQuery-like selectors, reactive components, SPA router, and state management with zero dependencies.",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -9,8 +9,10 @@
9
9
  },
10
10
  "files": [
11
11
  "src",
12
+ "tests",
12
13
  "dist",
13
14
  "cli",
15
+ "types",
14
16
  "index.js",
15
17
  "index.d.ts",
16
18
  "LICENSE",
@@ -19,9 +21,10 @@
19
21
  "scripts": {
20
22
  "build": "node cli/index.js build",
21
23
  "dev": "node cli/index.js dev zquery-website",
24
+ "dev:bundle": "node cli/index.js dev zquery-website --bundle",
22
25
  "dev-lib": "node cli/index.js build --watch",
23
26
  "bundle": "node cli/index.js bundle",
24
- "bundle:app": "node cli/index.js bundle zquery-website --minimal",
27
+ "bundle:app": "node cli/index.js bundle zquery-website",
25
28
  "test": "vitest run",
26
29
  "test:watch": "vitest"
27
30
  },
@@ -52,11 +55,9 @@
52
55
  "publishConfig": {
53
56
  "access": "public"
54
57
  },
55
- "dependencies": {
56
- "zero-http": "^0.2.3"
57
- },
58
58
  "devDependencies": {
59
59
  "jsdom": "^28.1.0",
60
- "vitest": "^4.0.18"
60
+ "vitest": "^4.0.18",
61
+ "zero-http": "^0.2.3"
61
62
  }
62
63
  }
package/src/component.js CHANGED
@@ -339,7 +339,7 @@ class Component {
339
339
  // Normalize items → [{id, label}, …]
340
340
  def._pages = (p.items || []).map(item => {
341
341
  if (typeof item === 'string') return { id: item, label: _titleCase(item) };
342
- return { id: item.id, label: item.label || _titleCase(item.id) };
342
+ return { ...item, label: item.label || _titleCase(item.id) };
343
343
  });
344
344
 
345
345
  // Build URL map for lazy per-page loading.
@@ -381,11 +381,14 @@ class Component {
381
381
  if (def.styleUrl && !def._styleLoaded) {
382
382
  const su = def.styleUrl;
383
383
  if (typeof su === 'string') {
384
- def._externalStyles = await _fetchResource(_resolveUrl(su, base));
384
+ const resolved = _resolveUrl(su, base);
385
+ def._externalStyles = await _fetchResource(resolved);
386
+ def._resolvedStyleUrls = [resolved];
385
387
  } else if (Array.isArray(su)) {
386
388
  const urls = su.map(u => _resolveUrl(u, base));
387
389
  const results = await Promise.all(urls.map(u => _fetchResource(u)));
388
390
  def._externalStyles = results.join('\n');
391
+ def._resolvedStyleUrls = urls;
389
392
  }
390
393
  def._styleLoaded = true;
391
394
  }
@@ -469,6 +472,11 @@ class Component {
469
472
  html = '';
470
473
  }
471
474
 
475
+ // Pre-expand z-html and z-text at string level so the morph engine
476
+ // can diff their content properly (instead of clearing + re-injecting
477
+ // on every re-render). Same pattern as z-for: parse → evaluate → serialize.
478
+ html = this._expandContentDirectives(html);
479
+
472
480
  // -- Slot distribution ----------------------------------------
473
481
  // Replace <slot> elements with captured slot content from parent.
474
482
  // <slot> → default slot content
@@ -512,6 +520,13 @@ class Component {
512
520
  const styleEl = document.createElement('style');
513
521
  styleEl.textContent = scoped;
514
522
  styleEl.setAttribute('data-zq-component', this._def._name || '');
523
+ styleEl.setAttribute('data-zq-scope', scopeAttr);
524
+ if (this._def._resolvedStyleUrls) {
525
+ styleEl.setAttribute('data-zq-style-urls', this._def._resolvedStyleUrls.join(' '));
526
+ if (this._def.styles) {
527
+ styleEl.setAttribute('data-zq-inline', this._def.styles);
528
+ }
529
+ }
515
530
  document.head.appendChild(styleEl);
516
531
  this._styleEl = styleEl;
517
532
  }
@@ -551,8 +566,10 @@ class Component {
551
566
 
552
567
  // Update DOM via morphing (diffing) — preserves unchanged nodes
553
568
  // First render uses innerHTML for speed; subsequent renders morph.
569
+ const _renderStart = typeof window !== 'undefined' && (window.__zqMorphHook || window.__zqRenderHook) ? performance.now() : 0;
554
570
  if (!this._mounted) {
555
571
  this._el.innerHTML = html;
572
+ if (_renderStart && window.__zqRenderHook) window.__zqRenderHook(this._el, performance.now() - _renderStart, 'mount', this._def._name);
556
573
  } else {
557
574
  morph(this._el, html);
558
575
  }
@@ -595,31 +612,31 @@ class Component {
595
612
  }
596
613
  }
597
614
 
598
- // Bind @event="method" and z-on:event="method" handlers via delegation
615
+ // Bind @event="method" and z-on:event="method" handlers via delegation.
616
+ //
617
+ // Optimization: on the FIRST render, we scan for event attributes, build
618
+ // a delegated handler map, and attach one listener per event type to the
619
+ // component root. On subsequent renders (re-bind), we only rebuild the
620
+ // internal binding map — existing DOM listeners are reused since they
621
+ // delegate to event.target.closest(selector) at fire time.
599
622
  _bindEvents() {
600
- // Clean up old delegated listeners
601
- this._listeners.forEach(({ event, handler }) => {
602
- this._el.removeEventListener(event, handler);
603
- });
604
- this._listeners = [];
623
+ // Always rebuild the binding map from current DOM
624
+ const eventMap = new Map(); // event → [{ selector, methodExpr, modifiers, el }]
605
625
 
606
- // Find all elements with @event or z-on:event attributes
607
626
  const allEls = this._el.querySelectorAll('*');
608
- const eventMap = new Map(); // event → [{ selector, method, modifiers }]
609
-
610
627
  allEls.forEach(child => {
611
- // Skip elements inside z-pre subtrees
612
628
  if (child.closest('[z-pre]')) return;
613
629
 
614
- [...child.attributes].forEach(attr => {
615
- // Support both @event and z-on:event syntax
630
+ const attrs = child.attributes;
631
+ for (let a = 0; a < attrs.length; a++) {
632
+ const attr = attrs[a];
616
633
  let raw;
617
- if (attr.name.startsWith('@')) {
618
- raw = attr.name.slice(1); // @click.prevent → click.prevent
634
+ if (attr.name.charCodeAt(0) === 64) { // '@'
635
+ raw = attr.name.slice(1);
619
636
  } else if (attr.name.startsWith('z-on:')) {
620
- raw = attr.name.slice(5); // z-on:click.prevent → click.prevent
637
+ raw = attr.name.slice(5);
621
638
  } else {
622
- return;
639
+ continue;
623
640
  }
624
641
 
625
642
  const parts = raw.split('.');
@@ -635,12 +652,45 @@ class Component {
635
652
 
636
653
  if (!eventMap.has(event)) eventMap.set(event, []);
637
654
  eventMap.get(event).push({ selector, methodExpr, modifiers, el: child });
638
- });
655
+ }
639
656
  });
640
657
 
658
+ // Store binding map for the delegated handlers to reference
659
+ this._eventBindings = eventMap;
660
+
661
+ // Only attach DOM listeners once — reuse on subsequent renders.
662
+ // The handlers close over `this` and read `this._eventBindings`
663
+ // at fire time, so they always use the latest binding map.
664
+ if (this._delegatedEvents) {
665
+ // Already attached — just make sure new event types are covered
666
+ for (const event of eventMap.keys()) {
667
+ if (!this._delegatedEvents.has(event)) {
668
+ this._attachDelegatedEvent(event, eventMap.get(event));
669
+ }
670
+ }
671
+ // Remove listeners for event types no longer in the template
672
+ for (const event of this._delegatedEvents.keys()) {
673
+ if (!eventMap.has(event)) {
674
+ const { handler, opts } = this._delegatedEvents.get(event);
675
+ this._el.removeEventListener(event, handler, opts);
676
+ this._delegatedEvents.delete(event);
677
+ // Also remove from _listeners array
678
+ this._listeners = this._listeners.filter(l => l.event !== event);
679
+ }
680
+ }
681
+ return;
682
+ }
683
+
684
+ this._delegatedEvents = new Map();
685
+
641
686
  // Register delegated listeners on the component root
642
687
  for (const [event, bindings] of eventMap) {
643
- // Determine listener options from modifiers
688
+ this._attachDelegatedEvent(event, bindings);
689
+ }
690
+ }
691
+
692
+ // Attach a single delegated listener for an event type
693
+ _attachDelegatedEvent(event, bindings) {
644
694
  const needsCapture = bindings.some(b => b.modifiers.includes('capture'));
645
695
  const needsPassive = bindings.some(b => b.modifiers.includes('passive'));
646
696
  const listenerOpts = (needsCapture || needsPassive)
@@ -648,7 +698,9 @@ class Component {
648
698
  : false;
649
699
 
650
700
  const handler = (e) => {
651
- for (const { selector, methodExpr, modifiers, el } of bindings) {
701
+ // Read bindings from live map always up to date after re-renders
702
+ const currentBindings = this._eventBindings?.get(event) || [];
703
+ for (const { selector, methodExpr, modifiers, el } of currentBindings) {
652
704
  if (!e.target.closest(selector)) continue;
653
705
 
654
706
  // .self — only fire if target is the element itself
@@ -718,7 +770,7 @@ class Component {
718
770
  };
719
771
  this._el.addEventListener(event, handler, listenerOpts);
720
772
  this._listeners.push({ event, handler });
721
- }
773
+ this._delegatedEvents.set(event, { handler, opts: listenerOpts });
722
774
  }
723
775
 
724
776
  // Bind z-ref="name" → this.refs.name
@@ -757,7 +809,7 @@ class Component {
757
809
  // Read current state value (supports dot-path keys)
758
810
  const currentVal = _getPath(this.state, key);
759
811
 
760
- // -- Set initial DOM value from state ------------------------
812
+ // -- Set initial DOM value from state (always sync) ----------
761
813
  if (tag === 'input' && type === 'checkbox') {
762
814
  el.checked = !!currentVal;
763
815
  } else if (tag === 'input' && type === 'radio') {
@@ -779,6 +831,11 @@ class Component {
779
831
  : isEditable ? 'input' : 'input';
780
832
 
781
833
  // -- Handler: read DOM → write to reactive state -------------
834
+ // Skip if already bound (morph preserves existing elements,
835
+ // so re-binding would stack duplicate listeners)
836
+ if (el._zqModelBound) return;
837
+ el._zqModelBound = true;
838
+
782
839
  const handler = () => {
783
840
  let val;
784
841
  if (type === 'checkbox') val = el.checked;
@@ -898,6 +955,41 @@ class Component {
898
955
  return temp.innerHTML;
899
956
  }
900
957
 
958
+ // ---------------------------------------------------------------------------
959
+ // _expandContentDirectives — Pre-morph z-html & z-text expansion
960
+ //
961
+ // Evaluates z-html and z-text directives at the string level so the morph
962
+ // engine receives HTML with the actual content inline. This lets the diff
963
+ // algorithm properly compare old vs new content (text nodes, child elements)
964
+ // instead of clearing + re-injecting on every re-render.
965
+ //
966
+ // Same parse → evaluate → serialize pattern as _expandZFor.
967
+ // ---------------------------------------------------------------------------
968
+ _expandContentDirectives(html) {
969
+ if (!html.includes('z-html') && !html.includes('z-text')) return html;
970
+
971
+ const temp = document.createElement('div');
972
+ temp.innerHTML = html;
973
+
974
+ // z-html: evaluate expression → inject as innerHTML
975
+ temp.querySelectorAll('[z-html]').forEach(el => {
976
+ if (el.closest('[z-pre]')) return;
977
+ const val = this._evalExpr(el.getAttribute('z-html'));
978
+ el.innerHTML = val != null ? String(val) : '';
979
+ el.removeAttribute('z-html');
980
+ });
981
+
982
+ // z-text: evaluate expression → inject as textContent (HTML-safe)
983
+ temp.querySelectorAll('[z-text]').forEach(el => {
984
+ if (el.closest('[z-pre]')) return;
985
+ const val = this._evalExpr(el.getAttribute('z-text'));
986
+ el.textContent = val != null ? String(val) : '';
987
+ el.removeAttribute('z-text');
988
+ });
989
+
990
+ return temp.innerHTML;
991
+ }
992
+
901
993
  // ---------------------------------------------------------------------------
902
994
  // _processDirectives — Post-innerHTML DOM-level directive processing
903
995
  // ---------------------------------------------------------------------------
@@ -950,25 +1042,36 @@ class Component {
950
1042
  });
951
1043
 
952
1044
  // -- z-bind:attr / :attr (dynamic attribute binding) -----------
953
- this._el.querySelectorAll('*').forEach(el => {
954
- if (el.closest('[z-pre]')) return;
955
- [...el.attributes].forEach(attr => {
1045
+ // Use TreeWalker instead of querySelectorAll('*') avoids
1046
+ // creating a flat array of every single descendant element.
1047
+ // TreeWalker visits nodes lazily; FILTER_REJECT skips z-pre subtrees
1048
+ // at the walker level (faster than per-node closest('[z-pre]') checks).
1049
+ const walker = document.createTreeWalker(this._el, NodeFilter.SHOW_ELEMENT, {
1050
+ acceptNode(n) {
1051
+ return n.hasAttribute('z-pre') ? NodeFilter.FILTER_REJECT : NodeFilter.FILTER_ACCEPT;
1052
+ }
1053
+ });
1054
+ let node;
1055
+ while ((node = walker.nextNode())) {
1056
+ const attrs = node.attributes;
1057
+ for (let i = attrs.length - 1; i >= 0; i--) {
1058
+ const attr = attrs[i];
956
1059
  let attrName;
957
1060
  if (attr.name.startsWith('z-bind:')) attrName = attr.name.slice(7);
958
- else if (attr.name.startsWith(':') && !attr.name.startsWith('::')) attrName = attr.name.slice(1);
959
- else return;
1061
+ else if (attr.name.charCodeAt(0) === 58 && attr.name.charCodeAt(1) !== 58) attrName = attr.name.slice(1); // ':' but not '::'
1062
+ else continue;
960
1063
 
961
1064
  const val = this._evalExpr(attr.value);
962
- el.removeAttribute(attr.name);
1065
+ node.removeAttribute(attr.name);
963
1066
  if (val === false || val === null || val === undefined) {
964
- el.removeAttribute(attrName);
1067
+ node.removeAttribute(attrName);
965
1068
  } else if (val === true) {
966
- el.setAttribute(attrName, '');
1069
+ node.setAttribute(attrName, '');
967
1070
  } else {
968
- el.setAttribute(attrName, String(val));
1071
+ node.setAttribute(attrName, String(val));
969
1072
  }
970
- });
971
- });
1073
+ }
1074
+ }
972
1075
 
973
1076
  // -- z-class (dynamic class binding) ---------------------------
974
1077
  this._el.querySelectorAll('[z-class]').forEach(el => {
@@ -1000,21 +1103,9 @@ class Component {
1000
1103
  el.removeAttribute('z-style');
1001
1104
  });
1002
1105
 
1003
- // -- z-html (innerHTML injection) ------------------------------
1004
- this._el.querySelectorAll('[z-html]').forEach(el => {
1005
- if (el.closest('[z-pre]')) return;
1006
- const val = this._evalExpr(el.getAttribute('z-html'));
1007
- el.innerHTML = val != null ? String(val) : '';
1008
- el.removeAttribute('z-html');
1009
- });
1010
-
1011
- // -- z-text (safe textContent binding) -------------------------
1012
- this._el.querySelectorAll('[z-text]').forEach(el => {
1013
- if (el.closest('[z-pre]')) return;
1014
- const val = this._evalExpr(el.getAttribute('z-text'));
1015
- el.textContent = val != null ? String(val) : '';
1016
- el.removeAttribute('z-text');
1017
- });
1106
+ // z-html and z-text are now pre-expanded at string level (before
1107
+ // morph) via _expandContentDirectives(), so the diff engine can
1108
+ // properly diff their content instead of clearing + re-injecting.
1018
1109
 
1019
1110
  // -- z-cloak (remove after render) -----------------------------
1020
1111
  this._el.querySelectorAll('[z-cloak]').forEach(el => {
@@ -1047,6 +1138,8 @@ class Component {
1047
1138
  }
1048
1139
  this._listeners.forEach(({ event, handler }) => this._el.removeEventListener(event, handler));
1049
1140
  this._listeners = [];
1141
+ this._delegatedEvents = null;
1142
+ this._eventBindings = null;
1050
1143
  if (this._styleEl) this._styleEl.remove();
1051
1144
  _instances.delete(this._el);
1052
1145
  this._el.innerHTML = '';
@@ -1197,6 +1290,36 @@ export function getRegistry() {
1197
1290
  return Object.fromEntries(_registry);
1198
1291
  }
1199
1292
 
1293
+ /**
1294
+ * Pre-load a component's external templates and styles so the next mount
1295
+ * renders synchronously (no blank flash while fetching).
1296
+ * Safe to call multiple times — skips if already loaded.
1297
+ * @param {string} name — registered component name
1298
+ * @returns {Promise<void>}
1299
+ */
1300
+ export async function prefetch(name) {
1301
+ const def = _registry.get(name);
1302
+ if (!def) return;
1303
+
1304
+ // Load templateUrl, styleUrl, and normalize pages config
1305
+ if ((def.templateUrl && !def._templateLoaded) ||
1306
+ (def.styleUrl && !def._styleLoaded) ||
1307
+ (def.pages && !def._pagesNormalized)) {
1308
+ await Component.prototype._loadExternals.call({ _def: def });
1309
+ }
1310
+
1311
+ // For pages-based components, prefetch ALL page templates so any
1312
+ // active page renders instantly on mount.
1313
+ if (def._pageUrls && def._externalTemplates) {
1314
+ const missing = Object.entries(def._pageUrls)
1315
+ .filter(([id]) => !(id in def._externalTemplates));
1316
+ if (missing.length) {
1317
+ const results = await Promise.all(missing.map(([, url]) => _fetchResource(url)));
1318
+ missing.forEach(([id], i) => { def._externalTemplates[id] = results[i]; });
1319
+ }
1320
+ }
1321
+ }
1322
+
1200
1323
 
1201
1324
  // ---------------------------------------------------------------------------
1202
1325
  // Global stylesheet loader