Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions build/wd_ts_test.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ load("@aspect_rules_js//js:defs.bzl", "js_test")
load("//:build/typescript.bzl", "js_name", "module_name")
load("//:build/wd_ts_project.bzl", "wd_ts_project")

def wd_ts_test(src, tsconfig_json, deps = [], eslintrc_json = None, composite = True, **kwargs):
def wd_ts_test(src, tsconfig_json, deps = [], data = [], eslintrc_json = None, composite = True, **kwargs):
"""Bazel rule to compile and run a TypeScript test"""

name = module_name(src)
Expand All @@ -20,7 +20,7 @@ def wd_ts_test(src, tsconfig_json, deps = [], eslintrc_json = None, composite =
js_test(
name = name,
entry_point = js_name(src),
data = deps + [name + "@compile"],
data = deps + data + [name + "@compile"],
tags = ["js-test"],
**kwargs
)
3 changes: 2 additions & 1 deletion src/rust/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ _Snapshot — the set drifts as crates come and go; `bazel query //src/rust/...`
- **JSG nullish values**: `Option<T>` is `T | undefined`; `Nullable<T>` is `T | null` (coerces `undefined` to `Null`, but method arguments remain required). Prefer `NullOrUndefined<T>` to `Option<Nullable<T>>` for three distinct `Some(T)`, `Null`, and `Undefined` states; omitted arguments become `Undefined`. `Lenient<T>` additionally preserves shallow mismatches as `Unconvertable`; converting it to `NullOrUndefined<T>` maps those to `Undefined`, and `Lenient::to_parts()` also returns the original JS value. See [Rust JSG nullish values](jsg/README.md#nullablet-and-nullorundefinedt).
- **JSG resources**: `#[jsg_resource]` on struct + impl block; `#[jsg_method]` auto-converts `snake_case` → `camelCase`; methods with `&self` become instance methods, methods without a receiver become static methods, and any other receiver is a compile error (JavaScript can re-enter a resource, so state that methods change goes in `Cell`/`RefCell` fields; never hold a `RefCell` borrow across a call that can run JavaScript, nor a `borrow_mut()` of one holding GC references across a call that can allocate on the V8 heap, wrap the resource or create or drop a `jsg::Rc` to it, since tracing skips a mutably borrowed `RefCell`); a method or accessor can take `this: jsg::Rc<Self>` as its first parameter instead of `&self` to get a `jsg::Rc` of its resource (like `JSG_THIS`) to hand to a callback; don't share the resource's state with a callback through an `std::rc::Rc` (see `jsg/README.md` §Reaching the resource from a callback); an eligible `#[jsg_method]` also gets a V8 Fast API callback (types in `jsg/fast_api.rs`) and must not call into V8; a `#[jsg_constructor]` returns `Self` or `Result<Self, E>` (an `Err` is thrown) and throws `TypeError` when called without `new`; `#[jsg_static_constant]` on `const` items exposes read-only numeric constants on both constructor and prototype (name kept as-is, no camelCase); `#[jsg_resource(nested_types(Foo, Name = Bar))]` on the impl block puts other resources' constructors on the prototype (`JSG_NESTED_TYPE`); resources integrate with GC via `Traced` + `GarbageCollected`: every named field is traced via `Traced::trace(&self.field, visitor)` and all non-traceable types use no-op `Traced` impls
- **Resources created by C++**: C++ creates a Rust resource through an `extern "Rust"` function of the crate that defines it, which returns the wrapper through `jsg::v8::run_ffi_callback`; C++ holds the wrapper (a `jsg::JsRef`), since it has no typed reference to a Rust resource, and checks a value's type through one that returns `jsg::resource::ffi_is::<R>`, a `kj::Maybe<bool>` that is none when an exception is pending. See `jsg/README.md` §Creating resources from C++.
- **Custom JSG registration**: `#[jsg_resource(custom_members)]` on an impl generates member factories but leaves `impl Resource` to the author. `Resource::members(flags)` uses ordinary Rust to select members, order, and property placement. Factories are `<rust_name>_member()` (constant names lowercased); ordinary property factories are `<getter_name>_member(PropertyPlacement)` (`Prototype`, `Instance` or `LazyInstance`) and include the paired setter. See `jsg-macros/README.md` §Custom member registration.
- **Custom JSG registration**: `#[jsg_resource(custom_members)]` on an impl generates member factories but leaves `impl Resource` to the author. `Resource::members(registry, flags)` uses ordinary Rust to select members, order, and property placement, and also drives RTTI under the export's flags. Factories are `<rust_name>_member()` (constant names lowercased); ordinary property factories are `<getter_name>_member(PropertyPlacement)` (`Prototype`, `Instance` or `LazyInstance`) and include the paired setter. See `jsg-macros/README.md` §Custom member registration.
- **JSG properties**: three property macros on `#[jsg_resource]` impl blocks — `#[jsg_property(prototype|instance|lazy_instance [, name = "..."] [, readonly])]` (`prototype` registers an accessor on the prototype, equivalent to `JSG_PROTOTYPE_PROPERTY`; `instance` registers an own data property via V8 `SetNativeDataProperty`, equivalent to `JSG_INSTANCE_PROPERTY`; `lazy_instance` registers an own data property via V8 `SetLazyDataProperty` whose getter runs once, on first access, equivalent to `JSG_LAZY_INSTANCE_PROPERTY`: it has no setter, is writable unless `readonly` (`JSG_LAZY_READONLY_INSTANCE_PROPERTY`); declaration order sets `Object.keys()` order for automatic registration; `readonly` is a compile-time check preventing a paired setter; `name = "..."` overrides the JS name; prefer `prototype` in almost all cases), `#[jsg_inspect_property]` (registered under a unique symbol, invisible to normal enumeration and string-key lookup, surfaced by `node:util` `inspect()`, equivalent to `JSG_INSPECT_PROPERTY`), and `#[jsg_wildcard_property]` (answers string-named properties the object and its prototypes lack, at most one per type, equivalent to `JSG_WILDCARD_PROPERTY`; see `jsg/README.md`); setter auto-detected from `set_` prefix; read-only when no setter present; getter/setter `.length` and `.name` are set correctly when `spec_compliant_property_attributes` compat flag is enabled
- **`jsg::v8::Global` / `TracedGlobal`**: persistent handles. A `Global` is for handles Rust owns outside resources: GC never visits it, so it is always strong, holds its handle inline (no box), has no `Traced` impl (it cannot be a resource field), and is `Send` and `Sync`. A `TracedGlobal` is for resource fields, which GC traces to collect cycles; it is `Send` (not `Sync`). Either may be dropped on any thread. Under the isolate lock its strong handle is reset at once; without the lock it is queued and reset at the next `IsolateBase::Lock`, through `IsolateBase::applyDeferredActions()`; once the `Realm` is dropped (under the lock, before `Dispose`) the handles are left to `Dispose`. See `jsg/isolate_state.rs`. Hand-written FFI that resets a V8 handle directly must hold the lock. Rust never resets a `TracedGlobal`'s traced reference: every GC visit lets go of it and makes the strong handle strong again (`ClearWeak`) before C++ installs a new one if needed, and a drop just lets go of it; V8 frees untraced nodes at the next full GC. See `GcVisitor::visit_traced_global`. A `TracedGlobal` keeps its handles in a `Box<GlobalSlot>`, since V8 holds the address of a weak handle's slot; the slot of a `TracedGlobal` dropped after the `Realm` is kept until the isolate is disposed
- **JSG parameters**: function `.length` is the Web IDL required-argument count under `spec_compliant_property_attributes` (`this: jsg::Rc<Self>` and a leading `&mut Lock` are not counted; counting stops at the first type whose `FromJS::is_optional()` returns true), so put optional parameters last
Expand Down Expand Up @@ -70,6 +70,7 @@ _Snapshot — the set drifts as crates come and go; `bazel query //src/rust/...`
- **FFI naming**: instance methods on an existing handle use a `local_<type>_<method>` prefix (e.g. `local_array_buffer_byte_length`). Static constructors that create a new value do **not** use the `local_` prefix — name them `<type>_<method>` (e.g. `array_buffer_new_with_mode`, `array_buffer_maybe_new`, `backing_store_new_resizable`).
- **FFI groups**: `v8.rs` `mod ffi`, `ffi.h`, and `ffi.c++` all use matching comment groups (e.g. `// Local<T>`, `// Local<Array>`, `// Local<TypedArray>`, `// Local<ArrayBuffer>`, `// Local<ArrayBufferView>`, `// Local<SharedArrayBuffer>`, `// BackingStore`, `// Unwrappers`, `// TracedGlobal<T>`, `// FunctionCallbackInfo`). When adding new FFI functions, place them in the correct group in **all three files**. Do not scatter related functions across groups.
- **Feature flags**: `Lock::feature_flags()` returns a capnp `compatibility_flags::Reader` for the current worker. Use `lock.feature_flags().get_node_js_compat()`. Flags are parsed once and stored in the `Realm` at construction; C++ passes canonical capnp bytes to `realm_create()`. Schema: `src/workerd/io/compatibility-date.capnp`, generated Rust bindings: `compatibility_date_capnp` crate.
- **RTTI**: JSG macros generate `TypeInfo` descriptions in `jsg/rtti.rs`, serialized using the shared `jsg/rtti.capnp` schema. `modules::add_builtin()` takes a capture-free `for<'s> Fn(&mut Lock<'s>) -> jsg::Result<Rc<Exports>>` callback; its inferred return type supplies both runtime wrapping and RTTI. RTTI export does not invoke callbacks. `ts_root` on a resource/struct marks an ambient TypeScript root; internal module exports are not roots by default. Custom conversion types implement `TypeInfo`.

## UNIT TEST FILES

Expand Down
1 change: 1 addition & 0 deletions src/rust/api/lib-test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the Apache 2.0 license found in the LICENSE file or at:
// https://opensource.org/licenses/Apache-2.0

use jsg::ToJS;
use jsg_test::Harness;

use super::*;
Expand Down
6 changes: 2 additions & 4 deletions src/rust/api/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@

use std::pin::Pin;

use jsg::ToJS;

use crate::dns::DnsUtil;
use crate::url::UrlUtil;

Expand All @@ -30,13 +28,13 @@ pub fn register_nodejs_modules(mut registry: Pin<&mut ffi::ModuleRegistry>) {
jsg::modules::add_builtin(
registry.as_mut(),
"node-internal:dns",
|lock| DnsUtil::new().to_js(lock),
|_lock| Ok(DnsUtil::new()),
jsg::modules::ModuleType::Internal,
);
jsg::modules::add_builtin(
registry,
"node-internal:url",
|lock| UrlUtil::new().to_js(lock),
|_lock| Ok(UrlUtil::new()),
jsg::modules::ModuleType::Internal,
);
}
Expand Down
17 changes: 17 additions & 0 deletions src/rust/jsg-compile-tests/duplicate-parameter-names.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Copyright (c) 2026 Cloudflare, Inc.
// Licensed under the Apache 2.0 license found in the LICENSE file or at:
// https://opensource.org/licenses/Apache-2.0

use jsg_macros::jsg_method;
use jsg_macros::jsg_resource;

#[jsg_resource]
struct Example;

#[jsg_resource]
impl Example {
#[jsg_method]
fn method(&self, _a: String, a: String) {
//~^ ERROR duplicate TypeScript parameter name `a`
}
}
19 changes: 16 additions & 3 deletions src/rust/jsg-compile-tests/ok-builtin-module.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,28 @@
// Licensed under the Apache 2.0 license found in the LICENSE file or at:
// https://opensource.org/licenses/Apache-2.0

// A builtin module's callback returns a handle of the scope it is given.
use jsg::ToJS;
// A builtin module's callback returns its exports resource, which registration wraps in the
// registry's scope.
use jsg::v8;
use jsg_macros::jsg_method;
use jsg_macros::jsg_resource;

#[jsg_resource]
pub struct Exports;

#[jsg_resource]
impl Exports {
#[jsg_method]
pub fn answer(&self) -> u32 {
42
}
}

pub fn register(registry: std::pin::Pin<&mut v8::ffi::ModuleRegistry>) {
jsg::modules::add_builtin(
registry,
"node-internal:example",
|lock| vec![1.0_f64].to_js(lock),
|_lock| Ok(jsg::Rc::new(Exports)),
jsg::modules::ModuleType::Internal,
);
}
50 changes: 32 additions & 18 deletions src/rust/jsg-macros/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ boilerplate when implementing the JSG type system for Rust-backed JavaScript API

## `#[jsg_struct]`

Generates `jsg::Struct`, `jsg::Type`, `jsg::ToJS`, and `jsg::FromJS` for a plain data
Generates `jsg::Struct`, `jsg::Type`, `jsg::TypeInfo`, `jsg::ToJS`, and `jsg::FromJS` for a plain data
struct. Only `pub` fields are projected into the JavaScript object. Use
`#[jsg_struct(name = "MyName")]` to override the JavaScript class name.

Expand All @@ -32,6 +32,9 @@ produces a property whose value is `null`. An absent input property becomes `Non
or `Undefined`, respectively. If all public fields are optional, a `null` or
`undefined` input is accepted as an empty dictionary.

`ts_root` marks a TypeScript generation root; it can be combined with `name`.
RTTI describes the public fields using their types' `TypeInfo` implementations.

```rust
#[jsg_struct]
pub struct CaaRecord {
Expand Down Expand Up @@ -120,18 +123,24 @@ Eligible methods also get a V8 Fast API callback; see "Fast API" in `src/rust/js

Generates JSG boilerplate for a resource type and its impl block.

**On a struct** — emits `jsg::Type`, `jsg::ToJS`, `jsg::FromJS`, and
**On a struct** — emits `jsg::Type`, `jsg::TypeInfo`, `jsg::ToJS`, `jsg::FromJS`, and
`jsg::GarbageCollected`. The `trace()` body is synthesised automatically for every
field whose type is or contains a traceable JSG handle (see [Garbage Collection](#garbage-collection) below).
Use `#[jsg_resource(name = "JSName")]` to override the JavaScript class name.
The optional `ts_root` argument marks a TypeScript generation root.

**On an impl block** — emits `jsg::Resource::members(flags)`, registering every
**On an impl block** — emits `jsg::Resource::members(registry, flags)`, registering every
`#[jsg_method]`, `#[jsg_constructor]`, `#[jsg_property]`, `#[jsg_inspect_property]`,
`#[jsg_wildcard_property]`, and `#[jsg_static_constant]` item. It also generates member factories used by both automatic
and custom registration.
Use `#[jsg_resource(nested_types(Foo, Name = Bar))]` to expose other resource types'
constructors on the prototype (equivalent to `JSG_NESTED_TYPE(Foo)` and
`JSG_NESTED_TYPE_NAMED(Bar, Name)` in C++ JSG).
Each factory also describes its member for RTTI, which describes a resource type with the
members its `Resource::members` registers, in registration order. Methods, properties,
constructors, and constants are described; inspect and wildcard properties are not. Parameter
names are retained, injected `this` and lock parameters are excluded, and fallible return types
describe their success value. See [RTTI](../jsg/README.md#rtti-and-typescript-declarations).

```rust
#[jsg_resource]
Expand All @@ -153,14 +162,22 @@ impl DnsUtil {
### Custom member registration

Use `#[jsg_resource(custom_members)]` on the impl block to generate the bindings and member
factories without an automatic `Resource` implementation. Implement `Resource::members(flags)`
to choose which members to expose, their order, and property placement using ordinary Rust.
The flags belong to the realm constructing the template, as in C++ `JSG_RESOURCE_TYPE(T, flags)`.
Templates are cached per resource type within that realm; registration runs when a template is
created, not on each property access.
factories without an automatic `Resource` implementation. Implement
`Resource::members(registry, flags)` to choose which members to expose, their order, and property
placement using ordinary Rust, adding each to `registry`. The flags belong to the realm
constructing the template, as in C++ `JSG_RESOURCE_TYPE(T, flags)`. Templates are cached per
resource type within that realm; registration runs when a template is created, not on each
property access.

RTTI export runs the same function with its own registry and the flags of the export, so the
TypeScript declarations follow the same choices, like the `Registry` parameter of C++
`registerMembers`. Template creation never runs the type descriptions, and RTTI export never
builds callbacks. A hand-built `jsg::Member` can also be added. RTTI describes a hand-built
`Member::NestedType` through its nested type, and omits other hand-built members, which carry no
type description.

```rust
use jsg::{Member, PropertyPlacement, Resource};
use jsg::{MemberRegistry, PropertyPlacement, Resource};
use jsg::feature_flags::compatibility_flags;
use jsg_macros::{jsg_constructor, jsg_property, jsg_resource};

Expand All @@ -180,25 +197,22 @@ impl Example {
}

impl Resource for Example {
fn members(flags: compatibility_flags::Reader<'_>) -> Vec<Member> {
fn members(registry: &mut impl MemberRegistry, flags: compatibility_flags::Reader<'_>) {
let placement = if flags.get_jsg_property_on_prototype_template() {
PropertyPlacement::Prototype
} else {
PropertyPlacement::Instance
};
let mut members = vec![
Self::constructor_member(),
Self::get_label_member(placement),
];
registry.add(Self::constructor_member());
registry.add(Self::get_label_member(placement));
if flags.get_cache_option_enabled() && flags.get_jsg_property_on_prototype_template() {
members.push(Self::get_cache_member(PropertyPlacement::Prototype));
registry.add(Self::get_cache_member(PropertyPlacement::Prototype));
}
members
}
}
```

Factories are private associated functions returning `jsg::Member`. Their names derive from
Factories are private associated functions returning `impl jsg::MemberDefinition`. Their names derive from
the **Rust declaration**, independently of any JavaScript `name = "..."` override:

| Annotated declaration | Factory |
Expand All @@ -217,7 +231,7 @@ handling apply in both modes. Getter and setter annotations must agree on placem
Inspect properties remain symbol-keyed and do not take a placement argument.
`LazyInstance` keeps the getter's `readonly` (see `lazy_instance` below); asking for it for a
property with a setter panics when the template is created.
Property registration follows the returned member list's order. V8 enumerates instance native
Properties are registered in the order they are added to `registry`. V8 enumerates instance native
data properties in reverse registration order, as it does for C++ JSG.
`nested_types(...)` cannot be combined with `custom_members`; a custom `members` lists nested
types itself, as `jsg::Member::NestedType { name, nested_type: jsg::NestedType::of::<Foo>() }`.
Expand Down
Loading
Loading