Skip to content

RFC: Import attributes for external module FFIs #6500

Closed
@cometkim

Description

@cometkim

Background

ESM Import Attributes has reached stage 3 (Import Assertion is deprecated, so it will never get standardized)

This is important because it can be massively used by various tools around the JavaScript ecosystem and even native ESM modules.

Previously, if we wanted to modify module semantics, we had to customize the complex configuration of the tool like bundlers. But now it can be done at module level.

For example, in the import statement for an image asset we could do this:

import imageDataUrl from './image.png' with { loader: 'url-loader' }
import imageUrl from './image.png' with { loader: 'file-loader' }

Another example is importing WebAssembly assets:

const wasmInstance = await import("foo.wasm", { type: "module", with: { type: "webassembly" } });

// or directly create a module worker
new Worker("foo.wasm", { type: "module", with: { type: "webassembly" } });

ReScript @module FFI currently lacks the resolution for this.

Detailed Design

This proposes a way to express import attributes by extending the existing FFI syntax.

@module syntax

If import attribute is needed, add { with: { ... } } argument to the @module directive.

The argument payload should be validated as a record type

type modulePayload<'a> = {
  from?: string,
  with?: ({..} as 'a),
}
// ModuleBindings.res
@module({ from: "foo.wasm", with: { "type": "webassembly" } }) @val
external wasmInstance: WebAssembly.Instance.t = "default"

@module({ from: "schema.json", with: { "type": "json" } }) @val
external jsonSchema: Json.t = "default"

// allow using it on non-default target
@module({ with: { "my-custom-loader": "i18n" } }) @val
external messages: Messages.t = "i18n.js"

should be compiled to:

import WasmInstance from "foo.wasm" with { type: "webassembly" };
import SchemaJson from "schema.json" with { type: "json" };
import * as Messages from "i18n.js" with { "my-custom-loader": "i18n" };

var wasmInstance = Instance;
var jsonSchema = SchemaJson;
var messages = Messages;

export {
  wasmInstance,
  jsonSchema,
  messages,
};

Dynamic imports

Dynamic imports include full module import (module mod = await MyModule) and partial import syntax (Js.import(MyModule.fn)).

Because ReScript modules are always JavaScript modules, the dynamic imports syntax doesn't need import attributes.

Note on dynamic imports of external modules
await Js.import(ModuleBindings.wasmInstance);

will be compiled to

await import("./ModuleBindings.bs.js").then(function (m) {
  return m.wasmInstance;
});

Which is legit.

However, its behavior isn't semantically equivalent to await import("foo.wasm", { type: "module", with: { type: "webassembly" } }). The ReScript version always requires additional loading of JavaScript modules, which adds unnecessary waterfall.

Maybe we need to reconsider its design or extra syntax to directly import assets.

CommonJS

There is no equivalent semantics in CommonJS. Users can still use the syntax, but the output will not change.

Exports

There are no specific use cases for exports attributes. Since ReScript's expression and output are always about JavaScript.

Maybe useful some integration cases

https://twitter.com/KrComet/status/1750541534074925441

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions