Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

More incremental impl of new module registry #2681

Merged
merged 1 commit into from
Sep 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/workerd/api/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -534,3 +534,9 @@ wd_test(
"//conditions:default": ["@platforms//:incompatible"],
}),
)

wd_test(
src = "tests/new-module-registry-test.wd-test",
args = ["--experimental"],
data = ["tests/new-module-registry-test.js"],
)
176 changes: 176 additions & 0 deletions src/workerd/api/tests/new-module-registry-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import {
notStrictEqual,
ok,
rejects,
strictEqual,
deepStrictEqual,
} from 'assert';
import { foo, default as def } from 'foo';
import { default as fs } from 'node:fs';
import { Buffer } from 'buffer';
const { foo: foo2, default: def2 } = await import('bar');

// Verify that import.meta.url is correct here.
strictEqual(import.meta.url, 'file:///worker');

// Verify that import.meta.main is true here.
ok(import.meta.main);

// Verify that import.meta.resolve provides correct results here.
// The input should be interpreted as a URL and normalized according
// to the rules in the WHATWG URL specification.
strictEqual(import.meta.resolve('./.././test/.././../foo'), 'file:///foo');

// There are four tests at this top level... one for the import of the node:assert
// module without the node: prefix specifier, two for the imports of the foo and
// bar modules from the worker, and one for the aliases node:fs module from the
// module worker.

strictEqual(foo, 1);
strictEqual(def, 2);
strictEqual(foo2, 1);
strictEqual(def2, 2);
strictEqual(fs, 'abc');

// Equivalent to the above, but using the file: URL scheme.
import { foo as foo3, default as def3 } from 'file:///foo';
strictEqual(foo, foo3);
strictEqual(def, def3);

import * as text from 'text';
strictEqual(text.default, 'abc');

import * as data from 'data';
strictEqual(Buffer.from(data.default).toString(), 'abcdef');

import * as json from 'json';
deepStrictEqual(json.default, { foo: 1 });

await rejects(import('invalid-json'), {
message: /Unexpected non-whitespace character after JSON/,
});

await rejects(import('module-not-found'), {
message: /Module not found: file:\/\/\/module-not-found/,
});

// Verify that a module is unable to perform IO operations at the top level, even if
// the dynamic import is initiated within the scope of an active IoContext.
export const noTopLevelIo = {
async test() {
await rejects(import('bad'), {
message: /^Disallowed operation called within global scope/,
});
},
};

// Verify that async local storage is propagated into dynamic imports.
export const alsPropagationDynamicImport = {
async test() {
const { AsyncLocalStorage } = await import('async_hooks');
globalThis.als = new AsyncLocalStorage();
const res = await globalThis.als.run(123, () => import('als'));
strictEqual(res.default, 123);
},
};

// Query strings and fragments create new instances of known modules.
export const queryAndFragment = {
async test() {
// Each resolves the same underlying module but creates a new instance.
// The exports should be the same but the module namespaces should be different.

const a = await import('foo?query');
const b = await import('foo#fragment');
const c = await import('foo?query#fragment');
const d = await import('foo');

strictEqual(a.default, 2);
strictEqual(a.foo, 1);
strictEqual(a.default, b.default);
strictEqual(a.default, c.default);
strictEqual(a.default, d.default);
strictEqual(a.foo, b.foo);
strictEqual(a.foo, c.foo);
strictEqual(a.foo, d.foo);

notStrictEqual(a, b);
notStrictEqual(a, c);
notStrictEqual(a, d);
notStrictEqual(b, c);
notStrictEqual(b, d);
notStrictEqual(c, d);

// The import.meta.url for each should match the specifier used to import the instance.
strictEqual(a.bar, 'file:///foo?query');
strictEqual(b.bar, 'file:///foo#fragment');
strictEqual(c.bar, 'file:///foo?query#fragment');
strictEqual(d.bar, 'file:///foo');
},
};

