Skip to content
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
20 changes: 20 additions & 0 deletions README.ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,15 @@ pub fn themed_component(
}
```

`glendix/js/object` は意図的にデータ構築だけを担当します。任意のプロパティの
読み取り・書き込み・削除、メソッド呼び出し、コンストラクター実行などの動的な
interop は、別の `glendix/js/reflect` モジュールにあります。Reflection は、
プロパティやメソッドの存在を型システムではなく呼び出し側が保証する
unsafe/dynamic な境界です。`__proto__` を通常のデータとして保持する保証は
`object.from_entries` にだけ適用されます。Reflection による代入は通常の
JavaScript setter の意味を維持するため、信頼できないプロパティ名を
`reflect.set` に渡してはいけません。

## WebAssembly 依存関係

Glendix は、ブラウザ toolchain が使用する次の標準的な静的 URL 形式の
Expand Down Expand Up @@ -268,6 +277,17 @@ JavaScript 設定、最終 MPK ビルドを担当します。
opaque 型 `glendix/js/array.JsArray(element)` は削除されたので、値の型は
`gleam/javascript/array.Array(element)` で注釈してください。

`glendix/js/object` はデータ構築専用になりました。従来の reflection 操作である
`get`、`set`、`delete`、`has`、`call_method`、
`call_method_without_arguments`、`new_instance` と `JsConstructor` 型は、新しい
`glendix/js/reflect` モジュールへ移動しました。関数名・ラベル・挙動は維持されて
います。従来の `object.get(from: handle, key: "x")` は
`import glendix/js/reflect` を追加し、
`reflect.get(from: handle, key: "x")` へ変更してください。データ構築関数
(`from_entries`、`empty`、`string`、`int`、`float`、`bool`、`from_object`) は
`glendix/js/object` に残り、`from_entries` は `__proto__` のようなキーでも
prototype pollution を起こさない `Object.fromEntries` の挙動を維持します。

## 開発

```sh
Expand Down
17 changes: 17 additions & 0 deletions README.ko.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,13 @@ pub fn themed_component(
}
```

`glendix/js/object`는 의도적으로 데이터 생성만 담당한다. 임의 속성의 읽기·쓰기·삭제,
메서드 호출, 생성자 실행 같은 동적 interop은 별도의 `glendix/js/reflect` 모듈에
있다. Reflection은 속성이나 메서드가 실제로 존재함을 타입 시스템이 아니라 호출자가
보장하는 unsafe/dynamic 경계다. `__proto__`를 일반 데이터로 보존하는 보장은
`object.from_entries`에만 적용된다. Reflection 대입은 일반 JavaScript setter 의미를
유지하므로 신뢰할 수 없는 속성 이름을 `reflect.set`에 전달하면 안 된다.

## WebAssembly 의존성

Glendix는 브라우저 도구가 사용하는 다음 표준 정적 URL 형식의 WebAssembly
Expand Down Expand Up @@ -262,6 +269,16 @@ JavaScript 어댑터를 더 이상 포함하지 않는다. `from_list`와 `to_li
타입 `glendix/js/array.JsArray(element)`는 제거되었으므로 값의 타입은
`gleam/javascript/array.Array(element)`로 표기한다.

`glendix/js/object`는 이제 데이터 생성만 담당한다. 기존 reflection 연산인 `get`,
`set`, `delete`, `has`, `call_method`, `call_method_without_arguments`,
`new_instance`와 `JsConstructor` 타입은 새 `glendix/js/reflect` 모듈로 이동했다.
함수 이름·label·동작은 유지된다. 기존
`object.get(from: handle, key: "x")` 호출은 `import glendix/js/reflect`를 추가하고
`reflect.get(from: handle, key: "x")`로 변경한다. 데이터 생성 함수
(`from_entries`, `empty`, `string`, `int`, `float`, `bool`, `from_object`)는
`glendix/js/object`에 남아 있으며, `from_entries`는 `__proto__` 같은 키에도
prototype pollution을 일으키지 않는 `Object.fromEntries` 동작을 유지한다.

## 개발

```sh
Expand Down
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,14 @@ pub fn themed_component(
}
```

`glendix/js/object` is deliberately data-only. Dynamic interop — reading,
writing, or deleting arbitrary properties, calling methods, and invoking
constructors — lives in the separate `glendix/js/reflect` module. Reflection is
the unsafe/dynamic boundary where the caller, not the type system, guarantees a
property or method exists. The `__proto__`-as-data guarantee applies to
`object.from_entries`; reflective assignment retains ordinary JavaScript setter
semantics, so never pass untrusted property names to `reflect.set`.

## WebAssembly dependencies

Glendix automatically packages browser WebAssembly modules referenced with the
Expand Down Expand Up @@ -332,6 +340,17 @@ common `list |> array.from_list |> array.to_list` usage is unchanged. The former
opaque `glendix/js/array.JsArray(element)` type is removed; annotate values with
`gleam/javascript/array.Array(element)` instead.

