1
0
Fork 0
mirror of https://github.com/terribleplan/next.js.git synced 2024-01-19 02:48:18 +00:00

Enable source maps in webpack chunking + bundling process (#3793)

* Removed combine-assets-plugin, since its very broken

* Bundle everything into app.js on production build

* Clean up

* Removed app.js from server routes

* Renamed app.js -> main.js and removed commons from loading

* Remove commons and react CommonChunks

* Removed the commons route

* Killing the entire build-stats hack for app.js

* Removed unused md5-file package
This commit is contained in:
Tomas Roos 2018-03-06 10:45:29 +01:00 committed by Tim Neutkens
parent a225da4bb1
commit 76582b8e43
9 changed files with 46 additions and 142 deletions

View file

@ -83,7 +83,6 @@
"http-status": "1.0.1", "http-status": "1.0.1",
"json-loader": "0.5.7", "json-loader": "0.5.7",
"loader-utils": "1.1.0", "loader-utils": "1.1.0",
"md5-file": "3.2.3",
"minimist": "1.2.0", "minimist": "1.2.0",
"mkdirp-then": "1.2.0", "mkdirp-then": "1.2.0",
"mv": "2.1.1", "mv": "2.1.1",

View file

@ -5,7 +5,6 @@ import webpack from 'webpack'
import getConfig from '../config' import getConfig from '../config'
import {PHASE_PRODUCTION_BUILD} from '../../lib/constants' import {PHASE_PRODUCTION_BUILD} from '../../lib/constants'
import getBaseWebpackConfig from './webpack' import getBaseWebpackConfig from './webpack'
import md5File from 'md5-file/promise'
export default async function build (dir, conf = null) { export default async function build (dir, conf = null) {
const config = getConfig(PHASE_PRODUCTION_BUILD, dir, conf) const config = getConfig(PHASE_PRODUCTION_BUILD, dir, conf)
@ -26,7 +25,6 @@ export default async function build (dir, conf = null) {
await runCompiler(configs) await runCompiler(configs)
await writeBuildStats(dir, config)
await writeBuildId(dir, buildId, config) await writeBuildId(dir, buildId, config)
} catch (err) { } catch (err) {
console.error(`> Failed to build`) console.error(`> Failed to build`)
@ -54,20 +52,6 @@ function runCompiler (compiler) {
}) })
} }
async function writeBuildStats (dir, config) {
// Here we can't use hashes in webpack chunks.
// That's because the "app.js" is not tied to a chunk.
// It's created by merging a few assets. (commons.js and main.js)
// So, we need to generate the hash ourself.
const assetHashMap = {
'app.js': {
hash: await md5File(join(dir, config.distDir, 'app.js'))
}
}
const buildStatsPath = join(dir, config.distDir, 'build-stats.json')
await fs.writeFile(buildStatsPath, JSON.stringify(assetHashMap), 'utf8')
}
async function writeBuildId (dir, buildId, config) { async function writeBuildId (dir, buildId, config) {
const buildIdPath = join(dir, config.distDir, 'BUILD_ID') const buildIdPath = join(dir, config.distDir, 'BUILD_ID')
await fs.writeFile(buildIdPath, buildId, 'utf8') await fs.writeFile(buildIdPath, buildId, 'utf8')

View file

@ -1,33 +0,0 @@
import { ConcatSource } from 'webpack-sources'
// This plugin combines a set of assets into a single asset
// This should be only used with text assets,
// otherwise the result is unpredictable.
export default class CombineAssetsPlugin {
constructor ({ input, output }) {
this.input = input
this.output = output
}
apply (compiler) {
compiler.plugin('compilation', (compilation) => {
// This is triggered after uglify and other optimizers have ran.
compilation.plugin('after-optimize-chunk-assets', (chunks) => {
const concat = new ConcatSource()
this.input.forEach((name) => {
const asset = compilation.assets[name]
if (!asset) return
// We add each matched asset from this.input to a new bundle
concat.add(asset)
// The original assets are kept because they show up when analyzing the bundle using webpack-bundle-analyzer
// See https://github.com/zeit/next.js/tree/canary/examples/with-webpack-bundle-analyzer
})
// Creates a new asset holding the concatted source
compilation.assets[this.output] = concat
})
})
}
}

View file

@ -6,7 +6,6 @@ import CaseSensitivePathPlugin from 'case-sensitive-paths-webpack-plugin'
import WriteFilePlugin from 'write-file-webpack-plugin' import WriteFilePlugin from 'write-file-webpack-plugin'
import FriendlyErrorsWebpackPlugin from 'friendly-errors-webpack-plugin' import FriendlyErrorsWebpackPlugin from 'friendly-errors-webpack-plugin'
import {getPages} from './webpack/utils' import {getPages} from './webpack/utils'
import CombineAssetsPlugin from './plugins/combine-assets-plugin'
import PagesPlugin from './plugins/pages-plugin' import PagesPlugin from './plugins/pages-plugin'
import NextJsSsrImportPlugin from './plugins/nextjs-ssr-import' import NextJsSsrImportPlugin from './plugins/nextjs-ssr-import'
import DynamicChunksPlugin from './plugins/dynamic-chunks-plugin' import DynamicChunksPlugin from './plugins/dynamic-chunks-plugin'
@ -248,53 +247,17 @@ export default async function getBaseWebpackConfig (dir, {dev = false, isServer
new webpack.DefinePlugin({ new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(dev ? 'development' : 'production') 'process.env.NODE_ENV': JSON.stringify(dev ? 'development' : 'production')
}), }),
!isServer && new CombineAssetsPlugin({
input: ['manifest.js', 'react.js', 'commons.js', 'main.js'],
output: 'app.js'
}),
!dev && new webpack.optimize.ModuleConcatenationPlugin(), !dev && new webpack.optimize.ModuleConcatenationPlugin(),
!isServer && new PagesPlugin(), !isServer && new PagesPlugin(),
!isServer && new DynamicChunksPlugin(), !isServer && new DynamicChunksPlugin(),
isServer && new NextJsSsrImportPlugin(), isServer && new NextJsSsrImportPlugin(),
!isServer && new webpack.optimize.CommonsChunkPlugin({ // In dev mode, we don't move anything to the commons bundle.
name: `commons`, // In production we move common modules into the existing main.js bundle
filename: `commons.js`, !dev && !isServer && new webpack.optimize.CommonsChunkPlugin({
name: 'main.js',
filename: 'main.js',
minChunks (module, count) { minChunks (module, count) {
// We need to move react-dom explicitly into common chunks. // react
// Otherwise, if some other page or module uses it, it might
// included in that bundle too.
if (module.context && module.context.indexOf(`${sep}react${sep}`) >= 0) {
return true
}
if (module.context && module.context.indexOf(`${sep}react-dom${sep}`) >= 0) {
return true
}
// In the dev we use on-demand-entries.
// So, it makes no sense to use commonChunks based on the minChunks count.
// Instead, we move all the code in node_modules into each of the pages.
if (dev) {
return false
}
// If there are one or two pages, only move modules to common if they are
// used in all of the pages. Otherwise, move modules used in at-least
// 1/2 of the total pages into commons.
if (totalPages <= 2) {
return count >= totalPages
}
return count >= totalPages * 0.5
}
}),
!isServer && new webpack.optimize.CommonsChunkPlugin({
name: 'react',
filename: 'react.js',
minChunks (module, count) {
if (dev) {
return false
}
if (module.resource && module.resource.includes(`${sep}react-dom${sep}`) && count >= 0) { if (module.resource && module.resource.includes(`${sep}react-dom${sep}`) && count >= 0) {
return true return true
} }
@ -302,11 +265,21 @@ export default async function getBaseWebpackConfig (dir, {dev = false, isServer
if (module.resource && module.resource.includes(`${sep}react${sep}`) && count >= 0) { if (module.resource && module.resource.includes(`${sep}react${sep}`) && count >= 0) {
return true return true
} }
// react end
return false // commons
// If there are one or two pages, only move modules to common if they are
// used in all of the pages. Otherwise, move modules used in at-least
// 1/2 of the total pages into commons.
if (totalPages <= 2) {
return count >= totalPages
}
return count >= totalPages * 0.5
// commons end
} }
}), }),
!isServer && new webpack.optimize.CommonsChunkPlugin({ // We use a manifest file in development to speed up HMR
dev && !isServer && new webpack.optimize.CommonsChunkPlugin({
name: 'manifest', name: 'manifest',
filename: 'manifest.js' filename: 'manifest.js'
}) })

View file

@ -40,8 +40,8 @@ export class Head extends Component {
getChunkPreloadLink (filename) { getChunkPreloadLink (filename) {
const { __NEXT_DATA__ } = this.context._documentProps const { __NEXT_DATA__ } = this.context._documentProps
let { buildStats, assetPrefix, buildId } = __NEXT_DATA__ let { assetPrefix, buildId } = __NEXT_DATA__
const hash = buildStats ? buildStats[filename].hash : buildId const hash = buildId
return ( return (
<link <link
@ -58,14 +58,13 @@ export class Head extends Component {
if (dev) { if (dev) {
return [ return [
this.getChunkPreloadLink('manifest.js'), this.getChunkPreloadLink('manifest.js'),
this.getChunkPreloadLink('commons.js'),
this.getChunkPreloadLink('main.js') this.getChunkPreloadLink('main.js')
] ]
} }
// In the production mode, we have a single asset with all the JS content. // In the production mode, we have a single asset with all the JS content.
return [ return [
this.getChunkPreloadLink('app.js') this.getChunkPreloadLink('main.js')
] ]
} }
@ -126,8 +125,8 @@ export class NextScript extends Component {
getChunkScript (filename, additionalProps = {}) { getChunkScript (filename, additionalProps = {}) {
const { __NEXT_DATA__ } = this.context._documentProps const { __NEXT_DATA__ } = this.context._documentProps
let { buildStats, assetPrefix, buildId } = __NEXT_DATA__ let { assetPrefix, buildId } = __NEXT_DATA__
const hash = buildStats ? buildStats[filename].hash : buildId const hash = buildId
return ( return (
<script <script
@ -143,14 +142,13 @@ export class NextScript extends Component {
if (dev) { if (dev) {
return [ return [
this.getChunkScript('manifest.js'), this.getChunkScript('manifest.js'),
this.getChunkScript('commons.js'),
this.getChunkScript('main.js') this.getChunkScript('main.js')
] ]
} }
// In the production mode, we have a single asset with all the JS content. // In the production mode, we have a single asset with all the JS content.
// So, we can load the script with async // So, we can load the script with async
return [this.getChunkScript('app.js', { async: true })] return [this.getChunkScript('main.js', { async: true })]
} }
getDynamicChunks () { getDynamicChunks () {

View file

@ -27,20 +27,12 @@ export default async function (dir, options, configuration) {
} }
const buildId = readFileSync(join(nextDir, 'BUILD_ID'), 'utf8') const buildId = readFileSync(join(nextDir, 'BUILD_ID'), 'utf8')
const buildStats = require(join(nextDir, 'build-stats.json'))
// Initialize the output directory // Initialize the output directory
const outDir = options.outdir const outDir = options.outdir
await del(join(outDir, '*')) await del(join(outDir, '*'))
await mkdirp(join(outDir, '_next', buildStats['app.js'].hash))
await mkdirp(join(outDir, '_next', buildId)) await mkdirp(join(outDir, '_next', buildId))
// Copy files
await cp(
join(nextDir, 'app.js'),
join(outDir, '_next', buildStats['app.js'].hash, 'app.js')
)
// Copy static directory // Copy static directory
if (existsSync(join(dir, 'static'))) { if (existsSync(join(dir, 'static'))) {
log(' copying "static" directory') log(' copying "static" directory')
@ -51,6 +43,12 @@ export default async function (dir, options, configuration) {
) )
} }
// Copy main.js
await cp(
join(nextDir, 'main.js'),
join(outDir, '_next', buildId, 'main.js')
)
// Copy .next/static directory // Copy .next/static directory
if (existsSync(join(nextDir, 'static'))) { if (existsSync(join(nextDir, 'static'))) {
log(' copying "static build" directory') log(' copying "static build" directory')
@ -88,7 +86,6 @@ export default async function (dir, options, configuration) {
const renderOpts = { const renderOpts = {
dir, dir,
dist: nextConfig.distDir, dist: nextConfig.distDir,
buildStats,
buildId, buildId,
nextExport: true, nextExport: true,
assetPrefix: nextConfig.assetPrefix.replace(/\/$/, ''), assetPrefix: nextConfig.assetPrefix.replace(/\/$/, ''),

View file

@ -49,8 +49,6 @@ export default class Server {
console.error(`> Could not find a valid build in the '${this.dist}' directory! Try building your app with 'next build' before starting the server.`) console.error(`> Could not find a valid build in the '${this.dist}' directory! Try building your app with 'next build' before starting the server.`)
process.exit(1) process.exit(1)
} }
this.buildStats = !dev ? require(join(this.dir, this.dist, 'build-stats.json')) : null
this.buildId = !dev ? this.readBuildId() : '-' this.buildId = !dev ? this.readBuildId() : '-'
this.renderOpts = { this.renderOpts = {
dev, dev,
@ -58,7 +56,6 @@ export default class Server {
dir: this.dir, dir: this.dir,
dist: this.dist, dist: this.dist,
hotReloader: this.hotReloader, hotReloader: this.hotReloader,
buildStats: this.buildStats,
buildId: this.buildId, buildId: this.buildId,
availableChunks: dev ? {} : getAvailableChunks(this.dir, this.dist) availableChunks: dev ? {} : getAvailableChunks(this.dir, this.dist)
} }
@ -170,27 +167,22 @@ export default class Server {
}, },
'/_next/:hash/main.js': async (req, res, params) => { '/_next/:hash/main.js': async (req, res, params) => {
if (!this.dev) return this.send404(res) if (this.dev) {
this.handleBuildHash('main.js', params.hash, res) this.handleBuildHash('main.js', params.hash, res)
const p = join(this.dir, this.dist, 'main.js') const p = join(this.dir, this.dist, 'main.js')
await this.serveStatic(req, res, p) await this.serveStatic(req, res, p)
}, } else {
const buildId = params.hash
if (!this.handleBuildId(buildId, res)) {
const error = new Error('INVALID_BUILD_ID')
const customFields = { buildIdMismatched: true }
'/_next/:hash/commons.js': async (req, res, params) => { return await renderScriptError(req, res, '/_error', error, customFields, this.renderOpts)
if (!this.dev) return this.send404(res) }
this.handleBuildHash('commons.js', params.hash, res) const p = join(this.dir, this.dist, 'main.js')
const p = join(this.dir, this.dist, 'commons.js')
await this.serveStatic(req, res, p)
},
'/_next/:hash/app.js': async (req, res, params) => {
if (this.dev) return this.send404(res)
this.handleBuildHash('app.js', params.hash, res)
const p = join(this.dir, this.dist, 'app.js')
await this.serveStatic(req, res, p) await this.serveStatic(req, res, p)
}
}, },
'/_next/:buildId/page/:path*.js.map': async (req, res, params) => { '/_next/:buildId/page/:path*.js.map': async (req, res, params) => {
@ -466,10 +458,6 @@ export default class Server {
return true return true
} }
if (hash !== this.buildStats[filename].hash) {
throw new Error(`Invalid Build File Hash(${hash}) for chunk: ${filename}`)
}
res.setHeader('Cache-Control', 'max-age=31536000, immutable') res.setHeader('Cache-Control', 'max-age=31536000, immutable')
return true return true
} }

View file

@ -37,7 +37,6 @@ async function doRender (req, res, pathname, query, {
err, err,
page, page,
buildId, buildId,
buildStats,
hotReloader, hotReloader,
assetPrefix, assetPrefix,
runtimeConfig, runtimeConfig,
@ -108,7 +107,6 @@ async function doRender (req, res, pathname, query, {
pathname, // the requested path pathname, // the requested path
query, query,
buildId, buildId,
buildStats,
assetPrefix, assetPrefix,
runtimeConfig, runtimeConfig,
nextExport, nextExport,

View file

@ -39,11 +39,11 @@ describe('Production Usage', () => {
describe('File locations', () => { describe('File locations', () => {
it('should build the app within the given `dist` directory', () => { it('should build the app within the given `dist` directory', () => {
expect(existsSync(join(__dirname, '/../dist/app.js'))).toBeTruthy() expect(existsSync(join(__dirname, '/../dist/main.js'))).toBeTruthy()
}) })
it('should not build the app within the default `.next` directory', () => { it('should not build the app within the default `.next` directory', () => {
expect(existsSync(join(__dirname, '/../.next/app.js'))).toBeFalsy() expect(existsSync(join(__dirname, '/../.next/main.js'))).toBeFalsy()
}) })
}) })
}) })