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
28 changes: 27 additions & 1 deletion packages/vite/hmr/server/core-bundle.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import * as path from 'node:path';
import { createHash } from 'node:crypto';
import { afterAll, afterEach, beforeEach, describe, expect, it } from 'vitest';

import { CORE_BUNDLE_PATH, buildCoreBundleEntryCode, buildCoreMainShimCode, buildCoreSubShimCode, computeCoreBundleCacheKey, createCoreBundleService, enumerateCoreModuleSubpaths, generateCoreBundle, isCorePerModuleServingEnabled, isExpectedCoreBundleExclusion, resolveCoreRootForBundle, saveCoreBundleToDisk, tryLoadCoreBundleFromDisk } from './core-bundle.js';
import { CORE_BUNDLE_PATH, buildCoreBundleEntryCode, buildCoreMainShimCode, buildCoreSubShimCode, computeCoreBundleCacheKey, createCoreBundleService, enumerateCoreModuleSubpaths, generateCoreBundle, isCorePerModuleServingEnabled, isExpectedCoreBundleExclusion, readCorePatchesSignature, resolveCoreRootForBundle, saveCoreBundleToDisk, tryLoadCoreBundleFromDisk } from './core-bundle.js';

describe('isCorePerModuleServingEnabled', () => {
const standalone = () => false;
Expand Down Expand Up @@ -208,6 +208,12 @@ describe('core bundle disk cache', () => {
expect(computeCoreBundleCacheKey({ ...baseKeyInput, nsConfigJson: '{"profiling":"timeline"}' })).not.toBe(key);
});

it('cache key changes with the core patch signature', () => {
const key = computeCoreBundleCacheKey(baseKeyInput);
expect(computeCoreBundleCacheKey({ ...baseKeyInput, corePatches: '' })).toBe(key);
expect(computeCoreBundleCacheKey({ ...baseKeyInput, corePatches: '@nativescript+core+9.1.1.patch:965:1' })).not.toBe(key);
});

it('round-trips a saved bundle and misses on key change', () => {
const key = computeCoreBundleCacheKey(baseKeyInput);
const state = makeState('export const core = 1;');
Expand Down Expand Up @@ -291,3 +297,23 @@ describe('generateCoreBundle (integration)', () => {
120000,
);
});

describe('readCorePatchesSignature', () => {
it('reflects only patch-package patches of core, and their edits', () => {
const root = mkdtempSync(path.join(tmpdir(), 'ns-core-patches-'));
try {
expect(readCorePatchesSignature(root)).toBe('');
mkdirSync(path.join(root, 'patches'));
writeFileSync(path.join(root, 'patches', '@nativescript+tailwind+4.0.9.patch'), 'tailwind');
expect(readCorePatchesSignature(root)).toBe('');
const corePatch = path.join(root, 'patches', '@nativescript+core+9.1.1.patch');
writeFileSync(corePatch, 'one');
const first = readCorePatchesSignature(root);
expect(first).toContain('@nativescript+core+9.1.1.patch');
writeFileSync(corePatch, 'one more');
expect(readCorePatchesSignature(root)).not.toBe(first);
} finally {
rmSync(root, { recursive: true, force: true });
}
});
});
33 changes: 29 additions & 4 deletions packages/vite/hmr/server/core-bundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,9 +250,10 @@ export function buildCoreBundleEntryCode(subs: readonly string[]): string {
// rarely change between dev-server starts, so the built payload is persisted
// under `node_modules/.ns-vite/` and reloaded when the cache key matches.
// See computeCoreBundleCacheKey for the exact inputs (corePkgMtimeMs detects
// reinstalls; the schema counter is bumped when the generation pipeline
// changes). Opt out with `NS_CORE_BUNDLE_NO_DISK_CACHE=1` (e.g. when
// hand-editing core inside node_modules).
// reinstalls, corePatches detects patch-package patches of core; the schema
// counter is bumped when the generation pipeline changes). Opt out with
// `NS_CORE_BUNDLE_NO_DISK_CACHE=1` (e.g. when hand-editing core inside
// node_modules).
// ============================================================================

const CORE_BUNDLE_DISK_CACHE_SCHEMA = 1;
Expand All @@ -262,12 +263,13 @@ function isCoreBundleDiskCacheDisabled(env: NodeJS.ProcessEnv = process.env): bo
return v === '1' || v === 'true';
}

export function computeCoreBundleCacheKey(input: { coreRoot: string; coreVersion: string; corePkgMtimeMs: number; platform: string; mode: string; flavor: string; defines: Record<string, string>; nsConfigJson: string; subs: readonly string[]; vitePackageVersion: string }): string {
export function computeCoreBundleCacheKey(input: { coreRoot: string; coreVersion: string; corePkgMtimeMs: number; corePatches?: string; platform: string; mode: string; flavor: string; defines: Record<string, string>; nsConfigJson: string; subs: readonly string[]; vitePackageVersion: string }): string {
const payload = JSON.stringify({
schema: CORE_BUNDLE_DISK_CACHE_SCHEMA,
coreRoot: input.coreRoot.replace(/\\/g, '/'),
coreVersion: input.coreVersion,
corePkgMtimeMs: input.corePkgMtimeMs,
corePatches: input.corePatches ?? '',
platform: input.platform,
mode: input.mode,
flavor: input.flavor,
Expand All @@ -279,6 +281,28 @@ export function computeCoreBundleCacheKey(input: { coreRoot: string; coreVersion
return createHash('sha1').update(payload).digest('hex');
}

/**
* patch-package rewrites files inside `node_modules/@nativescript/core` without
* touching its package.json, so `corePkgMtimeMs` cannot see a core patch land,
* change or go away. The project's `patches/@nativescript+core*.patch` files
* stand in for it: their names, sizes and mtimes join the cache key.
*/
export function readCorePatchesSignature(projectRoot: string): string {
try {
const dir = path.join(projectRoot, 'patches');
return readdirSync(dir)
.filter((name) => name.startsWith('@nativescript+core+') && name.endsWith('.patch'))
.sort()
.map((name) => {
const stat = statSync(path.join(dir, name));
return `${name}:${stat.size}:${Math.round(stat.mtimeMs)}`;
})
.join(';');
} catch {
return '';
}
}

function getCoreBundleCacheDir(projectRoot: string): string {
return path.join(projectRoot, 'node_modules', '.ns-vite');
}
Expand Down Expand Up @@ -473,6 +497,7 @@ export async function generateCoreBundle(options: GenerateCoreBundleOptions): Pr
coreRoot,
coreVersion,
corePkgMtimeMs,
corePatches: readCorePatchesSignature(projectRoot),
platform: String(platform),
mode: String(mode),
flavor: flavor ?? '',
Expand Down
Loading