`crossorigin` on <video>/<audio> forces a CORS-checked fetch. The
server-side renderer downloads media directly (no CORS) so renders
always work, but Studio preview runs in the browser — a media host that
omits Access-Control-Allow-Origin silently fails the load, so the media
shows blank/black in preview while the render looks fine, hiding the bug.
Plain displayed media never needs crossorigin; it's only required to read
pixels/samples back (canvas/WebGL texture, WebAudio createMediaElementSource)
and only when the host is known CORS-enabled. New rule
media_crossorigin_breaks_preview flags it as an error with that guidance.
Claude-Session: https://claude.ai/code/session_01NsmfF5FzhqXY6hZ8buXgUE
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(producer,lint): id-less media renders blank wash instead of footage
A timed <video>/<audio> identified only by a Studio-stamped `data-hf-id`
(no real `id`) rendered as a flat white/grey wash with dropped audio, and
lint stayed silent so it surfaced only at render.
Root cause, two layers:
- lint `readAttr(tag, "id")` used a `\b` boundary, which treats the hyphen
in `data-hf-id="…"` as a word break — so reading "id" matched the trailing
`id="…"` inside `data-hf-id` and returned a phantom id. `media_missing_id`
therefore never fired for media carrying only a data-hf-id. Switched to a
`(?<![\w-])` lookbehind so a short name can't match the tail of a longer
hyphenated attribute (also fixes "width" matching `data-width`, etc.).
- the render pipeline identifies media by the real `el.id`: frame extraction
keys injected stills as `__render_frame_<id>__`, the runtime frame-swap
matches on `el.id`, and the audio mixer selects `audio[id][src]`. An empty
`el.id` meant injected frames/audio never matched. compileForRender now
assigns a stable positional id to every id-less timed media element before
any stage parses or serves the HTML.
Adds a producer regression fixture (video with data-hf-id, no id) and a lint
test covering the data-hf-id/id collision. Baseline mp4 generated separately.
* test(producer): baseline for video-hfid-no-id regression fixture
Golden compiled.html + output.mp4 (generated on linux/amd64 in the
Dockerfile.test image). Compare-mode passes: compilation, visual (0 failed
frames), and audio (correlation 1.000). A regression to the blank-wash
behaviour fails the visual check.
* refactor: make @hyperframes/lint depend only on parsers, not core
Relocates the leaf utilities lint pulled from core — URL/asset-path helpers,
font aliases, and the slideshow manifest parser — into the standalone
@hyperframes/parsers base, and drops @hyperframes/core from lint's
dependencies. Core keeps back-compat re-export stubs at the old paths, so
producer/studio/cli are unchanged.
Why: lint was the lightweight validator from #1749, but depending on core
transitively pulled studio-server (hono) and bpm-detective — irrelevant to
linting. Now installing @hyperframes/lint pulls only parsers + postcss, and
the core<->lint dependency cycle is gone.
- parsers main entry stays browser-safe (pure utils only); the node:path
asset helpers live behind the new @hyperframes/parsers/asset-paths subpath
- slideshow parser exposed via @hyperframes/parsers/slideshow
* feat(lint): add browser entry; harden CSS url() regex (ReDoS)
@hyperframes/lint/browser — a fully client-side rule engine (lintHyperframeHtml,
lintMediaUrls, shouldBlockRender) with zero node: builtins, so browser-only
editors can validate compositions with no Node.js and no server round-trip.
Closes the browser-validation ask on #1749.
- shouldBlockRender extracted from the fs-bound project.ts into its own pure
module so the browser entry stays node-free
- pure composition primitives (data types, font aliases, URL helper) exposed via
a new recast-free @hyperframes/parsers/composition subpath, so the browser
bundle tree-shakes out the GSAP/recast machinery (verified: esbuild
platform=browser bundles with 0 node builtins)
- lint built with a platform:browser tsup pass — compile-time guarantee the
browser entry never pulls a node builtin
- harden CSS_URL_RE against polynomial ReDoS (CodeQL js/polynomial-redos);
behavior-preserving, verified against existing tests + an old/new parity check
- parsers/lint marked sideEffects:false
* refactor: extract @hyperframes/lint package from core
Moves all lint rules, hyperframeLinter, lintProject, and related types
from packages/core/src/lint/ into a new standalone packages/lint package.
Core keeps a thin re-export stub at @hyperframes/core/lint for backward
compatibility. Consumer imports (cli lint command, producer hyperframeLint)
are updated to import from @hyperframes/lint directly.
Depends on @hyperframes/parsers (PR #1755).
* fix: restore postcss-selector-parser in core (sourceMutation.ts still uses it)
* fix(ci): add parsers+lint to Dockerfile and build before preview tests
* chore: update bun.lock after restoring postcss-selector-parser dep
* test(cli): update lintProject test for string-dir signature from @hyperframes/lint
* refactor(core): single-source the lint engine in @hyperframes/lint
Delete core's byte-identical copy of the lint rule engine and re-point
staticGuard at @hyperframes/lint, so the render-time render-gate and the
studio preview share one rule engine instead of two copies that could
silently diverge. Back-compat preserved via the @hyperframes/core/lint stub.
Addresses review feedback on the dual-copy footgun.