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

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.
@@ -3389,28 +3389,87 @@ function createHmrWebSocketPlugin(opts) {
3389
3389
  console.log('[hmr-ws][solid] forced createMemo path in createProxy for NativeScript HMR');
3390
3390
  }
3391
3391
  }
3392
+ // Patch 2b (NS HMR escape hatch): expose `source` on the proxy
3393
+ // itself via a non-enumerable, well-known symbol-ish key. This lets
3394
+ // our HMR remount path bypass solid-refresh's proxy chain and call
3395
+ // the freshly-patched underlying Home function directly. solid-refresh's
3396
+ // proxy wraps everything in nested createMemo's, which under
3397
+ // universal-renderer + nested-context (TanStack RouterContextProvider)
3398
+ // causes accumulating zombie memos that all subscribe to M_initial.source
3399
+ // — the visible symptom is "every other save applies".
3400
+ //
3401
+ // With this hatch we can do `proxy.__$$ns_resolve()` to obtain the
3402
+ // current underlying component (e.g. M_initial.proxy after first save,
3403
+ // or the actual Home function on the deepest hop) and then call
3404
+ // untrack(() => extract until we reach the actual function), then mount
3405
+ // against THAT function — no HMRComp memo proxy chain at the page level.
3406
+ // NS HMR escape hatch: stash `source` on the HMRComp function
3407
+ // itself AND short-circuit the Proxy's `get` handler so that
3408
+ // `proxy.__$ns_resolveSource` returns our exposed function.
3409
+ // Without the get-handler short-circuit, accessing the property
3410
+ // on the Proxy goes through `return source()[property]` — which
3411
+ // asks the *current source value* (which may itself be another
3412
+ // solid-refresh proxy or the underlying user function) for the
3413
+ // property. The user function doesn't have `__$ns_resolveSource`,
3414
+ // and a chained proxy would re-enter its own get handler. Either
3415
+ // way we end up with `undefined` at the page-level remount and
3416
+ // can't unwrap.
3417
+ //
3418
+ // NOTE: `$$` in String.prototype.replace replacement is treated
3419
+ // as a literal `$`. We use a single `$` to avoid that footgun.
3420
+ // Match only the unique opening fragment to avoid getting tripped up
3421
+ // by whitespace differences after the AST normalizer ran.
3422
+ const newProxyMarker = `if (property === 'location' || property === 'name') {`;
3423
+ if (patchedCode.includes(newProxyMarker)) {
3424
+ // 1. Inject `__$ns_resolveSource` as a property on the HMRComp
3425
+ // function itself (so its closure captures `source`).
3426
+ // CRITICAL: assign at module-eval time (right after HMRComp
3427
+ // is defined / before `return new Proxy(...)`), NOT inside
3428
+ // the HMRComp body — the body only runs when the proxy is
3429
+ // called, so before first call the property is undefined
3430
+ // and the page-level remount unwrap finds nothing.
3431
+ const setupMarker = `setComponentProperty(HMRComp, 'name', refreshName);`;
3432
+ patchedCode = patchedCode.replace(setupMarker, `HMRComp.__$ns_resolveSource = function() { return source(); }; ${setupMarker}`);
3433
+ // 2. Make the Proxy `get` handler short-circuit our property
3434
+ // so callers can do `proxy.__$ns_resolveSource()` without
3435
+ // going through `source()[property]` (which would unwrap one
3436
+ // hop early or reach the user function which doesn't have it).
3437
+ patchedCode = patchedCode.replace(newProxyMarker, `if (property === '__$ns_resolveSource') { return HMRComp.__$ns_resolveSource; } ${newProxyMarker}`);
3438
+ if (verbose) {
3439
+ console.log('[hmr-ws][solid] exposed __$ns_resolveSource on createProxy for NS HMR escape hatch');
3440
+ }
3441
+ }
3392
3442
  // Patch 3: Inline patchRegistry call so updates apply immediately
3393
3443
  // on module re-evaluation (accept callbacks are not invoked by the HMR client).
3394
- // The injected `console.log` helpers run inside the user's runtime
3395
- // when @solid-refresh re-evaluates a module, so they are a runtime
3396
- // concern (stripped if the user disables the patch). Keeping them
3397
- // behind the patch sentinel rather than the dev-server `verbose`
3398
- // flag is intentional the patch only runs when Solid HMR fires.
3444
+ //
3445
+ // The injected diagnostic logs are gated on
3446
+ // `globalThis.__NS_ENV_VERBOSE__` so they're silent in
3447
+ // normal use but resurface immediately when the user
3448
+ // re-runs with verbose logging enabled. The flag is
3449
+ // seeded by `mainEntryPlugin` from the same `verbose`
3450
+ // option that drives this server-side log gating.
3451
+ // Without these the visible HMR signal is just "did it
3452
+ // apply" — with them, devs can answer "did `hot.data`
3453
+ // persist", "did `patchRegistry` actually swap the
3454
+ // proxy's signal source", and "did the registry
3455
+ // component count change" without reaching for an
3456
+ // inspector.
3399
3457
  const marker = 'hot.data[SOLID_REFRESH] = hot.data[SOLID_REFRESH] || registry;';
