diff --git a/packages/vite/hmr/server/ns-rt-bridge.spec.ts b/packages/vite/hmr/server/ns-rt-bridge.spec.ts index 3d1f834b31..e25c642dfd 100644 --- a/packages/vite/hmr/server/ns-rt-bridge.spec.ts +++ b/packages/vite/hmr/server/ns-rt-bridge.spec.ts @@ -212,4 +212,20 @@ describe('/ns/rt bridge builder', () => { const names = discoverNsvBridgeExports('/tmp/__no_such_project__'); expect(names.size).toBe(0); }); + + it('does not cache a registry miss in __ensure() and warns once', () => { + // The bridge can be evaluated before the vendor module is registered + // (a served module importing /ns/rt); caching `{}` then leaves every + // binding undefined for the life of the session. + const code = buildNsRtBridgeModule({ rtVer: '1', requireGuardSnippet: '', vendorExports: ['defineComponent'] }); + const ensure = code.slice(code.indexOf('function __ensure(){'), code.indexOf('export const __realm')); + expect(ensure).toContain('if (!vm) {'); + expect(ensure).toMatch(/if \(!vm\) \{[^\n]*return \{\};/); + expect(ensure).toContain('__NS_RT_MISS_WARNED__'); + expect(ensure).toContain("console.warn('[ns-rt] nativescript-vue is not registered"); + const missIndex = ensure.indexOf('if (!vm) {'); + const cacheIndex = ensure.indexOf('__cached_rt = rt;'); + expect(missIndex).toBeGreaterThan(-1); + expect(cacheIndex).toBeGreaterThan(missIndex); + }); }); diff --git a/packages/vite/hmr/server/ns-rt-bridge.ts b/packages/vite/hmr/server/ns-rt-bridge.ts index 0383d42272..ad77b064c2 100644 --- a/packages/vite/hmr/server/ns-rt-bridge.ts +++ b/packages/vite/hmr/server/ns-rt-bridge.ts @@ -91,7 +91,9 @@ export function buildNsRtBridgeModule(options: NsRtBridgeOptions): string { ` let vm = null;\n` + ` try { vm = reg && reg.has && reg.has('nativescript-vue') ? reg.get('nativescript-vue') : (typeof req==='function' ? req('nativescript-vue') : null); } catch {}\n` + ` if (!vm) { try { vm = reg && reg.has && reg.has('vue') ? reg.get('vue') : (typeof req==='function' ? req('vue') : null); } catch {} }\n` + - ` const rt = (vm && (vm.default ?? vm)) || {};\n` + + // A miss is not cached so a later call can pick up the registration + ` if (!vm) { if (!g.__NS_RT_MISS_WARNED__) { g.__NS_RT_MISS_WARNED__ = true; console.warn('[ns-rt] nativescript-vue is not registered in the vendor registry yet; bindings read from /ns/rt now are undefined'); } return {}; }\n` + + ` const rt = vm.default ?? vm;\n` + ` __cached_vm = vm;\n` + ` __cached_rt = rt;\n` + ` return rt;\n` + diff --git a/packages/vite/hmr/server/websocket-ns-m.ts b/packages/vite/hmr/server/websocket-ns-m.ts index 704cd7e3f9..016938bdaa 100644 --- a/packages/vite/hmr/server/websocket-ns-m.ts +++ b/packages/vite/hmr/server/websocket-ns-m.ts @@ -11,7 +11,7 @@ import { collapseLegacyNsMTags } from './websocket-ns-m-paths.js'; import { createNsMRequestContext, resolveNsMTransformedModule } from './websocket-ns-m-request.js'; import { setDeviceModuleHeaders } from './route-helpers.js'; import { CSS_MODULE_RE, buildCssRegisterSnippetFromVar, normalizeCssForDevice } from './css-device-module.js'; -import { assertNoOptimizedArtifacts, buildBootProgressSnippet, canonicalizeRtImports, classifyServedModule, dedupeRtNamedImportsAgainstDestructures, deduplicateLinkerImports, ensureDestructureCoreImports, ensureGuardPlainDynamicImports, ensureVariableDynamicImportHelper, expandStarExports, hoistTopLevelStaticImports, MODULE_IMPORT_ANALYSIS_PLUGINS, wrapCommonJsModuleForDevice, ensureWorkerEntryGlobalsImport } from './websocket-served-module-helpers.js'; +import { assertNoOptimizedArtifacts, buildBootProgressSnippet, canonicalizeRtImports, dedupeRtNamedImportsAgainstDestructures, deduplicateLinkerImports, ensureDestructureCoreImports, ensureGuardPlainDynamicImports, ensureVariableDynamicImportHelper, expandStarExports, hoistTopLevelStaticImports, MODULE_IMPORT_ANALYSIS_PLUGINS, wrapCommonJsModuleForDevice, ensureWorkerEntryGlobalsImport, classifyServedRequest } from './websocket-served-module-helpers.js'; import { cleanCode, collectImportDependencies, isWorkerEntryModuleId, processCodeForDevice, rewriteImports } from './websocket-device-transform.js'; import { REQUIRE_GUARD_SNIPPET } from './require-guard.js'; import { getServerOrigin } from './server-origin.js'; @@ -360,7 +360,7 @@ export function registerNsModuleServerRoute(server: ViteDevServer, options: Regi // must skip the app-source passes inside processCodeForDevice (AST // normalization, /ns/rt helper-alias injection). One classification // point — see classifyServedModule for the full case list. - const isNodeMod = classifyServedModule(resolvedCandidate || spec) === 'library'; + const isNodeMod = classifyServedRequest(spec, resolvedCandidate) === 'library'; code = processCodeForDevice(code, false, true, isNodeMod, resolvedCandidate || spec, isWorkerRealmRequest ? { workerRealm: true } : undefined); // import.meta.hot is JS-owned: cleanCode() strips Vite's browser // __vite__createHotContext assignment and processCodeForDevice diff --git a/packages/vite/hmr/server/websocket-served-module-helpers.spec.ts b/packages/vite/hmr/server/websocket-served-module-helpers.spec.ts new file mode 100644 index 0000000000..5820956b9e --- /dev/null +++ b/packages/vite/hmr/server/websocket-served-module-helpers.spec.ts @@ -0,0 +1,26 @@ +import { describe, expect, it } from 'vitest'; + +import { classifyServedModule, classifyServedRequest } from './websocket-served-module-helpers.js'; + +describe('classifyServedRequest', () => { + it('treats a package request as library code when its resolved id is a symlink target outside node_modules', () => { + // `file:` / `npm link` packages resolve to their real path, which has no + // node_modules segment; the request spec still identifies a vendor package. + expect(classifyServedRequest('/node_modules/nativescript-vue', '/Users/dev/nativescript-vue/dist/index.js')).toBe('library'); + expect(classifyServedRequest('/node_modules/nativescript-vue/dist/renderer/index.js', '/Users/dev/nativescript-vue/dist/renderer/index.js')).toBe('library'); + }); + + it('keeps library classification when only the resolved id is under node_modules', () => { + expect(classifyServedRequest('/src/app.ts', '/proj/node_modules/some-pkg/index.js')).toBe('library'); + }); + + it('classifies app sources as app', () => { + expect(classifyServedRequest('/src/components/Home.vue', '/proj/src/components/Home.vue')).toBe('app'); + expect(classifyServedRequest('/src/app.ts', null)).toBe('app'); + }); + + it('falls back to the spec when nothing resolved', () => { + expect(classifyServedRequest('/node_modules/pkg/index.js', null)).toBe('library'); + expect(classifyServedModule('/node_modules/pkg/index.js')).toBe('library'); + }); +}); diff --git a/packages/vite/hmr/server/websocket-served-module-helpers.ts b/packages/vite/hmr/server/websocket-served-module-helpers.ts index 6665968444..36e10e2c89 100644 --- a/packages/vite/hmr/server/websocket-served-module-helpers.ts +++ b/packages/vite/hmr/server/websocket-served-module-helpers.ts @@ -93,6 +93,17 @@ export function classifyServedModule(p: string | undefined | null): ServedModule return 'app'; } +/** + * Classifies a served request from both its spec and its resolved id. + * @param spec Request spec, such as `/node_modules/pkg`. + * @param resolvedId Resolved module id, if any. + */ +export function classifyServedRequest(spec: string | undefined | null, resolvedId: string | undefined | null): ServedModuleKind { + // A symlinked package resolves to a real path outside node_modules + if (classifyServedModule(resolvedId) === 'library') return 'library'; + return classifyServedModule(spec); +} + export const MODULE_IMPORT_ANALYSIS_PLUGINS = ['typescript', 'jsx', 'importMeta', 'topLevelAwait', 'classProperties', 'classPrivateProperties', 'classPrivateMethods', 'decorators-legacy'] as any; export type TopLevelImportRecord = {