From 7775fd5754d46eedbfa35a5eb03c0198f3e06105 Mon Sep 17 00:00:00 2001 From: James M Snell Date: Wed, 5 Aug 2026 09:59:53 -0700 Subject: [PATCH 1/4] Workers new module registry documentation --- .../2026-08-05-new-module-registry.mdx | 25 ++ .../new-module-registry.md | 29 ++ .../workers/reference/module-registry.mdx | 249 ++++++++++++++++++ .../runtime-apis/webassembly/javascript.mdx | 20 ++ 4 files changed, 323 insertions(+) create mode 100644 src/content/changelog/workers/2026-08-05-new-module-registry.mdx create mode 100644 src/content/compatibility-flags/new-module-registry.md create mode 100644 src/content/docs/workers/reference/module-registry.mdx diff --git a/src/content/changelog/workers/2026-08-05-new-module-registry.mdx b/src/content/changelog/workers/2026-08-05-new-module-registry.mdx new file mode 100644 index 00000000000..750cdd21f1e --- /dev/null +++ b/src/content/changelog/workers/2026-08-05-new-module-registry.mdx @@ -0,0 +1,25 @@ +--- +title: New module registry with import.meta, lazy compilation, and URL-based resolution +description: A new module registry implementation is available via the new_module_registry compatibility flag, bringing import.meta support, URL-based specifier resolution, lazy compilation, shared caching, and WebAssembly source phase imports. +products: + - workers +date: 2026-08-05 +--- + +A new module registry implementation is available via the `new_module_registry` [compatibility flag](/workers/configuration/compatibility-flags/#new-module-registry). The new registry resolves module specifiers as URLs rather than filesystem-style paths, and includes several new capabilities and performance improvements. + +## New capabilities + +- **`import.meta`** -- `import.meta.url`, `import.meta.main`, and `import.meta.resolve()` are now supported. +- **URL-based specifier resolution** -- relative imports resolve the same way `new URL(specifier, base)` does. Query strings and fragments create distinct module instances, matching browser behavior. +- **Singleton `node:` built-ins** -- `node:` modules return the same instance whether accessed via static `import`, dynamic `import()`, `require()`, or `process.getBuiltinModule()`. +- **Import attribute validation** -- import attributes (`with { type: 'json' }`) are correctly validated. Unrecognized attribute keys and type mismatches throw errors instead of being silently ignored. +- **`require(esm)` support** -- `require()` on an ES module follows Node.js `require(esm)` rules, including the `'module.exports'` named export convention and the top-level `await` restriction. +- **WebAssembly source phase imports** -- import a compiled `WebAssembly.Module` directly with `import source mod from './module.wasm'` or `await import.source('./module.wasm')`. + +## Performance improvements + +- **Lazy compilation** -- modules are compiled on first import rather than all at startup, reducing cold start time for Workers with modules that are not always used. +- **Shared compiled code** -- compiled V8 bytecode is produced once and shared across all isolate replicas of a Worker, eliminating redundant compilation. + +For the full reference, refer to the [module registry documentation](/workers/reference/module-registry/). diff --git a/src/content/compatibility-flags/new-module-registry.md b/src/content/compatibility-flags/new-module-registry.md new file mode 100644 index 00000000000..2ac9245c183 --- /dev/null +++ b/src/content/compatibility-flags/new-module-registry.md @@ -0,0 +1,29 @@ +--- +_build: + publishResources: false + render: never + list: never + +name: "New module registry" +sort_date: "2026-08-05" +enable_flag: "new_module_registry" +disable_flag: "legacy_module_registry" +--- + +The `new_module_registry` flag replaces the Workers module registry with a new +implementation that resolves specifiers as URLs instead of filesystem-style paths. + +When enabled, the new registry provides: + +- `import.meta.url`, `import.meta.main`, and `import.meta.resolve()` support. +- Specifiers are parsed and resolved as URLs, including query strings and fragments. +- `node:` built-ins resolve to the same module instance regardless of how they are reached. +- Import attributes (`with { type: 'json' }`) are correctly validated. +- `require()` on an ES module follows Node.js `require(esm)` rules. +- Consistent error classes and messages across all loading paths. +- Lazy module compilation and shared compiled code across V8 isolate replicas. +- WebAssembly source phase imports. + +This flag does not have a default-on date yet. You must add it explicitly to +your `compatibility_flags`. For more information, refer to the +[module registry reference](/workers/reference/module-registry/). diff --git a/src/content/docs/workers/reference/module-registry.mdx b/src/content/docs/workers/reference/module-registry.mdx new file mode 100644 index 00000000000..0b5dc67c922 --- /dev/null +++ b/src/content/docs/workers/reference/module-registry.mdx @@ -0,0 +1,249 @@ +--- +title: Module registry +pcx_content_type: reference +description: Reference for the new Workers module registry, which provides import.meta support, URL-based specifier resolution, lazy compilation, and more. +products: + - workers +sidebar: + order: 5 +--- + +import { WranglerConfig } from "~/components"; + +The Workers runtime uses a module registry to resolve import specifiers, compile modules, and hand them to V8 for linking and execution. A new module registry implementation is available behind the `new_module_registry` [compatibility flag](/workers/configuration/compatibility-flags/). + +The new registry resolves specifiers as URLs rather than filesystem-style paths. This enables `import.meta` support, more predictable resolution behavior, consistent errors, and performance improvements through lazy compilation and shared caching. + +:::note + +The `new_module_registry` flag does not have a default-on date yet. You must add it explicitly to opt in. Existing Workers continue to use the original registry. + +::: + +## Turn on the new module registry + +Add the `new_module_registry` compatibility flag to your Wrangler configuration file: + + + +```jsonc +{ + "compatibility_flags": ["new_module_registry"] +} +``` + + + +## `import.meta` + +The new registry adds support for `import.meta.url`, `import.meta.main`, and `import.meta.resolve()`. + +### `import.meta.url` + +Returns the URL of the current module as a string. + +```js +console.log(import.meta.url); +// "file:///bundle/index.js" +``` + +### `import.meta.main` + +Returns `true` only for the module configured as your Worker's entrypoint. Every other module returns `false`. + +```js +export default { + async fetch(request) { + return new Response(`main: ${import.meta.main}`); + // "main: true" for the entrypoint module + }, +}; +``` + +### `import.meta.resolve()` + +Resolves a specifier against the current module without importing it. This is a pure string transform that does not check whether the resolved URL corresponds to a real module. + +```js +import.meta.resolve("./utils.js"); // "file:///bundle/utils.js" +import.meta.resolve("./a/../utils.js"); // "file:///bundle/utils.js" +import.meta.resolve("fs"); // "node:fs" +``` + +`import.meta.resolve()` normalizes percent-encoding the same way `new URL()` does. It collapses dot segments but does not decode characters that are already percent-encoded. For example, `import.meta.resolve('%66oo.js')` resolves to `file:///bundle/%66oo.js`, not `file:///bundle/foo.js`. + +A `TypeError` is thrown for a specifier that cannot be parsed as a URL. + +## Specifiers are URLs + +Module specifiers are resolved as URLs using `new URL(specifier, base)`. This applies uniformly to all modules, though it matters most for modules that survive bundling: `node:` imports, Wasm/text/binary modules, and anything running without Wrangler's default bundler. + +Full URLs work as specifiers: + +```js +import { helper } from "file:///bundle/utils.js"; +``` + +### Query strings and fragments + +A specifier with a different query string or fragment is treated as a distinct module instance, following the same module-identity rules browsers use. Each instance gets its own `import.meta.url` and its own copy of top-level state. + +```js +// counter.js +let n = 0; +export function increment() { + return ++n; +} +``` + +```js +import { increment as incA } from "./counter.js?a"; +import { increment as incB } from "./counter.js?b"; + +incA(); // 1 +incA(); // 2 +incB(); // 1 — separate instance with its own copy of `n` +``` + +Importing the same specifier with the same query string returns the same instance. This does not provide a way to force re-evaluation on every import. + +## `node:` imports resolve consistently + +All paths to a `node:` built-in return the same module instance: static `import`, dynamic `import()`, `require()`, and `process.getBuiltinModule()`. + +```js +import { default as processA } from "node:process"; + +const viaImport = await import("node:process"); +const viaBuiltin = process.getBuiltinModule("node:process"); + +viaImport.default === processA; // true +viaBuiltin === processA; // true +``` + +A given built-in is a true singleton, including any module-level state, regardless of how your code reaches it. + +## Import attributes + +The new registry validates [import attributes](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import/with) correctly. `json` is the only supported attribute type: + +```js +import data from "./config.json" with { type: "json" }; +``` + +### Unsupported attribute types + +The `text` and `bytes` attribute types are recognized but not yet supported. They track the [Import Text](https://github.com/tc39/proposal-import-text) and [Import Bytes](https://github.com/tc39/proposal-import-bytes) TC39 proposals, which have not yet reached Stage 4. Using them produces a specific error: + +```js +import msg from "./message.txt" with { type: "text" }; +// TypeError: Import attribute type "text" is not yet supported +``` + +### Unknown attribute keys + +Any attribute key other than `type` throws an error: + +```js +import data from "./config.json" with { type: "json", cache: "no" }; +// TypeError: Unsupported import attribute: "cache" +``` + +### Type mismatches + +If the specified type does not match the actual module type, an error is thrown: + +```js +import data from "./utils.js" with { type: "json" }; +// TypeError: Module "./utils.js" is not of type "json" +``` + +## `require(esm)` + +When `require()` is called on an ES module, the registry follows Node.js [`require(esm)`](https://nodejs.org/api/modules.html#loading-ecmascript-modules-using-require) behavior: + +- If the module has a string-named export called `'module.exports'`, that value is returned. This is Node.js' mechanism for letting an ES module control what `require()` returns. +- Otherwise, `require()` returns the module's namespace object. +- For workerd `node:` built-ins, `require()` returns the default export directly so that patterns like `require('node:buffer').Buffer` work as expected. + +```js +// utils.mjs +const impl = { hello: "world" }; +export { impl as "module.exports" }; +export default "not this"; +``` + +```js +import { createRequire } from "node:module"; + +const myRequire = createRequire(import.meta.url); +myRequire("./utils.mjs"); // { hello: "world" } +``` + +### Top-level `await` restriction + +If a module or any module in its import graph uses top-level `await`, `require()` throws an error. This matches Node.js' [`ERR_REQUIRE_ASYNC_MODULE`](https://nodejs.org/api/errors.html#err_require_async_module) restriction. Use `import()` for modules that rely on top-level `await`. + +```js +// async-init.mjs +await Promise.resolve(); +export const ready = true; +``` + +```js +myRequire("./async-init.mjs"); +// Error: Top-level await is not supported in this context +// for module: file:///bundle/async-init.mjs +``` + +### Bundler compatibility + +If a required module sets a truthy `__cjsUnwrapDefault` export (a marker used by some bundlers), the default export is returned. This ensures existing prebuilt bundles continue to work. + +## Error handling + +All module resolution errors use consistent classes and messages regardless of whether the failure occurs through a static `import`, dynamic `import()`, or `require()`: + +| Error | Class | Example message | +| --------------------------- | ----------- | ---------------------------------------------- | +| Module not found | `Error` | `Module not found: file:///bundle/nope.js` | +| Invalid specifier | `TypeError` | `Invalid module specifier: https://` | +| Circular dependency | `Error` | (V8 circular dependency error) | +| Unsupported import attribute | `TypeError` | `Unsupported import attribute: "cache"` | + +## Lazy compilation and shared caches + +The new registry includes two performance improvements: + +### Lazy compilation + +Modules are not compiled until they are first imported. In the original registry, all modules in a Worker's bundle are compiled at startup regardless of whether they are ever used. With the new registry, a WebAssembly module that is only used on a rarely-hit route does not add to cold start time for other routes. + +### Shared compiled code + +Cloudflare runs multiple V8 isolate replicas of a Worker to spread load across CPU cores. The original registry compiles the bundle independently in each replica. The new registry produces compiled V8 bytecode once and shares it across all replicas of that Worker. The first replica to import a given module pays the full compilation cost. Subsequent replicas use the cached result. This applies to both JavaScript and WebAssembly modules. + +## WebAssembly source phase imports + +You can import the compiled-but-not-instantiated form of a WebAssembly module using [source phase imports](https://github.com/nicolo-ribaudo/tc39-proposal-esm-integration/blob/main/proposals/source-phase-imports.md): + +```js +import source wasmModule from "./add.wasm"; + +export default { + async fetch() { + const instance = await WebAssembly.instantiate(wasmModule, {}); + return new Response(String(instance.exports.add(1, 2))); + }, +}; +``` + +Source phase imports can also be used dynamically: + +```js +const wasmModule = await import.source("./add.wasm"); +``` + +Both forms return a `WebAssembly.Module` directly. Source phase imports are only supported for WebAssembly modules. Using them on any other module type throws a `SyntaxError`. + +For more information on using WebAssembly in Workers, refer to [Wasm in JavaScript](/workers/runtime-apis/webassembly/javascript/). diff --git a/src/content/docs/workers/runtime-apis/webassembly/javascript.mdx b/src/content/docs/workers/runtime-apis/webassembly/javascript.mdx index 352855c7310..53c9c31404c 100644 --- a/src/content/docs/workers/runtime-apis/webassembly/javascript.mdx +++ b/src/content/docs/workers/runtime-apis/webassembly/javascript.mdx @@ -86,6 +86,26 @@ export default { When invoked, this Worker should log `Hello from JavaScript: 42` and return `Success: 42`, demonstrating the ability to invoke Wasm methods with arguments from JavaScript and vice versa. +## Source phase imports + +With the [`new_module_registry`](/workers/configuration/compatibility-flags/#new-module-registry) compatibility flag, you can import a compiled-but-not-instantiated `WebAssembly.Module` directly using source phase imports: + +```js +import source wasmModule from "./simple.wasm"; + +const instance = await WebAssembly.instantiate(wasmModule, {}); +``` + +Source phase imports can also be used dynamically: + +```js +const wasmModule = await import.source("./simple.wasm"); +``` + +Both forms return a `WebAssembly.Module`. Source phase imports are only supported for WebAssembly modules. Using them on any other module type throws a `SyntaxError`. + +For more information, refer to [WebAssembly source phase imports](/workers/reference/module-registry/#webassembly-source-phase-imports). + ## Next steps In practice, you will likely compile a language of your choice (such as Rust) to WebAssembly binaries. Many languages provide a `bindgen` to simplify the interaction between JavaScript and Wasm. These tools may integrate with your JavaScript bundler, and provide an API other than the WebAssembly API for initializing and invoking your Wasm module. As an example, refer to the [Rust `wasm-bindgen` documentation](https://wasm-bindgen.github.io/wasm-bindgen/examples/without-a-bundler.html). From ccc5d03f8b2891753bfb3e9a57852e9b395f0a05 Mon Sep 17 00:00:00 2001 From: Brendan Irvine-Broque Date: Sat, 15 Aug 2026 20:55:49 -0700 Subject: [PATCH 2/4] [Workers] Improve module registry documentation --- .../2026-08-05-new-module-registry.mdx | 165 ++++++++++++++++-- .../new-module-registry.md | 13 +- .../workers/reference/module-registry.mdx | 103 +++++++++-- .../workers/runtime-apis/nodejs/index.mdx | 44 ++++- .../workers/runtime-apis/web-standards.mdx | 32 +++- 5 files changed, 311 insertions(+), 46 deletions(-) diff --git a/src/content/changelog/workers/2026-08-05-new-module-registry.mdx b/src/content/changelog/workers/2026-08-05-new-module-registry.mdx index 750cdd21f1e..d0088b56bf9 100644 --- a/src/content/changelog/workers/2026-08-05-new-module-registry.mdx +++ b/src/content/changelog/workers/2026-08-05-new-module-registry.mdx @@ -1,25 +1,162 @@ --- -title: New module registry with import.meta, lazy compilation, and URL-based resolution -description: A new module registry implementation is available via the new_module_registry compatibility flag, bringing import.meta support, URL-based specifier resolution, lazy compilation, shared caching, and WebAssembly source phase imports. +title: New module registry improves JavaScript and Node.js compatibility +description: Opt in to standards-based module resolution, import.meta, Node.js require(esm) behavior, lazy compilation, and shared compilation caches. products: - workers date: 2026-08-05 --- -A new module registry implementation is available via the `new_module_registry` [compatibility flag](/workers/configuration/compatibility-flags/#new-module-registry). The new registry resolves module specifiers as URLs rather than filesystem-style paths, and includes several new capabilities and performance improvements. +import { TypeScriptExample, WranglerConfig } from "~/components"; -## New capabilities +A new Workers module registry improves compatibility with [JavaScript modules](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules) and Node.js module loading. It also avoids compiling modules until your Worker imports them. -- **`import.meta`** -- `import.meta.url`, `import.meta.main`, and `import.meta.resolve()` are now supported. -- **URL-based specifier resolution** -- relative imports resolve the same way `new URL(specifier, base)` does. Query strings and fragments create distinct module instances, matching browser behavior. -- **Singleton `node:` built-ins** -- `node:` modules return the same instance whether accessed via static `import`, dynamic `import()`, `require()`, or `process.getBuiltinModule()`. -- **Import attribute validation** -- import attributes (`with { type: 'json' }`) are correctly validated. Unrecognized attribute keys and type mismatches throw errors instead of being silently ignored. -- **`require(esm)` support** -- `require()` on an ES module follows Node.js `require(esm)` rules, including the `'module.exports'` named export convention and the top-level `await` restriction. -- **WebAssembly source phase imports** -- import a compiled `WebAssembly.Module` directly with `import source mod from './module.wasm'` or `await import.source('./module.wasm')`. +Turn on the new registry with the `new_module_registry` [compatibility flag](/workers/configuration/compatibility-flags/#new-module-registry): -## Performance improvements + -- **Lazy compilation** -- modules are compiled on first import rather than all at startup, reducing cold start time for Workers with modules that are not always used. -- **Shared compiled code** -- compiled V8 bytecode is produced once and shared across all isolate replicas of a Worker, eliminating redundant compilation. +```jsonc +{ + "compatibility_date": "$today", + "compatibility_flags": ["new_module_registry"] +} +``` -For the full reference, refer to the [module registry documentation](/workers/reference/module-registry/). + + +You can then inspect the current module and resolve another module without importing it: + + + +```ts +export default { + async fetch() { + return Response.json({ + url: import.meta.url, + main: import.meta.main, + workerPath: import.meta.resolve("./worker.js"), + }); + }, +}; +``` + + + +The registry also validates [import attributes](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import/with). You can import an uploaded JSON module and receive an error if the declared type does not match: + +```js +import config from "./config.json" with { type: "json" }; +``` + +## Load ES modules with `require()` + +With [Node.js compatibility](/workers/runtime-apis/nodejs/) enabled, `require()` follows Node.js [`require(esm)`](https://nodejs.org/api/modules.html#loading-ecmascript-modules-using-require) behavior. An ES module can define what `require()` returns by exporting a value named `"module.exports"`: + +For this example, add `nodejs_compat` alongside `new_module_registry` in your `compatibility_flags`. + +```js title="modules/greeting.mjs" +const greeting = { message: "Hello from an ES module" }; + +export { greeting as "module.exports" }; +``` + + + +```ts +import { createRequire } from "node:module"; + +const require = createRequire(import.meta.url); +const greeting = require("../modules/greeting.mjs"); + +export default { + async fetch() { + return Response.json(greeting); + }, +}; +``` + + + +Node.js built-ins also resolve to the same module instance through `import`, `require()`, and `process.getBuiltinModule()`. + +## Import WebAssembly source + +[Source phase imports](https://github.com/tc39/proposal-source-phase-imports) return a compiled `WebAssembly.Module`. You can instantiate the module with the imports your application requires: + +```js +import source wasmModule from "./add.wasm"; + +const instance = await WebAssembly.instantiate(wasmModule, {}); +``` + +You can also load a WebAssembly source dynamically: + +```js +const wasmModule = await import.source("./add.wasm"); +``` + +For more information, refer to [Source phase imports](/workers/runtime-apis/webassembly/javascript/#source-phase-imports). + +Other changes let you: + +- Use `import.meta.filename` and `import.meta.dirname` for uploaded modules. +- Resolve module specifiers as URLs, including queries and fragments. +- Receive consistent error classes across module-loading APIs. + +## Load modules only when needed + +Modules compile when they are first imported. This can reduce startup work when you upload separate modules and load them conditionally with a [dynamic `import()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import). + +The dynamically imported module exports the code used by the `/report` route: + +```js title="modules/report.mjs" +export function generateReport() { + return "Your report is ready"; +} +``` + +For example, this Worker only imports `report.mjs` for requests to `/report`: + + + +```ts +export default { + async fetch(request) { + const url = new URL(request.url); + + if (url.pathname === "/report") { + const { generateReport } = await import("../modules/report.mjs"); + return new Response(generateReport()); + } + + return new Response("Hello, world!"); + }, +}; +``` + + + +Configure Wrangler to upload the dynamically imported module separately: + + + +```jsonc +{ + "compatibility_date": "$today", + "compatibility_flags": ["new_module_registry"], + "find_additional_modules": true, + "rules": [ + { + "type": "ESModule", + "globs": ["modules/**/*.mjs"] + } + ] +} +``` + + + +Compiled JavaScript and WebAssembly can also be reused across isolate replicas. Subsequent replicas can avoid compiling the same module again. + +Wrangler bundles most JavaScript into the entrypoint by default. To preserve separate modules for runtime loading, refer to [Find additional modules](/workers/wrangler/bundling/#find-additional-modules). + +For supported behavior and migration details, refer to the [module registry reference](/workers/reference/module-registry/). diff --git a/src/content/compatibility-flags/new-module-registry.md b/src/content/compatibility-flags/new-module-registry.md index 2ac9245c183..e42375da54d 100644 --- a/src/content/compatibility-flags/new-module-registry.md +++ b/src/content/compatibility-flags/new-module-registry.md @@ -10,20 +10,17 @@ enable_flag: "new_module_registry" disable_flag: "legacy_module_registry" --- -The `new_module_registry` flag replaces the Workers module registry with a new -implementation that resolves specifiers as URLs instead of filesystem-style paths. +The `new_module_registry` flag replaces the JavaScript Workers module registry with an implementation that resolves specifiers as URLs instead of filesystem-style paths. Python Workers continue to use the legacy module registry. When enabled, the new registry provides: -- `import.meta.url`, `import.meta.main`, and `import.meta.resolve()` support. +- `import.meta.url`, `import.meta.main`, `import.meta.filename`, `import.meta.dirname`, and `import.meta.resolve()` support. - Specifiers are parsed and resolved as URLs, including query strings and fragments. -- `node:` built-ins resolve to the same module instance regardless of how they are reached. +- With [`nodejs_compat`](/workers/runtime-apis/nodejs/), `node:` built-ins resolve to the same module instance regardless of how they are reached. - Import attributes (`with { type: 'json' }`) are correctly validated. -- `require()` on an ES module follows Node.js `require(esm)` rules. +- With `nodejs_compat`, `require()` on an ES module follows Node.js [`require(esm)`](https://nodejs.org/api/modules.html#loading-ecmascript-modules-using-require) rules. - Consistent error classes and messages across all loading paths. - Lazy module compilation and shared compiled code across V8 isolate replicas. - WebAssembly source phase imports. -This flag does not have a default-on date yet. You must add it explicitly to -your `compatibility_flags`. For more information, refer to the -[module registry reference](/workers/reference/module-registry/). +You must add this flag explicitly to your `compatibility_flags`. For examples and migration details, refer to the [module registry reference](/workers/reference/module-registry/). diff --git a/src/content/docs/workers/reference/module-registry.mdx b/src/content/docs/workers/reference/module-registry.mdx index 0b5dc67c922..1e3860d2a44 100644 --- a/src/content/docs/workers/reference/module-registry.mdx +++ b/src/content/docs/workers/reference/module-registry.mdx @@ -1,22 +1,22 @@ --- title: Module registry pcx_content_type: reference -description: Reference for the new Workers module registry, which provides import.meta support, URL-based specifier resolution, lazy compilation, and more. +description: Configure standards-based module resolution, import.meta, Node.js module loading, lazy compilation, and shared compilation caches in Workers. products: - workers sidebar: order: 5 --- -import { WranglerConfig } from "~/components"; +import { TypeScriptExample, WranglerConfig } from "~/components"; -The Workers runtime uses a module registry to resolve import specifiers, compile modules, and hand them to V8 for linking and execution. A new module registry implementation is available behind the `new_module_registry` [compatibility flag](/workers/configuration/compatibility-flags/). +The Workers runtime uses a module registry to resolve [JavaScript module specifiers](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules), compile modules, and provide them to V8. A new implementation is available through the [`new_module_registry` compatibility flag](/workers/configuration/compatibility-flags/#new-module-registry). The new registry resolves specifiers as URLs rather than filesystem-style paths. This enables `import.meta` support, more predictable resolution behavior, consistent errors, and performance improvements through lazy compilation and shared caching. :::note -The `new_module_registry` flag does not have a default-on date yet. You must add it explicitly to opt in. Existing Workers continue to use the original registry. +You must add the `new_module_registry` flag explicitly. Existing Workers continue to use the original registry. Python Workers always use the original registry. ::: @@ -28,6 +28,7 @@ Add the `new_module_registry` compatibility flag to your Wrangler configuration ```jsonc { + "compatibility_date": "$today", "compatibility_flags": ["new_module_registry"] } ``` @@ -36,7 +37,27 @@ Add the `new_module_registry` compatibility flag to your Wrangler configuration ## `import.meta` -The new registry adds support for `import.meta.url`, `import.meta.main`, and `import.meta.resolve()`. +[`import.meta`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import.meta) exposes metadata about the current JavaScript module. JavaScript hosts define the properties available on this object. + +Workers provides `url`, `main`, `filename`, `dirname`, and `resolve()` when the new module registry is enabled: + + + +```ts +export default { + async fetch() { + return Response.json({ + url: import.meta.url, + main: import.meta.main, + filename: import.meta.filename, + dirname: import.meta.dirname, + resolved: import.meta.resolve("./utils.js"), + }); + }, +}; +``` + + ### `import.meta.url` @@ -60,13 +81,30 @@ export default { }; ``` +### `import.meta.filename` + +Returns the absolute path of the current module without the `file:` protocol. For example, a module with the URL `file:///bundle/index.js` returns `/bundle/index.js`. + +This property is only available for `file:` modules. It is `undefined` for modules that use another protocol. + +### `import.meta.dirname` + +Returns the directory that contains `import.meta.filename`. For example, a module with the URL `file:///bundle/index.js` returns `/bundle`. + +This property is only available for `file:` modules. It is `undefined` for modules that use another protocol. + ### `import.meta.resolve()` -Resolves a specifier against the current module without importing it. This is a pure string transform that does not check whether the resolved URL corresponds to a real module. +[`import.meta.resolve()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import.meta/resolve) resolves a specifier against the current module without importing it. This string transformation does not check whether the resulting URL belongs to an uploaded module. ```js import.meta.resolve("./utils.js"); // "file:///bundle/utils.js" import.meta.resolve("./a/../utils.js"); // "file:///bundle/utils.js" +``` + +With [Node.js compatibility](/workers/runtime-apis/nodejs/) enabled, the registry also recognizes bare Node.js built-ins: + +```js import.meta.resolve("fs"); // "node:fs" ``` @@ -76,7 +114,9 @@ A `TypeError` is thrown for a specifier that cannot be parsed as a URL. ## Specifiers are URLs -Module specifiers are resolved as URLs using `new URL(specifier, base)`. This applies uniformly to all modules, though it matters most for modules that survive bundling: `node:` imports, Wasm/text/binary modules, and anything running without Wrangler's default bundler. +Module specifiers are resolved as URLs using `new URL(specifier, base)`. This applies to modules that reach the Workers runtime. + +Wrangler bundles most JavaScript dependencies into your entrypoint by default. This removes their original module boundaries before the runtime receives your code. Built-in `node:` imports and separate WebAssembly, text, or binary modules still reach the registry. You can also use [additional modules](/workers/wrangler/bundling/#find-additional-modules) or [turn off bundling](/workers/wrangler/bundling/#disable-bundling) to upload JavaScript modules separately. Full URLs work as specifiers: @@ -84,6 +124,8 @@ Full URLs work as specifiers: import { helper } from "file:///bundle/utils.js"; ``` +The registry only loads modules included with your Worker or provided by the runtime. An `https:` specifier does not fetch a module over the network. + ### Query strings and fragments A specifier with a different query string or fragment is treated as a distinct module instance, following the same module-identity rules browsers use. Each instance gets its own `import.meta.url` and its own copy of top-level state. @@ -109,7 +151,7 @@ Importing the same specifier with the same query string returns the same instanc ## `node:` imports resolve consistently -All paths to a `node:` built-in return the same module instance: static `import`, dynamic `import()`, `require()`, and `process.getBuiltinModule()`. +With [Node.js compatibility](/workers/runtime-apis/nodejs/) enabled, all paths to a `node:` built-in return the same per-isolate module instance: static `import`, dynamic `import()`, `require()`, and `process.getBuiltinModule()`. ```js import { default as processA } from "node:process"; @@ -204,12 +246,12 @@ If a required module sets a truthy `__cjsUnwrapDefault` export (a marker used by All module resolution errors use consistent classes and messages regardless of whether the failure occurs through a static `import`, dynamic `import()`, or `require()`: -| Error | Class | Example message | -| --------------------------- | ----------- | ---------------------------------------------- | -| Module not found | `Error` | `Module not found: file:///bundle/nope.js` | -| Invalid specifier | `TypeError` | `Invalid module specifier: https://` | -| Circular dependency | `Error` | (V8 circular dependency error) | -| Unsupported import attribute | `TypeError` | `Unsupported import attribute: "cache"` | +| Error | Class | Example message | +| ---------------------------- | ----------- | ------------------------------------------ | +| Module not found | `Error` | `Module not found: file:///bundle/nope.js` | +| Invalid specifier | `TypeError` | `Invalid module specifier: https://` | +| Circular dependency | `Error` | (V8 circular dependency error) | +| Unsupported import attribute | `TypeError` | `Unsupported import attribute: "cache"` | ## Lazy compilation and shared caches @@ -217,11 +259,40 @@ The new registry includes two performance improvements: ### Lazy compilation -Modules are not compiled until they are first imported. In the original registry, all modules in a Worker's bundle are compiled at startup regardless of whether they are ever used. With the new registry, a WebAssembly module that is only used on a rarely-hit route does not add to cold start time for other routes. +Modules are not compiled until they are first imported. Statically imported dependencies compile with their module graph. Separately uploaded modules that are reached through a [dynamic `import()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import) can defer compilation until the relevant code path runs. + +For example, this Worker only imports and compiles `report.mjs` for requests to `/report`: + +```js title="modules/report.mjs" +export function generateReport() { + return "Your report is ready"; +} +``` + + + +```ts +export default { + async fetch(request) { + const url = new URL(request.url); + + if (url.pathname === "/report") { + const { generateReport } = await import("../modules/report.mjs"); + return new Response(generateReport()); + } + + return new Response("Hello, world!"); + }, +}; +``` + + + +To keep `report.mjs` separate from the entrypoint, configure Wrangler to [find additional modules](/workers/wrangler/bundling/#find-additional-modules). ### Shared compiled code -Cloudflare runs multiple V8 isolate replicas of a Worker to spread load across CPU cores. The original registry compiles the bundle independently in each replica. The new registry produces compiled V8 bytecode once and shares it across all replicas of that Worker. The first replica to import a given module pays the full compilation cost. Subsequent replicas use the cached result. This applies to both JavaScript and WebAssembly modules. +Cloudflare can run multiple V8 isolate replicas of a Worker. The original registry compiles the bundle independently in each replica. The new registry can cache compiled JavaScript data and compiled WebAssembly modules for reuse by subsequent replicas. ## WebAssembly source phase imports diff --git a/src/content/docs/workers/runtime-apis/nodejs/index.mdx b/src/content/docs/workers/runtime-apis/nodejs/index.mdx index 5534347ae35..6607df2bb0d 100644 --- a/src/content/docs/workers/runtime-apis/nodejs/index.mdx +++ b/src/content/docs/workers/runtime-apis/nodejs/index.mdx @@ -7,7 +7,7 @@ products: - workers --- -import { Render, WranglerConfig } from "~/components"; +import { Render, TypeScriptExample, WranglerConfig } from "~/components"; When you write a Worker, you may need to import packages from [npm](https://www.npmjs.com/). Many npm packages rely on APIs from the [Node.js runtime](https://nodejs.org/en/about), and will not work unless these Node.js APIs are available. @@ -33,6 +33,46 @@ To enable built-in Node.js APIs and add polyfills, add the `nodejs_compat` compa +### Module loading + +The `nodejs_compat` flag provides Node.js APIs. The separate [`new_module_registry` compatibility flag](/workers/configuration/compatibility-flags/#new-module-registry) provides Node.js-compatible module-loading behavior. + +Enable both flags to use [`createRequire()`](https://nodejs.org/api/module.html#modulecreaterequirefilename) with `import.meta.url`, load ES modules through [`require()`](https://nodejs.org/api/modules.html#loading-ecmascript-modules-using-require), and receive the same built-in module instance through different loading APIs: + + + +```jsonc +{ + "compatibility_date": "$today", + "compatibility_flags": ["nodejs_compat", "new_module_registry"] +} +``` + + + + + +```ts +import { createRequire } from "node:module"; +import process from "node:process"; + +const require = createRequire(import.meta.url); +const requiredProcess = require("node:process"); + +export default { + async fetch() { + return Response.json({ + sameInstance: requiredProcess === process, + resolved: import.meta.resolve("process"), + }); + }, +}; +``` + + + +For resolution rules and CommonJS interoperability, refer to the [module registry reference](/workers/reference/module-registry/#requireesm). + ## Supported Node.js APIs The runtime APIs from Node.js listed in this section with the status "🟢 supported" are currently natively supported in the Workers Runtime. Items listed as "🟡 partially supported" include usable APIs, but do not implement the complete Node.js API surface. @@ -63,7 +103,7 @@ The runtime APIs from Node.js listed in this section with the status "🟢 suppo | [Process](/workers/runtime-apis/nodejs/process/) | 🟢 supported | | [Punycode](https://nodejs.org/docs/latest/api/punycode.html) (deprecated) | 🟢 supported | | [Query strings](https://nodejs.org/docs/latest/api/querystring.html) | 🟢 supported | -| [Stream](/workers/runtime-apis/nodejs/streams/) | 🟢 supported | +| [Stream](/workers/runtime-apis/nodejs/streams/) | 🟢 supported | | [String decoder](/workers/runtime-apis/nodejs/string-decoder/) | 🟢 supported | | [Test runner](/workers/runtime-apis/nodejs/test/) | 🟡 partially supported | | [Timers](/workers/runtime-apis/nodejs/timers/) | 🟢 supported | diff --git a/src/content/docs/workers/runtime-apis/web-standards.mdx b/src/content/docs/workers/runtime-apis/web-standards.mdx index 266fba039da..4b881802303 100644 --- a/src/content/docs/workers/runtime-apis/web-standards.mdx +++ b/src/content/docs/workers/runtime-apis/web-standards.mdx @@ -9,6 +9,8 @@ products: - workers --- +import { TypeScriptExample } from "~/components"; + ## JavaScript standards The Cloudflare Workers runtime is [built on top of the V8 JavaScript and WebAssembly engine](/workers/reference/how-workers-works/). The Workers runtime is updated at least once a week, to at least the version of V8 that is currently used by Google Chrome's stable release. This means you can safely use the latest JavaScript features, with no need for transpilers. @@ -24,6 +26,30 @@ All of the [standard built-in objects](https://developer.mozilla.org/en-US/docs/ - [`WebAssembly.instantiateStreaming`](https://developer.mozilla.org/en-US/docs/WebAssembly/JavaScript_interface/instantiateStreaming_static) - `Date.now()` returns the time of the last I/O; it does not advance during code execution. +### `import.meta` + +JavaScript hosts define the properties available through [`import.meta`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import.meta). With the [`new_module_registry` compatibility flag](/workers/configuration/compatibility-flags/#new-module-registry), Workers provides `import.meta.url`, `import.meta.main`, `import.meta.filename`, `import.meta.dirname`, and [`import.meta.resolve()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import.meta/resolve). + +Use these properties to inspect the current module or resolve another module without importing it: + + + +```ts +export default { + async fetch() { + return Response.json({ + currentModule: import.meta.url, + isEntrypoint: import.meta.main, + dependency: import.meta.resolve("./dependency.js"), + }); + }, +}; +``` + + + +For property behavior and module-loading details, refer to the [`import.meta` module registry reference](/workers/reference/module-registry/#importmeta). + --- ## Web standards and global APIs @@ -34,7 +60,6 @@ The following methods are available per the [Worker Global Scope](https://develo - atob() - Decodes a string of data which has been encoded using base-64 encoding. - - btoa() - Creates a base-64 encoded ASCII string from a string of binary data. @@ -42,16 +67,12 @@ The following methods are available per the [Worker Global Scope](https://develo - setInterval() - Schedules a function to execute every time a given number of milliseconds elapses. - - clearInterval() - Cancels the repeated execution set using [`setInterval()`](https://developer.mozilla.org/en-US/docs/Web/API/setInterval). - - setTimeout() - Schedules a function to execute in a given amount of time. - - clearTimeout() - Cancels the delayed execution set using [`setTimeout()`](https://developer.mozilla.org/en-US/docs/Web/API/setTimeout). - - [`scheduler.wait()`](/workers/runtime-apis/scheduler/) - Returns a Promise that resolves after a given number of milliseconds. An `await`-able alternative to `setTimeout()`. @@ -65,7 +86,6 @@ Timers are only available inside of [the Request Context](/workers/runtime-apis/ - performance.timeOrigin - Returns the high resolution time origin. Workers uses the UNIX epoch as the time origin, meaning that `performance.timeOrigin` will always return `0`. - - performance.now() - Returns a `DOMHighResTimeStamp` representing the number of milliseconds elapsed since `performance.timeOrigin`. Note that Workers intentionally reduces the precision of `performance.now()` such that it returns the time of the last I/O and does not advance during code execution. Effectively, because of this, and because `performance.timeOrigin` is always, `0`, `performance.now()` will always equal `Date.now()`, yielding a consistent view of the passage of time within a Worker. From 3eefb2a9165acc13388ec4dec68ac340d2cbe7e3 Mon Sep 17 00:00:00 2001 From: Brendan Irvine-Broque Date: Sat, 15 Aug 2026 20:57:00 -0700 Subject: [PATCH 3/4] [Workers] Prioritize module registry use cases --- .../2026-08-05-new-module-registry.mdx | 145 ++++++++++-------- 1 file changed, 82 insertions(+), 63 deletions(-) diff --git a/src/content/changelog/workers/2026-08-05-new-module-registry.mdx b/src/content/changelog/workers/2026-08-05-new-module-registry.mdx index d0088b56bf9..18faac1b40a 100644 --- a/src/content/changelog/workers/2026-08-05-new-module-registry.mdx +++ b/src/content/changelog/workers/2026-08-05-new-module-registry.mdx @@ -41,70 +41,9 @@ export default { -The registry also validates [import attributes](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import/with). You can import an uploaded JSON module and receive an error if the declared type does not match: - -```js -import config from "./config.json" with { type: "json" }; -``` - -## Load ES modules with `require()` - -With [Node.js compatibility](/workers/runtime-apis/nodejs/) enabled, `require()` follows Node.js [`require(esm)`](https://nodejs.org/api/modules.html#loading-ecmascript-modules-using-require) behavior. An ES module can define what `require()` returns by exporting a value named `"module.exports"`: - -For this example, add `nodejs_compat` alongside `new_module_registry` in your `compatibility_flags`. - -```js title="modules/greeting.mjs" -const greeting = { message: "Hello from an ES module" }; - -export { greeting as "module.exports" }; -``` - - - -```ts -import { createRequire } from "node:module"; - -const require = createRequire(import.meta.url); -const greeting = require("../modules/greeting.mjs"); - -export default { - async fetch() { - return Response.json(greeting); - }, -}; -``` - - - -Node.js built-ins also resolve to the same module instance through `import`, `require()`, and `process.getBuiltinModule()`. - -## Import WebAssembly source - -[Source phase imports](https://github.com/tc39/proposal-source-phase-imports) return a compiled `WebAssembly.Module`. You can instantiate the module with the imports your application requires: - -```js -import source wasmModule from "./add.wasm"; - -const instance = await WebAssembly.instantiate(wasmModule, {}); -``` - -You can also load a WebAssembly source dynamically: - -```js -const wasmModule = await import.source("./add.wasm"); -``` - -For more information, refer to [Source phase imports](/workers/runtime-apis/webassembly/javascript/#source-phase-imports). - -Other changes let you: - -- Use `import.meta.filename` and `import.meta.dirname` for uploaded modules. -- Resolve module specifiers as URLs, including queries and fragments. -- Receive consistent error classes across module-loading APIs. - ## Load modules only when needed -Modules compile when they are first imported. This can reduce startup work when you upload separate modules and load them conditionally with a [dynamic `import()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import). +Modules compile when they are first imported. This can reduce cold-start work when you upload separate modules and load them conditionally with a [dynamic `import()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import). The dynamically imported module exports the code used by the `/report` route: @@ -155,8 +94,88 @@ Configure Wrangler to upload the dynamically imported module separately: -Compiled JavaScript and WebAssembly can also be reused across isolate replicas. Subsequent replicas can avoid compiling the same module again. +The runtime does not compile `report.mjs` during cold starts for routes that do not import it. Compiled JavaScript and WebAssembly can also be reused across isolate replicas, which lets subsequent replicas avoid the same compilation work. Wrangler bundles most JavaScript into the entrypoint by default. To preserve separate modules for runtime loading, refer to [Find additional modules](/workers/wrangler/bundling/#find-additional-modules). +## Load ES modules with `require()` + +With [Node.js compatibility](/workers/runtime-apis/nodejs/) enabled, `require()` follows Node.js [`require(esm)`](https://nodejs.org/api/modules.html#loading-ecmascript-modules-using-require) behavior. An ES module can define what `require()` returns by exporting a value named `"module.exports"`. + +For this example, add `nodejs_compat` alongside `new_module_registry` in your `compatibility_flags`. + +```js title="modules/greeting.mjs" +const greeting = { message: "Hello from an ES module" }; + +export { greeting as "module.exports" }; +``` + + + +```ts +import { createRequire } from "node:module"; + +const require = createRequire(import.meta.url); +const greeting = require("../modules/greeting.mjs"); + +export default { + async fetch() { + return Response.json(greeting); + }, +}; +``` + + + +Node.js built-ins also resolve to the same module instance through `import`, `require()`, and `process.getBuiltinModule()`. + +## Use URL-based module specifiers + +Relative specifiers resolve with the same URL semantics as `new URL(specifier, base)`. Query strings and fragments identify separate module instances, which matches browser module behavior: + +```js title="modules/counter.mjs" +let count = 0; + +export function increment() { + return ++count; +} +``` + +```js title="modules/index.mjs" +import { increment as incrementA } from "./counter.mjs?instance=a"; +import { increment as incrementB } from "./counter.mjs?instance=b"; + +incrementA(); // 1 +incrementA(); // 2 +incrementB(); // 1, from a separate module instance +``` + +## Validate import attributes + +The registry validates [import attributes](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import/with). You can import an uploaded JSON module and receive an error if the declared type does not match: + +```js +import config from "./config.json" with { type: "json" }; +``` + +## Import WebAssembly source + +[Source phase imports](https://github.com/tc39/proposal-source-phase-imports) return a compiled `WebAssembly.Module`. You can instantiate the module with the imports your application requires: + +```js +import source wasmModule from "./add.wasm"; + +const instance = await WebAssembly.instantiate(wasmModule, {}); +``` + +You can also load WebAssembly source dynamically: + +```js +const wasmModule = await import.source("./add.wasm"); +``` + +For more information, refer to [Source phase imports](/workers/runtime-apis/webassembly/javascript/#source-phase-imports). + +Resolution failures also use consistent error classes and messages across static `import`, dynamic `import()`, and `require()`. + For supported behavior and migration details, refer to the [module registry reference](/workers/reference/module-registry/). From da4b8e5a009c58cf1f76809f36c925854514e846 Mon Sep 17 00:00:00 2001 From: Brendan Irvine-Broque Date: Sat, 15 Aug 2026 21:24:49 -0700 Subject: [PATCH 4/4] [Workers] Finalize module registry announcement --- .../2026-08-05-new-module-registry.mdx | 181 ------------------ .../2026-08-17-new-module-registry.mdx | 119 ++++++++++++ .../workers/reference/module-registry.mdx | 22 ++- .../workers/runtime-apis/nodejs/index.mdx | 8 +- .../workers/runtime-apis/web-standards.mdx | 12 +- 5 files changed, 149 insertions(+), 193 deletions(-) delete mode 100644 src/content/changelog/workers/2026-08-05-new-module-registry.mdx create mode 100644 src/content/changelog/workers/2026-08-17-new-module-registry.mdx diff --git a/src/content/changelog/workers/2026-08-05-new-module-registry.mdx b/src/content/changelog/workers/2026-08-05-new-module-registry.mdx deleted file mode 100644 index 18faac1b40a..00000000000 --- a/src/content/changelog/workers/2026-08-05-new-module-registry.mdx +++ /dev/null @@ -1,181 +0,0 @@ ---- -title: New module registry improves JavaScript and Node.js compatibility -description: Opt in to standards-based module resolution, import.meta, Node.js require(esm) behavior, lazy compilation, and shared compilation caches. -products: - - workers -date: 2026-08-05 ---- - -import { TypeScriptExample, WranglerConfig } from "~/components"; - -A new Workers module registry improves compatibility with [JavaScript modules](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules) and Node.js module loading. It also avoids compiling modules until your Worker imports them. - -Turn on the new registry with the `new_module_registry` [compatibility flag](/workers/configuration/compatibility-flags/#new-module-registry): - - - -```jsonc -{ - "compatibility_date": "$today", - "compatibility_flags": ["new_module_registry"] -} -``` - - - -You can then inspect the current module and resolve another module without importing it: - - - -```ts -export default { - async fetch() { - return Response.json({ - url: import.meta.url, - main: import.meta.main, - workerPath: import.meta.resolve("./worker.js"), - }); - }, -}; -``` - - - -## Load modules only when needed - -Modules compile when they are first imported. This can reduce cold-start work when you upload separate modules and load them conditionally with a [dynamic `import()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import). - -The dynamically imported module exports the code used by the `/report` route: - -```js title="modules/report.mjs" -export function generateReport() { - return "Your report is ready"; -} -``` - -For example, this Worker only imports `report.mjs` for requests to `/report`: - - - -```ts -export default { - async fetch(request) { - const url = new URL(request.url); - - if (url.pathname === "/report") { - const { generateReport } = await import("../modules/report.mjs"); - return new Response(generateReport()); - } - - return new Response("Hello, world!"); - }, -}; -``` - - - -Configure Wrangler to upload the dynamically imported module separately: - - - -```jsonc -{ - "compatibility_date": "$today", - "compatibility_flags": ["new_module_registry"], - "find_additional_modules": true, - "rules": [ - { - "type": "ESModule", - "globs": ["modules/**/*.mjs"] - } - ] -} -``` - - - -The runtime does not compile `report.mjs` during cold starts for routes that do not import it. Compiled JavaScript and WebAssembly can also be reused across isolate replicas, which lets subsequent replicas avoid the same compilation work. - -Wrangler bundles most JavaScript into the entrypoint by default. To preserve separate modules for runtime loading, refer to [Find additional modules](/workers/wrangler/bundling/#find-additional-modules). - -## Load ES modules with `require()` - -With [Node.js compatibility](/workers/runtime-apis/nodejs/) enabled, `require()` follows Node.js [`require(esm)`](https://nodejs.org/api/modules.html#loading-ecmascript-modules-using-require) behavior. An ES module can define what `require()` returns by exporting a value named `"module.exports"`. - -For this example, add `nodejs_compat` alongside `new_module_registry` in your `compatibility_flags`. - -```js title="modules/greeting.mjs" -const greeting = { message: "Hello from an ES module" }; - -export { greeting as "module.exports" }; -``` - - - -```ts -import { createRequire } from "node:module"; - -const require = createRequire(import.meta.url); -const greeting = require("../modules/greeting.mjs"); - -export default { - async fetch() { - return Response.json(greeting); - }, -}; -``` - - - -Node.js built-ins also resolve to the same module instance through `import`, `require()`, and `process.getBuiltinModule()`. - -## Use URL-based module specifiers - -Relative specifiers resolve with the same URL semantics as `new URL(specifier, base)`. Query strings and fragments identify separate module instances, which matches browser module behavior: - -```js title="modules/counter.mjs" -let count = 0; - -export function increment() { - return ++count; -} -``` - -```js title="modules/index.mjs" -import { increment as incrementA } from "./counter.mjs?instance=a"; -import { increment as incrementB } from "./counter.mjs?instance=b"; - -incrementA(); // 1 -incrementA(); // 2 -incrementB(); // 1, from a separate module instance -``` - -## Validate import attributes - -The registry validates [import attributes](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import/with). You can import an uploaded JSON module and receive an error if the declared type does not match: - -```js -import config from "./config.json" with { type: "json" }; -``` - -## Import WebAssembly source - -[Source phase imports](https://github.com/tc39/proposal-source-phase-imports) return a compiled `WebAssembly.Module`. You can instantiate the module with the imports your application requires: - -```js -import source wasmModule from "./add.wasm"; - -const instance = await WebAssembly.instantiate(wasmModule, {}); -``` - -You can also load WebAssembly source dynamically: - -```js -const wasmModule = await import.source("./add.wasm"); -``` - -For more information, refer to [Source phase imports](/workers/runtime-apis/webassembly/javascript/#source-phase-imports). - -Resolution failures also use consistent error classes and messages across static `import`, dynamic `import()`, and `require()`. - -For supported behavior and migration details, refer to the [module registry reference](/workers/reference/module-registry/). diff --git a/src/content/changelog/workers/2026-08-17-new-module-registry.mdx b/src/content/changelog/workers/2026-08-17-new-module-registry.mdx new file mode 100644 index 00000000000..001ca4a5014 --- /dev/null +++ b/src/content/changelog/workers/2026-08-17-new-module-registry.mdx @@ -0,0 +1,119 @@ +--- +title: New module registry in Workers improves JavaScript and Node.js compatibility +description: Opt in to standards-based module resolution, import.meta, Node.js require(esm) behavior, lazy compilation, and shared compilation caches. +products: + - workers +date: 2026-08-17 +--- + +import { TypeScriptExample, WranglerConfig } from "~/components"; + +We've rebuilt the module registry that handles how [ES Modules (ESM)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules) and [CommonJS modules](https://nodejs.org/api/modules.html#modules-commonjs-modules) are handled in the Workers runtime, increasing compatbility with standards and the ecosystem of NPM packages. + +When you enable the [`new_module_registry` compatbility flag](/workers/configuration/compatibility-flags/#new-module-registry) in your Worker: + +* The `import.meta.url`, `import.meta.main`, and `import.meta.resolve()` APIs are fully supported +* Dynamic imports (`await import("/modules/my-module.js");`) are compiled lazily, rather than upfront during your Worker's startup phase +* Your Worker's cold start time may be faster in production, because compiled code can be shared across V8 isolate replicas +* Module specifiers are parsed and resolved as real URLs, including query strings and fragments +* `node:` built-ins resolve to the same module instance no matter how you reach them +* Import attributes (`with { type: 'json' }`) are correctly validated +* `require()` on an ES module follows Node.js' [`require(esm)`](https://nodejs.org/api/modules.html#loading-ecmascript-modules-using-require) rules +* Errors use consistent classes and messages regardless of which loading path triggered them +* WebAssembly modules support [source phase imports](https://github.com/tc39/proposal-source-phase-imports) + +## `import.meta` APIs are now fully supported + +You can then inspect the current module and resolve another module without importing it: + + + +```ts +export default { + async fetch() { + return Response.json({ + url: import.meta.url, + main: import.meta.main, + workerPath: import.meta.resolve("./worker.js"), + }); + }, +}; +``` + + + +## Dynamic imports are compiled lazily + +Modules compile when they are first imported. This can reduce cold-start work when you upload separate modules and load them conditionally with a [dynamic `import()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import). + +For example, this Worker will only import `report.mjs` when the [isolate](/workers/reference/how-workers-works/) serves a request to `/report` for the first time: + + + +```ts +export default { + async fetch(request) { + const url = new URL(request.url); + + if (url.pathname === "/report") { + const { generateReport } = await import("../modules/report.mjs"); + return new Response(generateReport()); + } + + return new Response("Hello, world!"); + }, +}; +``` + + + +Wrangler bundles most JavaScript into the entrypoint by default. To preserve separate modules for runtime loading and take advantage of this new behavior, either use the [Cloudflare Vite Plugin](/workers/vite-plugin/), or configure [Wrangler CLI](/workers/wrangler/) to [upload the dynamically imported module separately](/workers/wrangler/bundling/#find-additional-modules): + + + +```jsonc +{ + "compatibility_date": "$today", + "compatibility_flags": ["new_module_registry"], + "find_additional_modules": true, + "rules": [ + { + "type": "ESModule", + "globs": ["modules/**/*.mjs"] + } + ] +} +``` + + + +## Source phase imports are now supported for WebAssembly modules + +[Source phase imports](/workers/runtime-apis/webassembly/javascript/#source-phase-imports) return a compiled `WebAssembly.Module`. You can instantiate the module with the imports your application requires: + +```js +import source wasmModule from "./add.wasm"; + +const instance = await WebAssembly.instantiate(wasmModule, {}); +``` + +You can also load WebAssembly source dynamically: + +```js +const wasmModule = await import.source("./add.wasm"); +``` + +## How to enable the `new_module_registry` compatbility flag + +Add the following to your Worker's [configuration file](/workers/configuration/): + + + +```jsonc +{ + "compatibility_date": "$today", + "compatibility_flags": ["new_module_registry"] +} +``` + + diff --git a/src/content/docs/workers/reference/module-registry.mdx b/src/content/docs/workers/reference/module-registry.mdx index 1e3860d2a44..65a8acfe6a2 100644 --- a/src/content/docs/workers/reference/module-registry.mdx +++ b/src/content/docs/workers/reference/module-registry.mdx @@ -44,14 +44,22 @@ Workers provides `url`, `main`, `filename`, `dirname`, and `resolve()` when the ```ts +const moduleMeta = import.meta as ImportMeta & { + readonly url: string; + readonly main: boolean; + readonly filename?: string; + readonly dirname?: string; + resolve(specifier: string): string; +}; + export default { async fetch() { return Response.json({ - url: import.meta.url, - main: import.meta.main, - filename: import.meta.filename, - dirname: import.meta.dirname, - resolved: import.meta.resolve("./utils.js"), + url: moduleMeta.url, + main: moduleMeta.main, + filename: moduleMeta.filename, + dirname: moduleMeta.dirname, + resolved: moduleMeta.resolve("./utils.js"), }); }, }; @@ -263,7 +271,7 @@ Modules are not compiled until they are first imported. Statically imported depe For example, this Worker only imports and compiles `report.mjs` for requests to `/report`: -```js title="modules/report.mjs" +```js title="src/modules/report.mjs" export function generateReport() { return "Your report is ready"; } @@ -277,7 +285,7 @@ export default { const url = new URL(request.url); if (url.pathname === "/report") { - const { generateReport } = await import("../modules/report.mjs"); + const { generateReport } = await import("./modules/report.mjs"); return new Response(generateReport()); } diff --git a/src/content/docs/workers/runtime-apis/nodejs/index.mdx b/src/content/docs/workers/runtime-apis/nodejs/index.mdx index 6607df2bb0d..2938b364fe2 100644 --- a/src/content/docs/workers/runtime-apis/nodejs/index.mdx +++ b/src/content/docs/workers/runtime-apis/nodejs/index.mdx @@ -56,14 +56,18 @@ Enable both flags to use [`createRequire()`](https://nodejs.org/api/module.html# import { createRequire } from "node:module"; import process from "node:process"; -const require = createRequire(import.meta.url); +const moduleMeta = import.meta as ImportMeta & { + readonly url: string; + resolve(specifier: string): string; +}; +const require = createRequire(moduleMeta.url); const requiredProcess = require("node:process"); export default { async fetch() { return Response.json({ sameInstance: requiredProcess === process, - resolved: import.meta.resolve("process"), + resolved: moduleMeta.resolve("process"), }); }, }; diff --git a/src/content/docs/workers/runtime-apis/web-standards.mdx b/src/content/docs/workers/runtime-apis/web-standards.mdx index 4b881802303..c15ad84be1c 100644 --- a/src/content/docs/workers/runtime-apis/web-standards.mdx +++ b/src/content/docs/workers/runtime-apis/web-standards.mdx @@ -35,12 +35,18 @@ Use these properties to inspect the current module or resolve another module wit ```ts +const moduleMeta = import.meta as ImportMeta & { + readonly url: string; + readonly main: boolean; + resolve(specifier: string): string; +}; + export default { async fetch() { return Response.json({ - currentModule: import.meta.url, - isEntrypoint: import.meta.main, - dependency: import.meta.resolve("./dependency.js"), + currentModule: moduleMeta.url, + isEntrypoint: moduleMeta.main, + dependency: moduleMeta.resolve("./dependency.js"), }); }, };