Skip to content
File

Blob: src/workerd/api/tests/new-module-registry-test.js

javascript405 lines
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
4import {
5 default as assert,
6 notStrictEqual,
7 ok,
8 rejects,
9 strictEqual,
10 throws,
11 deepStrictEqual,
12} from 'assert'; // Intentionally omit the 'node:' prefix
13import { foo, default as def } from 'foo';
14import { default as fs } from 'node:fs';
15import { Buffer } from 'buffer'; // Intentionally omit the 'node:' prefix
16import { foo as foo2, default as def2 } from 'bar';
17import { createRequire } from 'module'; // Intentionally omit the 'node:' prefix
18 
19import * as workers from 'cloudflare:workers';
20strictEqual(typeof workers, 'object');
21strictEqual(typeof workers.DurableObject, 'function');
22strictEqual(typeof workers.RpcPromise, 'function');
23strictEqual(typeof workers.RpcProperty, 'function');
24strictEqual(typeof workers.RpcStub, 'function');
25strictEqual(typeof workers.RpcTarget, 'function');
26strictEqual(typeof workers.ServiceStub, 'function');
27strictEqual(typeof workers.WorkerEntrypoint, 'function');
28strictEqual(typeof workers.WorkflowEntrypoint, 'function');
29strictEqual(typeof workers.waitUntil, 'function');
30strictEqual(typeof workers.withEnv, 'function');
31strictEqual(typeof workers.env, 'object');
32strictEqual(typeof workers.cache, 'object');
33 
34await rejects(import('cloudflare-internal:env'), {
35 message: /Module not found/,
36});
37await rejects(import('cloudflare-internal:filesystem'));
38 
39// Verify that import.meta.url is correct here.
40strictEqual(import.meta.url, 'file:///bundle/worker');
41 
42// Verify that import.meta.main is true here.
43ok(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.
48strictEqual(typeof globalThis.Buffer, 'function');
49strictEqual(globalThis.Buffer, Buffer);
50ok(globalThis.process);
51strictEqual(typeof globalThis.process, 'object');
52 
53// Verify that process.getBuiltinModule works correctly with
54// the new module registry.
55const builtinBuffer1 = process.getBuiltinModule('buffer');
56const builtinBuffer2 = process.getBuiltinModule('node:buffer');
57strictEqual(builtinBuffer1, builtinBuffer2);
58strictEqual(builtinBuffer1.Buffer, globalThis.Buffer);
59const nonExistent = process.getBuiltinModule('non-existent');
60strictEqual(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.
66console.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.
71strictEqual(import.meta.resolve('./.././test/.././.%2e/foo'), 'file:///foo');
72strictEqual(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 
79strictEqual(foo, 1);
80strictEqual(def, 2);
81strictEqual(foo2, 1);
82strictEqual(def2, 2);
83strictEqual(fs, 'abc');
84 
85// Equivalent to the above, but using the file: URL scheme.
86import { foo as foo3, default as def3 } from 'file:///bundle/foo';
87strictEqual(foo, foo3);
88strictEqual(def, def3);
89 
90import * as text from 'text';
91strictEqual(text.default, 'abc');
92 
93import * as data from 'data';
94strictEqual(Buffer.from(data.default).toString(), 'abcdef');
95 
96import * as json from 'json';
97deepStrictEqual(json.default, { foo: 1 });
98 
99// Synchronously resolved promises can be awaited.
100await Promise.resolve();
101 
102// CommonJS modules can be imported and should work as expected.
103import { default as cjs2 } from 'cjs2';
104strictEqual(cjs2.foo, 1);
105strictEqual(cjs2.bar, 2);
106strictEqual(cjs2.filename, 'cjs1');
107strictEqual(cjs2.dirname, '/bundle');
108strictEqual(cjs2.assert, assert);
109 
110// CommonJS modules can define named exports.
111import { foo as cjs1foo, bar as cjs1bar } from 'cjs1';
112strictEqual(cjs1foo, 1);
113strictEqual(cjs1bar, 2);
114 
115// The createRequire API works as expected.
116const myRequire = createRequire(import.meta.url);
117const customRequireCjs = myRequire('cjs1');
118strictEqual(customRequireCjs.foo, cjs1foo);
119strictEqual(customRequireCjs.bar, cjs1bar);
120 
121const customRequireCjs2 = myRequire(import.meta.resolve('cjs2'));
122strictEqual(customRequireCjs2.foo, cjs2.foo);
123 
124// When the module being imported throws an error during evaluation,
125// the error is propagated correctly.
126await 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.
132import { default as cjs4 } from 'cjs4';
133import { default as cjs5 } from 'cjs5';
134deepStrictEqual(cjs4, {});
135deepStrictEqual(cjs5, {});
136 
137// These dynamics imports can be top-level awaited because they
138// are immediately rejected with errors.
139await rejects(import('invalid-json'), {
140 message: /Unexpected non-whitespace character after JSON/,
141});
142 
143await rejects(import('module-not-found'), {
144 message: /Module not found: file:\/\/\/bundle\/module-not-found/,
145});
146 
147await rejects(import('file:///outside'), {
148 message: /Module not found/,
149});
150 
151await import('file:///bundle/outside');
152 
153const abc123 = await import('abc123');
154strictEqual(abc123.default, 1);
155 
156// Full URLs can be used as module specifiers. These would not
157// actually resolve to network requests.
158const mod = await import('https://example.com/mod');
159strictEqual(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.
165const mod2 = await import(' ./should/be/ok ');
166strictEqual(mod2.default, 1);
167 
168// UTF-8 percent-encoded of 部品 (Japanese for "component")
169const mod3 = await import('%E9%83%A8%E5%93%81');
170const mod4 = await import('部品'); // Get's converted into UTF-8 bytes in source
171const mod5 = await import('\u90e8\u54c1'); // Specifically UTF-16 code units in source
172const mod6 = await import('\xE9\x83\xA8\xE5\x93\x81');
173import { default as mod7 } from '部品';
174import { default as mod8 } from '\u90e8\u54c1';
175import { default as mod9 } from '\xE9\x83\xA8\xE5\x93\x81';
176import { default as mod10 } from '%E9%83%A8%E5%93%81';
177 
178strictEqual(mod3.default, 1);
179strictEqual(mod4.default, 1);
180strictEqual(mod5.default, 1);
181strictEqual(mod6.default, 1);
182strictEqual(mod7, 1);
183strictEqual(mod8, 1);
184strictEqual(mod9, 1);
185strictEqual(mod10, 1);
186 
187// The percent-encoded UTF-16 form of 部品 should not work.
188await rejects(import('%E8%90%C1%54'), {
189 message: /Module not found/,
190});
191await 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.
197await rejects(import('cjs6'), {
198 message: /^Top-level await is not supported/,
199});
200 
201// Cannot directly require an ESM with top-level await either.
202throws(() => 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.
208export 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.
217export 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.
228export 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.
263export 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 
274export const invalidUrlAsSpecifier = {
275 async test() {
276 await rejects(import('zebra: not a \x00 valid URL'), {
277 message: /Module not found/,
278 });
279 },
280};
281 
282export 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 
293export 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 
301import source wasmSource from 'wasm';
302export const wasmSourcePhaseTest = {
303 async test() {
304 ok(wasmSource instanceof WebAssembly.Module);
305 await WebAssembly.instantiate(wasmSource, {});
306 },
307};
308 
309export 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 
317export 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 
326export 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().
340export 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.
356export 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// ...