Skip to content

Commit 37c0fec

Browse files
Merge pull request #505 from gridaco/canary
Grida Canvas - emscripten node target (render without browser)
2 parents be43a2b + 896b005 commit 37c0fec

30 files changed

+1340
-484
lines changed
Lines changed: 34 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[profile.release]
2-
opt-level = 3 # optimize for size ("z" has runtime performance impact, keep this 3)
3-
lto = true # use link time optimization
4-
codegen-units = 1 # allow cross-crate inlining & dedup
2+
opt-level = 3 # optimize for size ("z" has runtime performance impact, keep this 3)
3+
lto = true # use link time optimization
4+
codegen-units = 1 # allow cross-crate inlining & dedup
55
# NOTE:
66
# Newer Rust stable toolchains ship `wasm32-unknown-emscripten` std/core built
77
# with `panic=unwind`. If we force `panic=abort` here, rustc fails with:
@@ -10,7 +10,7 @@ codegen-units = 1 # allow cross-crate inlining & dedup
1010
# with nightly `-Z build-std` (core + panic_abort) for wasm32-unknown-emscripten.
1111
# `panic=unwind` increases the wasm size by 0.5mb compared to `panic=abort`
1212
panic = "unwind"
13-
strip = "symbols" # strip symbols from final artifact
13+
strip = "symbols" # strip symbols from final artifact
1414
debug = false
1515

1616

@@ -34,31 +34,34 @@ linker = "emcc"
3434

3535
# Linker arguments passed to Emscripten
3636
rustflags = [
37-
# Allow undefined symbols (useful for dynamic linking and Skia integration)
38-
"-C", "link-arg=-sERROR_ON_UNDEFINED_SYMBOLS=0",
39-
40-
# Currently only targeting web (fs=false)
41-
# Target web environment (browser) instead of Node.js
42-
# We will be removing this once we figure out to make SSR/CSR not fail.
43-
"-C", "link-arg=-sENVIRONMENT=web",
44-
45-
# Enable WebGL2 support for hardware-accelerated graphics
46-
"-C", "link-arg=-sMAX_WEBGL_VERSION=2",
47-
"-C", "link-arg=-sUSE_WEBGL2=1",
48-
49-
# Allow dynamic memory growth (important for large graphics operations)
50-
"-C", "link-arg=-sALLOW_MEMORY_GROWTH=1",
51-
"-C", "link-arg=-sINITIAL_MEMORY=256MB",
52-
53-
# Create modular output (factory function instead of global Module)
54-
"-C", "link-arg=-sMODULARIZE=1",
55-
56-
# Set the export function name for the modular output
57-
"-C", "link-arg=-sEXPORT_NAME=createGridaCanvas",
58-
59-
# Export specific runtime methods for JavaScript access
60-
# GL: WebGL context access
61-
# UTF8 functions: String conversion utilities
62-
# HEAP functions: Direct memory access for performance-critical operations
63-
"-C", "link-arg=-sEXPORTED_RUNTIME_METHODS=['GL','lengthBytesUTF8','stringToUTF8','UTF8ToString','HEAP32','HEAPU8','HEAPU16','HEAPU32','HEAPF32']"
37+
# Allow undefined symbols (useful for dynamic linking and Skia integration)
38+
"-C",
39+
"link-arg=-sERROR_ON_UNDEFINED_SYMBOLS=0",
40+
# Currently only targeting web (fs=false)
41+
# Target both web (browser) and Node.js.
42+
# NOTE: this affects the generated Emscripten JS glue (loader/runtime selection).
43+
"-C",
44+
"link-arg=-sENVIRONMENT=web,node",
45+
# Enable WebGL2 support for hardware-accelerated graphics
46+
"-C",
47+
"link-arg=-sMAX_WEBGL_VERSION=2",
48+
"-C",
49+
"link-arg=-sUSE_WEBGL2=1",
50+
# Allow dynamic memory growth (important for large graphics operations)
51+
"-C",
52+
"link-arg=-sALLOW_MEMORY_GROWTH=1",
53+
"-C",
54+
"link-arg=-sINITIAL_MEMORY=256MB",
55+
# Create modular output (factory function instead of global Module)
56+
"-C",
57+
"link-arg=-sMODULARIZE=1",
58+
# Set the export function name for the modular output
59+
"-C",
60+
"link-arg=-sEXPORT_NAME=createGridaCanvas",
61+
# Export specific runtime methods for JavaScript access
62+
# GL: WebGL context access
63+
# UTF8 functions: String conversion utilities
64+
# HEAP functions: Direct memory access for performance-critical operations
65+
"-C",
66+
"link-arg=-sEXPORTED_RUNTIME_METHODS=['GL','lengthBytesUTF8','stringToUTF8','UTF8ToString','HEAP32','HEAPU8','HEAPU16','HEAPU32','HEAPF32']",
6467
]

