UNPKG

next

Version:

The React Framework

419 lines (418 loc) • 18.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.onDemandEntryHandler = onDemandEntryHandler; exports.getInvalidator = exports.entries = exports.BUILT = exports.BUILDING = exports.ADDED = void 0; var _events = require("events"); var _findPageFile = require("../lib/find-page-file"); var _entries = require("../../build/entries"); var _path = require("path"); var _normalizePathSep = require("../../shared/lib/page-path/normalize-path-sep"); var _normalizePagePath = require("../../shared/lib/page-path/normalize-page-path"); var _ensureLeadingSlash = require("../../shared/lib/page-path/ensure-leading-slash"); var _removePagePathTail = require("../../shared/lib/page-path/remove-page-path-tail"); var _output = require("../../build/output"); var _getRouteFromEntrypoint = _interopRequireDefault(require("../get-route-from-entrypoint")); var _utils = require("../../build/webpack/loaders/utils"); var _getPageStaticInfo = require("../../build/analysis/get-page-static-info"); var _utils1 = require("../../build/utils"); var _utils2 = require("../../shared/lib/utils"); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function treePathToEntrypoint(segmentPath, parentPath) { const [parallelRouteKey, segment] = segmentPath; // TODO: modify this path to cover parallelRouteKey convention const path = (parentPath ? parentPath + "/" : "") + (parallelRouteKey !== "children" ? parallelRouteKey + "/" : "") + (segment === "" ? "page" : segment); // Last segment if (segmentPath.length === 2) { return path; } const childSegmentPath = segmentPath.slice(2); return treePathToEntrypoint(childSegmentPath, path); } function convertDynamicParamTypeToSyntax(dynamicParamTypeShort, param) { switch(dynamicParamTypeShort){ case "c": return `[...${param}]`; case "oc": return `[[...${param}]]`; case "d": return `[${param}]`; default: throw new Error("Unknown dynamic param type"); } } function getEntrypointsFromTree(tree, isFirst, parentPath = []) { const [segment, parallelRoutes] = tree; const currentSegment = Array.isArray(segment) ? convertDynamicParamTypeToSyntax(segment[2], segment[0]) : segment; const currentPath = [ ...parentPath, currentSegment ]; if (!isFirst && currentSegment === "") { // TODO get rid of '' at the start of tree return [ treePathToEntrypoint(currentPath.slice(1)) ]; } return Object.keys(parallelRoutes).reduce((paths, key)=>{ const childTree = parallelRoutes[key]; const childPages = getEntrypointsFromTree(childTree, false, [ ...currentPath, key, ]); return [ ...paths, ...childPages ]; }, []); } const ADDED = Symbol("added"); exports.ADDED = ADDED; const BUILDING = Symbol("building"); exports.BUILDING = BUILDING; const BUILT = Symbol("built"); exports.BUILT = BUILT; const entries = {}; exports.entries = entries; // Make sure only one invalidation happens at a time∫ // Otherwise, webpack hash gets changed and it'll force the client to reload. class Invalidator { constructor(multiCompiler){ this.multiCompiler = multiCompiler; // contains an array of types of compilers currently building this.building = false; this.rebuildAgain = false; } invalidate(keys = []) { // If there's a current build is processing, we won't abort it by invalidating. // (If aborted, it'll cause a client side hard reload) // But let it to invalidate just after the completion. // So, it can re-build the queued pages at once. if (this.building) { this.rebuildAgain = true; return; } this.building = true; if (!keys || keys.length === 0) { var ref, ref1, ref2; (ref = this.multiCompiler.compilers[0].watching) == null ? void 0 : ref.invalidate(); (ref1 = this.multiCompiler.compilers[1].watching) == null ? void 0 : ref1.invalidate(); (ref2 = this.multiCompiler.compilers[2].watching) == null ? void 0 : ref2.invalidate(); return; } for (const key of keys){ if (key === "client") { var ref3; (ref3 = this.multiCompiler.compilers[0].watching) == null ? void 0 : ref3.invalidate(); } else if (key === "server") { var ref4; (ref4 = this.multiCompiler.compilers[1].watching) == null ? void 0 : ref4.invalidate(); } else if (key === "edgeServer") { var ref5; (ref5 = this.multiCompiler.compilers[2].watching) == null ? void 0 : ref5.invalidate(); } } } startBuilding() { this.building = true; } doneBuilding() { this.building = false; if (this.rebuildAgain) { this.rebuildAgain = false; this.invalidate(); } } } let invalidator; const getInvalidator = ()=>invalidator; exports.getInvalidator = getInvalidator; const doneCallbacks = new _events.EventEmitter(); const lastClientAccessPages = [ "" ]; const lastServerAccessPagesForAppDir = [ "" ]; function onDemandEntryHandler({ maxInactiveAge , multiCompiler , nextConfig , pagesBufferLength , pagesDir , rootDir , appDir }) { invalidator = new Invalidator(multiCompiler); const startBuilding = (_compilation)=>{ invalidator.startBuilding(); }; for (const compiler of multiCompiler.compilers){ compiler.hooks.make.tap("NextJsOnDemandEntries", startBuilding); } function getPagePathsFromEntrypoints(type, entrypoints, root) { const pagePaths = []; for (const entrypoint of entrypoints.values()){ const page = (0, _getRouteFromEntrypoint).default(entrypoint.name, root); if (page) { pagePaths.push(`${type}${page}`); } else if (root && entrypoint.name === "root" || (0, _utils1).isMiddlewareFilename(entrypoint.name)) { pagePaths.push(`${type}/${entrypoint.name}`); } } return pagePaths; } multiCompiler.hooks.done.tap("NextJsOnDemandEntries", (multiStats)=>{ if (invalidator.rebuildAgain) { return invalidator.doneBuilding(); } const [clientStats, serverStats, edgeServerStats] = multiStats.stats; const root = !!appDir; const pagePaths = [ ...getPagePathsFromEntrypoints("client", clientStats.compilation.entrypoints, root), ...getPagePathsFromEntrypoints("server", serverStats.compilation.entrypoints, root), ...edgeServerStats ? getPagePathsFromEntrypoints("edge-server", edgeServerStats.compilation.entrypoints, root) : [], ]; for (const page of pagePaths){ const entry = entries[page]; if (!entry) { continue; } if (entry.status !== BUILDING) { continue; } entry.status = BUILT; doneCallbacks.emit(page); } invalidator.doneBuilding(); }); const pingIntervalTime = Math.max(1000, Math.min(5000, maxInactiveAge)); setInterval(function() { disposeInactiveEntries(maxInactiveAge); }, pingIntervalTime + 1000).unref(); function handleAppDirPing(tree) { const pages = getEntrypointsFromTree(tree, true); for (const page of pages){ const pageKey = `server/${page}`; const entryInfo = entries[pageKey]; // If there's no entry, it may have been invalidated and needs to be re-built. if (!entryInfo) { // if (page !== lastEntry) client pings, but there's no entry for page return { invalid: true }; } // We don't need to maintain active state of anything other than BUILT entries if (entryInfo.status !== BUILT) continue; // If there's an entryInfo if (!lastServerAccessPagesForAppDir.includes(pageKey)) { lastServerAccessPagesForAppDir.unshift(pageKey); // Maintain the buffer max length // TODO: verify that the current pageKey is not at the end of the array as multiple entrypoints can exist if (lastServerAccessPagesForAppDir.length > pagesBufferLength) { lastServerAccessPagesForAppDir.pop(); } } entryInfo.lastActiveTime = Date.now(); entryInfo.dispose = false; } return { success: true }; } function handlePing(pg) { const page = (0, _normalizePathSep).normalizePathSep(pg); const pageKey = `client${page}`; const entryInfo = entries[pageKey]; // If there's no entry, it may have been invalidated and needs to be re-built. if (!entryInfo) { // if (page !== lastEntry) client pings, but there's no entry for page return { invalid: true }; } // 404 is an on demand entry but when a new page is added we have to refresh the page const toSend = page === "/_error" ? { invalid: true } : { success: true }; // We don't need to maintain active state of anything other than BUILT entries if (entryInfo.status !== BUILT) return; // If there's an entryInfo if (!lastClientAccessPages.includes(pageKey)) { lastClientAccessPages.unshift(pageKey); // Maintain the buffer max length if (lastClientAccessPages.length > pagesBufferLength) { lastClientAccessPages.pop(); } } entryInfo.lastActiveTime = Date.now(); entryInfo.dispose = false; return toSend; } return { async ensurePage (page, clientOnly) { const pagePathData = await findPagePathData(rootDir, pagesDir, page, nextConfig.pageExtensions, appDir); let entryAdded = false; const addPageEntry = (type)=>{ return new Promise((resolve, reject)=>{ const isServerComponent = _utils.serverComponentRegex.test(pagePathData.absolutePagePath); const isInsideAppDir = appDir && pagePathData.absolutePagePath.startsWith(appDir); const pageKey = `${type}${pagePathData.page}`; if (entries[pageKey]) { entries[pageKey].dispose = false; entries[pageKey].lastActiveTime = Date.now(); if (entries[pageKey].status === BUILT) { resolve(); return; } } else { if (type === "client" && (isServerComponent || isInsideAppDir)) { // Skip adding the client entry here. } else { entryAdded = true; entries[pageKey] = { absolutePagePath: pagePathData.absolutePagePath, bundlePath: pagePathData.bundlePath, dispose: false, lastActiveTime: Date.now(), status: ADDED }; } } doneCallbacks.once(pageKey, (err)=>{ if (err) return reject(err); resolve(); }); }); }; const staticInfo = await (0, _getPageStaticInfo).getPageStaticInfo({ pageFilePath: pagePathData.absolutePagePath, nextConfig }); const result = (0, _entries).runDependingOnPageType({ page: pagePathData.page, pageRuntime: staticInfo.runtime, onClient: ()=>addPageEntry("client"), onServer: ()=>addPageEntry("server"), onEdgeServer: ()=>addPageEntry("edge-server") }); const promises = Object.values(result); if (entryAdded) { (0, _output).reportTrigger(!clientOnly && promises.length > 1 ? `${pagePathData.page} (client and server)` : pagePathData.page); invalidator.invalidate(Object.keys(result)); } return Promise.all(promises); }, onHMR (client) { client.addEventListener("message", ({ data })=>{ try { const parsedData = JSON.parse(typeof data !== "string" ? data.toString() : data); if (parsedData.event === "ping") { const result = parsedData.appDirRoute ? handleAppDirPing(parsedData.tree) : handlePing(parsedData.page); client.send(JSON.stringify({ ...result, [parsedData.appDirRoute ? "action" : "event"]: "pong" })); } } catch (_) {} }); } }; } function disposeInactiveEntries(maxInactiveAge) { Object.keys(entries).forEach((page)=>{ const { lastActiveTime , status , dispose , bundlePath } = entries[page]; const isClientComponentsEntry = bundlePath.startsWith("app/") && page.startsWith("client/"); // Disposing client component entry is handled when disposing server component entry if (isClientComponentsEntry) return; // Skip pages already scheduled for disposing if (dispose) return; // This means this entry is currently building or just added // We don't need to dispose those entries. if (status !== BUILT) return; // We should not build the last accessed page even we didn't get any pings // Sometimes, it's possible our XHR ping to wait before completing other requests. // In that case, we should not dispose the current viewing page if (lastClientAccessPages.includes(page) || lastServerAccessPagesForAppDir.includes(page)) return; if (lastActiveTime && Date.now() - lastActiveTime > maxInactiveAge) { const isServerComponentsEntry = bundlePath.startsWith("app/") && page.startsWith("server/"); // Dispose client component entrypoint when server component entrypoint is disposed. if (isServerComponentsEntry) { entries[page.replace("server/", "client/")].dispose = true; } entries[page].dispose = true; } }); } /** * Attempts to find a page file path from the given pages absolute directory, * a page and allowed extensions. If the page can't be found it will throw an * error. It defaults the `/_error` page to Next.js internal error page. * * @param rootDir Absolute path to the project root. * @param pagesDir Absolute path to the pages folder with trailing `/pages`. * @param normalizedPagePath The page normalized (it will be denormalized). * @param pageExtensions Array of page extensions. */ async function findPagePathData(rootDir, pagesDir, page, extensions, appDir) { const normalizedPagePath = tryToNormalizePagePath(page); let pagePath = null; if ((0, _utils1).isMiddlewareFile(normalizedPagePath)) { pagePath = await (0, _findPageFile).findPageFile(rootDir, normalizedPagePath, extensions); if (!pagePath) { throw new _utils2.PageNotFoundError(normalizedPagePath); } const pageUrl = (0, _ensureLeadingSlash).ensureLeadingSlash((0, _removePagePathTail).removePagePathTail((0, _normalizePathSep).normalizePathSep(pagePath), { extensions })); return { absolutePagePath: (0, _path).join(rootDir, pagePath), bundlePath: normalizedPagePath.slice(1), page: _path.posix.normalize(pageUrl) }; } // Check appDir first falling back to pagesDir if (appDir) { pagePath = await (0, _findPageFile).findPageFile(appDir, normalizedPagePath, extensions); if (pagePath) { const pageUrl = (0, _ensureLeadingSlash).ensureLeadingSlash((0, _removePagePathTail).removePagePathTail((0, _normalizePathSep).normalizePathSep(pagePath), { keepIndex: true, extensions })); return { absolutePagePath: (0, _path).join(appDir, pagePath), bundlePath: _path.posix.join("app", (0, _normalizePagePath).normalizePagePath(pageUrl)), page: _path.posix.normalize(pageUrl) }; } } if (!pagePath) { pagePath = await (0, _findPageFile).findPageFile(pagesDir, normalizedPagePath, extensions); } if (pagePath !== null) { const pageUrl = (0, _ensureLeadingSlash).ensureLeadingSlash((0, _removePagePathTail).removePagePathTail((0, _normalizePathSep).normalizePathSep(pagePath), { extensions })); return { absolutePagePath: (0, _path).join(pagesDir, pagePath), bundlePath: _path.posix.join("pages", (0, _normalizePagePath).normalizePagePath(pageUrl)), page: _path.posix.normalize(pageUrl) }; } if (page === "/_error") { return { absolutePagePath: require.resolve("next/dist/pages/_error"), bundlePath: page, page: (0, _normalizePathSep).normalizePathSep(page) }; } else { throw new _utils2.PageNotFoundError(normalizedPagePath); } } function tryToNormalizePagePath(page) { try { return (0, _normalizePagePath).normalizePagePath(page); } catch (err) { console.error(err); throw new _utils2.PageNotFoundError(page); } } //# sourceMappingURL=on-demand-entry-handler.js.map