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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 41 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
[![NPM Version](https://img.shields.io/npm/v/@rescript/webapi/experimental)](https://www.npmjs.com/package/@rescript/webapi)

# experimental-rescript-webapi
# ReScript WebAPI

Experimental successor to [rescript-webapi](https://github.com/TheSpyder/rescript-webapi)

This package requires ReScript 13, which is currently in alpha.

## Getting started

Install the package using your favorite package manager:
Expand All @@ -12,22 +14,56 @@ Install the package using your favorite package manager:
npm i @rescript/webapi@experimental
```

and add `@rescript/webapi` to your `rescript.json`:
and add `@rescript/webapi` to your `rescript.json` with the features your app uses:

```json
{
"dependencies": [
"@rescript/webapi"
{
"name": "@rescript/webapi",
"features": ["WebAPI.Crypto", "WebAPI.Location"]
}
]
}
```

You can also open the namespace globally if you prefer global access to modules such as `Location` instead of using `WebAPI.Location`. Another option is to use `open WebAPI` when working with the `WebAPI` namespace.

```json
{
"compiler-flags": ["-open WebAPI"]
}
```

## Usage

The package exposes browser APIs under the `WebAPI` namespace. Use the module that owns the
browser interface, access record fields with `.`, and call global singleton methods directly.

```rescript
let location = WebAPI.Location.current
let href = location.href

WebAPI.Location.reload()
```

With `"-open WebAPI"`, the same code can be written without the `WebAPI.` prefix:

```rescript
let location = WebAPI.Window.current->WebAPI.Window.location
let location = Location.current
let href = location.href
location->WebAPI.Location.reload

Location.reload()
```

Object-owned APIs still use the value as the receiver. For example, if you also enable
`WebAPI.DOM`, you can work with document and element values like this:

```rescript
let document = WebAPI.Window.current->WebAPI.Window.document
let button = document->WebAPI.Document.createElement("button")

button->WebAPI.Element.setAttribute(~qualifiedName="type", ~value="button")
```

## Documentation
Expand Down
47 changes: 37 additions & 10 deletions docs/content/docs/api-surface.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,43 @@ description: A practical summary of how to use the public @rescript/webapi modul
slug: "api-surface"
---

The package exposes browser APIs under the `WebAPI` namespace. In normal application code,
add the package to `rescript.json`:
The package exposes browser APIs under the `WebAPI` namespace. It requires ReScript 13,
which is currently in alpha.

In normal application code, add the package to `rescript.json` with the features your app
uses:

```json
{
"dependencies": [
{
"name": "@rescript/webapi",
"features": ["WebAPI.Crypto", "WebAPI.Location"]
}
]
}
```

The `features` list controls which Web API source groups are available to your project. Add
each feature you use, for example `WebAPI.DOM`, `WebAPI.Fetch`, `WebAPI.Crypto`, or
`WebAPI.Location`.

You can optionally open the namespace globally if you prefer unqualified module names:

```json
{
"dependencies": ["@rescript/webapi"]
"compiler-flags": ["-open WebAPI"]
}
```

Use `WebAPI.Window.current` for the browser `window`. Interface-specific methods live on
public modules such as `WebAPI.Window`, `WebAPI.Location`, `WebAPI.Document`,
`WebAPI.Element`, `WebAPI.Request`, and `WebAPI.Response`.
Global singleton APIs such as `Location`, `Crypto`, and `Performance` expose direct
functions and properties. Object-owned APIs still use the value as the receiver.

```ReScript
let location = WebAPI.Window.current->WebAPI.Window.location
let location = WebAPI.Location.current
let href = location.href

location->WebAPI.Location.reload
WebAPI.Location.reload()
```

## Public module shape
Expand All @@ -36,6 +55,13 @@ let document = WebAPI.Window.current->WebAPI.Window.document
let element = document->WebAPI.Document.createElement("button")
```

With `"-open WebAPI"`, the same modules can be referenced without the `WebAPI.` prefix:

```ReScript
let location = Location.current
let id = Crypto.randomUUID()
```

Generated implementation modules such as `DomTypes`, `FetchTypes`, `EventTypes`, and
`UiEventsTypes` are internal. If you need to name a public type, use the public interface
module's `t` type when it has one, or use a dedicated public type module. Otherwise, let
Expand Down Expand Up @@ -241,6 +267,7 @@ let redirect = WebAPI.Response.redirect(~url="/login", ~status=302)
## DOM

DOM values are operated on through public interface modules.
`WebAPI.Element` is the method module for element values. When you need to name the element type explicitly, use `WebAPI.DOM.element`; most examples can rely on inference from `Document.createElement`, `Document.querySelector`, and related DOM helpers.

```ReScript
let document = WebAPI.Window.current->WebAPI.Window.document
Expand All @@ -262,8 +289,8 @@ switch maybeButton {
Use conversion helpers when moving between related DOM interface types.

```ReScript
let element = document->WebAPI.Document.createElement("div")
let node = element->WebAPI.Element.asNode
let element: WebAPI.DOM.element = document->WebAPI.Document.createElement("div")
let node: WebAPI.DOM.node = element->WebAPI.Element.asNode
```

## Visual Viewport
Expand Down
8 changes: 3 additions & 5 deletions docs/content/docs/contributing/api-module-structure.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -61,13 +61,11 @@ type rec node = {
// ... more properties
}

and element = {
// duplicated property from node
nodeName: string
// ... more properties
}
and element = Base.element
```

For shared DOM base interfaces such as `Element`, the structural record can live in a Base-owned module such as `Base__Element.res`. The DOM API module then keeps the familiar `DOM.element` alias while method modules use `DomTypes.element`.

## Auxiliary Types

Auxiliary types are used to represent types that are not directly related to the Web API but are used in the bindings.
Expand Down
27 changes: 27 additions & 0 deletions docs/content/docs/contributing/module-type-structure.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,30 @@ external checkValidity: htmlButtonElement => bool = "checkValidity"
`;

<Code code={buttonModule} title="DOMAPI/HTMLButtonElement.res" lang="ReScript"></Code>

## Shared element base

`Element.res` is a method module. It does not define the `element` record type directly. The shared DOM element type is owned by Base and threaded back into DOM through aliases:

```ReScript
// Base.res
type element = Base__Element.element

// DomTypes.res
type element = Base.element

// Element.res
include Impl({type t = DomTypes.element})
```

This keeps `Element.Impl` reusable for element subtypes while giving the package one shared base element type. Subtype modules should continue to include the nearest base method implementation:

```ReScript
// HTMLElement.res
include Element.Impl({type t = DomTypes.htmlElement})

// HTMLButtonElement.res
include HTMLElement.Impl({type t = DomTypes.htmlButtonElement})
```

Use `asElement` when a subtype needs to be passed to a function that expects the shared element type.
29 changes: 22 additions & 7 deletions docs/content/docs/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -34,30 +34,45 @@ Install the package using your favorite package manager:
</TabItem>
</Tabs>

and add `@rescript/webapi` to your `rescript.json`:
This package requires ReScript 13, which is currently in alpha.

Add `@rescript/webapi` to your `rescript.json` with the features your app uses:

export const rescriptJson = `
{
"dependencies": [
"@rescript/webapi"
{
"name": "@rescript/webapi",
"features": ["WebAPI.Crypto", "WebAPI.Location"]
}
]
}
`;

<Code lang="json" code={rescriptJson} ins={[3]}></Code>
<Code lang="json" code={rescriptJson} ins={[4, 5, 6, 7]}></Code>

You can optionally open the namespace globally if you prefer unqualified module names:

export const openWebAPIJson = `
{
"compiler-flags": ["-open WebAPI"]
}
`;

<Code lang="json" code={openWebAPIJson}></Code>

## Usage

After installing the package , you can use bindings for the various Web APIs as defined in [MDN](https://developer.mozilla.org/en-US/docs/Web/API).
After installing the package, you can use bindings for the various Web APIs as defined in [MDN](https://developer.mozilla.org/en-US/docs/Web/API).

export const rescriptSample = `
let location = WebAPI.Window.current->WebAPI.Window.location
let location = WebAPI.Location.current

// Access properties using \`.\`
let href = location.href

// Invoke methods using the \`->TypeModule.method\`
location->WebAPI.Location.reload
// Invoke global singleton methods directly
WebAPI.Location.reload()
`;

export const compiledSample = `
Expand Down
18 changes: 13 additions & 5 deletions docs/content/docs/philosophy.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ 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.

## ReScript v12
## ReScript 13

These bindings utilize new features introduced in ReScript v12; thus, they are not compatible with older versions of ReScript.
These bindings use feature-gated package dependencies and source groups from ReScript 13,
which is currently in alpha. They are not compatible with older versions of ReScript.

## Web APIs

Expand All @@ -21,9 +22,14 @@ The bindings are exposed under the `WebAPI` namespace with the same flat module
```ReScript
open WebAPI.DOM

let myElement: WebAPI.Element.t = document->WebAPI.Document.createElement("div")
let myElement: WebAPI.DOM.element = document->WebAPI.Document.createElement("div")
let id = myElement.id
```

Consumers can also configure `rescript.json` with `"compiler-flags": ["-open WebAPI"]`
when they want to use modules such as `DOM`, `Document`, and `Element` without the
`WebAPI.` prefix.

## 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.
Expand All @@ -34,6 +40,8 @@ Methods are modeled as functions in a separate module. The idea is that these wi

Inherited methods are duplicated in the inheriting module to eliminate the need to cast the type to the base type.

For DOM elements, `WebAPI.Element` is the method module. The element type itself is exposed as `WebAPI.DOM.element`. Most code does not need to annotate the type because functions such as `Document.createElement` and `Document.querySelector` already return it.

### Overloads

JavaScript supports function overloads, where a function can have multiple signatures. In ReScript, this is not possible, and a method can have multiple bindings with slightly different names. By entering the correct name, tooling should detect all variations of the method.
Expand All @@ -45,8 +53,8 @@ In some cases, type conversion will be required. Subtypes can safely be cast to
```ReScript
open WebAPI.DOM

let element: WebAPI.Element.t = document->WebAPI.Document.createElement("div")
let node: WebAPI.Node.t = element->WebAPI.Element.asNode
let element: WebAPI.DOM.element = document->WebAPI.Document.createElement("div")
let node: WebAPI.DOM.node = element->WebAPI.Element.asNode
```

Any other conversions should be treated as unsafe casts and used with caution, because the type system cannot guarantee they are valid at runtime.
1 change: 1 addition & 0 deletions docs/content/docs/project-status.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ slug: "project-status"

This project is highly experimental and subject to change.
It is a work in progress and might not cover every API.
It currently requires ReScript 13, which is also still in alpha.
The core idea is to prioritize ground coverage based on community needs.
We aim to focus on the most used APIs first and then expand from there.

Expand Down
11 changes: 4 additions & 7 deletions docs/llm.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import * as path from "node:path";
import { exec } from "node:child_process";
import { promisify } from "node:util";
import fs from "node:fs/promises";
import { featureSpecs } from "../scripts/unmonorepo/feature-spec.mjs";

const execAsync = promisify(exec);

Expand Down Expand Up @@ -33,7 +32,7 @@ async function getDocJson(filePath) {

async function processFile(filePath) {
const json = await getDocJson(filePath);
const relativePath = path.relative(path.join(import.meta.dirname, ".."), filePath);
const relativePath = normalizeRelativePath(filePath);
const moduleName = moduleNameForFile(relativePath);

const types = [];
Expand Down Expand Up @@ -95,7 +94,6 @@ Module: ${moduleName}${typeString}${functionString}
`;
}

const specByDir = new Map(featureSpecs.map((spec) => [spec.dirName, spec]));
const rootDir = path.join(import.meta.dirname, "..");
const rootConfig = JSON.parse(await fs.readFile(path.join(rootDir, "rescript.json"), "utf-8"));
const publicModulesBySourceDir = new Map(
Expand Down Expand Up @@ -130,14 +128,13 @@ function isPublicFile(filePath) {
}

function moduleNameForFile(relativePath) {
const [, dirName, fileName] = relativePath.split(path.sep);
const spec = specByDir.get(dirName);
const sourceDir = sourceDirForRelativePath(relativePath);

if (!spec) {
if (!sourceDir) {
throw new Error(`Unsupported source directory for documentation: ${relativePath}`);
}

const leafName = path.basename(fileName, ".res");
const leafName = path.basename(relativePath, ".res");

return `WebAPI.${leafName}`;
}
Expand Down
6 changes: 3 additions & 3 deletions docs/superpowers/specs/2026-04-22-unmonorepo-webapi-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ Example:

```json
{
"dependencies": ["@plain/dep", { "name": "@other/heavy", "features": ["WebAPI.WebCrypto"] }]
"dependencies": ["@plain/dep", { "name": "@other/heavy", "features": ["WebAPI.Crypto"] }]
}
```

Expand All @@ -114,7 +114,7 @@ The build remains feature-oriented:
- `WebAPI.Base`
- `WebAPI.DOM`
- `WebAPI.Fetch`
- `WebAPI.WebCrypto`
- `WebAPI.Crypto`
- and the rest of the former package surfaces

The unified build keeps the original flat module surface instead of adding generated feature entry modules. For example, consumers should use modules such as:
Expand All @@ -124,7 +124,7 @@ The unified build keeps the original flat module surface instead of adding gener
- `WebAPI.Headers`
- `WebAPI.URL`

Shared DOM base types should be owned by `DOM`, so common references stay short, for example `DOM.element` instead of `BaseDOM.element` or `Base.DOM.element`.
Shared DOM base types can be owned by `Base` when they are needed across feature boundaries, while `DOM` can keep short public aliases such as `DOM.element`. For example, `Base__Element.element` is exposed through `Base.element`, then reused as `DomTypes.element` and `DOM.element`.

## Internal Module Naming

Expand Down
Loading