Skip to content

refactor(catalog): relocate to paged-introspect + publish via introsp… #23

refactor(catalog): relocate to paged-introspect + publish via introsp…

refactor(catalog): relocate to paged-introspect + publish via introsp… #23

Workflow file for this run

name: publish-wasm
# Builds the engine's wasm-bindgen surfaces and publishes them as
# versioned npm packages that the (private) editor consumes across the
# package boundary — "decision B" of the migration plan: the editor never
# takes a Rust path dependency on the engine; it depends on these packages.
#
# PREREQUISITES before this can publish (one-time setup):
# 1. An npm organisation `@paged-media` with publish access.
# 2. A repo secret `NPM_TOKEN` (npm automation token, publish scope).
# 3. Tag a release, e.g. `git tag v0.1.0 && git push --tags`. The package
# version is derived from the tag.
# 4. In the editor repo, point `packages/client` at the published
# `@paged-media/canvas-wasm` (and devtools at `@paged-media/introspect-wasm`)
# instead of the in-repo `build-wasm.sh` output.
#
# Until those exist, this runs the build steps but the publish step is a
# no-op without NPM_TOKEN.
on:
push:
tags: ["v*"]
workflow_dispatch:
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# Pin the toolchain from rust-toolchain.toml (audit B21) — the
# published wasm must be built with the same compiler the rest of
# CI uses, not a drifting `@stable`.
- name: resolve pinned toolchain
id: pin
run: echo "channel=$(bash scripts/rust-channel.sh)" >> "$GITHUB_OUTPUT"
- uses: dtolnay/rust-toolchain@master
with:
toolchain: ${{ steps.pin.outputs.channel }}
targets: wasm32-unknown-unknown
- name: Install wasm-bindgen-cli (lockfile pin) + binaryen (pinned)
run: |
set -euo pipefail
WB=$(awk '/^name = "wasm-bindgen"$/{getline; gsub(/version = "|"/,""); print; exit}' Cargo.lock)
echo "wasm-bindgen pin: $WB"
cargo install wasm-bindgen-cli --version "$WB" --locked
# Do NOT use apt's binaryen: ubuntu-latest ships v116, which
# miscompiles wasm-bindgen's reference-types tables ("Table.grow
# failed" at runtime). Same pin as docs' pages.yml.
BINARYEN=126
curl -fsSL -o /tmp/binaryen.tgz \
"https://github.com/WebAssembly/binaryen/releases/download/version_${BINARYEN}/binaryen-version_${BINARYEN}-x86_64-linux.tar.gz"
tar -xzf /tmp/binaryen.tgz -C /tmp
echo "/tmp/binaryen-version_${BINARYEN}/bin" >> "$GITHUB_PATH"
"/tmp/binaryen-version_${BINARYEN}/bin/wasm-opt" --version
- uses: actions/setup-node@v4
with:
node-version: 20
registry-url: https://registry.npmjs.org
- name: Resolve version from tag
id: ver
run: echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"
# W0.9 dist-tag rider (protocol-governance §4): prerelease tags
# (`v0.N.0-rc.x` — anything with a `-suffix`) publish under the
# `next` dist-tag, never `latest`. A clean `0.N.P` publishes under
# npm's default (`latest`). This sets a `dist-tag` output the
# publish step appends as `--tag` only for prereleases.
- name: Derive npm dist-tag from version
id: disttag
run: |
set -euo pipefail
V="${{ steps.ver.outputs.version }}"
if [[ "$V" == *-* ]]; then
echo "tag=next" >> "$GITHUB_OUTPUT"
echo "prerelease version '$V' → npm dist-tag 'next' (not latest)"
else
echo "tag=latest" >> "$GITHUB_OUTPUT"
echo "release version '$V' → npm dist-tag 'latest' (default)"
fi
- name: Build @paged-media/canvas-wasm
run: |
set -euo pipefail
cargo build --release --target wasm32-unknown-unknown -p paged-canvas-wasm --features gpu
OUT=dist/canvas-wasm
mkdir -p "$OUT"
wasm-bindgen target/wasm32-unknown-unknown/release/paged_canvas_wasm.wasm \
--target web --out-dir "$OUT"
wasm-opt -Oz "$OUT/paged_canvas_wasm_bg.wasm" -o "$OUT/paged_canvas_wasm_bg.wasm.opt" \
&& mv "$OUT/paged_canvas_wasm_bg.wasm.opt" "$OUT/paged_canvas_wasm_bg.wasm"
node -e "require('fs').writeFileSync('$OUT/package.json', JSON.stringify({
name: '@paged-media/canvas-wasm',
version: '${{ steps.ver.outputs.version }}',
type: 'module',
main: 'paged_canvas_wasm.js',
types: 'paged_canvas_wasm.d.ts',
files: ['paged_canvas_wasm.js','paged_canvas_wasm_bg.wasm','paged_canvas_wasm.d.ts'],
license: 'MPL-2.0 OR LicenseRef-PMEL'
}, null, 2))"
- name: Build @paged-media/introspect-wasm
run: |
set -euo pipefail
cargo build --release --target wasm32-unknown-unknown -p paged-introspect-wasm
OUT=dist/introspect-wasm
mkdir -p "$OUT"
wasm-bindgen target/wasm32-unknown-unknown/release/paged_introspect_wasm.wasm \
--target web --out-dir "$OUT"
wasm-opt -Oz "$OUT/paged_introspect_wasm_bg.wasm" -o "$OUT/paged_introspect_wasm_bg.wasm.opt" \
&& mv "$OUT/paged_introspect_wasm_bg.wasm.opt" "$OUT/paged_introspect_wasm_bg.wasm"
# The static capability catalog (ADR 019) ships alongside the wasm so
# build-time consumers (plugin SDK sync, state ingest) read it without
# booting wasm; describeCatalog() returns the same data at runtime.
cp crates/paged-introspect/catalog.json "$OUT/catalog.json"
node -e "require('fs').writeFileSync('$OUT/package.json', JSON.stringify({
name: '@paged-media/introspect-wasm',
version: '${{ steps.ver.outputs.version }}',
type: 'module',
main: 'paged_introspect_wasm.js',
types: 'paged_introspect_wasm.d.ts',
files: ['paged_introspect_wasm.js','paged_introspect_wasm_bg.wasm','paged_introspect_wasm.d.ts','catalog.json'],
license: 'MPL-2.0 OR LicenseRef-PMEL'
}, null, 2))"
- name: Subset audit — paged-sdk must not link editor crates
run: |
set -euo pipefail
# "Sibling, not a shrunk app": the viewer/SDK wasm is
# renderer-core only. Any of these in its dependency tree is
# a boundary breach — fail loudly.
TREE=$(cargo tree -p paged-sdk --target wasm32-unknown-unknown --features gpu -e normal)
if echo "$TREE" | grep -E "paged-(mutate|canvas|script)"; then
echo "::error::paged-sdk wasm links an editor crate"; exit 1
fi
echo "subset audit OK — no paged-mutate/-canvas/-script in the tree"
- name: Build @paged-media/sdk (paged-sdk WebGPU ViewerSession)
run: |
set -euo pipefail
cargo build --release --target wasm32-unknown-unknown -p paged-sdk --features gpu
OUT=dist/sdk
mkdir -p "$OUT"
wasm-bindgen target/wasm32-unknown-unknown/release/paged_sdk.wasm \
--target web --out-dir "$OUT"
wasm-opt -Oz "$OUT/paged_sdk_bg.wasm" -o "$OUT/paged_sdk_bg.wasm.opt" \
&& mv "$OUT/paged_sdk_bg.wasm.opt" "$OUT/paged_sdk_bg.wasm"
# Size ratchet (§4.2): print opt + brotli sizes next to the
# recorded baseline (2026-06-06: opt 7.20 MB, brotli 2.21 MB).
brotli -k -q 11 "$OUT/paged_sdk_bg.wasm"
ls -la "$OUT"/paged_sdk_bg.wasm*
node -e "require('fs').writeFileSync('$OUT/package.json', JSON.stringify({
name: '@paged-media/sdk',
version: '${{ steps.ver.outputs.version }}',
type: 'module',
main: 'paged_sdk.js',
types: 'paged_sdk.d.ts',
files: ['paged_sdk.js','paged_sdk_bg.wasm','paged_sdk.d.ts'],
license: 'MPL-2.0 OR LicenseRef-PMEL'
}, null, 2))"
rm -f "$OUT/paged_sdk_bg.wasm.br"
- name: Build @paged-media/idml-viewer (TS wrapper + bundled wasm)
run: |
set -euo pipefail
cd web/idml-viewer
npm install --no-fund --no-audit
npm test
npm run build
mkdir -p wasm
cp ../../dist/sdk/paged_sdk.js ../../dist/sdk/paged_sdk_bg.wasm ../../dist/sdk/paged_sdk.d.ts wasm/
npm pkg set version="${{ steps.ver.outputs.version }}"
- name: Publish
if: ${{ env.NPM_TOKEN != '' }}
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
run: |
# Leading ./ is load-bearing: a bare `dist/canvas-wasm` is
# parsed by npm as a GitHub owner/repo shorthand, not a path
# (it tried git ls-remote github.com/dist/canvas-wasm.git).
#
# --tag from the dist-tag rider (W0.9): prereleases land on
# `next`, releases on `latest`. Passing `--tag latest`
# explicitly is a no-op vs npm's default, so the command is
# uniform either way.
TAG="${{ steps.disttag.outputs.tag }}"
echo "publishing under npm dist-tag: $TAG"
npm publish --access public --tag "$TAG" ./dist/canvas-wasm
npm publish --access public --tag "$TAG" ./dist/introspect-wasm
npm publish --access public --tag "$TAG" ./dist/sdk
npm publish --access public --tag "$TAG" ./web/idml-viewer