From c570806e20857d90cb44831f6c17da5781bf1587 Mon Sep 17 00:00:00 2001 From: Josh Vlk Date: Sun, 8 Nov 2026 18:01:59 -0500 Subject: [PATCH 1/4] 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/4] 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. From 17246226d5bae5b27a7ae7c4555f1d351575868e Mon Sep 17 00:00:00 2001 From: Josh Vlk Date: Tue, 10 Nov 2026 00:19:00 -0500 Subject: [PATCH 3/4] convert additional native API instances to opaque types --- CHANGELOG.md | 25 +- docs/content/docs/api-surface.mdx | 22 ++ .../docs/contributing/api-modelling.mdx | 5 + docs/content/docs/project-status.mdx | 3 + package.json | 6 +- scripts/check-native-inventory.mjs | 34 +- scripts/consumer-concrete-types.res | 21 ++ .../negative/CanvasTypeCannotBeSpread.res | 1 + .../ElementImplCannotForgeNativeType.res | 2 + .../EventImplCannotForgeNativeType.res | 2 + .../EventTargetImplCannotForgeNativeType.res | 2 + ...tendableEventImplCannotForgeNativeType.res | 2 + .../HTMLElementImplCannotForgeNativeType.res | 2 + .../NodeImplCannotForgeNativeType.res | 2 + .../negative/RequestRecordCannotCoerce.res | 37 ++ .../negative/RequestTypeCannotBeSpread.res | 1 + .../ResponseBackingTypeCannotBeSpread.res | 1 + scripts/native-instance-inventory.json | 342 ++++++++++++++++-- scripts/native-inventory-validation.mjs | 180 +++++++++ scripts/native-inventory-validation.test.mjs | 157 ++++++++ scripts/negative-fixtures.json | 60 +++ src/animation/AnimationEffect.res | 2 +- src/canvas/HTMLCanvasElement.res | 36 +- src/core/DOM.res | 34 +- src/cssom/CSSStyleValue.res | 2 +- src/dom-nodes/CharacterData.res | 5 +- src/dom-nodes/DOMImplementation.res | 4 +- src/dom-nodes/DocumentFragment.res | 5 +- src/dom-nodes/Element.res | 5 +- src/dom-nodes/Node.res | 4 +- src/event/AbortController.res | 8 +- src/event/Event.res | 4 +- src/event/EventTarget.res | 2 - src/event/ExtendableEvent.res | 5 +- src/fetch/FetchTypes.res | 82 +---- src/fetch/FormDataEntryValue.res | 27 +- src/fetch/Request.res | 88 ++++- src/file/BaseFileAndDirectoryEntries.res | 59 +-- src/file/Blob.res | 23 +- src/file/File.res | 37 +- src/file/FileAndDirectoryEntriesTypes.res | 2 +- src/file/FileList.res | 9 +- src/file/FileSystemDirectoryEntry.res | 12 +- src/file/FileSystemDirectoryHandle.res | 7 +- src/file/FileSystemDirectoryReader.res | 4 +- src/file/FileSystemEntry.res | 10 +- src/file/FileSystemFileHandle.res | 7 +- src/file/FileSystemHandle.res | 5 +- src/file/FileSystemWritableFileStream.res | 7 +- src/file/FileTypes.res | 77 +--- src/file/ReadableStream.res | 4 + src/file/WritableStream.res | 6 +- src/file/WritableStreamDefaultController.res | 8 +- src/html/HTMLElement.res | 6 +- src/messaging/PushEvent.res | 14 +- src/messaging/PushTypes.res | 8 +- src/svg/SVGElement.res | 5 +- src/svg/SVGGraphicsElement.res | 4 + src/window/CustomElementRegistry.res | 2 +- src/window/Document.res | 2 + src/window/XPathExpression.res | 2 +- tests/FetchAPI/FormData__test.res | 38 +- .../ServiceWorkerAPI/ServiceWorker__test.res | 2 +- tests/browser/BindingContracts__test.res | 43 ++- 64 files changed, 1217 insertions(+), 406 deletions(-) create mode 100644 scripts/fixtures/negative/CanvasTypeCannotBeSpread.res create mode 100644 scripts/fixtures/negative/ElementImplCannotForgeNativeType.res create mode 100644 scripts/fixtures/negative/EventImplCannotForgeNativeType.res create mode 100644 scripts/fixtures/negative/EventTargetImplCannotForgeNativeType.res create mode 100644 scripts/fixtures/negative/ExtendableEventImplCannotForgeNativeType.res create mode 100644 scripts/fixtures/negative/HTMLElementImplCannotForgeNativeType.res create mode 100644 scripts/fixtures/negative/NodeImplCannotForgeNativeType.res create mode 100644 scripts/fixtures/negative/RequestRecordCannotCoerce.res create mode 100644 scripts/fixtures/negative/RequestTypeCannotBeSpread.res create mode 100644 scripts/fixtures/negative/ResponseBackingTypeCannotBeSpread.res create mode 100644 scripts/native-inventory-validation.mjs create mode 100644 scripts/native-inventory-validation.test.mjs diff --git a/CHANGELOG.md b/CHANGELOG.md index a8e993ae..f96e6142 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,14 +13,23 @@ versioning and publication. ### Migration notes -- 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. +- This draft makes `Request`, `Response`, `URL`, `Crypto`, `SubtleCrypto`, `CryptoKey`, + `DOMException`, `DOMStringList`, `DOMTokenList`, `DOMStringMap`, `HTMLCanvasElement`, + `CSSStyleValue`, `AnimationEffect`, `XPathExpression`, `Headers`, `FormData`, `ReadableStream`, `WritableStream`, + `WritableStreamDefaultController`, `FileSystemWritableFileStream`, `Blob`, `File`, `FileList`, + `FileSystemEntry`, `FileSystemDirectoryEntry`, `FileSystemDirectoryReader`, `FileSystemHandle`, + `FileSystemDirectoryHandle`, `FileSystemFileHandle`, `Cache`, `CacheStorage`, `IDBFactory`, + `StorageManager`, `NavigationPreloadManager`, `PushEvent`, `Document`, `Window`, + `CustomElementRegistry`, and `AbortController` opaque native identities. Direct property access is + replaced by typed module accessors: for example, use + `Request.method(request)`, `Response.ok(response)`, `URL.getHref(url)`, `URL.setHref(url, href)`, + `HTMLCanvasElement.setWidth(canvas, width)`, and `Crypto.subtle(crypto)`. Initializers and option + dictionaries remain constructible records. `Blob.make` and `File.make` input parts now use typed + `Blob.blobPartFromString`/`Blob.blobPartFromBlob` helpers, and `FormDataEntryValue.decode` inspects + returned values. Generic `Impl` as-base conversions that accepted + arbitrary receiver types have been removed from the DOM core; specialized concrete conversions + remain where provided, and some descendant helpers are still migrating. 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 1a65fda5..a07edecb 100644 --- a/docs/content/docs/api-surface.mdx +++ b/docs/content/docs/api-surface.mdx @@ -110,6 +110,7 @@ alpha compiler. The SafeFetch API itself can also be used with ordinary `switch` Use `WebAPI.Fetch.fetch` for string URLs and `WebAPI.Fetch.fetchWithRequest` when you already have a `WebAPI.Request.t`. +Request properties are read through typed `WebAPI.Request` accessors, such as `Request.method(request)`. ```ReScript let response = await WebAPI.Fetch.fetch("https://example.com/api/users") @@ -260,6 +261,27 @@ let formDataBody = WebAPI.BodyInit.fromFormData(formData) let blobBody = WebAPI.BodyInit.fromBlob(blob) ``` +Build Blob and File data parts with typed helpers, then inspect values returned by `FormData` with +`FormDataEntryValue.decode`: + +```ReScript +let parts = [ + WebAPI.Blob.blobPartFromString("contents"), + WebAPI.Blob.blobPartFromBlob(blob), +] +let dataBlob = WebAPI.Blob.make(~blobParts=parts) + +switch WebAPI.FormData.get(formData, "upload")->Null.toOption { +| Some(value) => + switch WebAPI.FormDataEntryValue.decode(value) { + | Some(WebAPI.FormDataEntryValue.String(text)) => Console.log(text) + | Some(WebAPI.FormDataEntryValue.File(file)) => Console.log(WebAPI.File.name(file)) + | None => Console.log("unsupported FormData value") + } +| None => Console.log("missing upload") +} +``` + ## Headers Use `WebAPI.HeadersInit` when a fetch, request, or response initializer expects the diff --git a/docs/content/docs/contributing/api-modelling.mdx b/docs/content/docs/contributing/api-modelling.mdx index 19f1f620..4afe13ec 100644 --- a/docs/content/docs/contributing/api-modelling.mdx +++ b/docs/content/docs/contributing/api-modelling.mdx @@ -45,6 +45,11 @@ literal construction, record spread, and ordinary `:>` coercion, plus a positive 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. +Do not expose an unconstrained generic `Impl(T)` helper that converts arbitrary `T.t` to a native +base type with `%identity`. Keep a base conversion specialized to the concrete native receiver, or +use a runtime classifier when the source type is broad. Test the generic-functor route in packed +consumers as well as direct record construction. + ## Constructor overloads Constructors use a distinct naming convention: diff --git a/docs/content/docs/project-status.mdx b/docs/content/docs/project-status.mdx index 1b9b9ebd..c9fde0f3 100644 --- a/docs/content/docs/project-status.mdx +++ b/docs/content/docs/project-status.mdx @@ -47,6 +47,9 @@ The partial [native-instance inventory](https://github.com/rescript-lang/experim 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. +The inventory also tracks generic `Impl` upcasts that still accept arbitrary receiver types; these +are unresolved construction routes, so package-wide opacity must not be inferred from the converted +subset. 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 fad95971..8945c7d2 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 && node scripts/check-native-inventory.mjs", + "check:features": "node --test scripts/check-features.test.mjs scripts/native-inventory-validation.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/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", + "format": "rescript format && oxfmt ./tests/index.js ./tests/fixtures.json ./tests/browser/*.mjs ./scripts/negative-fixtures.json ./scripts/native-instance-inventory.json ./scripts/native-inventory-validation.mjs ./scripts/native-inventory-validation.test.mjs ./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/native-inventory-validation.mjs ./scripts/native-inventory-validation.test.mjs ./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-native-inventory.mjs b/scripts/check-native-inventory.mjs index ba4190cb..82523368 100644 --- a/scripts/check-native-inventory.mjs +++ b/scripts/check-native-inventory.mjs @@ -1,6 +1,7 @@ import { readFileSync } from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; +import { validateEntry, validateInventoryMetadata } from "./native-inventory-validation.mjs"; const scriptPath = fileURLToPath(import.meta.url); const repoRoot = path.resolve(path.dirname(scriptPath), ".."); @@ -9,31 +10,16 @@ const inventory = JSON.parse( ); 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}.`), - ]; -}); +const errors = [ + ...validateInventoryMetadata(inventory), + ...inventory.convertedNativeInstances.flatMap((entry) => + validateEntry(entry, { + backingSource: source(entry.backingFile), + publicSource: source(entry.publicFile), + }), + ), +]; if (errors.length > 0) { console.error(errors.join("\n")); diff --git a/scripts/consumer-concrete-types.res b/scripts/consumer-concrete-types.res index bf4ecb74..6ee02a69 100644 --- a/scripts/consumer-concrete-types.res +++ b/scripts/consumer-concrete-types.res @@ -104,3 +104,24 @@ let makePublicUrl = () => { WebAPI.URL.setHref(url, "https://example.com") WebAPI.URL.getHref(url) } + +let makePublicCanvas = () => { + let document = WebAPI.DOM.window->WebAPI.Window.document + let element = document->WebAPI.Document.createElement("canvas") + switch element->WebAPI.HTMLCanvasElement.classify { + | Some(canvas) => { + WebAPI.HTMLCanvasElement.setWidth(canvas, 320) + WebAPI.HTMLCanvasElement.setHeight(canvas, 200) + (WebAPI.HTMLCanvasElement.width(canvas), WebAPI.HTMLCanvasElement.height(canvas)) + } + | None => (0, 0) + } +} + +let makePublicBlob = () => { + let textPart = WebAPI.Blob.blobPartFromString("text") + let sourceBlob = WebAPI.Blob.make(~blobParts=[textPart]) + let blobPart = WebAPI.Blob.blobPartFromBlob(sourceBlob) + let composed = WebAPI.Blob.make(~blobParts=[blobPart]) + WebAPI.Blob.size(composed) +} diff --git a/scripts/fixtures/negative/CanvasTypeCannotBeSpread.res b/scripts/fixtures/negative/CanvasTypeCannotBeSpread.res new file mode 100644 index 00000000..93a42421 --- /dev/null +++ b/scripts/fixtures/negative/CanvasTypeCannotBeSpread.res @@ -0,0 +1 @@ +type canvasShape = {...WebAPI.HTMLCanvasElement.t} diff --git a/scripts/fixtures/negative/ElementImplCannotForgeNativeType.res b/scripts/fixtures/negative/ElementImplCannotForgeNativeType.res new file mode 100644 index 00000000..032fa3f5 --- /dev/null +++ b/scripts/fixtures/negative/ElementImplCannotForgeNativeType.res @@ -0,0 +1,2 @@ +module FakeElement = WebAPI.Element.Impl({type t = string}) +let forged: WebAPI.Element.t = FakeElement.asElement("not an element") diff --git a/scripts/fixtures/negative/EventImplCannotForgeNativeType.res b/scripts/fixtures/negative/EventImplCannotForgeNativeType.res new file mode 100644 index 00000000..ab09a1e2 --- /dev/null +++ b/scripts/fixtures/negative/EventImplCannotForgeNativeType.res @@ -0,0 +1,2 @@ +module FakeEvent = WebAPI.Event.Impl({type t = string}) +let forged: WebAPI.DOM.event = FakeEvent.asEvent("not an event") diff --git a/scripts/fixtures/negative/EventTargetImplCannotForgeNativeType.res b/scripts/fixtures/negative/EventTargetImplCannotForgeNativeType.res new file mode 100644 index 00000000..f0b7eb78 --- /dev/null +++ b/scripts/fixtures/negative/EventTargetImplCannotForgeNativeType.res @@ -0,0 +1,2 @@ +module FakeEventTarget = WebAPI.EventTarget.Impl({type t = string}) +let forged: WebAPI.DOM.eventTarget = FakeEventTarget.asEventTarget("not a target") diff --git a/scripts/fixtures/negative/ExtendableEventImplCannotForgeNativeType.res b/scripts/fixtures/negative/ExtendableEventImplCannotForgeNativeType.res new file mode 100644 index 00000000..0657fae0 --- /dev/null +++ b/scripts/fixtures/negative/ExtendableEventImplCannotForgeNativeType.res @@ -0,0 +1,2 @@ +module FakeExtendableEvent = WebAPI.ExtendableEvent.Impl({type t = string}) +let forged: WebAPI.DOM.extendableEvent = FakeExtendableEvent.asExtendableEvent("not an event") diff --git a/scripts/fixtures/negative/HTMLElementImplCannotForgeNativeType.res b/scripts/fixtures/negative/HTMLElementImplCannotForgeNativeType.res new file mode 100644 index 00000000..a9a1842f --- /dev/null +++ b/scripts/fixtures/negative/HTMLElementImplCannotForgeNativeType.res @@ -0,0 +1,2 @@ +module FakeHTMLElement = WebAPI.HTMLElement.Impl({type t = string}) +let forged: WebAPI.HTMLElement.t = FakeHTMLElement.asHTMLElement("not an element") diff --git a/scripts/fixtures/negative/NodeImplCannotForgeNativeType.res b/scripts/fixtures/negative/NodeImplCannotForgeNativeType.res new file mode 100644 index 00000000..6ecb41dc --- /dev/null +++ b/scripts/fixtures/negative/NodeImplCannotForgeNativeType.res @@ -0,0 +1,2 @@ +module FakeNode = WebAPI.Node.Impl({type t = string}) +let forged: WebAPI.Node.t = FakeNode.asNode("not a node") diff --git a/scripts/fixtures/negative/RequestRecordCannotCoerce.res b/scripts/fixtures/negative/RequestRecordCannotCoerce.res new file mode 100644 index 00000000..f6e0d005 --- /dev/null +++ b/scripts/fixtures/negative/RequestRecordCannotCoerce.res @@ -0,0 +1,37 @@ +type requestShape = { + method: string, + url: string, + headers: WebAPI.FetchTypes.headers, + destination: WebAPI.FetchTypes.requestDestination, + referrer: string, + referrerPolicy: WebAPI.FetchTypes.referrerPolicy, + mode: WebAPI.FetchTypes.requestMode, + credentials: WebAPI.FetchTypes.requestCredentials, + cache: WebAPI.FetchTypes.requestCache, + redirect: WebAPI.FetchTypes.requestRedirect, + integrity: string, + keepalive: bool, + signal: WebAPI.DOM.abortSignal, + body: Null.t>, + bodyUsed: bool, +} + +let original = WebAPI.Request.fromURL("https://example.com/") +let record: requestShape = { + method: WebAPI.Request.method(original), + url: WebAPI.Request.url(original), + headers: WebAPI.Request.headers(original), + destination: WebAPI.Request.destination(original), + referrer: WebAPI.Request.referrer(original), + referrerPolicy: WebAPI.Request.referrerPolicy(original), + mode: WebAPI.Request.mode(original), + credentials: WebAPI.Request.credentials(original), + cache: WebAPI.Request.cache(original), + redirect: WebAPI.Request.redirect(original), + integrity: WebAPI.Request.integrity(original), + keepalive: WebAPI.Request.keepalive(original), + signal: WebAPI.Request.signal(original), + body: WebAPI.Request.body(original), + bodyUsed: WebAPI.Request.bodyUsed(original), +} +let fake = (record :> WebAPI.Request.t) diff --git a/scripts/fixtures/negative/RequestTypeCannotBeSpread.res b/scripts/fixtures/negative/RequestTypeCannotBeSpread.res new file mode 100644 index 00000000..a6386132 --- /dev/null +++ b/scripts/fixtures/negative/RequestTypeCannotBeSpread.res @@ -0,0 +1 @@ +type requestShape = {...WebAPI.Request.t} diff --git a/scripts/fixtures/negative/ResponseBackingTypeCannotBeSpread.res b/scripts/fixtures/negative/ResponseBackingTypeCannotBeSpread.res new file mode 100644 index 00000000..08b0f41f --- /dev/null +++ b/scripts/fixtures/negative/ResponseBackingTypeCannotBeSpread.res @@ -0,0 +1 @@ +type responseShape = {...WebAPI.FetchTypes.response} diff --git a/scripts/native-instance-inventory.json b/scripts/native-instance-inventory.json index 8981762b..28a88daa 100644 --- a/scripts/native-instance-inventory.json +++ b/scripts/native-instance-inventory.json @@ -3,6 +3,7 @@ "status": "partial", "publicModuleCount": 291, "sourceFeatureGroupCount": 28, + "expectedConvertedNativeInstanceCount": 39, "convertedNativeInstances": [ { "name": "Response", @@ -11,17 +12,78 @@ "publicFile": "src/fetch/Response.res", "publicAlias": "type t = FetchTypes.response", "accessors": [ - "type_", - "url", - "redirected", - "status", - "ok", - "statusText", - "headers", - "body", - "bodyUsed" + { "name": "type_", "kind": "get", "property": "type" }, + { "name": "url", "kind": "get", "property": "url" }, + { "name": "redirected", "kind": "get", "property": "redirected" }, + { "name": "status", "kind": "get", "property": "status" }, + { "name": "ok", "kind": "get", "property": "ok" }, + { "name": "statusText", "kind": "get", "property": "statusText" }, + { "name": "headers", "kind": "get", "property": "headers" }, + { "name": "body", "kind": "get", "property": "body" }, + { "name": "bodyUsed", "kind": "get", "property": "bodyUsed" } ] }, + { + "name": "Request", + "backingFile": "src/fetch/FetchTypes.res", + "backingType": "request", + "publicFile": "src/fetch/Request.res", + "publicAlias": "type t = FetchTypes.request", + "accessors": [ + { "name": "method", "kind": "get", "property": "method" }, + { "name": "url", "kind": "get", "property": "url" }, + { "name": "headers", "kind": "get", "property": "headers" }, + { "name": "destination", "kind": "get", "property": "destination" }, + { "name": "referrer", "kind": "get", "property": "referrer" }, + { "name": "referrerPolicy", "kind": "get", "property": "referrerPolicy" }, + { "name": "mode", "kind": "get", "property": "mode" }, + { "name": "credentials", "kind": "get", "property": "credentials" }, + { "name": "cache", "kind": "get", "property": "cache" }, + { "name": "redirect", "kind": "get", "property": "redirect" }, + { "name": "integrity", "kind": "get", "property": "integrity" }, + { "name": "keepalive", "kind": "get", "property": "keepalive" }, + { "name": "signal", "kind": "get", "property": "signal" }, + { "name": "body", "kind": "get", "property": "body" }, + { "name": "bodyUsed", "kind": "get", "property": "bodyUsed" } + ] + }, + { + "name": "HTMLCanvasElement", + "backingFile": "src/canvas/HTMLCanvasElement.res", + "backingType": "t", + "publicFile": "src/canvas/HTMLCanvasElement.res", + "publicAlias": "type t", + "accessors": [ + { "name": "width", "kind": "get", "property": "width" }, + { "name": "setWidth", "kind": "set", "property": "width" }, + { "name": "height", "kind": "get", "property": "height" }, + { "name": "setHeight", "kind": "set", "property": "height" } + ] + }, + { + "name": "CSSStyleValue", + "backingFile": "src/cssom/CSSStyleValue.res", + "backingType": "t", + "publicFile": "src/cssom/CSSStyleValue.res", + "publicAlias": "type t", + "accessors": [] + }, + { + "name": "AnimationEffect", + "backingFile": "src/animation/AnimationEffect.res", + "backingType": "t", + "publicFile": "src/animation/AnimationEffect.res", + "publicAlias": "type t", + "accessors": [] + }, + { + "name": "XPathExpression", + "backingFile": "src/window/XPathExpression.res", + "backingType": "t", + "publicFile": "src/window/XPathExpression.res", + "publicAlias": "type t", + "accessors": [] + }, { "name": "Headers", "backingFile": "src/fetch/FetchTypes.res", @@ -38,6 +100,140 @@ "publicAlias": "type t = FetchTypes.formData", "accessors": [] }, + { + "name": "ReadableStream", + "backingFile": "src/file/FileTypes.res", + "backingType": "readableStream", + "publicFile": "src/file/ReadableStream.res", + "publicAlias": "type t<'r> = FileTypes.readableStream<'r>", + "accessors": [{ "name": "locked", "kind": "get", "property": "locked" }] + }, + { + "name": "WritableStream", + "backingFile": "src/file/FileTypes.res", + "backingType": "writableStream", + "publicFile": "src/file/WritableStream.res", + "publicAlias": "type t<'w> = FileTypes.writableStream<'w>", + "accessors": [{ "name": "locked", "kind": "get", "property": "locked" }] + }, + { + "name": "WritableStreamDefaultController", + "backingFile": "src/file/FileTypes.res", + "backingType": "writableStreamDefaultController", + "publicFile": "src/file/WritableStreamDefaultController.res", + "publicAlias": "type t = FileTypes.writableStreamDefaultController", + "accessors": [{ "name": "signal", "kind": "get", "property": "signal" }] + }, + { + "name": "FileSystemWritableFileStream", + "backingFile": "src/file/FileTypes.res", + "backingType": "fileSystemWritableFileStream", + "publicFile": "src/file/FileSystemWritableFileStream.res", + "publicAlias": "type t = FileTypes.fileSystemWritableFileStream", + "accessors": [] + }, + { + "name": "Blob", + "backingFile": "src/core/DOM.res", + "backingType": "blob", + "publicFile": "src/file/Blob.res", + "publicAlias": "type t = FileTypes.blob", + "accessors": [ + { "name": "size", "kind": "get", "property": "size" }, + { "name": "type_", "kind": "get", "property": "type" } + ] + }, + { + "name": "File", + "backingFile": "src/core/DOM.res", + "backingType": "file", + "publicFile": "src/file/File.res", + "publicAlias": "type t = FileTypes.file", + "accessors": [ + { "name": "size", "kind": "get", "property": "size" }, + { "name": "type_", "kind": "get", "property": "type" }, + { "name": "name", "kind": "get", "property": "name" }, + { "name": "lastModified", "kind": "get", "property": "lastModified" }, + { "name": "webkitRelativePath", "kind": "get", "property": "webkitRelativePath" } + ] + }, + { + "name": "FileList", + "backingFile": "src/core/DOM.res", + "backingType": "fileList", + "publicFile": "src/file/FileList.res", + "publicAlias": "type t = DOM.fileList", + "accessors": [{ "name": "length", "kind": "get", "property": "length" }] + }, + { + "name": "FileSystemEntry", + "backingFile": "src/file/BaseFileAndDirectoryEntries.res", + "backingType": "fileSystemEntry", + "publicFile": "src/file/FileSystemEntry.res", + "publicAlias": "type t = BaseFileAndDirectoryEntries.fileSystemEntry", + "accessors": [ + { "name": "isFile", "kind": "get", "property": "isFile" }, + { "name": "isDirectory", "kind": "get", "property": "isDirectory" }, + { "name": "name", "kind": "get", "property": "name" }, + { "name": "fullPath", "kind": "get", "property": "fullPath" }, + { "name": "filesystem", "kind": "get", "property": "filesystem" } + ] + }, + { + "name": "FileSystemDirectoryEntry", + "backingFile": "src/file/BaseFileAndDirectoryEntries.res", + "backingType": "fileSystemDirectoryEntry", + "publicFile": "src/file/FileSystemDirectoryEntry.res", + "publicAlias": "type t = BaseFileAndDirectoryEntries.fileSystemDirectoryEntry", + "accessors": [ + { "name": "isFile", "kind": "get", "property": "isFile" }, + { "name": "isDirectory", "kind": "get", "property": "isDirectory" }, + { "name": "name", "kind": "get", "property": "name" }, + { "name": "fullPath", "kind": "get", "property": "fullPath" }, + { "name": "filesystem", "kind": "get", "property": "filesystem" } + ] + }, + { + "name": "FileSystemDirectoryReader", + "backingFile": "src/file/FileAndDirectoryEntriesTypes.res", + "backingType": "fileSystemDirectoryReader", + "publicFile": "src/file/FileSystemDirectoryReader.res", + "publicAlias": "type t = FileAndDirectoryEntriesTypes.fileSystemDirectoryReader", + "accessors": [] + }, + { + "name": "FileSystemHandle", + "backingFile": "src/file/FileTypes.res", + "backingType": "fileSystemHandle", + "publicFile": "src/file/FileSystemHandle.res", + "publicAlias": "type t = FileTypes.fileSystemHandle", + "accessors": [ + { "name": "kind", "kind": "get", "property": "kind" }, + { "name": "name", "kind": "get", "property": "name" } + ] + }, + { + "name": "FileSystemDirectoryHandle", + "backingFile": "src/file/FileTypes.res", + "backingType": "fileSystemDirectoryHandle", + "publicFile": "src/file/FileSystemDirectoryHandle.res", + "publicAlias": "type t = FileTypes.fileSystemDirectoryHandle", + "accessors": [ + { "name": "kind", "kind": "get", "property": "kind" }, + { "name": "name", "kind": "get", "property": "name" } + ] + }, + { + "name": "FileSystemFileHandle", + "backingFile": "src/file/FileTypes.res", + "backingType": "fileSystemFileHandle", + "publicFile": "src/file/FileSystemFileHandle.res", + "publicAlias": "type t = FileTypes.fileSystemFileHandle", + "accessors": [ + { "name": "kind", "kind": "get", "property": "kind" }, + { "name": "name", "kind": "get", "property": "name" } + ] + }, { "name": "URL", "backingFile": "src/url/UrlTypes.res", @@ -45,28 +241,28 @@ "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": "getHref", "kind": "get", "property": "href" }, + { "name": "setHref", "kind": "set", "property": "href" }, + { "name": "getOrigin", "kind": "get", "property": "origin" }, + { "name": "getProtocol", "kind": "get", "property": "protocol" }, + { "name": "setProtocol", "kind": "set", "property": "protocol" }, + { "name": "getUsername", "kind": "get", "property": "username" }, + { "name": "setUsername", "kind": "set", "property": "username" }, + { "name": "getPassword", "kind": "get", "property": "password" }, + { "name": "setPassword", "kind": "set", "property": "password" }, + { "name": "getHost", "kind": "get", "property": "host" }, + { "name": "setHost", "kind": "set", "property": "host" }, + { "name": "getHostname", "kind": "get", "property": "hostname" }, + { "name": "setHostname", "kind": "set", "property": "hostname" }, + { "name": "getPort", "kind": "get", "property": "port" }, + { "name": "setPort", "kind": "set", "property": "port" }, + { "name": "getPathname", "kind": "get", "property": "pathname" }, + { "name": "setPathname", "kind": "set", "property": "pathname" }, + { "name": "getSearch", "kind": "get", "property": "search" }, + { "name": "setSearch", "kind": "set", "property": "search" }, + { "name": "getHash", "kind": "get", "property": "hash" }, + { "name": "setHash", "kind": "set", "property": "hash" }, + { "name": "getSearchParams", "kind": "get", "property": "searchParams" } ] }, { @@ -75,7 +271,14 @@ "backingType": "crypto", "publicFile": "src/crypto/Crypto.res", "publicAlias": "type t = WebCryptoTypes.crypto", - "accessors": ["subtle"] + "accessors": [ + { + "name": "subtle", + "kind": "alias", + "property": "subtle", + "backingAccessor": "cryptoSubtle" + } + ] }, { "name": "SubtleCrypto", @@ -92,10 +295,10 @@ "publicFile": "src/crypto/SubtleCrypto.res", "publicAlias": "type cryptoKey = WebCryptoTypes.cryptoKey", "accessors": [ - "cryptoKeyType", - "cryptoKeyExtractable", - "cryptoKeyAlgorithm", - "cryptoKeyUsages" + { "name": "cryptoKeyType", "kind": "get", "property": "type" }, + { "name": "cryptoKeyExtractable", "kind": "get", "property": "extractable" }, + { "name": "cryptoKeyAlgorithm", "kind": "get", "property": "algorithm" }, + { "name": "cryptoKeyUsages", "kind": "get", "property": "usages" } ] }, { @@ -104,7 +307,10 @@ "backingType": "domException", "publicFile": "src/dom-platform/DOMException.res", "publicAlias": "type t = DOM.domException", - "accessors": ["name", "message"] + "accessors": [ + { "name": "name", "kind": "get", "property": "name" }, + { "name": "message", "kind": "get", "property": "message" } + ] }, { "name": "DOMStringList", @@ -112,7 +318,7 @@ "backingType": "domStringList", "publicFile": "src/dom-platform/DOMStringList.res", "publicAlias": "type t = DOM.domStringList", - "accessors": ["length"] + "accessors": [{ "name": "length", "kind": "get", "property": "length" }] }, { "name": "DOMTokenList", @@ -120,7 +326,11 @@ "backingType": "domTokenList", "publicFile": "src/dom-platform/DOMTokenList.res", "publicAlias": "type t = DOM.domTokenList", - "accessors": ["length", "value", "setValue"] + "accessors": [ + { "name": "length", "kind": "get", "property": "length" }, + { "name": "value", "kind": "get", "property": "value" }, + { "name": "setValue", "kind": "set", "property": "value" } + ] }, { "name": "DOMStringMap", @@ -169,23 +379,75 @@ "publicFile": "src/workers/NavigationPreloadManager.res", "publicAlias": "type t = ServiceWorkerTypes.navigationPreloadManager", "accessors": [] + }, + { + "name": "PushEvent", + "backingFile": "src/messaging/PushTypes.res", + "backingType": "pushEvent", + "publicFile": "src/messaging/PushEvent.res", + "publicAlias": "type t = PushTypes.pushEvent", + "accessors": [{ "name": "data", "kind": "get", "property": "data" }] + }, + { + "name": "Document", + "backingFile": "src/core/DOM.res", + "backingType": "document", + "publicFile": "src/core/DOM.res", + "publicAlias": "type document", + "accessors": [] + }, + { + "name": "Window", + "backingFile": "src/core/DOM.res", + "backingType": "window", + "publicFile": "src/window/Window.res", + "publicAlias": "type t = DOM.window", + "accessors": [] + }, + { + "name": "CustomElementRegistry", + "backingFile": "src/core/DOM.res", + "backingType": "customElementRegistry", + "publicFile": "src/window/CustomElementRegistry.res", + "publicAlias": "type t = DOM.customElementRegistry", + "accessors": [] + }, + { + "name": "AbortController", + "backingFile": "src/core/DOM.res", + "backingType": "abortController", + "publicFile": "src/event/AbortController.res", + "publicAlias": "type t = DOM.abortController", + "accessors": [{ "name": "signal", "kind": "get", "property": "signal" }] } ], "constructibleDataExceptions": [ "src/fetch/FetchTypes.res:requestInit", "src/fetch/FetchTypes.res:responseInit", + "src/file/FileTypes.res:blobPart (built with Blob.blobPartFromString/Blob.blobPartFromBlob)", "src/crypto/WebCryptoTypes.res:cryptoKeyPair", "src/crypto/WebCryptoTypes.res:keyAlgorithm", "src/crypto/WebCryptoTypes.res:algorithm" ], + "dataResultUnionModels": [ + "src/fetch/FetchTypes.res:formDataEntryValue is abstract and decoded with FormDataEntryValue.decode because the compiler cannot unbox an abstract File payload" + ], "unresolvedUnknownBackedHandles": [ "stream reader and writer types", "worker context/source types modeled as unknown" ], + "knownUncheckedPublicGenericImplConversions": [ + "DomHTMLMediaElement.Impl.asHTMLMediaElement", + "MouseEvent.Impl.asMouseEvent", + "UIEvent.Impl.asUIEvent", + "AudioNode.Impl.asAudioNode", + "AudioScheduledSourceNode.Impl.asAudioScheduledSourceNode", + "BaseAudioContext.Impl.asBaseAudioContext" + ], "unconvertedNativeGroups": [ "DOMTree and DOM nodes", "HTML, SVG, Canvas", - "Fetch Request, File/Streams", + "File/Streams", "remaining Storage/IDB types, Workers other than NavigationPreloadManager, Messaging", "CSSOM/Geometry/Animation", "Media/WebAudio/Device/Performance/Navigator/Observers" diff --git a/scripts/native-inventory-validation.mjs b/scripts/native-inventory-validation.mjs new file mode 100644 index 00000000..ffe3fa98 --- /dev/null +++ b/scripts/native-inventory-validation.mjs @@ -0,0 +1,180 @@ +const expectedInventory = { + revision: "4888db2cc601ad568b76af6ed2add60da0ad96cb", + status: "partial", + publicModuleCount: 291, + sourceFeatureGroupCount: 28, + convertedCount: 39, + names: [ + "Response", + "Request", + "HTMLCanvasElement", + "CSSStyleValue", + "AnimationEffect", + "XPathExpression", + "Headers", + "FormData", + "ReadableStream", + "WritableStream", + "WritableStreamDefaultController", + "FileSystemWritableFileStream", + "URL", + "Crypto", + "SubtleCrypto", + "CryptoKey", + "DOMException", + "DOMStringList", + "DOMTokenList", + "DOMStringMap", + "Cache", + "CacheStorage", + "IDBFactory", + "StorageManager", + "NavigationPreloadManager", + "PushEvent", + "Document", + "Window", + "CustomElementRegistry", + "AbortController", + "Blob", + "File", + "FileList", + "FileSystemEntry", + "FileSystemDirectoryEntry", + "FileSystemDirectoryReader", + "FileSystemHandle", + "FileSystemDirectoryHandle", + "FileSystemFileHandle", + ], +}; + +const escaped = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + +export const stripComments = (text) => + text + .replace(/\/\*[\s\S]*?\*\//g, (comment) => comment.replace(/[^\r\n]/g, " ")) + .replace(/\/\/[^\r\n]*/g, ""); + +export const hasAbstractDeclaration = (source, typeName) => { + const code = stripComments(source); + const declaration = new RegExp( + `^[ \\t]*(?:@[A-Za-z][A-Za-z0-9_.]*(?:\\([^\\r\\n]*\\))?[ \\t]+)?(?:type(?:[ \\t]+rec)?|and)[ \\t]+${escaped(typeName)}(?:<[^>]+>)?[ \\t]*(?:\\r?\\n|$)`, + "gm", + ); + const matches = [...code.matchAll(declaration)]; + if (matches.length !== 1) return false; + + const rest = code.slice(matches[0].index + matches[0][0].length).trimStart(); + return !/^(?:=|\{|\|)/.test(rest); +}; + +export const hasExactAlias = (source, alias) => { + const match = + /^type ([A-Za-z][A-Za-z0-9_]*(?:<[^>]+>)?) = ([A-Za-z][A-Za-z0-9_.]*(?:<[^>]+>)?)$/.exec(alias); + if (match === null) { + const abstractType = /^type ([A-Za-z][A-Za-z0-9_]*)$/.exec(alias); + return abstractType !== null && hasAbstractDeclaration(source, abstractType[1]); + } + + const [, typeName, target] = match; + const declaration = new RegExp( + `^[ \\t]*type[ \\t]+${escaped(typeName)}[ \\t]*=[ \\t]*${escaped(target)}[ \\t]*\\r?$`, + "m", + ); + return declaration.test(stripComments(source)); +}; + +const hasNativeAccessor = (source, { name, kind, property }) => { + const lines = stripComments(source).split(/\r?\n/); + const declarationPattern = new RegExp( + `^[ \\t]*(?:.*[ \\t])?external[ \\t]+${escaped(name)}[ \\t]*:`, + ); + + for (const [index, line] of lines.entries()) { + if (!declarationPattern.test(line)) continue; + + const previousNonemptyLine = lines + .slice(0, index) + .reverse() + .find((candidate) => candidate.trim().length > 0); + const annotationSource = [previousNonemptyLine, line].filter(Boolean).join("\n"); + return ( + annotationSource.includes(`@${kind}`) && + new RegExp(`=[ \\t]*"${escaped(property)}"`).test(line) + ); + } + return false; +}; + +const hasAliasAccessor = (publicSource, backingSource, accessor) => { + const alias = new RegExp( + `^[ \\t]*let[ \\t]+${escaped(accessor.name)}[ \\t]*=[ \\t]*WebCryptoTypes\\.${escaped(accessor.backingAccessor)}[ \\t]*$`, + "m", + ); + return ( + alias.test(stripComments(publicSource)) && + hasNativeAccessor(backingSource, { + name: accessor.backingAccessor, + kind: "get", + property: accessor.property, + }) + ); +}; + +export const validateInventoryMetadata = (inventory) => { + const names = inventory.convertedNativeInstances.map(({ name }) => name); + return [ + ...(inventory.revision === expectedInventory.revision ? [] : ["Inventory revision changed."]), + ...(inventory.status === expectedInventory.status + ? [] + : ["Inventory must remain explicitly partial."]), + ...(inventory.publicModuleCount === expectedInventory.publicModuleCount + ? [] + : [`Expected ${expectedInventory.publicModuleCount} public modules.`]), + ...(inventory.sourceFeatureGroupCount === expectedInventory.sourceFeatureGroupCount + ? [] + : [`Expected ${expectedInventory.sourceFeatureGroupCount} source groups.`]), + ...(inventory.expectedConvertedNativeInstanceCount === expectedInventory.convertedCount && + names.length === expectedInventory.convertedCount + ? [] + : [`Expected exactly ${expectedInventory.convertedCount} converted native identities.`]), + ...(new Set(names).size === names.length ? [] : ["Inventory contains duplicate names."]), + ...expectedInventory.names + .filter((name) => !names.includes(name)) + .map((name) => `Inventory is missing converted native identity ${name}.`), + ...(inventory.constructibleDataExceptions?.length > 0 + ? [] + : ["Inventory must record constructible data exceptions."]), + ...(inventory.dataResultUnionModels?.length > 0 + ? [] + : ["Inventory must record result unions that need a separate opaque model."]), + ...(inventory.unresolvedUnknownBackedHandles?.length > 0 + ? [] + : ["Inventory must record unresolved unknown-backed handles."]), + ...(inventory.knownUncheckedPublicGenericImplConversions?.length > 0 + ? [] + : [ + "Inventory must record unchecked public generic conversions while the migration is partial.", + ]), + ...(inventory.unconvertedNativeGroups?.length > 0 + ? [] + : ["Inventory must record unconverted native groups."]), + ]; +}; + +export const validateEntry = (entry, { backingSource, publicSource }) => [ + ...(!hasAbstractDeclaration(backingSource, entry.backingType) + ? [`${entry.name} backing type ${entry.backingType} must remain abstract.`] + : []), + ...(!hasExactAlias(publicSource, entry.publicAlias) + ? [`${entry.name} must retain exact alias ${entry.publicAlias}.`] + : []), + ...entry.accessors.flatMap((accessor) => { + const exists = + accessor.kind === "alias" + ? hasAliasAccessor(publicSource, backingSource, accessor) + : hasNativeAccessor(publicSource, accessor); + return exists + ? [] + : [`${entry.name} accessor ${accessor.name} is missing or has the wrong property binding.`]; + }), +]; diff --git a/scripts/native-inventory-validation.test.mjs b/scripts/native-inventory-validation.test.mjs new file mode 100644 index 00000000..096cb5e6 --- /dev/null +++ b/scripts/native-inventory-validation.test.mjs @@ -0,0 +1,157 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { + hasAbstractDeclaration, + hasExactAlias, + validateEntry, + validateInventoryMetadata, +} from "./native-inventory-validation.mjs"; + +test("accepts an abstract type declaration", () => { + assert.equal( + hasAbstractDeclaration("@editor.completeFrom(Response)\ntype response\n", "response"), + true, + ); +}); + +test("rejects inline and multiline type representations", () => { + assert.equal(hasAbstractDeclaration("type response = {status: int}", "response"), false); + assert.equal(hasAbstractDeclaration("type response\n = {status: int}", "response"), false); +}); + +test("ignores abstract declarations inside comments", () => { + assert.equal( + hasAbstractDeclaration("/* type response */\ntype response = private {}", "response"), + false, + ); +}); + +test("requires an exact, uncommented public type alias", () => { + assert.equal( + hasExactAlias("type t = FetchTypes.response\n", "type t = FetchTypes.response"), + true, + ); + assert.equal( + hasExactAlias("type t = FetchTypes.responseInit\n", "type t = FetchTypes.response"), + false, + ); + assert.equal( + hasExactAlias("/* type t = FetchTypes.response */\ntype t\n", "type t = FetchTypes.response"), + false, + ); +}); + +test("checks accessor declarations, annotation kinds, and native property mappings", () => { + const entry = { + name: "Response", + backingType: "response", + publicAlias: "type t = FetchTypes.response", + accessors: [{ name: "status", kind: "get", property: "status" }], + }; + const validSource = ` +type t = FetchTypes.response +/** + * /Response/status + */ +@get external status: t => int = "status" +`; + assert.deepEqual( + validateEntry(entry, { + backingSource: "type response\n", + publicSource: validSource, + }), + [], + ); + assert.notDeepEqual( + validateEntry(entry, { + backingSource: "type response\n", + publicSource: "type t = FetchTypes.response\n/** /Response/status */\n", + }), + [], + ); + assert.notDeepEqual( + validateEntry(entry, { + backingSource: "type response\n", + publicSource: + 'type t = FetchTypes.response\n@get external status: t => string = "statusText"\n', + }), + [], + ); +}); + +test("prevents inventory metadata from silently shrinking or declaring completion", () => { + assert.deepEqual( + validateInventoryMetadata({ + revision: "4888db2cc601ad568b76af6ed2add60da0ad96cb", + status: "partial", + publicModuleCount: 291, + sourceFeatureGroupCount: 28, + expectedConvertedNativeInstanceCount: 39, + convertedNativeInstances: Array.from({ length: 39 }, (_, index) => ({ + name: [ + "Response", + "Request", + "HTMLCanvasElement", + "CSSStyleValue", + "AnimationEffect", + "XPathExpression", + "Headers", + "FormData", + "ReadableStream", + "WritableStream", + "WritableStreamDefaultController", + "FileSystemWritableFileStream", + "URL", + "Crypto", + "SubtleCrypto", + "CryptoKey", + "DOMException", + "DOMStringList", + "DOMTokenList", + "DOMStringMap", + "Cache", + "CacheStorage", + "IDBFactory", + "StorageManager", + "NavigationPreloadManager", + "PushEvent", + "Document", + "Window", + "CustomElementRegistry", + "AbortController", + "Blob", + "File", + "FileList", + "FileSystemEntry", + "FileSystemDirectoryEntry", + "FileSystemDirectoryReader", + "FileSystemHandle", + "FileSystemDirectoryHandle", + "FileSystemFileHandle", + ][index], + })), + constructibleDataExceptions: ["responseInit"], + dataResultUnionModels: ["formDataEntryValue"], + unresolvedUnknownBackedHandles: ["reader"], + knownUncheckedPublicGenericImplConversions: ["Node.Impl.asNode"], + unconvertedNativeGroups: ["DOMTree"], + }), + [], + ); + assert.notDeepEqual( + validateInventoryMetadata({ + revision: "4888db2cc601ad568b76af6ed2add60da0ad96cb", + status: "complete", + publicModuleCount: 0, + sourceFeatureGroupCount: 0, + expectedConvertedNativeInstanceCount: 0, + convertedNativeInstances: [], + constructibleDataExceptions: [], + dataResultUnionModels: [], + unresolvedUnknownBackedHandles: [], + knownUncheckedPublicGenericImplConversions: [], + unconvertedNativeGroups: [], + }), + [], + ); +}); diff --git a/scripts/negative-fixtures.json b/scripts/negative-fixtures.json index 341da188..2c46e64b 100644 --- a/scripts/negative-fixtures.json +++ b/scripts/negative-fixtures.json @@ -29,6 +29,12 @@ "features": ["WebAPI.Fetch"], "expectedDiagnostic": "is not a subtype of WebAPI.Response.t" }, + { + "name": "Request cannot be made from a complete ordinary record coercion", + "fixture": "scripts/fixtures/negative/RequestRecordCannotCoerce.res", + "features": ["WebAPI.Fetch"], + "expectedDiagnostic": "is not a subtype of WebAPI.Request.t" + }, { "name": "Response cannot be constructed as an ordinary record literal", "fixture": "scripts/fixtures/negative/ResponseLiteralCannotConstruct.res", @@ -41,6 +47,60 @@ "features": ["WebAPI.Fetch"], "expectedDiagnostic": "is not an object type" }, + { + "name": "Response backing type has no record representation to spread", + "fixture": "scripts/fixtures/negative/ResponseBackingTypeCannotBeSpread.res", + "features": ["WebAPI.Fetch"], + "expectedDiagnostic": "is not an object type" + }, + { + "name": "Request has no record representation to spread", + "fixture": "scripts/fixtures/negative/RequestTypeCannotBeSpread.res", + "features": ["WebAPI.Fetch"], + "expectedDiagnostic": "is not an object type" + }, + { + "name": "Canvas has no record representation to spread", + "fixture": "scripts/fixtures/negative/CanvasTypeCannotBeSpread.res", + "features": ["WebAPI.Canvas"], + "expectedDiagnostic": "is not an object type" + }, + { + "name": "Node generic implementation cannot manufacture native instances", + "fixture": "scripts/fixtures/negative/NodeImplCannotForgeNativeType.res", + "features": ["WebAPI.DOMNodes"], + "expectedDiagnostic": "The value asNode can't be found" + }, + { + "name": "Element generic implementation cannot manufacture native instances", + "fixture": "scripts/fixtures/negative/ElementImplCannotForgeNativeType.res", + "features": ["WebAPI.DOMNodes"], + "expectedDiagnostic": "The value asElement can't be found" + }, + { + "name": "HTMLElement generic implementation cannot manufacture native instances", + "fixture": "scripts/fixtures/negative/HTMLElementImplCannotForgeNativeType.res", + "features": ["WebAPI.HTML"], + "expectedDiagnostic": "The value asHTMLElement can't be found" + }, + { + "name": "Event generic implementation cannot manufacture native instances", + "fixture": "scripts/fixtures/negative/EventImplCannotForgeNativeType.res", + "features": ["WebAPI.Event"], + "expectedDiagnostic": "The value asEvent can't be found" + }, + { + "name": "EventTarget generic implementation cannot manufacture native instances", + "fixture": "scripts/fixtures/negative/EventTargetImplCannotForgeNativeType.res", + "features": ["WebAPI.Event"], + "expectedDiagnostic": "The value asEventTarget can't be found" + }, + { + "name": "ExtendableEvent generic implementation cannot manufacture native instances", + "fixture": "scripts/fixtures/negative/ExtendableEventImplCannotForgeNativeType.res", + "features": ["WebAPI.Event"], + "expectedDiagnostic": "The value asExtendableEvent can't be found" + }, { "name": "Response factory returns an opaque instance", "fixture": "scripts/fixtures/negative/ResponseInferredReturnHasNoFields.res", diff --git a/src/animation/AnimationEffect.res b/src/animation/AnimationEffect.res index 7cc89dd7..a35b6392 100644 --- a/src/animation/AnimationEffect.res +++ b/src/animation/AnimationEffect.res @@ -1,7 +1,7 @@ /** [See AnimationEffect on MDN](https://developer.mozilla.org/docs/Web/API/AnimationEffect) */ -type t = private {} +type t type fillMode = | @as("auto") Auto diff --git a/src/canvas/HTMLCanvasElement.res b/src/canvas/HTMLCanvasElement.res index a60d303d..27e531e9 100644 --- a/src/canvas/HTMLCanvasElement.res +++ b/src/canvas/HTMLCanvasElement.res @@ -1,21 +1,29 @@ /** Provides properties and methods for manipulating the layout and presentation of elements. The HTMLCanvasElement interface also inherits the properties and methods of the HTMLElement interface. [See HTMLCanvasElement on MDN](https://developer.mozilla.org/docs/Web/API/HTMLCanvasElement) -TODO: mark as private once mutating fields of private records is allowed */ -type t = { - ...DOMTree.htmlElement, - /** - Gets or sets the width of a canvas element on a document. - [Read more on MDN](https://developer.mozilla.org/docs/Web/API/HTMLCanvasElement/width) - */ - mutable width: int, - /** - Gets or sets the height of a canvas element on a document. - [Read more on MDN](https://developer.mozilla.org/docs/Web/API/HTMLCanvasElement/height) - */ - mutable height: int, -} +type t + +/** +Gets or sets the width of a canvas element on a document. +[Read more on MDN](https://developer.mozilla.org/docs/Web/API/HTMLCanvasElement/width) +*/ +@get external width: t => int = "width" +/** +Gets or sets the width of a canvas element on a document. +[Read more on MDN](https://developer.mozilla.org/docs/Web/API/HTMLCanvasElement/width) +*/ +@set external setWidth: (t, int) => unit = "width" +/** +Gets or sets the height of a canvas element on a document. +[Read more on MDN](https://developer.mozilla.org/docs/Web/API/HTMLCanvasElement/height) +*/ +@get external height: t => int = "height" +/** +Gets or sets the height of a canvas element on a document. +[Read more on MDN](https://developer.mozilla.org/docs/Web/API/HTMLCanvasElement/height) +*/ +@set external setHeight: (t, int) => unit = "height" include HTMLElement.Impl({type t = t}) diff --git a/src/core/DOM.res b/src/core/DOM.res index e33ab7eb..c5b75f62 100644 --- a/src/core/DOM.res +++ b/src/core/DOM.res @@ -4,7 +4,7 @@ Any web page loaded in the browser that serves as an entry point into the page's DOM tree. [See Document on MDN](https://developer.mozilla.org/docs/Web/API/Document) */ -type document = private {} +type document /** An abnormal event (called an exception) which occurs as a result of calling a method or accessing a property of a web API. @@ -21,7 +21,7 @@ A type returned by some APIs which contains a list of DOMString (strings). type domStringList @editor.completeFrom(Window) -type window = private {} +type window external window: window = "window" type autoFillBase = @@ -159,22 +159,13 @@ type predefinedColorSpace = A file-like object of immutable, raw data. [See Blob on MDN](https://developer.mozilla.org/docs/Web/API/Blob) */ -type blob = private { - size: int, - @as("type") - type_: string, -} +type blob /** Provides information about a file selected by or available to the user. [See File on MDN](https://developer.mozilla.org/docs/Web/API/File) */ -type file = private { - ...blob, - name: string, - lastModified: int, - webkitRelativePath: string, -} +type file type shareData = { mutable files?: array, @@ -216,7 +207,7 @@ type fragmentDirective = {} [See CustomElementRegistry on MDN](https://developer.mozilla.org/docs/Web/API/CustomElementRegistry) */ @editor.completeFrom(CustomElementRegistry) -type customElementRegistry = private {} +type customElementRegistry /** [See BarProp on MDN](https://developer.mozilla.org/docs/Web/API/BarProp) @@ -314,13 +305,7 @@ A controller object that allows you to abort one or more WebApiDOM requests as a [See AbortController on MDN](https://developer.mozilla.org/docs/Web/API/AbortController) */ @editor.completeFrom(AbortController) -type rec abortController = private { - /** -Returns the AbortSignal object associated with this object. -[Read more on MDN](https://developer.mozilla.org/docs/Web/API/AbortController/signal) -*/ - signal: abortSignal, -} +type abortController /** A signal object that allows you to communicate with a WebApiDOM request (such as a WebApiFetch) and abort it if required via an AbortController object. [See AbortSignal on MDN](https://developer.mozilla.org/docs/Web/API/AbortSignal) @@ -556,12 +541,7 @@ An object of this type is returned by the files property of the HTML ele [See FileList on MDN](https://developer.mozilla.org/docs/Web/API/FileList) */ @editor.completeFrom(FileList) -type fileList = private { - /** - [Read more on MDN](https://developer.mozilla.org/docs/Web/API/FileList/length) - */ - length: int, -} +type fileList /** A collection of Attr objects. Objects inside a NamedNodeMap are not in any particular order, unlike NodeList, although they may be accessed by an index as in an array. diff --git a/src/cssom/CSSStyleValue.res b/src/cssom/CSSStyleValue.res index 895dc1a3..5fa86f0f 100644 --- a/src/cssom/CSSStyleValue.res +++ b/src/cssom/CSSStyleValue.res @@ -2,7 +2,7 @@ [See CSSStyleValue on MDN](https://developer.mozilla.org/docs/Web/API/CSSStyleValue) */ @editor.completeFrom(CSSStyleValue) -type t = private {} +type t /** [Read more on MDN](https://developer.mozilla.org/docs/Web/API/CSSStyleValue/parse_static) diff --git a/src/dom-nodes/CharacterData.res b/src/dom-nodes/CharacterData.res index e9ec7107..709067ab 100644 --- a/src/dom-nodes/CharacterData.res +++ b/src/dom-nodes/CharacterData.res @@ -102,8 +102,6 @@ module Impl = ( ) => { include Node.Impl({type t = T.t}) - external asCharacterData: T.t => t = "%identity" - /** Inserts nodes just after node, while replacing strings in nodes with equivalent Text nodes. @@ -197,3 +195,6 @@ Throws a "HierarchyRequestError" DOMException if the constraints of the node tre } include Impl({type t = t}) + +external asNode: t => DOMTree.node = "%identity" +external asCharacterData: t => t = "%identity" diff --git a/src/dom-nodes/DOMImplementation.res b/src/dom-nodes/DOMImplementation.res index 40b46a92..ae20c3b4 100644 --- a/src/dom-nodes/DOMImplementation.res +++ b/src/dom-nodes/DOMImplementation.res @@ -98,9 +98,7 @@ type documentType = { An XML document. It inherits from the generic Document and does not add any specific methods or properties to it: nevertheless, several algorithms behave differently with the two types of documents. [See XMLDocument on MDN](https://developer.mozilla.org/docs/Web/API/XMLDocument) */ -type xmlDocument = { - ...DOM.document, -} +type xmlDocument = DOM.document /** [Read more on MDN](https://developer.mozilla.org/docs/Web/API/DOMImplementation/createDocumentType) diff --git a/src/dom-nodes/DocumentFragment.res b/src/dom-nodes/DocumentFragment.res index 28141d7d..f217e991 100644 --- a/src/dom-nodes/DocumentFragment.res +++ b/src/dom-nodes/DocumentFragment.res @@ -15,8 +15,6 @@ module Impl = ( ) => { include Node.Impl({type t = T.t}) - external asDocumentFragment: T.t => DOMTree.documentFragment = "%identity" - /** Inserts nodes after the last child of node, while replacing strings in nodes with equivalent Text nodes. @@ -94,3 +92,6 @@ Throws a "HierarchyRequestError" DOMException if the constraints of the node tre } include Impl({type t = t}) + +external asNode: t => DOMTree.node = "%identity" +external asDocumentFragment: t => DOMTree.documentFragment = "%identity" diff --git a/src/dom-nodes/Element.res b/src/dom-nodes/Element.res index 7ae7d68e..7fe5310a 100644 --- a/src/dom-nodes/Element.res +++ b/src/dom-nodes/Element.res @@ -9,8 +9,6 @@ module Impl = ( ) => { include Node.Impl({type t = T.t}) - external asElement: T.t => DOMTree.element = "%identity" - /** Inserts nodes just after node, while replacing strings in nodes with equivalent Text nodes. @@ -506,6 +504,9 @@ Returns true if qualifiedName is now present, and false otherwise. include Impl({type t = t}) +external asNode: t => DOMTree.node = "%identity" +external asElement: t => DOMTree.element = "%identity" + /** `isInstanceOf(value)` diff --git a/src/dom-nodes/Node.res b/src/dom-nodes/Node.res index 6d35787a..b6f6f995 100644 --- a/src/dom-nodes/Node.res +++ b/src/dom-nodes/Node.res @@ -9,8 +9,6 @@ module Impl = ( ) => { include EventTarget.Impl({type t = T.t}) - external asNode: T.t => DOMTree.node = "%identity" - type getRootNodeOptions = {mutable composed?: bool} /** @@ -112,3 +110,5 @@ Returns true if other is an inclusive descendant of node, and false otherwise. } include Impl({type t = t}) + +external asNode: t => DOMTree.node = "%identity" diff --git a/src/event/AbortController.res b/src/event/AbortController.res index 0c109eca..b85096a3 100644 --- a/src/event/AbortController.res +++ b/src/event/AbortController.res @@ -1,4 +1,10 @@ -type t = DOM.abortController = private {...DOM.abortController} +type t = DOM.abortController + +/** +Returns the AbortSignal object associated with this object. +[Read more on MDN](https://developer.mozilla.org/docs/Web/API/AbortController/signal) +*/ +@get external signal: t => DOM.abortSignal = "signal" @new external make: unit => DOM.abortController = "AbortController" diff --git a/src/event/Event.res b/src/event/Event.res index e8663c1e..845256cb 100644 --- a/src/event/Event.res +++ b/src/event/Event.res @@ -42,8 +42,6 @@ module Impl = ( type t }, ) => { - external asEvent: T.t => DOM.event = "%identity" - /** Returns the invocation target objects of event's path (objects on which listeners will be invoked), except for any nodes in shadow trees of which the shadow root's mode is "closed" that are not reachable from event's currentTarget. [Read more on MDN](https://developer.mozilla.org/docs/Web/API/Event/composedPath) @@ -74,3 +72,5 @@ When dispatched in a tree, invoking this method prevents event from reaching any } include Impl({type t = DOM.event}) + +external asEvent: DOM.event => DOM.event = "%identity" diff --git a/src/event/EventTarget.res b/src/event/EventTarget.res index a8b342d1..22de21d3 100644 --- a/src/event/EventTarget.res +++ b/src/event/EventTarget.res @@ -6,8 +6,6 @@ module Impl = ( type t }, ) => { - external asEventTarget: T.t => DOM.eventTarget = "%identity" - /** Appends an event listener for events whose type attribute value is type. The callback argument sets the callback that will be invoked when the event is dispatched. diff --git a/src/event/ExtendableEvent.res b/src/event/ExtendableEvent.res index 3cc6cc9d..0bfa25c8 100644 --- a/src/event/ExtendableEvent.res +++ b/src/event/ExtendableEvent.res @@ -5,10 +5,11 @@ module Impl = ( ) => { include Event.Impl({type t = T.t}) - external asExtendableEvent: T.t => DOM.extendableEvent = "%identity" - @send external waitUntil: (T.t, promise<'a>) => unit = "waitUntil" } include Impl({type t = DOM.extendableEvent}) + +external asEvent: DOM.extendableEvent => DOM.event = "%identity" +external asExtendableEvent: DOM.extendableEvent => DOM.extendableEvent = "%identity" diff --git a/src/fetch/FetchTypes.res b/src/fetch/FetchTypes.res index 3c475ef6..3db92bbd 100644 --- a/src/fetch/FetchTypes.res +++ b/src/fetch/FetchTypes.res @@ -82,81 +82,7 @@ This WebApiFetch API interface represents a resource request. [See Request on MDN](https://developer.mozilla.org/docs/Web/API/Request) */ @editor.completeFrom(Request) -type request = private { - /** - Returns request's HTTP method, which is "GET" by default. - [Read more on MDN](https://developer.mozilla.org/docs/Web/API/Request/method) - */ - method: string, - /** - Returns the WebApiURL of request as a string. - [Read more on MDN](https://developer.mozilla.org/docs/Web/API/Request/url) - */ - url: string, - /** - Returns a Headers object consisting of the headers associated with request. Note that headers added in the network layer by the user agent will not be accounted for in this object, e.g., the "Host" header. - [Read more on MDN](https://developer.mozilla.org/docs/Web/API/Request/headers) - */ - headers: headers, - /** - Returns the kind of resource requested by request, e.g., "document" or "script". - [Read more on MDN](https://developer.mozilla.org/docs/Web/API/Request/destination) - */ - destination: requestDestination, - /** - Returns the referrer of request. Its value can be a same-origin WebApiURL if explicitly set in init, the empty string to indicate no referrer, and "about:client" when defaulting to the global's default. This is used during fetching to determine the value of the `Referer` header of the request being made. - [Read more on MDN](https://developer.mozilla.org/docs/Web/API/Request/referrer) - */ - referrer: string, - /** - Returns the referrer policy associated with request. This is used during fetching to compute the value of the request's referrer. - [Read more on MDN](https://developer.mozilla.org/docs/Web/API/Request/referrerPolicy) - */ - referrerPolicy: referrerPolicy, - /** - Returns the mode associated with request, which is a string indicating whether the request will use CORS, or will be restricted to same-origin URLs. - [Read more on MDN](https://developer.mozilla.org/docs/Web/API/Request/mode) - */ - mode: requestMode, - /** - Returns the credentials mode associated with request, which is a string indicating whether credentials will be sent with the request always, never, or only when sent to a same-origin URL. - [Read more on MDN](https://developer.mozilla.org/docs/Web/API/Request/credentials) - */ - credentials: requestCredentials, - /** - Returns the cache mode associated with request, which is a string indicating how the request will interact with the browser's cache when fetching. - [Read more on MDN](https://developer.mozilla.org/docs/Web/API/Request/cache) - */ - cache: requestCache, - /** - Returns the redirect mode associated with request, which is a string indicating how redirects for the request will be handled during fetching. A request will follow redirects by default. - [Read more on MDN](https://developer.mozilla.org/docs/Web/API/Request/redirect) - */ - redirect: requestRedirect, - /** - Returns request's subresource integrity metadata, which is a cryptographic hash of the resource being fetched. Its value consists of multiple hashes separated by whitespace. [SRI] - [Read more on MDN](https://developer.mozilla.org/docs/Web/API/Request/integrity) - */ - integrity: string, - /** - Returns a boolean indicating whether or not request can outlive the global in which it was created. - [Read more on MDN](https://developer.mozilla.org/docs/Web/API/Request/keepalive) - */ - keepalive: bool, - /** - Returns the signal associated with request, which is an AbortSignal object indicating whether or not request has been aborted, and its abort event handler. - [Read more on MDN](https://developer.mozilla.org/docs/Web/API/Request/signal) - */ - signal: DOM.abortSignal, - /** - [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 request /** This WebApiFetch API interface represents the response to a request. @@ -178,10 +104,8 @@ type formData type requestInfo = unknown -@editor.completeFrom(FormDataEntryValue) @unboxed -type formDataEntryValue = - | String(string) - | File(FileTypes.file) +@editor.completeFrom(FormDataEntryValue) +type formDataEntryValue type requestInit = { /** diff --git a/src/fetch/FormDataEntryValue.res b/src/fetch/FormDataEntryValue.res index da0973b2..173e75bd 100644 --- a/src/fetch/FormDataEntryValue.res +++ b/src/fetch/FormDataEntryValue.res @@ -1,2 +1,25 @@ -@unboxed -type t = FetchTypes.formDataEntryValue = | ...FetchTypes.formDataEntryValue +/** +A native FormData value, returned as either a string or File by the browser. +[See FormDataEntryValue on MDN](https://developer.mozilla.org/docs/Web/API/FormDataEntryValue) +*/ +type t = FetchTypes.formDataEntryValue + +/** +Decoded result of inspecting a FormData entry value. +*/ +type decoded = + | String(string) + | File(File.t) + +let isString = (_: 'value): bool => %raw(`typeof param === "string"`) + +/** +Classifies a native FormData value as a string or File. +Returns None when the value is neither a string nor a File in the current realm. +*/ +let decode = (value: t): option => + if value->isString { + Some(String(Obj.magic(value))) + } else { + value->File.classify->Option.map(file => File(file)) + } diff --git a/src/fetch/Request.res b/src/fetch/Request.res index f51ad3fd..d8cf368b 100644 --- a/src/fetch/Request.res +++ b/src/fetch/Request.res @@ -1,7 +1,93 @@ /** [Read more on MDN](https://developer.mozilla.org/docs/Web/API/Request) */ -type t = FetchTypes.request = private {...FetchTypes.request} +type t = FetchTypes.request + +/** +Returns request's HTTP method, which is "GET" by default. +[Read more on MDN](https://developer.mozilla.org/docs/Web/API/Request/method) +*/ +@get external method: t => string = "method" +/** +Returns the URL of request as a string. +[Read more on MDN](https://developer.mozilla.org/docs/Web/API/Request/url) +*/ +@get external url: t => string = "url" +/** +Returns a Headers object consisting of the headers associated with request. Note that headers added +in the network layer by the user agent will not be accounted for in this object, for example the +"Host" header. +[Read more on MDN](https://developer.mozilla.org/docs/Web/API/Request/headers) +*/ +@get external headers: t => FetchTypes.headers = "headers" +/** +Returns the kind of resource requested by request, for example "document" or "script". +[Read more on MDN](https://developer.mozilla.org/docs/Web/API/Request/destination) +*/ +@get external destination: t => FetchTypes.requestDestination = "destination" +/** +Returns the referrer of request. Its value can be a same-origin URL if explicitly set in init, the +empty string to indicate no referrer, and "about:client" when defaulting to the global's default. +This is used during fetching to determine the value of the `Referer` header of the request being +made. +[Read more on MDN](https://developer.mozilla.org/docs/Web/API/Request/referrer) +*/ +@get external referrer: t => string = "referrer" +/** +Returns the referrer policy associated with request. This is used during fetching to compute the +value of the request's referrer. +[Read more on MDN](https://developer.mozilla.org/docs/Web/API/Request/referrerPolicy) +*/ +@get external referrerPolicy: t => FetchTypes.referrerPolicy = "referrerPolicy" +/** +Returns the mode associated with request, which is a string indicating whether the request will use +CORS, or will be restricted to same-origin URLs. +[Read more on MDN](https://developer.mozilla.org/docs/Web/API/Request/mode) +*/ +@get external mode: t => FetchTypes.requestMode = "mode" +/** +Returns the credentials mode associated with request, which is a string indicating whether +credentials will be sent with the request always, never, or only when sent to a same-origin URL. +[Read more on MDN](https://developer.mozilla.org/docs/Web/API/Request/credentials) +*/ +@get external credentials: t => FetchTypes.requestCredentials = "credentials" +/** +Returns the cache mode associated with request, which is a string indicating how the request will +interact with the browser's cache when fetching. +[Read more on MDN](https://developer.mozilla.org/docs/Web/API/Request/cache) +*/ +@get external cache: t => FetchTypes.requestCache = "cache" +/** +Returns the redirect mode associated with request, which is a string indicating how redirects for +the request will be handled during fetching. A request will follow redirects by default. +[Read more on MDN](https://developer.mozilla.org/docs/Web/API/Request/redirect) +*/ +@get external redirect: t => FetchTypes.requestRedirect = "redirect" +/** +Returns request's subresource integrity metadata, which is a cryptographic hash of the resource being +fetched. Its value consists of multiple hashes separated by whitespace. [SRI] +[Read more on MDN](https://developer.mozilla.org/docs/Web/API/Request/integrity) +*/ +@get external integrity: t => string = "integrity" +/** +Returns a boolean indicating whether or not request can outlive the global in which it was created. +[Read more on MDN](https://developer.mozilla.org/docs/Web/API/Request/keepalive) +*/ +@get external keepalive: t => bool = "keepalive" +/** +Returns the signal associated with request, which is an AbortSignal object indicating whether or not +request has been aborted, and its abort event handler. +[Read more on MDN](https://developer.mozilla.org/docs/Web/API/Request/signal) +*/ +@get external signal: t => DOM.abortSignal = "signal" +/** +[Read more on MDN](https://developer.mozilla.org/docs/Web/API/Request/body) +*/ +@get external body: t => Null.t> = "body" +/** +[Read more on MDN](https://developer.mozilla.org/docs/Web/API/Request/bodyUsed) +*/ +@get external bodyUsed: t => bool = "bodyUsed" type requestInit = FetchTypes.requestInit type bodyInit = BodyInit.t type headersInit = HeadersInit.t diff --git a/src/file/BaseFileAndDirectoryEntries.res b/src/file/BaseFileAndDirectoryEntries.res index 74dee7be..3e5fb5d5 100644 --- a/src/file/BaseFileAndDirectoryEntries.res +++ b/src/file/BaseFileAndDirectoryEntries.res @@ -4,67 +4,14 @@ [See FileSystemEntry on MDN](https://developer.mozilla.org/docs/Web/API/FileSystemEntry) */ @editor.completeFrom(FileSystemEntry) -type rec fileSystemEntry = private { - /** - [Read more on MDN](https://developer.mozilla.org/docs/Web/API/FileSystemEntry/isFile) - */ - isFile: bool, - /** - [Read more on MDN](https://developer.mozilla.org/docs/Web/API/FileSystemEntry/isDirectory) - */ - isDirectory: bool, - /** - [Read more on MDN](https://developer.mozilla.org/docs/Web/API/FileSystemEntry/name) - */ - name: string, - /** - [Read more on MDN](https://developer.mozilla.org/docs/Web/API/FileSystemEntry/fullPath) - */ - fullPath: string, - /** - [Read more on MDN](https://developer.mozilla.org/docs/Web/API/FileSystemEntry/filesystem) - */ - filesystem: fileSystem, -} +type rec fileSystemEntry /** [See FileSystemDirectoryEntry on MDN](https://developer.mozilla.org/docs/Web/API/FileSystemDirectoryEntry) */ -@editor.completeFrom(FileSystemDirectoryEntry) and fileSystemDirectoryEntry = private { - // Base properties from FileSystemEntry - /** - [Read more on MDN](https://developer.mozilla.org/docs/Web/API/FileSystemEntry/isFile) - */ - isFile: bool, - /** - [Read more on MDN](https://developer.mozilla.org/docs/Web/API/FileSystemEntry/isDirectory) - */ - isDirectory: bool, - /** - [Read more on MDN](https://developer.mozilla.org/docs/Web/API/FileSystemEntry/name) - */ - name: string, - /** - [Read more on MDN](https://developer.mozilla.org/docs/Web/API/FileSystemEntry/fullPath) - */ - fullPath: string, - /** - [Read more on MDN](https://developer.mozilla.org/docs/Web/API/FileSystemEntry/filesystem) - */ - filesystem: fileSystem, - // End base properties from FileSystemEntry -} +@editor.completeFrom(FileSystemDirectoryEntry) and fileSystemDirectoryEntry /** [See FileSystem on MDN](https://developer.mozilla.org/docs/Web/API/FileSystem) */ -and fileSystem = { - /** - [Read more on MDN](https://developer.mozilla.org/docs/Web/API/FileSystem/name) - */ - name: string, - /** - [Read more on MDN](https://developer.mozilla.org/docs/Web/API/FileSystem/root) - */ - root: fileSystemDirectoryEntry, -} +and fileSystem diff --git a/src/file/Blob.res b/src/file/Blob.res index a4142b94..b4581754 100644 --- a/src/file/Blob.res +++ b/src/file/Blob.res @@ -1,10 +1,30 @@ /** [Read more on MDN](https://developer.mozilla.org/docs/Web/API/Blob) */ -type t = FileTypes.blob = private {...FileTypes.blob} +type t = FileTypes.blob type blobPart = FileTypes.blobPart type blobPropertyBag = FileTypes.blobPropertyBag +/** +Creates a Blob part from a string without wrapping or copying the value. +[Read more on MDN](https://developer.mozilla.org/docs/Web/API/Blob/Blob) +*/ +external blobPartFromString: string => blobPart = "%identity" +/** +Creates a Blob part from an existing native Blob. +[Read more on MDN](https://developer.mozilla.org/docs/Web/API/Blob/Blob) +*/ +external blobPartFromBlob: t => blobPart = "%identity" + +/** +[Read more on MDN](https://developer.mozilla.org/docs/Web/API/Blob/size) +*/ +@get external size: t => int = "size" +/** +[Read more on MDN](https://developer.mozilla.org/docs/Web/API/Blob/type) +*/ +@get external type_: t => string = "type" + @new external make: (~blobParts: array=?, ~options: blobPropertyBag=?) => t = "Blob" @@ -13,7 +33,6 @@ module Impl = ( type t }, ) => { - external asBlob: T.t => t = "%identity" /** [Read more on MDN](https://developer.mozilla.org/docs/Web/API/Blob/slice) */ diff --git a/src/file/File.res b/src/file/File.res index f58080e6..ff6fde43 100644 --- a/src/file/File.res +++ b/src/file/File.res @@ -1,4 +1,26 @@ -type t = FileTypes.file = private {...FileTypes.file} +type t = FileTypes.file + +external asBlob: t => Blob.t = "%identity" +/** +[Read more on MDN](https://developer.mozilla.org/docs/Web/API/Blob/size) +*/ +@get external size: t => int = "size" +/** +[Read more on MDN](https://developer.mozilla.org/docs/Web/API/Blob/type) +*/ +@get external type_: t => string = "type" +/** +[Read more on MDN](https://developer.mozilla.org/docs/Web/API/File/name) +*/ +@get external name: t => string = "name" +/** +[Read more on MDN](https://developer.mozilla.org/docs/Web/API/File/lastModified) +*/ +@get external lastModified: t => int = "lastModified" +/** +[Read more on MDN](https://developer.mozilla.org/docs/Web/API/File/webkitRelativePath) +*/ +@get external webkitRelativePath: t => string = "webkitRelativePath" include Blob.Impl({type t = t}) @@ -12,4 +34,15 @@ external make: ( ~options: FileTypes.filePropertyBag=?, ) => t = "File" -let isInstanceOf = (_: 't): bool => %raw(`param instanceof File`) +let isInstanceOf = (_: 't): bool => + %raw(`typeof globalThis.File === "function" && param instanceof globalThis.File`) + +/** +Safely narrows a value to a native File instance in the current JavaScript realm. +*/ +let classify = (value: 'value): option => + if value->isInstanceOf { + Some(Obj.magic(value)) + } else { + None + } diff --git a/src/file/FileAndDirectoryEntriesTypes.res b/src/file/FileAndDirectoryEntriesTypes.res index 76e3fc84..50b73e7e 100644 --- a/src/file/FileAndDirectoryEntriesTypes.res +++ b/src/file/FileAndDirectoryEntriesTypes.res @@ -21,7 +21,7 @@ type fileSystem = BaseFileAndDirectoryEntries.fileSystem [See FileSystemDirectoryReader on MDN](https://developer.mozilla.org/docs/Web/API/FileSystemDirectoryReader) */ @editor.completeFrom(FileSystemDirectoryReader) -type fileSystemDirectoryReader = private {} +type fileSystemDirectoryReader type fileSystemFlags = { mutable create?: bool, diff --git a/src/file/FileList.res b/src/file/FileList.res index cca270eb..0644f409 100644 --- a/src/file/FileList.res +++ b/src/file/FileList.res @@ -1,4 +1,9 @@ -type t = DOM.fileList = private {...DOM.fileList} +type t = DOM.fileList + +/** +[Read more on MDN](https://developer.mozilla.org/docs/Web/API/FileList/length) +*/ +@get external length: t => int = "length" /** Returns the `WebApiFile` at the specified index. @@ -15,4 +20,4 @@ for i in 0 to fileList.length - 1 { [Read more on MDN](https://developer.mozilla.org/docs/Web/API/FileList/item) */ @send -external item: (DOM.fileList, int) => Null.t = "item" +external item: (t, int) => Null.t = "item" diff --git a/src/file/FileSystemDirectoryEntry.res b/src/file/FileSystemDirectoryEntry.res index fb943376..cbdc6301 100644 --- a/src/file/FileSystemDirectoryEntry.res +++ b/src/file/FileSystemDirectoryEntry.res @@ -1,9 +1,11 @@ -type t = BaseFileAndDirectoryEntries.fileSystemDirectoryEntry = private { - ...BaseFileAndDirectoryEntries.fileSystemDirectoryEntry, -} +type t = BaseFileAndDirectoryEntries.fileSystemDirectoryEntry -external asFileSystemEntry: FileAndDirectoryEntriesTypes.fileSystemDirectoryEntry => FileAndDirectoryEntriesTypes.fileSystemEntry = - "%identity" +external asFileSystemEntry: t => FileSystemEntry.t = "%identity" +@get external isFile: t => bool = "isFile" +@get external isDirectory: t => bool = "isDirectory" +@get external name: t => string = "name" +@get external fullPath: t => string = "fullPath" +@get external filesystem: t => FileAndDirectoryEntriesTypes.fileSystem = "filesystem" /** [Read more on MDN](https://developer.mozilla.org/docs/Web/API/FileSystemEntry/getParent) */ diff --git a/src/file/FileSystemDirectoryHandle.res b/src/file/FileSystemDirectoryHandle.res index d1f6ac45..15f3a2fb 100644 --- a/src/file/FileSystemDirectoryHandle.res +++ b/src/file/FileSystemDirectoryHandle.res @@ -1,7 +1,8 @@ -type t = FileTypes.fileSystemDirectoryHandle = private {...FileTypes.fileSystemDirectoryHandle} +type t = FileTypes.fileSystemDirectoryHandle -external asFileSystemHandle: FileTypes.fileSystemDirectoryHandle => FileTypes.fileSystemHandle = - "%identity" +external asFileSystemHandle: t => FileSystemHandle.t = "%identity" +@get external kind: t => FileTypes.fileSystemHandleKind = "kind" +@get external name: t => string = "name" /** [Read more on MDN](https://developer.mozilla.org/docs/Web/API/FileSystemHandle/isSameEntry) */ diff --git a/src/file/FileSystemDirectoryReader.res b/src/file/FileSystemDirectoryReader.res index 7ed1a21e..d1614ab9 100644 --- a/src/file/FileSystemDirectoryReader.res +++ b/src/file/FileSystemDirectoryReader.res @@ -1,6 +1,4 @@ -type t = FileAndDirectoryEntriesTypes.fileSystemDirectoryReader = private { - ...FileAndDirectoryEntriesTypes.fileSystemDirectoryReader, -} +type t = FileAndDirectoryEntriesTypes.fileSystemDirectoryReader /** [Read more on MDN](https://developer.mozilla.org/docs/Web/API/FileSystemDirectoryReader/readEntries) diff --git a/src/file/FileSystemEntry.res b/src/file/FileSystemEntry.res index 2796430f..36b16dd2 100644 --- a/src/file/FileSystemEntry.res +++ b/src/file/FileSystemEntry.res @@ -1,6 +1,10 @@ -type t = BaseFileAndDirectoryEntries.fileSystemEntry = private { - ...BaseFileAndDirectoryEntries.fileSystemEntry, -} +type t = BaseFileAndDirectoryEntries.fileSystemEntry + +@get external isFile: t => bool = "isFile" +@get external isDirectory: t => bool = "isDirectory" +@get external name: t => string = "name" +@get external fullPath: t => string = "fullPath" +@get external filesystem: t => FileAndDirectoryEntriesTypes.fileSystem = "filesystem" /** [Read more on MDN](https://developer.mozilla.org/docs/Web/API/FileSystemEntry/getParent) diff --git a/src/file/FileSystemFileHandle.res b/src/file/FileSystemFileHandle.res index 3f9bd5b5..f71e49d7 100644 --- a/src/file/FileSystemFileHandle.res +++ b/src/file/FileSystemFileHandle.res @@ -1,7 +1,8 @@ -type t = FileTypes.fileSystemFileHandle = private {...FileTypes.fileSystemFileHandle} +type t = FileTypes.fileSystemFileHandle -external asFileSystemHandle: FileTypes.fileSystemFileHandle => FileTypes.fileSystemHandle = - "%identity" +external asFileSystemHandle: t => FileSystemHandle.t = "%identity" +@get external kind: t => FileTypes.fileSystemHandleKind = "kind" +@get external name: t => string = "name" /** [Read more on MDN](https://developer.mozilla.org/docs/Web/API/FileSystemHandle/isSameEntry) */ diff --git a/src/file/FileSystemHandle.res b/src/file/FileSystemHandle.res index 6a145012..a8050e48 100644 --- a/src/file/FileSystemHandle.res +++ b/src/file/FileSystemHandle.res @@ -1,4 +1,7 @@ -type t = FileTypes.fileSystemHandle = private {...FileTypes.fileSystemHandle} +type t = FileTypes.fileSystemHandle + +@get external kind: t => FileTypes.fileSystemHandleKind = "kind" +@get external name: t => string = "name" /** [Read more on MDN](https://developer.mozilla.org/docs/Web/API/FileSystemHandle/isSameEntry) diff --git a/src/file/FileSystemWritableFileStream.res b/src/file/FileSystemWritableFileStream.res index 996002ee..6a95f0f4 100644 --- a/src/file/FileSystemWritableFileStream.res +++ b/src/file/FileSystemWritableFileStream.res @@ -1,9 +1,6 @@ -type t = FileTypes.fileSystemWritableFileStream = private { - ...FileTypes.fileSystemWritableFileStream, -} +type t = FileTypes.fileSystemWritableFileStream -external asWritableStream: FileTypes.fileSystemWritableFileStream => FileTypes.writableStream<'w> = - "%identity" +external asWritableStream: t => WritableStream.t = "%identity" /** [Read more on MDN](https://developer.mozilla.org/docs/Web/API/WritableStream/abort) */ diff --git a/src/file/FileTypes.res b/src/file/FileTypes.res index d50b7cd1..cbce21e3 100644 --- a/src/file/FileTypes.res +++ b/src/file/FileTypes.res @@ -20,118 +20,61 @@ A file-like object of immutable, raw data. Blobs represent data that isn't neces [See Blob on MDN](https://developer.mozilla.org/docs/Web/API/Blob) */ @editor.completeFrom(Blob) -type blob = DOM.blob = private { - /** - [Read more on MDN](https://developer.mozilla.org/docs/Web/API/Blob/size) - */ - size: int, - /** - [Read more on MDN](https://developer.mozilla.org/docs/Web/API/Blob/type) - */ - @as("type") - type_: string, -} +type blob = DOM.blob /** This Streams API interface represents a readable stream of byte data. The WebApiFetch API offers a concrete instance of a ReadableStream through the body property of a Response object. [See ReadableStream on MDN](https://developer.mozilla.org/docs/Web/API/ReadableStream) */ @editor.completeFrom(ReadableStream) -type readableStream<'r> = { - /** - [Read more on MDN](https://developer.mozilla.org/docs/Web/API/ReadableStream/locked) - */ - locked: bool, -} +type readableStream<+'r> /** This Streams API interface provides a standard abstraction for writing streaming data to a destination, known as a sink. This object comes with built-in backpressure and queuing. [See WritableStream on MDN](https://developer.mozilla.org/docs/Web/API/WritableStream) */ @editor.completeFrom(WritableStream) -type writableStream<'w> = { - /** - [Read more on MDN](https://developer.mozilla.org/docs/Web/API/WritableStream/locked) - */ - locked: bool, -} +type writableStream<+'w> /** This Streams API interface represents a controller allowing control of a WritableStream's state. When constructing a WritableStream, the underlying sink is given a corresponding WritableStreamDefaultController instance to manipulate. [See WritableStreamDefaultController on MDN](https://developer.mozilla.org/docs/Web/API/WritableStreamDefaultController) */ @editor.completeFrom(WritableStreamDefaultController) -type writableStreamDefaultController = private { - /** - [Read more on MDN](https://developer.mozilla.org/docs/Web/API/WritableStreamDefaultController/signal) - */ - signal: DOM.abortSignal, -} +type writableStreamDefaultController /** Provides information about files and allows JavaScript in a web page to access their content. [See WebApiFile on MDN](https://developer.mozilla.org/docs/Web/API/File) */ @editor.completeFrom(File) -type file = DOM.file = private { - ...blob, - /** - [Read more on MDN](https://developer.mozilla.org/docs/Web/API/File/name) - */ - name: string, - /** - [Read more on MDN](https://developer.mozilla.org/docs/Web/API/File/lastModified) - */ - lastModified: int, - /** - [Read more on MDN](https://developer.mozilla.org/docs/Web/API/File/webkitRelativePath) - */ - webkitRelativePath: string, -} +type file = DOM.file /** [See FileSystemHandle on MDN](https://developer.mozilla.org/docs/Web/API/FileSystemHandle) */ @editor.completeFrom(FileSystemHandle) -type fileSystemHandle = private { - /** - [Read more on MDN](https://developer.mozilla.org/docs/Web/API/FileSystemHandle/kind) - */ - kind: fileSystemHandleKind, - /** - [Read more on MDN](https://developer.mozilla.org/docs/Web/API/FileSystemHandle/name) - */ - name: string, -} +type fileSystemHandle /** [See FileSystemDirectoryHandle on MDN](https://developer.mozilla.org/docs/Web/API/FileSystemDirectoryHandle) */ @editor.completeFrom(FileSystemDirectoryHandle) -type fileSystemDirectoryHandle = private { - ...fileSystemHandle, -} +type fileSystemDirectoryHandle /** [See FileSystemFileHandle on MDN](https://developer.mozilla.org/docs/Web/API/FileSystemFileHandle) */ @editor.completeFrom(FileSystemFileHandle) -type fileSystemFileHandle = private { - ...fileSystemHandle, -} +type fileSystemFileHandle /** [See FileSystemWritableFileStream on MDN](https://developer.mozilla.org/docs/Web/API/FileSystemWritableFileStream) */ @editor.completeFrom(FileSystemWritableFileStream) -type fileSystemWritableFileStream = private { - ...writableStream, -} +type fileSystemWritableFileStream -@unboxed -type blobPart = - | String(string) - | Blob(blob) +type blobPart // | ArrayBuffer(ArrayBuffer.t) // | TypedArray(TypedArray.t) diff --git a/src/file/ReadableStream.res b/src/file/ReadableStream.res index 65011c93..0459faad 100644 --- a/src/file/ReadableStream.res +++ b/src/file/ReadableStream.res @@ -2,6 +2,10 @@ [Read more on MDN](https://developer.mozilla.org/docs/Web/API/ReadableStream) */ type t<'r> = FileTypes.readableStream<'r> +/** +[Read more on MDN](https://developer.mozilla.org/docs/Web/API/ReadableStream/locked) +*/ +@get external locked: t<'r> => bool = "locked" /** `make()` diff --git a/src/file/WritableStream.res b/src/file/WritableStream.res index d2cabeb4..34ab15fe 100644 --- a/src/file/WritableStream.res +++ b/src/file/WritableStream.res @@ -1,5 +1,9 @@ @editor.completeFrom(WritableStream) -type t<'w> = FileTypes.writableStream<'w> = {...FileTypes.writableStream<'w>} +type t<'w> = FileTypes.writableStream<'w> +/** +[Read more on MDN](https://developer.mozilla.org/docs/Web/API/WritableStream/locked) +*/ +@get external locked: t<'w> => bool = "locked" /** [Read more on MDN](https://developer.mozilla.org/docs/Web/API/WritableStream) diff --git a/src/file/WritableStreamDefaultController.res b/src/file/WritableStreamDefaultController.res index 83bdffae..e19976a3 100644 --- a/src/file/WritableStreamDefaultController.res +++ b/src/file/WritableStreamDefaultController.res @@ -1,6 +1,8 @@ -type t = FileTypes.writableStreamDefaultController = private { - ...FileTypes.writableStreamDefaultController, -} +type t = FileTypes.writableStreamDefaultController +/** +[Read more on MDN](https://developer.mozilla.org/docs/Web/API/WritableStreamDefaultController/signal) +*/ +@get external signal: t => DOM.abortSignal = "signal" /** [Read more on MDN](https://developer.mozilla.org/docs/Web/API/WritableStreamDefaultController/error) diff --git a/src/html/HTMLElement.res b/src/html/HTMLElement.res index 4f436620..17233d88 100644 --- a/src/html/HTMLElement.res +++ b/src/html/HTMLElement.res @@ -23,8 +23,6 @@ module Impl = ( ) => { include Element.Impl({type t = T.t}) - external asHTMLElement: T.t => DOMTree.htmlElement = "%identity" - /** Returns the element's current hidden state. @@ -107,6 +105,10 @@ rather than relying on coercion. include Impl({type t = t}) +external asNode: t => DOMTree.node = "%identity" +external asElement: t => DOMTree.element = "%identity" +external asHTMLElement: t => DOMTree.htmlElement = "%identity" + /** `isInstanceOf(value)` diff --git a/src/messaging/PushEvent.res b/src/messaging/PushEvent.res index 43afe1ee..7133c5ee 100644 --- a/src/messaging/PushEvent.res +++ b/src/messaging/PushEvent.res @@ -1,3 +1,15 @@ -type t = PushTypes.pushEvent = private {...PushTypes.pushEvent} +type t = PushTypes.pushEvent + +/** +[Read more on MDN](https://developer.mozilla.org/docs/Web/API/PushEvent) +*/ +external asEvent: t => DOM.event = "%identity" +external asExtendableEvent: t => DOM.extendableEvent = "%identity" + +/** +Returns the data associated with the event. +[Read more on MDN](https://developer.mozilla.org/docs/Web/API/PushEvent/data) +*/ +@get external data: t => Null.t = "data" include ExtendableEvent.Impl({type t = t}) diff --git a/src/messaging/PushTypes.res b/src/messaging/PushTypes.res index 2f7aa33f..f65d214a 100644 --- a/src/messaging/PushTypes.res +++ b/src/messaging/PushTypes.res @@ -79,10 +79,4 @@ type pushSubscriptionJSON = { type pushMessageData @editor.completeFrom(PushEvent) -type pushEvent = private { - ...DOM.extendableEvent, - /** - [Read more on MDN](https://developer.mozilla.org/docs/Web/API/PushEvent/data) - */ - data: Null.t, -} +type pushEvent diff --git a/src/svg/SVGElement.res b/src/svg/SVGElement.res index 252b1a26..278697b0 100644 --- a/src/svg/SVGElement.res +++ b/src/svg/SVGElement.res @@ -70,12 +70,13 @@ module Impl = ( }, ) => { include Element.Impl({type t = T.t}) - - external asSVGElement: T.t => t = "%identity" } include Impl({type t = t}) +external asNode: t => DOMTree.node = "%identity" +external asElement: t => DOMTree.element = "%identity" + /** SVG elements whose primary purpose is to directly render graphics into a group. [See SVGGraphicsElement on MDN](https://developer.mozilla.org/docs/Web/API/SVGGraphicsElement) diff --git a/src/svg/SVGGraphicsElement.res b/src/svg/SVGGraphicsElement.res index c67b72d1..36c4a756 100644 --- a/src/svg/SVGGraphicsElement.res +++ b/src/svg/SVGGraphicsElement.res @@ -2,6 +2,10 @@ type t = SVGElement.svgGraphicsElement = private {...SVGElement.svgGraphicsEleme include SVGElement.Impl({type t = SVGElement.svgGraphicsElement}) +external asSVGElement: t => SVGElement.t = "%identity" +external asElement: t => Element.t = "%identity" +external asNode: t => Node.t = "%identity" + /** [Read more on MDN](https://developer.mozilla.org/docs/Web/API/SVGGraphicsElement/getBBox) */ diff --git a/src/window/CustomElementRegistry.res b/src/window/CustomElementRegistry.res index ba2a96f0..6b02ae65 100644 --- a/src/window/CustomElementRegistry.res +++ b/src/window/CustomElementRegistry.res @@ -1,4 +1,4 @@ -type t = DOM.customElementRegistry = private {...DOM.customElementRegistry} +type t = DOM.customElementRegistry /** [Read more on MDN](https://developer.mozilla.org/docs/Web/API/CustomElementRegistry/define) diff --git a/src/window/Document.res b/src/window/Document.res index c6b0315b..6a1b514b 100644 --- a/src/window/Document.res +++ b/src/window/Document.res @@ -6,6 +6,8 @@ external make: unit => DOM.document = "Document" include Node.Impl({type t = DOM.document}) +external asNode: DOM.document => DOMTree.node = "%identity" + /** Returns the first element within node's descendants whose ID is elementId. [Read more on MDN](https://developer.mozilla.org/docs/Web/API/Document/getElementById) diff --git a/src/window/XPathExpression.res b/src/window/XPathExpression.res index 90c72b40..e2589927 100644 --- a/src/window/XPathExpression.res +++ b/src/window/XPathExpression.res @@ -2,7 +2,7 @@ This interface is a compiled XPath expression that can be evaluated on a document or specific node to return information its WebApiDOM tree. [See XPathExpression on MDN](https://developer.mozilla.org/docs/Web/API/XPathExpression) */ -type t = private {} +type t /** [Read more on MDN](https://developer.mozilla.org/docs/Web/API/XPathExpression/evaluate) diff --git a/tests/FetchAPI/FormData__test.res b/tests/FetchAPI/FormData__test.res index 180fa091..c96ab5b8 100644 --- a/tests/FetchAPI/FormData__test.res +++ b/tests/FetchAPI/FormData__test.res @@ -7,9 +7,10 @@ module EntryValue = FormDataEntryValue open EntryValue let logEntry = (~stringPrefix: string, ~filePrefix: string, entry: EntryValue.t) => - switch entry { - | String(value) => Console.log(`${stringPrefix}${value}`) - | File(file) => Console.log(`${filePrefix}${file.name}`) + switch EntryValue.decode(entry) { + | Some(String(value)) => Console.log(`${stringPrefix}${value}`) + | Some(File(file)) => Console.log(`${filePrefix}${File.name(file)}`) + | None => Console.log("Unsupported FormData entry") } let formData: FormData.t = FormData.make(~form=myForm) @@ -30,17 +31,31 @@ let _ = logEntry(~stringPrefix="String value: ", ~filePrefix="WebApiFile: ", entry) ) -// Create formDataEntryValue from string or file -let stringEntry = EntryValue.String("test value") +// Create actual string and file entries, then decode the native results. let blob: Blob.t = Blob.make(~blobParts=[]) let file: File.t = File.make(~fileBits=[], ~fileName="test.txt") -let fileEntry = EntryValue.File(file) +formData->FormData.append(~name="text-entry", ~value="test value") +formData->FormData.appendBlob( + ~name="file-entry", + ~blobValue=File.asBlob(file), + ~filename="test.txt", +) formData->FormData.appendBlob(~name="avatar", ~blobValue=blob) -logEntry(~stringPrefix="String entry: ", ~filePrefix="Unexpected file entry: ", stringEntry) +let stringEntry = formData->FormData.get("text-entry") +let _ = switch stringEntry->Null.toOption { +| Some(entry) => + logEntry(~stringPrefix="String entry: ", ~filePrefix="Unexpected file entry: ", entry) +| None => Console.log("No string entry") +} -logEntry(~stringPrefix="Unexpected string entry: ", ~filePrefix="File entry: ", fileEntry) +let fileEntry = formData->FormData.get("file-entry") +let _ = switch fileEntry->Null.toOption { +| Some(entry) => + logEntry(~stringPrefix="Unexpected string entry: ", ~filePrefix="File entry: ", entry) +| None => Console.log("No file entry") +} // Iterate over all entries in the FormData let entries: iterable<(string, EntryValue.t)> = formData->FormData.entries @@ -48,8 +63,9 @@ let _ = entries ->Array.fromIterable ->Array.forEach(((key, value)) => { - switch value { - | String(s) => Console.log(`${key}: ${s}`) - | File(f) => Console.log(`${key}: [WebApiFile] ${f.name}`) + switch EntryValue.decode(value) { + | Some(String(s)) => Console.log(`${key}: ${s}`) + | Some(File(f)) => Console.log(`${key}: [WebApiFile] ${File.name(f)}`) + | None => Console.log(`${key}: unsupported entry`) } }) diff --git a/tests/ServiceWorkerAPI/ServiceWorker__test.res b/tests/ServiceWorkerAPI/ServiceWorker__test.res index 4881e53c..950fa3d1 100644 --- a/tests/ServiceWorkerAPI/ServiceWorker__test.res +++ b/tests/ServiceWorkerAPI/ServiceWorker__test.res @@ -4,7 +4,7 @@ self->ServiceWorkerScope.addEventListener(EventType.Push, (event: PushEvent.t) = Console.log("received push event") // Extract data - let (title, body) = switch event.data->Null.toOption { + let (title, body) = switch PushEvent.data(event)->Null.toOption { | Some(data) => switch data->PushMessageData.json { | JSON.Object(dict{"title": JSON.String(title), "body": JSON.String(body)}) => (title, body) diff --git a/tests/browser/BindingContracts__test.res b/tests/browser/BindingContracts__test.res index 670b4b42..086eb157 100644 --- a/tests/browser/BindingContracts__test.res +++ b/tests/browser/BindingContracts__test.res @@ -67,16 +67,16 @@ assert(isUint8ArrayWithABC(requestBytes)) let emptyStateRequest = Request.fromURL("https://example.invalid/") assert(%raw(`emptyStateRequest.destination === "" && emptyStateRequest.referrerPolicy === ""`)) -let blobBytes = await Blob.make(~blobParts=[String("ABC")])->Blob.bytes +let blobBytes = await Blob.make(~blobParts=[Blob.blobPartFromString("ABC")])->Blob.bytes assert(isUint8ArrayWithABC(blobBytes)) -let byteStream = Blob.make(~blobParts=[String("ABC")])->Blob.stream +let byteStream = Blob.make(~blobParts=[Blob.blobPartFromString("ABC")])->Blob.stream let byteStreamReader = byteStream->ReadableStream.getReader let firstChunk = await byteStreamReader->readByteStreamChunk assert(containsABCByteChunk(firstChunk)) await byteStreamReader->cancelByteStreamReader let responseFromByteStream = Response.fromReadableStream( - Blob.make(~blobParts=[String("ABC")])->Blob.stream, + Blob.make(~blobParts=[Blob.blobPartFromString("ABC")])->Blob.stream, ) let responseStreamBytes = await responseFromByteStream->Response.bytes assert(isUint8ArrayWithABC(responseStreamBytes)) @@ -90,8 +90,19 @@ assert(isKnownNotificationPermission(permission)) switch Document.createElement(document, "canvas")->HTMLCanvasElement.classify { | Some(canvas) => { - canvas.width = 0 - canvas.height = 0 + canvas->HTMLCanvasElement.setWidth(320) + canvas->HTMLCanvasElement.setHeight(200) + assert(canvas->HTMLCanvasElement.width == 320) + assert(canvas->HTMLCanvasElement.height == 200) + canvas->HTMLCanvasElement.setHidden(HTMLElement.Hidden) + assert(canvas->HTMLCanvasElement.hidden == HTMLElement.Hidden) + canvas->HTMLCanvasElement.setHidden(HTMLElement.Visible) + switch canvas->HTMLCanvasElement.getContext2D { + | Null.Value(context) => context->CanvasRenderingContext2D.fillRect(~x=0., ~y=0., ~w=1., ~h=1.) + | Null => assert(false) + } + canvas->HTMLCanvasElement.setWidth(0) + canvas->HTMLCanvasElement.setHeight(0) await withResolver(resolve => canvas->HTMLCanvasElement.toBlob(~callback=_blob => { assert(isNull(_blob)) @@ -102,6 +113,28 @@ switch Document.createElement(document, "canvas")->HTMLCanvasElement.classify { | None => assert(false) } +let formData = FormData.make() +FormData.append(formData, ~name="text", ~value="native string") +switch FormData.get(formData, "text")->Null.toOption { +| Some(value) => + switch FormDataEntryValue.decode(value) { + | Some(FormDataEntryValue.String(text)) => assert(text == "native string") + | Some(FormDataEntryValue.File(_)) | None => assert(false) + } +| None => assert(false) +} + +let file = File.make(~fileBits=[Blob.blobPartFromString("native file")], ~fileName="contract.txt") +FormData.appendBlob(formData, ~name="file", ~blobValue=File.asBlob(file), ~filename="contract.txt") +switch FormData.get(formData, "file")->Null.toOption { +| Some(value) => + switch FormDataEntryValue.decode(value) { + | Some(FormDataEntryValue.File(decodedFile)) => assert(File.name(decodedFile) == "contract.txt") + | Some(FormDataEntryValue.String(_)) | None => assert(false) + } +| None => assert(false) +} + %raw(`globalThis.BindingContractsElement = class extends HTMLElement {}`) @scope("globalThis") From 1eb1f1a4a187cf15beb724b2fc921e3491f7d688 Mon Sep 17 00:00:00 2001 From: Josh Vlk Date: Tue, 10 Nov 2026 22:20:30 -0500 Subject: [PATCH 4/4] add opaque file and IndexedDB native identities --- CHANGELOG.md | 5 ++-- docs/content/docs/api-surface.mdx | 14 ++++++----- scripts/native-instance-inventory.json | 16 ++++++++++-- scripts/native-inventory-validation.mjs | 3 ++- scripts/native-inventory-validation.test.mjs | 5 ++-- src/fetch/FormDataEntryValue.res | 21 +++++++--------- src/storage/IDBDatabase.res | 20 +++++++++++++++ src/storage/IndexedDbTypes.res | 19 +------------- tests/FetchAPI/FormData__test.res | 26 +++++++++++--------- tests/browser/BindingContracts__test.res | 12 ++++----- 10 files changed, 81 insertions(+), 60 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f96e6142..161267a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,14 +19,15 @@ versioning and publication. `WritableStreamDefaultController`, `FileSystemWritableFileStream`, `Blob`, `File`, `FileList`, `FileSystemEntry`, `FileSystemDirectoryEntry`, `FileSystemDirectoryReader`, `FileSystemHandle`, `FileSystemDirectoryHandle`, `FileSystemFileHandle`, `Cache`, `CacheStorage`, `IDBFactory`, + `IDBDatabase`, `StorageManager`, `NavigationPreloadManager`, `PushEvent`, `Document`, `Window`, `CustomElementRegistry`, and `AbortController` opaque native identities. Direct property access is replaced by typed module accessors: for example, use `Request.method(request)`, `Response.ok(response)`, `URL.getHref(url)`, `URL.setHref(url, href)`, `HTMLCanvasElement.setWidth(canvas, width)`, and `Crypto.subtle(crypto)`. Initializers and option dictionaries remain constructible records. `Blob.make` and `File.make` input parts now use typed - `Blob.blobPartFromString`/`Blob.blobPartFromBlob` helpers, and `FormDataEntryValue.decode` inspects - returned values. Generic `Impl` as-base conversions that accepted + `Blob.blobPartFromString`/`Blob.blobPartFromBlob` helpers, and `FormDataEntryValue.asString` / + `FormDataEntryValue.asFile` inspect returned values. Generic `Impl` as-base conversions that accepted arbitrary receiver types have been removed from the DOM core; specialized concrete conversions remain where provided, and some descendant helpers are still migrating. The package-wide migration is incomplete; unconverted native instance types still expose the existing API. diff --git a/docs/content/docs/api-surface.mdx b/docs/content/docs/api-surface.mdx index a07edecb..2e21f096 100644 --- a/docs/content/docs/api-surface.mdx +++ b/docs/content/docs/api-surface.mdx @@ -262,7 +262,7 @@ let blobBody = WebAPI.BodyInit.fromBlob(blob) ``` Build Blob and File data parts with typed helpers, then inspect values returned by `FormData` with -`FormDataEntryValue.decode`: +`FormDataEntryValue.asString` and `FormDataEntryValue.asFile`: ```ReScript let parts = [ @@ -273,11 +273,13 @@ let dataBlob = WebAPI.Blob.make(~blobParts=parts) switch WebAPI.FormData.get(formData, "upload")->Null.toOption { | Some(value) => - switch WebAPI.FormDataEntryValue.decode(value) { - | Some(WebAPI.FormDataEntryValue.String(text)) => Console.log(text) - | Some(WebAPI.FormDataEntryValue.File(file)) => Console.log(WebAPI.File.name(file)) - | None => Console.log("unsupported FormData value") - } + switch WebAPI.FormDataEntryValue.asString(value) { + | Some(text) => Console.log(text) + | None => + switch WebAPI.FormDataEntryValue.asFile(value) { + | Some(file) => Console.log(WebAPI.File.name(file)) + | None => Console.log("unsupported FormData value") + } | None => Console.log("missing upload") } ``` diff --git a/scripts/native-instance-inventory.json b/scripts/native-instance-inventory.json index 28a88daa..24917be1 100644 --- a/scripts/native-instance-inventory.json +++ b/scripts/native-instance-inventory.json @@ -3,7 +3,7 @@ "status": "partial", "publicModuleCount": 291, "sourceFeatureGroupCount": 28, - "expectedConvertedNativeInstanceCount": 39, + "expectedConvertedNativeInstanceCount": 40, "convertedNativeInstances": [ { "name": "Response", @@ -364,6 +364,18 @@ "publicAlias": "type t = IndexedDbTypes.idbFactory", "accessors": [] }, + { + "name": "IDBDatabase", + "backingFile": "src/storage/IndexedDbTypes.res", + "backingType": "idbDatabase", + "publicFile": "src/storage/IDBDatabase.res", + "publicAlias": "type t = IndexedDbTypes.idbDatabase", + "accessors": [ + { "name": "name", "kind": "get", "property": "name" }, + { "name": "version", "kind": "get", "property": "version" }, + { "name": "objectStoreNames", "kind": "get", "property": "objectStoreNames" } + ] + }, { "name": "StorageManager", "backingFile": "src/storage/StorageTypes.res", @@ -430,7 +442,7 @@ "src/crypto/WebCryptoTypes.res:algorithm" ], "dataResultUnionModels": [ - "src/fetch/FetchTypes.res:formDataEntryValue is abstract and decoded with FormDataEntryValue.decode because the compiler cannot unbox an abstract File payload" + "src/fetch/FetchTypes.res:formDataEntryValue is abstract and inspected with FormDataEntryValue.asString/asFile because the compiler cannot unbox an abstract File payload" ], "unresolvedUnknownBackedHandles": [ "stream reader and writer types", diff --git a/scripts/native-inventory-validation.mjs b/scripts/native-inventory-validation.mjs index ffe3fa98..6b4c404f 100644 --- a/scripts/native-inventory-validation.mjs +++ b/scripts/native-inventory-validation.mjs @@ -3,7 +3,7 @@ const expectedInventory = { status: "partial", publicModuleCount: 291, sourceFeatureGroupCount: 28, - convertedCount: 39, + convertedCount: 40, names: [ "Response", "Request", @@ -28,6 +28,7 @@ const expectedInventory = { "Cache", "CacheStorage", "IDBFactory", + "IDBDatabase", "StorageManager", "NavigationPreloadManager", "PushEvent", diff --git a/scripts/native-inventory-validation.test.mjs b/scripts/native-inventory-validation.test.mjs index 096cb5e6..7e729b49 100644 --- a/scripts/native-inventory-validation.test.mjs +++ b/scripts/native-inventory-validation.test.mjs @@ -86,8 +86,8 @@ test("prevents inventory metadata from silently shrinking or declaring completio status: "partial", publicModuleCount: 291, sourceFeatureGroupCount: 28, - expectedConvertedNativeInstanceCount: 39, - convertedNativeInstances: Array.from({ length: 39 }, (_, index) => ({ + expectedConvertedNativeInstanceCount: 40, + convertedNativeInstances: Array.from({ length: 40 }, (_, index) => ({ name: [ "Response", "Request", @@ -112,6 +112,7 @@ test("prevents inventory metadata from silently shrinking or declaring completio "Cache", "CacheStorage", "IDBFactory", + "IDBDatabase", "StorageManager", "NavigationPreloadManager", "PushEvent", diff --git a/src/fetch/FormDataEntryValue.res b/src/fetch/FormDataEntryValue.res index 173e75bd..d7329302 100644 --- a/src/fetch/FormDataEntryValue.res +++ b/src/fetch/FormDataEntryValue.res @@ -4,22 +4,19 @@ A native FormData value, returned as either a string or File by the browser. */ type t = FetchTypes.formDataEntryValue -/** -Decoded result of inspecting a FormData entry value. -*/ -type decoded = - | String(string) - | File(File.t) - let isString = (_: 'value): bool => %raw(`typeof param === "string"`) /** -Classifies a native FormData value as a string or File. -Returns None when the value is neither a string nor a File in the current realm. +Returns the string value when this native FormData value is a string. */ -let decode = (value: t): option => +let asString = (value: t): option => if value->isString { - Some(String(Obj.magic(value))) + Some(Obj.magic(value)) } else { - value->File.classify->Option.map(file => File(file)) + None } + +/** +Classifies this native FormData value as a File in the current JavaScript realm. +*/ +let asFile = File.classify diff --git a/src/storage/IDBDatabase.res b/src/storage/IDBDatabase.res index 84f8e759..37634517 100644 --- a/src/storage/IDBDatabase.res +++ b/src/storage/IDBDatabase.res @@ -1,5 +1,25 @@ +type t = IndexedDbTypes.idbDatabase + include EventTarget.Impl({type t = IndexedDbTypes.idbDatabase}) +/** +Returns the name of the database. +[Read more on MDN](https://developer.mozilla.org/docs/Web/API/IDBDatabase/name) +*/ +@get external name: t => string = "name" + +/** +Returns the version of the database. +[Read more on MDN](https://developer.mozilla.org/docs/Web/API/IDBDatabase/version) +*/ +@get external version: t => int = "version" + +/** +Returns a list of the names of object stores in the database. +[Read more on MDN](https://developer.mozilla.org/docs/Web/API/IDBDatabase/objectStoreNames) +*/ +@get external objectStoreNames: t => DOM.domStringList = "objectStoreNames" + /** Returns a new transaction with the given mode ("readonly" or "readwrite") and scope which can be a single object store name or an array of names. [Read more on MDN](https://developer.mozilla.org/docs/Web/API/IDBDatabase/transaction) diff --git a/src/storage/IndexedDbTypes.res b/src/storage/IndexedDbTypes.res index b8b7192e..544206b3 100644 --- a/src/storage/IndexedDbTypes.res +++ b/src/storage/IndexedDbTypes.res @@ -32,24 +32,7 @@ This WebApiIndexedDB API interface provides a connection to a database; you can [See IDBDatabase on MDN](https://developer.mozilla.org/docs/Web/API/IDBDatabase) */ @editor.completeFrom(IDBDatabase) -type idbDatabase = private { - ...DOM.eventTarget, - /** - Returns the name of the database. - [Read more on MDN](https://developer.mozilla.org/docs/Web/API/IDBDatabase/name) - */ - name: string, - /** - Returns the version of the database. - [Read more on MDN](https://developer.mozilla.org/docs/Web/API/IDBDatabase/version) - */ - version: int, - /** - Returns a list of the names of object stores in the database. - [Read more on MDN](https://developer.mozilla.org/docs/Web/API/IDBDatabase/objectStoreNames) - */ - objectStoreNames: DOM.domStringList, -} +type idbDatabase /** [See IDBTransaction on MDN](https://developer.mozilla.org/docs/Web/API/IDBTransaction) diff --git a/tests/FetchAPI/FormData__test.res b/tests/FetchAPI/FormData__test.res index c96ab5b8..0162b063 100644 --- a/tests/FetchAPI/FormData__test.res +++ b/tests/FetchAPI/FormData__test.res @@ -4,13 +4,14 @@ external myForm: DOMTree.htmlFormElement = "myForm" module EntryValue = FormDataEntryValue -open EntryValue - let logEntry = (~stringPrefix: string, ~filePrefix: string, entry: EntryValue.t) => - switch EntryValue.decode(entry) { - | Some(String(value)) => Console.log(`${stringPrefix}${value}`) - | Some(File(file)) => Console.log(`${filePrefix}${File.name(file)}`) - | None => Console.log("Unsupported FormData entry") + switch EntryValue.asString(entry) { + | Some(value) => Console.log(`${stringPrefix}${value}`) + | None => + switch EntryValue.asFile(entry) { + | Some(file) => Console.log(`${filePrefix}${File.name(file)}`) + | None => Console.log("Unsupported FormData entry") + } } let formData: FormData.t = FormData.make(~form=myForm) @@ -31,7 +32,7 @@ let _ = logEntry(~stringPrefix="String value: ", ~filePrefix="WebApiFile: ", entry) ) -// Create actual string and file entries, then decode the native results. +// Create actual string and file entries, then inspect the native results. let blob: Blob.t = Blob.make(~blobParts=[]) let file: File.t = File.make(~fileBits=[], ~fileName="test.txt") @@ -63,9 +64,12 @@ let _ = entries ->Array.fromIterable ->Array.forEach(((key, value)) => { - switch EntryValue.decode(value) { - | Some(String(s)) => Console.log(`${key}: ${s}`) - | Some(File(f)) => Console.log(`${key}: [WebApiFile] ${File.name(f)}`) - | None => Console.log(`${key}: unsupported entry`) + switch EntryValue.asString(value) { + | Some(s) => Console.log(`${key}: ${s}`) + | None => + switch EntryValue.asFile(value) { + | Some(f) => Console.log(`${key}: [WebApiFile] ${File.name(f)}`) + | None => Console.log(`${key}: unsupported entry`) + } } }) diff --git a/tests/browser/BindingContracts__test.res b/tests/browser/BindingContracts__test.res index 086eb157..254780f4 100644 --- a/tests/browser/BindingContracts__test.res +++ b/tests/browser/BindingContracts__test.res @@ -117,9 +117,9 @@ let formData = FormData.make() FormData.append(formData, ~name="text", ~value="native string") switch FormData.get(formData, "text")->Null.toOption { | Some(value) => - switch FormDataEntryValue.decode(value) { - | Some(FormDataEntryValue.String(text)) => assert(text == "native string") - | Some(FormDataEntryValue.File(_)) | None => assert(false) + switch FormDataEntryValue.asString(value) { + | Some(text) => assert(text == "native string") + | None => assert(false) } | None => assert(false) } @@ -128,9 +128,9 @@ let file = File.make(~fileBits=[Blob.blobPartFromString("native file")], ~fileNa FormData.appendBlob(formData, ~name="file", ~blobValue=File.asBlob(file), ~filename="contract.txt") switch FormData.get(formData, "file")->Null.toOption { | Some(value) => - switch FormDataEntryValue.decode(value) { - | Some(FormDataEntryValue.File(decodedFile)) => assert(File.name(decodedFile) == "contract.txt") - | Some(FormDataEntryValue.String(_)) | None => assert(false) + switch FormDataEntryValue.asFile(value) { + | Some(decodedFile) => assert(File.name(decodedFile) == "contract.txt") + | None => assert(false) } | None => assert(false) }