diff --git a/.github/workflows/build_test_deploy.yml b/.github/workflows/build_test_deploy.yml index fe8dfad1dc11..fe28799b023e 100644 --- a/.github/workflows/build_test_deploy.yml +++ b/.github/workflows/build_test_deploy.yml @@ -51,7 +51,7 @@ jobs: run: sudo ethtool -K eth0 tx off rx off - name: Check non-docs only change - run: echo ::set-output name=DOCS_CHANGE::$(node scripts/run-for-change.js --not --type docs --exec echo 'nope') + run: echo "::set-output name=DOCS_CHANGE::$(node scripts/run-for-change.js --not --type docs --exec echo 'nope')" id: docs-change - run: echo ${{steps.docs-change.outputs.DOCS_CHANGE}} @@ -124,7 +124,7 @@ jobs: path: ./* key: ${{ github.sha }}-${{ github.run_number }} - - run: echo ::set-output name=SWC_CHANGE::$(node scripts/run-for-change.js --type next-swc --exec echo 'yup') + - run: echo "::set-output name=SWC_CHANGE::$(node scripts/run-for-change.js --type next-swc --exec echo 'yup')" id: swc-change - run: echo ${{ steps.swc-change.outputs.SWC_CHANGE }} @@ -1102,7 +1102,7 @@ jobs: with: fetch-depth: 25 - - run: echo ::set-output name=DOCS_CHANGE::$(node scripts/run-for-change.js --not --type docs --exec echo 'nope') + - run: echo "::set-output name=DOCS_CHANGE::$(node scripts/run-for-change.js --not --type docs --exec echo 'nope')" id: docs-change - name: Setup node @@ -1191,7 +1191,7 @@ jobs: with: fetch-depth: 25 - - run: echo ::set-output name=SWC_CHANGE::$(node scripts/run-for-change.js --type next-swc --exec echo 'yup') + - run: echo "::set-output name=SWC_CHANGE::$(node scripts/run-for-change.js --type next-swc --exec echo 'yup')" id: swc-change - run: echo ${{ steps.swc-change.outputs.SWC_CHANGE }} diff --git a/.github/workflows/pull_request_stats.yml b/.github/workflows/pull_request_stats.yml index 5a9dc25f116d..6a5abdc20436 100644 --- a/.github/workflows/pull_request_stats.yml +++ b/.github/workflows/pull_request_stats.yml @@ -24,7 +24,7 @@ jobs: fetch-depth: 25 - name: Check non-docs only change - run: echo ::set-output name=DOCS_CHANGE::$(node scripts/run-for-change.js --not --type docs --exec echo 'nope') + run: echo "::set-output name=DOCS_CHANGE::$(node scripts/run-for-change.js --not --type docs --exec echo 'nope')" id: docs-change - name: Setup node @@ -118,7 +118,7 @@ jobs: fetch-depth: 25 - name: Check non-docs only change - run: echo ::set-output name=DOCS_CHANGE::$(node scripts/run-for-change.js --not --type docs --exec echo 'nope') + run: echo "::set-output name=DOCS_CHANGE::$(node scripts/run-for-change.js --not --type docs --exec echo 'nope')" id: docs-change - uses: actions/download-artifact@v3 diff --git a/packages/next/build/entries.ts b/packages/next/build/entries.ts index aed59e8e069d..acd695ece191 100644 --- a/packages/next/build/entries.ts +++ b/packages/next/build/entries.ts @@ -46,6 +46,7 @@ import { normalizePathSep } from '../shared/lib/page-path/normalize-path-sep' import { normalizePagePath } from '../shared/lib/page-path/normalize-page-path' import { serverComponentRegex } from './webpack/loaders/utils' import { ServerRuntime } from '../types' +import { normalizeAppPath } from '../shared/lib/router/utils/app-paths' import { encodeMatchers } from './webpack/loaders/next-middleware-loader' type ObjectValue = T extends { [key: string]: infer V } ? V : never @@ -223,6 +224,7 @@ export function getAppEntry(opts: { name: string pagePath: string appDir: string + appPaths: string[] | null pageExtensions: string[] }) { return { @@ -353,6 +355,22 @@ export async function createEntrypoints(params: CreateEntrypointsParams) { const nestedMiddleware: string[] = [] let middlewareMatchers: MiddlewareMatcher[] | undefined = undefined + let appPathsPerRoute: Record = {} + if (appDir && appPaths) { + for (const pathname in appPaths) { + const normalizedPath = normalizeAppPath(pathname) || '/' + if (!appPathsPerRoute[normalizedPath]) { + appPathsPerRoute[normalizedPath] = [] + } + appPathsPerRoute[normalizedPath].push(pathname) + } + + // Make sure to sort parallel routes to make the result deterministic. + appPathsPerRoute = Object.fromEntries( + Object.entries(appPathsPerRoute).map(([k, v]) => [k, v.sort()]) + ) + } + const getEntryHandler = (mappings: Record, pagesType: 'app' | 'pages' | 'root') => async (page: string) => { @@ -431,10 +449,13 @@ export async function createEntrypoints(params: CreateEntrypointsParams) { }, onServer: () => { if (pagesType === 'app' && appDir) { + const matchedAppPaths = + appPathsPerRoute[normalizeAppPath(page) || '/'] server[serverBundlePath] = getAppEntry({ name: serverBundlePath, pagePath: mappings[page], appDir, + appPaths: matchedAppPaths, pageExtensions, }) } else if (isTargetLikeServerless(target)) { @@ -450,15 +471,18 @@ export async function createEntrypoints(params: CreateEntrypointsParams) { } }, onEdgeServer: () => { - const appDirLoader = - pagesType === 'app' - ? getAppEntry({ - name: serverBundlePath, - pagePath: mappings[page], - appDir: appDir!, - pageExtensions, - }).import - : '' + let appDirLoader: string = '' + if (pagesType === 'app') { + const matchedAppPaths = + appPathsPerRoute[normalizeAppPath(page) || '/'] + appDirLoader = getAppEntry({ + name: serverBundlePath, + pagePath: mappings[page], + appDir: appDir!, + appPaths: matchedAppPaths, + pageExtensions, + }).import + } edgeServer[serverBundlePath] = getEdgeServerEntry({ ...params, diff --git a/packages/next/build/index.ts b/packages/next/build/index.ts index 5f107284eb66..32786ae48f07 100644 --- a/packages/next/build/index.ts +++ b/packages/next/build/index.ts @@ -550,7 +550,9 @@ export default async function build( const pageKeys = { pages: Object.keys(mappedPages), app: mappedAppPages - ? Object.keys(mappedAppPages).map((key) => normalizeAppPath(key)) + ? Object.keys(mappedAppPages).map( + (key) => normalizeAppPath(key) || '/' + ) : undefined, } diff --git a/packages/next/build/webpack/loaders/next-app-loader.ts b/packages/next/build/webpack/loaders/next-app-loader.ts index f5fd9808bbb5..16c89ce73b35 100644 --- a/packages/next/build/webpack/loaders/next-app-loader.ts +++ b/packages/next/build/webpack/loaders/next-app-loader.ts @@ -5,86 +5,104 @@ import { getModuleBuildInfo } from './get-module-build-info' async function createTreeCodeFromPath({ pagePath, resolve, - removeExt, + resolveParallelSegments, }: { pagePath: string resolve: (pathname: string) => Promise - removeExt: (pathToRemoveExtensions: string) => string + resolveParallelSegments: ( + pathname: string + ) => [key: string, segment: string][] }) { - let tree: undefined | string const splittedPath = pagePath.split(/[\\/]/) const appDirPrefix = splittedPath[0] - const segments = ['', ...splittedPath.slice(1)] - - // segment.length - 1 because arrays start at 0 and we're decrementing - for (let i = segments.length - 1; i >= 0; i--) { - const segment = removeExt(segments[i]) - const segmentPath = segments.slice(0, i + 1).join('/') - - // First item in the list is the page which can't have layouts by itself - if (i === segments.length - 1) { - const resolvedPagePath = await resolve(pagePath) - // Use '' for segment as it's the page. There can't be a segment called '' so this is the safest way to add it. - tree = `['', {}, {filePath: ${JSON.stringify( - resolvedPagePath - )}, page: () => require(${JSON.stringify(resolvedPagePath)})}]` - continue - } - - // For segmentPath === '' avoid double `/` - const layoutPath = `${appDirPrefix}${segmentPath}/layout` - // For segmentPath === '' avoid double `/` - const loadingPath = `${appDirPrefix}${segmentPath}/loading` - - const resolvedLayoutPath = await resolve(layoutPath) - const resolvedLoadingPath = await resolve(loadingPath) + async function createSubtreePropsFromSegmentPath( + segments: string[] + ): Promise { + const segmentPath = segments.join('/') // Existing tree are the children of the current segment - const children = tree + const props: Record = {} + + // 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)) + } - tree = `['${segment}', { - ${ - // When there are no children the current index is the page component - children ? `children: ${children},` : '' + 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] = `['', {}, {filePath: ${JSON.stringify( + resolvedPagePath + )}, page: () => require(${JSON.stringify(resolvedPagePath)})}]` + continue } - }, { - filePath: ${JSON.stringify(resolvedLayoutPath)}, - ${ - resolvedLayoutPath - ? `layout: () => require(${JSON.stringify(resolvedLayoutPath)}),` - : '' - } - ${ - resolvedLoadingPath - ? `loading: () => require(${JSON.stringify(resolvedLoadingPath)}),` - : '' - } - }]` + + const subtree = await createSubtreePropsFromSegmentPath([ + ...segments, + parallelSegment, + ]) + + // For segmentPath === '' avoid double `/` + const layoutPath = `${appDirPrefix}${parallelSegmentPath}/layout` + // For segmentPath === '' avoid double `/` + const loadingPath = `${appDirPrefix}${parallelSegmentPath}/loading` + + const resolvedLayoutPath = await resolve(layoutPath) + const resolvedLoadingPath = await resolve(loadingPath) + + props[parallelKey] = `[ + '${parallelSegment}', + ${subtree}, + { + filePath: ${JSON.stringify(resolvedLayoutPath)}, + ${ + resolvedLayoutPath + ? `layout: () => require(${JSON.stringify(resolvedLayoutPath)}),` + : '' + } + ${ + resolvedLoadingPath + ? `loading: () => require(${JSON.stringify( + resolvedLoadingPath + )}),` + : '' + } + } + ]` + } + + return `{ + ${Object.entries(props) + .map(([key, value]) => `${key}: ${value}`) + .join(',\n')} + }` } - return `const tree = ${tree};` + const tree = await createSubtreePropsFromSegmentPath([]) + return `const tree = ${tree}.children;` } function createAbsolutePath(appDir: string, pathToTurnAbsolute: string) { return pathToTurnAbsolute.replace(/^private-next-app-dir/, appDir) } -function removeExtensions( - extensions: string[], - pathToRemoveExtensions: string -) { - const regex = new RegExp(`(${extensions.join('|')})$`.replace(/\./g, '\\.')) - return pathToRemoveExtensions.replace(regex, '') -} - const nextAppLoader: webpack.LoaderDefinitionFunction<{ name: string pagePath: string appDir: string + appPaths: string[] | null pageExtensions: string[] }> = async function nextAppLoader() { - const { name, appDir, pagePath, pageExtensions } = this.getOptions() || {} + const { name, appDir, appPaths, pagePath, pageExtensions } = + this.getOptions() || {} const buildInfo = getModuleBuildInfo((this as any)._module) buildInfo.route = { @@ -99,6 +117,24 @@ const nextAppLoader: webpack.LoaderDefinitionFunction<{ } const resolve = this.getResolve(resolveOptions) + const normalizedAppPaths = + typeof appPaths === 'string' ? [appPaths] : appPaths || [] + const resolveParallelSegments = (pathname: string) => { + const matched: Record = {} + 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) @@ -120,7 +156,7 @@ const nextAppLoader: webpack.LoaderDefinitionFunction<{ const treeCode = await createTreeCodeFromPath({ pagePath, resolve: resolver, - removeExt: (p) => removeExtensions(extensions, p), + resolveParallelSegments, }) const result = ` diff --git a/packages/next/server/base-server.ts b/packages/next/server/base-server.ts index 29b732fcd3a1..d2e5db5bf051 100644 --- a/packages/next/server/base-server.ts +++ b/packages/next/server/base-server.ts @@ -225,7 +225,7 @@ export default abstract class Server { private responseCache: ResponseCacheBase protected router: Router protected dynamicRoutes?: DynamicRoutes - protected appPathRoutes?: Record + protected appPathRoutes?: Record protected customRoutes: CustomRoutes protected serverComponentManifest?: any protected serverCSSManifest?: any @@ -239,12 +239,13 @@ export default abstract class Server { protected abstract getBuildId(): string protected abstract getFilesystemPaths(): Set - protected abstract findPageComponents( - pathname: string, - query: NextParsedUrlQuery, - params: Params, + protected abstract findPageComponents(params: { + pathname: string + query: NextParsedUrlQuery + params: Params isAppPath: boolean - ): Promise + appPaths?: string[] | null + }): Promise protected abstract getFontManifest(): FontManifest | undefined protected abstract getPrerenderManifest(): PrerenderManifest protected abstract getServerComponentManifest(): any @@ -758,11 +759,15 @@ export default abstract class Server { .filter((item): item is RoutingItem => Boolean(item)) } - protected getAppPathRoutes(): Record { - const appPathRoutes: Record = {} + protected getAppPathRoutes(): Record { + const appPathRoutes: Record = {} Object.keys(this.appPathsManifest || {}).forEach((entry) => { - appPathRoutes[normalizeAppPath(entry) || '/'] = entry + const normalizedPath = normalizeAppPath(entry) || '/' + if (!appPathRoutes[normalizedPath]) { + appPathRoutes[normalizedPath] = [] + } + appPathRoutes[normalizedPath].push(entry) }) return appPathRoutes } @@ -1504,7 +1509,7 @@ export default abstract class Server { } // map the route to the actual bundle name - protected getOriginalAppPath(route: string) { + protected getOriginalAppPaths(route: string) { if (this.nextConfig.experimental.appDir) { const originalAppPath = this.appPathRoutes?.[route] @@ -1523,18 +1528,22 @@ export default abstract class Server { ) { const { query, pathname } = ctx + const appPaths = this.getOriginalAppPaths(pathname) + let page = pathname - const appPath = this.getOriginalAppPath(pathname) - if (typeof appPath === 'string') { - page = appPath + if (Array.isArray(appPaths)) { + // When it's an array, we need to pass all parallel routes to the loader. + page = appPaths[0] } - const result = await this.findPageComponents( - page, + const result = await this.findPageComponents({ + pathname: page, query, - ctx.renderOpts.params || {}, - typeof appPath === 'string' - ) + params: ctx.renderOpts.params || {}, + isAppPath: Array.isArray(appPaths), + appPaths, + }) + if (result) { try { return await this.renderToResponseWithComponents(ctx, result) @@ -1737,7 +1746,12 @@ export default abstract class Server { // use static 404 page if available and is 404 response if (is404 && (await this.hasPage('/404'))) { - result = await this.findPageComponents('/404', query, {}, false) + result = await this.findPageComponents({ + pathname: '/404', + query, + params: {}, + isAppPath: false, + }) using404Page = result !== null } let statusPage = `/${res.statusCode}` @@ -1750,12 +1764,22 @@ export default abstract class Server { // skip ensuring /500 in dev mode as it isn't used and the // dev overlay is used instead if (statusPage !== '/500' || !this.renderOpts.dev) { - result = await this.findPageComponents(statusPage, query, {}, false) + result = await this.findPageComponents({ + pathname: statusPage, + query, + params: {}, + isAppPath: false, + }) } } if (!result) { - result = await this.findPageComponents('/_error', query, {}, false) + result = await this.findPageComponents({ + pathname: '/_error', + query, + params: {}, + isAppPath: false, + }) statusPage = '/_error' } diff --git a/packages/next/server/dev/hot-reloader.ts b/packages/next/server/dev/hot-reloader.ts index 921706767ce1..8e1d195b7c7b 100644 --- a/packages/next/server/dev/hot-reloader.ts +++ b/packages/next/server/dev/hot-reloader.ts @@ -272,7 +272,7 @@ export default class HotReloader { if (page === '/_error' || BLOCKED_PAGES.indexOf(page) === -1) { try { - await this.ensurePage(page, true) + await this.ensurePage({ page, clientOnly: true }) } catch (error) { await renderScriptError(pageBundleRes, getProperError(error)) return { finished: true } @@ -616,6 +616,7 @@ export default class HotReloader { isApp && this.appDir ? getAppEntry({ name: bundlePath, + appPaths: entryData.appPaths, pagePath: posix.join( APP_DIR_ALIAS, relative( @@ -693,6 +694,7 @@ export default class HotReloader { this.appDir && bundlePath.startsWith('app/') ? getAppEntry({ name: bundlePath, + appPaths: entryData.appPaths, pagePath: posix.join( APP_DIR_ALIAS, relative( @@ -1072,7 +1074,15 @@ export default class HotReloader { ) } - public async ensurePage(page: string, clientOnly: boolean): Promise { + public async ensurePage({ + page, + clientOnly, + appPaths, + }: { + page: string + clientOnly: boolean + appPaths?: string[] | null + }): Promise { // Make sure we don't re-build or dispose prebuilt pages if (page !== '/_error' && BLOCKED_PAGES.indexOf(page) !== -1) { return @@ -1083,6 +1093,10 @@ export default class HotReloader { if (error) { return Promise.reject(error) } - return this.onDemandEntries?.ensurePage(page, clientOnly) as any + return this.onDemandEntries?.ensurePage({ + page, + clientOnly, + appPaths, + }) as any } } diff --git a/packages/next/server/dev/next-dev-server.ts b/packages/next/server/dev/next-dev-server.ts index 78f4656d8532..920a6264845d 100644 --- a/packages/next/server/dev/next-dev-server.ts +++ b/packages/next/server/dev/next-dev-server.ts @@ -311,7 +311,7 @@ export default class DevServer extends Server { let middlewareMatchers: MiddlewareMatcher[] | undefined const routedPages: string[] = [] const knownFiles = wp.getTimeInfoEntries() - const appPaths: Record = {} + const appPaths: Record = {} const edgeRoutesSet = new Set() let envChange = false @@ -396,7 +396,10 @@ export default class DevServer extends Server { const originalPageName = pageName pageName = normalizeAppPath(pageName) || '/' - appPaths[pageName] = originalPageName + if (!appPaths[pageName]) { + appPaths[pageName] = [] + } + appPaths[pageName].push(originalPageName) if (routedPages.includes(pageName)) { continue @@ -542,13 +545,15 @@ export default class DevServer extends Server { nestedMiddleware = [] } - this.appPathRoutes = appPaths + // Make sure to sort parallel routes to make the result deterministic. + this.appPathRoutes = Object.fromEntries( + Object.entries(appPaths).map(([k, v]) => [k, v.sort()]) + ) const edgeRoutes = Array.from(edgeRoutesSet) this.edgeFunctions = getSortedRoutes(edgeRoutes).map((page) => { - const appPath = this.getOriginalAppPath(page) - - if (typeof appPath === 'string') { - page = appPath + const matchedAppPaths = this.getOriginalAppPaths(page) + if (Array.isArray(matchedAppPaths)) { + page = matchedAppPaths[0] } const edgeRegex = getRouteRegex(page) return { @@ -902,6 +907,7 @@ export default class DevServer extends Server { query: ParsedUrlQuery params: Params | undefined page: string + appPaths: string[] | null isAppPath: boolean }) { try { @@ -1118,11 +1124,20 @@ export default class DevServer extends Server { } protected async ensureMiddleware() { - return this.hotReloader!.ensurePage(this.actualMiddlewareFile!, false) + return this.hotReloader!.ensurePage({ + page: this.actualMiddlewareFile!, + clientOnly: false, + }) } - protected async ensureEdgeFunction(pathname: string) { - return this.hotReloader!.ensurePage(pathname, false) + protected async ensureEdgeFunction({ + page, + appPaths, + }: { + page: string + appPaths: string[] | null + }) { + return this.hotReloader!.ensurePage({ page, appPaths, clientOnly: false }) } generateRoutes() { @@ -1288,15 +1303,22 @@ export default class DevServer extends Server { } protected async ensureApiPage(pathname: string): Promise { - return this.hotReloader!.ensurePage(pathname, false) + return this.hotReloader!.ensurePage({ page: pathname, clientOnly: false }) } - protected async findPageComponents( - pathname: string, - query: ParsedUrlQuery, - params: Params, + protected async findPageComponents({ + pathname, + query, + params, + isAppPath, + appPaths, + }: { + pathname: string + query: ParsedUrlQuery + params: Params isAppPath: boolean - ): Promise { + appPaths?: string[] | null + }): Promise { await this.devReady const compilationErr = await this.getCompilationError(pathname) if (compilationErr) { @@ -1304,7 +1326,11 @@ export default class DevServer extends Server { throw new WrappedBuildError(compilationErr) } try { - await this.hotReloader!.ensurePage(pathname, false) + await this.hotReloader!.ensurePage({ + page: pathname, + appPaths, + clientOnly: false, + }) const serverComponents = this.nextConfig.experimental.serverComponents @@ -1315,7 +1341,7 @@ export default class DevServer extends Server { this.serverCSSManifest = super.getServerCSSManifest() } - return super.findPageComponents(pathname, query, params, isAppPath) + return super.findPageComponents({ pathname, query, params, isAppPath }) } catch (err) { if ((err as any).code !== 'ENOENT') { throw err @@ -1328,7 +1354,7 @@ export default class DevServer extends Server { await this.hotReloader!.buildFallbackError() // Build the error page to ensure the fallback is built too. // TODO: See if this can be moved into hotReloader or removed. - await this.hotReloader!.ensurePage('/_error', false) + await this.hotReloader!.ensurePage({ page: '/_error', clientOnly: false }) return await loadDefaultErrorComponents(this.distDir) } diff --git a/packages/next/server/dev/on-demand-entry-handler.ts b/packages/next/server/dev/on-demand-entry-handler.ts index c3ce13b67ac6..a0a2fe8bf171 100644 --- a/packages/next/server/dev/on-demand-entry-handler.ts +++ b/packages/next/server/dev/on-demand-entry-handler.ts @@ -41,7 +41,9 @@ function treePathToEntrypoint( // TODO-APP: modify this path to cover parallelRouteKey convention const path = (parentPath ? parentPath + '/' : '') + - (parallelRouteKey !== 'children' ? parallelRouteKey + '/' : '') + + (parallelRouteKey !== 'children' && !segment.startsWith('@') + ? parallelRouteKey + '/' + : '') + (segment === '' ? 'page' : segment) // Last segment @@ -143,6 +145,11 @@ interface Entry extends EntryType { * `/Users/Rick/project/pages/about/index.js` */ absolutePagePath: string + /** + * All parallel pages that match the same entry, for example: + * ['/parallel/@bar/nested/@a/page', '/parallel/@bar/nested/@b/page', '/parallel/@foo/nested/@a/page', '/parallel/@foo/nested/@b/page'] + */ + appPaths: string[] | null } interface ChildEntry extends EntryType { @@ -499,6 +506,7 @@ export function onDemandEntryHandler({ toSend = { success: true } } } + return toSend } @@ -545,7 +553,15 @@ export function onDemandEntryHandler({ } return { - async ensurePage(page: string, clientOnly: boolean): Promise { + async ensurePage({ + page, + clientOnly, + appPaths = null, + }: { + page: string + clientOnly: boolean + appPaths?: string[] | null + }): Promise { const stalledTime = 60 const stalledEnsureTimeout = setTimeout(() => { debug( @@ -597,6 +613,7 @@ export function onDemandEntryHandler({ entries[entryKey] = { type: EntryTypes.ENTRY, + appPaths, absolutePagePath: pagePathData.absolutePagePath, request: pagePathData.absolutePagePath, bundlePath: pagePathData.bundlePath, diff --git a/packages/next/server/next-server.ts b/packages/next/server/next-server.ts index 5e39ac360fc7..d762c1a05388 100644 --- a/packages/next/server/next-server.ts +++ b/packages/next/server/next-server.ts @@ -759,6 +759,7 @@ export default class NextNodeServer extends BaseServer { query, params, page, + appPaths: null, isAppPath: false, }) @@ -889,40 +890,47 @@ export default class NextNodeServer extends BaseServer { ctx: RequestContext, bubbleNoFallback: boolean ) { - const appPath = this.getOriginalAppPath(ctx.pathname) || undefined - let page = ctx.pathname - - const isAppPath = typeof appPath === 'string' - if (isAppPath) { - page = appPath - } - const edgeFunctions = this.getEdgeFunctions() || [] + if (edgeFunctions.length) { + const appPaths = this.getOriginalAppPaths(ctx.pathname) + const isAppPath = Array.isArray(appPaths) + + let page = ctx.pathname + if (isAppPath) { + // When it's an array, we need to pass all parallel routes to the loader. + page = appPaths[0] + } - for (const item of edgeFunctions) { - if (item.page === page) { - await this.runEdgeFunction({ - req: ctx.req, - res: ctx.res, - query: ctx.query, - params: ctx.renderOpts.params, - page: ctx.pathname, - appPath, - isAppPath: isAppPath, - }) - return null + for (const item of edgeFunctions) { + if (item.page === page) { + await this.runEdgeFunction({ + req: ctx.req, + res: ctx.res, + query: ctx.query, + params: ctx.renderOpts.params, + page, + appPaths, + isAppPath, + }) + return null + } } } return super.renderPageComponent(ctx, bubbleNoFallback) } - protected async findPageComponents( - pathname: string, - query: NextParsedUrlQuery, - params: Params, + protected async findPageComponents({ + pathname, + query, + params, + isAppPath, + }: { + pathname: string + query: NextParsedUrlQuery + params: Params | null isAppPath: boolean - ): Promise { + }): Promise { let paths = [ // try serving a static AMP version first query.amp @@ -1668,7 +1676,10 @@ export default class NextNodeServer extends BaseServer { * so that we can run it. */ protected async ensureMiddleware() {} - protected async ensureEdgeFunction(_pathname: string) {} + protected async ensureEdgeFunction(_params: { + page: string + appPaths: string[] | null + }) {} /** * This method gets all middleware matchers and execute them when the request @@ -2016,17 +2027,16 @@ export default class NextNodeServer extends BaseServer { query: ParsedUrlQuery params: Params | undefined page: string + appPaths: string[] | null isAppPath: boolean - appPath?: string onWarning?: (warning: Error) => void }): Promise { let middlewareInfo: ReturnType | undefined - // If it's edge app route, use appPath to find the edge SSR page - const page = params.isAppPath ? params.appPath! : params.page - await this.ensureEdgeFunction(page) + const page = params.page + await this.ensureEdgeFunction({ page, appPaths: params.appPaths }) middlewareInfo = this.getEdgeFunctionInfo({ - page: page, + page, middleware: false, }) diff --git a/packages/next/server/web-server.ts b/packages/next/server/web-server.ts index 0b966acb9b8e..db19c6ce486d 100644 --- a/packages/next/server/web-server.ts +++ b/packages/next/server/web-server.ts @@ -404,12 +404,17 @@ export default class NextWebServer extends BaseServer { // @TODO return true } - protected async findPageComponents( - pathname: string, - query: NextParsedUrlQuery, - params: Params | null, - _isAppPath: boolean - ) { + + protected async findPageComponents({ + pathname, + query, + params, + }: { + pathname: string + query: NextParsedUrlQuery + params: Params | null + isAppPath: boolean + }) { const result = await this.serverOptions.webServerConfig.loadComponent( pathname ) diff --git a/packages/next/shared/lib/router/utils/app-paths.ts b/packages/next/shared/lib/router/utils/app-paths.ts index bf69f05e0902..f48b324d1a6c 100644 --- a/packages/next/shared/lib/router/utils/app-paths.ts +++ b/packages/next/shared/lib/router/utils/app-paths.ts @@ -10,6 +10,10 @@ export function normalizeAppPath(pathname: string) { return acc } + if (segment.startsWith('@')) { + return acc + } + if (segment === 'page' && index === segments.length - 1) { return acc } diff --git a/test/e2e/app-dir/app/app/parallel/(new)/@baz/nested-2/page.server.js b/test/e2e/app-dir/app/app/parallel/(new)/@baz/nested-2/page.server.js new file mode 100644 index 000000000000..05c172f81b1f --- /dev/null +++ b/test/e2e/app-dir/app/app/parallel/(new)/@baz/nested-2/page.server.js @@ -0,0 +1,3 @@ +export default function Page() { + return
parallel/(new)/@baz/nested/page
+} diff --git a/test/e2e/app-dir/app/app/parallel/(new)/layout.server.js b/test/e2e/app-dir/app/app/parallel/(new)/layout.server.js new file mode 100644 index 000000000000..491d122a9633 --- /dev/null +++ b/test/e2e/app-dir/app/app/parallel/(new)/layout.server.js @@ -0,0 +1,10 @@ +export default function Layout({ baz }) { + return ( +
+ parallel/(new)/layout: +
+ {baz} +
+
+ ) +} diff --git a/test/e2e/app-dir/app/app/parallel/@bar/nested/@a/page.server.js b/test/e2e/app-dir/app/app/parallel/@bar/nested/@a/page.server.js new file mode 100644 index 000000000000..d1c6fc71e12e --- /dev/null +++ b/test/e2e/app-dir/app/app/parallel/@bar/nested/@a/page.server.js @@ -0,0 +1,3 @@ +export default function Page() { + return
parallel/@bar/nested/@a/page
+} diff --git a/test/e2e/app-dir/app/app/parallel/@bar/nested/@b/page.server.js b/test/e2e/app-dir/app/app/parallel/@bar/nested/@b/page.server.js new file mode 100644 index 000000000000..15ef0b7a3992 --- /dev/null +++ b/test/e2e/app-dir/app/app/parallel/@bar/nested/@b/page.server.js @@ -0,0 +1,3 @@ +export default function Page() { + return
parallel/@bar/nested/@b/page
+} diff --git a/test/e2e/app-dir/app/app/parallel/@bar/nested/layout.server.js b/test/e2e/app-dir/app/app/parallel/@bar/nested/layout.server.js new file mode 100644 index 000000000000..4ba7b399385d --- /dev/null +++ b/test/e2e/app-dir/app/app/parallel/@bar/nested/layout.server.js @@ -0,0 +1,16 @@ +export default function Parallel({ a, b, children }) { + return ( +
+ parallel/@bar/nested/layout +
+ {a} +
+
+ {b} +
+
+ {children} +
+
+ ) +} diff --git a/test/e2e/app-dir/app/app/parallel/@bar/page.server.js b/test/e2e/app-dir/app/app/parallel/@bar/page.server.js new file mode 100644 index 000000000000..568f5ed06461 --- /dev/null +++ b/test/e2e/app-dir/app/app/parallel/@bar/page.server.js @@ -0,0 +1,3 @@ +export default function Page() { + return
Bar
+} diff --git a/test/e2e/app-dir/app/app/parallel/@foo/nested/@a/page.server.js b/test/e2e/app-dir/app/app/parallel/@foo/nested/@a/page.server.js new file mode 100644 index 000000000000..31c604844adb --- /dev/null +++ b/test/e2e/app-dir/app/app/parallel/@foo/nested/@a/page.server.js @@ -0,0 +1,3 @@ +export default function Page() { + return
parallel/@foo/nested/@a/page
+} diff --git a/test/e2e/app-dir/app/app/parallel/@foo/nested/@b/page.server.js b/test/e2e/app-dir/app/app/parallel/@foo/nested/@b/page.server.js new file mode 100644 index 000000000000..79481da1a03a --- /dev/null +++ b/test/e2e/app-dir/app/app/parallel/@foo/nested/@b/page.server.js @@ -0,0 +1,3 @@ +export default function Page() { + return
parallel/@foo/nested/@b/page
+} diff --git a/test/e2e/app-dir/app/app/parallel/@foo/nested/layout.server.js b/test/e2e/app-dir/app/app/parallel/@foo/nested/layout.server.js new file mode 100644 index 000000000000..19d532a37424 --- /dev/null +++ b/test/e2e/app-dir/app/app/parallel/@foo/nested/layout.server.js @@ -0,0 +1,16 @@ +export default function Parallel({ a, b, children }) { + return ( +
+ parallel/@foo/nested/layout +
+ {a} +
+
+ {b} +
+
+ {children} +
+
+ ) +} diff --git a/test/e2e/app-dir/app/app/parallel/@foo/page.server.js b/test/e2e/app-dir/app/app/parallel/@foo/page.server.js new file mode 100644 index 000000000000..9e30ce2b2bc3 --- /dev/null +++ b/test/e2e/app-dir/app/app/parallel/@foo/page.server.js @@ -0,0 +1,3 @@ +export default function Page() { + return
Foo
+} diff --git a/test/e2e/app-dir/app/app/parallel/layout.server.js b/test/e2e/app-dir/app/app/parallel/layout.server.js new file mode 100644 index 000000000000..a1474ba6d07a --- /dev/null +++ b/test/e2e/app-dir/app/app/parallel/layout.server.js @@ -0,0 +1,18 @@ +import './style.css' + +export default function Parallel({ foo, bar, children }) { + return ( +
+ parallel/layout: +
+ {foo} +
+
+ {bar} +
+
+ {children} +
+
+ ) +} diff --git a/test/e2e/app-dir/app/app/parallel/nested/page.server.js b/test/e2e/app-dir/app/app/parallel/nested/page.server.js new file mode 100644 index 000000000000..d7992d71980e --- /dev/null +++ b/test/e2e/app-dir/app/app/parallel/nested/page.server.js @@ -0,0 +1,3 @@ +export default function Page() { + return
parallel/nested/page
+} diff --git a/test/e2e/app-dir/app/app/parallel/style.css b/test/e2e/app-dir/app/app/parallel/style.css new file mode 100644 index 000000000000..723ece56ac88 --- /dev/null +++ b/test/e2e/app-dir/app/app/parallel/style.css @@ -0,0 +1,15 @@ +div { + font-size: 16px; +} + +.parallel { + border: 1px solid; + margin: 10px; +} + +.parallel::before { + content: attr(title); + background: black; + color: white; + padding: 1px; +} diff --git a/test/e2e/app-dir/index.test.ts b/test/e2e/app-dir/index.test.ts index 51d41c4931d9..97e10d4ace06 100644 --- a/test/e2e/app-dir/index.test.ts +++ b/test/e2e/app-dir/index.test.ts @@ -273,6 +273,25 @@ describe('app dir', () => { } }) + it('should match parallel routes', async () => { + const html = await renderViaHTTP(next.url, '/parallel/nested') + expect(html).toContain('parallel/layout') + expect(html).toContain('parallel/@foo/nested/layout') + expect(html).toContain('parallel/@foo/nested/@a/page') + expect(html).toContain('parallel/@foo/nested/@b/page') + expect(html).toContain('parallel/@bar/nested/layout') + expect(html).toContain('parallel/@bar/nested/@a/page') + expect(html).toContain('parallel/@bar/nested/@b/page') + expect(html).toContain('parallel/nested/page') + }) + + it('should match parallel routes in route groups', async () => { + const html = await renderViaHTTP(next.url, '/parallel/nested-2') + expect(html).toContain('parallel/layout') + expect(html).toContain('parallel/(new)/layout') + expect(html).toContain('parallel/(new)/@baz/nested/page') + }) + describe('', () => { // TODO-APP: fix development test it.skip('should hard push', async () => {