including-modules
This commit is contained in:
+779
@@ -0,0 +1,779 @@
|
||||
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 getWebpackBundler from '../../../shared/lib/get-webpack-bundler';
|
||||
import { isAppBuiltinPage } from '../../utils';
|
||||
const PLUGIN_NAME = 'FlightClientEntryPlugin';
|
||||
const pluginState = getProxiedPluginState({
|
||||
// A map to track "action" -> "list of bundles".
|
||||
serverActions: {},
|
||||
edgeServerActions: {},
|
||||
serverActionModules: {},
|
||||
edgeServerActionModules: {},
|
||||
ssrModules: {},
|
||||
edgeSsrModules: {},
|
||||
rscModules: {},
|
||||
edgeRscModules: {},
|
||||
injectedClientEntries: {}
|
||||
});
|
||||
const POSSIBLE_SHARED_CONVENTIONS = [
|
||||
'template',
|
||||
'layout'
|
||||
];
|
||||
const STANDALONE_BUNDLE_CONVENTION = 'global-not-found';
|
||||
function deduplicateCSSImportsForEntry(mergedCSSimports) {
|
||||
// If multiple entry module connections are having the same CSS import,
|
||||
// we only need to have one module to keep track of that CSS import.
|
||||
// It is based on the fact that if a page or a layout is rendered in the
|
||||
// given entry, all its parent layouts are always rendered too.
|
||||
// This can avoid duplicate CSS imports in the generated CSS manifest,
|
||||
// for example, if a page and its parent layout are both using the same
|
||||
// CSS import, we only need to have the layout to keep track of that CSS
|
||||
// import.
|
||||
// To achieve this, we need to first collect all the CSS imports from
|
||||
// every connection, and deduplicate them in the order of layers from
|
||||
// top to bottom. The implementation can be generally described as:
|
||||
// - Sort by number of `/` in the request path (the more `/`, the deeper)
|
||||
// - When in the same depth, sort by the filename (template < layout < page and others)
|
||||
// Sort the connections as described above.
|
||||
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 = {};
|
||||
const trackedCSSImports = new Set();
|
||||
for (const [entryFilePath, cssImports] of sortedCSSImports){
|
||||
const entryConventionName = path.parse(entryFilePath).name;
|
||||
for (const cssImport of cssImports){
|
||||
// If the CSS import is already tracked, we can skip it.
|
||||
// Or if it's any standalone entry such as `global-not-found`, it won't share any resources with other entry, skip it.
|
||||
if (trackedCSSImports.has(cssImport) && STANDALONE_BUNDLE_CONVENTION !== entryConventionName) {
|
||||
continue;
|
||||
}
|
||||
// Only track CSS imports that are in files that can inherit CSS.
|
||||
if (POSSIBLE_SHARED_CONVENTIONS.includes(entryConventionName)) {
|
||||
trackedCSSImports.add(cssImport);
|
||||
}
|
||||
if (!dedupedCSSImports[entryFilePath]) {
|
||||
dedupedCSSImports[entryFilePath] = [];
|
||||
}
|
||||
dedupedCSSImports[entryFilePath].push(cssImport);
|
||||
}
|
||||
}
|
||||
return dedupedCSSImports;
|
||||
}
|
||||
export class FlightClientEntryPlugin {
|
||||
constructor(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) {
|
||||
compiler.hooks.finishMake.tapPromise(PLUGIN_NAME, (compilation)=>this.createClientEntries(compiler, compilation));
|
||||
compiler.hooks.afterCompile.tap(PLUGIN_NAME, (compilation)=>{
|
||||
const recordModule = (modId, mod)=>{
|
||||
var _mod_resourceResolveData, _mod_resourceResolveData1;
|
||||
// Match Resource is undefined unless an import is using the inline match resource syntax
|
||||
// https://webpack.js.org/api/loaders/#inline-matchresource
|
||||
const modPath = mod.matchResource || ((_mod_resourceResolveData = mod.resourceResolveData) == null ? void 0 : _mod_resourceResolveData.path);
|
||||
const modQuery = ((_mod_resourceResolveData1 = mod.resourceResolveData) == null ? void 0 : _mod_resourceResolveData1.query) || '';
|
||||
// query is already part of mod.resource
|
||||
// so it's only necessary to add it for matchResource or mod.resourceResolveData
|
||||
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 = {
|
||||
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;
|
||||
}
|
||||
// Check mod resource to exclude the empty resource module like virtual module created by next-flight-client-entry-loader
|
||||
if (typeof modId !== 'undefined' && modResource) {
|
||||
// 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 = path.relative(compiler.context, modResource);
|
||||
if (!ssrNamedModuleId.startsWith('.')) {
|
||||
// TODO use getModuleId instead
|
||||
ssrNamedModuleId = `./${normalizePathSep(ssrNamedModuleId)}`;
|
||||
}
|
||||
const 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, compilation) {
|
||||
const addClientEntryAndSSRModulesList = [];
|
||||
const createdSSRDependenciesForEntry = {};
|
||||
const addActionEntryList = [];
|
||||
const actionMapsPerEntry = {};
|
||||
const createdActionIds = new Set();
|
||||
// For each SC server compilation entry, we need to create its corresponding
|
||||
// client component entry.
|
||||
forEachEntryModule(compilation, ({ name, entryModule })=>{
|
||||
const internalClientComponentEntryImports = {};
|
||||
const actionEntryImports = new Map();
|
||||
const clientEntriesToInject = [];
|
||||
const mergedCSSimports = {};
|
||||
const moduleReferences = getModuleReferencesInOrder(entryModule, compilation.moduleGraph);
|
||||
for (const connection of moduleReferences){
|
||||
// Entry can be any user defined entry files such as layout, page, error, loading, etc.
|
||||
let entryRequest = connection.dependency.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);
|
||||
// Next.js internals are put into a separate entry.
|
||||
if (!isAbsoluteRequest) {
|
||||
Object.keys(clientComponentImports).forEach((value)=>internalClientComponentEntryImports[value] = new Set());
|
||||
if (!isAppRouterBuiltinPage) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// TODO-APP: Enable these lines. This ensures no entrypoint is created for layout/page when there are no client components.
|
||||
// Currently disabled because it causes test failures in CI.
|
||||
// if (clientImports.length === 0 && actionImports.length === 0) {
|
||||
// continue
|
||||
// }
|
||||
const relativeRequest = isAbsoluteRequest && !isAppRouterBuiltinPage ? path.relative(compilation.options.context, entryRequest) : entryRequest;
|
||||
// Replace file suffix as `.js` will be added.
|
||||
// bundlePath will have app/ prefix but not src/.
|
||||
// e.g. src/app/foo/page.js -> app/foo/page
|
||||
let bundlePath = normalizePathSep(relativeRequest.replace(/\.[^.\\/]+$/, '').replace(/^src[\\/]/, ''));
|
||||
// For metadata routes, the entry name can be used as the bundle path,
|
||||
// as it has been normalized already.
|
||||
// e.g.
|
||||
// When `relativeRequest` is 'src/app/sitemap.js',
|
||||
// `appDirRelativeRequest` will be '/sitemap.js'
|
||||
// then `isMetadataEntryFile` will be `true`
|
||||
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
|
||||
});
|
||||
// The webpack implementation of writing the client reference manifest relies on all entrypoints writing a page.js even when there is no client components in the page.
|
||||
// It needs the file in order to write the reference manifest for the path in the `.next/server` folder.
|
||||
// TODO-APP: This could be better handled, however Turbopack does not have the same problem as we resolve client components in a single graph.
|
||||
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
|
||||
});
|
||||
}
|
||||
}
|
||||
// Make sure CSS imports are deduplicated before injecting the client entry
|
||||
// and SSR modules.
|
||||
const dedupedCSSImports = deduplicateCSSImportsForEntry(mergedCSSimports);
|
||||
for (const clientEntryToInject of clientEntriesToInject){
|
||||
const injected = this.injectClientEntryAndSSRModules({
|
||||
...clientEntryToInject,
|
||||
clientImports: {
|
||||
...clientEntryToInject.clientComponentImports,
|
||||
...(dedupedCSSImports[clientEntryToInject.absolutePagePath] || []).reduce((res, curr)=>{
|
||||
res[curr] = new Set();
|
||||
return res;
|
||||
}, {})
|
||||
}
|
||||
});
|
||||
// Track all created SSR dependencies for each entry from the server layer.
|
||||
if (!createdSSRDependenciesForEntry[clientEntryToInject.entryName]) {
|
||||
createdSSRDependenciesForEntry[clientEntryToInject.entryName] = [];
|
||||
}
|
||||
createdSSRDependenciesForEntry[clientEntryToInject.entryName].push(injected[3]);
|
||||
addClientEntryAndSSRModulesList.push(injected);
|
||||
}
|
||||
if (!isAppRouteRoute(name)) {
|
||||
// Create internal app
|
||||
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
|
||||
}));
|
||||
}
|
||||
// Invalidate in development to trigger recompilation
|
||||
const invalidator = getInvalidator(compiler.outputPath);
|
||||
// Check if any of the entry injections need an invalidation
|
||||
if (invalidator && addClientEntryAndSSRModulesList.some(([shouldInvalidate])=>shouldInvalidate === true)) {
|
||||
invalidator.invalidate([
|
||||
COMPILER_NAMES.client
|
||||
]);
|
||||
}
|
||||
// Client compiler is invalidated before awaiting the compilation of the SSR
|
||||
// and RSC client component entries so that the client compiler is running
|
||||
// in parallel to the server compiler.
|
||||
await Promise.all(addClientEntryAndSSRModulesList.flatMap((addClientEntryAndSSRModules)=>[
|
||||
addClientEntryAndSSRModules[1],
|
||||
addClientEntryAndSSRModules[2]
|
||||
]));
|
||||
// Wait for action entries to be added.
|
||||
await Promise.all(addActionEntryList);
|
||||
const addedClientActionEntryList = [];
|
||||
const actionMapsPerClientEntry = {};
|
||||
// We need to create extra action entries that are created from the
|
||||
// client layer.
|
||||
// Start from each entry's created SSR dependency from our previous step.
|
||||
for (const [name, ssrEntryDependencies] of Object.entries(createdSSRDependenciesForEntry)){
|
||||
// Collect from all entries, e.g. layout.js, page.js, loading.js, ...
|
||||
// add aggregate them.
|
||||
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)){
|
||||
// If an action method is already created in the server layer, we don't
|
||||
// need to create it again in the action layer.
|
||||
// This is to avoid duplicate action instances and make sure the module
|
||||
// state is shared.
|
||||
let remainingClientImportedActions = false;
|
||||
const remainingActionEntryImports = new Map();
|
||||
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 }) {
|
||||
// action file path -> action names
|
||||
const collectedActions = new Map();
|
||||
// Keep track of checked modules to avoid infinite loops with recursive imports.
|
||||
const visitedModule = new Set();
|
||||
const visitedEntry = new Set();
|
||||
const collectActions = ({ entryRequest, resolvedModule })=>{
|
||||
const collectActionsInDep = (mod)=>{
|
||||
var _getModuleBuildInfo_rsc;
|
||||
if (!mod) return;
|
||||
const modResource = getModuleResource(mod);
|
||||
if (!modResource) return;
|
||||
if (visitedModule.has(modResource)) return;
|
||||
visitedModule.add(modResource);
|
||||
const actionIds = (_getModuleBuildInfo_rsc = getModuleBuildInfo(mod).rsc) == null ? void 0 : _getModuleBuildInfo_rsc.actionIds;
|
||||
if (actionIds) {
|
||||
collectedActions.set(modResource, Object.entries(actionIds).map(([id, exportedName])=>({
|
||||
id,
|
||||
exportedName,
|
||||
filename: path.posix.relative(this.projectDir, modResource)
|
||||
})));
|
||||
}
|
||||
// Collect used exported actions transversely.
|
||||
getModuleReferencesInOrder(mod, compilation.moduleGraph).forEach((connection)=>{
|
||||
collectActionsInDep(connection.resolvedModule);
|
||||
});
|
||||
};
|
||||
// Don't traverse the module graph anymore once hitting the action layer.
|
||||
if (entryRequest && !entryRequest.includes('next-flight-action-entry-loader')) {
|
||||
// Traverse the module graph to find all client components.
|
||||
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.request;
|
||||
// It is possible that the same entry is added multiple times in the
|
||||
// connection graph. We can just skip these to speed up the process.
|
||||
if (visitedEntry.has(request)) continue;
|
||||
visitedEntry.add(request);
|
||||
collectActions({
|
||||
entryRequest: request,
|
||||
resolvedModule: connection.resolvedModule
|
||||
});
|
||||
}
|
||||
}
|
||||
return collectedActions;
|
||||
}
|
||||
collectComponentInfoFromServerEntryDependency({ entryRequest, compilation, resolvedModule }) {
|
||||
// Keep track of checked modules to avoid infinite loops with recursive imports.
|
||||
const visitedOfClientComponentsTraverse = new Set();
|
||||
// Info to collect.
|
||||
const clientComponentImports = {};
|
||||
const actionImports = [];
|
||||
const CSSImports = new Set();
|
||||
const filterClientComponents = (mod, importedIdentifiers)=>{
|
||||
var _getModuleBuildInfo_rsc;
|
||||
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_rsc = getModuleBuildInfo(mod).rsc) == null ? void 0 : _getModuleBuildInfo_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.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)=>{
|
||||
var _connection_dependency;
|
||||
let dependencyIds = [];
|
||||
// `ids` are the identifiers that are imported from the dependency,
|
||||
// if it's present, it's an array of strings.
|
||||
if ((_connection_dependency = connection.dependency) == null ? void 0 : _connection_dependency.ids) {
|
||||
dependencyIds.push(...connection.dependency.ids);
|
||||
} else {
|
||||
dependencyIds = [
|
||||
'*'
|
||||
];
|
||||
}
|
||||
filterClientComponents(connection.resolvedModule, dependencyIds);
|
||||
});
|
||||
};
|
||||
// Traverse the module graph to find all client components.
|
||||
filterClientComponents(resolvedModule, []);
|
||||
return {
|
||||
clientComponentImports,
|
||||
cssImports: CSSImports.size ? {
|
||||
[entryRequest]: Array.from(CSSImports)
|
||||
} : {},
|
||||
actionImports
|
||||
};
|
||||
}
|
||||
injectClientEntryAndSSRModules({ compiler, compilation, entryName, clientImports, bundlePath, absolutePagePath }) {
|
||||
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]
|
||||
]
|
||||
}));
|
||||
// For the client entry, we always use the CJS build of Next.js. If the
|
||||
// server is using the ESM build (when using the Edge runtime), we need to
|
||||
// replace them.
|
||||
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
|
||||
})}!`;
|
||||
// Add for the client compilation
|
||||
// Inject the entry to the client compiler.
|
||||
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];
|
||||
// New version of the client loader
|
||||
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,
|
||||
// Add the entries to the server compiler for the SSR and RSC layers. The
|
||||
// promises are awaited later using `Promise.all` in order to parallelize
|
||||
// adding the entries.
|
||||
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 }) {
|
||||
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),
|
||||
__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;
|
||||
}
|
||||
}
|
||||
// Inject the entry to the server compiler
|
||||
const actionEntryDep = bundler.EntryPlugin.createDependency(actionLoader, {
|
||||
name: bundlePath
|
||||
});
|
||||
return this.addEntry(compilation, // Reuse compilation context.
|
||||
compiler.context, actionEntryDep, {
|
||||
name: entryName,
|
||||
layer: fromClient ? WEBPACK_LAYERS.actionBrowser : WEBPACK_LAYERS.reactServerComponents
|
||||
});
|
||||
}
|
||||
addEntry(compilation, context, dependency, options) /* Promise<module> */ {
|
||||
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, options);
|
||||
compilation.addModuleTree({
|
||||
context,
|
||||
dependency,
|
||||
contextInfo: {
|
||||
issuerLayer: options.layer
|
||||
}
|
||||
}, (err, module)=>{
|
||||
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) {
|
||||
const serverActions = {};
|
||||
const edgeServerActions = {};
|
||||
traverseModules(compilation, (mod, _chunk, chunkGroup, modId)=>{
|
||||
// Go through all action entries and record the module ID for each entry.
|
||||
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)}`));
|
||||
compilation.emitAsset(`${this.assetPrefix}${SERVER_REFERENCE_MANIFEST}.json`, new sources.RawSource(json));
|
||||
}
|
||||
}
|
||||
function addClientImport(mod, modRequest, clientComponentImports, importedIdentifiers, isFirstVisitModule) {
|
||||
var _getModuleBuildInfo_rsc;
|
||||
const clientEntryType = (_getModuleBuildInfo_rsc = getModuleBuildInfo(mod).rsc) == null ? void 0 : _getModuleBuildInfo_rsc.clientEntryType;
|
||||
const isCjsModule = clientEntryType === 'cjs';
|
||||
const assumedSourceType = getAssumedSourceType(mod, isCjsModule ? 'commonjs' : 'auto');
|
||||
const clientImportsSet = clientComponentImports[modRequest];
|
||||
if (importedIdentifiers[0] === '*') {
|
||||
// If there's collected import path with named import identifiers,
|
||||
// or there's nothing in collected imports are empty.
|
||||
// we should include the whole module.
|
||||
if (!isFirstVisitModule && [
|
||||
...clientImportsSet
|
||||
][0] !== '*') {
|
||||
clientComponentImports[modRequest] = new Set([
|
||||
'*'
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
const isAutoModuleSourceType = assumedSourceType === 'auto';
|
||||
if (isAutoModuleSourceType) {
|
||||
clientComponentImports[modRequest] = new Set([
|
||||
'*'
|
||||
]);
|
||||
} else {
|
||||
// If it's not analyzed as named ESM exports, e.g. if it's mixing `export *` with named exports,
|
||||
// We'll include all modules since it's not able to do tree-shaking.
|
||||
for (const name of importedIdentifiers){
|
||||
// For cjs module default import, we include the whole module since
|
||||
const isCjsDefaultImport = isCjsModule && name === 'default';
|
||||
// Always include __esModule along with cjs module default export,
|
||||
// to make sure it work with client module proxy from React.
|
||||
if (isCjsDefaultImport) {
|
||||
clientComponentImports[modRequest].add('__esModule');
|
||||
}
|
||||
clientComponentImports[modRequest].add(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
function getModuleResource(mod) {
|
||||
var _mod_resourceResolveData, _mod_resourceResolveData1, _mod_matchResource;
|
||||
const modPath = ((_mod_resourceResolveData = mod.resourceResolveData) == null ? void 0 : _mod_resourceResolveData.path) || '';
|
||||
const modQuery = ((_mod_resourceResolveData1 = mod.resourceResolveData) == null ? void 0 : _mod_resourceResolveData1.query) || '';
|
||||
// We have to always use the resolved request here to make sure the
|
||||
// server and client are using the same module path (required by RSC), as
|
||||
// the server compiler and client compiler have different resolve configs.
|
||||
let modResource = modPath + modQuery;
|
||||
// Context modules don't have a resource path, we use the identifier instead.
|
||||
if (mod.constructor.name === 'ContextModule') {
|
||||
modResource = mod.identifier();
|
||||
}
|
||||
// For the barrel optimization, we need to use the match resource instead
|
||||
// because there will be 2 modules for the same file (same resource path)
|
||||
// but they're different modules and can't be deduped via `visitedModule`.
|
||||
// The first module is a virtual re-export module created by the loader.
|
||||
if ((_mod_matchResource = mod.matchResource) == null ? void 0 : _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) {
|
||||
// e.g. next-metadata-route-loader?filePath=<some-url-encoded-path>&isDynamicRouteExtension=1!?__next_metadata_route__
|
||||
const query = request.split('!')[0].split('next-metadata-route-loader?')[1];
|
||||
return parse(query);
|
||||
}
|
||||
|
||||
//# sourceMappingURL=flight-client-entry-plugin.js.map
|
||||
Reference in New Issue
Block a user