Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add small canvas sample #7

Merged
merged 9 commits into from
Nov 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 1 addition & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,7 @@ jobs:
- name: Install dependencies
run: npm ci

- name: Install tool dependencies
working-directory: ./tools/TypeScript-DOM-lib-generator
run: npm ci

- name: Generate ReScript code
working-directory: ./tools/TypeScript-DOM-lib-generator
- name: Build ReScript code
run: npm run build

- name: Run tests
Expand Down
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@
/lib/
.bsb.lock
.astro
dist/
dist/
tmp/
15 changes: 15 additions & 0 deletions astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,21 @@ export default defineConfig({
editLink: {
baseUrl: 'https://github.com/rescript-lang/experimental-rescript-webapi/edit/main/',
},
sidebar: [
{
slug: '',
},
{
slug: 'design-philosophy',
},
{
slug: 'project-status',
},
{
label: 'Contributing',
autogenerate: { directory: 'contributing' },
},
],
customCss: ["./docs/styles/fonts.css", "./docs/styles/theme.css"],
expressiveCode: {
shiki: {
Expand Down
149 changes: 149 additions & 0 deletions docs/content/docs/contributing/api-modelling.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
---
title: API Modelling
description: Learn more about the API modelling process of @rescript/webapi.
slug: "04-api-modelling"
---

import { Aside, Code, Icon } from "@astrojs/starlight/components";

One of this projects goals is to provide a consistent and idiomatic API for the Web APIs.
The interopt story of ReScript is quite good, but it it has limitations.
JavaScript is a dynamic language and has a lot of flexibility.
ReScript is a statically typed language and has to model the dynamic parts of JavaScript in a static way.

## Dynamic parameters and properties

Some Web APIs have a parameter or property that can be multiple things.
In ReScript, you would model this as a variant type. This is an example wrapper around values with a key property to discriminate them.

In JavaScript, this strictness is not enforced and you can pass a string where a number is expected.
There are multiple strategies to model this in ReScript and it depends on the specific API which one is the best.

### Overloads

One example is [addEventListener](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener).
This can access either a boolean or an object as the third parameter.

```js
addEventListener(type, listener);
addEventListener(type, listener, options);
addEventListener(type, listener, useCapture);
```

Because, this is a method, we can model this as an overloaded function in ReScript.
The first two overloads are the same, so we can merge them into one with an optional options parameter.

```ReScript
@send
external addEventListener: (
htmlButtonElement,
eventType,
eventListener<'event>,
~options: addEventListenerOptions=?,
) => unit = "addEventListener"
```

The third overload takes a boolean and is worth using when you want to change the default of the `useCapture` boolean parameter.
We can use [a fixed argument](https://rescript-lang.org/docs/manual/latest/bind-to-js-function#fixed-arguments) to model this.

```ReScript
@send
external addEventListener_useCapture: (
htmlButtonElement,
~type_: eventType,
~callback: eventListener<'event>,
@as(json`true`) _,
) => unit = "addEventListener"
```

<Aside type="caution" title="Improving methods">
Be aware that inherited interfaces duplicate their inherited methods. This
means that improving `addEventListener` in the `EventTarget` interface
requires to update all interfaces that inherit from `EventTarget`.
</Aside>

### Decoded variants

We can be pragmatic with overloaded functions and use model them in various creative ways.
For properties, we **cannot do this unfortunately**. A propery can only be defined once and have a single type.

The strategy here is to use a decoded variant.

Example for the [fillStyle](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/fillStyle) property of the `CanvasRenderingContext2D` interface can be either a:

- `string`
- `CanvasGradient`
- `CanvasPattern`

These types are not all primitives and thus we cannot define it as [untagged variants](https://rescript-lang.org/docs/manual/latest/variant#untagged-variants).
What we can do instead is represent the type as an empty type and use a helper module to interact with this.

export const fillStyleDef = `
type fillStyle

type canvasRenderingContext2D = {
// ... other propeties
mutable fillStyle: fillStyle
}
`;

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

When we wish to read and write the `fillStyle` property, we can use a helper module to lift the type to an actual ReScript variant:

export const fillStyleModule = `
open Prelude
open CanvasAPI
open DOMAPI

external fromString: string => fillStyle = "%identity"
external fromCanvasGradient: canvasGradient => fillStyle = "%identity"
external fromCanvasPattern: canvasGradient => fillStyle = "%identity"

type decoded =
| String(string)
| CanvasGradient(canvasGradient)
| CanvasPattern(canvasPattern)

let decode = (t: fillStyle): decoded => {
if CanvasGradient.isInstanceOf(t) {
CanvasGradient(unsafeConversation(t))
} else if CanvasPattern.isInstanceOf(t) {
CanvasPattern(unsafeConversation(t))
} else {
String(unsafeConversation(t))
}
}
`

<Code
code={fillStyleModule}
title="DOMAPI/FillStyle.res"
lang="ReScript"
></Code>

We can now use `FillStyle.decode` to get the actual value of the `fillStyle` property.
And use `FillStyle.fromString`, `FillStyle.fromCanvasGradient`, and `FillStyle.fromCanvasPattern` to set the value.

```ReScript
let ctx = myCanvas->HTMLCanvasElement.getContext_2D

// Write
ctx.fillStyle = FillStyle.fromString("red")

// Read
switch ctx.fillStyle->FillStyle.decode {
| FillStyle.String(color) => Console.log(`Color: ${color}`)
| FillStyle.CanvasGradient(_) => Console.log("CanvasGradient")
| FillStyle.CanvasPattern(_) => Console.log("CanvasPattern")
}
```

<Icon
name="information"
color="var(--sl-color-text-accent)"
class="inline-icon"
size="1.5rem"
/>
Try and use `decoded` and `decode` as conventions for the type and function
names.
86 changes: 86 additions & 0 deletions docs/content/docs/contributing/code-generation.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
---
title: Code Generation
description: Learn more about the code generation process for @rescript/webapi.
slug: "03-code-generation"
---

The original bindings were generated using a modified version of [TypeScript-DOM-lib-generator](https://github.com/microsoft/TypeScript-DOM-lib-generator).
These bindings were a great starting point, but they are not perfect.
It is more than likely that you will need to **tweak the generated bindings by hand** to make them more idiomatic to ReScript.

For example the `window.fetch` function was generated as:

```ReScript
/**
[Read more on MDN](https://developer.mozilla.org/docs/Web/API/Window/fetch)
*/
@send
external fetch: (window, ~input: request, ~init: requestInit=?)
=> Promise.t<response> = "fetch"

/**
[Read more on MDN](https://developer.mozilla.org/docs/Web/API/Window/fetch)
*/
@send
external fetch2: (window, ~input: string, ~init: requestInit=?)
=> Promise.t<response> = "fetch"
```

While not that bad and usable, it can be improved:

- Rename `fetch2` to `fetch` because it is the more common usage of the function.
- Rename `fetch` to `fetch_with_request` for clarity that this is the "overload" with a `Request` object.
- Consider removing the named `~input` and `~init` arguments and use positional arguments instead.
Motivation: If the function does not have any parameters with the same type, it is more ergonomic to use positional arguments.
This heuristic is not set in stone and can be adjusted based on the specific function.
- The documentation can be improved.

```ReScript
/** TODO: add better docs */
@send
external fetch: (window, string, ~init: requestInit=?)
=> Promise.t<response> = "fetch"

/** TODO: add better docs */
@send
external fetch_withRequest: (window, request, ~init: requestInit=?)
=> Promise.t<response> = "fetch"
```

Once these changes are made, the bindings can be tested and then committed to the repository.
The generation does no longer happen automatically, so manual improvements will not be overwritten.

## Sandboxed Code Generation

Not every API was covered by the TypeScript-DOM-lib-generator.
Potentially, you want to add a new API to the bindings and star from code generation.

In [emitter.ts](https://github.com/rescript-lang/experimental-rescript-webapi/blob/main/tools/TypeScript-DOM-lib-generator/src/build/emitter.ts),
you can override the `interfaceHierarchy` with any new interface or type you want to add.

```typescript
interfaceHierarchy = [
{
name: "Temp",
entries: [
enums(["WebGLPowerPreference"]),
dictionaries([
"ImageBitmapRenderingContextSettings",
"WebGLContextAttributes",
]),
],
opens: [],
},
];
```

After running the generator (in `tools/TypeScript-DOM-lib-generator`):

```shell
npm run build
```

All the generated files will be in the `tmp` folder. You can use this as inspiration for your own bindings.

To figure out if you need `enums`, `dictionaries`, or `interfaces`, you can look at the [JSON dump](https://github.com/rescript-lang/experimental-rescript-webapi/blob/main/tools/TypeScript-DOM-lib-generator/dom-dump.json) file
and see how the TypeScript-DOM-lib-generator represents the shape you are after.
46 changes: 46 additions & 0 deletions docs/content/docs/contributing/documentation.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
---
title: "Documentation"
description: Learn more about the relevance of adding documentation to @rescript/webapi.
slug: "06-documentation"
---

After the bindings are generated, all you got was a link to the MDN documentation.
While again, not that bad, it can be improved by adding the core of the binding to the documentation.
And explain how it is supposed to be used.

## Importance

One could wonder how important documentation is in this case?
A link to MDN is enough, right? Well, not always.
When a method has multiple overloads, it can be hard to understand which one to use.
By adding an example usage, the user can see easily see which one to use.
It keeps the user inside the IDE and avoids context switching.

## Structure

The documentation for each binding should roughly follow this structure:

- signature
- key description (tip: check MDN for inspiration)
- example usage
- link to the MDN documentation

For example, the `window.fetch` function could be documented as:

````ReScript
/*
`fetch(string, init)`

Starts the process of fetching a resource from the network,
returning a promise that is fulfilled once the response is available.

```res
window->Window.fetch("https://rescript-lang.org")
```

[Read more on MDN](https://developer.mozilla.org/docs/Web/API/Window/fetch)
*/
@send
external fetch: (window, string, ~init: requestInit=?)
=> Promise.t<response> = "fetch"
````
60 changes: 60 additions & 0 deletions docs/content/docs/contributing/getting-started.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
---
title: Project setup
description: Practical information on how to get started to contribute to the project.
slug: "01-getting-started"
---

import { Aside } from "@astrojs/starlight/components";

<Aside type="caution">
**Real talk**: First time diving into this project? Might feel like a letdown.
But hey, the upside? It’s stupid easy to toss us a PR and make it better!
</Aside>

The [WebAPI](https://developer.mozilla.org/en-US/docs/Web/API) are vast and ever-growing. We need your help to make them better.
There is no way our [contributors](https://github.com/rescript-lang/experimental-rescript-webapi/graphs/contributors) can cover everything.
And that's where you come in! A small PR, focused on what you want to get out of this project can make a huge difference.

## Recommended workflow

We recommend the following overall workflow when developing for this repository:

- Fork this repository
- Always work in your fork
- Always keep your fork up to date

Before updating your fork, run this command:

```shell
git remote add upstream https://github.com/rescript-lang/experimental-rescript-webapi.git
```

This will make management of multiple forks and your own work easier over time.

### Updating your fork

We recommend the following commands to update your fork:

```shell
git checkout main
git clean -xdf
git fetch upstream
git rebase upstream/main
git push
```

Or more succinctly:

```shell
git checkout main && git clean -xdf && git fetch upstream && git rebase upstream/main && git push
```

This will update your fork with the latest from `rescript-lang/experimental-rescript-webapi` on your machine and push those updates to your remote fork.

## Initial build

Install the dependencies and compile the bindings using [Rewatch](https://github.com/rescript-lang/rewatch):

```shell
npm install && npm run build
```
Loading