Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions .github/workflows/web.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
name: web-playground

# Builds the WASM playground (web/) and deploys it to GitHub Pages.
# Enable once under repo Settings → Pages → Source: "GitHub Actions".

on:
push:
branches: [main]
paths: ['web/**', 'core/**', '.github/workflows/web.yml']
workflow_dispatch:

permissions:
contents: read
pages: write
id-token: write
Comment on lines +12 to +15

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Scope elevated permissions to the deploy job only.

Line 14 (pages: write) and Line 15 (id-token: write) are set workflow-wide; the build job does not require them. Move these permissions to jobs.deploy.permissions and keep top-level/job-build permissions read-only.

Suggested permission scoping
-permissions:
-  contents: read
-  pages: write
-  id-token: write
+permissions:
+  contents: read
@@
   build:
     runs-on: ubuntu-latest
+    permissions:
+      contents: read
@@
   deploy:
     needs: build
     runs-on: ubuntu-latest
+    permissions:
+      pages: write
+      id-token: write
🧰 Tools
🪛 zizmor (1.25.2)

[error] 14-14: overly broad permissions (excessive-permissions): pages: write is overly broad at the workflow level

(excessive-permissions)


[error] 15-15: overly broad permissions (excessive-permissions): id-token: write is overly broad at the workflow level

(excessive-permissions)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/web.yml around lines 12 - 15, The workflow-level
permissions block currently includes `pages: write` and `id-token: write`
permissions that are only required by the deploy job, not the build job. Remove
`pages: write` and `id-token: write` from the top-level permissions section,
keeping only `contents: read` at the workflow level. Then add a dedicated
`permissions` section to the deploy job in the jobs section with the full set of
permissions it needs: `contents: read`, `pages: write`, and `id-token: write`.
This ensures the build job operates with minimal required permissions while the
deploy job has access to the elevated permissions it requires.

Source: Linters/SAST tools


# One deploy at a time; let an in-progress run finish.
concurrency:
group: pages
cancel-in-progress: false

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install wasm target
run: rustup target add wasm32-unknown-unknown
- name: Cache cargo
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
web/target
key: web-wasm-${{ hashFiles('web/Cargo.toml', 'core/Cargo.toml') }}
- name: Build playground
run: ./web/build.sh
- uses: actions/upload-pages-artifact@v3
with:
path: web/dist

deploy:
needs: build
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deploy.outputs.page_url }}
steps:
- id: deploy
uses: actions/deploy-pages@v4
Comment on lines +26 to +51

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n .github/workflows/web.yml | head -60

Repository: PhysShell/griff

Length of output: 1648


🌐 Web query:

GitHub Actions security best practices pin commit SHA vs tag references official documentation

💡 Result:

GitHub’s official security guidance strongly recommends pinning GitHub Actions to a full-length commit SHA as the only way to ensure an action is used as an immutable release [1][2]. Key security considerations include: 1. Immutability: A commit SHA uniquely and immutably identifies a specific version of an action’s code [3][4]. In contrast, tags (e.g., @v1) and branch references (e.g., @main) are mutable; they can be moved or deleted by an action's maintainers at any time, potentially introducing malicious or breaking code into your workflows without your knowledge [1][3][5]. 2. Risk Mitigation: Pinning to a SHA mitigates the risk of supply chain attacks where a compromised account or repository adds backdoors or malicious payloads to an existing tag [1][6]. While tags rely on trust in the creator, SHAs provide cryptographic assurance that you are running the exact code you reviewed [1][7]. 3. Organizational Enforcement: GitHub supports organizational and repository-level policies that mandate the use of full-length commit SHAs [1][8]. When enforced, any workflow attempting to use an action pinned to a tag or branch will fail [8][4]. 4. Best Practices for Maintenance: Because commit SHAs are opaque and do not automatically receive updates, it is recommended to include the human-readable tag as a comment in your YAML file to maintain clarity [6][7]. Many organizations use automation tools like Renovate or similar dependency managers to automatically create pull requests that update SHA pins when the referenced tags move, balancing security with ease of maintenance [8][6][9]. Recommendation: Always pin third-party actions to a full-length commit SHA [1][6]. While you may continue to use tags if you explicitly trust the creator, understand that this exposes your pipeline to potential upstream compromises [1][5]. If you maintain your own actions, ensure that any internal references to sub-actions are also pinned to SHAs to prevent breaking workflows for users who have enforced SHA-pinning policies [4].

Citations:


🌐 Web query:

GitHub Actions checkout persist-credentials false security best practices token exposure

💡 Result:

