/** * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import path from "path"; import { webpack, sources } from "next/dist/compiled/webpack/webpack"; import { CLIENT_REFERENCE_MANIFEST, SYSTEM_ENTRYPOINTS } from "../../../shared/lib/constants"; import { relative } from "path"; import { getProxiedPluginState } from "../../build-context"; import { nonNullable } from "../../../lib/non-nullable"; import { WEBPACK_LAYERS } from "../../../lib/constants"; import { normalizePagePath } from "../../../shared/lib/page-path/normalize-page-path"; const pluginState = getProxiedPluginState({ serverModuleIds: {}, edgeServerModuleIds: {}, ASYNC_CLIENT_MODULES: [] }); function getAppPathRequiredChunks(chunkGroup) { return chunkGroup.chunks.map((requiredChunk)=>{ if (SYSTEM_ENTRYPOINTS.has(requiredChunk.name || "")) { return null; } // Get the actual chunk file names from the chunk file list. // It's possible that the chunk is generated via `import()`, in // that case the chunk file name will be '[name].[contenthash]' // instead of '[name]-[chunkhash]'. return [ ...requiredChunk.files ].map((file)=>{ // It's possible that a chunk also emits CSS files, that will // be handled separatedly. if (!file.endsWith(".js")) return null; if (file.endsWith(".hot-update.js")) return null; return requiredChunk.id + ":" + file; }); }).flat().filter(nonNullable); } // Normalize the entry names to their "group names" so a page can easily track // all the manifest items it needs from parent groups by looking up the group // segments: // - app/foo/loading -> app/foo // - app/foo/page -> app/foo // - app/(group)/@named/foo/page -> app/foo // - app/(.)foo/(..)bar/loading -> app/bar function entryNameToGroupName(entryName) { let groupName = entryName.slice(0, entryName.lastIndexOf("/")).replace(/\/@[^/]+/g, "")// Remove the group with lookahead to make sure it's not interception route .replace(/\/\([^/]+\)(?=(\/|$))/g, ""); // Interception routes groupName = groupName.replace(/^.+\/\(\.\.\.\)/g, "app/").replace(/\/\(\.\)/g, "/"); // Interception routes (recursive) while(/\/[^/]+\/\(\.\.\)/.test(groupName)){ groupName = groupName.replace(/\/[^/]+\/\(\.\.\)/g, "/"); } return groupName; } function mergeManifest(manifest, manifestToMerge) { Object.assign(manifest.clientModules, manifestToMerge.clientModules); Object.assign(manifest.ssrModuleMapping, manifestToMerge.ssrModuleMapping); Object.assign(manifest.edgeSSRModuleMapping, manifestToMerge.edgeSSRModuleMapping); Object.assign(manifest.entryCSSFiles, manifestToMerge.entryCSSFiles); } const PLUGIN_NAME = "ClientReferenceManifestPlugin"; export class ClientReferenceManifestPlugin { constructor(options){ this.dev = false; this.dev = options.dev; this.appDir = options.appDir; this.appDirBase = path.dirname(this.appDir) + path.sep; this.ASYNC_CLIENT_MODULES = new Set(pluginState.ASYNC_CLIENT_MODULES); } apply(compiler) { compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation, { normalModuleFactory })=>{ compilation.dependencyFactories.set(webpack.dependencies.ModuleDependency, normalModuleFactory); compilation.dependencyTemplates.set(webpack.dependencies.ModuleDependency, new webpack.dependencies.NullDependency.Template()); compilation.hooks.processAssets.tap({ name: PLUGIN_NAME, // Have to be in the optimize stage to run after updating the CSS // asset hash via extract mini css plugin. stage: webpack.Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE_HASH }, (assets)=>this.createAsset(assets, compilation, compiler.context)); }); } createAsset(assets, compilation, context) { const manifestsPerGroup = new Map(); const manifestEntryFiles = []; compilation.chunkGroups.forEach((chunkGroup)=>{ // By default it's the shared chunkGroup (main-app) for every page. let entryName = ""; const manifest = { ssrModuleMapping: {}, edgeSSRModuleMapping: {}, clientModules: {}, entryCSSFiles: {} }; if (chunkGroup.name && /^app[\\/]/.test(chunkGroup.name)) { // Absolute path without the extension const chunkEntryName = (this.appDirBase + chunkGroup.name).replace(/[\\/]/g, path.sep); manifest.entryCSSFiles[chunkEntryName] = chunkGroup.getFiles().filter((f)=>!f.startsWith("static/css/pages/") && f.endsWith(".css")); entryName = chunkGroup.name; } const requiredChunks = getAppPathRequiredChunks(chunkGroup); const recordModule = (id, mod)=>{ var _mod_resourceResolveData; // Skip all modules from the pages folder. if (mod.layer !== WEBPACK_LAYERS.appPagesBrowser) { return; } const resource = mod.type === "css/mini-extract" ? mod._identifier.slice(mod._identifier.lastIndexOf("!") + 1) : mod.resource; if (!resource) { return; } const moduleReferences = manifest.clientModules; const moduleIdMapping = manifest.ssrModuleMapping; const edgeModuleIdMapping = manifest.edgeSSRModuleMapping; // Note that this isn't that reliable as webpack is still possible to assign // additional queries to make sure there's no conflict even using the `named` // module ID strategy. let ssrNamedModuleId = relative(context, ((_mod_resourceResolveData = mod.resourceResolveData) == null ? void 0 : _mod_resourceResolveData.path) || resource); if (!ssrNamedModuleId.startsWith(".")) ssrNamedModuleId = `./${ssrNamedModuleId.replace(/\\/g, "/")}`; const isAsyncModule = this.ASYNC_CLIENT_MODULES.has(mod.resource); // The client compiler will always use the CJS Next.js build, so here we // also add the mapping for the ESM build (Edge runtime) to consume. const esmResource = /[\\/]next[\\/]dist[\\/]/.test(resource) ? resource.replace(/[\\/]next[\\/]dist[\\/]/, "/next/dist/esm/".replace(/\//g, path.sep)) : null; function addClientReference() { const exportName = resource; manifest.clientModules[exportName] = { id, name: "*", chunks: requiredChunks, async: isAsyncModule }; if (esmResource) { const edgeExportName = esmResource; manifest.clientModules[edgeExportName] = manifest.clientModules[exportName]; } } function addSSRIdMapping() { const exportName = resource; if (typeof pluginState.serverModuleIds[ssrNamedModuleId] !== "undefined") { moduleIdMapping[id] = moduleIdMapping[id] || {}; moduleIdMapping[id]["*"] = { ...manifest.clientModules[exportName], // During SSR, we don't have external chunks to load on the server // side with our architecture of Webpack / Turbopack. We can keep // this field empty to save some bytes. chunks: [], id: pluginState.serverModuleIds[ssrNamedModuleId] }; } if (typeof pluginState.edgeServerModuleIds[ssrNamedModuleId] !== "undefined") { edgeModuleIdMapping[id] = edgeModuleIdMapping[id] || {}; edgeModuleIdMapping[id]["*"] = { ...manifest.clientModules[exportName], // During SSR, we don't have external chunks to load on the server // side with our architecture of Webpack / Turbopack. We can keep // this field empty to save some bytes. chunks: [], id: pluginState.edgeServerModuleIds[ssrNamedModuleId] }; } } addClientReference(); addSSRIdMapping(); manifest.clientModules = moduleReferences; manifest.ssrModuleMapping = moduleIdMapping; manifest.edgeSSRModuleMapping = edgeModuleIdMapping; }; // Only apply following logic to client module requests from client entry, // or if the module is marked as client module. That's because other // client modules don't need to be in the manifest at all as they're // never be referenced by the server/client boundary. // This saves a lot of bytes in the manifest. chunkGroup.chunks.forEach((chunk)=>{ const entryMods = compilation.chunkGraph.getChunkEntryModulesIterable(chunk); for (const mod of entryMods){ if (mod.layer !== WEBPACK_LAYERS.appPagesBrowser) continue; const request = mod.request; if (!request || !request.includes("next-flight-client-entry-loader.js?")) { continue; } const connections = compilation.moduleGraph.getOutgoingConnections(mod); for (const connection of connections){ const dependency = connection.dependency; if (!dependency) continue; const clientEntryMod = compilation.moduleGraph.getResolvedModule(dependency); const modId = compilation.chunkGraph.getModuleId(clientEntryMod); if (modId !== null) { recordModule(modId, clientEntryMod); } else { var _connection_module; // If this is a concatenation, register each child to the parent ID. if (((_connection_module = connection.module) == null ? void 0 : _connection_module.constructor.name) === "ConcatenatedModule") { const concatenatedMod = connection.module; const concatenatedModId = compilation.chunkGraph.getModuleId(concatenatedMod); recordModule(concatenatedModId, clientEntryMod); } } } } }); // A page's entry name can have extensions. For example, these are both valid: // - app/foo/page // - app/foo/page.page if (/\/page(\.[^/]+)?$/.test(entryName)) { manifestEntryFiles.push(entryName.replace(/\/page(\.[^/]+)?$/, "/page")); } // Special case for the root not-found page. // dev: app/not-found // prod: app/_not-found if (/^app\/_?not-found(\.[^.]+)?$/.test(entryName)) { manifestEntryFiles.push(this.dev ? "app/not-found" : "app/_not-found"); } const groupName = entryNameToGroupName(entryName); if (!manifestsPerGroup.has(groupName)) { manifestsPerGroup.set(groupName, []); } manifestsPerGroup.get(groupName).push(manifest); }); // Generate per-page manifests. for (const pageName of manifestEntryFiles){ const mergedManifest = { ssrModuleMapping: {}, edgeSSRModuleMapping: {}, clientModules: {}, entryCSSFiles: {} }; const segments = [ ...entryNameToGroupName(pageName).split("/"), "page" ]; let group = ""; for (const segment of segments){ for (const manifest of manifestsPerGroup.get(group) || []){ mergeManifest(mergedManifest, manifest); } group += (group ? "/" : "") + segment; } const json = JSON.stringify(mergedManifest); const pagePath = pageName.replace(/%5F/g, "_"); const pageBundlePath = normalizePagePath(pagePath.slice("app".length)); assets["server/app" + pageBundlePath + "_" + CLIENT_REFERENCE_MANIFEST + ".js"] = new sources.RawSource(`globalThis.__RSC_MANIFEST=(globalThis.__RSC_MANIFEST||{});globalThis.__RSC_MANIFEST[${JSON.stringify(pagePath.slice("app".length))}]=${json}`); if (pagePath === "app/not-found") { // Create a separate special manifest for the root not-found page. assets["server/app/_not-found_" + CLIENT_REFERENCE_MANIFEST + ".js"] = new sources.RawSource(`globalThis.__RSC_MANIFEST=(globalThis.__RSC_MANIFEST||{});globalThis.__RSC_MANIFEST[${JSON.stringify("/_not-found")}]=${json}`); } } pluginState.ASYNC_CLIENT_MODULES = []; } } //# sourceMappingURL=flight-manifest-plugin.js.map