3400
3458
  if (patchedCode.includes(marker)) {
3401
3459
  const patchCode = [
3402
- `console.log('[solid-refresh][$$refreshESM] hot.data keys=', hot.data ? Object.keys(hot.data) : 'no-data', 'has=', !!(hot.data && hot.data[SOLID_REFRESH]));`,
3460
+ `var __nsRefreshVerbose = (typeof globalThis !== 'undefined') && !!globalThis.__NS_ENV_VERBOSE__;`,
3461
+ `if (__nsRefreshVerbose) console.log('[ns-hmr][solid-refresh][$$refreshESM] hot.data has SOLID_REFRESH=', !!(hot.data && hot.data[SOLID_REFRESH]), 'registry components=', registry.components ? registry.components.size : 0);`,
3403
3462
  `if (hot.data[SOLID_REFRESH]) {`,
3404
- ` console.log('[solid-refresh][$$refreshESM] patching: oldComponents=', hot.data[SOLID_REFRESH].components ? hot.data[SOLID_REFRESH].components.size : 0, 'newComponents=', registry.components ? registry.components.size : 0);`,
3405
- ` var _shouldInvalidate = patchRegistry(hot.data[SOLID_REFRESH], registry);`,
3406
- ` console.log('[solid-refresh][$$refreshESM] patchRegistry result: shouldInvalidate=', _shouldInvalidate);`,
3463
+ ` var __nsOldComponents = hot.data[SOLID_REFRESH].components ? hot.data[SOLID_REFRESH].components.size : 0;`,
3464
+ ` var __nsShouldInvalidate = patchRegistry(hot.data[SOLID_REFRESH], registry);`,
3465
+ ` if (__nsRefreshVerbose) console.log('[ns-hmr][solid-refresh][$$refreshESM] patched: oldComponents=', __nsOldComponents, 'newComponents=', registry.components ? registry.components.size : 0, 'shouldInvalidate=', __nsShouldInvalidate);`,
3407
3466
  `} else {`,
3408
- ` console.log('[solid-refresh][$$refreshESM] first load — creating registry, components=', registry.components ? registry.components.size : 0);`,
3467
+ ` if (__nsRefreshVerbose) console.log('[ns-hmr][solid-refresh][$$refreshESM] first load — no prior registry to patch');`,
3409
3468
  `}`,
3410
3469
  ].join('\n ');
3411
3470
  patchedCode = patchedCode.replace(marker, `${patchCode}\n ${marker}`);
3412
3471
  if (verbose) {
3413
- console.log('[hmr-ws][solid] added inline patchRegistry for NativeScript HMR');
3472
+ console.log('[hmr-ws][solid] added inline patchRegistry (with diagnostics) for NativeScript HMR');
3414
3473
  }
3415
3474
  }
3416
3475
  // Work on a copy to avoid mutating Vite's cached TransformResult
@@ -6179,7 +6238,21 @@ export const piniaSymbol = p.piniaSymbol;
6179
6238
  const code = transformed?.code || '';
6180
6239
  upsertGraphModule((mod.id || '').replace(/\?.*$/, ''), code, deps, {
6181
6240
  emitDeltaOnInsert: true,
6182
- broadcastDelta: ACTIVE_STRATEGY.flavor !== 'angular',
6241
+ // Defer the delta broadcast until AFTER the framework
6242
+ // hot-update handler has had a chance to invalidate the
6243
+ // shared transform-request cache + Vite's moduleGraph
6244
+ // for the changed file and its transitive importers.
6245
+ // Otherwise the client races: it receives the delta
6246
+ // (eviction + re-import via tagged URL) before the
6247
+ // server has purged its caches, and the re-import is
6248
+ // served from cache → V8 evaluates the previous save's
6249
+ // transformed code → patchRegistry runs against an
6250
+ // unchanged source → the visible page is "one save
6251
+ // behind". Angular has always taken this path; Solid
6252
+ // needs the same contract because Solid HMR depends
6253
+ // on the client re-fetching the just-changed module
6254
+ // to drive `solid-refresh.patchRegistry`.
6255
+ broadcastDelta: ACTIVE_STRATEGY.flavor !== 'angular' && ACTIVE_STRATEGY.flavor !== 'solid',
6183
6256
  });
