UNPKG

next

Version:

The React Framework

370 lines (369 loc) • 20 kB
import path from 'path'; import fs from 'fs/promises'; import { promisify } from 'util'; import { pathToFileURL } from 'url'; import * as Log from '../output/log'; import globOriginal from 'next/dist/compiled/glob'; import { interopDefault } from '../../lib/interop-default'; import { isMiddlewareFilename } from '../utils'; import { normalizePagePath } from '../../shared/lib/page-path/normalize-page-path'; import { normalizeAppPath } from '../../shared/lib/router/utils/app-paths'; import { AdapterOutputType } from '../../shared/lib/constants'; import { RenderingMode } from '../rendering-mode'; import { isDynamicRoute } from '../../shared/lib/router/utils'; const glob = promisify(globOriginal); export async function handleBuildComplete({ // dir, distDir, tracingRoot, adapterPath, pageKeys, appPageKeys, hasNodeMiddleware, hasInstrumentationHook, requiredServerFiles, routesManifest, prerenderManifest, middlewareManifest, functionsConfigManifest, hasStatic404 }) { const adapterMod = interopDefault(await import(pathToFileURL(require.resolve(adapterPath)).href)); if (typeof adapterMod.onBuildComplete === 'function') { Log.info(`Running onBuildComplete from ${adapterMod.name}`); try { const outputs = []; const staticFiles = await glob('**/*', { cwd: path.join(distDir, 'static') }); for (const file of staticFiles){ const pathname = path.posix.join('/_next/static', file); const filePath = path.join(distDir, 'static', file); outputs.push({ type: AdapterOutputType.STATIC_FILE, id: path.join('static', file), pathname, filePath }); } const sharedNodeAssets = {}; for (const file of requiredServerFiles){ // add to shared node assets const filePath = path.join(distDir, file); const fileOutputPath = path.relative(tracingRoot, filePath); sharedNodeAssets[fileOutputPath] = filePath; } if (hasInstrumentationHook) { const assets = await handleTraceFiles(path.join(distDir, 'server', 'instrumentation.js.nft.json')); const fileOutputPath = path.relative(tracingRoot, path.join(distDir, 'server', 'instrumentation.js')); sharedNodeAssets[fileOutputPath] = path.join(distDir, 'server', 'instrumentation.js'); Object.assign(sharedNodeAssets, assets); } async function handleTraceFiles(traceFilePath) { const assets = Object.assign({}, sharedNodeAssets); const traceData = JSON.parse(await fs.readFile(traceFilePath, 'utf8')); const traceFileDir = path.dirname(traceFilePath); for (const relativeFile of traceData.files){ const tracedFilePath = path.join(traceFileDir, relativeFile); const fileOutputPath = path.relative(tracingRoot, tracedFilePath); assets[fileOutputPath] = tracedFilePath; } return assets; } async function handleEdgeFunction(page, isMiddleware = false) { let type = AdapterOutputType.PAGES; const isAppPrefix = page.page.startsWith('app/'); const isAppPage = isAppPrefix && page.page.endsWith('/page'); const isAppRoute = isAppPrefix && page.page.endsWith('/route'); if (isMiddleware) { type = AdapterOutputType.MIDDLEWARE; } else if (isAppPage) { type = AdapterOutputType.APP_PAGE; } else if (isAppRoute) { type = AdapterOutputType.APP_ROUTE; } else if (page.page.startsWith('/api')) { type = AdapterOutputType.PAGES_API; } const output = { id: page.name, runtime: 'edge', pathname: isAppPrefix ? normalizeAppPath(page.name) : page.name, filePath: path.join(distDir, 'server', page.files.find((item)=>item.startsWith('server/app') || item.startsWith('server/pages')) || ''), assets: {}, type, config: type === AdapterOutputType.MIDDLEWARE ? { matchers: page.matchers } : {} }; function handleFile(file) { const originalPath = path.join(distDir, file); const fileOutputPath = path.join(path.relative(tracingRoot, distDir), file); if (!output.assets) { output.assets = {}; } output.assets[fileOutputPath] = originalPath; } for (const file of page.files){ handleFile(file); } for (const item of [ ...page.wasm || [], ...page.assets || [] ]){ handleFile(item.filePath); } outputs.push(output); } const edgeFunctionHandlers = []; for (const middleware of Object.values(middlewareManifest.middleware)){ if (isMiddlewareFilename(middleware.name)) { edgeFunctionHandlers.push(handleEdgeFunction(middleware, true)); } } for (const page of Object.values(middlewareManifest.functions)){ edgeFunctionHandlers.push(handleEdgeFunction(page)); } const pagesDistDir = path.join(distDir, 'server', 'pages'); const pageOutputMap = {}; for (const page of pageKeys){ if (middlewareManifest.functions.hasOwnProperty(page)) { continue; } const route = normalizePagePath(page); const pageFile = path.join(pagesDistDir, `${normalizePagePath(page)}.js`); const pageTraceFile = `${pageFile}.nft.json`; const assets = await handleTraceFiles(pageTraceFile).catch((err)=>{ if (err.code !== 'ENOENT' || page !== '/404' && page !== '/500') { Log.warn(`Failed to locate traced assets for ${pageFile}`, err); } return {}; }); const functionConfig = functionsConfigManifest.functions[route] || {}; const output = { id: route, type: page.startsWith('/api') ? AdapterOutputType.PAGES_API : AdapterOutputType.PAGES, filePath: pageTraceFile.replace(/\.nft\.json$/, ''), pathname: route, assets, runtime: 'nodejs', config: { maxDuration: functionConfig.maxDuration, preferredRegion: functionConfig.regions } }; pageOutputMap[page] = output; outputs.push(output); } if (hasNodeMiddleware) { const middlewareFile = path.join(distDir, 'server', 'middleware.js'); const middlewareTrace = `${middlewareFile}.nft.json`; const assets = await handleTraceFiles(middlewareTrace); const functionConfig = functionsConfigManifest.functions['/_middleware'] || {}; outputs.push({ pathname: '/_middleware', id: '/_middleware', assets, type: AdapterOutputType.MIDDLEWARE, runtime: 'nodejs', filePath: middlewareFile, config: { matchers: functionConfig.matchers } }); } const appOutputMap = {}; const appDistDir = path.join(distDir, 'server', 'app'); if (appPageKeys) { for (const page of appPageKeys){ if (middlewareManifest.functions.hasOwnProperty(page)) { continue; } const normalizedPage = normalizeAppPath(page); const pageFile = path.join(appDistDir, `${page}.js`); const pageTraceFile = `${pageFile}.nft.json`; const assets = await handleTraceFiles(pageTraceFile).catch((err)=>{ Log.warn(`Failed to copy traced files for ${pageFile}`, err); return {}; }); const functionConfig = functionsConfigManifest.functions[normalizedPage] || {}; const output = { pathname: normalizedPage, id: normalizedPage, assets, type: page.endsWith('/route') ? AdapterOutputType.APP_ROUTE : AdapterOutputType.APP_PAGE, runtime: 'nodejs', filePath: pageFile, config: { maxDuration: functionConfig.maxDuration, preferredRegion: functionConfig.regions } }; appOutputMap[normalizedPage] = output; outputs.push(output); } } const getParentOutput = (srcRoute, childRoute, allowMissing)=>{ const parentOutput = pageOutputMap[srcRoute] || appOutputMap[srcRoute]; if (!parentOutput && !allowMissing) { console.error({ appOutputs: Object.keys(appOutputMap), pageOutputs: Object.keys(pageOutputMap) }); throw Object.defineProperty(new Error(`Invariant: failed to find source route ${srcRoute} for prerender ${childRoute}`), "__NEXT_ERROR_CODE", { value: "E777", enumerable: false, configurable: true }); } return parentOutput; }; const { prefetchSegmentDirSuffix, prefetchSegmentSuffix, varyHeader, didPostponeHeader, contentTypeHeader } = routesManifest.rsc; const handleAppMeta = async (route, initialOutput)=>{ const meta = JSON.parse(await fs.readFile(path.join(appDistDir, `${route}.meta`), 'utf8').catch(()=>'{}')); if (meta.postponed && initialOutput.config) { initialOutput.config.postponed = meta.postponed; } if (meta == null ? void 0 : meta.segmentPaths) { const segmentsDir = path.join(appDistDir, `${route}${prefetchSegmentDirSuffix}`); for (const segmentPath of meta.segmentPaths){ var _initialOutput_fallback, _initialOutput_fallback1, _initialOutput_fallback2; const outputSegmentPath = path.join(appDistDir, route + prefetchSegmentDirSuffix, segmentPath) + prefetchSegmentSuffix; const fallbackPathname = path.join(segmentsDir, segmentPath + prefetchSegmentSuffix); outputs.push({ id: outputSegmentPath, pathname: outputSegmentPath, type: AdapterOutputType.PRERENDER, parentOutputId: initialOutput.parentOutputId, config: { ...initialOutput.config }, fallback: { filePath: fallbackPathname, initialExpiration: (_initialOutput_fallback = initialOutput.fallback) == null ? void 0 : _initialOutput_fallback.initialExpiration, initialRevalidate: (_initialOutput_fallback1 = initialOutput.fallback) == null ? void 0 : _initialOutput_fallback1.initialRevalidate, initialHeaders: { ...(_initialOutput_fallback2 = initialOutput.fallback) == null ? void 0 : _initialOutput_fallback2.initialHeaders, vary: varyHeader, 'content-type': contentTypeHeader, [didPostponeHeader]: '2' } } }); } } }; for(const route in prerenderManifest.routes){ var _routesManifest_dynamicRoutes_find; const { initialExpireSeconds: initialExpiration, initialRevalidateSeconds: initialRevalidate, initialHeaders, initialStatus, prefetchDataRoute, dataRoute, renderingMode, allowHeader, experimentalBypassFor } = prerenderManifest.routes[route]; const srcRoute = prerenderManifest.routes[route].srcRoute || route; const isAppPage = Boolean(appOutputMap[srcRoute]) || srcRoute === '/_not-found'; const isNotFoundTrue = prerenderManifest.notFoundRoutes.includes(route); let allowQuery; const routeKeys = (_routesManifest_dynamicRoutes_find = routesManifest.dynamicRoutes.find((item)=>item.page === srcRoute)) == null ? void 0 : _routesManifest_dynamicRoutes_find.routeKeys; if (!isDynamicRoute(srcRoute)) { // for non-dynamic routes we use an empty array since // no query values bust the cache for non-dynamic prerenders // prerendered paths also do not pass allowQuery as they match // during handle: 'filesystem' so should not cache differently // by query values allowQuery = []; } else if (routeKeys) { // if we have routeKeys in the routes-manifest we use those // for allowQuery for dynamic routes allowQuery = Object.values(routeKeys); } let filePath = path.join(isAppPage ? appDistDir : pagesDistDir, `${route}.${isAppPage && !dataRoute ? 'body' : 'html'}`); // we use the static 404 for notFound: true if available // if not we do a blocking invoke on first request if (isNotFoundTrue && hasStatic404) { filePath = path.join(pagesDistDir, '404.html'); } const initialOutput = { id: route, type: AdapterOutputType.PRERENDER, pathname: route, parentOutputId: srcRoute === '/_not-found' ? srcRoute : getParentOutput(srcRoute, route).id, fallback: !isNotFoundTrue || isNotFoundTrue && hasStatic404 ? { filePath, initialStatus, initialHeaders: { ...initialHeaders, vary: varyHeader, 'content-type': contentTypeHeader }, initialExpiration, initialRevalidate: initialRevalidate || 1 } : undefined, config: { allowQuery, allowHeader, renderingMode, bypassFor: experimentalBypassFor, bypassToken: prerenderManifest.preview.previewModeId } }; outputs.push(initialOutput); if (dataRoute) { let dataFilePath = path.join(pagesDistDir, `${route}.json`); if (isAppPage) { // When experimental PPR is enabled, we expect that the data // that should be served as a part of the prerender should // be from the prefetch data route. If this isn't enabled // for ppr, the only way to get the data is from the data // route. dataFilePath = path.join(appDistDir, prefetchDataRoute && renderingMode === RenderingMode.PARTIALLY_STATIC ? prefetchDataRoute : dataRoute); } outputs.push({ ...initialOutput, id: dataRoute, pathname: dataRoute, fallback: isNotFoundTrue ? undefined : { ...initialOutput.fallback, filePath: dataFilePath } }); } if (isAppPage) { await handleAppMeta(route, initialOutput); } } for(const dynamicRoute in prerenderManifest.dynamicRoutes){ var _routesManifest_dynamicRoutes_find1; const { fallback, fallbackExpire, fallbackRevalidate, fallbackHeaders, fallbackStatus, allowHeader, dataRoute, renderingMode, experimentalBypassFor } = prerenderManifest.dynamicRoutes[dynamicRoute]; const isAppPage = Boolean(appOutputMap[dynamicRoute]); const allowQuery = Object.values(((_routesManifest_dynamicRoutes_find1 = routesManifest.dynamicRoutes.find((item)=>item.page === dynamicRoute)) == null ? void 0 : _routesManifest_dynamicRoutes_find1.routeKeys) || {}); const initialOutput = { id: dynamicRoute, type: AdapterOutputType.PRERENDER, pathname: dynamicRoute, parentOutputId: getParentOutput(dynamicRoute, dynamicRoute).id, config: { allowQuery, allowHeader, renderingMode, bypassFor: experimentalBypassFor, bypassToken: prerenderManifest.preview.previewModeId }, fallback: typeof fallback === 'string' ? { filePath: path.join(isAppPage ? appDistDir : pagesDistDir, fallback), initialStatus: fallbackStatus, initialHeaders: fallbackHeaders, initialExpiration: fallbackExpire, initialRevalidate: fallbackRevalidate || 1 } : undefined }; outputs.push(initialOutput); if (isAppPage) { await handleAppMeta(dynamicRoute, initialOutput); } if (dataRoute) { outputs.push({ ...initialOutput, id: dataRoute, pathname: dataRoute, fallback: undefined }); } } await adapterMod.onBuildComplete({ routes: { dynamicRoutes: routesManifest.dynamicRoutes, rewrites: routesManifest.rewrites, redirects: routesManifest.redirects, headers: routesManifest.headers }, outputs }); } catch (err) { Log.error(`Failed to run onBuildComplete from ${adapterMod.name}`); throw err; } } } //# sourceMappingURL=build-complete.js.map