crates/grida-canvas-wasm/AGENTS.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,31 @@ as new apis are introduced via `main.rs`, we also need to update the `grida-canv
1010

1111
Note: the artifacts are git included for faster CI builds.
1212

13+
## Bundler compatibility (Next.js Turbopack + dual-env Emscripten glue)
14+
15+
When building the WASM glue with `-sENVIRONMENT=web,node`, Emscripten emits a Node-only branch that uses Node built-ins (`fs`, `path`, sometimes `crypto`). Some bundlers (notably **Next.js Turbopack**) will still try to resolve those built-ins when the module is imported from a Client Component and fail with **"Can't resolve 'fs'"**.
16+
17+
We keep a **single package** that works in both browser and Node by combining three measures:
18+
19+
- **Patch Emscripten glue to use `node:*` built-ins**
20+
- Script: `scripts/prebuild-patch-emscripten-js-glue.cjs`
21+
- Patched file: `lib/bin/grida-canvas-wasm.js`
22+
- Rewrites `require("fs")``require("node:fs")`, `require("path")``require("node:path")`, `require("crypto")``require("node:crypto")`
23+
- Reference issue: `https://github.com/emscripten-core/emscripten/issues/26134`
24+
25+
- **Do NOT bundle the Emscripten glue into `dist/index.*`**
26+
- `lib/index.ts` imports `./grida-canvas-wasm` (a thin wrapper)
27+
- Wrapper: `lib/grida-canvas-wasm.ts` imports `./bin/grida-canvas-wasm`
28+
- `tsup.config.ts` sets `external: ["./grida-canvas-wasm"]` and copies `lib/bin/*` via `publicDir`
29+
- Result: `dist/index.js` only imports `./grida-canvas-wasm`, and the actual glue stays as `dist/grida-canvas-wasm.js` (copied), so Turbopack never sees Node built-ins inside the bundled entry.
30+
31+
- **Run the glue patch before bundling**
32+
- `package.json` uses `prebuild` to patch `lib/bin/grida-canvas-wasm.js` before `tsup` runs.
33+
34+
⚠️ Notes:
35+
- Simply setting `tsup.external` to `["node:fs", "node:path", ...]` is **not sufficient** here; bundling the glue can still produce `require("fs")` in `dist/index.js`, which breaks Turbopack.
36+
- If you change `lib/index.ts` to import `./bin/grida-canvas-wasm` directly (no wrapper + external), re-verify `pnpm --filter editor build` with Turbopack.
37+
1338
## Build System
1439

1540
This project uses a unified `justfile` build system for all WASM build configurations.

crates/grida-canvas-wasm/README.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ WASM bindings for Grida Canvas.
88
pnpm install @grida/canvas-wasm
99
```
1010

11+
### Browser (WebGL)
12+
1113
```ts
1214
import init from "@grida/canvas-wasm";
1315

@@ -26,6 +28,29 @@ const scene = factory.createWebGLCanvasSurface(canvas);
2628
scene.loadDummyScene();
2729
```
2830

31+
### Node (raster export)
32+
33+
```ts
34+
import { createCanvas } from "@grida/canvas-wasm";
35+
import { readFileSync, writeFileSync } from "node:fs";
36+
37+
const canvas = await createCanvas({
38+
backend: "raster",
39+
width: 256,
40+
height: 256,
41+
});
42+
43+
const doc = readFileSync("example/rectangle.grida1", "utf8");
44+
canvas.loadScene(doc);
45+
46+
const { data } = canvas.exportNodeAs("rectangle", {
47+
format: "PNG",
48+
constraints: { type: "none", value: 1 },
49+
});
50+
51+
writeFileSync("out.png", Buffer.from(data));
52+
```
53+
2954
## Serving locally for development
3055

3156
For local development, this package has `serve` ready.
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
{
2+
"version": "0.90.0-beta+20260108",
3+
"document": {
4+
"nodes": {
5+
"gradient-rect": {
6+
"id": "gradient-rect",
7+
"name": "gradient-rect",
8+
"locked": false,
9+
"active": true,
10+
"layout_positioning": "absolute",
11+
"layout_inset_top": 24,
12+
"layout_inset_left": 24,
13+
"opacity": 1,
14+
"z_index": 0,
15+
"rotation": 0,
16+
"layout_target_width": 208,
17+
"layout_target_height": 208,
18+
"type": "rectangle",
19+
"corner_radius": 32,
20+
"effects": [],
21+
"stroke_width": 0,
22+
"stroke_cap": "butt",
23+
"fill_paints": [
24+
{
25+
"type": "linear_gradient",
26+
"active": true,
27+
"stops": [
28+
{
29+
"offset": 0,
30+
"color": { "r": 0.94, "g": 0.33, "b": 0.31, "a": 1 }
31+
},
32+
{
33+
"offset": 1,
34+
"color": { "r": 0.25, "g": 0.48, "b": 0.96, "a": 1 }
35+
}
36+
]
37+
}
38+
]
39+
},
40+
"main": {
41+
"type": "scene",
42+
"id": "main",
43+
"name": "main",
44+
"active": true,
45+
"locked": false,
46+
"constraints": {
47+
"children": "multiple"
48+
},
49+
"guides": [],
50+
"edges": [],
51+
"background_color": {
52+
"r": 0.96,
53+
"g": 0.96,
54+
"b": 0.96,
55+
"a": 1
56+
}
57+
}
58+
},
59+
"links": {
60+
"main": ["gradient-rect"]
61+
},
62+
"scenes_ref": ["main"],
63+
"bitmaps": {},
64+
"images": {},
65+
"properties": {}
66+
}
67+
}
68+

crates/grida-canvas-wasm/justfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ info:
7171
@echo "Configuration:"
7272
@echo " - Linker arguments configured in .cargo/config.toml"
7373
@echo " - ERROR_ON_UNDEFINED_SYMBOLS=0 (allow undefined symbols)"
74-
@echo " - ENVIRONMENT=web (web environment)"
74+
@echo " - ENVIRONMENT=web,node (web + node environments)"
7575
@echo " - MAX_WEBGL_VERSION=2 (WebGL2 support)"
7676
@echo " - ALLOW_MEMORY_GROWTH=1 (dynamic memory growth)"
7777
@echo " - MODULARIZE=1 (modular output)"
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
out

0 commit comments

Comments
 (0)