Skip to content

Commit 16510b4

Browse files
committed
webpack implementation
1 parent 8f50038 commit 16510b4

File tree

2 files changed

+184
-0
lines changed

2 files changed

+184
-0
lines changed

packages/next/src/build/webpack-config.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ import { getRspackCore, getRspackReactRefresh } from '../shared/lib/get-rspack'
9898
import { RspackProfilingPlugin } from './webpack/plugins/rspack-profiling-plugin'
9999
import getWebpackBundler from '../shared/lib/get-webpack-bundler'
100100
import type { NextBuildContext } from './build-context'
101+
import type { RootParamsLoaderOpts } from './webpack/loaders/next-root-params-loader'
101102

102103
type ExcludesFalse = <T>(x: T | false) => x is T
103104
type ClientEntries = {
@@ -1339,6 +1340,7 @@ export default async function getBaseWebpackConfig(
13391340
'modularize-import-loader',
13401341
'next-barrel-loader',
13411342
'next-error-browser-binary-loader',
1343+
'next-root-params-loader',
13421344
].reduce(
13431345
(alias, loader) => {
13441346
// using multiple aliases to replace `resolveLoader.modules`
@@ -1517,6 +1519,9 @@ export default async function getBaseWebpackConfig(
15171519
},
15181520
]
15191521
: []),
1522+
1523+
...getNextRootParamsRules({ isClient, appDir, pageExtensions }),
1524+
15201525
// TODO: FIXME: do NOT webpack 5 support with this
15211526
// x-ref: https://github.com/webpack/webpack/issues/11467
15221527
...(!config.experimental.fullySpecified
@@ -2717,3 +2722,47 @@ export default async function getBaseWebpackConfig(
27172722

27182723
return webpackConfig
27192724
}
2725+
2726+
function getNextRootParamsRules({
2727+
isClient,
2728+
appDir,
2729+
pageExtensions,
2730+
}: {
2731+
isClient: boolean
2732+
appDir: string | undefined
2733+
pageExtensions: string[]
2734+
}): webpack.RuleSetRule[] {
2735+
// Match resolved import of 'next/root-params'
2736+
const nextRootParamsModule = path.join(NEXT_PROJECT_ROOT, 'root-params.js')
2737+
2738+
// Handle invalid imports of 'next/root-params' that slip through our import validation.
2739+
const invalidImportRule = {
2740+
resource: nextRootParamsModule,
2741+
loader: 'next-invalid-import-error-loader',
2742+
options: {
2743+
message:
2744+
"'next/root-params' cannot be imported from a Client Component module. It should only be used from a Server Component.",
2745+
},
2746+
} satisfies webpack.RuleSetRule
2747+
2748+
if (isClient || !appDir) {
2749+
return [invalidImportRule]
2750+
}
2751+
2752+
return [
2753+
{
2754+
oneOf: [
2755+
{
2756+
resource: nextRootParamsModule,
2757+
issuerLayer: isWebpackServerOnlyLayer as (layer: string) => boolean,
2758+
loader: 'next-root-params-loader',
2759+
options: {
2760+
appDir,
2761+
pageExtensions,
2762+
} satisfies RootParamsLoaderOpts,
2763+
},
2764+
invalidImportRule,
2765+
],
2766+
},
2767+
]
2768+
}
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import type { webpack } from 'next/dist/compiled/webpack/webpack'
2+
import * as path from 'node:path'
3+
import * as fs from 'node:fs/promises'
4+
import { normalizeAppPath } from '../../../shared/lib/router/utils/app-paths'
5+
import { ensureLeadingSlash } from '../../../shared/lib/page-path/ensure-leading-slash'
6+
import { getSegmentParam } from '../../../server/app-render/get-segment-param'
7+
8+
export type RootParamsLoaderOpts = {
9+
appDir: string
10+
pageExtensions: string[]
11+
}
12+
13+
const rootParamsLoader: webpack.LoaderDefinitionFunction<RootParamsLoaderOpts> =
14+
async function () {
15+
const { appDir, pageExtensions } = this.getOptions()
16+
17+
const rootLayoutFilePaths = await findRootLayouts({
18+
appDir: appDir,
19+
pageExtensions,
20+
trackDirectory:
21+
// Track every directory we traverse in case a layout gets added to it
22+
// (which would make it the new root layout for that subtree).
23+
// This is relevant both in dev (for file watching) and in prod (for caching).
24+
(directory) => this.addContextDependency(directory),
25+
})
26+
27+
// Collect the param names from all root layouts.
28+
const allRootParams = new Set<string>()
29+
for (const rootLayoutFilePath of rootLayoutFilePaths) {
30+
const rootLayoutPath = normalizeAppPath(
31+
ensureLeadingSlash(
32+
path.dirname(path.relative(appDir, rootLayoutFilePath))
33+
)
34+
)
35+
const segments = rootLayoutPath.split('/')
36+
for (const segment of segments) {
37+
const param = getSegmentParam(segment)
38+
if (param !== null) {
39+
allRootParams.add(param.param)
40+
}
41+
}
42+
}
43+
44+
// If there's no root params, there's nothing to generate.
45+
if (allRootParams.size === 0) {
46+
return 'export {}'
47+
}
48+
49+
// Generate a getter for each root param we found.
50+
const sortedRootParamNames = Array.from(allRootParams).sort()
51+
const content = [
52+
`import { getRootParam } from 'next/dist/server/request/root-params';`,
53+
...sortedRootParamNames.map((paramName) => {
54+
return `export async function ${paramName}() { return getRootParam('${paramName}'); }`
55+
}),
56+
].join('\n')
57+
58+
return content
59+
}
60+
61+
async function findRootLayouts({
62+
appDir,
63+
pageExtensions,
64+
trackDirectory,
65+
}: {
66+
appDir: string
67+
pageExtensions: string[]
68+
trackDirectory: ((dirPath: string) => void) | undefined
69+
}) {
70+
const layoutFilenameRegex = new RegExp(
71+
`^layout\\.(?:${pageExtensions.join('|')})$`
72+
)
73+
74+
async function visit(directory: string): Promise<string[]> {
75+
let dir: Awaited<ReturnType<(typeof fs)['readdir']>>
76+
try {
77+
dir = await fs.readdir(directory, { withFileTypes: true })
78+
} catch (err) {
79+
// If the directory was removed before we managed to read it, just ignore it.
80+
if (
81+
err &&
82+
typeof err === 'object' &&
83+
'code' in err &&
84+
err.code === 'ENOENT'
85+
) {
86+
return []
87+
}
88+
89+
throw err
90+
}
91+
92+
trackDirectory?.(directory)
93+
94+
const subdirectories: string[] = []
95+
for (const entry of dir) {
96+
if (entry.isDirectory()) {
97+
// Directories that start with an underscore are excluded from routing, so we shouldn't look for layouts inside.
98+
if (entry.name[0] === '_') {
99+
continue
100+
}
101+
// Parallel routes cannot occur above a layout, so they can't contain a root layout.
102+
if (entry.name[0] === '@') {
103+
continue
104+
}
105+
106+
const absolutePathname = path.join(directory, entry.name)
107+
subdirectories.push(absolutePathname)
108+
} else if (entry.isFile()) {
109+
if (layoutFilenameRegex.test(entry.name)) {
110+
// We found a root layout, so we're not going to recurse into subdirectories,
111+
// meaning that we can skip the rest of the entries.
112+
// Note that we don't need to track any of the subdirectories as dependencies --
113+
// changes in the subdirectories will only become relevant if this root layout is (re)moved,
114+
// in which case the loader will re-run, traverse deeper (because it no longer stops at this root layout)
115+
// and then track those directories as needed.
116+
const rootLayoutPath = path.join(directory, entry.name)
117+
return [rootLayoutPath]
118+
}
119+
}
120+
}
121+
122+
if (subdirectories.length === 0) {
123+
return []
124+
}
125+
126+
const subdirectoryRootLayouts = await Promise.all(
127+
subdirectories.map((subdirectory) => visit(subdirectory))
128+
)
129+
return subdirectoryRootLayouts.flat(1)
130+
}
131+
132+
return visit(appDir)
133+
}
134+
135+
export default rootParamsLoader

0 commit comments

Comments
 (0)