Skip to content
This repository was archived by the owner on Mar 13, 2025. It is now read-only.

Commit 24916c6

Browse files
authored
Implement export_commonjs_default/export_commonjs_namespace flags (#468)
* Implement `export_commonjs_default`/`export_commonjs_namespace` flags Previously, the Workers runtime would incorrectly return `{ default: module.exports }` from `require()`, as opposed to just `module.exports`. The `export_commonjs_default` compatibility flag enables the correct behaviour. Miniflare previously implemented `export_commonjs_default` behaviour for `CommonJS` modules, but `export_commonjs_namespace` behaviour for all other types. This change switches everything to the correct `export_commonjs_default` by default, but allows old behaviour to be enabled by setting `export_commonjs_namespace`. Note, importing `CommonJS` from an `ESModule` is not affected. Ref: https://developers.cloudflare.com/workers/platform/compatibility-dates/#commonjs-modules-do-not-export-a-module-namespace * fixup! Implement `export_commonjs_default`/`export_commonjs_namespace` flags
1 parent 0cb151e commit 24916c6

File tree

11 files changed

+106
-23
lines changed

11 files changed

+106
-23
lines changed

packages/core/src/index.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -776,7 +776,8 @@ export class MiniflareCore<
776776
globalScope,
777777
script,
778778
rules,
779-
additionalModules
779+
additionalModules,
780+
this.#compat
780781
);
781782

782783
this.#scriptWatchPaths.clear();

