Skip to content
Open
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
10 changes: 7 additions & 3 deletions src/workerd/api/tests/wasm-source-url-test.wd-test
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,16 @@ using Workerd = import "/workerd/workerd.capnp";
# must recompile it rather than keep this worker's URL.
const unitTests :Workerd.Config = (
services = [
# With the flag, the module's frames name it by its module name.
# With the flag and the legacy registry, the module's frames name it by its module name.
( name = "wasm-source-url-test",
worker = (
modules = [
(name = "worker", esModule = embed "wasm-source-url-test.js"),
(name = "trap.wasm", wasm = embed "wasm-source-url.wasm"),
],
compatibilityFlags = ["nodejs_compat", "experimental", "wasm_source_url_support"],
compatibilityFlags = [
"nodejs_compat", "experimental", "wasm_source_url_support", "legacy_module_registry",
],
bindings = [
(name = "expectedFrame", text = "at trap.wasm:wasm-function[0]:0x"),
(name = "childFlag", text = "no_wasm_source_url_support"),
Expand Down Expand Up @@ -45,7 +47,9 @@ const unitTests :Workerd.Config = (
(name = "worker", esModule = embed "wasm-source-url-test.js"),
(name = "trap.wasm", wasm = embed "wasm-source-url.wasm"),
],
compatibilityFlags = ["nodejs_compat", "experimental", "no_wasm_source_url_support"],
compatibilityFlags = [
"nodejs_compat", "experimental", "no_wasm_source_url_support", "legacy_module_registry",
],
bindings = [
(name = "expectedFrame", text = "at wasm://wasm/"),
(name = "childFlag", text = "wasm_source_url_support"),
Expand Down
29 changes: 28 additions & 1 deletion src/workerd/io/compatibility-date-test.c++
Original file line number Diff line number Diff line change
Expand Up @@ -589,6 +589,33 @@ KJ_TEST("encode to full flag list") {
}
}

KJ_TEST("new module registry defaults on at 2026-11-17") {
auto compileFlags = [](kj::StringPtr date, kj::ArrayPtr<const kj::StringPtr> flags) {
capnp::MallocMessageBuilder message;
auto flagList = message.initRoot<capnp::List<capnp::Text>>(flags.size());
for (auto i: kj::indices(flags)) {
flagList.set(i, flags[i]);
}
auto output = message.getOrphanage().newOrphan<CompatibilityFlags>();
SimpleWorkerErrorReporter errorReporter;
compileCompatibilityFlags(date, flagList.asReader(), output.get(), errorReporter,
/*allowExperimentalFeatures=*/false, CompatibilityDateValidation::FUTURE_FOR_TEST, nullptr);
KJ_ASSERT(errorReporter.errors.empty());
return capnp::clone(output.getReader());
};

auto before = compileFlags("2026-11-16", {});
KJ_EXPECT(!isNewModuleRegistryEnabled(*before));
auto enabled = compileFlags("2026-11-17", {});
KJ_EXPECT(isNewModuleRegistryEnabled(*enabled));
auto optedIn = compileFlags("2026-11-16", {"new_module_registry"_kj});
KJ_EXPECT(isNewModuleRegistryEnabled(*optedIn));
auto optedOut = compileFlags("2026-11-17", {"legacy_module_registry"_kj});
KJ_EXPECT(!isNewModuleRegistryEnabled(*optedOut));
auto python = compileFlags("2026-11-17", {"python_workers"_kj});
KJ_EXPECT(!isNewModuleRegistryEnabled(*python));
}

KJ_TEST("compatibility dates must be Tuesday, Wednesday, or Thursday") {
// List of specific flags that are allowed to use non-conformant dates
// (already deployed and can't be changed for compatibility reasons)
Expand Down Expand Up @@ -766,7 +793,7 @@ KJ_TEST("isNewModuleRegistryEnabled ignores the flag for Python workers") {
KJ_EXPECT(check(true, false));
// Python workers always use the original module registry: the
// new_module_registry flag is ignored for them, whether set explicitly or
// (once the flag has a default-on date) implied by the compatibility date.
// implied by the compatibility date.
KJ_EXPECT(!check(true, true));
KJ_EXPECT(!check(false, true));
}
Expand Down
3 changes: 2 additions & 1 deletion src/workerd/io/compatibility-date.capnp
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# Copyright (c) 2017-2022 Cloudflare, Inc.
# Licensed under the Apache 2.0 license found in the LICENSE file or at:
# https://opensource.org/licenses/Apache-2.0
Expand Down Expand Up @@ -513,7 +513,8 @@

newModuleRegistry @52 :Bool
$compatEnableFlag("new_module_registry")
$compatDisableFlag("legacy_module_registry");
$compatDisableFlag("legacy_module_registry")
$compatEnableDate("2026-11-17");
# Enables the new module registry implementation, which handles module
# specifiers as URLs, implements import.meta (url/main/resolve), supports
# import attributes, and allows a registry to be shared across isolate
Expand Down
8 changes: 4 additions & 4 deletions src/workerd/io/features.h
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,10 @@ inline void configureJsgIsolate(jsg::Lock& js, CompatibilityFlags::Reader flags)
// Returns whether the new module registry implementation should be used for a
// worker with the given flags. Python workers always use the original module
// registry: the new_module_registry flag is ignored for them, whether it was
// listed explicitly or (once the flag gains an enable date) implied by the
// worker's compatibility date. Every decision about which registry to use MUST
// go through this function rather than reading getNewModuleRegistry() directly;
// otherwise a Python worker could end up split across the two registries.
// listed explicitly or implied by the worker's compatibility date. Every decision
// about which registry to use MUST go through this function rather than reading
// getNewModuleRegistry() directly; otherwise a Python worker could end up split
// across the two registries.
//
// Note that Cloudflare.compatibilityFlags.new_module_registry (as observed by
// user code) still reflects the configured flag value, not this function's
Expand Down
10 changes: 4 additions & 6 deletions src/workerd/server/factory/worker-factory.c++
Original file line number Diff line number Diff line change
Expand Up @@ -1567,13 +1567,14 @@ kj::Promise<kj::Own<CompiledWorker>> factory_new_worker(const WorkerFactory& con
// The file system roots use their defaults; the config does not expose mount points.
auto workerFs = newWorkerFileSystem(kj::heap<FsMap>(), getBundleDirectory(workerSource));

KJ_REQUIRE(experimental || moduleFallback == kj::none,
"The module fallback service is an experimental feature. "
"You must run workerd with `--experimental` to use the module fallback service.");

// Python workers never use the new module registry, whatever their flags say.
bool usingNewModuleRegistry = isNewModuleRegistryEnabled(featureFlags);
kj::Maybe<kj::Arc<jsg::modules::ModuleRegistry>> newModuleRegistry;
if (usingNewModuleRegistry) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[WARNING] A moduleFallback is still an experimental feature, but this branch now bypasses its only --experimental check. newWorkerdModuleRegistry() receives moduleFallback at lines 1577-1580 and adds an active fallback bundle when it is present (workerd-api.c++:944-983), while the remaining check is inside if (!usingNewModuleRegistry) at lines 1666-1670. Therefore a worker at the new default date can make fallback-service requests without --experimental.

Suggested change
if (usingNewModuleRegistry) {
if (usingNewModuleRegistry) {
KJ_REQUIRE(experimental || moduleFallback == kj::none,
"The module fallback service is an experimental feature. "
"You must run workerd with `--experimental` to use the module fallback service.");

KJ_REQUIRE(experimental,
"The new ModuleRegistry implementation is an experimental feature. "
"You must run workerd with `--experimental` to use this feature.");
// Module URLs follow the virtual file system: a module at "/foo/bar/baz.js" in the bundle
// is "file:///foo/bar/baz.js".
const jsg::Url& bundleBase = workerFs->getBundleRoot();
Expand Down Expand Up @@ -1670,9 +1671,6 @@ kj::Promise<kj::Own<CompiledWorker>> factory_new_worker(const WorkerFactory& con

if (!usingNewModuleRegistry) {
KJ_IF_SOME(fallback, moduleFallback) {
KJ_REQUIRE(experimental,
"The module fallback service is an experimental feature. "
"You must run workerd with `--experimental` to use the module fallback service.");
auto& apiIsolate = isolate->getApi();
apiIsolate.setModuleFallbackCallback(
[client = kj::heap<fallback::FallbackServiceClient>(kj::str(fallback)),
Expand Down
24 changes: 24 additions & 0 deletions src/workerd/server/server-test.c++
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,9 @@ class Server: public kj::PtrTarget {
void setDefaultCompatibilityDate(kj::String date) {
options->defaultCompatibilityDate = kj::mv(date);
}
void setTestCompatibilityDateOverride(kj::String date) {
options->testCompatibilityDateOverride = kj::mv(date);
}
void enableDebugPort(kj::String addr) {
debugPort = kj::str("loopback:", addr);
}
Expand Down Expand Up @@ -1033,6 +1036,27 @@ KJ_TEST("Server: serve Service Worker using the new module registry") {
conn.httpGet200("/service-worker", "NMR: http://foo/service-worker");
}

KJ_TEST("Server: module registry enabled by date starts without experimental mode") {
TestServer test(singleWorker(R"((
modules = [
( name = "worker",
esModule =
`import message from "./message.js";
`export default {
` fetch() { return new Response(message + ":" + import.meta.main); }
`}
),
( name = "message.js", esModule = "export default 'date-default';" )
]
))"_kj));

test.server.setTestCompatibilityDateOverride(kj::str("2026-11-17"));
test.start();

auto conn = test.connect("test-addr");
conn.httpGet200("/", "date-default:true");
}

// Note: Python workers ignore the new_module_registry flag entirely — they
// always use the original module registry. That decision is centralized in
// isNewModuleRegistryEnabled() (io/features.h) and unit-tested in
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,12 +88,12 @@ function sendJson(response, value) {
response.end(JSON.stringify(value));
}

async function runWorkerd(configPath) {
async function runWorkerd(configPath, extraArgs = ['--experimental']) {
return await new Promise((resolve, reject) => {
let output = '';
const child = spawn(
env.WORKERD_BINARY,
['test', configPath, '--experimental'],
['test', configPath, ...extraArgs],
{ stdio: ['ignore', 'pipe', 'pipe'] }
);
const timeout = setTimeout(() => {
Expand All @@ -114,6 +114,48 @@ async function runWorkerd(configPath) {
});
}

test('module fallback requires experimental mode with either registry', async (t) => {
const directory = await mkdtemp(
join(env.TEST_TMPDIR, 'module-fallback-gate-')
);
const cases = [
['date-default', '2026-11-17', []],
['explicit-opt-in', '2026-11-16', ['new_module_registry']],
['legacy-opt-out', '2026-11-17', ['legacy_module_registry']],
['legacy-default', '2026-11-16', []],
];

for (const [name, date, flags] of cases) {
await t.test(name, async () => {
const configPath = join(directory, `${name}.capnp`);
await writeFile(
configPath,
`using Workerd = import "/workerd/workerd.capnp";
const config :Workerd.Config = (
services = [(
name = "worker",
worker = (
compatibilityFlags = ${JSON.stringify(flags)},
moduleFallback = "127.0.0.1:1",
modules = [(
name = "worker.js",
esModule = "export const check = { test() {} };"
)]
)
)]
);`
);

const result = await runWorkerd(configPath, [`--compat-date=${date}`]);
assert.notEqual(result.code, 0, result.output);
assert.match(
result.output,
/The module fallback service is an experimental feature/
);
});
}
});

test('module fallback serves V1 and V2 module types', async () => {
const requests = [];
const handlerErrors = [];
Expand Down
Loading