FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

fix(vite): hmr propagation improvements on css, modals, scripts · NativeScript/NativeScript@86320ee · GitHub

Repository navigation

Commit 86320ee

Browse files
committed
fix(vite): hmr propagation improvements on css, modals, scripts
[skip ci]
1 parent 02b10f9 commit 86320ee

14 files changed

Lines changed: 690 additions & 46 deletions

‎packages/vite/hmr/client/css-handler.ts‎

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { getCore } from './utils.js';
22
import { getGlobalScope } from '../shared/runtime/global-scope.js';
3+
import { restyleRootAndModals } from '../shared/runtime/restyle-roots.js';
34

45
const VERBOSE = !!getGlobalScope().__NS_ENV_VERBOSE__;
56

@@ -79,16 +80,10 @@ export function applyCssText(cssText: string, tag: string = APP_CSS_TAG): void {
7980
if (VERBOSE) console.info('[ns-hmr] CSS applied via addCss (additive fallback)');
8081
}
8182
// NS caches computed styles — re-trigger styling on the root
82-
// (propagates to descendants via `eachDescendant`).
83+
// (propagates to descendants via `eachDescendant`) and on every
84+
// presented modal, which is a root of its own.
8385
try {
84-
const rootView = Application?.getRootView?.();
85-
if (rootView && typeof rootView._onCssStateChange === 'function') {
86-
rootView._onCssStateChange();
87-
} else if (rootView) {
88-
const cls = rootView.className || '';
89-
rootView.className = cls + ' ';
90-
rootView.className = cls;
91-
}
86+
restyleRootAndModals(Application?.getRootView?.());
9287
} catch {}
9388
} catch (e) {
9489
console.warn('[ns-hmr] CSS apply failed:', e?.message || String(e));

‎packages/vite/hmr/entry-runtime.ts‎

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { getGlobalScope } from './shared/runtime/global-scope.js';
2+
import { restyleRootAndModals } from './shared/runtime/restyle-roots.js';
23
import { markDevBootComplete } from './shared/runtime/boot-complete.js';
34
import { readNsRuntimeDevHostApi } from './shared/runtime/browser-runtime-contract.js';
45
import { installWorkerConstructorTracking } from './shared/runtime/worker-tracking.js';
@@ -156,16 +157,7 @@ export function installHttpCoreCssSupport(coreModule: any, verbose?: boolean): H
156157
const seenRoots = new Set<any>();
157158
for (const candidate of candidates) {
158159
try {
159-
const rootView = candidate?.getRootView?.();
160-
if (!rootView || seenRoots.has(rootView)) continue;
161-
seenRoots.add(rootView);
162-
if (typeof rootView._onCssStateChange === 'function') {
163-
rootView._onCssStateChange();
164-
} else {
165-
const cls = rootView.className || '';
166-
rootView.className = cls + ' ';
167-
rootView.className = cls;
168-
}
160+
restyleRootAndModals(candidate?.getRootView?.(), seenRoots);
169161
} catch {}
170162
}
171163
};