Setting persist-credentials: false in the actions/checkout GitHub Action is a security best practice recommended to prevent unintended token exposure [1][2][3]. By default, actions/checkout sets persist-credentials to true, which stores the GITHUB_TOKEN (or a provided personal access token) in the local.git/config file on the runner [4][5][3]. This enables subsequent steps in the same job to execute authenticated Git commands (such as git fetch or push) [4][6][7]. However, this also makes the token accessible to any process, script, or third-party action running in that job [8][1][9]. The primary security risks associated with keeping the default (persist-credentials: true) include: 1. Artifact Exposure (ArtiPACKED): If a workflow uploads the repository workspace as an artifact (e.g., using actions/upload-artifact), the.git directory—containing the persisted credentials—may be included [10][11][12]. If this artifact is public or accessible to unauthorized users, the token can be exfiltrated and misused [2][10][12]. 2. Increased Attack Surface: Any compromised or malicious third-party action within the workflow can read the.git/config file to obtain the token, potentially escalating privileges beyond what was intended for that specific action [1][9]. Recommendation: - Set persist-credentials: false for any checkout step that does not explicitly require authenticated Git operations (e.g., jobs focused solely on testing, building, or scanning) [1][9]. - If your workflow must perform Git operations (like pushing commits), consider using persist-credentials: false and managing authentication through other methods, or ensure that artifacts are configured to strictly exclude the.git directory (e.g., by using paths to target only specific build output folders) [2][10][12]. Using security auditing tools like zizmor or custom security policies can help detect and enforce the use of persist-credentials: false across your workflows [12][13].

Citations:


Pin all GitHub Actions by commit SHA and disable checkout credential persistence.

Lines 26, 30, 39, and 51 currently use tag refs (@v*) instead of full-length commit SHAs. Tag references are mutable—they can be moved, deleted, or compromised by a malicious maintainer—exposing your workflow to supply-chain attacks. Additionally, set persist-credentials: false on the checkout step to prevent the GITHUB_TOKEN from being stored in .git/config, which reduces the risk of token exfiltration if a third-party action is compromised or if artifacts leak.

Suggested hardening patch
-      - uses: actions/checkout@v4
+      - uses: actions/checkout@<full_commit_sha>
+        with:
+          persist-credentials: false
@@
-        uses: actions/cache@v4
+        uses: actions/cache@<full_commit_sha>
@@
-      - uses: actions/upload-pages-artifact@v3
+      - uses: actions/upload-pages-artifact@<full_commit_sha>
@@
-        uses: actions/deploy-pages@v4
+        uses: actions/deploy-pages@<full_commit_sha>
🧰 Tools
🪛 zizmor (1.25.2)

[warning] 26-26: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[error] 26-26: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)


[error] 30-30: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)


[error] 39-39: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)


[error] 51-51: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/web.yml around lines 26 - 51, Replace all GitHub Actions
version references with full-length commit SHAs to prevent supply-chain attacks
from mutable tag references. Update actions/checkout@v4 on line 26 to use its
commit SHA, actions/cache@v4 on line 30 to use its commit SHA,
actions/upload-pages-artifact@v3 on line 39 to use its commit SHA, and
actions/deploy-pages@v4 on line 51 to use its commit SHA. Additionally, add
persist-credentials: false as an option to the checkout action on line 26 to
prevent the GITHUB_TOKEN from being stored in the git config, reducing the risk
of token exfiltration.

Source: Linters/SAST tools

4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
members = ["core", "cli", "plugin", "preview"]
# `fuzz/` is an isolated nightly cargo-fuzz crate (ADR-0010); it is
# deliberately not a workspace member so stable `--workspace` builds,
# clippy, and tests never touch it.
exclude = ["fuzz"]
# clippy, and tests never touch it. `web/` is likewise wasm32-only (ADR-0024).
exclude = ["fuzz", "web"]
resolver = "2"

[workspace.package]
Expand Down
9 changes: 8 additions & 1 deletion core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,16 @@ categories.workspace = true
[dependencies]
midly = { workspace = true }
thiserror = { workspace = true }
guitarpro = { version = "0.3", default-features = false }
guitarpro = { version = "0.3", default-features = false, optional = true }
serde = { workspace = true }

[features]
default = ["gp"]
# Guitar Pro import. Pulls `guitarpro`/`zip` (and a `wasm-bindgen` subtree via
# `time`/`getrandom`); disable it (`default-features = false`) for lean,
# import-free wasm builds that only need MIDI — see ADR-0024.
gp = ["dep:guitarpro"]

