From f879573255f7ceaad56ec05646a25682a2b32815 Mon Sep 17 00:00:00 2001 From: Victor Berchet Date: Thu, 23 Oct 2025 13:50:58 +0200 Subject: [PATCH 1/2] initial working version --- packages/cloudflare/package.json | 2 +- packages/cloudflare/src/cli/adapter.ts | 162 ++++++++++++++++++ .../src/cli/build/open-next/compile-init.ts | 4 +- .../cli/build/open-next/createServerBundle.ts | 89 +++++++--- .../src/cli/build/patches/plugins/require.ts | 3 + pnpm-lock.yaml | 11 +- 6 files changed, 239 insertions(+), 32 deletions(-) create mode 100644 packages/cloudflare/src/cli/adapter.ts diff --git a/packages/cloudflare/package.json b/packages/cloudflare/package.json index 348602303..cd932868c 100644 --- a/packages/cloudflare/package.json +++ b/packages/cloudflare/package.json @@ -53,7 +53,7 @@ "homepage": "https://github.com/opennextjs/opennextjs-cloudflare", "dependencies": { "@dotenvx/dotenvx": "catalog:", - "@opennextjs/aws": "3.8.5", + "@opennextjs/aws": "https://pkg.pr.new/@opennextjs/aws@1027", "@types/rclone.js": "^0.6.3", "cloudflare": "^4.4.1", "enquirer": "^2.4.1", diff --git a/packages/cloudflare/src/cli/adapter.ts b/packages/cloudflare/src/cli/adapter.ts new file mode 100644 index 000000000..8dff073ba --- /dev/null +++ b/packages/cloudflare/src/cli/adapter.ts @@ -0,0 +1,162 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import fs from "node:fs"; +import { createRequire } from "node:module"; +import path from "node:path"; + +import { compileCache } from "@opennextjs/aws/build/compileCache.js"; +import { compileOpenNextConfig } from "@opennextjs/aws/build/compileConfig.js"; +import { compileTagCacheProvider } from "@opennextjs/aws/build/compileTagCacheProvider.js"; +import { createCacheAssets, createStaticAssets } from "@opennextjs/aws/build/createAssets.js"; +import { createMiddleware } from "@opennextjs/aws/build/createMiddleware.js"; +import * as buildHelper from "@opennextjs/aws/build/helper.js"; +import { addDebugFile } from "@opennextjs/aws/debug.js"; +import type { ContentUpdater } from "@opennextjs/aws/plugins/content-updater.js"; +import { inlineRouteHandler } from "@opennextjs/aws/plugins/inlineRouteHandlers.js"; +import type { NextConfig } from "@opennextjs/aws/types/next-types.js"; + +import { bundleServer } from "./build/bundle-server.js"; +import { compileEnvFiles } from "./build/open-next/compile-env-files.js"; +import { compileImages } from "./build/open-next/compile-images.js"; +import { compileInit } from "./build/open-next/compile-init.js"; +import { compileSkewProtection } from "./build/open-next/compile-skew-protection.js"; +import { compileDurableObjects } from "./build/open-next/compileDurableObjects.js"; +import { createServerBundle } from "./build/open-next/createServerBundle.js"; +import { inlineLoadManifest } from "./build/patches/plugins/load-manifest.js"; + +export type NextAdapterOutputs = { + pages: any[]; + pagesApi: any[]; + appPages: any[]; + appRoutes: any[]; +}; + +export type BuildCompleteCtx = { + routes: any; + outputs: NextAdapterOutputs; + projectDir: string; + repoRoot: string; + distDir: string; + config: NextConfig; + nextVersion: string; +}; + +type NextAdapter = { + name: string; + modifyConfig: (config: NextConfig, { phase }: { phase: string }) => Promise; + onBuildComplete: (ctx: BuildCompleteCtx) => Promise; +}; //TODO: use the one provided by Next + +let buildOpts: buildHelper.BuildOptions; + +export default { + name: "OpenNext", + + async modifyConfig(nextConfig) { + // We have to precompile the cache here, probably compile OpenNext config as well + const { config, buildDir } = await compileOpenNextConfig("open-next.config.ts", { + // TODO(vicb): do we need edge compile + compileEdge: true, + }); + + const require = createRequire(import.meta.url); + const openNextDistDir = path.dirname(require.resolve("@opennextjs/aws/index.js")); + + buildOpts = buildHelper.normalizeOptions(config, openNextDistDir, buildDir); + + buildHelper.initOutputDir(buildOpts); + + const cache = compileCache(buildOpts); + + // We then have to copy the cache files to the .next dir so that they are available at runtime + // TODO: use a better path, this one is temporary just to make it work + const tempCachePath = `${buildOpts.outputDir}/server-functions/default/.open-next/.build`; + fs.mkdirSync(tempCachePath, { recursive: true }); + fs.copyFileSync(cache.cache, path.join(tempCachePath, "cache.cjs")); + fs.copyFileSync(cache.composableCache, path.join(tempCachePath, "composable-cache.cjs")); + + //TODO: We should check the version of Next here, below 16 we'd throw or show a warning + return { + ...nextConfig, + cacheHandler: cache.cache, //TODO: compute that here, + cacheMaxMemorySize: 0, + experimental: { + ...nextConfig.experimental, + trustHostHeader: true, + cacheHandlers: { + default: cache.composableCache, + }, + }, + }; + }, + + async onBuildComplete(ctx: BuildCompleteCtx) { + console.log("OpenNext build will start now"); + + const configPath = path.join(buildOpts.appBuildOutputPath, ".open-next/.build/open-next.config.edge.mjs"); + if (!fs.existsSync(configPath)) { + throw new Error("Could not find compiled Open Next config, did you run the build command?"); + } + const openNextConfig = await import(configPath).then((mod) => mod.default); + + // TODO(vicb): save outputs + addDebugFile(buildOpts, "outputs.json", ctx); + + // Cloudflare specific + compileEnvFiles(buildOpts); + /* TODO(vicb): pass the wrangler config*/ + await compileInit(buildOpts, {} as any); + await compileImages(buildOpts); + await compileSkewProtection(buildOpts, openNextConfig); + + // Compile middleware + // TODO(vicb): `forceOnlyBuildOnce` is cloudflare specific + await createMiddleware(buildOpts, { forceOnlyBuildOnce: true }); + console.log("Middleware created"); + + createStaticAssets(buildOpts); + console.log("Static assets created"); + + if (buildOpts.config.dangerous?.disableIncrementalCache !== true) { + const { useTagCache } = createCacheAssets(buildOpts); + console.log("Cache assets created"); + if (useTagCache) { + await compileTagCacheProvider(buildOpts); + console.log("Tag cache provider compiled"); + } + } + + await createServerBundle( + buildOpts, + { + additionalPlugins: getAdditionalPluginsFactory(buildOpts, ctx), + }, + ctx + ); + + await compileDurableObjects(buildOpts); + + // TODO(vicb): pass minify `projectOpts` + await bundleServer(buildOpts, { minify: false } as any); + + console.log("OpenNext build complete."); + + // TODO(vicb): not needed on cloudflare + // console.log("Server bundle created"); + // await createRevalidationBundle(buildOpts); + // console.log("Revalidation bundle created"); + // await createImageOptimizationBundle(buildOpts); + // console.log("Image optimization bundle created"); + // await createWarmerBundle(buildOpts); + // console.log("Warmer bundle created"); + // await generateOutput(buildOpts); + // console.log("Output generated"); + }, +} satisfies NextAdapter; + +function getAdditionalPluginsFactory(buildOpts: buildHelper.BuildOptions, ctx: BuildCompleteCtx) { + return (updater: ContentUpdater) => [ + inlineRouteHandler(updater, ctx.outputs), + //externalChunksPlugin(outputs), + inlineLoadManifest(updater, buildOpts), + ]; +} diff --git a/packages/cloudflare/src/cli/build/open-next/compile-init.ts b/packages/cloudflare/src/cli/build/open-next/compile-init.ts index 6ccf4ffda..f39f570e3 100644 --- a/packages/cloudflare/src/cli/build/open-next/compile-init.ts +++ b/packages/cloudflare/src/cli/build/open-next/compile-init.ts @@ -1,7 +1,6 @@ import path from "node:path"; import { fileURLToPath } from "node:url"; -import { loadConfig } from "@opennextjs/aws/adapters/config/util.js"; import type { BuildOptions } from "@opennextjs/aws/build/helper.js"; import { build } from "esbuild"; import type { Unstable_Config } from "wrangler"; @@ -14,7 +13,8 @@ export async function compileInit(options: BuildOptions, wranglerConfig: Unstabl const templatesDir = path.join(currentDir, "../../templates"); const initPath = path.join(templatesDir, "init.js"); - const nextConfig = loadConfig(path.join(options.appBuildOutputPath, ".next")); + // TODO: need the next config here + const nextConfig = { basePath: "", deploymentId: "", trailingSlash: "" }; const basePath = nextConfig.basePath ?? ""; const deploymentId = nextConfig.deploymentId ?? ""; const trailingSlash = nextConfig.trailingSlash ?? false; diff --git a/packages/cloudflare/src/cli/build/open-next/createServerBundle.ts b/packages/cloudflare/src/cli/build/open-next/createServerBundle.ts index b78aafc0a..76da6dfb8 100644 --- a/packages/cloudflare/src/cli/build/open-next/createServerBundle.ts +++ b/packages/cloudflare/src/cli/build/open-next/createServerBundle.ts @@ -2,11 +2,13 @@ // Adapted for cloudflare workers import fs from "node:fs"; +import { createRequire } from "node:module"; 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 { copyAdapterFiles } from "@opennextjs/aws/build/copyAdapterFiles.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"; @@ -16,7 +18,7 @@ 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 type { ContentUpdater } from "@opennextjs/aws/plugins/content-updater.js"; +import { ContentUpdater } from "@opennextjs/aws/plugins/content-updater.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"; @@ -25,11 +27,10 @@ import type { FunctionOptions, SplittedFunctionOptions } from "@opennextjs/aws/t import { getCrossPlatformPathRegex } from "@opennextjs/aws/utils/regex.js"; import type { Plugin } from "esbuild"; -import { getOpenNextConfig } from "../../../api/config.js"; +import type { BuildCompleteCtx } from "../../adapter.js"; import { patchResRevalidate } from "../patches/plugins/res-revalidate.js"; import { patchUseCacheIO } from "../patches/plugins/use-cache.js"; import { normalizePath } from "../utils/index.js"; -import { copyWorkerdPackages } from "../utils/workerd.js"; interface CodeCustomization { // These patches are meant to apply on user and next generated code @@ -41,7 +42,9 @@ interface CodeCustomization { export async function createServerBundle( options: buildHelper.BuildOptions, - codeCustomization?: CodeCustomization + codeCustomization?: CodeCustomization, + /* TODO(vicb): optional to be backward compatible */ + buildCtx?: BuildCompleteCtx ) { const { config } = options; const foundRoutes = new Set(); @@ -60,7 +63,7 @@ export async function createServerBundle( if (fnOptions.runtime === "edge") { await generateEdgeBundle(name, options, fnOptions); } else { - await generateBundle(name, options, fnOptions, codeCustomization); + await generateBundle(name, options, fnOptions, codeCustomization, buildCtx); } }); @@ -112,23 +115,32 @@ export async function createServerBundle( } // Generate default function - await generateBundle("default", options, { - ...defaultFn, - // @ts-expect-error - Those string are RouteTemplate - routes: Array.from(remainingRoutes), - patterns: ["*"], - }); + await generateBundle( + "default", + options, + { + ...defaultFn, + // @ts-expect-error - Those string are RouteTemplate + routes: Array.from(remainingRoutes), + patterns: ["*"], + }, + codeCustomization, + buildCtx + ); } async function generateBundle( name: string, options: buildHelper.BuildOptions, fnOptions: SplittedFunctionOptions, - codeCustomization?: CodeCustomization + codeCustomization?: CodeCustomization, + buildCtx?: BuildCompleteCtx ) { const { appPath, appBuildOutputPath, config, outputDir, monorepoRoot } = options; logger.info(`Building server function: ${name}...`); + const require = createRequire(import.meta.url); + // Create output folder const outputPath = path.join(outputDir, "server-functions", name); @@ -181,21 +193,37 @@ async function generateBundle( // Copy env files buildHelper.copyEnvFile(appBuildOutputPath, packagePath, outputPath); - // Copy all necessary traced files - const { tracedFiles, manifests, nodePackages } = await copyTracedFiles({ - buildOutputPath: appBuildOutputPath, - packagePath, - outputDir: outputPath, - routes: fnOptions.routes ?? ["app/page.tsx"], - bundledNextServer: isBundled, - }); + let tracedFiles: string[] = []; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let manifests: any = {}; - if (getOpenNextConfig(options).cloudflare?.useWorkerdCondition !== false) { - // Next does not trace the "workerd" build condition - // So we need to copy the whole packages using the condition - await copyWorkerdPackages(options, nodePackages); + // Copy all necessary traced files + if (config.dangerous?.useAdapterOutputs) { + if (!buildCtx) { + throw new Error("should not happen"); + } + tracedFiles = await copyAdapterFiles(options, name, buildCtx.outputs); + //TODO: we should load manifests here + } else { + const oldTracedFileOutput = await copyTracedFiles({ + buildOutputPath: appBuildOutputPath, + packagePath, + outputDir: outputPath, + routes: fnOptions.routes ?? ["app/page.tsx"], + bundledNextServer: isBundled, + skipServerFiles: options.config.dangerous?.useAdapterOutputs === true, + }); + tracedFiles = oldTracedFileOutput.tracedFiles; + manifests = oldTracedFileOutput.manifests; } + // TODO(vicb): what should `nodePackages` be for the adapter + // if (getOpenNextConfig(options).cloudflare?.useWorkerdCondition !== false) { + // // Next does not trace the "workerd" build condition + // // So we need to copy the whole packages using the condition + // await copyWorkerdPackages(options, nodePackages); + // } + const additionalCodePatches = codeCustomization?.additionalCodePatches ?? []; await applyCodePatches(options, tracedFiles, manifests, [ @@ -229,9 +257,16 @@ async function generateBundle( const isAfter142 = buildHelper.compareSemver(options.nextVersion, ">=", "14.2"); const isAfter152 = buildHelper.compareSemver(options.nextVersion, ">=", "15.2.0"); const isAfter154 = buildHelper.compareSemver(options.nextVersion, ">=", "15.4.0"); + const useAdapterHandler = config.dangerous?.useAdapterOutputs === true; const disableRouting = isBefore13413 || config.middleware?.external; + const updater = new ContentUpdater(options); + + const additionalPlugins = codeCustomization?.additionalPlugins + ? codeCustomization.additionalPlugins(updater) + : []; + const plugins = [ openNextReplacementPlugin({ name: `requestHandlerOverride ${name}`, @@ -242,6 +277,7 @@ async function generateBundle( ...(isAfter142 ? ["patchAsyncStorage"] : []), ...(isAfter141 ? ["appendPrefetch"] : []), ...(isAfter154 ? [] : ["setInitialURL"]), + ...(useAdapterHandler ? ["useRequestHandler"] : ["useAdapterHandler"]), ], }), openNextReplacementPlugin({ @@ -253,6 +289,8 @@ async function generateBundle( ...(isAfter141 ? ["experimentalIncrementalCacheHandler"] : ["stableIncrementalCache"]), ...(isAfter152 ? [""] : ["composableCache"]), ], + replacements: [require.resolve("@opennextjs/aws/core/util.adapter.js")], + entireFile: useAdapterHandler, }), openNextResolvePlugin({ @@ -269,6 +307,9 @@ async function generateBundle( nextDir: path.join(options.appBuildOutputPath, ".next"), isInCloudflare: true, }), + ...additionalPlugins, + // The content updater plugin must be the last plugin + updater.plugin, ]; const outfileExt = fnOptions.runtime === "deno" ? "ts" : "mjs"; diff --git a/packages/cloudflare/src/cli/build/patches/plugins/require.ts b/packages/cloudflare/src/cli/build/patches/plugins/require.ts index 9f18a2dde..a480c8ab6 100644 --- a/packages/cloudflare/src/cli/build/patches/plugins/require.ts +++ b/packages/cloudflare/src/cli/build/patches/plugins/require.ts @@ -43,6 +43,9 @@ export function fixRequire(updater: ContentUpdater): Plugin { `require("next/dist/compiled/@opentelemetry/api")` ); + // The Adapters API build adds i.e. `__require(...)` when inlining the handlers + contents = contents.replace(/__require\d?\(/g, "require(").replace(/__require\d?\./g, "require."); + return contents; }, }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 813101975..f40d8db66 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1075,8 +1075,8 @@ importers: specifier: 'catalog:' version: 1.31.0 '@opennextjs/aws': - specifier: 3.8.5 - version: 3.8.5 + specifier: https://pkg.pr.new/@opennextjs/aws@1027 + version: https://pkg.pr.new/@opennextjs/aws@1027 '@types/rclone.js': specifier: ^0.6.3 version: 0.6.3 @@ -3817,8 +3817,9 @@ packages: '@octokit/types@13.10.0': resolution: {integrity: sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==} - '@opennextjs/aws@3.8.5': - resolution: {integrity: sha512-elpMb0fJZc0a1VtymedFa7P1lYcyOmt+Pwqyacpq2C/SvbETIeBlW/Xle/vY95ICtccKUxITI8MtonrCo2+2/Q==} + '@opennextjs/aws@https://pkg.pr.new/@opennextjs/aws@1027': + resolution: {tarball: https://pkg.pr.new/@opennextjs/aws@1027} + version: 3.8.5 hasBin: true '@opentelemetry/api@1.9.0': @@ -12953,7 +12954,7 @@ snapshots: dependencies: '@octokit/openapi-types': 24.2.0 - '@opennextjs/aws@3.8.5': + '@opennextjs/aws@https://pkg.pr.new/@opennextjs/aws@1027': dependencies: '@ast-grep/napi': 0.35.0 '@aws-sdk/client-cloudfront': 3.398.0 From fa79087c10bab1c87cc8bdd6461a5591dfd6637a Mon Sep 17 00:00:00 2001 From: Victor Berchet Date: Thu, 23 Oct 2025 14:04:09 +0200 Subject: [PATCH 2/2] Apply suggestion from @vicb --- packages/cloudflare/src/cli/build/open-next/compile-init.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cloudflare/src/cli/build/open-next/compile-init.ts b/packages/cloudflare/src/cli/build/open-next/compile-init.ts index f39f570e3..bae669193 100644 --- a/packages/cloudflare/src/cli/build/open-next/compile-init.ts +++ b/packages/cloudflare/src/cli/build/open-next/compile-init.ts @@ -13,7 +13,7 @@ export async function compileInit(options: BuildOptions, wranglerConfig: Unstabl const templatesDir = path.join(currentDir, "../../templates"); const initPath = path.join(templatesDir, "init.js"); - // TODO: need the next config here + // TODO: need the wrangler config here const nextConfig = { basePath: "", deploymentId: "", trailingSlash: "" }; const basePath = nextConfig.basePath ?? ""; const deploymentId = nextConfig.deploymentId ?? "";