Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/large-adults-report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@opennextjs/cloudflare": patch
---

Use the workerd build condition by default
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.5.7",
"@opennextjs/aws": "3.5.8",
"enquirer": "^2.4.1",
"glob": "catalog:",
"ts-tqdm": "^0.8.6"
Expand Down
35 changes: 34 additions & 1 deletion packages/cloudflare/src/api/config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { BaseOverride, LazyLoadedOverride, OpenNextConfig } from "@opennextjs/aws/types/open-next";
import type { BuildOptions } from "@opennextjs/aws/build/helper";
import {
BaseOverride,
LazyLoadedOverride,
OpenNextConfig as AwsOpenNextConfig,
} from "@opennextjs/aws/types/open-next";
import type { IncrementalCache, Queue, TagCache } from "@opennextjs/aws/types/overrides";

export type Override<T extends BaseOverride> = "dummy" | T | LazyLoadedOverride<T>;
Expand Down Expand Up @@ -47,6 +52,9 @@ export function defineCloudflareConfig(config: CloudflareOverrides = {}): OpenNe
},
// node:crypto is used to compute cache keys
edgeExternals: ["node:crypto"],
cloudflare: {
useWorkerdCondition: true,
},
};
}

Expand All @@ -73,3 +81,28 @@ function resolveQueue(value: CloudflareOverrides["queue"] = "dummy") {

return typeof value === "function" ? value : () => value;
}

interface OpenNextConfig extends AwsOpenNextConfig {
cloudflare?: {
/**
* Whether to use the "workerd" build conditions when bundling the server.
* It is recommended to set it to `true` so that code specifically targeted to the
* workerd runtime is bundled.
*
* See https://esbuild.github.io/api/#conditions
*
* @default true
*/
useWorkerdCondition?: boolean;
};
}

/**
* @param buildOpts build options from AWS
* @returns The OpenConfig specific to cloudflare
*/
export function getOpenNextConfig(buildOpts: BuildOptions): OpenNextConfig {
return buildOpts.config;
}

export type { OpenNextConfig };
2 changes: 1 addition & 1 deletion packages/cloudflare/src/api/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export * from "./cloudflare-context.js";
export { defineCloudflareConfig } from "./config.js";
export { defineCloudflareConfig, type OpenNextConfig } from "./config.js";
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { error } from "@opennextjs/aws/adapters/logger.js";
import type { OpenNextConfig } from "@opennextjs/aws/types/open-next.js";
import type { NextModeTagCache } from "@opennextjs/aws/types/overrides.js";

import type { OpenNextConfig } from "../../../api/config.js";
import { getCloudflareContext } from "../../cloudflare-context.js";
import { debugCache, FALLBACK_BUILD_ID } from "../internal.js";

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { error } from "@opennextjs/aws/adapters/logger.js";
import { generateShardId } from "@opennextjs/aws/core/routing/queue.js";
import type { OpenNextConfig } from "@opennextjs/aws/types/open-next";
import type { NextModeTagCache } from "@opennextjs/aws/types/overrides.js";
import { IgnorableError } from "@opennextjs/aws/utils/error.js";

import type { OpenNextConfig } from "../../../api/config.js";
import { getCloudflareContext } from "../../cloudflare-context";
import { debugCache } from "../internal";

Expand Down
2 changes: 1 addition & 1 deletion packages/cloudflare/src/cli/build/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import * as buildHelper from "@opennextjs/aws/build/helper.js";
import { BuildOptions } from "@opennextjs/aws/build/helper.js";
import { printHeader } from "@opennextjs/aws/build/utils.js";
import logger from "@opennextjs/aws/logger.js";
import { OpenNextConfig } from "@opennextjs/aws/types/open-next.js";

import { OpenNextConfig } from "../../api/config.js";
import type { ProjectOptions } from "../project-options.js";
import { bundleServer } from "./bundle-server.js";
import { compileCacheAssetsManifestSqlFile } from "./open-next/compile-cache-assets-manifest.js";
Expand Down
9 changes: 6 additions & 3 deletions packages/cloudflare/src/cli/build/bundle-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { type BuildOptions, getPackagePath } from "@opennextjs/aws/build/helper.
import { ContentUpdater } from "@opennextjs/aws/plugins/content-updater.js";
import { build, type Plugin } from "esbuild";

