1// Alchemy modifications are licensed under Apache-2.0.
2// This file includes third-party code; see /THIRD_PARTY_LICENSES.md.
3// Programmatic @opennextjs/cloudflare build runner — NO wrangler binary, NO
4// wrangler.json. Executed as a disposable child process (see Build.ts): the
5// upstream pipeline mutates cwd-coupled module state, spawns `next build`, and
6// can process.exit(1), so it must never run inside the harness process.
7//
8// Instead of importing `dist/cli/commands/utils/utils.js` (which imports
9// `unstable_readConfig` from "wrangler" at module scope), the two thin
10// `compileConfig` / `getNormalizedOptions` wrappers are vendored here (~15
11// lines) over `@opennextjs/aws` exports so that NOTHING on this path ever
12// imports wrangler code. (`ensureNextjsVersionSupported` still imports
13// `wrangler/package.json` — a JSON version read satisfied by the inert
14// `wrangler-stub` package this integration ships.)
15//
16// Usage: node runner.mjs '<json>' where json is Build.ts's RunnerConfig:
17// { appDir, configPath, compatibilityDate, skipNextBuild, minify, debug,
18// buildCommand }
19import { createRequire } from "node:module";
20import fs from "node:fs";
21import path from "node:path";
22import { pathToFileURL } from "node:url";
23
24const runnerConfig = JSON.parse(process.argv[2] ?? "{}");
25const appDir = path.resolve(runnerConfig.appDir ?? process.cwd());
26process.chdir(appDir); // upstream reads process.cwd() at module scope
27
28// Resolve the framework integration from the application's dependency tree.
29const require = createRequire(path.join(appDir, "package.json"));
30// The exports map doesn't expose "./package.json"; resolve the "." entry
31// (dist/api/index.js) and walk up to the package root.
32const cfApiIndex = require.resolve("@opennextjs/cloudflare");
33const cfRoot = path.resolve(path.dirname(cfApiIndex), "..", "..");
34const cfRequire = createRequire(path.join(cfRoot, "package.json"));
35
36/** Import a file from the @opennextjs/cloudflare dist (bypasses the exports map). */
37const importCf = (p) => import(pathToFileURL(path.join(cfRoot, p)).href);
38/** Import a subpath of @opennextjs/aws resolved from the cloudflare package. */
39const importAws = (p) =>
40 import(pathToFileURL(cfRequire.resolve(`@opennextjs/aws/${p}`)).href);
41
42const { compileOpenNextConfig } = await importAws("build/compileConfig.js");
43const { normalizeOptions } = await importAws("build/helper.js");
44const { default: logger } = await importAws("logger.js");
45const { ensureCloudflareConfig } = await importCf(
46 "dist/cli/build/utils/ensure-cf-config.js",
47);
48const { build } = await importCf("dist/cli/build/build.js");
49
50const configPath = runnerConfig.configPath ?? runnerConfig.generatedConfigPath;
51if (runnerConfig.generatedConfigPath) {
52 const resolveOverride = (name) =>
53 JSON.stringify(require.resolve(`@opennextjs/cloudflare/overrides/${name}`));
54 const writable = runnerConfig.cache === "kv";
55 fs.writeFileSync(
56 configPath,
57 `
58import { defineCloudflareConfig } from ${JSON.stringify(cfApiIndex)};
59import incrementalCache from ${resolveOverride(writable ? "incremental-cache/kv-incremental-cache" : "incremental-cache/static-assets-incremental-cache")};
60${writable ? `import queue from ${resolveOverride("queue/do-queue")};\nimport tagCache from ${resolveOverride("tag-cache/kv-next-tag-cache")};` : ""}
61export default defineCloudflareConfig({ incrementalCache${writable ? ", queue, tagCache" : ""} });
62`,
63 );
64}
65const { config, buildDir } = await compileOpenNextConfig(configPath, {
66 compileEdge: true,
67});
68ensureCloudflareConfig(config);
69config.buildCommand =
70 runnerConfig.buildCommand ?? config.buildCommand ?? "npx next build";
71
72const openNextDistDir = path.dirname(
73 cfRequire.resolve("@opennextjs/aws/index.js"),
74);
75const options = normalizeOptions(config, openNextDistDir, buildDir);
76logger.setLevel(runnerConfig.debug ? "debug" : "info");
77
78// Only these two Wrangler fields are read by the OpenNext build pipeline.
79const wranglerConfig = {
80 compatibility_date: runnerConfig.compatibilityDate,
81 assets: { run_worker_first: true },
82};
83const projectOptions = {
84 sourceDir: appDir,
85 skipNextBuild: !!runnerConfig.skipNextBuild,
86 skipWranglerConfigCheck: true,
87 minify: !!runnerConfig.minify,
88};
89await build(options, config, projectOptions, wranglerConfig, false);
90console.log(
91 "[@alchemy.run/frontend-frameworks/nextjs] OpenNext build finished OK",
92);
93fs.writeFileSync(
94 runnerConfig.outputPath,
95 JSON.stringify({
96 openNextDirectory: options.outputDir,
97 appBuildOutputPath: options.appBuildOutputPath,
98 }),
99);