From c570806e20857d90cb44831f6c17da5781bf1587 Mon Sep 17 00:00:00 2001 From: Josh Vlk Date: Sun, 8 Nov 2026 18:01:59 -0500 Subject: [PATCH 1/2] feat: add opaque native instance pilots --- CHANGELOG.md | 6 + docs/content/docs/api-surface.mdx | 39 +++--- .../docs/contributing/api-modelling.mdx | 20 +++ docs/content/docs/philosophy.mdx | 13 +- docs/content/docs/project-status.mdx | 15 +- package.json | 6 +- scripts/check-consumer.mjs | 7 +- scripts/check-native-inventory.mjs | 45 ++++++ scripts/consumer-concrete-types.res | 4 +- scripts/consumer-crypto.res | 2 +- .../negative/GeneratePairRejectsAlgorithm.res | 2 +- .../GenerateSymmetricRejectsAlgorithm.res | 2 +- .../negative/JwkIsNotBinaryKeyFormat.res | 4 +- .../negative/RawIsNotJwkKeyFormat.res | 4 +- .../ResponseInferredReturnHasNoFields.res | 2 + .../ResponseLiteralCannotConstruct.res | 12 ++ .../negative/ResponseRecordCannotCoerce.res | 26 ++++ .../negative/ResponseTypeCannotBeSpread.res | 1 + scripts/native-instance-inventory.json | 121 ++++++++++++++++ scripts/negative-fixtures.json | 24 ++++ src/core/DOM.res | 35 +---- src/crypto/Crypto.res | 5 + src/crypto/SubtleCrypto.res | 17 +++ src/crypto/WebCryptoTypes.res | 32 +---- src/dom-platform/DOMException.res | 12 ++ src/dom-platform/DOMStringList.res | 7 + src/dom-platform/DOMTokenList.res | 37 +++-- src/fetch/FetchTypes.res | 40 +----- src/fetch/Response.res | 39 +++++- src/safe-fetch/SafeFetch.res | 6 +- src/url/URL.res | 130 +++++++++++++++++- src/url/UrlTypes.res | 51 +------ tests/FetchAPI/SafeFetch__test.res | 4 +- tests/browser/BindingContracts__test.res | 2 +- 34 files changed, 569 insertions(+), 203 deletions(-) create mode 100644 scripts/check-native-inventory.mjs create mode 100644 scripts/fixtures/negative/ResponseInferredReturnHasNoFields.res create mode 100644 scripts/fixtures/negative/ResponseLiteralCannotConstruct.res create mode 100644 scripts/fixtures/negative/ResponseRecordCannotCoerce.res create mode 100644 scripts/fixtures/negative/ResponseTypeCannotBeSpread.res create mode 100644 scripts/native-instance-inventory.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 0835093c..34f233e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,12 @@ versioning and publication. ### Migration notes +- This draft makes `Response`, `URL`, `Crypto`, `CryptoKey`, `DOMException`, `DOMStringList`, and + `DOMTokenList` opaque native identities. Direct field access is replaced by typed module + accessors: for example, use `Response.ok(response)`, `URL.getHref(url)`, + `URL.setHref(url, href)`, `Crypto.subtle(crypto)`, and `SubtleCrypto.cryptoKeyType(key)`. + Initializers and option dictionaries remain constructible records. The package-wide migration is + incomplete; unconverted native instance types still expose the existing API. - WebSocket sends and worker `Request` fetches now preserve their native receiver (`this`) rather than calling receiver-bound methods unbound. - `WebAPI.Request.bytes`, `WebAPI.Response.bytes`, `WebAPI.Blob.bytes`, diff --git a/docs/content/docs/api-surface.mdx b/docs/content/docs/api-surface.mdx index a43cccb1..1a65fda5 100644 --- a/docs/content/docs/api-surface.mdx +++ b/docs/content/docs/api-surface.mdx @@ -114,12 +114,12 @@ when you already have a `WebAPI.Request.t`. ```ReScript let response = await WebAPI.Fetch.fetch("https://example.com/api/users") -if response.ok { +if WebAPI.Response.ok(response) { let body = await response->WebAPI.Response.json Console.log(body) } else { Console.log("Request failed") - Console.log(response.status) + Console.log(WebAPI.Response.status(response)) } ``` @@ -146,11 +146,11 @@ let text = await response->WebAPI.Response.text ### Result-based workflows -`WebAPI.Fetch` and `WebAPI.Response` are the raw browser bindings and remain unchanged. Use the -separate, opt-in `WebAPI.SafeFetch` module when request, HTTP status, and body-reading failures should -be handled as `result` values. The separate feature is an intentional breaking change for consumers -that restrict enabled features: add `WebAPI.SafeFetch` to use these helpers. Existing raw Fetch -clients are unaffected. +`WebAPI.Fetch` and `WebAPI.Response` are the raw browser bindings. `Response.t` is an opaque native +type, so read its properties through typed `WebAPI.Response` accessors instead of record fields. +Use the separate, opt-in `WebAPI.SafeFetch` module when request, HTTP status, and body-reading +failures should be handled as `result` values. The separate feature is an intentional breaking change +for consumers that restrict enabled features: add `WebAPI.SafeFetch` to use these helpers. | Stage | Result | Preserved context | | ----------------------- | --------------------------------------------- | ------------------------------- | @@ -160,8 +160,8 @@ clients are unaffected. `SafeFetch.fetch` accepts a URL, while `SafeFetch.fetchWithRequest` accepts an existing `WebAPI.Request.t`. Both promises fulfill with a `result` for these expected failures. A fulfilled -response is classified with the platform's `response.ok` value, so a 404 or 500 is distinct from a -request that did not produce a response. The public `fetchError` type contains `#FetchRejected(exn)` +response is classified with the platform's `Response.ok(response)` value, so a 404 or 500 is distinct +from a request that did not produce a response. The public `fetchError` type contains `#FetchRejected(exn)` and `#ResponseNotOk({response})`; `bodyError` contains `#BodyReadRejected(readError)`. `readError` has `response` and `cause` fields, and `response<'body>` retains the original response alongside the successful body. The function signatures expose extensible error rows (`[> fetchError]` and @@ -202,8 +202,8 @@ the application-specific `#InvalidUser`: ```ReScript switch await loadUser("https://example.com/api/users/42") { | Error(#FetchRejected(cause)) => reportUnavailable(cause) -| Error(#ResponseNotOk({response})) => handleHttpError(response.status) -| Error(#BodyReadRejected({response, cause})) => handleInvalidBody(response.status, cause) +| Error(#ResponseNotOk({response})) => handleHttpError(WebAPI.Response.status(response)) +| Error(#BodyReadRejected({response, cause})) => handleInvalidBody(WebAPI.Response.status(response), cause) | Error(#InvalidUser) => handleInvalidUser() | Ok(user) => showUser(user) } @@ -217,7 +217,7 @@ let response = await WebAPI.Fetch.fetch("https://example.com/api/users") switch response->WebAPI.SafeFetch.checkOk { | Ok(response) => useResponse(response) -| Error(#ResponseNotOk({response})) => handleHttpError(response.status) +| Error(#ResponseNotOk({response})) => handleHttpError(WebAPI.Response.status(response)) } ``` @@ -230,7 +230,7 @@ switch await WebAPI.SafeFetch.fetch("https://example.com/api/users/42") { switch await response->WebAPI.SafeFetch.text { | Ok({body}) => Console.error(body) | Error(#BodyReadRejected(_)) => - Console.error(`Request failed with ${Int.toString(response.status)}`) + Console.error(`Request failed with ${Int.toString(WebAPI.Response.status(response))}`) } | Error(#FetchRejected(cause)) => reportUnavailable(cause) | Ok(response) => useResponse(response) @@ -312,7 +312,7 @@ let initFromHeaders = WebAPI.HeadersInit.fromHeaders(clonedHeaders) Response headers are normal `Headers` values. ```ReScript -let contentType = response.headers->WebAPI.Headers.get("Content-Type") +let contentType = WebAPI.Response.headers(response)->WebAPI.Headers.get("Content-Type") switch contentType { | Null.Value(value) => Console.log(value) @@ -384,15 +384,15 @@ let created = WebAPI.Response.fromString( let empty = WebAPI.Response.fromNull(~init={status: 204}) ``` -Responses expose record fields for status metadata and methods for cloning or reading -the body. +Responses are opaque native objects. Read their status metadata with typed accessors such as +`Response.ok` and `Response.status`; use module functions for cloning or reading the body. ```ReScript -if created.ok { +if WebAPI.Response.ok(created) { let text = await created->WebAPI.Response.text Console.log(text) } else { - Console.log(created.statusText) + Console.log(WebAPI.Response.statusText(created)) } let copy = created->WebAPI.Response.clone @@ -483,10 +483,13 @@ switch maybeViewport { ## URLs URL helpers expose constructors and static functions on the same public module. +The native URL object is opaque; use typed accessors rather than direct property access. ```ReScript let url = WebAPI.URL.make(~url="/docs", ~base="https://rescript-lang.org") let canParse = WebAPI.URL.canParse(~url="/docs", ~base="https://rescript-lang.org") +let href = WebAPI.URL.getHref(url) +WebAPI.URL.setPathname(url, "/api") let json = url->WebAPI.URL.toJSON ``` diff --git a/docs/content/docs/contributing/api-modelling.mdx b/docs/content/docs/contributing/api-modelling.mdx index f5119614..19f1f620 100644 --- a/docs/content/docs/contributing/api-modelling.mdx +++ b/docs/content/docs/contributing/api-modelling.mdx @@ -25,6 +25,26 @@ let responseFromRequest = await WebAPI.Fetch.fetchWithRequest(request) When several signatures differ only by optional arguments, keep a single binding with an optional labeled argument when the browser API treats it as one operation. +## Native instances and record values + +Represent browser-owned objects as abstract native types in their canonical backing module +(`type response`, with no record representation). Public modules should alias that same type +identity and expose properties through typed native `@get` and `@set` bindings. Do not expose +record fields or create record-spread inheritance for native instances: a caller-defined record +must not be coercible to the native type with `:>`. Preserve native property names with `@as`, +nullable and generic types, throwing behavior, and field documentation on the accessor. + +This is intentionally different from dictionaries and data values. Request/Response initializers, +options, descriptors, callback dictionaries, and ordinary result records remain constructible +records when the browser accepts them as input or data. Classify each type by its runtime role, +not by a module or type name. + +When changing an existing instance API, migrate direct field reads and writes to the module's +typed accessors and document that source-level break. Add packed-consumer negative fixtures for +literal construction, record spread, and ordinary `:>` coercion, plus a positive consumer that +obtains a real instance through a factory and uses its accessors. Expected diagnostics must match +the actual opacity boundary rather than an unrelated type or missing-module error. + ## Constructor overloads Constructors use a distinct naming convention: diff --git a/docs/content/docs/philosophy.mdx b/docs/content/docs/philosophy.mdx index 04c156cb..78627ba0 100644 --- a/docs/content/docs/philosophy.mdx +++ b/docs/content/docs/philosophy.mdx @@ -4,7 +4,11 @@ description: Learn more about the design philosophy of @rescript/webapi. slug: "design-philosophy" --- -The core idea of these ReScript bindings is that each interface is modeled as a record type. Where possible, inheritance is represented using record spreading, and methods are modeled as functions in a separate module. This design allows for greater familiarity with the underlying JavaScript APIs, making it more friendly for newcomers. +The opaque native-object representation is being introduced across this package. For converted APIs, +ordinary records cannot stand in for browser instances; properties are exposed through typed +accessors and methods through separate module functions. Caller-owned dictionaries, options, and +ordinary data values remain constructible records. This distinction preserves the native JavaScript +object while making invalid lookalikes harder to express. ## ReScript 13 alpha compatibility @@ -32,7 +36,12 @@ let myElement: WebAPI.Element.t = document->WebAPI.Document.createElement("div") ## Interfaces -Since ReScript does not have the concept of classes or interfaces, the bindings represent the interfaces as record types. These record types can inherit properties from a "base" type through spreading. This can only be done if there are no circular references in the inheritance chain. If circular references exist, spreading is avoided, and the base properties are duplicated instead. +For converted interfaces, native instances are modeled with abstract types and typed native property +accessors. Other interfaces remain on their existing representation until migrated. Converted +inheritance relationships use the same type identities as their backing Web API bindings; accessor +receiver types remain compatible with base and derived interfaces. Do not use record spreading to +model native instances. Caller-owned dictionaries and plain data containers continue to use record +types. ## Methods diff --git a/docs/content/docs/project-status.mdx b/docs/content/docs/project-status.mdx index d7528876..1b9b9ebd 100644 --- a/docs/content/docs/project-status.mdx +++ b/docs/content/docs/project-status.mdx @@ -35,9 +35,18 @@ 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. +Approximately 80 bindings still carry private-record TODOs. The native-instance opacity migration +is underway using abstract types and typed property accessors; it intentionally replaces direct +record-field access for converted APIs. This draft covers only a small subset of the 291 public +modules, so unconverted native records remain constructible and the package-wide guarantee is not +yet complete. Do not infer opacity from a private record alone: ordinary structural coercion can +bypass that restriction. Generator reproducibility is also follow-up work, not an alpha release +gate. + +The partial [native-instance inventory](https://github.com/rescript-lang/experimental-rescript-webapi/blob/main/scripts/native-instance-inventory.json) +records the converted set, intentional constructible dictionary/data examples, and unconverted +groups. Stream reader/writer and worker context/source types still modeled as `unknown` have no +native identity to protect yet; they are explicitly unresolved and are not counted as opaque. 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 diff --git a/package.json b/package.json index 8c5e3984..fad95971 100644 --- a/package.json +++ b/package.json @@ -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-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-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-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" diff --git a/scripts/check-consumer.mjs b/scripts/check-consumer.mjs index 9f1ec437..8952be65 100644 --- a/scripts/check-consumer.mjs +++ b/scripts/check-consumer.mjs @@ -50,7 +50,7 @@ const validateTarballFiles = (files) => { } }; -const writeProject = (directory, sourceFile, features) => { +const writeProject = (directory, sourceFile, features, warningErrors = "+8") => { const sourceDirectory = path.join(directory, "src"); const fixturePath = path.resolve(repoRoot, sourceFile); if (!fixturePath.startsWith(`${path.join(repoRoot, "scripts")}${path.sep}`)) { @@ -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: warningErrors }, "package-specs": { module: "esmodule", "in-source": true }, }, null, @@ -192,6 +192,7 @@ const checkConsumers = async () => { name: "concrete public types", fixture: "scripts/consumer-concrete-types.res", features: null, + warningErrors: "+8+111", }, { name: "Fetch-only", @@ -217,7 +218,7 @@ const checkConsumers = async () => { ]; for (const scenario of scenarios) { - writeProject(projectDirectory, scenario.fixture, scenario.features); + writeProject(projectDirectory, scenario.fixture, scenario.features, scenario.warningErrors); compile(projectDirectory, scenario.expectedFailure ?? null); console.log(`[ok] packed ${scenario.name} consumer`); } diff --git a/scripts/check-native-inventory.mjs b/scripts/check-native-inventory.mjs new file mode 100644 index 00000000..ba4190cb --- /dev/null +++ b/scripts/check-native-inventory.mjs @@ -0,0 +1,45 @@ +import { readFileSync } from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const scriptPath = fileURLToPath(import.meta.url); +const repoRoot = path.resolve(path.dirname(scriptPath), ".."); +const inventory = JSON.parse( + readFileSync(path.join(path.dirname(scriptPath), "native-instance-inventory.json"), "utf8"), +); + +const source = (relativePath) => readFileSync(path.join(repoRoot, relativePath), "utf8"); +const escaped = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + +const abstractDeclaration = (text, typeName) => { + const match = new RegExp(`^type ${escaped(typeName)}(?:<[^>]+>)?(?:[ \\t]*\\r?$)`, "m").exec( + text, + ); + return match !== null; +}; + +const errors = inventory.convertedNativeInstances.flatMap((entry) => { + const backingSource = source(entry.backingFile); + const publicSource = source(entry.publicFile); + const aliasExists = publicSource.includes(entry.publicAlias); + const missingAccessors = entry.accessors.filter( + (accessor) => !new RegExp(`\\b${escaped(accessor)}\\b`).test(publicSource), + ); + + return [ + ...(!abstractDeclaration(backingSource, entry.backingType) + ? [`${entry.name} backing ${entry.backingFile} must remain an abstract type.`] + : []), + ...(!aliasExists ? [`${entry.name} must retain alias ${entry.publicAlias}.`] : []), + ...missingAccessors.map((accessor) => `${entry.name} is missing accessor ${accessor}.`), + ]; +}); + +if (errors.length > 0) { + console.error(errors.join("\n")); + process.exitCode = 1; +} else { + console.log( + `Validated ${inventory.convertedNativeInstances.length} abstract native identities from the partial inventory.`, + ); +} diff --git a/scripts/consumer-concrete-types.res b/scripts/consumer-concrete-types.res index c1aee495..bf4ecb74 100644 --- a/scripts/consumer-concrete-types.res +++ b/scripts/consumer-concrete-types.res @@ -101,6 +101,6 @@ let worklet: WebAPI.Worklet.t => WebAPI.Worklet.t = value => value let makePublicUrl = () => { let url: WebAPI.URL.t = WebAPI.URL.make(~url="https://example.invalid") - url.href = "https://example.com" - url.href + WebAPI.URL.setHref(url, "https://example.com") + WebAPI.URL.getHref(url) } diff --git a/scripts/consumer-crypto.res b/scripts/consumer-crypto.res index 79153a66..c762c759 100644 --- a/scripts/consumer-crypto.res +++ b/scripts/consumer-crypto.res @@ -1,7 +1,7 @@ @scope("globalThis") external crypto: WebAPI.Crypto.t = "crypto" -let subtle = crypto.subtle +let subtle = WebAPI.Crypto.subtle(crypto) let data = WebAPI.SubtleCrypto.binaryDataFromUint8Array(Uint8Array.fromArray([65, 66, 67])) diff --git a/scripts/fixtures/negative/GeneratePairRejectsAlgorithm.res b/scripts/fixtures/negative/GeneratePairRejectsAlgorithm.res index 9ed7654a..e742ec04 100644 --- a/scripts/fixtures/negative/GeneratePairRejectsAlgorithm.res +++ b/scripts/fixtures/negative/GeneratePairRejectsAlgorithm.res @@ -2,7 +2,7 @@ external crypto: WebAPI.Crypto.t = "crypto" let _pair = WebAPI.SubtleCrypto.generateEcdsaKeyPair( - crypto.subtle, + WebAPI.Crypto.subtle(crypto), ~algorithm=WebAPI.SubtleCrypto.algorithmObject({ name: "HMAC", hash: {name: "SHA-256"}, diff --git a/scripts/fixtures/negative/GenerateSymmetricRejectsAlgorithm.res b/scripts/fixtures/negative/GenerateSymmetricRejectsAlgorithm.res index cfc964a3..53b14317 100644 --- a/scripts/fixtures/negative/GenerateSymmetricRejectsAlgorithm.res +++ b/scripts/fixtures/negative/GenerateSymmetricRejectsAlgorithm.res @@ -2,7 +2,7 @@ external crypto: WebAPI.Crypto.t = "crypto" let _key = WebAPI.SubtleCrypto.generateKeySymmetric( - crypto.subtle, + WebAPI.Crypto.subtle(crypto), ~algorithm=WebAPI.SubtleCrypto.algorithmObject({name: "ECDSA", namedCurve: "P-256"}), ~hash=WebAPI.SubtleCrypto.algorithmObject({name: "SHA-256"}), ~length=256, diff --git a/scripts/fixtures/negative/JwkIsNotBinaryKeyFormat.res b/scripts/fixtures/negative/JwkIsNotBinaryKeyFormat.res index 82bcdd43..e496e998 100644 --- a/scripts/fixtures/negative/JwkIsNotBinaryKeyFormat.res +++ b/scripts/fixtures/negative/JwkIsNotBinaryKeyFormat.res @@ -2,14 +2,14 @@ external crypto: WebAPI.Crypto.t = "crypto" let key = await WebAPI.SubtleCrypto.generateKeySymmetric( - crypto.subtle, + WebAPI.Crypto.subtle(crypto), ~hash=WebAPI.SubtleCrypto.algorithmObject({name: "SHA-256"}), ~length=256, ~extractable=true, ~keyUsages=[WebAPI.SubtleCrypto.signUsage], ) let _export = WebAPI.SubtleCrypto.exportKeyBinary( - crypto.subtle, + WebAPI.Crypto.subtle(crypto), ~format=WebAPI.SubtleCrypto.Jwk, ~key, ) diff --git a/scripts/fixtures/negative/RawIsNotJwkKeyFormat.res b/scripts/fixtures/negative/RawIsNotJwkKeyFormat.res index 0a55cea4..059a0d13 100644 --- a/scripts/fixtures/negative/RawIsNotJwkKeyFormat.res +++ b/scripts/fixtures/negative/RawIsNotJwkKeyFormat.res @@ -2,14 +2,14 @@ external crypto: WebAPI.Crypto.t = "crypto" let key = await WebAPI.SubtleCrypto.generateKeySymmetric( - crypto.subtle, + WebAPI.Crypto.subtle(crypto), ~hash=WebAPI.SubtleCrypto.algorithmObject({name: "SHA-256"}), ~length=256, ~extractable=true, ~keyUsages=[WebAPI.SubtleCrypto.signUsage], ) let _export = WebAPI.SubtleCrypto.exportKeyJwk( - crypto.subtle, + WebAPI.Crypto.subtle(crypto), ~format=WebAPI.SubtleCrypto.Raw, ~key, ) diff --git a/scripts/fixtures/negative/ResponseInferredReturnHasNoFields.res b/scripts/fixtures/negative/ResponseInferredReturnHasNoFields.res new file mode 100644 index 00000000..efe480c2 --- /dev/null +++ b/scripts/fixtures/negative/ResponseInferredReturnHasNoFields.res @@ -0,0 +1,2 @@ +let response = WebAPI.Response.fromString("body") +let _ = response.ok diff --git a/scripts/fixtures/negative/ResponseLiteralCannotConstruct.res b/scripts/fixtures/negative/ResponseLiteralCannotConstruct.res new file mode 100644 index 00000000..89009848 --- /dev/null +++ b/scripts/fixtures/negative/ResponseLiteralCannotConstruct.res @@ -0,0 +1,12 @@ +let original = WebAPI.Response.fromString("body") +let fake: WebAPI.Response.t = { + type_: WebAPI.Response.type_(original), + url: WebAPI.Response.url(original), + redirected: WebAPI.Response.redirected(original), + status: WebAPI.Response.status(original), + ok: WebAPI.Response.ok(original), + statusText: WebAPI.Response.statusText(original), + headers: WebAPI.Response.headers(original), + body: WebAPI.Response.body(original), + bodyUsed: WebAPI.Response.bodyUsed(original), +} diff --git a/scripts/fixtures/negative/ResponseRecordCannotCoerce.res b/scripts/fixtures/negative/ResponseRecordCannotCoerce.res new file mode 100644 index 00000000..4a84e45c --- /dev/null +++ b/scripts/fixtures/negative/ResponseRecordCannotCoerce.res @@ -0,0 +1,26 @@ +type responseShape = { + @as("type") + type_: WebAPI.FetchTypes.responseType, + url: string, + redirected: bool, + status: int, + ok: bool, + statusText: string, + headers: WebAPI.FetchTypes.headers, + body: Null.t>, + bodyUsed: bool, +} + +let original = WebAPI.Response.fromString("body") +let record: responseShape = { + type_: WebAPI.Response.type_(original), + url: WebAPI.Response.url(original), + redirected: WebAPI.Response.redirected(original), + status: WebAPI.Response.status(original), + ok: WebAPI.Response.ok(original), + statusText: WebAPI.Response.statusText(original), + headers: WebAPI.Headers.make(), + body: WebAPI.Response.body(original), + bodyUsed: WebAPI.Response.bodyUsed(original), +} +let fake = (record :> WebAPI.Response.t) diff --git a/scripts/fixtures/negative/ResponseTypeCannotBeSpread.res b/scripts/fixtures/negative/ResponseTypeCannotBeSpread.res new file mode 100644 index 00000000..69db7ec4 --- /dev/null +++ b/scripts/fixtures/negative/ResponseTypeCannotBeSpread.res @@ -0,0 +1 @@ +type responseShape = {...WebAPI.Response.t} diff --git a/scripts/native-instance-inventory.json b/scripts/native-instance-inventory.json new file mode 100644 index 00000000..aad4d227 --- /dev/null +++ b/scripts/native-instance-inventory.json @@ -0,0 +1,121 @@ +{ + "revision": "4888db2cc601ad568b76af6ed2add60da0ad96cb", + "status": "partial", + "publicModuleCount": 291, + "sourceFeatureGroupCount": 28, + "convertedNativeInstances": [ + { + "name": "Response", + "backingFile": "src/fetch/FetchTypes.res", + "backingType": "response", + "publicFile": "src/fetch/Response.res", + "publicAlias": "type t = FetchTypes.response", + "accessors": [ + "type_", + "url", + "redirected", + "status", + "ok", + "statusText", + "headers", + "body", + "bodyUsed" + ] + }, + { + "name": "URL", + "backingFile": "src/url/UrlTypes.res", + "backingType": "url", + "publicFile": "src/url/URL.res", + "publicAlias": "type t = UrlTypes.url", + "accessors": [ + "getHref", + "setHref", + "getOrigin", + "getProtocol", + "setProtocol", + "getUsername", + "setUsername", + "getPassword", + "setPassword", + "getHost", + "setHost", + "getHostname", + "setHostname", + "getPort", + "setPort", + "getPathname", + "setPathname", + "getSearch", + "setSearch", + "getHash", + "setHash", + "getSearchParams" + ] + }, + { + "name": "Crypto", + "backingFile": "src/crypto/WebCryptoTypes.res", + "backingType": "crypto", + "publicFile": "src/crypto/Crypto.res", + "publicAlias": "type t = WebCryptoTypes.crypto", + "accessors": ["subtle"] + }, + { + "name": "CryptoKey", + "backingFile": "src/crypto/WebCryptoTypes.res", + "backingType": "cryptoKey", + "publicFile": "src/crypto/SubtleCrypto.res", + "publicAlias": "type cryptoKey = WebCryptoTypes.cryptoKey", + "accessors": [ + "cryptoKeyType", + "cryptoKeyExtractable", + "cryptoKeyAlgorithm", + "cryptoKeyUsages" + ] + }, + { + "name": "DOMException", + "backingFile": "src/core/DOM.res", + "backingType": "domException", + "publicFile": "src/dom-platform/DOMException.res", + "publicAlias": "type t = DOM.domException", + "accessors": ["name", "message"] + }, + { + "name": "DOMStringList", + "backingFile": "src/core/DOM.res", + "backingType": "domStringList", + "publicFile": "src/dom-platform/DOMStringList.res", + "publicAlias": "type t = DOM.domStringList", + "accessors": ["length"] + }, + { + "name": "DOMTokenList", + "backingFile": "src/core/DOM.res", + "backingType": "domTokenList", + "publicFile": "src/dom-platform/DOMTokenList.res", + "publicAlias": "type t = DOM.domTokenList", + "accessors": ["length", "value", "setValue"] + } + ], + "constructibleDataExceptions": [ + "src/fetch/FetchTypes.res:requestInit", + "src/fetch/FetchTypes.res:responseInit", + "src/crypto/WebCryptoTypes.res:cryptoKeyPair", + "src/crypto/WebCryptoTypes.res:keyAlgorithm", + "src/crypto/WebCryptoTypes.res:algorithm" + ], + "unresolvedUnknownBackedHandles": [ + "stream reader and writer types", + "worker context/source types modeled as unknown" + ], + "unconvertedNativeGroups": [ + "DOMTree and DOM nodes", + "HTML, SVG, Canvas", + "Fetch Request, File/Streams", + "Storage/IDB, Workers, Messaging", + "CSSOM/Geometry/Animation", + "Media/WebAudio/Device/Performance/Navigator/Observers" + ] +} diff --git a/scripts/negative-fixtures.json b/scripts/negative-fixtures.json index 98b868fb..341da188 100644 --- a/scripts/negative-fixtures.json +++ b/scripts/negative-fixtures.json @@ -23,6 +23,30 @@ "features": ["WebAPI.Fetch"], "expectedDiagnostic": "This has type:" }, + { + "name": "Response cannot be made from an ordinary record coercion", + "fixture": "scripts/fixtures/negative/ResponseRecordCannotCoerce.res", + "features": ["WebAPI.Fetch"], + "expectedDiagnostic": "is not a subtype of WebAPI.Response.t" + }, + { + "name": "Response cannot be constructed as an ordinary record literal", + "fixture": "scripts/fixtures/negative/ResponseLiteralCannotConstruct.res", + "features": ["WebAPI.Fetch"], + "expectedDiagnostic": "no corresponding record type is in scope" + }, + { + "name": "Response has no record representation to spread", + "fixture": "scripts/fixtures/negative/ResponseTypeCannotBeSpread.res", + "features": ["WebAPI.Fetch"], + "expectedDiagnostic": "is not an object type" + }, + { + "name": "Response factory returns an opaque instance", + "fixture": "scripts/fixtures/negative/ResponseInferredReturnHasNoFields.res", + "features": ["WebAPI.Fetch"], + "expectedDiagnostic": "Only records have fields that can be accessed" + }, { "name": "closest cannot narrow to an unrelated element type", "fixture": "scripts/fixtures/negative/ClosestCannotNarrow.res", diff --git a/src/core/DOM.res b/src/core/DOM.res index 50a9a5cd..e33ab7eb 100644 --- a/src/core/DOM.res +++ b/src/core/DOM.res @@ -11,29 +11,14 @@ An abnormal event (called an exception) which occurs as a result of calling a me [See DOMException on MDN](https://developer.mozilla.org/docs/Web/API/DOMException) */ @editor.completeFrom(DOMException) -type domException = { - /** - [Read more on MDN](https://developer.mozilla.org/docs/Web/API/DOMException/name) - */ - name: string, - /** - [Read more on MDN](https://developer.mozilla.org/docs/Web/API/DOMException/message) - */ - message: string, -} +type domException /** A type returned by some APIs which contains a list of DOMString (strings). [See DOMStringList on MDN](https://developer.mozilla.org/docs/Web/API/DOMStringList) */ @editor.completeFrom(DOMStringList) -type domStringList = { - /** - Returns the number of strings in strings. - [Read more on MDN](https://developer.mozilla.org/docs/Web/API/DOMStringList/length) - */ - length: int, -} +type domStringList @editor.completeFrom(Window) type window = private {} @@ -219,22 +204,8 @@ The state and the identity of the user agent. It allows scripts to query it and @editor.completeFrom(Navigator) type navigator -// TODO: mark as private once mutating fields of private records is allowed @editor.completeFrom(DOMTokenList) -type domTokenList = { - /** - Returns the number of tokens. - [Read more on MDN](https://developer.mozilla.org/docs/Web/API/DOMTokenList/length) - */ - length: int, - /** - Returns the associated set as string. - -Can be set, to change the associated attribute. - [Read more on MDN](https://developer.mozilla.org/docs/Web/API/DOMTokenList/value) - */ - mutable value: string, -} +type domTokenList /** [See FragmentDirective on MDN](https://developer.mozilla.org/docs/Web/API/FragmentDirective) diff --git a/src/crypto/Crypto.res b/src/crypto/Crypto.res index 485e9bd6..dc6e2a2a 100644 --- a/src/crypto/Crypto.res +++ b/src/crypto/Crypto.res @@ -10,3 +10,8 @@ external getRandomValues: (WebCryptoTypes.crypto, 't) => 't = "getRandomValues" @send external randomUUID: WebCryptoTypes.crypto => string = "randomUUID" type t = WebCryptoTypes.crypto + +/** +[Read more on MDN](https://developer.mozilla.org/docs/Web/API/Crypto/subtle) +*/ +let subtle = WebCryptoTypes.cryptoSubtle diff --git a/src/crypto/SubtleCrypto.res b/src/crypto/SubtleCrypto.res index 9d8f29ad..4f7ffb4a 100644 --- a/src/crypto/SubtleCrypto.res +++ b/src/crypto/SubtleCrypto.res @@ -14,6 +14,23 @@ type keyAlgorithm = WebCryptoTypes.keyAlgorithm type cryptoKey = WebCryptoTypes.cryptoKey type cryptoKeyPair = WebCryptoTypes.cryptoKeyPair +/** +[Read more on MDN](https://developer.mozilla.org/docs/Web/API/CryptoKey/type) +*/ +@get external cryptoKeyType: cryptoKey => keyType = "type" +/** +[Read more on MDN](https://developer.mozilla.org/docs/Web/API/CryptoKey/extractable) +*/ +@get external cryptoKeyExtractable: cryptoKey => bool = "extractable" +/** +[Read more on MDN](https://developer.mozilla.org/docs/Web/API/CryptoKey/algorithm) +*/ +@get external cryptoKeyAlgorithm: cryptoKey => keyAlgorithm = "algorithm" +/** +[Read more on MDN](https://developer.mozilla.org/docs/Web/API/CryptoKey/usages) +*/ +@get external cryptoKeyUsages: cryptoKey => array = "usages" + external algorithmString: string => algorithmIdentifier = "%identity" external algorithmObject: algorithm => algorithmIdentifier = "%identity" diff --git a/src/crypto/WebCryptoTypes.res b/src/crypto/WebCryptoTypes.res index 17565a5b..ce9d1d07 100644 --- a/src/crypto/WebCryptoTypes.res +++ b/src/crypto/WebCryptoTypes.res @@ -35,36 +35,18 @@ Basic cryptography features available in the current context. It allows access t [See Crypto on MDN](https://developer.mozilla.org/docs/Web/API/Crypto) */ @editor.completeFrom(Crypto) -type crypto = private { - /** - [Read more on MDN](https://developer.mozilla.org/docs/Web/API/Crypto/subtle) - */ - subtle: subtleCrypto, -} +type crypto + +/** + [Read more on MDN](https://developer.mozilla.org/docs/Web/API/Crypto/subtle) + */ +@get external cryptoSubtle: crypto => subtleCrypto = "subtle" /** The CryptoKey dictionary of the Web Crypto API represents a cryptographic key. [See CryptoKey on MDN](https://developer.mozilla.org/docs/Web/API/CryptoKey) */ -type cryptoKey = { - /** - [Read more on MDN](https://developer.mozilla.org/docs/Web/API/CryptoKey/type) - */ - @as("type") - type_: keyType, - /** - [Read more on MDN](https://developer.mozilla.org/docs/Web/API/CryptoKey/extractable) - */ - extractable: bool, - /** - [Read more on MDN](https://developer.mozilla.org/docs/Web/API/CryptoKey/algorithm) - */ - algorithm: keyAlgorithm, - /** - [Read more on MDN](https://developer.mozilla.org/docs/Web/API/CryptoKey/usages) - */ - usages: array, -} +type cryptoKey type cryptoKeyPair = { privateKey: cryptoKey, diff --git a/src/dom-platform/DOMException.res b/src/dom-platform/DOMException.res index 216b546d..4b456a1f 100644 --- a/src/dom-platform/DOMException.res +++ b/src/dom-platform/DOMException.res @@ -1,5 +1,17 @@ type t = DOM.domException +/** +[Read more on MDN](https://developer.mozilla.org/docs/Web/API/DOMException/name) +*/ +@get +external name: t => string = "name" + +/** +[Read more on MDN](https://developer.mozilla.org/docs/Web/API/DOMException/message) +*/ +@get +external message: t => string = "message" + /** [Read more on MDN](https://developer.mozilla.org/docs/Web/API/DOMException) */ diff --git a/src/dom-platform/DOMStringList.res b/src/dom-platform/DOMStringList.res index 42fdaa5e..4444b122 100644 --- a/src/dom-platform/DOMStringList.res +++ b/src/dom-platform/DOMStringList.res @@ -1,5 +1,12 @@ type t = DOM.domStringList +/** +Returns the number of strings in strings. +[Read more on MDN](https://developer.mozilla.org/docs/Web/API/DOMStringList/length) +*/ +@get +external length: t => int = "length" + /** Returns the string with index index from strings. [Read more on MDN](https://developer.mozilla.org/docs/Web/API/DOMStringList/item) diff --git a/src/dom-platform/DOMTokenList.res b/src/dom-platform/DOMTokenList.res index 931e826e..097cae75 100644 --- a/src/dom-platform/DOMTokenList.res +++ b/src/dom-platform/DOMTokenList.res @@ -1,18 +1,39 @@ -type t = DOM.domTokenList = {...DOM.domTokenList} +type t = DOM.domTokenList + +/** +Returns the number of tokens. +[Read more on MDN](https://developer.mozilla.org/docs/Web/API/DOMTokenList/length) +*/ +@get +external length: t => int = "length" + +/** +Returns the associated set as string. Can be set, to change the associated attribute. +[Read more on MDN](https://developer.mozilla.org/docs/Web/API/DOMTokenList/value) +*/ +@get +external value: t => string = "value" + +/** +Returns the associated set as string. Can be set, to change the associated attribute. +[Read more on MDN](https://developer.mozilla.org/docs/Web/API/DOMTokenList/value) +*/ +@set +external setValue: (t, string) => unit = "value" /** Returns the token with index index. [Read more on MDN](https://developer.mozilla.org/docs/Web/API/DOMTokenList/item) */ @send -external item: (DOM.domTokenList, int) => Null.t = "item" +external item: (t, int) => Null.t = "item" /** Returns true if token is present, and false otherwise. [Read more on MDN](https://developer.mozilla.org/docs/Web/API/DOMTokenList/contains) */ @send -external contains: (DOM.domTokenList, string) => bool = "contains" +external contains: (t, string) => bool = "contains" /** Adds all arguments passed, except those already present. @@ -23,7 +44,7 @@ Throws an "InvalidCharacterError" DOMException if one of the arguments contains [Read more on MDN](https://developer.mozilla.org/docs/Web/API/DOMTokenList/add) */ @throws(JsExn) @send -external add: (DOM.domTokenList, string) => unit = "add" +external add: (t, string) => unit = "add" /** Removes arguments passed, if they are present. @@ -34,7 +55,7 @@ Throws an "InvalidCharacterError" DOMException if one of the arguments contains [Read more on MDN](https://developer.mozilla.org/docs/Web/API/DOMTokenList/remove) */ @throws(JsExn) @send -external remove: (DOM.domTokenList, string) => unit = "remove" +external remove: (t, string) => unit = "remove" /** If force is not given, "toggles" token, removing it if it's present and adding it if it's not present. If force is true, adds token (same as add()). If force is false, removes token (same as remove()). @@ -47,7 +68,7 @@ Throws an "InvalidCharacterError" DOMException if token contains any spaces. [Read more on MDN](https://developer.mozilla.org/docs/Web/API/DOMTokenList/toggle) */ @throws(JsExn) @send -external toggle: (DOM.domTokenList, ~token: string, ~force: bool=?) => bool = "toggle" +external toggle: (t, ~token: string, ~force: bool=?) => bool = "toggle" /** Replaces token with newToken. @@ -60,7 +81,7 @@ Throws an "InvalidCharacterError" DOMException if one of the arguments contains [Read more on MDN](https://developer.mozilla.org/docs/Web/API/DOMTokenList/replace) */ @throws(JsExn) @send -external replace: (DOM.domTokenList, ~token: string, ~newToken: string) => bool = "replace" +external replace: (t, ~token: string, ~newToken: string) => bool = "replace" /** Returns true if token is in the associated attribute's supported tokens. Returns false otherwise. @@ -69,4 +90,4 @@ Throws a TypeError if the associated attribute has no supported tokens defined. [Read more on MDN](https://developer.mozilla.org/docs/Web/API/DOMTokenList/supports) */ @throws(JsExn) @send -external supports: (DOM.domTokenList, string) => bool = "supports" +external supports: (t, string) => bool = "supports" diff --git a/src/fetch/FetchTypes.res b/src/fetch/FetchTypes.res index 7a4648fd..4aae97e2 100644 --- a/src/fetch/FetchTypes.res +++ b/src/fetch/FetchTypes.res @@ -163,45 +163,7 @@ This WebApiFetch API interface represents the response to a request. [See Response on MDN](https://developer.mozilla.org/docs/Web/API/Response) */ @editor.completeFrom(Response) -type response = private { - /** - [Read more on MDN](https://developer.mozilla.org/docs/Web/API/Response/type) - */ - @as("type") - type_: responseType, - /** - [Read more on MDN](https://developer.mozilla.org/docs/Web/API/Response/url) - */ - url: string, - /** - [Read more on MDN](https://developer.mozilla.org/docs/Web/API/Response/redirected) - */ - redirected: bool, - /** - [Read more on MDN](https://developer.mozilla.org/docs/Web/API/Response/status) - */ - status: int, - /** - [Read more on MDN](https://developer.mozilla.org/docs/Web/API/Response/ok) - */ - ok: bool, - /** - [Read more on MDN](https://developer.mozilla.org/docs/Web/API/Response/statusText) - */ - statusText: string, - /** - [Read more on MDN](https://developer.mozilla.org/docs/Web/API/Response/headers) - */ - headers: headers, - /** - [Read more on MDN](https://developer.mozilla.org/docs/Web/API/Request/body) - */ - body: Null.t>, - /** - [Read more on MDN](https://developer.mozilla.org/docs/Web/API/Request/bodyUsed) - */ - bodyUsed: bool, -} +type response /** Provides a way to easily construct a set of key/value pairs representing form fields and their values, which can then be easily sent using the XMLHttpRequest.send() method. It uses the same format a form would use if the encoding type were set to "multipart/form-data". diff --git a/src/fetch/Response.res b/src/fetch/Response.res index aa76cfcb..22579783 100644 --- a/src/fetch/Response.res +++ b/src/fetch/Response.res @@ -1,7 +1,44 @@ /** [Read more on MDN](https://developer.mozilla.org/docs/Web/API/Response) */ -type t = FetchTypes.response = private {...FetchTypes.response} +type t = FetchTypes.response + +/** +[Read more on MDN](https://developer.mozilla.org/docs/Web/API/Response/type) +*/ +@get external type_: t => FetchTypes.responseType = "type" +/** +[Read more on MDN](https://developer.mozilla.org/docs/Web/API/Response/url) +*/ +@get external url: t => string = "url" +/** +[Read more on MDN](https://developer.mozilla.org/docs/Web/API/Response/redirected) +*/ +@get external redirected: t => bool = "redirected" +/** +[Read more on MDN](https://developer.mozilla.org/docs/Web/API/Response/status) +*/ +@get external status: t => int = "status" +/** +[Read more on MDN](https://developer.mozilla.org/docs/Web/API/Response/ok) +*/ +@get external ok: t => bool = "ok" +/** +[Read more on MDN](https://developer.mozilla.org/docs/Web/API/Response/statusText) +*/ +@get external statusText: t => string = "statusText" +/** +[Read more on MDN](https://developer.mozilla.org/docs/Web/API/Response/headers) +*/ +@get external headers: t => FetchTypes.headers = "headers" +/** +[Read more on MDN](https://developer.mozilla.org/docs/Web/API/Response/body) +*/ +@get external body: t => Null.t> = "body" +/** +[Read more on MDN](https://developer.mozilla.org/docs/Web/API/Response/bodyUsed) +*/ +@get external bodyUsed: t => bool = "bodyUsed" type responseInit = FetchTypes.responseInit type bodyInit = BodyInit.t type headersInit = HeadersInit.t diff --git a/src/safe-fetch/SafeFetch.res b/src/safe-fetch/SafeFetch.res index 0d02588c..f2dd8a72 100644 --- a/src/safe-fetch/SafeFetch.res +++ b/src/safe-fetch/SafeFetch.res @@ -17,7 +17,7 @@ type response<'body> = { } let checkOk = (response: Response.t) => - if response.ok { + if Response.ok(response) { Ok(response) } else { Error(#ResponseNotOk({response: response})) @@ -31,7 +31,7 @@ let fetchUrl = async (url, ~init=?) => { | None => Fetch.fetch(url) } ) - if response.ok { + if Response.ok(response) { Ok(response) } else { Error(#ResponseNotOk({response: response})) @@ -49,7 +49,7 @@ let fetchWithRequest = async (request, ~init=?) => { | None => Fetch.fetchWithRequest(request) } ) - if response.ok { + if Response.ok(response) { Ok(response) } else { Error(#ResponseNotOk({response: response})) diff --git a/src/url/URL.res b/src/url/URL.res index 3d04c6a7..3603543e 100644 --- a/src/url/URL.res +++ b/src/url/URL.res @@ -1,17 +1,139 @@ @editor.completeFrom(URL) -type t = UrlTypes.url = {...UrlTypes.url} +type t = UrlTypes.url + +/** +[Read more on MDN](https://developer.mozilla.org/docs/Web/API/URL/href) +*/ +@get +external getHref: t => string = "href" +/** +[Read more on MDN](https://developer.mozilla.org/docs/Web/API/URL/href) +*/ +@throws(JsExn) @set +external setHref: (t, string) => unit = "href" + +/** +[Read more on MDN](https://developer.mozilla.org/docs/Web/API/URL/origin) +*/ +@get +external getOrigin: t => string = "origin" + +/** +[Read more on MDN](https://developer.mozilla.org/docs/Web/API/URL/protocol) +*/ +@get +external getProtocol: t => string = "protocol" +/** +[Read more on MDN](https://developer.mozilla.org/docs/Web/API/URL/protocol) +*/ +@set +external setProtocol: (t, string) => unit = "protocol" + +/** +[Read more on MDN](https://developer.mozilla.org/docs/Web/API/URL/username) +*/ +@get +external getUsername: t => string = "username" +/** +[Read more on MDN](https://developer.mozilla.org/docs/Web/API/URL/username) +*/ +@set +external setUsername: (t, string) => unit = "username" + +/** +[Read more on MDN](https://developer.mozilla.org/docs/Web/API/URL/password) +*/ +@get +external getPassword: t => string = "password" +/** +[Read more on MDN](https://developer.mozilla.org/docs/Web/API/URL/password) +*/ +@set +external setPassword: (t, string) => unit = "password" + +/** +[Read more on MDN](https://developer.mozilla.org/docs/Web/API/URL/host) +*/ +@get +external getHost: t => string = "host" +/** +[Read more on MDN](https://developer.mozilla.org/docs/Web/API/URL/host) +*/ +@set +external setHost: (t, string) => unit = "host" + +/** +[Read more on MDN](https://developer.mozilla.org/docs/Web/API/URL/hostname) +*/ +@get +external getHostname: t => string = "hostname" +/** +[Read more on MDN](https://developer.mozilla.org/docs/Web/API/URL/hostname) +*/ +@set +external setHostname: (t, string) => unit = "hostname" + +/** +[Read more on MDN](https://developer.mozilla.org/docs/Web/API/URL/port) +*/ +@get +external getPort: t => string = "port" +/** +[Read more on MDN](https://developer.mozilla.org/docs/Web/API/URL/port) +*/ +@set +external setPort: (t, string) => unit = "port" + +/** +[Read more on MDN](https://developer.mozilla.org/docs/Web/API/URL/pathname) +*/ +@get +external getPathname: t => string = "pathname" +/** +[Read more on MDN](https://developer.mozilla.org/docs/Web/API/URL/pathname) +*/ +@set +external setPathname: (t, string) => unit = "pathname" + +/** +[Read more on MDN](https://developer.mozilla.org/docs/Web/API/URL/search) +*/ +@get +external getSearch: t => string = "search" +/** +[Read more on MDN](https://developer.mozilla.org/docs/Web/API/URL/search) +*/ +@set +external setSearch: (t, string) => unit = "search" + +/** +[Read more on MDN](https://developer.mozilla.org/docs/Web/API/URL/searchParams) +*/ +@get +external getSearchParams: t => UrlTypes.urlSearchParams = "searchParams" + +/** +[Read more on MDN](https://developer.mozilla.org/docs/Web/API/URL/hash) +*/ +@get +external getHash: t => string = "hash" +/** +[Read more on MDN](https://developer.mozilla.org/docs/Web/API/URL/hash) +*/ +@set +external setHash: (t, string) => unit = "hash" /** [Read more on MDN](https://developer.mozilla.org/docs/Web/API/URL) */ @throws(JsExn) @new -external make: (~url: string, ~base: string=?) => UrlTypes.url = "URL" +external make: (~url: string, ~base: string=?) => t = "URL" /** [Read more on MDN](https://developer.mozilla.org/docs/Web/API/URL/parse_static) */ @scope("URL") -external parse: (~url: string, ~base: string=?) => Null.t = "parse" +external parse: (~url: string, ~base: string=?) => Null.t = "parse" /** [Read more on MDN](https://developer.mozilla.org/docs/Web/API/URL/canParse_static) @@ -23,7 +145,7 @@ external canParse: (~url: string, ~base: string=?) => bool = "canParse" [Read more on MDN](https://developer.mozilla.org/docs/Web/API/URL/toJSON) */ @send -external toJSON: UrlTypes.url => string = "toJSON" +external toJSON: t => string = "toJSON" /** [Read more on MDN](https://developer.mozilla.org/docs/Web/API/URL/createObjectURL_static) diff --git a/src/url/UrlTypes.res b/src/url/UrlTypes.res index 7718a467..eccef42f 100644 --- a/src/url/UrlTypes.res +++ b/src/url/UrlTypes.res @@ -17,53 +17,4 @@ The WebApiURL interface represents an object providing static methods used for TODO: mark as private once mutating fields of private records is allowed */ @editor.completeFrom(URL) -type url = { - /** - [Read more on MDN](https://developer.mozilla.org/docs/Web/API/URL/href) - */ - mutable href: string, - /** - [Read more on MDN](https://developer.mozilla.org/docs/Web/API/URL/origin) - */ - origin: string, - /** - [Read more on MDN](https://developer.mozilla.org/docs/Web/API/URL/protocol) - */ - mutable protocol: string, - /** - [Read more on MDN](https://developer.mozilla.org/docs/Web/API/URL/username) - */ - mutable username: string, - /** - [Read more on MDN](https://developer.mozilla.org/docs/Web/API/URL/password) - */ - mutable password: string, - /** - [Read more on MDN](https://developer.mozilla.org/docs/Web/API/URL/host) - */ - mutable host: string, - /** - [Read more on MDN](https://developer.mozilla.org/docs/Web/API/URL/hostname) - */ - mutable hostname: string, - /** - [Read more on MDN](https://developer.mozilla.org/docs/Web/API/URL/port) - */ - mutable port: string, - /** - [Read more on MDN](https://developer.mozilla.org/docs/Web/API/URL/pathname) - */ - mutable pathname: string, - /** - [Read more on MDN](https://developer.mozilla.org/docs/Web/API/URL/search) - */ - mutable search: string, - /** - [Read more on MDN](https://developer.mozilla.org/docs/Web/API/URL/searchParams) - */ - searchParams: urlSearchParams, - /** - [Read more on MDN](https://developer.mozilla.org/docs/Web/API/URL/hash) - */ - mutable hash: string, -} +type url diff --git a/tests/FetchAPI/SafeFetch__test.res b/tests/FetchAPI/SafeFetch__test.res index f954f76e..398b9403 100644 --- a/tests/FetchAPI/SafeFetch__test.res +++ b/tests/FetchAPI/SafeFetch__test.res @@ -274,7 +274,7 @@ globalThis.fetch = () => Promise.resolve(globalThis.__safeFetchResponse) switch await loadUser("https://example.com/user", mustNotDecode) { | Error(#ResponseNotOk({response})) => { assert(isSame(response, %raw(`globalThis.__safeFetchResponse`))) - assert(!response.bodyUsed) + assert(!Response.bodyUsed(response)) } | Error(#FetchRejected(_) | #BodyReadRejected(_)) | Ok(_) => assert(false) } @@ -320,7 +320,7 @@ let uncheckedResponse = Response.fromString("not read", ~init={status: 500}) switch await uncheckedResponse->readCheckedText { | Error(#ResponseNotOk({response})) => { assert(isSame(response, uncheckedResponse)) - assert(!response.bodyUsed) + assert(!Response.bodyUsed(response)) } | Error(#BodyReadRejected(_)) | Ok(_) => assert(false) } diff --git a/tests/browser/BindingContracts__test.res b/tests/browser/BindingContracts__test.res index abc7d9b6..670b4b42 100644 --- a/tests/browser/BindingContracts__test.res +++ b/tests/browser/BindingContracts__test.res @@ -254,7 +254,7 @@ setSize(target, 20) await resizedAgain target->Element.remove -let subtle = crypto.subtle +let subtle = crypto->Crypto.subtle let digest = await subtle->SubtleCrypto.digest( ~algorithm=SubtleCrypto.algorithmObject({name: "SHA-256"}), ~data=Uint8Array.fromArray([65, 66, 67])->SubtleCrypto.binaryDataFromUint8Array, From ecfc66328d83f78f410de6c52dedb6d79ef4461c Mon Sep 17 00:00:00 2001 From: Josh Vlk Date: Sun, 8 Nov 2026 18:06:06 -0500 Subject: [PATCH 2/2] abstract additional empty native handles --- CHANGELOG.md | 14 +++-- scripts/native-instance-inventory.json | 74 +++++++++++++++++++++++- src/crypto/WebCryptoTypes.res | 2 +- src/dom-platform/DOMStringMap.res | 2 +- src/fetch/FetchTypes.res | 4 +- src/fetch/FormData.res | 2 +- src/fetch/Headers.res | 2 +- src/storage/Cache.res | 2 +- src/storage/CacheStorage.res | 2 +- src/storage/CacheTypes.res | 4 +- src/storage/IDBFactory.res | 2 +- src/storage/IndexedDbTypes.res | 2 +- src/storage/StorageManager.res | 2 +- src/storage/StorageTypes.res | 2 +- src/workers/NavigationPreloadManager.res | 4 +- src/workers/ServiceWorkerTypes.res | 2 +- 16 files changed, 97 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 34f233e3..a8e993ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,12 +13,14 @@ versioning and publication. ### Migration notes -- This draft makes `Response`, `URL`, `Crypto`, `CryptoKey`, `DOMException`, `DOMStringList`, and - `DOMTokenList` opaque native identities. Direct field access is replaced by typed module - accessors: for example, use `Response.ok(response)`, `URL.getHref(url)`, - `URL.setHref(url, href)`, `Crypto.subtle(crypto)`, and `SubtleCrypto.cryptoKeyType(key)`. - Initializers and option dictionaries remain constructible records. The package-wide migration is - incomplete; unconverted native instance types still expose the existing API. +- This draft makes `Response`, `URL`, `Crypto`, `CryptoKey`, `DOMException`, `DOMStringList`, + `DOMTokenList`, `DOMStringMap`, `Headers`, `FormData`, `Cache`, `CacheStorage`, `IDBFactory`, + `StorageManager`, and `NavigationPreloadManager` opaque native identities. Direct property access + on the first group is replaced by typed module accessors: for example, use + `Response.ok(response)`, `URL.getHref(url)`, `URL.setHref(url, href)`, `Crypto.subtle(crypto)`, + and `SubtleCrypto.cryptoKeyType(key)`. Initializers and option dictionaries remain constructible + records. The package-wide migration is incomplete; unconverted native instance types still expose + the existing API. - WebSocket sends and worker `Request` fetches now preserve their native receiver (`this`) rather than calling receiver-bound methods unbound. - `WebAPI.Request.bytes`, `WebAPI.Response.bytes`, `WebAPI.Blob.bytes`, diff --git a/scripts/native-instance-inventory.json b/scripts/native-instance-inventory.json index aad4d227..8981762b 100644 --- a/scripts/native-instance-inventory.json +++ b/scripts/native-instance-inventory.json @@ -22,6 +22,22 @@ "bodyUsed" ] }, + { + "name": "Headers", + "backingFile": "src/fetch/FetchTypes.res", + "backingType": "headers", + "publicFile": "src/fetch/Headers.res", + "publicAlias": "type t = FetchTypes.headers", + "accessors": [] + }, + { + "name": "FormData", + "backingFile": "src/fetch/FetchTypes.res", + "backingType": "formData", + "publicFile": "src/fetch/FormData.res", + "publicAlias": "type t = FetchTypes.formData", + "accessors": [] + }, { "name": "URL", "backingFile": "src/url/UrlTypes.res", @@ -61,6 +77,14 @@ "publicAlias": "type t = WebCryptoTypes.crypto", "accessors": ["subtle"] }, + { + "name": "SubtleCrypto", + "backingFile": "src/crypto/WebCryptoTypes.res", + "backingType": "subtleCrypto", + "publicFile": "src/crypto/SubtleCrypto.res", + "publicAlias": "type t = WebCryptoTypes.subtleCrypto", + "accessors": [] + }, { "name": "CryptoKey", "backingFile": "src/crypto/WebCryptoTypes.res", @@ -97,6 +121,54 @@ "publicFile": "src/dom-platform/DOMTokenList.res", "publicAlias": "type t = DOM.domTokenList", "accessors": ["length", "value", "setValue"] + }, + { + "name": "DOMStringMap", + "backingFile": "src/dom-platform/DOMStringMap.res", + "backingType": "t", + "publicFile": "src/dom-platform/DOMStringMap.res", + "publicAlias": "type t", + "accessors": [] + }, + { + "name": "Cache", + "backingFile": "src/storage/CacheTypes.res", + "backingType": "cache", + "publicFile": "src/storage/Cache.res", + "publicAlias": "type t = CacheTypes.cache", + "accessors": [] + }, + { + "name": "CacheStorage", + "backingFile": "src/storage/CacheTypes.res", + "backingType": "cacheStorage", + "publicFile": "src/storage/CacheStorage.res", + "publicAlias": "type t = CacheTypes.cacheStorage", + "accessors": [] + }, + { + "name": "IDBFactory", + "backingFile": "src/storage/IndexedDbTypes.res", + "backingType": "idbFactory", + "publicFile": "src/storage/IDBFactory.res", + "publicAlias": "type t = IndexedDbTypes.idbFactory", + "accessors": [] + }, + { + "name": "StorageManager", + "backingFile": "src/storage/StorageTypes.res", + "backingType": "storageManager", + "publicFile": "src/storage/StorageManager.res", + "publicAlias": "type t = StorageTypes.storageManager", + "accessors": [] + }, + { + "name": "NavigationPreloadManager", + "backingFile": "src/workers/ServiceWorkerTypes.res", + "backingType": "navigationPreloadManager", + "publicFile": "src/workers/NavigationPreloadManager.res", + "publicAlias": "type t = ServiceWorkerTypes.navigationPreloadManager", + "accessors": [] } ], "constructibleDataExceptions": [ @@ -114,7 +186,7 @@ "DOMTree and DOM nodes", "HTML, SVG, Canvas", "Fetch Request, File/Streams", - "Storage/IDB, Workers, Messaging", + "remaining Storage/IDB types, Workers other than NavigationPreloadManager, Messaging", "CSSOM/Geometry/Animation", "Media/WebAudio/Device/Performance/Navigator/Observers" ] diff --git a/src/crypto/WebCryptoTypes.res b/src/crypto/WebCryptoTypes.res index ce9d1d07..d31ae473 100644 --- a/src/crypto/WebCryptoTypes.res +++ b/src/crypto/WebCryptoTypes.res @@ -28,7 +28,7 @@ This Web Crypto API interface provides a number of low-level cryptographic funct [See SubtleCrypto on MDN](https://developer.mozilla.org/docs/Web/API/SubtleCrypto) */ @editor.completeFrom(SubtleCrypto) -type subtleCrypto = private {} +type subtleCrypto /** Basic cryptography features available in the current context. It allows access to a cryptographically strong random number generator and to cryptographic primitives. diff --git a/src/dom-platform/DOMStringMap.res b/src/dom-platform/DOMStringMap.res index 639c8bce..fa649c17 100644 --- a/src/dom-platform/DOMStringMap.res +++ b/src/dom-platform/DOMStringMap.res @@ -2,4 +2,4 @@ Used by the dataset HTML attribute to represent data for custom attributes added to elements. [See DOMStringMap on MDN](https://developer.mozilla.org/docs/Web/API/DOMStringMap) */ -type t = private {} +type t diff --git a/src/fetch/FetchTypes.res b/src/fetch/FetchTypes.res index 4aae97e2..3c475ef6 100644 --- a/src/fetch/FetchTypes.res +++ b/src/fetch/FetchTypes.res @@ -75,7 +75,7 @@ type requestPriority = [See Headers on MDN](https://developer.mozilla.org/docs/Web/API/Headers) */ @editor.completeFrom(Headers) -type headers = private {} +type headers /** This WebApiFetch API interface represents a resource request. @@ -170,7 +170,7 @@ Provides a way to easily construct a set of key/value pairs representing form fi [See FormData on MDN](https://developer.mozilla.org/docs/Web/API/FormData) */ @editor.completeFrom(FormData) -type formData = private {} +type formData @editor.completeFrom(HeadersInit) type headersInit diff --git a/src/fetch/FormData.res b/src/fetch/FormData.res index 6920666c..90ddd7a3 100644 --- a/src/fetch/FormData.res +++ b/src/fetch/FormData.res @@ -1,7 +1,7 @@ /** [Read more on MDN](https://developer.mozilla.org/docs/Web/API/FormData) */ -type t = FetchTypes.formData = private {...FetchTypes.formData} +type t = FetchTypes.formData type formDataEntryValue = FormDataEntryValue.t /** diff --git a/src/fetch/Headers.res b/src/fetch/Headers.res index cef5fc4d..dc27e935 100644 --- a/src/fetch/Headers.res +++ b/src/fetch/Headers.res @@ -1,4 +1,4 @@ -type t = FetchTypes.headers = private {...FetchTypes.headers} +type t = FetchTypes.headers /** [Read more on MDN](https://developer.mozilla.org/docs/Web/API/Headers) diff --git a/src/storage/Cache.res b/src/storage/Cache.res index 0b6519ee..d46f1e17 100644 --- a/src/storage/Cache.res +++ b/src/storage/Cache.res @@ -1,4 +1,4 @@ -type t = CacheTypes.cache = private {...CacheTypes.cache} +type t = CacheTypes.cache /** [Read more on MDN](https://developer.mozilla.org/docs/Web/API/Cache/match) diff --git a/src/storage/CacheStorage.res b/src/storage/CacheStorage.res index 0dc00bce..1f716add 100644 --- a/src/storage/CacheStorage.res +++ b/src/storage/CacheStorage.res @@ -1,4 +1,4 @@ -type t = CacheTypes.cacheStorage = private {...CacheTypes.cacheStorage} +type t = CacheTypes.cacheStorage /** [Read more on MDN](https://developer.mozilla.org/docs/Web/API/CacheStorage/match) diff --git a/src/storage/CacheTypes.res b/src/storage/CacheTypes.res index 58e17f51..5a8fe4c7 100644 --- a/src/storage/CacheTypes.res +++ b/src/storage/CacheTypes.res @@ -3,14 +3,14 @@ Provides a storage mechanism for Request / Response object pairs that are cached [See Cache on MDN](https://developer.mozilla.org/docs/Web/API/Cache) */ @editor.completeFrom(Cache) -type cache = private {} +type cache /** The storage for Cache objects. [See CacheStorage on MDN](https://developer.mozilla.org/docs/Web/API/CacheStorage) */ @editor.completeFrom(CacheStorage) -type cacheStorage = private {} +type cacheStorage type cacheQueryOptions = { mutable ignoreSearch?: bool, diff --git a/src/storage/IDBFactory.res b/src/storage/IDBFactory.res index acf4d67d..57e64ac6 100644 --- a/src/storage/IDBFactory.res +++ b/src/storage/IDBFactory.res @@ -1,4 +1,4 @@ -type t = IndexedDbTypes.idbFactory = private {...IndexedDbTypes.idbFactory} +type t = IndexedDbTypes.idbFactory /** Attempts to open a connection to the named database with the current version, or 1 if it does not already exist. If the request is successful request's result will be the connection. diff --git a/src/storage/IndexedDbTypes.res b/src/storage/IndexedDbTypes.res index cd7315dd..b8b7192e 100644 --- a/src/storage/IndexedDbTypes.res +++ b/src/storage/IndexedDbTypes.res @@ -25,7 +25,7 @@ In the following code snippet, we make a request to open a database, and include [See IDBFactory on MDN](https://developer.mozilla.org/docs/Web/API/IDBFactory) */ @editor.completeFrom(IDBFactory) -type idbFactory = private {} +type idbFactory /** This WebApiIndexedDB API interface provides a connection to a database; you can use an IDBDatabase object to open a transaction on your database then create, manipulate, and delete objects (data) in that database. The interface provides the only way to get and manage versions of the database. diff --git a/src/storage/StorageManager.res b/src/storage/StorageManager.res index 633df79c..c623b148 100644 --- a/src/storage/StorageManager.res +++ b/src/storage/StorageManager.res @@ -1,4 +1,4 @@ -type t = StorageTypes.storageManager = private {...StorageTypes.storageManager} +type t = StorageTypes.storageManager /** [Read more on MDN](https://developer.mozilla.org/docs/Web/API/StorageManager/persisted) diff --git a/src/storage/StorageTypes.res b/src/storage/StorageTypes.res index 5556aae6..b47ee458 100644 --- a/src/storage/StorageTypes.res +++ b/src/storage/StorageTypes.res @@ -3,7 +3,7 @@ [See StorageManager on MDN](https://developer.mozilla.org/docs/Web/API/StorageManager) */ @editor.completeFrom(StorageManager) -type storageManager = private {} +type storageManager type storageEstimate = { mutable usage?: int, diff --git a/src/workers/NavigationPreloadManager.res b/src/workers/NavigationPreloadManager.res index e754a008..96023f70 100644 --- a/src/workers/NavigationPreloadManager.res +++ b/src/workers/NavigationPreloadManager.res @@ -1,6 +1,4 @@ -type t = ServiceWorkerTypes.navigationPreloadManager = private { - ...ServiceWorkerTypes.navigationPreloadManager, -} +type t = ServiceWorkerTypes.navigationPreloadManager /** [Read more on MDN](https://developer.mozilla.org/docs/Web/API/NavigationPreloadManager/enable) diff --git a/src/workers/ServiceWorkerTypes.res b/src/workers/ServiceWorkerTypes.res index 1f16d684..c9979bdf 100644 --- a/src/workers/ServiceWorkerTypes.res +++ b/src/workers/ServiceWorkerTypes.res @@ -38,7 +38,7 @@ type serviceWorker = private { [See NavigationPreloadManager on MDN](https://developer.mozilla.org/docs/Web/API/NavigationPreloadManager) */ @editor.completeFrom(NavigationPreloadManager) -type navigationPreloadManager = private {} +type navigationPreloadManager /** This WebApiServiceWorker API interface represents the service worker registration. You register a service worker to control one or more pages that share the same origin.