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

Skip to content

Commit 13bd303

Browse files
committed
[webpack] generate types for next/root-params
1 parent a172a22 commit 13bd303

File tree

3 files changed

+107
-11
lines changed

3 files changed

+107
-11
lines changed

packages/next/errors.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -735,5 +735,6 @@
735735
"734": "\\`unstable_rootParams\\` must not be used within a client component. Next.js should be preventing it from being included in client components statically, but did not in this case.",
736736
"735": "Missing workStore in %s",
737737
"736": "Route %s used %s inside a Route Handler. Support for this API in Route Handlers is planned for a future version of Next.js.",
738-
"737": "%s was used inside a Server Action. This is not supported. Functions from 'next/root-params' can only be called in the context of a route."
738+
"737": "%s was used inside a Server Action. This is not supported. Functions from 'next/root-params' can only be called in the context of a route.",
739+
"738": "Unknown param kind %s"
739740
}

packages/next/src/build/webpack/loaders/next-root-params-loader.ts

Lines changed: 80 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,23 @@ import * as path from 'node:path'
33
import * as fs from 'node:fs/promises'
44
import { normalizeAppPath } from '../../../shared/lib/router/utils/app-paths'
55
import { ensureLeadingSlash } from '../../../shared/lib/page-path/ensure-leading-slash'
6+
import type { DynamicParamTypes } from '../../../server/app-render/types'
67
import { getSegmentParam } from '../../../server/app-render/get-segment-param'
8+
import { InvariantError } from '../../../shared/lib/invariant-error'
79

810
export type RootParamsLoaderOpts = {
911
appDir: string
1012
pageExtensions: string[]
1113
}
1214

13-
type CollectedRootParams = Set<string>
15+
export type CollectedRootParams = Map<string, Set<DynamicParamTypes>>
1416

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

1921
const allRootParams = await collectRootParamsFromFileSystem({
20-
appDir,
22+
appDir: appDir,
2123
pageExtensions,
2224
})
2325
// invalidate the result whenever a file/directory is added/removed inside the app dir or its subdirectories,
@@ -30,7 +32,7 @@ const rootParamsLoader: webpack.LoaderDefinitionFunction<RootParamsLoaderOpts> =
3032
}
3133

