@nativescript/vite 8.0.0-alpha.11 → 8.0.0-alpha.12

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.
@@ -2701,6 +2701,74 @@ function createHmrWebSocketPlugin(opts) {
2701
2701
  if (!wsAny.__NS_ANGULAR_FULL_RELOAD_FILTER_INSTALLED__) {
2702
2702
  const originalSend = server.ws.send.bind(server.ws);
2703
2703
  wsAny.__NS_ANGULAR_FULL_RELOAD_FILTER_INSTALLED__ = true;
2704
+ // Bridge Vite's stock WS broadcasts (`server.ws.send(...)`)
2705
+ // to our `/ns-hmr` WebSocket. Vite v8 keeps two completely
2706
+ // separate `WebSocketServer` instances: its own (default
2707
+ // path `/`, accepting `vite-hmr`/`vite-ping` protocols) and
2708
+ // ours (`/ns-hmr`, where the iOS device actually connects).
2709
+ // Plugin-emitted events like Analog's
2710
+ // `server.ws.send('angular:component-update', { id, ts })`
2711
+ // flow through Vite's `normalizedHotChannel.send` →
2712
+ // `wss.clients.forEach`, but those `wss.clients` are
2713
+ // EMPTY in NativeScript dev — the device never speaks the
2714
+ // `vite-hmr` protocol nor connects to `/`. Without a
2715
+ // bridge, every plugin-emitted custom event is logged on
2716
+ // the server (e.g. `(client) hmr update <html>`) but
2717
+ // silently dropped before reaching the device. Symptom:
2718
+ // the iOS HMR-applying overlay sticks at 5%
2719
+ // ("Preparing update") forever because Angular's compiled
2720
+ // `import.meta.hot.on('angular:component-update', cb)`
2721
+ // listeners never fire. We mirror the payload onto our
2722
+ // `/ns-hmr` clients here so the existing custom-event
2723
+ // dispatcher in `hmr/client/index.ts` (which forwards to
2724
+ // `__NS_DISPATCH_HOT_EVENT__`) actually runs.
2725
+ const bridgeToNsHmrClients = (payload, args) => {
2726
+ try {
2727
+ let normalized;
2728
+ if (typeof args[0] === 'string') {
2729
+ normalized = { type: 'custom', event: args[0], data: args[1] };
2730
+ }
2731
+ else {
2732
+ normalized = payload;
2733
+ }
2734
+ if (!normalized)
2735
+ return;
2736
+ // Vite's stock `update` payload includes per-module
2737
+ // HMR boundary info that our device-side client
2738
+ // has no handler for (we drive HMR via our own
2739
+ // `ns:angular-update`/`ns:hmr-delta`/`ns:css-updates`
2740
+ // messages). Forwarding it would just look like
2741
+ // noise to the client. Custom events
2742
+ // (`type: 'custom'`) — including
2743
+ // `angular:component-update` and Analog's
2744
+ // CSS-direct/inline `update` shorthand — DO need
2745
+ // to reach the device, since they drive the
2746
+ // in-place `ɵɵreplaceMetadata` template-swap path.
2747
+ // Filter the relay to those.
2748
+ if (normalized.type !== 'custom')
2749
+ return;
2750
+ const stringified = JSON.stringify(normalized);
2751
+ let recipients = 0;
2752
+ wss?.clients.forEach((client) => {
2753
+ try {
2754
+ if (client && client.readyState === 1) {
2755
+ client.send(stringified);
2756
+ recipients++;
2757
+ }
2758
+ }
2759
+ catch { }
2760
+ });
2761
+ if (verbose) {
2762
+ const event = normalized?.event;
2763
+ console.log(`[hmr-ws][bridge] forwarded ${normalized.type}${event ? `:${event}` : ''} payload to ${recipients} /ns-hmr client(s)`);
2764
+ }
2765
+ }
2766
+ catch (err) {
2767
+ if (verbose) {
2768
+ console.warn('[hmr-ws][bridge] failed to forward payload to /ns-hmr clients', err);
2769
+ }
2770
+ }
2771
+ };
2704
2772
  server.ws.send = ((payload, ...rest) => {
2705
2773
  pruneAngularReloadSuppressions();
2706
2774
  if (shouldSuppressViteFullReloadPayload({
@@ -2713,6 +2781,7 @@ function createHmrWebSocketPlugin(opts) {
2713
2781
  }
2714
2782
  return;
2715
2783
  }
2784
+ bridgeToNsHmrClients(payload, [payload, ...rest]);
2716
2785
  return originalSend(payload, ...rest);
2717
2786
  });
2718
2787
  }
@@ -3126,6 +3195,84 @@ function createHmrWebSocketPlugin(opts) {
3126
3195
  const urlObj = new URL(req.url || '', 'http://localhost');
3127
3196
  if (!urlObj.pathname.startsWith('/ns/m'))
3128
3197
  return next();
3198
+ // Delegate AnalogJS Angular component live-reload endpoints.
3199
+ //
3200
+ // Angular 21's `ɵɵgetReplaceMetadataURL` (in @angular/core
3201
+ // _debug_node-chunk.mjs) builds the metadata-replacement URL as
3202
+ // `new URL('./@ng/component?c=<id>&t=<ts>', import.meta.url).href`.
3203
+ // Because `import.meta.url` for a NS-served module is
3204
+ // `http://host:port/ns/m/<project-relative>/component.ts`, the
3205
+ // resolved metadata URL ends up *nested* under the component's
3206
+ // directory: `/ns/m/<dir>/@ng/component?c=...&t=...`.
3207
+ //
3208
+ // AnalogJS's `liveReloadPlugin` registers a middleware that matches
3209
+ // `/@ng/component` anywhere in `req.url` and returns either an empty
3210
+ // module body (no HMR update available) or the metadata-replacement
3211
+ // code (after a save invalidates the file). Without this delegation
3212
+ // the NS `/ns/m/` middleware would treat the path as a file lookup,
3213
+ // fail to resolve `@ng/component` against disk, and respond with
3214
+ // 404 — which surfaces as `HTTP fetch/compile failed` at the
3215
+ // component's own `_HmrLoad(Date.now())` call on initial boot and
3216
+ // blocks Angular component bootstrapping.
3217
+ //
3218
+ // Calling `next()` here lets AnalogJS's middleware (or any other
3219
+ // middleware later in the chain) handle the request. Analog's
3220
+ // middleware reads only the `?c=` query string and is pathname-
3221
+ // agnostic, so we don't need to rewrite `req.url` for it to work.
3222
+ //
3223
+ // HOWEVER: AnalogJS responds with an EMPTY body (`res.end('')`)
3224
+ // for non-invalidated component IDs (initial boot, before any
3225
+ // file save). The iOS HTTP ESM loader's
3226
+ // `LoadHttpModuleForUrl` (ModuleInternalCallbacks.mm) treats an
3227
+ // empty body as a fetch failure (`body.empty() → reject`), even
3228
+ // when the HTTP status is 200 OK. That bubbles up as
3229
+ // `HTTP fetch/compile failed` at the device's `__ns_import(...)`
3230
+ // inside each component's `_HmrLoad(Date.now())` and crashes
3231
+ // Angular's component bootstrap. To make Analog's empty
3232
+ // "no-update" response acceptable to the iOS loader, we wrap
3233
+ // `res.write` / `res.end` and substitute a minimal valid ESM
3234
+ // module body (`export {}`) when downstream writes nothing.
3235
+ // Non-empty bodies (real HMR update payloads after a save)
3236
+ // pass through unchanged.
3237
+ if (urlObj.pathname.includes('/@ng/component')) {
3238
+ const chunks = [];
3239
+ const origWrite = res.write.bind(res);
3240
+ const origEnd = res.end.bind(res);
3241
+ let ended = false;
3242
+ const captureChunk = (chunk) => {
3243
+ if (chunk == null)
3244
+ return;
3245
+ if (typeof chunk === 'string') {
3246
+ chunks.push(chunk);
3247
+ }
3248
+ else if (Buffer.isBuffer(chunk)) {
3249
+ chunks.push(chunk.toString('utf8'));
3250
+ }
3251
+ else {
3252
+ chunks.push(String(chunk));
3253
+ }
3254
+ };
3255
+ res.write = function (chunk, ..._args) {
3256
+ captureChunk(chunk);
3257
+ return true;
3258
+ };
3259
+ res.end = function (chunk, ..._args) {
3260
+ if (ended)
3261
+ return true;
3262
+ ended = true;
3263
+ captureChunk(chunk);
3264
+ let body = chunks.join('');
3265
+ if (body.length === 0) {
3266
+ body = '// [ns:m] empty Angular component metadata — substituted with valid empty module to satisfy iOS HTTP loader (rejects empty bodies)\nexport {};\n';
3267
+ }
3268
+ try {
3269
+ res.setHeader('Content-Length', Buffer.byteLength(body, 'utf8'));
3270
+ }
3271
+ catch { }
3272
+ return origEnd(body);
3273
+ };
3274
+ return next();
3275
+ }
3129
3276
  // Previously we awaited `populateInitialGraph(server)` here so
3130
3277
  // graphVersion would be non-zero for the first /ns/m request.
3131
3278
  // That gave deterministic URL tags but blocked the cold boot on a
@@ -4068,6 +4215,40 @@ export const piniaSymbol = p.piniaSymbol;
4068
4215
  `export const $navigateTo = (...a) => { const vm = (__cached_vm || (void __ensure(), __cached_vm)); const rt = __ensure(); try { if (!(g && g.Frame)) { const ns = (__ns_core_bridge && (__ns_core_bridge.__esModule && __ns_core_bridge.default ? __ns_core_bridge.default : (__ns_core_bridge.default || __ns_core_bridge))) || __ns_core_bridge || {}; if (ns) { if (!g.Frame && ns.Frame) g.Frame = ns.Frame; if (!g.Page && ns.Page) g.Page = ns.Page; if (!g.Application && (ns.Application||ns.app||ns.application)) g.Application = (ns.Application||ns.app||ns.application); } } } catch {} try { const hmrRealm = (g && g.__NS_HMR_REALM__) || 'unknown'; const hasTop = !!(g && g.Frame && g.Frame.topmost && g.Frame.topmost()); const top = hasTop ? g.Frame.topmost() : null; const ctor = top && top.constructor && top.constructor.name; } catch {} if (g && typeof g.__nsNavigateUsingApp === 'function') { try { return g.__nsNavigateUsingApp(...a); } catch (e) { console.error('[ns-rt] $navigateTo app navigator error', e); throw e; } } console.error('[ns-rt] $navigateTo unavailable: app navigator missing'); throw new Error('$navigateTo unavailable: app navigator missing'); } ;\n` +
4069
4216
  `export const $navigateBack = (...a) => { const vm = (__cached_vm || (void __ensure(), __cached_vm)); const rt = __ensure(); const impl = (vm && (vm.$navigateBack || (vm.default && vm.default.$navigateBack))) || (rt && (rt.$navigateBack || (rt.runtimeHelpers && rt.runtimeHelpers.navigateBack))); let res; try { const via = (impl && (impl === (vm && vm.$navigateBack) || impl === (vm && vm.default && vm.default.$navigateBack))) ? 'vm' : (impl ? 'rt' : 'none'); } catch {} try { if (typeof impl === 'function') res = impl(...a); } catch {} try { const top = (g && g.Frame && g.Frame.topmost && g.Frame.topmost()); if (!res && top && top.canGoBack && top.canGoBack()) { res = top.goBack(); } } catch {} try { const hook = g && (g.__NS_HMR_ON_NAVIGATE_BACK || g.__NS_HMR_ON_BACK || g.__nsAttemptBackRemount); if (typeof hook === 'function') hook(); } catch {} return res; }\n` +
4070
4217
  `export const $showModal = (...a) => { const vm = (__cached_vm || (void __ensure(), __cached_vm)); const rt = __ensure(); const impl = (vm && (vm.$showModal || (vm.default && vm.default.$showModal))) || (rt && (rt.$showModal || (rt.runtimeHelpers && rt.runtimeHelpers.showModal))); try { if (typeof impl === 'function') return impl(...a); } catch (e) { } return undefined; }\n` +
4218
+ // Vite client helpers re-exported through the runtime bridge.
4219
+ //
4220
+ // Vite's `vite:import-analysis` plugin rewrites unresolvable dynamic
4221
+ // imports (where the URL is not a static string literal) as
4222
+ // `__vite__injectQuery(<expr>, 'import')` and prepends an import
4223
+ // from `/@vite/client`. The /* @vite-ignore */ comment only
4224
+ // suppresses the warning, not the rewrite — Vite gates the rewrite
4225
+ // on `urlIsStringRE`, not `hasViteIgnoreRE`.
4226
+ //
4227
+ // In NativeScript dev, the AST normalizer (packages/vite/hmr/helpers/
4228
+ // ast-normalizer.ts) correctly strips the /@vite/client import (the
4229
+ // browser-only client module is not loadable on-device), then sees
4230
+ // the unbound `__vite__injectQuery` identifier and synthesizes
4231
+ // `const { vite__injectQuery: __vite__injectQuery } = __ns_rt_ns_1`
4232
+ // from this bridge. Without this export the destructure binds to
4233
+ // undefined and Angular 21's component HMR loader (and any other
4234
+ // caller of dynamic-import-with-non-literal-URL) fails with
4235
+ // `__vite__injectQuery is not a function` at module evaluation.
4236
+ //
4237
+ // This polyfill mirrors Vite 8's `__vite__injectQuery` in
4238
+ // node_modules/vite/dist/node/chunks/node.js — for relative or
4239
+ // absolute-path URLs it appends `?<queryToInject>` (preserving
4240
+ // existing search/hash); for already-absolute URLs (http(s):, etc.)
4241
+ // it returns the URL unchanged. Angular's `ɵɵgetReplaceMetadataURL`
4242
+ // returns absolute HTTP URLs, so this acts as a passthrough at
4243
+ // runtime, matching Vite's web behavior.
4244
+ `export const vite__injectQuery = (url, queryToInject) => {\n` +
4245
+ ` if (typeof url !== 'string') return url;\n` +
4246
+ ` if (url[0] !== '.' && url[0] !== '/') return url;\n` +
4247
+ ` const pathname = url.replace(/[?#].*$/, '');\n` +
4248
+ ` let search = '', hash = '';\n` +
4249
+ ` try { const u = new URL(url, 'http://vite.dev'); search = u.search || ''; hash = u.hash || ''; } catch {}\n` +
4250
+ ` return pathname + '?' + queryToInject + (search ? '&' + search.slice(1) : '') + (hash || '');\n` +
4251
+ `};\n` +
4071
4252
  `export default {\n` +
4072
4253
  ` defineComponent, resolveComponent, createVNode, createTextVNode, createCommentVNode,\n` +
4073
4254
  ` Fragment, Teleport, Transition, TransitionGroup, KeepAlive, Suspense, withCtx, openBlock,\n` +
@@ -4077,7 +4258,8 @@ export const piniaSymbol = p.piniaSymbol;
4077
4258
  ` isVNode, cloneVNode, isRef, ref, shallowRef, unref, computed, reactive, readonly, isReactive, isReadonly, toRaw, markRaw, shallowReactive, shallowReadonly,\n` +
4078
4259
  ` watch, watchEffect, watchPostEffect, watchSyncEffect, onBeforeMount, onMounted, onBeforeUpdate, onUpdated,\n` +
4079
4260
  ` onBeforeUnmount, onUnmounted, onActivated, onDeactivated, onErrorCaptured, onRenderTracked, onRenderTriggered, nextTick, h, provide, inject, vShow, createApp, registerElement,\n` +
4080
- ` $navigateTo, $navigateBack, $showModal\n` +
4261
+ ` $navigateTo, $navigateBack, $showModal,\n` +
4262
+ ` vite__injectQuery\n` +
4081
4263
  `};\n`;
4082
4264
  // Prepend guard and ship (harmless, keeps diagnostics consistent)
4083
4265
  code = REQUIRE_GUARD_SNIPPET + code;
@@ -6314,6 +6496,118 @@ export const piniaSymbol = p.piniaSymbol;
6314
6496
  // For Angular, react to component TS or external template HTML changes under /src
6315
6497
  const isHtml = file.endsWith('.html');
6316
6498
  const isTs = file.endsWith('.ts');
6499
+ // Web-style template HMR opt-in: when the user enables Angular's
6500
+ // `liveReload` (Analog's flag, mirrored from `--hmr` in
6501
+ // `configuration/angular.ts`), `.html` edits are owned by
6502
+ // Analog's `handleHotUpdate` which sends
6503
+ // `server.ws.send('angular:component-update', { id, timestamp })`.
6504
+ // The runtime listener registered in each compiled component
6505
+ // `.mjs` then dynamic-imports `/@ng/component?c=<id>&t=<ts>` and
6506
+ // calls `ɵɵreplaceMetadata` on the live class — swapping the
6507
+ // template definition AND walking live `LView`s to recreate
6508
+ // matching views in-place. NO Angular reboot, NO route navigation.
6509
+ //
6510
+ // The NS reboot path (`ns:angular-update` → `__reboot_ng_modules__`)
6511
+ // must be SKIPPED for HTML edits when this is on; otherwise both
6512
+ // fire, the reboot wins, and we lose the in-place swap. The
6513
+ // reboot path stays intact for `.ts` edits — those genuinely
6514
+ // change module-level code (services, route configs, NgModule
6515
+ // providers) that Angular's `ɵɵreplaceMetadata` can't reach.
6516
+ //
6517
+ // We detect "live reload mode is on" by checking that the
6518
+ // `analogjs-live-reload-plugin` registered itself with the
6519
+ // dev server. That plugin only exists when `liveReload: true`
6520
+ // was passed to `angular()` in `configuration/angular.ts`,
6521
+ // which gates on `hmrActive`. So this check is a clean
6522
+ // boolean: true iff the in-place pipeline is wired up.
6523
+ const angularLiveReloadActive = (server.config?.plugins ?? []).some((plugin) => plugin?.name === 'analogjs-live-reload-plugin');
6524
+ if (isHtml && angularLiveReloadActive) {
6525
+ updateMetrics.tAfterFramework = Date.now();
6526
+ if (verbose) {
6527
+ const rel = '/' +
6528
+ path.posix
6529
+ .normalize(path.relative(server.config.root || process.cwd(), file))
6530
+ .split(path.sep)
6531
+ .join('/');
6532
+ console.info(`[ns-hmr-diag][server] HTML edit handed off to Analog component-update path; skipping ns:angular-update broadcast (file=${rel})`);
6533
+ }
6534
+ // Re-query the moduleGraph for this file AFTER awaiting
6535
+ // `graphInitialPopulationPromise` (done at the top of
6536
+ // `handleHotUpdate`) and return the freshly-discovered
6537
+ // modules so they propagate to Analog's `handleHotUpdate`
6538
+ // in the same chain.
6539
+ //
6540
+ // Vite v8 builds the initial `mixedHmrContext.modules`
6541
+ // from `mixedModuleGraph.getModulesByFile(file)` BEFORE
6542
+ // any plugin runs. On the very first save after a cold
6543
+ // dev-server start, the moduleGraph for the changed
6544
+ // `.html` template has not yet been populated — that
6545
+ // population happens lazily via `populateInitialGraph`
6546
+ // → `transformRequest` → Analog's `transform` hook →
6547
+ // `addWatchFile(htmlFile)` → `vite:import-analysis`
6548
+ // consumes `_addedImports` and finally calls
6549
+ // `moduleGraph.updateModuleInfo` which registers the
6550
+ // `html → component.ts` importer relationship in
6551
+ // `fileToModulesMap`. All of that work races against the
6552
+ // file-watcher event for the `.html` edit, and the
6553
+ // watcher event almost always wins — so `ctx.modules`
6554
+ // arrives as `[]` even though the component is fully
6555
+ // compiled and ready to receive an in-place template
6556
+ // swap.
6557
+ //
6558
+ // Returning `undefined` here would propagate that empty
6559
+ // `ctx.modules` to the next plugin (Analog's handler),
6560
+ // which iterates with `ctx.modules.forEach(mod => mod
6561
+ // .importers.forEach(imp => …))` — a no-op when
6562
+ // `ctx.modules` is empty. Analog never broadcasts
6563
+ // `angular:component-update`, never marks anything
6564
+ // self-accepting, and Vite falls back to a `full-reload`
6565
+ // payload that the device runtime cannot honor (NS apps
6566
+ // don't have a browser-style page reload). The
6567
+ // user-visible symptom is exactly the "first save logs
6568
+ // `(client) page reload` and the simulator gets stuck
6569
+ // on the HMR-applying overlay forever" failure we hit
6570
+ // before this re-query was added.
6571
+ //
6572
+ // Since we already `await graphInitialPopulationPromise`
6573
+ // at the top of this function, by this point the
6574
+ // moduleGraph IS populated (every component file in
6575
+ // `src/` has been transformed and `addWatchFile` has
6576
+ // been consumed by `import-analysis`). A fresh
6577
+ // `getModulesByFile(file)` call now returns the template
6578
+ // module with the importing component's module in
6579
+ // `.importers`. Returning that array overwrites
6580
+ // `mixedHmrContext.modules` so Analog's handler — which
6581
+ // runs RIGHT AFTER us in the same chain — sees the
6582
+ // populated importer graph, identifies the component
6583
+ // class via `classNames.get(imp.id)`, and broadcasts
6584
+ // `angular:component-update` for `ɵɵreplaceMetadata`.
6585
+ //
6586
+ // We still skip the reboot path (`ns:angular-update`)
6587
+ // for HTML edits — control never reaches the
6588
+ // reboot-broadcast block below because of the `return`
6589
+ // here. The default-Vite-full-reload suppression is now
6590
+ // Analog's responsibility: it marks the changed module
6591
+ // self-accepting, which tells Vite the update is
6592
+ // handled and prevents the fallback.
6593
+ let resolvedModules = ctx.modules;
6594
+ try {
6595
+ const fresh = server.moduleGraph?.getModulesByFile?.(file);
6596
+ if (fresh && fresh.size > 0) {
6597
+ resolvedModules = [...fresh];
6598
+ if (verbose) {
6599
+ console.info(`[ns-hmr-diag][server] re-queried modules after graph population: count=${resolvedModules.length} (was ${ctx.modules?.length ?? 0})`);
6600
+ }
6601
+ }
6602
+ }
6603
+ catch (refetchErr) {
6604
+ if (verbose) {
6605
+ console.warn('[ns-hmr-diag][server] failed to re-query moduleGraph for html update', refetchErr);
6606
+ }
6607
+ }
6608
+ emitHmrUpdateSummary();
6609
+ return resolvedModules;
6610
+ }
6317
6611
  const angularHotUpdateRoots = collectAngularHotUpdateRoots({
6318
6612
  file,
6319
6613
  modules: ctx.modules,
@@ -6769,18 +7063,7 @@ export const piniaSymbol = p.piniaSymbol;
6769
7063
  const ext = file.match(/\.(?:[mc]?[jt]sx?)$/i)?.[0] || '';
6770
7064
  const baseSpec = '/' + path.posix.normalize(path.relative(root, file)).split(path.sep).join('/');
6771
7065
  const baseNoExt = ext ? baseSpec.replace(/\.(?:[mc]?[jt]sx?)$/i, '') : baseSpec;
6772
- const candidates = Array.from(new Set([
6773
- baseSpec,
6774
- baseNoExt,
6775
- baseNoExt + '.ts',
6776
- baseNoExt + '.tsx',
6777
- baseNoExt + '.js',
6778
- baseNoExt + '.jsx',
6779
- baseNoExt + '.mjs',
6780
- baseNoExt + '.mts',
6781
- baseNoExt + '.cts',
6782
- file,
6783
- ]));
7066
+ const candidates = Array.from(new Set([baseSpec, baseNoExt, baseNoExt + '.ts', baseNoExt + '.tsx', baseNoExt + '.js', baseNoExt + '.jsx', baseNoExt + '.mjs', baseNoExt + '.mts', baseNoExt + '.cts', file]));
6784
7067
  let freshCode = '';
6785
7068
  for (const cand of candidates) {
6786
7069
  try {