Skip to content

Commit fbd4ced

Browse files
authored
fix(vite): decode dots in prebundled subpath specifiers (#11466)
[skip ci]
1 parent 6b38d20 commit fbd4ced

3 files changed

Lines changed: 50 additions & 28 deletions

File tree

‎packages/vite/hmr/server/device-transform-helpers.ts‎

Lines changed: 11 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import * as path from 'path';
66
import { existsSync } from 'fs';
77
import * as PAT from './constants.js';
88
import { getProjectRootPath } from '../../helpers/project.js';
9-
import { isLikelyNativeScriptRuntimePluginSpecifier, isNativeScriptCoreModule, isNativeScriptPluginModule, normalizeNativeScriptCoreSpecifier, normalizeNodeModulesSpecifier, resolveNodeModulesPackageBoundary, resolveVendorFromCandidate, viteDepsPathToBareSpecifier } from './websocket-module-specifiers.js';
9+
import { decodeFlattenedId, isLikelyNativeScriptRuntimePluginSpecifier, isNativeScriptCoreModule, isNativeScriptPluginModule, normalizeNativeScriptCoreSpecifier, normalizeNodeModulesSpecifier, resolveNodeModulesPackageBoundary, resolveVendorFromCandidate, viteDepsPathToBareSpecifier } from './websocket-module-specifiers.js';
1010
import { collectTopLevelImportRecords } from './websocket-served-module-helpers.js';
1111

1212
// Bare specifiers and special skip patterns (virtual, data:, etc.)
@@ -17,6 +17,15 @@ const SKIP_PATTERNS = /^(?:data:|blob:|node:|virtual:|vite:|\0|\/@@?id|\/__vite|
1717
// the console on every served module.
1818
const warnedVendorMisses = new Set<string>();
1919

20+
function packageIsInstalled(packageName: string): boolean {
21+
if (!packageName) return false;
22+
try {
23+
return existsSync(path.join(getProjectRootPath(), 'node_modules', ...packageName.split('/'), 'package.json'));
24+
} catch {
25+
return false;
26+
}
27+
}
28+
2029
/**
2130
* Vendor-manifest miss fallback — e.g. `emoji-regex`, a transitive dep of
2231
* @nativescript/core that is never part of the vendor bundle. Dropping the
@@ -32,26 +41,6 @@ const warnedVendorMisses = new Set<string>();
3241
* being committed; if nothing resolves, the first candidate is used anyway —
3342
* a loud 404 on device beats a silent undefined binding.
3443
*/
35-
function decodeFlattenedDepId(flat: string): string {
36-
// Reverse Vite's flattenId, which encodes '.' as '__' and '/' (and ':') as
37-
// '_'. Split on the '__' (dot) boundaries first so a single '_' inside each
38-
// segment becomes a '/', then rejoin the segments with '.'. This avoids any
39-
// placeholder sentinel (a literal NUL would corrupt the served module).
40-
return flat
41-
.split('__')
42-
.map((segment) => segment.replace(/_/g, '/'))
43-
.join('.');
44-
}
45-
46-
function packageIsInstalled(packageName: string): boolean {
47-
if (!packageName) return false;
48-
try {
49-
return existsSync(path.join(getProjectRootPath(), 'node_modules', ...packageName.split('/'), 'package.json'));
50-
} catch {
51-
return false;
52-
}
53-
}
54-
5544
function bareSpecifierFromFlatDepPath(depPath: string): string {
5645
const flat = depPath.split('?')[0].replace(/\.m?js$/, '');
5746
// flattenId is lossy (names may contain '_'), so build candidates from the
@@ -64,7 +53,7 @@ function bareSpecifierFromFlatDepPath(depPath: string): string {
6453
const pushCandidate = (c: string) => {
6554
if (c && !candidates.includes(c)) candidates.push(c);
6655
};
67-
pushCandidate(decodeFlattenedDepId(flat));
56+
pushCandidate(decodeFlattenedId(flat));
6857
if (flat.startsWith('@')) {
6958
// Scope-only decode: just the first '_' is the scope separator.
7059
pushCandidate(flat.replace('_', '/'));
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
2+
3+
import { clearVendorManifest, registerVendorManifest } from '../shared/vendor/registry.js';
4+
import { viteDepsPathToBareSpecifier } from './websocket-module-specifiers.js';
5+
6+
describe('viteDepsPathToBareSpecifier', () => {
7+
beforeEach(() => {
8+
registerVendorManifest({ hash: 'test', modules: { pkg: {}, '@scope/pkg': {} } } as any);
9+
});
10+
afterEach(() => clearVendorManifest());
11+
12+
it('decodes the dots in a subpath under a vendored package', () => {
13+
expect(viteDepsPathToBareSpecifier('pkg_addons_env_file__js.js')).toBe('pkg/addons/env/file.js');
14+
});
15+
16+
it('decodes an extensionless subpath and a scoped package', () => {
17+
expect(viteDepsPathToBareSpecifier('pkg_examples_controls_orbit.js')).toBe('pkg/examples/controls/orbit');
18+
expect(viteDepsPathToBareSpecifier('@scope_pkg_lib_x__min.js')).toBe('@scope/pkg/lib/x.min');
19+
});
20+
21+
it('returns the package itself for its own prebundle', () => {
22+
expect(viteDepsPathToBareSpecifier('pkg.js')).toBe('pkg');
23+
});
24+
});

‎packages/vite/hmr/server/websocket-module-specifiers.ts‎

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,18 @@ export function extractVitePrebundleId(spec: string): string | null {
2727
return null;
2828
}
2929

30+
/**
31+
* Reverses Vite's flattenId, which encodes '.' as '__' and '/' as '_'. The
32+
* dots are split out first so `lib_file__js` decodes to `lib/file.js`, not
33+
* `lib/file//js`. Lossy for names that themselves contain '_'.
34+
*/
35+
export function decodeFlattenedId(flat: string): string {
36+
return flat
37+
.split('__')
38+
.map((segment) => segment.replace(/_/g, '/'))
39+
.join('.');
40+
}
41+
3042
export function getFlattenedManifestMap(manifest: VendorManifest): Map<string, string> {
3143
const map = new Map<string, string>();
3244
const mods = Object.keys(manifest.modules || {});
@@ -280,8 +292,7 @@ export function resolveVendorFromCandidate(specifier: string | null | undefined)
280292
return canonical;
281293
}
282294
if (flattenedId.startsWith(`${flatKey}_`)) {
283-
const flatSuffix = flattenedId.slice(flatKey.length + 1);
284-
const subpath = flatSuffix.replace(/_/g, '/');
295+
const subpath = decodeFlattenedId(flattenedId.slice(flatKey.length + 1));
285296
if (isFileDistSubpath(subpath)) {
286297
return canonical;
287298
}
@@ -291,7 +302,7 @@ export function resolveVendorFromCandidate(specifier: string | null | undefined)
291302
}
292303
}
293304
}
294-
const guessedId = flattenedId.replace(/__/g, '.').replace(/_/g, '/');
305+
const guessedId = decodeFlattenedId(flattenedId);
295306
if (guessedId && guessedId !== flattenedId) {
296307
const guessedCanonical = resolveVendorSpecifier(guessedId);
297308
if (guessedCanonical) {
@@ -640,9 +651,7 @@ export function viteDepsPathToBareSpecifier(depPath: string): string | null {
640651
}
641652

642653
if (bestKey && bestCanonical) {
643-
const flatSuffix = flatId.slice(bestKey.length + 1);
644-
const subpath = flatSuffix.replace(/_/g, '/');
645-
return `${bestCanonical}/${subpath}`;
654+
return `${bestCanonical}/${decodeFlattenedId(flatId.slice(bestKey.length + 1))}`;
646655
}
647656

648657
return null;

0 commit comments

Comments
 (0)