Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Server file inclusive Flying Shuttle #7128

Merged
merged 4 commits into from
Apr 24, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions packages/next/build/index.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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<void> {
Expand Down Expand Up @@ -149,7 +152,6 @@ export default async function build(dir: string, conf = null): Promise<void> {
isServer: false,
config,
target: config.target,
selectivePageBuildingCacheIdentifier,
entrypoints: entrypoints.client,
selectivePageBuilding,
}),
Expand All @@ -159,7 +161,6 @@ export default async function build(dir: string, conf = null): Promise<void> {
isServer: true,
config,
target: config.target,
selectivePageBuildingCacheIdentifier,
entrypoints: entrypoints.server,
selectivePageBuilding,
}),
Expand Down Expand Up @@ -194,6 +195,12 @@ export default async function build(dir: string, conf = null): Promise<void> {

if (isFlyingShuttle) {
console.log()

exportManifest({
dir: dir,
fileName: path.join(distDir, CHUNK_GRAPH_MANIFEST),
selectivePageBuildingCacheIdentifier,
})
}

if (result.errors.length > 0) {
Expand Down
8 changes: 5 additions & 3 deletions packages/next/build/webpack-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -20,7 +20,7 @@ import { importAutoDllPlugin } from './webpack/plugins/dll-import'
import { WebpackEntrypoints } from './entries'
type ExcludesFalse = <T>(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<webpack.Configuration> {
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<webpack.Configuration> {
const distDir = path.join(dir, config.distDir)
const defaultLoaders = {
babel: {
Expand Down Expand Up @@ -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
Expand Down
149 changes: 89 additions & 60 deletions packages/next/build/webpack/plugins/chunk-graph-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>()
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 []
Expand Down Expand Up @@ -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()) {
Expand Down Expand Up @@ -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) {
Expand All @@ -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)
Expand All @@ -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()
}
}
})
Expand All @@ -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()
}
}
})
}
Expand Down