Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
cbcda2d
chore: update rspack to 1.4.2
SyMind Jul 3, 2025
b10df6d
chore: update rspack to 1.4.4
SyMind Jul 7, 2025
97b405e
Merge branch 'canary' into rspack-1.4.2
SyMind Jul 7, 2025
d864ef3
update to 1.4.5
SyMind Jul 8, 2025
913f571
chore: merge origin canary
SyMind Jul 8, 2025
3a5986b
update snapshot
SyMind Jul 8, 2025
84989f7
types-and-precompiled
SyMind Jul 8, 2025
a453c91
feat: open persistent cache by default in rspack
SyMind Jul 8, 2025
00003e2
update snapshot
SyMind Jul 8, 2025
1bf056f
chore: try rspack canary version
SyMind Jul 11, 2025
73cc6e6
chore: merge origin canary
SyMind Jul 11, 2025
7e36e8b
update canary
SyMind Jul 11, 2025
5233590
merge origin canary
SyMind Jul 11, 2025
0c9642d
update snapshot
SyMind Jul 14, 2025
30aafa1
chore: merge origin canary
SyMind Jul 14, 2025
cabc286
rspack 1.4.8
SyMind Jul 16, 2025
e508d20
buildDependencies add babelConfigFile and jsConfigPath
SyMind Jul 16, 2025
0912af3
fix: telemetry test case
SyMind Jul 17, 2025
6089206
upgrade rspack
SyMind Jul 18, 2025
91d3b18
chore: merge canary
SyMind Jul 18, 2025
5fb33c2
feat: merge origin canary
SyMind Jul 18, 2025
adaf7c2
types-and-precompiled
SyMind Jul 18, 2025
d8503c2
chore: update rspack to 1.4.9
SyMind Jul 22, 2025
0974c8f
chore: merge origin canary
SyMind Jul 22, 2025
e5e623d
types-and-precompiled
SyMind Jul 22, 2025
2651036
init
SyMind Aug 18, 2025
8098c80
HotReloaderRspack for Rspack
SyMind Aug 19, 2025
61cad65
fix: should await super.start
SyMind Aug 19, 2025
bbf1b28
add lazy compilation middleware
SyMind Aug 19, 2025
61aebda
chore: merge persisent cache
SyMind Aug 20, 2025
4e07e55
builtEntriesCachePath
SyMind Aug 20, 2025
4fc60d5
add hash for built entries cache
SyMind Aug 20, 2025
1efdf43
merge
SyMind Aug 20, 2025
bfc2720
fix prettier
SyMind Aug 20, 2025
a688c81
fix: should not use createEntries
SyMind Aug 20, 2025
bd01aec
short hash
SyMind Aug 20, 2025
9923d54
fix: typo
SyMind Aug 20, 2025
6fbb92d
only cache entries that enabled cache
SyMind Aug 20, 2025
b8879c4
upgrade rspack
SyMind Aug 20, 2025
62ed5cd
remove outdata entries
SyMind Aug 20, 2025
f769ce4
fix: file removed
SyMind Aug 20, 2025
b057a76
fix: test/integration/amphtml/test/index.test.js
SyMind Aug 20, 2025
77f0dc6
chore: merge origin canary
SyMind Nov 6, 2025
2a5a9d2
fix: pnpm lock
SyMind Nov 10, 2025
a34ffa9
fix: merge conflicts
SyMind Nov 10, 2025
edfa059
types-and-precompiled
SyMind Nov 10, 2025
c39c7e5
feat: using HotReloaderRspack
SyMind Nov 10, 2025
a098942
fix: cache buildDependencies
SyMind Nov 10, 2025
64e191c
chore: rm test/integration/amphtml/test/index.test.js
SyMind Nov 10, 2025
37fb20e
fix: cache for middleware
SyMind Nov 10, 2025
ba670a6
update rspack 1.6.5
SyMind Nov 26, 2025
b9f9000
update @next/rspack-core
SyMind Nov 26, 2025
5239a8d
chore: merge origin canary
SyMind Nov 26, 2025
5618d0a
chore: update snapshot
SyMind Nov 28, 2025
714fa8d
chore: merge origin main
SyMind Nov 28, 2025
a657db5
lint
SyMind Nov 28, 2025
21f31e6
udpate snapshot
SyMind Nov 28, 2025
dc4c455
Merge branch 'canary' into rspack-persistent-cache
SyMind Dec 1, 2025
9c3bbcf
chore: merge origin canary
SyMind Dec 17, 2025
fa8f4ab
revert snapshot
SyMind Dec 17, 2025
5861ba2
fix: mcp-server test case
SyMind Dec 17, 2025
5549bc0
update snapshopt
SyMind Dec 17, 2025
ff21d1f
pnpm types-and-precompiled
SyMind Dec 17, 2025
824879c
fix: buildFallbackError
SyMind Dec 17, 2025
26eac3b
Merge branch 'canary' into rspack-persistent-cache
SyMind Dec 17, 2025
dc9af8a
add some comments
SyMind Dec 17, 2025
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
26 changes: 25 additions & 1 deletion packages/next/src/build/webpack-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2361,7 +2361,7 @@ export default async function getBaseWebpackConfig(
serverReferenceHashSalt: encryptionKey,
})

