Skip to content

Commit faca3e1

Browse files
vicbconico974
andauthored
Allow using external packages with a workerd build condition (#593)
Co-authored-by: conico974 <nicodorseuil@yahoo.fr>
1 parent f734868 commit faca3e1

16 files changed

+193
-62
lines changed

.changeset/large-adults-report.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@opennextjs/cloudflare": patch
3+
---
4+
5+
Use the workerd build condition by default

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.5.7",
56+
"@opennextjs/aws": "3.5.8",
5757
"enquirer": "^2.4.1",
5858
"glob": "catalog:",
5959
"ts-tqdm": "^0.8.6"

packages/cloudflare/src/api/config.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
import { BaseOverride, LazyLoadedOverride, OpenNextConfig } from "@opennextjs/aws/types/open-next";
1+
import type { BuildOptions } from "@opennextjs/aws/build/helper";
2+
import {
3+
BaseOverride,
4+
LazyLoadedOverride,
5+
OpenNextConfig as AwsOpenNextConfig,
6+
} from "@opennextjs/aws/types/open-next";
27
import type { IncrementalCache, Queue, TagCache } from "@opennextjs/aws/types/overrides";
38

49
export type Override<T extends BaseOverride> = "dummy" | T | LazyLoadedOverride<T>;
@@ -47,6 +52,9 @@ export function defineCloudflareConfig(config: CloudflareOverrides = {}): OpenNe
4752
},
4853
// node:crypto is used to compute cache keys
4954
edgeExternals: ["node:crypto"],
55+
cloudflare: {
56+
useWorkerdCondition: true,
57+
},
5058
};
5159
}
5260

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

7482
return typeof value === "function" ? value : () => value;
7583
}
84+
85+
interface OpenNextConfig extends AwsOpenNextConfig {
86+
cloudflare?: {
87+
/**
88+
* Whether to use the "workerd" build conditions when bundling the server.
89+
* It is recommended to set it to `true` so that code specifically targeted to the
90+
* workerd runtime is bundled.
91+
*
92+
* See https://esbuild.github.io/api/#conditions
93+
*
94+
* @default true
95+
*/
96+
useWorkerdCondition?: boolean;
97+
};
98+
}
99+
100+
/**
101+
* @param buildOpts build options from AWS
102+
* @returns The OpenConfig specific to cloudflare
103+
*/
104+
export function getOpenNextConfig(buildOpts: BuildOptions): OpenNextConfig {
105+
return buildOpts.config;
106+
}
107+
108+
export type { OpenNextConfig };

packages/cloudflare/src/api/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
export * from "./cloudflare-context.js";
2-
export { defineCloudflareConfig } from "./config.js";
2+
export { defineCloudflareConfig, type OpenNextConfig } from "./config.js";

packages/cloudflare/src/api/overrides/tag-cache/d1-next-tag-cache.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { error } from "@opennextjs/aws/adapters/logger.js";
2-
import type { OpenNextConfig } from "@opennextjs/aws/types/open-next.js";
32
import type { NextModeTagCache } from "@opennextjs/aws/types/overrides.js";
43

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

packages/cloudflare/src/api/overrides/tag-cache/do-sharded-tag-cache.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { error } from "@opennextjs/aws/adapters/logger.js";
22
import { generateShardId } from "@opennextjs/aws/core/routing/queue.js";
3-
import type { OpenNextConfig } from "@opennextjs/aws/types/open-next";
43
import type { NextModeTagCache } from "@opennextjs/aws/types/overrides.js";
54
import { IgnorableError } from "@opennextjs/aws/utils/error.js";
65

6+
import type { OpenNextConfig } from "../../../api/config.js";
77
import { getCloudflareContext } from "../../cloudflare-context";
88
import { debugCache } from "../internal";
99

packages/cloudflare/src/cli/build/build.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ import * as buildHelper from "@opennextjs/aws/build/helper.js";
66
import { BuildOptions } from "@opennextjs/aws/build/helper.js";
77
import { printHeader } from "@opennextjs/aws/build/utils.js";
88
import logger from "@opennextjs/aws/logger.js";
9-
import { OpenNextConfig } from "@opennextjs/aws/types/open-next.js";
109

10+
import { OpenNextConfig } from "../../api/config.js";
1111
import type { ProjectOptions } from "../project-options.js";
1212
import { bundleServer } from "./bundle-server.js";
1313
import { compileCacheAssetsManifestSqlFile } from "./open-next/compile-cache-assets-manifest.js";

packages/cloudflare/src/cli/build/bundle-server.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { type BuildOptions, getPackagePath } from "@opennextjs/aws/build/helper.
77
import { ContentUpdater } from "@opennextjs/aws/plugins/content-updater.js";
88
import { build, type Plugin } from "esbuild";
99

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

161-
console.log(`\x1b[35mWorker saved in \`${getOutputWorkerPath(buildOpts)}\` 🚀\n\x1b[0m`);
162+
console.log(
163+
`\x1b[35mWorker saved in \`${path.relative(buildOpts.appPath, getOutputWorkerPath(buildOpts))}\` 🚀\n\x1b[0m`
164+
);
162165
}
163166

