@aphrody/canvas
Aphrody's canvas package: a stable @aphrody/* namespace over the pinned upstream Skia
runtime @napi-rs/canvas (1.0.9, commit 8411071),
plus the M3 surface contract used by the M3 packages, SpaceUI, Tauri and the render crates.
Consumers import @aphrody/canvas and never depend on @napi-rs/canvas directly.
bun add @aphrody/canvas # Bun >= 1.4.2
Entry points
| Import | Native addon | Contents |
|---|---|---|
@aphrody/canvas |
loaded on import | The upstream API (createCanvas, loadImage, Image, GlobalFonts, Path2D, SKRSContext2D, ...), loadNativeCanvas(), createNativeCanvas(), loadNativeImage(), drawM3Scene(), drawM3Scene3d() and the site contract. |
@aphrody/canvas/site |
never | createM3CanvasSurface(), createBrowserCanvas(), configureBrowserCanvas(), selectCanvasBackend(). |
@aphrody/canvas/browser |
never | Same module as /site. |
import { createCanvas, GlobalFonts, type SKRSContext2D } from "@aphrody/canvas";
const canvas = createCanvas(320, 120);
const ctx: SKRSContext2D = canvas.getContext("2d");
ctx.fillRect(0, 0, 320, 120);
const png = await canvas.encode("png"); // also "webp", "avif", "jpeg"
aphrody-site and browser bundles must use the native-free entry point, so the Skia addon never
reaches a site bundle:
import { createBrowserCanvas, createM3CanvasSurface } from "@aphrody/canvas/site";
The package ships TypeScript sources for Bun ("default" conditions) and hand-written
declarations ("types" conditions); a Next.js consumer of /site lists the package in
transpilePackages.
Native Skia binary
The addon is resolved by the upstream loader (vendor/js-binding.js), first match wins:
NAPI_RS_NATIVE_LIBRARY_PATH: absolute path to askia.<platform>.node, for example one built by Aphrody fromthird_party/canvas. When set, it is the only candidate.vendor/skia.<platform>.nodenext to the loader (ignored by Git, never committed).- The upstream prebuilt package
@napi-rs/canvas-<platform>@1.0.9, declared inoptionalDependencies. Package managers skip the packages whoseos/cpudo not match the host (on Linux the loader then picksgnuormuslat runtime):win32-x64-msvc,win32-arm64-msvc,linux-x64-gnu,linux-x64-musl,linux-arm64-gnu,linux-arm64-musl,linux-arm-gnueabihf,linux-riscv64-gnu,darwin-x64,darwin-arm64,android-arm64.
optionalDependencies are generated from third_party/canvas/npm/* at the vendored version, so
the JavaScript loader and the binary always come from the same upstream release.
M3 scenes
drawM3Scene() and drawM3Scene3d() rasterize an M3 scene in Rust (aphrody_render, through
@aphrody/bun) and blit the PNG with the native addon. @aphrody/bun is an optional peer
dependency: it is imported only when one of these two functions runs, and a missing install
throws @aphrody/canvas: drawM3Scene needs the optional peer dependency @aphrody/bun. The scene
types (M3Scene, M3Scene3d, M3Render3dPolicy) come from scene-types.d.ts, a copy of
packages/bun/src/scene-types.ts, so type-checking never needs @aphrody/bun.
Layout and upstream
third_party/canvas is a Git submodule on the non-fork upstream and is never patched in place;
Aphrody behavior goes in this facade or in the Rust render crates. The files the package needs
from other trees are copies:
| Path | Source |
|---|---|
vendor/index.js, js-binding.js, geometry.js, load-image.js, index.d.ts, LICENSE |
third_party/canvas (MIT, upstream copyright notice kept) |
vendor/package.json |
{"type": "commonjs"}: the upstream loader is CommonJS, this package is ESM |
vendor/UPSTREAM.md |
Provenance: commit, version, license, SHA-256 of every copy |
scene-types.d.ts |
packages/bun/src/scene-types.ts |
The copies are committed rather than generated at pack time:
bun test, the type-check and every workspace consumer work from a plain checkout, with no build step and without initializing the submodule;- the published tarball is exactly the reviewed tree (
fileslists every shipped path); - drift is still impossible to publish:
scripts/vendor-upstream.ts --checkruns asprepack(it fails if the submodule is missing), andvendor.test.tscompares every copy with its source when the submodule is checked out and with the SHA-256 invendor/UPSTREAM.mdotherwise.
Copies are byte-identical after line-ending normalization to LF.
Updating upstream
git -C third_party/canvas fetch && git -C third_party/canvas checkout <reviewed-sha>
cd packages/canvas
bun run vendor # rewrites vendor/, scene-types.d.ts and optionalDependencies
# keep aphrodyCanvasVersion in site.ts, site.d.ts and index.d.ts equal to the
# package version (the release-aligned Aphrody version), then run the gates below
Validation
cd packages/canvas
bun test # contract, native encode (PNG/WebP/AVIF), vendor drift, manifest
bun run typecheck # sources, then published declarations with skipLibCheck: false
bun run lint && bun run format:check
bun run vendor:check
bun pm pack --destination <dir>
Publishing
.github/workflows/release-m3-packages.yml publishes this package before
@aphrody/material-web (which depends on it) when an m3-v* tag is pushed. The manifest holds
no workspace: or catalog: specifier, so the packed package.json is the registry
package.json. Manual equivalent, run by the owner:
cd packages/canvas && bun publish --access public --tag latest --registry https://registry.npmjs.org