Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
2 changes: 1 addition & 1 deletion packages/cloudflare/src/cli/build/bundle-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ export async function bundleServer(buildOpts: BuildOptions): Promise<void> {
// Apply updater updates, must be the last plugin
updater.plugin,
] as Plugin[],
external: ["./middleware/handler.mjs", "*.wasm"],
external: ["./middleware/handler.mjs"],
alias: {
// Note: it looks like node-fetch is actually not necessary for us, so we could replace it with an empty shim
// but just to be safe we replace it with a module that re-exports the native fetch
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ export function setWranglerExternal() {
setup: async (build: PluginBuild) => {
const namespace = "wrangler-externals-plugin";

build.onResolve({ filter: /(\.bin|\.wasm\?module)$/ }, ({ path, importer }) => {
//TODO: Ideally in the future we would like to analyze the files in case they are using wasm in a Node way (i.e. WebAssembly.instantiate)
build.onResolve({ filter: /(\.bin|\.wasm(\?module))$/ }, ({ path, importer }) => {
return {
path: resolve(dirname(importer), path),
namespace,
Expand Down
186 changes: 173 additions & 13 deletions packages/cloudflare/src/cli/build/utils/workerd.spec.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,31 @@
import { describe, expect, test } from "vitest";

import { hasBuildCondition } from "./workerd";

describe("hasBuildConditions", () => {
test("undefined", () => {
expect(hasBuildCondition(undefined, "workerd")).toBe(false);
});
import { transformBuildCondition, transformPackageJson } from "./workerd";

describe("transformBuildCondition", () => {
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);
const workerd = transformBuildCondition(exports, "workerd");
const defaultExport = transformBuildCondition(exports, "default");
const moduleExport = transformBuildCondition(exports, "module");

expect(workerd.hasBuildCondition).toBe(true);
expect(workerd.transformedExports).toEqual({
workerd: "./path/to/workerd.js",
});
expect(defaultExport.hasBuildCondition).toBe(true);
expect(defaultExport.transformedExports).toEqual({
default: "./path/to/default.js",
});
expect(moduleExport.hasBuildCondition).toBe(false);
expect(moduleExport.transformedExports).toEqual({
workerd: "./path/to/workerd.js",
default: "./path/to/default.js",
});
});

test("nested", () => {
Expand All @@ -24,14 +34,50 @@ describe("hasBuildConditions", () => {
"./server": {
"react-server": {
workerd: "./server.edge.js",
other: "./server.js",
},
default: "./server.js",
},
};

expect(hasBuildCondition(exports, "workerd")).toBe(true);
expect(hasBuildCondition(exports, "default")).toBe(true);
expect(hasBuildCondition(exports, "module")).toBe(false);
const workerd = transformBuildCondition(exports, "workerd");
const defaultExport = transformBuildCondition(exports, "default");
const moduleExport = transformBuildCondition(exports, "module");

expect(workerd.hasBuildCondition).toBe(true);
expect(workerd.transformedExports).toEqual({
".": "/path/to/index.js",
"./server": {
"react-server": {
workerd: "./server.edge.js",
},
default: "./server.js",
},
});

expect(defaultExport.hasBuildCondition).toBe(true);
expect(defaultExport.transformedExports).toEqual({
".": "/path/to/index.js",
"./server": {
"react-server": {
workerd: "./server.edge.js",
other: "./server.js",
},
default: "./server.js",
},
});

expect(moduleExport.hasBuildCondition).toBe(false);
expect(moduleExport.transformedExports).toEqual({
".": "/path/to/index.js",
"./server": {
"react-server": {
workerd: "./server.edge.js",
other: "./server.js",
},
default: "./server.js",
},
});
});

test("only consider leaves", () => {
Expand All @@ -44,6 +90,120 @@ describe("hasBuildConditions", () => {
},
};

expect(hasBuildCondition(exports, "workerd")).toBe(false);
const workerd = transformBuildCondition(exports, "workerd");

expect(workerd.hasBuildCondition).toBe(false);
expect(workerd.transformedExports).toEqual({
".": "/path/to/index.js",
"./server": {
workerd: {
default: "./server.edge.js",
},
},
});
});
});

describe("transformPackageJson", () => {
test("no exports nor imports", () => {
const json = {
name: "test",
main: "index.js",
version: "1.0.0",
description: "test package",
};

const { transformed, hasBuildCondition } = transformPackageJson(json);

expect(transformed).toEqual(json);
expect(hasBuildCondition).toBe(false);
});

test("exports only with no workerd condition", () => {
const json = {
name: "test",
exports: {
".": "./index.js",
"./server": "./server.js",
},
};

const { transformed, hasBuildCondition } = transformPackageJson(json);

expect(transformed).toEqual(json);
expect(hasBuildCondition).toBe(false);
});

test("exports only with nested workerd condition", () => {
const json = {
name: "test",
exports: {
".": "./index.js",
"./server": {
workerd: "./server.edge.js",
other: "./server.js",
},
},
};
const { transformed, hasBuildCondition } = transformPackageJson(json);
expect(transformed).toEqual({
name: "test",
exports: {
".": "./index.js",
"./server": {
workerd: "./server.edge.js",
},
},
});
expect(hasBuildCondition).toBe(true);
});

test("imports only with top level workerd condition", () => {
const json = {
name: "test",
imports: {
workerd: "./server.edge.js",
default: "./server.js",
},
};
const { transformed, hasBuildCondition } = transformPackageJson(json);
expect(transformed).toEqual({
name: "test",
imports: {
workerd: "./server.edge.js",
},
});
expect(hasBuildCondition).toBe(true);
});

test("exports and imports with workerd condition both nested and top level", () => {
const json = {
name: "test",
exports: {
".": "./index.js",
"./server": {
workerd: "./server.edge.js",
other: "./server.js",
},
},
imports: {
workerd: "./server.edge.js",
default: "./server.js",
},
};
const { transformed, hasBuildCondition } = transformPackageJson(json);
expect(transformed).toEqual({
name: "test",
exports: {
".": "./index.js",
"./server": {
workerd: "./server.edge.js",
},
},
imports: {
workerd: "./server.edge.js",
},
});
expect(hasBuildCondition).toBe(true);
});
});
79 changes: 59 additions & 20 deletions packages/cloudflare/src/cli/build/utils/workerd.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,27 +7,67 @@ 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
* This function transforms the exports (or imports) object from the package.json
* to only include the build condition if found (e.g. "workerd") and remove everything else.
* If no build condition is found, it keeps everything as is.
* It also returns a boolean indicating if the build condition was found.
* @param exports The exports (or imports) object from the package.json
* @param condition The build condition to look for
* @returns An object with the transformed exports and a boolean indicating if the build condition was found
*/
export function hasBuildCondition(
exports: { [key: string]: unknown } | undefined,
condition: string
): boolean {
if (!exports) {
return false;
}
export function transformBuildCondition(exports: { [key: string]: unknown }, condition: string) {
const transformed: { [key: string]: unknown } = {};
const hasTopLevelBuildCondition = Object.keys(exports).some(
(key) => key === condition && typeof exports[key] === "string"
);
let hasBuildCondition = hasTopLevelBuildCondition;
for (const [key, value] of Object.entries(exports)) {
if (typeof value === "object" && value != null) {
if (hasBuildCondition(value as { [key: string]: unknown }, condition)) {
return true;
}
const { transformedExports, hasBuildCondition: innerBuildCondition } = transformBuildCondition(
value as { [key: string]: unknown },
condition
);
transformed[key] = transformedExports;
hasBuildCondition = hasBuildCondition || innerBuildCondition;
} else {
if (key === condition) {
return true;
// If it doesn't have the build condition, we need to keep everything as is
// If it has the build condition, we need to keep only the build condition
// and remove everything else
if (!hasTopLevelBuildCondition) {
transformed[key] = value;
} else if (key === condition) {
transformed[key] = value;
}
}
}
return false;
return { transformedExports: transformed, hasBuildCondition };
}
// We only care about these 2 fields
interface PackageJson {
name: string;
exports?: { [key: string]: unknown };
imports?: { [key: string]: unknown };
}

/**
*
* @param json The package.json object
* @returns An object with the transformed package.json and a boolean indicating if the build condition was found
*/
export function transformPackageJson(json: PackageJson) {
const transformed: PackageJson = { ...json };
let hasBuildCondition = false;
if (json.exports) {
const exp = transformBuildCondition(json.exports, "workerd");
transformed.exports = exp.transformedExports;
hasBuildCondition = exp.hasBuildCondition;
}
if (json.imports) {
const imp = transformBuildCondition(json.imports, "workerd");
transformed.imports = imp.transformedExports;
hasBuildCondition = hasBuildCondition || imp.hasBuildCondition;
}
return { transformed, hasBuildCondition };
}

export async function copyWorkerdPackages(options: BuildOptions, nodePackages: Map<string, string>) {
Expand All @@ -38,17 +78,16 @@ export async function copyWorkerdPackages(options: BuildOptions, nodePackages: M
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 pkgJson = JSON.parse(await fs.readFile(path.join(src, "package.json"), "utf8"));
const { transformed, hasBuildCondition } = transformPackageJson(pkgJson);
const match = src.match(isNodeModuleRegex);
if (
match?.groups?.pkg &&
externalPackages.includes(match.groups.pkg) &&
hasBuildCondition(exports, "workerd")
) {
if (match?.groups?.pkg && externalPackages.includes(match.groups.pkg) && hasBuildCondition) {
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 });
// Write the transformed package.json
await fs.writeFile(path.join(dst, "package.json"), JSON.stringify(transformed), "utf8");
}
} catch {
logger.error(`Failed to copy ${src}`);
Expand Down