diff --git a/.travis.yml b/.travis.yml index 37af205d..d7e835b1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,7 +8,7 @@ } }, language: "node_js", - node_js: ["6"], + node_js: ["8"], cache: { directories: ["node_modules"] }, diff --git a/appveyor.yml b/appveyor.yml index 9b6ee2d9..8ee5e51e 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,6 +1,6 @@ environment: matrix: - - nodejs_version: "6" + - nodejs_version: "8" # Install scripts. (runs after repo cloning) install: diff --git a/build/babel/plugins/handle-import.js b/build/babel/plugins/handle-import.js deleted file mode 100644 index 813687bf..00000000 --- a/build/babel/plugins/handle-import.js +++ /dev/null @@ -1,85 +0,0 @@ -// Based on https://github.com/airbnb/babel-plugin-dynamic-import-webpack -// We've added support for SSR with this version -import template from '@babel/template' -import syntax from '@babel/plugin-syntax-dynamic-import' -import { dirname, resolve, sep } from 'path' -import Crypto from 'crypto' - -const TYPE_IMPORT = 'Import' - -/* - Added "typeof require.resolveWeak !== 'function'" check instead of - "typeof window === 'undefined'" to support dynamic impports in non-webpack environments. - "require.resolveWeak" and "require.ensure" are webpack specific methods. - They would fail in Node/CommonJS environments. -*/ - -const buildImport = (args) => (template(` - ( - typeof require.resolveWeak !== 'function' ? - new (require('next/dynamic').SameLoopPromise)((resolve, reject) => { - eval('require.ensure = function (deps, callback) { callback(require) }') - require.ensure([], (require) => { - let m = require(SOURCE) - m.__webpackChunkName = '${args.name}.js' - resolve(m); - }, 'chunks/${args.name}.js'); - }) - : - new (require('next/dynamic').SameLoopPromise)((resolve, reject) => { - const weakId = require.resolveWeak(SOURCE) - try { - const weakModule = __webpack_require__(weakId) - return resolve(weakModule) - } catch (err) {} - - require.ensure([], (require) => { - try { - let m = require(SOURCE) - m.__webpackChunkName = '${args.name}' - resolve(m) - } catch(error) { - reject(error) - } - }, 'chunks/${args.name}'); - }) - ) -`)) - -export function getModulePath (sourceFilename, moduleName) { - // resolve only if it's a local module - const modulePath = (moduleName[0] === '.') - ? resolve(dirname(sourceFilename), moduleName) : moduleName - - const cleanedModulePath = modulePath - .replace(/(index){0,1}\.js$/, '') // remove .js, index.js - .replace(/[/\\]$/, '') // remove end slash - - return cleanedModulePath -} - -export default () => ({ - inherits: syntax, - - visitor: { - CallExpression (path, state) { - if (path.node.callee.type === TYPE_IMPORT) { - const moduleName = path.node.arguments[0].value - const sourceFilename = state.file.opts.filename - - const modulePath = getModulePath(sourceFilename, moduleName) - const modulePathHash = Crypto.createHash('md5').update(modulePath).digest('hex') - - const relativeModulePath = modulePath.replace(`${process.cwd()}${sep}`, '') - const name = `${relativeModulePath.replace(/[^\w]/g, '_')}_${modulePathHash}` - - const newImport = buildImport({ - name - })({ - SOURCE: path.node.arguments - }) - path.replaceWith(newImport) - } - } - } -}) diff --git a/build/babel/plugins/react-loadable-plugin.js b/build/babel/plugins/react-loadable-plugin.js new file mode 100644 index 00000000..c9f42a67 --- /dev/null +++ b/build/babel/plugins/react-loadable-plugin.js @@ -0,0 +1,125 @@ +// This file is https://github.com/jamiebuilds/react-loadable/blob/master/src/babel.js +// Modified to also look for `next/dynamic` +// Modified to put `webpack` and `modules` under `loadableGenerated` to be backwards compatible with next/dynamic which has a `modules` key +// Modified to support `dynamic(import('something'))` and `dynamic(import('something'), options) +export default function ({ types: t, template }) { + return { + visitor: { + ImportDeclaration (path) { + let source = path.node.source.value + if (source !== 'next/dynamic' && source !== 'react-loadable') return + + let defaultSpecifier = path.get('specifiers').find(specifier => { + return specifier.isImportDefaultSpecifier() + }) + + if (!defaultSpecifier) return + + let bindingName = defaultSpecifier.node.local.name + let binding = path.scope.getBinding(bindingName) + + binding.referencePaths.forEach(refPath => { + let callExpression = refPath.parentPath + + if ( + callExpression.isMemberExpression() && + callExpression.node.computed === false && + callExpression.get('property').isIdentifier({ name: 'Map' }) + ) { + callExpression = callExpression.parentPath + } + + if (!callExpression.isCallExpression()) return + + let args = callExpression.get('arguments') + if (args.length > 2) throw callExpression.error + + let loader + let options + + if (!args[0]) { + return + } + + if (args[0].isCallExpression()) { + if (!args[1]) { + callExpression.pushContainer('arguments', t.objectExpression([])) + } + args = callExpression.get('arguments') + loader = args[0] + options = args[1] + } else { + options = args[0] + } + + if (!options.isObjectExpression()) return + + let properties = options.get('properties') + let propertiesMap = {} + + properties.forEach(property => { + let key = property.get('key') + propertiesMap[key.node.name] = property + }) + + if (propertiesMap.loadableGenerated) { + return + } + + if (propertiesMap.loader) { + loader = propertiesMap.loader.get('value') + } + + if (propertiesMap.modules) { + loader = propertiesMap.modules.get('value') + } + + let loaderMethod = loader + let dynamicImports = [] + + loaderMethod.traverse({ + Import (path) { + dynamicImports.push(path.parentPath) + } + }) + + if (!dynamicImports.length) return + + options.pushContainer( + 'properties', + t.objectProperty( + t.identifier('loadableGenerated'), + t.objectExpression([ + t.objectProperty( + t.identifier('webpack'), + t.arrowFunctionExpression( + [], + t.arrayExpression( + dynamicImports.map(dynamicImport => { + return t.callExpression( + t.memberExpression( + t.identifier('require'), + t.identifier('resolveWeak') + ), + [dynamicImport.get('arguments')[0].node] + ) + }) + ) + ) + ), + t.objectProperty( + t.identifier('modules'), + t.arrayExpression( + dynamicImports.map(dynamicImport => { + return dynamicImport.get('arguments')[0].node + }) + ) + ) + ]) + ) + ) + }) + } + } + } +} diff --git a/build/babel/preset.js b/build/babel/preset.js index 8db67b90..1d9620c3 100644 --- a/build/babel/preset.js +++ b/build/babel/preset.js @@ -33,7 +33,8 @@ module.exports = (context, opts = {}) => ({ ], plugins: [ require('babel-plugin-react-require'), - require('./plugins/handle-import'), + require('@babel/plugin-syntax-dynamic-import'), + require('./plugins/react-loadable-plugin'), [require('@babel/plugin-proposal-class-properties'), opts['class-properties'] || {}], require('@babel/plugin-proposal-object-rest-spread'), [require('@babel/plugin-transform-runtime'), opts['transform-runtime'] || { diff --git a/build/index.js b/build/index.js index cb4f2f67..5e66eaad 100644 --- a/build/index.js +++ b/build/index.js @@ -51,7 +51,7 @@ function runCompiler (compiler) { return reject(error) } - resolve(jsonStats) + resolve() }) }) } diff --git a/build/webpack.js b/build/webpack.js index 084258a4..671a70b9 100644 --- a/build/webpack.js +++ b/build/webpack.js @@ -1,21 +1,27 @@ // @flow import type {NextConfig} from '../server/config' -import path, {sep} from 'path' +import path from 'path' import webpack from 'webpack' import resolve from 'resolve' -import UglifyJSPlugin from 'uglifyjs-webpack-plugin' import CaseSensitivePathPlugin from 'case-sensitive-paths-webpack-plugin' import WriteFilePlugin from 'write-file-webpack-plugin' import FriendlyErrorsWebpackPlugin from 'friendly-errors-webpack-plugin' +import WebpackBar from 'webpackbar' import {getPages} from './webpack/utils' import PagesPlugin from './webpack/plugins/pages-plugin' import NextJsSsrImportPlugin from './webpack/plugins/nextjs-ssr-import' -import DynamicChunksPlugin from './webpack/plugins/dynamic-chunks-plugin' +import NextJsSSRModuleCachePlugin from './webpack/plugins/nextjs-ssr-module-cache' +import NextJsRequireCacheHotReloader from './webpack/plugins/nextjs-require-cache-hot-reloader' import UnlinkFilePlugin from './webpack/plugins/unlink-file-plugin' import PagesManifestPlugin from './webpack/plugins/pages-manifest-plugin' import BuildManifestPlugin from './webpack/plugins/build-manifest-plugin' -import {SERVER_DIRECTORY, NEXT_PROJECT_ROOT, NEXT_PROJECT_ROOT_NODE_MODULES, NEXT_PROJECT_ROOT_DIST, DEFAULT_PAGES_DIR} from '../lib/constants' +import ChunkNamesPlugin from './webpack/plugins/chunk-names-plugin' +import { ReactLoadablePlugin } from './webpack/plugins/react-loadable-plugin' +import {SERVER_DIRECTORY, NEXT_PROJECT_ROOT, NEXT_PROJECT_ROOT_NODE_MODULES, NEXT_PROJECT_ROOT_DIST, DEFAULT_PAGES_DIR, REACT_LOADABLE_MANIFEST} from '../lib/constants' +// The externals config makes sure that +// on the server side when modules are +// in node_modules they don't get compiled by webpack function externalsConfig (dir, isServer) { const externals = [] @@ -50,6 +56,46 @@ function externalsConfig (dir, isServer) { return externals } +function optimizationConfig ({dir, dev, isServer, totalPages}) { + if (isServer) { + return { + // runtimeChunk: 'single', + splitChunks: false, + minimize: false + } + } + + const config: any = { + runtimeChunk: { + name: 'static/commons/runtime.js' + }, + splitChunks: false + } + + if (dev) { + return config + } + + // Only enabled in production + // This logic will create a commons bundle + // with modules that are used in 50% of all pages + return { + ...config, + splitChunks: { + chunks: 'all', + cacheGroups: { + default: false, + vendors: false, + commons: { + name: 'commons', + chunks: 'all', + minChunks: totalPages > 2 ? totalPages * 0.5 : 2 + } + } + } + } +} + type BaseConfigContext = {| dev: boolean, isServer: boolean, @@ -81,22 +127,26 @@ export default async function getBaseWebpackConfig (dir: string, {dev = false, i .split(process.platform === 'win32' ? ';' : ':') .filter((p) => !!p) + const outputPath = path.join(dir, config.distDir, isServer ? SERVER_DIRECTORY : '') const pagesEntries = await getPages(dir, {nextPagesDir: DEFAULT_PAGES_DIR, dev, isServer, pageExtensions: config.pageExtensions.join('|')}) const totalPages = Object.keys(pagesEntries).length const clientEntries = !isServer ? { - 'main.js': [ - dev && !isServer && path.join(NEXT_PROJECT_ROOT_DIST, 'client', 'webpack-hot-middleware-client'), - dev && !isServer && path.join(NEXT_PROJECT_ROOT_DIST, 'client', 'on-demand-entries-client'), + // Backwards compatibility + 'main.js': [], + 'static/commons/main.js': [ path.join(NEXT_PROJECT_ROOT_DIST, 'client', (dev ? `next-dev` : 'next')) ].filter(Boolean) } : {} let webpackConfig = { + mode: dev ? 'development' : 'production', devtool: dev ? 'cheap-module-source-map' : false, name: isServer ? 'server' : 'client', cache: true, target: isServer ? 'node' : 'web', externals: externalsConfig(dir, isServer), + optimization: optimizationConfig({dir, dev, isServer, totalPages}), + recordsPath: path.join(outputPath, 'records.json'), context: dir, // Kept as function to be backwards compatible entry: async () => { @@ -107,11 +157,19 @@ export default async function getBaseWebpackConfig (dir: string, {dev = false, i } }, output: { - path: path.join(dir, config.distDir, isServer ? SERVER_DIRECTORY : ''), - filename: '[name]', + path: outputPath, + filename: ({chunk}) => { + // Use `[name]-[chunkhash].js` in production + if (!dev && (chunk.name === 'static/commons/main.js' || chunk.name === 'static/commons/runtime.js')) { + return chunk.name.replace(/\.js$/, '-' + chunk.renderedHash + '.js') + } + return '[name]' + }, libraryTarget: 'commonjs2', - // This saves chunks with the name given via require.ensure() - chunkFilename: dev ? '[name].js' : '[name]-[chunkhash].js', + hotUpdateChunkFilename: 'static/webpack/[id].[hash].hot-update.js', + hotUpdateMainFilename: 'static/webpack/[hash].hot-update.json', + // This saves chunks with the name given via `import()` + chunkFilename: isServer ? `${dev ? '[name]' : '[chunkhash]'}.js` : `static/chunks/${dev ? '[name]' : '[chunkhash]'}.js`, strictModuleExceptionHandling: true }, performance: { hints: false }, @@ -150,11 +208,21 @@ export default async function getBaseWebpackConfig (dir: string, {dev = false, i ].filter(Boolean) }, plugins: [ - new webpack.IgnorePlugin(/(precomputed)/, /node_modules.+(elliptic)/), - dev && new webpack.NoEmitOnErrorsPlugin(), + // This plugin makes sure `output.filename` is used for entry chunks + new ChunkNamesPlugin(), + !isServer && new ReactLoadablePlugin({ + filename: REACT_LOADABLE_MANIFEST + }), + new WebpackBar({ + name: isServer ? 'server' : 'client' + }), dev && !isServer && new FriendlyErrorsWebpackPlugin(), - dev && new webpack.NamedModulesPlugin(), - dev && !isServer && new webpack.HotModuleReplacementPlugin(), // Hot module replacement + new webpack.IgnorePlugin(/(precomputed)/, /node_modules.+(elliptic)/), + // Even though require.cache is server only we have to clear assets from both compilations + // This is because the client compilation generates the build manifest that's used on the server side + dev && new NextJsRequireCacheHotReloader(), + dev && !isServer && new webpack.HotModuleReplacementPlugin(), + dev && new webpack.NoEmitOnErrorsPlugin(), dev && new UnlinkFilePlugin(), dev && new CaseSensitivePathPlugin(), // Since on macOS the filesystem is case-insensitive this will make sure your path are case-sensitive dev && new WriteFilePlugin({ @@ -163,15 +231,6 @@ export default async function getBaseWebpackConfig (dir: string, {dev = false, i // required not to cache removed files useHashIndex: false }), - !isServer && !dev && new UglifyJSPlugin({ - parallel: true, - sourceMap: false, - uglifyOptions: { - mangle: { - safari10: true - } - } - }), new webpack.DefinePlugin({ 'process.env.NODE_ENV': JSON.stringify(dev ? 'development' : 'production') }), @@ -179,58 +238,8 @@ export default async function getBaseWebpackConfig (dir: string, {dev = false, i isServer && new PagesManifestPlugin(), !isServer && new BuildManifestPlugin(), !isServer && new PagesPlugin(), - !isServer && new DynamicChunksPlugin(), isServer && new NextJsSsrImportPlugin(), - // In dev mode, we don't move anything to the commons bundle. - // In production we move common modules into the existing main.js bundle - !isServer && new webpack.optimize.CommonsChunkPlugin({ - name: 'main.js', - filename: dev ? 'static/commons/main.js' : 'static/commons/main-[chunkhash].js', - minChunks (module, count) { - // React and React DOM are used everywhere in Next.js. So they should always be common. Even in development mode, to speed up compilation. - if (module.resource && module.resource.includes(`${sep}react-dom${sep}`) && count >= 0) { - return true - } - - if (module.resource && module.resource.includes(`${sep}react${sep}`) && count >= 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 - } - - // Check if the module is used in the _app.js bundle - // Because _app.js is used on every page we don't want to - // duplicate them in other bundles. - const chunks = module.getChunks() - const appBundlePath = path.normalize('bundles/pages/_app.js') - const inAppBundle = chunks.some(chunk => chunk.entryModule - ? chunk.entryModule.name === appBundlePath - : null - ) - - if (inAppBundle && chunks.length > 1) { - return true - } - - // 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 - } - }), - // We use a manifest file in development to speed up HMR - dev && !isServer && new webpack.optimize.CommonsChunkPlugin({ - name: 'manifest.js', - filename: dev ? 'static/commons/manifest.js' : 'static/commons/manifest-[chunkhash].js' - }) + isServer && new NextJsSSRModuleCachePlugin({outputPath}) ].filter(Boolean) } @@ -238,5 +247,23 @@ export default async function getBaseWebpackConfig (dir: string, {dev = false, i webpackConfig = config.webpack(webpackConfig, {dir, dev, isServer, buildId, config, defaultLoaders, totalPages}) } + // Backwards compat for `main.js` entry key + const originalEntry = webpackConfig.entry + webpackConfig.entry = async () => { + const entry: any = {...await originalEntry()} + + // Server compilation doesn't have main.js + if (typeof entry['main.js'] !== 'undefined') { + entry['static/commons/main.js'] = [ + ...entry['main.js'], + ...entry['static/commons/main.js'] + ] + + delete entry['main.js'] + } + + return entry + } + return webpackConfig } diff --git a/build/webpack/plugins/build-manifest-plugin.js b/build/webpack/plugins/build-manifest-plugin.js index be2e5fee..a74a6817 100644 --- a/build/webpack/plugins/build-manifest-plugin.js +++ b/build/webpack/plugins/build-manifest-plugin.js @@ -1,15 +1,57 @@ // @flow import { RawSource } from 'webpack-sources' -import {BUILD_MANIFEST} from '../../../lib/constants' +import {BUILD_MANIFEST, ROUTE_NAME_REGEX, IS_BUNDLED_PAGE_REGEX} from '../../../lib/constants' // 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 { apply (compiler: any) { - compiler.plugin('emit', (compilation, callback) => { + compiler.hooks.emit.tapAsync('NextJsBuildManifest', (compilation, callback) => { const {chunks} = compilation const assetMap = {pages: {}, css: []} + const mainJsChunk = chunks.find((c) => c.name === 'static/commons/main.js') + const mainJsFiles = mainJsChunk && mainJsChunk.files.length > 0 ? mainJsChunk.files.filter((file) => /\.js$/.test(file)) : [] + + // 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 + } + + const pagePath = result[1] + + if (!pagePath) { + continue + } + + const filesForEntry = [] + + for (const chunk of entrypoint.chunks) { + // If there's no name + if (!chunk.name || !chunk.files) { + continue + } + + for (const file of chunk.files) { + // Only `.js` files are added for now. In the future we can also handle other file types. + if (/\.map$/.test(file) || /\.hot-update\.js$/.test(file) || !/\.js$/.test(file)) { + continue + } + + // These are manually added to _document.js + if (IS_BUNDLED_PAGE_REGEX.exec(file)) { + continue + } + + filesForEntry.push(file.replace(/\\/g, '/')) + } + } + + assetMap.pages[`/${pagePath.replace(/\\/g, '/')}`] = [...filesForEntry, ...mainJsFiles] + } + for (const chunk of chunks) { if (!chunk.name || !chunk.files) { continue @@ -39,7 +81,11 @@ export default class BuildManifestPlugin { } } - compilation.assets[BUILD_MANIFEST] = new RawSource(JSON.stringify(assetMap)) + if (typeof assetMap.pages['/index'] !== 'undefined') { + assetMap.pages['/'] = assetMap.pages['/index'] + } + + compilation.assets[BUILD_MANIFEST] = new RawSource(JSON.stringify(assetMap, null, 2)) callback() }) } diff --git a/build/webpack/plugins/chunk-names-plugin.js b/build/webpack/plugins/chunk-names-plugin.js new file mode 100644 index 00000000..43e8de4e --- /dev/null +++ b/build/webpack/plugins/chunk-names-plugin.js @@ -0,0 +1,32 @@ +// This plugin mirrors webpack 3 `filename` and `chunkfilename` behavior +// This fixes https://github.com/webpack/webpack/issues/6598 +// This plugin is based on https://github.com/researchgate/webpack/commit/2f28947fa0c63ccbb18f39c0098bd791a2c37090 +export default class ChunkNamesPlugin { + apply (compiler) { + compiler.hooks.compilation.tap('NextJsChunkNamesPlugin', (compilation) => { + compilation.chunkTemplate.hooks.renderManifest.intercept({ + register (tapInfo) { + if (tapInfo.name === 'JavascriptModulesPlugin') { + const originalMethod = tapInfo.fn + tapInfo.fn = (result, options) => { + let filenameTemplate + const chunk = options.chunk + const outputOptions = options.outputOptions + if (chunk.filenameTemplate) { + filenameTemplate = chunk.filenameTemplate + } else if (chunk.hasEntryModule()) { + filenameTemplate = outputOptions.filename + } else { + filenameTemplate = outputOptions.chunkFilename + } + + options.chunk.filenameTemplate = filenameTemplate + return originalMethod(result, options) + } + } + return tapInfo + } + }) + }) + } +} diff --git a/build/webpack/plugins/dynamic-chunks-plugin.js b/build/webpack/plugins/dynamic-chunks-plugin.js deleted file mode 100644 index 02418a06..00000000 --- a/build/webpack/plugins/dynamic-chunks-plugin.js +++ /dev/null @@ -1,48 +0,0 @@ -import { ConcatSource } from 'webpack-sources' - -const isImportChunk = /^chunks[/\\]/ -const matchChunkName = /^chunks[/\\](.*)$/ - -class DynamicChunkTemplatePlugin { - apply (chunkTemplate) { - chunkTemplate.plugin('render', function (modules, chunk) { - if (!isImportChunk.test(chunk.name)) { - return modules - } - - const chunkName = matchChunkName.exec(chunk.name)[1] - const source = new ConcatSource() - - source.add(` - __NEXT_REGISTER_CHUNK('${chunkName}', function() { - `) - source.add(modules) - source.add(` - }) - `) - - return source - }) - } -} - -export default class DynamicChunksPlugin { - apply (compiler) { - compiler.plugin('compilation', (compilation) => { - compilation.chunkTemplate.apply(new DynamicChunkTemplatePlugin()) - - compilation.plugin('additional-chunk-assets', (chunks) => { - chunks = chunks.filter(chunk => - isImportChunk.test(chunk.name) && compilation.assets[chunk.name] - ) - - chunks.forEach((chunk) => { - // This is to support, webpack dynamic import support with HMR - const copyFilename = `chunks/${chunk.name}` - compilation.additionalChunkAssets.push(copyFilename) - compilation.assets[copyFilename] = compilation.assets[chunk.name] - }) - }) - }) - } -} diff --git a/build/webpack/plugins/nextjs-require-cache-hot-reloader.js b/build/webpack/plugins/nextjs-require-cache-hot-reloader.js new file mode 100644 index 00000000..4a5a39dc --- /dev/null +++ b/build/webpack/plugins/nextjs-require-cache-hot-reloader.js @@ -0,0 +1,32 @@ +// @flow + +function deleteCache (path: string) { + delete require.cache[path] +} + +// This plugin flushes require.cache after emitting the files. Providing 'hot reloading' of server files. +export default class ChunkNamesPlugin { + prevAssets: null | {[string]: {existsAt: string}} + constructor () { + this.prevAssets = null + } + apply (compiler: any) { + compiler.hooks.afterEmit.tapAsync('NextJsRequireCacheHotReloader', (compilation, callback) => { + const { assets } = compilation + + if (this.prevAssets) { + for (const f of Object.keys(assets)) { + deleteCache(assets[f].existsAt) + } + for (const f of Object.keys(this.prevAssets)) { + if (!assets[f]) { + deleteCache(this.prevAssets[f].existsAt) + } + } + } + this.prevAssets = assets + + callback() + }) + } +} diff --git a/build/webpack/plugins/nextjs-ssr-import.js b/build/webpack/plugins/nextjs-ssr-import.js index f9cc0b3b..f40e7db3 100644 --- a/build/webpack/plugins/nextjs-ssr-import.js +++ b/build/webpack/plugins/nextjs-ssr-import.js @@ -4,8 +4,8 @@ import { join, resolve, relative, dirname } from 'path' // to work with Next.js SSR export default class NextJsSsrImportPlugin { apply (compiler) { - compiler.plugin('compilation', (compilation) => { - compilation.mainTemplate.plugin('require-ensure', (code, chunk) => { + compiler.hooks.compilation.tap('NextJsSSRImport', (compilation) => { + compilation.mainTemplate.hooks.requireEnsure.tap('NextJsSSRImport', (code, chunk) => { // Update to load chunks from our custom chunks directory const outputPath = resolve('/') const pagePath = join('/', dirname(chunk.name)) @@ -13,16 +13,7 @@ export default class NextJsSsrImportPlugin { // Make sure even in windows, the path looks like in unix // Node.js require system will convert it accordingly const relativePathToBaseDirNormalized = relativePathToBaseDir.replace(/\\/g, '/') - let updatedCode = code.replace('require("./"', `require("${relativePathToBaseDirNormalized}/"`) - - // Replace a promise equivalent which runs in the same loop - // If we didn't do this webpack's module loading process block us from - // doing SSR for chunks - updatedCode = updatedCode.replace( - 'return Promise.resolve();', - `return require('next/dynamic').SameLoopPromise.resolve();` - ) - return updatedCode + return code.replace('require("./"', `require("${relativePathToBaseDirNormalized}/"`) }) }) } diff --git a/build/webpack/plugins/nextjs-ssr-module-cache.js b/build/webpack/plugins/nextjs-ssr-module-cache.js new file mode 100644 index 00000000..176c910f --- /dev/null +++ b/build/webpack/plugins/nextjs-ssr-module-cache.js @@ -0,0 +1,52 @@ +import webpack from 'webpack' +import { RawSource } from 'webpack-sources' +import { join, relative, dirname } from 'path' + +const SSR_MODULE_CACHE_FILENAME = 'ssr-module-cache.js' + +// By default webpack keeps initialized modules per-module. +// This means that if you have 2 entrypoints loaded into the same app +// they will *not* share the same instance +// This creates many issues when developers / libraries rely on the singleton pattern +// As this pattern assumes every module will have 1 instance +// This plugin overrides webpack's code generation step to replace `installedModules` +// The replacement is a require for a file that's also generated here that only exports an empty object +// Because of Node.js's single instance modules this makes webpack share all initialized instances +// Do note that this module is only geared towards the `node` compilation target. +// For the client side compilation we use `runtimeChunk: 'single'` +export default class NextJsSsrImportPlugin { + constructor (options) { + this.options = options + } + apply (compiler) { + const {outputPath} = this.options + compiler.hooks.emit.tapAsync('NextJsSSRModuleCache', (compilation, callback) => { + compilation.assets[SSR_MODULE_CACHE_FILENAME] = new RawSource(` + /* This cache is used by webpack for instantiated modules */ + module.exports = {} + `) + callback() + }) + compiler.hooks.compilation.tap('NextJsSSRModuleCache', (compilation) => { + compilation.mainTemplate.hooks.localVars.intercept({ + register (tapInfo) { + if (tapInfo.name === 'MainTemplate') { + tapInfo.fn = (source, chunk) => { + const pagePath = join(outputPath, dirname(chunk.name)) + const relativePathToBaseDir = relative(pagePath, join(outputPath, SSR_MODULE_CACHE_FILENAME)) + // Make sure even in windows, the path looks like in unix + // Node.js require system will convert it accordingly + const relativePathToBaseDirNormalized = relativePathToBaseDir.replace(/\\/g, '/') + return webpack.Template.asString([ + source, + '// The module cache', + `var installedModules = require('${relativePathToBaseDirNormalized}');` + ]) + } + } + return tapInfo + } + }) + }) + } +} diff --git a/build/webpack/plugins/pages-manifest-plugin.js b/build/webpack/plugins/pages-manifest-plugin.js index 3b497479..9034539c 100644 --- a/build/webpack/plugins/pages-manifest-plugin.js +++ b/build/webpack/plugins/pages-manifest-plugin.js @@ -7,7 +7,7 @@ import {PAGES_MANIFEST, ROUTE_NAME_REGEX} from '../../../lib/constants' // It's also used by next export to provide defaultPathMap export default class PagesManifestPlugin { apply (compiler: any) { - compiler.plugin('emit', (compilation, callback) => { + compiler.hooks.emit.tapAsync('NextJsPagesManifest', (compilation, callback) => { const {entries} = compilation const pages = {} diff --git a/build/webpack/plugins/pages-plugin.js b/build/webpack/plugins/pages-plugin.js index 537ec77a..316df442 100644 --- a/build/webpack/plugins/pages-plugin.js +++ b/build/webpack/plugins/pages-plugin.js @@ -5,47 +5,41 @@ import { ROUTE_NAME_REGEX } from '../../../lib/constants' -class PageChunkTemplatePlugin { - apply (chunkTemplate) { - chunkTemplate.plugin('render', function (modules, chunk) { - if (!IS_BUNDLED_PAGE_REGEX.test(chunk.name)) { - return modules - } - - let routeName = ROUTE_NAME_REGEX.exec(chunk.name)[1] - - // We need to convert \ into / when we are in windows - // to get the proper route name - // Here we need to do windows check because it's possible - // to have "\" in the filename in unix. - // Anyway if someone did that, he'll be having issues here. - // But that's something we cannot avoid. - if (/^win/.test(process.platform)) { - routeName = routeName.replace(/\\/g, '/') - } - - routeName = `/${routeName.replace(/(^|\/)index$/, '')}` - - const source = new ConcatSource() - - source.add(`__NEXT_REGISTER_PAGE('${routeName}', function() { - var comp = - `) - source.add(modules) - source.add(` - return { page: comp.default } - }) - `) - - return source - }) - } -} - export default class PagesPlugin { apply (compiler: any) { - compiler.plugin('compilation', (compilation) => { - compilation.chunkTemplate.apply(new PageChunkTemplatePlugin()) + compiler.hooks.compilation.tap('PagesPlugin', (compilation) => { + compilation.chunkTemplate.hooks.render.tap('PagesPluginRenderPageRegister', (modules, chunk) => { + if (!IS_BUNDLED_PAGE_REGEX.test(chunk.name)) { + return modules + } + + let routeName = ROUTE_NAME_REGEX.exec(chunk.name)[1] + + // We need to convert \ into / when we are in windows + // to get the proper route name + // Here we need to do windows check because it's possible + // to have "\" in the filename in unix. + // Anyway if someone did that, he'll be having issues here. + // But that's something we cannot avoid. + if (/^win/.test(process.platform)) { + routeName = routeName.replace(/\\/g, '/') + } + + routeName = `/${routeName.replace(/(^|\/)index$/, '')}` + + const source = new ConcatSource() + + source.add(`__NEXT_REGISTER_PAGE('${routeName}', function() { + var comp = + `) + source.add(modules) + source.add(` + return { page: comp.default } + }) + `) + + return source + }) }) } } diff --git a/build/webpack/plugins/react-loadable-plugin.js b/build/webpack/plugins/react-loadable-plugin.js new file mode 100644 index 00000000..e4796409 --- /dev/null +++ b/build/webpack/plugins/react-loadable-plugin.js @@ -0,0 +1,58 @@ +// Implementation of this PR: https://github.com/jamiebuilds/react-loadable/pull/132 +// Modified to strip out unneeded results for Next's specific use case +const url = require('url') + +function buildManifest (compiler, compilation) { + let context = compiler.options.context + let manifest = {} + + compilation.chunks.forEach(chunk => { + chunk.files.forEach(file => { + for (const module of chunk.modulesIterable) { + let id = module.id + let name = typeof module.libIdent === 'function' ? module.libIdent({ context }) : null + // If it doesn't end in `.js` Next.js can't handle it right now. + if (!file.match(/\.js$/) || !file.match(/^static\/chunks\//)) { + return + } + let publicPath = url.resolve(compilation.outputOptions.publicPath || '', file) + + let currentModule = module + if (module.constructor.name === 'ConcatenatedModule') { + currentModule = module.rootModule + } + if (!manifest[currentModule.rawRequest]) { + manifest[currentModule.rawRequest] = [] + } + + manifest[currentModule.rawRequest].push({ id, name, file, publicPath }) + } + }) + }) + + return manifest +} + +class ReactLoadablePlugin { + constructor (opts = {}) { + this.filename = opts.filename + } + + apply (compiler) { + compiler.hooks.emit.tapAsync('ReactLoadableManifest', (compilation, callback) => { + const manifest = buildManifest(compiler, compilation) + var json = JSON.stringify(manifest, null, 2) + compilation.assets[this.filename] = { + source () { + return json + }, + size () { + return json.length + } + } + callback() + }) + } +} + +exports.ReactLoadablePlugin = ReactLoadablePlugin diff --git a/build/webpack/plugins/unlink-file-plugin.js b/build/webpack/plugins/unlink-file-plugin.js index 11522038..7e98f75f 100644 --- a/build/webpack/plugins/unlink-file-plugin.js +++ b/build/webpack/plugins/unlink-file-plugin.js @@ -6,6 +6,7 @@ import { IS_BUNDLED_PAGE_REGEX } from '../../../lib/constants' const unlink = promisify(fs.unlink) +// Makes sure removed pages are removed from `.next` in development export default class UnlinkFilePlugin { prevAssets: any constructor () { @@ -13,7 +14,7 @@ export default class UnlinkFilePlugin { } apply (compiler: any) { - compiler.plugin('after-emit', (compilation, callback) => { + compiler.hooks.afterEmit.tapAsync('NextJsUnlinkRemovedPages', (compilation, callback) => { const removed = Object.keys(this.prevAssets) .filter((a) => IS_BUNDLED_PAGE_REGEX.test(a) && !compilation.assets[a]) diff --git a/build/webpack/utils.js b/build/webpack/utils.js index 76f75271..f4a11e5b 100644 --- a/build/webpack/utils.js +++ b/build/webpack/utils.js @@ -15,8 +15,7 @@ export async function getPagePaths (dir, {dev, isServer, pageExtensions}) { if (dev) { // In development we only compile _document.js, _error.js and _app.js when starting, since they're always needed. All other pages are compiled with on demand entries - // _document also has to be in the client compiler in development because we want to detect HMR changes and reload the client - pages = await glob(`pages/+(_document|_app|_error).+(${pageExtensions})`, { cwd: dir }) + pages = await glob(isServer ? `pages/+(_document|_app|_error).+(${pageExtensions})` : `pages/+(_app|_error).+(${pageExtensions})`, { cwd: dir }) } else { // In production get all pages from the pages directory pages = await glob(isServer ? `pages/**/*.+(${pageExtensions})` : `pages/**/!(_document)*.+(${pageExtensions})`, { cwd: dir }) diff --git a/client/dev-error-overlay.js b/client/dev-error-overlay.js deleted file mode 100644 index b85867c0..00000000 --- a/client/dev-error-overlay.js +++ /dev/null @@ -1,41 +0,0 @@ -// @flow -import React from 'react' -import {applySourcemaps} from './source-map-support' -import ErrorDebug, {styles} from '../lib/error-debug' -import type {RuntimeError, ErrorReporterProps} from './error-boundary' - -type State = {| - mappedError: null | RuntimeError -|} - -// This component is only used in development, sourcemaps are applied on the fly because componentDidCatch is not async -class DevErrorOverlay extends React.Component { - state = { - mappedError: null - } - - componentDidMount () { - const {error} = this.props - - // Since componentDidMount doesn't handle errors we use then/catch here - applySourcemaps(error).then(() => { - this.setState({mappedError: error}) - }).catch((caughtError) => { - this.setState({mappedError: caughtError}) - }) - } - - render () { - const {mappedError} = this.state - const {info} = this.props - if (mappedError === null) { - return
-

