UNPKG

next

Version:

The React Framework

440 lines (439 loc) • 21.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "exportPages", { enumerable: true, get: function() { return exportPages; } }); require("../server/node-environment"); const _path = require("path"); const _promises = /*#__PURE__*/ _interop_require_default(require("fs/promises")); const _loadcomponents = require("../server/load-components"); const _isdynamic = require("../shared/lib/router/utils/is-dynamic"); const _normalizepagepath = require("../shared/lib/page-path/normalize-page-path"); const _normalizelocalepath = require("../shared/lib/i18n/normalize-locale-path"); const _trace = require("../trace"); const _setuphttpagentenv = require("../server/setup-http-agent-env"); const _iserror = /*#__PURE__*/ _interop_require_default(require("../lib/is-error")); const _requestmeta = require("../server/request-meta"); const _apppaths = require("../shared/lib/router/utils/app-paths"); const _mockrequest = require("../server/lib/mock-request"); const _isapprouteroute = require("../lib/is-app-route-route"); const _ciinfo = require("../server/ci-info"); const _approute = require("./routes/app-route"); const _apppage = require("./routes/app-page"); const _pages = require("./routes/pages"); const _getparams = require("./helpers/get-params"); const _createincrementalcache = require("./helpers/create-incremental-cache"); const _ispostpone = require("../server/lib/router-utils/is-postpone"); const _isdynamicusageerror = require("./helpers/is-dynamic-usage-error"); const _bailouttocsr = require("../shared/lib/lazy-dynamic/bailout-to-csr"); const _turborepoaccesstrace = require("../build/turborepo-access-trace"); const _fallbackparams = require("../server/request/fallback-params"); const _needsexperimentalreact = require("../lib/needs-experimental-react"); const _staticgenerationbailout = require("../client/components/static-generation-bailout"); const _multifilewriter = require("../lib/multi-file-writer"); function _interop_require_default(obj) { return obj && obj.__esModule ? obj : { default: obj }; } process.env.NEXT_IS_EXPORT_WORKER = 'true'; const envConfig = require('../shared/lib/runtime-config.external'); globalThis.__NEXT_DATA__ = { nextExport: true }; class TimeoutError extends Error { constructor(...args){ super(...args), this.code = 'NEXT_EXPORT_TIMEOUT_ERROR'; } } class ExportPageError extends Error { constructor(...args){ super(...args), this.code = 'NEXT_EXPORT_PAGE_ERROR'; } } async function exportPageImpl(input, fileWriter) { var _req_url; const { path, pathMap, distDir, pagesDataDir, buildExport = false, serverRuntimeConfig, subFolders = false, optimizeCss, disableOptimizedLoading, debugOutput = false, enableExperimentalReact, ampValidatorPath, trailingSlash, sriEnabled } = input; if (enableExperimentalReact) { process.env.__NEXT_EXPERIMENTAL_REACT = 'true'; } const { page, // The parameters that are currently unknown. _fallbackRouteParams = [], // Check if this is an `app/` page. _isAppDir: isAppDir = false, // Check if this should error when dynamic usage is detected. _isDynamicError: isDynamicError = false, // If this page supports partial prerendering, then we need to pass that to // the renderOpts. _isRoutePPREnabled: isRoutePPREnabled, // If this is a prospective render, we don't actually want to persist the // result, we just want to use it to error the build if there's a problem. _isProspectiveRender: isProspectiveRender = false, // Pull the original query out. query: originalQuery = {} } = pathMap; const fallbackRouteParams = (0, _fallbackparams.getFallbackRouteParams)(_fallbackRouteParams); let query = { ...originalQuery }; const pathname = (0, _apppaths.normalizeAppPath)(page); const isDynamic = (0, _isdynamic.isDynamicRoute)(page); const outDir = isAppDir ? (0, _path.join)(distDir, 'server/app') : input.outDir; const filePath = (0, _normalizepagepath.normalizePagePath)(path); const ampPath = `${filePath}.amp`; let renderAmpPath = ampPath; let updatedPath = pathMap._ssgPath || path; let locale = pathMap._locale || input.renderOpts.locale; if (input.renderOpts.locale) { const localePathResult = (0, _normalizelocalepath.normalizeLocalePath)(path, input.renderOpts.locales); if (localePathResult.detectedLocale) { updatedPath = localePathResult.pathname; locale = localePathResult.detectedLocale; if (locale === input.renderOpts.defaultLocale) { renderAmpPath = `${(0, _normalizepagepath.normalizePagePath)(updatedPath)}.amp`; } } } // We need to show a warning if they try to provide query values // for an auto-exported page since they won't be available const hasOrigQueryValues = Object.keys(originalQuery).length > 0; // Check if the page is a specified dynamic route const { pathname: nonLocalizedPath } = (0, _normalizelocalepath.normalizeLocalePath)(path, input.renderOpts.locales); let params; if (isDynamic && page !== nonLocalizedPath) { const normalizedPage = isAppDir ? (0, _apppaths.normalizeAppPath)(page) : page; params = (0, _getparams.getParams)(normalizedPage, updatedPath); } const { req, res } = (0, _mockrequest.createRequestResponseMocks)({ url: updatedPath }); // If this is a status code page, then set the response code. for (const statusCode of [ 404, 500 ]){ if ([ `/${statusCode}`, `/${statusCode}.html`, `/${statusCode}/index.html` ].some((p)=>p === updatedPath || `/${locale}${p}` === updatedPath)) { res.statusCode = statusCode; } } // Ensure that the URL has a trailing slash if it's configured. if (trailingSlash && !((_req_url = req.url) == null ? void 0 : _req_url.endsWith('/'))) { req.url += '/'; } if (locale && buildExport && input.renderOpts.domainLocales && input.renderOpts.domainLocales.some((dl)=>{ var _dl_locales; return dl.defaultLocale === locale || ((_dl_locales = dl.locales) == null ? void 0 : _dl_locales.includes(locale || '')); })) { (0, _requestmeta.addRequestMeta)(req, 'isLocaleDomain', true); } envConfig.setConfig({ serverRuntimeConfig, publicRuntimeConfig: input.renderOpts.runtimeConfig }); const getHtmlFilename = (p)=>subFolders ? `${p}${_path.sep}index.html` : `${p}.html`; let htmlFilename = getHtmlFilename(filePath); // dynamic routes can provide invalid extensions e.g. /blog/[...slug] returns an // extension of `.slug]` const pageExt = isDynamic || isAppDir ? '' : (0, _path.extname)(page); const pathExt = isDynamic || isAppDir ? '' : (0, _path.extname)(path); // force output 404.html for backwards compat if (path === '/404.html') { htmlFilename = path; } else if (pageExt !== pathExt && pathExt !== '') { const isBuiltinPaths = [ '/500', '/404' ].some((p)=>p === path || p === path + '.html'); // If the ssg path has .html extension, and it's not builtin paths, use it directly // Otherwise, use that as the filename instead const isHtmlExtPath = !isBuiltinPaths && path.endsWith('.html'); htmlFilename = isHtmlExtPath ? getHtmlFilename(path) : path; } else if (path === '/') { // If the path is the root, just use index.html htmlFilename = 'index.html'; } const baseDir = (0, _path.join)(outDir, (0, _path.dirname)(htmlFilename)); let htmlFilepath = (0, _path.join)(outDir, htmlFilename); await _promises.default.mkdir(baseDir, { recursive: true }); const components = await (0, _loadcomponents.loadComponents)({ distDir, page, isAppPath: isAppDir, isDev: false, sriEnabled }); // Handle App Routes. if (isAppDir && (0, _isapprouteroute.isAppRouteRoute)(page)) { return (0, _approute.exportAppRoute)(req, res, params, page, components.routeModule, input.renderOpts.incrementalCache, input.renderOpts.cacheLifeProfiles, htmlFilepath, fileWriter, input.renderOpts.experimental, input.buildId); } // During the export phase in next build, if it's using PPR we can serve streaming metadata // when it's available. When we're building the PPR rendering result, we don't need to rely // on the user agent. The result can be determined to serve streaming on infrastructure level. const serveStreamingMetadata = !!isRoutePPREnabled; const renderOpts = { ...components, ...input.renderOpts, ampPath: renderAmpPath, params, optimizeCss, disableOptimizedLoading, locale, supportsDynamicResponse: false, serveStreamingMetadata, experimental: { ...input.renderOpts.experimental, isRoutePPREnabled } }; if (_ciinfo.hasNextSupport) { renderOpts.isRevalidate = true; } // Handle App Pages if (isAppDir) { const sharedContext = { buildId: input.buildId }; // If this is a prospective render, don't return any metrics or revalidate // timings as we aren't persisting this render (it was only to error). if (isProspectiveRender) { return (0, _apppage.prospectiveRenderAppPage)(req, res, page, pathname, query, fallbackRouteParams, renderOpts, sharedContext); } return (0, _apppage.exportAppPage)(req, res, page, path, pathname, query, fallbackRouteParams, renderOpts, htmlFilepath, debugOutput, isDynamicError, fileWriter, sharedContext); } const sharedContext = { buildId: input.buildId, deploymentId: input.renderOpts.deploymentId, customServer: undefined }; const renderContext = { isFallback: pathMap._pagesFallback ?? false, isDraftMode: false, developmentNotFoundSourcePage: undefined }; return (0, _pages.exportPagesPage)(req, res, path, page, query, params, htmlFilepath, htmlFilename, ampPath, subFolders, outDir, ampValidatorPath, pagesDataDir, buildExport, isDynamic, sharedContext, renderContext, hasOrigQueryValues, renderOpts, components, fileWriter); } async function exportPages(input) { const { exportPathMap, paths, dir, distDir, outDir, cacheHandler, cacheMaxMemorySize, fetchCacheKeyPrefix, pagesDataDir, renderOpts, nextConfig, options } = input; // If the fetch cache was enabled, we need to create an incremental // cache instance for this page. const incrementalCache = await (0, _createincrementalcache.createIncrementalCache)({ cacheHandler, cacheMaxMemorySize, fetchCacheKeyPrefix, distDir, dir, // skip writing to disk in minimal mode for now, pending some // changes to better support it flushToDisk: !_ciinfo.hasNextSupport, cacheHandlers: nextConfig.experimental.cacheHandlers }); renderOpts.incrementalCache = incrementalCache; const maxConcurrency = nextConfig.experimental.staticGenerationMaxConcurrency ?? 8; const results = []; const exportPageWithRetry = async (path, maxAttempts)=>{ const pathMap = exportPathMap[path]; const { page } = exportPathMap[path]; const pageKey = page !== path ? `${page}: ${path}` : path; let attempt = 0; let result; while(attempt < maxAttempts){ try { var _nextConfig_experimental_amp, _nextConfig_experimental_sri; result = await Promise.race([ exportPage({ path, pathMap, distDir, outDir, pagesDataDir, renderOpts, ampValidatorPath: ((_nextConfig_experimental_amp = nextConfig.experimental.amp) == null ? void 0 : _nextConfig_experimental_amp.validator) || undefined, trailingSlash: nextConfig.trailingSlash, serverRuntimeConfig: nextConfig.serverRuntimeConfig, subFolders: nextConfig.trailingSlash && !options.buildExport, buildExport: options.buildExport, optimizeCss: nextConfig.experimental.optimizeCss, disableOptimizedLoading: nextConfig.experimental.disableOptimizedLoading, parentSpanId: input.parentSpanId, httpAgentOptions: nextConfig.httpAgentOptions, debugOutput: options.debugOutput, enableExperimentalReact: (0, _needsexperimentalreact.needsExperimentalReact)(nextConfig), sriEnabled: Boolean((_nextConfig_experimental_sri = nextConfig.experimental.sri) == null ? void 0 : _nextConfig_experimental_sri.algorithm), buildId: input.buildId }), // If exporting the page takes longer than the timeout, reject the promise. new Promise((_, reject)=>{ setTimeout(()=>{ reject(new TimeoutError()); }, nextConfig.staticPageGenerationTimeout * 1000); }) ]); // If there was an error in the export, throw it immediately. In the catch block, we might retry the export, // or immediately fail the build, depending on user configuration. We might also continue on and attempt other pages. if (result && 'error' in result) { throw new ExportPageError(); } break; } catch (err) { // The only error that should be caught here is an ExportError, as `exportPage` doesn't throw and instead returns an object with an `error` property. // This is an overly cautious check to ensure that we don't accidentally catch an unexpected error. if (!(err instanceof ExportPageError || err instanceof TimeoutError)) { throw err; } if (err instanceof TimeoutError) { // If the export times out, we will restart the worker up to 3 times. maxAttempts = 3; } // We've reached the maximum number of attempts if (attempt >= maxAttempts - 1) { // Log a message if we've reached the maximum number of attempts. // We only care to do this if maxAttempts was configured. if (maxAttempts > 1) { console.info(`Failed to build ${pageKey} after ${maxAttempts} attempts.`); } // If prerenderEarlyExit is enabled, we'll exit the build immediately. if (nextConfig.experimental.prerenderEarlyExit) { console.error(`Export encountered an error on ${pageKey}, exiting the build.`); process.exit(1); } else { // Otherwise, this is a no-op. The build will continue, and a summary of failed pages will be displayed at the end. } } else { // Otherwise, we have more attempts to make. Wait before retrying if (err instanceof TimeoutError) { console.info(`Failed to build ${pageKey} (attempt ${attempt + 1} of ${maxAttempts}) because it took more than ${nextConfig.staticPageGenerationTimeout} seconds. Retrying again shortly.`); } else { console.info(`Failed to build ${pageKey} (attempt ${attempt + 1} of ${maxAttempts}). Retrying again shortly.`); } // Exponential backoff with random jitter to avoid thundering herd on retries const baseDelay = 500 // 500ms ; const maxDelay = 2000 // 2 seconds ; const delay = Math.min(baseDelay * Math.pow(2, attempt), maxDelay); const jitter = Math.random() * 0.3 * delay // Add up to 30% random jitter ; await new Promise((r)=>setTimeout(r, delay + jitter)); } } attempt++; } return { result, path, pageKey }; }; for(let i = 0; i < paths.length; i += maxConcurrency){ const subset = paths.slice(i, i + maxConcurrency); const subsetResults = await Promise.all(subset.map((path)=>exportPageWithRetry(path, nextConfig.experimental.staticGenerationRetryCount ?? 1))); results.push(...subsetResults); } return results; } async function exportPage(input) { (0, _trace.trace)('export-page', input.parentSpanId).setAttribute('path', input.path); // Configure the http agent. (0, _setuphttpagentenv.setHttpClientAndAgentOptions)({ httpAgentOptions: input.httpAgentOptions }); const fileWriter = new _multifilewriter.MultiFileWriter({ writeFile: (filePath, data)=>_promises.default.writeFile(filePath, data), mkdir: (dir)=>_promises.default.mkdir(dir, { recursive: true }) }); const exportPageSpan = (0, _trace.trace)('export-page-worker', input.parentSpanId); const start = Date.now(); const turborepoAccessTraceResult = new _turborepoaccesstrace.TurborepoAccessTraceResult(); // Export the page. let result; try { result = await exportPageSpan.traceAsyncFn(()=>(0, _turborepoaccesstrace.turborepoTraceAccess)(()=>exportPageImpl(input, fileWriter), turborepoAccessTraceResult)); // Wait for all the files to flush to disk. await fileWriter.wait(); // If there was no result, then we can exit early. if (!result) return; // If there was an error, then we can exit early. if ('error' in result) { return { error: result.error, duration: Date.now() - start }; } } catch (err) { console.error(`Error occurred prerendering page "${input.path}". Read more: https://nextjs.org/docs/messages/prerender-error`); // bailoutToCSRError errors should not leak to the user as they are not actionable; they're // a framework signal if (!(0, _bailouttocsr.isBailoutToCSRError)(err)) { // A static generation bailout error is a framework signal to fail static generation but // and will encode a reason in the error message. If there is a message, we'll print it. // Otherwise there's nothing to show as we don't want to leak an error internal error stack to the user. if ((0, _staticgenerationbailout.isStaticGenBailoutError)(err)) { if (err.message) { console.error(`Error: ${err.message}`); } } else if ((0, _iserror.default)(err) && err.stack) { console.error(err.stack); } else { console.error(err); } } return { error: true, duration: Date.now() - start }; } // Notify the parent process that we processed a page (used by the progress activity indicator) process.send == null ? void 0 : process.send.call(process, [ 3, { type: 'activity' } ]); // Otherwise we can return the result. return { duration: Date.now() - start, ampValidations: result.ampValidations, cacheControl: result.cacheControl, metadata: result.metadata, ssgNotFound: result.ssgNotFound, hasEmptyPrelude: result.hasEmptyPrelude, hasPostponed: result.hasPostponed, turborepoAccessTraceResult: turborepoAccessTraceResult.serialize(), fetchMetrics: result.fetchMetrics }; } process.on('unhandledRejection', (err)=>{ // if it's a postpone error, it'll be handled later // when the postponed promise is actually awaited. if ((0, _ispostpone.isPostpone)(err)) { return; } // we don't want to log these errors if ((0, _isdynamicusageerror.isDynamicUsageError)(err)) { return; } console.error(err); }); process.on('rejectionHandled', ()=>{ // It is ok to await a Promise late in Next.js as it allows for better // prefetching patterns to avoid waterfalls. We ignore logging these. // We should've already errored in anyway unhandledRejection. }); const FATAL_UNHANDLED_NEXT_API_EXIT_CODE = 78; process.on('uncaughtException', (err)=>{ if ((0, _isdynamicusageerror.isDynamicUsageError)(err)) { console.error('A Next.js API that uses exceptions to signal framework behavior was uncaught. This suggests improper usage of a Next.js API. The original error is printed below and the build will now exit.'); console.error(err); process.exit(FATAL_UNHANDLED_NEXT_API_EXIT_CODE); } else { console.error(err); } }); //# sourceMappingURL=worker.js.map