diff --git a/packages/next/build/entries.ts b/packages/next/build/entries.ts index 10cc81da0b0d8..7181d6f8c9718 100644 --- a/packages/next/build/entries.ts +++ b/packages/next/build/entries.ts @@ -15,12 +15,11 @@ 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 { parse } from '../build/swc' import { isFlightPage, withoutRSCExtensions } from './utils' +import { normalizePathSep } from '../shared/lib/page-path/normalize-path-sep' +import { normalizePagePath } from '../shared/lib/page-path/normalize-page-path' type ObjectValue = T extends { [key: string]: infer V } ? V : never @@ -233,7 +232,6 @@ export function getEdgeServerEntry(opts: { isDev: boolean page: string pages: { [page: string]: string } - ssrEntries: Map }): ObjectValue { if (opts.page.match(MIDDLEWARE_ROUTE)) { const loaderParams: MiddlewareLoaderOptions = { @@ -258,10 +256,6 @@ export function getEdgeServerEntry(opts: { stringifiedConfig: JSON.stringify(opts.config), } - ssrEntries.set(opts.bundlePath, { - requireFlightManifest: isFlightPage(opts.config, opts.absolutePagePath), - }) - return `next-middleware-ssr-loader?${stringify(loaderParams)}!` } @@ -375,7 +369,6 @@ export async function createEntrypoints(params: CreateEntrypointsParams) { bundlePath: clientBundlePath, isDev: false, page, - ssrEntries, }) }, }) diff --git a/packages/next/build/index.ts b/packages/next/build/index.ts index af721400e21f6..e096a315d1906 100644 --- a/packages/next/build/index.ts +++ b/packages/next/build/index.ts @@ -63,7 +63,7 @@ import { __ApiPreviewProps } from '../server/api-utils' import loadConfig from '../server/config' import { isTargetLikeServerless } from '../server/utils' import { BuildManifest } from '../server/get-page-files' -import { normalizePagePath } from '../server/normalize-page-path' +import { normalizePagePath } from '../shared/lib/page-path/normalize-page-path' import { getPagePath } from '../server/require' import * as ciEnvironment from '../telemetry/ci-info' import { diff --git a/packages/next/build/utils.ts b/packages/next/build/utils.ts index 96deac92b3fb7..74e29050550be 100644 --- a/packages/next/build/utils.ts +++ b/packages/next/build/utils.ts @@ -26,10 +26,6 @@ import { isDynamicRoute } from '../shared/lib/router/utils/is-dynamic' import escapePathDelimiters from '../shared/lib/router/utils/escape-path-delimiters' import { findPageFile } from '../server/lib/find-page-file' import { GetStaticPaths, PageConfig } from 'next/types' -import { - denormalizePagePath, - normalizePagePath, -} from '../server/normalize-page-path' import { BuildManifest } from '../server/get-page-files' import { removePathTrailingSlash } from '../client/normalize-trailing-slash' import { UnwrapPromise } from '../lib/coalesced-function' @@ -42,6 +38,8 @@ import isError from '../lib/is-error' import { recursiveDelete } from '../lib/recursive-delete' import { Sema } from 'next/dist/compiled/async-sema' import { MiddlewareManifest } from './webpack/plugins/middleware-plugin' +import { denormalizePagePath } from '../shared/lib/page-path/denormalize-page-path' +import { normalizePagePath } from '../shared/lib/page-path/normalize-page-path' const { builtinModules } = require('module') const RESERVED_PAGE = /^\/(_app|_error|_document|api(\/|$))/ diff --git a/packages/next/build/webpack/loaders/get-module-build-info.ts b/packages/next/build/webpack/loaders/get-module-build-info.ts new file mode 100644 index 0000000000000..6f0be47f61dd1 --- /dev/null +++ b/packages/next/build/webpack/loaders/get-module-build-info.ts @@ -0,0 +1,29 @@ +import { webpack5 } from 'next/dist/compiled/webpack/webpack' + +/** + * A getter for module build info that casts to the type it should have. + * We also expose here types to make easier to use it. + */ +export function getModuleBuildInfo(webpackModule: webpack5.Module) { + return webpackModule.buildInfo as { + nextEdgeMiddleware?: EdgeMiddlewareMeta + nextEdgeSSR?: EdgeSSRMeta + nextUsedEnvVars?: Set + nextWasmMiddlewareBinding?: WasmBinding + usingIndirectEval?: boolean | Set + } +} + +export interface EdgeMiddlewareMeta { + page: string +} + +export interface EdgeSSRMeta { + isServerComponent: boolean + page: string +} + +export interface WasmBinding { + filePath: string + name: string +} diff --git a/packages/next/build/webpack/loaders/next-middleware-loader.ts b/packages/next/build/webpack/loaders/next-middleware-loader.ts index 3beab89b7f4a0..cccfbd4cc0b2b 100644 --- a/packages/next/build/webpack/loaders/next-middleware-loader.ts +++ b/packages/next/build/webpack/loaders/next-middleware-loader.ts @@ -1,3 +1,4 @@ +import { getModuleBuildInfo } from './get-module-build-info' import { stringifyRequest } from '../stringify-request' export type MiddlewareLoaderOptions = { @@ -8,6 +9,10 @@ export type MiddlewareLoaderOptions = { export default function middlewareLoader(this: any) { const { absolutePagePath, page }: MiddlewareLoaderOptions = this.getOptions() const stringifiedPagePath = stringifyRequest(this, absolutePagePath) + const buildInfo = getModuleBuildInfo(this._module) + buildInfo.nextEdgeMiddleware = { + page: page.replace(/\/_middleware$/, '') || '/', + } return ` import { adapter } from 'next/dist/server/web/adapter' 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 6f88f41b71022..ff6f665b50fc2 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,3 +1,4 @@ +import { getModuleBuildInfo } from '../get-module-build-info' import { stringifyRequest } from '../../stringify-request' export type MiddlewareSSRLoaderQuery = { @@ -27,7 +28,13 @@ export default async function middlewareSSRLoader(this: any) { absoluteErrorPath, isServerComponent, stringifiedConfig, - }: MiddlewareSSRLoaderQuery = this.getOptions() + } = this.getOptions() + + const buildInfo = getModuleBuildInfo(this._module) + buildInfo.nextEdgeSSR = { + isServerComponent: isServerComponent === 'true', + page: page, + } 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 8c84c07c0d1e0..8ed7dd7212dc7 100644 --- a/packages/next/build/webpack/loaders/next-middleware-wasm-loader.ts +++ b/packages/next/build/webpack/loaders/next-middleware-wasm-loader.ts @@ -1,15 +1,11 @@ +import { getModuleBuildInfo } from './get-module-build-info' import crypto from 'crypto' -export type WasmBinding = { - filePath: string - name: string -} - export default function MiddlewareWasmLoader(this: any, source: Buffer) { const name = `wasm_${sha1(source)}` const filePath = `edge-chunks/${name}.wasm` - const binding: WasmBinding = { filePath: `server/${filePath}`, name } - this._module.buildInfo.nextWasmMiddlewareBinding = binding + const buildInfo = getModuleBuildInfo(this._module) + buildInfo.nextWasmMiddlewareBinding = { filePath: `server/${filePath}`, name } this.emitFile(`/${filePath}`, source, null) return `module.exports = ${name};` } diff --git a/packages/next/build/webpack/loaders/next-serverless-loader/page-handler.ts b/packages/next/build/webpack/loaders/next-serverless-loader/page-handler.ts index f15b47b4d3e10..806299a229f48 100644 --- a/packages/next/build/webpack/loaders/next-serverless-loader/page-handler.ts +++ b/packages/next/build/webpack/loaders/next-serverless-loader/page-handler.ts @@ -6,7 +6,7 @@ import { getUtils, vercelHeader, ServerlessHandlerCtx } from './utils' import { renderToHTML } from '../../../../server/render' import { tryGetPreviewData } from '../../../../server/api-utils/node' -import { denormalizePagePath } from '../../../../server/denormalize-page-path' +import { denormalizePagePath } from '../../../../shared/lib/page-path/denormalize-page-path' import { setLazyProp, getCookieParser } from '../../../../server/api-utils' import { getRedirectStatus } from '../../../../lib/load-custom-routes' import getRouteNoAssetPath from '../../../../shared/lib/router/utils/get-route-from-asset-path' 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 12662bf4b298a..3aedda04071b1 100644 --- a/packages/next/build/webpack/loaders/next-serverless-loader/utils.ts +++ b/packages/next/build/webpack/loaders/next-serverless-loader/utils.ts @@ -23,7 +23,7 @@ import { __ApiPreviewProps } from '../../../../server/api-utils' import { acceptLanguage } from '../../../../server/accept-header' import { detectLocaleCookie } from '../../../../shared/lib/i18n/detect-locale-cookie' import { detectDomainLocale } from '../../../../shared/lib/i18n/detect-domain-locale' -import { denormalizePagePath } from '../../../../server/denormalize-page-path' +import { denormalizePagePath } from '../../../../shared/lib/page-path/denormalize-page-path' import cookie from 'next/dist/compiled/cookie' import { TEMPORARY_REDIRECT_STATUS } from '../../../../shared/lib/constants' import { addRequestMeta } from '../../../../server/request-meta' diff --git a/packages/next/build/webpack/plugins/middleware-plugin.ts b/packages/next/build/webpack/plugins/middleware-plugin.ts index 32acdda5c82dc..19d03032570c9 100644 --- a/packages/next/build/webpack/plugins/middleware-plugin.ts +++ b/packages/next/build/webpack/plugins/middleware-plugin.ts @@ -1,20 +1,16 @@ -import { webpack, sources, webpack5 } from 'next/dist/compiled/webpack/webpack' +import type { EdgeMiddlewareMeta } from '../loaders/get-module-build-info' +import type { EdgeSSRMeta, WasmBinding } from '../loaders/get-module-build-info' import { getMiddlewareRegex } from '../../../shared/lib/router/utils' +import { getModuleBuildInfo } from '../loaders/get-module-build-info' import { getSortedRoutes } from '../../../shared/lib/router/utils' +import { webpack, sources, webpack5 } from 'next/dist/compiled/webpack/webpack' import { - MIDDLEWARE_MANIFEST, - MIDDLEWARE_FLIGHT_MANIFEST, + EDGE_RUNTIME_WEBPACK, MIDDLEWARE_BUILD_MANIFEST, + MIDDLEWARE_FLIGHT_MANIFEST, + MIDDLEWARE_MANIFEST, MIDDLEWARE_REACT_LOADABLE_MANIFEST, - EDGE_RUNTIME_WEBPACK, } from '../../../shared/lib/constants' -import { nonNullable } from '../../../lib/non-nullable' -import type { WasmBinding } from '../loaders/next-middleware-wasm-loader' - -const PLUGIN_NAME = 'MiddlewarePlugin' -const MIDDLEWARE_FULL_ROUTE_REGEX = /^pages[/\\]?(.*)\/_middleware$/ - -export const ssrEntries = new Map() export interface MiddlewareManifest { version: 1 @@ -32,6 +28,14 @@ export interface MiddlewareManifest { } } +interface EntryMetadata { + edgeMiddleware?: EdgeMiddlewareMeta + edgeSSR?: EdgeSSRMeta + env: Set + wasmBindings: Set +} + +const NAME = 'MiddlewarePlugin' const middlewareManifest: MiddlewareManifest = { sortedMiddleware: [], clientInfo: [], @@ -39,348 +43,366 @@ const middlewareManifest: MiddlewareManifest = { version: 1, } -function getPageFromEntrypointName(pagePath: string) { - const ssrEntryInfo = ssrEntries.get(pagePath) - const result = MIDDLEWARE_FULL_ROUTE_REGEX.exec(pagePath) - const page = result - ? `/${result[1]}` - : ssrEntryInfo - ? pagePath.slice('pages'.length).replace(/\/index$/, '') || '/' - : null - return page -} - -interface PerRoute { - envPerRoute: Map - wasmPerRoute: Map -} +export default class MiddlewarePlugin { + dev: boolean -function getEntrypointInfo( - compilation: webpack5.Compilation, - { envPerRoute, wasmPerRoute }: PerRoute -) { - const entrypoints = compilation.entrypoints - const infos = [] - for (const entrypoint of entrypoints.values()) { - if (!entrypoint.name) continue - - const ssrEntryInfo = ssrEntries.get(entrypoint.name) - const page = getPageFromEntrypointName(entrypoint.name) - if (!page) { - continue - } + constructor({ dev }: { dev: boolean }) { + this.dev = dev + } - const entryFiles = entrypoint - .getFiles() - .filter((file: string) => !file.endsWith('.hot-update.js')) - - const files = ssrEntryInfo - ? [ - ssrEntryInfo.requireFlightManifest - ? `server/${MIDDLEWARE_FLIGHT_MANIFEST}.js` - : null, - `server/${MIDDLEWARE_BUILD_MANIFEST}.js`, - `server/${MIDDLEWARE_REACT_LOADABLE_MANIFEST}.js`, - ...entryFiles.map((file) => 'server/' + file), - ].filter(nonNullable) - : entryFiles.map((file: string) => { - return 'server/' + file + apply(compiler: webpack5.Compiler) { + compiler.hooks.compilation.tap(NAME, (compilation, params) => { + const { hooks } = params.normalModuleFactory + + /** + * This is the static code analysis phase. + */ + const codeAnalyzer = getCodeAnalizer({ dev: this.dev, compiler }) + hooks.parser.for('javascript/auto').tap(NAME, codeAnalyzer) + hooks.parser.for('javascript/dynamic').tap(NAME, codeAnalyzer) + hooks.parser.for('javascript/esm').tap(NAME, codeAnalyzer) + + /** + * Extract all metadata for the entry points in a Map object. + */ + const metadataByEntry = new Map() + compilation.hooks.afterOptimizeModules.tap( + NAME, + getExtractMetadata({ + compilation, + compiler, + dev: this.dev, + metadataByEntry, }) + ) - infos.push({ - env: envPerRoute.get(entrypoint.name) || [], - wasm: wasmPerRoute.get(entrypoint.name) || [], - files, - name: entrypoint.name, - page, - regexp: getMiddlewareRegex(page, !ssrEntryInfo).namedRegex!, + /** + * Emit the middleware manifest. + */ + compilation.hooks.processAssets.tap( + { + name: 'NextJsMiddlewareManifest', + stage: (webpack as any).Compilation.PROCESS_ASSETS_STAGE_ADDITIONS, + }, + getCreateAssets({ compilation, metadataByEntry }) + ) }) } - return infos } -export default class MiddlewarePlugin { +function getCodeAnalizer(params: { dev: boolean + compiler: webpack5.Compiler +}) { + return (parser: webpack5.javascript.JavascriptParser) => { + const { + dev, + compiler: { webpack: wp }, + } = params + const { hooks } = parser + + /** + * This expression handler allows to wrap a dynamic code expression with a + * function call where we can warn about dynamic code not being allowed + * but actually execute the expression. + */ + const handleWrapExpression = (expr: any) => { + if (parser.state.module?.layer !== 'middleware') { + return + } - constructor({ dev }: { dev: boolean }) { - this.dev = dev - } + if (dev) { + const { ConstDependency } = wp.dependencies + const dep1 = new ConstDependency( + '__next_eval__(function() { return ', + expr.range[0] + ) + dep1.loc = expr.loc + parser.state.module.addPresentationalDependency(dep1) + const dep2 = new ConstDependency('})', expr.range[1]) + dep2.loc = expr.loc + parser.state.module.addPresentationalDependency(dep2) + } - createAssets( - compilation: webpack5.Compilation, - assets: any, - { envPerRoute, wasmPerRoute }: PerRoute - ) { - const infos = getEntrypointInfo(compilation, { envPerRoute, wasmPerRoute }) - infos.forEach((info) => { - middlewareManifest.middleware[info.page] = info - }) + handleExpression() + return true + } - middlewareManifest.sortedMiddleware = getSortedRoutes( - Object.keys(middlewareManifest.middleware) - ) - middlewareManifest.clientInfo = middlewareManifest.sortedMiddleware.map( - (key) => { - const middleware = middlewareManifest.middleware[key] - return [key, !!ssrEntries.get(middleware.name)] + /** + * For an expression this will check the graph to ensure it is being used + * by exports. Then it will store in the module buildInfo a boolean to + * express that it contains dynamic code and, if it is available, the + * module path that is using it. + */ + const handleExpression = () => { + if (parser.state.module?.layer !== 'middleware') { + return } - ) - - 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, - }) - } -} + wp.optimize.InnerGraph.onUsage(parser.state, (used = true) => { + const buildInfo = getModuleBuildInfo(parser.state.module) + if (buildInfo.usingIndirectEval === true || used === false) { + return + } -function collectAssets( - compiler: webpack5.Compiler, - createAssets: ( - compilation: webpack5.Compilation, - assets: any, - { envPerRoute, wasmPerRoute }: PerRoute - ) => void, - options: { dev: boolean; pluginName: string } -) { - const wp = compiler.webpack - compiler.hooks.compilation.tap( - options.pluginName, - (compilation, { normalModuleFactory }) => { - const envPerRoute = new Map() - const wasmPerRoute = new Map() - - compilation.hooks.afterOptimizeModules.tap(PLUGIN_NAME, () => { - const { moduleGraph } = compilation as any - envPerRoute.clear() - - for (const [name, info] of compilation.entries) { - if (info.options.runtime === EDGE_RUNTIME_WEBPACK) { - const middlewareEntries = new Set() - const env = new Set() - const wasm = new Set() - - const addEntriesFromDependency = (dep: any) => { - const module = moduleGraph.getModule(dep) - if (module) { - middlewareEntries.add(module) - } - } - - const runtime = wp.util.runtime.getEntryRuntime(compilation, name) - - info.dependencies.forEach(addEntriesFromDependency) - info.includeDependencies.forEach(addEntriesFromDependency) - - const queue = new Set(middlewareEntries) - for (const module of queue) { - const { buildInfo } = module - if (buildInfo.nextWasmMiddlewareBinding) { - wasm.add(buildInfo.nextWasmMiddlewareBinding) - } - if ( - !options.dev && - buildInfo && - isUsedByExports({ - module, - moduleGraph, - runtime, - usedByExports: buildInfo.usingIndirectEval, - }) - ) { - if ( - /node_modules[\\/]regenerator-runtime[\\/]runtime\.js/.test( - module.identifier() - ) - ) - continue - - const error = new wp.WebpackError( - `Dynamic Code Evaluation (e. g. 'eval', 'new Function') not allowed in Middleware ${name}${ - typeof buildInfo.usingIndirectEval !== 'boolean' - ? `\nUsed by ${Array.from( - buildInfo.usingIndirectEval - ).join(', ')}` - : '' - }` - ) - error.module = module - compilation.errors.push(error) - } - - if (buildInfo?.nextUsedEnvVars !== undefined) { - for (const envName of buildInfo.nextUsedEnvVars) { - env.add(envName) - } - } - - const connections = moduleGraph.getOutgoingConnections(module) - for (const connection of connections) { - if (connection.module) { - queue.add(connection.module) - } - } - } - - envPerRoute.set(name, Array.from(env)) - wasmPerRoute.set(name, Array.from(wasm)) - } + if (!buildInfo.usingIndirectEval || used === true) { + buildInfo.usingIndirectEval = used + return } + + buildInfo.usingIndirectEval = new Set([ + ...Array.from(buildInfo.usingIndirectEval), + ...Array.from(used), + ]) }) + } - const handler = (parser: webpack5.javascript.JavascriptParser) => { - const isMiddlewareModule = () => { - return parser.state.module?.layer === 'middleware' + /** + * A handler for calls to `process.env` where we identify the name of the + * ENV variable being assigned and store it in the module info. + */ + const handleCallMemberChain = (_: unknown, members: string[]) => { + if (members.length >= 2 && members[0] === 'env') { + const buildInfo = getModuleBuildInfo(parser.state.module) + if (buildInfo.nextUsedEnvVars === undefined) { + buildInfo.nextUsedEnvVars = new Set() } - const wrapExpression = (expr: any) => { - if (!isMiddlewareModule()) { - return - } - - if (options.dev) { - const dep1 = new wp.dependencies.ConstDependency( - '__next_eval__(function() { return ', - expr.range[0] - ) - dep1.loc = expr.loc - parser.state.module.addPresentationalDependency(dep1) - const dep2 = new wp.dependencies.ConstDependency( - '})', - expr.range[1] - ) - dep2.loc = expr.loc - parser.state.module.addPresentationalDependency(dep2) - } - expressionHandler() + buildInfo.nextUsedEnvVars.add(members[1]) + if (parser.state.module?.layer !== 'middleware') { return true } + } + } - const flagModule = ( - usedByExports: boolean | Set | undefined - ) => { - if (usedByExports === undefined) usedByExports = true - const old = parser.state.module.buildInfo.usingIndirectEval - if (old === true || usedByExports === false) return - if (!old || usedByExports === true) { - parser.state.module.buildInfo.usingIndirectEval = usedByExports - return - } - const set = new Set(old) - for (const item of usedByExports) { - set.add(item) - } - parser.state.module.buildInfo.usingIndirectEval = set - } + /** + * A noop handler to skip analyzing some cases. + */ + const noop = () => + parser.state.module?.layer === 'middleware' ? true : undefined + + hooks.call.for('eval').tap(NAME, handleWrapExpression) + hooks.call.for('global.eval').tap(NAME, handleWrapExpression) + hooks.call.for('Function').tap(NAME, handleWrapExpression) + hooks.call.for('global.Function').tap(NAME, handleWrapExpression) + hooks.new.for('Function').tap(NAME, handleWrapExpression) + hooks.new.for('global.Function').tap(NAME, handleWrapExpression) + hooks.expression.for('eval').tap(NAME, handleExpression) + hooks.expression.for('Function').tap(NAME, handleExpression) + hooks.expression.for('global.eval').tap(NAME, handleExpression) + hooks.expression.for('global.Function').tap(NAME, handleExpression) + hooks.expression.for('Function.prototype').tap(NAME, noop) + hooks.expression.for('global.Function.prototype').tap(NAME, noop) + hooks.callMemberChain.for('process').tap(NAME, handleCallMemberChain) + hooks.expressionMemberChain.for('process').tap(NAME, handleCallMemberChain) + } +} - const expressionHandler = () => { - if (!isMiddlewareModule()) return +function getExtractMetadata(params: { + compilation: webpack5.Compilation + compiler: webpack5.Compiler + dev: boolean + metadataByEntry: Map +}) { + const { dev, compilation, metadataByEntry, compiler } = params + const { webpack: wp } = compiler + return () => { + metadataByEntry.clear() + + for (const [entryName, entryData] of compilation.entries) { + if (entryData.options.runtime !== EDGE_RUNTIME_WEBPACK) { + // Only process edge runtime entries + continue + } - wp.optimize.InnerGraph.onUsage(parser.state, flagModule) + const { moduleGraph } = compilation + const entryModules = new Set() + const addEntriesFromDependency = (dependency: any) => { + const module = moduleGraph.getModule(dependency) + if (module) { + entryModules.add(module) } + } - const ignore = () => { - if (!isMiddlewareModule()) return + entryData.dependencies.forEach(addEntriesFromDependency) + entryData.includeDependencies.forEach(addEntriesFromDependency) - return true - } + const entryMetadata: EntryMetadata = { + env: new Set(), + wasmBindings: new Set(), + } - // wrapping - parser.hooks.call.for('eval').tap(PLUGIN_NAME, wrapExpression) - parser.hooks.call.for('global.eval').tap(PLUGIN_NAME, wrapExpression) - parser.hooks.call.for('Function').tap(PLUGIN_NAME, wrapExpression) - parser.hooks.call - .for('global.Function') - .tap(PLUGIN_NAME, wrapExpression) - parser.hooks.new.for('Function').tap(PLUGIN_NAME, wrapExpression) - parser.hooks.new.for('global.Function').tap(PLUGIN_NAME, wrapExpression) - - // fallbacks - parser.hooks.expression.for('eval').tap(PLUGIN_NAME, expressionHandler) - parser.hooks.expression - .for('Function') - .tap(PLUGIN_NAME, expressionHandler) - parser.hooks.expression - .for('Function.prototype') - .tap(PLUGIN_NAME, ignore) - parser.hooks.expression - .for('global.eval') - .tap(PLUGIN_NAME, expressionHandler) - parser.hooks.expression - .for('global.Function') - .tap(PLUGIN_NAME, expressionHandler) - parser.hooks.expression - .for('global.Function.prototype') - .tap(PLUGIN_NAME, ignore) - - const memberChainHandler = (_expr: any, members: string[]) => { - if (members.length >= 2 && members[0] === 'env') { - const envName = members[1] - const { buildInfo } = parser.state.module - if (buildInfo.nextUsedEnvVars === undefined) { - buildInfo.nextUsedEnvVars = new Set() - } - - buildInfo.nextUsedEnvVars.add(envName) - if (isMiddlewareModule()) return true + for (const entryModule of entryModules) { + const buildInfo = getModuleBuildInfo(entryModule) + + /** + * When building for production checks if the module is using `eval` + * and in such case produces a compilation error. The module has to + * be in use. + */ + if ( + !dev && + buildInfo.usingIndirectEval && + isUsingIndirectEvalAndUsedByExports({ + entryModule: entryModule, + moduleGraph: moduleGraph, + runtime: wp.util.runtime.getEntryRuntime(compilation, entryName), + usingIndirectEval: buildInfo.usingIndirectEval, + wp, + }) + ) { + const id = entryModule.identifier() + if (/node_modules[\\/]regenerator-runtime[\\/]runtime\.js/.test(id)) { + continue } - } - - parser.hooks.callMemberChain - .for('process') - .tap(PLUGIN_NAME, memberChainHandler) - parser.hooks.expressionMemberChain - .for('process') - .tap(PLUGIN_NAME, memberChainHandler) - } + const error = new wp.WebpackError( + `Dynamic Code Evaluation (e. g. 'eval', 'new Function') not allowed in Middleware ${entryName}${ + typeof buildInfo.usingIndirectEval !== 'boolean' + ? `\nUsed by ${Array.from(buildInfo.usingIndirectEval).join( + ', ' + )}` + : '' + }` + ) + + error.module = entryModule + compilation.errors.push(error) + } - normalModuleFactory.hooks.parser - .for('javascript/auto') - .tap(PLUGIN_NAME, handler) + /** + * The entry module has to be either a page or a middleware and hold + * the corresponding metadata. + */ + if (buildInfo?.nextEdgeSSR) { + entryMetadata.edgeSSR = buildInfo.nextEdgeSSR + } else if (buildInfo?.nextEdgeMiddleware) { + entryMetadata.edgeMiddleware = buildInfo.nextEdgeMiddleware + } - normalModuleFactory.hooks.parser - .for('javascript/dynamic') - .tap(PLUGIN_NAME, handler) + /** + * If there are env vars found in the module, append them to the set + * of env vars for the entry. + */ + if (buildInfo?.nextUsedEnvVars !== undefined) { + for (const envName of buildInfo.nextUsedEnvVars) { + entryMetadata.env.add(envName) + } + } - normalModuleFactory.hooks.parser - .for('javascript/esm') - .tap(PLUGIN_NAME, handler) + /** + * If the module is a WASM module we read the binding information and + * append it to the entry wasm bindings. + */ + if (buildInfo?.nextWasmMiddlewareBinding) { + entryMetadata.wasmBindings.add(buildInfo.nextWasmMiddlewareBinding) + } - // @ts-ignore TODO: Remove ignore when webpack 5 is stable - compilation.hooks.processAssets.tap( - { - name: 'NextJsMiddlewareManifest', - // @ts-ignore TODO: Remove ignore when webpack 5 is stable - stage: webpack.Compilation.PROCESS_ASSETS_STAGE_ADDITIONS, - }, - (assets: any) => { - createAssets(compilation, assets, { envPerRoute, wasmPerRoute }) + /** + * Append to the list of modules to process outgoingConnections from + * the module that is being processed. + */ + for (const conn of moduleGraph.getOutgoingConnections(entryModule)) { + if (conn.module) { + entryModules.add(conn.module) + } } - ) + } + + metadataByEntry.set(entryName, entryMetadata) } - ) + } } -function isUsedByExports(args: { - module: webpack5.Module +/** + * Checks the value of usingIndirectEval and when it is a set of modules it + * check if any of the modules is actually being used. If the value is + * simply truthy it will return true. + */ +function isUsingIndirectEvalAndUsedByExports(args: { + entryModule: webpack5.Module moduleGraph: webpack5.ModuleGraph runtime: any - usedByExports: boolean | Set | undefined + usingIndirectEval: true | Set + wp: typeof webpack5 }): boolean { - const { moduleGraph, runtime, module, usedByExports } = args - if (usedByExports === undefined) return false - if (typeof usedByExports === 'boolean') return usedByExports - const exportsInfo = moduleGraph.getExportsInfo(module) - const wp = webpack as unknown as typeof webpack5 - for (const exportName of usedByExports) { - if (exportsInfo.getUsed(exportName, runtime) !== wp.UsageState.Unused) + const { moduleGraph, runtime, entryModule, usingIndirectEval, wp } = args + if (typeof usingIndirectEval === 'boolean') { + return usingIndirectEval + } + + const exportsInfo = moduleGraph.getExportsInfo(entryModule) + for (const exportName of usingIndirectEval) { + if (exportsInfo.getUsed(exportName, runtime) !== wp.UsageState.Unused) { return true + } } + return false } + +function getCreateAssets(params: { + compilation: webpack5.Compilation + metadataByEntry: Map +}) { + const { compilation, metadataByEntry } = params + return (assets: any) => { + for (const entrypoint of compilation.entrypoints.values()) { + if (!entrypoint.name) { + continue + } + + // There should always be metadata for the entrypoint. + const metadata = metadataByEntry.get(entrypoint.name) + const page = metadata?.edgeMiddleware?.page || metadata?.edgeSSR?.page + if (!page) { + continue + } + + middlewareManifest.middleware[page] = { + env: Array.from(metadata.env), + files: getEntryFiles(entrypoint.getFiles(), metadata), + name: entrypoint.name, + page: page, + regexp: getMiddlewareRegex(page, !metadata.edgeSSR).namedRegex!, + wasm: Array.from(metadata.wasmBindings), + } + } + + middlewareManifest.sortedMiddleware = getSortedRoutes( + Object.keys(middlewareManifest.middleware) + ) + + middlewareManifest.clientInfo = middlewareManifest.sortedMiddleware.map( + (key) => [ + key, + !!metadataByEntry.get(middlewareManifest.middleware[key].name)?.edgeSSR, + ] + ) + + assets[MIDDLEWARE_MANIFEST] = new sources.RawSource( + JSON.stringify(middlewareManifest, null, 2) + ) + } +} + +function getEntryFiles(entryFiles: string[], meta: EntryMetadata) { + const files: string[] = [] + if (meta.edgeSSR) { + if (meta.edgeSSR.isServerComponent) { + files.push(`server/${MIDDLEWARE_FLIGHT_MANIFEST}.js`) + } + + files.push( + `server/${MIDDLEWARE_BUILD_MANIFEST}.js`, + `server/${MIDDLEWARE_REACT_LOADABLE_MANIFEST}.js` + ) + } + + files.push( + ...entryFiles + .filter((file) => !file.endsWith('.hot-update.js')) + .map((file) => 'server/' + file) + ) + return files +} diff --git a/packages/next/export/index.ts b/packages/next/export/index.ts index 9eff2e8a40e7d..8191dd75e7f34 100644 --- a/packages/next/export/index.ts +++ b/packages/next/export/index.ts @@ -34,10 +34,8 @@ import { NextConfigComplete } from '../server/config-shared' import { eventCliSession } from '../telemetry/events' import { hasNextSupport } from '../telemetry/ci-info' import { Telemetry } from '../telemetry/storage' -import { - normalizePagePath, - denormalizePagePath, -} from '../server/normalize-page-path' +import { normalizePagePath } from '../shared/lib/page-path/normalize-page-path' +import { denormalizePagePath } from '../shared/lib/page-path/denormalize-page-path' import { loadEnvConfig } from '@next/env' import { PrerenderManifest } from '../build' import { PagesManifest } from '../build/webpack/plugins/pages-manifest-plugin' diff --git a/packages/next/export/worker.ts b/packages/next/export/worker.ts index 3679b28eebd1d..e68d20456d088 100644 --- a/packages/next/export/worker.ts +++ b/packages/next/export/worker.ts @@ -13,7 +13,7 @@ import { loadComponents } from '../server/load-components' import { isDynamicRoute } from '../shared/lib/router/utils/is-dynamic' import { getRouteMatcher } from '../shared/lib/router/utils/route-matcher' import { getRouteRegex } from '../shared/lib/router/utils/route-regex' -import { normalizePagePath } from '../server/normalize-page-path' +import { normalizePagePath } from '../shared/lib/page-path/normalize-page-path' import { SERVER_PROPS_EXPORT_ERROR } from '../lib/constants' import '../server/node-polyfill-fetch' import { requireFontManifest } from '../server/require' diff --git a/packages/next/server/base-server.ts b/packages/next/server/base-server.ts index 1d76fa5df548e..f47d30f94a0c8 100644 --- a/packages/next/server/base-server.ts +++ b/packages/next/server/base-server.ts @@ -50,7 +50,7 @@ import { isBlockedPage, isBot } from './utils' import RenderResult from './render-result' import { removePathTrailingSlash } from '../client/normalize-trailing-slash' import getRouteFromAssetPath from '../shared/lib/router/utils/get-route-from-asset-path' -import { denormalizePagePath } from './denormalize-page-path' +import { denormalizePagePath } from '../shared/lib/page-path/denormalize-page-path' import { normalizeLocalePath } from '../shared/lib/i18n/normalize-locale-path' import * as Log from '../build/output/log' import { detectDomainLocale } from '../shared/lib/i18n/detect-domain-locale' @@ -133,7 +133,6 @@ export default abstract class Server { protected quiet: boolean protected nextConfig: NextConfigComplete protected distDir: string - protected pagesDir?: string protected publicDir: string protected hasStaticDir: boolean protected pagesManifest?: PagesManifest diff --git a/packages/next/server/denormalize-page-path.ts b/packages/next/server/denormalize-page-path.ts deleted file mode 100644 index e3e2f1bcecd00..0000000000000 --- a/packages/next/server/denormalize-page-path.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { isDynamicRoute } from '../shared/lib/router/utils' - -export function normalizePathSep(path: string): string { - return path.replace(/\\/g, '/') -} - -export function denormalizePagePath(page: string) { - page = normalizePathSep(page) - if (page.startsWith('/index/') && !isDynamicRoute(page)) { - page = page.slice(6) - } else if (page === '/index') { - page = '/' - } - return page -} diff --git a/packages/next/server/dev/hot-reloader.ts b/packages/next/server/dev/hot-reloader.ts index 0e3d8c96812ab..d17049d479664 100644 --- a/packages/next/server/dev/hot-reloader.ts +++ b/packages/next/server/dev/hot-reloader.ts @@ -21,14 +21,15 @@ import { BLOCKED_PAGES } from '../../shared/lib/constants' import { __ApiPreviewProps } from '../api-utils' import { getPathMatch } from '../../shared/lib/router/utils/path-match' import { findPageFile } from '../lib/find-page-file' -import onDemandEntryHandler, { - entries, +import { BUILDING, + entries, + onDemandEntryHandler, } from './on-demand-entry-handler' -import { denormalizePagePath, normalizePathSep } from '../normalize-page-path' +import { denormalizePagePath } from '../../shared/lib/page-path/denormalize-page-path' +import { normalizePathSep } from '../../shared/lib/page-path/normalize-path-sep' import getRouteFromEntrypoint from '../get-route-from-entrypoint' import { fileExists } from '../../lib/file-exists' -import { ssrEntries } from '../../build/webpack/plugins/middleware-plugin' import { difference } from '../../build/utils' import { NextConfigComplete } from '../config-shared' import { CustomRoutes } from '../../lib/load-custom-routes' @@ -558,7 +559,6 @@ export default class HotReloader { isDev: true, page, pages: this.pagesMapping, - ssrEntries, }), }) } @@ -830,7 +830,9 @@ export default class HotReloader { ) }) - this.onDemandEntries = onDemandEntryHandler(this.watcher, multiCompiler, { + this.onDemandEntries = onDemandEntryHandler({ + multiCompiler, + watcher: this.watcher, pagesDir: this.pagesDir, nextConfig: this.config, ...(this.config.onDemandEntries as { diff --git a/packages/next/server/dev/next-dev-server.ts b/packages/next/server/dev/next-dev-server.ts index 9a76c9eb61809..d23353c1ceeef 100644 --- a/packages/next/server/dev/next-dev-server.ts +++ b/packages/next/server/dev/next-dev-server.ts @@ -40,7 +40,8 @@ import { isDynamicRoute, } from '../../shared/lib/router/utils' import Server, { WrappedBuildError } from '../next-server' -import { normalizePagePath } from '../normalize-page-path' +import { normalizePagePath } from '../../shared/lib/page-path/normalize-page-path' +import { absolutePathToPage } from '../../shared/lib/page-path/absolute-path-to-page' import Router from '../router' import { getPathMatch } from '../../shared/lib/router/utils/path-match' import { hasBasePath, replaceBasePath } from '../router-utils' @@ -94,6 +95,7 @@ export default class DevServer extends Server { private isCustomServer: boolean protected sortedRoutes?: string[] private addedUpgradeListener = false + private pagesDir: string protected staticPathsWorker?: { [key: string]: any } & { loadStaticPaths: typeof import('./static-paths-worker').loadStaticPaths @@ -241,10 +243,8 @@ export default class DevServer extends Server { let resolved = false return new Promise((resolve, reject) => { - const pagesDir = this.pagesDir - // Watchpack doesn't emit an event for an empty directory - fs.readdir(pagesDir!, (_, files) => { + fs.readdir(this.pagesDir, (_, files) => { if (files?.length) { return } @@ -256,7 +256,7 @@ export default class DevServer extends Server { }) let wp = (this.webpackWatcher = new Watchpack()) - wp.watch([], [pagesDir!], 0) + wp.watch([], [this.pagesDir], 0) wp.on('aggregated', async () => { const routedMiddleware = [] @@ -269,20 +269,21 @@ export default class DevServer extends Server { continue } + const pageName = absolutePathToPage( + this.pagesDir, + fileName, + this.nextConfig.pageExtensions + ) + if (regexMiddleware.test(fileName)) { routedMiddleware.push( - `/${relative(pagesDir!, fileName).replace(/\\+/g, '/')}` + `/${relative(this.pagesDir, fileName).replace(/\\+/g, '/')}` .replace(/^\/+/g, '/') .replace(regexMiddleware, '/') ) continue } - let pageName = - '/' + relative(pagesDir!, fileName).replace(/\\+/g, '/') - pageName = pageName.replace(regexPageExtension, '') - pageName = pageName.replace(/\/index$/, '') || '/' - invalidatePageRuntimeCache(fileName, safeTime) const pageRuntimeConfig = await getPageRuntime( fileName, @@ -360,12 +361,7 @@ export default class DevServer extends Server { async prepare(): Promise { setGlobal('distDir', this.distDir) setGlobal('phase', PHASE_DEVELOPMENT_SERVER) - await verifyTypeScriptSetup( - this.dir, - this.pagesDir!, - false, - this.nextConfig - ) + await verifyTypeScriptSetup(this.dir, this.pagesDir, false, this.nextConfig) this.customRoutes = await loadCustomRoutes(this.nextConfig) @@ -383,7 +379,7 @@ export default class DevServer extends Server { } this.hotReloader = new HotReloader(this.dir, { - pagesDir: this.pagesDir!, + pagesDir: this.pagesDir, distDir: this.distDir, config: this.nextConfig, previewProps: this.getPreviewProps(), @@ -408,7 +404,7 @@ export default class DevServer extends Server { eventCliSession(this.distDir, this.nextConfig, { webpackVersion: 5, cliCommand: 'dev', - isSrcDir: relative(this.dir, this.pagesDir!).startsWith('src'), + isSrcDir: relative(this.dir, this.pagesDir).startsWith('src'), hasNowJson: !!(await findUp('now.json', { cwd: this.dir })), isCustomServer: this.isCustomServer, }) @@ -448,7 +444,7 @@ export default class DevServer extends Server { } const pageFile = await findPageFile( - this.pagesDir!, + this.pagesDir, normalizedPath, this.nextConfig.pageExtensions ) diff --git a/packages/next/server/dev/on-demand-entry-handler.ts b/packages/next/server/dev/on-demand-entry-handler.ts index 3f39e8f7ba85a..08eabdff3a532 100644 --- a/packages/next/server/dev/on-demand-entry-handler.ts +++ b/packages/next/server/dev/on-demand-entry-handler.ts @@ -5,7 +5,10 @@ import { EventEmitter } from 'events' import { findPageFile } from '../lib/find-page-file' import { getPageRuntime, runDependingOnPageType } from '../../build/entries' import { join, posix } from 'path' -import { normalizePagePath, normalizePathSep } from '../normalize-page-path' +import { normalizePathSep } from '../../shared/lib/page-path/normalize-path-sep' +import { normalizePagePath } from '../../shared/lib/page-path/normalize-page-path' +import { ensureLeadingSlash } from '../../shared/lib/page-path/ensure-leading-slash' +import { removePagePathTail } from '../../shared/lib/page-path/remove-page-path-tail' import { pageNotFoundError } from '../require' import { reportTrigger } from '../../build/output' import getRouteFromEntrypoint from '../get-route-from-entrypoint' @@ -15,37 +18,56 @@ export const BUILDING = Symbol('building') export const BUILT = Symbol('built') export const entries: { + /** + * The key composed of the compiler name and the page. For example: + * `edge-server/about` + */ [page: string]: { - bundlePath: string + /** + * The absolute page to the page file. For example: + * `/Users/Rick/project/pages/about/index.js` + */ absolutePagePath: string - status?: typeof ADDED | typeof BUILDING | typeof BUILT - lastActiveTime?: number + /** + * Path to the page file relative to the dist folder with no extension. + * For example: `pages/about/index` + */ + bundlePath: string + /** + * Tells if a page is scheduled to be disposed. + */ dispose?: boolean + /** + * Timestamp with the last time the page was active. + */ + lastActiveTime?: number + /** + * Page build status. + */ + status?: typeof ADDED | typeof BUILDING | typeof BUILT } } = {} -export default function onDemandEntryHandler( - watcher: any, - multiCompiler: webpack.MultiCompiler, - { - pagesDir, - nextConfig, - maxInactiveAge, - pagesBufferLength, - }: { - pagesDir: string - nextConfig: NextConfigComplete - maxInactiveAge: number - pagesBufferLength: number - } -) { - const { compilers } = multiCompiler - const invalidator = new Invalidator(watcher, multiCompiler) - - let lastClientAccessPages = [''] - let doneCallbacks: EventEmitter | null = new EventEmitter() +export function onDemandEntryHandler({ + maxInactiveAge, + multiCompiler, + nextConfig, + pagesBufferLength, + pagesDir, + watcher, +}: { + maxInactiveAge: number + multiCompiler: webpack.MultiCompiler + nextConfig: NextConfigComplete + pagesBufferLength: number + pagesDir: string + watcher: any +}) { + const invalidator = new Invalidator(watcher) + const doneCallbacks: EventEmitter | null = new EventEmitter() + const lastClientAccessPages = [''] - for (const compiler of compilers) { + for (const compiler of multiCompiler.compilers) { compiler.hooks.make.tap( 'NextJsOnDemandEntries', (_compilation: webpack.Compilation) => { @@ -55,12 +77,12 @@ export default function onDemandEntryHandler( } function getPagePathsFromEntrypoints( - type: string, - entrypoints: any - ): string[] { - const pagePaths = [] + type: 'client' | 'server' | 'edge-server', + entrypoints: Map + ) { + const pagePaths: string[] = [] for (const entrypoint of entrypoints.values()) { - const page = getRouteFromEntrypoint(entrypoint.name) + const page = getRouteFromEntrypoint(entrypoint.name!) if (page) { pagePaths.push(`${type}${page}`) } @@ -110,17 +132,14 @@ export default function onDemandEntryHandler( const pingIntervalTime = Math.max(1000, Math.min(5000, maxInactiveAge)) - const disposeHandler = setInterval(function () { - disposeInactiveEntries(watcher, lastClientAccessPages, maxInactiveAge) - }, pingIntervalTime + 1000) - - disposeHandler.unref() + setInterval(function () { + disposeInactiveEntries(lastClientAccessPages, maxInactiveAge) + }, pingIntervalTime + 1000).unref() function handlePing(pg: string) { const page = normalizePathSep(pg) const pageKey = `client${page}` const entryInfo = entries[pageKey] - let toSend // If there's no entry, it may have been invalidated and needs to be re-built. if (!entryInfo) { @@ -129,11 +148,7 @@ export default function onDemandEntryHandler( } // 404 is an on demand entry but when a new page is added we have to refresh the page - if (page === '/_error') { - toSend = { invalid: true } - } else { - toSend = { success: true } - } + const toSend = page === '/_error' ? { invalid: true } : { success: true } // We don't need to maintain active state of anything other than BUILT entries if (entryInfo.status !== BUILT) return @@ -153,64 +168,59 @@ export default function onDemandEntryHandler( } return { - async ensurePage(_page: string, clientOnly: boolean) { - const { absolutePagePath, bundlePath, page } = await getPageInfo({ - pageExtensions: nextConfig.pageExtensions, - page: _page, + async ensurePage(page: string, clientOnly: boolean) { + const pagePathData = await findPagePathData( pagesDir, - }) + page, + nextConfig.pageExtensions + ) - let entriesChanged = false + let entryAdded = 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 - const pageKey = `${type}${page}` - const entryInfo = entries[pageKey] - - if (entryInfo) { - entryInfo.lastActiveTime = Date.now() - entryInfo.dispose = false - if (entryInfo.status === BUILT) { + const pageKey = `${type}${pagePathData.page}` + if (entries[pageKey]) { + entries[pageKey].dispose = false + entries[pageKey].lastActiveTime = Date.now() + if (entries[pageKey].status === BUILT) { resolve() return } - - doneCallbacks!.once(pageKey, handleCallback) - return - } - - entriesChanged = true - - entries[pageKey] = { - bundlePath, - absolutePagePath, - status: ADDED, - lastActiveTime: Date.now(), - dispose: false, + } else { + entryAdded = true + entries[pageKey] = { + absolutePagePath: pagePathData.absolutePagePath, + bundlePath: pagePathData.bundlePath, + dispose: false, + lastActiveTime: Date.now(), + status: ADDED, + } } - doneCallbacks!.once(pageKey, handleCallback) - function handleCallback(err: Error) { + doneCallbacks!.once(pageKey, (err: Error) => { if (err) return reject(err) resolve() - } + }) }) } const promises = runDependingOnPageType({ - page, - pageRuntime: await getPageRuntime(absolutePagePath, nextConfig), + page: pagePathData.page, + pageRuntime: await getPageRuntime( + pagePathData.absolutePagePath, + nextConfig + ), onClient: () => addPageEntry('client'), onServer: () => addPageEntry('server'), onEdgeServer: () => addPageEntry('edge-server'), }) - if (entriesChanged) { + if (entryAdded) { reportTrigger( !clientOnly && promises.length > 1 - ? `${page} (client and server)` - : page + ? `${pagePathData.page} (client and server)` + : pagePathData.page ) invalidator.invalidate() } @@ -220,9 +230,10 @@ export default function onDemandEntryHandler( onHMR(client: ws) { client.addEventListener('message', ({ data }) => { - data = typeof data !== 'string' ? data.toString() : data try { - const parsedData = JSON.parse(data) + const parsedData = JSON.parse( + typeof data !== 'string' ? data.toString() : data + ) if (parsedData.event === 'ping') { const result = handlePing(parsedData.page) @@ -240,8 +251,7 @@ export default function onDemandEntryHandler( } function disposeInactiveEntries( - _watcher: any, - lastClientAccessPages: any, + lastClientAccessPages: string[], maxInactiveAge: number ) { Object.keys(entries).forEach((page) => { @@ -268,13 +278,11 @@ function disposeInactiveEntries( // Make sure only one invalidation happens at a time // Otherwise, webpack hash gets changed and it'll force the client to reload. class Invalidator { - private multiCompiler: webpack.MultiCompiler private watcher: any private building: boolean public rebuildAgain: boolean - constructor(watcher: any, multiCompiler: webpack.MultiCompiler) { - this.multiCompiler = multiCompiler + constructor(watcher: any) { this.watcher = watcher // contains an array of types of compilers currently building this.building = false @@ -309,54 +317,50 @@ 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) - } +/** + * Attempts to find a page file path from the given pages absolute directory, + * a page and allowed extensions. If the page can't be found it will throw an + * error. It defaults the `/_error` page to Next.js internal error page. + * + * @param pagesDir Absolute path to the pages folder with trailing `/pages`. + * @param normalizedPagePath The page normalized (it will be denormalized). + * @param pageExtensions Array of page extensions. + */ +async function findPagePathData( + pagesDir: string, + page: string, + extensions: string[] +) { + const normalizedPagePath = tryToNormalizePagePath(page) + const pagePath = await findPageFile(pagesDir, normalizedPagePath, extensions) + if (pagePath !== null) { + const pageUrl = ensureLeadingSlash( + removePagePathTail(normalizePathSep(pagePath), extensions) + ) - 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) + return { + absolutePagePath: join(pagesDir, pagePath), + bundlePath: posix.join('pages', normalizePagePath(pageUrl)), + page: posix.normalize(pageUrl), } } - if (pagePath.startsWith('next/dist/pages/')) { + if (page === '/_error') { return { - page: normalizePathSep(page), + absolutePagePath: require.resolve('next/dist/pages/_error'), bundlePath: page, - absolutePagePath: require.resolve(pagePath), + page: normalizePathSep(page), } + } else { + throw pageNotFoundError(normalizedPagePath) } +} - 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)), +function tryToNormalizePagePath(page: string) { + try { + return normalizePagePath(page) + } catch (err) { + console.error(err) + throw pageNotFoundError(page) } } diff --git a/packages/next/server/get-page-files.ts b/packages/next/server/get-page-files.ts index cd7dc93e660cb..5e6945cbdf53b 100644 --- a/packages/next/server/get-page-files.ts +++ b/packages/next/server/get-page-files.ts @@ -1,4 +1,5 @@ -import { normalizePagePath, denormalizePagePath } from './normalize-page-path' +import { denormalizePagePath } from '../shared/lib/page-path/denormalize-page-path' +import { normalizePagePath } from '../shared/lib/page-path/normalize-page-path' export type BuildManifest = { devFiles: readonly string[] diff --git a/packages/next/server/incremental-cache.ts b/packages/next/server/incremental-cache.ts index 8b949e8105b03..bb309aed2a11b 100644 --- a/packages/next/server/incremental-cache.ts +++ b/packages/next/server/incremental-cache.ts @@ -3,7 +3,7 @@ import type { CacheFs } from '../shared/lib/utils' import LRUCache from 'next/dist/compiled/lru-cache' import path from '../shared/lib/isomorphic/path' import { PrerenderManifest } from '../build' -import { normalizePagePath } from './normalize-page-path' +import { normalizePagePath } from '../shared/lib/page-path/normalize-page-path' import { IncrementalCacheValue, IncrementalCacheEntry } from './response-cache' function toRoute(pathname: string): string { diff --git a/packages/next/server/lib/find-page-file.ts b/packages/next/server/lib/find-page-file.ts index a8bb85406e6ac..b7e60c2b7dd56 100644 --- a/packages/next/server/lib/find-page-file.ts +++ b/packages/next/server/lib/find-page-file.ts @@ -1,65 +1,62 @@ -import { join, sep as pathSeparator, normalize } from 'path' -import chalk from '../../lib/chalk' -import { warn } from '../../build/output/log' -import { promises } from 'fs' -import { denormalizePagePath } from '../normalize-page-path' import { fileExists } from '../../lib/file-exists' +import { getPagePaths } from '../../shared/lib/page-path/get-page-paths' +import { nonNullable } from '../../lib/non-nullable' +import { join, sep, normalize } from 'path' +import { promises } from 'fs' +import { warn } from '../../build/output/log' +import chalk from '../../lib/chalk' -async function isTrueCasePagePath(pagePath: string, pagesDir: string) { - const pageSegments = normalize(pagePath).split(pathSeparator).filter(Boolean) - - const segmentExistsPromises = pageSegments.map(async (segment, i) => { - const segmentParentDir = join(pagesDir, ...pageSegments.slice(0, i)) - const parentDirEntries = await promises.readdir(segmentParentDir) - return parentDirEntries.includes(segment) - }) - - return (await Promise.all(segmentExistsPromises)).every(Boolean) -} - +/** + * Finds a page file with the given parameters. If the page is duplicated with + * multiple extensions it will throw, otherwise it will return the *relative* + * path to the page file or null if it is not found. + * + * @param pagesDir Absolute path to the pages folder with trailing `/pages`. + * @param normalizedPagePath The page normalized (it will be denormalized). + * @param pageExtensions Array of page extensions. + */ export async function findPageFile( - rootDir: string, + pagesDir: string, normalizedPagePath: string, pageExtensions: string[] ): Promise { - const foundPagePaths: string[] = [] - - const page = denormalizePagePath(normalizedPagePath) - - for (const extension of pageExtensions) { - if (!normalizedPagePath.endsWith('/index')) { - const relativePagePath = `${page}.${extension}` - const pagePath = join(rootDir, relativePagePath) - - if (await fileExists(pagePath)) { - foundPagePaths.push(relativePagePath) - } - } - - const relativePagePathWithIndex = join(page, `index.${extension}`) - const pagePathWithIndex = join(rootDir, relativePagePathWithIndex) - if (await fileExists(pagePathWithIndex)) { - foundPagePaths.push(relativePagePathWithIndex) - } - } + const pagePaths = getPagePaths(normalizedPagePath, pageExtensions) + const [existingPath, ...others] = ( + await Promise.all( + pagePaths.map(async (path) => + (await fileExists(join(pagesDir, path))) ? path : null + ) + ) + ).filter(nonNullable) - if (foundPagePaths.length < 1) { + if (!existingPath) { return null } - if (!(await isTrueCasePagePath(foundPagePaths[0], rootDir))) { + if (!(await isTrueCasePagePath(existingPath, pagesDir))) { return null } - if (foundPagePaths.length > 1) { + if (others.length > 0) { warn( `Duplicate page detected. ${chalk.cyan( - join('pages', foundPagePaths[0]) + join('pages', existingPath) )} and ${chalk.cyan( - join('pages', foundPagePaths[1]) + join('pages', others[0]) )} both resolve to ${chalk.cyan(normalizedPagePath)}.` ) } - return foundPagePaths[0] + return existingPath +} + +async function isTrueCasePagePath(pagePath: string, pagesDir: string) { + const pageSegments = normalize(pagePath).split(sep).filter(Boolean) + const segmentExistsPromises = pageSegments.map(async (segment, i) => { + const segmentParentDir = join(pagesDir, ...pageSegments.slice(0, i)) + const parentDirEntries = await promises.readdir(segmentParentDir) + return parentDirEntries.includes(segment) + }) + + return (await Promise.all(segmentExistsPromises)).every(Boolean) } diff --git a/packages/next/server/next-server.ts b/packages/next/server/next-server.ts index 0d8b3ad7a65a9..c0a5e91ccef76 100644 --- a/packages/next/server/next-server.ts +++ b/packages/next/server/next-server.ts @@ -55,7 +55,7 @@ import BaseServer, { stringifyQuery, } from './base-server' import { getMiddlewareInfo, getPagePath, requireFontManifest } from './require' -import { normalizePagePath } from './normalize-page-path' +import { normalizePagePath } from '../shared/lib/page-path/normalize-page-path' import { loadComponents } from './load-components' import isError, { getProperError } from '../lib/is-error' import { FontManifest } from './font-utils' diff --git a/packages/next/server/normalize-page-path.ts b/packages/next/server/normalize-page-path.ts deleted file mode 100644 index 236504838d080..0000000000000 --- a/packages/next/server/normalize-page-path.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { posix } from '../shared/lib/isomorphic/path' -import { isDynamicRoute } from '../shared/lib/router/utils' - -export { normalizePathSep, denormalizePagePath } from './denormalize-page-path' - -export function normalizePagePath(page: string): string { - // If the page is `/` we need to append `/index`, otherwise the returned directory root will be bundles instead of pages - if (page === '/') { - page = '/index' - } else if (/^\/index(\/|$)/.test(page) && !isDynamicRoute(page)) { - page = `/index${page}` - } - // Resolve on anything that doesn't start with `/` - if (!page.startsWith('/')) { - page = `/${page}` - } - // Throw when using ../ etc in the pathname - const resolvedPage = posix.normalize(page) - if (page !== resolvedPage) { - throw new Error( - `Requested and resolved page mismatch: ${page} ${resolvedPage}` - ) - } - return page -} diff --git a/packages/next/server/render.tsx b/packages/next/server/render.tsx index e4a2e62567162..cd3af5d6d4fdb 100644 --- a/packages/next/server/render.tsx +++ b/packages/next/server/render.tsx @@ -55,8 +55,8 @@ import { loadGetInitialProps, } from '../shared/lib/utils' import { HtmlContext } from '../shared/lib/html-context' -import { denormalizePagePath } from './denormalize-page-path' -import { normalizePagePath } from './normalize-page-path' +import { normalizePagePath } from '../shared/lib/page-path/normalize-page-path' +import { denormalizePagePath } from '../shared/lib/page-path/denormalize-page-path' import { getRequestMeta, NextParsedUrlQuery } from './request-meta' import { allowedStatusCodes, diff --git a/packages/next/server/require.ts b/packages/next/server/require.ts index a5cb61665bb0c..ee436ab9b64e9 100644 --- a/packages/next/server/require.ts +++ b/packages/next/server/require.ts @@ -7,11 +7,12 @@ import { SERVER_DIRECTORY, SERVERLESS_DIRECTORY, } from '../shared/lib/constants' -import { normalizePagePath, denormalizePagePath } from './normalize-page-path' import { normalizeLocalePath } from '../shared/lib/i18n/normalize-locale-path' +import { normalizePagePath } from '../shared/lib/page-path/normalize-page-path' +import { denormalizePagePath } from '../shared/lib/page-path/denormalize-page-path' import type { PagesManifest } from '../build/webpack/plugins/pages-manifest-plugin' import type { MiddlewareManifest } from '../build/webpack/plugins/middleware-plugin' -import type { WasmBinding } from '../build/webpack/loaders/next-middleware-wasm-loader' +import type { WasmBinding } from '../build/webpack/loaders/get-module-build-info' export function pageNotFoundError(page: string): Error { const err: any = new Error(`Cannot find module for page: ${page}`) diff --git a/packages/next/server/web/sandbox/context.ts b/packages/next/server/web/sandbox/context.ts index 9e96005bbccb9..11f989b6fd483 100644 --- a/packages/next/server/web/sandbox/context.ts +++ b/packages/next/server/web/sandbox/context.ts @@ -10,7 +10,7 @@ import { AbortSignal, } from 'next/dist/compiled/abort-controller' import vm from 'vm' -import type { WasmBinding } from '../../../build/webpack/loaders/next-middleware-wasm-loader' +import type { WasmBinding } from '../../../build/webpack/loaders/get-module-build-info' const WEBPACK_HASH_REGEX = /__webpack_require__\.h = function\(\) \{ return "[0-9a-f]+"; \}/g diff --git a/packages/next/server/web/sandbox/sandbox.ts b/packages/next/server/web/sandbox/sandbox.ts index b24451abcb262..85596235a4a15 100644 --- a/packages/next/server/web/sandbox/sandbox.ts +++ b/packages/next/server/web/sandbox/sandbox.ts @@ -1,4 +1,4 @@ -import type { WasmBinding } from '../../../build/webpack/loaders/next-middleware-wasm-loader' +import type { WasmBinding } from '../../../build/webpack/loaders/get-module-build-info' import type { RequestData, FetchEventResult } from '../types' import { getModuleContext } from './context' diff --git a/packages/next/shared/lib/flatten.ts b/packages/next/shared/lib/flatten.ts new file mode 100644 index 0000000000000..6767730a7a81f --- /dev/null +++ b/packages/next/shared/lib/flatten.ts @@ -0,0 +1,30 @@ +type Flattened = T extends Array ? Flattened : T + +/** + * Returns a new list by pulling every item out of it (and all its sub-arrays) + * and putting them in a new array, depth-first. Stolen from Ramda. + */ +export function flatten(list: T): Flattened[] { + let jlen: number, + j: number, + value, + idx = 0, + result = [] + + while (idx < list.length) { + if (Array.isArray(list[idx])) { + value = flatten(list[idx]) + j = 0 + jlen = value.length + while (j < jlen) { + result[result.length] = value[j] + j += 1 + } + } else { + result[result.length] = list[idx] + } + idx += 1 + } + + return result as Flattened[] +} diff --git a/packages/next/shared/lib/i18n/get-locale-metadata.ts b/packages/next/shared/lib/i18n/get-locale-metadata.ts index c2f1e3d99d53d..f62273bd6758a 100644 --- a/packages/next/shared/lib/i18n/get-locale-metadata.ts +++ b/packages/next/shared/lib/i18n/get-locale-metadata.ts @@ -1,5 +1,5 @@ import { acceptLanguage } from '../../../server/accept-header' -import { denormalizePagePath } from '../../../server/denormalize-page-path' +import { denormalizePagePath } from '../page-path/denormalize-page-path' import { detectDomainLocale } from './detect-domain-locale' import { formatUrl } from '../router/utils/format-url' import { normalizeLocalePath } from './normalize-locale-path' diff --git a/packages/next/shared/lib/page-path/absolute-path-to-page.ts b/packages/next/shared/lib/page-path/absolute-path-to-page.ts new file mode 100644 index 0000000000000..6b2cd91a6e3df --- /dev/null +++ b/packages/next/shared/lib/page-path/absolute-path-to-page.ts @@ -0,0 +1,25 @@ +import { ensureLeadingSlash } from './ensure-leading-slash' +import { normalizePathSep } from './normalize-path-sep' +import { relative } from '../isomorphic/path' +import { removePagePathTail } from './remove-page-path-tail' + +/** + * Given the absolute path to the pages folder, an absolute file path for a + * page and the page extensions, this function will return the page path + * relative to the pages folder. It doesn't consider index tail. Example: + * - `/Users/rick/my-project/pages/foo/bar/baz.js` -> `/foo/bar/baz` + * + * @param pagesDir Absolute path to the pages folder. + * @param filepath Absolute path to the page. + * @param extensions Extensions allowed for the page. + */ +export function absolutePathToPage( + pagesDir: string, + pagePath: string, + extensions: string[] +) { + return removePagePathTail( + normalizePathSep(ensureLeadingSlash(relative(pagesDir, pagePath))), + extensions + ) +} diff --git a/packages/next/shared/lib/page-path/denormalize-page-path.ts b/packages/next/shared/lib/page-path/denormalize-page-path.ts new file mode 100644 index 0000000000000..adbc2ad8c44ae --- /dev/null +++ b/packages/next/shared/lib/page-path/denormalize-page-path.ts @@ -0,0 +1,19 @@ +import { isDynamicRoute } from '../router/utils' +import { normalizePathSep } from './normalize-path-sep' + +/** + * Performs the opposite transformation of `normalizePagePath`. Note that + * this function is not idempotent either in cases where there are multiple + * leading `/index` for the page. Examples: + * - `/index` -> `/` + * - `/index/foo` -> `/foo` + * - `/index/index` -> `/index` + */ +export function denormalizePagePath(page: string) { + let _page = normalizePathSep(page) + return _page.startsWith('/index/') && !isDynamicRoute(_page) + ? _page.slice(6) + : _page !== '/index' + ? _page + : '/' +} diff --git a/packages/next/shared/lib/page-path/ensure-leading-slash.ts b/packages/next/shared/lib/page-path/ensure-leading-slash.ts new file mode 100644 index 0000000000000..d23a6d30e84dc --- /dev/null +++ b/packages/next/shared/lib/page-path/ensure-leading-slash.ts @@ -0,0 +1,7 @@ +/** + * For a given page path, this function ensures that there is a leading slash. + * If there is not a leading slash, one is added, otherwise it is noop. + */ +export function ensureLeadingSlash(path: string) { + return path.startsWith('/') ? path : `/${path}` +} diff --git a/packages/next/shared/lib/page-path/get-page-paths.ts b/packages/next/shared/lib/page-path/get-page-paths.ts new file mode 100644 index 0000000000000..bdf9db2124202 --- /dev/null +++ b/packages/next/shared/lib/page-path/get-page-paths.ts @@ -0,0 +1,22 @@ +import { denormalizePagePath } from './denormalize-page-path' +import { flatten } from '../flatten' +import { join } from '../isomorphic/path' + +/** + * Calculate all possible pagePaths for a given normalized pagePath along with + * allowed extensions. This can be used to check which one of the files exists + * and to debug inspected locations. + * + * @param normalizedPagePath Normalized page path (it will denormalize). + * @param extensions Allowed extensions. + */ +export function getPagePaths(normalizedPagePath: string, extensions: string[]) { + const page = denormalizePagePath(normalizedPagePath) + return flatten( + extensions.map((extension) => { + return !normalizedPagePath.endsWith('/index') + ? [`${page}.${extension}`, join(page, `index.${extension}`)] + : [join(page, `index.${extension}`)] + }) + ) +} diff --git a/packages/next/shared/lib/page-path/normalize-page-path.ts b/packages/next/shared/lib/page-path/normalize-page-path.ts new file mode 100644 index 0000000000000..1df8cef5856ad --- /dev/null +++ b/packages/next/shared/lib/page-path/normalize-page-path.ts @@ -0,0 +1,31 @@ +import { ensureLeadingSlash } from './ensure-leading-slash' +import { isDynamicRoute } from '../router/utils' +import { posix } from '../isomorphic/path' + +/** + * Takes a page and transforms it into its file counterpart ensuring that the + * output is normalized. Note this function is not idempotent because a page + * `/index` can be referencing `/index/index.js` and `/index/index` could be + * referencing `/index/index/index.js`. Examples: + * - `/` -> `/index` + * - `/index/foo` -> `/index/index/foo` + * - `/index` -> `/index/index` + */ +export function normalizePagePath(page: string): string { + const normalized = ensureLeadingSlash( + /^\/index(\/|$)/.test(page) && !isDynamicRoute(page) + ? `/index${page}` + : page === '/' + ? '/index' + : page + ) + + const resolvedPage = posix.normalize(normalized) + if (resolvedPage !== normalized) { + throw new Error( + `Requested and resolved page mismatch: ${normalized} ${resolvedPage}` + ) + } + + return normalized +} diff --git a/packages/next/shared/lib/page-path/normalize-path-sep.ts b/packages/next/shared/lib/page-path/normalize-path-sep.ts new file mode 100644 index 0000000000000..a3df0473e4528 --- /dev/null +++ b/packages/next/shared/lib/page-path/normalize-path-sep.ts @@ -0,0 +1,8 @@ +/** + * For a given page path, this function ensures that there is no backslash + * escaping slashes in the path. Example: + * - `foo\/bar\/baz` -> `foo/bar/baz` + */ +export function normalizePathSep(path: string): string { + return path.replace(/\\/g, '/') +} diff --git a/packages/next/shared/lib/page-path/remove-page-path-tail.ts b/packages/next/shared/lib/page-path/remove-page-path-tail.ts new file mode 100644 index 0000000000000..3b0d94df46a19 --- /dev/null +++ b/packages/next/shared/lib/page-path/remove-page-path-tail.ts @@ -0,0 +1,19 @@ +import { normalizePathSep } from './normalize-path-sep' + +/** + * Removes the file extension for a page and the trailing `index` if it exists + * making sure to not return an empty string. The page head is not touched + * and returned as it is passed. Examples: + * - `/foo/bar/baz/index.js` -> `/foo/bar/baz` + * - `/foo/bar/baz.js` -> `/foo/bar/baz` + * + * @param pagePath A page to a page file (absolute or relative) + * @param extensions Extensions allowed for the page. + */ +export function removePagePathTail(pagePath: string, extensions: string[]) { + return ( + normalizePathSep(pagePath) + .replace(new RegExp(`\\.+(?:${extensions.join('|')})$`), '') + .replace(/\/index$/, '') || '/' + ) +} diff --git a/packages/next/shared/lib/router/router.ts b/packages/next/shared/lib/router/router.ts index a2bdea45efd97..d36d38a9ff64d 100644 --- a/packages/next/shared/lib/router/router.ts +++ b/packages/next/shared/lib/router/router.ts @@ -18,7 +18,7 @@ import { } from '../../../client/route-loader' import { handleClientScriptLoad } from '../../../client/script' import isError, { getProperError } from '../../../lib/is-error' -import { denormalizePagePath } from '../../../server/denormalize-page-path' +import { denormalizePagePath } from '../page-path/denormalize-page-path' import { normalizeLocalePath } from '../i18n/normalize-locale-path' import mitt from '../mitt' import { diff --git a/test/integration/production-swcminify/test/index.test.js b/test/integration/production-swcminify/test/index.test.js index 6fa1b211e1aba..cd4697912c1cb 100644 --- a/test/integration/production-swcminify/test/index.test.js +++ b/test/integration/production-swcminify/test/index.test.js @@ -75,12 +75,12 @@ describe.skip('Production Usage with swcMinify', () => { expect(serverTrace.version).toBe(1) expect( serverTrace.files.some((file) => - file.includes('next/dist/server/send-payload/index.js') + file.includes('next/dist/shared/lib/page-path/normalize-page-path.js') ) ).toBe(true) expect( serverTrace.files.some((file) => - file.includes('next/dist/server/normalize-page-path.js') + file.includes('next/dist/shared/lib/page-path/normalize-page-path.js') ) ).toBe(true) expect( diff --git a/test/integration/production/test/index.test.js b/test/integration/production/test/index.test.js index aa77bb1698bde..33029d2b957de 100644 --- a/test/integration/production/test/index.test.js +++ b/test/integration/production/test/index.test.js @@ -105,7 +105,7 @@ describe('Production Usage', () => { ).toBe(true) expect( serverTrace.files.some((file) => - file.includes('next/dist/server/normalize-page-path.js') + file.includes('next/dist/shared/lib/page-path/normalize-page-path.js') ) ).toBe(true) expect( diff --git a/test/unit/find-page-file.test.ts b/test/unit/find-page-file.test.ts index 7520c03f2532b..67473c7bdb8cd 100644 --- a/test/unit/find-page-file.test.ts +++ b/test/unit/find-page-file.test.ts @@ -1,6 +1,6 @@ /* eslint-env jest */ import { findPageFile } from 'next/dist/server/lib/find-page-file' -import { normalizePagePath } from 'next/dist/server/normalize-page-path' +import { normalizePagePath } from 'next/dist/shared/lib/page-path/normalize-page-path' import { join } from 'path' diff --git a/test/unit/isolated/require-page.test.ts b/test/unit/isolated/require-page.test.ts index 33c8ba1fb1b7e..9bb314c25dc67 100644 --- a/test/unit/isolated/require-page.test.ts +++ b/test/unit/isolated/require-page.test.ts @@ -1,8 +1,8 @@ /* eslint-env jest */ import { join } from 'path' -import { normalizePagePath } from 'next/dist/server/normalize-page-path' import { SERVER_DIRECTORY, CLIENT_STATIC_FILES_PATH } from 'next/constants' +import { normalizePagePath } from 'next/dist/shared/lib/page-path/normalize-page-path' import { requirePage, getPagePath,