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

Replace pages-plugin with loader (#5994)

* Remove unused argument

* Replace pages-plugin with loader

* Add loader-utils types

* Remove logs

* Bring back previous deposal behavior

* Remove console.log

* Remove webpack/utils as it’s no longer in use

* Remove hot-self-accept-loader

* Error Recovery tests

* Make hotSelfAccept a noop default loader

* Fix windows deleted/added

* Remove logging

* Remove unused variables

* Remove log

* Simplify entrypoint generation

* Don’t return the function

* Fix _app test

* Remove code that’s always true

* Move aliases to constants

* Use alias

* Join pages alias in reduce

* Default pages differently

* Loop over pages instead of manually defining

* Move entry generation into common function

* Update packages/next/build/webpack/loaders/next-client-pages-loader.ts

Co-Authored-By: timneutkens <tim@timneutkens.nl>

* Update packages/next/build/webpack/loaders/next-client-pages-loader.ts
This commit is contained in:
Tim Neutkens 2019-01-08 23:10:32 +01:00 committed by GitHub
parent 3a3347dc5f
commit 9ffd23eeef
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 201 additions and 511 deletions

View file

@ -0,0 +1,67 @@
import {join} from 'path'
import {stringify} from 'querystring'
import {PAGES_DIR_ALIAS, DOT_NEXT_ALIAS} from '../lib/constants'
import {ServerlessLoaderQuery} from './webpack/loaders/next-serverless-loader'
type PagesMapping = {
[page: string]: string
}
export function createPagesMapping(pagePaths: string[], extensions: string[]): PagesMapping {
const pages: PagesMapping = pagePaths.reduce((result: PagesMapping, pagePath): PagesMapping => {
const page = `/${pagePath.replace(new RegExp(`\\.+(${extensions.join('|')})$`), '').replace(/\\/g, '/')}`.replace(/\/index$/, '')
result[page === '' ? '/' : page] = join(PAGES_DIR_ALIAS, pagePath).replace(/\\/g, '/')
return result
}, {})
pages['/_app'] = pages['/_app'] || 'next/dist/pages/_app'
pages['/_error'] = pages['/_error'] || 'next/dist/pages/_error'
pages['/_document'] = pages['/_document'] || 'next/dist/pages/_document'
return pages
}
type WebpackEntrypoints = {
[bundle: string]: string|string[]
}
type Entrypoints = {
client: WebpackEntrypoints
server: WebpackEntrypoints
}
export function createEntrypoints(pages: PagesMapping, target: 'server'|'serverless', buildId: string, config: any): Entrypoints {
const client: WebpackEntrypoints = {}
const server: WebpackEntrypoints = {}
const defaultServerlessOptions = {
absoluteAppPath: pages['/_app'],
absoluteDocumentPath: pages['/_document'],
absoluteErrorPath: pages['/_error'],
distDir: DOT_NEXT_ALIAS,
buildId,
assetPrefix: config.assetPrefix,
generateEtags: config.generateEtags
}
Object.keys(pages).forEach((page) => {
const absolutePagePath = pages[page]
const bundleFile = page === '/' ? '/index.js' : `${page}.js`
const bundlePath = join('static', buildId, 'pages', bundleFile)
if(target === 'serverless') {
const serverlessLoaderOptions: ServerlessLoaderQuery = {page, absolutePagePath, ...defaultServerlessOptions}
server[join('pages', bundleFile)] = `next-serverless-loader?${stringify(serverlessLoaderOptions)}!`
} else if(target === 'server') {
server[bundlePath] = [absolutePagePath]
}
if (page === '/_document') {
return
}
client[bundlePath] = `next-client-pages-loader?${stringify({page, absolutePagePath})}!`
})
return {
client,
server
}
}

View file

@ -9,8 +9,7 @@ import {isWriteable} from './is-writeable'
import {runCompiler, CompilerResult} from './compiler'
import globModule from 'glob'
import {promisify} from 'util'
import {stringify} from 'querystring'
import {ServerlessLoaderQuery} from './webpack/loaders/next-serverless-loader'
import {createPagesMapping, createEntrypoints} from './entries'
const glob = promisify(globModule)
@ -29,58 +28,11 @@ export default async function build (dir: string, conf = null): Promise<void> {
const pagesDir = join(dir, 'pages')
const pagePaths = await collectPages(pagesDir, config.pageExtensions)
type Result = {[page: string]: string}
const pages: Result = pagePaths.reduce((result: Result, pagePath): Result => {
let page = `/${pagePath.replace(new RegExp(`\\.+(${config.pageExtensions.join('|')})$`), '').replace(/\\/g, '/')}`.replace(/\/index$/, '')
page = page === '' ? '/' : page
result[page] = pagePath
return result
}, {})
let entrypoints
if (config.target === 'serverless') {
const serverlessEntrypoints: any = {}
// Because on Windows absolute paths in the generated code can break because of numbers, eg 1 in the path,
// we have to use a private alias
const pagesDirAlias = 'private-next-pages'
const dotNextDirAlias = 'private-dot-next'
const absoluteAppPath = pages['/_app'] ? join(pagesDirAlias, pages['/_app']).replace(/\\/g, '/') : 'next/dist/pages/_app'
const absoluteDocumentPath = pages['/_document'] ? join(pagesDirAlias, pages['/_document']).replace(/\\/g, '/') : 'next/dist/pages/_document'
const absoluteErrorPath = pages['/_error'] ? join(pagesDirAlias, pages['/_error']).replace(/\\/g, '/') : 'next/dist/pages/_error'
const defaultOptions = {
absoluteAppPath,
absoluteDocumentPath,
absoluteErrorPath,
distDir: dotNextDirAlias,
buildId,
assetPrefix: config.assetPrefix,
generateEtags: config.generateEtags
}
Object.keys(pages).forEach(async (page) => {
if (page === '/_app' || page === '/_document') {
return
}
const absolutePagePath = join(pagesDirAlias, pages[page]).replace(/\\/g, '/')
const bundleFile = page === '/' ? '/index.js' : `${page}.js`
const serverlessLoaderOptions: ServerlessLoaderQuery = {page, absolutePagePath, ...defaultOptions}
serverlessEntrypoints[join('pages', bundleFile)] = `next-serverless-loader?${stringify(serverlessLoaderOptions)}!`
})
const errorPage = join('pages', '/_error.js')
if (!serverlessEntrypoints[errorPage]) {
const serverlessLoaderOptions: ServerlessLoaderQuery = {page: '/_error', absolutePagePath: 'next/dist/pages/_error', ...defaultOptions}
serverlessEntrypoints[errorPage] = `next-serverless-loader?${stringify(serverlessLoaderOptions)}!`
}
entrypoints = serverlessEntrypoints
}
const pages = createPagesMapping(pagePaths, config.pageExtensions)
const entrypoints = createEntrypoints(pages, config.target, buildId, config)
const configs: any = await Promise.all([
getBaseWebpackConfig(dir, { buildId, isServer: false, config, target: config.target }),
getBaseWebpackConfig(dir, { buildId, isServer: true, config, target: config.target, entrypoints })
getBaseWebpackConfig(dir, { buildId, isServer: false, config, target: config.target, entrypoints: entrypoints.client }),
getBaseWebpackConfig(dir, { buildId, isServer: true, config, target: config.target, entrypoints: entrypoints.server })
])
let result: CompilerResult = {warnings: [], errors: []}

View file

@ -4,8 +4,6 @@ import resolve from 'resolve'
import CaseSensitivePathPlugin from 'case-sensitive-paths-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 NextJsSSRModuleCachePlugin from './webpack/plugins/nextjs-ssr-module-cache'
import NextJsRequireCacheHotReloader from './webpack/plugins/nextjs-require-cache-hot-reloader'
@ -15,7 +13,7 @@ import BuildManifestPlugin from './webpack/plugins/build-manifest-plugin'
import ChunkNamesPlugin from './webpack/plugins/chunk-names-plugin'
import { ReactLoadablePlugin } from './webpack/plugins/react-loadable-plugin'
import {SERVER_DIRECTORY, REACT_LOADABLE_MANIFEST, CLIENT_STATIC_FILES_RUNTIME_WEBPACK, CLIENT_STATIC_FILES_RUNTIME_MAIN} from 'next-server/constants'
import {NEXT_PROJECT_ROOT, NEXT_PROJECT_ROOT_NODE_MODULES, NEXT_PROJECT_ROOT_DIST_CLIENT, DEFAULT_PAGES_DIR} from '../lib/constants'
import {NEXT_PROJECT_ROOT, NEXT_PROJECT_ROOT_NODE_MODULES, NEXT_PROJECT_ROOT_DIST_CLIENT, DEFAULT_PAGES_DIR, PAGES_DIR_ALIAS, DOT_NEXT_ALIAS} from '../lib/constants'
import AutoDllPlugin from 'autodll-webpack-plugin'
import TerserPlugin from 'terser-webpack-plugin'
import AssetsSizePlugin from './webpack/plugins/assets-size-plugin'
@ -139,22 +137,15 @@ function optimizationConfig ({ dev, isServer, totalPages, target }) {
return config
}
export default async function getBaseWebpackConfig (dir, {dev = false, isServer = false, buildId, config, target = 'server', entrypoints = false}) {
export default async function getBaseWebpackConfig (dir, {dev = false, isServer = false, buildId, config, target = 'server', entrypoints}) {
const defaultLoaders = {
babel: {
loader: 'next-babel-loader',
options: {dev, isServer, cwd: dir}
},
// Backwards compat
hotSelfAccept: {
loader: 'hot-self-accept-loader',
options: {
include: [
path.join(dir, 'pages')
],
// All pages are javascript files. So we apply hot-self-accept-loader here to facilitate hot reloading of pages.
// This makes sure plugins just have to implement `pageExtensions` instead of also implementing the loader
extensions: new RegExp(`\\.+(${config.pageExtensions.join('|')})$`)
}
loader: 'noop-loader'
}
}
@ -166,8 +157,7 @@ export default async function getBaseWebpackConfig (dir, {dev = false, isServer
const distDir = path.join(dir, config.distDir)
const outputDir = target === 'serverless' ? 'serverless' : SERVER_DIRECTORY
const outputPath = path.join(distDir, isServer ? outputDir : '')
const pagesEntries = await getPages(dir, {nextPagesDir: DEFAULT_PAGES_DIR, dev, buildId, isServer, pageExtensions: config.pageExtensions.join('|')})
const totalPages = Object.keys(pagesEntries).length
const totalPages = Object.keys(entrypoints).length
const clientEntries = !isServer ? {
// Backwards compatibility
'main.js': [],
@ -186,8 +176,8 @@ export default async function getBaseWebpackConfig (dir, {dev = false, isServer
],
alias: {
next: NEXT_PROJECT_ROOT,
'private-next-pages': path.join(dir, 'pages'),
'private-dot-next': distDir
[PAGES_DIR_ALIAS]: path.join(dir, 'pages'),
[DOT_NEXT_ALIAS]: distDir
},
mainFields: isServer ? ['main'] : ['browser', 'module', 'main']
}
@ -205,13 +195,9 @@ export default async function getBaseWebpackConfig (dir, {dev = false, isServer
context: dir,
// Kept as function to be backwards compatible
entry: async () => {
if (entrypoints) {
return entrypoints
}
return {
...clientEntries,
// Only _error and _document when in development. The rest is handled by on-demand-entries
...pagesEntries
...entrypoints
}
},
output: {
@ -244,11 +230,6 @@ export default async function getBaseWebpackConfig (dir, {dev = false, isServer
},
module: {
rules: [
dev && !isServer && {
test: defaultLoaders.hotSelfAccept.options.extensions,
include: defaultLoaders.hotSelfAccept.options.include,
use: defaultLoaders.hotSelfAccept
},
{
test: /\.(js|jsx)$/,
include: [dir, NEXT_PROJECT_ROOT_DIST_CLIENT, DEFAULT_PAGES_DIR, /next-server[\\/]dist[\\/]lib/],
@ -309,7 +290,6 @@ export default async function getBaseWebpackConfig (dir, {dev = false, isServer
}),
target !== 'serverless' && isServer && new PagesManifestPlugin(),
!isServer && new BuildManifestPlugin(),
!isServer && new PagesPlugin(),
isServer && new NextJsSsrImportPlugin(),
target !== 'serverless' && isServer && new NextJsSSRModuleCachePlugin({outputPath}),
!isServer && !dev && new AssetsSizePlugin(buildId, distDir)

View file

@ -1,50 +0,0 @@
import { relative } from 'path'
import loaderUtils from 'loader-utils'
module.exports = function (content, sourceMap) {
this.cacheable()
const options = loaderUtils.getOptions(this)
if (!options.extensions) {
throw new Error('extensions is not provided to hot-self-accept-loader. Please upgrade all next-plugins to the latest version.')
}
if (!options.include) {
throw new Error('include option is not provided to hot-self-accept-loader. Please upgrade all next-plugins to the latest version.')
}
const route = getRoute(this.resourcePath, options)
// Webpack has a built in system to prevent default from colliding, giving it a random letter per export.
// We can safely check if Component is undefined since all other pages imported into the entrypoint don't have __webpack_exports__.default
this.callback(null, `${content}
(function (Component, route) {
if(!Component) return
if (!module.hot) return
module.hot.accept()
Component.__route = route
if (module.hot.status() === 'idle') return
var components = next.router.components
for (var r in components) {
if (!components.hasOwnProperty(r)) continue
if (components[r].Component.__route === route) {
next.router.update(r, Component)
}
}
})(typeof __webpack_exports__ !== 'undefined' ? __webpack_exports__.default : (module.exports.default || module.exports), ${JSON.stringify(route)})
`, sourceMap)
}
function getRoute (resourcePath, options) {
const dir = options.include.find((d) => resourcePath.indexOf(d) === 0)
if (!dir) {
throw new Error(`'hot-self-accept-loader' was called on a file that isn't a page.`)
}
const path = relative(dir, resourcePath).replace(options.extensions, '.js')
return '/' + path.replace(/((^|\/)index)?\.js$/, '')
}

View file

@ -0,0 +1,29 @@
import {loader} from 'webpack'
import loaderUtils from 'loader-utils'
export type ClientPagesLoaderOptions = {
absolutePagePath: string,
page: string
}
const nextClientPagesLoader: loader.Loader = function () {
const {absolutePagePath, page}: any = loaderUtils.getOptions(this)
const stringifiedAbsolutePagePath = JSON.stringify(absolutePagePath)
const stringifiedPage = JSON.stringify(page)
return `
(window.__NEXT_P=window.__NEXT_P||[]).push([${stringifiedPage}, function() {
var page = require(${stringifiedAbsolutePagePath})
if(module.hot) {
module.hot.accept(${stringifiedAbsolutePagePath}, function() {
if(!next.router.components[${stringifiedPage}]) return
var updatedPage = require(${stringifiedAbsolutePagePath})
next.router.update(${stringifiedPage}, updatedPage.default || updatedPage)
})
}
return { page: page.default || page }
}]);
`
}
export default nextClientPagesLoader

View file

@ -0,0 +1 @@
module.exports = (source) => source

View file

@ -1,53 +0,0 @@
import { ConcatSource } from 'webpack-sources'
import {
IS_BUNDLED_PAGE_REGEX,
ROUTE_NAME_REGEX
} from 'next-server/constants'
export default class PagesPlugin {
apply (compiler) {
compiler.hooks.compilation.tap('PagesPlugin', (compilation) => {
// This hook is triggered right before a module gets wrapped into it's initializing function,
// For example when you look at the source of a bundle you'll see an object holding `'pages/_app.js': function(module, etc, etc)`
// This hook triggers right before that code is added and wraps the module into `__NEXT_REGISTER_PAGE` when the module is a page
// The reason we're doing this is that we don't want to execute the page code which has potential side effects before switching to a route
compilation.moduleTemplates.javascript.hooks.render.tap('PagesPluginRenderPageRegister', (moduleSourcePostModule, module, options) => {
const {chunk} = options
// check if the current module is the entry module, we only want to wrap the topmost module
if (chunk.entryModule !== module) {
return moduleSourcePostModule
}
// Check if the chunk is a page
if (!IS_BUNDLED_PAGE_REGEX.test(chunk.name)) {
return moduleSourcePostModule
}
// Match the route the chunk belongs to
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(
`(window.__NEXT_P=window.__NEXT_P||[]).push(['${routeName}', function() {\n`,
moduleSourcePostModule,
'\nreturn { page: module.exports.default }',
'}]);'
)
return source
})
})
}
}

View file

@ -1,80 +0,0 @@
import path from 'path'
import promisify from '../../lib/promisify'
import globModule from 'glob'
import {CLIENT_STATIC_FILES_PATH} from 'next-server/constants'
const glob = promisify(globModule)
export async function getPages (dir, {nextPagesDir, dev, buildId, isServer, pageExtensions}) {
const pageFiles = await getPagePaths(dir, {dev, isServer, pageExtensions})
return getPageEntries(pageFiles, {nextPagesDir, buildId, isServer, pageExtensions})
}
export async function getPagePaths (dir, {dev, isServer, pageExtensions}) {
let pages
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
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 })
}
return pages
}
// Convert page path into single entry
export function createEntry (filePath, {buildId = '', name, pageExtensions} = {}) {
const parsedPath = path.parse(filePath)
let entryName = name || filePath
// This makes sure we compile `pages/blog/index.js` to `pages/blog.js`.
// Excludes `pages/index.js` from this rule since we do want `/` to route to `pages/index.js`
if (parsedPath.dir !== 'pages' && parsedPath.name === 'index') {
entryName = `${parsedPath.dir}.js`
}
// Makes sure supported extensions are stripped off. The outputted file should always be `.js`
if (pageExtensions) {
entryName = entryName.replace(new RegExp(`\\.+(${pageExtensions})$`), '.js')
}
return {
name: path.join(CLIENT_STATIC_FILES_PATH, buildId, entryName),
files: [parsedPath.root ? filePath : `./${filePath}`] // The entry always has to be an array.
}
}
// Convert page paths into entries
export function getPageEntries (pagePaths, {nextPagesDir, buildId, isServer = false, pageExtensions} = {}) {
const entries = {}
for (const filePath of pagePaths) {
const entry = createEntry(filePath, {pageExtensions, buildId})
entries[entry.name] = entry.files
}
const appPagePath = path.join(nextPagesDir, '_app.js')
const appPageEntry = createEntry(appPagePath, {buildId, name: 'pages/_app.js'}) // default app.js
if (!entries[appPageEntry.name]) {
entries[appPageEntry.name] = appPageEntry.files
}
const errorPagePath = path.join(nextPagesDir, '_error.js')
const errorPageEntry = createEntry(errorPagePath, {buildId, name: 'pages/_error.js'}) // default error.js
if (!entries[errorPageEntry.name]) {
entries[errorPageEntry.name] = errorPageEntry.files
}
if (isServer) {
const documentPagePath = path.join(nextPagesDir, '_document.js')
const documentPageEntry = createEntry(documentPagePath, {buildId, name: 'pages/_document.js'}) // default _document.js
if (!entries[documentPageEntry.name]) {
entries[documentPageEntry.name] = documentPageEntry.files
}
}
return entries
}

View file

@ -1,65 +1,5 @@
import 'event-source-polyfill'
import connect from './dev-error-overlay/hot-dev-client'
import Router from 'next/router'
const handlers = {
reload (route) {
// If the App component changes we have to reload the current route, this is handled by hot-self-accept-loader
// So we just return
if (route === '/_app') {
return
}
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
}
// 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, this is handled by hot-self-accept-loader
// So we just return
if (route === '/_app') {
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.warn(`Hard reloading due to no default component in page: ${route}`)
window.location.reload()
}
}
}
export default ({assetPrefix}) => {
const options = {
path: `${assetPrefix}/_next/webpack-hmr`
@ -68,13 +8,24 @@ export default ({assetPrefix}) => {
const devClient = connect(options)
devClient.subscribeToHmrEvent((obj) => {
const fn = handlers[obj.action]
if (fn) {
const data = obj.data || []
fn(...data)
} else {
throw new Error('Unexpected action ' + obj.action)
if (obj.action === 'reloadPage') {
return window.location.reload()
}
if (obj.action === 'removedPage') {
const [page] = obj.data
if (page === window.next.router.pathname) {
return window.location.reload()
}
return
}
if (obj.action === 'addedPage') {
const [page] = obj.data
if (page === window.next.router.pathname && typeof window.next.router.components[page] === 'undefined') {
return window.location.reload()
}
return
}
throw new Error('Unexpected action ' + obj.action)
})
return devClient

View file

@ -5,3 +5,8 @@ export const NEXT_PROJECT_ROOT_NODE_MODULES = join(NEXT_PROJECT_ROOT, 'node_modu
export const DEFAULT_PAGES_DIR = join(NEXT_PROJECT_ROOT_DIST, 'pages')
export const NEXT_PROJECT_ROOT_DIST_CLIENT = join(NEXT_PROJECT_ROOT_DIST, 'client')
export const NEXT_PROJECT_ROOT_DIST_SERVER = join(NEXT_PROJECT_ROOT_DIST, 'server')
// Because on Windows absolute paths in the generated code can break because of numbers, eg 1 in the path,
// we have to use a private alias
export const PAGES_DIR_ALIAS = 'private-next-pages'
export const DOT_NEXT_ALIAS = 'private-dot-next'

View file

@ -48,6 +48,7 @@
"@babel/runtime": "7.1.2",
"@babel/runtime-corejs2": "7.1.2",
"@babel/template": "7.1.2",
"@types/loader-utils": "1.1.3",
"@types/node-fetch": "2.1.4",
"@types/rimraf": "2.0.2",
"ansi-html": "0.0.7",

View file

@ -1,4 +1,4 @@
import { join, relative, sep, normalize } from 'path'
import { join, normalize } from 'path'
import WebpackDevMiddleware from 'webpack-dev-middleware'
import WebpackHotMiddleware from 'webpack-hot-middleware'
import errorOverlayMiddleware from './lib/error-overlay-middleware'
@ -7,8 +7,13 @@ import onDemandEntryHandler, {normalizePage} from './on-demand-entry-handler'
import webpack from 'webpack'
import WebSocket from 'ws'
import getBaseWebpackConfig from '../build/webpack-config'
import {IS_BUNDLED_PAGE_REGEX, ROUTE_NAME_REGEX, BLOCKED_PAGES, CLIENT_STATIC_FILES_PATH} from 'next-server/constants'
import {IS_BUNDLED_PAGE_REGEX, ROUTE_NAME_REGEX, BLOCKED_PAGES} from 'next-server/constants'
import {route} from 'next-server/dist/server/router'
import globModule from 'glob'
import {promisify} from 'util'
import {createPagesMapping, createEntrypoints} from '../build/entries'
const glob = promisify(globModule)
export async function renderScriptError (res, error) {
// Asks CDNs and others to not to cache the errored page
@ -92,10 +97,6 @@ export default class HotReloader {
this.webpackHotMiddleware = null
this.initialized = false
this.stats = null
this.compilationErrors = null
this.prevChunkNames = null
this.prevFailedChunkNames = null
this.prevChunkHashes = null
this.serverPrevDocumentHash = null
this.config = config
@ -115,7 +116,7 @@ export default class HotReloader {
// we have to compile the page using on-demand-entries, this middleware will handle doing that
// by adding the page to on-demand-entries, waiting till it's done
// and then the bundle will be served like usual by the actual route in server/index.js
const handlePageBundleRequest = async (req, res, parsedUrl) => {
const handlePageBundleRequest = async (res, parsedUrl) => {
const {pathname} = parsedUrl
const params = matchNextPageBundleRequest(pathname)
if (!params) {
@ -145,7 +146,7 @@ export default class HotReloader {
return {}
}
const {finished} = await handlePageBundleRequest(req, res, parsedUrl)
const {finished} = await handlePageBundleRequest(res, parsedUrl)
for (const fn of this.middlewares) {
await new Promise((resolve, reject) => {
@ -169,6 +170,16 @@ export default class HotReloader {
}))
}
async getWebpackConfig () {
const pagePaths = await glob(`+(_app|_document|_error).+(${this.config.pageExtensions.join('|')})`, {cwd: join(this.dir, 'pages')})
const pages = createPagesMapping(pagePaths, this.config.pageExtensions)
const entrypoints = createEntrypoints(pages, 'server', this.buildId, this.config)
return Promise.all([
getBaseWebpackConfig(this.dir, { dev: true, isServer: false, config: this.config, buildId: this.buildId, entrypoints: entrypoints.client }),
getBaseWebpackConfig(this.dir, { dev: true, isServer: true, config: this.config, buildId: this.buildId, entrypoints: entrypoints.server })
])
}
async start () {
await this.clean()
@ -188,10 +199,7 @@ export default class HotReloader {
})
})
const configs = await Promise.all([
getBaseWebpackConfig(this.dir, { dev: true, isServer: false, config: this.config, buildId: this.buildId }),
getBaseWebpackConfig(this.dir, { dev: true, isServer: true, config: this.config, buildId: this.buildId })
])
const configs = await this.getWebpackConfig()
this.addWsPort(configs)
const multiCompiler = webpack(configs)
@ -220,10 +228,7 @@ export default class HotReloader {
await this.clean()
const configs = await Promise.all([
getBaseWebpackConfig(this.dir, { dev: true, isServer: false, config: this.config, buildId: this.buildId }),
getBaseWebpackConfig(this.dir, { dev: true, isServer: true, config: this.config, buildId: this.buildId })
])
const configs = await this.getWebpackConfig()
this.addWsPort(configs)
const compiler = webpack(configs)
@ -280,8 +285,7 @@ export default class HotReloader {
}
// Notify reload to reload the page, as _document.js was changed (different hash)
this.send('reload', '/_document')
this.send('reloadPage')
this.serverPrevDocumentHash = documentChunk.hash
})
@ -293,61 +297,32 @@ export default class HotReloader {
.filter(name => IS_BUNDLED_PAGE_REGEX.test(name))
)
const failedChunkNames = new Set(Object.keys(erroredPages(compilation)))
const chunkHashes = new Map(
compilation.chunks
.filter(c => IS_BUNDLED_PAGE_REGEX.test(c.name))
.map((c) => [c.name, c.hash])
)
if (this.initialized) {
// detect chunks which have to be replaced with a new template
// e.g, pages/index.js <-> pages/_error.js
const added = diff(chunkNames, this.prevChunkNames)
const removed = diff(this.prevChunkNames, chunkNames)
const succeeded = diff(this.prevFailedChunkNames, failedChunkNames)
const addedPages = diff(chunkNames, this.prevChunkNames)
const removedPages = diff(this.prevChunkNames, chunkNames)
// reload all failed chunks to replace the templace to the error ones,
// and to update error content
const failed = failedChunkNames
const rootDir = join(CLIENT_STATIC_FILES_PATH, this.buildId, 'pages')
for (const n of new Set([...added, ...succeeded, ...removed, ...failed])) {
const route = toRoute(relative(rootDir, n))
this.send('reload', route)
if (addedPages.size > 0) {
for (const addedPage of addedPages) {
let page = '/' + ROUTE_NAME_REGEX.exec(addedPage)[1].replace(/\\/g, '/')
page = page === '/index' ? '/' : page
this.send('addedPage', page)
}
}
let changedPageRoutes = []
for (const [n, hash] of chunkHashes) {
if (!this.prevChunkHashes.has(n)) continue
if (this.prevChunkHashes.get(n) === hash) continue
const route = toRoute(relative(rootDir, n))
changedPageRoutes.push(route)
}
// This means `/_app` is most likely included in the list, or a page was added/deleted in this compilation run.
// This means we should filter out `/_app` because `/_app` will be re-rendered with the page reload.
if (added.size !== 0 || removed.size !== 0 || changedPageRoutes.length > 1) {
changedPageRoutes = changedPageRoutes.filter((route) => route !== '/_app' && route !== '/_document')
}
for (const changedPageRoute of changedPageRoutes) {
// notify change to recover from runtime errors
this.send('change', changedPageRoute)
if (removedPages.size > 0) {
for (const removedPage of removedPages) {
let page = '/' + ROUTE_NAME_REGEX.exec(removedPage)[1].replace(/\\/g, '/')
page = page === '/index' ? '/' : page
this.send('removedPage', page)
}
}
}
this.initialized = true
this.stats = stats
this.compilationErrors = null
this.prevChunkNames = chunkNames
this.prevFailedChunkNames = failedChunkNames
this.prevChunkHashes = chunkHashes
})
// We dont watch .git/ .next/ and node_modules for changes
@ -381,7 +356,6 @@ export default class HotReloader {
const onDemandEntries = onDemandEntryHandler(webpackDevMiddleware, multiCompiler, {
dir: this.dir,
buildId: this.buildId,
dev: true,
reload: this.reload.bind(this),
pageExtensions: this.config.pageExtensions,
wsPort: this.wsPort,
@ -433,7 +407,7 @@ export default class HotReloader {
async ensurePage (page) {
// Make sure we don't re-build or dispose prebuilt pages
if (page === '/_error' || page === '/_document' || page === '/_app') {
if (BLOCKED_PAGES.indexOf(page) !== -1) {
return
}
await this.onDemandEntries.ensurePage(page)
@ -443,8 +417,3 @@ export default class HotReloader {
function diff (a, b) {
return new Set([...a].filter((v) => !b.has(v)))
}
function toRoute (file) {
const f = sep === '\\' ? file.replace(/\\/g, '/') : file
return ('/' + f).replace(/(\/index)?\.js$/, '') || '/'
}

View file

@ -6,8 +6,8 @@ import promisify from '../lib/promisify'
import globModule from 'glob'
import {pageNotFoundError} from 'next-server/dist/server/require'
import {normalizePagePath} from 'next-server/dist/server/normalize-page-path'
import {createEntry} from '../build/webpack/utils'
import { ROUTE_NAME_REGEX, IS_BUNDLED_PAGE_REGEX } from 'next-server/constants'
import {stringify} from 'querystring'
const ADDED = Symbol('added')
const BUILDING = Symbol('building')
@ -30,7 +30,6 @@ function addEntry (compilation, context, name, entry) {
export default function onDemandEntryHandler (devMiddleware, multiCompiler, {
buildId,
dir,
dev,
reload,
pageExtensions,
maxInactiveAge,
@ -51,20 +50,17 @@ export default function onDemandEntryHandler (devMiddleware, multiCompiler, {
invalidator.startBuilding()
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
}
const { name, absolutePagePath } = entries[page]
try {
await access(absolutePagePath, (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)
return addEntry(compilation, compiler.context, name, [compiler.name === 'client' ? `next-client-pages-loader?${stringify({page, absolutePagePath})}!` : absolutePagePath])
})
return Promise.all(allEntries)
@ -178,17 +174,19 @@ export default function onDemandEntryHandler (devMiddleware, multiCompiler, {
}
const extensions = pageExtensions.join('|')
const paths = await glob(`pages/{${normalizedPagePath}/index,${normalizedPagePath}}.+(${extensions})`, {cwd: dir})
const pagesDir = join(dir, 'pages')
const paths = await glob(`{${normalizedPagePath.slice(1)}/index,${normalizedPagePath.slice(1)}}.+(${extensions})`, {cwd: pagesDir})
if (paths.length === 0) {
throw pageNotFoundError(normalizedPagePath)
}
const relativePathToPage = paths[0]
const pathname = join(dir, relativePathToPage)
const {name, files} = createEntry(relativePathToPage, {buildId, pageExtensions: extensions})
const pagePath = paths[0]
let pageUrl = `/${pagePath.replace(new RegExp(`\\.+(${extensions})$`), '').replace(/\\/g, '/')}`.replace(/\/index$/, '')
pageUrl = pageUrl === '' ? '/' : pageUrl
const bundleFile = pageUrl === '/' ? '/index.js' : `${pageUrl}.js`
const name = join('static', buildId, 'pages', bundleFile)
const absolutePagePath = join(pagesDir, pagePath)
await new Promise((resolve, reject) => {
const entryInfo = entries[page]
@ -207,7 +205,7 @@ export default function onDemandEntryHandler (devMiddleware, multiCompiler, {
console.log(`> Building page: ${page}`)
entries[page] = { name, entry: files, pathname, status: ADDED }
entries[page] = { name, absolutePagePath, status: ADDED }
doneCallbacks.once(page, handleCallback)
invalidator.invalidate()

View file

@ -3,7 +3,7 @@ import webdriver from 'next-webdriver'
import { join } from 'path'
import { check, File, waitFor, getReactErrorOverlayContent, getBrowserBodyText } from 'next-test-utils'
export default (context) => {
export default (context, renderViaHTTP) => {
describe('Error Recovery', () => {
it('should recover from 404 after a page has been added', async () => {
let browser
@ -110,6 +110,8 @@ export default (context) => {
const aboutPage = new File(join(__dirname, '../', 'pages', 'hmr', 'about.js'))
let browser
try {
await renderViaHTTP('/hmr/about')
aboutPage.replace('</div>', 'div')
browser = await webdriver(context.appPort, '/hmr/contact')

View file

@ -1,90 +0,0 @@
/* eslint-env jest */
import {normalize, join} from 'path'
import {getPageEntries, createEntry} from 'next/dist/build/webpack/utils'
const buildId = 'development'
describe('createEntry', () => {
it('Should turn a path into a page entry', () => {
const entry = createEntry('pages/index.js')
expect(entry.name).toBe(normalize(`static/pages/index.js`))
expect(entry.files[0]).toBe('./pages/index.js')
})
it('Should have a custom name', () => {
const entry = createEntry('pages/index.js', {name: 'something-else.js'})
expect(entry.name).toBe(normalize(`static/something-else.js`))
expect(entry.files[0]).toBe('./pages/index.js')
})
it('Should allow custom extension like .ts to be turned into .js', () => {
const entry = createEntry('pages/index.ts', {pageExtensions: ['js', 'ts'].join('|')})
expect(entry.name).toBe(normalize('static/pages/index.js'))
expect(entry.files[0]).toBe('./pages/index.ts')
})
it('Should allow custom extension like .jsx to be turned into .js', () => {
const entry = createEntry('pages/index.jsx', {pageExtensions: ['jsx', 'js'].join('|')})
expect(entry.name).toBe(normalize('static/pages/index.js'))
expect(entry.files[0]).toBe('./pages/index.jsx')
})
it('Should allow custom extension like .tsx to be turned into .js', () => {
const entry = createEntry('pages/index.tsx', {pageExtensions: ['tsx', 'ts'].join('|')})
expect(entry.name).toBe(normalize('static/pages/index.js'))
expect(entry.files[0]).toBe('./pages/index.tsx')
})
it('Should allow custom extension like .tsx to be turned into .js with another order', () => {
const entry = createEntry('pages/index.tsx', {pageExtensions: ['ts', 'tsx'].join('|')})
expect(entry.name).toBe(normalize('static/pages/index.js'))
expect(entry.files[0]).toBe('./pages/index.tsx')
})
it('Should turn pages/blog/index.js into pages/blog.js', () => {
const entry = createEntry('pages/blog/index.js')
expect(entry.name).toBe(normalize('static/pages/blog.js'))
expect(entry.files[0]).toBe('./pages/blog/index.js')
})
it('Should add buildId when provided', () => {
const entry = createEntry('pages/blog/index.js', {buildId})
expect(entry.name).toBe(normalize(`static/${buildId}/pages/blog.js`))
expect(entry.files[0]).toBe('./pages/blog/index.js')
})
})
describe('getPageEntries', () => {
const nextPagesDir = join(__dirname, '..', '..', 'dist', 'pages')
it('Should return paths', () => {
const pagePaths = ['pages/index.js']
const pageEntries = getPageEntries(pagePaths, {nextPagesDir})
expect(pageEntries[normalize('static/pages/index.js')][0]).toBe('./pages/index.js')
})
it('Should include default _error', () => {
const pagePaths = ['pages/index.js']
const pageEntries = getPageEntries(pagePaths, {nextPagesDir})
expect(pageEntries[normalize('static/pages/_error.js')][0]).toMatch(/dist[/\\]pages[/\\]_error\.js/)
})
it('Should not include default _error when _error.js is inside the pages directory', () => {
const pagePaths = ['pages/index.js', 'pages/_error.js']
const pageEntries = getPageEntries(pagePaths, {nextPagesDir})
expect(pageEntries[normalize('static/pages/_error.js')][0]).toBe('./pages/_error.js')
})
it('Should include default _document when isServer is true', () => {
const pagePaths = ['pages/index.js']
const pageEntries = getPageEntries(pagePaths, {nextPagesDir, isServer: true})
expect(pageEntries[normalize('static/pages/_document.js')][0]).toMatch(/dist[/\\]pages[/\\]_document\.js/)
})
it('Should not include default _document when _document.js is inside the pages directory', () => {
const pagePaths = ['pages/index.js', 'pages/_document.js']
const pageEntries = getPageEntries(pagePaths, {nextPagesDir, isServer: true})
expect(pageEntries[normalize('static/pages/_document.js')][0]).toBe('./pages/_document.js')
})
})

View file

@ -1411,6 +1411,14 @@
"@types/minimatch" "*"
"@types/node" "*"
"@types/loader-utils@1.1.3":
version "1.1.3"
resolved "https://registry.yarnpkg.com/@types/loader-utils/-/loader-utils-1.1.3.tgz#82b9163f2ead596c68a8c03e450fbd6e089df401"
integrity sha512-euKGFr2oCB3ASBwG39CYJMR3N9T0nanVqXdiH7Zu/Nqddt6SmFRxytq/i2w9LQYNQekEtGBz+pE3qG6fQTNvRg==
dependencies:
"@types/node" "*"
"@types/webpack" "*"
"@types/mime@*":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.0.tgz#5a7306e367c539b9f6543499de8dd519fac37a8b"
@ -1502,7 +1510,7 @@
dependencies:
source-map "^0.6.1"
"@types/webpack@4.4.22":
"@types/webpack@*", "@types/webpack@4.4.22":
version "4.4.22"
resolved "https://registry.yarnpkg.com/@types/webpack/-/webpack-4.4.22.tgz#c4a5ea8b74a31b579537515bcfe86d2b2a34382c"
integrity sha512-PxAAzli3krZX9rCeONSR5Z9v4CR/2HPsKsiVRFNDo9OZefN+dTemteMHZnYkddOu4bqoYqJTJ724gLy0ZySXOw==