rsnext/packages/next/build/webpack/plugins/flight-manifest-plugin.ts
Shu Ding 201f98e81a
Per-page runtime (#35011)
Partially implements #31317 and #31506. There're also some trade-offs made with this PR: since we can't know if a certain runtime will be used or not beforehand, we have to start both runtime compilers (Node.js and Edge) and then generate entrypoints correspondingly.

Note that with this PR, the global runtime is still required to use the per-page runtime.

## Bug

- [ ] Related issues linked using `fixes #number`
- [x] Integration tests added
- [ ] Errors have helpful link attached, see `contributing.md`

## Feature

- [ ] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR.
- [ ] Related issues linked using `fixes #number`
- [ ] Integration tests added
- [ ] Documentation added
- [ ] Telemetry added. In case of a feature if it's used or not.
- [ ] Errors have helpful link attached, see `contributing.md`

## Documentation / Examples

- [ ] Make sure the linting passes by running `yarn lint`
2022-03-08 20:55:14 +00:00

134 lines
4.3 KiB
TypeScript

/**
* 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'
import { MIDDLEWARE_FLIGHT_MANIFEST } from '../../../shared/lib/constants'
// 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('../');
type Options = {
dev: boolean
clientComponentsRegex: RegExp
}
const PLUGIN_NAME = 'FlightManifestPlugin'
export class FlightManifestPlugin {
dev: boolean = false
clientComponentsRegex: RegExp
constructor(options: Options) {
if (typeof options.dev === 'boolean') {
this.dev = options.dev
}
this.clientComponentsRegex = options.clientComponentsRegex
}
apply(compiler: any) {
compiler.hooks.compilation.tap(
PLUGIN_NAME,
(compilation: any, { normalModuleFactory }: any) => {
compilation.dependencyFactories.set(
(webpack as any).dependencies.ModuleDependency,
normalModuleFactory
)
compilation.dependencyTemplates.set(
(webpack as any).dependencies.ModuleDependency,
new (webpack as any).dependencies.NullDependency.Template()
)
}
)
// Only for webpack 5
compiler.hooks.make.tap(PLUGIN_NAME, (compilation: any) => {
compilation.hooks.processAssets.tap(
{
name: PLUGIN_NAME,
// @ts-ignore TODO: Remove ignore when webpack 5 is stable
stage: webpack.Compilation.PROCESS_ASSETS_STAGE_ADDITIONS,
},
(assets: any) => this.createAsset(assets, compilation)
)
})
}
createAsset(assets: any, compilation: any) {
const manifest: any = {}
const { clientComponentsRegex } = this
compilation.chunkGroups.forEach((chunkGroup: any) => {
function recordModule(id: string, _chunk: any, mod: any) {
const resource = mod.resource?.replace(/\?__sc_client__$/, '')
// TODO: Hook into deps instead of the target module.
// That way we know by the type of dep whether to include.
// It also resolves conflicts when the same module is in multiple chunks.
const isNextClientComponent = /next[\\/](link|image)/.test(resource)
if (!clientComponentsRegex.test(resource) && !isNextClientComponent) {
return
}
const moduleExports: any = manifest[resource] || {}
const exportsInfo = compilation.moduleGraph.getExportsInfo(mod)
const moduleExportedKeys = ['', '*'].concat(
[...exportsInfo.exports]
.map((exportInfo) => {
if (exportInfo.provided) {
return exportInfo.name
}
return null
})
.filter(Boolean)
)
moduleExportedKeys.forEach((name) => {
if (!moduleExports[name]) {
moduleExports[name] = {
id,
name,
chunks: [],
}
}
})
manifest[resource] = moduleExports
}
chunkGroup.chunks.forEach((chunk: any) => {
const chunkModules =
compilation.chunkGraph.getChunkModulesIterable(chunk)
for (const mod of chunkModules) {
let modId = compilation.chunkGraph.getModuleId(mod)
// remove resource query on production
if (typeof modId === 'string') {
modId = modId.split('?')[0]
}
recordModule(modId, chunk, mod)
// If this is a concatenation, register each child to the parent ID.
if (mod.modules) {
mod.modules.forEach((concatenatedMod: any) => {
recordModule(modId, chunk, concatenatedMod)
})
}
}
})
})
// With switchable runtime, we need to emit the manifest files for both
// runtimes.
const file = `server/${MIDDLEWARE_FLIGHT_MANIFEST}`
const json = JSON.stringify(manifest)
assets[file + '.js'] = new sources.RawSource('self.__RSC_MANIFEST=' + json)
assets[file + '.json'] = new sources.RawSource(json)
}
}