2021-10-26 18:50:56 +02:00
|
|
|
/**
|
|
|
|
* 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 { webpack, sources } from 'next/dist/compiled/webpack/webpack'
|
2022-06-26 23:01:26 +02:00
|
|
|
import { FLIGHT_MANIFEST } from '../../../shared/lib/constants'
|
2022-05-27 19:43:42 +02:00
|
|
|
import { relative } from 'path'
|
2022-09-18 02:00:16 +02:00
|
|
|
import { isClientComponentModule } from '../loaders/utils'
|
2021-10-26 18:50:56 +02:00
|
|
|
|
|
|
|
// This is the module that will be used to anchor all client references to.
|
|
|
|
// I.e. it will have all the client files as async deps from this point on.
|
|
|
|
// We use the Flight client implementation because you can't get to these
|
|
|
|
// without the client runtime so it's the first time in the loading sequence
|
|
|
|
// you might want them.
|
|
|
|
// const clientFileName = require.resolve('../');
|
|
|
|
|
2022-07-29 00:35:52 +02:00
|
|
|
interface Options {
|
2021-10-26 18:50:56 +02:00
|
|
|
dev: boolean
|
|
|
|
}
|
|
|
|
|
2022-08-12 15:01:19 +02:00
|
|
|
/**
|
|
|
|
* Webpack module id
|
|
|
|
*/
|
|
|
|
// TODO-APP ensure `null` is included as it is used.
|
|
|
|
type ModuleId = string | number /*| null*/
|
2022-07-29 00:35:52 +02:00
|
|
|
|
2022-08-12 15:01:19 +02:00
|
|
|
export type ManifestChunks = Array<`${string}:${string}` | string>
|
2022-07-29 00:35:52 +02:00
|
|
|
|
|
|
|
interface ManifestNode {
|
|
|
|
[moduleExport: string]: {
|
|
|
|
/**
|
|
|
|
* Webpack module id
|
|
|
|
*/
|
|
|
|
id: ModuleId
|
|
|
|
/**
|
|
|
|
* Export name
|
|
|
|
*/
|
|
|
|
name: string
|
|
|
|
/**
|
|
|
|
* Chunks for the module. JS and CSS.
|
|
|
|
*/
|
|
|
|
chunks: ManifestChunks
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-08-12 15:01:19 +02:00
|
|
|
export type FlightManifest = {
|
2022-07-29 00:35:52 +02:00
|
|
|
__ssr_module_mapping__: {
|
|
|
|
[moduleId: string]: ManifestNode
|
|
|
|
}
|
|
|
|
} & {
|
|
|
|
[modulePath: string]: ManifestNode
|
|
|
|
}
|
|
|
|
|
2022-08-12 15:01:19 +02:00
|
|
|
export type FlightCSSManifest = {
|
|
|
|
[modulePath: string]: string[]
|
|
|
|
}
|
|
|
|
|
2021-10-26 18:50:56 +02:00
|
|
|
const PLUGIN_NAME = 'FlightManifestPlugin'
|
|
|
|
|
|
|
|
export class FlightManifestPlugin {
|
2022-07-29 00:35:52 +02:00
|
|
|
dev: Options['dev'] = false
|
2021-10-26 18:50:56 +02:00
|
|
|
|
|
|
|
constructor(options: Options) {
|
2022-07-29 00:35:52 +02:00
|
|
|
this.dev = options.dev
|
2021-10-26 18:50:56 +02:00
|
|
|
}
|
|
|
|
|
2022-08-16 11:55:37 +02:00
|
|
|
apply(compiler: webpack.Compiler) {
|
2021-10-26 18:50:56 +02:00
|
|
|
compiler.hooks.compilation.tap(
|
|
|
|
PLUGIN_NAME,
|
2022-07-29 00:35:52 +02:00
|
|
|
(compilation, { normalModuleFactory }) => {
|
2021-10-26 18:50:56 +02:00
|
|
|
compilation.dependencyFactories.set(
|
|
|
|
(webpack as any).dependencies.ModuleDependency,
|
|
|
|
normalModuleFactory
|
|
|
|
)
|
|
|
|
compilation.dependencyTemplates.set(
|
|
|
|
(webpack as any).dependencies.ModuleDependency,
|
|
|
|
new (webpack as any).dependencies.NullDependency.Template()
|
|
|
|
)
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
2022-07-29 00:35:52 +02:00
|
|
|
compiler.hooks.make.tap(PLUGIN_NAME, (compilation) => {
|
2022-05-18 13:18:28 +02:00
|
|
|
compilation.hooks.processAssets.tap(
|
|
|
|
{
|
|
|
|
name: PLUGIN_NAME,
|
2022-07-13 07:00:56 +02:00
|
|
|
// 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,
|
2022-05-18 13:18:28 +02:00
|
|
|
},
|
2022-07-29 00:35:52 +02:00
|
|
|
(assets) => this.createAsset(assets, compilation, compiler.context)
|
2022-05-18 13:18:28 +02:00
|
|
|
)
|
|
|
|
})
|
|
|
|
}
|
2022-05-13 19:48:53 +02:00
|
|
|
|
2022-07-29 00:35:52 +02:00
|
|
|
createAsset(
|
2022-08-16 11:55:37 +02:00
|
|
|
assets: webpack.Compilation['assets'],
|
|
|
|
compilation: webpack.Compilation,
|
2022-07-29 00:35:52 +02:00
|
|
|
context: string
|
|
|
|
) {
|
|
|
|
const manifest: FlightManifest = {
|
|
|
|
__ssr_module_mapping__: {},
|
|
|
|
}
|
2022-05-25 00:04:27 +02:00
|
|
|
const dev = this.dev
|
2022-05-24 16:54:26 +02:00
|
|
|
|
2022-09-18 02:00:16 +02:00
|
|
|
const clientRequestsSet = new Set()
|
|
|
|
|
|
|
|
// Collect client requests
|
|
|
|
function collectClientRequest(mod: webpack.NormalModule) {
|
|
|
|
if (mod.resource === '' && mod.buildInfo.rsc) {
|
|
|
|
const { requests = [] } = mod.buildInfo.rsc
|
|
|
|
requests.forEach((r: string) => {
|
2022-09-20 12:40:27 +02:00
|
|
|
clientRequestsSet.add(r)
|
2022-09-18 02:00:16 +02:00
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
compilation.chunkGroups.forEach((chunkGroup) => {
|
|
|
|
chunkGroup.chunks.forEach((chunk: webpack.Chunk) => {
|
|
|
|
const chunkModules = compilation.chunkGraph.getChunkModulesIterable(
|
|
|
|
chunk
|
|
|
|
// TODO: Update type so that it doesn't have to be cast.
|
|
|
|
) as Iterable<webpack.NormalModule>
|
|
|
|
for (const mod of chunkModules) {
|
|
|
|
collectClientRequest(mod)
|
|
|
|
const anyModule = mod as any
|
|
|
|
if (anyModule.modules) {
|
|
|
|
for (const subMod of anyModule.modules) collectClientRequest(subMod)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
2022-07-29 00:35:52 +02:00
|
|
|
compilation.chunkGroups.forEach((chunkGroup) => {
|
2022-08-25 18:40:16 +02:00
|
|
|
const cssResourcesInChunkGroup = new Set<string>()
|
2022-08-23 01:09:56 +02:00
|
|
|
let entryFilepath: string = ''
|
|
|
|
|
2022-07-06 19:35:20 +02:00
|
|
|
function recordModule(
|
2022-08-16 11:55:37 +02:00
|
|
|
chunk: webpack.Chunk,
|
2022-07-29 00:35:52 +02:00
|
|
|
id: ModuleId,
|
2022-08-16 11:55:37 +02:00
|
|
|
mod: webpack.NormalModule
|
2022-07-06 19:35:20 +02:00
|
|
|
) {
|
2022-07-13 07:00:56 +02:00
|
|
|
const isCSSModule =
|
2022-09-18 02:00:16 +02:00
|
|
|
mod.resource?.endsWith('.css') ||
|
2022-07-13 07:00:56 +02:00
|
|
|
mod.type === 'css/mini-extract' ||
|
2022-09-18 02:00:16 +02:00
|
|
|
(!!mod.loaders &&
|
2022-07-13 07:00:56 +02:00
|
|
|
(dev
|
2022-07-29 00:35:52 +02:00
|
|
|
? mod.loaders.some((item) =>
|
2022-07-13 07:00:56 +02:00
|
|
|
item.loader.includes('next-style-loader/index.js')
|
|
|
|
)
|
2022-07-29 00:35:52 +02:00
|
|
|
: mod.loaders.some((item) =>
|
2022-07-13 07:00:56 +02:00
|
|
|
item.loader.includes('mini-css-extract-plugin/loader.js')
|
|
|
|
)))
|
|
|
|
|
|
|
|
const resource =
|
|
|
|
mod.type === 'css/mini-extract'
|
2022-07-29 00:35:52 +02:00
|
|
|
? // @ts-expect-error TODO: use `identifier()` instead.
|
|
|
|
mod._identifier.slice(mod._identifier.lastIndexOf('!') + 1)
|
2022-07-13 07:00:56 +02:00
|
|
|
: mod.resource
|
|
|
|
|
2022-09-18 02:00:16 +02:00
|
|
|
if (!resource) {
|
|
|
|
return
|
|
|
|
}
|
2022-05-10 21:20:13 +02:00
|
|
|
|
2022-07-29 00:35:52 +02:00
|
|
|
const moduleExports = manifest[resource] || {}
|
|
|
|
const moduleIdMapping = manifest.__ssr_module_mapping__
|
2022-05-27 19:43:42 +02:00
|
|
|
|
|
|
|
// 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.
|
2022-07-13 07:00:56 +02:00
|
|
|
let ssrNamedModuleId = relative(
|
|
|
|
context,
|
|
|
|
mod.resourceResolveData?.path || resource
|
|
|
|
)
|
2022-06-02 17:43:25 +02:00
|
|
|
if (!ssrNamedModuleId.startsWith('.'))
|
2022-08-17 19:14:03 +02:00
|
|
|
// TODO use getModuleId instead
|
|
|
|
ssrNamedModuleId = `./${ssrNamedModuleId.replace(/\\/g, '/')}`
|
2021-10-26 18:50:56 +02:00
|
|
|
|
2022-07-13 07:00:56 +02:00
|
|
|
if (isCSSModule) {
|
2022-07-11 17:23:21 +02:00
|
|
|
if (!manifest[resource]) {
|
2022-07-21 14:38:04 +02:00
|
|
|
const chunks = [...chunk.files].filter((f) => f.endsWith('.css'))
|
|
|
|
manifest[resource] = {
|
|
|
|
default: {
|
|
|
|
id,
|
2022-07-11 17:23:21 +02:00
|
|
|
name: 'default',
|
|
|
|
chunks,
|
2022-07-21 14:38:04 +02:00
|
|
|
},
|
|
|
|
}
|
2022-07-11 17:23:21 +02:00
|
|
|
}
|
2022-08-23 01:09:56 +02:00
|
|
|
|
|
|
|
if (chunkGroup.name) {
|
2022-08-25 18:40:16 +02:00
|
|
|
cssResourcesInChunkGroup.add(resource)
|
2022-08-23 01:09:56 +02:00
|
|
|
}
|
|
|
|
|
2022-07-11 17:23:21 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-09-18 02:00:16 +02:00
|
|
|
// Only apply following logic to client module requests from client entry,
|
|
|
|
// or if the module is marked as client module.
|
|
|
|
if (!clientRequestsSet.has(resource) && !isClientComponentModule(mod)) {
|
2022-07-11 17:23:21 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-09-18 02:00:16 +02:00
|
|
|
if (/[\\/](page|layout)\.(ts|js)x?$/.test(resource)) {
|
2022-08-23 01:09:56 +02:00
|
|
|
entryFilepath = resource
|
|
|
|
}
|
|
|
|
|
2021-10-26 18:50:56 +02:00
|
|
|
const exportsInfo = compilation.moduleGraph.getExportsInfo(mod)
|
2022-05-29 20:53:12 +02:00
|
|
|
const cjsExports = [
|
2022-07-29 00:35:52 +02:00
|
|
|
...new Set([
|
|
|
|
...mod.dependencies.map((dep) => {
|
|
|
|
// Match CommonJsSelfReferenceDependency
|
|
|
|
if (dep.type === 'cjs self exports reference') {
|
|
|
|
// @ts-expect-error: TODO: Fix Dependency type
|
|
|
|
if (dep.base === 'module.exports') {
|
|
|
|
return 'default'
|
2022-05-29 20:53:12 +02:00
|
|
|
}
|
2022-07-29 00:35:52 +02:00
|
|
|
|
|
|
|
// `exports.foo = ...`, `exports.default = ...`
|
|
|
|
// @ts-expect-error: TODO: Fix Dependency type
|
|
|
|
if (dep.base === 'exports') {
|
|
|
|
// @ts-expect-error: TODO: Fix Dependency type
|
|
|
|
return dep.names.filter((name: any) => name !== '__esModule')
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return null
|
|
|
|
}),
|
|
|
|
]),
|
2022-05-29 20:53:12 +02:00
|
|
|
]
|
|
|
|
|
2022-09-18 02:00:16 +02:00
|
|
|
function getAppPathRequiredChunks() {
|
|
|
|
return chunkGroup.chunks.map((requiredChunk: webpack.Chunk) => {
|
|
|
|
return (
|
|
|
|
requiredChunk.id +
|
|
|
|
':' +
|
|
|
|
(requiredChunk.name || requiredChunk.id) +
|
|
|
|
(dev ? '' : '-' + requiredChunk.hash)
|
|
|
|
)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2022-05-29 20:53:12 +02:00
|
|
|
const moduleExportedKeys = ['', '*']
|
|
|
|
.concat(
|
2022-07-06 19:35:20 +02:00
|
|
|
[...exportsInfo.exports]
|
|
|
|
.filter((exportInfo) => exportInfo.provided)
|
|
|
|
.map((exportInfo) => exportInfo.name),
|
2022-05-29 20:53:12 +02:00
|
|
|
...cjsExports
|
|
|
|
)
|
|
|
|
.filter((name) => name !== null)
|
2021-10-26 18:50:56 +02:00
|
|
|
|
|
|
|
moduleExportedKeys.forEach((name) => {
|
2022-09-18 02:00:16 +02:00
|
|
|
// If the chunk is from `app/` chunkGroup, use it first.
|
|
|
|
// This make sure not to load the overlapped chunk from `pages/` chunkGroup
|
|
|
|
if (!moduleExports[name] || chunkGroup.name?.startsWith('app/')) {
|
|
|
|
const requiredChunks = getAppPathRequiredChunks()
|
2022-07-06 19:35:20 +02:00
|
|
|
|
2021-10-26 18:50:56 +02:00
|
|
|
moduleExports[name] = {
|
2022-05-27 19:43:42 +02:00
|
|
|
id,
|
2021-10-26 18:50:56 +02:00
|
|
|
name,
|
2022-08-23 01:09:56 +02:00
|
|
|
chunks: requiredChunks,
|
2021-10-26 18:50:56 +02:00
|
|
|
}
|
|
|
|
}
|
2022-08-12 15:01:19 +02:00
|
|
|
|
|
|
|
moduleIdMapping[id] = moduleIdMapping[id] || {}
|
2022-09-18 02:00:16 +02:00
|
|
|
moduleIdMapping[id][name] = {
|
|
|
|
...moduleExports[name],
|
|
|
|
id: ssrNamedModuleId,
|
2022-06-02 17:43:25 +02:00
|
|
|
}
|
2021-10-26 18:50:56 +02:00
|
|
|
})
|
2022-05-27 19:43:42 +02:00
|
|
|
|
2022-03-08 21:55:14 +01:00
|
|
|
manifest[resource] = moduleExports
|
2022-06-02 17:43:25 +02:00
|
|
|
manifest.__ssr_module_mapping__ = moduleIdMapping
|
2021-10-26 18:50:56 +02:00
|
|
|
}
|
|
|
|
|
2022-08-16 11:55:37 +02:00
|
|
|
chunkGroup.chunks.forEach((chunk: webpack.Chunk) => {
|
2022-07-29 00:35:52 +02:00
|
|
|
const chunkModules = compilation.chunkGraph.getChunkModulesIterable(
|
|
|
|
chunk
|
|
|
|
// TODO: Update type so that it doesn't have to be cast.
|
2022-08-16 11:55:37 +02:00
|
|
|
) as Iterable<webpack.NormalModule>
|
2021-10-26 18:50:56 +02:00
|
|
|
for (const mod of chunkModules) {
|
2022-05-27 19:43:42 +02:00
|
|
|
const modId = compilation.chunkGraph.getModuleId(mod)
|
2022-05-24 16:54:26 +02:00
|
|
|
|
|
|
|
recordModule(chunk, modId, mod)
|
2022-05-10 21:20:13 +02:00
|
|
|
|
2021-10-26 18:50:56 +02:00
|
|
|
// If this is a concatenation, register each child to the parent ID.
|
2022-07-29 00:35:52 +02:00
|
|
|
// TODO: remove any
|
2022-07-06 19:35:20 +02:00
|
|
|
const anyModule = mod as any
|
|
|
|
if (anyModule.modules) {
|
|
|
|
anyModule.modules.forEach((concatenatedMod: any) => {
|
2022-05-24 16:54:26 +02:00
|
|
|
recordModule(chunk, modId, concatenatedMod)
|
2021-10-26 18:50:56 +02:00
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
2022-08-23 01:09:56 +02:00
|
|
|
|
|
|
|
const clientCSSManifest: any = manifest.__client_css_manifest__ || {}
|
|
|
|
if (entryFilepath) {
|
2022-08-25 18:40:16 +02:00
|
|
|
clientCSSManifest[entryFilepath] = Array.from(cssResourcesInChunkGroup)
|
2022-08-23 01:09:56 +02:00
|
|
|
}
|
|
|
|
manifest.__client_css_manifest__ = clientCSSManifest
|
2021-10-26 18:50:56 +02:00
|
|
|
})
|
|
|
|
|
2022-06-26 23:01:26 +02:00
|
|
|
const file = 'server/' + FLIGHT_MANIFEST
|
2022-05-24 16:54:26 +02:00
|
|
|
const json = JSON.stringify(manifest)
|
2022-03-08 21:55:14 +01:00
|
|
|
|
2022-07-29 00:35:52 +02:00
|
|
|
assets[file + '.js'] = new sources.RawSource(
|
|
|
|
'self.__RSC_MANIFEST=' + json
|
|
|
|
// Work around webpack 4 type of RawSource being used
|
|
|
|
// TODO: use webpack 5 type by default
|
2022-08-16 11:55:37 +02:00
|
|
|
) as unknown as webpack.sources.RawSource
|
2022-07-29 00:35:52 +02:00
|
|
|
assets[file + '.json'] = new sources.RawSource(
|
|
|
|
json
|
|
|
|
// Work around webpack 4 type of RawSource being used
|
|
|
|
// TODO: use webpack 5 type by default
|
2022-08-16 11:55:37 +02:00
|
|
|
) as unknown as webpack.sources.RawSource
|
2021-10-26 18:50:56 +02:00
|
|
|
}
|
|
|
|
}
|