#My Workflow to get JS Docs working with Gleam + JS FFI!

1 messages · Page 1 of 1 (latest)

trim mountain
#

I'm very excited to have finally figured out a pretty solid way to work with .

Disclaimer: Do not do this if you're publishing the generated JS code (f.e. to npm). This will leave invalid references in your JS Doc comments!
This is however fine if you're publishing a gleam package or building an application.

The "trick" is to use import annotation that link to the javascript in the build/dev/javascript directory.

Here is a simplified setup from my https://github.com/daniellionel01/glaze/tree/main/glaze_oat project.

Project structure (I removed all non-relevant modules):

.
├── gleam.toml
├── manifest.toml
├── README.md
└── src
    └── glaze
        ├── oat
        │   ├── globals.d.ts
        │   ├── toast_ffi.mjs
        │   ├── toast.gleam
        │   ├── toast.mjs.d.ts
        └── oat.gleam

globals.d.ts contains a definition for ot which is the toast library I'm using.

toast.gleam (simplified)

// ... Placement Type ...
// ... Variant Type ...

// used in `toast_ffi.mjs`
pub fn variant_to_string(variant: Variant) { ... }
pub fn placement_to_string(placement: Placement) { ... }

pub opaque type Options {
  Options(variant: Variant, placement: Placement, duration_ms: Int)
}

@external(javascript, "./toast_ffi.mjs", "toast")
pub fn toast(title: String, description: String, options: Options) -> Nil

toast_ffi.mjs

//@ts-check

import { variant_to_string, placement_to_string } from "./toast.mjs";

/**
 * @param {string} title - The main heading text of the toast notification.
 * @param {string} description - Additional descriptive text shown below the title.
 * @param {import("../../../build/dev/javascript/glaze_oat/glaze/oat/toast.mjs").Options} options
 *
 * @returns {void}
 */
export function toast(title, description, options) {
  let options_args = {
    variant: variant_to_string(options.variant),
    placement: placement_to_string(options.placement),
    duration: options.duration_ms,
  };
  ot.toast(title, description, options_args);
}
// toast.mjs.d.ts
export * from "../../../build/dev/javascript/glaze_oat/glaze/oat/toast.d.mts";

Without the toast.mjs.d.ts file the import in toast_ffi.mjs from ./toast.mjs will fail.

And in toast_ffi.mjs we link the generated toast.mjs file, since that contains the Options class.

Also you need to enable typescript declarations in gleam.toml if you're working with opaque types.
In my particular case that was because Options is opaque and without typescript declarations it is not exported.

[javascript]
typescript_declarations = true

As long as we build the project regularly, this will keep the references accurate.

Only hickup that is left are the deprecation warnings, because Options is a custom type and that constructor has been deprecated with the new JS FFI API.
I'm not sure if we can do anything about this.

So yeah, quite a bit of work, but it feels very nice to have solid auto completion and warnings lucysparkles

trim mountain
#

When Options is not opaque you can simply do this, without any deprecation warnings:

//@ts-check

import {
  variant_to_string,
  placement_to_string,
  Options,
  Options$Options$duration_ms,
  Options$Options$placement,
  Options$Options$variant,
} from "./toast.mjs";

/**
 * @param {string} title - The main heading text of the toast notification.
 * @param {string} description - Additional descriptive text shown below the title.
 * @param {Options} options
 */
export function toast(title, description, options) {
  let options_args = {
    variant: variant_to_string(Options$Options$variant(options)),
    placement: placement_to_string(Options$Options$placement(options)),
    duration: Options$Options$duration_ms(options),
  };
  ot.toast(title, description, options_args);
}

But the toast.mjs.d.ts file is still vital for the ./toast.mjs import at the top to be picked up by your IDE.

jade scaffold
#

Nice! The goal with that is that is to get autocomplete for imported Gleam modules in JS FFI code is that right?

trim mountain
jade scaffold
#

In typescript I'm using this approach for that:

"rootDirs": ["src", "dev", "test", "build/dev/javascript/wobble"],

this makes it so all those become one "virtual" root directory, so that you can just import your gleam modules from typescript as if you were already in the build directory so to speak.
Might also be possible to do it that way without typescript somehow?