`glendix/js/object` is now data-only. Its reflection operations moved unchanged
to the new `glendix/js/reflect` module: `get`, `set`, `delete`, `has`,
`call_method`, `call_method_without_arguments`, and `new_instance`, together
with the `JsConstructor` type. Their names, labels, and behavior are preserved,
so migrate a pre-split call such as `object.get(from: handle, key: "x")` to
`reflect.get(from: handle, key: "x")` (add `import glendix/js/reflect`). Data
construction (`from_entries`, `empty`, `string`, `int`, `float`, `bool`,
`from_object`) stays in `glendix/js/object`, and `from_entries` keeps its
prototype-pollution-safe `Object.fromEntries` behavior for keys such as
`__proto__`.

## Development

```sh
Expand Down
100 changes: 16 additions & 84 deletions src/glendix/js/object.gleam
Original file line number Diff line number Diff line change
@@ -1,4 +1,19 @@
//// Creates and manipulates typed JavaScript object handles.
//// Builds plain, prototype-safe JavaScript data objects from typed entries.
////
//// This module owns *data-only* object construction. It converts typed Gleam
//// scalars into opaque JavaScript values and assembles them into plain objects
//// whose keys are always ordinary own data properties. Dynamic interop such as
//// property reads and writes, method calls, and constructor invocation lives in
//// the separate `glendix/js/reflect` module, so this data boundary never
//// depends on arbitrary reflection.
////
//// Construction keeps a small bespoke FFI (`Object.fromEntries`) on purpose: no
//// ecosystem package builds a live, prototype-pollution-safe plain object.
//// The public `gleam/javascript` API only covers arrays, promises, and symbols,
//// while Plinth has no general plain-object builder. A `gleam/json` round-trip
//// would be indirect and lossy for live handles. The retained FFI guarantees
//// that even a `__proto__` entry is stored as ordinary own data instead of
//// invoking the legacy prototype setter.
////

/// Represents a JavaScript value whose runtime shape is intentionally opaque.
Expand All @@ -7,9 +22,6 @@ pub type JsValue
/// Represents a JavaScript object handle.
pub type JsObject

/// Represents a JavaScript constructor handle.
pub type JsConstructor

/// Represents a JavaScript boolean value.
pub type JsBoolean {
/// JavaScript `true`.
Expand Down Expand Up @@ -61,55 +73,6 @@ pub fn empty() -> JsObject {
empty_object_raw()
}

/// Reads an object property.
pub fn get(from object: JsObject, key key: String) -> JsValue {
get_property_raw(object, key)
}

/// Mutates an object property and returns the same object handle.
pub fn set(
on object: JsObject,
key key: String,
to value: JsValue,
) -> JsObject {
set_property_raw(object, key, value)
}

/// Deletes an object property and returns the same object handle.
pub fn delete(from object: JsObject, key key: String) -> JsObject {
delete_property_raw(object, key)
}

/// Reports whether an object has the given property.
pub fn has(in object: JsObject, key key: String) -> Bool {
has_property_raw(object, key)
}

/// Calls an object method with a list of arguments.
pub fn call_method(
on object: JsObject,
named method: String,
with arguments: List(JsValue),
) -> JsValue {
call_method_raw(object, method, arguments)
}

/// Calls an object method without arguments.
pub fn call_method_without_arguments(
on object: JsObject,
named method: String,
) -> JsValue {
call_method_without_arguments_raw(object, method)
}

/// Creates an object with JavaScript's `new` operator.
pub fn new_instance(
using constructor: JsConstructor,
with arguments: List(JsValue),
) -> JsObject {
new_instance_raw(constructor, arguments)
}

// -- FFI --
@external(javascript, "./object_ffi.mjs", "identity")
fn string_raw(value: String) -> JsValue
Expand All @@ -131,34 +94,3 @@ fn create_object_raw(entries: List(#(String, JsValue))) -> JsObject

@external(javascript, "./object_ffi.mjs", "empty_object")
fn empty_object_raw() -> JsObject

@external(javascript, "./object_ffi.mjs", "get_property")
fn get_property_raw(object: JsObject, key: String) -> JsValue

@external(javascript, "./object_ffi.mjs", "set_property")
fn set_property_raw(object: JsObject, key: String, value: JsValue) -> JsObject

@external(javascript, "./object_ffi.mjs", "delete_property")
fn delete_property_raw(object: JsObject, key: String) -> JsObject

@external(javascript, "./object_ffi.mjs", "has_property")
fn has_property_raw(object: JsObject, key: String) -> Bool

@external(javascript, "./object_ffi.mjs", "call_method")
fn call_method_raw(
object: JsObject,
method: String,
arguments: List(JsValue),
) -> JsValue

@external(javascript, "./object_ffi.mjs", "call_method_0")
fn call_method_without_arguments_raw(
object: JsObject,
method: String,
) -> JsValue

@external(javascript, "./object_ffi.mjs", "new_instance")
fn new_instance_raw(
constructor: JsConstructor,
arguments: List(JsValue),
) -> JsObject
23 changes: 0 additions & 23 deletions src/glendix/js/object_ffi.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,29 +7,6 @@ export function create_object(entries) {
export function empty_object() {
return {};
}
export function get_property(obj, key) {
return obj[key];
}
export function set_property(obj, key, value) {
obj[key] = value;
return obj;
}
export function delete_property(obj, key) {
delete obj[key];
return obj;
}
export function has_property(obj, key) {
return key in obj;
}
export function call_method(obj, method, args) {
return obj[method](...args.toArray());
}
export function call_method_0(obj, method) {
return obj[method]();
}
export function new_instance(constructor, args) {
return new constructor(...args.toArray());
}
export function identity(value) {
return value;
}
110 changes: 110 additions & 0 deletions src/glendix/js/reflect.gleam
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
//// Performs dynamic JavaScript reflection against object handles.
////
//// These operations are the interop boundary: they read and write arbitrary
//// properties, invoke methods, and call constructors by name at runtime. They
//// are inherently dynamic and unsafe in the sense that the caller, not the
//// type system, guarantees a property exists, a method is callable, or a value
//// is a constructor. Keep this surface minimal and prefer the data-only
//// `glendix/js/object` module whenever a plain data object is enough.
////
//// The `__proto__`-as-data guarantee belongs specifically to
//// `object.from_entries`. `set` deliberately preserves ordinary JavaScript
//// assignment semantics, so a key such as `__proto__` can invoke an inherited
//// setter. Do not pass untrusted property names to reflection operations.
////
//// Object handles and values flow through `glendix/js/object`, so both modules
//// share one typed representation of JavaScript objects and values.
////

import glendix/js/object

/// Represents a JavaScript constructor handle.
pub type JsConstructor

/// Reads an object property.
pub fn get(from handle: object.JsObject, key key: String) -> object.JsValue {
get_property_raw(handle, key)
}

/// Mutates an object property and returns the same object handle.
pub fn set(
on handle: object.JsObject,
key key: String,
to value: object.JsValue,
) -> object.JsObject {
set_property_raw(handle, key, value)
}

/// Deletes an object property and returns the same object handle.
pub fn delete(
from handle: object.JsObject,
key key: String,
) -> object.JsObject {
delete_property_raw(handle, key)
}

/// Reports whether an object or its prototype chain has the given property.
pub fn has(in handle: object.JsObject, key key: String) -> Bool {
has_property_raw(handle, key)
}

/// Calls an object method with a list of arguments.
pub fn call_method(
on handle: object.JsObject,
named method: String,
with arguments: List(object.JsValue),
) -> object.JsValue {
call_method_raw(handle, method, arguments)
}

/// Calls an object method without arguments.
pub fn call_method_without_arguments(
on handle: object.JsObject,
named method: String,
) -> object.JsValue {
call_method_without_arguments_raw(handle, method)
}

/// Creates an object with JavaScript's `new` operator.
pub fn new_instance(
using constructor: JsConstructor,
with arguments: List(object.JsValue),
) -> object.JsObject {
new_instance_raw(constructor, arguments)
}

// -- FFI --
@external(javascript, "./reflect_ffi.mjs", "get_property")
fn get_property_raw(handle: object.JsObject, key: String) -> object.JsValue

@external(javascript, "./reflect_ffi.mjs", "set_property")
fn set_property_raw(
handle: object.JsObject,
key: String,
value: object.JsValue,
) -> object.JsObject

@external(javascript, "./reflect_ffi.mjs", "delete_property")
fn delete_property_raw(handle: object.JsObject, key: String) -> object.JsObject

@external(javascript, "./reflect_ffi.mjs", "has_property")
fn has_property_raw(handle: object.JsObject, key: String) -> Bool

@external(javascript, "./reflect_ffi.mjs", "call_method")
fn call_method_raw(
handle: object.JsObject,
method: String,
arguments: List(object.JsValue),
) -> object.JsValue

@external(javascript, "./reflect_ffi.mjs", "call_method_0")
fn call_method_without_arguments_raw(
handle: object.JsObject,
method: String,
) -> object.JsValue

@external(javascript, "./reflect_ffi.mjs", "new_instance")
fn new_instance_raw(
constructor: JsConstructor,
arguments: List(object.JsValue),
) -> object.JsObject
23 changes: 23 additions & 0 deletions src/glendix/js/reflect_ffi.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
export function get_property(object, key) {
return object[key];
}
export function set_property(object, key, value) {
object[key] = value;
return object;
}
export function delete_property(object, key) {
delete object[key];
return object;
}
export function has_property(object, key) {
return key in object;
}
export function call_method(object, method, args) {
return object[method](...args.toArray());
}
export function call_method_0(object, method) {
return object[method]();
}
export function new_instance(constructor, args) {
return new constructor(...args.toArray());
}
Loading
Loading