Skip to content

Commit 9d1b405

Browse files
WebAssembly Playground demo and interactive Quarto examples (#185)
* Create Wasm library and demo * Tweak demo website style * Build ggsql playground into quarto documentation * Switch playground to wasm directory * Interactive Quarto examples * Add --skip-opt arg to build-wasm.sh * Playground on right --------- Co-authored-by: Thomas Lin Pedersen <thomas.pedersen@posit.co>
1 parent 3c86cae commit 9d1b405

32 files changed

Lines changed: 4837 additions & 18 deletions

.github/workflows/publish.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,11 @@ on:
88
workflow_dispatch:
99

1010
jobs:
11+
build-wasm:
12+
uses: ./.github/workflows/wasm.yaml
13+
1114
publish-website:
15+
needs: build-wasm
1216
runs-on: ubuntu-latest
1317
permissions:
1418
contents: write
@@ -22,6 +26,12 @@ jobs:
2226
sudo docker image prune --all --force
2327
sudo docker builder prune -a
2428
29+
- name: Download WASM demo artifact
30+
uses: actions/download-artifact@v4
31+
with:
32+
name: wasm-demo
33+
path: doc/wasm
34+
2535
- name: Setup Node.js
2636
uses: actions/setup-node@v4
2737
with:

.github/workflows/wasm.yaml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ on:
66
pull_request:
77
branches: [main]
88
workflow_dispatch:
9+
workflow_call:
910

1011
jobs:
1112
build:
@@ -47,10 +48,24 @@ jobs:
4748
- name: Install wasm-pack
4849
run: cargo install wasm-pack
4950

51+
- name: Build WASM library
52+
working-directory: ggsql-wasm/library
53+
run: npm install && npm run build
54+
5055
- name: Build WASM package
5156
working-directory: ggsql-wasm
5257
run: wasm-pack build --target web --profile wasm --no-opt
5358

5459
- name: Optimise WASM binary
5560
working-directory: ggsql-wasm
5661
run: wasm-opt pkg/ggsql_wasm_bg.wasm -o pkg/ggsql_wasm_bg.wasm -Oz --all-features
62+
63+
- name: Build WASM demo
64+
working-directory: ggsql-wasm/demo
65+
run: npm install && npm run build
66+
67+
- name: Upload demo artifact
68+
uses: actions/upload-artifact@v4
69+
with:
70+
name: wasm-demo
71+
path: ggsql-wasm/demo/dist/

doc/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
/.quarto/
22
**/*.quarto_ipynb
33
_site
4+
wasm

doc/_quarto.yml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
project:
22
type: website
3+
resources:
4+
- wasm/**
35

46
website:
57
title: "ggsql"
@@ -51,6 +53,9 @@ website:
5153
href: gallery/index.qmd
5254
- href: faq.qmd
5355
text: FAQ
56+
right:
57+
- text: Playground
58+
href: wasm/index.html
5459
tools:
5560
- icon: github
5661
menu:
@@ -123,3 +128,14 @@ format:
123128
window.plausible=window.plausible||function(){(plausible.q=plausible.q||[]).push(arguments)},plausible.init=plausible.init||function(i){plausible.o=i||{}};
124129
plausible.init()
125130
</script>
131+
include-after-body:
132+
- text: |
133+
<script type="module">
134+
const offset = document.querySelector('meta[name="quarto:offset"]')?.content || './';
135+
const base = offset + 'wasm/';
136+
const link = document.createElement('link');
137+
link.rel = 'stylesheet';
138+
link.href = base + 'quarto.css';
139+
document.head.appendChild(link);
140+
import(base + 'quarto.js');
141+
</script>

ggsql-wasm/Cargo.toml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,17 @@ crate-type = ["cdylib"]
1313

1414
[dependencies]
1515
wasm-bindgen = "0.2"
16+
wasm-bindgen-futures = "0.4"
1617
js-sys = "0.3"
1718
csv = "1"
18-
polars = { version = "0.52", default-features = false, features = ["sql", "dtype-full"] }
19-
ggsql = { path = "../src", default-features = false, features = ["polars-sql", "vegalite"] }
19+
polars = { version = "0.52", default-features = false, features = ["dtype-full"] }
20+
ggsql = { path = "../src", default-features = false, features = ["vegalite", "sqlite", "builtin-data"] }
21+
serde_json = "1"
2022

2123
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
2224
tokio = { version = "1.35", features = ["full"] }
2325

2426
[target.'cfg(target_arch = "wasm32")'.dependencies]
2527
tokio = { version = "1.35", default-features = false }
28+
sqlite-wasm-rs = "0.5.2"
2629

ggsql-wasm/build-wasm.sh

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5+
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
6+
7+
SKIP_BINARY=false
8+
SKIP_OPT=false
9+
for arg in "$@"; do
10+
case "$arg" in
11+
--skip-binary) SKIP_BINARY=true ;;
12+
--skip-opt) SKIP_OPT=true ;;
13+
*) echo "Unknown argument: $arg" >&2; exit 1 ;;
14+
esac
15+
done
16+
17+
check_wasm32_support() {
18+
local cc="${CC:-clang}"
19+
if ! echo "int main(){return 0;}" | \
20+
"$cc" -target wasm32-unknown-unknown -c -o /dev/null -x c - 2>/dev/null; then
21+
echo "Error: '$cc' does not support the wasm32-unknown-unknown target." >&2
22+
echo "Install an LLVM/clang toolchain with wasm backend support (e.g. 'sudo apt-get install llvm' on Debian/Ubuntu)." >&2
23+
exit 1
24+
fi
25+
}
26+
27+
echo "Building WASM library..."
28+
(cd "$SCRIPT_DIR/library" && npm install && npm run build)
29+
30+
if [ "$SKIP_BINARY" = false ]; then
31+
echo "Checking wasm32 compiler support..."
32+
check_wasm32_support
33+
34+
echo "Building WASM binary..."
35+
(cd "$SCRIPT_DIR" && wasm-pack build --target web --profile wasm --no-opt)
36+
37+
if [ "$SKIP_OPT" = false ]; then
38+
echo "Optimising WASM binary..."
39+
(cd "$SCRIPT_DIR" && wasm-opt pkg/ggsql_wasm_bg.wasm -o pkg/ggsql_wasm_bg.wasm -Oz --all-features)
40+
else
41+
echo "Skipping wasm-opt (--skip-opt)."
42+
fi
43+
else
44+
echo "Skipping WASM binary build (--skip-binary)."
45+
fi
46+
47+
echo "Building WASM demo and Quarto integration..."
48+
(cd "$SCRIPT_DIR/demo" && npm install && npm run build)
49+
50+
echo "Copying output to doc/wasm..."
51+
rm -rf "$REPO_ROOT/doc/wasm"
52+
cp -r "$SCRIPT_DIR/demo/dist" "$REPO_ROOT/doc/wasm"
53+
54+
echo "Done! Output is in: $REPO_ROOT/doc/wasm"

ggsql-wasm/demo/build.mjs

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import * as esbuild from "esbuild";
2+
import { copyFileSync, mkdirSync } from "fs";
3+
import { dirname, join } from "path";
4+
import { fileURLToPath } from "url";
5+
6+
const __dirname = dirname(fileURLToPath(import.meta.url));
7+
const isWatch = process.argv.includes("--watch");
8+
const distDir = join(__dirname, "dist");
9+
10+
// Ensure dist/ directory exists
11+
mkdirSync(distDir, { recursive: true });
12+
13+
// Copy static files
14+
console.log("Copying static files...");
15+
copyFileSync(join(__dirname, "src/index.html"), join(distDir, "index.html"));
16+
copyFileSync(
17+
join(__dirname, "../pkg/ggsql_wasm_bg.wasm"),
18+
join(distDir, "ggsql_wasm_bg.wasm"),
19+
);
20+
copyFileSync(
21+
join(__dirname, "node_modules/vscode-oniguruma/release/onig.wasm"),
22+
join(distDir, "onig.wasm"),
23+
);
24+
copyFileSync(
25+
join(__dirname, "../../ggsql-vscode/syntaxes/ggsql.tmLanguage.json"),
26+
join(distDir, "ggsql.tmLanguage.json"),
27+
);
28+
29+
// Build Monaco editor web worker
30+
console.log("Building Monaco editor worker...");
31+
await esbuild.build({
32+
entryPoints: [
33+
join(
34+
__dirname,
35+
"node_modules/monaco-editor/esm/vs/editor/editor.worker.js",
36+
),
37+
],
38+
bundle: true,
39+
outfile: join(distDir, "editor.worker.js"),
40+
format: "iife",
41+
});
42+
43+
// Shared build options
44+
const sharedOptions = {
45+
bundle: true,
46+
format: "esm",
47+
platform: "browser",
48+
target: "es2020",
49+
sourcemap: true,
50+
nodePaths: [join(__dirname, "node_modules")],
51+
loader: {
52+
".ttf": "file",
53+
},
54+
};
55+
56+
// Build playground bundle
57+
const playgroundOptions = {
58+
...sharedOptions,
59+
entryPoints: [join(__dirname, "src/main.ts")],
60+
outfile: join(distDir, "bundle.js"),
61+
};
62+
63+
// Build quarto integration bundle
64+
const quartoOptions = {
65+
...sharedOptions,
66+
entryPoints: [join(__dirname, "src/quarto/main.ts")],
67+
outfile: join(distDir, "quarto.js"),
68+
loader: {
69+
...sharedOptions.loader,
70+
".css": "css",
71+
},
72+
};
73+
74+
if (isWatch) {
75+
console.log("Starting watch mode...");
76+
const playgroundCtx = await esbuild.context(playgroundOptions);
77+
const quartoCtx = await esbuild.context(quartoOptions);
78+
await Promise.all([playgroundCtx.watch(), quartoCtx.watch()]);
79+
console.log("Watching for changes...");
80+
} else {
81+
console.log("Building bundles...");
82+
await Promise.all([
83+
esbuild.build(playgroundOptions),
84+
esbuild.build(quartoOptions),
85+
]);
86+
console.log("Build complete!");
87+
}

0 commit comments

Comments
 (0)