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:
parent
3a3347dc5f
commit
9ffd23eeef
67
packages/next/build/entries.ts
Normal file
67
packages/next/build/entries.ts
Normal 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
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,8 +9,7 @@ import {isWriteable} from './is-writeable'
|
||||||
import {runCompiler, CompilerResult} from './compiler'
|
import {runCompiler, CompilerResult} from './compiler'
|
||||||
import globModule from 'glob'
|
import globModule from 'glob'
|
||||||
import {promisify} from 'util'
|
import {promisify} from 'util'
|
||||||
import {stringify} from 'querystring'
|
import {createPagesMapping, createEntrypoints} from './entries'
|
||||||
import {ServerlessLoaderQuery} from './webpack/loaders/next-serverless-loader'
|
|
||||||
|
|
||||||
const glob = promisify(globModule)
|
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 pagesDir = join(dir, 'pages')
|
||||||
|
|
||||||
const pagePaths = await collectPages(pagesDir, config.pageExtensions)
|
const pagePaths = await collectPages(pagesDir, config.pageExtensions)
|
||||||
type Result = {[page: string]: string}
|
const pages = createPagesMapping(pagePaths, config.pageExtensions)
|
||||||
const pages: Result = pagePaths.reduce((result: Result, pagePath): Result => {
|
const entrypoints = createEntrypoints(pages, config.target, buildId, config)
|
||||||
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 configs: any = await Promise.all([
|
const configs: any = await Promise.all([
|
||||||
getBaseWebpackConfig(dir, { buildId, isServer: false, config, target: config.target }),
|
getBaseWebpackConfig(dir, { buildId, isServer: false, config, target: config.target, entrypoints: entrypoints.client }),
|
||||||
getBaseWebpackConfig(dir, { buildId, isServer: true, config, target: config.target, entrypoints })
|
getBaseWebpackConfig(dir, { buildId, isServer: true, config, target: config.target, entrypoints: entrypoints.server })
|
||||||
])
|
])
|
||||||
|
|
||||||
let result: CompilerResult = {warnings: [], errors: []}
|
let result: CompilerResult = {warnings: [], errors: []}
|
||||||
|
|
|
@ -4,8 +4,6 @@ import resolve from 'resolve'
|
||||||
import CaseSensitivePathPlugin from 'case-sensitive-paths-webpack-plugin'
|
import CaseSensitivePathPlugin from 'case-sensitive-paths-webpack-plugin'
|
||||||
import FriendlyErrorsWebpackPlugin from 'friendly-errors-webpack-plugin'
|
import FriendlyErrorsWebpackPlugin from 'friendly-errors-webpack-plugin'
|
||||||
import WebpackBar from 'webpackbar'
|
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 NextJsSsrImportPlugin from './webpack/plugins/nextjs-ssr-import'
|
||||||
import NextJsSSRModuleCachePlugin from './webpack/plugins/nextjs-ssr-module-cache'
|
import NextJsSSRModuleCachePlugin from './webpack/plugins/nextjs-ssr-module-cache'
|
||||||
import NextJsRequireCacheHotReloader from './webpack/plugins/nextjs-require-cache-hot-reloader'
|
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 ChunkNamesPlugin from './webpack/plugins/chunk-names-plugin'
|
||||||
import { ReactLoadablePlugin } from './webpack/plugins/react-loadable-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 {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 AutoDllPlugin from 'autodll-webpack-plugin'
|
||||||
import TerserPlugin from 'terser-webpack-plugin'
|
import TerserPlugin from 'terser-webpack-plugin'
|
||||||
import AssetsSizePlugin from './webpack/plugins/assets-size-plugin'
|
import AssetsSizePlugin from './webpack/plugins/assets-size-plugin'
|
||||||
|
@ -139,22 +137,15 @@ function optimizationConfig ({ dev, isServer, totalPages, target }) {
|
||||||
return config
|
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 = {
|
const defaultLoaders = {
|
||||||
babel: {
|
babel: {
|
||||||
loader: 'next-babel-loader',
|
loader: 'next-babel-loader',
|
||||||
options: {dev, isServer, cwd: dir}
|
options: {dev, isServer, cwd: dir}
|
||||||
},
|
},
|
||||||
|
// Backwards compat
|
||||||
hotSelfAccept: {
|
hotSelfAccept: {
|
||||||
loader: 'hot-self-accept-loader',
|
loader: 'noop-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('|')})$`)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -166,8 +157,7 @@ export default async function getBaseWebpackConfig (dir, {dev = false, isServer
|
||||||
const distDir = path.join(dir, config.distDir)
|
const distDir = path.join(dir, config.distDir)
|
||||||
const outputDir = target === 'serverless' ? 'serverless' : SERVER_DIRECTORY
|
const outputDir = target === 'serverless' ? 'serverless' : SERVER_DIRECTORY
|
||||||
const outputPath = path.join(distDir, isServer ? outputDir : '')
|
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(entrypoints).length
|
||||||
const totalPages = Object.keys(pagesEntries).length
|
|
||||||
const clientEntries = !isServer ? {
|
const clientEntries = !isServer ? {
|
||||||
// Backwards compatibility
|
// Backwards compatibility
|
||||||
'main.js': [],
|
'main.js': [],
|
||||||
|
@ -186,8 +176,8 @@ export default async function getBaseWebpackConfig (dir, {dev = false, isServer
|
||||||
],
|
],
|
||||||
alias: {
|
alias: {
|
||||||
next: NEXT_PROJECT_ROOT,
|
next: NEXT_PROJECT_ROOT,
|
||||||
'private-next-pages': path.join(dir, 'pages'),
|
[PAGES_DIR_ALIAS]: path.join(dir, 'pages'),
|
||||||
'private-dot-next': distDir
|
[DOT_NEXT_ALIAS]: distDir
|
||||||
},
|
},
|
||||||
mainFields: isServer ? ['main'] : ['browser', 'module', 'main']
|
mainFields: isServer ? ['main'] : ['browser', 'module', 'main']
|
||||||
}
|
}
|
||||||
|
@ -205,13 +195,9 @@ export default async function getBaseWebpackConfig (dir, {dev = false, isServer
|
||||||
context: dir,
|
context: dir,
|
||||||
// Kept as function to be backwards compatible
|
// Kept as function to be backwards compatible
|
||||||
entry: async () => {
|
entry: async () => {
|
||||||
if (entrypoints) {
|
|
||||||
return entrypoints
|
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
...clientEntries,
|
...clientEntries,
|
||||||
// Only _error and _document when in development. The rest is handled by on-demand-entries
|
...entrypoints
|
||||||
...pagesEntries
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
output: {
|
output: {
|
||||||
|
@ -244,11 +230,6 @@ export default async function getBaseWebpackConfig (dir, {dev = false, isServer
|
||||||
},
|
},
|
||||||
module: {
|
module: {
|
||||||
rules: [
|
rules: [
|
||||||
dev && !isServer && {
|
|
||||||
test: defaultLoaders.hotSelfAccept.options.extensions,
|
|
||||||
include: defaultLoaders.hotSelfAccept.options.include,
|
|
||||||
use: defaultLoaders.hotSelfAccept
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
test: /\.(js|jsx)$/,
|
test: /\.(js|jsx)$/,
|
||||||
include: [dir, NEXT_PROJECT_ROOT_DIST_CLIENT, DEFAULT_PAGES_DIR, /next-server[\\/]dist[\\/]lib/],
|
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(),
|
target !== 'serverless' && isServer && new PagesManifestPlugin(),
|
||||||
!isServer && new BuildManifestPlugin(),
|
!isServer && new BuildManifestPlugin(),
|
||||||
!isServer && new PagesPlugin(),
|
|
||||||
isServer && new NextJsSsrImportPlugin(),
|
isServer && new NextJsSsrImportPlugin(),
|
||||||
target !== 'serverless' && isServer && new NextJsSSRModuleCachePlugin({outputPath}),
|
target !== 'serverless' && isServer && new NextJsSSRModuleCachePlugin({outputPath}),
|
||||||
!isServer && !dev && new AssetsSizePlugin(buildId, distDir)
|
!isServer && !dev && new AssetsSizePlugin(buildId, distDir)
|
||||||
|
|
|
@ -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$/, '')
|
|
||||||
}
|
|
|
@ -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
|
1
packages/next/build/webpack/loaders/noop-loader.js
Normal file
1
packages/next/build/webpack/loaders/noop-loader.js
Normal file
|
@ -0,0 +1 @@
|
||||||
|
module.exports = (source) => source
|
|
@ -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
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -1,65 +1,5 @@
|
||||||
import 'event-source-polyfill'
|
import 'event-source-polyfill'
|
||||||
import connect from './dev-error-overlay/hot-dev-client'
|
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}) => {
|
export default ({assetPrefix}) => {
|
||||||
const options = {
|
const options = {
|
||||||
path: `${assetPrefix}/_next/webpack-hmr`
|
path: `${assetPrefix}/_next/webpack-hmr`
|
||||||
|
@ -68,13 +8,24 @@ export default ({assetPrefix}) => {
|
||||||
const devClient = connect(options)
|
const devClient = connect(options)
|
||||||
|
|
||||||
devClient.subscribeToHmrEvent((obj) => {
|
devClient.subscribeToHmrEvent((obj) => {
|
||||||
const fn = handlers[obj.action]
|
if (obj.action === 'reloadPage') {
|
||||||
if (fn) {
|
return window.location.reload()
|
||||||
const data = obj.data || []
|
|
||||||
fn(...data)
|
|
||||||
} else {
|
|
||||||
throw new Error('Unexpected action ' + obj.action)
|
|
||||||
}
|
}
|
||||||
|
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
|
return devClient
|
||||||
|
|
|
@ -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 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_CLIENT = join(NEXT_PROJECT_ROOT_DIST, 'client')
|
||||||
export const NEXT_PROJECT_ROOT_DIST_SERVER = join(NEXT_PROJECT_ROOT_DIST, 'server')
|
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'
|
||||||
|
|
|
@ -48,6 +48,7 @@
|
||||||
"@babel/runtime": "7.1.2",
|
"@babel/runtime": "7.1.2",
|
||||||
"@babel/runtime-corejs2": "7.1.2",
|
"@babel/runtime-corejs2": "7.1.2",
|
||||||
"@babel/template": "7.1.2",
|
"@babel/template": "7.1.2",
|
||||||
|
"@types/loader-utils": "1.1.3",
|
||||||
"@types/node-fetch": "2.1.4",
|
"@types/node-fetch": "2.1.4",
|
||||||
"@types/rimraf": "2.0.2",
|
"@types/rimraf": "2.0.2",
|
||||||
"ansi-html": "0.0.7",
|
"ansi-html": "0.0.7",
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { join, relative, sep, normalize } from 'path'
|
import { join, normalize } from 'path'
|
||||||
import WebpackDevMiddleware from 'webpack-dev-middleware'
|
import WebpackDevMiddleware from 'webpack-dev-middleware'
|
||||||
import WebpackHotMiddleware from 'webpack-hot-middleware'
|
import WebpackHotMiddleware from 'webpack-hot-middleware'
|
||||||
import errorOverlayMiddleware from './lib/error-overlay-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 webpack from 'webpack'
|
||||||
import WebSocket from 'ws'
|
import WebSocket from 'ws'
|
||||||
import getBaseWebpackConfig from '../build/webpack-config'
|
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 {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) {
|
export async function renderScriptError (res, error) {
|
||||||
// Asks CDNs and others to not to cache the errored page
|
// Asks CDNs and others to not to cache the errored page
|
||||||
|
@ -92,10 +97,6 @@ export default class HotReloader {
|
||||||
this.webpackHotMiddleware = null
|
this.webpackHotMiddleware = null
|
||||||
this.initialized = false
|
this.initialized = false
|
||||||
this.stats = null
|
this.stats = null
|
||||||
this.compilationErrors = null
|
|
||||||
this.prevChunkNames = null
|
|
||||||
this.prevFailedChunkNames = null
|
|
||||||
this.prevChunkHashes = null
|
|
||||||
this.serverPrevDocumentHash = null
|
this.serverPrevDocumentHash = null
|
||||||
|
|
||||||
this.config = config
|
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
|
// 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
|
// 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
|
// 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 {pathname} = parsedUrl
|
||||||
const params = matchNextPageBundleRequest(pathname)
|
const params = matchNextPageBundleRequest(pathname)
|
||||||
if (!params) {
|
if (!params) {
|
||||||
|
@ -145,7 +146,7 @@ export default class HotReloader {
|
||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
|
|
||||||
const {finished} = await handlePageBundleRequest(req, res, parsedUrl)
|
const {finished} = await handlePageBundleRequest(res, parsedUrl)
|
||||||
|
|
||||||
for (const fn of this.middlewares) {
|
for (const fn of this.middlewares) {
|
||||||
await new Promise((resolve, reject) => {
|
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 () {
|
async start () {
|
||||||
await this.clean()
|
await this.clean()
|
||||||
|
|
||||||
|
@ -188,10 +199,7 @@ export default class HotReloader {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
const configs = await Promise.all([
|
const configs = await this.getWebpackConfig()
|
||||||
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 })
|
|
||||||
])
|
|
||||||
this.addWsPort(configs)
|
this.addWsPort(configs)
|
||||||
|
|
||||||
const multiCompiler = webpack(configs)
|
const multiCompiler = webpack(configs)
|
||||||
|
@ -220,10 +228,7 @@ export default class HotReloader {
|
||||||
|
|
||||||
await this.clean()
|
await this.clean()
|
||||||
|
|
||||||
const configs = await Promise.all([
|
const configs = await this.getWebpackConfig()
|
||||||
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 })
|
|
||||||
])
|
|
||||||
this.addWsPort(configs)
|
this.addWsPort(configs)
|
||||||
|
|
||||||
const compiler = webpack(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)
|
// Notify reload to reload the page, as _document.js was changed (different hash)
|
||||||
this.send('reload', '/_document')
|
this.send('reloadPage')
|
||||||
|
|
||||||
this.serverPrevDocumentHash = documentChunk.hash
|
this.serverPrevDocumentHash = documentChunk.hash
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -293,61 +297,32 @@ export default class HotReloader {
|
||||||
.filter(name => IS_BUNDLED_PAGE_REGEX.test(name))
|
.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) {
|
if (this.initialized) {
|
||||||
// detect chunks which have to be replaced with a new template
|
// detect chunks which have to be replaced with a new template
|
||||||
// e.g, pages/index.js <-> pages/_error.js
|
// e.g, pages/index.js <-> pages/_error.js
|
||||||
const added = diff(chunkNames, this.prevChunkNames)
|
const addedPages = diff(chunkNames, this.prevChunkNames)
|
||||||
const removed = diff(this.prevChunkNames, chunkNames)
|
const removedPages = diff(this.prevChunkNames, chunkNames)
|
||||||
const succeeded = diff(this.prevFailedChunkNames, failedChunkNames)
|
|
||||||
|
|
||||||
// reload all failed chunks to replace the templace to the error ones,
|
if (addedPages.size > 0) {
|
||||||
// and to update error content
|
for (const addedPage of addedPages) {
|
||||||
const failed = failedChunkNames
|
let page = '/' + ROUTE_NAME_REGEX.exec(addedPage)[1].replace(/\\/g, '/')
|
||||||
|
page = page === '/index' ? '/' : page
|
||||||
const rootDir = join(CLIENT_STATIC_FILES_PATH, this.buildId, 'pages')
|
this.send('addedPage', page)
|
||||||
|
}
|
||||||
for (const n of new Set([...added, ...succeeded, ...removed, ...failed])) {
|
|
||||||
const route = toRoute(relative(rootDir, n))
|
|
||||||
this.send('reload', route)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let changedPageRoutes = []
|
if (removedPages.size > 0) {
|
||||||
|
for (const removedPage of removedPages) {
|
||||||
for (const [n, hash] of chunkHashes) {
|
let page = '/' + ROUTE_NAME_REGEX.exec(removedPage)[1].replace(/\\/g, '/')
|
||||||
if (!this.prevChunkHashes.has(n)) continue
|
page = page === '/index' ? '/' : page
|
||||||
if (this.prevChunkHashes.get(n) === hash) continue
|
this.send('removedPage', page)
|
||||||
|
}
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.initialized = true
|
this.initialized = true
|
||||||
this.stats = stats
|
this.stats = stats
|
||||||
this.compilationErrors = null
|
|
||||||
this.prevChunkNames = chunkNames
|
this.prevChunkNames = chunkNames
|
||||||
this.prevFailedChunkNames = failedChunkNames
|
|
||||||
this.prevChunkHashes = chunkHashes
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// We don’t watch .git/ .next/ and node_modules for changes
|
// We don’t watch .git/ .next/ and node_modules for changes
|
||||||
|
@ -381,7 +356,6 @@ export default class HotReloader {
|
||||||
const onDemandEntries = onDemandEntryHandler(webpackDevMiddleware, multiCompiler, {
|
const onDemandEntries = onDemandEntryHandler(webpackDevMiddleware, multiCompiler, {
|
||||||
dir: this.dir,
|
dir: this.dir,
|
||||||
buildId: this.buildId,
|
buildId: this.buildId,
|
||||||
dev: true,
|
|
||||||
reload: this.reload.bind(this),
|
reload: this.reload.bind(this),
|
||||||
pageExtensions: this.config.pageExtensions,
|
pageExtensions: this.config.pageExtensions,
|
||||||
wsPort: this.wsPort,
|
wsPort: this.wsPort,
|
||||||
|
@ -433,7 +407,7 @@ export default class HotReloader {
|
||||||
|
|
||||||
async ensurePage (page) {
|
async ensurePage (page) {
|
||||||
// Make sure we don't re-build or dispose prebuilt pages
|
// 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
|
return
|
||||||
}
|
}
|
||||||
await this.onDemandEntries.ensurePage(page)
|
await this.onDemandEntries.ensurePage(page)
|
||||||
|
@ -443,8 +417,3 @@ export default class HotReloader {
|
||||||
function diff (a, b) {
|
function diff (a, b) {
|
||||||
return new Set([...a].filter((v) => !b.has(v)))
|
return new Set([...a].filter((v) => !b.has(v)))
|
||||||
}
|
}
|
||||||
|
|
||||||
function toRoute (file) {
|
|
||||||
const f = sep === '\\' ? file.replace(/\\/g, '/') : file
|
|
||||||
return ('/' + f).replace(/(\/index)?\.js$/, '') || '/'
|
|
||||||
}
|
|
||||||
|
|
|
@ -6,8 +6,8 @@ import promisify from '../lib/promisify'
|
||||||
import globModule from 'glob'
|
import globModule from 'glob'
|
||||||
import {pageNotFoundError} from 'next-server/dist/server/require'
|
import {pageNotFoundError} from 'next-server/dist/server/require'
|
||||||
import {normalizePagePath} from 'next-server/dist/server/normalize-page-path'
|
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 { ROUTE_NAME_REGEX, IS_BUNDLED_PAGE_REGEX } from 'next-server/constants'
|
||||||
|
import {stringify} from 'querystring'
|
||||||
|
|
||||||
const ADDED = Symbol('added')
|
const ADDED = Symbol('added')
|
||||||
const BUILDING = Symbol('building')
|
const BUILDING = Symbol('building')
|
||||||
|
@ -30,7 +30,6 @@ function addEntry (compilation, context, name, entry) {
|
||||||
export default function onDemandEntryHandler (devMiddleware, multiCompiler, {
|
export default function onDemandEntryHandler (devMiddleware, multiCompiler, {
|
||||||
buildId,
|
buildId,
|
||||||
dir,
|
dir,
|
||||||
dev,
|
|
||||||
reload,
|
reload,
|
||||||
pageExtensions,
|
pageExtensions,
|
||||||
maxInactiveAge,
|
maxInactiveAge,
|
||||||
|
@ -51,20 +50,17 @@ export default function onDemandEntryHandler (devMiddleware, multiCompiler, {
|
||||||
invalidator.startBuilding()
|
invalidator.startBuilding()
|
||||||
|
|
||||||
const allEntries = Object.keys(entries).map(async (page) => {
|
const allEntries = Object.keys(entries).map(async (page) => {
|
||||||
const { name, entry } = entries[page]
|
const { name, absolutePagePath } = entries[page]
|
||||||
const files = Array.isArray(entry) ? entry : [entry]
|
try {
|
||||||
// Is just one item. But it's passed as an array.
|
await access(absolutePagePath, (fs.constants || fs).W_OK)
|
||||||
for (const file of files) {
|
} catch (err) {
|
||||||
try {
|
console.warn('Page was removed', page)
|
||||||
await access(join(dir, file), (fs.constants || fs).W_OK)
|
delete entries[page]
|
||||||
} catch (err) {
|
return
|
||||||
console.warn('Page was removed', page)
|
|
||||||
delete entries[page]
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
entries[page].status = BUILDING
|
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)
|
return Promise.all(allEntries)
|
||||||
|
@ -178,17 +174,19 @@ export default function onDemandEntryHandler (devMiddleware, multiCompiler, {
|
||||||
}
|
}
|
||||||
|
|
||||||
const extensions = pageExtensions.join('|')
|
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) {
|
if (paths.length === 0) {
|
||||||
throw pageNotFoundError(normalizedPagePath)
|
throw pageNotFoundError(normalizedPagePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
const relativePathToPage = paths[0]
|
const pagePath = paths[0]
|
||||||
|
let pageUrl = `/${pagePath.replace(new RegExp(`\\.+(${extensions})$`), '').replace(/\\/g, '/')}`.replace(/\/index$/, '')
|
||||||
const pathname = join(dir, relativePathToPage)
|
pageUrl = pageUrl === '' ? '/' : pageUrl
|
||||||
|
const bundleFile = pageUrl === '/' ? '/index.js' : `${pageUrl}.js`
|
||||||
const {name, files} = createEntry(relativePathToPage, {buildId, pageExtensions: extensions})
|
const name = join('static', buildId, 'pages', bundleFile)
|
||||||
|
const absolutePagePath = join(pagesDir, pagePath)
|
||||||
|
|
||||||
await new Promise((resolve, reject) => {
|
await new Promise((resolve, reject) => {
|
||||||
const entryInfo = entries[page]
|
const entryInfo = entries[page]
|
||||||
|
@ -207,7 +205,7 @@ export default function onDemandEntryHandler (devMiddleware, multiCompiler, {
|
||||||
|
|
||||||
console.log(`> Building page: ${page}`)
|
console.log(`> Building page: ${page}`)
|
||||||
|
|
||||||
entries[page] = { name, entry: files, pathname, status: ADDED }
|
entries[page] = { name, absolutePagePath, status: ADDED }
|
||||||
doneCallbacks.once(page, handleCallback)
|
doneCallbacks.once(page, handleCallback)
|
||||||
|
|
||||||
invalidator.invalidate()
|
invalidator.invalidate()
|
||||||
|
|
|
@ -3,7 +3,7 @@ import webdriver from 'next-webdriver'
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
import { check, File, waitFor, getReactErrorOverlayContent, getBrowserBodyText } from 'next-test-utils'
|
import { check, File, waitFor, getReactErrorOverlayContent, getBrowserBodyText } from 'next-test-utils'
|
||||||
|
|
||||||
export default (context) => {
|
export default (context, renderViaHTTP) => {
|
||||||
describe('Error Recovery', () => {
|
describe('Error Recovery', () => {
|
||||||
it('should recover from 404 after a page has been added', async () => {
|
it('should recover from 404 after a page has been added', async () => {
|
||||||
let browser
|
let browser
|
||||||
|
@ -110,6 +110,8 @@ export default (context) => {
|
||||||
const aboutPage = new File(join(__dirname, '../', 'pages', 'hmr', 'about.js'))
|
const aboutPage = new File(join(__dirname, '../', 'pages', 'hmr', 'about.js'))
|
||||||
let browser
|
let browser
|
||||||
try {
|
try {
|
||||||
|
await renderViaHTTP('/hmr/about')
|
||||||
|
|
||||||
aboutPage.replace('</div>', 'div')
|
aboutPage.replace('</div>', 'div')
|
||||||
|
|
||||||
browser = await webdriver(context.appPort, '/hmr/contact')
|
browser = await webdriver(context.appPort, '/hmr/contact')
|
||||||
|
|
|
@ -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')
|
|
||||||
})
|
|
||||||
})
|
|
10
yarn.lock
10
yarn.lock
|
@ -1411,6 +1411,14 @@
|
||||||
"@types/minimatch" "*"
|
"@types/minimatch" "*"
|
||||||
"@types/node" "*"
|
"@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@*":
|
"@types/mime@*":
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.0.tgz#5a7306e367c539b9f6543499de8dd519fac37a8b"
|
resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.0.tgz#5a7306e367c539b9f6543499de8dd519fac37a8b"
|
||||||
|
@ -1502,7 +1510,7 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
source-map "^0.6.1"
|
source-map "^0.6.1"
|
||||||
|
|
||||||
"@types/webpack@4.4.22":
|
"@types/webpack@*", "@types/webpack@4.4.22":
|
||||||
version "4.4.22"
|
version "4.4.22"
|
||||||
resolved "https://registry.yarnpkg.com/@types/webpack/-/webpack-4.4.22.tgz#c4a5ea8b74a31b579537515bcfe86d2b2a34382c"
|
resolved "https://registry.yarnpkg.com/@types/webpack/-/webpack-4.4.22.tgz#c4a5ea8b74a31b579537515bcfe86d2b2a34382c"
|
||||||
integrity sha512-PxAAzli3krZX9rCeONSR5Z9v4CR/2HPsKsiVRFNDo9OZefN+dTemteMHZnYkddOu4bqoYqJTJ724gLy0ZySXOw==
|
integrity sha512-PxAAzli3krZX9rCeONSR5Z9v4CR/2HPsKsiVRFNDo9OZefN+dTemteMHZnYkddOu4bqoYqJTJ724gLy0ZySXOw==
|
||||||
|
|
Loading…
Reference in a new issue