⚡ Sangsakala Proxy 🏠 Home
Skip to content
Open
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/websocket-bindings.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,22 @@ describe('ensureNativeScriptModuleBindings — package metadata NativeScript det
expect(text).toMatch(/(?:const|var)\s+SyncStatus\s*=\s*__nsPick\(__nsVendorModule_\d+,\s*['"]SyncStatus['"]\)/);
});

it('keeps preserved imports in source order with re-exports', () => {
// Hoisting only `./html` evaluated it (and its ./web) before ./scroll: TDZ on `Scroll`.
const input = [`export { Scroll } from './scroll';`, `export * from './web';`, `import './html';`, `import { defineStore } from 'pinia';`, `export const s = defineStore;`].join('\n');

for (const out of [ensureNativeScriptModuleBindings(input), ensureNativeScriptModuleBindings(input.replace(/^import \{ defineStore.*$/m, ''))]) {
const scroll = out.indexOf(`export { Scroll } from './scroll';`);
const web = out.indexOf(`export * from './web';`);
const html = out.indexOf(`import './html';`);

expect(scroll).toBeGreaterThanOrEqual(0);
expect(web).toBeGreaterThan(scroll);
expect(html).toBeGreaterThan(web);
expect(out.split(`import './html';`)).toHaveLength(2);
}
});

it('is safe to run twice on the same module text', () => {
const input = `import { defineStore } from "pinia";\nexport const useServices = defineStore('services', () => ({}));\n`;
const once = ensureNativeScriptModuleBindings(input);
Expand Down
65 changes: 42 additions & 23 deletions packages/vite/hmr/server/websocket-module-bindings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,19 @@ function rewriteWorkerReExportSpecifiers(code: string): string {
return code;
}

function stripTopLevelImportRecords(code: string, records: TopLevelImportRecord[]): string {
function collectTopLevelReExportDeclarations(code: string): Array<{ start: number; end: number; text: string }> {
try {
const body = babelParse(code, { sourceType: 'module', plugins: [...MODULE_IMPORT_ANALYSIS_PLUGINS] }).program.body;
return body
.filter((node) => node.type === 'ExportAllDeclaration' || (node.type === 'ExportNamedDeclaration' && !!node.source))
.filter((node) => typeof node.start === 'number' && typeof node.end === 'number')
.map((node) => ({ start: node.start!, end: node.end!, text: code.slice(node.start!, node.end!).trim() }));
} catch {
return [];
}
}

function stripTopLevelImportRecords(code: string, records: Array<{ start: number; end: number }>): string {
let stripped = code;
for (const record of [...records].sort((left, right) => right.start - left.start)) {
stripped = stripped.slice(0, record.start) + stripped.slice(record.end);
Expand All @@ -237,7 +249,7 @@ export function ensureNativeScriptModuleBindings(code: string, options?: EnsureN
return code;
}

const preservedImports: string[] = [];
const preservedImports: Array<{ start: number; text: string }> = [];
const modules = new Map<string, NativeScriptImportBinding>();

const getModuleBinding = (canonical: string) => {
Expand Down Expand Up @@ -278,27 +290,28 @@ export function ensureNativeScriptModuleBindings(code: string, options?: EnsureN
if (!original) {
continue;
}
const preserve = (text: string) => preservedImports.push({ start: record.start, text });
if (record.isTypeOnly) {
preservedImports.push(original);
preserve(original);
continue;
}

const rawSpec = record.source;
const specifier = rawSpec.replace(PAT.QUERY_PATTERN, '');
const workerSpecifier = options?.vendorImportsAsHttp ? getWorkerNodeModulesSpecifier(specifier) : null;
if (workerSpecifier) {
preservedImports.push(rewritePreservedImportSpecifier(original, rawSpec, workerSpecifier));
preserve(rewritePreservedImportSpecifier(original, rawSpec, workerSpecifier));
continue;
}
const preservedSpecifier = getPreservedImportSpecifier(specifier, options);

if (!record.clause) {
if (preservedSpecifier) {
preservedImports.push(rewritePreservedImportSpecifier(original, rawSpec, preservedSpecifier));
preserve(rewritePreservedImportSpecifier(original, rawSpec, preservedSpecifier));
continue;
}
if (isEsmFrameworkPackageSpecifier(specifier)) {
preservedImports.push(original);
preserve(original);
continue;
}
const runtimePluginBareSpecifier = resolveInternalRuntimePluginBareSpecifier(specifier, getProjectRootPath());
Expand All @@ -314,18 +327,18 @@ export function ensureNativeScriptModuleBindings(code: string, options?: EnsureN
// ("does not provide an export named ..."). When vendorImportsAsHttp
// is set (worker serve), fall through to the HTTP rewrite below.
if (options?.preserveNonPluginVendorImports && !runtimePluginSpecifier && !options?.vendorImportsAsHttp) {
preservedImports.push(original);
preserve(original);
continue;
}
if (!canonical && isLikelyNativeScriptPluginSpecifier(specifier, getProjectRootPath()) && !isUnvendoredPackage(specifier)) {
canonical = specifier;
}
if (canonical && /^@nativescript\/core(\b|\/)/i.test(canonical)) {
preservedImports.push(original);
preserve(original);
continue;
}
if (!canonical) {
preservedImports.push(original);
preserve(original);
continue;
}
if (options?.vendorImportsAsHttp) {
Expand All @@ -343,7 +356,7 @@ export function ensureNativeScriptModuleBindings(code: string, options?: EnsureN
// both drops subpath-only exports ("does not provide an export
// named 'TableName'") and manufactures import cycles through
// the barrel (lodash-es `add.js` ↔ root → TDZ fatal).
preservedImports.push(rewritePreservedImportSpecifier(original, rawSpec, `/ns/m/node_modules/${normalizeNodeModulesSpecifier(specifier) || (/^[^./]/.test(specifier) ? specifier : canonical)}?ns_worker=1`));
preserve(rewritePreservedImportSpecifier(original, rawSpec, `/ns/m/node_modules/${normalizeNodeModulesSpecifier(specifier) || (/^[^./]/.test(specifier) ? specifier : canonical)}?ns_worker=1`));
continue;
}
const binding = getModuleBinding(canonical);
Expand All @@ -352,11 +365,11 @@ export function ensureNativeScriptModuleBindings(code: string, options?: EnsureN
}

if (preservedSpecifier) {
preservedImports.push(rewritePreservedImportSpecifier(original, rawSpec, preservedSpecifier));
preserve(rewritePreservedImportSpecifier(original, rawSpec, preservedSpecifier));
continue;
}
if (isEsmFrameworkPackageSpecifier(specifier)) {
preservedImports.push(original);
preserve(original);
continue;
}
const runtimePluginBareSpecifier = resolveInternalRuntimePluginBareSpecifier(specifier, getProjectRootPath());
Expand All @@ -369,24 +382,24 @@ export function ensureNativeScriptModuleBindings(code: string, options?: EnsureN
// realm's import map; in a worker serve (vendorImportsAsHttp) it must not
// win over the /ns/m HTTP rewrite.
if (options?.preserveNonPluginVendorImports && !runtimePluginSpecifier && !options?.vendorImportsAsHttp) {
preservedImports.push(original);
preserve(original);
continue;
}
if (!canonical && isLikelyNativeScriptPluginSpecifier(specifier, getProjectRootPath()) && !isUnvendoredPackage(specifier)) {
canonical = specifier;
}
if (canonical && /^@nativescript\/core(\b|\/)/i.test(canonical)) {
preservedImports.push(original);
preserve(original);
continue;
}
if (!canonical) {
preservedImports.push(original);
preserve(original);
continue;
}
if (options?.vendorImportsAsHttp) {
// See the side-effect branch above for the `ns_worker=1` rationale
// and why the URL must keep the importer's concrete subpath.
preservedImports.push(rewritePreservedImportSpecifier(original, rawSpec, `/ns/m/node_modules/${normalizeNodeModulesSpecifier(specifier) || (/^[^./]/.test(specifier) ? specifier : canonical)}?ns_worker=1`));
preserve(rewritePreservedImportSpecifier(original, rawSpec, `/ns/m/node_modules/${normalizeNodeModulesSpecifier(specifier) || (/^[^./]/.test(specifier) ? specifier : canonical)}?ns_worker=1`));
continue;
}

Expand Down Expand Up @@ -415,14 +428,21 @@ export function ensureNativeScriptModuleBindings(code: string, options?: EnsureN
binding.default.add(trimmed);
}

code = stripTopLevelImportRecords(code, importRecords);
// Re-exports move with the imports: ESM evaluates module requests in source order.
const reExports = collectTopLevelReExportDeclarations(code);
for (const reExport of reExports) {
preservedImports.push({ start: reExport.start, text: reExport.text });
}
code = stripTopLevelImportRecords(code, [...importRecords, ...reExports]);
const preserved = preservedImports.length
? preservedImports
.sort((left, right) => left.start - right.start)
.map((entry) => entry.text)
.join('\n') + '\n'
: '';

if (!modules.size) {
if (preservedImports.length) {
const preserved = preservedImports.join('\n') + '\n';
return preserved + code;
}
return code;
return preserved + code;
}

let injection = 'var __nsVendorRegistry = (globalThis.__nsVendorRegistry ||= new Map());\n';
Expand Down Expand Up @@ -470,7 +490,6 @@ export function ensureNativeScriptModuleBindings(code: string, options?: EnsureN
}

injection += '\n';
const preserved = preservedImports.length ? preservedImports.join('\n') + '\n' : '';
return preserved + injection + code;
}

Expand Down
22 changes: 21 additions & 1 deletion packages/vite/hmr/server/websocket-served-module-helpers.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest';

import { classifyServedModule, classifyServedRequest, hasEsmDefaultExport } from './websocket-served-module-helpers.js';
import { classifyServedModule, classifyServedRequest, hasEsmDefaultExport, hoistTopLevelStaticImports } 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', () => {
Expand Down Expand Up @@ -44,3 +44,23 @@ describe('hasEsmDefaultExport', () => {
expect(hasEsmDefaultExport('export default (;')).toBeNull();
});
});

describe('hoistTopLevelStaticImports', () => {
const hoist = (...code: string[]) =>
hoistTopLevelStaticImports(code.join('\n'))
.split('\n')
.filter((line) => line.trim());

it('keeps a side-effect import after the re-exports it follows', () => {
// Hoisting only the import evaluated ./web before ./scroll (TDZ on `Scroll`).
expect(hoist('const __DEV__ = true;', "export { Scroll } from './scroll';", "export * from './web';", "import './html';", 'export function f() {}')).toEqual(["export { Scroll } from './scroll';", "export * from './web';", "import './html';", 'const __DEV__ = true;', 'export function f() {}']);
});

it('hoists imports above preceding statements', () => {
expect(hoist('const a = 1;', "import { b } from './b';", 'console.log(a, b);')).toEqual(["import { b } from './b';", 'const a = 1;', 'console.log(a, b);']);
});

it('leaves local export declarations in place', () => {
expect(hoist('const x = 1;', 'export { x };', "import './a';")).toEqual(["import './a';", 'const x = 1;', 'export { x };']);
});
});
20 changes: 19 additions & 1 deletion packages/vite/hmr/server/websocket-served-module-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,8 +167,26 @@ export function collectTopLevelImportRecords(code: string): TopLevelImportRecord
}
}

function collectTopLevelModuleRequests(code: string): Array<{ start: number; end: number; text: string }> {
if (!code || typeof code !== 'string' || !/\b(?:import|export)\b/.test(code)) {
return [];
}

try {
const body = babelParse(code, { sourceType: 'module', plugins: MODULE_IMPORT_ANALYSIS_PLUGINS }).program.body;
return body
.filter((node) => t.isImportDeclaration(node) || t.isExportAllDeclaration(node) || (t.isExportNamedDeclaration(node) && !!node.source))
.filter((node) => typeof node.start === 'number' && typeof node.end === 'number')
.map((node) => ({ start: node.start!, end: node.end!, text: code.slice(node.start!, node.end!) }));
} catch {
return [];
}
}

/** Hoists top-level imports and `export ... from` declarations, keeping their order. */
export function hoistTopLevelStaticImports(code: string): string {
const imports = collectTopLevelImportRecords(code);
// Re-exports move too: ESM evaluates every module request in source order.
const imports = collectTopLevelModuleRequests(code);
if (!imports.length) {
return code;
}
Expand Down
Loading