File
Blob: src/workerd/api/tests/new-module-registry-test.js
| 1 | // Copyright (c) 2024 Cloudflare, Inc. |
| 2 | // Licensed under the Apache 2.0 license found in the LICENSE file or at: |
| 3 | // https://opensource.org/licenses/Apache-2.0 |
| 4 | import { |
| 5 | default as assert, |
| 6 | notStrictEqual, |
| 7 | ok, |
| 8 | rejects, |
| 9 | strictEqual, |
| 10 | throws, |
| 11 | deepStrictEqual, |
| 12 | } from 'assert'; // Intentionally omit the 'node:' prefix |
| 13 | import { foo, default as def } from 'foo'; |
| 14 | import { default as fs } from 'node:fs'; |
| 15 | import { Buffer } from 'buffer'; // Intentionally omit the 'node:' prefix |
| 16 | import { foo as foo2, default as def2 } from 'bar'; |
| 17 | import { createRequire } from 'module'; // Intentionally omit the 'node:' prefix |
| 18 | |
| 19 | import * as workers from 'cloudflare:workers'; |
| 20 | strictEqual(typeof workers, 'object'); |
| 21 | strictEqual(typeof workers.DurableObject, 'function'); |
| 22 | strictEqual(typeof workers.RpcPromise, 'function'); |
| 23 | strictEqual(typeof workers.RpcProperty, 'function'); |
| 24 | strictEqual(typeof workers.RpcStub, 'function'); |
| 25 | strictEqual(typeof workers.RpcTarget, 'function'); |
| 26 | strictEqual(typeof workers.ServiceStub, 'function'); |
| 27 | strictEqual(typeof workers.WorkerEntrypoint, 'function'); |
| 28 | strictEqual(typeof workers.WorkflowEntrypoint, 'function'); |
| 29 | strictEqual(typeof workers.waitUntil, 'function'); |
| 30 | strictEqual(typeof workers.withEnv, 'function'); |
| 31 | strictEqual(typeof workers.env, 'object'); |
| 32 | strictEqual(typeof workers.cache, 'object'); |
| 33 | |
| 34 | await rejects(import('cloudflare-internal:env'), { |
| 35 | message: /Module not found/, |
| 36 | }); |
| 37 | await rejects(import('cloudflare-internal:filesystem')); |
| 38 | |
| 39 | // Verify that import.meta.url is correct here. |
| 40 | strictEqual(import.meta.url, 'file:///bundle/worker'); |
| 41 | |
| 42 | // Verify that import.meta.main is true here. |
| 43 | ok(import.meta.main); |
| 44 | |
| 45 | // When running in nodejs_compat_v2 mode, the globalThis.Buffer |
| 46 | // and globalThis.process properties are resolved from the module |
| 47 | // registry. Let's make sure we get good values here. |
| 48 | strictEqual(typeof globalThis.Buffer, 'function'); |
| 49 | strictEqual(globalThis.Buffer, Buffer); |
| 50 | ok(globalThis.process); |
| 51 | strictEqual(typeof globalThis.process, 'object'); |
| 52 | |
| 53 | // Verify that process.getBuiltinModule works correctly with |
| 54 | // the new module registry. |
| 55 | const builtinBuffer1 = process.getBuiltinModule('buffer'); |
| 56 | const builtinBuffer2 = process.getBuiltinModule('node:buffer'); |
| 57 | strictEqual(builtinBuffer1, builtinBuffer2); |
| 58 | strictEqual(builtinBuffer1.Buffer, globalThis.Buffer); |
| 59 | const nonExistent = process.getBuiltinModule('non-existent'); |
| 60 | strictEqual(nonExistent, undefined); |
| 61 | |
| 62 | // Our internal implementation of console.log depend on the module registry |
| 63 | // to get the node-internal:internal_inspect module. This console.log makes |
| 64 | // sure that works correctly without crashing when using the new module |
| 65 | // registry implementation. |
| 66 | console.log(import.meta); |
| 67 | |
| 68 | // Verify that import.meta.resolve provides correct results here. |
| 69 | // The input should be interpreted as a URL and normalized according |
| 70 | // to the rules in the WHATWG URL specification. |
| 71 | strictEqual(import.meta.resolve('./.././test/.././.%2e/foo'), 'file:///foo'); |
| 72 | strictEqual(import.meta.resolve('foo'), 'file:///bundle/foo'); |
| 73 | |
| 74 | // There are four tests at this top level... one for the import of the node:assert |
| 75 | // module without the node: prefix specifier, two for the imports of the foo and |
| 76 | // bar modules from the worker, and one for the aliases node:fs module from the |
| 77 | // module worker. |
| 78 | |
| 79 | strictEqual(foo, 1); |
| 80 | strictEqual(def, 2); |
| 81 | strictEqual(foo2, 1); |
| 82 | strictEqual(def2, 2); |
| 83 | strictEqual(fs, 'abc'); |
| 84 | |
| 85 | // Equivalent to the above, but using the file: URL scheme. |
| 86 | import { foo as foo3, default as def3 } from 'file:///bundle/foo'; |
| 87 | strictEqual(foo, foo3); |
| 88 | strictEqual(def, def3); |
| 89 | |
| 90 | import * as text from 'text'; |
| 91 | strictEqual(text.default, 'abc'); |
| 92 | |
| 93 | import * as data from 'data'; |
| 94 | strictEqual(Buffer.from(data.default).toString(), 'abcdef'); |
| 95 | |
| 96 | import * as json from 'json'; |
| 97 | deepStrictEqual(json.default, { foo: 1 }); |
| 98 | |
| 99 | // Synchronously resolved promises can be awaited. |
| 100 | await Promise.resolve(); |
| 101 | |
| 102 | // CommonJS modules can be imported and should work as expected. |
| 103 | import { default as cjs2 } from 'cjs2'; |
| 104 | strictEqual(cjs2.foo, 1); |
| 105 | strictEqual(cjs2.bar, 2); |
| 106 | strictEqual(cjs2.filename, 'cjs1'); |
| 107 | strictEqual(cjs2.dirname, '/bundle'); |
| 108 | strictEqual(cjs2.assert, assert); |
| 109 | |
| 110 | // CommonJS modules can define named exports. |
| 111 | import { foo as cjs1foo, bar as cjs1bar } from 'cjs1'; |
| 112 | strictEqual(cjs1foo, 1); |
| 113 | strictEqual(cjs1bar, 2); |
| 114 | |
| 115 | // The createRequire API works as expected. |
| 116 | const myRequire = createRequire(import.meta.url); |
| 117 | const customRequireCjs = myRequire('cjs1'); |
| 118 | strictEqual(customRequireCjs.foo, cjs1foo); |
| 119 | strictEqual(customRequireCjs.bar, cjs1bar); |
| 120 | |
| 121 | const customRequireCjs2 = myRequire(import.meta.resolve('cjs2')); |
| 122 | strictEqual(customRequireCjs2.foo, cjs2.foo); |
| 123 | |
| 124 | // When the module being imported throws an error during evaluation, |
| 125 | // the error is propagated correctly. |
| 126 | await rejects(import('file:///bundle/cjs3'), { |
| 127 | message: 'boom', |
| 128 | }); |
| 129 | |
| 130 | // The modules cjs4 and cjs5 have a circular dependency on each other. |
| 131 | // They should both load without error. |
| 132 | import { default as cjs4 } from 'cjs4'; |
| 133 | import { default as cjs5 } from 'cjs5'; |
| 134 | deepStrictEqual(cjs4, {}); |
| 135 | deepStrictEqual(cjs5, {}); |
| 136 | |
| 137 | // These dynamics imports can be top-level awaited because they |
| 138 | // are immediately rejected with errors. |
| 139 | await rejects(import('invalid-json'), { |
| 140 | message: /Unexpected non-whitespace character after JSON/, |
| 141 | }); |
| 142 | |
| 143 | await rejects(import('module-not-found'), { |
| 144 | message: /Module not found: file:\/\/\/bundle\/module-not-found/, |
| 145 | }); |
| 146 | |
| 147 | await rejects(import('file:///outside'), { |
| 148 | message: /Module not found/, |
| 149 | }); |
| 150 | |
| 151 | await import('file:///bundle/outside'); |
| 152 | |
| 153 | const abc123 = await import('abc123'); |
| 154 | strictEqual(abc123.default, 1); |
| 155 | |
| 156 | // Full URLs can be used as module specifiers. These would not |
| 157 | // actually resolve to network requests. |
| 158 | const mod = await import('https://example.com/mod'); |
| 159 | strictEqual(mod.default, 'example'); |
| 160 | |
| 161 | // Whitespace and leading ./, ../, and multiple slashes are |
| 162 | // stripped and ignored when setting up the module registry, |
| 163 | // so they should resolve as expected here as being relative |
| 164 | // to the bundle base URL. |
| 165 | const mod2 = await import(' ./should/be/ok '); |
| 166 | strictEqual(mod2.default, 1); |
| 167 | |
| 168 | // UTF-8 percent-encoded of 部品 (Japanese for "component") |
| 169 | const mod3 = await import('%E9%83%A8%E5%93%81'); |
| 170 | const mod4 = await import('部品'); // Get's converted into UTF-8 bytes in source |
| 171 | const mod5 = await import('\u90e8\u54c1'); // Specifically UTF-16 code units in source |
| 172 | const mod6 = await import('\xE9\x83\xA8\xE5\x93\x81'); |
| 173 | import { default as mod7 } from '部品'; |
| 174 | import { default as mod8 } from '\u90e8\u54c1'; |
| 175 | import { default as mod9 } from '\xE9\x83\xA8\xE5\x93\x81'; |
| 176 | import { default as mod10 } from '%E9%83%A8%E5%93%81'; |
| 177 | |
| 178 | strictEqual(mod3.default, 1); |
| 179 | strictEqual(mod4.default, 1); |
| 180 | strictEqual(mod5.default, 1); |
| 181 | strictEqual(mod6.default, 1); |
| 182 | strictEqual(mod7, 1); |
| 183 | strictEqual(mod8, 1); |
| 184 | strictEqual(mod9, 1); |
| 185 | strictEqual(mod10, 1); |
| 186 | |
| 187 | // The percent-encoded UTF-16 form of 部品 should not work. |
| 188 | await rejects(import('%E8%90%C1%54'), { |
| 189 | message: /Module not found/, |
| 190 | }); |
| 191 | await rejects(import('%90%E8%54%C1'), { |
| 192 | message: /Module not found/, |
| 193 | }); |
| 194 | |
| 195 | // The cjs6 module attempts to require and esm with a top-level await, which is rejected |
| 196 | // following node.js' established require(esm) precedent. |
| 197 | await rejects(import('cjs6'), { |
| 198 | message: /^Top-level await is not supported/, |
| 199 | }); |
| 200 | |
| 201 | // Cannot directly require an ESM with top-level await either. |
| 202 | throws(() => myRequire('tla'), { |
| 203 | message: /^Top-level await is not supported/, |
| 204 | }); |
| 205 | |
| 206 | // Verify that a module is unable to perform IO operations at the top level, even if |
| 207 | // the dynamic import is initiated within the scope of an active IoContext. |
| 208 | export const noTopLevelIo = { |
| 209 | async test() { |
| 210 | await rejects(import('bad'), { |
| 211 | message: /^Disallowed operation called within global scope/, |
| 212 | }); |
| 213 | }, |
| 214 | }; |
| 215 | |
| 216 | // Verify that async local storage is propagated into dynamic imports. |
| 217 | export const alsPropagationDynamicImport = { |
| 218 | async test() { |
| 219 | const { AsyncLocalStorage } = await import('async_hooks'); |
| 220 | globalThis.als = new AsyncLocalStorage(); |
| 221 | const res = await globalThis.als.run(123, () => import('als')); |
| 222 | strictEqual(res.default, 123); |
| 223 | delete globalThis.als; |
| 224 | }, |
| 225 | }; |
| 226 | |
| 227 | // Query strings and fragments create new instances of known modules. |
| 228 | export const queryAndFragment = { |
| 229 | async test() { |
| 230 | // Each resolves the same underlying module but creates a new instance. |
| 231 | // The exports should be the same but the module namespaces should be different. |
| 232 | |
| 233 | const a = await import('foo?query'); |
| 234 | const b = await import('foo#fragment'); |
| 235 | const c = await import('foo?query#fragment'); |
| 236 | const d = await import('foo'); |
| 237 | |
| 238 | strictEqual(a.default, 2); |
| 239 | strictEqual(a.foo, 1); |
| 240 | strictEqual(a.default, b.default); |
| 241 | strictEqual(a.default, c.default); |
| 242 | strictEqual(a.default, d.default); |
| 243 | strictEqual(a.foo, b.foo); |
| 244 | strictEqual(a.foo, c.foo); |
| 245 | strictEqual(a.foo, d.foo); |
| 246 | |
| 247 | notStrictEqual(a, b); |
| 248 | notStrictEqual(a, c); |
| 249 | notStrictEqual(a, d); |
| 250 | notStrictEqual(b, c); |
| 251 | notStrictEqual(b, d); |
| 252 | notStrictEqual(c, d); |
| 253 | |
| 254 | // The import.meta.url for each should match the specifier used to import the instance. |
| 255 | strictEqual(a.bar, 'file:///bundle/foo?query'); |
| 256 | strictEqual(b.bar, 'file:///bundle/foo#fragment'); |
| 257 | strictEqual(c.bar, 'file:///bundle/foo?query#fragment'); |
| 258 | strictEqual(d.bar, 'file:///bundle/foo'); |
| 259 | }, |
| 260 | }; |
| 261 | |
| 262 | // Unrecognized import attributes are rejected. |
| 263 | export const importAssertionsFail = { |
| 264 | async test() { |
| 265 | await rejects(import('ia'), { |
| 266 | message: /^Unsupported import attribute: "a"/, |
| 267 | }); |
| 268 | await rejects(import('foo', { with: { a: 'abc' } }), { |
| 269 | message: /^Unsupported import attribute: "a"/, |
| 270 | }); |
| 271 | }, |
| 272 | }; |
| 273 | |
| 274 | export const invalidUrlAsSpecifier = { |
| 275 | async test() { |
| 276 | await rejects(import('zebra: not a \x00 valid URL'), { |
| 277 | message: /Module not found/, |
| 278 | }); |
| 279 | }, |
| 280 | }; |
| 281 | |
| 282 | export const evalErrorsInEsmTopLevel = { |
| 283 | async test() { |
| 284 | await rejects(import('esm-error'), { |
| 285 | message: /boom/, |
| 286 | }); |
| 287 | await rejects(import('esm-error-dynamic'), { |
| 288 | message: /boom/, |
| 289 | }); |
| 290 | }, |
| 291 | }; |
| 292 | |
| 293 | export const wasmModuleTest = { |
| 294 | async test() { |
| 295 | const { default: wasm } = await import('wasm'); |
| 296 | ok(wasm instanceof WebAssembly.Module); |
| 297 | await WebAssembly.instantiate(wasm, {}); |
| 298 | }, |
| 299 | }; |
| 300 | |
| 301 | import source wasmSource from 'wasm'; |
| 302 | export const wasmSourcePhaseTest = { |
| 303 | async test() { |
| 304 | ok(wasmSource instanceof WebAssembly.Module); |
| 305 | await WebAssembly.instantiate(wasmSource, {}); |
| 306 | }, |
| 307 | }; |
| 308 | |
| 309 | export const wasmDynamicSourcePhaseTest = { |
| 310 | async test() { |
| 311 | const wasmSource = await import.source('wasm'); |
| 312 | ok(wasmSource instanceof WebAssembly.Module); |
| 313 | await WebAssembly.instantiate(wasmSource, {}); |
| 314 | }, |
| 315 | }; |
| 316 | |
| 317 | export const wasmDynamicSourcePhaseFailureTest = { |
| 318 | async test() { |
| 319 | await rejects(import.source('foo'), { |
| 320 | message: |
| 321 | 'Source phase import not available for module: file:///bundle/foo', |
| 322 | }); |
| 323 | }, |
| 324 | }; |
| 325 | |
| 326 | export const complexModuleTest = { |
| 327 | async test() { |
| 328 | const { abc } = await import('complex'); |
| 329 | strictEqual(abc.foo, 1); |
| 330 | strictEqual(abc.def.bar, 2); |
| 331 | |
| 332 | const { default: abc2 } = await import('abc'); |
| 333 | strictEqual(abc2, 'file:///bundle/abc'); |
| 334 | }, |
| 335 | }; |
| 336 | |
| 337 | // Regression test: getBuiltinModule called from a function defined in eval'd |
| 338 | // code should work. Previously failed on first invocation with "top-level await" |
| 339 | // error due to extra microtask tick from wrapSimplePromise(). |
| 340 | export const getBuiltinModuleFromEval = { |
| 341 | async test(_, env) { |
| 342 | const code = `"use strict";async (exports)=>{ |
| 343 | exports.getPath = () => process.getBuiltinModule("node:path"); |
| 344 | }`; |
| 345 | const exports = {}; |
| 346 | const fn = env.unsafe.eval(code); |
| 347 | await fn(exports); |
| 348 | |
| 349 | const path = exports.getPath(); |
| 350 | ok(path.join, 'node:path should have join'); |
| 351 | }, |
| 352 | }; |
| 353 | |
| 354 | // Regression test: createRequire called from a function defined in eval'd |
| 355 | // code should work. Same root cause as getBuiltinModuleFromEval. |
| 356 | export const createRequireFromEval = { |
| 357 | async test(_, env) { |
| 358 | const code = `"use strict";async (exports)=>{ |
| 359 | const { createRequire } = process.getBuiltinModule("node:module"); |
| 360 | const require = createRequire("/"); |
| 361 | exports.getUtil = () => require("node:util"); |
| 362 | }`; |
| 363 | const exports = {}; |
| 364 | const fn = env.unsafe.eval(code); |
| 365 | await fn(exports); |
| 366 | |
| 367 | const util = exports.getUtil(); |
| 368 | ok(util.promisify, 'node:util should have promisify'); |
| 369 | }, |
| 370 | }; |
| 371 | |
| 372 | // TODO(now): Tests |
| 373 | // * [x] Include tests for all known module types |
| 374 | // * [x] ESM |
| 375 | // * [x] CommonJS |
| 376 | // * [x] Text |
| 377 | // * [x] Data |
| 378 | // * [x] JSON |
| 379 | // * [x] WASM |
| 380 | // * [x] Python (works, but still needs to be fully tested) |
| 381 | // * [x] IO is forbidden in top-level module scope |
| 382 | // * [x] Async local storage context is propagated into dynamic imports |
| 383 | // * [x] Static import correctly handles node: modules with/without the node: prefix |
| 384 | // * [x] Dynamic import correctly handles node: modules with/without the node: prefix |
| 385 | // * [x] Worker bundle can alias node: modules |
| 386 | // * [x] modules not found are correctly reported as errors |
| 387 | // * [x] Errors during ESM evaluation are correctly reported |
| 388 | // * [x] Errors during dynamic import are correctly reported |
| 389 | // * [x] Errors in JSON module parsing are correctly reported |
| 390 | // * [x] Module specifiers are correctly handled as URLs |
| 391 | // * [x] Querys and fragments resolve new instances of known modules |
| 392 | // * [x] URL resolution works correctly |
| 393 | // * [x] Invalid URLs are correctly reported as errors |
| 394 | // * [x] Import attributes should be rejected |
| 395 | // * [x] require(...) Works in CommonJs Modules |
| 396 | // * [x] require(...) correctly handles node: modules with/without the node: prefix |
| 397 | // * [x] Circular dependencies are correctly handled |
| 398 | // * [x] Errors during CommonJs evaluation are correctly reported |
| 399 | // * [x] CommonJs modules correctly expose named exports |
| 400 | // * [x] require('module').createRequire API works as expected |
| 401 | // * [x] Entry point ESM with no default export is correctly reported as error |
| 402 | // * [ ] Fallback service works as expected |
| 403 | // * [x] console.log output correctly uses node-internal:inspect for output |
| 404 | // ... |