next
Version:
The React Framework
156 lines (155 loc) • 8.06 kB
JavaScript
import path from 'path';
import fs from 'fs/promises';
import { createHash } from 'crypto';
import HotReloaderWebpack from './hot-reloader-webpack';
import { BUILT, EntryTypes, getEntries } from './on-demand-entry-handler';
import { COMPILER_NAMES } from '../../shared/lib/constants';
/**
* Rspack Persistent Cache Strategy for Next.js Development
*
* Rspack's persistent caching differs from Webpack in how it manages module graphs.
* While Webpack incrementally updates modules, Rspack operates on complete module
* graph snapshots for cache restoration.
*
* Problem:
* - Next.js dev server starts with no page modules in the initial entry points
* - When Rspack restores from persistent cache, it finds no modules and purges
* the entire module graph
* - Later page requests find no cached module information, preventing cache reuse
*
* Solution:
* - Track successfully built page entries after each compilation
* - Restore these entries on dev server restart to maintain module graph continuity
* - This ensures previously compiled pages can leverage persistent cache for faster builds
*/ export default class HotReloaderRspack extends HotReloaderWebpack {
async afterCompile(multiCompiler) {
// Always initialize the fallback error watcher for Rspack.
// Rspack may restore/retain the previous build's error state, so without this
// a page that previously failed to build might not be rebuilt on the next request.
await super.buildFallbackError();
const rspackStartSpan = this.hotReloaderSpan.traceChild('rspack-after-compile');
await rspackStartSpan.traceAsyncFn(async ()=>{
const hash = createHash('sha1');
multiCompiler.compilers.forEach((compiler)=>{
const cache = compiler.options.cache;
if (typeof cache === 'object' && 'version' in cache && cache.version) {
hash.update(cache.version);
if (compiler.name === COMPILER_NAMES.client) {
this.isClientCacheEnabled = true;
} else if (compiler.name === COMPILER_NAMES.server) {
this.isServerCacheEnabled = true;
} else if (compiler.name === COMPILER_NAMES.edgeServer) {
this.isEdgeServerCacheEnabled = true;
}
} else {
hash.update('-');
}
return undefined;
});
this.builtEntriesCachePath = path.join(this.distDir, 'cache', 'rspack', hash.digest('hex').substring(0, 16), 'built-entries.json');
const hasBuiltEntriesCache = await fs.access(this.builtEntriesCachePath).then(()=>true, ()=>false);
if (hasBuiltEntriesCache) {
try {
const builtEntries = JSON.parse(await fs.readFile(this.builtEntriesCachePath, 'utf-8') || '{}');
await Promise.all(Object.keys(builtEntries).map(async (entryKey)=>{
const entryData = builtEntries[entryKey];
const isEntry = entryData.type === EntryTypes.ENTRY;
const isChildEntry = entryData.type === EntryTypes.CHILD_ENTRY;
// Check if the page was removed or disposed and remove it
if (isEntry) {
const pageExists = !entryData.dispose && await fs.access(entryData.absolutePagePath).then(()=>true, ()=>false);
if (!pageExists) {
delete builtEntries[entryKey];
return;
} else if (!('hash' in builtEntries[entryKey]) || builtEntries[entryKey].hash !== await calculateFileHash(entryData.absolutePagePath)) {
delete builtEntries[entryKey];
return;
}
}
// For child entries, if it has an entry file and it's gone, remove it
if (isChildEntry) {
if (entryData.absoluteEntryFilePath) {
const pageExists = !entryData.dispose && await fs.access(entryData.absoluteEntryFilePath).then(()=>true, ()=>false);
if (!pageExists) {
delete builtEntries[entryKey];
return;
} else {
if (!('hash' in builtEntries[entryKey]) || builtEntries[entryKey].hash !== await calculateFileHash(entryData.absoluteEntryFilePath)) {
delete builtEntries[entryKey];
return;
}
}
}
}
}));
Object.assign(getEntries(multiCompiler.outputPath), builtEntries);
} catch (error) {
console.error('Rspack failed to read built entries cache: ', error);
}
}
});
}
async ensurePage({ page, clientOnly, appPaths, definition, isApp, url }) {
await super.ensurePage({
page,
clientOnly,
appPaths,
definition,
isApp,
url
});
const entries = getEntries(this.multiCompiler.outputPath);
const builtEntries = {};
await Promise.all(Object.keys(entries).map(async (entryName)=>{
const entry = entries[entryName];
if (entry.status !== BUILT) return;
const result = /^(client|server|edge-server)@(app|pages|root)@(.*)/g.exec(entryName);
const [, key /* pageType */ , , ] = result// this match should always happen
;
if (key === 'client' && !this.isClientCacheEnabled) return;
if (key === 'server' && !this.isServerCacheEnabled) return;
if (key === 'edge-server' && !this.isEdgeServerCacheEnabled) return;
// TODO: Rspack does not store middleware entries in persistent cache, causing
// test/integration/middleware-src/test/index.test.ts to fail. This is a temporary
// workaround to skip middleware entry caching until Rspack properly supports it.
if (page === '/middleware') {
return;
}
let hash;
if (entry.type === EntryTypes.ENTRY) {
hash = await calculateFileHash(entry.absolutePagePath);
} else if (entry.absoluteEntryFilePath) {
hash = await calculateFileHash(entry.absoluteEntryFilePath);
}
if (!hash) {
return;
}
builtEntries[entryName] = entry;
builtEntries[entryName].hash = hash;
}));
const hasBuitEntriesCache = await fs.access(this.builtEntriesCachePath).then(()=>true, ()=>false);
try {
if (!hasBuitEntriesCache) {
await fs.mkdir(path.dirname(this.builtEntriesCachePath), {
recursive: true
});
}
await fs.writeFile(this.builtEntriesCachePath, JSON.stringify(builtEntries, null, 2));
} catch (error) {
console.error('Rspack failed to write built entries cache: ', error);
}
}
constructor(...args){
super(...args), this.isClientCacheEnabled = false, this.isServerCacheEnabled = false, this.isEdgeServerCacheEnabled = false;
}
}
async function calculateFileHash(filePath, algorithm = 'sha256') {
if (!await fs.access(filePath).then(()=>true, ()=>false)) {
return;
}
const fileBuffer = await fs.readFile(filePath);
const hash = createHash(algorithm);
hash.update(fileBuffer);
return hash.digest('hex');
}
//# sourceMappingURL=hot-reloader-rspack.js.map