164167
/**

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,10 @@ import type { FunctionOptions, SplittedFunctionOptions } from "@opennextjs/aws/t
3333
import { getCrossPlatformPathRegex } from "@opennextjs/aws/utils/regex.js";
3434
import type { Plugin } from "esbuild";
3535

36+
import { getOpenNextConfig } from "../../../api/config.js";
3637
import { patchResRevalidate } from "../patches/plugins/res-revalidate.js";
3738
import { normalizePath } from "../utils/index.js";
39+
import { copyWorkerdPackages } from "../utils/workerd.js";
3840

3941
interface CodeCustomization {
4042
// These patches are meant to apply on user and next generated code
@@ -180,14 +182,20 @@ async function generateBundle(
180182
buildHelper.copyEnvFile(appBuildOutputPath, packagePath, outputPath);
181183

182184
// Copy all necessary traced files
183-
const { tracedFiles, manifests } = await copyTracedFiles({
185+
const { tracedFiles, manifests, nodePackages } = await copyTracedFiles({
184186
buildOutputPath: appBuildOutputPath,
185187
packagePath,
186188
outputDir: outputPath,
187189
routes: fnOptions.routes ?? ["app/page.tsx"],
188190
bundledNextServer: isBundled,
189191
});
190192

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);
197+
}
198+
191199
const additionalCodePatches = codeCustomization?.additionalCodePatches ?? [];
192200

193201
await applyCodePatches(options, tracedFiles, manifests, [

packages/cloudflare/src/cli/build/utils/ensure-cf-config.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import logger from "@opennextjs/aws/logger.js";
2-
import type { OpenNextConfig } from "@opennextjs/aws/types/open-next.js";
2+
3+
import type { OpenNextConfig } from "../../../api/config.js";
34

45
/**
56
* Ensures open next is configured for cloudflare.
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { describe, expect, test } from "vitest";
2+
3+
import { hasBuildCondition } from "./workerd";
4+
5+
describe("hasBuildConditions", () => {
6+
test("undefined", () => {
7+
expect(hasBuildCondition(undefined, "workerd")).toBe(false);
8+
});
9+
10+
test("top level", () => {
11+
const exports = {
12+
workerd: "./path/to/workerd.js",
13+
default: "./path/to/default.js",
14+
};
15+
16+
expect(hasBuildCondition(exports, "workerd")).toBe(true);
17+
expect(hasBuildCondition(exports, "default")).toBe(true);
18+
expect(hasBuildCondition(exports, "module")).toBe(false);
19+
});
20+
21+
test("nested", () => {
22+
const exports = {
23+
".": "/path/to/index.js",
24+
"./server": {
25+
"react-server": {
26+
workerd: "./server.edge.js",
27+
},
28+
default: "./server.js",
29+
},
30+
};
31+
32+
expect(hasBuildCondition(exports, "workerd")).toBe(true);
33+
expect(hasBuildCondition(exports, "default")).toBe(true);
34+
expect(hasBuildCondition(exports, "module")).toBe(false);
35+
});
36+
37+
test("only consider leaves", () => {
38+
const exports = {
39+
".": "/path/to/index.js",
40+
"./server": {
41+
workerd: {
42+
default: "./server.edge.js",
43+
},
44+
},
45+
};
46+
47+
expect(hasBuildCondition(exports, "workerd")).toBe(false);
48+
});
49+
});
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import fs from "node:fs/promises";
2+
import path from "node:path";
3+
4+
import { loadConfig } from "@opennextjs/aws/adapters/config/util.js";
5+
import type { BuildOptions } from "@opennextjs/aws/build/helper.js";
6+
import logger from "@opennextjs/aws/logger.js";
7+
import { getCrossPlatformPathRegex } from "@opennextjs/aws/utils/regex.js";
8+
9+
/**
10+
* Return whether the passed export map has the given condition
11+
*/
12+
export function hasBuildCondition(
13+
exports: { [key: string]: unknown } | undefined,
14+
condition: string
15+
): boolean {
16+
if (!exports) {
17+
return false;
18+
}
19+
for (const [key, value] of Object.entries(exports)) {
20+
if (typeof value === "object" && value != null) {
21+
if (hasBuildCondition(value as { [key: string]: unknown }, condition)) {
22+
return true;
23+
}
24+
} else {
25+
if (key === condition) {
26+
return true;
27+
}
28+
}
29+
}
30+
return false;
31+
}
32+
33+
export async function copyWorkerdPackages(options: BuildOptions, nodePackages: Map<string, string>) {
34+
const isNodeModuleRegex = getCrossPlatformPathRegex(`.*/node_modules/(?<pkg>.*)`, { escape: false });
35+
36+
// Copy full external packages when they use "workerd" build condition
37+
const nextConfig = loadConfig(path.join(options.appBuildOutputPath, ".next"));
38+
const externalPackages = nextConfig.serverExternalPackages ?? [];
39+
for (const [src, dst] of nodePackages.entries()) {
40+
try {
41+
const { exports } = JSON.parse(await fs.readFile(path.join(src, "package.json"), "utf8"));
42+
const match = src.match(isNodeModuleRegex);
43+
if (
44+
match?.groups?.pkg &&
45+
externalPackages.includes(match.groups.pkg) &&
46+
hasBuildCondition(exports, "workerd")
47+
) {
48+
logger.debug(
49+
`Copying package using a workerd condition: ${path.relative(options.appPath, src)} -> ${path.relative(options.appPath, dst)}`
50+
);
51+
fs.cp(src, dst, { recursive: true, force: true });
52+
}
53+
} catch {
54+
logger.error(`Failed to copy ${src}`);
55+
}
56+
}
57+
}

packages/cloudflare/src/cli/commands/deploy.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { BuildOptions } from "@opennextjs/aws/build/helper.js";
2-
import { OpenNextConfig } from "@opennextjs/aws/types/open-next.js";
32

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

packages/cloudflare/src/cli/commands/preview.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { BuildOptions } from "@opennextjs/aws/build/helper.js";
2-
import { OpenNextConfig } from "@opennextjs/aws/types/open-next.js";
32

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

packages/cloudflare/src/cli/commands/upload.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { BuildOptions } from "@opennextjs/aws/build/helper.js";
2-
import { OpenNextConfig } from "@opennextjs/aws/types/open-next.js";
32

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

0 commit comments

Comments
 (0)