packages/runner-vm/src/index.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import vm from "vm";
22
import {
33
AdditionalModules,
4+
Compatibility,
45
Context,
56
ProcessedModuleRule,
67
ScriptBlueprint,
@@ -44,7 +45,8 @@ export class VMScriptRunner implements ScriptRunner {
4445
globalScope: Context,
4546
blueprint: ScriptBlueprint,
4647
modulesRules?: ProcessedModuleRule[],
47-
additionalModules?: AdditionalModules
48+
additionalModules?: AdditionalModules,
49+
compat?: Compatibility
4850
): Promise<ScriptRunnerResult> {
4951
// If we're using modules, make sure --experimental-vm-modules is enabled
5052
if (modulesRules && !("SourceTextModule" in vm)) {
@@ -55,7 +57,8 @@ export class VMScriptRunner implements ScriptRunner {
5557
}
5658
// Also build a linker if we're using modules
5759
const linker =
58-
modulesRules && new ModuleLinker(modulesRules, additionalModules ?? {});
60+
modulesRules &&
61+
new ModuleLinker(modulesRules, additionalModules ?? {}, compat);
5962

6063
let context = this.context;
6164
if (context) {

packages/runner-vm/src/linker.ts

+35-13
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import path from "path";
44
import vm, { SourceTextModuleImportModuleDynamically } from "vm";
55
import {
66
AdditionalModules,
7+
Compatibility,
78
Context,
89
ProcessedModuleRule,
910
STRING_SCRIPT_PATH,
@@ -32,10 +33,14 @@ export class ModuleLinker {
3233
readonly #referencedPathSizes = new Map<string, number>();
3334
readonly #moduleCache = new Map<string, vm.Module>();
3435
readonly #cjsModuleCache = new Map<string, CommonJSModule>();
36+
// Make sure we always return the same objects from `require()` when the
37+
// `export_commonjs_default` compatibility flag is disabled
38+
readonly #namespaceCache = new WeakMap<any, any>();
3539

3640
constructor(
37-
private moduleRules: ProcessedModuleRule[],
38-
private additionalModules: AdditionalModules
41+
private readonly moduleRules: ProcessedModuleRule[],
42+
private readonly additionalModules: AdditionalModules,
43+
private readonly compat?: Compatibility
3944
) {}
4045

4146
get referencedPaths(): IterableIterator<string> {
@@ -110,7 +115,7 @@ export class ModuleLinker {
110115
});
111116
break;
112117
case "CommonJS":
113-
const exports = this.loadCommonJSModule(
118+
const mod = this.loadCommonJSModule(
114119
errorBase,
115120
identifier,
116121
spec,
@@ -119,7 +124,7 @@ export class ModuleLinker {
119124
module = new vm.SyntheticModule<{ default: Context }>(
120125
["default"],
121126
function () {
122-
this.setExport("default", exports);
127+
this.setExport("default", mod.exports);
123128
},
124129
moduleOptions
125130
);
@@ -190,20 +195,20 @@ export class ModuleLinker {
190195
identifier: string,
191196
spec: string,
192197
context: vm.Context
193-
): any {
198+
): CommonJSModule {
194199
// If we've already seen a module with the same identifier, return it, to
195200
// handle import cycles
196201
const cached = this.#cjsModuleCache.get(identifier);
197-
if (cached) return cached.exports;
202+
if (cached) return cached;
198203

199204
const additionalModule = this.additionalModules[spec];
200205
const module: CommonJSModule = { exports: {} };
201206

202207
// If this is an additional module, return it immediately
203208
if (additionalModule) {
204-
module.exports.default = additionalModule.default;
209+
module.exports = additionalModule.default;
205210
this.#cjsModuleCache.set(identifier, module);
206-
return module.exports;
211+
return module;
207212
}
208213

209214
const rule = this.moduleRules.find((rule) => rule.include.test(identifier));
@@ -242,21 +247,21 @@ export class ModuleLinker {
242247
moduleWrapper(module.exports, require, module);
243248
break;
244249
case "Text":
245-
module.exports.default = data.toString("utf8");
250+
module.exports = data.toString("utf8");
246251
break;
247252
case "Data":
248-
module.exports.default = viewToBuffer(data);
253+
module.exports = viewToBuffer(data);
249254
break;
250255
case "CompiledWasm":
251-
module.exports.default = new WebAssembly.Module(data);
256+
module.exports = new WebAssembly.Module(data);
252257
break;
253258
default:
254259
throw new VMScriptRunnerError(
255260
"ERR_MODULE_UNSUPPORTED",
256261
`${errorBase}: ${rule.type} modules are unsupported`
257262
);
258263
}
259-
return module.exports;
264+
return module;
260265
}
261266

262267
private createRequire(referencingIdentifier: string, context: vm.Context) {
@@ -266,7 +271,24 @@ export class ModuleLinker {
266271
const errorBase = `Unable to resolve "${relative}" dependency "${spec}"`;
267272
// Get path to specified module relative to referencing module
268273
const identifier = path.resolve(referencingDirname, spec);
269-
return this.loadCommonJSModule(errorBase, identifier, spec, context);
274+
const mod = this.loadCommonJSModule(errorBase, identifier, spec, context);
275+
// https://developers.cloudflare.com/workers/platform/compatibility-dates/#commonjs-modules-do-not-export-a-module-namespace
276+
if (
277+
this.compat === undefined ||
278+
this.compat.isEnabled("export_commonjs_default")
279+
) {
280+
return mod.exports;
281+
} else {
282+
// Make sure we always return the same object for an identifier
283+
let ns = this.#namespaceCache.get(mod);
284+
if (ns !== undefined) return ns;
285+
ns = Object.defineProperty({}, "default", {
286+
get: () => mod.exports,
287+
enumerable: true,
288+
});
289+
this.#namespaceCache.set(mod, ns);
290+
return ns;
291+
}
270292
};
271293
}
272294
}
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
const additional = require("ADDITIONAL");
2-
module.exports = `CommonJS ${additional.default}`;
2+
module.exports = `CommonJS ${additional}`;
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
const addModule = require("./add.wasm");
2-
const instance = new WebAssembly.Instance(addModule.default);
2+
const instance = new WebAssembly.Instance(addModule);
33
exports.add1 = (a) => instance.exports.add(a, 1);
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
const data = require("./data.bin");
2-
module.exports = `CommonJS ${new TextDecoder().decode(data.default).trimEnd()}`;
2+
module.exports = `CommonJS ${new TextDecoder().decode(data).trimEnd()}`;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
const cjs = require("./cjs.cjs");
2+
const txt = require("./text.txt");
3+
const txt2 = require("./text.txt");
4+
module.exports = function () {
5+
return { cjs, txt, txt2 };
6+
};
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
const text = require("./text.txt");
2-
module.exports = `CommonJS ${text.default.trimEnd()}`;
2+
module.exports = `CommonJS ${text.trimEnd()}`;

packages/runner-vm/test/linker.spec.ts

+44-2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import path from "path";
44
import { TextDecoder } from "util";
55
import { VMScriptRunner, VMScriptRunnerError } from "@miniflare/runner-vm";
66
import {
7+
Compatibility,
78
Context,
89
ModuleRule,
910
ProcessedModuleRule,
@@ -35,9 +36,16 @@ const processedModuleRules = moduleRules.map<ProcessedModuleRule>((rule) => ({
3536

3637
async function run(
3738
code: string,
38-
globalScope: Context = {}
39+
globalScope: Context = {},
40+
compat?: Compatibility
3941
): Promise<ScriptRunnerResult> {
40-
return runner.run(globalScope, { code, filePath }, processedModuleRules);
42+
return runner.run(
43+
globalScope,
44+
{ code, filePath },
45+
processedModuleRules,
46+
undefined,
47+
compat
48+
);
4149
}
4250

4351
test("ModuleLinker: links ESModule module via ES module", async (t) => {
@@ -310,3 +318,37 @@ test("ModuleLinker: permits dynamic import of statically linked module", async (
310318
dynamic: "ESModule test",
311319
});
312320
});
321+
322+
test("ModuleLinker: respects export_commonjs_namespace compatibility flag", async (t) => {
323+
let compat = new Compatibility(undefined, ["export_commonjs_default"]);
324+
let result = await run(
325+
`
326+
import ns from "./cjsnamespace.cjs";
327+
export default function() {
328+
return ns;
329+
}
330+
`,
331+
undefined,
332+
compat
333+
);
334+
let exports = await result.exports.default()();
335+
t.is(exports.cjs, "CommonJS test");
336+
t.is(exports.txt.trimEnd(), "Text test");
337+
t.is(exports.txt, exports.txt2);
338+
339+
compat = new Compatibility(undefined, ["export_commonjs_namespace"]);
340+
result = await run(
341+
`
342+
import ns from "./cjsnamespace.cjs";
343+
export default function() {
344+
return ns;
345+
}
346+
`,
347+
undefined,
348+
compat
349+
);
350+
exports = await result.exports.default()();
351+
t.is(exports.cjs.default, "CommonJS test");
352+
t.is(exports.txt.default.trimEnd(), "Text test");
353+
t.is(exports.txt, exports.txt2);
354+
});

packages/shared/src/compat.ts

+7
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export type CompatibilityEnableFlag =
1414
| "nodejs_compat"
1515
| "streams_enable_constructors"
1616
| "transformstream_enable_standard_constructor"
17+
| "export_commonjs_default"
1718
| "r2_list_honor_include"
1819
| "global_navigator"
1920
| "durable_object_fetch_requires_full_url"
@@ -23,6 +24,7 @@ export type CompatibilityEnableFlag =
2324
export type CompatibilityDisableFlag =
2425
| "streams_disable_constructors"
2526
| "transformstream_disable_standard_constructor"
27+
| "export_commonjs_namespace"
2628
| "no_global_navigator"
2729
| "durable_object_fetch_allows_relative_url"
2830
| "fetch_treats_unknown_protocols_as_http"
@@ -45,6 +47,11 @@ const FEATURES: CompatibilityFeature[] = [
4547
enableFlag: "transformstream_enable_standard_constructor",
4648
disableFlag: "transformstream_disable_standard_constructor",
4749
},
50+
{
51+
defaultAsOf: "2022-10-31",
52+
enableFlag: "export_commonjs_default",
53+
disableFlag: "export_commonjs_namespace",
54+
},
4855
{
4956
defaultAsOf: "2022-08-04",
5057
enableFlag: "r2_list_honor_include",

packages/shared/src/runner.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { Compatibility } from "./compat";
12
import { Matcher } from "./data";
23
import { AdditionalModules, Context } from "./plugin";
34

@@ -38,6 +39,7 @@ export interface ScriptRunner {
3839
globalScope: Context,
3940
blueprint: ScriptBlueprint,
4041
modulesRules?: ProcessedModuleRule[],
41-
additionalModules?: AdditionalModules
42+
additionalModules?: AdditionalModules,
43+
compat?: Compatibility
4244
): Promise<ScriptRunnerResult>;
4345
}

0 commit comments

Comments
 (0)