···8899import { Config } from "../config.js";
1010import * as patches from "./patches/index.js";
1111-import { normalizePath } from "./utils/index.js";
1111+import { normalizePath, patchCodeWithValidations } from "./utils/index.js";
12121313/** The dist directory of the Cloudflare adapter package */
1414const packageDistDir = path.join(path.dirname(fileURLToPath(import.meta.url)), "../..");
···188188 }));
189189 },
190190 };
191191-}
192192-193193-/**
194194- * Applies multiple code patches in order to a given piece of code, at each step it validates that the code
195195- * has actually been patched/changed, if not an error is thrown
196196- *
197197- * @param code the code to apply the patches to
198198- * @param patches array of tuples, containing a string indicating the target of the patching (for logging) and
199199- * a patching function that takes a string (pre-patch code) and returns a string (post-patch code)
200200- * @returns the patched code
201201- */
202202-async function patchCodeWithValidations(
203203- code: string,
204204- patches: [string, (code: string) => string | Promise<string>, opts?: { isOptional?: boolean }][]
205205-): Promise<string> {
206206- console.log(`Applying code patches:`);
207207- let patchedCode = code;
208208-209209- for (const [target, patchFunction, opts] of patches) {
210210- console.log(` - patching ${target}`);
211211-212212- const prePatchCode = patchedCode;
213213- patchedCode = await patchFunction(patchedCode);
214214-215215- if (!opts?.isOptional && prePatchCode === patchedCode) {
216216- throw new Error(`Failed to patch ${target}`);
217217- }
218218- }
219219-220220- console.log(`All ${patches.length} patches applied\n`);
221221- return patchedCode;
222191}
223192224193/**
-279
packages/cloudflare/src/cli/build/index.ts
···11-import { cpSync, existsSync, readFileSync, writeFileSync } from "node:fs";
22-import { createRequire } from "node:module";
33-import { dirname, join } from "node:path";
44-55-import { buildNextjsApp, setStandaloneBuildMode } from "@opennextjs/aws/build/buildNextApp.js";
66-import { compileCache } from "@opennextjs/aws/build/compileCache.js";
77-import { compileOpenNextConfig } from "@opennextjs/aws/build/compileConfig.js";
88-import { createCacheAssets, createStaticAssets } from "@opennextjs/aws/build/createAssets.js";
99-import { createMiddleware } from "@opennextjs/aws/build/createMiddleware.js";
1010-import * as buildHelper from "@opennextjs/aws/build/helper.js";
1111-import { printHeader, showWarningOnWindows } from "@opennextjs/aws/build/utils.js";
1212-import logger from "@opennextjs/aws/logger.js";
1313-import type { OpenNextConfig } from "@opennextjs/aws/types/open-next.js";
1414-1515-import { getPackageTemplatesDirPath } from "../../utils/get-package-templates-dir-path.js";
1616-import type { ProjectOptions } from "../config.js";
1717-import { containsDotNextDir, getConfig } from "../config.js";
1818-import { askConfirmation } from "../utils/ask-confirmation.js";
1919-import { bundleServer } from "./bundle-server.js";
2020-import { compileEnvFiles } from "./open-next/compile-env-files.js";
2121-import { copyCacheAssets } from "./open-next/copyCacheAssets.js";
2222-import { createServerBundle } from "./open-next/createServerBundle.js";
2323-2424-/**
2525- * Builds the application in a format that can be passed to workerd
2626- *
2727- * It saves the output in a `.worker-next` directory
2828- *
2929- * @param projectOpts The options for the project
3030- */
3131-export async function build(projectOpts: ProjectOptions): Promise<void> {
3232- printHeader("Cloudflare build");
3333-3434- showWarningOnWindows();
3535-3636- const baseDir = projectOpts.sourceDir;
3737- const require = createRequire(import.meta.url);
3838- const openNextDistDir = dirname(require.resolve("@opennextjs/aws/index.js"));
3939-4040- await createOpenNextConfigIfNotExistent(projectOpts);
4141-4242- const { config, buildDir } = await compileOpenNextConfig(baseDir);
4343-4444- ensureCloudflareConfig(config);
4545-4646- // Initialize options
4747- const options = buildHelper.normalizeOptions(config, openNextDistDir, buildDir);
4848- logger.setLevel(options.debug ? "debug" : "info");
4949-5050- // Do not minify the code so that we can apply string replacement patch.
5151- // Note that wrangler will still minify the bundle.
5252- options.minify = false;
5353-5454- // Pre-build validation
5555- buildHelper.checkRunningInsideNextjsApp(options);
5656- logger.info(`App directory: ${options.appPath}`);
5757- buildHelper.printNextjsVersion(options);
5858- ensureNextjsVersionSupported(options);
5959- buildHelper.printOpenNextVersion(options);
6060-6161- if (projectOpts.skipNextBuild) {
6262- logger.warn("Skipping Next.js build");
6363- } else {
6464- // Build the next app
6565- printHeader("Building Next.js app");
6666- setStandaloneBuildMode(options);
6767- buildNextjsApp(options);
6868- }
6969-7070- if (!containsDotNextDir(projectOpts.sourceDir)) {
7171- throw new Error(`.next folder not found in ${projectOpts.sourceDir}`);
7272- }
7373-7474- // Generate deployable bundle
7575- printHeader("Generating bundle");
7676- buildHelper.initOutputDir(options);
7777-7878- // Compile cache.ts
7979- compileCache(options);
8080-8181- // Compile .env files
8282- compileEnvFiles(options);
8383-8484- // Compile middleware
8585- await createMiddleware(options, { forceOnlyBuildOnce: true });
8686-8787- createStaticAssets(options);
8888-8989- if (config.dangerous?.disableIncrementalCache !== true) {
9090- createCacheAssets(options);
9191- copyCacheAssets(options);
9292- }
9393-9494- await createServerBundle(options);
9595-9696- // TODO: drop this copy.
9797- // Copy the .next directory to the output directory so it can be mutated.
9898- cpSync(join(projectOpts.sourceDir, ".next"), join(projectOpts.outputDir, ".next"), { recursive: true });
9999-100100- const projConfig = getConfig(projectOpts);
101101-102102- // TODO: rely on options only.
103103- await bundleServer(projConfig, options);
104104-105105- if (!projectOpts.skipWranglerConfigCheck) {
106106- await createWranglerConfigIfNotExistent(projectOpts);
107107- }
108108-109109- logger.info("OpenNext build complete.");
110110-}
111111-112112-/**
113113- * Creates a `open-next.config.ts` file for the user if it doesn't exist, but only after asking for the user's confirmation.
114114- *
115115- * If the user refuses an error is thrown (since the file is mandatory).
116116- *
117117- * @param projectOpts The options for the project
118118- */
119119-async function createOpenNextConfigIfNotExistent(projectOpts: ProjectOptions): Promise<void> {
120120- const openNextConfigPath = join(projectOpts.sourceDir, "open-next.config.ts");
121121-122122- if (!existsSync(openNextConfigPath)) {
123123- const answer = await askConfirmation(
124124- "Missing required `open-next.config.ts` file, do you want to create one?"
125125- );
126126-127127- if (!answer) {
128128- throw new Error("The `open-next.config.ts` file is required, aborting!");
129129- }
130130-131131- cpSync(join(getPackageTemplatesDirPath(), "defaults", "open-next.config.ts"), openNextConfigPath);
132132- }
133133-}
134134-135135-/**
136136- * Ensures open next is configured for cloudflare.
137137- *
138138- * @param config OpenNext configuration.
139139- */
140140-function ensureCloudflareConfig(config: OpenNextConfig) {
141141- const requirements = {
142142- dftUseCloudflareWrapper: config.default?.override?.wrapper === "cloudflare-node",
143143- dftUseEdgeConverter: config.default?.override?.converter === "edge",
144144- dftMaybeUseCache:
145145- config.default?.override?.incrementalCache === "dummy" ||
146146- typeof config.default?.override?.incrementalCache === "function",
147147- dftUseDummyTagCacheAndQueue:
148148- config.default?.override?.tagCache === "dummy" && config.default?.override?.queue === "dummy",
149149- disableCacheInterception: config.dangerous?.enableCacheInterception !== true,
150150- mwIsMiddlewareExternal: config.middleware?.external == true,
151151- mwUseCloudflareWrapper: config.middleware?.override?.wrapper === "cloudflare-edge",
152152- mwUseEdgeConverter: config.middleware?.override?.converter === "edge",
153153- mwUseFetchProxy: config.middleware?.override?.proxyExternalRequest === "fetch",
154154- };
155155-156156- if (Object.values(requirements).some((satisfied) => !satisfied)) {
157157- throw new Error(
158158- "The `open-next.config.ts` should have a default export like this:\n\n" +
159159- `{
160160- default: {
161161- override: {
162162- wrapper: "cloudflare-node",
163163- converter: "edge",
164164- incrementalCache: "dummy" | function,
165165- tagCache: "dummy",
166166- queue: "dummy",
167167- },
168168- },
169169-170170- middleware: {
171171- external: true,
172172- override: {
173173- wrapper: "cloudflare-edge",
174174- converter: "edge",
175175- proxyExternalRequest: "fetch",
176176- },
177177- },
178178-179179- "dangerous": {
180180- "enableCacheInterception": false
181181- },
182182- }\n\n`.replace(/^ {8}/gm, "")
183183- );
184184- }
185185-}
186186-187187-/**
188188- * Creates a `wrangler.json` file for the user if a wrangler config file doesn't already exist,
189189- * but only after asking for the user's confirmation.
190190- *
191191- * If the user refuses a warning is shown (which offers ways to opt out of this check to the user).
192192- *
193193- * Note: we generate a wrangler.json file with comments instead of using the jsonc extension,
194194- * we decided to do that since json is more common than jsonc, wrangler also parses
195195- * them in the same way and we also expect developers to associate `wrangler.json`
196196- * files to the jsonc language
197197- *
198198- * @param projectOpts The options for the project
199199- */
200200-async function createWranglerConfigIfNotExistent(projectOpts: ProjectOptions): Promise<void> {
201201- const possibleExts = ["toml", "json", "jsonc"];
202202-203203- const wranglerConfigFileExists = possibleExts.some((ext) =>
204204- existsSync(join(projectOpts.sourceDir, `wrangler.${ext}`))
205205- );
206206- if (wranglerConfigFileExists) {
207207- return;
208208- }
209209-210210- const answer = await askConfirmation(
211211- "No `wrangler.(toml|json|jsonc)` config file found, do you want to create one?"
212212- );
213213-214214- if (!answer) {
215215- console.warn(
216216- "No Wrangler config file created" +
217217- "\n" +
218218- "(to avoid this check use the `--skipWranglerConfigCheck` flag or set a `SKIP_WRANGLER_CONFIG_CHECK` environment variable to `yes`)"
219219- );
220220- return;
221221- }
222222-223223- let wranglerConfig = readFileSync(join(getPackageTemplatesDirPath(), "defaults", "wrangler.json"), "utf8");
224224-225225- const appName = getAppNameFromPackageJson(projectOpts.sourceDir) ?? "app-name";
226226- if (appName) {
227227- wranglerConfig = wranglerConfig.replace('"app-name"', JSON.stringify(appName.replaceAll("_", "-")));
228228- }
229229-230230- const compatDate = await getLatestCompatDate();
231231- if (compatDate) {
232232- wranglerConfig = wranglerConfig.replace(
233233- /"compatibility_date": "\d{4}-\d{2}-\d{2}"/,
234234- `"compatibility_date": ${JSON.stringify(compatDate)}`
235235- );
236236- }
237237-238238- writeFileSync(join(projectOpts.sourceDir, "wrangler.json"), wranglerConfig);
239239-}
240240-241241-function getAppNameFromPackageJson(sourceDir: string): string | undefined {
242242- try {
243243- const packageJsonStr = readFileSync(join(sourceDir, "package.json"), "utf8");
244244- const packageJson: Record<string, string> = JSON.parse(packageJsonStr);
245245- if (typeof packageJson.name === "string") return packageJson.name;
246246- } catch {
247247- /* empty */
248248- }
249249-}
250250-251251-export async function getLatestCompatDate(): Promise<string | undefined> {
252252- try {
253253- const resp = await fetch(`https://registry.npmjs.org/workerd`);
254254- const latestWorkerdVersion = (
255255- (await resp.json()) as {
256256- "dist-tags": { latest: string };
257257- }
258258- )["dist-tags"].latest;
259259-260260- // The format of the workerd version is `major.yyyymmdd.patch`.
261261- const match = latestWorkerdVersion.match(/\d+\.(\d{4})(\d{2})(\d{2})\.\d+/);
262262-263263- if (match) {
264264- const [, year, month, date] = match;
265265- const compatDate = `${year}-${month}-${date}`;
266266-267267- return compatDate;
268268- }
269269- } catch {
270270- /* empty */
271271- }
272272-}
273273-274274-function ensureNextjsVersionSupported(options: buildHelper.BuildOptions) {
275275- if (buildHelper.compareSemver(options.nextVersion, "14.0.0") < 0) {
276276- logger.error("Next.js version unsupported, please upgrade to version 14 or greater.");
277277- process.exit(1);
278278- }
279279-}
···11+/**
22+ * Applies multiple code patches in order to a given piece of code, at each step it validates that the code
33+ * has actually been patched/changed, if not an error is thrown
44+ *
55+ * @param code the code to apply the patches to
66+ * @param patches array of tuples, containing a string indicating the target of the patching (for logging) and
77+ * a patching function that takes a string (pre-patch code) and returns a string (post-patch code)
88+ * @returns the patched code
99+ */
1010+export async function patchCodeWithValidations(
1111+ code: string,
1212+ patches: [string, (code: string) => string | Promise<string>, opts?: { isOptional?: boolean }][]
1313+): Promise<string> {
1414+ console.log(`Applying code patches:`);
1515+ let patchedCode = code;
1616+1717+ for (const [target, patchFunction, opts] of patches) {
1818+ console.log(` - patching ${target}`);
1919+2020+ const prePatchCode = patchedCode;
2121+ patchedCode = await patchFunction(patchedCode);
2222+2323+ if (!opts?.isOptional && prePatchCode === patchedCode) {
2424+ throw new Error(`Failed to patch ${target}`);
2525+ }
2626+ }
2727+2828+ console.log(`All ${patches.length} patches applied\n`);
2929+ return patchedCode;
3030+}
···11+import { cpSync, existsSync, readFileSync, writeFileSync } from "node:fs";
22+import { join } from "node:path";
33+44+import { getPackageTemplatesDirPath } from "../../../utils/get-package-templates-dir-path.js";
55+import type { ProjectOptions } from "../../config.js";
66+import { askConfirmation } from "../../utils/ask-confirmation.js";
77+88+/**
99+ * Creates a `wrangler.json` file for the user if a wrangler config file doesn't already exist,
1010+ * but only after asking for the user's confirmation.
1111+ *
1212+ * If the user refuses a warning is shown (which offers ways to opt out of this check to the user).
1313+ *
1414+ * Note: we generate a wrangler.json file with comments instead of using the jsonc extension,
1515+ * we decided to do that since json is more common than jsonc, wrangler also parses
1616+ * them in the same way and we also expect developers to associate `wrangler.json`
1717+ * files to the jsonc language
1818+ *
1919+ * @param projectOpts The options for the project
2020+ */
2121+export async function createWranglerConfigIfNotExistent(projectOpts: ProjectOptions): Promise<void> {
2222+ const possibleExts = ["toml", "json", "jsonc"];
2323+2424+ const wranglerConfigFileExists = possibleExts.some((ext) =>
2525+ existsSync(join(projectOpts.sourceDir, `wrangler.${ext}`))
2626+ );
2727+ if (wranglerConfigFileExists) {
2828+ return;
2929+ }
3030+3131+ const answer = await askConfirmation(
3232+ "No `wrangler.(toml|json|jsonc)` config file found, do you want to create one?"
3333+ );
3434+3535+ if (!answer) {
3636+ console.warn(
3737+ "No Wrangler config file created" +
3838+ "\n" +
3939+ "(to avoid this check use the `--skipWranglerConfigCheck` flag or set a `SKIP_WRANGLER_CONFIG_CHECK` environment variable to `yes`)"
4040+ );
4141+ return;
4242+ }
4343+4444+ let wranglerConfig = readFileSync(join(getPackageTemplatesDirPath(), "defaults", "wrangler.json"), "utf8");
4545+4646+ const appName = getAppNameFromPackageJson(projectOpts.sourceDir) ?? "app-name";
4747+ if (appName) {
4848+ wranglerConfig = wranglerConfig.replace('"app-name"', JSON.stringify(appName.replaceAll("_", "-")));
4949+ }
5050+5151+ const compatDate = await getLatestCompatDate();
5252+ if (compatDate) {
5353+ wranglerConfig = wranglerConfig.replace(
5454+ /"compatibility_date": "\d{4}-\d{2}-\d{2}"/,
5555+ `"compatibility_date": ${JSON.stringify(compatDate)}`
5656+ );
5757+ }
5858+5959+ writeFileSync(join(projectOpts.sourceDir, "wrangler.json"), wranglerConfig);
6060+}
6161+6262+function getAppNameFromPackageJson(sourceDir: string): string | undefined {
6363+ try {
6464+ const packageJsonStr = readFileSync(join(sourceDir, "package.json"), "utf8");
6565+ const packageJson: Record<string, string> = JSON.parse(packageJsonStr);
6666+ if (typeof packageJson.name === "string") return packageJson.name;
6767+ } catch {
6868+ /* empty */
6969+ }
7070+}
7171+7272+export async function getLatestCompatDate(): Promise<string | undefined> {
7373+ try {
7474+ const resp = await fetch(`https://registry.npmjs.org/workerd`);
7575+ const latestWorkerdVersion = (
7676+ (await resp.json()) as {
7777+ "dist-tags": { latest: string };
7878+ }
7979+ )["dist-tags"].latest;
8080+8181+ // The format of the workerd version is `major.yyyymmdd.patch`.
8282+ const match = latestWorkerdVersion.match(/\d+\.(\d{4})(\d{2})(\d{2})\.\d+/);
8383+8484+ if (match) {
8585+ const [, year, month, date] = match;
8686+ const compatDate = `${year}-${month}-${date}`;
8787+8888+ return compatDate;
8989+ }
9090+ } catch {
9191+ /* empty */
9292+ }
9393+}
9494+9595+/**
9696+ * Creates a `open-next.config.ts` file for the user if it doesn't exist, but only after asking for the user's confirmation.
9797+ *
9898+ * If the user refuses an error is thrown (since the file is mandatory).
9999+ *
100100+ * @param projectOpts The options for the project
101101+ */
102102+export async function createOpenNextConfigIfNotExistent(projectOpts: ProjectOptions): Promise<void> {
103103+ const openNextConfigPath = join(projectOpts.sourceDir, "open-next.config.ts");
104104+105105+ if (!existsSync(openNextConfigPath)) {
106106+ const answer = await askConfirmation(
107107+ "Missing required `open-next.config.ts` file, do you want to create one?"
108108+ );
109109+110110+ if (!answer) {
111111+ throw new Error("The `open-next.config.ts` file is required, aborting!");
112112+ }
113113+114114+ cpSync(join(getPackageTemplatesDirPath(), "defaults", "open-next.config.ts"), openNextConfigPath);
115115+ }
116116+}
···44import mockFs from "mock-fs";
55import { afterEach, beforeEach, describe, expect, it } from "vitest";
6677-import { extractProjectEnvVars } from "./extract-project-env-vars";
77+import { extractProjectEnvVars } from "./extract-project-env-vars.js";
8899const options = { monorepoRoot: "", appPath: "" } as BuildOptions;
1010
+3
packages/cloudflare/src/cli/build/utils/index.ts
···11+export * from "./apply-patches.js";
22+export * from "./create-config-files.js";
33+export * from "./ensure-cf-config.js";
14export * from "./extract-project-env-vars.js";
25export * from "./normalize-path.js";
36export * from "./ts-parse-file.js";
+1-1
packages/cloudflare/src/cli/index.ts
···22import { resolve } from "node:path";
3344import { getArgs } from "./args.js";
55-import { build } from "./build/index.js";
55+import { build } from "./build/build.js";
6677const nextAppDir = process.cwd();
88