diff --git a/.changeset/slow-suits-deliver.md b/.changeset/slow-suits-deliver.md new file mode 100644 index 000000000..460cda00e --- /dev/null +++ b/.changeset/slow-suits-deliver.md @@ -0,0 +1,6 @@ +--- +'@cloudflare/next-on-pages': minor +--- + +utilize Wrangler new capability of dynamically importing code to avoid the evaluation/run of javascript code +when not necessary, reducing the app's startup time (which causes apps to often hit the script startup CPU time limit) diff --git a/package.json b/package.json index 3cefee263..9c98e9d73 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,8 @@ "zodcli": "^0.0.4" }, "peerDependencies": { - "vercel": "^28.0.2" + "vercel": "^28.0.2", + "wrangler": "^2.20.0" }, "devDependencies": { "@changesets/cli": "^2.26.0", diff --git a/src/buildApplication/buildWorkerFile.ts b/src/buildApplication/buildWorkerFile.ts index 2f84ad8c8..370be601e 100644 --- a/src/buildApplication/buildWorkerFile.ts +++ b/src/buildApplication/buildWorkerFile.ts @@ -1,6 +1,6 @@ import { writeFile } from 'fs/promises'; -import { join } from 'path'; import type { Plugin } from 'esbuild'; +import { join } from 'path'; import { build } from 'esbuild'; import { tmpdir } from 'os'; import { cliSuccess } from '../cli'; @@ -14,17 +14,24 @@ import type { ProcessedVercelOutput } from './processVercelOutput'; * @returns Record for the build output map. */ export function constructBuildOutputRecord(item: BuildOutputItem) { - return item.type === 'static' - ? `{ type: ${JSON.stringify(item.type)} }` - : item.type === 'override' - ? `{ + if (item.type === 'static') { + return `{ type: ${JSON.stringify(item.type)} }`; + } + + if (item.type === 'override') { + return `{ type: ${JSON.stringify(item.type)}, path: ${item.path ? JSON.stringify(item.path) : undefined}, headers: ${item.headers ? JSON.stringify(item.headers) : undefined} - }` - : `{ + }`; + } + + return `{ type: ${JSON.stringify(item.type)}, - entrypoint: AsyncLocalStoragePromise.then(() => import('${item.entrypoint}')) + entrypoint: AsyncLocalStoragePromise.then(() => import('${item.entrypoint.replace( + /^\.vercel\/output\/static\/_worker\.js\/__next-on-pages-dist__\//, + './__next-on-pages-dist__/' + )}')), }`; } @@ -49,7 +56,13 @@ export async function buildWorkerFile( .join(',')}};` ); - const outputFile = join('.vercel', 'output', 'static', '_worker.js'); + const outputFile = join( + '.vercel', + 'output', + 'static', + '_worker.js', + 'index.js' + ); await build({ entryPoints: [join(__dirname, '..', 'templates', '_worker.js')], @@ -60,7 +73,7 @@ export async function buildWorkerFile( inject: [functionsFile], target: 'es2022', platform: 'neutral', - external: ['node:async_hooks', 'node:buffer'], + external: ['node:async_hooks', 'node:buffer', './__next-on-pages-dist__/*'], define: { __CONFIG__: JSON.stringify(vercelConfig), }, diff --git a/src/buildApplication/generateFunctionsMap.ts b/src/buildApplication/generateFunctionsMap.ts index 62e472c1e..89f952364 100644 --- a/src/buildApplication/generateFunctionsMap.ts +++ b/src/buildApplication/generateFunctionsMap.ts @@ -13,7 +13,6 @@ import { } from '../utils'; import type { CliOptions } from '../cli'; import { cliError, cliWarn } from '../cli'; -import { tmpdir } from 'os'; import type * as AST from 'ast-types/gen/kinds'; import assert from 'node:assert'; import type { PrerenderedFileData } from './fixPrerenderedRoutes'; @@ -35,10 +34,18 @@ export async function generateFunctionsMap( functionsDir: string, disableChunksDedup: CliOptions['disableChunksDedup'] ): Promise { + const nextOnPagesDistDir = join( + '.vercel', + 'output', + 'static', + '_worker.js', + '__next-on-pages-dist__' + ); + const processingSetup = { functionsDir, - tmpFunctionsDir: join(tmpdir(), Math.random().toString(36).slice(2)), - tmpWebpackDir: join(tmpdir(), Math.random().toString(36).slice(2)), + distFunctionsDir: join(nextOnPagesDistDir, 'functions'), + distWebpackDir: join(nextOnPagesDistDir, 'chunks'), disableChunksDedup, }; @@ -52,7 +59,7 @@ export async function generateFunctionsMap( if (!disableChunksDedup) { await buildWebpackChunkFiles( processingResults.webpackChunks, - processingSetup.tmpWebpackDir + processingSetup.distWebpackDir ); } @@ -217,14 +224,14 @@ async function processFuncDirectory( if (!setup.disableChunksDedup) { const { updatedFunctionContents, extractedWebpackChunks } = - extractWebpackChunks(setup.tmpWebpackDir, contents, webpackChunks); + extractWebpackChunks(contents, functionFile, webpackChunks); contents = updatedFunctionContents; extractedWebpackChunks.forEach((value, key) => webpackChunks.set(key, value) ); } - const newFilePath = join(setup.tmpFunctionsDir, `${relativePath}.js`); + const newFilePath = join(setup.distFunctionsDir, `${relativePath}.js`); await mkdir(dirname(newFilePath), { recursive: true }); await writeFile(newFilePath, contents); @@ -282,16 +289,20 @@ function fixFunctionContents(contents: string) { * those chunks */ function extractWebpackChunks( - tmpWebpackDir: string, functionContents: string, + filePath: string, existingWebpackChunks: Map ): { updatedFunctionContents: string; extractedWebpackChunks: Map; } { + const getChunkImport = getChunkImportFn(filePath); + const webpackChunks = new Map(); const webpackChunksCodeReplaceMap = new Map(); + const webpackChunksImports: string[] = []; + const parsedContents = parse(functionContents, { ecmaVersion: 'latest', sourceType: 'module', @@ -323,15 +334,11 @@ function extractWebpackChunks( webpackChunks.set(key, chunkExpressionCode); - const chunkFilePath = join(tmpWebpackDir, `${key}.js`); - - const newChunkExpressionCode = `require(${JSON.stringify( - chunkFilePath - )}).default`; + webpackChunksImports.push(getChunkImport(key)); webpackChunksCodeReplaceMap.set( chunkExpressionCode, - newChunkExpressionCode + getChunkIdentifier(key) ); }); @@ -340,7 +347,9 @@ function extractWebpackChunks( }); return { - updatedFunctionContents: functionContents, + updatedFunctionContents: [...webpackChunksImports, functionContents].join( + ';\n' + ), extractedWebpackChunks: webpackChunks, }; } @@ -398,8 +407,8 @@ async function tryToFixFaviconFunc(): Promise { type ProcessingSetup = { functionsDir: string; - tmpFunctionsDir: string; - tmpWebpackDir: string; + distFunctionsDir: string; + distWebpackDir: string; disableChunksDedup: boolean; }; @@ -478,3 +487,37 @@ function assertSelfWebpackChunk_N_E(expression: AST.NodeKind): void { assert(expression.property.type === 'Identifier'); assert(expression.property.name === 'webpackChunk_N_E'); } + +function getChunkIdentifier(chunkKey: number): string { + return `__chunk_${chunkKey}`; +} + +function getChunkImportFn(functionPath: string): (chunkKey: number) => string { + const functionNestingLevel = getFunctionNestingLevel(functionPath); + const accountForNestingPath = `../`.repeat(functionNestingLevel); + return chunkKey => { + const chunkIdentifier = getChunkIdentifier(chunkKey); + const chunkPath = `${accountForNestingPath}__next-on-pages-dist__/chunks/${chunkKey}.js`; + return `import ${chunkIdentifier} from '${chunkPath}'`; + }; +} + +const functionsDir = resolve('.vercel', 'output', 'functions'); + +function getFunctionNestingLevel(functionPath: string): number { + let nestingLevel = -1; + try { + const relativePath = relative(functionPath, functionsDir); + nestingLevel = relativePath.split('..').length - 1; + } catch { + /* empty */ + } + + if (nestingLevel < 0) { + throw new Error( + `Error: could not determine nesting level of the following function: ${functionPath}` + ); + } + + return nestingLevel; +}