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, }) },