1
0
Fork 0
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:
Tim Neutkens 2018-07-24 11:24:40 +02:00 committed by GitHub
parent e2b518525c
commit 75476a9136
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
73 changed files with 3038 additions and 2456 deletions

View file

@ -8,7 +8,7 @@
}
},
language: "node_js",
node_js: ["6"],
node_js: ["8"],
cache: {
directories: ["node_modules"]
},

View file

@ -1,6 +1,6 @@
environment:
matrix:
- nodejs_version: "6"
- nodejs_version: "8"
# Install scripts. (runs after repo cloning)
install:

View file

@ -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)
}
}
}
})

View 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
})
)
)
])
)
)
})
}
}
}
}

View file

@ -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'] || {

View file

@ -51,7 +51,7 @@ function runCompiler (compiler) {
return reject(error)
}
resolve(jsonStats)
resolve()
})
})
}

View file

@ -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
}

View file

@ -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()
})
}

View 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
}
})
})
}
}

View file

@ -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]
})
})
})
}
}

View 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()
})
}
}

View file

@ -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}/"`)
})
})
}

View 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
}
})
})
}
}

View file

@ -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 = {}

View file

@ -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
})
})
}
}

View 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

View file

@ -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])

View file

@ -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 })

View file

@ -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

View 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]
}

View 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

View 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)
}
}

View file

@ -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

View file

@ -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 })
}

View file

@ -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)
})

View file

@ -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'
})

View file

@ -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
}

View file

@ -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
View 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
View 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
View 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'>;
}

View file

@ -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'

View file

@ -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)
}

View file

@ -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}>

View file

@ -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]

View file

@ -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)
}

View file

@ -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) {

View file

@ -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",

View file

@ -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 }) =>

View file

@ -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} />}

View file

@ -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

View file

@ -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)))
}

View file

@ -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) {

View file

@ -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
}
}

View file

@ -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
}

View file

@ -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) {

View file

@ -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
}

View file

@ -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\//

View file

@ -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 = {

View file

@ -0,0 +1,5 @@
import currentState from '../shared-module'
export default () => {
return <p id='currentstate'>{currentState()}</p>
}

View file

@ -0,0 +1,9 @@
let moduleState = 'INITIAL'
export function setState (state) {
moduleState = state
}
export default function currentState () {
return moduleState
}

View file

@ -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()
})
})
}

View file

@ -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')
})
})
})
}

View file

@ -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>
)
})

View file

@ -1,3 +1,4 @@
import dynamic from 'next/dynamic'
const Hello = dynamic(import('../../components/hello1'))

View file

@ -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()
}
}
})
})

View file

@ -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', () => {
// })
})
}

View file

@ -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()
}
}
})
})
}

View file

@ -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()
}
})

View file

@ -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 () => {

View file

@ -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
}
})
// })
}

View file

@ -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))

View file

@ -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')

View file

@ -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>
)
})

View file

@ -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()
})
})

View file

@ -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')

View file

@ -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`)
}

View file

@ -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')
})
})
})

View file

@ -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 = {}) {

View file

@ -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()
})
})
})
})

View file

@ -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')
})
})
})
})

View file

@ -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', () => {

1531
yarn.lock

File diff suppressed because it is too large Load diff