Skip to content
Draft
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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,12 @@ versioning and publication.
browser's successful result is `undefined`, not a database.
- `WebAPI.Notification.t` no longer contains a static permission field. Read that value through
`WebAPI.Notification.permission`.
- Native browser-instance record construction and record-update copies are being restricted with
private record views. Reads remain direct; writes to protected mutable properties migrate to
typed setters such as `canvas->WebAPI.HTMLCanvasElement.setWidth(640)`. Options, initializers,
callback dictionaries, and plain data records remain constructible. This compile-time restriction
does not prevent a deliberate explicit structural coercion (`:>`) from a recreated public record
shape, unsafe casts, or JavaScript fabrication.

For the intended installation, compiler range, runtime/browser checks, and known limitations,
see the [project status](https://rescript-lang.github.io/experimental-rescript-webapi/project-status/)
Expand Down
40 changes: 40 additions & 0 deletions docs/content/docs/contributing/api-modelling.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,46 @@ Keep null and empty states explicit when they are part of the browser contract.
for nullable results and variants for meaningful empty values; do not replace native results
with invented defaults or unchecked casts.

## Native instance types

Model browser-owned objects as private records at both the backing declaration and every public
record view. Keep direct field reads when useful, but expose typed native setters for writable
properties instead of asking callers to mutate a private field:

```ReScript
let canvas = WebAPI.Document.createElement(document, "canvas")->WebAPI.HTMLCanvasElement.classify
switch canvas {
| Some(canvas) =>
let width = canvas.width
canvas->WebAPI.HTMLCanvasElement.setWidth(width + 1)
| None => ()
}
```

Use names such as `setWidth`, preserve the JavaScript property name and existing field type, and
keep throwing behavior on setters whose native operation can throw. Put shared properties on the
existing `Impl` chain so derived interfaces reuse their base setters. Do not make initializer
dictionaries, option records, callback data, or ordinary result records private merely because they
are exported beside a native interface.

For `CSSStyleDeclaration`, keep direct reads but use its existing `setProperty` operation for
individual declarations, using CSS property names such as `"background-color"` rather than the
camel-cased IDL field name. Both `float` and `cssFloat` address the `"float"` CSS property.
Replacing the complete declaration block uses `CSSStyleDeclaration.setCssText`; do not route that
operation through `setProperty`.

```ReScript
style->WebAPI.CSSStyleDeclaration.setProperty(~property="background-color", ~value="red")
style->WebAPI.CSSStyleDeclaration.setProperty(~property="float", ~value="left")
style->WebAPI.CSSStyleDeclaration.setCssText("background-color: blue")
```

Private records prevent ordinary literals and record-update copies, but ReScript currently permits
a deliberate explicit structural coercion such as `ordinaryRecord :> Native.t` when a caller
recreates the full public shape. This representation is not an unforgeable native brand; do not
describe it as preventing deliberate coercions, unsafe casts, or JavaScript fabrication. A stronger
guarantee requires an opaque representation and corresponding getter APIs.

## Verification

Add compile-only, Node runtime, or browser runtime fixtures according to the API being modeled.
Expand Down
17 changes: 14 additions & 3 deletions docs/content/docs/project-status.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,20 @@ implementation modules and internal type-holder modules are not part of that pub
Bindings are maintained and corrected by hand as well as generated; the generator is not a
reproducible source of truth for every checked-in binding.

Approximately 80 bindings still carry private-record TODOs; a blanket conversion is deferred
because the current compiler reports mutation warning 111. Generator reproducibility is also
follow-up work, not an alpha release gate.
Native-instance privacy is being implemented in the private-record alternative for
[#403](https://github.com/rescript-lang/experimental-rescript-webapi/issues/403). Protected
instances retain direct reads and use typed setters for supported writes; caller-owned option and
initializer records remain constructible. This is not yet complete across the package while the
implementation is under review. The private-record representation also permits a deliberate
explicit structural coercion (`:>`) from a recreated public shape, so it does not provide an
unforgeable native-instance brand. Generator reproducibility remains separate follow-up work.

Some signatures still use `unknown` placeholders instead of a modeled native identity. In
particular, stream reader/writer types, DOM/Canvas rendering-context unions, and the performance
entry-list handle are not protected by this record policy. Input and mixed placeholders such as
stream source/sink/strategy types, request information, IDB keys, media providers, and algorithm
identifiers are also not native-instance guarantees. See the checked native-instance inventory for
the exact current disposition; these placeholders must not be counted as protected records.

CI uses Node 26 for ReScript compilation and Node runtime fixtures. Its Playwright suite runs
compiled Chromium regressions for the confirmed alpha contracts, including typed byte/audio
Expand Down
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,12 @@
"test:browser": "rescript clean && npm run build && playwright test",
"test:negative": "node scripts/check-consumer.mjs --negative-only",
"build": "rescript",
"check:features": "node --test scripts/check-features.test.mjs && node scripts/check-features.mjs",
"check:features": "node --test scripts/check-features.test.mjs && node scripts/check-features.mjs && node scripts/check-native-instance-inventory.mjs",
"check:consumer": "node scripts/check-consumer.mjs",
"check:release": "node --test scripts/validate-release-artifact.test.mjs scripts/check-release.test.mjs && node scripts/check-release.mjs",
"check:docs-links": "node --test scripts/check-doc-links.test.mjs scripts/check-doc-type-links.test.mjs && node scripts/check-doc-links.mjs",
"format": "rescript format && oxfmt ./tests/index.js ./tests/fixtures.json ./tests/browser/*.mjs ./scripts/negative-fixtures.json ./scripts/validate-release-artifact.mjs ./scripts/validate-release-artifact.test.mjs ./playwright.config.mjs ./astro.config.mjs ./scripts/check-consumer.mjs ./scripts/check-release.mjs ./scripts/check-release.test.mjs ./scripts/check-features.mjs ./scripts/check-features.test.mjs ./scripts/check-doc-links.mjs ./scripts/check-doc-links.test.mjs ./scripts/check-doc-type-links.test.mjs ./package.json ./README.md ./CHANGELOG.md ./docs && prettier --write .github/workflows/*.yml ./docs/pages",
"format:check": "rescript format --check && oxfmt ./tests/index.js ./tests/fixtures.json ./tests/browser/*.mjs ./scripts/negative-fixtures.json ./scripts/validate-release-artifact.mjs ./scripts/validate-release-artifact.test.mjs ./playwright.config.mjs ./astro.config.mjs ./scripts/check-consumer.mjs ./scripts/check-release.mjs ./scripts/check-release.test.mjs ./scripts/check-features.mjs ./scripts/check-features.test.mjs ./scripts/check-doc-links.mjs ./scripts/check-doc-links.test.mjs ./scripts/check-doc-type-links.test.mjs ./package.json ./README.md ./CHANGELOG.md ./docs --check && prettier --check .github/workflows/*.yml ./docs/pages",
"format": "rescript format && oxfmt ./tests/index.js ./tests/fixtures.json ./tests/browser/*.mjs ./scripts/negative-fixtures.json ./scripts/native-instance-inventory.json ./scripts/check-native-instance-inventory.mjs ./scripts/validate-release-artifact.mjs ./scripts/validate-release-artifact.test.mjs ./playwright.config.mjs ./astro.config.mjs ./scripts/check-consumer.mjs ./scripts/check-release.mjs ./scripts/check-release.test.mjs ./scripts/check-features.mjs ./scripts/check-features.test.mjs ./scripts/check-doc-links.mjs ./scripts/check-doc-links.test.mjs ./scripts/check-doc-type-links.test.mjs ./package.json ./README.md ./CHANGELOG.md ./docs && prettier --write .github/workflows/*.yml ./docs/pages",
"format:check": "rescript format --check && oxfmt ./tests/index.js ./tests/fixtures.json ./tests/browser/*.mjs ./scripts/negative-fixtures.json ./scripts/native-instance-inventory.json ./scripts/check-native-instance-inventory.mjs ./scripts/validate-release-artifact.mjs ./scripts/validate-release-artifact.test.mjs ./playwright.config.mjs ./astro.config.mjs ./scripts/check-consumer.mjs ./scripts/check-release.mjs ./scripts/check-release.test.mjs ./scripts/check-features.mjs ./scripts/check-features.test.mjs ./scripts/check-doc-links.mjs ./scripts/check-doc-links.test.mjs ./scripts/check-doc-type-links.test.mjs ./package.json ./README.md ./CHANGELOG.md ./docs --check && prettier --check .github/workflows/*.yml ./docs/pages",
"docs": "astro dev",
"prebuild:docs": "node docs/llm.js",
"build:docs": "astro build"
Expand Down
3 changes: 3 additions & 0 deletions rescript.json
Original file line number Diff line number Diff line change
Expand Up @@ -192,8 +192,11 @@
"ImageBitmapRenderingContext",
"ImageData",
"OffscreenCanvas",
"OffscreenCanvasRenderingContext2D",
"Path2D",
"VideoColorSpace",
"WebGLRenderingContext",
"WebGL2RenderingContext",
"VideoFrame"
]
},
Expand Down
36 changes: 35 additions & 1 deletion scripts/check-consumer.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ const writeProject = (directory, sourceFile, features) => {
dependencies:
features === null ? ["@rescript/webapi"] : [{ name: "@rescript/webapi", features }],
"experimental-features": { LetUnwrap: true },
warnings: { error: "+8" },
warnings: { error: "+8+111" },
"package-specs": { module: "esmodule", "in-source": true },
},
null,
Expand All @@ -80,6 +80,29 @@ const writeProject = (directory, sourceFile, features) => {
const compilerCli = (directory) =>
path.join(directory, "node_modules", "rescript", "cli", "rescript.js");

const verifyPrivateRecordRelaxation = (directory) => {
const backingFile = path.join(
directory,
"node_modules",
"@rescript",
"webapi",
"src/core/DOM.res",
);
const source = readFileSync(backingFile, "utf8");
const privateDeclaration = "type domTokenList = private {";
if (source.split(privateDeclaration).length !== 2) {
throw new Error("Expected exactly one private DOM.domTokenList backing declaration.");
}
writeFileSync(backingFile, source.replace(privateDeclaration, "type domTokenList = {"));
writeProject(directory, "scripts/fixtures/negative/DOMTokenListBackingRouteIsPrivate.res", [
"WebAPI.DOM",
]);
compile(directory);
console.log(
"[ok] DOM backing-route fixture compiles when only its privacy restriction is relaxed",
);
};

const compile = (directory, expectedFailure = null) => {
const cli = compilerCli(directory);
const clean = spawnSync(process.execPath, [cli, "clean"], { cwd: directory });
Expand Down Expand Up @@ -193,6 +216,16 @@ const checkConsumers = async () => {
fixture: "scripts/consumer-concrete-types.res",
features: null,
},
{
name: "native setters and constructible request options",
fixture: "scripts/consumer-native-setters.res",
features: null,
},
{
name: "documented private-record coercion limitation",
fixture: "scripts/consumer-private-coercion-caveat.res",
features: null,
},
{
name: "Fetch-only",
fixture: "scripts/consumer-fetch.res",
Expand Down Expand Up @@ -247,6 +280,7 @@ const checkConsumers = async () => {
compile(projectDirectory, negativeCase.expectedDiagnostic);
console.log(`[ok] expected rejection: ${negativeCase.name}`);
}
verifyPrivateRecordRelaxation(projectDirectory);
console.log(`[ok] tarball contents (${packagedFiles.length} files)`);
} finally {
rmSync(tempDirectory, { recursive: true, force: true });
Expand Down
Loading
Loading