const cache: any = {
const cache: webpack.Configuration['cache'] = {
type: 'filesystem',
// Disable memory cache in development in favor of our own MemoryWithGcCachePlugin.
maxMemoryGenerations: dev ? 0 : Infinity, // Infinity is default value for production in webpack currently.
Expand Down Expand Up @@ -2407,6 +2407,30 @@ export default async function getBaseWebpackConfig(

webpack5Config.cache = cache

if (isRspack) {
const buildDependencies: string[] = []
if (config.configFile) {
buildDependencies.push(config.configFile)
}
if (babelConfigFile) {
buildDependencies.push(babelConfigFile)
}
if (jsConfigPath) {
buildDependencies.push(jsConfigPath)
}

// @ts-ignore
webpack5Config.experiments.cache = {
type: 'persistent',
buildDependencies,
storage: {
type: 'filesystem',
directory: cache.cacheDirectory,
},
version: `${__dirname}|${process.env.__NEXT_VERSION}|${configVars}`,
}
}

if (process.env.NEXT_WEBPACK_LOGGING) {
const infra = process.env.NEXT_WEBPACK_LOGGING.includes('infrastructure')
const profileClient =
Expand Down
243 changes: 243 additions & 0 deletions packages/next/src/server/dev/hot-reloader-rspack.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
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 type { __ApiPreviewProps } from '../api-utils'
import type { RouteDefinition } from '../route-definitions/route-definition'
import type { MultiCompiler } from 'webpack'
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 {
private builtEntriesCachePath?: string

private isClientCacheEnabled = false
private isServerCacheEnabled = false
private isEdgeServerCacheEnabled = false

public async afterCompile(multiCompiler: MultiCompiler): Promise<void> {
// 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: ReturnType<typeof getEntries> = 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)
Comment thread
SyMind marked this conversation as resolved.
} catch (error) {
console.error('Rspack failed to read built entries cache: ', error)
}
}
})
}

public async ensurePage({
page,
clientOnly,
appPaths,
definition,
isApp,
url,
}: {
page: string
clientOnly: boolean
appPaths?: ReadonlyArray<string> | null
isApp?: boolean
definition?: RouteDefinition
url?: string
}): Promise<void> {
await super.ensurePage({
page,
clientOnly,
appPaths,
definition,
isApp,
url,
})
const entries = getEntries(this.multiCompiler!.outputPath)
const builtEntries: { [entryName: string]: any } = {}
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)
Comment thread
SyMind marked this conversation as resolved.
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: string | undefined
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)
}
}
}

