UNPKG

next

Version:

The React Framework

675 lines (674 loc) • 32.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.renderScriptError = renderScriptError; exports.default = void 0; var _middleware = require("next/dist/compiled/@next/react-dev-overlay/middleware"); var _hotMiddleware = require("./hot-middleware"); var _path = require("path"); var _webpack = require("next/dist/compiled/webpack/webpack"); var _entries = require("../../build/entries"); var _output = require("../../build/output"); var _webpackConfig = _interopRequireDefault(require("../../build/webpack-config")); var _constants = require("../../lib/constants"); var _recursiveDelete = require("../../lib/recursive-delete"); var _constants1 = require("../../shared/lib/constants"); var _router = require("../router"); var _findPageFile = require("../lib/find-page-file"); var _onDemandEntryHandler = _interopRequireWildcard(require("./on-demand-entry-handler")); var _normalizePagePath = require("../normalize-page-path"); var _getRouteFromEntrypoint = _interopRequireDefault(require("../get-route-from-entrypoint")); var _fileExists = require("../../lib/file-exists"); var _middlewarePlugin = require("../../build/webpack/plugins/middleware-plugin"); var _querystring = require("querystring"); var _utils = require("../../build/utils"); var _utils1 = require("../../shared/lib/utils"); var _trace = require("../../trace"); var _isError = require("../../lib/is-error"); var _ws = _interopRequireDefault(require("next/dist/compiled/ws")); var _fs = require("fs"); var _config = require("../config"); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for(var key in obj){ if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = Object.defineProperty && Object.getOwnPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : {}; if (desc.get || desc.set) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } } newObj.default = obj; return newObj; } } const wsServer = new _ws.default.Server({ noServer: true }); async function renderScriptError(res, error, { verbose =true } = {}) { // Asks CDNs and others to not to cache the errored page res.setHeader('Cache-Control', 'no-cache, no-store, max-age=0, must-revalidate'); if (error.code === 'ENOENT') { res.statusCode = 404; res.end('404 - Not Found'); return; } if (verbose) { console.error(error.stack); } res.statusCode = 500; res.end('500 - Internal Error'); } function addCorsSupport(req, res) { const isApiRoute = req.url.match(_constants.API_ROUTE); // API routes handle their own CORS headers if (isApiRoute) { return { preflight: false }; } if (!req.headers.origin) { return { preflight: false }; } res.setHeader('Access-Control-Allow-Origin', req.headers.origin); res.setHeader('Access-Control-Allow-Methods', 'OPTIONS, GET'); // Based on https://github.com/primus/access-control/blob/4cf1bc0e54b086c91e6aa44fb14966fa5ef7549c/index.js#L158 if (req.headers['access-control-request-headers']) { res.setHeader('Access-Control-Allow-Headers', req.headers['access-control-request-headers']); } if (req.method === 'OPTIONS') { res.writeHead(200); res.end(); return { preflight: true }; } return { preflight: false }; } const matchNextPageBundleRequest = (0, _router).route('/_next/static/chunks/pages/:path*.js(\\.map|)'); // Recursively look up the issuer till it ends up at the root function findEntryModule(issuer) { if (issuer.issuer) { return findEntryModule(issuer.issuer); } return issuer; } function erroredPages(compilation) { const failedPages = {}; for (const error of compilation.errors){ if (!error.module) { continue; } const entryModule = findEntryModule(error.module); const { name } = entryModule; if (!name) { continue; } // Only pages have to be reloaded const enhancedName = (0, _getRouteFromEntrypoint).default(name); if (!enhancedName) { continue; } if (!failedPages[enhancedName]) { failedPages[enhancedName] = []; } failedPages[enhancedName].push(error); } return failedPages; } class HotReloader { constructor(dir, { config , pagesDir , distDir , buildId , previewProps , rewrites }){ this.clientError = null; this.serverError = null; this.pagesMapping = {}; this.buildId = buildId; this.dir = dir; this.middlewares = []; this.pagesDir = pagesDir; this.distDir = distDir; this.clientStats = null; this.serverStats = null; this.serverPrevDocumentHash = null; this.config = config; this.runtime = config.experimental.runtime; this.hasReactRoot = (0, _config).shouldUseReactRoot(); this.hasServerComponents = this.hasReactRoot && !!config.experimental.serverComponents; this.previewProps = previewProps; this.rewrites = rewrites; this.hotReloaderSpan = (0, _trace).trace('hot-reloader', undefined, { version: "12.1.6-canary.1" }); // Ensure the hotReloaderSpan is flushed immediately as it's the parentSpan for all processing // of the current `next dev` invocation. this.hotReloaderSpan.stop(); } async run(req, res, parsedUrl) { // Usually CORS support is not needed for the hot-reloader (this is dev only feature) // With when the app runs for multi-zones support behind a proxy, // the current page is trying to access this URL via assetPrefix. // That's when the CORS support is needed. const { preflight } = addCorsSupport(req, res); if (preflight) { return {}; } // When a request comes in that is a page bundle, e.g. /_next/static/<buildid>/pages/index.js // 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 (pageBundleRes, parsedPageBundleUrl)=>{ const { pathname } = parsedPageBundleUrl; const params = matchNextPageBundleRequest(pathname); if (!params) { return {}; } let decodedPagePath; try { decodedPagePath = `/${params.path.map((param)=>decodeURIComponent(param) ).join('/')}`; } catch (_) { throw new _utils1.DecodeError('failed to decode param'); } const page = (0, _normalizePagePath).denormalizePagePath(decodedPagePath); if (page === '/_error' || _constants1.BLOCKED_PAGES.indexOf(page) === -1) { try { await this.ensurePage(page, true); } catch (error) { await renderScriptError(pageBundleRes, (0, _isError).getProperError(error)); return { finished: true }; } const errors = await this.getCompilationErrors(page); if (errors.length > 0) { await renderScriptError(pageBundleRes, errors[0], { verbose: false }); return { finished: true }; } } return {}; }; const { finished } = await handlePageBundleRequest(res, parsedUrl); for (const fn of this.middlewares){ await new Promise((resolve, reject)=>{ fn(req, res, (err)=>{ if (err) return reject(err); resolve(); }); }); } return { finished }; } onHMR(req, _res, head) { wsServer.handleUpgrade(req, req.socket, head, (client)=>{ var ref, ref1; (ref = this.webpackHotMiddleware) === null || ref === void 0 ? void 0 : ref.onHMR(client); (ref1 = this.onDemandEntries) === null || ref1 === void 0 ? void 0 : ref1.onHMR(client); }); } async clean(span) { return span.traceChild('clean').traceAsyncFn(()=>(0, _recursiveDelete).recursiveDelete((0, _path).join(this.dir, this.config.distDir), /^cache/) ); } async getWebpackConfig(span) { const webpackConfigSpan = span.traceChild('get-webpack-config'); return webpackConfigSpan.traceAsyncFn(async ()=>{ const pagePaths = await webpackConfigSpan.traceChild('get-page-paths').traceAsyncFn(()=>Promise.all([ (0, _findPageFile).findPageFile(this.pagesDir, '/_app', this.config.pageExtensions), (0, _findPageFile).findPageFile(this.pagesDir, '/_document', this.config.pageExtensions), ]) ); this.pagesMapping = webpackConfigSpan.traceChild('create-pages-mapping').traceFn(()=>(0, _entries).createPagesMapping(pagePaths.filter((i)=>i !== null ), this.config.pageExtensions, { isDev: true, hasServerComponents: this.hasServerComponents }) ); const entrypoints = await webpackConfigSpan.traceChild('create-entrypoints').traceAsyncFn(()=>(0, _entries).createEntrypoints(this.pagesMapping, 'server', this.buildId, this.previewProps, this.config, [], this.pagesDir, true) ); return webpackConfigSpan.traceChild('generate-webpack-config').traceAsyncFn(()=>Promise.all([ (0, _webpackConfig).default(this.dir, { dev: true, isServer: false, config: this.config, buildId: this.buildId, pagesDir: this.pagesDir, rewrites: this.rewrites, entrypoints: entrypoints.client, runWebpackSpan: this.hotReloaderSpan, hasReactRoot: this.hasReactRoot }), (0, _webpackConfig).default(this.dir, { dev: true, isServer: true, config: this.config, buildId: this.buildId, pagesDir: this.pagesDir, rewrites: this.rewrites, entrypoints: entrypoints.server, runWebpackSpan: this.hotReloaderSpan, hasReactRoot: this.hasReactRoot }), // The edge runtime is only supported with React root. this.hasReactRoot ? (0, _webpackConfig).default(this.dir, { dev: true, isServer: true, isEdgeRuntime: true, config: this.config, buildId: this.buildId, pagesDir: this.pagesDir, rewrites: this.rewrites, entrypoints: entrypoints.edgeServer, runWebpackSpan: this.hotReloaderSpan, hasReactRoot: this.hasReactRoot }) : null, ].filter(Boolean)) ); }); } async buildFallbackError() { if (this.fallbackWatcher) return; const fallbackConfig = await (0, _webpackConfig).default(this.dir, { runWebpackSpan: this.hotReloaderSpan, dev: true, isServer: false, config: this.config, buildId: this.buildId, pagesDir: this.pagesDir, rewrites: { beforeFiles: [], afterFiles: [], fallback: [] }, isDevFallback: true, entrypoints: (await (0, _entries).createEntrypoints({ '/_app': 'next/dist/pages/_app', '/_error': 'next/dist/pages/_error' }, 'server', this.buildId, this.previewProps, this.config, [], this.pagesDir, true)).client, hasReactRoot: this.hasReactRoot }); const fallbackCompiler = (0, _webpack).webpack(fallbackConfig); this.fallbackWatcher = await new Promise((resolve)=>{ let bootedFallbackCompiler = false; fallbackCompiler.watch(// @ts-ignore webpack supports an array of watchOptions when using a multiCompiler fallbackConfig.watchOptions, // Errors are handled separately (_err)=>{ if (!bootedFallbackCompiler) { bootedFallbackCompiler = true; resolve(true); } }); }); } async start() { const startSpan = this.hotReloaderSpan.traceChild('start'); startSpan.stop() // Stop immediately to create an artificial parent span ; await this.clean(startSpan); // Ensure distDir exists before writing package.json await _fs.promises.mkdir(this.distDir, { recursive: true }); const distPackageJsonPath = (0, _path).join(this.distDir, 'package.json'); // Ensure commonjs handling is used for files in the distDir (generally .next) // Files outside of the distDir can be "type": "module" await _fs.promises.writeFile(distPackageJsonPath, '{"type": "commonjs"}'); const configs = await this.getWebpackConfig(startSpan); for (const config1 of configs){ const defaultEntry = config1.entry; config1.entry = async (...args)=>{ // @ts-ignore entry is always a function const entrypoints = await defaultEntry(...args); const isClientCompilation = config1.name === 'client'; const isNodeServerCompilation = config1.name === 'server'; const isEdgeServerCompilation = config1.name === 'edge-server'; await Promise.all(Object.keys(_onDemandEntryHandler.entries).map(async (pageKey)=>{ const isClientKey = pageKey.startsWith('client'); const isEdgeServerKey = pageKey.startsWith('edge-server'); if (isClientKey !== isClientCompilation) return; if (isEdgeServerKey !== isEdgeServerCompilation) return; const page = pageKey.slice(isClientKey ? 'client'.length : isEdgeServerKey ? 'edge-server'.length : 'server'.length); const isMiddleware = !!page.match(_constants.MIDDLEWARE_ROUTE); if (isClientCompilation && page.match(_constants.API_ROUTE) && !isMiddleware) { return; } if (!isClientCompilation && isMiddleware) { return; } const { bundlePath , absolutePagePath , dispose } = _onDemandEntryHandler.entries[pageKey]; const pageExists = !dispose && await (0, _fileExists).fileExists(absolutePagePath); if (!pageExists) { // page was removed or disposed delete _onDemandEntryHandler.entries[pageKey]; return; } const isApiRoute = page.match(_constants.API_ROUTE); const isCustomError = (0, _utils).isCustomErrorPage(page); const isReserved = (0, _utils).isReservedPage(page); const isServerComponent = this.hasServerComponents && (0, _utils).isFlightPage(this.config, absolutePagePath); const pageRuntimeConfig = await (0, _entries).getPageRuntime(absolutePagePath, this.config); const isEdgeSSRPage = pageRuntimeConfig === 'edge' && !isApiRoute; if (isNodeServerCompilation && isEdgeSSRPage && !isCustomError) { return; } if (isEdgeServerCompilation && !isEdgeSSRPage) { return; } _onDemandEntryHandler.entries[pageKey].status = _onDemandEntryHandler.BUILDING; const pageLoaderOpts = { page, absolutePagePath }; if (isClientCompilation) { if (isMiddleware) { // Middleware entrypoints[bundlePath] = (0, _entries).finalizeEntrypoint({ name: bundlePath, value: `next-middleware-loader?${(0, _querystring).stringify(pageLoaderOpts)}!`, isServer: false, isMiddleware: true }); } else { // A page route entrypoints[bundlePath] = (0, _entries).finalizeEntrypoint({ name: bundlePath, value: `next-client-pages-loader?${(0, _querystring).stringify(pageLoaderOpts)}!`, isServer: false }); // Tell the middleware plugin of the client compilation // that this route is a page. if (isEdgeSSRPage) { if (isServerComponent) { _middlewarePlugin.ssrEntries.set(bundlePath, { requireFlightManifest: true }); } else if (!isCustomError && !isReserved) { _middlewarePlugin.ssrEntries.set(bundlePath, { requireFlightManifest: false }); } } } } else if (isEdgeServerCompilation) { if (!isReserved) { entrypoints[bundlePath] = (0, _entries).finalizeEntrypoint({ name: '[name].js', value: `next-middleware-ssr-loader?${(0, _querystring).stringify({ dev: true, page, stringifiedConfig: JSON.stringify(this.config), absoluteAppPath: this.pagesMapping['/_app'], absoluteAppServerPath: this.pagesMapping['/_app.server'], absoluteDocumentPath: this.pagesMapping['/_document'], absoluteErrorPath: this.pagesMapping['/_error'], absolute404Path: this.pagesMapping['/404'] || '', absolutePagePath, isServerComponent, buildId: this.buildId })}!`, isServer: false, isEdgeServer: true }); } } else if (isNodeServerCompilation) { let request = (0, _path).relative(config1.context, absolutePagePath); if (!(0, _path).isAbsolute(request) && !request.startsWith('../')) { request = `./${request}`; } entrypoints[bundlePath] = (0, _entries).finalizeEntrypoint({ name: bundlePath, value: request, isServer: true }); } })); return entrypoints; }; } // Enable building of client compilation before server compilation in development // @ts-ignore webpack 5 configs.parallelism = 1; const multiCompiler = (0, _webpack).webpack(configs); (0, _output).watchCompilers(multiCompiler.compilers[0], multiCompiler.compilers[1], multiCompiler.compilers[2] || null); // Watch for changes to client/server page files so we can tell when just // the server file changes and trigger a reload for GS(S)P pages const changedClientPages = new Set(); const changedServerPages = new Set(); const prevClientPageHashes = new Map(); const prevServerPageHashes = new Map(); const trackPageChanges = (pageHashMap, changedItems)=>(stats)=>{ try { stats.entrypoints.forEach((entry, key)=>{ if (key.startsWith('pages/')) { // TODO this doesn't handle on demand loaded chunks entry.chunks.forEach((chunk)=>{ if (chunk.id === key) { const modsIterable = stats.chunkGraph.getChunkModulesIterable(chunk); let chunksHash = new _webpack.StringXor(); modsIterable.forEach((mod)=>{ if (mod.resource && mod.resource.replace(/\\/g, '/').includes(key)) { // use original source to calculate hash since mod.hash // includes the source map in development which changes // every time for both server and client so we calculate // the hash without the source map for the page module const hash = require('crypto').createHash('sha256').update(mod.originalSource().buffer()).digest().toString('hex'); chunksHash.add(hash); } else { // for non-pages we can use the module hash directly const hash = stats.chunkGraph.getModuleHash(mod, chunk.runtime); chunksHash.add(hash); } }); const prevHash = pageHashMap.get(key); const curHash = chunksHash.toString(); if (prevHash && prevHash !== curHash) { changedItems.add(key); } pageHashMap.set(key, curHash); } }); } }); } catch (err) { console.error(err); } } ; multiCompiler.compilers[0].hooks.emit.tap('NextjsHotReloaderForClient', trackPageChanges(prevClientPageHashes, changedClientPages)); multiCompiler.compilers[1].hooks.emit.tap('NextjsHotReloaderForServer', trackPageChanges(prevServerPageHashes, changedServerPages)); // This plugin watches for changes to _document.js and notifies the client side that it should reload the page multiCompiler.compilers[1].hooks.failed.tap('NextjsHotReloaderForServer', (err)=>{ this.serverError = err; this.serverStats = null; }); multiCompiler.compilers[1].hooks.done.tap('NextjsHotReloaderForServer', (stats)=>{ this.serverError = null; this.serverStats = stats; const { compilation } = stats; // We only watch `_document` for changes on the server compilation // the rest of the files will be triggered by the client compilation const documentChunk = compilation.namedChunks.get('pages/_document'); // If the document chunk can't be found we do nothing if (!documentChunk) { console.warn('_document.js chunk not found'); return; } // Initial value if (this.serverPrevDocumentHash === null) { this.serverPrevDocumentHash = documentChunk.hash || null; return; } // If _document.js didn't change we don't trigger a reload if (documentChunk.hash === this.serverPrevDocumentHash) { return; } // Notify reload to reload the page, as _document.js was changed (different hash) this.send('reloadPage'); this.serverPrevDocumentHash = documentChunk.hash || null; }); multiCompiler.hooks.done.tap('NextjsHotReloaderForServer', ()=>{ const serverOnlyChanges = (0, _utils).difference(changedServerPages, changedClientPages); const middlewareChanges = Array.from(changedClientPages).filter((name)=>name.match(_constants.MIDDLEWARE_ROUTE) ); changedClientPages.clear(); changedServerPages.clear(); if (middlewareChanges.length > 0) { this.send({ event: 'middlewareChanges' }); } if (serverOnlyChanges.length > 0) { this.send({ event: 'serverOnlyChanges', pages: serverOnlyChanges.map((pg)=>(0, _normalizePagePath).denormalizePagePath(pg.slice('pages'.length)) ) }); } }); multiCompiler.compilers[0].hooks.failed.tap('NextjsHotReloaderForClient', (err)=>{ this.clientError = err; this.clientStats = null; }); multiCompiler.compilers[0].hooks.done.tap('NextjsHotReloaderForClient', (stats)=>{ this.clientError = null; this.clientStats = stats; const { compilation } = stats; const chunkNames = new Set([ ...compilation.namedChunks.keys() ].filter((name)=>!!(0, _getRouteFromEntrypoint).default(name) )); if (this.prevChunkNames) { // detect chunks which have to be replaced with a new template // e.g, pages/index.js <-> pages/_error.js const addedPages = diff(chunkNames, this.prevChunkNames); const removedPages = diff(this.prevChunkNames, chunkNames); if (addedPages.size > 0) { for (const addedPage of addedPages){ const page = (0, _getRouteFromEntrypoint).default(addedPage); this.send('addedPage', page); } } if (removedPages.size > 0) { for (const removedPage of removedPages){ const page = (0, _getRouteFromEntrypoint).default(removedPage); this.send('removedPage', page); } } } this.prevChunkNames = chunkNames; }); this.webpackHotMiddleware = new _hotMiddleware.WebpackHotMiddleware(multiCompiler.compilers); let booted = false; this.watcher = await new Promise((resolve)=>{ const watcher = multiCompiler.watch(// @ts-ignore webpack supports an array of watchOptions when using a multiCompiler configs.map((config)=>config.watchOptions ), // Errors are handled separately (_err)=>{ if (!booted) { booted = true; resolve(watcher); } }); }); this.onDemandEntries = (0, _onDemandEntryHandler).default(this.watcher, multiCompiler, { pagesDir: this.pagesDir, nextConfig: this.config, ...this.config.onDemandEntries }); this.middlewares = [ (0, _middleware).getOverlayMiddleware({ rootDirectory: this.dir, stats: ()=>this.clientStats , serverStats: ()=>this.serverStats }), ]; } async stop() { await new Promise((resolve, reject)=>{ this.watcher.close((err)=>err ? reject(err) : resolve(true) ); }); if (this.fallbackWatcher) { await new Promise((resolve, reject)=>{ this.fallbackWatcher.close((err)=>err ? reject(err) : resolve(true) ); }); } } async getCompilationErrors(page) { var ref, ref2; const normalizedPage = (0, _normalizePagePath).normalizePathSep(page); if (this.clientError || this.serverError) { return [ this.clientError || this.serverError ]; } else if ((ref = this.clientStats) === null || ref === void 0 ? void 0 : ref.hasErrors()) { const { compilation } = this.clientStats; const failedPages = erroredPages(compilation); // If there is an error related to the requesting page we display it instead of the first error if (failedPages[normalizedPage] && failedPages[normalizedPage].length > 0) { return failedPages[normalizedPage]; } // If none were found we still have to show the other errors return this.clientStats.compilation.errors; } else if ((ref2 = this.serverStats) === null || ref2 === void 0 ? void 0 : ref2.hasErrors()) { const { compilation } = this.serverStats; const failedPages = erroredPages(compilation); // If there is an error related to the requesting page we display it instead of the first error if (failedPages[normalizedPage] && failedPages[normalizedPage].length > 0) { return failedPages[normalizedPage]; } // If none were found we still have to show the other errors return this.serverStats.compilation.errors; } return []; } send(action, ...args) { this.webpackHotMiddleware.publish(action && typeof action === 'object' ? action : { action, data: args }); } async ensurePage(page, clientOnly = false) { var ref; // Make sure we don't re-build or dispose prebuilt pages if (page !== '/_error' && _constants1.BLOCKED_PAGES.indexOf(page) !== -1) { return; } const error = clientOnly ? this.clientError : this.serverError || this.clientError; if (error) { return Promise.reject(error); } return (ref = this.onDemandEntries) === null || ref === void 0 ? void 0 : ref.ensurePage(page, clientOnly); } } exports.default = HotReloader; function diff(a, b) { return new Set([ ...a ].filter((v)=>!b.has(v) )); } //# sourceMappingURL=hot-reloader.js.map