Loading stacktrace...

-
- } - - return - } -} - -export default DevErrorOverlay diff --git a/client/dev-error-overlay/eventsource.js b/client/dev-error-overlay/eventsource.js new file mode 100644 index 00000000..90191cd9 --- /dev/null +++ b/client/dev-error-overlay/eventsource.js @@ -0,0 +1,59 @@ +function EventSourceWrapper (options) { + var source + var lastActivity = new Date() + var listeners = [] + + if (!options.timeout) { + options.timeout = 20 * 1000 + } + + init() + var timer = setInterval(function () { + if ((new Date() - lastActivity) > options.timeout) { + handleDisconnect() + } + }, options.timeout / 2) + + function init () { + source = new window.EventSource(options.path) + source.onopen = handleOnline + source.onerror = handleDisconnect + source.onmessage = handleMessage + } + + function handleOnline () { + if (options.log) console.log('[HMR] connected') + lastActivity = new Date() + } + + function handleMessage (event) { + lastActivity = new Date() + for (var i = 0; i < listeners.length; i++) { + listeners[i](event) + } + } + + function handleDisconnect () { + clearInterval(timer) + source.close() + setTimeout(init, options.timeout) + } + + return { + addMessageListener: function (fn) { + listeners.push(fn) + } + } +} + +export function getEventSourceWrapper (options) { + if (!window.__whmEventSourceWrapper) { + window.__whmEventSourceWrapper = {} + } + if (!window.__whmEventSourceWrapper[options.path]) { + // cache the wrapper for other entries loaded on + // the same page with the same options.path + window.__whmEventSourceWrapper[options.path] = EventSourceWrapper(options) + } + return window.__whmEventSourceWrapper[options.path] +} diff --git a/client/dev-error-overlay/format-webpack-messages.js b/client/dev-error-overlay/format-webpack-messages.js new file mode 100644 index 00000000..2814d300 --- /dev/null +++ b/client/dev-error-overlay/format-webpack-messages.js @@ -0,0 +1,133 @@ +// This file is based on https://github.com/facebook/create-react-app/blob/v1.1.4/packages/react-dev-utils/formatWebpackMessages.js +// It's been edited to remove chalk + +'use strict' + +// WARNING: this code is untranspiled and is used in browser too. +// Please make sure any changes are in ES5 or contribute a Babel compile step. + +// Some custom utilities to prettify Webpack output. +// This is quite hacky and hopefully won't be needed when Webpack fixes this. +// https://github.com/webpack/webpack/issues/2878 + +var friendlySyntaxErrorLabel = 'Syntax error:' + +function isLikelyASyntaxError (message) { + return message.indexOf(friendlySyntaxErrorLabel) !== -1 +} + +// Cleans up webpack error messages. +// eslint-disable-next-line no-unused-vars +function formatMessage (message, isError) { + var lines = message.split('\n') + + if (lines.length > 2 && lines[1] === '') { + // Remove extra newline. + lines.splice(1, 1) + } + + // Remove webpack-specific loader notation from filename. + // Before: + // ./~/css-loader!./~/postcss-loader!./src/App.css + // After: + // ./src/App.css + if (lines[0].lastIndexOf('!') !== -1) { + lines[0] = lines[0].substr(lines[0].lastIndexOf('!') + 1) + } + + // Remove unnecessary stack added by `thread-loader` + var threadLoaderIndex = -1 + lines.forEach(function (line, index) { + if (threadLoaderIndex !== -1) { + return + } + if (/thread.loader/i.test(line)) { + threadLoaderIndex = index + } + }) + + if (threadLoaderIndex !== -1) { + lines = lines.slice(0, threadLoaderIndex) + } + + lines = lines.filter(function (line) { + // Webpack adds a list of entry points to warning messages: + // @ ./src/index.js + // @ multi react-scripts/~/react-dev-utils/webpackHotDevClient.js ... + // It is misleading (and unrelated to the warnings) so we clean it up. + // It is only useful for syntax errors but we have beautiful frames for them. + return line.indexOf(' @ ') !== 0 + }) + + // line #0 is filename + // line #1 is the main error message + if (!lines[0] || !lines[1]) { + return lines.join('\n') + } + + // Cleans up verbose "module not found" messages for files and packages. + if (lines[1].indexOf('Module not found: ') === 0) { + lines = [ + lines[0], + // Clean up message because "Module not found: " is descriptive enough. + lines[1] + .replace("Cannot resolve 'file' or 'directory' ", '') + .replace('Cannot resolve module ', '') + .replace('Error: ', '') + .replace('[CaseSensitivePathsPlugin] ', '') + ] + } + + // Cleans up syntax error messages. + if (lines[1].indexOf('Module build failed: ') === 0) { + lines[1] = lines[1].replace( + 'Module build failed: SyntaxError:', + friendlySyntaxErrorLabel + ) + } + + // Clean up export errors. + // TODO: we should really send a PR to Webpack for this. + var exportError = /\s*(.+?)\s*(")?export '(.+?)' was not found in '(.+?)'/ + if (lines[1].match(exportError)) { + lines[1] = lines[1].replace( + exportError, + "$1 '$4' does not contain an export named '$3'." + ) + } + + // Reassemble the message. + message = lines.join('\n') + // Internal stacks are generally useless so we strip them... with the + // exception of stacks containing `webpack:` because they're normally + // from user code generated by WebPack. For more information see + // https://github.com/facebook/create-react-app/pull/1050 + message = message.replace( + /^\s*at\s((?!webpack:).)*:\d+:\d+[\s)]*(\n|$)/gm, + '' + ) // at ... ...:x:y + + return message.trim() +} + +function formatWebpackMessages (json) { + var formattedErrors = json.errors.map(function (message) { + return formatMessage(message, true) + }) + var formattedWarnings = json.warnings.map(function (message) { + return formatMessage(message, false) + }) + var result = { + errors: formattedErrors, + warnings: formattedWarnings + } + if (result.errors.some(isLikelyASyntaxError)) { + // If there are any syntax errors, show just them. + // This prevents a confusing ESLint parsing error + // preceding a much more useful Babel syntax error. + result.errors = result.errors.filter(isLikelyASyntaxError) + } + return result +} + +module.exports = formatWebpackMessages diff --git a/client/dev-error-overlay/hot-dev-client.js b/client/dev-error-overlay/hot-dev-client.js new file mode 100644 index 00000000..61010924 --- /dev/null +++ b/client/dev-error-overlay/hot-dev-client.js @@ -0,0 +1,273 @@ +/* eslint-disable camelcase */ +// This file is based on https://github.com/facebook/create-react-app/blob/v1.1.4/packages/react-dev-utils/webpackHotDevClient.js +// It's been edited to rely on webpack-hot-middleware and to be more compatible with SSR / Next.js + +'use strict' +import {getEventSourceWrapper} from './eventsource' +import formatWebpackMessages from './format-webpack-messages' +import * as ErrorOverlay from 'react-error-overlay' +// import url from 'url' +import stripAnsi from 'strip-ansi' +import {rewriteStacktrace} from '../source-map-support' + +const { + distDir +} = window.__NEXT_DATA__ + +// This alternative WebpackDevServer combines the functionality of: +// https://github.com/webpack/webpack-dev-server/blob/webpack-1/client/index.js +// https://github.com/webpack/webpack/blob/webpack-1/hot/dev-server.js + +// It only supports their simplest configuration (hot updates on same server). +// It makes some opinionated choices on top, like adding a syntax error overlay +// that looks similar to our console output. The error overlay is inspired by: +// https://github.com/glenjamin/webpack-hot-middleware + +// This is a modified version of create-react-app's webpackHotDevClient.js +// It implements webpack-hot-middleware's EventSource events instead of webpack-dev-server's websocket. +// https://github.com/facebook/create-react-app/blob/25184c4e91ebabd16fe1cde3d8630830e4a36a01/packages/react-dev-utils/webpackHotDevClient.js + +let hadRuntimeError = false +let customHmrEventHandler +export default function connect (options) { + // We need to keep track of if there has been a runtime error. + // Essentially, we cannot guarantee application state was not corrupted by the + // runtime error. To prevent confusing behavior, we forcibly reload the entire + // application. This is handled below when we are notified of a compile (code + // change). + // See https://github.com/facebook/create-react-app/issues/3096 + ErrorOverlay.startReportingRuntimeErrors({ + onError: function () { + hadRuntimeError = true + }, + filename: '/_next/static/commons/manifest.js' + }) + + if (module.hot && typeof module.hot.dispose === 'function') { + module.hot.dispose(function () { + // TODO: why do we need this? + ErrorOverlay.stopReportingRuntimeErrors() + }) + } + + getEventSourceWrapper(options).addMessageListener((event) => { + // This is the heartbeat event + if (event.data === '\uD83D\uDC93') { + return + } + try { + processMessage(event) + } catch (ex) { + console.warn('Invalid HMR message: ' + event.data + '\n' + ex) + } + }) + + return { + subscribeToHmrEvent (handler) { + customHmrEventHandler = handler + }, + prepareError (err) { + // Temporary workaround for https://github.com/facebook/create-react-app/issues/4760 + // Should be removed once the fix lands + hadRuntimeError = true + // react-error-overlay expects a type of `Error` + const error = new Error(err.message) + error.name = err.name + error.stack = err.stack + rewriteStacktrace(error, distDir) + return error + } + } +} + +// Remember some state related to hot module replacement. +var isFirstCompilation = true +var mostRecentCompilationHash = null +var hasCompileErrors = false + +function clearOutdatedErrors () { + // Clean up outdated compile errors, if any. + if (typeof console !== 'undefined' && typeof console.clear === 'function') { + if (hasCompileErrors) { + console.clear() + } + } +} + +// Successful compilation. +function handleSuccess () { + const isHotUpdate = !isFirstCompilation + isFirstCompilation = false + hasCompileErrors = false + + // Attempt to apply hot updates or reload. + if (isHotUpdate) { + tryApplyUpdates(function onHotUpdateSuccess () { + // Only dismiss it when we're sure it's a hot update. + // Otherwise it would flicker right before the reload. + ErrorOverlay.dismissBuildError() + }) + } +} + +// Compilation with warnings (e.g. ESLint). +function handleWarnings (warnings) { + clearOutdatedErrors() + + // Print warnings to the console. + const formatted = formatWebpackMessages({ + warnings: warnings, + errors: [] + }) + + if (typeof console !== 'undefined' && typeof console.warn === 'function') { + for (let i = 0; i < formatted.warnings.length; i++) { + if (i === 5) { + console.warn( + 'There were more warnings in other files.\n' + + 'You can find a complete log in the terminal.' + ) + break + } + console.warn(stripAnsi(formatted.warnings[i])) + } + } +} + +// Compilation with errors (e.g. syntax error or missing modules). +function handleErrors (errors) { + clearOutdatedErrors() + + isFirstCompilation = false + hasCompileErrors = true + + // "Massage" webpack messages. + var formatted = formatWebpackMessages({ + errors: errors, + warnings: [] + }) + + // Only show the first error. + ErrorOverlay.reportBuildError(formatted.errors[0]) + + // Also log them to the console. + if (typeof console !== 'undefined' && typeof console.error === 'function') { + for (var i = 0; i < formatted.errors.length; i++) { + console.error(stripAnsi(formatted.errors[i])) + } + } +} + +// There is a newer version of the code available. +function handleAvailableHash (hash) { + // Update last known compilation hash. + mostRecentCompilationHash = hash +} + +// Handle messages from the server. +function processMessage (e) { + const obj = JSON.parse(e.data) + switch (obj.action) { + case 'building': { + console.log( + '[HMR] bundle ' + (obj.name ? "'" + obj.name + "' " : '') + + 'rebuilding' + ) + break + } + case 'built': + case 'sync': { + clearOutdatedErrors() + + if (obj.hash) { + handleAvailableHash(obj.hash) + } + + if (obj.warnings.length > 0) { + handleWarnings(obj.warnings) + break + } + + if (obj.errors.length > 0) { + // When there is a compilation error coming from SSR we have to reload the page on next successful compile + if (obj.action === 'sync') { + hadRuntimeError = true + } + handleErrors(obj.errors) + break + } + + handleSuccess() + break + } + default: { + if (customHmrEventHandler) { + customHmrEventHandler(obj) + break + } + break + } + } +} + +// Is there a newer version of this code available? +function isUpdateAvailable () { + /* globals __webpack_hash__ */ + // __webpack_hash__ is the hash of the current compilation. + // It's a global variable injected by Webpack. + return mostRecentCompilationHash !== __webpack_hash__ +} + +// Webpack disallows updates in other states. +function canApplyUpdates () { + return module.hot.status() === 'idle' +} + +// Attempt to update code on the fly, fall back to a hard reload. +async function tryApplyUpdates (onHotUpdateSuccess) { + if (!module.hot) { + // HotModuleReplacementPlugin is not in Webpack configuration. + console.error('HotModuleReplacementPlugin is not in Webpack configuration.') + // window.location.reload(); + return + } + + if (!isUpdateAvailable() || !canApplyUpdates()) { + return + } + + function handleApplyUpdates (err, updatedModules) { + if (err || hadRuntimeError) { + if (err) { + console.warn('Error while applying updates, reloading page', err) + } + if (hadRuntimeError) { + console.warn('Had runtime error previously, reloading page') + } + window.location.reload() + return + } + + if (typeof onHotUpdateSuccess === 'function') { + // Maybe we want to do something. + onHotUpdateSuccess() + } + + if (isUpdateAvailable()) { + // While we were updating, there was a new update! Do it again. + tryApplyUpdates() + } + } + + // https://webpack.github.io/docs/hot-module-replacement.html#check + try { + const updatedModules = await module.hot.check(/* autoApply */ { + ignoreUnaccepted: true + }) + if (updatedModules) { + handleApplyUpdates(null, updatedModules) + } + } catch (err) { + handleApplyUpdates(err, null) + } +} diff --git a/client/error-boundary.js b/client/error-boundary.js index 4dc8cf73..c25cbdb7 100644 --- a/client/error-boundary.js +++ b/client/error-boundary.js @@ -1,66 +1,25 @@ // @flow import * as React from 'react' -import {polyfill} from 'react-lifecycles-compat' type ComponentDidCatchInfo = { componentStack: string } -export type Info = null | ComponentDidCatchInfo - -export type RuntimeError = Error & {| - module: ?{| - rawRequest: string - |} -|} - -export type ErrorReporterProps = {|error: RuntimeError, info: Info|} -type ErrorReporterComponent = React.ComponentType - type Props = {| - ErrorReporter: null | ErrorReporterComponent, - onError: (error: RuntimeError, info: ComponentDidCatchInfo) => void, + onError: (error: Error, info: ComponentDidCatchInfo) => void, children: React.ComponentType<*> |} -type State = {| - error: null | RuntimeError, - info: Info -|} - -class ErrorBoundary extends React.Component { - state = { - error: null, - info: null - } - static getDerivedStateFromProps () { - return { - error: null, - info: null - } - } - componentDidCatch (error: RuntimeError, info: ComponentDidCatchInfo) { +class ErrorBoundary extends React.Component { + componentDidCatch (error: Error, info: ComponentDidCatchInfo) { const {onError} = this.props - - // onError is provided in production - if (onError) { - onError(error, info) - } else { - this.setState({ error, info }) - } + // onError is required + onError(error, info) } render () { - const {ErrorReporter, children} = this.props - const {error, info} = this.state - if (ErrorReporter && error) { - return - } - + const {children} = this.props return React.Children.only(children) } } -// Makes sure we can use React 16.3 lifecycles and still support older versions of React. -polyfill(ErrorBoundary) - export default ErrorBoundary diff --git a/client/index.js b/client/index.js index 2427e264..2707ff40 100644 --- a/client/index.js +++ b/client/index.js @@ -8,9 +8,10 @@ import PageLoader from '../lib/page-loader' import * as asset from '../lib/asset' import * as envConfig from '../lib/runtime-config' import ErrorBoundary from './error-boundary' +import Loadable from 'react-loadable' // Polyfill Promise globally -// This is needed because Webpack2's dynamic loading(common chunks) code +// This is needed because Webpack's dynamic loading(common chunks) code // depends on Promise. // So, we need to polyfill it. // See: https://github.com/webpack/webpack/issues/4254 @@ -26,18 +27,19 @@ const { pathname, query, buildId, - chunks, assetPrefix, runtimeConfig }, location } = window +const prefix = assetPrefix || '' + // With dynamic assetPrefix it's no longer possible to set assetPrefix at the build time // So, this is how we do it in the client side at runtime -__webpack_public_path__ = `${assetPrefix}/_next/webpack/` //eslint-disable-line +__webpack_public_path__ = `${prefix}/_next/` //eslint-disable-line // Initialize next/asset with the assetPrefix -asset.setAssetPrefix(assetPrefix) +asset.setAssetPrefix(prefix) // Initialize next/config with the environment configuration envConfig.setConfig({ serverRuntimeConfig: {}, @@ -46,48 +48,33 @@ envConfig.setConfig({ const asPath = getURL() -const pageLoader = new PageLoader(buildId, assetPrefix) +const pageLoader = new PageLoader(buildId, prefix) window.__NEXT_LOADED_PAGES__.forEach(({ route, fn }) => { pageLoader.registerPage(route, fn) }) delete window.__NEXT_LOADED_PAGES__ - -window.__NEXT_LOADED_CHUNKS__.forEach(({ chunkName, fn }) => { - pageLoader.registerChunk(chunkName, fn) -}) -delete window.__NEXT_LOADED_CHUNKS__ - window.__NEXT_REGISTER_PAGE = pageLoader.registerPage.bind(pageLoader) -window.__NEXT_REGISTER_CHUNK = pageLoader.registerChunk.bind(pageLoader) const headManager = new HeadManager() const appContainer = document.getElementById('__next') const errorContainer = document.getElementById('__next-error') let lastAppProps +let webpackHMR export let router export let ErrorComponent -let DevErrorOverlay let Component let App -let stripAnsi = (s) => s -let applySourcemaps = (e) => e export const emitter = new EventEmitter() export default async ({ - DevErrorOverlay: passedDevErrorOverlay, - stripAnsi: passedStripAnsi, - applySourcemaps: passedApplySourcemaps + webpackHMR: passedWebpackHMR } = {}) => { - // Wait for all the dynamic chunks to get loaded - for (const chunkName of chunks) { - await pageLoader.waitForChunk(chunkName) + // This makes sure this specific line is removed in production + if (process.env.NODE_ENV === 'development') { + webpackHMR = passedWebpackHMR } - - stripAnsi = passedStripAnsi || stripAnsi - applySourcemaps = passedApplySourcemaps || applySourcemaps - DevErrorOverlay = passedDevErrorOverlay ErrorComponent = await pageLoader.loadPage('/_error') App = await pageLoader.loadPage('/_app') @@ -104,6 +91,8 @@ export default async ({ initialErr = error } + await Loadable.preloadReady() + router = createRouter(pathname, query, asPath, { initialProps: props, pageLoader, @@ -132,7 +121,6 @@ export async function render (props) { try { await doRender(props) } catch (err) { - if (err.abort) return await renderError({...props, err}) } } @@ -141,26 +129,15 @@ export async function render (props) { // 404 and 500 errors are special kind of errors // and they are still handle via the main render method. export async function renderError (props) { - const {App, err, errorInfo} = props - - // In development we apply sourcemaps to the error - if (process.env.NODE_ENV !== 'production') { - await applySourcemaps(err) - } - - const str = stripAnsi(`${err.message}\n${err.stack}${errorInfo ? `\n\n${errorInfo.componentStack}` : ''}`) - console.error(str) + const {App, err} = props if (process.env.NODE_ENV !== 'production') { - // We need to unmount the current app component because it's - // in the inconsistant state. - // Otherwise, we need to face issues when the issue is fixed and - // it's get notified via HMR - ReactDOM.unmountComponentAtNode(appContainer) - renderReactElement(, errorContainer) - return + throw webpackHMR.prepareError(err) } + // Make sure we log the error to the console, otherwise users can't track down issues. + console.error(err) + // In production we do a normal render with the `ErrorComponent` as component. // If we've gotten here upon initial render, we can use the props from the server. // Otherwise, we need to call `getInitialProps` on `App` before mounting. @@ -193,25 +170,27 @@ async function doRender ({ App, Component, props, hash, err, emitter: emitterPro // We need to clear any existing runtime error messages ReactDOM.unmountComponentAtNode(errorContainer) - let onError = null - - if (process.env.NODE_ENV !== 'development') { - onError = async (error, errorInfo) => { + // In development runtime errors are caught by react-error-overlay. + if (process.env.NODE_ENV === 'development') { + renderReactElement(( + + ), appContainer) + } else { + // In production we catch runtime errors using componentDidCatch which will trigger renderError. + const onError = async (error) => { try { - await renderError({App, err: error, errorInfo}) + await renderError({App, err: error}) } catch (err) { console.error('Error while rendering error page: ', err) } } + renderReactElement(( + + + + ), appContainer) } - // In development we render a wrapper component that catches runtime errors. - renderReactElement(( - - - - ), appContainer) - emitterProp.emit('after-reactdom-render', { Component, ErrorComponent, appProps }) } diff --git a/client/next-dev.js b/client/next-dev.js index 5e1299b8..7f825903 100644 --- a/client/next-dev.js +++ b/client/next-dev.js @@ -1,16 +1,20 @@ -import stripAnsi from 'strip-ansi' import initNext, * as next from './' -import DevErrorOverlay from './dev-error-overlay' import initOnDemandEntries from './on-demand-entries-client' import initWebpackHMR from './webpack-hot-middleware-client' -import {applySourcemaps} from './source-map-support' + +const { + __NEXT_DATA__: { + assetPrefix + } +} = window + +const prefix = assetPrefix || '' +const webpackHMR = initWebpackHMR({assetPrefix: prefix}) window.next = next - -initNext({ DevErrorOverlay, applySourcemaps, stripAnsi }) +initNext({ webpackHMR }) .then((emitter) => { - initOnDemandEntries() - initWebpackHMR() + initOnDemandEntries({assetPrefix: prefix}) let lastScroll @@ -33,7 +37,6 @@ initNext({ DevErrorOverlay, applySourcemaps, stripAnsi }) lastScroll = null } }) - }) - .catch((err) => { - console.error(stripAnsi(`${err.message}\n${err.stack}`)) + }).catch((err) => { + console.error('Error was not caught', err) }) diff --git a/client/on-demand-entries-client.js b/client/on-demand-entries-client.js index 65ed2c29..d2c656ce 100644 --- a/client/on-demand-entries-client.js +++ b/client/on-demand-entries-client.js @@ -3,20 +3,14 @@ import Router from '../lib/router' import fetch from 'unfetch' -const { - __NEXT_DATA__: { - assetPrefix - } -} = window - -export default () => { +export default ({assetPrefix}) => { Router.ready(() => { Router.router.events.on('routeChangeComplete', ping) }) async function ping () { try { - const url = `${assetPrefix}/_next/on-demand-entries-ping?page=${Router.pathname}` + const url = `${assetPrefix || ''}/_next/on-demand-entries-ping?page=${Router.pathname}` const res = await fetch(url, { credentials: 'omit' }) diff --git a/client/source-map-support.js b/client/source-map-support.js index f29526fe..1ff4a09b 100644 --- a/client/source-map-support.js +++ b/client/source-map-support.js @@ -1,54 +1,27 @@ // @flow -import fetch from 'unfetch' const filenameRE = /\(([^)]+\.js):(\d+):(\d+)\)$/ -export async function applySourcemaps (e: any): Promise { - if (!e || typeof e.stack !== 'string' || e.sourceMapsApplied) { +export function rewriteStacktrace (e: any, distDir: string): void { + if (!e || typeof e.stack !== 'string') { return } const lines = e.stack.split('\n') - const result = await Promise.all(lines.map((line) => { - return rewriteTraceLine(line) - })) + const result = lines.map((line) => { + return rewriteTraceLine(line, distDir) + }) e.stack = result.join('\n') - // This is to make sure we don't apply the sourcemaps twice on the same object - e.sourceMapsApplied = true } -async function rewriteTraceLine (trace: string): Promise { +function rewriteTraceLine (trace: string, distDir: string): string { const m = trace.match(filenameRE) if (m == null) { return trace } - - const filePath = m[1] - if (filePath.match(/node_modules/)) { - return trace - } - - const mapPath = `${filePath}.map` - - const res = await fetch(mapPath) - if (res.status !== 200) { - return trace - } - - const mapContents = await res.json() - const {SourceMapConsumer} = require('source-map') - const map = new SourceMapConsumer(mapContents) - const originalPosition = map.originalPositionFor({ - line: Number(m[2]), - column: Number(m[3]) - }) - - if (originalPosition.source != null) { - const { source, line, column } = originalPosition - const mappedPosition = `(${source.replace(/^webpack:\/\/\//, '')}:${String(line)}:${String(column)})` - return trace.replace(filenameRE, mappedPosition) - } - + const filename = m[1] + const filenameLink = filename.replace(distDir, '/_next/development').replace(/\\/g, '/') + trace = trace.replace(filename, filenameLink) return trace } diff --git a/client/webpack-hot-middleware-client.js b/client/webpack-hot-middleware-client.js index 1fd58e57..8331a398 100644 --- a/client/webpack-hot-middleware-client.js +++ b/client/webpack-hot-middleware-client.js @@ -1,83 +1,73 @@ import 'event-source-polyfill' -import webpackHotMiddlewareClient from 'webpack-hot-middleware/client?autoConnect=false&overlay=false&reload=true' +import connect from './dev-error-overlay/hot-dev-client' import Router from '../lib/router' -const { - __NEXT_DATA__: { - assetPrefix - } -} = window - -export default () => { - webpackHotMiddlewareClient.setOptionsAndConnect({ - path: `${assetPrefix}/_next/webpack-hmr` - }) - - const handlers = { - reload (route) { - if (route === '/_error') { - for (const r of Object.keys(Router.components)) { - const { err } = Router.components[r] - if (err) { - // reload all error routes - // which are expected to be errors of '/_error' routes - Router.reload(r) - } +const handlers = { + reload (route) { + if (route === '/_error') { + for (const r of Object.keys(Router.components)) { + const { err } = Router.components[r] + if (err) { + // reload all error routes + // which are expected to be errors of '/_error' routes + Router.reload(r) } - return } + return + } - // If the App component changes we have to reload the current route - if (route === '/_app') { - Router.reload(Router.route) - return - } + // If the App component changes we have to reload the current route + if (route === '/_app') { + Router.reload(Router.route) + return + } - // Since _document is server only we need to reload the full page when it changes. - if (route === '/_document') { - window.location.reload() - return - } + // Since _document is server only we need to reload the full page when it changes. + if (route === '/_document') { + window.location.reload() + return + } + Router.reload(route) + }, + + change (route) { + // If the App component changes we have to reload the current route + if (route === '/_app') { + Router.reload(Router.route) + return + } + + const { err, Component } = Router.components[route] || {} + + if (err) { + // reload to recover from runtime errors Router.reload(route) - }, + } - change (route) { - // If the App component changes we have to reload the current route - if (route === '/_app') { - Router.reload(Router.route) - return - } + if (Router.route !== route) { + // If this is a not a change for a currently viewing page. + // We don't need to worry about it. + return + } - // Since _document is server only we need to reload the full page when it changes. - if (route === '/_document') { - window.location.reload() - return - } - - const { err, Component } = Router.components[route] || {} - - if (err) { - // reload to recover from runtime errors - Router.reload(route) - } - - if (Router.route !== route) { - // If this is a not a change for a currently viewing page. - // We don't need to worry about it. - return - } - - if (!Component) { - // This only happens when we create a new page without a default export. - // If you removed a default export from a exising viewing page, this has no effect. - console.log(`Hard reloading due to no default component in page: ${route}`) - window.location.reload() - } + if (!Component) { + // This only happens when we create a new page without a default export. + // If you removed a default export from a exising viewing page, this has no effect. + console.warn(`Hard reloading due to no default component in page: ${route}`) + window.location.reload() } } +} - webpackHotMiddlewareClient.subscribe((obj) => { +export default ({assetPrefix}) => { + const options = { + path: `${assetPrefix}/_next/webpack-hmr` + } + + const devClient = connect(options) + + devClient.subscribeToHmrEvent((obj) => { const fn = handlers[obj.action] if (fn) { const data = obj.data || [] @@ -86,4 +76,6 @@ export default () => { throw new Error('Unexpected action ' + obj.action) } }) + + return devClient } diff --git a/flow-typed/npm/glob_vx.x.x.js b/flow-typed/npm/glob_vx.x.x.js new file mode 100644 index 00000000..cc7f947a --- /dev/null +++ b/flow-typed/npm/glob_vx.x.x.js @@ -0,0 +1,46 @@ +// flow-typed signature: 6d6b4e28b1ef5b7419a59c32761d27f5 +// flow-typed version: <>/glob_v7.1.2/flow_v0.73.0 + +/** + * This is an autogenerated libdef stub for: + * + * 'glob' + * + * Fill this stub out by replacing all the `any` types. + * + * Once filled out, we encourage you to share your work with the + * community by sending a pull request to: + * https://github.com/flowtype/flow-typed + */ + +declare module 'glob' { + declare module.exports: any; +} + +/** + * We include stubs for each file inside this npm package in case you need to + * require those files directly. Feel free to delete any files that aren't + * needed. + */ +declare module 'glob/common' { + declare module.exports: any; +} + +declare module 'glob/glob' { + declare module.exports: any; +} + +declare module 'glob/sync' { + declare module.exports: any; +} + +// Filename aliases +declare module 'glob/common.js' { + declare module.exports: $Exports<'glob/common'>; +} +declare module 'glob/glob.js' { + declare module.exports: $Exports<'glob/glob'>; +} +declare module 'glob/sync.js' { + declare module.exports: $Exports<'glob/sync'>; +} diff --git a/flow-typed/npm/react-loadable_vx.x.x.js b/flow-typed/npm/react-loadable_vx.x.x.js new file mode 100644 index 00000000..8d368f32 --- /dev/null +++ b/flow-typed/npm/react-loadable_vx.x.x.js @@ -0,0 +1,60 @@ +// flow-typed signature: 5c815d97bc322b44aa30656fc2619bb0 +// flow-typed version: <>/react-loadable_v5.x/flow_v0.73.0 + +/** + * This is an autogenerated libdef stub for: + * + * 'react-loadable' + * + * Fill this stub out by replacing all the `any` types. + * + * Once filled out, we encourage you to share your work with the + * community by sending a pull request to: + * https://github.com/flowtype/flow-typed + */ + +declare module 'react-loadable' { + declare module.exports: any; +} + +/** + * We include stubs for each file inside this npm package in case you need to + * require those files directly. Feel free to delete any files that aren't + * needed. + */ +declare module 'react-loadable/babel' { + declare module.exports: any; +} + +declare module 'react-loadable/lib/babel' { + declare module.exports: any; +} + +declare module 'react-loadable/lib/index' { + declare module.exports: any; +} + +declare module 'react-loadable/lib/webpack' { + declare module.exports: any; +} + +declare module 'react-loadable/webpack' { + declare module.exports: any; +} + +// Filename aliases +declare module 'react-loadable/babel.js' { + declare module.exports: $Exports<'react-loadable/babel'>; +} +declare module 'react-loadable/lib/babel.js' { + declare module.exports: $Exports<'react-loadable/lib/babel'>; +} +declare module 'react-loadable/lib/index.js' { + declare module.exports: $Exports<'react-loadable/lib/index'>; +} +declare module 'react-loadable/lib/webpack.js' { + declare module.exports: $Exports<'react-loadable/lib/webpack'>; +} +declare module 'react-loadable/webpack.js' { + declare module.exports: $Exports<'react-loadable/webpack'>; +} diff --git a/flow-typed/npm/webpackbar_vx.x.x.js b/flow-typed/npm/webpackbar_vx.x.x.js new file mode 100644 index 00000000..bbf7d791 --- /dev/null +++ b/flow-typed/npm/webpackbar_vx.x.x.js @@ -0,0 +1,60 @@ +// flow-typed signature: 83ca23a55b5361dc350877279882bf56 +// flow-typed version: <>/webpackbar_v2.6.1/flow_v0.73.0 + +/** + * This is an autogenerated libdef stub for: + * + * 'webpackbar' + * + * Fill this stub out by replacing all the `any` types. + * + * Once filled out, we encourage you to share your work with the + * community by sending a pull request to: + * https://github.com/flowtype/flow-typed + */ + +declare module 'webpackbar' { + declare module.exports: any; +} + +/** + * We include stubs for each file inside this npm package in case you need to + * require those files directly. Feel free to delete any files that aren't + * needed. + */ +declare module 'webpackbar/dist/cjs' { + declare module.exports: any; +} + +declare module 'webpackbar/dist/description' { + declare module.exports: any; +} + +declare module 'webpackbar/dist/index' { + declare module.exports: any; +} + +declare module 'webpackbar/dist/profile' { + declare module.exports: any; +} + +declare module 'webpackbar/dist/utils' { + declare module.exports: any; +} + +// Filename aliases +declare module 'webpackbar/dist/cjs.js' { + declare module.exports: $Exports<'webpackbar/dist/cjs'>; +} +declare module 'webpackbar/dist/description.js' { + declare module.exports: $Exports<'webpackbar/dist/description'>; +} +declare module 'webpackbar/dist/index.js' { + declare module.exports: $Exports<'webpackbar/dist/index'>; +} +declare module 'webpackbar/dist/profile.js' { + declare module.exports: $Exports<'webpackbar/dist/profile'>; +} +declare module 'webpackbar/dist/utils.js' { + declare module.exports: $Exports<'webpackbar/dist/utils'>; +} diff --git a/lib/constants.js b/lib/constants.js index cd73adc2..5fe9407f 100644 --- a/lib/constants.js +++ b/lib/constants.js @@ -5,6 +5,7 @@ export const PHASE_PRODUCTION_SERVER = 'phase-production-server' export const PHASE_DEVELOPMENT_SERVER = 'phase-development-server' export const PAGES_MANIFEST = 'pages-manifest.json' export const BUILD_MANIFEST = 'build-manifest.json' +export const REACT_LOADABLE_MANIFEST = 'react-loadable-manifest.json' export const SERVER_DIRECTORY = 'server' export const CONFIG_FILE = 'next.config.js' export const BUILD_ID_FILE = 'BUILD_ID' diff --git a/lib/dynamic.js b/lib/dynamic.js index 18d91543..1fda4be2 100644 --- a/lib/dynamic.js +++ b/lib/dynamic.js @@ -1,262 +1,134 @@ +// @flow +import type {ElementType} from 'react' + import React from 'react' -import { getDisplayName } from './utils' +import Loadable from 'react-loadable' -let currentChunks = new Set() +type ImportedComponent = Promise -export default function dynamicComponent (p, o) { - let promise - let options +type ComponentMapping = {[componentName: string]: ImportedComponent} - if (p instanceof SameLoopPromise) { - promise = p - options = o || {} - } else { - // Now we are trying to use the modules and render fields in options to load modules. - if (!p.modules || !p.render) { - const errorMessage = '`next/dynamic` options should contain `modules` and `render` fields' - throw new Error(errorMessage) - } +type NextDynamicOptions = { + loader?: ComponentMapping | () => ImportedComponent, + loading: ElementType, + timeout?: number, + delay?: number, + ssr?: boolean, + render?: (props: any, loaded: {[componentName: string]: ElementType}) => ElementType, + modules?: () => ComponentMapping, + loadableGenerated?: { + webpack?: any, + modules?: any + } +} - if (o) { - const errorMessage = 'Add additional `next/dynamic` options to the first argument containing the `modules` and `render` fields' - throw new Error(errorMessage) - } +type LoadableOptions = { + loader?: ComponentMapping | () => ImportedComponent, + loading: ElementType, + timeout?: number, + delay?: number, + render?: (props: any, loaded: {[componentName: string]: ElementType}) => ElementType, + webpack?: any, + modules?: any +} - options = p +const isServerSide = typeof window === 'undefined' + +export function noSSR (LoadableInitializer: (loadableOptions: LoadableOptions) => ElementType, loadableOptions: LoadableOptions) { + let LoadableComponent + + // Removing webpack and modules means react-loadable won't try preloading + delete loadableOptions.webpack + delete loadableOptions.modules + + // This check is neccesary to prevent react-loadable from initializing on the server + if (!isServerSide) { + LoadableComponent = LoadableInitializer(loadableOptions) } - return class DynamicComponent extends React.Component { - constructor (...args) { - super(...args) - - this.LoadingComponent = options.loading ? options.loading : () => (

loading...

) - this.ssr = options.ssr === false ? options.ssr : true - - this.state = { AsyncComponent: null, asyncElement: null } - this.isServer = typeof window === 'undefined' - - // This flag is used to load the bundle again, if needed - this.loadBundleAgain = null - // This flag keeps track of the whether we are loading a bundle or not. - this.loadingBundle = false - - if (this.ssr) { - this.load() - } - } - - load () { - if (promise) { - this.loadComponent() - } else { - this.loadBundle(this.props) - } - } - - loadComponent () { - promise.then((m) => { - const AsyncComponent = m.default || m - // Set a readable displayName for the wrapper component - const asyncCompName = getDisplayName(AsyncComponent) - if (asyncCompName) { - DynamicComponent.displayName = `DynamicComponent for ${asyncCompName}` - } - - if (this.mounted) { - this.setState({ AsyncComponent }) - } else { - if (this.isServer) { - registerChunk(m.__webpackChunkName) - } - this.state.AsyncComponent = AsyncComponent - } - }) - } - - loadBundle (props) { - this.loadBundleAgain = null - this.loadingBundle = true - - // Run this for prop changes as well. - const modulePromiseMap = options.modules(props) - const moduleNames = Object.keys(modulePromiseMap) - let remainingPromises = moduleNames.length - const moduleMap = {} - - const renderModules = () => { - if (this.loadBundleAgain) { - this.loadBundle(this.loadBundleAgain) - return - } - - this.loadingBundle = false - DynamicComponent.displayName = 'DynamicBundle' - const asyncElement = options.render(props, moduleMap) - if (this.mounted) { - this.setState({ asyncElement }) - } else { - this.state.asyncElement = asyncElement - } - } - - const loadModule = (name) => { - const promise = modulePromiseMap[name] - promise.then((m) => { - const Component = m.default || m - if (this.isServer) { - registerChunk(m.__webpackChunkName) - } - moduleMap[name] = Component - remainingPromises-- - if (remainingPromises === 0) { - renderModules() - } - }) - } - - moduleNames.forEach(loadModule) - } + return class NoSSR extends React.Component { + state = { mounted: false } componentDidMount () { - this.mounted = true - if (!this.ssr) { - this.load() - } - } - - componentWillReceiveProps (nextProps) { - if (promise) return - - this.setState({ asyncElement: null }) - - if (this.loadingBundle) { - this.loadBundleAgain = nextProps - return - } - - this.loadBundle(nextProps) - } - - componentWillUnmount () { - this.mounted = false + this.setState({mounted: true}) } render () { - const { AsyncComponent, asyncElement } = this.state - const { LoadingComponent } = this + const {mounted} = this.state - if (asyncElement) return asyncElement - if (AsyncComponent) return () - - return () - } - } -} - -export function registerChunk (chunk) { - currentChunks.add(chunk) -} - -export function flushChunks () { - const chunks = Array.from(currentChunks) - currentChunks.clear() - return chunks -} - -export class SameLoopPromise { - static resolve (value) { - const promise = new SameLoopPromise((done) => done(value)) - return promise - } - - constructor (cb) { - this.onResultCallbacks = [] - this.onErrorCallbacks = [] - this.cb = cb - } - - setResult (result) { - this.gotResult = true - this.result = result - this.onResultCallbacks.forEach((cb) => cb(result)) - this.onResultCallbacks = [] - } - - setError (error) { - this.gotError = true - this.error = error - this.onErrorCallbacks.forEach((cb) => cb(error)) - this.onErrorCallbacks = [] - } - - then (onResult, onError) { - this.runIfNeeded() - const promise = new SameLoopPromise() - - const handleError = () => { - if (onError) { - promise.setResult(onError(this.error)) - } else { - promise.setError(this.error) + if (mounted && LoadableComponent) { + return } + + // Run loading component on the server and when mounting, when mounted we load the LoadableComponent + return } - - const handleResult = () => { - promise.setResult(onResult(this.result)) - } - - if (this.gotResult) { - handleResult() - return promise - } - - if (this.gotError) { - handleError() - return promise - } - - this.onResultCallbacks.push(handleResult) - this.onErrorCallbacks.push(handleError) - - return promise - } - - catch (onError) { - this.runIfNeeded() - const promise = new SameLoopPromise() - - const handleError = () => { - promise.setResult(onError(this.error)) - } - - const handleResult = () => { - promise.setResult(this.result) - } - - if (this.gotResult) { - handleResult() - return promise - } - - if (this.gotError) { - handleError() - return promise - } - - this.onErrorCallbacks.push(handleError) - this.onResultCallbacks.push(handleResult) - - return promise - } - - runIfNeeded () { - if (!this.cb) return - if (this.ran) return - - this.ran = true - this.cb( - (result) => this.setResult(result), - (error) => this.setError(error) - ) } } + +export default function dynamic (dynamicOptions: any, options: NextDynamicOptions) { + let loadableFn = Loadable + let loadableOptions: NextDynamicOptions = { + // A loading component is not required, so we default it + loading: ({error, isLoading}) => { + if (process.env.NODE_ENV === 'development') { + if (isLoading) { + return

loading...

+ } + if (error) { + return

{error.message}
{error.stack}

+ } + } + + return

