| | import type { |
| | CssImports, |
| | ClientComponentImports, |
| | } from '../loaders/next-flight-client-entry-loader' |
| |
|
| | import { webpack } from 'next/dist/compiled/webpack/webpack' |
| | import { parse, stringify } from 'querystring' |
| | import path from 'path' |
| | import { sources } from 'next/dist/compiled/webpack/webpack' |
| | import { |
| | getInvalidator, |
| | getEntries, |
| | EntryTypes, |
| | getEntryKey, |
| | } from '../../../server/dev/on-demand-entry-handler' |
| | import { |
| | WEBPACK_LAYERS, |
| | WEBPACK_RESOURCE_QUERIES, |
| | } from '../../../lib/constants' |
| | import { |
| | APP_CLIENT_INTERNALS, |
| | BARREL_OPTIMIZATION_PREFIX, |
| | COMPILER_NAMES, |
| | DEFAULT_RUNTIME_WEBPACK, |
| | EDGE_RUNTIME_WEBPACK, |
| | SERVER_REFERENCE_MANIFEST, |
| | } from '../../../shared/lib/constants' |
| | import { |
| | UNDERSCORE_NOT_FOUND_ROUTE_ENTRY, |
| | UNDERSCORE_GLOBAL_ERROR_ROUTE_ENTRY, |
| | } from '../../../shared/lib/entry-constants' |
| | import { |
| | isClientComponentEntryModule, |
| | isCSSMod, |
| | regexCSS, |
| | } from '../loaders/utils' |
| | import { |
| | traverseModules, |
| | forEachEntryModule, |
| | formatBarrelOptimizedResource, |
| | getModuleReferencesInOrder, |
| | } from '../utils' |
| | import { normalizePathSep } from '../../../shared/lib/page-path/normalize-path-sep' |
| | import { getProxiedPluginState } from '../../build-context' |
| | import { PAGE_TYPES } from '../../../lib/page-types' |
| | import { getModuleBuildInfo } from '../loaders/get-module-build-info' |
| | import { getAssumedSourceType } from '../loaders/next-flight-loader' |
| | import { isAppRouteRoute } from '../../../lib/is-app-route-route' |
| | import { |
| | DEFAULT_METADATA_ROUTE_EXTENSIONS, |
| | isMetadataRouteFile, |
| | } from '../../../lib/metadata/is-metadata-route' |
| | import type { MetadataRouteLoaderOptions } from '../loaders/next-metadata-route-loader' |
| | import type { FlightActionEntryLoaderActions } from '../loaders/next-flight-action-entry-loader' |
| | import getWebpackBundler from '../../../shared/lib/get-webpack-bundler' |
| | import { isAppBuiltinPage } from '../../utils' |
| |
|
| | interface Options { |
| | dev: boolean |
| | appDir: string |
| | isEdgeServer: boolean |
| | encryptionKey: string |
| | } |
| |
|
| | const PLUGIN_NAME = 'FlightClientEntryPlugin' |
| |
|
| | type Actions = { |
| | [actionId: string]: { |
| | exportedName?: string |
| | filename?: string |
| | workers: { |
| | [name: string]: { |
| | moduleId: string | number |
| | async: boolean |
| | } |
| | } |
| | |
| | layer: { |
| | [name: string]: string |
| | } |
| | } |
| | } |
| |
|
| | type ActionIdNamePair = { id: string; exportedName?: string; filename?: string } |
| |
|
| | export type ActionManifest = { |
| | |
| | encryptionKey: string |
| | node: Actions |
| | edge: Actions |
| | } |
| |
|
| | export interface ModuleInfo { |
| | moduleId: string | number |
| | async: boolean |
| | } |
| |
|
| | const pluginState = getProxiedPluginState({ |
| | |
| | serverActions: {} as ActionManifest['node'], |
| | edgeServerActions: {} as ActionManifest['edge'], |
| |
|
| | serverActionModules: {} as { |
| | [workerName: string]: { |
| | server?: ModuleInfo |
| | client?: ModuleInfo |
| | } |
| | }, |
| |
|
| | edgeServerActionModules: {} as { |
| | [workerName: string]: { |
| | server?: ModuleInfo |
| | client?: ModuleInfo |
| | } |
| | }, |
| |
|
| | ssrModules: {} as { [ssrModuleId: string]: ModuleInfo }, |
| | edgeSsrModules: {} as { [ssrModuleId: string]: ModuleInfo }, |
| |
|
| | rscModules: {} as { [rscModuleId: string]: ModuleInfo }, |
| | edgeRscModules: {} as { [rscModuleId: string]: ModuleInfo }, |
| |
|
| | injectedClientEntries: {} as Record<string, string>, |
| | }) |
| |
|
| | const POSSIBLE_SHARED_CONVENTIONS = ['template', 'layout'] |
| | const STANDALONE_BUNDLE_CONVENTION = 'global-not-found' |
| |
|
| | function deduplicateCSSImportsForEntry(mergedCSSimports: CssImports) { |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | |
| | const sortedCSSImports = Object.entries(mergedCSSimports).sort((a, b) => { |
| | const [aPath] = a |
| | const [bPath] = b |
| |
|
| | const aDepth = aPath.split('/').length |
| | const bDepth = bPath.split('/').length |
| |
|
| | if (aDepth !== bDepth) { |
| | return aDepth - bDepth |
| | } |
| |
|
| | const aName = path.parse(aPath).name |
| | const bName = path.parse(bPath).name |
| |
|
| | const indexA = POSSIBLE_SHARED_CONVENTIONS.indexOf(aName) |
| | const indexB = POSSIBLE_SHARED_CONVENTIONS.indexOf(bName) |
| |
|
| | if (indexA === -1) return 1 |
| | if (indexB === -1) return -1 |
| | return indexA - indexB |
| | }) |
| |
|
| | const dedupedCSSImports: CssImports = {} |
| | const trackedCSSImports = new Set<string>() |
| |
|
| | for (const [entryFilePath, cssImports] of sortedCSSImports) { |
| | const entryConventionName = path.parse(entryFilePath).name |
| |
|
| | for (const cssImport of cssImports) { |
| | |
| | |
| | if ( |
| | trackedCSSImports.has(cssImport) && |
| | STANDALONE_BUNDLE_CONVENTION !== entryConventionName |
| | ) { |
| | continue |
| | } |
| |
|
| | |
| | if (POSSIBLE_SHARED_CONVENTIONS.includes(entryConventionName)) { |
| | trackedCSSImports.add(cssImport) |
| | } |
| |
|
| | if (!dedupedCSSImports[entryFilePath]) { |
| | dedupedCSSImports[entryFilePath] = [] |
| | } |
| | dedupedCSSImports[entryFilePath].push(cssImport) |
| | } |
| | } |
| |
|
| | return dedupedCSSImports |
| | } |
| |
|
| | export class FlightClientEntryPlugin { |
| | dev: boolean |
| | appDir: string |
| | projectDir: string |
| | encryptionKey: string |
| | isEdgeServer: boolean |
| | assetPrefix: string |
| | webpackRuntime: string |
| |
|
| | constructor(options: Options) { |
| | this.dev = options.dev |
| | this.appDir = options.appDir |
| | this.projectDir = path.join(options.appDir, '..') |
| | this.isEdgeServer = options.isEdgeServer |
| | this.assetPrefix = !this.dev && !this.isEdgeServer ? '../' : '' |
| | this.encryptionKey = options.encryptionKey |
| | this.webpackRuntime = this.isEdgeServer |
| | ? EDGE_RUNTIME_WEBPACK |
| | : DEFAULT_RUNTIME_WEBPACK |
| | } |
| |
|
| | apply(compiler: webpack.Compiler) { |
| | compiler.hooks.finishMake.tapPromise(PLUGIN_NAME, (compilation) => |
| | this.createClientEntries(compiler, compilation) |
| | ) |
| |
|
| | compiler.hooks.afterCompile.tap(PLUGIN_NAME, (compilation) => { |
| | const recordModule = (modId: string, mod: any) => { |
| | |
| | |
| | const modPath = mod.matchResource || mod.resourceResolveData?.path |
| | const modQuery = mod.resourceResolveData?.query || '' |
| | |
| | |
| | const modResource = modPath |
| | ? modPath.startsWith(BARREL_OPTIMIZATION_PREFIX) |
| | ? formatBarrelOptimizedResource(mod.resource, modPath) |
| | : modPath + modQuery |
| | : mod.resource |
| |
|
| | if (typeof modId !== 'undefined' && modResource) { |
| | if (mod.layer === WEBPACK_LAYERS.reactServerComponents) { |
| | const key = path |
| | .relative(compiler.context, modResource) |
| | .replace(/\/next\/dist\/esm\//, '/next/dist/') |
| |
|
| | const moduleInfo: ModuleInfo = { |
| | moduleId: modId, |
| | async: compilation.moduleGraph.isAsync(mod), |
| | } |
| |
|
| | if (this.isEdgeServer) { |
| | pluginState.edgeRscModules[key] = moduleInfo |
| | } else { |
| | pluginState.rscModules[key] = moduleInfo |
| | } |
| | } |
| | } |
| |
|
| | if (mod.layer !== WEBPACK_LAYERS.serverSideRendering) { |
| | return |
| | } |
| |
|
| | |
| | if (typeof modId !== 'undefined' && modResource) { |
| | |
| | |
| | |
| | let ssrNamedModuleId = path.relative(compiler.context, modResource) |
| |
|
| | if (!ssrNamedModuleId.startsWith('.')) { |
| | |
| | ssrNamedModuleId = `./${normalizePathSep(ssrNamedModuleId)}` |
| | } |
| |
|
| | const moduleInfo: ModuleInfo = { |
| | moduleId: modId, |
| | async: compilation.moduleGraph.isAsync(mod), |
| | } |
| |
|
| | if (this.isEdgeServer) { |
| | pluginState.edgeSsrModules[ |
| | ssrNamedModuleId.replace(/\/next\/dist\/esm\//, '/next/dist/') |
| | ] = moduleInfo |
| | } else { |
| | pluginState.ssrModules[ssrNamedModuleId] = moduleInfo |
| | } |
| | } |
| | } |
| |
|
| | traverseModules(compilation, (mod, _chunk, _chunkGroup, modId) => { |
| | if (modId) recordModule(modId, mod) |
| | }) |
| | }) |
| |
|
| | compiler.hooks.make.tap(PLUGIN_NAME, (compilation) => { |
| | compilation.hooks.processAssets.tapPromise( |
| | { |
| | name: PLUGIN_NAME, |
| | stage: webpack.Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE_HASH, |
| | }, |
| | () => this.createActionAssets(compilation) |
| | ) |
| | }) |
| | } |
| |
|
| | async createClientEntries( |
| | compiler: webpack.Compiler, |
| | compilation: webpack.Compilation |
| | ) { |
| | const addClientEntryAndSSRModulesList: Array< |
| | ReturnType<typeof this.injectClientEntryAndSSRModules> |
| | > = [] |
| | const createdSSRDependenciesForEntry: Record< |
| | string, |
| | ReturnType<typeof this.injectClientEntryAndSSRModules>[3][] |
| | > = {} |
| |
|
| | const addActionEntryList: Array<ReturnType<typeof this.injectActionEntry>> = |
| | [] |
| | const actionMapsPerEntry: Record< |
| | string, |
| | Map<string, ActionIdNamePair[]> |
| | > = {} |
| | const createdActionIds = new Set<string>() |
| |
|
| | |
| | |
| | forEachEntryModule(compilation, ({ name, entryModule }) => { |
| | const internalClientComponentEntryImports: ClientComponentImports = {} |
| | const actionEntryImports = new Map<string, ActionIdNamePair[]>() |
| | const clientEntriesToInject = [] |
| | const mergedCSSimports: CssImports = {} |
| |
|
| | const moduleReferences = getModuleReferencesInOrder( |
| | entryModule, |
| | compilation.moduleGraph |
| | ) |
| | for (const connection of moduleReferences) { |
| | |
| | let entryRequest = ( |
| | connection.dependency as unknown as webpack.NormalModule |
| | ).request |
| |
|
| | if (entryRequest.endsWith(WEBPACK_RESOURCE_QUERIES.metadataRoute)) { |
| | const { filePath, isDynamicRouteExtension } = |
| | getMetadataRouteResource(entryRequest) |
| |
|
| | if (isDynamicRouteExtension === '1') { |
| | entryRequest = filePath |
| | } |
| | } |
| |
|
| | const { clientComponentImports, actionImports, cssImports } = |
| | this.collectComponentInfoFromServerEntryDependency({ |
| | entryRequest, |
| | compilation, |
| | resolvedModule: connection.resolvedModule, |
| | }) |
| |
|
| | actionImports.forEach(([dep, actions]) => |
| | actionEntryImports.set(dep, actions) |
| | ) |
| |
|
| | const isAbsoluteRequest = path.isAbsolute(entryRequest) |
| | const isAppRouterBuiltinPage = isAppBuiltinPage(entryRequest) |
| |
|
| | |
| | if (!isAbsoluteRequest) { |
| | Object.keys(clientComponentImports).forEach( |
| | (value) => (internalClientComponentEntryImports[value] = new Set()) |
| | ) |
| | if (!isAppRouterBuiltinPage) { |
| | continue |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| |
|
| | const relativeRequest = |
| | isAbsoluteRequest && !isAppRouterBuiltinPage |
| | ? path.relative(compilation.options.context!, entryRequest) |
| | : entryRequest |
| |
|
| | |
| | |
| | |
| | let bundlePath = normalizePathSep( |
| | relativeRequest.replace(/\.[^.\\/]+$/, '').replace(/^src[\\/]/, '') |
| | ) |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | const appDirRelativeRequest = relativeRequest |
| | .replace(/^src[\\/]/, '') |
| | .replace(/^app[\\/]/, '/') |
| | const isMetadataEntryFile = isMetadataRouteFile( |
| | appDirRelativeRequest, |
| | DEFAULT_METADATA_ROUTE_EXTENSIONS, |
| | true |
| | ) |
| | if (isMetadataEntryFile) { |
| | bundlePath = name |
| | } |
| |
|
| | Object.assign(mergedCSSimports, cssImports) |
| | clientEntriesToInject.push({ |
| | compiler, |
| | compilation, |
| | entryName: name, |
| | clientComponentImports, |
| | bundlePath, |
| | absolutePagePath: entryRequest, |
| | }) |
| |
|
| | |
| | |
| | |
| | if ( |
| | name === `app${UNDERSCORE_NOT_FOUND_ROUTE_ENTRY}` && |
| | bundlePath === 'app/not-found' |
| | ) { |
| | clientEntriesToInject.push({ |
| | compiler, |
| | compilation, |
| | entryName: name, |
| | clientComponentImports: {}, |
| | bundlePath: `app${UNDERSCORE_NOT_FOUND_ROUTE_ENTRY}`, |
| | absolutePagePath: entryRequest, |
| | }) |
| | } |
| |
|
| | if ( |
| | name === `app${UNDERSCORE_NOT_FOUND_ROUTE_ENTRY}` && |
| | bundlePath === 'app/global-not-found' |
| | ) { |
| | clientEntriesToInject.push({ |
| | compiler, |
| | compilation, |
| | entryName: name, |
| | clientComponentImports, |
| | bundlePath: `app${UNDERSCORE_NOT_FOUND_ROUTE_ENTRY}`, |
| | absolutePagePath: entryRequest, |
| | }) |
| | } |
| |
|
| | if (name === `app${UNDERSCORE_GLOBAL_ERROR_ROUTE_ENTRY}`) { |
| | clientEntriesToInject.push({ |
| | compiler, |
| | compilation, |
| | entryName: name, |
| | clientComponentImports, |
| | bundlePath: `app${UNDERSCORE_GLOBAL_ERROR_ROUTE_ENTRY}`, |
| | absolutePagePath: entryRequest, |
| | }) |
| | } |
| | } |
| |
|
| | |
| | |
| | const dedupedCSSImports = deduplicateCSSImportsForEntry(mergedCSSimports) |
| | for (const clientEntryToInject of clientEntriesToInject) { |
| | const injected = this.injectClientEntryAndSSRModules({ |
| | ...clientEntryToInject, |
| | clientImports: { |
| | ...clientEntryToInject.clientComponentImports, |
| | ...( |
| | dedupedCSSImports[clientEntryToInject.absolutePagePath] || [] |
| | ).reduce<ClientComponentImports>((res, curr) => { |
| | res[curr] = new Set() |
| | return res |
| | }, {}), |
| | }, |
| | }) |
| |
|
| | |
| | if (!createdSSRDependenciesForEntry[clientEntryToInject.entryName]) { |
| | createdSSRDependenciesForEntry[clientEntryToInject.entryName] = [] |
| | } |
| | createdSSRDependenciesForEntry[clientEntryToInject.entryName].push( |
| | injected[3] |
| | ) |
| |
|
| | addClientEntryAndSSRModulesList.push(injected) |
| | } |
| |
|
| | if (!isAppRouteRoute(name)) { |
| | |
| | addClientEntryAndSSRModulesList.push( |
| | this.injectClientEntryAndSSRModules({ |
| | compiler, |
| | compilation, |
| | entryName: name, |
| | clientImports: { ...internalClientComponentEntryImports }, |
| | bundlePath: APP_CLIENT_INTERNALS, |
| | }) |
| | ) |
| | } |
| |
|
| | if (actionEntryImports.size > 0) { |
| | if (!actionMapsPerEntry[name]) { |
| | actionMapsPerEntry[name] = new Map() |
| | } |
| | actionMapsPerEntry[name] = new Map([ |
| | ...actionMapsPerEntry[name], |
| | ...actionEntryImports, |
| | ]) |
| | } |
| | }) |
| |
|
| | for (const [name, actionEntryImports] of Object.entries( |
| | actionMapsPerEntry |
| | )) { |
| | addActionEntryList.push( |
| | this.injectActionEntry({ |
| | compiler, |
| | compilation, |
| | actions: actionEntryImports, |
| | entryName: name, |
| | bundlePath: name, |
| | createdActionIds, |
| | }) |
| | ) |
| | } |
| |
|
| | |
| | const invalidator = getInvalidator(compiler.outputPath) |
| | |
| | if ( |
| | invalidator && |
| | addClientEntryAndSSRModulesList.some( |
| | ([shouldInvalidate]) => shouldInvalidate === true |
| | ) |
| | ) { |
| | invalidator.invalidate([COMPILER_NAMES.client]) |
| | } |
| |
|
| | |
| | |
| | |
| | await Promise.all( |
| | addClientEntryAndSSRModulesList.flatMap((addClientEntryAndSSRModules) => [ |
| | addClientEntryAndSSRModules[1], |
| | addClientEntryAndSSRModules[2], |
| | ]) |
| | ) |
| |
|
| | |
| | await Promise.all(addActionEntryList) |
| |
|
| | const addedClientActionEntryList: Promise<any>[] = [] |
| | const actionMapsPerClientEntry: Record< |
| | string, |
| | Map<string, ActionIdNamePair[]> |
| | > = {} |
| |
|
| | |
| | |
| | |
| | for (const [name, ssrEntryDependencies] of Object.entries( |
| | createdSSRDependenciesForEntry |
| | )) { |
| | |
| | |
| | const actionEntryImports = this.collectClientActionsFromDependencies({ |
| | compilation, |
| | dependencies: ssrEntryDependencies, |
| | }) |
| |
|
| | if (actionEntryImports.size > 0) { |
| | if (!actionMapsPerClientEntry[name]) { |
| | actionMapsPerClientEntry[name] = new Map() |
| | } |
| | actionMapsPerClientEntry[name] = new Map([ |
| | ...actionMapsPerClientEntry[name], |
| | ...actionEntryImports, |
| | ]) |
| | } |
| | } |
| |
|
| | for (const [entryName, actionEntryImports] of Object.entries( |
| | actionMapsPerClientEntry |
| | )) { |
| | |
| | |
| | |
| | |
| | let remainingClientImportedActions = false |
| | const remainingActionEntryImports = new Map<string, ActionIdNamePair[]>() |
| | for (const [dep, actions] of actionEntryImports) { |
| | const remainingActionNames = [] |
| | for (const action of actions) { |
| | if (!createdActionIds.has(entryName + '@' + action.id)) { |
| | remainingActionNames.push(action) |
| | } |
| | } |
| | if (remainingActionNames.length > 0) { |
| | remainingActionEntryImports.set(dep, remainingActionNames) |
| | remainingClientImportedActions = true |
| | } |
| | } |
| |
|
| | if (remainingClientImportedActions) { |
| | addedClientActionEntryList.push( |
| | this.injectActionEntry({ |
| | compiler, |
| | compilation, |
| | actions: remainingActionEntryImports, |
| | entryName, |
| | bundlePath: entryName, |
| | fromClient: true, |
| | createdActionIds, |
| | }) |
| | ) |
| | } |
| | } |
| |
|
| | await Promise.all(addedClientActionEntryList) |
| | } |
| |
|
| | collectClientActionsFromDependencies({ |
| | compilation, |
| | dependencies, |
| | }: { |
| | compilation: webpack.Compilation |
| | dependencies: ReturnType<typeof webpack.EntryPlugin.createDependency>[] |
| | }) { |
| | |
| | const collectedActions = new Map<string, ActionIdNamePair[]>() |
| |
|
| | |
| | const visitedModule = new Set<string>() |
| | const visitedEntry = new Set<string>() |
| |
|
| | const collectActions = ({ |
| | entryRequest, |
| | resolvedModule, |
| | }: { |
| | entryRequest: string |
| | resolvedModule: any |
| | }) => { |
| | const collectActionsInDep = (mod: webpack.NormalModule): void => { |
| | if (!mod) return |
| |
|
| | const modResource = getModuleResource(mod) |
| |
|
| | if (!modResource) return |
| |
|
| | if (visitedModule.has(modResource)) return |
| | visitedModule.add(modResource) |
| |
|
| | const actionIds = getModuleBuildInfo(mod).rsc?.actionIds |
| | if (actionIds) { |
| | collectedActions.set( |
| | modResource, |
| | Object.entries(actionIds).map(([id, exportedName]) => ({ |
| | id, |
| | exportedName, |
| | filename: path.posix.relative(this.projectDir, modResource), |
| | })) |
| | ) |
| | } |
| |
|
| | |
| | getModuleReferencesInOrder(mod, compilation.moduleGraph).forEach( |
| | (connection: any) => { |
| | collectActionsInDep( |
| | connection.resolvedModule as webpack.NormalModule |
| | ) |
| | } |
| | ) |
| | } |
| |
|
| | |
| | if ( |
| | entryRequest && |
| | !entryRequest.includes('next-flight-action-entry-loader') |
| | ) { |
| | |
| | collectActionsInDep(resolvedModule) |
| | } |
| | } |
| |
|
| | for (const entryDependency of dependencies) { |
| | const ssrEntryModule = |
| | compilation.moduleGraph.getResolvedModule(entryDependency)! |
| | for (const connection of getModuleReferencesInOrder( |
| | ssrEntryModule, |
| | compilation.moduleGraph |
| | )) { |
| | const depModule = connection.dependency |
| | const request = (depModule as unknown as webpack.NormalModule).request |
| |
|
| | |
| | |
| | if (visitedEntry.has(request)) continue |
| | visitedEntry.add(request) |
| |
|
| | collectActions({ |
| | entryRequest: request, |
| | resolvedModule: connection.resolvedModule, |
| | }) |
| | } |
| | } |
| |
|
| | return collectedActions |
| | } |
| |
|
| | collectComponentInfoFromServerEntryDependency({ |
| | entryRequest, |
| | compilation, |
| | resolvedModule, |
| | }: { |
| | entryRequest: string |
| | compilation: webpack.Compilation |
| | resolvedModule: any |
| | }): { |
| | cssImports: CssImports |
| | clientComponentImports: ClientComponentImports |
| | actionImports: [string, ActionIdNamePair[]][] |
| | } { |
| | |
| | const visitedOfClientComponentsTraverse = new Set() |
| |
|
| | |
| | const clientComponentImports: ClientComponentImports = {} |
| | const actionImports: [string, ActionIdNamePair[]][] = [] |
| | const CSSImports = new Set<string>() |
| |
|
| | const filterClientComponents = ( |
| | mod: webpack.NormalModule, |
| | importedIdentifiers: string[] |
| | ): void => { |
| | if (!mod) return |
| |
|
| | const modResource = getModuleResource(mod) |
| |
|
| | if (!modResource) return |
| | if (visitedOfClientComponentsTraverse.has(modResource)) { |
| | if (clientComponentImports[modResource]) { |
| | addClientImport( |
| | mod, |
| | modResource, |
| | clientComponentImports, |
| | importedIdentifiers, |
| | false |
| | ) |
| | } |
| | return |
| | } |
| | visitedOfClientComponentsTraverse.add(modResource) |
| |
|
| | const actionIds = getModuleBuildInfo(mod).rsc?.actionIds |
| | if (actionIds) { |
| | actionImports.push([ |
| | modResource, |
| | Object.entries(actionIds).map(([id, exportedName]) => ({ |
| | id, |
| | exportedName, |
| | filename: path.posix.relative(this.projectDir, modResource), |
| | })), |
| | ]) |
| | } |
| |
|
| | if (isCSSMod(mod)) { |
| | const sideEffectFree = |
| | mod.factoryMeta && (mod.factoryMeta as any).sideEffectFree |
| |
|
| | if (sideEffectFree) { |
| | const unused = !compilation.moduleGraph |
| | .getExportsInfo(mod) |
| | .isModuleUsed(this.webpackRuntime) |
| |
|
| | if (unused) return |
| | } |
| |
|
| | CSSImports.add(modResource) |
| | } else if (isClientComponentEntryModule(mod)) { |
| | if (!clientComponentImports[modResource]) { |
| | clientComponentImports[modResource] = new Set() |
| | } |
| | addClientImport( |
| | mod, |
| | modResource, |
| | clientComponentImports, |
| | importedIdentifiers, |
| | true |
| | ) |
| |
|
| | return |
| | } |
| |
|
| | getModuleReferencesInOrder(mod, compilation.moduleGraph).forEach( |
| | (connection: any) => { |
| | let dependencyIds: string[] = [] |
| |
|
| | |
| | |
| | if (connection.dependency?.ids) { |
| | dependencyIds.push(...connection.dependency.ids) |
| | } else { |
| | dependencyIds = ['*'] |
| | } |
| |
|
| | filterClientComponents(connection.resolvedModule, dependencyIds) |
| | } |
| | ) |
| | } |
| |
|
| | |
| | filterClientComponents(resolvedModule, []) |
| |
|
| | return { |
| | clientComponentImports, |
| | cssImports: CSSImports.size |
| | ? { |
| | [entryRequest]: Array.from(CSSImports), |
| | } |
| | : {}, |
| | actionImports, |
| | } |
| | } |
| |
|
| | injectClientEntryAndSSRModules({ |
| | compiler, |
| | compilation, |
| | entryName, |
| | clientImports, |
| | bundlePath, |
| | absolutePagePath, |
| | }: { |
| | compiler: webpack.Compiler |
| | compilation: webpack.Compilation |
| | entryName: string |
| | clientImports: ClientComponentImports |
| | bundlePath: string |
| | absolutePagePath?: string |
| | }): [ |
| | shouldInvalidate: boolean, |
| | addSSREntryPromise: Promise<void>, |
| | addRSCEntryPromise: Promise<void>, |
| | ssrDep: ReturnType<typeof webpack.EntryPlugin.createDependency>, |
| | ] { |
| | const bundler = getWebpackBundler() |
| | let shouldInvalidate = false |
| |
|
| | const modules = Object.keys(clientImports) |
| | .sort((a, b) => (regexCSS.test(b) ? 1 : a.localeCompare(b))) |
| | .map((clientImportPath) => ({ |
| | request: clientImportPath, |
| | ids: [...clientImports[clientImportPath]], |
| | })) |
| |
|
| | |
| | |
| | |
| | const clientBrowserLoader = `next-flight-client-entry-loader?${stringify({ |
| | modules: (this.isEdgeServer |
| | ? modules.map(({ request, ids }) => ({ |
| | request: request.replace( |
| | /[\\/]next[\\/]dist[\\/]esm[\\/]/, |
| | '/next/dist/'.replace(/\//g, path.sep) |
| | ), |
| | ids, |
| | })) |
| | : modules |
| | ).map((x) => JSON.stringify(x)), |
| | server: false, |
| | })}!` |
| |
|
| | const clientServerLoader = `next-flight-client-entry-loader?${stringify({ |
| | modules: modules.map((x) => JSON.stringify(x)), |
| | server: true, |
| | })}!` |
| |
|
| | |
| | |
| | if (this.dev) { |
| | const entries = getEntries(compiler.outputPath) |
| | const pageKey = getEntryKey( |
| | COMPILER_NAMES.client, |
| | PAGE_TYPES.APP, |
| | bundlePath |
| | ) |
| |
|
| | if (!entries[pageKey]) { |
| | entries[pageKey] = { |
| | type: EntryTypes.CHILD_ENTRY, |
| | parentEntries: new Set([entryName]), |
| | absoluteEntryFilePath: absolutePagePath, |
| | bundlePath, |
| | request: clientBrowserLoader, |
| | dispose: false, |
| | lastActiveTime: Date.now(), |
| | } |
| | shouldInvalidate = true |
| | } else { |
| | const entryData = entries[pageKey] |
| | |
| | if (entryData.request !== clientBrowserLoader) { |
| | entryData.request = clientBrowserLoader |
| | shouldInvalidate = true |
| | } |
| | if (entryData.type === EntryTypes.CHILD_ENTRY) { |
| | entryData.parentEntries.add(entryName) |
| | } |
| | entryData.dispose = false |
| | entryData.lastActiveTime = Date.now() |
| | } |
| | } else { |
| | pluginState.injectedClientEntries[bundlePath] = clientBrowserLoader |
| | } |
| |
|
| | const clientComponentSSREntryDep = bundler.EntryPlugin.createDependency( |
| | clientServerLoader, |
| | { name: bundlePath } |
| | ) |
| |
|
| | const clientComponentRSCEntryDep = bundler.EntryPlugin.createDependency( |
| | clientServerLoader, |
| | { name: bundlePath } |
| | ) |
| |
|
| | return [ |
| | shouldInvalidate, |
| | |
| | |
| | |
| | this.addEntry(compilation, compiler.context, clientComponentSSREntryDep, { |
| | name: entryName, |
| | layer: WEBPACK_LAYERS.serverSideRendering, |
| | }), |
| | this.addEntry(compilation, compiler.context, clientComponentRSCEntryDep, { |
| | name: entryName, |
| | layer: WEBPACK_LAYERS.reactServerComponents, |
| | }), |
| | clientComponentSSREntryDep, |
| | ] |
| | } |
| |
|
| | injectActionEntry({ |
| | compiler, |
| | compilation, |
| | actions, |
| | entryName, |
| | bundlePath, |
| | fromClient, |
| | createdActionIds, |
| | }: { |
| | compiler: webpack.Compiler |
| | compilation: webpack.Compilation |
| | actions: Map<string, ActionIdNamePair[]> |
| | entryName: string |
| | bundlePath: string |
| | createdActionIds: Set<string> |
| | fromClient?: boolean |
| | }) { |
| | const bundler = getWebpackBundler() |
| | const actionsArray = Array.from(actions.entries()) |
| | for (const [, actionsFromModule] of actions) { |
| | for (const { id } of actionsFromModule) { |
| | createdActionIds.add(entryName + '@' + id) |
| | } |
| | } |
| |
|
| | if (actionsArray.length === 0) { |
| | return Promise.resolve() |
| | } |
| |
|
| | const actionLoader = `next-flight-action-entry-loader?${stringify({ |
| | actions: JSON.stringify( |
| | actionsArray satisfies FlightActionEntryLoaderActions |
| | ), |
| | __client_imported__: fromClient, |
| | })}!` |
| |
|
| | const currentCompilerServerActions = this.isEdgeServer |
| | ? pluginState.edgeServerActions |
| | : pluginState.serverActions |
| |
|
| | for (const [, actionsFromModule] of actionsArray) { |
| | for (const { id, exportedName, filename } of actionsFromModule) { |
| | if (typeof currentCompilerServerActions[id] === 'undefined') { |
| | currentCompilerServerActions[id] = { |
| | workers: {}, |
| | layer: {}, |
| | filename, |
| | exportedName, |
| | } |
| | } |
| | currentCompilerServerActions[id].workers[bundlePath] = { |
| | moduleId: '', |
| | async: false, |
| | } |
| |
|
| | currentCompilerServerActions[id].layer[bundlePath] = fromClient |
| | ? WEBPACK_LAYERS.actionBrowser |
| | : WEBPACK_LAYERS.reactServerComponents |
| | } |
| | } |
| |
|
| | |
| | const actionEntryDep = bundler.EntryPlugin.createDependency(actionLoader, { |
| | name: bundlePath, |
| | }) |
| |
|
| | return this.addEntry( |
| | compilation, |
| | |
| | compiler.context, |
| | actionEntryDep, |
| | { |
| | name: entryName, |
| | layer: fromClient |
| | ? WEBPACK_LAYERS.actionBrowser |
| | : WEBPACK_LAYERS.reactServerComponents, |
| | } |
| | ) |
| | } |
| |
|
| | addEntry( |
| | compilation: webpack.Compilation, |
| | context: string, |
| | dependency: webpack.Dependency, |
| | options: webpack.EntryOptions |
| | ): Promise<any> { |
| | return new Promise((resolve, reject) => { |
| | if ('rspack' in compilation.compiler) { |
| | compilation.addInclude(context, dependency, options, (err, module) => { |
| | if (err) { |
| | return reject(err) |
| | } |
| |
|
| | compilation.moduleGraph |
| | .getExportsInfo(module!) |
| | .setUsedInUnknownWay( |
| | this.isEdgeServer ? EDGE_RUNTIME_WEBPACK : DEFAULT_RUNTIME_WEBPACK |
| | ) |
| | return resolve(module) |
| | }) |
| | } else { |
| | const entry = compilation.entries.get(options.name!)! |
| | entry.includeDependencies.push(dependency) |
| | compilation.hooks.addEntry.call(entry as any, options) |
| | compilation.addModuleTree( |
| | { |
| | context, |
| | dependency, |
| | contextInfo: { issuerLayer: options.layer }, |
| | }, |
| | (err: any, module: any) => { |
| | if (err) { |
| | compilation.hooks.failedEntry.call(dependency, options, err) |
| | return reject(err) |
| | } |
| |
|
| | compilation.hooks.succeedEntry.call(dependency, options, module) |
| |
|
| | compilation.moduleGraph |
| | .getExportsInfo(module) |
| | .setUsedInUnknownWay( |
| | this.isEdgeServer |
| | ? EDGE_RUNTIME_WEBPACK |
| | : DEFAULT_RUNTIME_WEBPACK |
| | ) |
| |
|
| | return resolve(module) |
| | } |
| | ) |
| | } |
| | }) |
| | } |
| |
|
| | async createActionAssets(compilation: webpack.Compilation) { |
| | const serverActions: ActionManifest['node'] = {} |
| | const edgeServerActions: ActionManifest['edge'] = {} |
| |
|
| | traverseModules(compilation, (mod, _chunk, chunkGroup, modId) => { |
| | |
| | if ( |
| | chunkGroup.name && |
| | mod.request && |
| | modId && |
| | /next-flight-action-entry-loader/.test(mod.request) |
| | ) { |
| | const fromClient = /&__client_imported__=true/.test(mod.request) |
| |
|
| | const mapping = this.isEdgeServer |
| | ? pluginState.edgeServerActionModules |
| | : pluginState.serverActionModules |
| |
|
| | if (!mapping[chunkGroup.name]) { |
| | mapping[chunkGroup.name] = {} |
| | } |
| |
|
| | mapping[chunkGroup.name][fromClient ? 'client' : 'server'] = { |
| | moduleId: modId, |
| | async: compilation.moduleGraph.isAsync(mod), |
| | } |
| | } |
| | }) |
| |
|
| | for (let id in pluginState.serverActions) { |
| | const action = pluginState.serverActions[id] |
| | for (let name in action.workers) { |
| | const modId = |
| | pluginState.serverActionModules[name][ |
| | action.layer[name] === WEBPACK_LAYERS.actionBrowser |
| | ? 'client' |
| | : 'server' |
| | ] |
| | action.workers[name] = modId! |
| | } |
| | serverActions[id] = action |
| | } |
| |
|
| | for (let id in pluginState.edgeServerActions) { |
| | const action = pluginState.edgeServerActions[id] |
| | for (let name in action.workers) { |
| | const modId = |
| | pluginState.edgeServerActionModules[name][ |
| | action.layer[name] === WEBPACK_LAYERS.actionBrowser |
| | ? 'client' |
| | : 'server' |
| | ] |
| | action.workers[name] = modId! |
| | } |
| | edgeServerActions[id] = action |
| | } |
| |
|
| | const serverManifest = { |
| | node: serverActions, |
| | edge: edgeServerActions, |
| | encryptionKey: this.encryptionKey, |
| | } |
| | const edgeServerManifest = { |
| | ...serverManifest, |
| | encryptionKey: 'process.env.NEXT_SERVER_ACTIONS_ENCRYPTION_KEY', |
| | } |
| |
|
| | const json = JSON.stringify(serverManifest, null, this.dev ? 2 : undefined) |
| | const edgeJson = JSON.stringify( |
| | edgeServerManifest, |
| | null, |
| | this.dev ? 2 : undefined |
| | ) |
| |
|
| | compilation.emitAsset( |
| | `${this.assetPrefix}${SERVER_REFERENCE_MANIFEST}.js`, |
| | new sources.RawSource( |
| | `self.__RSC_SERVER_MANIFEST=${JSON.stringify(edgeJson)}` |
| | ) as unknown as webpack.sources.RawSource |
| | ) |
| | compilation.emitAsset( |
| | `${this.assetPrefix}${SERVER_REFERENCE_MANIFEST}.json`, |
| | new sources.RawSource(json) as unknown as webpack.sources.RawSource |
| | ) |
| | } |
| | } |
| |
|
| | function addClientImport( |
| | mod: webpack.NormalModule, |
| | modRequest: string, |
| | clientComponentImports: ClientComponentImports, |
| | importedIdentifiers: string[], |
| | isFirstVisitModule: boolean |
| | ) { |
| | const clientEntryType = getModuleBuildInfo(mod).rsc?.clientEntryType |
| | const isCjsModule = clientEntryType === 'cjs' |
| | const assumedSourceType = getAssumedSourceType( |
| | mod, |
| | isCjsModule ? 'commonjs' : 'auto' |
| | ) |
| |
|
| | const clientImportsSet = clientComponentImports[modRequest] |
| |
|
| | if (importedIdentifiers[0] === '*') { |
| | |
| | |
| | |
| | if (!isFirstVisitModule && [...clientImportsSet][0] !== '*') { |
| | clientComponentImports[modRequest] = new Set(['*']) |
| | } |
| | } else { |
| | const isAutoModuleSourceType = assumedSourceType === 'auto' |
| | if (isAutoModuleSourceType) { |
| | clientComponentImports[modRequest] = new Set(['*']) |
| | } else { |
| | |
| | |
| | for (const name of importedIdentifiers) { |
| | |
| | const isCjsDefaultImport = isCjsModule && name === 'default' |
| |
|
| | |
| | |
| | if (isCjsDefaultImport) { |
| | clientComponentImports[modRequest].add('__esModule') |
| | } |
| |
|
| | clientComponentImports[modRequest].add(name) |
| | } |
| | } |
| | } |
| | } |
| |
|
| | function getModuleResource(mod: webpack.NormalModule): string { |
| | const modPath: string = mod.resourceResolveData?.path || '' |
| | const modQuery = mod.resourceResolveData?.query || '' |
| | |
| | |
| | |
| | let modResource: string = modPath + modQuery |
| |
|
| | |
| | if (mod.constructor.name === 'ContextModule') { |
| | modResource = mod.identifier() |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | if (mod.matchResource?.startsWith(BARREL_OPTIMIZATION_PREFIX)) { |
| | modResource = mod.matchResource + ':' + modResource |
| | } |
| |
|
| | if (mod.resource === `?${WEBPACK_RESOURCE_QUERIES.metadataRoute}`) { |
| | return getMetadataRouteResource(mod.rawRequest).filePath |
| | } |
| |
|
| | return modResource |
| | } |
| |
|
| | function getMetadataRouteResource(request: string): MetadataRouteLoaderOptions { |
| | |
| | const query = request.split('!')[0].split('next-metadata-route-loader?')[1] |
| |
|
| | return parse(query) as MetadataRouteLoaderOptions |
| | } |
| |
|