@nativescript/vite 0.0.2 → 1.0.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 (71) hide show
  1. package/README.md +37 -0
  2. package/bin/cli.cjs +32 -0
  3. package/configuration/angular.js +66 -175
  4. package/configuration/angular.js.map +1 -1
  5. package/configuration/base.js +18 -21
  6. package/configuration/base.js.map +1 -1
  7. package/configuration/javascript.js +6 -5
  8. package/configuration/javascript.js.map +1 -1
  9. package/configuration/typescript.js +6 -5
  10. package/configuration/typescript.js.map +1 -1
  11. package/helpers/{angular-linker.js → angular/angular-linker.js} +27 -19
  12. package/helpers/angular/angular-linker.js.map +1 -0
  13. package/helpers/angular/shared-linker.d.ts +4 -0
  14. package/helpers/angular/shared-linker.js +39 -0
  15. package/helpers/angular/shared-linker.js.map +1 -0
  16. package/helpers/angular/util.d.ts +1 -0
  17. package/helpers/angular/util.js +67 -0
  18. package/helpers/angular/util.js.map +1 -0
  19. package/helpers/global-defines.d.ts +2 -0
  20. package/helpers/global-defines.js +5 -0
  21. package/helpers/global-defines.js.map +1 -1
  22. package/helpers/init.d.ts +1 -0
  23. package/helpers/init.js +119 -0
  24. package/helpers/init.js.map +1 -0
  25. package/helpers/logging.js +4 -0
  26. package/helpers/logging.js.map +1 -1
  27. package/helpers/main-entry.js +12 -4
  28. package/helpers/main-entry.js.map +1 -1
  29. package/helpers/nativeclass-transform.js +1 -1
  30. package/helpers/nativeclass-transform.js.map +1 -1
  31. package/helpers/utils.d.ts +4 -0
  32. package/helpers/utils.js +55 -0
  33. package/helpers/utils.js.map +1 -1
  34. package/hmr/client/index.js +257 -84
  35. package/hmr/client/index.js.map +1 -1
  36. package/hmr/entry-runtime.js +1 -1
  37. package/hmr/entry-runtime.js.map +1 -1
  38. package/hmr/frameworks/angular/client/index.d.ts +8 -0
  39. package/hmr/frameworks/angular/client/index.js +59 -0
  40. package/hmr/frameworks/angular/client/index.js.map +1 -0
  41. package/hmr/frameworks/angular/server/linker.d.ts +1 -0
  42. package/hmr/frameworks/angular/server/linker.js +101 -0
  43. package/hmr/frameworks/angular/server/linker.js.map +1 -0
  44. package/hmr/frameworks/angular/server/strategy.js +106 -20
  45. package/hmr/frameworks/angular/server/strategy.js.map +1 -1
  46. package/hmr/frameworks/solid/server/strategy.d.ts +2 -0
  47. package/hmr/frameworks/solid/server/strategy.js +56 -0
  48. package/hmr/frameworks/solid/server/strategy.js.map +1 -0
  49. package/hmr/frameworks/typescript/server/strategy.d.ts +2 -0
  50. package/hmr/frameworks/typescript/server/strategy.js +125 -0
  51. package/hmr/frameworks/typescript/server/strategy.js.map +1 -0
  52. package/hmr/frameworks/vue/client/index.js +7 -3
  53. package/hmr/frameworks/vue/client/index.js.map +1 -1
  54. package/hmr/frameworks/vue/server/strategy.js +3 -4
  55. package/hmr/frameworks/vue/server/strategy.js.map +1 -1
  56. package/hmr/server/index.js +5 -1
  57. package/hmr/server/index.js.map +1 -1
  58. package/hmr/server/websocket.d.ts +6 -0
  59. package/hmr/server/websocket.js +117 -26
  60. package/hmr/server/websocket.js.map +1 -1
  61. package/hmr/shared/vendor/manifest.d.ts +1 -0
  62. package/hmr/shared/vendor/manifest.js +38 -14
  63. package/hmr/shared/vendor/manifest.js.map +1 -1
  64. package/index.js +11 -0
  65. package/index.js.map +1 -1
  66. package/package.json +12 -9
  67. package/shims/angular-animations-stub.d.ts +62 -2
  68. package/shims/angular-animations-stub.js +132 -6
  69. package/shims/angular-animations-stub.js.map +1 -1
  70. package/helpers/angular-linker.js.map +0 -1
  71. /package/helpers/{angular-linker.d.ts → angular/angular-linker.d.ts} +0 -0