6184
6257
  }
6185
6258
  catch (error) {
@@ -6563,6 +6636,190 @@ export const piniaSymbol = p.piniaSymbol;
6563
6636
  const gm = graph.get(normalizedId);
6564
6637
  console.log('[hmr-ws][solid] delta module', { id: gm?.id, hash: gm?.hash });
6565
6638
  }
6639
+ // Purge the shared transform-request cache AND Vite's own
6640
+ // moduleGraph transformResult cache for the changed file
6641
+ // AND every transitive importer.
6642
+ //
6643
+ // Why this matters for Solid HMR specifically:
6644
+ // - The HMR client evicts V8's module cache for the
6645
+ // canonical /ns/m/<path> URL and re-imports the module.
6646
+ // - The dev server resolves /ns/m/* by calling
6647
+ // `sharedTransformRequest(...)`, which has a 60s TTL on
6648
+ // transform results to amortize cost across HMR
6649
+ // cycles. The shared cache wraps `server.transformRequest`,
6650
+ // which itself caches the compiled output on each
6651
+ // `ModuleNode.transformResult`. Both layers must be
6652
+ // invalidated, or the re-import resolves to whatever
6653
+ // the previous save populated.
6654
+ // - Without invalidation at *both* layers, the second
6655
+ // save of a file within the cache window returns the
6656
+ // FIRST save's transform — V8 evaluates stale code,
6657
+ // `solid-refresh.patchRegistry` runs against an
6658
+ // unchanged source body, and the visible page picks
6659
+ // up the previous save's edit instead of the current
6660
+ // one (the "one-save-behind" symptom users reported).
6661
+ //
6662
+ // Critically, transitive importers must also be invalidated
6663
+ // because TanStack file-based routing (and similar frameworks)
6664
+ // use route files that statically import their components.
6665
+ // When `home.tsx` changes, `routes/index.tsx`'s transform
6666
+ // output references the imported home module identity. Even
6667
+ // though the route file's source bytes did not change, its
6668
+ // *resolved* import target has — and its cached transform
6669
+ // might still encode the previous resolution. Forcing a
6670
+ // fresh transform of the importer guarantees the route
6671
+ // file's `import Home from ...` re-resolves against the
6672
+ // freshly evaluated home module on V8 side.
6673
+ //
6674
+ // The Angular path performs the equivalent purge via
6675
+ // `collectAngularTransformCacheInvalidationUrls` /
6676
+ // `sharedTransformRequest.invalidateMany`. We replicate
6677
+ // that contract for Solid here. The transitive walk is
6678
+ // bounded the same way (max depth 16, node_modules /
6679
+ // virtual ids excluded) so vendor packages stay hot.
6680
+ try {
6681
+ const projectRoot = server.config.root || process.cwd();
6682
+ const cacheInvalidationUrls = new Set();
6683
+ const addCacheKey = (rawId) => {
6684
+ const id = String(rawId || '');
6685
+ if (!id)
6686
+ return;
6687
+ const cacheKey = canonicalizeTransformRequestCacheKey(id, projectRoot);
6688
+ cacheInvalidationUrls.add(cacheKey);
6689
+ const noQuery = cacheKey.replace(/\?.*$/, '');
6690
+ const stripped = noQuery.replace(/\.(?:[mc]?[jt]sx?)$/i, '');
6691
+ if (stripped !== noQuery) {
6692
+ cacheInvalidationUrls.add(stripped);
6693
+ }
6694
+ };
6695
+ addCacheKey(file);
6696
+ const rootModules = server.moduleGraph.getModulesByFile?.(file);
6697
+ const transitiveImporters = collectAngularTransitiveImportersForInvalidation({
6698
+ modules: rootModules ? Array.from(rootModules) : [],
6699
+ isExcluded: (id) => id.includes('/node_modules/') || isRuntimeGraphExcludedPath(id),
6700
+ maxDepth: 16,
6701
+ });
6702
+ // Invalidate Vite's moduleGraph for the changed file +
6703
+ // every transitive importer so `server.transformRequest`
6704
+ // re-runs the transform pipeline instead of returning
6705
+ // the cached `ModuleNode.transformResult`. We call
6706
+ // `onFileChange` (Vite's authoritative file-changed
6707
+ // signal — walks all module variants including `?v=`,
6708
+ // `?import`, `?t=`) AND per-module `invalidateModule`
6709
+ // for transitive importers (which onFileChange
6710
+ // doesn't reach).
6711
+ try {
6712
+ server.moduleGraph.onFileChange(file);
6713
+ }
6714
+ catch { }
6715
+ if (rootModules) {
6716
+ for (const mod of rootModules) {
6717
+ try {
6718
+ server.moduleGraph.invalidateModule(mod);
6719
+ }
6720
+ catch { }
6721
+ }
6722
+ }
6723
+ for (const mod of transitiveImporters) {
6724
+ addCacheKey(mod?.id);
6725
+ try {
6726
+ server.moduleGraph.invalidateModule(mod);
6727
+ }
6728
+ catch { }
6729
+ }
6730
+ if (cacheInvalidationUrls.size && sharedTransformRequest) {
6731
+ sharedTransformRequest.invalidateMany(cacheInvalidationUrls);
6732
+ if (verbose) {
6733
+ console.log('[hmr-ws][solid] purged shared transform cache entries:', cacheInvalidationUrls.size, 'transitiveImporters=', transitiveImporters.length);
6734
+ }
6735
+ }
6736
+ // Sledgehammer: nuke EVERY entry in sharedTransformRequest's
6737
+ // result cache. The targeted `invalidateMany` above only
6738
+ // clears keys we know about. The `/ns/m/` handler iterates
6739
+ // a long list of candidate extensions (`.ts`, `.js`, `.tsx`,
6740
+ // `.jsx`, `.mjs`, `.mts`, `.cts`, `.vue`, `index.*`) and
6741
+ // EACH candidate is a separate cache key. If a previous
6742
+ // serve populated cache for `/src/components/home.js` (via
6743
+ // extension fallback that resolves to `home.tsx`), our
6744
+ // targeted invalidate misses it and iOS HITs the stale
6745
+ // entry — serving the previous save's transformed code.
6746
+ try {
6747
+ sharedTransformRequest.clear();
6748
+ }
6749
+ catch { }
6750
+ }
6751
+ catch (e) {
6752
+ if (verbose)
6753
+ console.warn('[hmr-ws][solid] transform cache invalidation failed', e);
6754
+ }
6755
+ // Re-run the transform AFTER all caches are invalidated, then
6756
+ // re-upsert the graph so the broadcast hash matches the freshly-
6757
+ // transformed content. The common upsert block above ran
6758
+ // `server.transformRequest` BEFORE invalidation — at that
6759
+ // moment Vite's auto-invalidate hadn't fired yet (it runs after
6760
+ // `plugin.handleHotUpdate`), so the result it cached was the
6761
+ // previous save's. Without this re-transform, the broadcast
6762
+ // carries a stale hash and iOS evaluates the previous save's
6763
+ // bytes ("one save behind").
6764
+ //
6765
+ // We pre-populate the cache for every extension variant Vite's
6766
+ // /ns/m/ handler might try, so the first request from iOS hits
6767
+ // fresh data regardless of which candidate it resolves first.
6768
+ try {
6769
+ const ext = file.match(/\.(?:[mc]?[jt]sx?)$/i)?.[0] || '';
6770
+ const baseSpec = '/' + path.posix.normalize(path.relative(root, file)).split(path.sep).join('/');
6771
+ 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
+ ]));
6784
+ let freshCode = '';
6785
+ for (const cand of candidates) {
6786
+ try {
6787
+ const fresh = await sharedTransformRequest(cand, 30000);
6788
+ if (fresh?.code && !freshCode)
6789
+ freshCode = fresh.code;
6790
+ }
6791
+ catch { }
6792
+ }
6793
+ if (freshCode) {
6794
+ const existingGm = graph.get(normalizedId);
6795
+ const existingDeps = existingGm?.deps || [];
6796
+ upsertGraphModule(normalizedId, freshCode, existingDeps, {
6797
+ broadcastDelta: false,
6798
+ });
6799
+ }
6800
+ }
6801
+ catch (e) {
6802
+ if (verbose)
6803
+ console.warn('[hmr-ws][solid] post-invalidation re-transform failed', e);
6804
+ }
6805
+ // Broadcast the (now-fresh) delta. Suppressing this in the
6806
+ // common upsert block (`broadcastDelta: ACTIVE_STRATEGY.flavor
6807
+ // !== 'solid'`) and emitting it here ensures the client's
6808
+ // eviction + re-import doesn't race the server's cache
6809
+ // invalidation.
6810
+ try {
6811
+ const gm = graph.get(normalizedId);
6812
+ if (gm) {
6813
+ emitDelta([gm], []);
6814
+ if (verbose) {
6815
+ console.log('[hmr-ws][solid] broadcast delta after cache invalidation', { id: gm.id, hash: gm.hash });
6816
+ }
6817
+ }
6818
+ }
6819
+ catch (e) {
6820
+ if (verbose)
6821
+ console.warn('[hmr-ws][solid] post-invalidation broadcast failed', e);
6822
+ }
6566
6823
  }
6567
6824
  catch (e) {
6568
6825
  if (verbose)