From 54f07243425ee79a55cd28cf50381de06bd849ca Mon Sep 17 00:00:00 2001 From: Javi Velasco Date: Tue, 19 Apr 2022 18:19:36 +0200 Subject: [PATCH 1/7] Refactor `path-match` --- .../loaders/next-serverless-loader/utils.ts | 9 +- packages/next/server/base-server.ts | 9 +- packages/next/server/dev/hot-reloader.ts | 7 +- packages/next/server/dev/next-dev-server.ts | 13 +-- packages/next/server/next-server.ts | 14 +-- packages/next/server/router.ts | 8 +- packages/next/server/server-route-utils.ts | 15 ++- .../shared/lib/router/utils/path-match.ts | 101 +++++++++++------- .../lib/router/utils/resolve-rewrites.ts | 10 +- 9 files changed, 104 insertions(+), 82 deletions(-) diff --git a/packages/next/build/webpack/loaders/next-serverless-loader/utils.ts b/packages/next/build/webpack/loaders/next-serverless-loader/utils.ts index 391675d76b94..12662bf4b298 100644 --- a/packages/next/build/webpack/loaders/next-serverless-loader/utils.ts +++ b/packages/next/build/webpack/loaders/next-serverless-loader/utils.ts @@ -12,7 +12,7 @@ import type { BaseNextRequest } from '../../../../server/base-http' import { format as formatUrl, UrlWithParsedQuery, parse as parseUrl } from 'url' import { parse as parseQs, ParsedUrlQuery } from 'querystring' import { normalizeLocalePath } from '../../../../shared/lib/i18n/normalize-locale-path' -import pathMatch from '../../../../shared/lib/router/utils/path-match' +import { getPathMatch } from '../../../../shared/lib/router/utils/path-match' import { getRouteRegex } from '../../../../shared/lib/router/utils/route-regex' import { getRouteMatcher } from '../../../../shared/lib/router/utils/route-matcher' import { @@ -28,8 +28,6 @@ import cookie from 'next/dist/compiled/cookie' import { TEMPORARY_REDIRECT_STATUS } from '../../../../shared/lib/constants' import { addRequestMeta } from '../../../../server/request-meta' -const getCustomRouteMatcher = pathMatch(true) - export const vercelHeader = 'x-vercel-id' export type ServerlessHandlerCtx = { @@ -103,7 +101,10 @@ export function getUtils({ } const checkRewrite = (rewrite: Rewrite): boolean => { - const matcher = getCustomRouteMatcher(rewrite.source) + const matcher = getPathMatch(rewrite.source, { + removeUnnamedParams: true, + strict: true, + }) let params = matcher(parsedUrl.pathname) if (rewrite.has && params) { diff --git a/packages/next/server/base-server.ts b/packages/next/server/base-server.ts index 4ed04501fcb5..1153c8639bf5 100644 --- a/packages/next/server/base-server.ts +++ b/packages/next/server/base-server.ts @@ -41,7 +41,8 @@ import { import * as envConfig from '../shared/lib/runtime-config' import { DecodeError, normalizeRepeatedSlashes } from '../shared/lib/utils' import { isTargetLikeServerless } from './utils' -import Router, { route } from './router' +import Router from './router' +import { getPathMatch } from '../shared/lib/router/utils/path-match' import { setRevalidateHeaders } from './send-payload/revalidate-headers' import { IncrementalCache } from './incremental-cache' import { execOnce } from '../shared/lib/utils' @@ -692,7 +693,7 @@ export default abstract class Server { const fsRoutes: Route[] = [ ...this.generateFsStaticRoutes(), { - match: route('/_next/data/:path*'), + match: getPathMatch('/_next/data/:path*'), type: 'route', name: '_next/data catchall', fn: async (req, res, params, _parsedUrl) => { @@ -768,7 +769,7 @@ export default abstract class Server { }, ...imageRoutes, { - match: route('/_next/:path*'), + match: getPathMatch('/_next/:path*'), type: 'route', name: '_next catchall', // This path is needed because `render()` does a check for `/_next` and the calls the routing again @@ -804,7 +805,7 @@ export default abstract class Server { const catchAllMiddleware = this.generateCatchAllMiddlewareRoute() const catchAllRoute: Route = { - match: route('/:path*'), + match: getPathMatch('/:path*'), type: 'route', name: 'Catchall render', fn: async (req, res, _params, parsedUrl) => { diff --git a/packages/next/server/dev/hot-reloader.ts b/packages/next/server/dev/hot-reloader.ts index 8e813c162432..50551b31ed12 100644 --- a/packages/next/server/dev/hot-reloader.ts +++ b/packages/next/server/dev/hot-reloader.ts @@ -17,7 +17,7 @@ import { API_ROUTE, MIDDLEWARE_ROUTE } from '../../lib/constants' import { recursiveDelete } from '../../lib/recursive-delete' import { BLOCKED_PAGES } from '../../shared/lib/constants' import { __ApiPreviewProps } from '../api-utils' -import { route } from '../router' +import { getPathMatch } from '../../shared/lib/router/utils/path-match' import { findPageFile } from '../lib/find-page-file' import onDemandEntryHandler, { entries, @@ -101,7 +101,7 @@ function addCorsSupport(req: IncomingMessage, res: ServerResponse) { return { preflight: false } } -const matchNextPageBundleRequest = route( +const matchNextPageBundleRequest = getPathMatch( '/_next/static/chunks/pages/:path*.js(\\.map|)' ) @@ -238,8 +238,7 @@ export default class HotReloader { parsedPageBundleUrl: UrlObject ): Promise<{ finished?: true }> => { const { pathname } = parsedPageBundleUrl - const params: { path: string[] } | null = - matchNextPageBundleRequest(pathname) + const params = matchNextPageBundleRequest<{ path: string[] }>(pathname) if (!params) { return {} } diff --git a/packages/next/server/dev/next-dev-server.ts b/packages/next/server/dev/next-dev-server.ts index 2bbde53f9322..5b323e44f541 100644 --- a/packages/next/server/dev/next-dev-server.ts +++ b/packages/next/server/dev/next-dev-server.ts @@ -41,7 +41,8 @@ import { } from '../../shared/lib/router/utils' import Server, { WrappedBuildError } from '../next-server' import { normalizePagePath } from '../normalize-page-path' -import Router, { route } from '../router' +import Router from '../router' +import { getPathMatch } from '../../shared/lib/router/utils/path-match' import { hasBasePath, replaceBasePath } from '../router-utils' import { eventCliSession } from '../../telemetry/events' import { Telemetry } from '../../telemetry/storage' @@ -199,7 +200,7 @@ export default class DevServer extends Server { // We use unshift so that we're sure the routes is defined before Next's default routes this.router.addFsRoute({ - match: route(path), + match: getPathMatch(path), type: 'route', name: `${path} exportpathmap route`, fn: async (req, res, _params, parsedUrl) => { @@ -754,7 +755,7 @@ export default class DevServer extends Server { // In development we expose all compiled files for react-error-overlay's line show feature // We use unshift so that we're sure the routes is defined before Next's default routes fsRoutes.unshift({ - match: route('/_next/development/:path*'), + match: getPathMatch('/_next/development/:path*'), type: 'route', name: '_next/development catchall', fn: async (req, res, params) => { @@ -767,7 +768,7 @@ export default class DevServer extends Server { }) fsRoutes.unshift({ - match: route( + match: getPathMatch( `/_next/${CLIENT_STATIC_FILES_PATH}/${this.buildId}/${DEV_CLIENT_PAGES_MANIFEST}` ), type: 'route', @@ -789,7 +790,7 @@ export default class DevServer extends Server { }) fsRoutes.unshift({ - match: route( + match: getPathMatch( `/_next/${CLIENT_STATIC_FILES_PATH}/${this.buildId}/${DEV_MIDDLEWARE_MANIFEST}` ), type: 'route', @@ -814,7 +815,7 @@ export default class DevServer extends Server { }) fsRoutes.push({ - match: route('/:path*'), + match: getPathMatch('/:path*'), type: 'route', requireBasePath: false, name: 'catchall public directory route', diff --git a/packages/next/server/next-server.ts b/packages/next/server/next-server.ts index 2291f6514f08..24825cd127f3 100644 --- a/packages/next/server/next-server.ts +++ b/packages/next/server/next-server.ts @@ -38,7 +38,7 @@ import { recursiveReadDirSync } from './lib/recursive-readdir-sync' import { format as formatUrl, UrlWithParsedQuery } from 'url' import compression from 'next/dist/compiled/compression' import HttpProxy from 'next/dist/compiled/http-proxy' -import { route } from './router' +import { getPathMatch } from '../shared/lib/router/utils/path-match' import { run } from './web/sandbox' import { NodeNextRequest, NodeNextResponse } from './base-http/node' @@ -182,7 +182,7 @@ export default class NextNodeServer extends BaseServer { protected generateImageRoutes(): Route[] { return [ { - match: route('/_next/image'), + match: getPathMatch('/_next/image'), type: 'route', name: '_next/image catchall', fn: async (req, res, _params, parsedUrl) => { @@ -287,7 +287,7 @@ export default class NextNodeServer extends BaseServer { // (but it should support as many params as needed, separated by '/') // Otherwise this will lead to a pretty simple DOS attack. // See more: https://github.com/vercel/next.js/issues/2617 - match: route('/static/:path*'), + match: getPathMatch('/static/:path*'), name: 'static catchall', fn: async (req, res, params, parsedUrl) => { const p = join(this.dir, 'static', ...params.path) @@ -308,7 +308,7 @@ export default class NextNodeServer extends BaseServer { protected generateFsStaticRoutes(): Route[] { return [ { - match: route('/_next/static/:path*'), + match: getPathMatch('/_next/static/:path*'), type: 'route', name: '_next/static catchall', fn: async (req, res, params, parsedUrl) => { @@ -357,7 +357,7 @@ export default class NextNodeServer extends BaseServer { return [ { - match: route('/:path*'), + match: getPathMatch('/:path*'), name: 'public folder catchall', fn: async (req, res, params, parsedUrl) => { const pathParts: string[] = params.path || [] @@ -875,7 +875,7 @@ export default class NextNodeServer extends BaseServer { // (but it should support as many params as needed, separated by '/') // Otherwise this will lead to a pretty simple DOS attack. // See more: https://github.com/vercel/next.js/issues/2617 - match: route('/static/:path*'), + match: getPathMatch('/static/:path*'), name: 'static catchall', fn: async (req, res, params, parsedUrl) => { const p = join(this.dir, 'static', ...params.path) @@ -1026,7 +1026,7 @@ export default class NextNodeServer extends BaseServer { if (this.minimalMode) return undefined return { - match: route('/:path*'), + match: getPathMatch('/:path*'), type: 'route', name: 'middleware catchall', fn: async (req, res, _params, parsed) => { diff --git a/packages/next/server/router.ts b/packages/next/server/router.ts index 51a591e15ce4..1546c97a2bd6 100644 --- a/packages/next/server/router.ts +++ b/packages/next/server/router.ts @@ -2,15 +2,13 @@ import type { ParsedUrlQuery } from 'querystring' import type { BaseNextRequest, BaseNextResponse } from './base-http' import { getNextInternalQuery, NextUrlWithParsedQuery } from './request-meta' -import pathMatch from '../shared/lib/router/utils/path-match' +import { getPathMatch } from '../shared/lib/router/utils/path-match' import { removePathTrailingSlash } from '../client/normalize-trailing-slash' import { normalizeLocalePath } from '../shared/lib/i18n/normalize-locale-path' import { RouteHas } from '../lib/load-custom-routes' import { matchHas } from '../shared/lib/router/utils/prepare-destination' import { getRequestMeta } from './request-meta' -export const route = pathMatch() - export type Params = { [param: string]: any } export type RouteMatch = (pathname: string | null | undefined) => false | Params @@ -242,7 +240,7 @@ export default class Router { type: 'route', name: 'page checker', requireBasePath: false, - match: route('/:path*'), + match: getPathMatch('/:path*'), fn: async ( checkerReq, checkerRes, @@ -276,7 +274,7 @@ export default class Router { type: 'route', name: 'dynamic route/page check', requireBasePath: false, - match: route('/:path*'), + match: getPathMatch('/:path*'), fn: async ( _checkerReq, _checkerRes, diff --git a/packages/next/server/server-route-utils.ts b/packages/next/server/server-route-utils.ts index 9c17bc3c2cb1..881ca8f14705 100644 --- a/packages/next/server/server-route-utils.ts +++ b/packages/next/server/server-route-utils.ts @@ -9,7 +9,7 @@ import type { BaseNextRequest } from './base-http' import type { ParsedUrlQuery } from 'querystring' import { getRedirectStatus, modifyRouteRegex } from '../lib/load-custom-routes' -import pathMatch from '../shared/lib/router/utils/path-match' +import { getPathMatch } from '../shared/lib/router/utils/path-match' import { compileNonPath, prepareDestination, @@ -19,8 +19,6 @@ import { stringify as stringifyQs } from 'querystring' import { format as formatUrl } from 'url' import { normalizeRepeatedSlashes } from '../shared/lib/utils' -const getCustomRouteMatcher = pathMatch(true) - export const getCustomRoute = ({ type, rule, @@ -30,16 +28,17 @@ export const getCustomRoute = ({ type: RouteType restrictedRedirectPaths: string[] }) => { - const match = getCustomRouteMatcher( - rule.source, - !(rule as any).internal + const match = getPathMatch(rule.source, { + strict: true, + removeUnnamedParams: true, + regexModifier: !(rule as any).internal ? (regex: string) => modifyRouteRegex( regex, type === 'redirect' ? restrictedRedirectPaths : undefined ) - : undefined - ) + : undefined, + }) return { ...rule, diff --git a/packages/next/shared/lib/router/utils/path-match.ts b/packages/next/shared/lib/router/utils/path-match.ts index 784c082927bb..f6fed0c8c6b9 100644 --- a/packages/next/shared/lib/router/utils/path-match.ts +++ b/packages/next/shared/lib/router/utils/path-match.ts @@ -1,52 +1,73 @@ -import * as pathToRegexp from 'next/dist/compiled/path-to-regexp' +import type { Key } from 'next/dist/compiled/path-to-regexp' +import { pathToRegexp } from 'next/dist/compiled/path-to-regexp' +import { regexpToFunction } from 'next/dist/compiled/path-to-regexp' -export { pathToRegexp } - -export const matcherOptions: pathToRegexp.TokensToRegexpOptions & - pathToRegexp.ParseOptions = { - sensitive: false, - delimiter: '/', +interface Options { + /** + * A transformer function that will be applied to the regexp generated + * from the provided path and path-to-regexp. + */ + regexModifier?: (regex: string) => string + /** + * When passed to true the function will remove all unnamed parameters + * from the matched parameters. + */ + removeUnnamedParams?: boolean + /** + * When true the regexp won't allow an optional trailing delimiter + * to match. + */ + strict?: boolean } -export const customRouteMatcherOptions: pathToRegexp.TokensToRegexpOptions & - pathToRegexp.ParseOptions = { - ...matcherOptions, - strict: true, -} +/** + * Generates a path matcher function for a given path and options based on + * path-to-regexp. By default the match will case insesitive, non strict + * and delimited by `/`. + */ +export function getPathMatch(path: string, options?: Options) { + const keys: Key[] = [] + const regexp = pathToRegexp(path, keys, { + delimiter: '/', + sensitive: false, + strict: options?.strict, + }) -export default (customRoute = false) => { - return (path: string, regexModifier?: (regex: string) => string) => { - const keys: pathToRegexp.Key[] = [] - let matcherRegex = pathToRegexp.pathToRegexp( - path, - keys, - customRoute ? customRouteMatcherOptions : matcherOptions - ) + const matcher = regexpToFunction( + options?.regexModifier + ? new RegExp(options.regexModifier(regexp.source), regexp.flags) + : regexp, + keys + ) - if (regexModifier) { - const regexSource = regexModifier(matcherRegex.source) - matcherRegex = new RegExp(regexSource, matcherRegex.flags) + /** + * A matcher function that will check if a given pathname matches the path + * given in the builder function. When the path does not match it will return + * `false` but if it does it will return an object with the matched params + * merged with the params provided in the second argument. + */ + return ( + pathname?: string | null, + params?: any + ): false | T => { + const res = pathname == null ? false : matcher(pathname) + if (!res) { + return false } - const matcher = pathToRegexp.regexpToFunction(matcherRegex, keys) - - return (pathname: string | null | undefined, params?: any) => { - const res = pathname == null ? false : matcher(pathname) - if (!res) { - return false - } - - if (customRoute) { - for (const key of keys) { - // unnamed params should be removed as they - // are not allowed to be used in the destination - if (typeof key.name === 'number') { - delete (res.params as any)[key.name] - } + /** + * If unnamed params are not allowed to allowed they must be removed from + * the matched parameters. path-to-regexp uses "string" for named and + * "number" for unnamed parameters. + */ + if (options?.removeUnnamedParams) { + for (const key of keys) { + if (typeof key.name === 'number') { + delete (res.params as any)[key.name] } } - - return { ...params, ...res.params } } + + return { ...params, ...res.params } } } diff --git a/packages/next/shared/lib/router/utils/resolve-rewrites.ts b/packages/next/shared/lib/router/utils/resolve-rewrites.ts index c69bac0091a2..f631a426d011 100644 --- a/packages/next/shared/lib/router/utils/resolve-rewrites.ts +++ b/packages/next/shared/lib/router/utils/resolve-rewrites.ts @@ -1,5 +1,5 @@ import { ParsedUrlQuery } from 'querystring' -import pathMatch from './path-match' +import { getPathMatch } from './path-match' import { matchHas, prepareDestination } from './prepare-destination' import { Rewrite } from '../../../../lib/load-custom-routes' import { removePathTrailingSlash } from '../../../../client/normalize-trailing-slash' @@ -7,8 +7,6 @@ import { normalizeLocalePath } from '../../i18n/normalize-locale-path' import { parseRelativeUrl } from './parse-relative-url' import { delBasePath } from '../router' -const customRouteMatcher = pathMatch(true) - export default function resolveRewrites( asPath: string, pages: string[], @@ -36,7 +34,11 @@ export default function resolveRewrites( let resolvedHref const handleRewrite = (rewrite: Rewrite) => { - const matcher = customRouteMatcher(rewrite.source) + const matcher = getPathMatch(rewrite.source, { + removeUnnamedParams: true, + strict: true, + }) + let params = matcher(parsedAs.pathname) if (rewrite.has && params) { From a5d1442ae1cea2689ce7164f91521392bfe31ebb Mon Sep 17 00:00:00 2001 From: Javi Velasco Date: Wed, 20 Apr 2022 18:50:30 +0200 Subject: [PATCH 2/7] Simplify `createPagesMapping` --- packages/next/build/entries.ts | 105 +++++++++++------------ packages/next/build/index.ts | 30 ++++--- packages/next/build/utils.ts | 11 --- packages/next/server/dev/hot-reloader.ts | 19 ++-- 4 files changed, 76 insertions(+), 89 deletions(-) diff --git a/packages/next/build/entries.ts b/packages/next/build/entries.ts index 0ff3181b393e..87c901b8cc4f 100644 --- a/packages/next/build/entries.ts +++ b/packages/next/build/entries.ts @@ -13,6 +13,7 @@ import { MIDDLEWARE_ROUTE } from '../lib/constants' import { __ApiPreviewProps } from '../server/api-utils' import { isTargetLikeServerless } from '../server/utils' import { normalizePagePath } from '../server/normalize-page-path' +import { normalizePathSep } from '../server/denormalize-page-path' import { warn } from './output/log' import { MiddlewareLoaderOptions } from './webpack/loaders/next-middleware-loader' import { ClientPagesLoaderOptions } from './webpack/loaders/next-client-pages-loader' @@ -32,48 +33,44 @@ import { } from '../shared/lib/constants' type ObjectValue = T extends { [key: string]: infer V } ? V : never -export type PagesMapping = { - [page: string]: string -} -export function getPageFromPath(pagePath: string, extensions: string[]) { - const rawExtensions = getRawPageExtensions(extensions) - const pickedExtensions = pagePath.includes('/_app.server.') - ? rawExtensions - : extensions - let page = pagePath.replace( - new RegExp(`\\.+(${pickedExtensions.join('|')})$`), - '' - ) - page = page.replace(/\\/g, '/').replace(/\/index$/, '') +export function getPageFromPath(pagePath: string, pageExtensions: string[]) { + const extensions = pagePath.includes('/_app.server.') + ? getRawPageExtensions(pageExtensions) + : pageExtensions + + const page = normalizePathSep( + pagePath.replace(new RegExp(`\\.+(${extensions.join('|')})$`), '') + ).replace(/\/index$/, '') + return page === '' ? '/' : page } -export function createPagesMapping( - pagePaths: string[], - extensions: string[], - { - isDev, - hasServerComponents, - }: { - isDev: boolean - hasServerComponents: boolean - } -): PagesMapping { - const previousPages: PagesMapping = {} - - // Do not process .d.ts files inside the `pages` folder - pagePaths = extensions.includes('ts') - ? pagePaths.filter((pagePath) => !pagePath.endsWith('.d.ts')) - : pagePaths +export function createPagesMapping({ + hasServerComponents, + isDev, + pageExtensions, + pagePaths, +}: { + hasServerComponents: boolean + isDev: boolean + pageExtensions: string[] + pagePaths: string[] +}): { [page: string]: string } { + const previousPages: { [key: string]: string } = {} + const pages = pagePaths.reduce<{ [key: string]: string }>( + (result, pagePath) => { + // Do not process .d.ts files inside the `pages` folder + if (pagePath.endsWith('.d.ts') && pageExtensions.includes('ts')) { + return result + } - const pages: PagesMapping = pagePaths.reduce( - (result: PagesMapping, pagePath): PagesMapping => { - const pageKey = getPageFromPath(pagePath, extensions) + const pageKey = getPageFromPath(pagePath, pageExtensions) + // Assume that if there's a Client Component, that there is + // a matching Server Component that will map to the page. + // so we will not process it if (hasServerComponents && /\.client$/.test(pageKey)) { - // Assume that if there's a Client Component, that there is - // a matching Server Component that will map to the page. return result } @@ -88,32 +85,30 @@ export function createPagesMapping( } else { previousPages[pageKey] = pagePath } - result[pageKey] = join(PAGES_DIR_ALIAS, pagePath).replace(/\\/g, '/') + result[pageKey] = normalizePathSep(join(PAGES_DIR_ALIAS, pagePath)) return result }, {} ) - // we alias these in development and allow webpack to - // allow falling back to the correct source file so - // that HMR can work properly when a file is added/removed + // In development we always alias these to allow Webpack to fallback to + // the correct source file so that HMR can work properly when a file is + // added or removed. if (isDev) { - if (hasServerComponents) { - pages['/_app.server'] = `${PAGES_DIR_ALIAS}/_app.server` - } - pages['/_app'] = `${PAGES_DIR_ALIAS}/_app` - pages['/_error'] = `${PAGES_DIR_ALIAS}/_error` - pages['/_document'] = `${PAGES_DIR_ALIAS}/_document` - } else { - if (hasServerComponents) { - pages['/_app.server'] = - pages['/_app.server'] || 'next/dist/pages/_app.server' - } - pages['/_app'] = pages['/_app'] || 'next/dist/pages/_app' - pages['/_error'] = pages['/_error'] || 'next/dist/pages/_error' - pages['/_document'] = pages['/_document'] || 'next/dist/pages/_document' + delete pages['/_app'] + delete pages['/_app.server'] + delete pages['/_error'] + delete pages['/_document'] + } + + const root = isDev ? PAGES_DIR_ALIAS : 'next/dist/pages' + return { + '/_app': `${root}/_app`, + '/_error': `${root}/_error`, + '/_document': `${root}/_document`, + ...(hasServerComponents ? { '/_app.server': `${root}/_app.server` } : {}), + ...pages, } - return pages } type Entrypoints = { @@ -230,7 +225,7 @@ export function invalidatePageRuntimeCache( } export async function createEntrypoints( - pages: PagesMapping, + pages: { [page: string]: string }, target: 'server' | 'serverless' | 'experimental-serverless-trace', buildId: string, previewMode: __ApiPreviewProps, diff --git a/packages/next/build/index.ts b/packages/next/build/index.ts index 24b3b721ee4b..9937d7c19e74 100644 --- a/packages/next/build/index.ts +++ b/packages/next/build/index.ts @@ -15,6 +15,7 @@ import { STATIC_STATUS_PAGE_GET_INITIAL_PROPS_ERROR, PUBLIC_DIR_MIDDLEWARE_CONFLICT, MIDDLEWARE_ROUTE, + PAGES_DIR_ALIAS, } from '../lib/constants' import { fileExists } from '../lib/file-exists' import { findPagesDir } from '../lib/find-pages-dir' @@ -88,7 +89,6 @@ import * as Log from './output/log' import createSpinner from './spinner' import { trace, flushAllTraces, setGlobal } from '../trace' import { - collectPages, detectConflictingPaths, computeFromManifest, getJsPageSizeInKb, @@ -112,6 +112,7 @@ import { MiddlewareManifest } from './webpack/plugins/middleware-plugin' import type { webpack5 as webpack } from 'next/dist/compiled/webpack/webpack' import { recursiveCopy } from '../lib/recursive-copy' import { shouldUseReactRoot } from '../server/config' +import { recursiveReadDir } from '../lib/recursive-readdir' export type SsgRoute = { initialRevalidateSeconds: number | false @@ -283,9 +284,14 @@ export default async function build( const isLikeServerless = isTargetLikeServerless(target) - const pagePaths: string[] = await nextBuildSpan + const pagePaths = await nextBuildSpan .traceChild('collect-pages') - .traceAsyncFn(() => collectPages(pagesDir, config.pageExtensions)) + .traceAsyncFn(() => + recursiveReadDir( + pagesDir, + new RegExp(`\\.(?:${config.pageExtensions.join('|')})$`) + ) + ) // needed for static exporting since we want to replace with HTML // files @@ -301,9 +307,11 @@ export default async function build( const mappedPages = nextBuildSpan .traceChild('create-pages-mapping') .traceFn(() => - createPagesMapping(pagePaths, config.pageExtensions, { - isDev: false, + createPagesMapping({ hasServerComponents, + isDev: false, + pageExtensions: config.pageExtensions, + pagePaths, }) ) @@ -322,16 +330,12 @@ export default async function build( ) ) const pageKeys = Object.keys(mappedPages) - const hasMiddleware = pageKeys.some((page) => MIDDLEWARE_ROUTE.test(page)) const conflictingPublicFiles: string[] = [] - const hasCustomErrorPage: boolean = - mappedPages['/_error'].startsWith('private-next-pages') - const hasPages404 = Boolean( - mappedPages['/404'] && - mappedPages['/404'].startsWith('private-next-pages') - ) + const hasPages404 = mappedPages['/404']?.startsWith(PAGES_DIR_ALIAS) + const hasCustomErrorPage = + mappedPages['/_error'].startsWith(PAGES_DIR_ALIAS) - if (hasMiddleware) { + if (pageKeys.some((page) => MIDDLEWARE_ROUTE.test(page))) { Log.warn( `using beta Middleware (not covered by semver) - https://nextjs.org/docs/messages/beta-middleware` ) diff --git a/packages/next/build/utils.ts b/packages/next/build/utils.ts index d9d7a058aaed..8d1144839d6b 100644 --- a/packages/next/build/utils.ts +++ b/packages/next/build/utils.ts @@ -21,7 +21,6 @@ import { MIDDLEWARE_ROUTE, } from '../lib/constants' import prettyBytes from '../lib/pretty-bytes' -import { recursiveReadDir } from '../lib/recursive-readdir' import { getRouteMatcher, getRouteRegex } from '../shared/lib/router/utils' import { isDynamicRoute } from '../shared/lib/router/utils/is-dynamic' import escapePathDelimiters from '../shared/lib/router/utils/escape-path-delimiters' @@ -62,16 +61,6 @@ const fsStat = (file: string) => { return (fileStats[file] = fileSize(file)) } -export function collectPages( - directory: string, - pageExtensions: string[] -): Promise { - return recursiveReadDir( - directory, - new RegExp(`\\.(?:${pageExtensions.join('|')})$`) - ) -} - export interface PageInfo { isHybridAmp?: boolean size: number diff --git a/packages/next/server/dev/hot-reloader.ts b/packages/next/server/dev/hot-reloader.ts index 50551b31ed12..703aa5e9d43e 100644 --- a/packages/next/server/dev/hot-reloader.ts +++ b/packages/next/server/dev/hot-reloader.ts @@ -9,7 +9,6 @@ import { createEntrypoints, createPagesMapping, finalizeEntrypoint, - PagesMapping, } from '../../build/entries' import { watchCompilers } from '../../build/output' import getBaseWebpackConfig from '../../build/webpack-config' @@ -171,7 +170,7 @@ export default class HotReloader { private rewrites: CustomRoutes['rewrites'] private fallbackWatcher: any private hotReloaderSpan: Span - private pagesMapping: PagesMapping = {} + private pagesMapping: { [key: string]: string } = {} constructor( dir: string, @@ -393,14 +392,14 @@ export default class HotReloader { this.pagesMapping = webpackConfigSpan .traceChild('create-pages-mapping') .traceFn(() => - createPagesMapping( - pagePaths.filter((i) => i !== null) as string[], - this.config.pageExtensions, - { - isDev: true, - hasServerComponents: this.hasServerComponents, - } - ) + createPagesMapping({ + hasServerComponents: this.hasServerComponents, + isDev: true, + pageExtensions: this.config.pageExtensions, + pagePaths: pagePaths.filter( + (i): i is string => typeof i === 'string' + ), + }) ) const entrypoints = await webpackConfigSpan From f613a7b3eba8ff58fd208eb174d82c2c5bd7f3fc Mon Sep 17 00:00:00 2001 From: Javi Velasco Date: Thu, 21 Apr 2022 11:12:24 +0200 Subject: [PATCH 3/7] Rename `getRawPageExtensions` -> `withoutRSCExtensions` --- packages/next/build/entries.ts | 10 ++++++++-- packages/next/build/utils.ts | 9 +++++++-- packages/next/build/webpack-config.ts | 4 ++-- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/packages/next/build/entries.ts b/packages/next/build/entries.ts index 87c901b8cc4f..7fdaf4d0260c 100644 --- a/packages/next/build/entries.ts +++ b/packages/next/build/entries.ts @@ -21,10 +21,10 @@ import { ServerlessLoaderQuery } from './webpack/loaders/next-serverless-loader' import { LoadedEnvFiles } from '@next/env' import { parse } from '../build/swc' import { - getRawPageExtensions, isCustomErrorPage, isFlightPage, isReservedPage, + withoutRSCExtensions, } from './utils' import { ssrEntries } from './webpack/plugins/middleware-plugin' import { @@ -34,9 +34,14 @@ import { type ObjectValue = T extends { [key: string]: infer V } ? V : never +/** + * For a given page path removes the provided extensions. `/_app.server` is a + * special case because it is the only page where we want to preserve the RSC + * server extension. + */ export function getPageFromPath(pagePath: string, pageExtensions: string[]) { const extensions = pagePath.includes('/_app.server.') - ? getRawPageExtensions(pageExtensions) + ? withoutRSCExtensions(pageExtensions) : pageExtensions const page = normalizePathSep( @@ -85,6 +90,7 @@ export function createPagesMapping({ } else { previousPages[pageKey] = pagePath } + result[pageKey] = normalizePathSep(join(PAGES_DIR_ALIAS, pagePath)) return result }, diff --git a/packages/next/build/utils.ts b/packages/next/build/utils.ts index 8d1144839d6b..96deac92b3fb 100644 --- a/packages/next/build/utils.ts +++ b/packages/next/build/utils.ts @@ -1092,7 +1092,12 @@ export function detectConflictingPaths( } } -export function getRawPageExtensions(pageExtensions: string[]): string[] { +/** + * With RSC we automatically add .server and .client to page extensions. This + * function allows to remove them for cases where we just need to strip out + * the actual extension keeping the .server and .client. + */ +export function withoutRSCExtensions(pageExtensions: string[]): string[] { return pageExtensions.filter( (ext) => !ext.startsWith('client.') && !ext.startsWith('server.') ) @@ -1106,7 +1111,7 @@ export function isFlightPage( return false } - const rawPageExtensions = getRawPageExtensions( + const rawPageExtensions = withoutRSCExtensions( nextConfig.pageExtensions || [] ) return rawPageExtensions.some((ext) => { diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts index 3233a516d28f..fb6451e0b7b7 100644 --- a/packages/next/build/webpack-config.ts +++ b/packages/next/build/webpack-config.ts @@ -49,7 +49,7 @@ import { TelemetryPlugin, } from './webpack/plugins/telemetry-plugin' import type { Span } from '../trace' -import { getRawPageExtensions } from './utils' +import { withoutRSCExtensions } from './utils' import browserslist from 'next/dist/compiled/browserslist' import loadJsConfig from './load-jsconfig' import { getMiddlewareSourceMapPlugins } from './webpack/plugins/middleware-source-maps-plugin' @@ -471,7 +471,7 @@ export default async function getBaseWebpackConfig( } const rawPageExtensions = hasServerComponents - ? getRawPageExtensions(config.pageExtensions) + ? withoutRSCExtensions(config.pageExtensions) : config.pageExtensions const serverComponentsRegex = new RegExp( From 72358ae7eda03793c0031e7da34cc0e847dc0f6f Mon Sep 17 00:00:00 2001 From: Javi Velasco Date: Fri, 22 Apr 2022 10:13:25 +0200 Subject: [PATCH 4/7] Remove unused `functions-manifest-plugin.ts` --- .../plugins/functions-manifest-plugin.ts | 156 ------------------ packages/next/shared/lib/constants.ts | 1 - 2 files changed, 157 deletions(-) delete mode 100644 packages/next/build/webpack/plugins/functions-manifest-plugin.ts diff --git a/packages/next/build/webpack/plugins/functions-manifest-plugin.ts b/packages/next/build/webpack/plugins/functions-manifest-plugin.ts deleted file mode 100644 index 21d94ec59fc8..000000000000 --- a/packages/next/build/webpack/plugins/functions-manifest-plugin.ts +++ /dev/null @@ -1,156 +0,0 @@ -// TODO: rewrite against the stable edge functions standard - -import { relative } from 'path' -import { sources, webpack5 } from 'next/dist/compiled/webpack/webpack' -import { normalizePagePath } from '../../../server/normalize-page-path' -import { FUNCTIONS_MANIFEST } from '../../../shared/lib/constants' -import { getPageFromPath } from '../../entries' -import { collectAssets, getEntrypointInfo, PerRoute } from './middleware-plugin' - -const PLUGIN_NAME = 'FunctionsManifestPlugin' -export interface FunctionsManifest { - version: 1 - pages: { - [page: string]: { - runtime?: string - env: string[] - files: string[] - name: string - page: string - regexp: string - } - } -} - -function containsPath(outer: string, inner: string) { - const rel = relative(outer, inner) - return !rel.startsWith('../') && rel !== '..' -} -export default class FunctionsManifestPlugin { - dev: boolean - pagesDir: string - pageExtensions: string[] - isEdgeRuntime: boolean - pagesRuntime: Map - - constructor({ - dev, - pagesDir, - pageExtensions, - isEdgeRuntime, - }: { - dev: boolean - pagesDir: string - pageExtensions: string[] - isEdgeRuntime: boolean - }) { - this.dev = dev - this.pagesDir = pagesDir - this.isEdgeRuntime = isEdgeRuntime - this.pageExtensions = pageExtensions - this.pagesRuntime = new Map() - } - - createAssets( - compilation: webpack5.Compilation, - assets: any, - perRoute: PerRoute, - isEdgeRuntime: boolean - ) { - const functionsManifest: FunctionsManifest = { - version: 1, - pages: {}, - } - - const infos = getEntrypointInfo(compilation, perRoute, isEdgeRuntime) - infos.forEach((info) => { - const { page } = info - // TODO: use global default runtime instead of 'web' - const pageRuntime = this.pagesRuntime.get(page) - const isWebRuntime = - pageRuntime === 'edge' || (this.isEdgeRuntime && !pageRuntime) - functionsManifest.pages[page] = { - // Not assign if it's nodejs runtime, project configured node version is used instead - ...(isWebRuntime && { runtime: 'web' }), - ...info, - } - }) - - const assetPath = (this.isEdgeRuntime ? '' : 'server/') + FUNCTIONS_MANIFEST - assets[assetPath] = new sources.RawSource( - JSON.stringify(functionsManifest, null, 2) - ) - } - - apply(compiler: webpack5.Compiler) { - const handler = (parser: webpack5.javascript.JavascriptParser) => { - parser.hooks.exportSpecifier.tap( - PLUGIN_NAME, - (statement: any, _identifierName: string, exportName: string) => { - const { resource } = parser.state.module - const isPagePath = containsPath(this.pagesDir, resource) - // Only parse exported config in pages - if (!isPagePath) { - return - } - const { declaration } = statement - if (exportName === 'config') { - const varDecl = declaration.declarations[0] - const { properties } = varDecl.init - const prop = properties.find((p: any) => p.key.name === 'runtime') - if (!prop) return - const runtime = prop.value.value - if (!['nodejs', 'edge'].includes(runtime)) - throw new Error( - `The runtime option can only be 'nodejs' or 'edge'` - ) - - // @ts-ignore buildInfo exists on Module - parser.state.module.buildInfo.NEXT_runtime = runtime - } - } - ) - } - - compiler.hooks.compilation.tap( - PLUGIN_NAME, - ( - compilation: webpack5.Compilation, - { normalModuleFactory: factory }: any - ) => { - factory.hooks.parser.for('javascript/auto').tap(PLUGIN_NAME, handler) - factory.hooks.parser.for('javascript/esm').tap(PLUGIN_NAME, handler) - - compilation.hooks.seal.tap(PLUGIN_NAME, () => { - for (const entryData of compilation.entries.values()) { - for (const dependency of entryData.dependencies) { - // @ts-ignore TODO: webpack 5 types - const module = compilation.moduleGraph.getModule(dependency) - const outgoingConnections = - compilation.moduleGraph.getOutgoingConnectionsByModule(module) - if (!outgoingConnections) return - const entryModules = outgoingConnections.keys() - for (const mod of entryModules) { - const runtime = mod?.buildInfo?.NEXT_runtime - if (runtime) { - // @ts-ignore: TODO: webpack 5 types - const normalizedPagePath = normalizePagePath(mod.userRequest) - const pagePath = normalizedPagePath.replace(this.pagesDir, '') - const page = getPageFromPath(pagePath, this.pageExtensions) - this.pagesRuntime.set(page, runtime) - break - } - } - } - } - }) - } - ) - - collectAssets(compiler, this.createAssets.bind(this), { - dev: this.dev, - pluginName: PLUGIN_NAME, - isEdgeRuntime: this.isEdgeRuntime, - }) - } -} diff --git a/packages/next/shared/lib/constants.ts b/packages/next/shared/lib/constants.ts index a51b645e395a..790eaedca07c 100644 --- a/packages/next/shared/lib/constants.ts +++ b/packages/next/shared/lib/constants.ts @@ -12,7 +12,6 @@ export const ROUTES_MANIFEST = 'routes-manifest.json' export const IMAGES_MANIFEST = 'images-manifest.json' export const SERVER_FILES_MANIFEST = 'required-server-files.json' export const DEV_CLIENT_PAGES_MANIFEST = '_devPagesManifest.json' -export const FUNCTIONS_MANIFEST = 'functions-manifest.json' export const MIDDLEWARE_MANIFEST = 'middleware-manifest.json' export const DEV_MIDDLEWARE_MANIFEST = '_devMiddlewareManifest.json' export const REACT_LOADABLE_MANIFEST = 'react-loadable-manifest.json' From b4eb8b0791e23e8a903d523b4bd4b5cb6d9d9609 Mon Sep 17 00:00:00 2001 From: Javi Velasco Date: Fri, 22 Apr 2022 10:15:01 +0200 Subject: [PATCH 5/7] Enable `eval-source-map` for the edge server compiler --- packages/next/build/webpack/config/blocks/base.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/next/build/webpack/config/blocks/base.ts b/packages/next/build/webpack/config/blocks/base.ts index fe7a9ddca9e0..2e07b5867e9f 100644 --- a/packages/next/build/webpack/config/blocks/base.ts +++ b/packages/next/build/webpack/config/blocks/base.ts @@ -25,13 +25,11 @@ export const base = curry(function base( if (process.env.__NEXT_TEST_MODE && !process.env.__NEXT_TEST_WITH_DEVTOOL) { config.devtool = false } else { - if (!ctx.isEdgeRuntime) { - // `eval-source-map` provides full-fidelity source maps for the - // original source, including columns and original variable names. - // This is desirable so the in-browser debugger can correctly pause - // and show scoped variables with their original names. - config.devtool = 'eval-source-map' - } + // `eval-source-map` provides full-fidelity source maps for the + // original source, including columns and original variable names. + // This is desirable so the in-browser debugger can correctly pause + // and show scoped variables with their original names. + config.devtool = 'eval-source-map' } } else { // Enable browser sourcemaps: From 581c7bc8257238e7ca2500ab0747736163186daf Mon Sep 17 00:00:00 2001 From: Javi Velasco Date: Sat, 23 Apr 2022 00:57:05 +0200 Subject: [PATCH 6/7] Use Edge Compiler for Middleware & Refactor --- packages/next/build/entries.ts | 386 ++++++------ packages/next/build/index.ts | 100 ++-- packages/next/build/webpack-config.ts | 560 +++++++++--------- .../next-middleware-ssr-loader/index.ts | 16 +- .../loaders/next-middleware-wasm-loader.ts | 4 +- .../webpack/plugins/middleware-plugin.ts | 92 +-- .../plugins/middleware-source-maps-plugin.ts | 4 +- .../terser-webpack-plugin/src/index.js | 2 +- packages/next/server/base-server.ts | 13 +- packages/next/server/dev/hot-reloader.ts | 353 +++++------ packages/next/server/dev/next-dev-server.ts | 18 +- .../server/dev/on-demand-entry-handler.ts | 155 +++-- packages/next/server/next-server.ts | 20 +- packages/next/shared/lib/constants.ts | 4 +- .../index.test.ts | 2 +- .../core/pages/global/_middleware.js | 1 - .../middleware/core/test/index.test.js | 5 +- .../test/index.test.js | 2 +- test/production/required-server-files.test.ts | 2 +- 19 files changed, 805 insertions(+), 934 deletions(-) diff --git a/packages/next/build/entries.ts b/packages/next/build/entries.ts index 7fdaf4d0260c..10cc81da0b0d 100644 --- a/packages/next/build/entries.ts +++ b/packages/next/build/entries.ts @@ -1,36 +1,26 @@ -import type { - PageRuntime, - NextConfigComplete, - NextConfig, -} from '../server/config-shared' +import type { ClientPagesLoaderOptions } from './webpack/loaders/next-client-pages-loader' +import type { MiddlewareLoaderOptions } from './webpack/loaders/next-middleware-loader' +import type { MiddlewareSSRLoaderQuery } from './webpack/loaders/next-middleware-ssr-loader' +import type { NextConfigComplete, NextConfig } from '../server/config-shared' +import type { PageRuntime } from '../server/config-shared' +import type { ServerlessLoaderQuery } from './webpack/loaders/next-serverless-loader' import type { webpack5 } from 'next/dist/compiled/webpack/webpack' +import type { LoadedEnvFiles } from '@next/env' import fs from 'fs' import chalk from 'next/dist/compiled/chalk' import { posix, join } from 'path' import { stringify } from 'querystring' import { API_ROUTE, DOT_NEXT_ALIAS, PAGES_DIR_ALIAS } from '../lib/constants' +import { EDGE_RUNTIME_WEBPACK } from '../shared/lib/constants' import { MIDDLEWARE_ROUTE } from '../lib/constants' import { __ApiPreviewProps } from '../server/api-utils' import { isTargetLikeServerless } from '../server/utils' import { normalizePagePath } from '../server/normalize-page-path' import { normalizePathSep } from '../server/denormalize-page-path' +import { ssrEntries } from './webpack/plugins/middleware-plugin' import { warn } from './output/log' -import { MiddlewareLoaderOptions } from './webpack/loaders/next-middleware-loader' -import { ClientPagesLoaderOptions } from './webpack/loaders/next-client-pages-loader' -import { ServerlessLoaderQuery } from './webpack/loaders/next-serverless-loader' -import { LoadedEnvFiles } from '@next/env' import { parse } from '../build/swc' -import { - isCustomErrorPage, - isFlightPage, - isReservedPage, - withoutRSCExtensions, -} from './utils' -import { ssrEntries } from './webpack/plugins/middleware-plugin' -import { - MIDDLEWARE_RUNTIME_WEBPACK, - MIDDLEWARE_SSR_RUNTIME_WEBPACK, -} from '../shared/lib/constants' +import { isFlightPage, withoutRSCExtensions } from './utils' type ObjectValue = T extends { [key: string]: infer V } ? V : never @@ -117,12 +107,6 @@ export function createPagesMapping({ } } -type Entrypoints = { - client: webpack5.EntryObject - server: webpack5.EntryObject - edgeServer: webpack5.EntryObject -} - const cachedPageRuntimeConfig = new Map() // @TODO: We should limit the maximum concurrency of this function as there @@ -230,155 +214,171 @@ export function invalidatePageRuntimeCache( } } -export async function createEntrypoints( - pages: { [page: string]: string }, - target: 'server' | 'serverless' | 'experimental-serverless-trace', - buildId: string, - previewMode: __ApiPreviewProps, - config: NextConfigComplete, - loadedEnvFiles: LoadedEnvFiles, - pagesDir: string, +interface CreateEntrypointsParams { + buildId: string + config: NextConfigComplete + envFiles: LoadedEnvFiles isDev?: boolean -): Promise { - const client: webpack5.EntryObject = {} - const server: webpack5.EntryObject = {} - const edgeServer: webpack5.EntryObject = {} + pages: { [page: string]: string } + pagesDir: string + previewMode: __ApiPreviewProps + target: 'server' | 'serverless' | 'experimental-serverless-trace' +} + +export function getEdgeServerEntry(opts: { + absolutePagePath: string + buildId: string + bundlePath: string + config: NextConfigComplete + isDev: boolean + page: string + pages: { [page: string]: string } + ssrEntries: Map +}): ObjectValue { + if (opts.page.match(MIDDLEWARE_ROUTE)) { + const loaderParams: MiddlewareLoaderOptions = { + absolutePagePath: opts.absolutePagePath, + page: opts.page, + } + + return `next-middleware-loader?${stringify(loaderParams)}!` + } + + const loaderParams: MiddlewareSSRLoaderQuery = { + absolute500Path: opts.pages['/500'] || '', + absoluteAppPath: opts.pages['/_app'], + absoluteAppServerPath: opts.pages['/_app.server'], + absoluteDocumentPath: opts.pages['/_document'], + absoluteErrorPath: opts.pages['/_error'], + absolutePagePath: opts.absolutePagePath, + buildId: opts.buildId, + dev: opts.isDev, + isServerComponent: isFlightPage(opts.config, opts.absolutePagePath), + page: opts.page, + stringifiedConfig: JSON.stringify(opts.config), + } + + ssrEntries.set(opts.bundlePath, { + requireFlightManifest: isFlightPage(opts.config, opts.absolutePagePath), + }) + + return `next-middleware-ssr-loader?${stringify(loaderParams)}!` +} - const hasRuntimeConfig = - Object.keys(config.publicRuntimeConfig).length > 0 || - Object.keys(config.serverRuntimeConfig).length > 0 - const hasReactRoot = !!config.experimental.reactRoot - - const defaultServerlessOptions = { - absoluteAppPath: pages['/_app'], - absoluteAppServerPath: pages['/_app.server'], - absoluteDocumentPath: pages['/_document'], - absoluteErrorPath: pages['/_error'], - absolute404Path: pages['/404'] || '', +export function getServerlessEntry(opts: { + absolutePagePath: string + buildId: string + config: NextConfigComplete + envFiles: LoadedEnvFiles + page: string + previewMode: __ApiPreviewProps + pages: { [page: string]: string } +}): ObjectValue { + const loaderParams: ServerlessLoaderQuery = { + absolute404Path: opts.pages['/404'] || '', + absoluteAppPath: opts.pages['/_app'], + absoluteAppServerPath: opts.pages['/_app.server'], + absoluteDocumentPath: opts.pages['/_document'], + absoluteErrorPath: opts.pages['/_error'], + absolutePagePath: opts.absolutePagePath, + assetPrefix: opts.config.assetPrefix, + basePath: opts.config.basePath, + buildId: opts.buildId, + canonicalBase: opts.config.amp.canonicalBase || '', distDir: DOT_NEXT_ALIAS, - buildId, - assetPrefix: config.assetPrefix, - generateEtags: config.generateEtags ? 'true' : '', - poweredByHeader: config.poweredByHeader ? 'true' : '', - canonicalBase: config.amp.canonicalBase || '', - basePath: config.basePath, - runtimeConfig: hasRuntimeConfig - ? JSON.stringify({ - publicRuntimeConfig: config.publicRuntimeConfig, - serverRuntimeConfig: config.serverRuntimeConfig, - }) - : '', - previewProps: JSON.stringify(previewMode), + generateEtags: opts.config.generateEtags ? 'true' : '', + i18n: opts.config.i18n ? JSON.stringify(opts.config.i18n) : '', // base64 encode to make sure contents don't break webpack URL loading - loadedEnvFiles: Buffer.from(JSON.stringify(loadedEnvFiles)).toString( + loadedEnvFiles: Buffer.from(JSON.stringify(opts.envFiles)).toString( 'base64' ), - i18n: config.i18n ? JSON.stringify(config.i18n) : '', - reactRoot: hasReactRoot ? 'true' : '', + page: opts.page, + poweredByHeader: opts.config.poweredByHeader ? 'true' : '', + previewProps: JSON.stringify(opts.previewMode), + reactRoot: !!opts.config.experimental.reactRoot ? 'true' : '', + runtimeConfig: + Object.keys(opts.config.publicRuntimeConfig).length > 0 || + Object.keys(opts.config.serverRuntimeConfig).length > 0 + ? JSON.stringify({ + publicRuntimeConfig: opts.config.publicRuntimeConfig, + serverRuntimeConfig: opts.config.serverRuntimeConfig, + }) + : '', } + return `next-serverless-loader?${stringify(loaderParams)}!` +} + +export function getClientEntry(opts: { + absolutePagePath: string + page: string +}) { + const loaderOptions: ClientPagesLoaderOptions = { + absolutePagePath: opts.absolutePagePath, + page: opts.page, + } + + const pageLoader = `next-client-pages-loader?${stringify(loaderOptions)}!` + + // Make sure next/router is a dependency of _app or else chunk splitting + // might cause the router to not be able to load causing hydration + // to fail + return opts.page === '/_app' + ? [pageLoader, require.resolve('../client/router')] + : pageLoader +} + +export async function createEntrypoints(params: CreateEntrypointsParams) { + const { config, pages, pagesDir, isDev, target } = params + const edgeServer: webpack5.EntryObject = {} + const server: webpack5.EntryObject = {} + const client: webpack5.EntryObject = {} + await Promise.all( Object.keys(pages).map(async (page) => { - const absolutePagePath = pages[page] const bundleFile = normalizePagePath(page) - const isApiRoute = page.match(API_ROUTE) - const clientBundlePath = posix.join('pages', bundleFile) const serverBundlePath = posix.join('pages', bundleFile) - const isLikeServerless = isTargetLikeServerless(target) - const isReserved = isReservedPage(page) - const isCustomError = isCustomErrorPage(page) - const isFlight = isFlightPage(config, absolutePagePath) - const isInternalPages = !absolutePagePath.startsWith(PAGES_DIR_ALIAS) - const pageFilePath = isInternalPages - ? require.resolve(absolutePagePath) - : join(pagesDir, absolutePagePath.replace(PAGES_DIR_ALIAS, '')) - const pageRuntime = await getPageRuntime(pageFilePath, config, isDev) - const isEdgeRuntime = pageRuntime === 'edge' - - if (page.match(MIDDLEWARE_ROUTE)) { - const loaderOpts: MiddlewareLoaderOptions = { - absolutePagePath: pages[page], - page, - } - - client[clientBundlePath] = `next-middleware-loader?${stringify( - loaderOpts - )}!` - return - } - - if (isEdgeRuntime && !isReserved && !isCustomError && !isApiRoute) { - ssrEntries.set(clientBundlePath, { requireFlightManifest: isFlight }) - edgeServer[serverBundlePath] = finalizeEntrypoint({ - name: '[name].js', - value: `next-middleware-ssr-loader?${stringify({ - dev: false, + runDependingOnPageType({ + page, + pageRuntime: await getPageRuntime( + !pages[page].startsWith(PAGES_DIR_ALIAS) + ? require.resolve(pages[page]) + : join(pagesDir, pages[page].replace(PAGES_DIR_ALIAS, '')), + config, + isDev + ), + onClient: () => { + client[clientBundlePath] = getClientEntry({ + absolutePagePath: pages[page], page, - stringifiedConfig: JSON.stringify(config), - absolute500Path: pages['/500'] || '', - absolutePagePath, - isServerComponent: isFlight, - ...defaultServerlessOptions, - } as any)}!`, - isServer: false, - isEdgeServer: true, - }) - } - - if (isApiRoute && isLikeServerless) { - const serverlessLoaderOptions: ServerlessLoaderQuery = { - page, - absolutePagePath, - ...defaultServerlessOptions, - } - server[serverBundlePath] = `next-serverless-loader?${stringify( - serverlessLoaderOptions - )}!` - } else if (isApiRoute || target === 'server') { - if (!isEdgeRuntime || isReserved || isCustomError) { - server[serverBundlePath] = [absolutePagePath] - } - } else if ( - isLikeServerless && - page !== '/_app' && - page !== '/_app.server' && - page !== '/_document' && - !isEdgeRuntime - ) { - const serverlessLoaderOptions: ServerlessLoaderQuery = { - page, - absolutePagePath, - ...defaultServerlessOptions, - } - server[serverBundlePath] = `next-serverless-loader?${stringify( - serverlessLoaderOptions - )}!` - } - - if (page === '/_document' || page === '/_app.server') { - return - } - - if (!isApiRoute) { - const pageLoaderOpts: ClientPagesLoaderOptions = { - page, - absolutePagePath, - } - const pageLoader = `next-client-pages-loader?${stringify( - pageLoaderOpts - )}!` - - // Make sure next/router is a dependency of _app or else chunk splitting - // might cause the router to not be able to load causing hydration - // to fail - - client[clientBundlePath] = - page === '/_app' - ? [pageLoader, require.resolve('../client/router')] - : pageLoader - } + }) + }, + onServer: () => { + if (isTargetLikeServerless(target)) { + if (page !== '/_app' && page !== '/_document') { + server[serverBundlePath] = getServerlessEntry({ + ...params, + absolutePagePath: pages[page], + page, + }) + } + } else { + server[serverBundlePath] = [pages[page]] + } + }, + onEdgeServer: () => { + edgeServer[serverBundlePath] = getEdgeServerEntry({ + ...params, + absolutePagePath: pages[page], + bundlePath: clientBundlePath, + isDev: false, + page, + ssrEntries, + }) + }, + }) }) ) @@ -389,25 +389,49 @@ export async function createEntrypoints( } } +export function runDependingOnPageType(params: { + onClient: () => T + onEdgeServer: () => T + onServer: () => T + page: string + pageRuntime: PageRuntime +}) { + if (params.page.match(MIDDLEWARE_ROUTE)) { + return [params.onEdgeServer()] + } else if (params.page.match(API_ROUTE)) { + return [params.onServer()] + } else if (params.page === '/_document') { + return [params.onServer()] + } else if ( + params.page === '/_app' || + params.page === '/_error' || + params.page === '/404' || + params.page === '/500' + ) { + return [params.onClient(), params.onServer()] + } else { + return [ + params.onClient(), + params.pageRuntime === 'edge' ? params.onEdgeServer() : params.onServer(), + ] + } +} + export function finalizeEntrypoint({ name, + compilerType, value, - isServer, - isMiddleware, - isEdgeServer, }: { - isServer: boolean + compilerType?: 'client' | 'server' | 'edge-server' name: string value: ObjectValue - isMiddleware?: boolean - isEdgeServer?: boolean }): ObjectValue { const entry = typeof value !== 'object' || Array.isArray(value) ? { import: value } : value - if (isServer) { + if (compilerType === 'server') { const isApi = name.startsWith('pages/api/') return { publicPath: isApi ? '' : undefined, @@ -417,34 +441,18 @@ export function finalizeEntrypoint({ } } - if (isEdgeServer) { - const ssrMiddlewareEntry = { - library: { - name: ['_ENTRIES', `middleware_[name]`], - type: 'assign', - }, - runtime: MIDDLEWARE_SSR_RUNTIME_WEBPACK, - asyncChunks: false, - ...entry, - } - return ssrMiddlewareEntry - } - if (isMiddleware) { - const middlewareEntry = { - filename: 'server/[name].js', - layer: 'middleware', - library: { - name: ['_ENTRIES', `middleware_[name]`], - type: 'assign', - }, - runtime: MIDDLEWARE_RUNTIME_WEBPACK, + if (compilerType === 'edge-server') { + return { + layer: MIDDLEWARE_ROUTE.test(name) ? 'middleware' : undefined, + library: { name: ['_ENTRIES', `middleware_[name]`], type: 'assign' }, + runtime: EDGE_RUNTIME_WEBPACK, asyncChunks: false, ...entry, } - return middlewareEntry } if ( + // Client special cases name !== 'polyfills' && name !== 'main' && name !== 'amp' && diff --git a/packages/next/build/index.ts b/packages/next/build/index.ts index 9937d7c19e74..c230b63cf834 100644 --- a/packages/next/build/index.ts +++ b/packages/next/build/index.ts @@ -318,17 +318,18 @@ export default async function build( const entrypoints = await nextBuildSpan .traceChild('create-entrypoints') .traceAsyncFn(() => - createEntrypoints( - mappedPages, - target, + createEntrypoints({ buildId, - previewProps, config, - loadedEnvFiles, + envFiles: loadedEnvFiles, + isDev: false, + pages: mappedPages, pagesDir, - false - ) + previewMode: previewProps, + target, + }) ) + const pageKeys = Object.keys(mappedPages) const conflictingPublicFiles: string[] = [] const hasPages404 = mappedPages['/404']?.startsWith(PAGES_DIR_ALIAS) @@ -564,11 +565,8 @@ export default async function build( ) ) - const manifestPath = path.join( - distDir, - isLikeServerless ? SERVERLESS_DIRECTORY : SERVER_DIRECTORY, - PAGES_MANIFEST - ) + const serverDir = isLikeServerless ? SERVERLESS_DIRECTORY : SERVER_DIRECTORY + const manifestPath = path.join(distDir, serverDir, PAGES_MANIFEST) const requiredServerFiles = nextBuildSpan .traceChild('generate-required-server-files') @@ -599,12 +597,7 @@ export default async function build( ] : []), REACT_LOADABLE_MANIFEST, - config.optimizeFonts - ? path.join( - isLikeServerless ? SERVERLESS_DIRECTORY : SERVER_DIRECTORY, - FONT_MANIFEST - ) - : null, + config.optimizeFonts ? path.join(serverDir, FONT_MANIFEST) : null, BUILD_ID_FILE, ] .filter(nonNullable) @@ -618,49 +611,37 @@ export default async function build( await (async () => { // IIFE to isolate locals and avoid retaining memory too long const runWebpackSpan = nextBuildSpan.traceChild('run-webpack-compiler') + + const commonWebpackOptions = { + buildId, + config, + hasReactRoot, + pagesDir, + reactProductionProfiling, + rewrites, + runWebpackSpan, + target, + } + const configs = await runWebpackSpan .traceChild('generate-webpack-config') .traceAsyncFn(() => Promise.all([ getBaseWebpackConfig(dir, { - buildId, - reactProductionProfiling, - isServer: false, - config, - target, - pagesDir, + ...commonWebpackOptions, + compilerType: 'client', entrypoints: entrypoints.client, - rewrites, - runWebpackSpan, - hasReactRoot, }), getBaseWebpackConfig(dir, { - buildId, - reactProductionProfiling, - isServer: true, - config, - target, - pagesDir, + ...commonWebpackOptions, + compilerType: 'server', entrypoints: entrypoints.server, - rewrites, - runWebpackSpan, - hasReactRoot, }), - hasReactRoot - ? getBaseWebpackConfig(dir, { - buildId, - reactProductionProfiling, - isServer: true, - isEdgeRuntime: true, - config, - target, - pagesDir, - entrypoints: entrypoints.edgeServer, - rewrites, - runWebpackSpan, - hasReactRoot, - }) - : null, + getBaseWebpackConfig(dir, { + ...commonWebpackOptions, + compilerType: 'edge-server', + entrypoints: entrypoints.edgeServer, + }), ]) ) @@ -1468,7 +1449,7 @@ export default async function build( const middlewareManifest: MiddlewareManifest = JSON.parse( await promises.readFile( - path.join(distDir, SERVER_DIRECTORY, MIDDLEWARE_MANIFEST), + path.join(distDir, serverDir, MIDDLEWARE_MANIFEST), 'utf8' ) ) @@ -1655,10 +1636,6 @@ export default async function build( const serverBundle = getPagePath(page, distDir, isLikeServerless) await promises.unlink(serverBundle) } - const serverOutputDir = path.join( - distDir, - isLikeServerless ? SERVERLESS_DIRECTORY : SERVER_DIRECTORY - ) const moveExportedPage = async ( originPage: string, @@ -1681,7 +1658,7 @@ export default async function build( const relativeDest = path .relative( - serverOutputDir, + path.join(distDir, serverDir), path.join( path.join( pagePath, @@ -1698,12 +1675,6 @@ export default async function build( ) .replace(/\\/g, '/') - const dest = path.join( - distDir, - isLikeServerless ? SERVERLESS_DIRECTORY : SERVER_DIRECTORY, - relativeDest - ) - if ( !isSsg && !( @@ -1718,6 +1689,7 @@ export default async function build( pagesManifest[page] = relativeDest } + const dest = path.join(distDir, serverDir, relativeDest) const isNotFound = ssgNotFoundPaths.includes(page) // for SSG files with i18n the non-prerendered variants are @@ -1763,7 +1735,7 @@ export default async function build( ) const updatedDest = path.join( distDir, - isLikeServerless ? SERVERLESS_DIRECTORY : SERVER_DIRECTORY, + serverDir, updatedRelativeDest ) diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts index fb6451e0b7b7..177ed279ec54 100644 --- a/packages/next/build/webpack-config.ts +++ b/packages/next/build/webpack-config.ts @@ -10,7 +10,6 @@ import { NEXT_PROJECT_ROOT, NEXT_PROJECT_ROOT_DIST_CLIENT, PAGES_DIR_ALIAS, - MIDDLEWARE_ROUTE, } from '../lib/constants' import { fileExists } from '../lib/file-exists' import { CustomRoutes } from '../lib/load-custom-routes.js' @@ -303,33 +302,34 @@ export default async function getBaseWebpackConfig( { buildId, config, + compilerType, dev = false, - isServer = false, - isEdgeRuntime = false, + entrypoints, + hasReactRoot, + isDevFallback = false, pagesDir, - target = 'server', reactProductionProfiling = false, - entrypoints, rewrites, - isDevFallback = false, runWebpackSpan, - hasReactRoot, + target = 'server', }: { buildId: string config: NextConfigComplete + compilerType: 'client' | 'server' | 'edge-server' dev?: boolean - isServer?: boolean - isEdgeRuntime?: boolean + entrypoints: webpack5.EntryObject + hasReactRoot: boolean + isDevFallback?: boolean pagesDir: string - target?: string reactProductionProfiling?: boolean - entrypoints: webpack5.EntryObject rewrites: CustomRoutes['rewrites'] - isDevFallback?: boolean runWebpackSpan: Span - hasReactRoot: boolean + target?: string } ): Promise { + const isClient = compilerType === 'client' + const isEdgeServer = compilerType === 'edge-server' + const isNodeServer = compilerType === 'server' const { useTypeScript, jsConfig, resolvedBaseUrl } = await loadJsConfig( dir, config @@ -339,9 +339,6 @@ export default async function getBaseWebpackConfig( rewrites.beforeFiles.length > 0 || rewrites.afterFiles.length > 0 || rewrites.fallback.length > 0 - const hasReactRefresh: boolean = dev && !isServer - - const runtime = config.experimental.runtime // Make sure `reactRoot` is enabled when React 18 or experimental is detected. if (hasReactRoot) { @@ -349,11 +346,12 @@ export default async function getBaseWebpackConfig( } // Only inform during one of the builds - if (!isServer && config.experimental.reactRoot && !hasReactRoot) { + if (isClient && config.experimental.reactRoot && !hasReactRoot) { // It's fine to only mention React 18 here as we don't recommend people to try experimental. Log.warn('You have to use React 18 to use `experimental.reactRoot`.') } - if (!isServer && runtime && !hasReactRoot) { + + if (isClient && config.experimental.runtime && !hasReactRoot) { throw new Error( '`experimental.runtime` requires `experimental.reactRoot` to be enabled along with React 18.' ) @@ -364,7 +362,6 @@ export default async function getBaseWebpackConfig( ) } - const targetWeb = isEdgeRuntime || !isServer const hasConcurrentFeatures = hasReactRoot const hasServerComponents = hasConcurrentFeatures && !!config.experimental.serverComponents @@ -372,13 +369,13 @@ export default async function getBaseWebpackConfig( ? true : config.experimental.disableOptimizedLoading - if (!isServer) { - if (runtime === 'edge') { + if (isClient) { + if (config.experimental.runtime === 'edge') { Log.warn( 'You are using the experimental Edge Runtime with `experimental.runtime`.' ) } - if (runtime === 'nodejs') { + if (config.experimental.runtime === 'nodejs') { Log.warn( 'You are using the experimental Node.js Runtime with `experimental.runtime`.' ) @@ -438,14 +435,14 @@ export default async function getBaseWebpackConfig( loggedIgnoredCompilerOptions = true } - const getBabelOrSwcLoader = (isMiddleware: boolean) => { + const getBabelOrSwcLoader = () => { return useSWCLoader ? { loader: 'next-swc-loader', options: { - isServer: isMiddleware || isServer, + isServer: isNodeServer || isEdgeServer, pagesDir, - hasReactRefresh: !isMiddleware && hasReactRefresh, + hasReactRefresh: dev && isClient, fileReading: config.experimental.swcFileReading, nextConfig: config, jsConfig, @@ -455,19 +452,19 @@ export default async function getBaseWebpackConfig( loader: require.resolve('./babel/loader/index'), options: { configFile: babelConfigFile, - isServer: isMiddleware ? true : isServer, + isServer: isNodeServer || isEdgeServer, distDir, pagesDir, cwd: dir, development: dev, - hasReactRefresh: isMiddleware ? false : hasReactRefresh, + hasReactRefresh: dev && isClient, hasJsxRuntime: true, }, } } const defaultLoaders = { - babel: getBabelOrSwcLoader(false), + babel: getBabelOrSwcLoader(), } const rawPageExtensions = hasServerComponents @@ -490,15 +487,19 @@ export default async function getBaseWebpackConfig( .split(process.platform === 'win32' ? ';' : ':') .filter((p) => !!p) - const isServerless = target === 'serverless' - const isServerlessTrace = target === 'experimental-serverless-trace' // Intentionally not using isTargetLikeServerless helper - const isLikeServerless = isServerless || isServerlessTrace + const isLikeServerless = + target === 'serverless' || target === 'experimental-serverless-trace' + + const outputPath = + isNodeServer || isEdgeServer + ? path.join( + distDir, + isLikeServerless ? SERVERLESS_DIRECTORY : SERVER_DIRECTORY + ) + : distDir - const outputDir = isLikeServerless ? SERVERLESS_DIRECTORY : SERVER_DIRECTORY - const outputPath = path.join(distDir, isServer ? outputDir : '') - const totalPages = Object.keys(entrypoints).length - const clientEntries = !isServer + const clientEntries = isClient ? ({ // Backwards compatibility 'main.js': [], @@ -582,7 +583,7 @@ export default async function getBaseWebpackConfig( const resolveConfig = { // Disable .mjs for node_modules bundling - extensions: !targetWeb + extensions: isNodeServer ? [ '.js', '.mjs', @@ -612,10 +613,10 @@ export default async function getBaseWebpackConfig( [PAGES_DIR_ALIAS]: pagesDir, [DOT_NEXT_ALIAS]: distDir, - ...(targetWeb ? getOptimizedAliases() : {}), + ...(isClient || isEdgeServer ? getOptimizedAliases() : {}), ...getReactProfilingInProduction(), - ...(targetWeb + ...(isClient || isEdgeServer ? { [clientResolveRewrites]: hasRewrites ? clientResolveRewrites @@ -626,15 +627,17 @@ export default async function getBaseWebpackConfig( setimmediate: 'next/dist/compiled/setimmediate', }, - ...(isEdgeRuntime + ...(isClient || isEdgeServer ? { fallback: { process: require.resolve('./polyfills/process'), }, } : undefined), - mainFields: targetWeb - ? (isEdgeRuntime ? [] : ['browser']).concat(['module', 'main']) + mainFields: isClient + ? ['browser', 'module', 'main'] + : isEdgeServer + ? ['module', 'main'] : ['main', 'module'], plugins: [], } @@ -713,83 +716,6 @@ export default async function getBaseWebpackConfig( addPackagePath(packageName, dir) } - // Select appropriate SplitChunksPlugin config for this build - const splitChunksConfig: webpack.Options.SplitChunksOptions | false = dev - ? false - : { - // Keep main and _app chunks unsplitted in webpack 5 - // as we don't need a separate vendor chunk from that - // and all other chunk depend on them so there is no - // duplication that need to be pulled out. - chunks: (chunk) => - !/^(polyfills|main|pages\/_app)$/.test(chunk.name) && - !MIDDLEWARE_ROUTE.test(chunk.name), - cacheGroups: { - framework: { - chunks: (chunk: webpack.compilation.Chunk) => - !chunk.name?.match(MIDDLEWARE_ROUTE), - name: 'framework', - test(module) { - const resource = - module.nameForCondition && module.nameForCondition() - if (!resource) { - return false - } - return topLevelFrameworkPaths.some((packagePath) => - resource.startsWith(packagePath) - ) - }, - priority: 40, - // Don't let webpack eliminate this chunk (prevents this chunk from - // becoming a part of the commons chunk) - enforce: true, - }, - lib: { - test(module: { - size: Function - nameForCondition: Function - }): boolean { - return ( - module.size() > 160000 && - /node_modules[/\\]/.test(module.nameForCondition() || '') - ) - }, - name(module: { - type: string - libIdent?: Function - updateHash: (hash: crypto.Hash) => void - }): string { - const hash = crypto.createHash('sha1') - if (isModuleCSS(module)) { - module.updateHash(hash) - } else { - if (!module.libIdent) { - throw new Error( - `Encountered unknown module type: ${module.type}. Please open an issue.` - ) - } - - hash.update(module.libIdent({ context: dir })) - } - - return hash.digest('hex').substring(0, 8) - }, - priority: 30, - minChunks: 1, - reuseExistingChunk: true, - }, - middleware: { - chunks: (chunk: webpack.compilation.Chunk) => - chunk.name?.match(MIDDLEWARE_ROUTE), - filename: 'server/middleware-chunks/[name].js', - minChunks: 2, - enforce: true, - }, - }, - maxInitialRequests: 25, - minSize: 20000, - } - const crossOrigin = config.crossOrigin const looseEsmExternals = config.experimental?.esmExternals === 'loose' @@ -958,74 +884,75 @@ export default async function getBaseWebpackConfig( let webpackConfig: webpack.Configuration = { parallelism: Number(process.env.NEXT_WEBPACK_PARALLELISM) || undefined, - externals: targetWeb - ? // make sure importing "next" is handled gracefully for client - // bundles in case a user imported types and it wasn't removed - // TODO: should we warn/error for this instead? - [ - 'next', - ...(isEdgeRuntime - ? [ - { - '@builder.io/partytown': '{}', - 'next/dist/compiled/etag': '{}', - 'next/dist/compiled/chalk': '{}', - 'react-dom': '{}', - }, - ] - : []), - ] - : !isServerless - ? [ - ({ - context, - request, - dependencyType, - getResolve, - }: { - context: string - request: string - dependencyType: string - getResolve: ( - options: any - ) => ( - resolveContext: string, - resolveRequest: string, - callback: ( - err?: Error, - result?: string, - resolveData?: { descriptionFileData?: { type?: any } } + externals: + isClient || isEdgeServer + ? // make sure importing "next" is handled gracefully for client + // bundles in case a user imported types and it wasn't removed + // TODO: should we warn/error for this instead? + [ + 'next', + ...(isEdgeServer + ? [ + { + '@builder.io/partytown': '{}', + 'next/dist/compiled/etag': '{}', + 'next/dist/compiled/chalk': '{}', + 'react-dom': '{}', + }, + ] + : []), + ] + : target !== 'serverless' + ? [ + ({ + context, + request, + dependencyType, + getResolve, + }: { + context: string + request: string + dependencyType: string + getResolve: ( + options: any + ) => ( + resolveContext: string, + resolveRequest: string, + callback: ( + err?: Error, + result?: string, + resolveData?: { descriptionFileData?: { type?: any } } + ) => void ) => void - ) => void - }) => - handleExternals(context, request, dependencyType, (options) => { - const resolveFunction = getResolve(options) - return (resolveContext: string, requestToResolve: string) => - new Promise((resolve, reject) => { - resolveFunction( - resolveContext, - requestToResolve, - (err, result, resolveData) => { - if (err) return reject(err) - if (!result) return resolve([null, false]) - const isEsm = /\.js$/i.test(result) - ? resolveData?.descriptionFileData?.type === 'module' - : /\.mjs$/i.test(result) - resolve([result, isEsm]) - } - ) - }) - }), - ] - : [ - // When the 'serverless' target is used all node_modules will be compiled into the output bundles - // So that the 'serverless' bundles have 0 runtime dependencies - 'next/dist/compiled/@ampproject/toolbox-optimizer', // except this one - - // Mark this as external if not enabled so it doesn't cause a - // webpack error from being missing - ...(config.experimental.optimizeCss ? [] : ['critters']), - ], + }) => + handleExternals(context, request, dependencyType, (options) => { + const resolveFunction = getResolve(options) + return (resolveContext: string, requestToResolve: string) => + new Promise((resolve, reject) => { + resolveFunction( + resolveContext, + requestToResolve, + (err, result, resolveData) => { + if (err) return reject(err) + if (!result) return resolve([null, false]) + const isEsm = /\.js$/i.test(result) + ? resolveData?.descriptionFileData?.type === 'module' + : /\.mjs$/i.test(result) + resolve([result, isEsm]) + } + ) + }) + }), + ] + : [ + // When the 'serverless' target is used all node_modules will be compiled into the output bundles + // So that the 'serverless' bundles have 0 runtime dependencies + 'next/dist/compiled/@ampproject/toolbox-optimizer', // except this one + + // Mark this as external if not enabled so it doesn't cause a + // webpack error from being missing + ...(config.experimental.optimizeCss ? [] : ['critters']), + ], optimization: { // @ts-ignore: TODO remove ts-ignore when webpack 4 is removed emitOnErrors: !dev, @@ -1037,22 +964,94 @@ export default async function getBaseWebpackConfig( moduleIds: 'named', } : {}), - splitChunks: isServer - ? dev - ? false - : ({ - filename: isEdgeRuntime ? 'chunks/[name].js' : '[name].js', - // allow to split entrypoints - chunks: ({ name }: any) => !name?.match(MIDDLEWARE_ROUTE), - // size of files is not so relevant for server build - // we want to prefer deduplication to load less code - minSize: 1000, - } as any) - : splitChunksConfig, - runtimeChunk: isServer - ? undefined - : { name: CLIENT_STATIC_FILES_RUNTIME_WEBPACK }, - minimize: !dev && targetWeb, + splitChunks: ((): webpack.Options.SplitChunksOptions | false => { + if (dev) { + return false + } + + if (isNodeServer) { + return { + // @ts-ignore + filename: '[name].js', + chunks: 'all', + minSize: 1000, + } + } + + if (isEdgeServer) { + return { + // @ts-ignore + filename: 'edge-chunks/[name].js', + chunks: 'all', + minChunks: 2, + } + } + + return { + // Keep main and _app chunks unsplitted in webpack 5 + // as we don't need a separate vendor chunk from that + // and all other chunk depend on them so there is no + // duplication that need to be pulled out. + chunks: (chunk) => !/^(polyfills|main|pages\/_app)$/.test(chunk.name), + cacheGroups: { + framework: { + chunks: 'all', + name: 'framework', + test(module) { + const resource = module.nameForCondition?.() + return resource + ? topLevelFrameworkPaths.some((pkgPath) => + resource.startsWith(pkgPath) + ) + : false + }, + priority: 40, + // Don't let webpack eliminate this chunk (prevents this chunk from + // becoming a part of the commons chunk) + enforce: true, + }, + lib: { + test(module: { + size: Function + nameForCondition: Function + }): boolean { + return ( + module.size() > 160000 && + /node_modules[/\\]/.test(module.nameForCondition() || '') + ) + }, + name(module: { + type: string + libIdent?: Function + updateHash: (hash: crypto.Hash) => void + }): string { + const hash = crypto.createHash('sha1') + if (isModuleCSS(module)) { + module.updateHash(hash) + } else { + if (!module.libIdent) { + throw new Error( + `Encountered unknown module type: ${module.type}. Please open an issue.` + ) + } + hash.update(module.libIdent({ context: dir })) + } + + return hash.digest('hex').substring(0, 8) + }, + priority: 30, + minChunks: 1, + reuseExistingChunk: true, + }, + }, + maxInitialRequests: 25, + minSize: 20000, + } + })(), + runtimeChunk: isClient + ? { name: CLIENT_STATIC_FILES_RUNTIME_WEBPACK } + : undefined, + minimize: !dev && isClient, minimizer: [ // Minify JavaScript (compiler: webpack.Compiler) => { @@ -1101,29 +1100,28 @@ export default async function getBaseWebpackConfig( // we must set publicPath to an empty value to override the default of // auto which doesn't work in IE11 publicPath: `${config.assetPrefix || ''}/_next/`, - path: - isServer && !dev && !isEdgeRuntime - ? path.join(outputPath, 'chunks') - : outputPath, + path: !dev && isNodeServer ? path.join(outputPath, 'chunks') : outputPath, // On the server we don't use hashes - filename: isServer - ? !dev && !isEdgeRuntime - ? `../[name].js` - : `[name].js` - : `static/chunks/${isDevFallback ? 'fallback/' : ''}[name]${ - dev ? '' : '-[contenthash]' - }.js`, - library: targetWeb ? '_N_E' : undefined, - libraryTarget: targetWeb ? 'assign' : 'commonjs2', + filename: + isNodeServer || isEdgeServer + ? dev || isEdgeServer + ? `[name].js` + : `../[name].js` + : `static/chunks/${isDevFallback ? 'fallback/' : ''}[name]${ + dev ? '' : '-[contenthash]' + }.js`, + library: isClient || isEdgeServer ? '_N_E' : undefined, + libraryTarget: isClient || isEdgeServer ? 'assign' : 'commonjs2', hotUpdateChunkFilename: 'static/webpack/[id].[fullhash].hot-update.js', hotUpdateMainFilename: 'static/webpack/[fullhash].[runtime].hot-update.json', // This saves chunks with the name given via `import()` - chunkFilename: isServer - ? '[name].js' - : `static/chunks/${isDevFallback ? 'fallback/' : ''}${ - dev ? '[name]' : '[name].[contenthash]' - }.js`, + chunkFilename: + isNodeServer || isEdgeServer + ? '[name].js' + : `static/chunks/${isDevFallback ? 'fallback/' : ''}${ + dev ? '[name]' : '[name].[contenthash]' + }.js`, strictModuleExceptionHandling: true, crossOriginLoading: crossOrigin, webassemblyModuleFilename: 'static/wasm/[modulehash].wasm', @@ -1174,7 +1172,7 @@ export default async function getBaseWebpackConfig( ] : []), ...(hasServerComponents - ? isServer + ? isNodeServer || isEdgeServer ? [ // RSC server compilation loaders { @@ -1219,18 +1217,19 @@ export default async function getBaseWebpackConfig( { ...codeCondition, issuerLayer: 'middleware', - use: getBabelOrSwcLoader(true), + use: getBabelOrSwcLoader(), }, { ...codeCondition, - use: hasReactRefresh - ? [ - require.resolve( - 'next/dist/compiled/@next/react-refresh-utils/loader' - ), - defaultLoaders.babel, - ] - : defaultLoaders.babel, + use: + dev && isClient + ? [ + require.resolve( + 'next/dist/compiled/@next/react-refresh-utils/loader' + ), + defaultLoaders.babel, + ] + : defaultLoaders.babel, }, ], }, @@ -1242,7 +1241,7 @@ export default async function getBaseWebpackConfig( issuer: { not: regexLikeCss }, dependency: { not: ['url'] }, options: { - isServer, + isServer: isNodeServer || isEdgeServer, isDev: dev, basePath: config.basePath, assetPrefix: config.assetPrefix, @@ -1250,7 +1249,7 @@ export default async function getBaseWebpackConfig( }, ] : []), - ...(!isServer && !isEdgeRuntime + ...(isEdgeServer || isClient ? [ { oneOf: [ @@ -1329,19 +1328,19 @@ export default async function getBaseWebpackConfig( }, plugins: [ ...(!dev && - !isServer && + isEdgeServer && !!config.experimental.middlewareSourceMaps && !config.productionBrowserSourceMaps ? getMiddlewareSourceMapPlugins() : []), - hasReactRefresh && new ReactRefreshWebpackPlugin(webpack), + dev && isClient && new ReactRefreshWebpackPlugin(webpack), // Makes sure `Buffer` and `process` are polyfilled in client and flight bundles (same behavior as webpack 4) - targetWeb && + (isClient || isEdgeServer) && new webpack.ProvidePlugin({ // Buffer is used by getInlineScriptSource Buffer: [require.resolve('buffer'), 'Buffer'], // Avoid process being overridden when in web run time - ...(!isServer && { process: [require.resolve('process')] }), + ...(isClient && { process: [require.resolve('process')] }), }), new webpack.DefinePlugin({ ...Object.keys(process.env).reduce( @@ -1365,21 +1364,21 @@ export default async function getBaseWebpackConfig( 'process.env.NODE_ENV': JSON.stringify( dev ? 'development' : 'production' ), - ...(isServer && { + ...((isNodeServer || isEdgeServer) && { 'process.env.NEXT_RUNTIME': JSON.stringify( - isEdgeRuntime ? 'edge' : 'nodejs' + isEdgeServer ? 'edge' : 'nodejs' ), }), 'process.env.__NEXT_NEW_LINK_BEHAVIOR': JSON.stringify( config.experimental.newNextLinkBehavior ), 'process.env.__NEXT_CROSS_ORIGIN': JSON.stringify(crossOrigin), - 'process.browser': JSON.stringify(!isServer), + 'process.browser': JSON.stringify(isClient), 'process.env.__NEXT_TEST_MODE': JSON.stringify( process.env.__NEXT_TEST_MODE ), // This is used in client/dev-error-overlay/hot-dev-client.js to replace the dist directory - ...(dev && !isServer + ...(dev && (isClient || isEdgeServer) ? { 'process.env.__NEXT_DIST_DIR': JSON.stringify(distDir), } @@ -1434,7 +1433,7 @@ export default async function getBaseWebpackConfig( 'process.env.__NEXT_I18N_SUPPORT': JSON.stringify(!!config.i18n), 'process.env.__NEXT_I18N_DOMAINS': JSON.stringify(config.i18n?.domains), 'process.env.__NEXT_ANALYTICS_ID': JSON.stringify(config.analyticsId), - ...(isServer + ...(isNodeServer || isEdgeServer ? { // Fix bad-actors in the npm ecosystem (e.g. `node-formidable`) // This is typically found in unmaintained modules from the @@ -1447,7 +1446,7 @@ export default async function getBaseWebpackConfig( ...(config.experimental.pageEnv && dev ? { 'process.env': ` - new Proxy(${!targetWeb ? 'process.env' : '{}'}, { + new Proxy(${isNodeServer ? 'process.env' : '{}'}, { get(target, prop) { if (typeof target[prop] === 'undefined') { console.warn(\`An environment variable (\${prop}) that was not provided in the environment was accessed.\nSee more info here: https://nextjs.org/docs/messages/missing-env-value\`) @@ -1459,7 +1458,7 @@ export default async function getBaseWebpackConfig( } : {}), }), - !isServer && + isClient && new ReactLoadablePlugin({ filename: REACT_LOADABLE_MANIFEST, pagesDir, @@ -1468,10 +1467,10 @@ export default async function getBaseWebpackConfig( : undefined, dev, }), - targetWeb && new DropClientPage(), + (isClient || isEdgeServer) && new DropClientPage(), config.outputFileTracing && !isLikeServerless && - isServer && + (isNodeServer || isEdgeServer) && !dev && new TraceEntryPointsPlugin({ appDir: dir, @@ -1497,7 +1496,7 @@ export default async function getBaseWebpackConfig( } = require('./webpack/plugins/nextjs-require-cache-hot-reloader') const devPlugins = [new NextJsRequireCacheHotReloader()] - if (targetWeb) { + if (isClient || isEdgeServer) { devPlugins.push(new webpack.HotModuleReplacementPlugin()) } @@ -1509,18 +1508,19 @@ export default async function getBaseWebpackConfig( resourceRegExp: /react-is/, contextRegExp: /next[\\/]dist[\\/]/, }), - ((isServerless && isServer) || isEdgeRuntime) && new ServerlessPlugin(), - isServer && + target === 'serverless' && + (isNodeServer || isEdgeServer) && + new ServerlessPlugin(), + (isNodeServer || isEdgeServer) && new PagesManifestPlugin({ serverless: isLikeServerless, dev, - isEdgeRuntime, + isEdgeRuntime: isEdgeServer, }), // MiddlewarePlugin should be after DefinePlugin so NEXT_PUBLIC_* // replacement is done before its process.env.* handling - (!isServer || isEdgeRuntime) && - new MiddlewarePlugin({ dev, isEdgeRuntime }), - !isServer && + isEdgeServer && new MiddlewarePlugin({ dev }), + isClient && new BuildManifestPlugin({ buildId, rewrites, @@ -1530,8 +1530,7 @@ export default async function getBaseWebpackConfig( new ProfilingPlugin({ runWebpackSpan }), config.optimizeFonts && !dev && - isServer && - !isEdgeRuntime && + isNodeServer && (function () { const { FontStylesheetGatheringPlugin } = require('./webpack/plugins/font-stylesheet-gathering-plugin') as { @@ -1542,7 +1541,7 @@ export default async function getBaseWebpackConfig( }) })(), new WellKnownErrorsPlugin(), - !isServer && + isClient && new CopyFilePlugin({ filePath: require.resolve('./polyfills/polyfill-nomodule'), cacheKey: process.env.__NEXT_VERSION as string, @@ -1555,10 +1554,10 @@ export default async function getBaseWebpackConfig( }, }), hasServerComponents && - !isServer && + isClient && new FlightManifestPlugin({ dev, pageExtensions: rawPageExtensions }), !dev && - !isServer && + isClient && new TelemetryPlugin( new Map( [ @@ -1634,7 +1633,7 @@ export default async function getBaseWebpackConfig( }, } - if (targetWeb) { + if (isClient || isEdgeServer) { webpack5Config.output!.enabledLibraryTypes = ['assign'] } @@ -1694,12 +1693,12 @@ export default async function getBaseWebpackConfig( assetPrefix: config.assetPrefix, disableOptimizedLoading, target, - isEdgeRuntime, + isEdgeRuntime: isEdgeServer, reactProductionProfiling, webpack: !!config.webpack, hasRewrites, reactRoot: config.experimental.reactRoot, - runtime, + runtime: config.experimental.runtime, swcMinify: config.swcMinify, swcLoader: useSWCLoader, removeConsole: config.compiler?.removeConsole, @@ -1739,8 +1738,12 @@ export default async function getBaseWebpackConfig( const summaryServer = process.env.NEXT_WEBPACK_LOGGING.includes('summary-server') - const profile = (profileClient && !isServer) || (profileServer && isServer) - const summary = (summaryClient && !isServer) || (summaryServer && isServer) + const profile = + (profileClient && isClient) || + (profileServer && (isNodeServer || isEdgeServer)) + const summary = + (summaryClient && isClient) || + (summaryServer && (isNodeServer || isEdgeServer)) const logDefault = !infra && !profile && !summary @@ -1793,9 +1796,9 @@ export default async function getBaseWebpackConfig( rootDirectory: dir, customAppFile: new RegExp(escapeStringRegexp(path.join(pagesDir, `_app`))), isDevelopment: dev, - isServer, - isEdgeRuntime, - targetWeb, + isServer: isNodeServer || isEdgeServer, + isEdgeRuntime: isEdgeServer, + targetWeb: isClient || isEdgeServer, assetPrefix: config.assetPrefix || '', sassOptions: config.sassOptions, productionBrowserSourceMaps: config.productionBrowserSourceMaps, @@ -1814,15 +1817,15 @@ export default async function getBaseWebpackConfig( webpackConfig = config.webpack(webpackConfig, { dir, dev, - isServer, + isServer: isNodeServer || isEdgeServer, buildId, config, defaultLoaders, - totalPages, + totalPages: Object.keys(entrypoints).length, webpack, - ...(isServer + ...(isNodeServer || isEdgeServer ? { - nextRuntime: isEdgeRuntime ? 'edge' : 'nodejs', + nextRuntime: isEdgeServer ? 'edge' : 'nodejs', } : {}), }) @@ -1978,7 +1981,7 @@ export default async function getBaseWebpackConfig( if (hasUserCssConfig) { // only show warning for one build - if (isServer) { + if (isNodeServer || isEdgeServer) { console.warn( chalk.yellow.bold('Warning: ') + chalk.bold( @@ -2014,13 +2017,13 @@ export default async function getBaseWebpackConfig( } // Inject missing React Refresh loaders so that development mode is fast: - if (hasReactRefresh) { + if (dev && isClient) { attachReactRefresh(webpackConfig, defaultLoaders.babel) } // check if using @zeit/next-typescript and show warning if ( - isServer && + (isNodeServer || isEdgeServer) && webpackConfig.module && Array.isArray(webpackConfig.module.rules) ) { @@ -2164,15 +2167,12 @@ export default async function getBaseWebpackConfig( } delete entry['main.js'] - if (!isEdgeRuntime) { - for (const name of Object.keys(entry)) { - entry[name] = finalizeEntrypoint({ - value: entry[name], - isServer, - isMiddleware: MIDDLEWARE_ROUTE.test(name), - name, - }) - } + for (const name of Object.keys(entry)) { + entry[name] = finalizeEntrypoint({ + value: entry[name], + compilerType, + name, + }) } return entry diff --git a/packages/next/build/webpack/loaders/next-middleware-ssr-loader/index.ts b/packages/next/build/webpack/loaders/next-middleware-ssr-loader/index.ts index 1a5957629e9d..1297d01566b2 100644 --- a/packages/next/build/webpack/loaders/next-middleware-ssr-loader/index.ts +++ b/packages/next/build/webpack/loaders/next-middleware-ssr-loader/index.ts @@ -1,5 +1,19 @@ import { stringifyRequest } from '../../stringify-request' +export type MiddlewareSSRLoaderQuery = { + absolute500Path: string + absoluteAppPath: string + absoluteAppServerPath: string + absoluteDocumentPath: string + absoluteErrorPath: string + absolutePagePath: string + buildId: string + dev: boolean + isServerComponent: boolean + page: string + stringifiedConfig: string +} + export default async function middlewareSSRLoader(this: any) { const { dev, @@ -13,7 +27,7 @@ export default async function middlewareSSRLoader(this: any) { absoluteErrorPath, isServerComponent, stringifiedConfig, - } = this.getOptions() + }: MiddlewareSSRLoaderQuery = this.getOptions() const stringifiedPagePath = stringifyRequest(this, absolutePagePath) const stringifiedAppPath = stringifyRequest(this, absoluteAppPath) diff --git a/packages/next/build/webpack/loaders/next-middleware-wasm-loader.ts b/packages/next/build/webpack/loaders/next-middleware-wasm-loader.ts index 58cf76c9b5e4..8c84c07c0d1e 100644 --- a/packages/next/build/webpack/loaders/next-middleware-wasm-loader.ts +++ b/packages/next/build/webpack/loaders/next-middleware-wasm-loader.ts @@ -7,8 +7,8 @@ export type WasmBinding = { export default function MiddlewareWasmLoader(this: any, source: Buffer) { const name = `wasm_${sha1(source)}` - const filePath = `server/middleware-chunks/${name}.wasm` - const binding: WasmBinding = { filePath, name } + const filePath = `edge-chunks/${name}.wasm` + const binding: WasmBinding = { filePath: `server/${filePath}`, name } this._module.buildInfo.nextWasmMiddlewareBinding = binding this.emitFile(`/${filePath}`, source, null) return `module.exports = ${name};` diff --git a/packages/next/build/webpack/plugins/middleware-plugin.ts b/packages/next/build/webpack/plugins/middleware-plugin.ts index 02981076035c..32acdda5c82d 100644 --- a/packages/next/build/webpack/plugins/middleware-plugin.ts +++ b/packages/next/build/webpack/plugins/middleware-plugin.ts @@ -6,8 +6,7 @@ import { MIDDLEWARE_FLIGHT_MANIFEST, MIDDLEWARE_BUILD_MANIFEST, MIDDLEWARE_REACT_LOADABLE_MANIFEST, - MIDDLEWARE_RUNTIME_WEBPACK, - MIDDLEWARE_SSR_RUNTIME_WEBPACK, + EDGE_RUNTIME_WEBPACK, } from '../../../shared/lib/constants' import { nonNullable } from '../../../lib/non-nullable' import type { WasmBinding } from '../loaders/next-middleware-wasm-loader' @@ -51,15 +50,14 @@ function getPageFromEntrypointName(pagePath: string) { return page } -export type PerRoute = { +interface PerRoute { envPerRoute: Map wasmPerRoute: Map } -export function getEntrypointInfo( +function getEntrypointInfo( compilation: webpack5.Compilation, - { envPerRoute, wasmPerRoute }: PerRoute, - isEdgeRuntime: boolean + { envPerRoute, wasmPerRoute }: PerRoute ) { const entrypoints = compilation.entrypoints const infos = [] @@ -67,12 +65,7 @@ export function getEntrypointInfo( if (!entrypoint.name) continue const ssrEntryInfo = ssrEntries.get(entrypoint.name) - - if (ssrEntryInfo && !isEdgeRuntime) continue - if (!ssrEntryInfo && isEdgeRuntime) continue - const page = getPageFromEntrypointName(entrypoint.name) - if (!page) { continue } @@ -90,7 +83,9 @@ export function getEntrypointInfo( `server/${MIDDLEWARE_REACT_LOADABLE_MANIFEST}.js`, ...entryFiles.map((file) => 'server/' + file), ].filter(nonNullable) - : entryFiles.map((file: string) => file) + : entryFiles.map((file: string) => { + return 'server/' + file + }) infos.push({ env: envPerRoute.get(entrypoint.name) || [], @@ -106,30 +101,17 @@ export function getEntrypointInfo( export default class MiddlewarePlugin { dev: boolean - isEdgeRuntime: boolean - - constructor({ - dev, - isEdgeRuntime, - }: { - dev: boolean - isEdgeRuntime: boolean - }) { + + constructor({ dev }: { dev: boolean }) { this.dev = dev - this.isEdgeRuntime = isEdgeRuntime } createAssets( compilation: webpack5.Compilation, assets: any, - { envPerRoute, wasmPerRoute }: PerRoute, - isEdgeRuntime: boolean + { envPerRoute, wasmPerRoute }: PerRoute ) { - const infos = getEntrypointInfo( - compilation, - { envPerRoute, wasmPerRoute }, - isEdgeRuntime - ) + const infos = getEntrypointInfo(compilation, { envPerRoute, wasmPerRoute }) infos.forEach((info) => { middlewareManifest.middleware[info.page] = info }) @@ -140,52 +122,36 @@ export default class MiddlewarePlugin { middlewareManifest.clientInfo = middlewareManifest.sortedMiddleware.map( (key) => { const middleware = middlewareManifest.middleware[key] - const ssrEntryInfo = ssrEntries.get(middleware.name) - return [key, !!ssrEntryInfo] + return [key, !!ssrEntries.get(middleware.name)] } ) - assets[ - this.isEdgeRuntime ? MIDDLEWARE_MANIFEST : `server/${MIDDLEWARE_MANIFEST}` - ] = new sources.RawSource(JSON.stringify(middlewareManifest, null, 2)) + assets[MIDDLEWARE_MANIFEST] = new sources.RawSource( + JSON.stringify(middlewareManifest, null, 2) + ) } apply(compiler: webpack5.Compiler) { collectAssets(compiler, this.createAssets.bind(this), { dev: this.dev, pluginName: PLUGIN_NAME, - isEdgeRuntime: this.isEdgeRuntime, }) } } -export function collectAssets( +function collectAssets( compiler: webpack5.Compiler, createAssets: ( compilation: webpack5.Compilation, assets: any, - { envPerRoute, wasmPerRoute }: PerRoute, - isEdgeRuntime: boolean + { envPerRoute, wasmPerRoute }: PerRoute ) => void, - options: { - dev: boolean - pluginName: string - isEdgeRuntime: boolean - } + options: { dev: boolean; pluginName: string } ) { const wp = compiler.webpack compiler.hooks.compilation.tap( options.pluginName, (compilation, { normalModuleFactory }) => { - compilation.hooks.afterChunks.tap(options.pluginName, () => { - const middlewareRuntimeChunk = compilation.namedChunks.get( - MIDDLEWARE_RUNTIME_WEBPACK - ) - if (middlewareRuntimeChunk) { - middlewareRuntimeChunk.filenameTemplate = 'server/[name].js' - } - }) - const envPerRoute = new Map() const wasmPerRoute = new Map() @@ -194,10 +160,7 @@ export function collectAssets( envPerRoute.clear() for (const [name, info] of compilation.entries) { - if ( - info.options.runtime === MIDDLEWARE_SSR_RUNTIME_WEBPACK || - info.options.runtime === MIDDLEWARE_RUNTIME_WEBPACK - ) { + if (info.options.runtime === EDGE_RUNTIME_WEBPACK) { const middlewareEntries = new Set() const env = new Set() const wasm = new Set() @@ -236,6 +199,7 @@ export function collectAssets( ) ) continue + const error = new wp.WebpackError( `Dynamic Code Evaluation (e. g. 'eval', 'new Function') not allowed in Middleware ${name}${ typeof buildInfo.usingIndirectEval !== 'boolean' @@ -270,11 +234,14 @@ export function collectAssets( }) const handler = (parser: webpack5.javascript.JavascriptParser) => { - const isMiddlewareModule = () => - parser.state.module && parser.state.module.layer === 'middleware' + const isMiddlewareModule = () => { + return parser.state.module?.layer === 'middleware' + } const wrapExpression = (expr: any) => { - if (!isMiddlewareModule()) return + if (!isMiddlewareModule()) { + return + } if (options.dev) { const dep1 = new wp.dependencies.ConstDependency( @@ -393,12 +360,7 @@ export function collectAssets( stage: webpack.Compilation.PROCESS_ASSETS_STAGE_ADDITIONS, }, (assets: any) => { - createAssets( - compilation, - assets, - { envPerRoute, wasmPerRoute }, - options.isEdgeRuntime - ) + createAssets(compilation, assets, { envPerRoute, wasmPerRoute }) } ) } diff --git a/packages/next/build/webpack/plugins/middleware-source-maps-plugin.ts b/packages/next/build/webpack/plugins/middleware-source-maps-plugin.ts index 8ec8e21b004d..9ca1f771c77b 100644 --- a/packages/next/build/webpack/plugins/middleware-source-maps-plugin.ts +++ b/packages/next/build/webpack/plugins/middleware-source-maps-plugin.ts @@ -11,9 +11,9 @@ export const getMiddlewareSourceMapPlugins = () => { filename: '[file].map', include: [ // Middlewares are the only ones who have `server/pages/[name]` as their filename - /^server\/pages\//, + /^pages\//, // All middleware chunks - /^server\/middleware-chunks\//, + /^edge-chunks\//, ], }), new MiddlewareSourceMapsPlugin(), diff --git a/packages/next/build/webpack/plugins/terser-webpack-plugin/src/index.js b/packages/next/build/webpack/plugins/terser-webpack-plugin/src/index.js index 44cf399ebd98..ca6ff6b83b62 100644 --- a/packages/next/build/webpack/plugins/terser-webpack-plugin/src/index.js +++ b/packages/next/build/webpack/plugins/terser-webpack-plugin/src/index.js @@ -101,7 +101,7 @@ export class TerserPlugin { // and doesn't provide too much of a benefit as it's server-side if ( name.match( - /(middleware-runtime\.js|middleware-chunks|_middleware\.js$)/ + /(edge-runtime-webpack\.js|edge-chunks|_middleware\.js$)/ ) ) { return false diff --git a/packages/next/server/base-server.ts b/packages/next/server/base-server.ts index 1153c8639bf5..0bf6d06dfab6 100644 --- a/packages/next/server/base-server.ts +++ b/packages/next/server/base-server.ts @@ -356,11 +356,7 @@ export default abstract class Server { fs: this.getCacheFilesystem(), dev, distDir: this.distDir, - pagesDir: join( - this.distDir, - this._isLikeServerless ? SERVERLESS_DIRECTORY : SERVER_DIRECTORY, - 'pages' - ), + pagesDir: join(this.serverDistDir, 'pages'), locales: this.nextConfig.i18n?.locales, max: this.nextConfig.experimental.isrMemoryCacheSize, flushToDisk: !minimalMode && this.nextConfig.experimental.isrFlushToDisk, @@ -1967,6 +1963,13 @@ export default abstract class Server { protected get _isLikeServerless(): boolean { return isTargetLikeServerless(this.nextConfig.target) } + + protected get serverDistDir() { + return join( + this.distDir, + this._isLikeServerless ? SERVERLESS_DIRECTORY : SERVER_DIRECTORY + ) + } } export function prepareServerlessUrl( diff --git a/packages/next/server/dev/hot-reloader.ts b/packages/next/server/dev/hot-reloader.ts index 703aa5e9d43e..048505a564eb 100644 --- a/packages/next/server/dev/hot-reloader.ts +++ b/packages/next/server/dev/hot-reloader.ts @@ -9,6 +9,9 @@ import { createEntrypoints, createPagesMapping, finalizeEntrypoint, + getClientEntry, + getEdgeServerEntry, + runDependingOnPageType, } from '../../build/entries' import { watchCompilers } from '../../build/output' import getBaseWebpackConfig from '../../build/webpack-config' @@ -25,15 +28,8 @@ import onDemandEntryHandler, { import { denormalizePagePath, normalizePathSep } from '../normalize-page-path' import getRouteFromEntrypoint from '../get-route-from-entrypoint' import { fileExists } from '../../lib/file-exists' -import { ClientPagesLoaderOptions } from '../../build/webpack/loaders/next-client-pages-loader' import { ssrEntries } from '../../build/webpack/plugins/middleware-plugin' -import { stringify } from 'querystring' -import { - difference, - isCustomErrorPage, - isFlightPage, - isReservedPage, -} from '../../build/utils' +import { difference } from '../../build/utils' import { NextConfigComplete } from '../config-shared' import { CustomRoutes } from '../../lib/load-custom-routes' import { DecodeError } from '../../shared/lib/utils' @@ -160,6 +156,7 @@ export default class HotReloader { private hasReactRoot: boolean public clientStats: webpack5.Stats | null public serverStats: webpack5.Stats | null + public edgeServerStats: webpack5.Stats | null private clientError: Error | null = null private serverError: Error | null = null private serverPrevDocumentHash: string | null @@ -197,6 +194,7 @@ export default class HotReloader { this.distDir = distDir this.clientStats = null this.serverStats = null + this.edgeServerStats = null this.serverPrevDocumentHash = null this.config = config @@ -405,62 +403,48 @@ export default class HotReloader { const entrypoints = await webpackConfigSpan .traceChild('create-entrypoints') .traceAsyncFn(() => - createEntrypoints( - this.pagesMapping, - 'server', - this.buildId, - this.previewProps, - this.config, - [], - this.pagesDir, - true - ) + createEntrypoints({ + buildId: this.buildId, + config: this.config, + envFiles: [], + isDev: true, + pages: this.pagesMapping, + pagesDir: this.pagesDir, + previewMode: this.previewProps, + target: 'server', + }) ) + const commonWebpackOptions = { + dev: true, + buildId: this.buildId, + config: this.config, + hasReactRoot: this.hasReactRoot, + pagesDir: this.pagesDir, + rewrites: this.rewrites, + runWebpackSpan: this.hotReloaderSpan, + } + return webpackConfigSpan .traceChild('generate-webpack-config') .traceAsyncFn(() => - Promise.all( - [ - getBaseWebpackConfig(this.dir, { - dev: true, - isServer: false, - config: this.config, - buildId: this.buildId, - pagesDir: this.pagesDir, - rewrites: this.rewrites, - entrypoints: entrypoints.client, - runWebpackSpan: this.hotReloaderSpan, - hasReactRoot: this.hasReactRoot, - }), - getBaseWebpackConfig(this.dir, { - dev: true, - isServer: true, - config: this.config, - buildId: this.buildId, - pagesDir: this.pagesDir, - rewrites: this.rewrites, - entrypoints: entrypoints.server, - runWebpackSpan: this.hotReloaderSpan, - hasReactRoot: this.hasReactRoot, - }), - // The edge runtime is only supported with React root. - this.hasReactRoot - ? getBaseWebpackConfig(this.dir, { - dev: true, - isServer: true, - isEdgeRuntime: true, - config: this.config, - buildId: this.buildId, - pagesDir: this.pagesDir, - rewrites: this.rewrites, - entrypoints: entrypoints.edgeServer, - runWebpackSpan: this.hotReloaderSpan, - hasReactRoot: this.hasReactRoot, - }) - : null, - ].filter(Boolean) as webpack.Configuration[] - ) + Promise.all([ + getBaseWebpackConfig(this.dir, { + ...commonWebpackOptions, + compilerType: 'client', + entrypoints: entrypoints.client, + }), + getBaseWebpackConfig(this.dir, { + ...commonWebpackOptions, + compilerType: 'server', + entrypoints: entrypoints.server, + }), + getBaseWebpackConfig(this.dir, { + ...commonWebpackOptions, + compilerType: 'edge-server', + entrypoints: entrypoints.edgeServer, + }), + ]) ) }) } @@ -471,7 +455,7 @@ export default class HotReloader { const fallbackConfig = await getBaseWebpackConfig(this.dir, { runWebpackSpan: this.hotReloaderSpan, dev: true, - isServer: false, + compilerType: 'client', config: this.config, buildId: this.buildId, pagesDir: this.pagesDir, @@ -482,19 +466,19 @@ export default class HotReloader { }, isDevFallback: true, entrypoints: ( - await createEntrypoints( - { + await createEntrypoints({ + buildId: this.buildId, + config: this.config, + envFiles: [], + isDev: true, + pages: { '/_app': 'next/dist/pages/_app', '/_error': 'next/dist/pages/_error', }, - 'server', - this.buildId, - this.previewProps, - this.config, - [], - this.pagesDir, - true - ) + pagesDir: this.pagesDir, + previewMode: this.previewProps, + target: 'server', + }) ).client, hasReactRoot: this.hasReactRoot, }) @@ -544,125 +528,71 @@ export default class HotReloader { await Promise.all( Object.keys(entries).map(async (pageKey) => { - const isClientKey = pageKey.startsWith('client') - const isEdgeServerKey = pageKey.startsWith('edge-server') - - if (isClientKey !== isClientCompilation) return - if (isEdgeServerKey !== isEdgeServerCompilation) return - - const page = pageKey.slice( - isClientKey - ? 'client'.length - : isEdgeServerKey - ? 'edge-server'.length - : 'server'.length - ) - const isMiddleware = !!page.match(MIDDLEWARE_ROUTE) - - if (isClientCompilation && page.match(API_ROUTE) && !isMiddleware) { - return - } - - if (!isClientCompilation && isMiddleware) { - return - } - const { bundlePath, absolutePagePath, dispose } = entries[pageKey] + const result = /^(client|server|edge-server)(.*)/g.exec(pageKey) + const [, key, page] = result! // this match should always happen + if (key === 'client' && !isClientCompilation) return + if (key === 'server' && !isNodeServerCompilation) return + if (key === 'edge-server' && !isEdgeServerCompilation) return + + // Check if the page was removed or disposed and remove it const pageExists = !dispose && (await fileExists(absolutePagePath)) if (!pageExists) { - // page was removed or disposed delete entries[pageKey] return } - const isApiRoute = page.match(API_ROUTE) - const isCustomError = isCustomErrorPage(page) - const isReserved = isReservedPage(page) - const isServerComponent = - this.hasServerComponents && - isFlightPage(this.config, absolutePagePath) - - const pageRuntimeConfig = await getPageRuntime( - absolutePagePath, - this.config - ) - const isEdgeSSRPage = pageRuntimeConfig === 'edge' && !isApiRoute - - if (isNodeServerCompilation && isEdgeSSRPage && !isCustomError) { - return - } - if (isEdgeServerCompilation && !isEdgeSSRPage) { - return - } - - entries[pageKey].status = BUILDING - const pageLoaderOpts: ClientPagesLoaderOptions = { + runDependingOnPageType({ page, - absolutePagePath, - } - - if (isClientCompilation) { - if (isMiddleware) { - // Middleware - entrypoints[bundlePath] = finalizeEntrypoint({ - name: bundlePath, - value: `next-middleware-loader?${stringify(pageLoaderOpts)}!`, - isServer: false, - isMiddleware: true, - }) - } else { - // A page route - entrypoints[bundlePath] = finalizeEntrypoint({ - name: bundlePath, - value: `next-client-pages-loader?${stringify( - pageLoaderOpts - )}!`, - isServer: false, - }) - - // Tell the middleware plugin of the client compilation - // that this route is a page. - if (isEdgeSSRPage) { - if (isServerComponent) { - ssrEntries.set(bundlePath, { requireFlightManifest: true }) - } else if (!isCustomError && !isReserved) { - ssrEntries.set(bundlePath, { requireFlightManifest: false }) - } + pageRuntime: await getPageRuntime(absolutePagePath, this.config), + onEdgeServer: () => { + if (isEdgeServerCompilation) { + entries[pageKey].status = BUILDING + entrypoints[bundlePath] = finalizeEntrypoint({ + compilerType: 'edge-server', + name: bundlePath, + value: getEdgeServerEntry({ + absolutePagePath, + buildId: this.buildId, + bundlePath, + config: this.config, + isDev: true, + page, + pages: this.pagesMapping, + ssrEntries, + }), + }) } - } - } else if (isEdgeServerCompilation) { - if (!isReserved) { - entrypoints[bundlePath] = finalizeEntrypoint({ - name: '[name].js', - value: `next-middleware-ssr-loader?${stringify({ - dev: true, - page, - stringifiedConfig: JSON.stringify(this.config), - absoluteAppPath: this.pagesMapping['/_app'], - absoluteAppServerPath: this.pagesMapping['/_app.server'], - absoluteDocumentPath: this.pagesMapping['/_document'], - absoluteErrorPath: this.pagesMapping['/_error'], - absolute404Path: this.pagesMapping['/404'] || '', - absolutePagePath, - isServerComponent, - buildId: this.buildId, - } as any)}!`, - isServer: false, - isEdgeServer: true, - }) - } - } else if (isNodeServerCompilation) { - let request = relative(config.context!, absolutePagePath) - if (!isAbsolute(request) && !request.startsWith('../')) { - request = `./${request}` - } + }, + onClient: () => { + if (isClientCompilation) { + entries[pageKey].status = BUILDING + entrypoints[bundlePath] = finalizeEntrypoint({ + name: bundlePath, + compilerType: 'client', + value: getClientEntry({ + absolutePagePath, + page, + }), + }) + } + }, + onServer: () => { + if (isNodeServerCompilation) { + entries[pageKey].status = BUILDING + let request = relative(config.context!, absolutePagePath) + if (!isAbsolute(request) && !request.startsWith('../')) { + request = `./${request}` + } - entrypoints[bundlePath] = finalizeEntrypoint({ - name: bundlePath, - value: request, - isServer: true, - }) - } + entrypoints[bundlePath] = finalizeEntrypoint({ + compilerType: 'server', + name: bundlePath, + value: request, + }) + } + }, + }) }) ) @@ -679,15 +609,17 @@ export default class HotReloader { watchCompilers( multiCompiler.compilers[0], multiCompiler.compilers[1], - multiCompiler.compilers[2] || null + multiCompiler.compilers[2] ) // Watch for changes to client/server page files so we can tell when just // the server file changes and trigger a reload for GS(S)P pages const changedClientPages = new Set() const changedServerPages = new Set() + const changedEdgeServerPages = new Set() const prevClientPageHashes = new Map() const prevServerPageHashes = new Map() + const prevEdgeServerPageHashes = new Map() const trackPageChanges = (pageHashMap: Map, changedItems: Set) => @@ -752,6 +684,10 @@ export default class HotReloader { 'NextjsHotReloaderForServer', trackPageChanges(prevServerPageHashes, changedServerPages) ) + multiCompiler.compilers[2].hooks.emit.tap( + 'NextjsHotReloaderForServer', + trackPageChanges(prevEdgeServerPageHashes, changedEdgeServerPages) + ) // This plugin watches for changes to _document.js and notifies the client side that it should reload the page multiCompiler.compilers[1].hooks.failed.tap( @@ -761,6 +697,15 @@ export default class HotReloader { this.serverStats = null } ) + + multiCompiler.compilers[2].hooks.done.tap( + 'NextjsHotReloaderForServer', + (stats) => { + this.serverError = null + this.edgeServerStats = stats + } + ) + multiCompiler.compilers[1].hooks.done.tap( 'NextjsHotReloaderForServer', (stats) => { @@ -799,11 +744,12 @@ export default class HotReloader { changedServerPages, changedClientPages ) - const middlewareChanges = Array.from(changedClientPages).filter((name) => - name.match(MIDDLEWARE_ROUTE) + const middlewareChanges = Array.from(changedEdgeServerPages).filter( + (name) => name.match(MIDDLEWARE_ROUTE) ) changedClientPages.clear() changedServerPages.clear() + changedEdgeServerPages.clear() if (middlewareChanges.length > 0) { this.send({ @@ -918,41 +864,26 @@ export default class HotReloader { } public async getCompilationErrors(page: string) { - const normalizedPage = normalizePathSep(page) + const getErrors = ({ compilation }: webpack5.Stats) => { + const failedPages = erroredPages(compilation) + const normalizedPage = normalizePathSep(page) + // If there is an error related to the requesting page we display it instead of the first error + return failedPages[normalizedPage]?.length > 0 + ? failedPages[normalizedPage] + : compilation.errors + } if (this.clientError || this.serverError) { return [this.clientError || this.serverError] } else if (this.clientStats?.hasErrors()) { - const { compilation } = this.clientStats - const failedPages = erroredPages(compilation) - - // If there is an error related to the requesting page we display it instead of the first error - if ( - failedPages[normalizedPage] && - failedPages[normalizedPage].length > 0 - ) { - return failedPages[normalizedPage] - } - - // If none were found we still have to show the other errors - return this.clientStats.compilation.errors + return getErrors(this.clientStats) } else if (this.serverStats?.hasErrors()) { - const { compilation } = this.serverStats - const failedPages = erroredPages(compilation) - - // If there is an error related to the requesting page we display it instead of the first error - if ( - failedPages[normalizedPage] && - failedPages[normalizedPage].length > 0 - ) { - return failedPages[normalizedPage] - } - - // If none were found we still have to show the other errors - return this.serverStats.compilation.errors + return getErrors(this.serverStats) + } else if (this.edgeServerStats?.hasErrors()) { + return getErrors(this.edgeServerStats) + } else { + return [] } - - return [] } public send(action?: string | any, ...args: any[]): void { diff --git a/packages/next/server/dev/next-dev-server.ts b/packages/next/server/dev/next-dev-server.ts index 5b323e44f541..9a76c9eb6180 100644 --- a/packages/next/server/dev/next-dev-server.ts +++ b/packages/next/server/dev/next-dev-server.ts @@ -541,16 +541,20 @@ export default class DevServer extends Server { const result = await super.runMiddleware({ ...params, onWarning: (warn) => { - this.logErrorWithOriginalStack(warn, 'warning', 'client') + this.logErrorWithOriginalStack(warn, 'warning', 'edge-server') }, }) result?.waitUntil.catch((error) => - this.logErrorWithOriginalStack(error, 'unhandledRejection', 'client') + this.logErrorWithOriginalStack( + error, + 'unhandledRejection', + 'edge-server' + ) ) return result } catch (error) { - this.logErrorWithOriginalStack(error, undefined, 'client') + this.logErrorWithOriginalStack(error, undefined, 'edge-server') const preflight = params.request.method === 'HEAD' && @@ -627,7 +631,7 @@ export default class DevServer extends Server { private async logErrorWithOriginalStack( err?: unknown, type?: 'unhandledRejection' | 'uncaughtException' | 'warning', - stats: 'server' | 'client' = 'server' + stats: 'server' | 'edge-server' = 'server' ) { let usedOriginalStack = false @@ -638,9 +642,9 @@ export default class DevServer extends Server { if (frame.lineNumber && frame?.file) { const compilation = - stats === 'client' - ? this.hotReloader?.clientStats?.compilation - : this.hotReloader?.serverStats?.compilation + stats === 'server' + ? this.hotReloader?.serverStats?.compilation + : this.hotReloader?.edgeServerStats?.compilation const moduleId = frame.file!.replace( /^(webpack-internal:\/\/\/|file:\/\/)/, diff --git a/packages/next/server/dev/on-demand-entry-handler.ts b/packages/next/server/dev/on-demand-entry-handler.ts index 6df016d7e59e..3f39e8f7ba85 100644 --- a/packages/next/server/dev/on-demand-entry-handler.ts +++ b/packages/next/server/dev/on-demand-entry-handler.ts @@ -1,16 +1,14 @@ +import type ws from 'ws' +import type { webpack5 as webpack } from 'next/dist/compiled/webpack/webpack' +import type { NextConfigComplete } from '../config-shared' import { EventEmitter } from 'events' +import { findPageFile } from '../lib/find-page-file' +import { getPageRuntime, runDependingOnPageType } from '../../build/entries' import { join, posix } from 'path' -import type { webpack5 as webpack } from 'next/dist/compiled/webpack/webpack' import { normalizePagePath, normalizePathSep } from '../normalize-page-path' import { pageNotFoundError } from '../require' -import { findPageFile } from '../lib/find-page-file' -import getRouteFromEntrypoint from '../get-route-from-entrypoint' -import { API_ROUTE, MIDDLEWARE_ROUTE } from '../../lib/constants' import { reportTrigger } from '../../build/output' -import type ws from 'ws' -import { NextConfigComplete } from '../config-shared' -import { isCustomErrorPage } from '../../build/utils' -import { getPageRuntime } from '../../build/entries' +import getRouteFromEntrypoint from '../get-route-from-entrypoint' export const ADDED = Symbol('added') export const BUILDING = Symbol('building') @@ -155,65 +153,15 @@ export default function onDemandEntryHandler( } return { - async ensurePage(page: string, clientOnly: boolean) { - let normalizedPagePath: string - try { - normalizedPagePath = normalizePagePath(page) - } catch (err) { - console.error(err) - throw pageNotFoundError(page) - } - - let pagePath = await findPageFile( + async ensurePage(_page: string, clientOnly: boolean) { + const { absolutePagePath, bundlePath, page } = await getPageInfo({ + pageExtensions: nextConfig.pageExtensions, + page: _page, pagesDir, - normalizedPagePath, - nextConfig.pageExtensions - ) - - // Default the /_error route to the Next.js provided default page - if (page === '/_error' && pagePath === null) { - pagePath = 'next/dist/pages/_error' - } - - if (pagePath === null) { - throw pageNotFoundError(normalizedPagePath) - } - - let bundlePath: string - let absolutePagePath: string - if (pagePath.startsWith('next/dist/pages/')) { - bundlePath = page - absolutePagePath = require.resolve(pagePath) - } else { - let pageUrl = pagePath.replace(/\\/g, '/') - - pageUrl = `${pageUrl[0] !== '/' ? '/' : ''}${pageUrl - .replace( - new RegExp(`\\.+(?:${nextConfig.pageExtensions.join('|')})$`), - '' - ) - .replace(/\/index$/, '')}` - - pageUrl = pageUrl === '' ? '/' : pageUrl - const bundleFile = normalizePagePath(pageUrl) - bundlePath = posix.join('pages', bundleFile) - absolutePagePath = join(pagesDir, pagePath) - page = posix.normalize(pageUrl) - } - - const normalizedPage = normalizePathSep(page) - - const isMiddleware = normalizedPage.match(MIDDLEWARE_ROUTE) - const isApiRoute = normalizedPage.match(API_ROUTE) && !isMiddleware - const pageRuntimeConfig = await getPageRuntime( - absolutePagePath, - nextConfig - ) - const isEdgeServer = pageRuntimeConfig === 'edge' - - const isCustomError = isCustomErrorPage(page) + }) let entriesChanged = false + const addPageEntry = (type: 'client' | 'server' | 'edge-server') => { return new Promise((resolve, reject) => { // Makes sure the page that is being kept in on-demand-entries matches the webpack output @@ -250,29 +198,24 @@ export default function onDemandEntryHandler( }) } - const isClientOrMiddleware = clientOnly || isMiddleware - - const promise = isApiRoute - ? addPageEntry('server') - : isClientOrMiddleware - ? addPageEntry('client') - : Promise.all([ - addPageEntry('client'), - addPageEntry( - isEdgeServer && !isCustomError ? 'edge-server' : 'server' - ), - ]) + const promises = runDependingOnPageType({ + page, + pageRuntime: await getPageRuntime(absolutePagePath, nextConfig), + onClient: () => addPageEntry('client'), + onServer: () => addPageEntry('server'), + onEdgeServer: () => addPageEntry('edge-server'), + }) if (entriesChanged) { reportTrigger( - isApiRoute || isMiddleware || clientOnly - ? normalizedPage - : `${normalizedPage} (client and server)` + !clientOnly && promises.length > 1 + ? `${page} (client and server)` + : page ) invalidator.invalidate() } - return promise + return Promise.all(promises) }, onHMR(client: ws) { @@ -365,3 +308,55 @@ class Invalidator { } } } + +async function getPageInfo(opts: { + page: string + pageExtensions: string[] + pagesDir: string +}) { + const { page, pagesDir, pageExtensions } = opts + + let normalizedPagePath: string + + try { + normalizedPagePath = normalizePagePath(page) + } catch (err) { + console.error(err) + throw pageNotFoundError(page) + } + + let pagePath = await findPageFile( + pagesDir, + normalizedPagePath, + pageExtensions + ) + + if (pagePath === null) { + // Default the /_error route to the Next.js provided default page + if (page === '/_error') { + pagePath = 'next/dist/pages/_error' + } else { + throw pageNotFoundError(normalizedPagePath) + } + } + + if (pagePath.startsWith('next/dist/pages/')) { + return { + page: normalizePathSep(page), + bundlePath: page, + absolutePagePath: require.resolve(pagePath), + } + } + + let pageUrl = pagePath.replace(/\\/g, '/') + pageUrl = `${pageUrl[0] !== '/' ? '/' : ''}${pageUrl + .replace(new RegExp(`\\.+(?:${pageExtensions.join('|')})$`), '') + .replace(/\/index$/, '')}` + pageUrl = pageUrl === '' ? '/' : pageUrl + + return { + bundlePath: posix.join('pages', normalizePagePath(pageUrl)), + absolutePagePath: join(pagesDir, pagePath), + page: normalizePathSep(posix.normalize(pageUrl)), + } +} diff --git a/packages/next/server/next-server.ts b/packages/next/server/next-server.ts index 24825cd127f3..0d8b3ad7a65a 100644 --- a/packages/next/server/next-server.ts +++ b/packages/next/server/next-server.ts @@ -24,7 +24,6 @@ import { addRequestMeta, getRequestMeta } from './request-meta' import { PAGES_MANIFEST, BUILD_ID_FILE, - SERVER_DIRECTORY, MIDDLEWARE_MANIFEST, CLIENT_STATIC_FILES_PATH, CLIENT_STATIC_FILES_RUNTIME, @@ -32,7 +31,6 @@ import { ROUTES_MANIFEST, MIDDLEWARE_FLIGHT_MANIFEST, CLIENT_PUBLIC_FILES_PATH, - SERVERLESS_DIRECTORY, } from '../shared/lib/constants' import { recursiveReadDirSync } from './lib/recursive-readdir-sync' import { format as formatUrl, UrlWithParsedQuery } from 'url' @@ -156,12 +154,7 @@ export default class NextNodeServer extends BaseServer { } protected getPagesManifest(): PagesManifest | undefined { - const serverBuildDir = join( - this.distDir, - this._isLikeServerless ? SERVERLESS_DIRECTORY : SERVER_DIRECTORY - ) - const pagesManifestPath = join(serverBuildDir, PAGES_MANIFEST) - return require(pagesManifestPath) + return require(join(this.serverDistDir, PAGES_MANIFEST)) } protected getBuildId(): string { @@ -939,14 +932,9 @@ export default class NextNodeServer extends BaseServer { } protected getMiddlewareManifest(): MiddlewareManifest | undefined { - if (!this.minimalMode) { - const middlewareManifestPath = join( - join(this.distDir, SERVER_DIRECTORY), - MIDDLEWARE_MANIFEST - ) - return require(middlewareManifestPath) - } - return undefined + return !this.minimalMode + ? require(join(this.serverDistDir, MIDDLEWARE_MANIFEST)) + : undefined } protected generateRewrites({ diff --git a/packages/next/shared/lib/constants.ts b/packages/next/shared/lib/constants.ts index 790eaedca07c..bedca6446bb5 100644 --- a/packages/next/shared/lib/constants.ts +++ b/packages/next/shared/lib/constants.ts @@ -44,9 +44,7 @@ export const CLIENT_STATIC_FILES_RUNTIME_AMP = `amp` export const CLIENT_STATIC_FILES_RUNTIME_WEBPACK = `webpack` // static/runtime/polyfills.js export const CLIENT_STATIC_FILES_RUNTIME_POLYFILLS_SYMBOL = Symbol(`polyfills`) -// server/middleware-flight-runtime.js -export const MIDDLEWARE_SSR_RUNTIME_WEBPACK = 'middleware-ssr-runtime' -export const MIDDLEWARE_RUNTIME_WEBPACK = 'middleware-runtime' +export const EDGE_RUNTIME_WEBPACK = 'edge-runtime-webpack' export const TEMPORARY_REDIRECT_STATUS = 307 export const PERMANENT_REDIRECT_STATUS = 308 export const STATIC_PROPS_ID = '__N_SSG' diff --git a/test/e2e/middleware-can-use-wasm-files/index.test.ts b/test/e2e/middleware-can-use-wasm-files/index.test.ts index a1459d89ece6..808bff344490 100644 --- a/test/e2e/middleware-can-use-wasm-files/index.test.ts +++ b/test/e2e/middleware-can-use-wasm-files/index.test.ts @@ -65,7 +65,7 @@ describe('middleware can use wasm files', () => { wasm: [ { filePath: - 'server/middleware-chunks/wasm_58ccff8b2b94b5dac6ef8957082ecd8f6d34186d.wasm', + 'server/edge-chunks/wasm_58ccff8b2b94b5dac6ef8957082ecd8f6d34186d.wasm', name: 'wasm_58ccff8b2b94b5dac6ef8957082ecd8f6d34186d', }, ], diff --git a/test/integration/middleware/core/pages/global/_middleware.js b/test/integration/middleware/core/pages/global/_middleware.js index 7e3020009875..964148bc1ef0 100644 --- a/test/integration/middleware/core/pages/global/_middleware.js +++ b/test/integration/middleware/core/pages/global/_middleware.js @@ -6,7 +6,6 @@ export async function middleware(request, ev) { return NextResponse.json({ process: { env: process.env, - nextTick: typeof process.nextTick, }, }) } diff --git a/test/integration/middleware/core/test/index.test.js b/test/integration/middleware/core/test/index.test.js index a5e44c99ce90..5e0ba76d9c90 100644 --- a/test/integration/middleware/core/test/index.test.js +++ b/test/integration/middleware/core/test/index.test.js @@ -104,7 +104,7 @@ describe('Middleware base tests', () => { for (const key of Object.keys(manifest.middleware)) { const middleware = manifest.middleware[key] expect(middleware.files).toContainEqual( - expect.stringContaining('middleware-runtime') + expect.stringContaining('server/edge-runtime-webpack') ) expect(middleware.files).not.toContainEqual( expect.stringContaining('static/chunks/') @@ -133,9 +133,6 @@ describe('Middleware base tests', () => { MIDDLEWARE_TEST: 'asdf', NEXT_RUNTIME: 'edge', }, - // it's poflyfilled since there is the "process" module - // as a devDepencies of the next package - nextTick: 'function', }, }) }) diff --git a/test/integration/react-streaming-and-server-components/test/index.test.js b/test/integration/react-streaming-and-server-components/test/index.test.js index 4e526e8ede82..0d361be78ee2 100644 --- a/test/integration/react-streaming-and-server-components/test/index.test.js +++ b/test/integration/react-streaming-and-server-components/test/index.test.js @@ -88,8 +88,8 @@ const edgeRuntimeBasicSuite = { it('should generate middleware SSR manifests for edge runtime', async () => { const distServerDir = join(distDir, 'server') const files = [ + 'edge-runtime-webpack.js', 'middleware-build-manifest.js', - 'middleware-ssr-runtime.js', 'middleware-flight-manifest.js', 'middleware-flight-manifest.json', 'middleware-manifest.json', diff --git a/test/production/required-server-files.test.ts b/test/production/required-server-files.test.ts index 405e87c26cc8..dd557db3a8e9 100644 --- a/test/production/required-server-files.test.ts +++ b/test/production/required-server-files.test.ts @@ -150,7 +150,7 @@ describe('should set-up next', () => { it('should output middleware correctly', async () => { expect( await fs.pathExists( - join(next.testDir, 'standalone/.next/server/middleware-runtime.js') + join(next.testDir, 'standalone/.next/server/edge-runtime-webpack.js') ) ).toBe(true) expect( From 50014171710bac8346f9d19efe2307e3bb32e3c5 Mon Sep 17 00:00:00 2001 From: Javi Velasco Date: Wed, 27 Apr 2022 09:53:59 +0200 Subject: [PATCH 7/7] Update some comments Co-authored-by: JJ Kasper Update packages/next/shared/lib/router/utils/path-match.ts Co-authored-by: JJ Kasper Update packages/next/shared/lib/router/utils/path-match.ts Co-authored-by: JJ Kasper --- packages/next/shared/lib/router/utils/path-match.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/next/shared/lib/router/utils/path-match.ts b/packages/next/shared/lib/router/utils/path-match.ts index f6fed0c8c6b9..613d29023f9b 100644 --- a/packages/next/shared/lib/router/utils/path-match.ts +++ b/packages/next/shared/lib/router/utils/path-match.ts @@ -9,7 +9,7 @@ interface Options { */ regexModifier?: (regex: string) => string /** - * When passed to true the function will remove all unnamed parameters + * When true the function will remove all unnamed parameters * from the matched parameters. */ removeUnnamedParams?: boolean @@ -22,7 +22,7 @@ interface Options { /** * Generates a path matcher function for a given path and options based on - * path-to-regexp. By default the match will case insesitive, non strict + * path-to-regexp. By default the match will be case insesitive, non strict * and delimited by `/`. */ export function getPathMatch(path: string, options?: Options) { @@ -56,7 +56,7 @@ export function getPathMatch(path: string, options?: Options) { } /** - * If unnamed params are not allowed to allowed they must be removed from + * If unnamed params are not allowed they must be removed from * the matched parameters. path-to-regexp uses "string" for named and * "number" for unnamed parameters. */