@@ -8,6 +8,9 @@
8
8
  import { setHMRWsUrl, getHMRWsUrl, pendingModuleFetches, deriveHttpOrigin, setHttpOriginForVite, moduleFetchCache, requestModuleFromServer, getHttpOriginForVite, normalizeSpec, hmrMetrics, graph, setGraphVersion, getGraphVersion, getCurrentApp, getRootFrame, setCurrentApp, setRootFrame, getCore, attachDiagnosticsToFrame, logUiSnapshot } from './utils.js';
9
9
  import { handleCssUpdates } from './css-handler.js';
10
10
  const VERBOSE = typeof __NS_ENV_VERBOSE__ !== 'undefined' && __NS_ENV_VERBOSE__;
11
+ const APP_ROOT_VIRTUAL = typeof __NS_APP_ROOT_VIRTUAL__ === 'string' && __NS_APP_ROOT_VIRTUAL__ ? __NS_APP_ROOT_VIRTUAL__ : '/src';
12
+ const APP_VIRTUAL_WITH_SLASH = APP_ROOT_VIRTUAL.endsWith('/') ? APP_ROOT_VIRTUAL : `${APP_ROOT_VIRTUAL}/`;
13
+ const APP_MAIN_ENTRY_SPEC = `${APP_VIRTUAL_WITH_SLASH}app.ts`;
11
14
  // Policy: by default, let the app's own main entry mount initially; HMR client handles updates/remounts only.
12
15
  // Flip this to true via global __NS_HMR_ALLOW_INITIAL_MOUNT__ if you need the client to perform the first mount.
13
16
  const ALLOW_INITIAL_MOUNT = !!globalThis.__NS_HMR_ALLOW_INITIAL_MOUNT__;
@@ -87,6 +90,7 @@ installDeepDiagnostics();
87
90
  * Flavor hooks
88
91
  */
89
92
  import { installNsVueDevShims, ensureBackWrapperInstalled, getRootForVue, loadSfcComponent, ensureVueGlobals, ensurePiniaOnApp, addSfcMapping, recordVuePayloadChanges, handleVueSfcRegistry, handleVueSfcRegistryUpdate } from '../frameworks/vue/client/index.js';
93
+ import { handleAngularHotUpdateMessage, installAngularHmrClientHooks } from '../frameworks/angular/client/index.js';
90
94
  switch (__NS_TARGET_FLAVOR__) {
91
95
  case 'vue':
92
96
  if (VERBOSE) {
@@ -101,6 +105,7 @@ switch (__NS_TARGET_FLAVOR__) {
101
105
  }
102
106
  catch { }
103
107
  }
108
+ installAngularHmrClientHooks();
104
109
  break;
105
110
  }
106
111
  // Global frame diagnostics: instrument Frame.navigate and Frame.topmost to detect
