From 847dfc26653bcf64532acbdff96ad8fddca65ca8 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Fri, 19 May 2023 08:45:18 -0600 Subject: [PATCH] Pages Module Transition (#49962) This serves to create the loader that will allow us to transition more of the rendering logic into each of the bundled entrypoints. This only applies to pages routes inside the `pages/` folder in the node environment. fix NEXT-985 --------- Co-authored-by: JJ Kasper --- packages/next/src/build/entries.ts | 38 ++++++---- packages/next/src/build/webpack-config.ts | 1 + .../webpack/loaders/next-route-loader.ts | 64 +++++++++++++++++ .../plugins/next-trace-entrypoints-plugin.ts | 70 +++++++++--------- .../parseNotFoundError.ts | 2 +- .../error-overlay/format-webpack-messages.ts | 2 +- packages/next/src/lib/is-internal-pathname.ts | 3 + packages/next/src/server/dev/hot-reloader.ts | 71 ++++++++++++------- 8 files changed, 176 insertions(+), 75 deletions(-) create mode 100644 packages/next/src/build/webpack/loaders/next-route-loader.ts create mode 100644 packages/next/src/lib/is-internal-pathname.ts diff --git a/packages/next/src/build/entries.ts b/packages/next/src/build/entries.ts index 2c25c58cc547..1be15aab1373 100644 --- a/packages/next/src/build/entries.ts +++ b/packages/next/src/build/entries.ts @@ -52,6 +52,8 @@ import { EdgeFunctionLoaderOptions } from './webpack/loaders/next-edge-function- import { isAppRouteRoute } from '../lib/is-app-route-route' import { normalizeMetadataRoute } from '../lib/metadata/get-metadata-route' import { fileExists } from '../lib/file-exists' +import { getRouteLoaderEntry } from './webpack/loaders/next-route-loader' +import { isInternalPathname } from '../lib/is-internal-pathname' export async function getStaticInfoIncludingLayouts({ isInsideAppDir, @@ -538,7 +540,7 @@ export async function createEntrypoints( // server compiler inject them instead. } else { client[clientBundlePath] = getClientEntry({ - absolutePagePath: mappings[page], + absolutePagePath, page, }) } @@ -549,7 +551,7 @@ export async function createEntrypoints( server[serverBundlePath] = getAppEntry({ page, name: serverBundlePath, - pagePath: mappings[page], + pagePath: absolutePagePath, appDir, appPaths: matchedAppPaths, pageExtensions, @@ -558,16 +560,26 @@ export async function createEntrypoints( nextConfigOutput: config.output, preferredRegion: staticInfo.preferredRegion, }) - } else { - if (isInstrumentationHookFile(page) && pagesType === 'root') { - server[serverBundlePath.replace('src/', '')] = { - import: mappings[page], - // the '../' is needed to make sure the file is not chunked - filename: `../${INSTRUMENTATION_HOOK_FILENAME}.js`, - } - } else { - server[serverBundlePath] = [mappings[page]] + } else if (isInstrumentationHookFile(page) && pagesType === 'root') { + server[serverBundlePath.replace('src/', '')] = { + import: absolutePagePath, + // the '../' is needed to make sure the file is not chunked + filename: `../${INSTRUMENTATION_HOOK_FILENAME}.js`, } + } else if ( + !isAPIRoute(page) && + !isMiddlewareFile(page) && + !isInternalPathname(absolutePagePath) + ) { + server[serverBundlePath] = [ + getRouteLoaderEntry({ + page, + absolutePagePath, + preferredRegion: staticInfo.preferredRegion, + }), + ] + } else { + server[serverBundlePath] = [absolutePagePath] } }, onEdgeServer: () => { @@ -577,7 +589,7 @@ export async function createEntrypoints( appDirLoader = getAppEntry({ name: serverBundlePath, page, - pagePath: mappings[page], + pagePath: absolutePagePath, appDir: appDir!, appPaths: matchedAppPaths, pageExtensions, @@ -596,7 +608,7 @@ export async function createEntrypoints( edgeServer[normalizedServerBundlePath] = getEdgeServerEntry({ ...params, rootDir, - absolutePagePath: mappings[page], + absolutePagePath: absolutePagePath, bundlePath: clientBundlePath, isDev: false, isServerComponent, diff --git a/packages/next/src/build/webpack-config.ts b/packages/next/src/build/webpack-config.ts index cdd5c4327990..d1291eaea505 100644 --- a/packages/next/src/build/webpack-config.ts +++ b/packages/next/src/build/webpack-config.ts @@ -1781,6 +1781,7 @@ export default async function getBaseWebpackConfig( 'next-middleware-asset-loader', 'next-middleware-wasm-loader', 'next-app-loader', + 'next-route-loader', 'next-font-loader', 'next-invalid-import-error-loader', 'next-metadata-route-loader', diff --git a/packages/next/src/build/webpack/loaders/next-route-loader.ts b/packages/next/src/build/webpack/loaders/next-route-loader.ts new file mode 100644 index 000000000000..31927aa52658 --- /dev/null +++ b/packages/next/src/build/webpack/loaders/next-route-loader.ts @@ -0,0 +1,64 @@ +import type { webpack } from 'next/dist/compiled/webpack/webpack' + +import { stringify } from 'querystring' +import { getModuleBuildInfo } from './get-module-build-info' + +/** + * The options for the route loader. + */ +type RouteLoaderOptions = { + /** + * The page name for this particular route. + */ + page: string + + /** + * The preferred region for this route. + */ + preferredRegion: string | string[] | undefined + + /** + * The absolute path to the userland page file. + */ + absolutePagePath: string +} + +/** + * Returns the loader entry for a given page. + * + * @param query the options to create the loader entry + * @returns the encoded loader entry + */ +export function getRouteLoaderEntry(query: RouteLoaderOptions): string { + return `next-route-loader?${stringify(query)}!` +} + +/** + * Handles the `next-route-loader` options. + * @returns the loader definition function + */ +const loader: webpack.LoaderDefinitionFunction = + function () { + const { page, preferredRegion, absolutePagePath } = this.getOptions() + + // Ensure we only run this loader for as a module. + if (!this._module) { + throw new Error('Invariant: expected this to reference a module') + } + + // Attach build info to the module. + const buildInfo = getModuleBuildInfo(this._module) + buildInfo.route = { + page, + absolutePagePath, + preferredRegion, + } + + return ` + // Next.js Route Loader + export * from ${JSON.stringify(absolutePagePath)} + export { default } from ${JSON.stringify(absolutePagePath)} + ` + } + +export default loader diff --git a/packages/next/src/build/webpack/plugins/next-trace-entrypoints-plugin.ts b/packages/next/src/build/webpack/plugins/next-trace-entrypoints-plugin.ts index 76dbcaa3affd..a95e94f7e3d7 100644 --- a/packages/next/src/build/webpack/plugins/next-trace-entrypoints-plugin.ts +++ b/packages/next/src/build/webpack/plugins/next-trace-entrypoints-plugin.ts @@ -285,20 +285,24 @@ export class TraceEntryPointsPlugin implements webpack.WebpackPluginInstance { // don't include the entry itself in the trace entryFiles.delete(nodePath.join(outputPath, `../${entrypoint.name}.js`)) + const finalFiles: string[] = [] + + for (const file of new Set([ + ...entryFiles, + ...allEntryFiles, + ...(this.entryTraces.get(entrypoint.name) || []), + ])) { + if (file) { + finalFiles.push( + nodePath.relative(traceOutputPath, file).replace(/\\/g, '/') + ) + } + } + assets[traceOutputName] = new sources.RawSource( JSON.stringify({ version: TRACE_OUTPUT_VERSION, - files: [ - ...new Set([ - ...entryFiles, - ...allEntryFiles, - ...(this.entryTraces.get(entrypoint.name) || []), - ]), - ].map((file) => { - return nodePath - .relative(traceOutputPath, file) - .replace(/\\/g, '/') - }), + files: finalFiles, }) ) } @@ -369,42 +373,35 @@ export class TraceEntryPointsPlugin implements webpack.WebpackPluginInstance { entryModMap.set(absolutePath, entryMod) entryNameMap.set(absolutePath, name) } - } else { - // If there was no `route` property, we can assume that it was something custom instead. - // In order to trace these we add them to the additionalEntries map. - if (entryMod.request) { - let curMap = additionalEntries.get(name) - - if (!curMap) { - curMap = new Map() - additionalEntries.set(name, curMap) - } - depModMap.set(entryMod.request, entryMod) - curMap.set(entryMod.resource, entryMod) - } } - } - - if (entryMod && entryMod.resource) { - const normalizedResource = entryMod.resource.replace( - /\\/g, - '/' - ) - if (normalizedResource.includes('pages/')) { - entryNameMap.set(entryMod.resource, name) - entryModMap.set(entryMod.resource, entryMod) - } else { + // If there was no `route` property, we can assume that it was something custom instead. + // In order to trace these we add them to the additionalEntries map. + if (entryMod.request) { let curMap = additionalEntries.get(name) if (!curMap) { curMap = new Map() additionalEntries.set(name, curMap) } - depModMap.set(entryMod.resource, entryMod) + depModMap.set(entryMod.request, entryMod) curMap.set(entryMod.resource, entryMod) } } + + if (entryMod && entryMod.resource) { + entryNameMap.set(entryMod.resource, name) + entryModMap.set(entryMod.resource, entryMod) + + let curMap = additionalEntries.get(name) + + if (!curMap) { + curMap = new Map() + additionalEntries.set(name, curMap) + } + depModMap.set(entryMod.resource, entryMod) + curMap.set(entryMod.resource, entryMod) + } } } }) @@ -545,6 +542,7 @@ export class TraceEntryPointsPlugin implements webpack.WebpackPluginInstance { this.tracingRoot, entry ) + const curExtraEntries = additionalEntries.get(entryName) const finalDeps = new Set() diff --git a/packages/next/src/build/webpack/plugins/wellknown-errors-plugin/parseNotFoundError.ts b/packages/next/src/build/webpack/plugins/wellknown-errors-plugin/parseNotFoundError.ts index 99ee9ce189fe..20e7e0b7002d 100644 --- a/packages/next/src/build/webpack/plugins/wellknown-errors-plugin/parseNotFoundError.ts +++ b/packages/next/src/build/webpack/plugins/wellknown-errors-plugin/parseNotFoundError.ts @@ -136,7 +136,7 @@ export async function getNotFoundError( .filter( (name) => name && - !/next-(app|middleware|client-pages|flight-(client|server|client-entry))-loader\.js/.test( + !/next-(app|middleware|client-pages|route|flight-(client|server|client-entry))-loader\.js/.test( name ) && !/css-loader.+\.js/.test(name) diff --git a/packages/next/src/client/dev/error-overlay/format-webpack-messages.ts b/packages/next/src/client/dev/error-overlay/format-webpack-messages.ts index 618bfb2af4d0..190479963ea3 100644 --- a/packages/next/src/client/dev/error-overlay/format-webpack-messages.ts +++ b/packages/next/src/client/dev/error-overlay/format-webpack-messages.ts @@ -48,7 +48,7 @@ function formatMessage( message.moduleTrace && message.moduleTrace.filter( (trace: any) => - !/next-(middleware|client-pages|edge-function)-loader\.js/.test( + !/next-(middleware|client-pages|route|edge-function)-loader\.js/.test( trace.originName ) ) diff --git a/packages/next/src/lib/is-internal-pathname.ts b/packages/next/src/lib/is-internal-pathname.ts new file mode 100644 index 000000000000..b3f114e15f84 --- /dev/null +++ b/packages/next/src/lib/is-internal-pathname.ts @@ -0,0 +1,3 @@ +export function isInternalPathname(pathname: string): boolean { + return pathname.startsWith('next/dist/pages/') +} diff --git a/packages/next/src/server/dev/hot-reloader.ts b/packages/next/src/server/dev/hot-reloader.ts index c4168f943033..4a545d9327bc 100644 --- a/packages/next/src/server/dev/hot-reloader.ts +++ b/packages/next/src/server/dev/hot-reloader.ts @@ -42,7 +42,11 @@ import { denormalizePagePath } from '../../shared/lib/page-path/denormalize-page import { normalizePathSep } from '../../shared/lib/page-path/normalize-path-sep' import getRouteFromEntrypoint from '../get-route-from-entrypoint' import { fileExists } from '../../lib/file-exists' -import { difference, isMiddlewareFilename } from '../../build/utils' +import { + difference, + isMiddlewareFile, + isMiddlewareFilename, +} from '../../build/utils' import { DecodeError } from '../../shared/lib/utils' import { Span, trace } from '../../trace' import { getProperError } from '../../lib/is-error' @@ -53,6 +57,9 @@ import { getRegistry } from '../../lib/helpers/get-registry' import { RouteMatch } from '../future/route-matches/route-match' import type { Telemetry } from '../../telemetry/storage' import { parseVersionInfo, VersionInfo } from './parse-version-info' +import { isAPIRoute } from '../../lib/is-api-route' +import { getRouteLoaderEntry } from '../../build/webpack/loaders/next-route-loader' +import { isInternalPathname } from '../../lib/is-internal-pathname' function diff(a: Set, b: Set) { return new Set([...a].filter((v) => !b.has(v))) @@ -820,33 +827,49 @@ export default class HotReloader { ) { relativeRequest = `./${relativeRequest}` } + + let value: { import: string; layer?: string } | string + if (isAppPath) { + value = getAppEntry({ + name: bundlePath, + page, + appPaths: entryData.appPaths, + pagePath: posix.join( + APP_DIR_ALIAS, + relative( + this.appDir!, + entryData.absolutePagePath + ).replace(/\\/g, '/') + ), + appDir: this.appDir!, + pageExtensions: this.config.pageExtensions, + rootDir: this.dir, + isDev: true, + tsconfigPath: this.config.typescript.tsconfigPath, + basePath: this.config.basePath, + assetPrefix: this.config.assetPrefix, + nextConfigOutput: this.config.output, + preferredRegion: staticInfo.preferredRegion, + }) + } else if ( + !isAPIRoute(page) && + !isMiddlewareFile(page) && + !isInternalPathname(relativeRequest) + ) { + value = getRouteLoaderEntry({ + page, + absolutePagePath: relativeRequest, + preferredRegion: staticInfo.preferredRegion, + }) + } else { + value = relativeRequest + } + entrypoints[bundlePath] = finalizeEntrypoint({ compilerType: COMPILER_NAMES.server, name: bundlePath, isServerComponent, - value: isAppPath - ? getAppEntry({ - name: bundlePath, - page, - appPaths: entryData.appPaths, - pagePath: posix.join( - APP_DIR_ALIAS, - relative( - this.appDir!, - entryData.absolutePagePath - ).replace(/\\/g, '/') - ), - appDir: this.appDir!, - pageExtensions: this.config.pageExtensions, - rootDir: this.dir, - isDev: true, - tsconfigPath: this.config.typescript.tsconfigPath, - basePath: this.config.basePath, - assetPrefix: this.config.assetPrefix, - nextConfigOutput: this.config.output, - preferredRegion: staticInfo.preferredRegion, - }) - : relativeRequest, + value, hasAppDir, }) },