UNPKG

@aziontech/opennextjs-azion

Version:
268 lines (267 loc) 13 kB
/** * This code was originally copied and modified from the @opennextjs/cloudflare repository. * Significant changes have been made to adapt it for use with Azion. */ // Copy-Edit of @opennextjs/aws packages/open-next/src/build/createServerBundle.ts import fs from "node:fs"; import path from "node:path"; import { loadMiddlewareManifest } from "@opennextjs/aws/adapters/config/util.js"; import { bundleNextServer } from "@opennextjs/aws/build/bundleNextServer.js"; import { compileCache } from "@opennextjs/aws/build/compileCache.js"; import { copyTracedFiles } from "@opennextjs/aws/build/copyTracedFiles.js"; import { copyMiddlewareResources, generateEdgeBundle } from "@opennextjs/aws/build/edge/createEdgeBundle.js"; import * as buildHelper from "@opennextjs/aws/build/helper.js"; import { installDependencies } from "@opennextjs/aws/build/installDeps.js"; import { applyCodePatches } from "@opennextjs/aws/build/patch/codePatcher.js"; import * as awsPatches from "@opennextjs/aws/build/patch/patches/index.js"; import logger from "@opennextjs/aws/logger.js"; import { minifyAll } from "@opennextjs/aws/minimize-js.js"; import { openNextEdgePlugins } from "@opennextjs/aws/plugins/edge.js"; import { openNextExternalMiddlewarePlugin } from "@opennextjs/aws/plugins/externalMiddleware.js"; import { openNextReplacementPlugin } from "@opennextjs/aws/plugins/replacement.js"; import { openNextResolvePlugin } from "@opennextjs/aws/plugins/resolve.js"; import { getCrossPlatformPathRegex } from "@opennextjs/aws/utils/regex.js"; import { patchFetchCacheSetMissingWaitUntil } from "../patches/plugins/patch-fetch-cacheset-missing-waituntil.js"; import { patchFetchCacheForISR, patchUnstableCacheForISR } from "../patches/plugins/path-fetch-cache-isr.js"; import { patchResRevalidate } from "../patches/plugins/res-revalidate.js"; import { patchUseCacheIO } from "../patches/plugins/use-cache.js"; import { normalizePath } from "../utils/index.js"; export async function createServerBundle(options, codeCustomization) { const { config } = options; const foundRoutes = new Set(); // Get all functions to build const defaultFn = config.default; const functions = Object.entries(config.functions ?? {}); // Recompile cache.ts as ESM if any function is using Deno runtime if (defaultFn.runtime === "deno" || functions.some(([, fn]) => fn.runtime === "deno")) { compileCache(options, "esm"); } const promises = functions.map(async ([name, fnOptions]) => { const routes = fnOptions.routes; routes.forEach((route) => foundRoutes.add(route)); if (fnOptions.runtime === "edge") { await generateEdgeBundle(name, options, fnOptions); } else { await generateBundle(name, options, fnOptions, codeCustomization); } }); //TODO: throw an error if not all edge runtime routes has been bundled in a separate function // We build every other function than default before so we know which route there is left await Promise.all(promises); const remainingRoutes = new Set(); const { appBuildOutputPath } = options; // Find remaining routes const serverPath = path.join(appBuildOutputPath, ".next/standalone", buildHelper.getPackagePath(options), ".next/server"); // Find app dir routes if (fs.existsSync(path.join(serverPath, "app"))) { const appPath = path.join(serverPath, "app"); buildHelper.traverseFiles(appPath, ({ relativePath }) => relativePath.endsWith("page.js") || relativePath.endsWith("route.js"), ({ relativePath }) => { const route = `app/${relativePath.replace(/\.js$/, "")}`; if (!foundRoutes.has(route)) { remainingRoutes.add(route); } }); } // Find pages dir routes if (fs.existsSync(path.join(serverPath, "pages"))) { const pagePath = path.join(serverPath, "pages"); buildHelper.traverseFiles(pagePath, ({ relativePath }) => relativePath.endsWith(".js"), ({ relativePath }) => { const route = `pages/${relativePath.replace(/\.js$/, "")}`; if (!foundRoutes.has(route)) { remainingRoutes.add(route); } }); } // Generate default function await generateBundle("default", options, { ...defaultFn, // @ts-expect-error - Those string are RouteTemplate routes: Array.from(remainingRoutes), patterns: ["*"], }); } async function generateBundle(name, options, fnOptions, codeCustomization) { const { appPath, appBuildOutputPath, config, outputDir, monorepoRoot } = options; logger.info(`Building server function: ${name}...`); // Create output folder const outputPath = path.join(outputDir, "server-functions", name); // Resolve path to the Next.js app if inside the monorepo // note: if user's app is inside a monorepo, standalone mode places // `node_modules` inside `.next/standalone`, and others inside // `.next/standalone/package/path` (ie. `.next`, `server.js`). // We need to output the handler file inside the package path. const packagePath = buildHelper.getPackagePath(options); const outPackagePath = path.join(outputPath, packagePath); fs.mkdirSync(outPackagePath, { recursive: true }); const ext = fnOptions.runtime === "deno" ? "mjs" : "cjs"; // Normal cache fs.copyFileSync(path.join(options.buildDir, `cache.${ext}`), path.join(outPackagePath, "cache.cjs")); // Composable cache fs.copyFileSync(path.join(options.buildDir, `composable-cache.${ext}`), path.join(outPackagePath, "composable-cache.cjs")); if (fnOptions.runtime === "deno") { addDenoJson(outputPath, packagePath); } // Bundle next server if necessary const isBundled = fnOptions.experimentalBundledNextServer ?? false; if (isBundled) { await bundleNextServer(outPackagePath, appPath, { minify: options.minify, }); } // Copy middleware if (!config.middleware?.external) { fs.copyFileSync(path.join(options.buildDir, "middleware.mjs"), path.join(outPackagePath, "middleware.mjs")); const middlewareManifest = loadMiddlewareManifest(path.join(options.appBuildOutputPath, ".next")); copyMiddlewareResources(options, middlewareManifest.middleware["/"], outPackagePath); } // Copy open-next.config.mjs buildHelper.copyOpenNextConfig(options.buildDir, outPackagePath, true); // Copy env files buildHelper.copyEnvFile(appBuildOutputPath, packagePath, outputPath); // Copy all necessary traced files const { tracedFiles, manifests } = await copyTracedFiles({ buildOutputPath: appBuildOutputPath, packagePath, outputDir: outputPath, routes: fnOptions.routes ?? ["app/page.tsx"], bundledNextServer: isBundled, }); const additionalCodePatches = codeCustomization?.additionalCodePatches ?? []; await applyCodePatches(options, tracedFiles, manifests, [ // OpenNext specific patches awsPatches.patchUseCacheForISR, awsPatches.patchNextServer, awsPatches.patchEnvVars, awsPatches.patchBackgroundRevalidation, // Azion specific patches patchFetchCacheSetMissingWaitUntil, patchFetchCacheForISR, patchUnstableCacheForISR, patchResRevalidate, patchUseCacheIO, ...additionalCodePatches, ]); // Build Lambda code // note: bundle in OpenNext package b/c the adapter relies on the // "serverless-http" package which is not a dependency in user's // Next.js app. const disableNextPrebundledReact = buildHelper.compareSemver(options.nextVersion, ">=", "13.5.1") || buildHelper.compareSemver(options.nextVersion, "<=", "13.4.1"); const overrides = fnOptions.override ?? {}; const isBefore13413 = buildHelper.compareSemver(options.nextVersion, "<=", "13.4.13"); const isAfter141 = buildHelper.compareSemver(options.nextVersion, ">=", "14.1"); // const isAfter142 = buildHelper.compareSemver(options.nextVersion, ">=", "14.2"); const isAfter1350 = buildHelper.compareSemver(options.nextVersion, ">=", "13.5.0"); // console.log(`isAfter1350: ${isAfter1350}`); const isAfter152 = buildHelper.compareSemver(options.nextVersion, ">=", "15.2.0"); const isAfter154 = buildHelper.compareSemver(options.nextVersion, ">=", "15.4.0"); const disableRouting = isBefore13413 || config.middleware?.external; const plugins = [ openNextReplacementPlugin({ name: `requestHandlerOverride ${name}`, target: getCrossPlatformPathRegex("core/requestHandler.js"), deletes: [ ...(disableNextPrebundledReact ? ["applyNextjsPrebundledReact"] : []), ...(disableRouting ? ["withRouting"] : []), ...(isAfter1350 ? ["patchAsyncStorage"] : []), ...(isAfter141 ? ["appendPrefetch"] : []), ...(isAfter154 ? [] : ["setInitialURL"]), ], }), openNextReplacementPlugin({ name: `utilOverride ${name}`, target: getCrossPlatformPathRegex("core/util.js"), deletes: [ ...(disableNextPrebundledReact ? ["requireHooks"] : []), ...(isBefore13413 ? ["trustHostHeader"] : ["requestHandlerHost"]), ...(isAfter141 ? ["experimentalIncrementalCacheHandler"] : ["stableIncrementalCache"]), ...(isAfter152 ? [""] : ["composableCache"]), ], }), openNextResolvePlugin({ fnName: name, overrides, }), // `openNextExternalMiddlewarePlugin` should only be used with an external middleware ...(config.middleware?.external ? [openNextExternalMiddlewarePlugin(path.join(options.openNextDistDir, "core/edgeFunctionHandler.js"))] : []), openNextEdgePlugins({ nextDir: path.join(options.appBuildOutputPath, ".next"), isInCloudflare: true, }), ]; const outfileExt = fnOptions.runtime === "deno" ? "ts" : "mjs"; await buildHelper.esbuildAsync({ entryPoints: [path.join(options.openNextDistDir, "adapters", "server-adapter.js")], outfile: path.join(outputPath, packagePath, `index.${outfileExt}`), external: ["./middleware.mjs"], banner: { js: [ `globalThis.monorepoPackagePath = "${normalizePath(packagePath)}";`, name === "default" ? "" : `globalThis.fnName = "${name}"; `, ].join(""), }, plugins, alias: { ...(isBundled ? { "next/dist/server/next-server.js": "./next-server.runtime.prod.js", } : {}), }, }, options); const isMonorepo = monorepoRoot !== appPath; if (isMonorepo) { addMonorepoEntrypoint(outputPath, packagePath); } installDependencies(outputPath, fnOptions.install); if (fnOptions.minify) { await minifyServerBundle(outputPath); } const shouldGenerateDocker = shouldGenerateDockerfile(fnOptions); if (shouldGenerateDocker) { fs.writeFileSync(path.join(outputPath, "Dockerfile"), typeof shouldGenerateDocker === "string" ? shouldGenerateDocker : ` FROM node:18-alpine WORKDIR /app COPY . /app EXPOSE 3000 CMD ["node", "index.mjs"] `); } } function shouldGenerateDockerfile(options) { return options.override?.generateDockerfile ?? false; } // Add deno.json file to enable "bring your own node_modules" mode. // TODO: this won't be necessary in Deno 2. See https://github.com/denoland/deno/issues/23151 function addDenoJson(outputPath, packagePath) { const config = { // Enable "bring your own node_modules" mode // and allow `__proto__` unstable: ["byonm", "fs", "unsafe-proto"], }; fs.writeFileSync(path.join(outputPath, packagePath, "deno.json"), JSON.stringify(config, null, 2)); } //TODO: check if this PR is still necessary https://github.com/opennextjs/opennextjs-aws/pull/341 function addMonorepoEntrypoint(outputPath, packagePath) { // Note: in the monorepo case, the handler file is output to // `.next/standalone/package/path/index.mjs`, but we want // the Lambda function to be able to find the handler at // the root of the bundle. We will create a dummy `index.mjs` // that re-exports the real handler. fs.writeFileSync(path.join(outputPath, "index.mjs"), `export { handler } from "./${normalizePath(packagePath)}/index.mjs";`); } async function minifyServerBundle(outputDir) { logger.info("Minimizing server function..."); await minifyAll(outputDir, { compress_json: true, mangle: true, }); }