rsnext/packages/next/build/webpack/loaders/next-app-loader.ts
Tim Neutkens 833a67ff8c
Rename 404 -> not-found for new router (#40941)
As discussed with @sebmarkbage, the handling is more about showing a not found component than it is about a specific status code as these can come in late with streaming.



## Bug

- [ ] Related issues linked using `fixes #number`
- [ ] Integration tests added
- [ ] Errors have a helpful link attached, see `contributing.md`

## Feature

- [ ] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR.
- [ ] Related issues linked using `fixes #number`
- [ ] Integration tests added
- [ ] Documentation added
- [ ] Telemetry added. In case of a feature if it's used or not.
- [ ] Errors have a helpful link attached, see `contributing.md`

## Documentation / Examples

- [ ] Make sure the linting passes by running `pnpm lint`
- [ ] The "examples guidelines" are followed from [our contributing doc](https://github.com/vercel/next.js/blob/canary/contributing/examples/adding-examples.md)
2022-09-27 13:50:21 +00:00

209 lines
6.8 KiB
TypeScript

import type webpack from 'webpack'
import type { ValueOf } from '../../../shared/lib/constants'
import { NODE_RESOLVE_OPTIONS } from '../../webpack-config'
import { getModuleBuildInfo } from './get-module-build-info'
export const FILE_TYPES = {
layout: 'layout',
template: 'template',
error: 'error',
loading: 'loading',
'not-found': 'not-found',
} as const
// TODO-APP: check if this can be narrowed.
type ComponentModule = () => any
export type ComponentsType = {
readonly [componentKey in ValueOf<typeof FILE_TYPES>]?: ComponentModule
} & {
readonly layoutOrPagePath?: string
readonly page?: ComponentModule
}
async function createTreeCodeFromPath({
pagePath,
resolve,
resolveParallelSegments,
}: {
pagePath: string
resolve: (pathname: string) => Promise<string | undefined>
resolveParallelSegments: (
pathname: string
) => [key: string, segment: string][]
}) {
const splittedPath = pagePath.split(/[\\/]/)
const appDirPrefix = splittedPath[0]
async function createSubtreePropsFromSegmentPath(
segments: string[]
): Promise<string> {
const segmentPath = segments.join('/')
// Existing tree are the children of the current segment
const props: Record<string, string> = {}
// We need to resolve all parallel routes in this level.
const parallelSegments: [key: string, segment: string][] = []
if (segments.length === 0) {
parallelSegments.push(['children', ''])
} else {
parallelSegments.push(...resolveParallelSegments(segmentPath))
}
for (const [parallelKey, parallelSegment] of parallelSegments) {
const parallelSegmentPath = segmentPath + '/' + parallelSegment
if (parallelSegment === 'page') {
const matchedPagePath = `${appDirPrefix}${parallelSegmentPath}`
const resolvedPagePath = await resolve(matchedPagePath)
// Use '' for segment as it's the page. There can't be a segment called '' so this is the safest way to add it.
props[parallelKey] = `['', {}, {layoutOrPagePath: ${JSON.stringify(
resolvedPagePath
)}, page: () => require(${JSON.stringify(resolvedPagePath)})}]`
continue
}
const subtree = await createSubtreePropsFromSegmentPath([
...segments,
parallelSegment,
])
// `page` is not included here as it's added above.
const filePaths = await Promise.all(
Object.values(FILE_TYPES).map(async (file) => {
return [
file,
await resolve(`${appDirPrefix}${parallelSegmentPath}/${file}`),
] as const
})
)
props[parallelKey] = `[
'${parallelSegment}',
${subtree},
{
${filePaths
.filter(([, filePath]) => filePath !== undefined)
.map(([file, filePath]) => {
if (filePath === undefined) {
return ''
}
return `${
file === FILE_TYPES.layout
? `layoutOrPagePath: '${filePath}',`
: ''
}'${file}': () => require(${JSON.stringify(filePath)}),`
})
.join('\n')}
}
]`
}
return `{
${Object.entries(props)
.map(([key, value]) => `${key}: ${value}`)
.join(',\n')}
}`
}
const tree = await createSubtreePropsFromSegmentPath([])
return `const tree = ${tree}.children;`
}
function createAbsolutePath(appDir: string, pathToTurnAbsolute: string) {
return pathToTurnAbsolute.replace(/^private-next-app-dir/, appDir)
}
const nextAppLoader: webpack.LoaderDefinitionFunction<{
name: string
pagePath: string
appDir: string
appPaths: string[] | null
pageExtensions: string[]
nextRuntime: string
}> = async function nextAppLoader() {
const { name, appDir, appPaths, pagePath, pageExtensions, nextRuntime } =
this.getOptions() || {}
const buildInfo = getModuleBuildInfo((this as any)._module)
buildInfo.route = {
page: name.replace(/^app/, ''),
absolutePagePath: createAbsolutePath(appDir, pagePath),
}
const extensions = pageExtensions.map((extension) => `.${extension}`)
const resolveOptions: any = {
...NODE_RESOLVE_OPTIONS,
extensions,
}
const resolve = this.getResolve(resolveOptions)
const normalizedAppPaths =
typeof appPaths === 'string' ? [appPaths] : appPaths || []
const resolveParallelSegments = (pathname: string) => {
const matched: Record<string, string> = {}
for (const path of normalizedAppPaths) {
if (path.startsWith(pathname + '/')) {
const restPath = path.slice(pathname.length + 1)
const matchedSegment = restPath.split('/')[0]
const matchedKey = matchedSegment.startsWith('@')
? matchedSegment.slice(1)
: 'children'
matched[matchedKey] = matchedSegment
}
}
return Object.entries(matched)
}
const resolver = async (pathname: string) => {
try {
const resolved = await resolve(this.rootContext, pathname)
this.addDependency(resolved)
return resolved
} catch (err: any) {
const absolutePath = createAbsolutePath(appDir, pathname)
for (const ext of extensions) {
const absolutePathWithExtension = `${absolutePath}${ext}`
this.addMissingDependency(absolutePathWithExtension)
}
if (err.message.includes("Can't resolve")) {
return undefined
}
throw err
}
}
const treeCode = await createTreeCodeFromPath({
pagePath,
resolve: resolver,
resolveParallelSegments,
})
const rootDistFolder = nextRuntime === 'edge' ? 'next/dist/esm' : 'next/dist'
const result = `
export ${treeCode}
export const AppRouter = require('${rootDistFolder}/client/components/app-router.client.js').default
export const LayoutRouter = require('${rootDistFolder}/client/components/layout-router.client.js').default
export const RenderFromTemplateContext = require('${rootDistFolder}/client/components/render-from-template-context.client.js').default
export const HotReloader = ${
// Disable HotReloader component in production
this.mode === 'development'
? `require('${rootDistFolder}/client/components/hot-reloader.client.js').default`
: 'null'
}
export const staticGenerationAsyncStorage = require('${rootDistFolder}/client/components/static-generation-async-storage.js').staticGenerationAsyncStorage
export const requestAsyncStorage = require('${rootDistFolder}/client/components/request-async-storage.js').requestAsyncStorage
export const serverHooks = require('${rootDistFolder}/client/components/hooks-server-context.js')
export const renderToReadableStream = require('next/dist/compiled/react-server-dom-webpack/writer.browser.server').renderToReadableStream
export const __next_app_webpack_require__ = __webpack_require__
`
return result
}
export default nextAppLoader