diff --git a/packages/next/build/index.ts b/packages/next/build/index.ts index 0cae99314a920..2f9ee1b69e245 100644 --- a/packages/next/build/index.ts +++ b/packages/next/build/index.ts @@ -1,9 +1,11 @@ import chalk from 'chalk' -import { PHASE_PRODUCTION_BUILD, BLOCKED_PAGES } from 'next-server/constants' +import { + CHUNK_GRAPH_MANIFEST, + PHASE_PRODUCTION_BUILD, +} from 'next-server/constants' import loadConfig from 'next-server/next-config' import nanoid from 'next/dist/compiled/nanoid/index.js' import path from 'path' -import fs from 'fs' import formatWebpackMessages from '../client/dev-error-overlay/format-webpack-messages' import { recursiveDelete } from '../lib/recursive-delete' @@ -20,6 +22,7 @@ import { printTreeView, } from './utils' import getBaseWebpackConfig from './webpack-config' +import { exportManifest } from './webpack/plugins/chunk-graph-plugin' import { writeBuildId } from './write-build-id' export default async function build(dir: string, conf = null): Promise { @@ -149,7 +152,6 @@ export default async function build(dir: string, conf = null): Promise { isServer: false, config, target: config.target, - selectivePageBuildingCacheIdentifier, entrypoints: entrypoints.client, selectivePageBuilding, }), @@ -159,7 +161,6 @@ export default async function build(dir: string, conf = null): Promise { isServer: true, config, target: config.target, - selectivePageBuildingCacheIdentifier, entrypoints: entrypoints.server, selectivePageBuilding, }), @@ -194,6 +195,12 @@ export default async function build(dir: string, conf = null): Promise { if (isFlyingShuttle) { console.log() + + exportManifest({ + dir: dir, + fileName: path.join(distDir, CHUNK_GRAPH_MANIFEST), + selectivePageBuildingCacheIdentifier, + }) } if (result.errors.length > 0) { diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts index b63f86f690882..4b6d6c6b90fce 100644 --- a/packages/next/build/webpack-config.ts +++ b/packages/next/build/webpack-config.ts @@ -7,7 +7,7 @@ import PagesManifestPlugin from './webpack/plugins/pages-manifest-plugin' import BuildManifestPlugin from './webpack/plugins/build-manifest-plugin' import ChunkNamesPlugin from './webpack/plugins/chunk-names-plugin' import { ReactLoadablePlugin } from './webpack/plugins/react-loadable-plugin' -import { SERVER_DIRECTORY, REACT_LOADABLE_MANIFEST, CHUNK_GRAPH_MANIFEST, CLIENT_STATIC_FILES_RUNTIME_WEBPACK, CLIENT_STATIC_FILES_RUNTIME_MAIN } from 'next-server/constants' +import { SERVER_DIRECTORY, REACT_LOADABLE_MANIFEST, CLIENT_STATIC_FILES_RUNTIME_WEBPACK, CLIENT_STATIC_FILES_RUNTIME_MAIN } from 'next-server/constants' import { NEXT_PROJECT_ROOT, NEXT_PROJECT_ROOT_DIST_CLIENT, PAGES_DIR_ALIAS, DOT_NEXT_ALIAS } from '../lib/constants' import {TerserPlugin} from './webpack/plugins/terser-webpack-plugin/src/index' import { ServerlessPlugin } from './webpack/plugins/serverless-plugin' @@ -20,7 +20,7 @@ import { importAutoDllPlugin } from './webpack/plugins/dll-import' import { WebpackEntrypoints } from './entries' type ExcludesFalse = (x: T | false) => x is T -export default async function getBaseWebpackConfig (dir: string, {dev = false, debug = false, isServer = false, buildId, config, target = 'server', entrypoints, selectivePageBuilding = false, selectivePageBuildingCacheIdentifier = ''}: {dev?: boolean, debug?: boolean, isServer?: boolean, buildId: string, config: any, target?: string, entrypoints: WebpackEntrypoints, selectivePageBuilding?: boolean, selectivePageBuildingCacheIdentifier?: string}): Promise { +export default async function getBaseWebpackConfig (dir: string, {dev = false, debug = false, isServer = false, buildId, config, target = 'server', entrypoints, selectivePageBuilding = false}: {dev?: boolean, debug?: boolean, isServer?: boolean, buildId: string, config: any, target?: string, entrypoints: WebpackEntrypoints, selectivePageBuilding?: boolean}): Promise { const distDir = path.join(dir, config.distDir) const defaultLoaders = { babel: { @@ -281,7 +281,9 @@ export default async function getBaseWebpackConfig (dir: string, {dev = false, d !isServer && new ReactLoadablePlugin({ filename: REACT_LOADABLE_MANIFEST }), - !isServer && selectivePageBuilding && new ChunkGraphPlugin(buildId, path.resolve(dir), { filename: CHUNK_GRAPH_MANIFEST, selectivePageBuildingCacheIdentifier }), + selectivePageBuilding && new ChunkGraphPlugin(buildId, { + dir, distDir, isServer + }), !isServer && new DropClientPage(), ...(dev ? (() => { // Even though require.cache is server only we have to clear assets from both compilations diff --git a/packages/next/build/webpack/plugins/chunk-graph-plugin.ts b/packages/next/build/webpack/plugins/chunk-graph-plugin.ts index 34973db0e74eb..96089d3782030 100644 --- a/packages/next/build/webpack/plugins/chunk-graph-plugin.ts +++ b/packages/next/build/webpack/plugins/chunk-graph-plugin.ts @@ -6,6 +6,56 @@ import path from 'path' import { parse } from 'querystring' import { Compiler, Plugin } from 'webpack' +type StringDictionary = { [pageName: string]: string[] } +const manifest: { + pages: StringDictionary + pageChunks: StringDictionary + chunks: StringDictionary +} = { + pages: {}, + pageChunks: {}, + chunks: {}, +} + +export function exportManifest({ + dir, + fileName, + selectivePageBuildingCacheIdentifier, +}: { + dir: string + fileName: string + selectivePageBuildingCacheIdentifier: string +}) { + const finalManifest = { + ...manifest, + hashes: {} as { [pageName: string]: string }, + } + + const allFiles = new Set() + for (const page of Object.keys(finalManifest.pages)) { + finalManifest.pages[page].forEach(f => allFiles.add(f)) + } + + finalManifest.hashes = [...allFiles].sort().reduce( + (acc, cur) => + Object.assign( + acc, + fs.existsSync(path.join(dir, cur)) + ? { + [cur]: createHash('sha1') + .update(selectivePageBuildingCacheIdentifier) + .update(fs.readFileSync(path.join(dir, cur))) + .digest('hex'), + } + : undefined + ), + {} + ) + + const json = JSON.stringify(finalManifest, null, 2) + EOL + fs.writeFileSync(fileName, json) +} + function getFiles(dir: string, modules: any[]): string[] { if (!(modules && modules.length)) { return [] @@ -49,45 +99,34 @@ function getFiles(dir: string, modules: any[]): string[] { export class ChunkGraphPlugin implements Plugin { private buildId: string private dir: string - private filename: string - private selectivePageBuildingCacheIdentifier: string + private distDir: string + private isServer: boolean constructor( buildId: string, - dir: string, { - filename, - selectivePageBuildingCacheIdentifier, - }: { filename?: string; selectivePageBuildingCacheIdentifier?: string } = {} + dir, + distDir, + isServer, + }: { + dir: string + distDir: string + isServer: boolean + } ) { this.buildId = buildId this.dir = dir - this.filename = filename || 'chunk-graph-manifest.json' - this.selectivePageBuildingCacheIdentifier = - selectivePageBuildingCacheIdentifier || '' + this.distDir = distDir + this.isServer = isServer } apply(compiler: Compiler) { const { dir } = this compiler.hooks.emit.tap('ChunkGraphPlugin', compilation => { - type StringDictionary = { [pageName: string]: string[] } - const manifest: { - pages: StringDictionary - pageChunks: StringDictionary - chunks: StringDictionary - hashes: { [pageName: string]: string } - } = { - pages: {}, - pageChunks: {}, - chunks: {}, - hashes: {}, - } - const sharedFiles = [] as string[] const sharedChunks = [] as string[] const pages: StringDictionary = {} const pageChunks: StringDictionary = {} - const allFiles = new Set() compilation.chunks.forEach(chunk => { if (!chunk.hasEntryModule()) { @@ -118,11 +157,15 @@ export class ChunkGraphPlugin implements Plugin { const modules = [...chunkModules.values()] const files = getFiles(dir, modules) + // we don't care about node_modules (yet) because we invalidate the + // entirety of flying shuttle on package changes .filter(val => !val.includes('node_modules')) + // build artifacts shouldn't be considered, so we ensure all paths + // are outside of this directory + .filter(val => path.relative(this.distDir, val).startsWith('..')) + // convert from absolute path to be portable across operating systems + // and directories .map(f => path.relative(dir, f)) - .sort() - - files.forEach(f => allFiles.add(f)) let pageName: string | undefined if (chunk.entryModule && chunk.entryModule.loaders) { @@ -133,8 +176,7 @@ export class ChunkGraphPlugin implements Plugin { }: { loader?: string | null options?: string | null - }) => - loader && loader.includes('next-client-pages-loader') && options + }) => loader && loader.match(/next-(\w+-)+loader/) && options ) if (entryLoader) { const { page } = parse(entryLoader.options) @@ -161,7 +203,9 @@ export class ChunkGraphPlugin implements Plugin { sharedFiles.push(...files) sharedChunks.push(...involvedChunks) } else { - manifest.chunks[chunk.name] = files + manifest.chunks[chunk.name] = [ + ...new Set([...(manifest.chunks[chunk.name] || []), ...files]), + ].sort() } } }) @@ -174,41 +218,26 @@ export class ChunkGraphPlugin implements Plugin { : name for (const page in pages) { - manifest.pages[page] = [...pages[page], ...sharedFiles] - manifest.pageChunks[page] = [ + manifest.pages[page] = [ ...new Set([ - ...pageChunks[page], - ...pageChunks[page].map(getLambdaChunk), - ...sharedChunks, - ...sharedChunks.map(getLambdaChunk), + ...(manifest.pages[page] || []), + ...pages[page], + ...sharedFiles, ]), ].sort() - } - manifest.hashes = ([...allFiles] as string[]).sort().reduce( - (acc, cur) => - Object.assign( - acc, - fs.existsSync(path.join(dir, cur)) - ? { - [cur]: createHash('sha1') - .update(this.selectivePageBuildingCacheIdentifier) - .update(fs.readFileSync(path.join(dir, cur))) - .digest('hex'), - } - : undefined - ), - {} - ) - - const json = JSON.stringify(manifest, null, 2) + EOL - compilation.assets[this.filename] = { - source() { - return json - }, - size() { - return json.length - }, + // There's no chunks to save from serverless bundles + if (!this.isServer) { + manifest.pageChunks[page] = [ + ...new Set([ + ...(manifest.pageChunks[page] || []), + ...pageChunks[page], + ...pageChunks[page].map(getLambdaChunk), + ...sharedChunks, + ...sharedChunks.map(getLambdaChunk), + ]), + ].sort() + } } }) }