Skip to content

Commit 695c414

Browse files
authored
Initial working version with the Adapters API (#961)
* initial working version * Apply suggestion from @vicb
1 parent c399fa8 commit 695c414

File tree

6 files changed

+239
-32
lines changed

6 files changed

+239
-32
lines changed

packages/cloudflare/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@
5353
"homepage": "https://github.com/opennextjs/opennextjs-cloudflare",
5454
"dependencies": {
5555
"@dotenvx/dotenvx": "catalog:",
56-
"@opennextjs/aws": "3.8.5",
56+
"@opennextjs/aws": "https://pkg.pr.new/@opennextjs/aws@1027",
5757
"@types/rclone.js": "^0.6.3",
5858
"cloudflare": "^4.4.1",
5959
"enquirer": "^2.4.1",
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
/* eslint-disable @typescript-eslint/no-explicit-any */
2+
import fs from "node:fs";
3+
import { createRequire } from "node:module";
4+
import path from "node:path";
5+
6+
import { compileCache } from "@opennextjs/aws/build/compileCache.js";
7+
import { compileOpenNextConfig } from "@opennextjs/aws/build/compileConfig.js";
8+
import { compileTagCacheProvider } from "@opennextjs/aws/build/compileTagCacheProvider.js";
9+
import { createCacheAssets, createStaticAssets } from "@opennextjs/aws/build/createAssets.js";
10+
import { createMiddleware } from "@opennextjs/aws/build/createMiddleware.js";
11+
import * as buildHelper from "@opennextjs/aws/build/helper.js";
12+
import { addDebugFile } from "@opennextjs/aws/debug.js";
13+
import type { ContentUpdater } from "@opennextjs/aws/plugins/content-updater.js";
14+
import { inlineRouteHandler } from "@opennextjs/aws/plugins/inlineRouteHandlers.js";
15+
import type { NextConfig } from "@opennextjs/aws/types/next-types.js";
16+
17+
import { bundleServer } from "./build/bundle-server.js";
18+
import { compileEnvFiles } from "./build/open-next/compile-env-files.js";
19+
import { compileImages } from "./build/open-next/compile-images.js";
20+
import { compileInit } from "./build/open-next/compile-init.js";
21+
import { compileSkewProtection } from "./build/open-next/compile-skew-protection.js";
22+
import { compileDurableObjects } from "./build/open-next/compileDurableObjects.js";
23+
import { createServerBundle } from "./build/open-next/createServerBundle.js";
24+
import { inlineLoadManifest } from "./build/patches/plugins/load-manifest.js";
25+
26+
export type NextAdapterOutputs = {
27+
pages: any[];
28+
pagesApi: any[];
29+
appPages: any[];
30+
appRoutes: any[];
31+
};
32+
33+
export type BuildCompleteCtx = {
34+
routes: any;
35+
outputs: NextAdapterOutputs;
36+
projectDir: string;
37+
repoRoot: string;
38+
distDir: string;
39+
config: NextConfig;
40+
nextVersion: string;
41+
};
42+
43+
type NextAdapter = {
44+
name: string;
45+
modifyConfig: (config: NextConfig, { phase }: { phase: string }) => Promise<NextConfig>;
46+
onBuildComplete: (ctx: BuildCompleteCtx) => Promise<void>;
47+
}; //TODO: use the one provided by Next
48+
49+
let buildOpts: buildHelper.BuildOptions;
50+
51+
export default {
52+
name: "OpenNext",
53+
54+
async modifyConfig(nextConfig) {
55+
// We have to precompile the cache here, probably compile OpenNext config as well
56+
const { config, buildDir } = await compileOpenNextConfig("open-next.config.ts", {
57+
// TODO(vicb): do we need edge compile
58+
compileEdge: true,
59+
});
60+
61+
const require = createRequire(import.meta.url);
62+
const openNextDistDir = path.dirname(require.resolve("@opennextjs/aws/index.js"));
63+
64+
buildOpts = buildHelper.normalizeOptions(config, openNextDistDir, buildDir);
65+
66+
buildHelper.initOutputDir(buildOpts);
67+
68+
const cache = compileCache(buildOpts);
69+
70+
// We then have to copy the cache files to the .next dir so that they are available at runtime
71+
// TODO: use a better path, this one is temporary just to make it work
72+
const tempCachePath = `${buildOpts.outputDir}/server-functions/default/.open-next/.build`;
73+
fs.mkdirSync(tempCachePath, { recursive: true });
74+
fs.copyFileSync(cache.cache, path.join(tempCachePath, "cache.cjs"));
75+
fs.copyFileSync(cache.composableCache, path.join(tempCachePath, "composable-cache.cjs"));
76+
77+
//TODO: We should check the version of Next here, below 16 we'd throw or show a warning
78+
return {
79+
...nextConfig,
80+
cacheHandler: cache.cache, //TODO: compute that here,
81+
cacheMaxMemorySize: 0,
82+
experimental: {
83+
...nextConfig.experimental,
84+
trustHostHeader: true,
85+
cacheHandlers: {
86+
default: cache.composableCache,
87+
},
88+
},
89+
};
90+
},
91+
92+
async onBuildComplete(ctx: BuildCompleteCtx) {
93+
console.log("OpenNext build will start now");
94+
95+
const configPath = path.join(buildOpts.appBuildOutputPath, ".open-next/.build/open-next.config.edge.mjs");
96+
if (!fs.existsSync(configPath)) {
97+
throw new Error("Could not find compiled Open Next config, did you run the build command?");
98+
}
99+
const openNextConfig = await import(configPath).then((mod) => mod.default);
100+
101+
// TODO(vicb): save outputs
102+
addDebugFile(buildOpts, "outputs.json", ctx);
103+
104+
// Cloudflare specific
105+
compileEnvFiles(buildOpts);
106+
/* TODO(vicb): pass the wrangler config*/
107+
await compileInit(buildOpts, {} as any);
108+
await compileImages(buildOpts);
109+
await compileSkewProtection(buildOpts, openNextConfig);
110+
111+
// Compile middleware
112+
// TODO(vicb): `forceOnlyBuildOnce` is cloudflare specific
113+
await createMiddleware(buildOpts, { forceOnlyBuildOnce: true });
114+
console.log("Middleware created");
115+
116+
createStaticAssets(buildOpts);
117+
console.log("Static assets created");
118+
119+
if (buildOpts.config.dangerous?.disableIncrementalCache !== true) {
120+
const { useTagCache } = createCacheAssets(buildOpts);
121+
console.log("Cache assets created");
122+
if (useTagCache) {
123+
await compileTagCacheProvider(buildOpts);
124+
console.log("Tag cache provider compiled");
125+
}
126+
}
127+
128+
await createServerBundle(
129+
buildOpts,
130+
{
131+
additionalPlugins: getAdditionalPluginsFactory(buildOpts, ctx),
132+
},
133+
ctx
134+
);
135+
136+
await compileDurableObjects(buildOpts);
137+
138+
// TODO(vicb): pass minify `projectOpts`
139+
await bundleServer(buildOpts, { minify: false } as any);
140+
141+
console.log("OpenNext build complete.");
142+
143+
// TODO(vicb): not needed on cloudflare
144+
// console.log("Server bundle created");
145+
// await createRevalidationBundle(buildOpts);
146+
// console.log("Revalidation bundle created");
147+
// await createImageOptimizationBundle(buildOpts);
148+
// console.log("Image optimization bundle created");
149+
// await createWarmerBundle(buildOpts);
150+
// console.log("Warmer bundle created");
151+
// await generateOutput(buildOpts);
152+
// console.log("Output generated");
153+
},
154+
} satisfies NextAdapter;
155+
156+
function getAdditionalPluginsFactory(buildOpts: buildHelper.BuildOptions, ctx: BuildCompleteCtx) {
157+
return (updater: ContentUpdater) => [
158+
inlineRouteHandler(updater, ctx.outputs),
159+
//externalChunksPlugin(outputs),
160+
inlineLoadManifest(updater, buildOpts),
161+
];
162+
}

packages/cloudflare/src/cli/build/open-next/compile-init.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import path from "node:path";
22
import { fileURLToPath } from "node:url";
33

4-
import { loadConfig } from "@opennextjs/aws/adapters/config/util.js";
54
import type { BuildOptions } from "@opennextjs/aws/build/helper.js";
65
import { build } from "esbuild";
76
import type { Unstable_Config } from "wrangler";
@@ -14,7 +13,8 @@ export async function compileInit(options: BuildOptions, wranglerConfig: Unstabl
1413
const templatesDir = path.join(currentDir, "../../templates");
1514
const initPath = path.join(templatesDir, "init.js");
1615

17-
const nextConfig = loadConfig(path.join(options.appBuildOutputPath, ".next"));
16+
// TODO: need the wrangler config here
17+
const nextConfig = { basePath: "", deploymentId: "", trailingSlash: "" };
1818
const basePath = nextConfig.basePath ?? "";
1919
const deploymentId = nextConfig.deploymentId ?? "";
2020
const trailingSlash = nextConfig.trailingSlash ?? false;

packages/cloudflare/src/cli/build/open-next/createServerBundle.ts

Lines changed: 65 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@
22
// Adapted for cloudflare workers
33

44
import fs from "node:fs";
5+
import { createRequire } from "node:module";
56
import path from "node:path";
67

78
import { loadMiddlewareManifest } from "@opennextjs/aws/adapters/config/util.js";
89
import { bundleNextServer } from "@opennextjs/aws/build/bundleNextServer.js";
910
import { compileCache } from "@opennextjs/aws/build/compileCache.js";
11+
import { copyAdapterFiles } from "@opennextjs/aws/build/copyAdapterFiles.js";
1012
import { copyTracedFiles } from "@opennextjs/aws/build/copyTracedFiles.js";
1113
import { copyMiddlewareResources, generateEdgeBundle } from "@opennextjs/aws/build/edge/createEdgeBundle.js";
1214
import * as buildHelper from "@opennextjs/aws/build/helper.js";
@@ -16,7 +18,7 @@ import { applyCodePatches } from "@opennextjs/aws/build/patch/codePatcher.js";
1618
import * as awsPatches from "@opennextjs/aws/build/patch/patches/index.js";
1719
import logger from "@opennextjs/aws/logger.js";
1820
import { minifyAll } from "@opennextjs/aws/minimize-js.js";
19-
import type { ContentUpdater } from "@opennextjs/aws/plugins/content-updater.js";
21+
import { ContentUpdater } from "@opennextjs/aws/plugins/content-updater.js";
2022
import { openNextEdgePlugins } from "@opennextjs/aws/plugins/edge.js";
2123
import { openNextExternalMiddlewarePlugin } from "@opennextjs/aws/plugins/externalMiddleware.js";
2224
import { openNextReplacementPlugin } from "@opennextjs/aws/plugins/replacement.js";
@@ -25,11 +27,10 @@ import type { FunctionOptions, SplittedFunctionOptions } from "@opennextjs/aws/t
2527
import { getCrossPlatformPathRegex } from "@opennextjs/aws/utils/regex.js";
2628
import type { Plugin } from "esbuild";
2729

28-
import { getOpenNextConfig } from "../../../api/config.js";
30+
import type { BuildCompleteCtx } from "../../adapter.js";
2931
import { patchResRevalidate } from "../patches/plugins/res-revalidate.js";
3032
import { patchUseCacheIO } from "../patches/plugins/use-cache.js";
3133
import { normalizePath } from "../utils/index.js";
32-
import { copyWorkerdPackages } from "../utils/workerd.js";
3334

3435
interface CodeCustomization {
3536
// These patches are meant to apply on user and next generated code
@@ -41,7 +42,9 @@ interface CodeCustomization {
4142

4243
export async function createServerBundle(
4344
options: buildHelper.BuildOptions,
44-
codeCustomization?: CodeCustomization
45+
codeCustomization?: CodeCustomization,
46+
/* TODO(vicb): optional to be backward compatible */
47+
buildCtx?: BuildCompleteCtx
4548
) {
4649
const { config } = options;
4750
const foundRoutes = new Set<string>();
@@ -60,7 +63,7 @@ export async function createServerBundle(
6063
if (fnOptions.runtime === "edge") {
6164
await generateEdgeBundle(name, options, fnOptions);
6265
} else {
63-
await generateBundle(name, options, fnOptions, codeCustomization);
66+
await generateBundle(name, options, fnOptions, codeCustomization, buildCtx);
6467
}
6568
});
6669

@@ -112,23 +115,32 @@ export async function createServerBundle(
112115
}
113116

114117
// Generate default function
115-
await generateBundle("default", options, {
116-
...defaultFn,
117-
// @ts-expect-error - Those string are RouteTemplate
118-
routes: Array.from(remainingRoutes),
119-
patterns: ["*"],
120-
});
118+
await generateBundle(
119+
"default",
120+
options,
121+
{
122+
...defaultFn,
123+
// @ts-expect-error - Those string are RouteTemplate
124+
routes: Array.from(remainingRoutes),
125+
patterns: ["*"],
126+
},
127+
codeCustomization,
128+
buildCtx
129+
);
121130
}
122131

123132
async function generateBundle(
124133
name: string,
125134
options: buildHelper.BuildOptions,
126135
fnOptions: SplittedFunctionOptions,
127-
codeCustomization?: CodeCustomization
136+
codeCustomization?: CodeCustomization,
137+
buildCtx?: BuildCompleteCtx
128138
) {
129139
const { appPath, appBuildOutputPath, config, outputDir, monorepoRoot } = options;
130140
logger.info(`Building server function: ${name}...`);
131141

142+
const require = createRequire(import.meta.url);
143+
132144
// Create output folder
133145
const outputPath = path.join(outputDir, "server-functions", name);
134146

@@ -181,21 +193,37 @@ async function generateBundle(
181193
// Copy env files
182194
buildHelper.copyEnvFile(appBuildOutputPath, packagePath, outputPath);
183195

184-
// Copy all necessary traced files
185-
const { tracedFiles, manifests, nodePackages } = await copyTracedFiles({
186-
buildOutputPath: appBuildOutputPath,
187-
packagePath,
188-
outputDir: outputPath,
189-
routes: fnOptions.routes ?? ["app/page.tsx"],
190-
bundledNextServer: isBundled,
191-
});
196+
let tracedFiles: string[] = [];
197+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
198+
let manifests: any = {};
192199

193-
if (getOpenNextConfig(options).cloudflare?.useWorkerdCondition !== false) {
194-
// Next does not trace the "workerd" build condition
195-
// So we need to copy the whole packages using the condition
196-
await copyWorkerdPackages(options, nodePackages);
200+
// Copy all necessary traced files
201+
if (config.dangerous?.useAdapterOutputs) {
202+
if (!buildCtx) {
203+
throw new Error("should not happen");
204+
}
205+
tracedFiles = await copyAdapterFiles(options, name, buildCtx.outputs);
206+
//TODO: we should load manifests here
207+
} else {
208+
const oldTracedFileOutput = await copyTracedFiles({
209+
buildOutputPath: appBuildOutputPath,
210+
packagePath,
211+
outputDir: outputPath,
212+
routes: fnOptions.routes ?? ["app/page.tsx"],
213+
bundledNextServer: isBundled,
214+
skipServerFiles: options.config.dangerous?.useAdapterOutputs === true,
215+
});
216+
tracedFiles = oldTracedFileOutput.tracedFiles;
217+
manifests = oldTracedFileOutput.manifests;
197218
}
198219

220+
// TODO(vicb): what should `nodePackages` be for the adapter
221+
// if (getOpenNextConfig(options).cloudflare?.useWorkerdCondition !== false) {
222+
// // Next does not trace the "workerd" build condition
223+
// // So we need to copy the whole packages using the condition
224+
// await copyWorkerdPackages(options, nodePackages);
225+
// }
226+
199227
const additionalCodePatches = codeCustomization?.additionalCodePatches ?? [];
200228

201229
await applyCodePatches(options, tracedFiles, manifests, [
@@ -229,9 +257,16 @@ async function generateBundle(
229257
const isAfter142 = buildHelper.compareSemver(options.nextVersion, ">=", "14.2");
230258
const isAfter152 = buildHelper.compareSemver(options.nextVersion, ">=", "15.2.0");
231259
const isAfter154 = buildHelper.compareSemver(options.nextVersion, ">=", "15.4.0");
260+
const useAdapterHandler = config.dangerous?.useAdapterOutputs === true;
232261

233262
const disableRouting = isBefore13413 || config.middleware?.external;
234263

264+
const updater = new ContentUpdater(options);
265+
266+
const additionalPlugins = codeCustomization?.additionalPlugins
267+
? codeCustomization.additionalPlugins(updater)
268+
: [];
269+
235270
const plugins = [
236271
openNextReplacementPlugin({
237272
name: `requestHandlerOverride ${name}`,
@@ -242,6 +277,7 @@ async function generateBundle(
242277
...(isAfter142 ? ["patchAsyncStorage"] : []),
243278
...(isAfter141 ? ["appendPrefetch"] : []),
244279
...(isAfter154 ? [] : ["setInitialURL"]),
280+
...(useAdapterHandler ? ["useRequestHandler"] : ["useAdapterHandler"]),
245281
],
246282
}),
247283
openNextReplacementPlugin({
@@ -253,6 +289,8 @@ async function generateBundle(
253289
...(isAfter141 ? ["experimentalIncrementalCacheHandler"] : ["stableIncrementalCache"]),
254290
...(isAfter152 ? [""] : ["composableCache"]),
255291
],
292+
replacements: [require.resolve("@opennextjs/aws/core/util.adapter.js")],
293+
entireFile: useAdapterHandler,
256294
}),
257295

258296
openNextResolvePlugin({
@@ -269,6 +307,9 @@ async function generateBundle(
269307
nextDir: path.join(options.appBuildOutputPath, ".next"),
270308
isInCloudflare: true,
271309
}),
310+
...additionalPlugins,
311+
// The content updater plugin must be the last plugin
312+
updater.plugin,
272313
];
273314

274315
const outfileExt = fnOptions.runtime === "deno" ? "ts" : "mjs";

packages/cloudflare/src/cli/build/patches/plugins/require.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ export function fixRequire(updater: ContentUpdater): Plugin {
4343
`require("next/dist/compiled/@opentelemetry/api")`
4444
);
4545

46+
// The Adapters API build adds i.e. `__require(...)` when inlining the handlers
47+
contents = contents.replace(/__require\d?\(/g, "require(").replace(/__require\d?\./g, "require.");
48+
4649
return contents;
4750
},
4851
},

0 commit comments

Comments
 (0)