@@ -293,6 +298,9 @@ function installDeepDiagnostics() {
293
298
  let initialMounted = !!globalThis.__NS_HMR_BOOT_COMPLETE__;
294
299
  // Prevent duplicate initial-mount scheduling across rapid full-graph broadcasts and re-evaluations
295
300
  let initialMounting = !!globalThis.__NS_HMR_INITIAL_MOUNT_IN_PROGRESS__;
301
+ // TypeScript flavor: track registry modules and inferred main id
302
+ let tsModuleSet = null;
303
+ let tsMainId = null;
296
304
  const changedQueue = [];
297
305
  let processingQueue = false;
298
306
  // Detect whether the early placeholder root is still active on screen
@@ -322,7 +330,7 @@ function applyFullGraph(payload) {
322
330
  if (VERBOSE)
323
331
  console.log('[hmr][graph] full graph applied version', getGraphVersion(), 'modules=', graph.size);
324
332
  // Guarded initial mount rescue: if app hasn't replaced the placeholder shortly after graph arrives,
325
- // perform a one-time mount. This waits briefly to let /src/app.ts start() run first to avoid double mounts.
333
+ // perform a one-time mount. This waits briefly to let the app's main entry start() run first to avoid double mounts.
326
334
  try {
327
335
  const g = globalThis;
328
336
  const bootDone = !!g.__NS_HMR_BOOT_COMPLETE__;
@@ -391,26 +399,72 @@ function applyFullGraph(payload) {
391
399
  catch { }
392
400
  return;
393
401
  }
394
- // If placeholder still active and no real root, try a one-time gentle initial mount
402
+ // If placeholder still active and no real root, try a one-time gentle initial mount.
403
+ // Vue: prefer SFCs; TypeScript: import the app main TS module and let performResetRoot handle it once.
395
404
  const placeholderActive = isPlaceholderActive();
396
405
  if (!placeholderActive)
397
406
  return;
398
407
  if (VERBOSE)
399
- console.log('[hmr][init] placeholder persists after delay; performing one-time initial mount');
400
- // Prefer first .vue under /src/app.ts deps, else first .vue in graph
401
- let candidate = null;
402
- const appEntry = graph.get('/src/app.ts');
403
- if (appEntry && Array.isArray(appEntry.deps)) {
404
- const vueDep = appEntry.deps.find((d) => typeof d === 'string' && /\.vue$/i.test(d));
405
- if (vueDep)
406
- candidate = vueDep;
408
+ console.log('[hmr][init] placeholder persists after delay; evaluating rescue policy');
409
+ // Flavor-specific rescue handling
410
+ if (__NS_TARGET_FLAVOR__ === 'typescript') {
411
+ // For TS apps, perform a one-time resetRootView to the conventional
412
+ // app root module. This mimics what Application.run would do and
413
+ // replaces the placeholder with the real UI without trying to
414
+ // treat app.ts as a component.
415
+ try {
416
+ const App = getCore('Application') || g.Application;
417
+ if (App && typeof App.resetRootView === 'function') {
418
+ if (VERBOSE)
419
+ console.log('[hmr][init] TS flavor: performing rescue resetRootView to moduleName=app-root');
420
+ initialMounting = true;
421
+ try {
422
+ g.__NS_HMR_INITIAL_MOUNT_IN_PROGRESS__ = true;
423
+ }
424
+ catch { }
425
+ App.resetRootView({ moduleName: 'app-root' });
426
+ initialMounted = true;
427
+ try {
428
+ g.__NS_HMR_BOOT_COMPLETE__ = true;
429
+ }
430
+ catch { }
431
+ if (VERBOSE)
432
+ console.log('[hmr][init] TS rescue resetRootView complete');
433
+ }
434
+ else if (VERBOSE) {
435
+ console.warn('[hmr][init] TS flavor: Application.resetRootView unavailable; cannot perform rescue');
436
+ }
437
+ }
438
+ catch (e) {
439
+ console.warn('[hmr][init] TS rescue resetRootView failed', e);
440
+ }
441
+ finally {
442
+ initialMounting = false;
443
+ try {
444
+ g.__NS_HMR_INITIAL_MOUNT_IN_PROGRESS__ = false;
445
+ }
446
+ catch { }
447
+ }
448
+ return;
407
449
  }
408
- if (!candidate) {
409
- for (const id of graph.keys()) {
410
- if (/\.vue$/i.test(id)) {
411
- candidate = id;
412
- break;
450
+ let candidate = null;
451
+ switch (__NS_TARGET_FLAVOR__) {
452
+ case 'vue': {
453
+ const appEntry = graph.get(APP_MAIN_ENTRY_SPEC);
454
+ if (appEntry && Array.isArray(appEntry.deps)) {
455
+ const vueDep = appEntry.deps.find((d) => typeof d === 'string' && /\.vue$/i.test(d));
456
+ if (vueDep)
457
+ candidate = vueDep;
413
458
  }
459
+ if (!candidate) {
460
+ for (const id of graph.keys()) {
461
+ if (/\.vue$/i.test(id)) {
462
+ candidate = id;
463
+ break;
464
+ }
465
+ }
466
+ }
467
+ break;
414
468
  }
415
469
  }
416
470
  if (!candidate)
@@ -459,28 +513,36 @@ function applyFullGraph(payload) {
459
513
  }
460
514
  catch { }
461
515
  // On first full graph, if we have not mounted yet, attempt an initial mount
462
- // by choosing a likely Vue root component and performing a resetRootView with it.
516
+ // by choosing a likely root component for the active flavor and performing a resetRootView with it.
463
517
  try {
464
518
  // Short-circuit if boot is complete or an initial mount is already underway (across realms/evals)
465
519
  const bootDone = !!globalThis.__NS_HMR_BOOT_COMPLETE__;
466
520
  const bootInProgress = !!globalThis.__NS_HMR_INITIAL_MOUNT_IN_PROGRESS__ || initialMounting;
467
- // Only allow initial mount when explicitly enabled. Rely on the app's own /src/app.ts start() for the first mount
521
+ // Only allow initial mount when explicitly enabled. Rely on the app's own main entry start() for the first mount
468
522
  // to avoid double-mount races that can cause duplicate navigation logs.
469
523
  if (ALLOW_INITIAL_MOUNT && !initialMounted && !bootDone && !bootInProgress && !getCurrentApp() && !getRootFrame()) {
470
- // Prefer the first .vue dependency of /src/app.ts, else first .vue in graph
471
524
  let candidate = null;
472
- const appEntry = graph.get('/src/app.ts');
473
- if (appEntry && Array.isArray(appEntry.deps)) {
474
- const vueDep = appEntry.deps.find((d) => typeof d === 'string' && /\.vue$/i.test(d));
475
- if (vueDep)
476
- candidate = vueDep;
477
- }
478
- if (!candidate) {
479
- for (const id of graph.keys()) {
480
- if (/\.vue$/i.test(id)) {
481
- candidate = id;
482
- break;
525
+ switch (__NS_TARGET_FLAVOR__) {
526
+ case 'vue': {
527
+ const appEntry = graph.get(APP_MAIN_ENTRY_SPEC);
528
+ if (appEntry && Array.isArray(appEntry.deps)) {
529
+ const vueDep = appEntry.deps.find((d) => typeof d === 'string' && /\.vue$/i.test(d));
530
+ if (vueDep)
531
+ candidate = vueDep;
532
+ }
533
+ if (!candidate) {
534
+ for (const id of graph.keys()) {
535
+ if (/\.vue$/i.test(id)) {
536
+ candidate = id;
537
+ break;
538
+ }
539
+ }
483
540
  }
541
+ break;
542
+ }
543
+ case 'typescript': {
544
+ // For TS flavor, do not perform client-driven initial mount; rely on Application.run.
545
+ return;
484
546
  }
485
547
  }
486
548
  if (candidate) {
@@ -493,7 +555,7 @@ function applyFullGraph(payload) {
493
555
  (async () => {
494
556
  try {
495
557
  if (VERBOSE)
496
- console.log('[hmr][init] mounting initial Vue root from', candidate);
558
+ console.log('[hmr][init] mounting initial root from', candidate, 'flavor=', __NS_TARGET_FLAVOR__);
497
559
  // Android-only: avoid racing entry-runtime reset and Activity bring-up
498
560
  try {
499
561
  const g = globalThis;
@@ -526,7 +588,23 @@ function applyFullGraph(payload) {
526
588
  }
527
589
  }
528
590
  catch { }
529
- const comp = await loadSfcComponent(candidate, 'initial_mount');
591
+ let comp = null;
592
+ switch (__NS_TARGET_FLAVOR__) {
593
+ case 'vue':
594
+ comp = await loadSfcComponent(candidate, 'initial_mount');
595
+ break;
596
+ case 'typescript':
597
+ try {
598
+ const url = await requestModuleFromServer(candidate);
599
+ const mod = await import(/* @vite-ignore */ url);
600
+ comp = mod && (mod.default || mod);
601
+ }
602
+ catch (e) {
603
+ if (VERBOSE)
604
+ console.warn('[hmr][init] TS initial mount failed to import', candidate, e);
605
+ }
606
+ break;
607
+ }
530
608
  if (comp) {
531
609
  const ok = await performResetRoot(comp);
532
610
  if (ok) {
@@ -560,21 +638,31 @@ function applyFullGraph(payload) {
560
638
  })();
561
639
  }
562
640
  else if (VERBOSE) {
563
- console.warn('[hmr][init] no Vue component found in graph to mount initially');
641
+ console.warn('[hmr][init] no component found in graph to mount initially for flavor', __NS_TARGET_FLAVOR__);
564
642
  }
565
643
  }
566
644
  }
567
645
  catch { }
568
646
  }
569
647
  function applyDelta(payload) {
570
- if (payload.baseVersion !== getGraphVersion()) {
648
+ // If versions are out of sync, request a full graph resync, but still
649
+ // opportunistically queue the changed ids so the client can react once
650
+ // the resync arrives. This is especially important for simple TS flavors
651
+ // where we only need a signal that "something changed" to trigger a
652
+ // resetRootView-driven hot update.
653
+ const currentVersion = getGraphVersion();
654
+ const versionMatches = payload.baseVersion === currentVersion;
655
+ if (!versionMatches) {
571
656
  if (VERBOSE)
572
- console.warn('[hmr][graph] version mismatch requesting resync', payload.baseVersion, getGraphVersion());
573
- // Request resync (simple ping) – server will resend full graph (protocol extension placeholder)
574
- hmrSocket?.send(JSON.stringify({ type: 'ns:hmr-resync-request' }));
575
- return;
657
+ console.warn('[hmr][graph] version mismatch requesting resync', payload.baseVersion, currentVersion);
658
+ try {
659
+ hmrSocket?.send(JSON.stringify({ type: 'ns:hmr-resync-request' }));
660
+ }
661
+ catch { }
662
+ }
663
+ if (versionMatches) {
664
+ setGraphVersion(payload.newVersion);
576
665
  }
577
- setGraphVersion(payload.newVersion);
578
666
  const changed = payload.changed || [];
579
667
  switch (__NS_TARGET_FLAVOR__) {
580
668
  case 'vue':
@@ -582,17 +670,19 @@ function applyDelta(payload) {
582
670
  break;
583
671
  }
584
672
  (payload.changed || []).forEach((m) => {
585
- if (m && m.id)
586
- graph.set(m.id, { id: m.id, deps: m.deps || [], hash: m.hash || '' });
673
+ if (!m || !m.id)
674
+ return;
675
+ // Always update the local graph view with the announced module metadata
676
+ graph.set(m.id, { id: m.id, deps: m.deps || [], hash: m.hash || '' });
587
677
  });
588
678
  (payload.removed || []).forEach((r) => {
589
679
  graph.delete(r);
590
680
  });
591
681
  if (VERBOSE)
592
- console.log('[hmr][graph] delta applied newVersion', getGraphVersion(), 'changed=', (payload.changed || []).length, 'removed=', (payload.removed || []).length);
682
+ console.log('[hmr][graph] delta applied newVersion', getGraphVersion(), 'changed=', (payload.changed || []).length, 'removed=', (payload.removed || []).length, 'baseVersion=', payload.baseVersion);
593
683
  // Queue evaluation of changed modules (placeholder pipeline)
594
684
  if (payload.changed?.length) {
595
- // HARD SUPPRESS: the very first delta (baseVersion 0) commonly includes /src/app.ts which is already evaluated during bootstrap.
685
+ // HARD SUPPRESS: the very first delta (baseVersion 0) commonly includes the app main entry which is already evaluated during bootstrap.
596
686
  // Importing it again with a cache-bust often produces a spurious module-not-found (timestamp param treated as distinct file).
597
687
  const isInitial = payload.baseVersion === 0;
598
688
  // Filter out virtual helper ids (e.g. '\0plugin-vue:export-helper') which we do not evaluate directly
@@ -620,6 +710,7 @@ function applyDelta(payload) {
620
710
  if (VERBOSE)
621
711
  console.warn('[hmr][prefetch] failed', e);
622
712
  }
713
+ const isAppMainEntryId = (value) => value.endsWith(APP_MAIN_ENTRY_SPEC);
623
714
  for (const id of realIds) {
624
715
  // We now rely on SFC registry update events to trigger root resets for .vue files directly
625
716
  if (/\.vue$/i.test(id)) {
@@ -627,16 +718,16 @@ function applyDelta(payload) {
627
718
  console.log('[hmr][delta] skipping queue for .vue id; will handle on registry update', id);
628
719
  continue;
629
720
  }
630
- if (isInitial && /\/src\/app\.ts$/.test(id)) {
721
+ if (isInitial && isAppMainEntryId(id)) {
631
722
  if (VERBOSE)
632
- console.log('[hmr][delta] suppressing initial /src/app.ts evaluation');
723
+ console.log(`[hmr][delta] suppressing initial ${APP_MAIN_ENTRY_SPEC} evaluation`);
633
724
  continue;
634
725
  }
635
- if (/\/src\/app\.ts$/.test(id)) {
726
+ if (isAppMainEntryId(id)) {
636
727
  try {
637
728
  const exists = globalThis.require?.(id) || globalThis.__nsGetModuleExports?.(id);
638
729
  if (!exists && VERBOSE)
639
- console.log('[hmr][delta] skipping unresolved /src/app.ts change');
730
+ console.log(`[hmr][delta] skipping unresolved ${APP_MAIN_ENTRY_SPEC} change`);
640
731
  if (!exists)
641
732
  continue;
642
733
  }
@@ -828,8 +919,72 @@ async function processQueue() {
828
919
  if (processingQueue)
829
920
  return;
830
921
  processingQueue = true;
831
- // ...existing code...
832
- processingQueue = false;
922
+ try {
923
+ // Simple deterministic drain of the queue. We currently focus on TS flavor
924
+ // by re-importing changed modules (to refresh their HTTP ESM copies) and
925
+ // then performing a root reset so the UI reflects the new code.
926
+ const seen = new Set();
927
+ const drained = [];
928
+ while (changedQueue.length) {
929
+ const id = changedQueue.shift();
930
+ if (!id || typeof id !== 'string')
931
+ continue;
932
+ if (seen.has(id))
933
+ continue;
934
+ seen.add(id);
935
+ drained.push(id);
936
+ }
937
+ if (!drained.length)
938
+ return;
939
+ if (VERBOSE)
940
+ console.log('[hmr][queue] processing changed ids', drained);
941
+ // Evaluate changed modules best-effort; failures shouldn't completely break HMR.
942
+ for (const id of drained) {
943
+ try {
944
+ const spec = normalizeSpec(id);
945
+ const url = await requestModuleFromServer(spec);
946
+ if (!url)
947
+ continue;
948
+ if (VERBOSE)
949
+ console.log('[hmr][queue] re-import', { id, spec, url });
950
+ await import(/* @vite-ignore */ url);
951
+ }
952
+ catch (e) {
953
+ if (VERBOSE)
954
+ console.warn('[hmr][queue] re-import failed for', id, e);
955
+ }
956
+ }
957
+ // After evaluating the batch, perform flavor-specific UI refresh.
958
+ switch (__NS_TARGET_FLAVOR__) {
959
+ case 'vue':
960
+ // Vue SFCs are handled via the registry update path; nothing to do here.
961
+ break;
962
+ case 'typescript': {
963
+ // For TS apps, always reset back to the conventional app root.
964
+ // This preserves the shell (Frame, ActionBar, etc.) that the app's
965
+ // own bootstrapping wires up via `Application.run`.
966
+ try {
967
+ const g = globalThis;
968
+ const App = getCore('Application') || g.Application;
969
+ if (!App || typeof App.resetRootView !== 'function') {
970
+ if (VERBOSE)
971
+ console.warn('[hmr][queue] TS flavor: Application.resetRootView unavailable; skipping UI refresh');
972
+ break;
973
+ }
974
+ if (VERBOSE)
975
+ console.log('[hmr][queue] TS flavor: resetRootView(app-root) after changes');
976
+ App.resetRootView({ moduleName: 'app-root' });
977
+ }
978
+ catch (e) {
979
+ console.warn('[hmr][queue] TS flavor: resetRootView(app-root) failed', e);
980
+ }
981
+ break;
982
+ }
983
+ }
984
+ }
985
+ finally {
986
+ processingQueue = false;
987
+ }
833
988
  }
834
989
  let hmrSocket = null;
835
990
  // Track server-announced batches for each version so we can import in-order client-side
@@ -993,6 +1148,33 @@ async function handleHmrMessage(ev) {
993
1148
  applyFullGraph(msg);
994
1149
  return;
995
1150
  }
1151
+ if (msg.type === 'ns:ts-module-registry') {
1152
+ try {
1153
+ const mods = Array.isArray(msg.modules) ? msg.modules.filter((m) => typeof m === 'string') : [];
1154
+ if (mods.length) {
1155
+ if (!tsModuleSet)
1156
+ tsModuleSet = new Set();
1157
+ mods.forEach((m) => tsModuleSet.add(m));
1158
+ // Prefer explicit app main entry if present, else first module
1159
+ if (mods.includes(APP_MAIN_ENTRY_SPEC)) {
1160
+ tsMainId = APP_MAIN_ENTRY_SPEC;
1161
+ }
1162
+ else if (!tsMainId) {
1163
+ tsMainId = mods[0];
1164
+ }
1165
+ if (VERBOSE)
1166
+ console.log('[hmr-client][ts-registry] registered TS modules', {
1167
+ count: tsModuleSet.size,
1168
+ mainId: tsMainId,
1169
+ });
1170
+ }
1171
+ }
1172
+ catch (e) {
1173
+ if (VERBOSE)
1174
+ console.warn('[hmr-client][ts-registry] failed to record', e);
1175
+ }
1176
+ return;
1177
+ }
996
1178
  if (msg.type === 'ns:hmr-delta') {
997
1179
  setGraphVersion(Number(msg.newVersion || getGraphVersion() || 0));
998
1180
  try {
@@ -1004,41 +1186,7 @@ async function handleHmrMessage(ev) {
1004
1186
  applyDelta(msg);
1005
1187
  return;
1006
1188
  }
1007
- else if (msg.type === 'ns:angular-update') {
1008
- try {
1009
- if (VERBOSE)
1010
- console.log('[hmr-client][angular] update', msg);
1011
- // Minimal safe policy for Angular today: if the app provides a bootstrap
1012
- // factory via globalThis.__NS_ANGULAR_BOOTSTRAP__, perform a full root
1013
- // replacement. Otherwise, log a guidance message.
1014
- const g = globalThis;
1015
- const App = getCore('Application') || g.Application;
1016
- const bootstrap = g.__NS_ANGULAR_BOOTSTRAP__;
1017
- if (typeof App?.resetRootView === 'function' && typeof bootstrap === 'function') {
1018
- if (VERBOSE)
1019
- console.log('[hmr-client][angular] resetRootView via bootstrap factory');
1020
- try {
1021
- g.__NS_DEV_RESET_IN_PROGRESS__ = true;
1022
- }
1023
- catch { }
1024
- App.resetRootView({ create: () => bootstrap() });
1025
- setTimeout(() => {
1026
- try {
1027
- g.__NS_DEV_RESET_IN_PROGRESS__ = false;
1028
- }
1029
- catch { }
1030
- }, 0);
1031
- return;
1032
- }
1033
- if (VERBOSE)
1034
- console.warn('[hmr-client][angular] No __NS_ANGULAR_BOOTSTRAP__ factory found; consider exposing one from main.ts');
1035
- }
1036
- catch (e) {
1037
- try {
1038
- console.warn('[hmr-client][angular] failed to handle update', e && (e.message || e));
1039
- }
1040
- catch { }
1041
- }
1189
+ else if (handleAngularHotUpdateMessage(msg, { getCore, verbose: VERBOSE })) {
1042
1190
  return;
1043
1191
  }
1044
1192
  }
@@ -1203,6 +1351,31 @@ async function performResetRoot(newComponent) {
1203
1351
  case 'vue':
1204
1352
  cachedRoot = getRootForVue(newComponent, state);
1205
1353
  break;
1354
+ case 'typescript': {
1355
+ // For TS flavor, treat the component as a factory or direct NS view.
1356
+ let root = null;
1357
+ try {
1358
+ if (typeof newComponent === 'function') {
1359
+ root = newComponent();
1360
+ }
1361
+ else {
1362
+ root = newComponent;
1363
+ }
1364
+ }
1365
+ catch (e) {
1366
+ console.warn('[hmr-client][ts] root factory invocation failed', e);
1367
+ }
1368
+ cachedRoot = root || {};
1369
+ // Heuristic: if the root "looks" like a Frame, prefer frame semantics
1370
+ try {
1371
+ const name = String(cachedRoot?.constructor?.name || '').replace(/^_+/, '');
1372
+ if (/^Frame(\$\d+)?$/.test(name)) {
1373
+ rootKind = 'frame';
1374
+ }
1375
+ }
1376
+ catch { }
1377
+ break;
1378
+ }
1206
1379
  }
1207
1380
  return cachedRoot;
1208
1381
  }
@@ -1524,7 +1697,7 @@ export function initHmrClient(opts) {
1524
1697
  setHMRWsUrl(opts.wsUrl);
1525
1698
  }
1526
1699
  if (VERBOSE)
1527
- console.log('[hmr-client] Initializing Vue HMR client', getHMRWsUrl() ? `(ws: ${getHMRWsUrl()})` : '');
1700
+ console.log('[hmr-client] Initializing HMR client', getHMRWsUrl() ? `(ws: ${getHMRWsUrl()})` : '');
1528
1701
  // Prevent duplicate client initialization across re-evaluations
1529
1702
  const g = globalThis;
1530
1703
  if (g.__NS_HMR_CLIENT_ACTIVE__) {