Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions packages/vite/hmr/server/ns-rt-bridge.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
4 changes: 3 additions & 1 deletion packages/vite/hmr/server/ns-rt-bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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` +
Expand Down
4 changes: 2 additions & 2 deletions packages/vite/hmr/server/websocket-ns-m.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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
Expand Down
26 changes: 26 additions & 0 deletions packages/vite/hmr/server/websocket-served-module-helpers.spec.ts
Original file line number Diff line number Diff line change
@@ -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');
});
});
11 changes: 11 additions & 0 deletions packages/vite/hmr/server/websocket-served-module-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
Loading