Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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 next 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