packages feed

ghc-wasm-bindgen 0.2.0.0 → 0.3.0.0

raw patch · 9 files changed

+70/−50 lines, 9 files

Files

README.md view
@@ -10,9 +10,7 @@  ```ts import { area, Circle, largest, Rect, scale } from "shape";-import { init } from "shape/runtime"; -await init(wasmUrl); area(Rect({ width: 3, height: 4 }));  // 12 scale(2, Circle(1));                  // { $tag: "Circle", contents: 2 } largest([]);                          // null@@ -59,7 +57,7 @@ - An argument that does not fit its type (an out-of-range integer, a `Char`   that is not one code point, an unknown constructor, …) throws a `TypeError`   before reaching Haskell.-- An exception in Haskell throws a `HaskellError`.+- An exception in Haskell throws a `HaskellError` (exported by the bindings).  ## Usage @@ -85,16 +83,11 @@     "version": "0.1.0",     "type": "module",     "exports": {-        ".": { "types": "./src/shape.d.ts", "default": "./src/shape.js" },-        "./runtime": { "types": "./src/ghc_wasm_bindgen_runtime.d.ts", "default": "./src/ghc_wasm_bindgen_runtime.js" },-        "./shape.wasm": "./src/shape.wasm"+        ".": { "types": "./src/shape.d.ts", "default": "./src/shape.js" }     } } ``` -Then import `shape`, `shape/runtime` (`init`, `HaskellError`) and-`shape/shape.wasm`.- ### `generate`: plain source files  ```sh@@ -111,6 +104,25 @@ "$(wasm32-wasi-ghc --print-libdir)"/post-link.mjs -i shape.wasm -o out/ghc_wasm_jsffi.js wasm-opt -Oz shape.wasm -o out/shape.wasm ```++`shape.wasm` must end up next to `shape.js`.++## Loading++`shape.js` loads `shape.wasm` from its own directory with a top-level+`await`, so importing it is enough: the functions are ready to call. If the+`.wasm` cannot be loaded, the import fails.++This needs ES2022 modules (top-level `await`): current browsers, Node 20.16++or 22.3+, and bundlers targeting ES2022 or later. The `.wasm` is referenced as+`new URL("./shape.wasm", import.meta.url)`:++- webpack 5, Vite and Parcel copy it to the output on their own. With Vite,+  exclude the package from dependency pre-bundling+  (`optimizeDeps: { exclude: ["shape"] }`), which would break that reference+  in development.+- esbuild leaves the reference as is: copy `shape.wasm` next to the bundle+  (the [example](example/) does this).  ## Installation 
app/Main.hs view
@@ -21,7 +21,8 @@     , "          (ghc-wasm-meta) on PATH."     , "generate  For plain source files: writes NameWasmBindings.hs and the Haskell"     , "          runtime to --hs-out, NAME.js, NAME.d.ts and the JS runtime to --js-out."-    , "          Compiling them is up to you."+    , "          Compiling them is up to you; NAME.js loads NAME.wasm from its own"+    , "          directory."     , ""     , "  MODULE.hs      module whose export list is the API (e.g. src/ShapeWasm.hs)"     , "  --name NAME    package and file name (e.g. shape)"@@ -56,7 +57,7 @@         | null (getModule o) -> go rest (setModule f o) setModule getModule setName flags         | otherwise -> Left ("unexpected argument " ++ f)     name n-      | n `elem` ["ghc_wasm_bindgen_runtime", "ghc_wasm_bindgen_internal", "ghc_wasm_jsffi"] = Left ("--name " ++ n ++ " would overwrite the runtime files")+      | n `elem` ["ghc_wasm_bindgen_internal", "ghc_wasm_jsffi"] = Left ("--name " ++ n ++ " would overwrite the runtime files")       | null n || not (all (\c -> isAlphaNum c || c `elem` ("_-" :: String)) n) = Left ("--name must be letters, digits, _ or -: " ++ show n)       | otherwise = Right n 
ghc-wasm-bindgen.cabal view
@@ -1,6 +1,6 @@ cabal-version:      3.0 name:               ghc-wasm-bindgen-version:            0.2.0.0+version:            0.3.0.0 synopsis:           JavaScript/TypeScript bindings for GHC's WASM backend description:     Generate JavaScript and TypeScript bindings for a pure Haskell module@@ -22,8 +22,6 @@ extra-doc-files:     README.md extra-source-files:-    runtime-js/ghc_wasm_bindgen_runtime.js-    runtime-js/ghc_wasm_bindgen_runtime.d.ts     runtime-js/ghc_wasm_bindgen_internal.js     runtime-js/ghc_wasm_bindgen_internal.d.ts     runtime-js/ghc_wasm_jsffi.d.ts
runtime-js/ghc_wasm_bindgen_internal.d.ts view
@@ -1,9 +1,10 @@ // ghc-wasm-bindgen runtime internals, imported by the generated bindings. -import type { Either } from "./ghc_wasm_bindgen_runtime";+/** `Either e a` from Haskell. */+export type Either<E, A> = { Left: E } | { Right: A }; -/** Instantiates the .wasm module; must resolve before any binding is called. */-export function init(source: string | URL | BufferSource): Promise<void>;+/** Instantiates the .wasm module; called by the generated module. */+export function load(url: URL): Promise<void>;  /** A Haskell exception raised during a call. */ export class HaskellError extends Error {}
runtime-js/ghc_wasm_bindgen_internal.js view
@@ -3,21 +3,13 @@ import jsffiImports from "./ghc_wasm_jsffi.js";  let wasm;-let ready; -export function init(source) {-    return (ready ??= load(source));-}--async function load(source) {-    if (typeof source === "string" || source instanceof URL) {-        const res = await fetch(source);-        if (!res.ok) throw new Error(`ghc-wasm-bindgen: fetching ${source} failed (${res.status})`);-        source = await res.arrayBuffer();-    }+// Instantiates the .wasm module. Called once, by the generated module's+// top-level await, so the bindings are ready as soon as it is imported.+export async function load(url) {     // jsffiImports needs the exports, which exist only after instantiation.     const knot = {};-    const { instance } = await WebAssembly.instantiate(source, {+    const { instance } = await WebAssembly.instantiate(await read(url), {         ghc_wasm_jsffi: jsffiImports(knot),         wasi_snapshot_preview1: wasi(knot),     });@@ -26,6 +18,16 @@     wasm = knot; } +// Node's fetch does not read file: URLs. getBuiltinModule (rather than an+// import) keeps bundlers from trying to resolve node:fs for the browser.+async function read(url) {+    const fs = url.protocol === "file:" && globalThis.process?.getBuiltinModule?.("node:fs/promises");+    if (fs) return fs.readFile(url);+    const res = await fetch(url);+    if (!res.ok) throw new Error(`ghc-wasm-bindgen: fetching ${url} failed (${res.status})`);+    return res.arrayBuffer();+}+ // WASI subset the RTS uses for pure code; other calls return ENOSYS. function wasi(exports) {     const view = () => new DataView(exports.memory.buffer);@@ -72,7 +74,6 @@  // __ghcWasmBindgenError is set by GhcWasmBindgen.Runtime.guarded. export function call(fn, ...args) {-    if (!wasm) throw new Error("ghc-wasm-bindgen: await init() before calling the API");     const result = wasm[fn](...args);     const err = wasm.__ghcWasmBindgenError;     if (err !== undefined) {
− runtime-js/ghc_wasm_bindgen_runtime.d.ts
@@ -1,5 +0,0 @@-// Public interface of the ghc-wasm-bindgen runtime.--export { init, HaskellError } from "./ghc_wasm_bindgen_internal";--export type Either<E, A> = { Left: E } | { Right: A };
− runtime-js/ghc_wasm_bindgen_runtime.js
@@ -1,3 +0,0 @@-// Public interface of the ghc-wasm-bindgen runtime.--export { init, HaskellError } from "./ghc_wasm_bindgen_internal.js";
src/GhcWasmBindgen.hs view
@@ -27,6 +27,7 @@ import Data.FileEmbed (embedFile) import Data.List (isSuffixOf, stripPrefix) import Data.Maybe (mapMaybe)+import qualified Data.Text as T import qualified Data.Text.Encoding as TE import System.Directory import System.Exit (ExitCode (..))@@ -149,10 +150,8 @@ -- | The JS bindings and runtime, by file name. jsFiles :: String -> Api -> [(FilePath, ByteString)] jsFiles name api =-  [ (name <.> "js", TE.encodeUtf8 (emitJs api))+  [ (name <.> "js", TE.encodeUtf8 (emitJs (T.pack name) api))   , (name <.> "d.ts", TE.encodeUtf8 (emitDts api))-  , ("ghc_wasm_bindgen_runtime.js", $(embedFile "runtime-js/ghc_wasm_bindgen_runtime.js"))-  , ("ghc_wasm_bindgen_runtime.d.ts", $(embedFile "runtime-js/ghc_wasm_bindgen_runtime.d.ts"))   , ("ghc_wasm_bindgen_internal.js", $(embedFile "runtime-js/ghc_wasm_bindgen_internal.js"))   , ("ghc_wasm_bindgen_internal.d.ts", $(embedFile "runtime-js/ghc_wasm_bindgen_internal.d.ts"))   , ("ghc_wasm_jsffi.d.ts", $(embedFile "runtime-js/ghc_wasm_jsffi.d.ts"))
src/GhcWasmBindgen/EmitJavaScript.hs view
@@ -1,6 +1,6 @@--- | Emits the JS side of the bindings: @NAME.js@ (constructors, an--- encode/decode function per type, the API functions) and @NAME.d.ts@ (their--- TypeScript types).+-- | Emits the JS side of the bindings: @NAME.js@ (loading of @NAME.wasm@,+-- constructors, an encode/decode function per type, the API functions) and+-- @NAME.d.ts@ (their TypeScript types). -- -- The generated code never sees how values cross to Haskell: it converts -- them with the encode/decode functions of ghc_wasm_bindgen_internal.js, which@@ -16,19 +16,35 @@ header :: Api -> Text header api = "// Generated by ghc-wasm-bindgen from " <> apiModule api <> ". Do not edit." --- | @NAME.js@-emitJs :: Api -> Text-emitJs api =+-- | @NAME.js@. Loads @NAME.wasm@, found next to it, with a top-level await:+-- importing the module is enough to use it.+emitJs :: Text -> Api -> Text+emitJs name api =   T.unlines $-    [header api, "", "import * as $rt from \"./ghc_wasm_bindgen_internal.js\";", ""]+    [ header api+    , ""+    , "import * as $rt from \"./ghc_wasm_bindgen_internal.js\";"+    , ""+    , "export { HaskellError } from \"./ghc_wasm_bindgen_internal.js\";"+    , ""+    , "await $rt.load(new URL(" <> quote ("./" <> name <> ".wasm") <> ", import.meta.url));"+    , ""+    ]       ++ concatMap jsType (apiTypes api)       ++ intercalate [""] (map jsFunction (apiFns api)) --- | @NAME.d.ts@+-- | @NAME.d.ts@. Its imports name the @.js@ file, as @node16@/@nodenext@+-- module resolution requires (and @bundler@ accepts). emitDts :: Api -> Text emitDts api =   T.unlines $-    [header api, "", "import type * as $rt from \"./ghc_wasm_bindgen_runtime\";", ""]+    [ header api+    , ""+    , "import type * as $rt from \"./ghc_wasm_bindgen_internal.js\";"+    , ""+    , "export { HaskellError, type Either } from \"./ghc_wasm_bindgen_internal.js\";"+    , ""+    ]       ++ concatMap dtsType (apiTypes api)       ++ map dtsFunction (apiFns api)