import { getOpenNextConfig } from "../../api/config.js";
import { patchVercelOgLibrary } from "./patches/ast/patch-vercel-og-library.js";
import { patchWebpackRuntime } from "./patches/ast/webpack-runtime.js";
import * as patches from "./patches/index.js";
Expand Down Expand Up @@ -80,13 +81,13 @@ export async function bundleServer(buildOpts: BuildOptions): Promise<void> {
// Next traces files using the default conditions from `nft` (`node`, `require`, `import` and `default`)
//
// Because we use the `node` platform for this build, the "module" condition is used when no conditions are defined.
// We explicitly set the conditions to an empty array to disable the "module" condition in order to match Next tracing.
// The conditions are always set (should it be to an empty array) to disable the "module" condition.
//
// See:
// - default nft conditions: https://github.com/vercel/nft/blob/2b55b01/readme.md#exports--imports
// - Next no explicit override: https://github.com/vercel/next.js/blob/2efcf11/packages/next/src/build/collect-build-traces.ts#L287
// - ESBuild `node` platform: https://esbuild.github.io/api/#platform
conditions: [],
conditions: getOpenNextConfig(buildOpts).cloudflare?.useWorkerdCondition === false ? [] : ["workerd"],
plugins: [
shimRequireHook(buildOpts),
inlineDynamicRequires(updater, buildOpts),
Expand Down Expand Up @@ -158,7 +159,9 @@ export async function bundleServer(buildOpts: BuildOptions): Promise<void> {
);
}

console.log(`\x1b[35mWorker saved in \`${getOutputWorkerPath(buildOpts)}\` 🚀\n\x1b[0m`);
console.log(
`\x1b[35mWorker saved in \`${path.relative(buildOpts.appPath, getOutputWorkerPath(buildOpts))}\` 🚀\n\x1b[0m`
);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,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 { patchResRevalidate } from "../patches/plugins/res-revalidate.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 Down Expand Up @@ -180,14 +182,20 @@ async function generateBundle(
buildHelper.copyEnvFile(appBuildOutputPath, packagePath, outputPath);

// Copy all necessary traced files
const { tracedFiles, manifests } = await copyTracedFiles({
const { tracedFiles, manifests, nodePackages } = await copyTracedFiles({
buildOutputPath: appBuildOutputPath,
packagePath,
outputDir: outputPath,
routes: fnOptions.routes ?? ["app/page.tsx"],
bundledNextServer: isBundled,
});

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
3 changes: 2 additions & 1 deletion packages/cloudflare/src/cli/build/utils/ensure-cf-config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import logger from "@opennextjs/aws/logger.js";
import type { OpenNextConfig } from "@opennextjs/aws/types/open-next.js";

import type { OpenNextConfig } from "../../../api/config.js";

/**
* Ensures open next is configured for cloudflare.
Expand Down
49 changes: 49 additions & 0 deletions packages/cloudflare/src/cli/build/utils/workerd.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { describe, expect, test } from "vitest";

import { hasBuildCondition } from "./workerd";

describe("hasBuildConditions", () => {
test("undefined", () => {
expect(hasBuildCondition(undefined, "workerd")).toBe(false);
});

test("top level", () => {
const exports = {
workerd: "./path/to/workerd.js",
default: "./path/to/default.js",
};

expect(hasBuildCondition(exports, "workerd")).toBe(true);
expect(hasBuildCondition(exports, "default")).toBe(true);
expect(hasBuildCondition(exports, "module")).toBe(false);
});

test("nested", () => {
const exports = {
".": "/path/to/index.js",
"./server": {
"react-server": {
workerd: "./server.edge.js",
},
default: "./server.js",
},
};

expect(hasBuildCondition(exports, "workerd")).toBe(true);
expect(hasBuildCondition(exports, "default")).toBe(true);
expect(hasBuildCondition(exports, "module")).toBe(false);
});

test("only consider leaves", () => {
const exports = {
".": "/path/to/index.js",
"./server": {
workerd: {
default: "./server.edge.js",
},
},
};

expect(hasBuildCondition(exports, "workerd")).toBe(false);
});
});
57 changes: 57 additions & 0 deletions packages/cloudflare/src/cli/build/utils/workerd.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import fs from "node:fs/promises";
import path from "node:path";

import { loadConfig } from "@opennextjs/aws/adapters/config/util.js";
import type { BuildOptions } from "@opennextjs/aws/build/helper.js";
import logger from "@opennextjs/aws/logger.js";
import { getCrossPlatformPathRegex } from "@opennextjs/aws/utils/regex.js";

/**
* Return whether the passed export map has the given condition
*/
export function hasBuildCondition(
exports: { [key: string]: unknown } | undefined,
condition: string
): boolean {
if (!exports) {
return false;
}
for (const [key, value] of Object.entries(exports)) {
if (typeof value === "object" && value != null) {
if (hasBuildCondition(value as { [key: string]: unknown }, condition)) {
return true;
}
} else {
if (key === condition) {
return true;
}
}
}
return false;
}

export async function copyWorkerdPackages(options: BuildOptions, nodePackages: Map<string, string>) {
const isNodeModuleRegex = getCrossPlatformPathRegex(`.*/node_modules/(?<pkg>.*)`, { escape: false });

// Copy full external packages when they use "workerd" build condition
const nextConfig = loadConfig(path.join(options.appBuildOutputPath, ".next"));
const externalPackages = nextConfig.serverExternalPackages ?? [];
for (const [src, dst] of nodePackages.entries()) {
try {
const { exports } = JSON.parse(await fs.readFile(path.join(src, "package.json"), "utf8"));
const match = src.match(isNodeModuleRegex);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should warn the user if !externalPackages.includes(match.groups.pkg) && hasBuildCondition(exports, "workerd")

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure what the best thing to do is here, do you have something in mind?

hasBuildCondition(...) check for traced packages (nodePackages).

When I test with postgres, it would not be traced unless added in serverExternalPackages - if not there it is bundled.

I think the warning would trigger for react-dom and packages in server-external-packages.json

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ho yeah right.

I think we should still add the warning here, at least for the packages you mentionned (we should probably filter some like react-dom)

One thing we could do is maintain a list of known deps that cause trouble like postgres and look if they're in the user package.json
That's not ideal, but at least it will help a bit.

Another option would be to look at the lockfile, but it will show a lot of false positive (especially for monorepo) and would be complex to maintain (especially for bun and the binary lockfile)

I can't think of anything else at the moment

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI, i don't consider this blocking, we could figure this out later if you prefer

if (
match?.groups?.pkg &&
externalPackages.includes(match.groups.pkg) &&
hasBuildCondition(exports, "workerd")
) {
logger.debug(
`Copying package using a workerd condition: ${path.relative(options.appPath, src)} -> ${path.relative(options.appPath, dst)}`
);
fs.cp(src, dst, { recursive: true, force: true });
}
} catch {
logger.error(`Failed to copy ${src}`);
}
}
}
2 changes: 1 addition & 1 deletion packages/cloudflare/src/cli/commands/deploy.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { BuildOptions } from "@opennextjs/aws/build/helper.js";
import { OpenNextConfig } from "@opennextjs/aws/types/open-next.js";

import type { OpenNextConfig } from "../../api/config.js";
import { getWranglerEnvironmentFlag, runWrangler } from "../utils/run-wrangler.js";
import { populateCache } from "./populate-cache.js";

Expand Down
2 changes: 1 addition & 1 deletion packages/cloudflare/src/cli/commands/preview.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { BuildOptions } from "@opennextjs/aws/build/helper.js";
import { OpenNextConfig } from "@opennextjs/aws/types/open-next.js";

import type { OpenNextConfig } from "../../api/config.js";
import { getWranglerEnvironmentFlag, runWrangler } from "../utils/run-wrangler.js";
import { populateCache } from "./populate-cache.js";

Expand Down
2 changes: 1 addition & 1 deletion packages/cloudflare/src/cli/commands/upload.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { BuildOptions } from "@opennextjs/aws/build/helper.js";
import { OpenNextConfig } from "@opennextjs/aws/types/open-next.js";

import type { OpenNextConfig } from "../../api/config.js";
import { getWranglerEnvironmentFlag, runWrangler } from "../utils/run-wrangler.js";
import { populateCache } from "./populate-cache.js";

Expand Down
Loading