未验证 提交 c5cc6072 编写于 作者: T Tim Neutkens 提交者: GitHub

Use single webpack runtimeChunk for Node.js compilation (#14722)

Webpack 5 supports a single runtimechunk for the Node.js compilation, this solves sharing modules between entrypoints.
上级 260707de
......@@ -660,7 +660,9 @@ export default async function getBaseWebpackConfig(
nodeEnv: false,
splitChunks: isServer ? false : splitChunksConfig,
runtimeChunk: isServer
? undefined
? isWebpack5 && !isLikeServerless
? { name: 'webpack-runtime' }
: undefined
: { name: CLIENT_STATIC_FILES_RUNTIME_WEBPACK },
minimize: !(dev || isServer),
minimizer: [
......@@ -926,7 +928,8 @@ export default async function getBaseWebpackConfig(
}),
isServerless && isServer && new ServerlessPlugin(),
isServer && new PagesManifestPlugin(isLikeServerless),
target === 'server' &&
!isWebpack5 &&
target === 'server' &&
isServer &&
new NextJsSSRModuleCachePlugin({ outputPath }),
isServer && new NextJsSsrImportPlugin(),
......
import { Compiler, Plugin, version } from 'webpack'
import { realpathSync } from 'fs'
import path from 'path'
const isWebpack5 = parseInt(version!) === 5
function deleteCache(path: string) {
function deleteCache(filePath: string) {
try {
delete require.cache[realpathSync(path)]
delete require.cache[realpathSync(filePath)]
} catch (e) {
if (e.code !== 'ENOENT') throw e
} finally {
delete require.cache[path]
delete require.cache[filePath]
}
}
......@@ -32,10 +33,17 @@ export class NextJsRequireCacheHotReloader implements Plugin {
}
)
compiler.hooks.afterEmit.tap(PLUGIN_NAME, () => {
for (const path of this.previousOutputPathsWebpack5) {
if (!this.currentOutputPathsWebpack5.has(path)) {
deleteCache(path)
compiler.hooks.afterEmit.tap(PLUGIN_NAME, (compilation) => {
const runtimeChunkPath = path.join(
compilation.outputOptions.path,
'webpack-runtime.js'
)
deleteCache(runtimeChunkPath)
for (const outputPath of this.previousOutputPathsWebpack5) {
if (!this.currentOutputPathsWebpack5.has(outputPath)) {
deleteCache(outputPath)
}
}
......
......@@ -32,7 +32,10 @@ export default class PagesManifestPlugin implements Plugin {
const files = entrypoint
.getFiles()
.filter((file: string) => file.endsWith('.js'))
.filter(
(file: string) =>
!file.includes('webpack-runtime') && file.endsWith('.js')
)
if (files.length > 1) {
console.log(
......
Copyright JS Foundation and other contributors
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
'Software'), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
module.exports=function(e,n){"use strict";var t={};function __webpack_require__(n){if(t[n]){return t[n].exports}var r=t[n]={i:n,l:false,exports:{}};e[n].call(r.exports,r,r.exports,__webpack_require__);r.l=true;return r.exports}__webpack_require__.ab=__dirname+"/";function startup(){return __webpack_require__(340)}return startup()}({340:function(e,n,t){e.exports=webpackHotMiddleware;var r=t(430);var i=r.pathMatch;function webpackHotMiddleware(e,n){n=n||{};n.log=typeof n.log=="undefined"?console.log.bind(console):n.log;n.path=n.path||"/__webpack_hmr";n.heartbeat=n.heartbeat||10*1e3;var t=createEventStream(n.heartbeat);var r=null;var a=false;if(e.hooks){e.hooks.invalid.tap("webpack-hot-middleware",onInvalid);e.hooks.done.tap("webpack-hot-middleware",onDone)}else{e.plugin("invalid",onInvalid);e.plugin("done",onDone)}function onInvalid(){if(a)return;r=null;if(n.log)n.log("webpack building...");t.publish({action:"building"})}function onDone(e){if(a)return;r=e;publishStats("built",r,t,n.log)}var o=function(e,o,u){if(a)return u();if(!i(e.url,n.path))return u();t.handler(e,o);if(r){publishStats("sync",r,t)}};o.publish=function(e){if(a)return;t.publish(e)};o.close=function(){if(a)return;a=true;t.close();t=null};return o}function createEventStream(e){var n=0;var t={};function everyClient(e){Object.keys(t).forEach(function(n){e(t[n])})}var r=setInterval(function heartbeatTick(){everyClient(function(e){e.write("data: 💓\n\n")})},e).unref();return{close:function(){clearInterval(r);everyClient(function(e){if(!e.finished)e.end()});t={}},handler:function(e,r){var i={"Access-Control-Allow-Origin":"*","Content-Type":"text/event-stream;charset=utf-8","Cache-Control":"no-cache, no-transform","X-Accel-Buffering":"no"};var a=!(parseInt(e.httpVersion)>=2);if(a){e.socket.setKeepAlive(true);Object.assign(i,{Connection:"keep-alive"})}r.writeHead(200,i);r.write("\n");var o=n++;t[o]=r;e.on("close",function(){if(!r.finished)r.end();delete t[o]})},publish:function(e){everyClient(function(n){n.write("data: "+JSON.stringify(e)+"\n\n")})}}}function publishStats(e,n,t,r){var i=n.toJson({all:false,cached:true,children:true,modules:true,timings:true,hash:true});var a=extractBundles(i);a.forEach(function(i){var o=i.name||"";if(a.length===1&&!o&&n.compilation){o=n.compilation.name||""}if(r){r("webpack built "+(o?o+" ":"")+i.hash+" in "+i.time+"ms")}t.publish({name:o,action:e,time:i.time,hash:i.hash,warnings:i.warnings||[],errors:i.errors||[],modules:buildModuleMap(i.modules)})})}function extractBundles(e){if(e.modules)return[e];if(e.children&&e.children.length)return e.children;return[e]}function buildModuleMap(e){var n={};e.forEach(function(e){n[e.id]=e.name});return n}},430:function(e,n,t){var r=t(835).parse;n.pathMatch=function(e,n){try{return r(e).pathname===n}catch(e){return false}}},835:function(e){e.exports=require("url")}});
\ No newline at end of file
{"name":"webpack-hot-middleware","main":"middleware.js","author":"Glen Mailer <glen@stainlessed.co.uk>","license":"MIT"}
......@@ -211,8 +211,7 @@
"thread-loader": "2.1.3",
"typescript": "3.8.3",
"unfetch": "4.1.0",
"unistore": "3.4.1",
"webpack-hot-middleware": "2.25.0"
"unistore": "3.4.1"
},
"engines": {
"node": ">=10.13.0"
......
// Based on https://github.com/webpack-contrib/webpack-hot-middleware/blob/9708d781ae0e46179cf8ea1a94719de4679aaf53/middleware.js
// Included License below
// Copyright JS Foundation and other contributors
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// 'Software'), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
import type webpack from 'webpack'
import type http from 'http'
export class WebpackHotMiddleware {
eventStream: EventStream
latestStats: webpack.Stats | null
closed: boolean
constructor(compiler: webpack.Compiler) {
this.eventStream = new EventStream()
this.latestStats = null
this.closed = false
compiler.hooks.invalid.tap('webpack-hot-middleware', this.onInvalid)
compiler.hooks.done.tap('webpack-hot-middleware', this.onDone)
}
onInvalid = () => {
if (this.closed) return
this.latestStats = null
this.eventStream.publish({ action: 'building' })
}
onDone = (statsResult: webpack.Stats) => {
if (this.closed) return
// Keep hold of latest stats so they can be propagated to new clients
this.latestStats = statsResult
this.publishStats('built', this.latestStats)
}
middleware = (
req: http.IncomingMessage,
res: http.ServerResponse,
next: () => void
) => {
if (this.closed) return next()
if (!req.url?.startsWith('/_next/webpack-hmr')) return next()
this.eventStream.handler(req, res)
if (this.latestStats) {
// Explicitly not passing in `log` fn as we don't want to log again on
// the server
this.publishStats('sync', this.latestStats)
}
}
publishStats = (action: string, statsResult: webpack.Stats) => {
const stats = statsResult.toJson({
all: false,
hash: true,
warnings: true,
errors: true,
})
this.eventStream.publish({
action: action,
hash: stats.hash,
warnings: stats.warnings || [],
errors: stats.errors || [],
})
}
publish = (payload: any) => {
if (this.closed) return
this.eventStream.publish(payload)
}
close = () => {
if (this.closed) return
// Can't remove compiler plugins, so we just set a flag and noop if closed
// https://github.com/webpack/tapable/issues/32#issuecomment-350644466
this.closed = true
this.eventStream.close()
}
}
class EventStream {
clients: Set<http.ServerResponse>
interval: NodeJS.Timeout
constructor() {
this.clients = new Set()
this.interval = setInterval(this.heartbeatTick, 2500).unref()
}
heartbeatTick = () => {
this.everyClient((client) => {
client.write('data: \uD83D\uDC93\n\n')
})
}
everyClient(fn: (client: http.ServerResponse) => void) {
for (const client of this.clients) {
fn(client)
}
}
close() {
clearInterval(this.interval)
this.everyClient((client) => {
if (!client.finished) client.end()
})
this.clients.clear()
}
handler(req: http.IncomingMessage, res: http.ServerResponse) {
const headers = {
'Access-Control-Allow-Origin': '*',
'Content-Type': 'text/event-stream;charset=utf-8',
'Cache-Control': 'no-cache, no-transform',
// While behind nginx, event stream should not be buffered:
// http://nginx.org/docs/http/ngx_http_proxy_module.html#proxy_buffering
'X-Accel-Buffering': 'no',
}
const isHttp1 = !(parseInt(req.httpVersion) >= 2)
if (isHttp1) {
req.socket.setKeepAlive(true)
Object.assign(headers, {
Connection: 'keep-alive',
})
}
res.writeHead(200, headers)
res.write('\n')
this.clients.add(res)
req.on('close', () => {
if (!res.finished) res.end()
this.clients.delete(res)
})
}
publish(payload: any) {
this.everyClient((client) => {
client.write('data: ' + JSON.stringify(payload) + '\n\n')
})
}
}
import { getOverlayMiddleware } from '@next/react-dev-overlay/lib/middleware'
import { NextHandleFunction } from 'connect'
import { IncomingMessage, ServerResponse } from 'http'
import WebpackHotMiddleware from 'next/dist/compiled/webpack-hot-middleware'
import { WebpackHotMiddleware } from './hot-middleware'
import { join, normalize, relative as relativePath, sep } from 'path'
import { UrlObject } from 'url'
import webpack from 'webpack'
......@@ -135,9 +135,7 @@ export default class HotReloader {
private buildId: string
private middlewares: any[]
private pagesDir: string
private webpackHotMiddleware:
| (NextHandleFunction & WebpackHotMiddleware.EventStream)
| null
private webpackHotMiddleware: (NextHandleFunction & any) | null
private config: any
private stats: any
private serverStats: any
......@@ -416,13 +414,8 @@ export default class HotReloader {
}
)
this.webpackHotMiddleware = WebpackHotMiddleware(
multiCompiler.compilers[0],
{
path: '/_next/webpack-hmr',
log: false,
heartbeat: 2500,
}
this.webpackHotMiddleware = new WebpackHotMiddleware(
multiCompiler.compilers[0]
)
let booted = false
......@@ -452,8 +445,8 @@ export default class HotReloader {
this.middlewares = [
// must come before hotMiddleware
this.onDemandEntries.middleware(),
this.webpackHotMiddleware,
this.onDemandEntries.middleware,
this.webpackHotMiddleware.middleware,
errorOverlayMiddleware({ dir: this.dir }),
getOverlayMiddleware({
rootDirectory: this.dir,
......
......@@ -211,26 +211,24 @@ export default function onDemandEntryHandler(
})
},
middleware() {
return (req: IncomingMessage, res: ServerResponse, next: Function) => {
if (!/^\/_next\/webpack-hmr/.test(req.url!)) return next()
const { query } = parse(req.url!, true)
const page = query.page
if (!page) return next()
const runPing = () => {
const data = handlePing(query.page as string)
if (!data) return
res.write('data: ' + JSON.stringify(data) + '\n\n')
}
const pingInterval = setInterval(() => runPing(), 5000)
middleware(req: IncomingMessage, res: ServerResponse, next: Function) {
if (!req.url?.startsWith('/_next/webpack-hmr')) return next()
const { query } = parse(req.url!, true)
const page = query.page
if (!page) return next()
req.on('close', () => {
clearInterval(pingInterval)
})
next()
const runPing = () => {
const data = handlePing(query.page as string)
if (!data) return
res.write('data: ' + JSON.stringify(data) + '\n\n')
}
const pingInterval = setInterval(() => runPing(), 5000)
req.on('close', () => {
clearInterval(pingInterval)
})
next()
},
}
}
......
......@@ -470,17 +470,6 @@ export async function ncc_unistore(task, opts) {
.target('compiled/unistore')
}
// eslint-disable-next-line camelcase
externals['webpack-hot-middleware'] =
'next/dist/compiled/webpack-hot-middleware'
export async function ncc_webpack_hot_middleware(task, opts) {
await task
.source(
opts.src || relative(__dirname, require.resolve('webpack-hot-middleware'))
)
.ncc({ packageName: 'webpack-hot-middleware', externals })
.target('compiled/webpack-hot-middleware')
}
externals['terser-webpack-plugin'] = 'next/dist/compiled/terser-webpack-plugin'
export async function ncc_terser_webpack_plugin(task, opts) {
await task
......@@ -518,60 +507,60 @@ export async function copy_ncced(task) {
}
export async function ncc(task) {
await task.clear('compiled').parallel([
'ncc_amphtml_validator',
'ncc_arg',
'ncc_async_retry',
'ncc_async_sema',
'ncc_babel_loader',
'ncc_cache_loader',
'ncc_chalk',
'ncc_ci_info',
'ncc_compression',
'ncc_conf',
'ncc_content_type',
'ncc_cookie',
'ncc_debug',
'ncc_devalue',
'ncc_dotenv',
'ncc_dotenv_expand',
'ncc_escape_string_regexp',
'ncc_etag',
'ncc_file_loader',
'ncc_find_up',
'ncc_fresh',
'ncc_gzip_size',
'ncc_http_proxy',
'ncc_ignore_loader',
'ncc_is_docker',
'ncc_is_wsl',
'ncc_json5',
'ncc_jsonwebtoken',
'ncc_launch_editor',
'ncc_lodash_curry',
'ncc_lru_cache',
'ncc_nanoid',
'ncc_node_fetch',
'ncc_ora',
'ncc_postcss_flexbugs_fixes',
'ncc_postcss_loader',
'ncc_postcss_preset_env',
'ncc_raw_body',
'ncc_recast',
'ncc_resolve',
'ncc_send',
'ncc_source_map',
'ncc_string_hash',
'ncc_strip_ansi',
'ncc_terser',
'ncc_text_table',
'ncc_thread_loader',
'ncc_unistore',
// 'ncc_webpack_dev_middleware',
'ncc_webpack_hot_middleware',
'ncc_terser_webpack_plugin',
'ncc_comment_json',
])
await task
.clear('compiled')
.parallel([
'ncc_amphtml_validator',
'ncc_arg',
'ncc_async_retry',
'ncc_async_sema',
'ncc_babel_loader',
'ncc_cache_loader',
'ncc_chalk',
'ncc_ci_info',
'ncc_compression',
'ncc_conf',
'ncc_content_type',
'ncc_cookie',
'ncc_debug',
'ncc_devalue',
'ncc_dotenv',
'ncc_dotenv_expand',
'ncc_escape_string_regexp',
'ncc_etag',
'ncc_file_loader',
'ncc_find_up',
'ncc_fresh',
'ncc_gzip_size',
'ncc_http_proxy',
'ncc_ignore_loader',
'ncc_is_docker',
'ncc_is_wsl',
'ncc_json5',
'ncc_jsonwebtoken',
'ncc_launch_editor',
'ncc_lodash_curry',
'ncc_lru_cache',
'ncc_nanoid',
'ncc_node_fetch',
'ncc_ora',
'ncc_postcss_flexbugs_fixes',
'ncc_postcss_loader',
'ncc_postcss_preset_env',
'ncc_raw_body',
'ncc_recast',
'ncc_resolve',
'ncc_send',
'ncc_source_map',
'ncc_string_hash',
'ncc_strip_ansi',
'ncc_terser',
'ncc_text_table',
'ncc_thread_loader',
'ncc_unistore',
'ncc_terser_webpack_plugin',
'ncc_comment_json',
])
}
export async function compile(task) {
......
......@@ -207,10 +207,6 @@ declare module 'next/dist/compiled/unistore' {
export = m
}
declare module 'next/dist/compiled/webpack-hot-middleware' {
import m from 'webpack-hot-middleware'
export = m
}
declare module 'next/dist/compiled/terser-webpack-plugin' {
import m from 'terser-webpack-plugin'
export = m
......
......@@ -27,6 +27,27 @@ export type OriginalStackFrameResponse = {
type Source = { map: () => RawSourceMap } | null
const isWebpack5 = parseInt(webpack.version!) === 5
function getModuleSource(compilation: any, module: any): any {
if (isWebpack5) {
return (
(module &&
compilation.codeGenerationResults
.get(module)
?.sources.get('javascript')) ??
null
)
}
return (
module?.source(
compilation.dependencyTemplates,
compilation.runtimeTemplate
) ?? null
)
}
function getSourcePath(source: string) {
// Webpack prefixes certain source paths with this path
if (source.startsWith('webpack:///')) {
......@@ -72,15 +93,10 @@ function getOverlayMiddleware(options: OverlayMiddlewareOptions) {
const compilation = isServerSide
? options.serverStats()?.compilation
: options.stats()?.compilation
const m = compilation?.modules?.find(
const module = [...compilation.modules].find(
(searchModule) => searchModule.id === id
)
return (
m?.source(
compilation.dependencyTemplates,
compilation.runtimeTemplate
) ?? null
)
return getModuleSource(compilation, module)
} catch (err) {
console.error(`Failed to lookup module by ID ("${id}"):`, err)
return null
......
......@@ -145,7 +145,9 @@ export async function sandbox(id = nanoid(), initialFiles = new Map()) {
p.shadowRoot.querySelector('[data-nextjs-dialog-header')
)
const root = portal.shadowRoot
return root.querySelector('[data-nextjs-dialog-header]').innerText
return root
.querySelector('[data-nextjs-dialog-header]')
.innerText.replace(/__WEBPACK_DEFAULT_EXPORT__/, 'Unknown')
})
: ''
......
......@@ -247,7 +247,13 @@ export default (context, renderViaHTTP) => {
)
expect(await hasRedbox(browser)).toBe(true)
expect(await getRedboxHeader(browser)).toMatchInlineSnapshot(`
// TODO: Replace this when webpack 5 is the default
expect(
(await getRedboxHeader(browser)).replace(
'__WEBPACK_DEFAULT_EXPORT__',
'Unknown'
)
).toMatchInlineSnapshot(`
" 1 of 1 unhandled error
Server Error
......
......@@ -466,7 +466,9 @@ export async function getRedboxHeader(browser) {
.call(document.querySelectorAll('nextjs-portal'))
.find((p) => p.shadowRoot.querySelector('[data-nextjs-dialog-header'))
const root = portal.shadowRoot
return root.querySelector('[data-nextjs-dialog-header]').innerText
return root
.querySelector('[data-nextjs-dialog-header]')
.innerText.replace(/__WEBPACK_DEFAULT_EXPORT__/, 'Unknown')
})
}
......@@ -480,8 +482,9 @@ export async function getRedboxSource(browser) {
)
)
const root = portal.shadowRoot
return root.querySelector('[data-nextjs-codeframe], [data-nextjs-terminal]')
.innerText
return root
.querySelector('[data-nextjs-codeframe], [data-nextjs-terminal]')
.innerText.replace(/__WEBPACK_DEFAULT_EXPORT__/, 'Unknown')
})
}
......
......@@ -3425,10 +3425,6 @@ ansi-escapes@^4.2.1:
dependencies:
type-fest "^0.8.1"
ansi-html@0.0.7:
version "0.0.7"
resolved "https://registry.yarnpkg.com/ansi-html/-/ansi-html-0.0.7.tgz#813584021962a9e9e6fd039f940d12f56ca7859e"
ansi-regex@^2.0.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df"
......@@ -7805,10 +7801,6 @@ html-encoding-sniffer@^2.0.1:
dependencies:
whatwg-encoding "^1.0.5"
html-entities@^1.2.0:
version "1.2.1"
resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.2.1.tgz#0df29351f0721163515dfb9e5543e5f6eed5162f"
htmlparser2@4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-4.1.0.tgz#9a4ef161f2e4625ebf7dfbe6c0a2f52d18a59e78"
......@@ -15949,15 +15941,6 @@ webpack-bundle-analyzer@3.6.1:
opener "^1.5.1"
ws "^6.0.0"
webpack-hot-middleware@2.25.0:
version "2.25.0"
resolved "https://registry.yarnpkg.com/webpack-hot-middleware/-/webpack-hot-middleware-2.25.0.tgz#4528a0a63ec37f8f8ef565cf9e534d57d09fe706"
dependencies:
ansi-html "0.0.7"
html-entities "^1.2.0"
querystring "^0.2.0"
strip-ansi "^3.0.0"
webpack-sources@1.4.3, webpack-sources@^1.1.0, webpack-sources@^1.4.0, webpack-sources@^1.4.1, webpack-sources@^1.4.3:
version "1.4.3"
resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.4.3.tgz#eedd8ec0b928fbf1cbfe994e22d2d890f330a933"
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册