Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
[webpack] generate next/root-params
  • Loading branch information
lubieowoce committed Jul 28, 2025
commit a4e012b4c362d29afc7df6c9b32aa35df45ebe32
90 changes: 90 additions & 0 deletions packages/next/src/build/webpack-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@ import { getRspackCore, getRspackReactRefresh } from '../shared/lib/get-rspack'
import { RspackProfilingPlugin } from './webpack/plugins/rspack-profiling-plugin'
import getWebpackBundler from '../shared/lib/get-webpack-bundler'
import type { NextBuildContext } from './build-context'
import type { RootParamsLoaderOpts } from './webpack/loaders/next-root-params-loader'
import type { InvalidImportLoaderOpts } from './webpack/loaders/next-invalid-import-error-loader'

type ExcludesFalse = <T>(x: T | false) => x is T
type ClientEntries = {
Expand Down Expand Up @@ -1364,6 +1366,7 @@ export default async function getBaseWebpackConfig(
'modularize-import-loader',
'next-barrel-loader',
'next-error-browser-binary-loader',
'next-root-params-loader',
].reduce(
(alias, loader) => {
// using multiple aliases to replace `resolveLoader.modules`
Expand Down Expand Up @@ -1543,6 +1546,18 @@ export default async function getBaseWebpackConfig(
},
]
: []),

...getNextRootParamsRules({
isRootParamsEnabled:
config.experimental.rootParams ??
// `experimental.dynamicIO` implies `experimental.rootParams`.
config.experimental.cacheComponents ??
false,
isClient,
appDir,
pageExtensions,
}),

// TODO: FIXME: do NOT webpack 5 support with this
// x-ref: https://github.com/webpack/webpack/issues/11467
...(!config.experimental.fullySpecified
Expand Down Expand Up @@ -2754,3 +2769,78 @@ export default async function getBaseWebpackConfig(

return webpackConfig
}

function getNextRootParamsRules({
isRootParamsEnabled,
isClient,
appDir,
pageExtensions,
}: {
isRootParamsEnabled: boolean
isClient: boolean
appDir: string | undefined
pageExtensions: string[]
}): webpack.RuleSetRule[] {
// Match resolved import of 'next/root-params'
const nextRootParamsModule = path.join(NEXT_PROJECT_ROOT, 'root-params.js')

const createInvalidImportRule = (message: string) => {
return {
resource: nextRootParamsModule,
loader: 'next-invalid-import-error-loader',
options: {
message,
} satisfies InvalidImportLoaderOpts,
} satisfies webpack.RuleSetRule
}

// Hard-error if the flag is not enabled, regardless of if we're on the server or on the client.
if (!isRootParamsEnabled) {
return [
createInvalidImportRule(
"'next/root-params' can only be imported when `experimental.rootParams` is enabled."
),
]
}

// If there's no app-dir (and thus no layouts), there's no sensible way to use 'next/root-params',
// because we wouldn't generate any getters.
if (!appDir) {
return [
createInvalidImportRule(
"'next/root-params' can only be used with the App Directory."
),
]
}

// In general, the compiler should prevent importing 'next/root-params' from client modules, but it doesn't catch everything.
// If an import slips through our validation, make it error.
const invalidClientImportRule = createInvalidImportRule(
"'next/root-params' cannot be imported from a Client Component module. It should only be used from a Server Component."
)

// in the browser compilation we can skip the server rules, because we know all imports will be invalid.
if (isClient) {
return [invalidClientImportRule]
}

return [
{
oneOf: [
{
resource: nextRootParamsModule,
issuerLayer: shouldUseReactServerCondition as (
layer: string
) => boolean,
loader: 'next-root-params-loader',
options: {
appDir,
pageExtensions,
} satisfies RootParamsLoaderOpts,
},
// if the rule above didn't match, we're in the SSR layer (or something else that isn't server-only).
invalidClientImportRule,
],
},
]
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
export default function nextInvalidImportErrorLoader(this: any) {
const { message } = this.getOptions()
throw new Error(message)
}
import type webpack from 'webpack'

export type InvalidImportLoaderOpts = { message: string }

const nextInvalidImportErrorLoader: webpack.LoaderDefinitionFunction<InvalidImportLoaderOpts> =
function () {
const { message } = this.getOptions()
throw new Error(message)
}

export default nextInvalidImportErrorLoader
171 changes: 171 additions & 0 deletions packages/next/src/build/webpack/loaders/next-root-params-loader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import type { webpack } from 'next/dist/compiled/webpack/webpack'
import * as path from 'node:path'
import * as fs from 'node:fs/promises'
import { normalizeAppPath } from '../../../shared/lib/router/utils/app-paths'
import { ensureLeadingSlash } from '../../../shared/lib/page-path/ensure-leading-slash'
import { getSegmentParam } from '../../../server/app-render/get-segment-param'

export type RootParamsLoaderOpts = {
appDir: string
pageExtensions: string[]
}

type CollectedRootParams = Set<string>

const rootParamsLoader: webpack.LoaderDefinitionFunction<RootParamsLoaderOpts> =
async function () {
const { appDir, pageExtensions } = this.getOptions()

const allRootParams = await collectRootParamsFromFileSystem({
appDir,
pageExtensions,
// Track every directory we traverse in case a layout gets added to it
// (which would make it the new root layout for that subtree).
// This is relevant both in dev (for file watching) and in prod (for caching).
trackDirectory: (directory) => this.addContextDependency(directory),
})

// If there's no root params, there's nothing to generate.
if (allRootParams.size === 0) {
return 'export {}'
}

// Generate a getter for each root param we found.
const sortedRootParamNames = Array.from(allRootParams).sort()
const content = [
`import { getRootParam } from 'next/dist/server/request/root-params';`,
...sortedRootParamNames.map((paramName) => {
return `export function ${paramName}() { return getRootParam('${paramName}'); }`
}),
].join('\n')

return content
}

export default rootParamsLoader

async function collectRootParamsFromFileSystem(
opts: Parameters<typeof findRootLayouts>[0]
) {
return collectRootParams({
appDir: opts.appDir,
rootLayoutFilePaths: await findRootLayouts(opts),
})
}

function collectRootParams({
rootLayoutFilePaths,
appDir,
}: {
rootLayoutFilePaths: string[]
appDir: string
}): CollectedRootParams {
const allRootParams: CollectedRootParams = new Set()

for (const rootLayoutFilePath of rootLayoutFilePaths) {
const params = getParamsFromLayoutFilePath({
appDir,
layoutFilePath: rootLayoutFilePath,
})
for (const param of params) {
allRootParams.add(param)
}
}

return allRootParams
}

async function findRootLayouts({
appDir,
pageExtensions,
trackDirectory,
}: {
appDir: string
pageExtensions: string[]
trackDirectory: ((dirPath: string) => void) | undefined
}) {
const layoutFilenameRegex = new RegExp(
`^layout\\.(?:${pageExtensions.join('|')})$`
)

async function visit(directory: string): Promise<string[]> {
let dir: Awaited<ReturnType<(typeof fs)['readdir']>>
try {
dir = await fs.readdir(directory, { withFileTypes: true })
} catch (err) {
// If the directory was removed before we managed to read it, just ignore it.
if (
err &&
typeof err === 'object' &&
'code' in err &&
err.code === 'ENOENT'
) {
return []
}

throw err
}

trackDirectory?.(directory)

const subdirectories: string[] = []
for (const entry of dir) {
if (entry.isDirectory()) {
// Directories that start with an underscore are excluded from routing, so we shouldn't look for layouts inside.
if (entry.name[0] === '_') {
continue
}
// Parallel routes cannot occur above a layout, so they can't contain a root layout.
if (entry.name[0] === '@') {
continue
}

const absolutePathname = path.join(directory, entry.name)
subdirectories.push(absolutePathname)
} else if (entry.isFile()) {
if (layoutFilenameRegex.test(entry.name)) {
// We found a root layout, so we're not going to recurse into subdirectories,
// meaning that we can skip the rest of the entries.
// Note that we don't need to track any of the subdirectories as dependencies --
// changes in the subdirectories will only become relevant if this root layout is (re)moved,
// in which case the loader will re-run, traverse deeper (because it no longer stops at this root layout)
// and then track those directories as needed.
const rootLayoutPath = path.join(directory, entry.name)
return [rootLayoutPath]
}
}
}

if (subdirectories.length === 0) {
return []
}

const subdirectoryRootLayouts = await Promise.all(
subdirectories.map((subdirectory) => visit(subdirectory))
)
return subdirectoryRootLayouts.flat(1)
}

return visit(appDir)
}

function getParamsFromLayoutFilePath({
appDir,
layoutFilePath,
}: {
appDir: string
layoutFilePath: string
}): string[] {
const rootLayoutPath = normalizeAppPath(
ensureLeadingSlash(path.dirname(path.relative(appDir, layoutFilePath)))
)
const segments = rootLayoutPath.split('/')
const paramNames: string[] = []
for (const segment of segments) {
const param = getSegmentParam(segment)
if (param !== null) {
paramNames.push(param.param)
}
}
return paramNames
}