Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/cloudflare/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
162 changes: 162 additions & 0 deletions packages/cloudflare/src/cli/adapter.ts
Original file line number Diff line number Diff line change
@@ -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<NextConfig>;
onBuildComplete: (ctx: BuildCompleteCtx) => Promise<void>;
}; //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),
];
}
4 changes: 2 additions & 2 deletions packages/cloudflare/src/cli/build/open-next/compile-init.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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 wrangler config here
const nextConfig = { basePath: "", deploymentId: "", trailingSlash: "" };
const basePath = nextConfig.basePath ?? "";
const deploymentId = nextConfig.deploymentId ?? "";
const trailingSlash = nextConfig.trailingSlash ?? false;
Expand Down
89 changes: 65 additions & 24 deletions packages/cloudflare/src/cli/build/open-next/createServerBundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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";
Expand All @@ -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
Expand All @@ -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<string>();
Expand All @@ -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);
}
});

Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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, [
Expand Down Expand Up @@ -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}`,
Expand All @@ -242,6 +277,7 @@ async function generateBundle(
...(isAfter142 ? ["patchAsyncStorage"] : []),
...(isAfter141 ? ["appendPrefetch"] : []),
...(isAfter154 ? [] : ["setInitialURL"]),
...(useAdapterHandler ? ["useRequestHandler"] : ["useAdapterHandler"]),
],
}),
openNextReplacementPlugin({
Expand All @@ -253,6 +289,8 @@ async function generateBundle(
...(isAfter141 ? ["experimentalIncrementalCacheHandler"] : ["stableIncrementalCache"]),
...(isAfter152 ? [""] : ["composableCache"]),
],
replacements: [require.resolve("@opennextjs/aws/core/util.adapter.js")],
entireFile: useAdapterHandler,
}),

openNextResolvePlugin({
Expand All @@ -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";
Expand Down
3 changes: 3 additions & 0 deletions packages/cloudflare/src/cli/build/patches/plugins/require.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
},
},
Expand Down
Loading
Loading