UNPKG

@aziontech/opennextjs-azion

Version:
205 lines (204 loc) 11.9 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. */ import fs from "node:fs"; import { readFile, writeFile } from "node:fs/promises"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { getPackagePath } from "@opennextjs/aws/build/helper.js"; import { ContentUpdater } from "@opennextjs/aws/plugins/content-updater.js"; import { build } from "esbuild"; import { patchVercelOgLibrary } from "./patches/ast/patch-vercel-og-library.js"; import { patchWebpackRuntime } from "./patches/ast/webpack-runtime.js"; import * as patches from "./patches/index.js"; import { setBundlerExternal } from "./patches/plugins/bundler-externals.js"; import { inlineDynamicRequireLoadComponents } from "./patches/plugins/dynamic-require-load-components.js"; import { inlineDynamicRequires } from "./patches/plugins/dynamic-requires.js"; import { inlineFindDir } from "./patches/plugins/find-dir.js"; import { patchInstrumentation } from "./patches/plugins/instrumentation.js"; import { inlineLoadManifest } from "./patches/plugins/load-manifest.js"; import { patchNextServer } from "./patches/plugins/next-server.js"; import { patchNodeEnvironment } from "./patches/plugins/node-environment.js"; import { handleOptionalDependencies } from "./patches/plugins/optional-deps.js"; import { patchPagesRouterContext } from "./patches/plugins/pages-router-context.js"; import { patchDepdDeprecations } from "./patches/plugins/patch-depd-deprecations.js"; import { inlinePatchRewriteInvokeHeaders, inlinePatchRewriteURLSource, } from "./patches/plugins/patch-rewrite-invoke-headers.js"; import { inlinePatchRewriteRouter } from "./patches/plugins/patch-rewrite-router.js"; import { fixRequire } from "./patches/plugins/require.js"; import { shimRequireHook } from "./patches/plugins/require-hook.js"; import { patchRouteModules } from "./patches/plugins/route-module.js"; import { needsExperimentalReact, normalizePath, patchCodeWithValidations } from "./utils/index.js"; /** The dist directory of the Azion package */ const packageDistDir = path.join(path.dirname(fileURLToPath(import.meta.url)), "../"); /** * List of optional Next.js dependencies. * They are not required for Next.js to run but only needed to enabled specific features. * When one of those dependency is required, it should be installed by the application. */ const optionalDependencies = [ "caniuse-lite", "critters", "jimp", "probe-image-size", // `server.edge` is not available in react-dom@18 "react-dom/server.edge", ]; /** * Bundle the Open Next server. */ export async function bundleServer(buildOpts) { patches.copyPackageCliFiles(packageDistDir, buildOpts); const { appPath, outputDir, monorepoRoot } = buildOpts; const baseManifestPath = path.join(outputDir, "server-functions/default", getPackagePath(buildOpts), ".next"); const serverFiles = path.join(baseManifestPath, "required-server-files.json"); const nextConfig = JSON.parse(fs.readFileSync(serverFiles, "utf-8")).config; console.log(`\x1b[35m⚙️ Bundling the OpenNext server...\n\x1b[0m`); await patchWebpackRuntime(buildOpts); patchVercelOgLibrary(buildOpts); const outputPath = path.join(outputDir, "server-functions", "default"); const packagePath = getPackagePath(buildOpts); const openNextServer = path.join(outputPath, packagePath, `index.mjs`); const openNextServerBundle = path.join(outputPath, packagePath, `handler.mjs`); const updater = new ContentUpdater(buildOpts); const result = await build({ entryPoints: [openNextServer], bundle: true, outfile: openNextServerBundle, format: "esm", target: "esnext", minify: false, metafile: true, // Next traces files using the default conditions from `nft` (`node`, `require`, `import` and `default`) // // Because we use the `node` platform for this build, the "module" condition is used when no conditions are defined. // We explicitly set the conditions to an empty array to disable the "module" condition in order to match Next tracing. // // See: // - default nft conditions: https://github.com/vercel/nft/blob/2b55b01/readme.md#exports--imports // - Next no explicit override: https://github.com/vercel/next.js/blob/2efcf11/packages/next/src/build/collect-build-traces.ts#L287 // - ESBuild `node` platform: https://esbuild.github.io/api/#platform conditions: [], plugins: [ shimRequireHook(buildOpts), setBundlerExternal(buildOpts), inlineDynamicRequireLoadComponents(updater, buildOpts), inlineDynamicRequires(updater, buildOpts), fixRequire(updater), handleOptionalDependencies(optionalDependencies), patchInstrumentation(updater, buildOpts), patchPagesRouterContext(buildOpts), inlineFindDir(updater, buildOpts), inlineLoadManifest(updater, buildOpts), patchNextServer(updater, buildOpts), patchRouteModules(updater, buildOpts), patchDepdDeprecations(updater), inlinePatchRewriteRouter(updater), inlinePatchRewriteInvokeHeaders(updater), inlinePatchRewriteURLSource(updater), patchNodeEnvironment(updater), // Apply updater updates, must be the last plugin updater.plugin, ], external: ["./middleware/handler.mjs", "async_hooks"], alias: { // Note: it looks like node-fetch is actually not necessary for us, so we could replace it with an empty shim // but just to be safe we replace it with a module that re-exports the native fetch // we do this to both save on bundle size (there isn't really any benefit in us shipping the node-fetch code) // and also get rid of a warning in the terminal caused by the package (because it performs an === comparison with -0) "next/dist/compiled/node-fetch": path.join(buildOpts.outputDir, "azion-runtime/shims/fetch.js"), // Note: we apply an empty shim to next/dist/compiled/ws because it generates two `eval`s: // eval("require")("bufferutil"); // eval("require")("utf-8-validate"); "next/dist/compiled/ws": path.join(buildOpts.outputDir, "azion-runtime/shims/empty.js"), // The toolbox optimizer pulls severals MB of dependencies (`caniuse-lite`, `terser`, `acorn`, ...) // Drop it to optimize the code size // See https://github.com/vercel/next.js/blob/6eb235c/packages/next/src/server/optimize-amp.ts "next/dist/compiled/@ampproject/toolbox-optimizer": path.join(buildOpts.outputDir, "azion-runtime/shims/throw.js"), // Note: we apply an empty shim to next/dist/compiled/edge-runtime since (amongst others) it generated the following `eval`: // eval(getModuleCode)(module, module.exports, throwingRequire, params.context, ...Object.values(params.scopedContext)); // which comes from https://github.com/vercel/edge-runtime/blob/6e96b55f/packages/primitives/src/primitives/load.js#L57-L63 "next/dist/compiled/edge-runtime": path.join(buildOpts.outputDir, "azion-runtime/shims/empty.js"), // `@next/env` is a library Next.js uses for loading dotenv files, for obvious reasons we need to stub it here // source: https://github.com/vercel/next.js/tree/0ac10d79720/packages/next-env "@next/env": path.join(buildOpts.outputDir, "azion-runtime/shims/env.js"), }, define: { // config file used by Next.js, see: https://github.com/vercel/next.js/blob/68a7128/packages/next/src/build/utils.ts#L2137-L2139 "process.env.__NEXT_PRIVATE_STANDALONE_CONFIG": JSON.stringify(JSON.stringify(nextConfig)), // Next.js tried to access __dirname so we need to define it __dirname: '""', // Note: we need the __non_webpack_require__ variable declared as it is used by next-server: // https://github.com/vercel/next.js/blob/be0c3283/packages/next/src/server/next-server.ts#L116-L119 __non_webpack_require__: "require", // We make sure that environment variables that Next.js expects are properly defined "process.env.NEXT_RUNTIME": '"nodejs"', "process.env.NODE_ENV": '"production"', // The 2 following defines are used to reduce the bundle size by removing unnecessary code // Next uses different precompiled renderers (i.e. `app-page.runtime.prod.js`) based on if you use `TURBOPACK` or some experimental React features // Turbopack is not supported for build at the moment, so we disable it "process.env.TURBOPACK": "false", // This define should be safe to use for Next 14.2+, earlier versions (13.5 and less) will cause trouble "process.env.__NEXT_EXPERIMENTAL_REACT": `${needsExperimentalReact(nextConfig)}`, // Fix `res.validate` in Next 15.4 (together with the `route-module` patch) "process.env.__NEXT_TRUST_HOST_HEADER": "true", }, banner: { js: `import {setInterval, clearInterval, setTimeout, clearTimeout, setImmediate, clearImmediate} from "node:timers"; // this temporary fix is necessary to avoid the following error on Next 15 // ReferenceError: queueMicrotask is not defined if (typeof queueMicrotask === "undefined") { globalThis.queueMicrotask = (callback) => { return Promise.resolve().then(callback); }; } `, }, platform: "node", }); fs.writeFileSync(openNextServerBundle + ".meta.json", JSON.stringify(result.metafile, null, 2)); await updateWorkerBundledCode(openNextServerBundle, buildOpts); const isMonorepo = monorepoRoot !== appPath; if (isMonorepo) { fs.writeFileSync(path.join(outputPath, "handler.mjs"), `export { handler } from "./${normalizePath(packagePath)}/handler.mjs";`); } console.log(`\x1b[35mWorker saved in \`${getOutputWorkerPath(buildOpts)}\` 🚀\n\x1b[0m`); } /** * This function applies patches required for the code to run on workers. */ export async function updateWorkerBundledCode(workerOutputFile, buildOpts) { const code = await readFile(workerOutputFile, "utf8"); const patchedCode = await patchCodeWithValidations(code, [ ["require", patches.patchRequire], [ "'require(this.middlewareManifestPath)'", (code) => patches.inlineMiddlewareManifestRequire(code, buildOpts), { isOptional: true }, ], [ "'require(this.middlewareManifestPath)'", (code) => patches.inlineMiddlewareManifestRequire(code, buildOpts), ], [ "`require.resolve` call", // workers do not support dynamic require nor require.resolve (code) => code.replace('require.resolve("./cache.cjs")', '"unused"'), ], [ "`require.resolve composable cache` call", // workers do not support dynamic require nor require.resolve (code) => code.replace('require.resolve("./composable-cache.cjs")', '"unused"'), ], ]); await writeFile(workerOutputFile, patchedCode); } /** * Gets the path of the worker.js file generated by the build process * * @param buildOpts the open-next build options * @returns the path of the worker.js file that the build process generates */ export function getOutputWorkerPath(buildOpts) { return path.join(buildOpts.outputDir, "worker.js"); }