ggsql / ggsql-wasm
CLAUDE.md566 starsChanged 52 days agoMITView on GitHub
Compiles the ggsql core to WebAssembly for browsers. It powers the playground at /doc/wasm/ and is published as the ggsql-wasm npm package. Plots use ggsql's own SVG renderer; there is no JavaScript charting library. End-user playground: <https://ggsql.org/wasm/>. This file describes the build. pkg/ is the npm package, not generated wasm-pack output. Its source, manifest, license and lock file are committed. Only pkg/dist/, demo/dist/ and doc/wasm/ are generated and ignored. The package entry point is pkg/src/ggsql.ts. It wraps wasm-bindgen's init and…
- Installs packages
Files
ggsql-wasm/ — WebAssembly bindings
Compiles the ggsql core to WebAssembly for browsers. It powers the playground
at /doc/wasm/ and is published as the ggsql-wasm npm
package. Plots use ggsql's own SVG renderer; there is no JavaScript charting
library.
End-user playground: https://ggsql.org/wasm/. This file describes the build.
Layout
ggsql-wasm/
├── Cargo.toml cdylib; ggsql with default-features = false + svg, webfonts, sqlite, builtin-data, spatial
├── build-wasm.sh cargo → wasm-bindgen → wasm-opt → package → demo → doc/wasm
├── src/lib.rs wasm-bindgen API; receives JS converters during init
├── fonts/ Roboto faces served beside the wasm
├── pkg/ checked-in source for the published npm package
│ ├── package.json package manifest, including the release version
│ ├── build.mjs bundles the TypeScript client and copies fonts
│ ├── tsconfig.json emits declarations into dist/
│ ├── src/ client, converters and extension loader
│ └── dist/ generated publish contents (gitignored)
└── demo/ browser demo and Quarto integration
├── package.json consumes file:../pkg
├── build.mjs
└── src/pkg/ is the npm package, not generated wasm-pack output. Its source, manifest,
license and lock file are committed. Only pkg/dist/, demo/dist/ and
doc/wasm/ are generated and ignored.
The package entry point is pkg/src/ggsql.ts. It wraps wasm-bindgen's init and
initSync, supplies the CSV and Parquet converters to Rust through
setConverters, wires the extension loader, re-exports the generated API, and
adds PlotView and font loading. wasm-bindgen writes its glue directly into
pkg/dist/, next to the client bundle and generated declarations.
The re-export list is written out rather than export *, so that
setConverters stays internal: it is the hook init uses to hand the
converters to Rust, and a caller who replaced them would break every registered
format. Adding a binding to lib.rs therefore means adding it to that list too.
Toolchain
- Rust stable, not the workspace 1.86 MSRV. The nested
rust-toolchain.tomlselects stable for builds run from this directory. The wasm crate has norust-version. - Rust target
wasm32-unknown-unknown. wasm-bindgen-cliwith exactly the version recorded forwasm-bindgenin/Cargo.lock. The build checks this because the CLI and crate schema versions must agree. Install the required version with:cargo install -f wasm-bindgen-cli --version <version>- A clang/LLVM with wasm backend support. The build script verifies it with a one-line compile probe, and on macOS falls back to Homebrew's LLVM by itself (see below).
wasm-optfrom binaryen for the-Ozoptimization step. This is notwasm-tools, which is a different project and has no equivalent; usebrew install binaryenorcargo install wasm-opt. The optimizer is passed the wasm features rustc emits by name rather than--all-features: that flag turns on everything binaryen knows, and binaryen 132 emits compact imports under it, which browsers refuse to compile ("Invalid import kind 127").- Node.js for
pkg/anddemo/.
Apple's clang has no wasm backend, so on macOS the probe fails for the default
compiler. check_compiler then looks for $(brew --prefix llvm)/bin/clang and
uses that instead, exporting CC, AR and the directory on PATH — cc-rs
resolves llvm-ar by name for a wasm target, so CC on its own is not enough.
Install it with brew install llvm; nothing needs exporting by hand.
The fallback only applies when the caller set no CC. An explicit CC that
cannot target wasm is reported rather than overridden, so naming a compiler
still means getting that compiler.
Build
The full build is:
cd ggsql-wasm
./build-wasm.shIt runs these phases in order:
cargo build --target wasm32-unknown-unknown --profile wasm -p ggsql-wasm.wasm-bindgen --target web --keep-lld-exportsintopkg/dist/.--keep-lld-exportspreserves symbols needed by loadable SQLite extensions.wasm-opt -Ozonpkg/dist/ggsql_wasm_bg.wasm.npm install && npm run buildinpkg/. esbuild bundles the TypeScript client and hyparquet, TypeScript emits declarations, and the fonts are copied intodist/fonts/.- Downloads or reuses
mod_spatialite.wasmunder/target/wasm-extensions/. npm install && npm run buildindemo/, then copies SpatiaLite intodemo/dist/.- Copies
demo/dist/to/doc/wasm/.
Phase 4 also stamps pkg/package.json's version from the crate's, so the npm
version cannot drift from the Rust one; cargo pkgid reads it, and the npm
install that follows carries it into pkg/package-lock.json.
Flags:
--skip-binaryreusespkg/dist/ggsql_wasm_bg.wasmand rebuilds the client and demo (phases 4–7). Use it while iterating on TypeScript or demo code. It fails until a full build has produced the glue and wasm once.--skip-optcompiles the binary but skips the optimization pass.
Each prerequisite is checked only when the step that needs it will run, which is
what makes those flags worth having: --skip-binary needs neither clang,
wasm-bindgen nor wasm-opt, and --skip-opt does not need wasm-opt. CI's
non-release wasm job takes --skip-opt and installs no binaryen at all.
pkg/tsconfig.json uses rootDirs: ["./src", "./dist"] so the client import
of ./ggsql_wasm.js resolves to wasm-bindgen's declaration in dist/.
Consequently npm run typecheck in pkg/ requires a prior wasm-bindgen run;
the glue is deliberately not hand-maintained.
Fonts are the thing that surprises people
A browser enumerates no system fonts. The font collection starts empty,
sans-serif resolves to nothing, and a plot is drawn with no text and incorrect
layout measurements.
registerDefaultFonts() fetches the four Roboto faces in fonts/, registers
them with the Rust shaper, points sans-serif at the family names returned by
the font files, and declares matching browser @font-face rules. PlotView
then names the registered family on the SVG root so browser rendering uses the
same face that ggsql measured.
Three details are load-bearing:
registerFontreturns family names, and they matter. A generic is an indirection through the font context rather than a name, so registering Roboto does not on its own makesans-serifmean Roboto —setGenericFamilydoes, and it takes names. The only place a family's name exists is inside the file; guessing it from the filename resolves to nothing at shaping time.- One file per weight and style. The shaper selects faces by weight, width
and style and does not understand CSS
unicode-range; multiple subsets sharing a family name can let one without basic Latin win the attribute match, turning every tick label to tofu while the bold title still renders. - The browser has to resolve the same face the shaper measured. Every run is
placed with one anchor plus
textLength, so the browser fits whatever it resolves into the width the shaper measured — a different face gets horizontally scaled to fit, which reads as plausible and is wrong differently on every platform. Hence both the@font-facerules and the family named on the SVG root.
registerFontFromUrl(url, { genericFor }) supports a page's own typography.
The webfonts feature on ggsql is what lets registerFont take the WOFF and
WOFF2 containers a font CDN serves as well as sfnt bytes, unwrapping them before
the shaper sees them; it costs about 55 kB brotli, and is worth it because WOFF2
is how a font arrives at a web page. Without the feature a container is refused
by name rather than reaching the shaper and silently registering nothing.
Font registration is process-global, permanent, and must happen before the first
draw.
Wasm-specific constraints
Cargo.toml contains wasm32-only dependency overrides:
getrandomanduuiduse their JavaScript randomness support.sqlite-wasm-rsprovides SQLite in the browser.tokiodisables its default host I/O features.
ODBC is unavailable in the browser.
The svg writer rather than a rasterising one: it needs no GPU adapter and no
WebGL2 context, and it pulls no vello_hybrid, vello_common, glifo or
naga — 25 fewer crates than the canvas path. It also puts no ceiling on how
many plots a page can show, which a docs page carrying several would otherwise
run into. The cost is one DOM element per mark, so a very dense plot is heavier
here than it would be on a canvas.
The CSV and Parquet converters live in TypeScript, and the package's init
hands them to Rust with setConverters. So a page has to enter through the
package: a context built on the glue's own init has no converters, and
register_csv, register_parquet and register_builtin_datasets fail with
"CSV and Parquet converters are not configured".
Distribution
- npm: publish
pkg/.package.jsonincludes onlydist/, while npm automatically includesLICENSEand the manifest. Its version is stamped from the crate's bybuild-wasm.sh, so there is nothing to bump by hand. - GitHub Releases: the wasm binary is also attached to releases.
- Docs:
build-wasm.shcopiesdemo/dist/to/doc/wasm/, which Quarto serves. Both are generated.
See also
/CLAUDE.md— workspace overview./src/CLAUDE.md— the underlyingggsqllibrary./doc/CLAUDE.md— docs and playground embedding.
In this repository7 other files it gives its agents.
- CLAUDE.mdCLAUDE.md
- doc/CLAUDE.mdCLAUDE.md
- ggsql-cli/CLAUDE.mdCLAUDE.md
- ggsql-jupyter/CLAUDE.mdCLAUDE.md
- ggsql-vscode/CLAUDE.mdCLAUDE.md
- src/CLAUDE.mdCLAUDE.md
- tree-sitter-ggsql/CLAUDE.mdCLAUDE.md
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

