mirror of
https://github.com/terribleplan/next.js.git
synced 2024-01-19 02:48:18 +00:00
[WIP] Webpack 4, react-error-overlay, react-loadable (#4639)
Webpack 4, react-error-overlay, react-loadable (major)
This commit is contained in:
parent
e2b518525c
commit
75476a9136
|
@ -8,7 +8,7 @@
|
|||
}
|
||||
},
|
||||
language: "node_js",
|
||||
node_js: ["6"],
|
||||
node_js: ["8"],
|
||||
cache: {
|
||||
directories: ["node_modules"]
|
||||
},
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
environment:
|
||||
matrix:
|
||||
- nodejs_version: "6"
|
||||
- nodejs_version: "8"
|
||||
|
||||
# Install scripts. (runs after repo cloning)
|
||||
install:
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
125
build/babel/plugins/react-loadable-plugin.js
vendored
Normal file
125
build/babel/plugins/react-loadable-plugin.js
vendored
Normal file
|
@ -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
|
||||
})
|
||||
)
|
||||
)
|
||||
])
|
||||
)
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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'] || {
|
||||
|
|
|
@ -51,7 +51,7 @@ function runCompiler (compiler) {
|
|||
return reject(error)
|
||||
}
|
||||
|
||||
resolve(jsonStats)
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
177
build/webpack.js
177
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
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
})
|
||||
}
|
||||
|
|
32
build/webpack/plugins/chunk-names-plugin.js
Normal file
32
build/webpack/plugins/chunk-names-plugin.js
Normal file
|
@ -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
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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]
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
32
build/webpack/plugins/nextjs-require-cache-hot-reloader.js
Normal file
32
build/webpack/plugins/nextjs-require-cache-hot-reloader.js
Normal file
|
@ -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()
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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}/"`)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
52
build/webpack/plugins/nextjs-ssr-module-cache.js
Normal file
52
build/webpack/plugins/nextjs-ssr-module-cache.js
Normal file
|
@ -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
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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 = {}
|
||||
|
||||
|
|
|
@ -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
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
58
build/webpack/plugins/react-loadable-plugin.js
vendored
Normal file
58
build/webpack/plugins/react-loadable-plugin.js
vendored
Normal file
|
@ -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
|
|
@ -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])
|
||||
|
||||
|
|
|
@ -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 })
|
||||
|
|
|
@ -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<ErrorReporterProps, State> {
|
||||
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 <div style={styles.errorDebug}>
|
||||
<h1 style={styles.heading}>Loading stacktrace...</h1>
|
||||
</div>
|
||||
}
|
||||
|
||||
return <ErrorDebug error={mappedError} info={info} />
|
||||
}
|
||||
}
|
||||
|
||||
export default DevErrorOverlay
|
59
client/dev-error-overlay/eventsource.js
Normal file
59
client/dev-error-overlay/eventsource.js
Normal file
|
@ -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]
|
||||
}
|
133
client/dev-error-overlay/format-webpack-messages.js
Normal file
133
client/dev-error-overlay/format-webpack-messages.js
Normal file
|
@ -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
|
273
client/dev-error-overlay/hot-dev-client.js
Normal file
273
client/dev-error-overlay/hot-dev-client.js
Normal file
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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<ErrorReporterProps>
|
||||
|
||||
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<Props, State> {
|
||||
state = {
|
||||
error: null,
|
||||
info: null
|
||||
}
|
||||
static getDerivedStateFromProps () {
|
||||
return {
|
||||
error: null,
|
||||
info: null
|
||||
}
|
||||
}
|
||||
componentDidCatch (error: RuntimeError, info: ComponentDidCatchInfo) {
|
||||
class ErrorBoundary extends React.Component<Props> {
|
||||
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 <ErrorReporter error={error} info={info} />
|
||||
}
|
||||
|
||||
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
|
||||
|
|
|
@ -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(<DevErrorOverlay error={err} />, 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((
|
||||
<App {...appProps} />
|
||||
), 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((
|
||||
<ErrorBoundary onError={onError}>
|
||||
<App {...appProps} />
|
||||
</ErrorBoundary>
|
||||
), appContainer)
|
||||
}
|
||||
|
||||
// In development we render a wrapper component that catches runtime errors.
|
||||
renderReactElement((
|
||||
<ErrorBoundary ErrorReporter={DevErrorOverlay} onError={onError}>
|
||||
<App {...appProps} />
|
||||
</ErrorBoundary>
|
||||
), appContainer)
|
||||
|
||||
emitterProp.emit('after-reactdom-render', { Component, ErrorComponent, appProps })
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
|
|
|
@ -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'
|
||||
})
|
||||
|
|
|
@ -1,54 +1,27 @@
|
|||
// @flow
|
||||
import fetch from 'unfetch'
|
||||
const filenameRE = /\(([^)]+\.js):(\d+):(\d+)\)$/
|
||||
|
||||
export async function applySourcemaps (e: any): Promise<void> {
|
||||
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<string> {
|
||||
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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
46
flow-typed/npm/glob_vx.x.x.js
vendored
Normal file
46
flow-typed/npm/glob_vx.x.x.js
vendored
Normal file
|
@ -0,0 +1,46 @@
|
|||
// flow-typed signature: 6d6b4e28b1ef5b7419a59c32761d27f5
|
||||
// flow-typed version: <<STUB>>/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'>;
|
||||
}
|
60
flow-typed/npm/react-loadable_vx.x.x.js
vendored
Normal file
60
flow-typed/npm/react-loadable_vx.x.x.js
vendored
Normal file
|
@ -0,0 +1,60 @@
|
|||
// flow-typed signature: 5c815d97bc322b44aa30656fc2619bb0
|
||||
// flow-typed version: <<STUB>>/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'>;
|
||||
}
|
60
flow-typed/npm/webpackbar_vx.x.x.js
vendored
Normal file
60
flow-typed/npm/webpackbar_vx.x.x.js
vendored
Normal file
|
@ -0,0 +1,60 @@
|
|||
// flow-typed signature: 83ca23a55b5361dc350877279882bf56
|
||||
// flow-typed version: <<STUB>>/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'>;
|
||||
}
|
|
@ -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'
|
||||
|
|
360
lib/dynamic.js
360
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<null|ElementType>
|
||||
|
||||
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 : () => (<p>loading...</p>)
|
||||
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<any, {mounted: boolean}> {
|
||||
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 (<AsyncComponent {...this.props} />)
|
||||
|
||||
return (<LoadingComponent {...this.props} />)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 <LoadableComponent {...this.props} />
|
||||
}
|
||||
|
||||
// Run loading component on the server and when mounting, when mounted we load the LoadableComponent
|
||||
return <loadableOptions.loading error={null} isLoading pastDelay={false} timedOut={false} />
|
||||
}
|
||||
|
||||
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 <p>loading...</p>
|
||||
}
|
||||
if (error) {
|
||||
return <p>{error.message}<br />{error.stack}</p>
|
||||
}
|
||||
}
|
||||
|
||||
return <p>loading...</p>
|
||||
}
|
||||
}
|
||||
|
||||
// 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: () => <p>Loading something</p>})
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -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 (
|
||||
<div style={styles.errorDebug}>
|
||||
|
@ -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) => (
|
||||
<div>
|
||||
<div style={styles.heading}>{message || name}</div>
|
||||
<pre style={styles.stack}>
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
29
lib/utils.js
29
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) {
|
||||
|
|
19
package.json
19
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",
|
||||
|
|
|
@ -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 }) =>
|
||||
|
|
|
@ -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 <link
|
||||
key={filename}
|
||||
getPreloadMainLinks () {
|
||||
const { assetPrefix, files } = this.context._documentProps
|
||||
if(!files || files.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return files.map((file) => (
|
||||
<link
|
||||
key={file}
|
||||
nonce={this.props.nonce}
|
||||
rel='preload'
|
||||
href={`${assetPrefix}/_next/${file}`}
|
||||
as='script'
|
||||
/>
|
||||
})
|
||||
}
|
||||
|
||||
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) => (
|
||||
<link
|
||||
key={chunk}
|
||||
rel='preload'
|
||||
href={`${assetPrefix}/_next/webpack/chunks/${chunk}`}
|
||||
as='script'
|
||||
nonce={this.props.nonce}
|
||||
/>
|
||||
))
|
||||
}
|
||||
|
||||
getPreloadDynamicChunks () {
|
||||
const { dynamicImports, assetPrefix } = this.context._documentProps
|
||||
return dynamicImports.map((bundle) => {
|
||||
return <script
|
||||
async
|
||||
key={bundle.file}
|
||||
src={`${assetPrefix}/_next/${bundle.file}`}
|
||||
as='script'
|
||||
nonce={this.props.nonce}
|
||||
/>
|
||||
})
|
||||
}
|
||||
|
||||
render () {
|
||||
const { head, styles, __NEXT_DATA__ } = this.context._documentProps
|
||||
const { page, pathname, buildId, assetPrefix } = __NEXT_DATA__
|
||||
const { head, styles, assetPrefix, __NEXT_DATA__ } = this.context._documentProps
|
||||
const { page, pathname, buildId } = __NEXT_DATA__
|
||||
const pagePathname = getPagePathname(pathname)
|
||||
|
||||
return <head {...this.props}>
|
||||
|
@ -132,84 +116,56 @@ export class NextScript extends Component {
|
|||
_documentProps: PropTypes.any
|
||||
}
|
||||
|
||||
getChunkScript (filename, additionalProps = {}) {
|
||||
const { __NEXT_DATA__, buildManifest } = this.context._documentProps
|
||||
let { assetPrefix, buildId } = __NEXT_DATA__
|
||||
|
||||
const files = buildManifest[filename]
|
||||
|
||||
getScripts () {
|
||||
const { assetPrefix, files } = this.context._documentProps
|
||||
if(!files || files.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return files.map((file) => (
|
||||
<script
|
||||
key={filename}
|
||||
key={file}
|
||||
src={`${assetPrefix}/_next/${file}`}
|
||||
nonce={this.props.nonce}
|
||||
{...additionalProps}
|
||||
async
|
||||
/>
|
||||
))
|
||||
}
|
||||
|
||||
getScripts () {
|
||||
const { dev } = this.context._documentProps
|
||||
if (dev) {
|
||||
return [
|
||||
...this.getChunkScript('manifest.js'),
|
||||
...this.getChunkScript('main.js')
|
||||
]
|
||||
}
|
||||
|
||||
// In the production mode, we have a single asset with all the JS content.
|
||||
// So, we can load the script with async
|
||||
return [...this.getChunkScript('main.js', { async: true })]
|
||||
}
|
||||
|
||||
getDynamicChunks () {
|
||||
const { chunks, __NEXT_DATA__ } = this.context._documentProps
|
||||
let { assetPrefix } = __NEXT_DATA__
|
||||
return (
|
||||
<Fragment>
|
||||
{chunks.filenames.map((chunk) => (
|
||||
<script
|
||||
async
|
||||
key={chunk}
|
||||
src={`${assetPrefix}/_next/webpack/chunks/${chunk}`}
|
||||
const { dynamicImports, assetPrefix } = this.context._documentProps
|
||||
return dynamicImports.map((bundle) => {
|
||||
return <script
|
||||
async
|
||||
key={bundle.file}
|
||||
src={`${assetPrefix}/_next/${bundle.file}`}
|
||||
nonce={this.props.nonce}
|
||||
/>
|
||||
))}
|
||||
</Fragment>
|
||||
)
|
||||
/>
|
||||
})
|
||||
}
|
||||
|
||||
render () {
|
||||
const { staticMarkup, __NEXT_DATA__, chunks } = this.context._documentProps
|
||||
const { page, pathname, buildId, assetPrefix } = __NEXT_DATA__
|
||||
const { staticMarkup, assetPrefix, __NEXT_DATA__ } = this.context._documentProps
|
||||
const { page, pathname, buildId } = __NEXT_DATA__
|
||||
const pagePathname = getPagePathname(pathname)
|
||||
|
||||
__NEXT_DATA__.chunks = chunks.names
|
||||
|
||||
return <Fragment>
|
||||
{staticMarkup ? null : <script nonce={this.props.nonce} dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
__NEXT_DATA__ = ${htmlescape(__NEXT_DATA__)}
|
||||
module={}
|
||||
__NEXT_LOADED_PAGES__ = []
|
||||
__NEXT_LOADED_CHUNKS__ = []
|
||||
|
||||
__NEXT_REGISTER_PAGE = function (route, fn) {
|
||||
__NEXT_LOADED_PAGES__.push({ route: route, fn: fn })
|
||||
}
|
||||
}${page === '_error' ? `
|
||||
|
||||
__NEXT_REGISTER_CHUNK = function (chunkName, fn) {
|
||||
__NEXT_LOADED_CHUNKS__.push({ chunkName: chunkName, fn: fn })
|
||||
}
|
||||
|
||||
${page === '_error' && `
|
||||
__NEXT_REGISTER_PAGE(${htmlescape(pathname)}, function() {
|
||||
var error = new Error('Page does not exist: ${htmlescape(pathname)}')
|
||||
error.statusCode = 404
|
||||
|
||||
return { error: error }
|
||||
})
|
||||
`}
|
||||
})`: ''}
|
||||
`
|
||||
}} />}
|
||||
{page !== '/_error' && <script async id={`__NEXT_PAGE__${pathname}`} src={`${assetPrefix}/_next/${buildId}/page${pagePathname}`} nonce={this.props.nonce} />}
|
||||
|
|
|
@ -7,7 +7,6 @@ import { existsSync, readFileSync, writeFileSync } from 'fs'
|
|||
import loadConfig from './config'
|
||||
import {PHASE_EXPORT, SERVER_DIRECTORY, PAGES_MANIFEST, CONFIG_FILE, BUILD_ID_FILE} from '../lib/constants'
|
||||
import { renderToHTML } from './render'
|
||||
import { getAvailableChunks } from './utils'
|
||||
import { setAssetPrefix } from '../lib/asset'
|
||||
import * as envConfig from '../lib/runtime-config'
|
||||
|
||||
|
@ -90,8 +89,7 @@ export default async function (dir, options, configuration) {
|
|||
distDir,
|
||||
dev: false,
|
||||
staticMarkup: false,
|
||||
hotReloader: null,
|
||||
availableChunks: getAvailableChunks(distDir, false)
|
||||
hotReloader: null
|
||||
}
|
||||
|
||||
const {serverRuntimeConfig, publicRuntimeConfig} = nextConfig
|
||||
|
|
|
@ -1,30 +1,64 @@
|
|||
import { join, relative, sep } from 'path'
|
||||
import { join, relative, sep, normalize } from 'path'
|
||||
import WebpackDevMiddleware from 'webpack-dev-middleware'
|
||||
import WebpackHotMiddleware from 'webpack-hot-middleware'
|
||||
import del from 'del'
|
||||
import onDemandEntryHandler from './on-demand-entry-handler'
|
||||
import onDemandEntryHandler, {normalizePage} from './on-demand-entry-handler'
|
||||
import webpack from 'webpack'
|
||||
import getBaseWebpackConfig from '../build/webpack'
|
||||
import {
|
||||
addCorsSupport
|
||||
} from './utils'
|
||||
import {IS_BUNDLED_PAGE_REGEX} from '../lib/constants'
|
||||
import {IS_BUNDLED_PAGE_REGEX, ROUTE_NAME_REGEX} from '../lib/constants'
|
||||
|
||||
// Recursively look up the issuer till it ends up at the root
|
||||
function findEntryModule (issuer) {
|
||||
if (issuer.issuer) {
|
||||
return findEntryModule(issuer.issuer)
|
||||
}
|
||||
|
||||
return issuer
|
||||
}
|
||||
|
||||
function erroredPages (compilation, options = {enhanceName: (name) => name}) {
|
||||
const failedPages = {}
|
||||
for (const error of compilation.errors) {
|
||||
const entryModule = findEntryModule(error.origin)
|
||||
const {name} = entryModule
|
||||
if (!name) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Only pages have to be reloaded
|
||||
if (!IS_BUNDLED_PAGE_REGEX.test(name)) {
|
||||
continue
|
||||
}
|
||||
|
||||
const enhancedName = options.enhanceName(name)
|
||||
|
||||
if (!failedPages[enhancedName]) {
|
||||
failedPages[enhancedName] = []
|
||||
}
|
||||
|
||||
failedPages[enhancedName].push(error)
|
||||
}
|
||||
|
||||
return failedPages
|
||||
}
|
||||
|
||||
export default class HotReloader {
|
||||
constructor (dir, { quiet, config, buildId } = {}) {
|
||||
this.buildId = buildId
|
||||
this.dir = dir
|
||||
this.quiet = quiet
|
||||
this.middlewares = []
|
||||
this.webpackDevMiddleware = null
|
||||
this.webpackHotMiddleware = null
|
||||
this.initialized = false
|
||||
this.stats = null
|
||||
this.compilationErrors = null
|
||||
this.prevAssets = null
|
||||
this.prevChunkNames = null
|
||||
this.prevFailedChunkNames = null
|
||||
this.prevChunkHashes = null
|
||||
this.serverPrevDocumentHash = null
|
||||
|
||||
this.config = config
|
||||
}
|
||||
|
@ -61,9 +95,9 @@ export default class HotReloader {
|
|||
getBaseWebpackConfig(this.dir, { dev: true, isServer: true, config: this.config, buildId: this.buildId })
|
||||
])
|
||||
|
||||
const compiler = webpack(configs)
|
||||
const multiCompiler = webpack(configs)
|
||||
|
||||
const buildTools = await this.prepareBuildTools(compiler)
|
||||
const buildTools = await this.prepareBuildTools(multiCompiler)
|
||||
this.assignBuildTools(buildTools)
|
||||
|
||||
this.stats = (await this.waitUntilValid()).stats[0]
|
||||
|
@ -113,29 +147,42 @@ export default class HotReloader {
|
|||
]
|
||||
}
|
||||
|
||||
async prepareBuildTools (compiler) {
|
||||
// This flushes require.cache after emitting the files. Providing 'hot reloading' of server files.
|
||||
compiler.compilers.forEach((singleCompiler) => {
|
||||
singleCompiler.plugin('after-emit', (compilation, callback) => {
|
||||
const { assets } = compilation
|
||||
async prepareBuildTools (multiCompiler) {
|
||||
// This plugin watches for changes to _document.js and notifies the client side that it should reload the page
|
||||
multiCompiler.compilers[1].hooks.done.tap('NextjsHotReloaderForServer', (stats) => {
|
||||
if (!this.initialized) {
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
const {compilation} = stats
|
||||
|
||||
callback()
|
||||
})
|
||||
// We only watch `_document` for changes on the server compilation
|
||||
// the rest of the files will be triggered by the client compilation
|
||||
const documentChunk = compilation.chunks.find(c => c.name === normalize('bundles/pages/_document.js'))
|
||||
// If the document chunk can't be found we do nothing
|
||||
if (!documentChunk) {
|
||||
console.warn('_document.js chunk not found')
|
||||
return
|
||||
}
|
||||
|
||||
// Initial value
|
||||
if (this.serverPrevDocumentHash === null) {
|
||||
this.serverPrevDocumentHash = documentChunk.hash
|
||||
return
|
||||
}
|
||||
|
||||
// If _document.js didn't change we don't trigger a reload
|
||||
if (documentChunk.hash === this.serverPrevDocumentHash) {
|
||||
return
|
||||
}
|
||||
|
||||
// Notify reload to reload the page, as _document.js was changed (different hash)
|
||||
this.send('reload', '/_document')
|
||||
|
||||
this.serverPrevDocumentHash = documentChunk.hash
|
||||
})
|
||||
|
||||
compiler.compilers[0].plugin('done', (stats) => {
|
||||
multiCompiler.compilers[0].hooks.done.tap('NextjsHotReloaderForClient', (stats) => {
|
||||
const { compilation } = stats
|
||||
const chunkNames = new Set(
|
||||
compilation.chunks
|
||||
|
@ -143,12 +190,7 @@ export default class HotReloader {
|
|||
.filter(name => IS_BUNDLED_PAGE_REGEX.test(name))
|
||||
)
|
||||
|
||||
const failedChunkNames = new Set(compilation.errors
|
||||
.map((e) => e.module.reasons)
|
||||
.reduce((a, b) => a.concat(b), [])
|
||||
.map((r) => r.module.chunks)
|
||||
.reduce((a, b) => a.concat(b), [])
|
||||
.map((c) => c.name))
|
||||
const failedChunkNames = new Set(Object.keys(erroredPages(compilation)))
|
||||
|
||||
const chunkHashes = new Map(
|
||||
compilation.chunks
|
||||
|
@ -169,7 +211,7 @@ export default class HotReloader {
|
|||
|
||||
const rootDir = join('bundles', 'pages')
|
||||
|
||||
for (const n of new Set([...added, ...removed, ...failed, ...succeeded])) {
|
||||
for (const n of new Set([...added, ...succeeded, ...removed, ...failed])) {
|
||||
const route = toRoute(relative(rootDir, n))
|
||||
this.send('reload', route)
|
||||
}
|
||||
|
@ -211,10 +253,9 @@ export default class HotReloader {
|
|||
]
|
||||
|
||||
let webpackDevMiddlewareConfig = {
|
||||
publicPath: `/_next/webpack/`,
|
||||
publicPath: `/_next/static/webpack`,
|
||||
noInfo: true,
|
||||
quiet: true,
|
||||
clientLogLevel: 'warning',
|
||||
logLevel: 'silent',
|
||||
watchOptions: { ignored }
|
||||
}
|
||||
|
||||
|
@ -223,15 +264,15 @@ export default class HotReloader {
|
|||
webpackDevMiddlewareConfig = this.config.webpackDevMiddleware(webpackDevMiddlewareConfig)
|
||||
}
|
||||
|
||||
const webpackDevMiddleware = WebpackDevMiddleware(compiler, webpackDevMiddlewareConfig)
|
||||
const webpackDevMiddleware = WebpackDevMiddleware(multiCompiler, webpackDevMiddlewareConfig)
|
||||
|
||||
const webpackHotMiddleware = WebpackHotMiddleware(compiler.compilers[0], {
|
||||
const webpackHotMiddleware = WebpackHotMiddleware(multiCompiler.compilers[0], {
|
||||
path: '/_next/webpack-hmr',
|
||||
log: false,
|
||||
heartbeat: 2500
|
||||
})
|
||||
|
||||
const onDemandEntries = onDemandEntryHandler(webpackDevMiddleware, compiler.compilers, {
|
||||
const onDemandEntries = onDemandEntryHandler(webpackDevMiddleware, multiCompiler.compilers, {
|
||||
dir: this.dir,
|
||||
dev: true,
|
||||
reload: this.reload.bind(this),
|
||||
|
@ -253,30 +294,29 @@ export default class HotReloader {
|
|||
})
|
||||
}
|
||||
|
||||
async getCompilationErrors () {
|
||||
async getCompilationErrors (page) {
|
||||
const normalizedPage = normalizePage(page)
|
||||
// When we are reloading, we need to wait until it's reloaded properly.
|
||||
await this.onDemandEntries.waitUntilReloaded()
|
||||
|
||||
if (!this.compilationErrors) {
|
||||
this.compilationErrors = new Map()
|
||||
|
||||
if (this.stats.hasErrors()) {
|
||||
const { compiler, errors } = this.stats.compilation
|
||||
|
||||
for (const err of errors) {
|
||||
for (const r of err.module.reasons) {
|
||||
for (const c of r.module.chunks) {
|
||||
// get the path of the bundle file
|
||||
const path = join(compiler.outputPath, c.name)
|
||||
const errors = this.compilationErrors.get(path) || []
|
||||
this.compilationErrors.set(path, errors.concat([err]))
|
||||
}
|
||||
}
|
||||
if (this.stats.hasErrors()) {
|
||||
const {compilation} = this.stats
|
||||
const failedPages = erroredPages(compilation, {
|
||||
enhanceName (name) {
|
||||
return '/' + ROUTE_NAME_REGEX.exec(name)[1]
|
||||
}
|
||||
})
|
||||
|
||||
// If there is an error related to the requesting page we display it instead of the first error
|
||||
if (failedPages[normalizedPage] && failedPages[normalizedPage].length > 0) {
|
||||
return failedPages[normalizedPage]
|
||||
}
|
||||
|
||||
// If none were found we still have to show the other errors
|
||||
return this.stats.compilation.errors
|
||||
}
|
||||
|
||||
return this.compilationErrors
|
||||
return []
|
||||
}
|
||||
|
||||
send (action, ...args) {
|
||||
|
@ -292,10 +332,6 @@ export default class HotReloader {
|
|||
}
|
||||
}
|
||||
|
||||
function deleteCache (path) {
|
||||
delete require.cache[path]
|
||||
}
|
||||
|
||||
function diff (a, b) {
|
||||
return new Set([...a].filter((v) => !b.has(v)))
|
||||
}
|
||||
|
|
|
@ -13,13 +13,12 @@ import {
|
|||
renderScriptError
|
||||
} from './render'
|
||||
import Router from './router'
|
||||
import { getAvailableChunks, isInternalUrl } from './utils'
|
||||
import { isInternalUrl } from './utils'
|
||||
import loadConfig from './config'
|
||||
import {PHASE_PRODUCTION_SERVER, PHASE_DEVELOPMENT_SERVER, BLOCKED_PAGES, BUILD_ID_FILE} from '../lib/constants'
|
||||
import * as asset from '../lib/asset'
|
||||
import * as envConfig from '../lib/runtime-config'
|
||||
import { isResSent } from '../lib/utils'
|
||||
import isAsyncSupported from './lib/is-async-supported'
|
||||
|
||||
// We need to go up one more level since we are in the `dist` directory
|
||||
import pkg from '../../package'
|
||||
|
@ -53,7 +52,6 @@ export default class Server {
|
|||
distDir: this.distDir,
|
||||
hotReloader: this.hotReloader,
|
||||
buildId: this.buildId,
|
||||
availableChunks: dev ? {} : getAvailableChunks(this.distDir, dev),
|
||||
generateEtags
|
||||
}
|
||||
|
||||
|
@ -107,21 +105,6 @@ export default class Server {
|
|||
}
|
||||
|
||||
async prepare () {
|
||||
if (this.dev && process.stdout.isTTY && isAsyncSupported()) {
|
||||
try {
|
||||
const checkForUpdate = require('update-check')
|
||||
const update = await checkForUpdate(pkg, {
|
||||
distTag: pkg.version.includes('canary') ? 'canary' : 'latest'
|
||||
})
|
||||
if (update) {
|
||||
// bgRed from chalk
|
||||
console.log(`\u001B[41mUPDATE AVAILABLE\u001B[49m The latest version of \`next\` is ${update.latest}`)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error checking updates', err)
|
||||
}
|
||||
}
|
||||
|
||||
await this.defineRoutes()
|
||||
if (this.hotReloader) {
|
||||
await this.hotReloader.start()
|
||||
|
@ -145,22 +128,6 @@ export default class Server {
|
|||
|
||||
async defineRoutes () {
|
||||
const routes = {
|
||||
// This is to support, webpack dynamic imports in production.
|
||||
'/_next/webpack/chunks/:name': async (req, res, params) => {
|
||||
// Cache aggressively in production
|
||||
if (!this.dev) {
|
||||
res.setHeader('Cache-Control', 'public, max-age=31536000, immutable')
|
||||
}
|
||||
const p = join(this.distDir, 'chunks', params.name)
|
||||
await this.serveStatic(req, res, p)
|
||||
},
|
||||
|
||||
// This is to support, webpack dynamic import support with HMR
|
||||
'/_next/webpack/:id': async (req, res, params) => {
|
||||
const p = join(this.distDir, 'chunks', params.id)
|
||||
await this.serveStatic(req, res, p)
|
||||
},
|
||||
|
||||
'/_next/:buildId/page/:path*.js.map': async (req, res, params) => {
|
||||
const paths = params.path || ['']
|
||||
const page = `/${paths.join('/')}`
|
||||
|
@ -193,7 +160,7 @@ export default class Server {
|
|||
return await renderScriptError(req, res, page, error)
|
||||
}
|
||||
|
||||
const compilationErr = await this.getCompilationError()
|
||||
const compilationErr = await this.getCompilationError(page)
|
||||
if (compilationErr) {
|
||||
return await renderScriptError(req, res, page, compilationErr)
|
||||
}
|
||||
|
@ -214,8 +181,9 @@ export default class Server {
|
|||
|
||||
'/_next/static/:path*': async (req, res, params) => {
|
||||
// The commons folder holds commonschunk files
|
||||
// The chunks folder holds dynamic entries
|
||||
// In development they don't have a hash, and shouldn't be cached by the browser.
|
||||
if (params.path[0] === 'commons') {
|
||||
if (params.path[0] === 'commons' || params.path[0] === 'chunks') {
|
||||
if (this.dev) {
|
||||
res.setHeader('Cache-Control', 'no-store, must-revalidate')
|
||||
} else {
|
||||
|
@ -226,15 +194,6 @@ export default class Server {
|
|||
await this.serveStatic(req, res, p)
|
||||
},
|
||||
|
||||
// It's very important keep this route's param optional.
|
||||
// (but it should support as many as params, seperated by '/')
|
||||
// Othewise this will lead to a pretty simple DOS attack.
|
||||
// See more: https://github.com/zeit/next.js/issues/2617
|
||||
'/_next/:path*': async (req, res, params) => {
|
||||
const p = join(__dirname, '..', 'client', ...(params.path || []))
|
||||
await this.serveStatic(req, res, p)
|
||||
},
|
||||
|
||||
// It's very important keep this route's param optional.
|
||||
// (but it should support as many as params, seperated by '/')
|
||||
// Othewise this will lead to a pretty simple DOS attack.
|
||||
|
@ -267,6 +226,23 @@ export default class Server {
|
|||
}
|
||||
}
|
||||
|
||||
// In development we expose all compiled files for react-error-overlay's line show feature
|
||||
if (this.dev) {
|
||||
routes['/_next/development/:path*'] = async (req, res, params) => {
|
||||
const p = join(this.distDir, ...(params.path || []))
|
||||
await this.serveStatic(req, res, p)
|
||||
}
|
||||
}
|
||||
|
||||
// It's very important keep this route's param optional.
|
||||
// (but it should support as many as params, seperated by '/')
|
||||
// Othewise this will lead to a pretty simple DOS attack.
|
||||
// See more: https://github.com/zeit/next.js/issues/2617
|
||||
routes['/_next/:path*'] = async (req, res, params) => {
|
||||
const p = join(__dirname, '..', 'client', ...(params.path || []))
|
||||
await this.serveStatic(req, res, p)
|
||||
}
|
||||
|
||||
routes['/:path*'] = async (req, res, params, parsedUrl) => {
|
||||
const { pathname, query } = parsedUrl
|
||||
await this.render(req, res, pathname, query, parsedUrl)
|
||||
|
@ -332,7 +308,7 @@ export default class Server {
|
|||
|
||||
async renderToHTML (req, res, pathname, query) {
|
||||
if (this.dev) {
|
||||
const compilationErr = await this.getCompilationError()
|
||||
const compilationErr = await this.getCompilationError(pathname)
|
||||
if (compilationErr) {
|
||||
res.statusCode = 500
|
||||
return this.renderErrorToHTML(compilationErr, req, res, pathname, query)
|
||||
|
@ -347,8 +323,6 @@ export default class Server {
|
|||
res.statusCode = 404
|
||||
return this.renderErrorToHTML(null, req, res, pathname, query)
|
||||
} else {
|
||||
const {applySourcemaps} = require('./lib/source-map-support')
|
||||
await applySourcemaps(err)
|
||||
if (!this.quiet) console.error(err)
|
||||
res.statusCode = 500
|
||||
return this.renderErrorToHTML(err, req, res, pathname, query)
|
||||
|
@ -363,7 +337,7 @@ export default class Server {
|
|||
|
||||
async renderErrorToHTML (err, req, res, pathname, query) {
|
||||
if (this.dev) {
|
||||
const compilationErr = await this.getCompilationError()
|
||||
const compilationErr = await this.getCompilationError(pathname)
|
||||
if (compilationErr) {
|
||||
res.statusCode = 500
|
||||
return renderErrorToHTML(compilationErr, req, res, pathname, query, this.renderOpts)
|
||||
|
@ -438,14 +412,14 @@ export default class Server {
|
|||
return true
|
||||
}
|
||||
|
||||
async getCompilationError () {
|
||||
async getCompilationError (page) {
|
||||
if (!this.hotReloader) return
|
||||
|
||||
const errors = await this.hotReloader.getCompilationErrors()
|
||||
if (!errors.size) return
|
||||
const errors = await this.hotReloader.getCompilationErrors(page)
|
||||
if (errors.length === 0) return
|
||||
|
||||
// Return the very first error we found.
|
||||
return Array.from(errors.values())[0][0]
|
||||
return errors[0]
|
||||
}
|
||||
|
||||
send404 (res) {
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
// based on https://github.com/timneutkens/is-async-supported
|
||||
const vm = require('vm')
|
||||
|
||||
module.exports = function isAsyncSupported () {
|
||||
try {
|
||||
// eslint-disable-next-line no-new
|
||||
new vm.Script('(async () => ({}))()')
|
||||
return true
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
}
|
|
@ -1,57 +0,0 @@
|
|||
// @flow
|
||||
const filenameRE = /\(([^)]+\.js):(\d+):(\d+)\)$/
|
||||
|
||||
export async function applySourcemaps (e: any): Promise<void> {
|
||||
if (!e || typeof e.stack !== 'string' || e.sourceMapsApplied) {
|
||||
return
|
||||
}
|
||||
|
||||
const lines = e.stack.split('\n')
|
||||
|
||||
const result = await Promise.all(lines.map((line) => {
|
||||
return rewriteTraceLine(line)
|
||||
}))
|
||||
|
||||
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<string> {
|
||||
const m = trace.match(filenameRE)
|
||||
if (m == null) {
|
||||
return trace
|
||||
}
|
||||
|
||||
const filePath = m[1]
|
||||
const mapPath = `${filePath}.map`
|
||||
|
||||
// Load these on demand.
|
||||
const fs = require('fs')
|
||||
const promisify = require('../../lib/promisify')
|
||||
|
||||
const readFile = promisify(fs.readFile)
|
||||
const access = promisify(fs.access)
|
||||
|
||||
try {
|
||||
await access(mapPath, (fs.constants || fs).R_OK)
|
||||
} catch (err) {
|
||||
return trace
|
||||
}
|
||||
|
||||
const mapContents = await readFile(mapPath)
|
||||
const {SourceMapConsumer} = require('source-map')
|
||||
const map = new SourceMapConsumer(JSON.parse(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)
|
||||
}
|
||||
|
||||
return trace
|
||||
}
|
|
@ -2,7 +2,7 @@ import DynamicEntryPlugin from 'webpack/lib/DynamicEntryPlugin'
|
|||
import { EventEmitter } from 'events'
|
||||
import { join } from 'path'
|
||||
import { parse } from 'url'
|
||||
import touch from 'touch'
|
||||
import fs from 'fs'
|
||||
import promisify from '../lib/promisify'
|
||||
import globModule from 'glob'
|
||||
import {normalizePagePath, pageNotFoundError} from './require'
|
||||
|
@ -14,6 +14,7 @@ const BUILDING = Symbol('building')
|
|||
const BUILT = Symbol('built')
|
||||
|
||||
const glob = promisify(globModule)
|
||||
const access = promisify(fs.access)
|
||||
|
||||
export default function onDemandEntryHandler (devMiddleware, compilers, {
|
||||
dir,
|
||||
|
@ -27,7 +28,6 @@ export default function onDemandEntryHandler (devMiddleware, compilers, {
|
|||
let lastAccessPages = ['']
|
||||
let doneCallbacks = new EventEmitter()
|
||||
const invalidator = new Invalidator(devMiddleware)
|
||||
let touchedAPage = false
|
||||
let reloading = false
|
||||
let stopped = false
|
||||
let reloadCallbacks = new EventEmitter()
|
||||
|
@ -35,12 +35,23 @@ export default function onDemandEntryHandler (devMiddleware, compilers, {
|
|||
const currentBuilders = new Set()
|
||||
|
||||
compilers.forEach(compiler => {
|
||||
compiler.plugin('make', function (compilation, done) {
|
||||
compiler.hooks.make.tapAsync('NextJsOnDemandEntries', function (compilation, done) {
|
||||
invalidator.startBuilding()
|
||||
currentBuilders.add(compiler.name)
|
||||
|
||||
const allEntries = Object.keys(entries).map((page) => {
|
||||
const allEntries = Object.keys(entries).map(async (page) => {
|
||||
const { name, entry } = entries[page]
|
||||
const files = Array.isArray(entry) ? entry : [entry]
|
||||
// Is just one item. But it's passed as an array.
|
||||
for (const file of files) {
|
||||
try {
|
||||
await access(join(dir, file), (fs.constants || fs).W_OK)
|
||||
} catch (err) {
|
||||
console.warn('Page was removed', page)
|
||||
delete entries[page]
|
||||
return
|
||||
}
|
||||
}
|
||||
entries[page].status = BUILDING
|
||||
return addEntry(compilation, compiler.context, name, entry)
|
||||
})
|
||||
|
@ -50,7 +61,7 @@ export default function onDemandEntryHandler (devMiddleware, compilers, {
|
|||
.catch(done)
|
||||
})
|
||||
|
||||
compiler.plugin('done', function (stats) {
|
||||
compiler.hooks.done.tap('NextJsOnDemandEntries', function (stats) {
|
||||
// Wait until all the compilers mark the build as done.
|
||||
currentBuilders.delete(compiler.name)
|
||||
if (currentBuilders.size !== 0) return
|
||||
|
@ -81,17 +92,6 @@ export default function onDemandEntryHandler (devMiddleware, compilers, {
|
|||
const entryInfo = entries[page]
|
||||
if (entryInfo.status !== BUILDING) return
|
||||
|
||||
// With this, we are triggering a filesystem based watch trigger
|
||||
// It'll memorize some timestamp related info related to common files used
|
||||
// in the page
|
||||
// That'll reduce the page building time significantly.
|
||||
if (!touchedAPage) {
|
||||
setTimeout(() => {
|
||||
touch.sync(entryInfo.pathname)
|
||||
}, 1000)
|
||||
touchedAPage = true
|
||||
}
|
||||
|
||||
entryInfo.status = BUILT
|
||||
entries[page].lastActiveTime = Date.now()
|
||||
doneCallbacks.emit(page)
|
||||
|
@ -289,8 +289,11 @@ function disposeInactiveEntries (devMiddleware, entries, lastAccessPages, maxIna
|
|||
|
||||
// /index and / is the same. So, we need to identify both pages as the same.
|
||||
// This also applies to sub pages as well.
|
||||
function normalizePage (page) {
|
||||
return page.replace(/\/index$/, '/')
|
||||
export function normalizePage (page) {
|
||||
if (page === '/index' || page === '/') {
|
||||
return '/'
|
||||
}
|
||||
return page.replace(/\/index$/, '')
|
||||
}
|
||||
|
||||
function sendJson (res, payload) {
|
||||
|
|
113
server/render.js
113
server/render.js
|
@ -1,18 +1,27 @@
|
|||
import { join } from 'path'
|
||||
import { createElement } from 'react'
|
||||
import React from 'react'
|
||||
import { renderToString, renderToStaticMarkup } from 'react-dom/server'
|
||||
import send from 'send'
|
||||
import generateETag from 'etag'
|
||||
import fresh from 'fresh'
|
||||
import requirePage from './require'
|
||||
import requirePage, {normalizePagePath} from './require'
|
||||
import { Router } from '../lib/router'
|
||||
import { loadGetInitialProps, isResSent } from '../lib/utils'
|
||||
import { getAvailableChunks } from './utils'
|
||||
import Head, { defaultHead } from '../lib/head'
|
||||
import ErrorDebug from '../lib/error-debug'
|
||||
import { flushChunks } from '../lib/dynamic'
|
||||
import { BUILD_MANIFEST, SERVER_DIRECTORY } from '../lib/constants'
|
||||
import { applySourcemaps } from './lib/source-map-support'
|
||||
import Loadable from 'react-loadable'
|
||||
import { BUILD_MANIFEST, REACT_LOADABLE_MANIFEST, SERVER_DIRECTORY } from '../lib/constants'
|
||||
|
||||
// Based on https://github.com/jamiebuilds/react-loadable/pull/132
|
||||
function getBundles (manifest, moduleIds) {
|
||||
return moduleIds.reduce((bundles, moduleId) => {
|
||||
if (typeof manifest[moduleId] === 'undefined') {
|
||||
return bundles
|
||||
}
|
||||
|
||||
return bundles.concat(manifest[moduleId])
|
||||
}, [])
|
||||
}
|
||||
|
||||
const logger = console
|
||||
|
||||
|
@ -41,30 +50,30 @@ async function doRender (req, res, pathname, query, {
|
|||
hotReloader,
|
||||
assetPrefix,
|
||||
runtimeConfig,
|
||||
availableChunks,
|
||||
distDir,
|
||||
dir,
|
||||
dev = false,
|
||||
staticMarkup = false,
|
||||
nextExport = false
|
||||
nextExport
|
||||
} = {}) {
|
||||
page = page || pathname
|
||||
|
||||
await applySourcemaps(err)
|
||||
|
||||
if (hotReloader) { // In dev mode we use on demand entries to compile the page before rendering
|
||||
await ensurePage(page, { dir, hotReloader })
|
||||
}
|
||||
|
||||
const documentPath = join(distDir, SERVER_DIRECTORY, 'bundles', 'pages', '_document')
|
||||
const appPath = join(distDir, SERVER_DIRECTORY, 'bundles', 'pages', '_app')
|
||||
const buildManifest = require(join(distDir, BUILD_MANIFEST))
|
||||
let [Component, Document, App] = await Promise.all([
|
||||
let [buildManifest, reactLoadableManifest, Component, Document, App] = await Promise.all([
|
||||
require(join(distDir, BUILD_MANIFEST)),
|
||||
require(join(distDir, REACT_LOADABLE_MANIFEST)),
|
||||
requirePage(page, {distDir}),
|
||||
require(documentPath),
|
||||
require(appPath)
|
||||
])
|
||||
|
||||
await Loadable.preloadAll() // Make sure all dynamic imports are loaded
|
||||
|
||||
Component = Component.default || Component
|
||||
|
||||
if (typeof Component !== 'function') {
|
||||
|
@ -77,10 +86,18 @@ async function doRender (req, res, pathname, query, {
|
|||
const ctx = { err, req, res, pathname, query, asPath }
|
||||
const router = new Router(pathname, query, asPath)
|
||||
const props = await loadGetInitialProps(App, {Component, router, ctx})
|
||||
const files = [
|
||||
...new Set([
|
||||
...buildManifest.pages[normalizePagePath(page)],
|
||||
...buildManifest.pages[normalizePagePath('/_app')],
|
||||
...buildManifest.pages[normalizePagePath('/_error')]
|
||||
])
|
||||
]
|
||||
|
||||
// the response might be finshed on the getinitialprops call
|
||||
if (isResSent(res)) return
|
||||
|
||||
let reactLoadableModules = []
|
||||
const renderPage = (options = Page => Page) => {
|
||||
let EnhancedApp = App
|
||||
let EnhancedComponent = Component
|
||||
|
@ -97,7 +114,13 @@ async function doRender (req, res, pathname, query, {
|
|||
}
|
||||
}
|
||||
|
||||
const app = createElement(EnhancedApp, { Component: EnhancedComponent, router, ...props })
|
||||
const app = <Loadable.Capture report={moduleName => reactLoadableModules.push(moduleName)}>
|
||||
<EnhancedApp {...{
|
||||
Component: EnhancedComponent,
|
||||
router,
|
||||
...props
|
||||
}} />
|
||||
</Loadable.Capture>
|
||||
|
||||
const render = staticMarkup ? renderToStaticMarkup : renderToString
|
||||
|
||||
|
@ -107,7 +130,7 @@ async function doRender (req, res, pathname, query, {
|
|||
|
||||
try {
|
||||
if (err && dev) {
|
||||
errorHtml = render(createElement(ErrorDebug, { error: err }))
|
||||
errorHtml = render(<ErrorDebug error={err} />)
|
||||
} else if (err) {
|
||||
html = render(app)
|
||||
} else {
|
||||
|
@ -116,34 +139,39 @@ async function doRender (req, res, pathname, query, {
|
|||
} finally {
|
||||
head = Head.rewind() || defaultHead()
|
||||
}
|
||||
const chunks = loadChunks({ dev, distDir, availableChunks })
|
||||
|
||||
return { html, head, errorHtml, chunks, buildManifest }
|
||||
return { html, head, errorHtml, buildManifest }
|
||||
}
|
||||
|
||||
const docProps = await loadGetInitialProps(Document, { ...ctx, renderPage })
|
||||
const dynamicImports = getBundles(reactLoadableManifest, reactLoadableModules)
|
||||
|
||||
if (isResSent(res)) return
|
||||
|
||||
if (!Document.prototype || !Document.prototype.isReactComponent) throw new Error('_document.js is not exporting a React component')
|
||||
const doc = createElement(Document, {
|
||||
const doc = <Document {...{
|
||||
__NEXT_DATA__: {
|
||||
props,
|
||||
page, // the rendered page
|
||||
pathname, // the requested path
|
||||
query,
|
||||
buildId,
|
||||
assetPrefix,
|
||||
runtimeConfig,
|
||||
nextExport,
|
||||
err: (err) ? serializeError(dev, err) : null
|
||||
// Used in development to replace paths for react-error-overlay
|
||||
distDir: dev ? distDir : undefined,
|
||||
props, // The result of getInitialProps
|
||||
page, // The rendered page
|
||||
pathname, // The requested path
|
||||
query, // querystring parsed / passed by the user
|
||||
buildId, // buildId is used to facilitate caching of page bundles, we send it to the client so that pageloader knows where to load bundles
|
||||
assetPrefix: assetPrefix === '' ? undefined : assetPrefix, // send assetPrefix to the client side when configured, otherwise don't sent in the resulting HTML
|
||||
runtimeConfig, // runtimeConfig if provided, otherwise don't sent in the resulting HTML
|
||||
nextExport, // If this is a page exported by `next export`
|
||||
err: (err) ? serializeError(dev, err) : undefined // Error if one happened, otherwise don't sent in the resulting HTML
|
||||
},
|
||||
dev,
|
||||
dir,
|
||||
staticMarkup,
|
||||
buildManifest,
|
||||
files,
|
||||
dynamicImports,
|
||||
assetPrefix, // We always pass assetPrefix as a top level property since _document needs it to render, even though the client side might not need it
|
||||
...docProps
|
||||
})
|
||||
}} />
|
||||
|
||||
return '<!DOCTYPE html>' + renderToStaticMarkup(doc)
|
||||
}
|
||||
|
@ -190,15 +218,6 @@ export function sendHTML (req, res, html, method, { dev, generateEtags }) {
|
|||
res.end(method === 'HEAD' ? null : html)
|
||||
}
|
||||
|
||||
export function sendJSON (res, obj, method) {
|
||||
if (isResSent(res)) return
|
||||
|
||||
const json = JSON.stringify(obj)
|
||||
res.setHeader('Content-Type', 'application/json')
|
||||
res.setHeader('Content-Length', Buffer.byteLength(json))
|
||||
res.end(method === 'HEAD' ? null : json)
|
||||
}
|
||||
|
||||
function errorToJSON (err) {
|
||||
const { name, message, stack } = err
|
||||
const json = { name, message, stack }
|
||||
|
@ -240,25 +259,3 @@ async function ensurePage (page, { dir, hotReloader }) {
|
|||
|
||||
await hotReloader.ensurePage(page)
|
||||
}
|
||||
|
||||
function loadChunks ({ dev, distDir, availableChunks }) {
|
||||
const flushedChunks = flushChunks()
|
||||
const response = {
|
||||
names: [],
|
||||
filenames: []
|
||||
}
|
||||
|
||||
if (dev) {
|
||||
availableChunks = getAvailableChunks(distDir, dev)
|
||||
}
|
||||
|
||||
for (var chunk of flushedChunks) {
|
||||
const filename = availableChunks[chunk]
|
||||
if (filename) {
|
||||
response.names.push(chunk)
|
||||
response.filenames.push(filename)
|
||||
}
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
|
|
@ -1,30 +1,3 @@
|
|||
import { join } from 'path'
|
||||
import { readdirSync, existsSync } from 'fs'
|
||||
|
||||
export function getChunkNameFromFilename (filename, dev) {
|
||||
if (dev) {
|
||||
return filename.replace(/\.[^.]*$/, '')
|
||||
}
|
||||
return filename.replace(/-[^-]*$/, '')
|
||||
}
|
||||
|
||||
export function getAvailableChunks (distDir, dev) {
|
||||
const chunksDir = join(distDir, 'chunks')
|
||||
if (!existsSync(chunksDir)) return {}
|
||||
|
||||
const chunksMap = {}
|
||||
const chunkFiles = readdirSync(chunksDir)
|
||||
|
||||
chunkFiles.forEach(filename => {
|
||||
if (/\.js$/.test(filename)) {
|
||||
const chunkName = getChunkNameFromFilename(filename, dev)
|
||||
chunksMap[chunkName] = filename
|
||||
}
|
||||
})
|
||||
|
||||
return chunksMap
|
||||
}
|
||||
|
||||
const internalPrefixes = [
|
||||
/^\/_next\//,
|
||||
/^\/static\//
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
import App, {Container} from 'next/app'
|
||||
import React from 'react'
|
||||
import {setState} from '../shared-module'
|
||||
|
||||
setState(typeof window === 'undefined' ? 'UPDATED' : 'UPDATED CLIENT')
|
||||
|
||||
class Layout extends React.Component {
|
||||
state = {
|
||||
|
|
5
test/integration/app-document/pages/shared.js
Normal file
5
test/integration/app-document/pages/shared.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
import currentState from '../shared-module'
|
||||
|
||||
export default () => {
|
||||
return <p id='currentstate'>{currentState()}</p>
|
||||
}
|
9
test/integration/app-document/shared-module.js
Normal file
9
test/integration/app-document/shared-module.js
Normal file
|
@ -0,0 +1,9 @@
|
|||
let moduleState = 'INITIAL'
|
||||
|
||||
export function setState (state) {
|
||||
moduleState = state
|
||||
}
|
||||
|
||||
export default function currentState () {
|
||||
return moduleState
|
||||
}
|
|
@ -8,63 +8,71 @@ import { check } from 'next-test-utils'
|
|||
export default (context, render) => {
|
||||
describe('Client side', () => {
|
||||
it('should detect the changes to pages/_app.js and display it', async () => {
|
||||
const browser = await webdriver(context.appPort, '/')
|
||||
const text = await browser
|
||||
.elementByCss('#hello-hmr').text()
|
||||
expect(text).toBe('Hello HMR')
|
||||
|
||||
const appPath = join(__dirname, '../', 'pages', '_app.js')
|
||||
|
||||
const originalContent = readFileSync(appPath, 'utf8')
|
||||
const editedContent = originalContent.replace('Hello HMR', 'Hi HMR')
|
||||
let browser
|
||||
try {
|
||||
browser = await webdriver(context.appPort, '/')
|
||||
const text = await browser.elementByCss('#hello-hmr').text()
|
||||
expect(text).toBe('Hello HMR')
|
||||
|
||||
// change the content
|
||||
writeFileSync(appPath, editedContent, 'utf8')
|
||||
// change the content
|
||||
const editedContent = originalContent.replace('Hello HMR', 'Hi HMR')
|
||||
writeFileSync(appPath, editedContent, 'utf8')
|
||||
|
||||
await check(
|
||||
() => browser.elementByCss('body').text(),
|
||||
/Hi HMR/
|
||||
)
|
||||
await check(
|
||||
() => browser.elementByCss('body').text(),
|
||||
/Hi HMR/
|
||||
)
|
||||
|
||||
// add the original content
|
||||
writeFileSync(appPath, originalContent, 'utf8')
|
||||
// add the original content
|
||||
writeFileSync(appPath, originalContent, 'utf8')
|
||||
|
||||
await check(
|
||||
() => browser.elementByCss('body').text(),
|
||||
/Hello HMR/
|
||||
)
|
||||
|
||||
browser.close()
|
||||
await check(
|
||||
() => browser.elementByCss('body').text(),
|
||||
/Hello HMR/
|
||||
)
|
||||
} finally {
|
||||
writeFileSync(appPath, originalContent, 'utf8')
|
||||
if (browser) {
|
||||
browser.close()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('should detect the changes to pages/_document.js and display it', async () => {
|
||||
const browser = await webdriver(context.appPort, '/')
|
||||
const text = await browser
|
||||
.elementByCss('#hello-hmr').text()
|
||||
expect(text).toBe('Hello HMR')
|
||||
|
||||
const appPath = join(__dirname, '../', 'pages', '_document.js')
|
||||
|
||||
const originalContent = readFileSync(appPath, 'utf8')
|
||||
const editedContent = originalContent.replace('Hello Document HMR', 'Hi Document HMR')
|
||||
let browser
|
||||
try {
|
||||
browser = await webdriver(context.appPort, '/')
|
||||
const text = await browser
|
||||
.elementByCss('#hello-hmr').text()
|
||||
expect(text).toBe('Hello HMR')
|
||||
|
||||
// change the content
|
||||
writeFileSync(appPath, editedContent, 'utf8')
|
||||
const editedContent = originalContent.replace('Hello Document HMR', 'Hi Document HMR')
|
||||
|
||||
await check(
|
||||
() => browser.elementByCss('body').text(),
|
||||
/Hi Document HMR/
|
||||
)
|
||||
// change the content
|
||||
writeFileSync(appPath, editedContent, 'utf8')
|
||||
|
||||
// add the original content
|
||||
writeFileSync(appPath, originalContent, 'utf8')
|
||||
await check(
|
||||
() => browser.elementByCss('body').text(),
|
||||
/Hi Document HMR/
|
||||
)
|
||||
|
||||
await check(
|
||||
() => browser.elementByCss('body').text(),
|
||||
/Hello Document HMR/
|
||||
)
|
||||
// add the original content
|
||||
writeFileSync(appPath, originalContent, 'utf8')
|
||||
|
||||
browser.close()
|
||||
await check(
|
||||
() => browser.elementByCss('body').text(),
|
||||
/Hello Document HMR/
|
||||
)
|
||||
} finally {
|
||||
writeFileSync(appPath, originalContent, 'utf8')
|
||||
if (browser) {
|
||||
browser.close()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('should keep state between page navigations', async () => {
|
||||
|
@ -80,5 +88,13 @@ export default (context, render) => {
|
|||
expect(switchedRandomNumer).toBe(randomNumber)
|
||||
browser.close()
|
||||
})
|
||||
|
||||
it('It should share module state with pages', async () => {
|
||||
const browser = await webdriver(context.appPort, '/shared')
|
||||
|
||||
const text = await browser.elementByCss('#currentstate').text()
|
||||
expect(text).toBe('UPDATED CLIENT')
|
||||
browser.close()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
|
@ -66,6 +66,13 @@ export default function ({ app }, suiteName, render, fetch) {
|
|||
const $ = await get$('/')
|
||||
expect($('hello-app').text() === 'Hello App')
|
||||
})
|
||||
|
||||
// For example react context uses shared module state
|
||||
// Also known as singleton modules
|
||||
test('It should share module state with pages', async () => {
|
||||
const $ = await get$('/shared')
|
||||
expect($('#currentstate').text() === 'UPDATED')
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
|
@ -7,11 +7,8 @@ const HelloBundle = dynamic({
|
|||
modules: (props) => {
|
||||
const components = {
|
||||
HelloContext: import('../../components/hello-context'),
|
||||
Hello1: import('../../components/hello1')
|
||||
}
|
||||
|
||||
if (props.showMore) {
|
||||
components.Hello2 = import('../../components/hello2')
|
||||
Hello1: import('../../components/hello1'),
|
||||
Hello2: import('../../components/hello2')
|
||||
}
|
||||
|
||||
return components
|
||||
|
@ -21,7 +18,7 @@ const HelloBundle = dynamic({
|
|||
<h1>{props.title}</h1>
|
||||
<HelloContext />
|
||||
<Hello1 />
|
||||
{Hello2? <Hello2 /> : null}
|
||||
{props.showMore ? <Hello2 /> : null}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
|
||||
import dynamic from 'next/dynamic'
|
||||
|
||||
const Hello = dynamic(import('../../components/hello1'))
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/* global describe, it, expect */
|
||||
|
||||
import webdriver from 'next-webdriver'
|
||||
import {waitFor} from 'next-test-utils'
|
||||
import {waitFor, getReactErrorOverlayContent} from 'next-test-utils'
|
||||
|
||||
export default (context, render) => {
|
||||
describe('Client Navigation', () => {
|
||||
|
@ -98,16 +98,21 @@ export default (context, render) => {
|
|||
|
||||
describe('with empty getInitialProps()', () => {
|
||||
it('should render an error', async () => {
|
||||
const browser = await webdriver(context.appPort, '/nav')
|
||||
const preText = await browser
|
||||
.elementByCss('#empty-props').click()
|
||||
.waitForElementByCss('pre')
|
||||
.elementByCss('pre').text()
|
||||
let browser
|
||||
try {
|
||||
browser = await webdriver(context.appPort, '/nav')
|
||||
await browser.elementByCss('#empty-props').click()
|
||||
|
||||
const expectedErrorMessage = '"EmptyInitialPropsPage.getInitialProps()" should resolve to an object. But found "null" instead.'
|
||||
expect(preText.includes(expectedErrorMessage)).toBeTruthy()
|
||||
await waitFor(3000)
|
||||
|
||||
browser.close()
|
||||
expect(await getReactErrorOverlayContent(browser)).toMatch(
|
||||
/should resolve to an object\. But found "null" instead\./
|
||||
)
|
||||
} finally {
|
||||
if (browser) {
|
||||
browser.close()
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -480,7 +485,7 @@ export default (context, render) => {
|
|||
browser.close()
|
||||
})
|
||||
|
||||
it('should work with dir/ page ', async () => {
|
||||
it('should work with dir/ page', async () => {
|
||||
const browser = await webdriver(context.appPort, '/nested-cdm')
|
||||
const text = await browser.elementByCss('p').text()
|
||||
|
||||
|
@ -566,19 +571,33 @@ export default (context, render) => {
|
|||
|
||||
describe('runtime errors', () => {
|
||||
it('should show ErrorDebug when a client side error is thrown inside a component', async () => {
|
||||
const browser = await webdriver(context.appPort, '/error-inside-browser-page')
|
||||
await waitFor(2000)
|
||||
const text = await browser.elementByCss('body').text()
|
||||
expect(text).toMatch(/An Expected error occured/)
|
||||
expect(text).toMatch(/pages\/error-inside-browser-page\.js:5:0/)
|
||||
let browser
|
||||
try {
|
||||
browser = await webdriver(context.appPort, '/error-inside-browser-page')
|
||||
await waitFor(3000)
|
||||
const text = await getReactErrorOverlayContent(browser)
|
||||
expect(text).toMatch(/An Expected error occured/)
|
||||
expect(text).toMatch(/pages\/error-inside-browser-page\.js:5/)
|
||||
} finally {
|
||||
if (browser) {
|
||||
browser.close()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('should show ErrorDebug when a client side error is thrown outside a component', async () => {
|
||||
const browser = await webdriver(context.appPort, '/error-in-the-browser-global-scope')
|
||||
await waitFor(2000)
|
||||
const text = await browser.elementByCss('body').text()
|
||||
expect(text).toMatch(/An Expected error occured/)
|
||||
expect(text).toMatch(/pages\/error-in-the-browser-global-scope\.js:2:0/)
|
||||
let browser
|
||||
try {
|
||||
browser = await webdriver(context.appPort, '/error-in-the-browser-global-scope')
|
||||
await waitFor(3000)
|
||||
const text = await getReactErrorOverlayContent(browser)
|
||||
expect(text).toMatch(/An Expected error occured/)
|
||||
expect(text).toMatch(/error-in-the-browser-global-scope\.js:2/)
|
||||
} finally {
|
||||
if (browser) {
|
||||
browser.close()
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
@ -1,31 +1,72 @@
|
|||
/* global describe, it, expect */
|
||||
import webdriver from 'next-webdriver'
|
||||
import cheerio from 'cheerio'
|
||||
import { waitFor } from 'next-test-utils'
|
||||
import { waitFor, check } from 'next-test-utils'
|
||||
|
||||
export default (context, render) => {
|
||||
async function get$ (path, query) {
|
||||
const html = await render(path, query)
|
||||
return cheerio.load(html)
|
||||
}
|
||||
describe('Dynamic import', () => {
|
||||
describe('with SSR', () => {
|
||||
async function get$ (path, query) {
|
||||
const html = await render(path, query)
|
||||
return cheerio.load(html)
|
||||
}
|
||||
|
||||
describe('default behavior', () => {
|
||||
it('should render dynamic import components', async () => {
|
||||
const $ = await get$('/dynamic/ssr')
|
||||
expect($('p').text()).toBe('Hello World 1')
|
||||
expect($('body').text()).toMatch(/Hello World 1/)
|
||||
})
|
||||
|
||||
it('should stop render dynamic import components', async () => {
|
||||
it('should render even there are no physical chunk exists', async () => {
|
||||
let browser
|
||||
try {
|
||||
browser = await webdriver(context.appPort, '/dynamic/no-chunk')
|
||||
await check(() => browser.elementByCss('body').text(), /Welcome, normal/)
|
||||
await check(() => browser.elementByCss('body').text(), /Welcome, dynamic/)
|
||||
} finally {
|
||||
if (browser) {
|
||||
browser.close()
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
describe('ssr:false option', () => {
|
||||
it('Should render loading on the server side', async () => {
|
||||
const $ = await get$('/dynamic/no-ssr')
|
||||
expect($('p').text()).toBe('loading...')
|
||||
})
|
||||
|
||||
it('should stop render dynamic import components with custom loading', async () => {
|
||||
it('should render the component on client side', async () => {
|
||||
let browser
|
||||
try {
|
||||
browser = await webdriver(context.appPort, '/dynamic/no-ssr')
|
||||
await check(() => browser.elementByCss('body').text(), /Hello World 1/)
|
||||
} finally {
|
||||
if (browser) {
|
||||
browser.close()
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('custom loading', () => {
|
||||
it('should render custom loading on the server side when `ssr:false` and `loading` is provided', async () => {
|
||||
const $ = await get$('/dynamic/no-ssr-custom-loading')
|
||||
expect($('p').text()).toBe('LOADING')
|
||||
})
|
||||
|
||||
it('should render the component on client side', async () => {
|
||||
let browser
|
||||
try {
|
||||
browser = await webdriver(context.appPort, '/dynamic/no-ssr-custom-loading')
|
||||
await check(() => browser.elementByCss('body').text(), /Hello World 1/)
|
||||
} finally {
|
||||
if (browser) {
|
||||
browser.close()
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('Import mapping', () => {
|
||||
it('should render dynamic imports bundle', async () => {
|
||||
const $ = await get$('/dynamic/bundle')
|
||||
const bodyText = $('body').text()
|
||||
|
@ -41,31 +82,17 @@ export default (context, render) => {
|
|||
expect(/Hello World 1/.test(bodyText)).toBe(true)
|
||||
expect(/Hello World 2/.test(bodyText)).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with browser', () => {
|
||||
it('should render the page client side', async () => {
|
||||
const browser = await webdriver(context.appPort, '/dynamic/no-ssr-custom-loading')
|
||||
|
||||
while (true) {
|
||||
const bodyText = await browser
|
||||
.elementByCss('body').text()
|
||||
if (/Hello World 1/.test(bodyText)) break
|
||||
await waitFor(1000)
|
||||
}
|
||||
|
||||
browser.close()
|
||||
})
|
||||
|
||||
it('should render even there are no physical chunk exists', async () => {
|
||||
const browser = await webdriver(context.appPort, '/dynamic/no-chunk')
|
||||
it('should render components', async () => {
|
||||
const browser = await webdriver(context.appPort, '/dynamic/bundle')
|
||||
|
||||
while (true) {
|
||||
const bodyText = await browser
|
||||
.elementByCss('body').text()
|
||||
if (
|
||||
/Welcome, normal/.test(bodyText) &&
|
||||
/Welcome, dynamic/.test(bodyText)
|
||||
/Dynamic Bundle/.test(bodyText) &&
|
||||
/Hello World 1/.test(bodyText) &&
|
||||
!(/Hello World 2/.test(bodyText))
|
||||
) break
|
||||
await waitFor(1000)
|
||||
}
|
||||
|
@ -73,60 +100,47 @@ export default (context, render) => {
|
|||
browser.close()
|
||||
})
|
||||
|
||||
describe('with bundle', () => {
|
||||
it('should render components', async () => {
|
||||
const browser = await webdriver(context.appPort, '/dynamic/bundle')
|
||||
it('should render support React context', async () => {
|
||||
const browser = await webdriver(context.appPort, '/dynamic/bundle')
|
||||
|
||||
while (true) {
|
||||
const bodyText = await browser
|
||||
.elementByCss('body').text()
|
||||
if (
|
||||
/Dynamic Bundle/.test(bodyText) &&
|
||||
/Hello World 1/.test(bodyText) &&
|
||||
!(/Hello World 2/.test(bodyText))
|
||||
) break
|
||||
await waitFor(1000)
|
||||
}
|
||||
while (true) {
|
||||
const bodyText = await browser
|
||||
.elementByCss('body').text()
|
||||
if (
|
||||
/ZEIT Rocks/.test(bodyText)
|
||||
) break
|
||||
await waitFor(1000)
|
||||
}
|
||||
|
||||
browser.close()
|
||||
})
|
||||
browser.close()
|
||||
})
|
||||
|
||||
it('should render support React context', async () => {
|
||||
const browser = await webdriver(context.appPort, '/dynamic/bundle')
|
||||
it('should load new components and render for prop changes', async () => {
|
||||
const browser = await webdriver(context.appPort, '/dynamic/bundle')
|
||||
|
||||
while (true) {
|
||||
const bodyText = await browser
|
||||
.elementByCss('body').text()
|
||||
if (
|
||||
/ZEIT Rocks/.test(bodyText)
|
||||
) break
|
||||
await waitFor(1000)
|
||||
}
|
||||
await browser
|
||||
.waitForElementByCss('#toggle-show-more')
|
||||
.elementByCss('#toggle-show-more').click()
|
||||
|
||||
browser.close()
|
||||
})
|
||||
while (true) {
|
||||
const bodyText = await browser
|
||||
.elementByCss('body').text()
|
||||
if (
|
||||
/Dynamic Bundle/.test(bodyText) &&
|
||||
/Hello World 1/.test(bodyText) &&
|
||||
/Hello World 2/.test(bodyText)
|
||||
) break
|
||||
await waitFor(1000)
|
||||
}
|
||||
|
||||
it('should load new components and render for prop changes', async () => {
|
||||
const browser = await webdriver(context.appPort, '/dynamic/bundle')
|
||||
|
||||
await browser
|
||||
.waitForElementByCss('#toggle-show-more')
|
||||
.elementByCss('#toggle-show-more').click()
|
||||
|
||||
while (true) {
|
||||
const bodyText = await browser
|
||||
.elementByCss('body').text()
|
||||
if (
|
||||
/Dynamic Bundle/.test(bodyText) &&
|
||||
/Hello World 1/.test(bodyText) &&
|
||||
/Hello World 2/.test(bodyText)
|
||||
) break
|
||||
await waitFor(1000)
|
||||
}
|
||||
|
||||
browser.close()
|
||||
})
|
||||
browser.close()
|
||||
})
|
||||
})
|
||||
|
||||
// describe('with browser', () => {
|
||||
|
||||
// describe('with bundle', () => {
|
||||
|
||||
// })
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,101 +1,121 @@
|
|||
/* global describe, it, expect */
|
||||
import webdriver from 'next-webdriver'
|
||||
import { join } from 'path'
|
||||
import { check, File, waitFor } from 'next-test-utils'
|
||||
import { check, File, waitFor, getReactErrorOverlayContent } from 'next-test-utils'
|
||||
|
||||
export default (context, render) => {
|
||||
describe('Error Recovery', () => {
|
||||
it('should detect syntax errors and recover', async () => {
|
||||
const browser = await webdriver(context.appPort, '/hmr/about')
|
||||
const text = await browser
|
||||
.elementByCss('p').text()
|
||||
expect(text).toBe('This is the about page.')
|
||||
|
||||
let browser
|
||||
const aboutPage = new File(join(__dirname, '../', 'pages', 'hmr', 'about.js'))
|
||||
aboutPage.replace('</div>', 'div')
|
||||
try {
|
||||
browser = await webdriver(context.appPort, '/hmr/about')
|
||||
const text = await browser
|
||||
.elementByCss('p').text()
|
||||
expect(text).toBe('This is the about page.')
|
||||
|
||||
await check(
|
||||
() => browser.elementByCss('body').text(),
|
||||
/Unterminated JSX contents/
|
||||
)
|
||||
aboutPage.replace('</div>', 'div')
|
||||
|
||||
aboutPage.restore()
|
||||
await waitFor(3000)
|
||||
|
||||
await check(
|
||||
() => browser.elementByCss('body').text(),
|
||||
/This is the about page/
|
||||
)
|
||||
expect(await getReactErrorOverlayContent(browser)).toMatch(/Unterminated JSX contents/)
|
||||
|
||||
browser.close()
|
||||
aboutPage.restore()
|
||||
|
||||
await check(
|
||||
() => browser.elementByCss('body').text(),
|
||||
/This is the about page/
|
||||
)
|
||||
} finally {
|
||||
aboutPage.restore()
|
||||
|
||||
if (browser) {
|
||||
browser.close()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('should not show the default HMR error overlay', async () => {
|
||||
const browser = await webdriver(context.appPort, '/hmr/about')
|
||||
const text = await browser
|
||||
.elementByCss('p').text()
|
||||
expect(text).toBe('This is the about page.')
|
||||
|
||||
let browser
|
||||
const aboutPage = new File(join(__dirname, '../', 'pages', 'hmr', 'about.js'))
|
||||
aboutPage.replace('</div>', 'div')
|
||||
try {
|
||||
browser = await webdriver(context.appPort, '/hmr/about')
|
||||
const text = await browser
|
||||
.elementByCss('p').text()
|
||||
expect(text).toBe('This is the about page.')
|
||||
|
||||
await check(
|
||||
() => browser.elementByCss('body').text(),
|
||||
/Unterminated JSX contents/
|
||||
)
|
||||
aboutPage.replace('</div>', 'div')
|
||||
|
||||
await waitFor(2000)
|
||||
await waitFor(3000)
|
||||
|
||||
// Check for the error overlay
|
||||
const bodyHtml = await browser.elementByCss('body').getAttribute('innerHTML')
|
||||
expect(bodyHtml.includes('webpack-hot-middleware-clientOverlay')).toBeFalsy()
|
||||
expect(await getReactErrorOverlayContent(browser)).toMatch(/Unterminated JSX contents/)
|
||||
|
||||
aboutPage.restore()
|
||||
browser.close()
|
||||
await waitFor(2000)
|
||||
|
||||
// Check for the error overlay
|
||||
const bodyHtml = await browser.elementByCss('body').getAttribute('innerHTML')
|
||||
expect(bodyHtml.includes('webpack-hot-middleware-clientOverlay')).toBeFalsy()
|
||||
} finally {
|
||||
aboutPage.restore()
|
||||
if (browser) {
|
||||
browser.close()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('should show the error on all pages', async () => {
|
||||
const aboutPage = new File(join(__dirname, '../', 'pages', 'hmr', 'about.js'))
|
||||
aboutPage.replace('</div>', 'div')
|
||||
let browser
|
||||
try {
|
||||
aboutPage.replace('</div>', 'div')
|
||||
|
||||
const browser = await webdriver(context.appPort, '/hmr/contact')
|
||||
browser = await webdriver(context.appPort, '/hmr/contact')
|
||||
|
||||
await check(
|
||||
() => browser.elementByCss('body').text(),
|
||||
/Unterminated JSX contents/
|
||||
)
|
||||
await waitFor(3000)
|
||||
|
||||
aboutPage.restore()
|
||||
expect(await getReactErrorOverlayContent(browser)).toMatch(/Unterminated JSX contents/)
|
||||
|
||||
await check(
|
||||
() => browser.elementByCss('body').text(),
|
||||
/This is the contact page/
|
||||
)
|
||||
aboutPage.restore()
|
||||
|
||||
browser.close()
|
||||
await check(
|
||||
() => browser.elementByCss('body').text(),
|
||||
/This is the contact page/
|
||||
)
|
||||
} finally {
|
||||
aboutPage.restore()
|
||||
if (browser) {
|
||||
browser.close()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('should detect runtime errors on the module scope', async () => {
|
||||
const browser = await webdriver(context.appPort, '/hmr/about')
|
||||
const text = await browser
|
||||
.elementByCss('p').text()
|
||||
expect(text).toBe('This is the about page.')
|
||||
|
||||
let browser
|
||||
const aboutPage = new File(join(__dirname, '../', 'pages', 'hmr', 'about.js'))
|
||||
aboutPage.replace('export', 'aa=20;\nexport')
|
||||
try {
|
||||
browser = await webdriver(context.appPort, '/hmr/about')
|
||||
const text = await browser
|
||||
.elementByCss('p').text()
|
||||
expect(text).toBe('This is the about page.')
|
||||
|
||||
await check(
|
||||
() => browser.elementByCss('body').text(),
|
||||
/aa is not defined/
|
||||
)
|
||||
aboutPage.replace('export', 'aa=20;\nexport')
|
||||
|
||||
aboutPage.restore()
|
||||
await waitFor(3000)
|
||||
|
||||
await check(
|
||||
() => browser.elementByCss('body').text(),
|
||||
/This is the about page/
|
||||
)
|
||||
expect(await getReactErrorOverlayContent(browser)).toMatch(/aa is not defined/)
|
||||
|
||||
browser.close()
|
||||
aboutPage.restore()
|
||||
|
||||
await check(
|
||||
() => browser.elementByCss('body').text(),
|
||||
/This is the about page/
|
||||
)
|
||||
} finally {
|
||||
aboutPage.restore()
|
||||
if (browser) {
|
||||
browser.close()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('should recover from errors in the render function', async () => {
|
||||
|
@ -107,10 +127,9 @@ export default (context, render) => {
|
|||
const aboutPage = new File(join(__dirname, '../', 'pages', 'hmr', 'about.js'))
|
||||
aboutPage.replace('return', 'throw new Error("an-expected-error");\nreturn')
|
||||
|
||||
await check(
|
||||
() => browser.elementByCss('body').text(),
|
||||
/an-expected-error/
|
||||
)
|
||||
await waitFor(3000)
|
||||
|
||||
expect(await getReactErrorOverlayContent(browser)).toMatch(/an-expected-error/)
|
||||
|
||||
aboutPage.restore()
|
||||
|
||||
|
@ -131,10 +150,9 @@ export default (context, render) => {
|
|||
const aboutPage = new File(join(__dirname, '../', 'pages', 'hmr', 'about.js'))
|
||||
aboutPage.replace('export default', 'export default "not-a-page"\nexport const fn = ')
|
||||
|
||||
await check(
|
||||
() => browser.elementByCss('body').text(),
|
||||
/The default export is not a React Component/
|
||||
)
|
||||
await waitFor(3000)
|
||||
|
||||
expect(await browser.elementByCss('body').text()).toMatch(/The default export is not a React Component/)
|
||||
|
||||
aboutPage.restore()
|
||||
|
||||
|
@ -174,10 +192,9 @@ export default (context, render) => {
|
|||
const browser = await webdriver(context.appPort, '/hmr')
|
||||
await browser.elementByCss('#error-in-gip-link').click()
|
||||
|
||||
await check(
|
||||
() => browser.elementByCss('body').text(),
|
||||
/an-expected-error-in-gip/
|
||||
)
|
||||
await waitFor(1500)
|
||||
|
||||
expect(await getReactErrorOverlayContent(browser)).toMatch(/an-expected-error-in-gip/)
|
||||
|
||||
const erroredPage = new File(join(__dirname, '../', 'pages', 'hmr', 'error-in-gip.js'))
|
||||
erroredPage.replace('throw error', 'return {}')
|
||||
|
@ -194,10 +211,9 @@ export default (context, render) => {
|
|||
it('should recover after an error reported via SSR', async () => {
|
||||
const browser = await webdriver(context.appPort, '/hmr/error-in-gip')
|
||||
|
||||
await check(
|
||||
() => browser.elementByCss('body').text(),
|
||||
/an-expected-error-in-gip/
|
||||
)
|
||||
await waitFor(1500)
|
||||
|
||||
expect(await getReactErrorOverlayContent(browser)).toMatch(/an-expected-error-in-gip/)
|
||||
|
||||
const erroredPage = new File(join(__dirname, '../', 'pages', 'hmr', 'error-in-gip.js'))
|
||||
erroredPage.replace('throw error', 'return {}')
|
||||
|
@ -212,24 +228,37 @@ export default (context, render) => {
|
|||
})
|
||||
|
||||
it('should recover from 404 after a page has been added', async () => {
|
||||
const browser = await webdriver(context.appPort, '/hmr/new-page')
|
||||
let browser
|
||||
let newPage
|
||||
try {
|
||||
browser = await webdriver(context.appPort, '/hmr/new-page')
|
||||
|
||||
await check(
|
||||
() => browser.elementByCss('body').text(),
|
||||
/This page could not be found/
|
||||
)
|
||||
expect(await browser.elementByCss('body').text()).toMatch(/This page could not be found/)
|
||||
|
||||
// Add the page
|
||||
const newPage = new File(join(__dirname, '../', 'pages', 'hmr', 'new-page.js'))
|
||||
newPage.write('export default () => (<div>the-new-page</div>)')
|
||||
// Add the page
|
||||
newPage = new File(join(__dirname, '../', 'pages', 'hmr', 'new-page.js'))
|
||||
newPage.write('export default () => (<div id="new-page">the-new-page</div>)')
|
||||
|
||||
await check(
|
||||
() => browser.elementByCss('body').text(),
|
||||
/the-new-page/
|
||||
)
|
||||
await check(
|
||||
() => {
|
||||
if (!browser.hasElementById('new-page')) {
|
||||
throw new Error('waiting')
|
||||
}
|
||||
|
||||
newPage.delete()
|
||||
browser.close()
|
||||
return browser.elementByCss('body').text()
|
||||
},
|
||||
/the-new-page/
|
||||
)
|
||||
|
||||
// expect(await browser.elementByCss('body').text()).toMatch(/the-new-page/)
|
||||
} finally {
|
||||
if (newPage) {
|
||||
newPage.delete()
|
||||
}
|
||||
if (browser) {
|
||||
browser.close()
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
|
@ -11,9 +11,9 @@ export default (context, renderViaHTTP) => {
|
|||
it('should load the page properly', async () => {
|
||||
const contactPagePath = join(__dirname, '../', 'pages', 'hmr', 'contact.js')
|
||||
const newContactPagePath = join(__dirname, '../', 'pages', 'hmr', '_contact.js')
|
||||
|
||||
let browser
|
||||
try {
|
||||
const browser = await webdriver(context.appPort, '/hmr/contact')
|
||||
browser = await webdriver(context.appPort, '/hmr/contact')
|
||||
const text = await browser
|
||||
.elementByCss('p').text()
|
||||
expect(text).toBe('This is the contact page.')
|
||||
|
@ -21,10 +21,9 @@ export default (context, renderViaHTTP) => {
|
|||
// Rename the file to mimic a deleted page
|
||||
renameSync(contactPagePath, newContactPagePath)
|
||||
|
||||
// wait until the 404 page comes
|
||||
await check(
|
||||
() => browser.elementByCss('body').text(),
|
||||
/(This page could not be found|ENOENT)/
|
||||
/This page could not be found/
|
||||
)
|
||||
|
||||
// Rename the file back to the original filename
|
||||
|
@ -35,9 +34,10 @@ export default (context, renderViaHTTP) => {
|
|||
() => browser.elementByCss('body').text(),
|
||||
/This is the contact page/
|
||||
)
|
||||
|
||||
browser.close()
|
||||
} finally {
|
||||
if (browser) {
|
||||
browser.close()
|
||||
}
|
||||
if (existsSync(newContactPagePath)) {
|
||||
renameSync(newContactPagePath, contactPagePath)
|
||||
}
|
||||
|
@ -47,131 +47,145 @@ export default (context, renderViaHTTP) => {
|
|||
|
||||
describe('editing a page', () => {
|
||||
it('should detect the changes and display it', async () => {
|
||||
const browser = await webdriver(context.appPort, '/hmr/about')
|
||||
const text = await browser
|
||||
.elementByCss('p').text()
|
||||
expect(text).toBe('This is the about page.')
|
||||
let browser
|
||||
try {
|
||||
browser = await webdriver(context.appPort, '/hmr/about')
|
||||
const text = await browser
|
||||
.elementByCss('p').text()
|
||||
expect(text).toBe('This is the about page.')
|
||||
|
||||
const aboutPagePath = join(__dirname, '../', 'pages', 'hmr', 'about.js')
|
||||
const aboutPagePath = join(__dirname, '../', 'pages', 'hmr', 'about.js')
|
||||
|
||||
const originalContent = readFileSync(aboutPagePath, 'utf8')
|
||||
const editedContent = originalContent.replace('This is the about page', 'COOL page')
|
||||
const originalContent = readFileSync(aboutPagePath, 'utf8')
|
||||
const editedContent = originalContent.replace('This is the about page', 'COOL page')
|
||||
|
||||
// change the content
|
||||
writeFileSync(aboutPagePath, editedContent, 'utf8')
|
||||
// change the content
|
||||
writeFileSync(aboutPagePath, editedContent, 'utf8')
|
||||
|
||||
await check(
|
||||
() => browser.elementByCss('body').text(),
|
||||
/COOL page/
|
||||
)
|
||||
await check(
|
||||
() => browser.elementByCss('body').text(),
|
||||
/COOL page/
|
||||
)
|
||||
|
||||
// add the original content
|
||||
writeFileSync(aboutPagePath, originalContent, 'utf8')
|
||||
// add the original content
|
||||
writeFileSync(aboutPagePath, originalContent, 'utf8')
|
||||
|
||||
await check(
|
||||
() => browser.elementByCss('body').text(),
|
||||
/This is the about page/
|
||||
)
|
||||
|
||||
browser.close()
|
||||
await check(
|
||||
() => browser.elementByCss('body').text(),
|
||||
/This is the about page/
|
||||
)
|
||||
} finally {
|
||||
if (browser) {
|
||||
browser.close()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('should not reload unrelated pages', async () => {
|
||||
const browser = await webdriver(context.appPort, '/hmr/counter')
|
||||
const text = await browser
|
||||
.elementByCss('button').click()
|
||||
.elementByCss('button').click()
|
||||
.elementByCss('p').text()
|
||||
expect(text).toBe('COUNT: 2')
|
||||
let browser
|
||||
try {
|
||||
browser = await webdriver(context.appPort, '/hmr/counter')
|
||||
const text = await browser
|
||||
.elementByCss('button').click()
|
||||
.elementByCss('button').click()
|
||||
.elementByCss('p').text()
|
||||
expect(text).toBe('COUNT: 2')
|
||||
|
||||
const aboutPagePath = join(__dirname, '../', 'pages', 'hmr', 'about.js')
|
||||
const aboutPagePath = join(__dirname, '../', 'pages', 'hmr', 'about.js')
|
||||
|
||||
const originalContent = readFileSync(aboutPagePath, 'utf8')
|
||||
const editedContent = originalContent.replace('This is the about page', 'COOL page')
|
||||
const originalContent = readFileSync(aboutPagePath, 'utf8')
|
||||
const editedContent = originalContent.replace('This is the about page', 'COOL page')
|
||||
|
||||
// Change the about.js page
|
||||
writeFileSync(aboutPagePath, editedContent, 'utf8')
|
||||
// Change the about.js page
|
||||
writeFileSync(aboutPagePath, editedContent, 'utf8')
|
||||
|
||||
// wait for 5 seconds
|
||||
await waitFor(5000)
|
||||
// wait for 5 seconds
|
||||
await waitFor(5000)
|
||||
|
||||
// Check whether the this page has reloaded or not.
|
||||
const newText = await browser
|
||||
.elementByCss('p').text()
|
||||
expect(newText).toBe('COUNT: 2')
|
||||
// Check whether the this page has reloaded or not.
|
||||
const newText = await browser
|
||||
.elementByCss('p').text()
|
||||
expect(newText).toBe('COUNT: 2')
|
||||
|
||||
// restore the about page content.
|
||||
writeFileSync(aboutPagePath, originalContent, 'utf8')
|
||||
|
||||
browser.close()
|
||||
// restore the about page content.
|
||||
writeFileSync(aboutPagePath, originalContent, 'utf8')
|
||||
} finally {
|
||||
if (browser) {
|
||||
browser.close()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Added because of a regression in react-hot-loader, see issues: #4246 #4273
|
||||
// Also: https://github.com/zeit/styled-jsx/issues/425
|
||||
it('should update styles correctly', async () => {
|
||||
const browser = await webdriver(context.appPort, '/hmr/style')
|
||||
const pTag = await browser.elementByCss('.hmr-style-page p')
|
||||
const initialFontSize = await pTag.getComputedCss('font-size')
|
||||
|
||||
expect(initialFontSize).toBe('100px')
|
||||
|
||||
const pagePath = join(__dirname, '../', 'pages', 'hmr', 'style.js')
|
||||
|
||||
const originalContent = readFileSync(pagePath, 'utf8')
|
||||
const editedContent = originalContent.replace('100px', '200px')
|
||||
|
||||
// Change the page
|
||||
writeFileSync(pagePath, editedContent, 'utf8')
|
||||
|
||||
// wait for 5 seconds
|
||||
await waitFor(5000)
|
||||
|
||||
let browser
|
||||
try {
|
||||
// Check whether the this page has reloaded or not.
|
||||
const editedPTag = await browser.elementByCss('.hmr-style-page p')
|
||||
const editedFontSize = await editedPTag.getComputedCss('font-size')
|
||||
browser = await webdriver(context.appPort, '/hmr/style')
|
||||
const pTag = await browser.elementByCss('.hmr-style-page p')
|
||||
const initialFontSize = await pTag.getComputedCss('font-size')
|
||||
|
||||
expect(editedFontSize).toBe('200px')
|
||||
expect(initialFontSize).toBe('100px')
|
||||
|
||||
const pagePath = join(__dirname, '../', 'pages', 'hmr', 'style.js')
|
||||
|
||||
const originalContent = readFileSync(pagePath, 'utf8')
|
||||
const editedContent = originalContent.replace('100px', '200px')
|
||||
|
||||
// Change the page
|
||||
writeFileSync(pagePath, editedContent, 'utf8')
|
||||
|
||||
// wait for 5 seconds
|
||||
await waitFor(5000)
|
||||
|
||||
try {
|
||||
// Check whether the this page has reloaded or not.
|
||||
const editedPTag = await browser.elementByCss('.hmr-style-page p')
|
||||
const editedFontSize = await editedPTag.getComputedCss('font-size')
|
||||
|
||||
expect(editedFontSize).toBe('200px')
|
||||
} finally {
|
||||
// Finally is used so that we revert the content back to the original regardless of the test outcome
|
||||
// restore the about page content.
|
||||
writeFileSync(pagePath, originalContent, 'utf8')
|
||||
}
|
||||
} finally {
|
||||
// Finally is used so that we revert the content back to the original regardless of the test outcome
|
||||
// restore the about page content.
|
||||
writeFileSync(pagePath, originalContent, 'utf8')
|
||||
if (browser) {
|
||||
browser.close()
|
||||
}
|
||||
}
|
||||
|
||||
browser.close()
|
||||
})
|
||||
|
||||
// Added because of a regression in react-hot-loader, see issues: #4246 #4273
|
||||
// Also: https://github.com/zeit/styled-jsx/issues/425
|
||||
it('should update styles in a stateful component correctly', async () => {
|
||||
const browser = await webdriver(context.appPort, '/hmr/style-stateful-component')
|
||||
const pTag = await browser.elementByCss('.hmr-style-page p')
|
||||
const initialFontSize = await pTag.getComputedCss('font-size')
|
||||
|
||||
expect(initialFontSize).toBe('100px')
|
||||
|
||||
let browser
|
||||
const pagePath = join(__dirname, '../', 'pages', 'hmr', 'style-stateful-component.js')
|
||||
|
||||
const originalContent = readFileSync(pagePath, 'utf8')
|
||||
const editedContent = originalContent.replace('100px', '200px')
|
||||
|
||||
// Change the page
|
||||
writeFileSync(pagePath, editedContent, 'utf8')
|
||||
|
||||
// wait for 5 seconds
|
||||
await waitFor(5000)
|
||||
|
||||
try {
|
||||
browser = await webdriver(context.appPort, '/hmr/style-stateful-component')
|
||||
const pTag = await browser.elementByCss('.hmr-style-page p')
|
||||
const initialFontSize = await pTag.getComputedCss('font-size')
|
||||
|
||||
expect(initialFontSize).toBe('100px')
|
||||
const editedContent = originalContent.replace('100px', '200px')
|
||||
|
||||
// Change the page
|
||||
writeFileSync(pagePath, editedContent, 'utf8')
|
||||
|
||||
// wait for 5 seconds
|
||||
await waitFor(5000)
|
||||
|
||||
// Check whether the this page has reloaded or not.
|
||||
const editedPTag = await browser.elementByCss('.hmr-style-page p')
|
||||
const editedFontSize = await editedPTag.getComputedCss('font-size')
|
||||
|
||||
expect(editedFontSize).toBe('200px')
|
||||
} finally {
|
||||
// Finally is used so that we revert the content back to the original regardless of the test outcome
|
||||
// restore the about page content.
|
||||
if (browser) {
|
||||
browser.close()
|
||||
}
|
||||
writeFileSync(pagePath, originalContent, 'utf8')
|
||||
browser.close()
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
@ -63,6 +63,16 @@ export default function ({ app }, suiteName, render, fetch) {
|
|||
expect(html).toContain('<div>World</div>')
|
||||
})
|
||||
|
||||
it('should render the page without `err` property', async () => {
|
||||
const html = await render('/')
|
||||
expect(html).not.toContain('"err"')
|
||||
})
|
||||
|
||||
it('should render the page without `nextExport` property', async () => {
|
||||
const html = await render('/')
|
||||
expect(html).not.toContain('"nextExport"')
|
||||
})
|
||||
|
||||
test('renders styled jsx', async () => {
|
||||
const $ = await get$('/styled-jsx')
|
||||
const styleId = $('#blue-box').attr('class')
|
||||
|
@ -112,14 +122,13 @@ export default function ({ app }, suiteName, render, fetch) {
|
|||
test('error-inside-page', async () => {
|
||||
const $ = await get$('/error-inside-page')
|
||||
expect($('pre').text()).toMatch(/This is an expected error/)
|
||||
// Check if the the source map line is correct
|
||||
expect($('body').text()).toMatch(/pages\/error-inside-page\.js:2:0/)
|
||||
// Sourcemaps are applied by react-error-overlay, so we can't check them on SSR.
|
||||
})
|
||||
|
||||
test('error-in-the-global-scope', async () => {
|
||||
const $ = await get$('/error-in-the-global-scope')
|
||||
expect($('pre').text()).toMatch(/aa is not defined/)
|
||||
expect($('body').text()).toMatch(/pages\/error-in-the-global-scope\.js:1:0/)
|
||||
// Sourcemaps are applied by react-error-overlay, so we can't check them on SSR.
|
||||
})
|
||||
|
||||
test('asPath', async () => {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
const withCSS = require('@zeit/next-css')
|
||||
// const withCSS = require('@zeit/next-css')
|
||||
const webpack = require('webpack')
|
||||
module.exports = withCSS({
|
||||
// module.exports = withCSS({
|
||||
module.exports = {
|
||||
onDemandEntries: {
|
||||
// Make sure entries are not getting disposed.
|
||||
maxInactiveAge: 1000 * 60 * 60
|
||||
|
@ -21,4 +22,5 @@ module.exports = withCSS({
|
|||
|
||||
return config
|
||||
}
|
||||
})
|
||||
// })
|
||||
}
|
||||
|
|
|
@ -24,8 +24,8 @@ describe('Configuration', () => {
|
|||
// pre-build all pages at the start
|
||||
await Promise.all([
|
||||
renderViaHTTP(context.appPort, '/next-config'),
|
||||
renderViaHTTP(context.appPort, '/build-id'),
|
||||
renderViaHTTP(context.appPort, '/webpack-css')
|
||||
renderViaHTTP(context.appPort, '/build-id')
|
||||
// renderViaHTTP(context.appPort, '/webpack-css')
|
||||
])
|
||||
})
|
||||
afterAll(() => killApp(context.server))
|
||||
|
|
|
@ -9,15 +9,15 @@ export default function ({ app }, suiteName, render, fetch) {
|
|||
}
|
||||
|
||||
describe(suiteName, () => {
|
||||
test('renders css imports', async () => {
|
||||
const $ = await get$('/webpack-css')
|
||||
expect($('._46QtCORzC4BWRnIseSbG-').text() === 'Hello World')
|
||||
})
|
||||
// test('renders css imports', async () => {
|
||||
// const $ = await get$('/webpack-css')
|
||||
// expect($('._46QtCORzC4BWRnIseSbG-').text() === 'Hello World')
|
||||
// })
|
||||
|
||||
test('renders non-js imports from node_modules', async () => {
|
||||
const $ = await get$('/webpack-css')
|
||||
expect($('._2pRSkKTPDMGLMnmsEkP__J').text() === 'Hello World')
|
||||
})
|
||||
// test('renders non-js imports from node_modules', async () => {
|
||||
// const $ = await get$('/webpack-css')
|
||||
// expect($('._2pRSkKTPDMGLMnmsEkP__J').text() === 'Hello World')
|
||||
// })
|
||||
|
||||
test('renders server config on the server only', async () => {
|
||||
const $ = await get$('/next-config')
|
||||
|
|
|
@ -4,16 +4,12 @@ import Router from 'next/router'
|
|||
import PropTypes from 'prop-types'
|
||||
|
||||
const HelloBundle = dynamic({
|
||||
modules: (props) => {
|
||||
modules: () => {
|
||||
const components = {
|
||||
HelloContext: import('../../components/hello-context'),
|
||||
Hello1: import('../../components/hello1')
|
||||
Hello1: import('../../components/hello1'),
|
||||
Hello2: import('../../components/hello2')
|
||||
}
|
||||
|
||||
if (props.showMore) {
|
||||
components.Hello2 = import('../../components/hello2')
|
||||
}
|
||||
|
||||
return components
|
||||
},
|
||||
render: (props, { HelloContext, Hello1, Hello2 }) => (
|
||||
|
@ -21,7 +17,7 @@ const HelloBundle = dynamic({
|
|||
<h1>{props.title}</h1>
|
||||
<HelloContext />
|
||||
<Hello1 />
|
||||
{Hello2? <Hello2 /> : null}
|
||||
{props.showMore ? <Hello2 /> : null}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
|
|
@ -15,6 +15,7 @@ import webdriver from 'next-webdriver'
|
|||
import fetch from 'node-fetch'
|
||||
import dynamicImportTests from '../../basic/test/dynamic'
|
||||
import security from './security'
|
||||
import {BUILD_MANIFEST, REACT_LOADABLE_MANIFEST} from 'next/constants'
|
||||
|
||||
const appDir = join(__dirname, '../')
|
||||
let appPort
|
||||
|
@ -55,7 +56,8 @@ describe('Production Usage', () => {
|
|||
|
||||
it('should set Cache-Control header', async () => {
|
||||
const buildId = readFileSync(join(__dirname, '../.next/BUILD_ID'), 'utf8')
|
||||
const buildManifest = require('../.next/build-manifest.json')
|
||||
const buildManifest = require(join('../.next', BUILD_MANIFEST))
|
||||
const reactLoadableManifest = require(join('../.next', REACT_LOADABLE_MANIFEST))
|
||||
const url = `http://localhost:${appPort}/_next/`
|
||||
|
||||
const resources = []
|
||||
|
@ -64,17 +66,20 @@ describe('Production Usage', () => {
|
|||
resources.push(`${url}${buildId}/page/index.js`)
|
||||
|
||||
// test dynamic chunk
|
||||
const chunkKey = Object.keys(buildManifest).find((x) => x.includes('chunks/'))
|
||||
resources.push(url + 'webpack/' + buildManifest[chunkKey])
|
||||
resources.push(url + reactLoadableManifest['../../components/hello1'][0].publicPath)
|
||||
|
||||
// test main.js
|
||||
const mainJsKey = Object.keys(buildManifest).find((x) => x === 'main.js')
|
||||
resources.push(url + buildManifest[mainJsKey])
|
||||
resources.push(url + buildManifest['static/commons/main.js'][0])
|
||||
|
||||
const responses = await Promise.all(resources.map((resource) => fetch(resource)))
|
||||
|
||||
responses.forEach((res) => {
|
||||
expect(res.headers.get('Cache-Control')).toBe('public, max-age=31536000, immutable')
|
||||
try {
|
||||
expect(res.headers.get('Cache-Control')).toBe('public, max-age=31536000, immutable')
|
||||
} catch (err) {
|
||||
err.message = res.url + ' ' + err.message
|
||||
throw err
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -110,6 +115,7 @@ describe('Production Usage', () => {
|
|||
const headingText = await browser.elementByCss('h1').text()
|
||||
// This makes sure we render statusCode on the client side correctly
|
||||
expect(headingText).toBe('500')
|
||||
browser.close()
|
||||
})
|
||||
|
||||
it('should render a client side component error', async () => {
|
||||
|
@ -117,6 +123,7 @@ describe('Production Usage', () => {
|
|||
await waitFor(2000)
|
||||
const text = await browser.elementByCss('body').text()
|
||||
expect(text).toMatch(/An unexpected error has occurred\./)
|
||||
browser.close()
|
||||
})
|
||||
|
||||
it('should call getInitialProps on _error page during a client side component error', async () => {
|
||||
|
@ -124,6 +131,7 @@ describe('Production Usage', () => {
|
|||
await waitFor(2000)
|
||||
const text = await browser.elementByCss('body').text()
|
||||
expect(text).toMatch(/This page could not be found\./)
|
||||
browser.close()
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
/* global describe, it, expect */
|
||||
|
||||
import {join} from 'path'
|
||||
import loadConfig from '../../dist/server/config'
|
||||
import {PHASE_DEVELOPMENT_SERVER} from '../../dist/lib/constants'
|
||||
import loadConfig from 'next/dist/server/config'
|
||||
import {PHASE_DEVELOPMENT_SERVER} from 'next/constants'
|
||||
|
||||
const pathToConfig = join(__dirname, '_resolvedata', 'without-function')
|
||||
const pathToConfigFn = join(__dirname, '_resolvedata', 'with-function')
|
||||
|
|
|
@ -8,10 +8,13 @@ import { spawn } from 'child_process'
|
|||
import { readFileSync, writeFileSync, existsSync, unlinkSync } from 'fs'
|
||||
import fkill from 'fkill'
|
||||
|
||||
import server from '../../dist/server/next'
|
||||
import build from '../../dist/build'
|
||||
import _export from '../../dist/server/export'
|
||||
import _pkg from '../../package.json'
|
||||
// `next` here is the symlink in `test/node_modules/next` which points to the root directory.
|
||||
// This is done so that requiring from `next` works.
|
||||
// The reason we don't import the relative path `../../dist/<etc>` is that it would lead to inconsistent module singletons
|
||||
import server from 'next/dist/server/next'
|
||||
import build from 'next/dist/build'
|
||||
import _export from 'next/dist/server/export'
|
||||
import _pkg from 'next/package.json'
|
||||
|
||||
export const nextServer = server
|
||||
export const nextBuild = build
|
||||
|
@ -148,10 +151,21 @@ export async function startStaticServer (dir) {
|
|||
}
|
||||
|
||||
export async function check (contentFn, regex) {
|
||||
while (true) {
|
||||
let found = false
|
||||
setTimeout(() => {
|
||||
if (found) {
|
||||
return
|
||||
}
|
||||
console.error('TIMED OUT CHECK: ', regex)
|
||||
throw new Error('TIMED OUT')
|
||||
}, 1000 * 30)
|
||||
while (!found) {
|
||||
try {
|
||||
const newContent = await contentFn()
|
||||
if (regex.test(newContent)) break
|
||||
if (regex.test(newContent)) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
await waitFor(1000)
|
||||
} catch (ex) {}
|
||||
}
|
||||
|
@ -183,3 +197,29 @@ export class File {
|
|||
this.write(this.originalContent)
|
||||
}
|
||||
}
|
||||
|
||||
// react-error-overlay uses an iframe so we have to read the contents from the frame
|
||||
export async function getReactErrorOverlayContent (browser) {
|
||||
let found = false
|
||||
setTimeout(() => {
|
||||
if (found) {
|
||||
return
|
||||
}
|
||||
console.error('TIMED OUT CHECK FOR IFRAME')
|
||||
throw new Error('TIMED OUT CHECK FOR IFRAME')
|
||||
}, 1000 * 30)
|
||||
while (!found) {
|
||||
try {
|
||||
const hasIframe = await browser.hasElementByCssSelector('iframe')
|
||||
if (!hasIframe) {
|
||||
throw new Error('Waiting for iframe')
|
||||
}
|
||||
|
||||
found = true
|
||||
return browser.eval(`document.querySelector('iframe').contentWindow.document.body.innerHTML`)
|
||||
} catch (ex) {
|
||||
await waitFor(1000)
|
||||
}
|
||||
}
|
||||
return browser.eval(`document.querySelector('iframe').contentWindow.document.body.innerHTML`)
|
||||
}
|
||||
|
|
|
@ -1,42 +0,0 @@
|
|||
/* global describe, it, expect */
|
||||
import { getModulePath } from '../../dist/build/babel/plugins/handle-import'
|
||||
|
||||
function cleanPath (mPath) {
|
||||
return mPath
|
||||
.replace(/\\/g, '/')
|
||||
.replace(/^.*:/, '')
|
||||
}
|
||||
|
||||
describe('handle-import-babel-plugin', () => {
|
||||
describe('getModulePath', () => {
|
||||
it('should not do anything to NPM modules', () => {
|
||||
const mPath = getModulePath('/abc/pages/about.js', 'cool-module')
|
||||
expect(mPath).toBe('cool-module')
|
||||
})
|
||||
|
||||
it('should not do anything to private NPM modules', () => {
|
||||
const mPath = getModulePath('/abc/pages/about.js', '@zeithq/cool-module')
|
||||
expect(mPath).toBe('@zeithq/cool-module')
|
||||
})
|
||||
|
||||
it('should resolve local modules', () => {
|
||||
const mPath = getModulePath('/abc/pages/about.js', '../components/hello.js')
|
||||
expect(cleanPath(mPath)).toBe('/abc/components/hello')
|
||||
})
|
||||
|
||||
it('should remove index.js', () => {
|
||||
const mPath = getModulePath('/abc/pages/about.js', '../components/c1/index.js')
|
||||
expect(cleanPath(mPath)).toBe('/abc/components/c1')
|
||||
})
|
||||
|
||||
it('should remove .js', () => {
|
||||
const mPath = getModulePath('/abc/pages/about.js', '../components/bb.js')
|
||||
expect(cleanPath(mPath)).toBe('/abc/components/bb')
|
||||
})
|
||||
|
||||
it('should remove end slash', () => {
|
||||
const mPath = getModulePath('/abc/pages/about.js', '../components/bb/')
|
||||
expect(cleanPath(mPath)).toBe('/abc/components/bb')
|
||||
})
|
||||
})
|
||||
})
|
|
@ -1,5 +1,5 @@
|
|||
/* global describe, it, expect */
|
||||
import Router from '../../dist/lib/router/router'
|
||||
import Router from 'next/dist/lib/router/router'
|
||||
|
||||
class PageLoader {
|
||||
constructor (options = {}) {
|
||||
|
|
|
@ -1,137 +0,0 @@
|
|||
/* global describe, it, expect */
|
||||
|
||||
import { SameLoopPromise } from '../../dist/lib/dynamic'
|
||||
|
||||
describe('SameLoopPromise', () => {
|
||||
describe('basic api', () => {
|
||||
it('should support basic promise resolving', (done) => {
|
||||
const promise = new SameLoopPromise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve(1000)
|
||||
}, 100)
|
||||
})
|
||||
|
||||
promise.then((value) => {
|
||||
expect(value).toBe(1000)
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('should support resolving in the same loop', () => {
|
||||
let gotValue = null
|
||||
const promise = new SameLoopPromise((resolve) => {
|
||||
resolve(1000)
|
||||
})
|
||||
|
||||
promise.then((value) => {
|
||||
gotValue = value
|
||||
})
|
||||
|
||||
expect(gotValue).toBe(1000)
|
||||
})
|
||||
|
||||
it('should support basic promise rejecting', (done) => {
|
||||
const error = new Error('Hello Error')
|
||||
const promise = new SameLoopPromise((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
reject(error)
|
||||
}, 100)
|
||||
})
|
||||
|
||||
promise.catch((err) => {
|
||||
expect(err).toBe(error)
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('should support rejecting in the same loop', () => {
|
||||
const error = new Error('Hello Error')
|
||||
let gotError = null
|
||||
const promise = new SameLoopPromise((resolve, reject) => {
|
||||
reject(error)
|
||||
})
|
||||
|
||||
promise.catch((err) => {
|
||||
gotError = err
|
||||
})
|
||||
|
||||
expect(gotError).toBe(error)
|
||||
})
|
||||
})
|
||||
|
||||
describe('complex usage', () => {
|
||||
it('should support a chain of promises', (done) => {
|
||||
const promise = new SameLoopPromise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve(1000)
|
||||
}, 100)
|
||||
})
|
||||
|
||||
promise
|
||||
.then((value) => value * 2)
|
||||
.then((value) => value + 10)
|
||||
.then((value) => {
|
||||
expect(value).toBe(2010)
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle the error inside the then', (done) => {
|
||||
const error = new Error('1000')
|
||||
const promise = new SameLoopPromise((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
reject(error)
|
||||
}, 100)
|
||||
})
|
||||
|
||||
promise
|
||||
.then(
|
||||
() => 4000,
|
||||
(err) => parseInt(err.message)
|
||||
)
|
||||
.then((value) => value + 10)
|
||||
.then((value) => {
|
||||
expect(value).toBe(1010)
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('should catch the error at the end', (done) => {
|
||||
const error = new Error('1000')
|
||||
const promise = new SameLoopPromise((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
reject(error)
|
||||
}, 100)
|
||||
})
|
||||
|
||||
promise
|
||||
.then((value) => value * 2)
|
||||
.then((value) => value + 10)
|
||||
.catch((err) => {
|
||||
expect(err).toBe(error)
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('should catch and proceed', (done) => {
|
||||
const error = new Error('1000')
|
||||
const promise = new SameLoopPromise((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
reject(error)
|
||||
}, 100)
|
||||
})
|
||||
|
||||
promise
|
||||
.then((value) => value * 2)
|
||||
.then((value) => value + 10)
|
||||
.catch((err) => {
|
||||
expect(err).toBe(error)
|
||||
return 5000
|
||||
})
|
||||
.then((value) => {
|
||||
expect(value).toBe(5000)
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
|
@ -1,31 +0,0 @@
|
|||
/* global describe, it, expect */
|
||||
|
||||
import { getChunkNameFromFilename } from '../../dist/server/utils'
|
||||
|
||||
describe('Server utils', () => {
|
||||
describe('getChunkNameFromFilename', () => {
|
||||
describe('development mode (no chunkhash)', () => {
|
||||
it('should strip the extension from the filename', () => {
|
||||
const filename = 'foo_bar_0123456789abcdef.js'
|
||||
expect(getChunkNameFromFilename(filename, true)).toBe('foo_bar_0123456789abcdef')
|
||||
})
|
||||
|
||||
it('should only strip the extension even if there\'s a hyphen in the name', () => {
|
||||
const filename = 'foo-bar-0123456789abcdef.js'
|
||||
expect(getChunkNameFromFilename(filename, true)).toBe('foo-bar-0123456789abcdef')
|
||||
})
|
||||
})
|
||||
|
||||
describe('production mode (with chunkhash)', () => {
|
||||
it('should strip the hash from the filename', () => {
|
||||
const filename = 'foo_bar_0123456789abcdef-0123456789abcdef.js'
|
||||
expect(getChunkNameFromFilename(filename, false)).toBe('foo_bar_0123456789abcdef')
|
||||
})
|
||||
|
||||
it('should only strip the part after the last hyphen in the filename', () => {
|
||||
const filename = 'foo-bar-0123456789abcdef-0123456789abcdef.js'
|
||||
expect(getChunkNameFromFilename(filename, false)).toBe('foo-bar-0123456789abcdef')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
|
@ -1,6 +1,6 @@
|
|||
/* global describe, it, expect */
|
||||
|
||||
import shallowEquals from '../../dist/lib/shallow-equals'
|
||||
import shallowEquals from 'next/dist/lib/shallow-equals'
|
||||
|
||||
describe('Shallow Equals', () => {
|
||||
it('should be true if both objects are the same', () => {
|
||||
|
|
Loading…
Reference in a new issue