a815ba9f79
This PR adds support for [Middleware as per RFC ](https://github.com/vercel/next.js/discussions/29750). ## 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
264 lines
9 KiB
TypeScript
264 lines
9 KiB
TypeScript
import devalue from 'next/dist/compiled/devalue'
|
|
import { webpack, sources } from 'next/dist/compiled/webpack/webpack'
|
|
import {
|
|
BUILD_MANIFEST,
|
|
CLIENT_STATIC_FILES_PATH,
|
|
CLIENT_STATIC_FILES_RUNTIME_MAIN,
|
|
CLIENT_STATIC_FILES_RUNTIME_POLYFILLS_SYMBOL,
|
|
CLIENT_STATIC_FILES_RUNTIME_REACT_REFRESH,
|
|
CLIENT_STATIC_FILES_RUNTIME_AMP,
|
|
} from '../../../shared/lib/constants'
|
|
import { BuildManifest } from '../../../server/get-page-files'
|
|
import getRouteFromEntrypoint from '../../../server/get-route-from-entrypoint'
|
|
import { ampFirstEntryNamesMap } from './next-drop-client-page-plugin'
|
|
import { Rewrite } from '../../../lib/load-custom-routes'
|
|
import { getSortedRoutes } from '../../../shared/lib/router/utils'
|
|
import { spans } from './profiling-plugin'
|
|
import { CustomRoutes } from '../../../lib/load-custom-routes'
|
|
|
|
type DeepMutable<T> = { -readonly [P in keyof T]: DeepMutable<T[P]> }
|
|
|
|
export type ClientBuildManifest = Record<string, string[]>
|
|
|
|
// This function takes the asset map generated in BuildManifestPlugin and creates a
|
|
// reduced version to send to the client.
|
|
function generateClientManifest(
|
|
compiler: any,
|
|
compilation: any,
|
|
assetMap: BuildManifest,
|
|
rewrites: CustomRoutes['rewrites']
|
|
): string {
|
|
const compilationSpan = spans.get(compilation) || spans.get(compiler)
|
|
const genClientManifestSpan = compilationSpan?.traceChild(
|
|
'NextJsBuildManifest-generateClientManifest'
|
|
)
|
|
|
|
return genClientManifestSpan?.traceFn(() => {
|
|
const clientManifest: ClientBuildManifest = {
|
|
// TODO: update manifest type to include rewrites
|
|
__rewrites: rewrites as any,
|
|
}
|
|
const appDependencies = new Set(assetMap.pages['/_app'])
|
|
const sortedPageKeys = getSortedRoutes(Object.keys(assetMap.pages))
|
|
|
|
sortedPageKeys.forEach((page) => {
|
|
const dependencies = assetMap.pages[page]
|
|
|
|
if (page === '/_app') return
|
|
// Filter out dependencies in the _app entry, because those will have already
|
|
// been loaded by the client prior to a navigation event
|
|
const filteredDeps = dependencies.filter(
|
|
(dep) => !appDependencies.has(dep)
|
|
)
|
|
|
|
// The manifest can omit the page if it has no requirements
|
|
if (filteredDeps.length) {
|
|
clientManifest[page] = filteredDeps
|
|
}
|
|
})
|
|
// provide the sorted pages as an array so we don't rely on the object's keys
|
|
// being in order and we don't slow down look-up time for page assets
|
|
clientManifest.sortedPages = sortedPageKeys
|
|
|
|
return devalue(clientManifest)
|
|
})
|
|
}
|
|
|
|
function getEntrypointFiles(entrypoint: any): string[] {
|
|
return (
|
|
entrypoint
|
|
?.getFiles()
|
|
.filter((file: string) => {
|
|
// We don't want to include `.hot-update.js` files into the initial page
|
|
return /(?<!\.hot-update)\.(js|css)($|\?)/.test(file)
|
|
})
|
|
.map((file: string) => file.replace(/\\/g, '/')) ?? []
|
|
)
|
|
}
|
|
|
|
const processRoute = (r: Rewrite) => {
|
|
const rewrite = { ...r }
|
|
|
|
// omit external rewrite destinations since these aren't
|
|
// handled client-side
|
|
if (!rewrite.destination.startsWith('/')) {
|
|
delete (rewrite as any).destination
|
|
}
|
|
return rewrite
|
|
}
|
|
|
|
// This plugin creates a build-manifest.json for all assets that are being output
|
|
// It has a mapping of "entry" filename to real filename. Because the real filename can be hashed in production
|
|
export default class BuildManifestPlugin {
|
|
private buildId: string
|
|
private rewrites: CustomRoutes['rewrites']
|
|
private isDevFallback: boolean
|
|
|
|
constructor(options: {
|
|
buildId: string
|
|
rewrites: CustomRoutes['rewrites']
|
|
isDevFallback?: boolean
|
|
}) {
|
|
this.buildId = options.buildId
|
|
this.isDevFallback = !!options.isDevFallback
|
|
this.rewrites = {
|
|
beforeFiles: [],
|
|
afterFiles: [],
|
|
fallback: [],
|
|
}
|
|
this.rewrites.beforeFiles = options.rewrites.beforeFiles.map(processRoute)
|
|
this.rewrites.afterFiles = options.rewrites.afterFiles.map(processRoute)
|
|
this.rewrites.fallback = options.rewrites.fallback.map(processRoute)
|
|
}
|
|
|
|
createAssets(compiler: any, compilation: any, assets: any) {
|
|
const compilationSpan = spans.get(compilation) || spans.get(compiler)
|
|
const createAssetsSpan = compilationSpan?.traceChild(
|
|
'NextJsBuildManifest-createassets'
|
|
)
|
|
return createAssetsSpan?.traceFn(() => {
|
|
const entrypoints: Map<string, any> = compilation.entrypoints
|
|
const assetMap: DeepMutable<BuildManifest> = {
|
|
polyfillFiles: [],
|
|
devFiles: [],
|
|
ampDevFiles: [],
|
|
lowPriorityFiles: [],
|
|
pages: { '/_app': [] },
|
|
ampFirstPages: [],
|
|
}
|
|
|
|
const ampFirstEntryNames = ampFirstEntryNamesMap.get(compilation)
|
|
if (ampFirstEntryNames) {
|
|
for (const entryName of ampFirstEntryNames) {
|
|
const pagePath = getRouteFromEntrypoint(entryName)
|
|
if (!pagePath) {
|
|
continue
|
|
}
|
|
|
|
assetMap.ampFirstPages.push(pagePath)
|
|
}
|
|
}
|
|
|
|
const mainFiles = new Set(
|
|
getEntrypointFiles(entrypoints.get(CLIENT_STATIC_FILES_RUNTIME_MAIN))
|
|
)
|
|
|
|
const compilationAssets: {
|
|
name: string
|
|
source: typeof sources.RawSource
|
|
info: object
|
|
}[] = compilation.getAssets()
|
|
|
|
assetMap.polyfillFiles = compilationAssets
|
|
.filter((p) => {
|
|
// Ensure only .js files are passed through
|
|
if (!p.name.endsWith('.js')) {
|
|
return false
|
|
}
|
|
|
|
return (
|
|
p.info && CLIENT_STATIC_FILES_RUNTIME_POLYFILLS_SYMBOL in p.info
|
|
)
|
|
})
|
|
.map((v) => v.name)
|
|
|
|
assetMap.devFiles = getEntrypointFiles(
|
|
entrypoints.get(CLIENT_STATIC_FILES_RUNTIME_REACT_REFRESH)
|
|
).filter((file) => !mainFiles.has(file))
|
|
|
|
assetMap.ampDevFiles = getEntrypointFiles(
|
|
entrypoints.get(CLIENT_STATIC_FILES_RUNTIME_AMP)
|
|
)
|
|
|
|
const systemEntrypoints = new Set([
|
|
CLIENT_STATIC_FILES_RUNTIME_MAIN,
|
|
CLIENT_STATIC_FILES_RUNTIME_REACT_REFRESH,
|
|
CLIENT_STATIC_FILES_RUNTIME_AMP,
|
|
])
|
|
|
|
for (const entrypoint of compilation.entrypoints.values()) {
|
|
if (systemEntrypoints.has(entrypoint.name)) continue
|
|
const pagePath = getRouteFromEntrypoint(entrypoint.name)
|
|
|
|
if (!pagePath) {
|
|
continue
|
|
}
|
|
|
|
const filesForPage = getEntrypointFiles(entrypoint)
|
|
|
|
assetMap.pages[pagePath] = [...new Set([...mainFiles, ...filesForPage])]
|
|
}
|
|
|
|
if (!this.isDevFallback) {
|
|
// Add the runtime build manifest file (generated later in this file)
|
|
// as a dependency for the app. If the flag is false, the file won't be
|
|
// downloaded by the client.
|
|
assetMap.lowPriorityFiles.push(
|
|
`${CLIENT_STATIC_FILES_PATH}/${this.buildId}/_buildManifest.js`
|
|
)
|
|
// Add the runtime ssg manifest file as a lazy-loaded file dependency.
|
|
// We also stub this file out for development mode (when it is not
|
|
// generated).
|
|
const srcEmptySsgManifest = `self.__SSG_MANIFEST=new Set;self.__SSG_MANIFEST_CB&&self.__SSG_MANIFEST_CB()`
|
|
|
|
const ssgManifestPath = `${CLIENT_STATIC_FILES_PATH}/${this.buildId}/_ssgManifest.js`
|
|
assetMap.lowPriorityFiles.push(ssgManifestPath)
|
|
assets[ssgManifestPath] = new sources.RawSource(srcEmptySsgManifest)
|
|
|
|
const srcEmptyMiddlewareManifest = `self.__MIDDLEWARE_MANIFEST=new Set;self.__MIDDLEWARE_MANIFEST_CB&&self.__MIDDLEWARE_MANIFEST_CB()`
|
|
const middlewareManifestPath = `${CLIENT_STATIC_FILES_PATH}/${this.buildId}/_middlewareManifest.js`
|
|
assetMap.lowPriorityFiles.push(middlewareManifestPath)
|
|
assets[middlewareManifestPath] = new sources.RawSource(
|
|
srcEmptyMiddlewareManifest
|
|
)
|
|
}
|
|
|
|
assetMap.pages = Object.keys(assetMap.pages)
|
|
.sort()
|
|
// eslint-disable-next-line
|
|
.reduce((a, c) => ((a[c] = assetMap.pages[c]), a), {} as any)
|
|
|
|
let buildManifestName = BUILD_MANIFEST
|
|
|
|
if (this.isDevFallback) {
|
|
buildManifestName = `fallback-${BUILD_MANIFEST}`
|
|
}
|
|
|
|
assets[buildManifestName] = new sources.RawSource(
|
|
JSON.stringify(assetMap, null, 2)
|
|
)
|
|
|
|
if (!this.isDevFallback) {
|
|
const clientManifestPath = `${CLIENT_STATIC_FILES_PATH}/${this.buildId}/_buildManifest.js`
|
|
|
|
assets[clientManifestPath] = new sources.RawSource(
|
|
`self.__BUILD_MANIFEST = ${generateClientManifest(
|
|
compiler,
|
|
compilation,
|
|
assetMap,
|
|
this.rewrites
|
|
)};self.__BUILD_MANIFEST_CB && self.__BUILD_MANIFEST_CB()`
|
|
)
|
|
}
|
|
|
|
return assets
|
|
})
|
|
}
|
|
|
|
apply(compiler: webpack.Compiler) {
|
|
compiler.hooks.make.tap('NextJsBuildManifest', (compilation) => {
|
|
// @ts-ignore TODO: Remove ignore when webpack 5 is stable
|
|
compilation.hooks.processAssets.tap(
|
|
{
|
|
name: 'NextJsBuildManifest',
|
|
// @ts-ignore TODO: Remove ignore when webpack 5 is stable
|
|
stage: webpack.Compilation.PROCESS_ASSETS_STAGE_ADDITIONS,
|
|
},
|
|
(assets: any) => {
|
|
this.createAssets(compiler, compilation, assets)
|
|
}
|
|
)
|
|
})
|
|
return
|
|
}
|
|
}
|