3234
// Generate a getter for each root param we found.
33-
const sortedRootParamNames = Array.from(allRootParams).sort()
35+
const sortedRootParamNames = Array.from(allRootParams.keys()).sort()
3436
const content = [
3537
`import { getRootParam } from 'next/dist/server/request/root-params';`,
3638
...sortedRootParamNames.map((paramName) => {
@@ -43,7 +45,7 @@ const rootParamsLoader: webpack.LoaderDefinitionFunction<RootParamsLoaderOpts> =
4345

4446
export default rootParamsLoader
4547

46-
async function collectRootParamsFromFileSystem(
48+
export async function collectRootParamsFromFileSystem(
4749
opts: Parameters<typeof findRootLayouts>[0]
4850
) {
4951
return collectRootParams({
@@ -59,15 +61,22 @@ function collectRootParams({
5961
rootLayoutFilePaths: string[]
6062
appDir: string
6163
}): CollectedRootParams {
62-
const allRootParams: CollectedRootParams = new Set()
64+
// Collect the param names and kinds from all root layouts.
65+
// Note that if multiple root layouts use the same param name, it can have multiple kinds.
66+
const allRootParams: CollectedRootParams = new Map()
6367

6468
for (const rootLayoutFilePath of rootLayoutFilePaths) {
6569
const params = getParamsFromLayoutFilePath({
6670
appDir,
6771
layoutFilePath: rootLayoutFilePath,
6872
})
6973
for (const param of params) {
70-
allRootParams.add(param)
74+
const { param: paramName, type: paramKind } = param
75+
let paramKinds = allRootParams.get(paramName)
76+
if (!paramKinds) {
77+
allRootParams.set(paramName, (paramKinds = new Set()))
78+
}
79+
paramKinds.add(paramKind)
7180
}
7281
}
7382

@@ -144,23 +153,84 @@ async function findRootLayouts({
144153
return visit(appDir)
145154
}
146155

156+
type ParamInfo = { param: string; type: DynamicParamTypes }
157+
147158
function getParamsFromLayoutFilePath({
148159
appDir,
149160
layoutFilePath,
150161
}: {
151162
appDir: string
152163
layoutFilePath: string
153-
}): string[] {
164+
}): ParamInfo[] {
154165
const rootLayoutPath = normalizeAppPath(
155166
ensureLeadingSlash(path.dirname(path.relative(appDir, layoutFilePath)))
156167
)
157168
const segments = rootLayoutPath.split('/')
158-
const paramNames: string[] = []
169+
const params: ParamInfo[] = []
159170
for (const segment of segments) {
160171
const param = getSegmentParam(segment)
161172
if (param !== null) {
162-
paramNames.push(param.param)
173+
params.push(param)
174+
}
175+
}
176+
return params
177+
}
178+
179+
//=============================================
180+
// Type declarations
181+
//=============================================
182+
183+
export function generateDeclarations(rootParams: CollectedRootParams) {
184+
const sortedRootParamNames = Array.from(rootParams.keys()).sort()
185+
const declarationLines = sortedRootParamNames
186+
.map((paramName) => {
187+
// A param can have multiple kinds (in different root layouts).
188+
// In that case, we'll need to union the types together together.
189+
const paramKinds = Array.from(rootParams.get(paramName)!)
190+
const possibleTypesForParam = paramKinds.map((kind) =>
191+
getTypescriptTypeFromParamKind(kind)
192+
)
193+
// A root param getter can be called
194+
// - in a route handler (not yet implemented)
195+
// - a server action (unsupported)
196+
// - in another root layout that doesn't share the same root params.
197+
// For this reason, we currently always want `... | undefined` in the type.
198+
possibleTypesForParam.push(`undefined`)
199+
200+
const paramType = unionTsTypes(possibleTypesForParam)
201+
202+
return [
203+
` /** Allows reading the '${paramName}' root param. */`,
204+
` export function ${paramName}(): Promise<${paramType}>`,
205+
].join('\n')
206+
})
207+
.join('\n\n')
208+
209+
return `declare module 'next/root-params' {\n${declarationLines}\n}\n`
210+
}
211+
212+
function getTypescriptTypeFromParamKind(kind: DynamicParamTypes): string {
213+
switch (kind) {
214+
case 'catchall':
215+
case 'catchall-intercepted': {
216+
return `string[]`
217+
}
218+
case 'optional-catchall': {
219+
return `string[] | undefined`
220+
}
221+
case 'dynamic':
222+
case 'dynamic-intercepted': {
223+
return `string`
224+
}
225+
default: {
226+
kind satisfies never
227+
throw new InvariantError(`Unknown param kind ${kind}`)
163228
}
164229
}
165-
return paramNames
230+
}
231+
232+
function unionTsTypes(types: string[]) {
233+
if (types.length === 0) return 'never'
234+
if (types.length === 1) return types[0]
235+
return types.map((type) => `(${type})`).join(' | ')
166236
}

packages/next/src/build/webpack/plugins/next-types-plugin/index.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ import type { PageExtensions } from '../../../page-extensions-type'
1818
import { devPageFiles } from './shared'
1919
import { getProxiedPluginState } from '../../../build-context'
2020
import type { CacheLife } from '../../../../server/use-cache/cache-life'
21+
import {
22+
collectRootParamsFromFileSystem,
23+
generateDeclarations,
24+
} from '../../loaders/next-root-params-loader'
2125

2226
const PLUGIN_NAME = 'NextTypesPlugin'
2327

@@ -237,6 +241,7 @@ async function collectNamedSlots(layoutPath: string) {
237241
// possible to provide the same experience for dynamic routes.
238242

239243
const pluginState = getProxiedPluginState({
244+
// used for unstable_rootParams()
240245
collectedRootParams: {} as Record<string, string[]>,
241246
routeTypes: {
242247
edge: {
@@ -1038,6 +1043,14 @@ export class NextTypesPlugin {
10381043
pluginState.routeTypes.node.static = []
10391044
}
10401045

1046+
// We can't rely on webpack's module graph, because in dev we do on-demand compilation,
1047+
// so we'd miss the layouts that haven't been compiled yet
1048+
const rootParamsPromise = collectRootParamsFromFileSystem({
1049+
appDir: this.appDir,
1050+
pageExtensions: this.pageExtensions,
1051+
trackDirectory: undefined,
1052+
})
1053+
10411054
compilation.chunkGroups.forEach((chunkGroup) => {
10421055
chunkGroup.chunks.forEach((chunk) => {
10431056
if (!chunk.name) return
@@ -1076,6 +1089,7 @@ export class NextTypesPlugin {
10761089

10771090
await Promise.all(promises)
10781091

1092+
// unstable_rootParams()
10791093
const rootParams = getRootParamsFromLayouts(
10801094
pluginState.collectedRootParams
10811095
)
@@ -1095,6 +1109,17 @@ export class NextTypesPlugin {
10951109
)
10961110
}
10971111

1112+
// next/root-params
1113+
const collectedRootParams = await rootParamsPromise
1114+
const rootParamsTypesPath = path.join(
1115+
assetDirRelative,
1116+
'types/root-params.d.ts'
1117+
)
1118+
compilation.emitAsset(
1119+
rootParamsTypesPath,
1120+
new sources.RawSource(generateDeclarations(collectedRootParams))
1121+
)
1122+
10981123
// Support `"moduleResolution": "Node16" | "NodeNext"` with `"type": "module"`
10991124

11001125
const packageJsonAssetPath = path.join(

0 commit comments

Comments
 (0)