async function calculateFileHash(
filePath: string,
algorithm: string = 'sha256'
): Promise<string | undefined> {
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')
}
29 changes: 16 additions & 13 deletions packages/next/src/server/dev/hot-reloader-webpack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,18 +226,18 @@ function erroredPages(compilation: webpack.Compilation) {
export default class HotReloaderWebpack implements NextJsHotReloaderInterface {
private hasAppRouterEntrypoints: boolean
private hasPagesRouterEntrypoints: boolean
private dir: string
private buildId: string
protected dir: string
protected buildId: string
private encryptionKey: string
private middlewares: ((
protected middlewares: ((
req: IncomingMessage,
res: ServerResponse,
next: () => void
) => Promise<void>)[]
private pagesDir?: string
private distDir: string
protected pagesDir?: string
protected distDir: string
private webpackHotMiddleware?: WebpackHotMiddleware
private config: NextConfigComplete
protected config: NextConfigComplete
private clientStats: webpack.Stats | null
private clientError: Error | null = null
private serverError: Error | null = null
Expand All @@ -246,13 +246,13 @@ export default class HotReloaderWebpack implements NextJsHotReloaderInterface {
private serverChunkNames?: Set<string>
private prevChunkNames?: Set<any>
private onDemandEntries?: ReturnType<typeof onDemandEntryHandler>
private previewProps: __ApiPreviewProps
protected previewProps: __ApiPreviewProps
private watcher: any
private rewrites: CustomRoutes['rewrites']
private fallbackWatcher: any
private hotReloaderSpan: Span
protected hotReloaderSpan: Span
private pagesMapping: { [key: string]: string } = {}
private appDir?: string
protected appDir?: string
private telemetry: Telemetry
private resetFetch: () => void
private lockfile: Lockfile | undefined
Expand Down Expand Up @@ -1243,6 +1243,8 @@ export default class HotReloaderWebpack implements NextJsHotReloaderInterface {
this.activeWebpackConfigs
) as unknown as webpack.MultiCompiler

await this.afterCompile(this.multiCompiler)

// Copy over the filesystem so that it is shared between all compilers.
const inputFileSystem = this.multiCompiler.compilers[0].inputFileSystem
for (const compiler of this.multiCompiler.compilers) {
Expand Down Expand Up @@ -1643,7 +1645,7 @@ export default class HotReloaderWebpack implements NextJsHotReloaderInterface {
}),
})

this.middlewares = [
this.middlewares.push(
getOverlayMiddleware({
rootDirectory: this.dir,
isSrcDir: this.isSrcDir,
Expand Down Expand Up @@ -1694,9 +1696,8 @@ export default class HotReloaderWebpack implements NextJsHotReloaderInterface {
getDevServerUrl: () => process.env.__NEXT_PRIVATE_ORIGIN,
}),
]
: []),
]

: [])
)
setStackFrameResolver(async (request) => {
return getOriginalStackFrames({
isServer: request.isServer,
Expand All @@ -1711,6 +1712,8 @@ export default class HotReloaderWebpack implements NextJsHotReloaderInterface {
})
}

protected async afterCompile(_multiCompiler: webpack.MultiCompiler) {}

public invalidate(
{ reloadAfterInvalidation }: { reloadAfterInvalidation: boolean } = {
reloadAfterInvalidation: false,
Expand Down
4 changes: 2 additions & 2 deletions packages/next/src/server/dev/on-demand-entry-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import type {
} from '../../shared/lib/app-router-types'
import type { CompilerNameValues } from '../../shared/lib/constants'
import type { RouteDefinition } from '../route-definitions/route-definition'
import type HotReloaderWebpack from './hot-reloader-webpack'

import createDebug from 'next/dist/compiled/debug'
import { EventEmitter } from 'events'
Expand Down Expand Up @@ -39,6 +38,7 @@ import { PAGE_SEGMENT_KEY } from '../../shared/lib/segment'
import {
HMR_MESSAGE_SENT_TO_BROWSER,
HMR_MESSAGE_SENT_TO_SERVER,
type NextJsHotReloaderInterface,
} from './hot-reloader-types'
import { isAppPageRouteDefinition } from '../route-definitions/app-page-route-definition'
import { scheduleOnNextTick } from '../../lib/scheduler'
Expand Down Expand Up @@ -548,7 +548,7 @@ export function onDemandEntryHandler({
rootDir,
appDir,
}: {
hotReloader: HotReloaderWebpack
hotReloader: NextJsHotReloaderInterface
maxInactiveAge: number
multiCompiler: webpack.MultiCompiler
nextConfig: NextConfigComplete
Expand Down
Loading
Loading