loading...

+ } + } + + // Support for direct import(), eg: dynamic(import('../hello-world')) + if (typeof dynamicOptions.then === 'function') { + loadableOptions.loader = () => dynamicOptions + // Support for having first argument being options, eg: dynamic({loader: import('../hello-world')}) + } else if (typeof dynamicOptions === 'object') { + loadableOptions = {...loadableOptions, ...dynamicOptions} + } + + // Support for passing options, eg: dynamic(import('../hello-world'), {loading: () =>

Loading something

}) + loadableOptions = {...loadableOptions, ...options} + + // Support for `render` when using a mapping, eg: `dynamic({ modules: () => {return {HelloWorld: import('../hello-world')}, render(props, loaded) {} } }) + if (dynamicOptions.render) { + loadableOptions.render = (loaded, props) => dynamicOptions.render(props, loaded) + } + // Support for `modules` when using a mapping, eg: `dynamic({ modules: () => {return {HelloWorld: import('../hello-world')}, render(props, loaded) {} } }) + if (dynamicOptions.modules) { + loadableFn = Loadable.Map + const loadModules = {} + const modules = dynamicOptions.modules() + Object.keys(modules).forEach(key => { + const value = modules[key] + if (typeof value.then === 'function') { + loadModules[key] = () => value.then(mod => mod.default || mod) + return + } + loadModules[key] = value + }) + loadableOptions.loader = loadModules + } + + // coming from build/babel/plugins/react-loadable-plugin.js + if (loadableOptions.loadableGenerated) { + loadableOptions = {...loadableOptions, ...loadableOptions.loadableGenerated} + delete loadableOptions.loadableGenerated + } + + // support for disabling server side rendering, eg: dynamic(import('../hello-world'), {ssr: false}) + if (typeof loadableOptions.ssr === 'boolean') { + if (!loadableOptions.ssr) { + delete loadableOptions.ssr + return noSSR(loadableFn, loadableOptions) + } + delete loadableOptions.ssr + } + + return loadableFn(loadableOptions) +} diff --git a/lib/error-debug.js b/lib/error-debug.js index 808b0224..655e38a2 100644 --- a/lib/error-debug.js +++ b/lib/error-debug.js @@ -2,11 +2,10 @@ import React from 'react' import ansiHTML from 'ansi-html' import Head from './head' -import type {ErrorReporterProps} from '../client/error-boundary' // This component is rendered through dev-error-overlay on the client side. // On the server side it's rendered directly -export default function ErrorDebug ({error, info}: ErrorReporterProps) { +export default function ErrorDebug ({error, info}: any) { const { name, message, module } = error return (
@@ -23,7 +22,7 @@ export default function ErrorDebug ({error, info}: ErrorReporterProps) { ) } -const StackTrace = ({ error: { name, message, stack }, info }: ErrorReporterProps) => ( +const StackTrace = ({ error: { name, message, stack }, info }: any) => (
{message || name}
diff --git a/lib/page-loader.js b/lib/page-loader.js
index 40d13449..a2e85b51 100644
--- a/lib/page-loader.js
+++ b/lib/page-loader.js
@@ -12,9 +12,6 @@ export default class PageLoader {
     this.pageLoadedHandlers = {}
     this.pageRegisterEvents = new EventEmitter()
     this.loadingRoutes = {}
-
-    this.chunkRegisterEvents = new EventEmitter()
-    this.loadedChunks = {}
   }
 
   normalizeRoute (route) {
@@ -113,28 +110,6 @@ export default class PageLoader {
     }
   }
 
-  registerChunk (chunkName, regFn) {
-    const chunk = regFn()
-    this.loadedChunks[chunkName] = true
-    this.chunkRegisterEvents.emit(chunkName, chunk)
-  }
-
-  waitForChunk (chunkName, regFn) {
-    const loadedChunk = this.loadedChunks[chunkName]
-    if (loadedChunk) {
-      return Promise.resolve(true)
-    }
-
-    return new Promise((resolve) => {
-      const register = (chunk) => {
-        this.chunkRegisterEvents.off(chunkName, register)
-        resolve(chunk)
-      }
-
-      this.chunkRegisterEvents.on(chunkName, register)
-    })
-  }
-
   clearCache (route) {
     route = this.normalizeRoute(route)
     delete this.pageCache[route]
diff --git a/lib/router/router.js b/lib/router/router.js
index 3cb299f7..c7be3bec 100644
--- a/lib/router/router.js
+++ b/lib/router/router.js
@@ -88,6 +88,12 @@ export default class Router {
     const newData = { ...data, Component }
     this.components[route] = newData
 
+    // pages/_app.js updated
+    if (route === '/_app') {
+      this.notify(this.components[this.route])
+      return
+    }
+
     if (route === this.route) {
       this.notify(newData)
     }
diff --git a/lib/utils.js b/lib/utils.js
index b858c261..22faec6a 100644
--- a/lib/utils.js
+++ b/lib/utils.js
@@ -15,21 +15,24 @@ export function execOnce (fn) {
 }
 
 export function deprecated (fn, message) {
-  if (process.env.NODE_ENV === 'production') return fn
-
-  let warned = false
-  const newFn = function (...args) {
-    if (!warned) {
-      warned = true
-      console.error(message)
+  // else is used here so that webpack/uglify will remove the code block depending on the build environment
+  if (process.env.NODE_ENV === 'production') {
+    return fn
+  } else {
+    let warned = false
+    const newFn = function (...args) {
+      if (!warned) {
+        warned = true
+        console.error(message)
+      }
+      return fn.apply(this, args)
     }
-    return fn.apply(this, args)
+
+    // copy all properties
+    Object.assign(newFn, fn)
+
+    return newFn
   }
-
-  // copy all properties
-  Object.assign(newFn, fn)
-
-  return newFn
 }
 
 export function printAndExit (message, code = 1) {
diff --git a/package.json b/package.json
index 21a20d9e..251d6cd8 100644
--- a/package.json
+++ b/package.json
@@ -69,14 +69,14 @@
     "babel-loader": "8.0.0-beta.3",
     "babel-plugin-react-require": "3.0.0",
     "babel-plugin-transform-react-remove-prop-types": "0.4.13",
-    "case-sensitive-paths-webpack-plugin": "2.1.1",
+    "case-sensitive-paths-webpack-plugin": "2.1.2",
     "cross-spawn": "5.1.0",
     "del": "3.0.0",
     "etag": "1.8.1",
     "event-source-polyfill": "0.0.12",
     "find-up": "2.1.0",
     "fresh": "0.5.2",
-    "friendly-errors-webpack-plugin": "1.6.1",
+    "friendly-errors-webpack-plugin": "1.7.0",
     "glob": "7.1.2",
     "hoist-non-react-statics": "2.5.0",
     "htmlescape": "1.1.1",
@@ -88,25 +88,24 @@
     "path-to-regexp": "2.1.0",
     "prop-types": "15.6.0",
     "prop-types-exact": "1.1.1",
-    "react-lifecycles-compat": "3.0.4",
+    "react-error-overlay": "4.0.0",
+    "react-loadable": "5.4.0",
     "recursive-copy": "2.0.6",
     "resolve": "1.5.0",
     "send": "0.16.1",
     "source-map": "0.5.7",
     "strip-ansi": "3.0.1",
     "styled-jsx": "2.2.7",
-    "touch": "3.1.0",
-    "uglifyjs-webpack-plugin": "1.1.6",
     "unfetch": "3.0.0",
-    "update-check": "1.5.2",
     "url": "0.11.0",
     "uuid": "3.1.0",
     "walk": "2.3.9",
-    "webpack": "3.10.0",
-    "webpack-dev-middleware": "1.12.0",
-    "webpack-hot-middleware": "2.19.1",
+    "webpack": "4.16.1",
+    "webpack-dev-middleware": "3.1.3",
+    "webpack-hot-middleware": "2.22.2",
     "webpack-sources": "1.1.0",
-    "write-file-webpack-plugin": "4.2.0"
+    "webpackbar": "2.6.1",
+    "write-file-webpack-plugin": "4.3.2"
   },
   "devDependencies": {
     "@babel/preset-flow": "7.0.0-beta.43",
diff --git a/readme.md b/readme.md
index d9946f4e..16a0ee7e 100644
--- a/readme.md
+++ b/readme.md
@@ -948,14 +948,12 @@ export default () =>
 import dynamic from 'next/dynamic'
 
 const HelloBundle = dynamic({
-  modules: props => {
+  modules: () => {
     const components = {
       Hello1: import('../components/hello1'),
       Hello2: import('../components/hello2')
     }
 
-    // Add remove components based on props
-
     return components
   },
   render: (props, { Hello1, Hello2 }) =>
diff --git a/server/document.js b/server/document.js
index 5dc0a6c0..92fe56c6 100644
--- a/server/document.js
+++ b/server/document.js
@@ -10,9 +10,9 @@ const Fragment = React.Fragment || function Fragment ({ children }) {
 
 export default class Document extends Component {
   static getInitialProps ({ renderPage }) {
-    const { html, head, errorHtml, chunks, buildManifest } = renderPage()
+    const { html, head, errorHtml, buildManifest } = renderPage()
     const styles = flush()
-    return { html, head, errorHtml, chunks, styles, buildManifest }
+    return { html, head, errorHtml, styles, buildManifest }
   }
 
   static childContextTypes = {
@@ -43,55 +43,39 @@ export class Head extends Component {
     nonce: PropTypes.string
   }
 
-  getChunkPreloadLink (filename) {
-    const { __NEXT_DATA__, buildManifest } = this.context._documentProps
-    let { assetPrefix, buildId } = __NEXT_DATA__
-
-    const files = buildManifest[filename]
-
-    return files.map(file => {
-      return  (
+      
-    })
-  }
-
-  getPreloadMainLinks () {
-    const { dev } = this.context._documentProps
-    if (dev) {
-      return [
-        ...this.getChunkPreloadLink('manifest.js'),
-        ...this.getChunkPreloadLink('main.js')
-      ]
-    }
-
-    // In the production mode, we have a single asset with all the JS content.
-    return [
-      ...this.getChunkPreloadLink('main.js')
-    ]
-  }
-
-  getPreloadDynamicChunks () {
-    const { chunks, __NEXT_DATA__ } = this.context._documentProps
-    let { assetPrefix } = __NEXT_DATA__
-    return chunks.filenames.map((chunk) => (
-      
     ))
   }
 
+  getPreloadDynamicChunks () {
+    const { dynamicImports, assetPrefix } = this.context._documentProps
+    return dynamicImports.map((bundle) => {
+      return