Skip to content

v0.3.0-prerelease — Phase 2: Browser Runtime (live.js)

Pre-release
Pre-release

Choose a tag to compare

@Samuel0101010 Samuel0101010 released this 22 May 13:25

Vanilla-JS IIFE bundle (dist/live.js) for in-page picking, configure, generation,
and variant cycling. 31.30 kB minified — well under the 50 kB live.js budget.

Built by a 4-agent team (architect / coder / security / tester) with disjoint
file ownership. No React, no DOM-deps, no Node imports — Browser-target ES2020.

What landed

Contracts (src/contracts/browser.ts, 467 lines)

  • BrowserState 5-kind discriminated union + 18-entry STATE_TRANSITIONS table
  • PickResult, Variant, ParameterBinding, Annotation, BridgeClient interface
  • SanitizeModule interface — single trust-boundary for user/agent strings
  • WispDesignGlobal / InitOptions / WispDesignHandle for the IIFE boot API
  • Constants split into src/browser/constants.ts (pure, no zod) to keep the
    IIFE bundle free of zod side effects

Browser modules (src/browser/, ~2500 lines across 10 files)

  • state-machine.ts — pure reducer, invalid event returns input unchanged
  • picker.tsdocument.elementFromPoint + pickable() (>=20x20,
    excludes WISP UI / html / body / script / style / meta / visibility:hidden)
  • multi-select.ts — Cmd/Ctrl-click-add toggle (Improvement #1 vs Impeccables single-select)
  • floating-bar.ts — 3-mode bar (configure / generating / cycling)
  • parameter-sliders.tszero-roundtrip CSS-var binding (Killer-Feature)
  • annotations.ts — structured {targetId, kind, note} — NOT PNG flatten (Improvement #7)
  • variant-render.ts@scope-based 3-variant cycling; variant 0 = live
    original (preserves listeners), variants 1+ are clones
  • index.ts — IIFE entry, window.WispDesign.init({ token, bridgeUrl })
  • bridge-client.ts — fetch POST + EventSource subscribe (Long-Poll fallback)

Security (src/browser/sanitize.ts, 302 lines)

  • escapeHtml, sanitizeFreeText (strips <script>/<style>/<iframe>/
    <object>/<embed>/<svg> blocks + on*= handlers + js:/data:/vbscript:
    prefixes + C0+DEL controls; word-boundary truncate at 75% threshold)
  • trustedCssVar (rejects url(/@import/expression(/{/}/;/backslash)
  • trustedSelector (deep :has() check rejects nested iframe/script)
  • Returns structured SanitizeError { code, message, detail? }

Tests (tests/browser/, 94 new tests across 7 files)

  • sanitize.test.ts (28) — XSS payloads, CSS-var + selector rejection
  • state-machine.test.ts (19) — transitions, self-loops, invalid-event silence
  • picker.test.ts (15) — pickable() filter, selector chains, attribute subset
  • multi-select.test.ts (6) — modifier toggle, [data-wisp-ui] exclusion
  • variant-render.test.ts (9) — wrapper, 3 siblings, @scope rule injection
  • bundle-size.test.ts (5) — dist/live.js exists, < 50000 bytes, IIFE banner
  • constants-drift.test.ts (12) — byte-equivalence guard between
    src/browser/constants.ts and src/contracts/browser.ts (CI catches drift)

Cumulative: 171/171 tests green (94 browser + 77 bridge + 2 smoke).

Tooling

  • tsup multi-entry: CLI ESM (dist/index.js, 9.74 kB) + Browser IIFE
    (dist/live.js, 31.30 kB, globalName: WispDesign, target es2020, minified,
    noExternal: [/.*/], outExtension strips the .global infix)
  • vitest environmentMatchGlobs: tests/browser/** runs jsdom, rest pure Node
  • jsdom + @types/jsdom as devDeps

Notable engineering decision

Bundle was 91.17 kB initiallysanitize.ts value-imported
FREE_TEXT_MAX_LEN from contracts/browser.js, which triggered top-level
z.object(...) evaluation and inlined the full zod runtime (~60 kB overhead).

Fix: rewire sanitize.ts to import from src/browser/constants.ts (pure
constants, no zod). The other 9 browser modules already did this — only
sanitize.ts was the offender. constants-drift.test.ts now guards against
future drift between constants.ts and contracts/browser.ts.

Result: 31.30 kB. 60 kB reduction in a single import rewire.

Pinned behaviors

  • escapeHtml is NOT idempotent (&amp;&amp;amp;)
  • textPreview length is 81 when truncated ( ellipsis appended after 80 char slice)
  • sanitizeFreeText strips the leading whitespace inside <img on…=…> along with
    the handler — result lacks a trailing space inside the tag
  • jsdom does not implement @scope cascade — variant-render structural tests
    assert the @scope rule is in the <style> textContent, not that styles apply

Quality gates

$ npm run check
typecheck:  clean
test:       171 passed in 13 files (2.45s)
build:      index.js 9.74 kB + live.js 31.30 kB

$ node dist/index.js doctor
7/7 OK

Next

Phase 3 — Source-Edit Engine (src/source/). Marker-bounded fs.writeFileSync
splice, byte-equivalent inject/remove roundtrip, carbonize CSS @scope
permanent on accept, per-session .wisp/sessions/<id>.jsonl undo log
(Improvement #2 vs Impeccables no-undo), refuse-edit safety on generated files.