‎packages/vite/hmr/frameworks/vue/client/index.ts‎

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { getCore, getCurrentApp, graph, invalidateModulesByUrls, normalizeSpec, resolveHmrHttpOrigin, safeDynImport, safeReadDefault, setCurrentApp } from '../../../client/utils.js';
2+
import { APPLIED_IN_PLACE } from './vue-sfc-update-overlay.js';
23
import { getGlobalScope } from '../../../shared/runtime/global-scope.js';
34
import { resolveVendorModule } from '../../../shared/runtime/vendor-resolve.js';
45
import { findSfcAncestors } from './dep-propagation.js';
@@ -517,27 +518,43 @@ function openHmrReplaceNavWindow(): () => void {
517518

518519
/**
519520
* In-place Vue HMR. The assembled SFC stamps a stable `comp.__hmrId`, so Vue's
520-
* registerHMR has a record for every mounted instance. Calling the real
521-
* `__VUE_HMR_RUNTIME__.reload(id, newComp)` re-renders just those instances in
522-
* place — preserving the App.vue shell (drawer, nav, router), current route, and
523-
* scroll — instead of a whole-tree resetRootView. Returns false (→ resetRoot
524-
* fallback) when the real runtime is absent (stub/prod build), the component has
525-
* no id, or reload throws. A reload that matches no live instances is a no-op
526-
* (component not currently displayed); the caller treats that as handled.
521+
* registerHMR has a record for every mounted instance, and the runtime can
522+
* patch exactly those instances instead of replacing the whole root view.
523+
*
524+
* Which runtime entry point matters, and it is not a performance question.
525+
* `reload` re-instantiates; for an instance with no parent Vue routes that to
526+
* `appContext.reload()`, which tears down and rebuilds the hosted view. Every
527+
* page pushed with `$navigateTo` and every sheet shown with `$showModal` is
528+
* such a root, so a `reload` there costs a re-navigation at best and a
529+
* dismissed sheet at worst. `rerender` swaps the render function on each live
530+
* instance and updates it where it stands — parented or not.
531+
*
532+
* So when the server reports the save left every script block untouched
533+
* (`rerenderOnly`), take `rerender`: the edit lands inside pushed pages and
534+
* presented modals with nothing remounted. Anything that could have changed
535+
* behavior still needs `reload`.
536+
*
537+
* Returns false (→ resetRoot fallback) when the real runtime is absent
538+
* (stub/prod build), the component has no id, or the call throws. An update
539+
* that matches no live instances is a no-op (component not currently
540+
* displayed); the caller treats that as handled.
527541
*/
528-
function tryInPlaceVueReload(comp: any): boolean {
542+
function tryInPlaceVueReload(comp: any, rerenderOnly = false): boolean {
529543
try {
530544
const rt: any = (getGlobalScope() as any).__VUE_HMR_RUNTIME__;
531545
const id = comp && comp.__hmrId;
532-
if (!rt || typeof rt.reload !== 'function' || !id) return false;
546+
if (!rt || !id) return false;
547+
const canRerender = rerenderOnly && typeof rt.rerender === 'function' && typeof comp.render === 'function';
548+
if (!canRerender && typeof rt.reload !== 'function') return false;
533549
// Vue queues the remount on its scheduler (microtask flush) — the
534550
// Frame navigation happens inside that flush, not synchronously in
535551
// reload(). Hold the replace-navigation window through the flush:
536552
// nextTick resolves after the job queue drains; the timeout is a
537553
// safety net when nextTick is unavailable.
538554
const closeWindow = openHmrReplaceNavWindow();
539555
try {
540-
rt.reload(id, comp);
556+
if (canRerender) rt.rerender(id, comp.render);
557+
else rt.reload(id, comp);
541558
} catch (e) {
542559
closeWindow();
543560
throw e;
@@ -549,10 +566,10 @@ function tryInPlaceVueReload(comp: any): boolean {
549566
}
550567
} catch {}
551568
setTimeout(closeWindow, 500);
552-
if (__NS_ENV_VERBOSE__) console.log('[hmr][vue] in-place reload', id);
569+
if (__NS_ENV_VERBOSE__) console.log(`[hmr][vue] in-place ${canRerender ? 'rerender' : 'reload'}`, id);
553570
return true;
554571
} catch (e) {
555-
if (__NS_ENV_VERBOSE__) console.warn('[hmr][vue] in-place reload failed; resetRoot fallback', e);
572+
if (__NS_ENV_VERBOSE__) console.warn('[hmr][vue] in-place update failed; resetRoot fallback', e);
556573
return false;
557574
}
558575
}
@@ -576,8 +593,8 @@ export async function handleVueSfcRegistryUpdate(msg: any, graphVersion: number)
576593
// Preferred path: patch the changed component's mounted instances in
577594
// place (App.vue shell, route, scroll all survive). Returning null tells
578595
// the overlay no resetRootView is needed.
579-
if (tryInPlaceVueReload(comp)) {
580-
return null;
596+
if (tryInPlaceVueReload(comp, msg?.rerenderOnly === true)) {
597+
return APPLIED_IN_PLACE;
581598
}
582599
// Fallback (real Vue HMR runtime unavailable): a child SFC with required
583600
// props cannot be a standalone root — mounting it bare crashes (missing

‎packages/vite/hmr/frameworks/vue/client/navigate-app.spec.ts‎

Lines changed: 101 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { describe, expect, it } from 'vitest';
1+
import { describe, expect, it, vi } from 'vitest';
2+
import { installNavigatedPageHmrReload } from './navigate-app';
23
import { readFileSync } from 'fs';
34
import path from 'path';
45
import { fileURLToPath } from 'url';
@@ -52,3 +53,102 @@ describe('__nsNavigateUsingApp prop forwarding', () => {
5253
expect(navigateSrc).toMatch(/AppFactory\(normalizeComponent\(comp,/);
5354
});
5455
});
56+
57+
describe('installNavigatedPageHmrReload', () => {
58+
function makeFrame(currentPage: any) {
59+
const onceHandlers: Record<string, Array<() => void>> = {};
60+
return {
61+
currentPage,
62+
replacePage: vi.fn(),
63+
once: vi.fn((event: string, cb: () => void) => {
64+
(onceHandlers[event] ||= []).push(cb);
65+
}),
66+
fire(event: string) {
67+
const list = onceHandlers[event] || [];
68+
onceHandlers[event] = [];
69+
list.forEach((cb) => cb());
70+
},
71+
};
72+
}
73+
74+
function makePage(frame: any) {
75+
const onceHandlers: Array<() => void> = [];
76+
return {
77+
frame,
78+
once: vi.fn((_event: string, cb: () => void) => {
79+
onceHandlers.push(cb);
80+
}),
81+
fireNavigatedTo() {
82+
const list = onceHandlers.splice(0);
83+
list.forEach((cb) => cb());
84+
},
85+
};
86+
}
87+
88+
it("overwrites the app context's reload (Vue's DEV default renders into the detached NSVRoot)", () => {
89+
const ctx: Record<string, any> = { reload: () => 'vue-default' };
90+
const app = { _context: ctx, unmount: vi.fn() };
91+
const page = makePage(makeFrame(null));
92+
expect(installNavigatedPageHmrReload({ app, page, rebuild: () => ({}) })).toBe(true);
93+
expect(typeof ctx.reload).toBe('function');
94+
expect(ctx.reload()).not.toBe('vue-default');
95+
});
96+
97+
it('replaces the current entry with a rebuilt page and releases the old app after the swap', () => {
98+
const ctx: Record<string, any> = {};
99+
const app = { _context: ctx, unmount: vi.fn() };
100+
const frame = makeFrame(null);
101+
const page = makePage(frame);
102+
frame.currentPage = page;
103+
const freshPage = { fresh: true };
104+
installNavigatedPageHmrReload({ app, page, rebuild: () => freshPage });
105+
106+
ctx.reload();
107+
108+
expect(frame.replacePage).toHaveBeenCalledTimes(1);
109+
const entry = frame.replacePage.mock.calls[0][0];
110+
expect(entry.animated).toBe(false);
111+
expect(entry.create()).toBe(freshPage);
112+
// old app released only once the fresh page is actually in place
113+
expect(app.unmount).not.toHaveBeenCalled();
114+
frame.fire('navigatedTo');
115+
expect(app.unmount).toHaveBeenCalledTimes(1);
116+
});
117+
118+
it('stands down for a page with no frame (already replaced or disposed)', () => {
119+
const ctx: Record<string, any> = {};
120+
const app = { _context: ctx, unmount: vi.fn() };
121+
const rebuild = vi.fn();
122+
const page = makePage(null);
123+
installNavigatedPageHmrReload({ app, page, rebuild });
124+
125+
ctx.reload();
126+
127+
expect(rebuild).not.toHaveBeenCalled();
128+
expect(page.once).not.toHaveBeenCalled();
129+
});
130+
131+
it('defers a backstack page to its next navigatedTo, coalescing repeated reloads', () => {
132+
const ctx: Record<string, any> = {};
133+
const app = { _context: ctx, unmount: vi.fn() };
134+
const frame = makeFrame({ someOtherPage: true });
135+
const page = makePage(frame);
136+
const freshPage = { fresh: true };
137+
installNavigatedPageHmrReload({ app, page, rebuild: () => freshPage });
138+
139+
ctx.reload();
140+
ctx.reload();
141+
ctx.reload();
142+
expect(page.once).toHaveBeenCalledTimes(1);
143+
expect(frame.replacePage).not.toHaveBeenCalled();
144+
145+
// the user comes back to the page
146+
frame.currentPage = page;
147+
page.fireNavigatedTo();
148+
expect(frame.replacePage).toHaveBeenCalledTimes(1);
149+
});
150+
151+
it('refuses an app with no context', () => {
152+
expect(installNavigatedPageHmrReload({ app: {} as any, page: {}, rebuild: () => ({}) })).toBe(false);
153+
});
154+
});

‎packages/vite/hmr/frameworks/vue/client/navigate-app.ts‎

Lines changed: 74 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,73 @@ export function normalizeComponent(input: any, nameHint?: string): any {
5454
return input;
5555
}
5656

57+
/** The slice of a mounted Vue app the navigated-page reload needs. */
58+
export interface NavigatedPageHmrReloadHandle {
59+
app: { unmount?: () => void; _context?: Record<string, any> };
60+
/** The NativeScript Page hosting the mounted component. */
61+
page: any;
62+
/** Builds a fresh page from the current component definition (and installs a new reload on it). */
63+
rebuild: () => any;
64+
}
65+
66+
/**
67+
* Install `appContext.reload` on a page mounted by `__nsNavigateUsingApp`.
68+
*
69+
* Vue routes an HMR `reload` of a parentless instance — which every navigated
70+
* page mounted through this backend is — to `instance.appContext.reload()`.
71+
* Vue's own DEV default for that hook re-renders into the app's root container,
72+
* but here that container is the detached `NSVRoot` the page was mounted from,
73+
* so the on-screen page never changes. The stock nativescript-vue `$navigateTo`
74+
* installs its `reloadPage` for exactly this reason; this is that contract for
75+
* the HMR navigation backend.
76+
*
77+
* The installed hook rebuilds the destination from the (HMR-mutated) component
78+
* and `replacePage`s it into the frame, so the current entry is swapped in
79+
* place and the backstack is untouched. A page that is not currently shown
80+
* defers to its next `navigatedTo`; a page with no frame at all was already
81+
* replaced or disposed, so its instance is stale and the hook stands down.
82+
*/
83+
export function installNavigatedPageHmrReload({ app, page, rebuild }: NavigatedPageHmrReloadHandle): boolean {
84+
const ctx = app && app._context;
85+
if (!ctx || !page) return false;
86+
let pendingReturn = false;
87+
ctx.reload = () => {
88+
try {
89+
const frame = page.frame;
90+
if (!frame) return;
91+
if (frame.currentPage !== page) {
92+
if (pendingReturn) return;
93+
pendingReturn = true;
94+
try {
95+
page.once('navigatedTo', () => {
96+
pendingReturn = false;
97+
try {
98+
ctx.reload();
99+
} catch {}
100+
});
101+
} catch {
102+
pendingReturn = false;
103+
}
104+
return;
105+
}
106+
frame.replacePage({ create: () => rebuild(), animated: false } as any);
107+
try {
108+
// Release the replaced app once the fresh page is in, so its
109+
// instance leaves Vue's HMR registry — otherwise every later
110+
// reload also runs against the stale instance.
111+
frame.once('navigatedTo', () => {
112+
try {
113+
app.unmount?.();
114+
} catch {}
115+
});
116+
} catch {}
117+
} catch (e) {
118+
console.warn('[app-nav] HMR reload of navigated page failed', e);
119+
}
120+
};
121+
return true;
122+
}
123+
57124
// Deterministic navigation using the current Vue app instance rather than vendor-held rootApp.
58125
function __nsNavigateUsingApp(comp: any, opts: any = {}) {
59126
const g = getGlobalScope();
@@ -112,19 +179,20 @@ function __nsNavigateUsingApp(comp: any, opts: any = {}) {
112179
if (!nativeView) throw new Error('navigation mount did not yield a nativeView');
113180
const P = getCore('Page');
114181
const ctorName = String(nativeView?.constructor?.name || '').replace(/^_+/, '');
115-
if (ctorName === 'Page' || /^Page(\$\d+)?$/.test(ctorName)) {
116-
return nativeView;
117-
}
118-
if (typeof P === 'function') {
182+
let page = nativeView;
183+
if (!(ctorName === 'Page' || /^Page(\$\d+)?$/.test(ctorName)) && typeof P === 'function') {
119184
const pg = new (P as any)();
120185
(pg as any).content = nativeView;
121186
// Hide default ActionBar for wrapped views to avoid double bars
122187
try {
123188
(pg as any).actionBarHidden = true;
124189
} catch {}
125-
return pg;
190+
page = pg;
126191
}
127-
return nativeView; // fallback
192+
try {
193+
installNavigatedPageHmrReload({ app, page, rebuild: buildTarget });
194+
} catch {}
195+
return page;
128196
};
129197
let frame = opts && (opts as any).frame ? (opts as any).frame : getRootFrame();
130198
if (!frame) {

0 commit comments

Comments
 (0)

Back | FazBrowse Home | New Git URL