2019-08-20 08:28:09 +02:00
|
|
|
import devalue from 'devalue'
|
2019-03-12 05:01:50 +01:00
|
|
|
import {
|
|
|
|
BUILD_MANIFEST,
|
2019-08-08 19:14:33 +02:00
|
|
|
CLIENT_STATIC_FILES_PATH,
|
2019-05-29 13:57:26 +02:00
|
|
|
CLIENT_STATIC_FILES_RUNTIME_MAIN,
|
2019-08-20 08:28:09 +02:00
|
|
|
IS_BUNDLED_PAGE_REGEX,
|
|
|
|
ROUTE_NAME_REGEX,
|
2019-09-04 16:00:54 +02:00
|
|
|
} from '../../../next-server/lib/constants'
|
2019-08-20 08:28:09 +02:00
|
|
|
import { Compiler } from 'webpack'
|
|
|
|
import { RawSource } from 'webpack-sources'
|
2018-04-12 09:47:42 +02:00
|
|
|
|
2019-08-08 19:14:33 +02:00
|
|
|
interface AssetMap {
|
|
|
|
devFiles: string[]
|
|
|
|
pages: {
|
|
|
|
'/_app': string[]
|
|
|
|
[s: string]: string[]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// This function takes the asset map generated in BuildManifestPlugin and creates a
|
|
|
|
// reduced version to send to the client.
|
|
|
|
const generateClientManifest = (
|
|
|
|
assetMap: AssetMap,
|
|
|
|
isModern: boolean
|
|
|
|
): string => {
|
|
|
|
const clientManifest: { [s: string]: string[] } = {}
|
|
|
|
const appDependencies = new Set(assetMap.pages['/_app'])
|
2019-08-20 21:32:03 +02:00
|
|
|
|
2019-08-08 19:14:33 +02:00
|
|
|
Object.entries(assetMap.pages).forEach(([page, dependencies]) => {
|
2019-08-20 21:32:03 +02:00
|
|
|
if (page === '/_app') return
|
2019-08-08 19:14:33 +02:00
|
|
|
// 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) && /\.module\.js$/.test(dep) === isModern
|
|
|
|
)
|
|
|
|
|
|
|
|
// The manifest can omit the page if it has no requirements
|
|
|
|
if (filteredDeps.length) {
|
|
|
|
clientManifest[page] = filteredDeps
|
|
|
|
}
|
|
|
|
})
|
2019-08-20 08:28:09 +02:00
|
|
|
return devalue(clientManifest)
|
2019-08-08 19:14:33 +02:00
|
|
|
}
|
|
|
|
|
2018-04-12 09:47:42 +02:00
|
|
|
// 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 {
|
2019-08-08 19:14:33 +02:00
|
|
|
private buildId: string
|
|
|
|
private clientManifest: boolean
|
|
|
|
private modern: boolean
|
|
|
|
|
|
|
|
constructor(options: {
|
|
|
|
buildId: string
|
|
|
|
clientManifest: boolean
|
|
|
|
modern: boolean
|
|
|
|
}) {
|
|
|
|
this.buildId = options.buildId
|
|
|
|
this.clientManifest = options.clientManifest
|
|
|
|
this.modern = options.modern
|
|
|
|
}
|
|
|
|
|
2019-05-29 13:57:26 +02:00
|
|
|
apply(compiler: Compiler) {
|
2019-03-12 05:01:50 +01:00
|
|
|
compiler.hooks.emit.tapAsync(
|
|
|
|
'NextJsBuildManifest',
|
|
|
|
(compilation, callback) => {
|
|
|
|
const { chunks } = compilation
|
2019-08-08 19:14:33 +02:00
|
|
|
const assetMap: AssetMap = { devFiles: [], pages: { '/_app': [] } }
|
2018-04-12 09:47:42 +02:00
|
|
|
|
2019-03-12 05:01:50 +01:00
|
|
|
const mainJsChunk = chunks.find(
|
|
|
|
c => c.name === CLIENT_STATIC_FILES_RUNTIME_MAIN
|
|
|
|
)
|
2019-04-23 23:12:33 +02:00
|
|
|
const mainJsFiles: string[] =
|
2019-03-12 05:01:50 +01:00
|
|
|
mainJsChunk && mainJsChunk.files.length > 0
|
2019-04-23 23:12:33 +02:00
|
|
|
? mainJsChunk.files.filter((file: string) => /\.js$/.test(file))
|
2019-03-12 05:01:50 +01:00
|
|
|
: []
|
2018-07-24 11:24:40 +02:00
|
|
|
|
2019-03-12 05:01:50 +01:00
|
|
|
for (const filePath of Object.keys(compilation.assets)) {
|
|
|
|
const path = filePath.replace(/\\/g, '/')
|
|
|
|
if (/^static\/development\/dll\//.test(path)) {
|
|
|
|
assetMap.devFiles.push(path)
|
|
|
|
}
|
2018-07-24 11:24:40 +02:00
|
|
|
}
|
|
|
|
|
2019-03-12 05:01:50 +01:00
|
|
|
// compilation.entrypoints is a Map object, so iterating over it 0 is the key and 1 is the value
|
|
|
|
for (const [, entrypoint] of compilation.entrypoints.entries()) {
|
|
|
|
const result = ROUTE_NAME_REGEX.exec(entrypoint.name)
|
|
|
|
if (!result) {
|
|
|
|
continue
|
|
|
|
}
|
2018-07-24 11:24:40 +02:00
|
|
|
|
2019-03-12 05:01:50 +01:00
|
|
|
const pagePath = result[1]
|
2018-07-24 11:24:40 +02:00
|
|
|
|
2019-03-12 05:01:50 +01:00
|
|
|
if (!pagePath) {
|
2018-07-24 11:24:40 +02:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2019-04-23 23:12:33 +02:00
|
|
|
const filesForEntry: string[] = []
|
2019-03-12 05:01:50 +01:00
|
|
|
for (const chunk of entrypoint.chunks) {
|
|
|
|
// If there's no name or no files
|
|
|
|
if (!chunk.name || !chunk.files) {
|
2018-07-24 11:24:40 +02:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2019-03-12 05:01:50 +01:00
|
|
|
for (const file of chunk.files) {
|
|
|
|
if (/\.map$/.test(file) || /\.hot-update\.js$/.test(file)) {
|
|
|
|
continue
|
|
|
|
}
|
2018-07-30 15:48:02 +02:00
|
|
|
|
2019-03-12 05:01:50 +01:00
|
|
|
// Only `.js` and `.css` files are added for now. In the future we can also handle other file types.
|
|
|
|
if (!/\.js$/.test(file) && !/\.css$/.test(file)) {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
// The page bundles are manually added to _document.js as they need extra properties
|
|
|
|
if (IS_BUNDLED_PAGE_REGEX.exec(file)) {
|
|
|
|
continue
|
|
|
|
}
|
2018-07-24 11:24:40 +02:00
|
|
|
|
2019-03-12 05:01:50 +01:00
|
|
|
filesForEntry.push(file.replace(/\\/g, '/'))
|
|
|
|
}
|
2018-07-24 11:24:40 +02:00
|
|
|
}
|
|
|
|
|
2019-03-12 05:01:50 +01:00
|
|
|
assetMap.pages[`/${pagePath.replace(/\\/g, '/')}`] = [
|
|
|
|
...filesForEntry,
|
2019-05-29 13:57:26 +02:00
|
|
|
...mainJsFiles,
|
2019-03-12 05:01:50 +01:00
|
|
|
]
|
|
|
|
}
|
2018-07-24 11:24:40 +02:00
|
|
|
|
2019-03-12 05:01:50 +01:00
|
|
|
if (typeof assetMap.pages['/index'] !== 'undefined') {
|
|
|
|
assetMap.pages['/'] = assetMap.pages['/index']
|
|
|
|
}
|
2018-07-24 11:24:40 +02:00
|
|
|
|
2019-08-08 19:14:33 +02:00
|
|
|
// 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.
|
|
|
|
if (this.clientManifest) {
|
|
|
|
assetMap.pages['/_app'].push(
|
|
|
|
`${CLIENT_STATIC_FILES_PATH}/${this.buildId}/_buildManifest.js`
|
|
|
|
)
|
|
|
|
if (this.modern) {
|
|
|
|
assetMap.pages['/_app'].push(
|
|
|
|
`${CLIENT_STATIC_FILES_PATH}/${
|
|
|
|
this.buildId
|
|
|
|
}/_buildManifest.module.js`
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-03-12 05:01:50 +01:00
|
|
|
assetMap.pages = Object.keys(assetMap.pages)
|
|
|
|
.sort()
|
|
|
|
// eslint-disable-next-line
|
2019-04-23 23:12:33 +02:00
|
|
|
.reduce((a, c) => ((a[c] = assetMap.pages[c]), a), {} as any)
|
2019-03-11 22:34:41 +01:00
|
|
|
|
2019-03-12 05:01:50 +01:00
|
|
|
compilation.assets[BUILD_MANIFEST] = new RawSource(
|
|
|
|
JSON.stringify(assetMap, null, 2)
|
|
|
|
)
|
2019-08-08 19:14:33 +02:00
|
|
|
|
|
|
|
if (this.clientManifest) {
|
|
|
|
const clientManifestPath = `${CLIENT_STATIC_FILES_PATH}/${
|
|
|
|
this.buildId
|
|
|
|
}/_buildManifest.js`
|
|
|
|
|
|
|
|
compilation.assets[clientManifestPath] = new RawSource(
|
2019-08-20 08:28:09 +02:00
|
|
|
`self.__BUILD_MANIFEST = ${generateClientManifest(
|
2019-08-08 19:14:33 +02:00
|
|
|
assetMap,
|
|
|
|
false
|
2019-08-20 08:28:09 +02:00
|
|
|
)};` + `self.__BUILD_MANIFEST_CB && self.__BUILD_MANIFEST_CB()`
|
2019-08-08 19:14:33 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
if (this.modern) {
|
|
|
|
const modernClientManifestPath = `${CLIENT_STATIC_FILES_PATH}/${
|
|
|
|
this.buildId
|
|
|
|
}/_buildManifest.module.js`
|
|
|
|
|
|
|
|
compilation.assets[modernClientManifestPath] = new RawSource(
|
2019-08-20 08:28:09 +02:00
|
|
|
`self.__BUILD_MANIFEST = ${generateClientManifest(
|
2019-08-08 19:14:33 +02:00
|
|
|
assetMap,
|
|
|
|
true
|
2019-08-20 08:28:09 +02:00
|
|
|
)};` + `self.__BUILD_MANIFEST_CB && self.__BUILD_MANIFEST_CB()`
|
2019-08-08 19:14:33 +02:00
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
2019-03-12 05:01:50 +01:00
|
|
|
callback()
|
|
|
|
}
|
|
|
|
)
|
2018-04-12 09:47:42 +02:00
|
|
|
}
|
|
|
|
}
|