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
33 changes: 11 additions & 22 deletions packages/vite/hmr/server/device-transform-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import * as path from 'path';
import { existsSync } from 'fs';
import * as PAT from './constants.js';
import { getProjectRootPath } from '../../helpers/project.js';
import { isLikelyNativeScriptRuntimePluginSpecifier, isNativeScriptCoreModule, isNativeScriptPluginModule, normalizeNativeScriptCoreSpecifier, normalizeNodeModulesSpecifier, resolveNodeModulesPackageBoundary, resolveVendorFromCandidate, viteDepsPathToBareSpecifier } from './websocket-module-specifiers.js';
import { decodeFlattenedId, isLikelyNativeScriptRuntimePluginSpecifier, isNativeScriptCoreModule, isNativeScriptPluginModule, normalizeNativeScriptCoreSpecifier, normalizeNodeModulesSpecifier, resolveNodeModulesPackageBoundary, resolveVendorFromCandidate, viteDepsPathToBareSpecifier } from './websocket-module-specifiers.js';
import { collectTopLevelImportRecords } from './websocket-served-module-helpers.js';

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

function packageIsInstalled(packageName: string): boolean {
if (!packageName) return false;
try {
return existsSync(path.join(getProjectRootPath(), 'node_modules', ...packageName.split('/'), 'package.json'));
} catch {
return false;
}
}

/**
* Vendor-manifest miss fallback — e.g. `emoji-regex`, a transitive dep of
* @nativescript/core that is never part of the vendor bundle. Dropping the
Expand All @@ -32,26 +41,6 @@ const warnedVendorMisses = new Set<string>();
* being committed; if nothing resolves, the first candidate is used anyway —
* a loud 404 on device beats a silent undefined binding.
*/
function decodeFlattenedDepId(flat: string): string {
// Reverse Vite's flattenId, which encodes '.' as '__' and '/' (and ':') as
// '_'. Split on the '__' (dot) boundaries first so a single '_' inside each
// segment becomes a '/', then rejoin the segments with '.'. This avoids any
// placeholder sentinel (a literal NUL would corrupt the served module).
return flat
.split('__')
.map((segment) => segment.replace(/_/g, '/'))
.join('.');
}

function packageIsInstalled(packageName: string): boolean {
if (!packageName) return false;
try {
return existsSync(path.join(getProjectRootPath(), 'node_modules', ...packageName.split('/'), 'package.json'));
} catch {
return false;
}
}

function bareSpecifierFromFlatDepPath(depPath: string): string {
const flat = depPath.split('?')[0].replace(/\.m?js$/, '');
// flattenId is lossy (names may contain '_'), so build candidates from the
Expand All @@ -64,7 +53,7 @@ function bareSpecifierFromFlatDepPath(depPath: string): string {
const pushCandidate = (c: string) => {
if (c && !candidates.includes(c)) candidates.push(c);
};
pushCandidate(decodeFlattenedDepId(flat));
pushCandidate(decodeFlattenedId(flat));
if (flat.startsWith('@')) {
// Scope-only decode: just the first '_' is the scope separator.
pushCandidate(flat.replace('_', '/'));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { afterEach, beforeEach, describe, expect, it } from 'vitest';

import { clearVendorManifest, registerVendorManifest } from '../shared/vendor/registry.js';
import { viteDepsPathToBareSpecifier } from './websocket-module-specifiers.js';

describe('viteDepsPathToBareSpecifier', () => {
beforeEach(() => {
registerVendorManifest({ hash: 'test', modules: { pkg: {}, '@scope/pkg': {} } } as any);
});
afterEach(() => clearVendorManifest());

it('decodes the dots in a subpath under a vendored package', () => {
expect(viteDepsPathToBareSpecifier('pkg_addons_env_file__js.js')).toBe('pkg/addons/env/file.js');
});

it('decodes an extensionless subpath and a scoped package', () => {
expect(viteDepsPathToBareSpecifier('pkg_examples_controls_orbit.js')).toBe('pkg/examples/controls/orbit');
expect(viteDepsPathToBareSpecifier('@scope_pkg_lib_x__min.js')).toBe('@scope/pkg/lib/x.min');
});

it('returns the package itself for its own prebundle', () => {
expect(viteDepsPathToBareSpecifier('pkg.js')).toBe('pkg');
});
});
21 changes: 15 additions & 6 deletions packages/vite/hmr/server/websocket-module-specifiers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,18 @@ export function extractVitePrebundleId(spec: string): string | null {
return null;
}

/**
* Reverses Vite's flattenId, which encodes '.' as '__' and '/' as '_'. The
* dots are split out first so `lib_file__js` decodes to `lib/file.js`, not
* `lib/file//js`. Lossy for names that themselves contain '_'.
*/
export function decodeFlattenedId(flat: string): string {
return flat
.split('__')
.map((segment) => segment.replace(/_/g, '/'))
.join('.');
}

export function getFlattenedManifestMap(manifest: VendorManifest): Map<string, string> {
const map = new Map<string, string>();
const mods = Object.keys(manifest.modules || {});
Expand Down Expand Up @@ -280,8 +292,7 @@ export function resolveVendorFromCandidate(specifier: string | null | undefined)
return canonical;
}
if (flattenedId.startsWith(`${flatKey}_`)) {
const flatSuffix = flattenedId.slice(flatKey.length + 1);
const subpath = flatSuffix.replace(/_/g, '/');
const subpath = decodeFlattenedId(flattenedId.slice(flatKey.length + 1));
if (isFileDistSubpath(subpath)) {
return canonical;
}
Expand All @@ -291,7 +302,7 @@ export function resolveVendorFromCandidate(specifier: string | null | undefined)
}
}
}
const guessedId = flattenedId.replace(/__/g, '.').replace(/_/g, '/');
const guessedId = decodeFlattenedId(flattenedId);
if (guessedId && guessedId !== flattenedId) {
const guessedCanonical = resolveVendorSpecifier(guessedId);
if (guessedCanonical) {
Expand Down Expand Up @@ -589,9 +600,7 @@ export function viteDepsPathToBareSpecifier(depPath: string): string | null {
}

if (bestKey && bestCanonical) {
const flatSuffix = flatId.slice(bestKey.length + 1);
const subpath = flatSuffix.replace(/_/g, '/');
return `${bestCanonical}/${subpath}`;
return `${bestCanonical}/${decodeFlattenedId(flatId.slice(bestKey.length + 1))}`;
}

return null;
Expand Down
Loading