Skip to content
Draft
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
9 changes: 9 additions & 0 deletions .changeset/experimental-preserve-commonjs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@cloudflare/vite-plugin": minor
"@cloudflare/workers-utils": minor
"wrangler": minor
---

Experimentally preserve npm CommonJS module boundaries with the new module registry

When the `new_module_registry` compatibility flag is enabled, Wrangler and the Cloudflare Vite plugin now preserve statically reachable npm CommonJS files as runtime CommonJS modules instead of embedding them in the Worker ES module. This experimental path preserves CommonJS globals, relative `require()` calls, cycles, JSON imports, and ESM default and named interop across builds, local development, and Vite preview.
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,12 @@ describe("detectModuleType", () => {
expect(detectModuleType(filename)).toBe(expected);
}
);

it("prefers a preserved JavaScript module type over the file extension", ({
expect,
}) => {
expect(detectModuleType("package/index.js", "commonjs")).toBe("cjs");
expect(detectModuleType("package/data.json", "commonjs")).toBe("cjs");
expect(detectModuleType("package/index.cjs", "esmodule")).toBe("esm");
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { describe, it } from "vitest";
import {
getExperimentalCommonJsModuleName,
isExperimentalCommonJsModuleReference,
} from "../plugins/commonjs-module-registry";

describe("experimental CommonJS module references", () => {
it("finds direct and workerd file URL references", ({ expect }) => {
const emittedName = "__cloudflare_cjs__/abc/package/index.js";
const reference = `__CLOUDFLARE_CJS_MODULE__/worker/${emittedName}`;

expect(getExperimentalCommonJsModuleName(reference)).toBe(emittedName);
expect(
getExperimentalCommonJsModuleName(`file:///bundle/${reference}`)
).toBe(emittedName);
expect(isExperimentalCommonJsModuleReference("ordinary-package")).toBe(
false
);
});
});
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import { describe, test } from "vitest";
import { getModulesFromManifest } from "../miniflare-options";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { removeDirSync } from "@cloudflare/workers-utils";
import { describe, onTestFinished, test } from "vitest";
import {
getModulesFromManifest,
getPreviewModules,
} from "../miniflare-options";

describe("getModulesFromManifest", () => {
test("hoists `mainModule` to index 0 even when it is not first in the manifest", ({
Expand Down Expand Up @@ -100,3 +107,28 @@ describe("getModulesFromManifest", () => {
).toThrow(/`mainModule` "missing\.js" is missing from `modules`/);
});
});

describe("getPreviewModules", () => {
test("uses the first matching rule when globs overlap", ({ expect }) => {
const root = fs.mkdtempSync(
path.join(os.tmpdir(), "vite-preview-modules-")
);
onTestFinished(() => removeDirSync(root));
fs.mkdirSync(path.join(root, "preserved"));
fs.writeFileSync(path.join(root, "index.js"), "export default {};");
fs.writeFileSync(
path.join(root, "preserved", "module.js"),
"module.exports = {};"
);

const result = getPreviewModules(path.join(root, "index.js"), [
{ type: "CommonJS", include: ["preserved/module.js"] },
{ type: "ESModule", include: ["**/*.js"] },
]);

expect(result.modules).toEqual([
{ type: "ESModule", path: "index.js" },
{ type: "CommonJS", path: "preserved/module.js" },
]);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -148,4 +148,32 @@ describe("getWorkerOutputConfig", () => {
},
]);
});

test("declares preserved CommonJS modules before the broad ESM rule", ({
expect,
}) => {
const root = createRoot();
const outputConfig = getOutputConfig({
inputWorkerConfig: workerConfig(root),
workerOutputDirectory: "dist/api_worker",
resolvedViteConfig: resolvedViteConfig(root),
entryFileName: "index.js",
includeAssets: false,
commonJsModuleNames: [
"__cloudflare_cjs__/abc/package/index.js",
"__cloudflare_cjs__/abc/package/data.json",
],
});

expect(outputConfig.rules).toEqual([
{
type: "CommonJS",
globs: [
"__cloudflare_cjs__/abc/package/index.js",
"__cloudflare_cjs__/abc/package/data.json",
],
},
{ type: "ESModule", globs: ["**/*.js", "**/*.mjs"] },
]);
});
});
13 changes: 11 additions & 2 deletions packages/vite-plugin-cloudflare/src/cloudflare-environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { CoreHeaders } from "miniflare";
import * as vite from "vite";
import { nodeBuiltinsRE } from "./nodejs-compat";
import { additionalModuleRE } from "./plugins/additional-modules";
import { isExperimentalCommonJsModuleReference } from "./plugins/commonjs-module-registry";
import {
ENVIRONMENT_NAME_HEADER,
GET_EXPORT_TYPES_PATH,
Expand Down Expand Up @@ -167,7 +168,10 @@ export class CloudflareDevEnvironment extends vite.DevEnvironment {
options?: FetchFunctionOptions
): Promise<vite.FetchResult> {
// Additional modules (CompiledWasm, Data, Text)
if (additionalModuleRE.test(id)) {
if (
additionalModuleRE.test(id) ||
isExperimentalCommonJsModuleReference(id)
) {
return {
externalize: id,
type: "module",
Expand Down Expand Up @@ -222,6 +226,9 @@ export function createCloudflareEnvironmentOptions({
isParentEnvironment: boolean;
hasNodeJsCompat: boolean;
}): vite.EnvironmentOptions {
const preserveCommonJs = workerConfig.compatibility_flags?.includes(
"new_module_registry"
);
const rollupOptions = isParentEnvironment
? {
input: {
Expand Down Expand Up @@ -288,7 +295,9 @@ export function createCloudflareEnvironmentOptions({
},
optimizeDeps: {
// Note: ssr pre-bundling is opt-in and we need to enable it by setting `noDiscovery` to false
noDiscovery: false,
// CommonJS must reach the registry plugin before optimization converts it to ESM.
noDiscovery: preserveCommonJs,
include: preserveCommonJs ? [] : undefined,
// Workaround for https://github.com/vitejs/vite/issues/20867
// Longer term solution is to use full-bundle mode rather than `optimizeDeps`
ignoreOutdatedRequests: true,
Expand Down
2 changes: 2 additions & 0 deletions packages/vite-plugin-cloudflare/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { PluginContext } from "./context";
import { resolvePluginConfig } from "./plugin-config";
import { additionalModulesPlugin } from "./plugins/additional-modules";
import { buildOutputPlugin } from "./plugins/build-output";
import { commonJsModuleRegistryPlugin } from "./plugins/commonjs-module-registry";
import { configPlugin } from "./plugins/config";
import { debugPlugin } from "./plugins/debug";
import { devPlugin } from "./plugins/dev";
Expand Down Expand Up @@ -115,6 +116,7 @@ export function cloudflare(pluginConfig: PluginConfig = {}): vite.Plugin[] {
triggerHandlersPlugin(ctx),
virtualModulesPlugin(ctx),
virtualClientFallbackPlugin(ctx),
commonJsModuleRegistryPlugin(ctx),
outputPlugin,
wasmHelperPlugin(ctx),
additionalModulesPlugin(ctx),
Expand Down
30 changes: 25 additions & 5 deletions packages/vite-plugin-cloudflare/src/miniflare-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import {
import { getContainerOptions, getDockerPath } from "./containers";
import { getInputInspectorPort } from "./debug";
import { additionalModuleRE } from "./plugins/additional-modules";
import { getExperimentalCommonJsModule } from "./plugins/commonjs-module-registry";
import { ENVIRONMENT_NAME_HEADER } from "./shared";
import { checkForNpmUpdate } from "./update-check";
import {
Expand Down Expand Up @@ -596,6 +597,18 @@ export async function getDevMiniflareOptions(
}

const rawSpecifier = parsed.rawSpecifier;
const commonJsModule = getExperimentalCommonJsModule(
ctx,
parsed.specifier
);
if (commonJsModule !== undefined) {
return MiniflareResponse.json({
name: parsed.specifier,
[commonJsModule.sourceType === "commonjs"
? "commonJsModule"
: "esModule"]: commonJsModule.transformedSource,
});
}
assert(
rawSpecifier,
`Unexpected error: no specifier in request to module fallback service.`
Expand Down Expand Up @@ -645,14 +658,15 @@ export async function getDevMiniflareOptions(
};
}

function getPreviewModules(
export function getPreviewModules(
main: string,
modulesRules: SourcelessWorkerOptions["modulesRules"]
) {
assert(modulesRules, `Unexpected error: 'modulesRules' is undefined`);
const rootPath = path.dirname(main);
const entryPath = path.basename(main);

const seen = new Set([entryPath]);
return {
rootPath,
modules: [
Expand All @@ -661,12 +675,18 @@ function getPreviewModules(
path: entryPath,
} as const,
...modulesRules.flatMap(({ type, include }) =>
globSync(include, { cwd: rootPath, ignore: entryPath }).map(
(globPath) => ({
globSync(include, { cwd: rootPath, ignore: entryPath })
.filter((globPath) => {
if (seen.has(globPath)) {
return false;
}
seen.add(globPath);
return true;
})
.map((globPath) => ({
type,
path: globPath,
})
)
}))
),
],
} satisfies Pick<WorkerOptions, "rootPath" | "modules">;
Expand Down
18 changes: 16 additions & 2 deletions packages/vite-plugin-cloudflare/src/plugins/build-output.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ import {
} from "@cloudflare/build-output-utils";
import { MAIN_ENTRY_NAME } from "../cloudflare-environment";
import { createPlugin } from "../utils";
import { getExperimentalCommonJsModuleTypes } from "./commonjs-module-registry";
import type { ModuleType } from "@cloudflare/config";
import type { ExperimentalJavaScriptSourceType } from "@cloudflare/workers-utils";

/**
* Build Output Specification plugin. Replaces `outputConfigPlugin` when
Expand Down Expand Up @@ -69,6 +71,10 @@ export const buildOutputPlugin = createPlugin("build-output", (ctx) => {
}

const modules: Record<string, { type: ModuleType }> = {};
const commonJsModuleTypes = getExperimentalCommonJsModuleTypes(
ctx,
this.environment.name
);
for (const fileName of Object.keys(bundle)) {
// Skip Vite's own manifest emitted via `build.manifest: true`.
if (fileName === ".vite/manifest.json") {
Expand All @@ -80,7 +86,9 @@ export const buildOutputPlugin = createPlugin("build-output", (ctx) => {
if (importedAssetPaths.has(fileName)) {
continue;
}
modules[fileName] = { type: detectModuleType(fileName) };
modules[fileName] = {
type: detectModuleType(fileName, commonJsModuleTypes.get(fileName)),
};
}

await writeWorkerConfig(ctx.resolvedViteConfig.root, workerNewConfig, {
Expand Down Expand Up @@ -108,7 +116,13 @@ export const buildOutputPlugin = createPlugin("build-output", (ctx) => {
/**
* Map a bundle filename to its declared module type.
*/
export function detectModuleType(filename: string): ModuleType {
export function detectModuleType(
filename: string,
explicitType?: ExperimentalJavaScriptSourceType
): ModuleType {
if (explicitType !== undefined) {
return explicitType === "commonjs" ? "cjs" : "esm";
}
const ext = path.extname(filename).toLowerCase();

switch (ext) {
Expand Down
Loading
Loading