[dev-dependencies]
serde_json = { workspace = true }
proptest = { workspace = true }
Expand Down
18 changes: 11 additions & 7 deletions core/src/import.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@
//! content, so the product accepts either a `.gp3/.gp4/.gp5/.gpx` tab or a
//! `.mid` file through one entry point.

#[cfg(feature = "gp")]
use crate::gp::{self, GpImportError};
use crate::{
gp::{self, GpImportError},
midi::{self, MidiError},
score::Score,
};
Expand All @@ -14,6 +15,7 @@ use crate::{
#[derive(Debug, thiserror::Error)]
pub enum ImportError {
/// The bytes looked like Guitar Pro, but the Guitar Pro adapter failed.
#[cfg(feature = "gp")]
#[error("Guitar Pro import failed: {0}")]
Gp(#[from] GpImportError),
/// The bytes were not Guitar Pro, and the MIDI adapter failed.
Expand All @@ -26,13 +28,15 @@ pub enum ImportError {
/// Guitar Pro is tried first (it has a recognisable header); anything its
/// detector rejects falls through to the MIDI importer. A Guitar Pro *parse*
/// failure surfaces as [`ImportError::Gp`] rather than being masked by the MIDI
/// fallback.
/// fallback. Without the `gp` feature only MIDI is recognised.
pub fn import_score_auto(data: &[u8]) -> Result<Score, ImportError> {
match gp::import_gp_score(data) {
Ok(score) => Ok(score),
Err(GpImportError::UnsupportedFormat) => {
midi::import_score(data).map_err(ImportError::Midi)
#[cfg(feature = "gp")]
{
match gp::import_gp_score(data) {
Ok(score) => return Ok(score),
Err(GpImportError::UnsupportedFormat) => {} // fall through to MIDI
Err(other) => return Err(ImportError::Gp(other)),
}
Err(other) => Err(ImportError::Gp(other)),
}
midi::import_score(data).map_err(ImportError::Midi)
}
1 change: 1 addition & 0 deletions core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ pub mod feature;
pub mod fretboard;
pub mod generate;
pub mod gesture;
#[cfg(feature = "gp")]
pub mod gp;
pub mod import;
pub mod midi;
Expand Down
100 changes: 100 additions & 0 deletions docs/adr/0024-web-wasm-frontend-for-mobile.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# ADR 0024: Ship the egui frontend to the browser (WASM) for mobile testing

Date: 2026-06-16
Status: Proposed

## Context

griff today runs only as a desktop CLI and a `ratatui` terminal preview (S8) —
both tied to a computer terminal. The primary author/tester works almost
entirely from a phone, so iterating on generation (tweaking seed / mode /
variation and *hearing* the result) is impractical: every test means getting to
a desktop.

The pieces for a browser build are already in place:

- **`griff-core` is WASM-clean.** It is pure compute — no filesystem, threads,
wall-clock, or the `rand` crate (the seeded PRNG is a hand-rolled `SplitMix64`
finalizer); all file I/O lives in the CLI. It compiles to
`wasm32-unknown-unknown` today (verified 2026-06-16), `serde` and collections
included.
- **ADR-0016 already chose `egui`** as the GUI renderer over a shared UI core
(view-model → interaction core → scene → renderers), and **`eframe` builds the
*same* egui app to native desktop and web/WASM**. A browser build is therefore
not a new frontend — it is the planned egui renderer targeting the browser.
- **S8 already lists** `eframe/egui window` and `MIDI playback` as its remaining
items; the pure `PianoRollView` / `Analysis` projections exist.

What the browser changes versus the native plan is narrow: (1) audio — S8 planned
`midir`, which has no web backend; (2) input — no filesystem; (3) build/hosting.

## Decision

1. **The canonical web frontend is the `eframe`/`egui` app compiled to
`wasm32-unknown-unknown`** — the same renderer ADR-0016 specifies. Native and
web share one codebase; the browser is a *build target*, not a fork. That is
the M2 target.

2. **The M1 MVP is a thin, throwaway front, not egui** — an *import-free*
`cdylib` (`web/`, no `wasm-bindgen`, no framework) that exports three C-ABI
functions, plus a static `index.html` + `app.js` that loads the `.wasm` with
`WebAssembly.instantiate(bytes, {})` and marshals a small JSON result through
Comment on lines +38 to +41

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

ABI wording mismatch: this MVP exports two C-ABI functions, not three.

Please update the ADR text to match the implemented/exported contract (arrange, arrange_len, plus linear memory export).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/adr/0024-web-wasm-frontend-for-mobile.md` around lines 38 - 41, Update
the wording in the M1 MVP description to correctly reflect the number of
exported C-ABI functions. Change "exports three C-ABI functions" to "exports two
C-ABI functions" and list the actual exported contract: the two C-ABI functions
arrange and arrange_len, plus the linear memory export. This ensures the ADR
text accurately matches the implemented contract.

linear memory. This unlocks phone testing now without the egui/Trunk/
wasm-bindgen toolchain. It is disposable, not a second canonical renderer, so
it carries no ADR-0016 divergence debt; egui replaces it at M2.

3. **`griff-core` gains a default-on `gp` feature** so the wasm build can drop
the Guitar Pro importer (`guitarpro`/`zip` → `time`/`getrandom` →
`wasm-bindgen`/`js-sys`). With `default-features = false` the module is
genuinely import-free and ~90 KiB; the CLI and tests keep `gp` on and are
unchanged.

4. **Audio on web is WebAudio**, not the Web MIDI API (absent on iOS Safari,
patchy on mobile) and not `midir` (no web backend). The MVP uses a placeholder
oscillator synth fed note events from core; a bundled SoundFont (guitar tone)
is a follow-up. The playback *driver* is the one per-target seam.

5. **Input is a fixed in-code sample** for the MVP (a file picker / drag-drop
later); the CLI keeps path-based I/O.

6. **Build and host: `cargo build --target wasm32` → copy the `.wasm` beside the
static files → GitHub Pages** (`web/build.sh`, `.github/workflows/web.yml`).
No Trunk or `wasm-bindgen` for the MVP. A URL, no install.

7. **Determinism is unaffected** (SPEC §6): the same controls yield the same
output in the browser too; the engine's seeded PRNG never touches wall-clock
or OS randomness.

## Consequences

- The maintainer can run complement — and the `VariationControl` knob — on a
phone via a URL. That is the actual ask.
- The import-free `cdylib` needs no build tooling beyond the stock wasm target:
`cargo build --target wasm32-unknown-unknown` then static hosting. Tiny payload
(~90 KiB, ~35 KiB gzipped).
- The `gp` feature gate also benefits any future wasm/plugin target that only
needs MIDI; it is a clean, default-on split.
- At M2 the per-target surface becomes: one egui codebase for desktop + web, with
the playback driver (`midir` native / WebAudio web) and input (fs vs picker)
behind seams; a SoundFont (license-checked) lands for a real tone.
- Accepted: the MVP synth is a placeholder (sawtooth + envelope), the roll is a
throwaway canvas painter, and the sample part A is fixed — all replaced as M2/M3
land.
- Accepted: mobile browsers require a user gesture before audio starts (a tap to
unlock the `AudioContext`); SoundFont licensing/bundling is a real chore.
- Accepted: the MVP roll is throwaway; the canonical piano-roll still needs the
ADR-0016 Scene/Viewport work (S8).
- Out of scope for the MVP: offline PWA install, and corpus curation /
persistence on web (the `preview/design/` curation dock — later).

## Roadmap

Extends ADR-0016 and advances the S8 "egui window + playback" items toward a web
target. If it grows beyond a playground it earns its own appended stage
(append-only, per the stage-label audit).

## See also

- [`0016-shared-ui-core-across-frontends.md`](0016-shared-ui-core-across-frontends.md)
- [`0007-clap-first-plugin-target.md`](0007-clap-first-plugin-target.md)
- [`../stages/S8-preview-app.md`](../stages/S8-preview-app.md)
1 change: 1 addition & 0 deletions docs/adr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ immutable; supersede it with a new one. New ADRs: copy
| [0021](0021-property-invariants-over-canonical-score.md) | Property-based invariants over the canonical Score | Proposed |
| [0022](0022-repeat-unfolding-as-projection.md) | Repeat unfolding is a projection, not a model rewrite | Proposed |
| [0023](0023-variation-control-for-complement.md) | Control pitch/contour spread of complementary parts | Proposed |
| [0024](0024-web-wasm-frontend-for-mobile.md) | Ship the egui frontend to the browser (WASM) for mobile testing | Proposed |

See also: [`../SPEC.md`](../SPEC.md), [`../glossary.md`](../glossary.md),
[`../decisions.log.md`](../decisions.log.md).
3 changes: 3 additions & 0 deletions web/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/target
/dist
Cargo.lock
31 changes: 31 additions & 0 deletions web/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Excluded from the root workspace (see root Cargo.toml `exclude`): this crate
# only builds for `wasm32-unknown-unknown`, like `fuzz/` only builds on nightly.
[package]
name = "griff-web"
version = "0.0.0"
edition = "2021"
publish = false
description = "Browser playground (WASM) for the griff engine — see docs/adr/0024."

[lib]
# `rlib` alongside `cdylib` lets the host-side unit tests link the crate; the
# wasm build still emits the cdylib.
crate-type = ["cdylib", "rlib"]

# `default-features = false` drops the GP importer (guitarpro/zip/time/getrandom
# → wasm-bindgen), leaving a lean, import-free wasm module.
[dependencies]
griff-core = { path = "../core", default-features = false }

# A pure-compute WASM module: no unwinding (smaller, no import surface), squeezed
# for mobile download.
[profile.release]
panic = "abort"
opt-level = "z"
lto = true
codegen-units = 1
strip = true

# Own workspace root (like `fuzz/`): isolates the wasm-only target and profile
# from the stable host workspace.
[workspace]
55 changes: 55 additions & 0 deletions web/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# griff web playground (WASM)

A browser front for the complement arranger — built so the engine can be driven
(and *heard*) from a phone, no install. See
[`docs/adr/0024-web-wasm-frontend-for-mobile.md`](../docs/adr/0024-web-wasm-frontend-for-mobile.md).

This is the **MVP** (ADR-0024 §2): a deliberately thin, throwaway front — no
`wasm-bindgen`, no framework. `griff-web` is an *import-free* `cdylib` that
exports three C-ABI functions; the page (`static/`) loads the `.wasm` with
`WebAssembly.instantiate(bytes, {})` and marshals a small JSON result through
linear memory. The canonical `egui` frontend (ADR-0016) replaces it at M2.

## What it does

Builds a fixed sample lead (part A) and a generated complement (part B) entirely
in the browser, with live controls for **mode**, **seed**, **register offset**,
and **pitch spread** (the ADR-0023 `VariationControl`, audible on the grid-locked
modes). Deterministic: the same controls always produce the same result.

## Build & run locally

```sh
./web/build.sh # → web/dist/ (wasm + static)
python3 -m http.server -d web/dist 8080 # open http://localhost:8080
```

The crate is wasm32-only and excluded from the root workspace (like `fuzz/`), so
stable `--workspace` builds/clippy/tests never touch it. It depends on
`griff-core` with `default-features = false`, dropping the Guitar Pro importer
(`guitarpro`/`zip`/`time`/`getrandom` → `wasm-bindgen`) — that is what keeps the
module import-free and ~90 KiB.

## ABI

| export | signature | meaning |
| --- | --- | --- |
| `arrange` | `(mode:u32, seed:u32, offset:i32, variation:f32) -> *const u8` | arrange; returns a pointer to JSON in linear memory |
| `arrange_len` | `() -> usize` | byte length of the last result |
| `memory` | — | the linear memory JS reads the JSON from |

`mode`: 0 `rhythm_lock`, 1 `register_contrast`, 2 `call_response`,
3 `support_layer`, 4 `octave_double`, 5 `counter_melody`.

Result JSON: `{ppqn, tempo, realized_spread, error, tracks:[{name, role, notes:[{p,s,d,v}]}]}`.

## Deploy

`.github/workflows/web.yml` builds `web/dist` and publishes it to GitHub Pages on
pushes to the default branch (enable Pages → "GitHub Actions" in repo settings).

## Notes / next

- Audio is a placeholder WebAudio synth (sawtooth + envelope, A left / B right).
A real SoundFont (guitar tone) is a follow-up.
- Input is a fixed in-code sample; a file picker / drag-drop comes later.
23 changes: 23 additions & 0 deletions web/build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#!/usr/bin/env bash
# Build the griff web playground into web/dist/ (static, deployable anywhere).
#
# ./web/build.sh # release build → web/dist
# python3 -m http.server -d web/dist 8080 # then open http://localhost:8080
#
# No wasm-bindgen / Trunk: griff-web is an import-free cdylib (ADR-0024), so the
# .wasm is copied next to the static files and loaded with WebAssembly.instantiate.
set -euo pipefail
here="$(cd "$(dirname "$0")" && pwd)"
out="$here/dist"

rustup target add wasm32-unknown-unknown >/dev/null 2>&1 || true
( cd "$here" && cargo build --release --target wasm32-unknown-unknown )

rm -rf "$out"
mkdir -p "$out"
cp "$here"/static/* "$out"/
cp "$here"/target/wasm32-unknown-unknown/release/griff_web.wasm "$out"/

size=$(wc -c < "$out/griff_web.wasm")
echo "built web/dist ($((size / 1024)) KiB wasm) — serve it with:"
echo " python3 -m http.server -d \"$out\" 8080"
Loading