// We do not currently support import assertions/attributes. Per the recommendation
// in the spec, we throw an error when they are encountered.
export const importAssertionsFail = {
async test() {
await rejects(import('ia'), {
message: /^Import attributes are not supported/,
});
await rejects(import('foo', { with: { a: 'abc' } }), {
message: /^Import attributes are not supported/,
});
},
};

export const invalidUrlAsSpecifier = {
async test() {
await rejects(import('zebra: not a \x00 valid URL'), {
message: /Module not found/,
});
},
};

export const evalErrorsInEsmTopLevel = {
async test() {
await rejects(import('esm-error'), {
message: /boom/,
});
await rejects(import('esm-error-dynamic'), {
message: /boom/,
});
},
};

// TODO(now): Tests
// * [ ] Include tests for all known module types
// * [x] ESM
// * [ ] CommonJS
// * [x] Text
// * [x] Data
// * [x] JSON
// * [ ] WASM
// * [ ] Python
// * [x] IO is forbidden in top-level module scope
// * [x] Async local storage context is propagated into dynamic imports
// * [x] Static import correctly handles node: modules with/without the node: prefix
// * [x] Dynamic import correctly handles node: modules with/without the node: prefix
// * [x] Worker bundle can alias node: modules
// * [x] modules not found are correctly reported as errors
// * [x] Errors during ESM evaluation are correctly reported
// * [x] Errors during dynamic import are correctly reported
// * [x] Errors in JSON module parsing are correctly reported
// * [x] Module specifiers are correctly handled as URLs
// * [x] Querys and fragments resolve new instances of known modules
// * [x] URL resolution works correctly
// * [x] Invalid URLs are correctly reported as errors
// * [x] Import assertions should be rejected
// * [ ] require(...) Works in CommonJs Modules
// * [ ] require(...) correctly handles node: modules with/without the node: prefix
// * [ ] Circular dependencies are correctly handled
// * [ ] Errors during CommonJs evaluation are correctly reported
// * [ ] Entry point ESM with no default export is correctly reported as error
// * [ ] CommonJs modules correctly expose named exports
// * [ ] require('module').createRequire API works as expected
// * [ ] Fallback service works as expected
// * [ ] console.log output correctly uses node-internal:inspect for output
// ...
43 changes: 43 additions & 0 deletions src/workerd/api/tests/new-module-registry-test.wd-test
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using Workerd = import "/workerd/workerd.capnp";

const unitTests :Workerd.Config = (
services = [
( name = "new-module-registry-test",
worker = (
modules = [
(name = "worker", esModule = embed "new-module-registry-test.js"),
(name = "foo", esModule = "export const foo = 1; export default 2; export const bar = import.meta.url"),
(name = "bar", esModule = "export const foo = 1; export default 2;"),
(name = "node:fs", esModule = "export default 'abc'"),

# Intentionally bad module to test error handling.
# Evaluation will error because i/o is not permitted at top-level scope.
(name = "bad", esModule = "export default 1; setTimeout(() => {}, 10)"),

# Ensure that async context is propagated into a dynamic import.
(name = "als", esModule = "export default globalThis.als.getStore()"),

# Import assertions are not supported currently
(name = "ia", esModule = "import * as def from 'foo' with { a: 'test' }"),

# Errors on ESM eval should be reported properly in both static and
# dynamic imports.
(name = "esm-error", esModule = "export default 1; throw new Error('boom');"),
(name = "esm-error-dynamic", esModule = "export * as d from 'esm-error'"),

# Other module types work
(name = "text", text = "abc"),
(name = "data", data = "abcdef"),
(name = "json", json = "{ \"foo\": 1 }"),
(name = "invalid-json", json = "1n"),
],
compatibilityDate = "2024-07-01",
compatibilityFlags = [
"nodejs_compat_v2",
"new_module_registry",
"experimental",
],
)
),
],
);
Loading