diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dc783fb9bb..0f59e88938 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,6 +49,7 @@ jobs: filters: | code: - "packages/**" + - "examples/**" - "scripts/**" - "package.json" - "bun.lock" diff --git a/.gitignore b/.gitignore index 52719ff212..f50acc4bc2 100644 --- a/.gitignore +++ b/.gitignore @@ -97,8 +97,15 @@ examples/* !examples/k8s-jobs/** !examples/gcp-cloud-run !examples/gcp-cloud-run/** +!examples/react-player +!examples/react-player/** # …but never the local smoke run's build/render artifacts. examples/gcp-cloud-run/scripts/gcp-smoke-artifacts/ +# …and never the react-player example's vite build output or node_modules +# (the blanket dist/ and node_modules/ rules match unanchored, but the +# `!examples/react-player/**` negation above would otherwise re-include them). +examples/react-player/dist/ +examples/react-player/node_modules/ packages/studio/data/ .desloppify/ diff --git a/bun.lock b/bun.lock index f277c12249..0ce9c4c07f 100644 --- a/bun.lock +++ b/bun.lock @@ -21,9 +21,27 @@ "yaml": "^2.9.0", }, }, + "examples/react-player": { + "name": "@hyperframes/react-player-example", + "version": "0.7.69", + "dependencies": { + "@hyperframes/player": "workspace:*", + "react": "^19.0.0", + "react-dom": "^19.0.0", + }, + "devDependencies": { + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", + "@vitejs/plugin-react": "^4.0.0", + "happy-dom": "^20.9.0", + "typescript": "^5.0.0", + "vite": "^6.4.2", + "vitest": "^3.2.4", + }, + }, "packages/aws-lambda": { "name": "@hyperframes/aws-lambda", - "version": "0.7.60", + "version": "0.7.69", "dependencies": { "@aws-sdk/client-s3": "^3.700.0", "@aws-sdk/client-sfn": "^3.700.0", @@ -55,7 +73,7 @@ }, "packages/cli": { "name": "@hyperframes/cli", - "version": "0.7.60", + "version": "0.7.69", "bin": { "hyperframes": "./bin/hyperframes.mjs", }, @@ -106,7 +124,7 @@ }, "packages/core": { "name": "@hyperframes/core", - "version": "0.7.60", + "version": "0.7.69", "dependencies": { "@chenglou/pretext": "^0.0.5", "@hyperframes/lint": "workspace:*", @@ -131,7 +149,7 @@ }, "packages/engine": { "name": "@hyperframes/engine", - "version": "0.7.60", + "version": "0.7.69", "dependencies": { "@hono/node-server": "^1.13.0", "@hyperframes/core": "workspace:^", @@ -150,7 +168,7 @@ }, "packages/gcp-cloud-run": { "name": "@hyperframes/gcp-cloud-run", - "version": "0.7.60", + "version": "0.7.69", "dependencies": { "@google-cloud/storage": "^7.14.0", "@google-cloud/workflows": "^4.2.0", @@ -170,7 +188,7 @@ }, "packages/lint": { "name": "@hyperframes/lint", - "version": "0.7.60", + "version": "0.7.69", "dependencies": { "@hyperframes/parsers": "workspace:*", "htmlparser2": "^10.1.0", @@ -188,7 +206,7 @@ }, "packages/parsers": { "name": "@hyperframes/parsers", - "version": "0.7.60", + "version": "0.7.69", "dependencies": { "@babel/parser": "^7.27.0", "acorn": "^8.17.0", @@ -208,22 +226,32 @@ }, "packages/player": { "name": "@hyperframes/player", - "version": "0.7.60", + "version": "0.7.69", "dependencies": { "@hyperframes/core": "workspace:*", }, "devDependencies": { "@types/bun": "^1.1.0", + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", "gsap": "^3.12.5", "puppeteer-core": "^25.2.1", + "react": "^19.0.0", + "react-dom": "^19.0.0", "tsup": "^8.0.0", "typescript": "^5.0.0", "vitest": "^3.2.4", }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + }, + "optionalPeers": [ + "react", + ], }, "packages/producer": { "name": "@hyperframes/producer", - "version": "0.7.60", + "version": "0.7.69", "dependencies": { "@fontsource/archivo-black": "^5.2.8", "@fontsource/eb-garamond": "^5.2.7", @@ -268,7 +296,7 @@ }, "packages/sdk": { "name": "@hyperframes/sdk", - "version": "0.7.60", + "version": "0.7.69", "dependencies": { "@hyperframes/core": "workspace:*", "@hyperframes/parsers": "workspace:*", @@ -298,7 +326,7 @@ }, "packages/shader-transitions": { "name": "@hyperframes/shader-transitions", - "version": "0.7.60", + "version": "0.7.69", "dependencies": { "html2canvas": "^1.4.1", }, @@ -310,7 +338,7 @@ }, "packages/studio": { "name": "@hyperframes/studio", - "version": "0.7.60", + "version": "0.7.69", "dependencies": { "@codemirror/autocomplete": "^6.20.1", "@codemirror/commands": "^6.10.3", @@ -358,7 +386,7 @@ }, "packages/studio-server": { "name": "@hyperframes/studio-server", - "version": "0.7.60", + "version": "0.7.69", "dependencies": { "@hyperframes/core": "workspace:*", "@hyperframes/parsers": "workspace:*", @@ -741,6 +769,8 @@ "@hyperframes/producer": ["@hyperframes/producer@workspace:packages/producer"], + "@hyperframes/react-player-example": ["@hyperframes/react-player-example@workspace:examples/react-player"], + "@hyperframes/sdk": ["@hyperframes/sdk@workspace:packages/sdk"], "@hyperframes/sdk-playground": ["@hyperframes/sdk-playground@workspace:packages/sdk-playground"], diff --git a/examples/react-player/README.md b/examples/react-player/README.md new file mode 100644 index 0000000000..e33a230bc0 --- /dev/null +++ b/examples/react-player/README.md @@ -0,0 +1,22 @@ +# @hyperframes/react-player-example + +Demo app for the React bindings shipped at [`@hyperframes/player/react`](../player#react). Renders the `` component against a bundled copy of the `motion-blur` registry example and exercises the full binding surface: + +- Props → attributes (`controls`, `muted`, `loop`, `playbackRate`) via the option toggles +- Event callbacks (`onReady`, `onPlay`, `onPause`, `onTimeUpdate`, `onEnded`, `onScenes`, `onError`) via the event log +- The imperative ref handle (`play()`, `pause()`, `seek()`) via the custom transport bar + +## Run + +```bash +bun install # from the repo root +bun run --filter '@hyperframes/{parsers,lint,studio-server}' build +bun run --filter @hyperframes/core build +bun run --filter @hyperframes/player build # the react subpath resolves to player dist +cd examples/react-player +bun run dev +``` + +The demo composition lives at `public/composition/index.html` — an unmodified copy of `registry/examples/motion-blur`. Swap in any composition (e.g. one scaffolded with `hyperframes init`) by replacing that file or pointing `COMPOSITION_SRC` in `src/App.tsx` elsewhere. + +This package is private and never published. diff --git a/examples/react-player/index.html b/examples/react-player/index.html new file mode 100644 index 0000000000..98673857c1 --- /dev/null +++ b/examples/react-player/index.html @@ -0,0 +1,12 @@ + + + + + + HyperFrames Player — React Playground + + +
+ + + diff --git a/examples/react-player/package.json b/examples/react-player/package.json new file mode 100644 index 0000000000..f9bada8473 --- /dev/null +++ b/examples/react-player/package.json @@ -0,0 +1,26 @@ +{ + "name": "@hyperframes/react-player-example", + "version": "0.7.69", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "typecheck": "tsc --noEmit", + "test": "vitest run" + }, + "dependencies": { + "@hyperframes/player": "workspace:*", + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", + "@vitejs/plugin-react": "^4.0.0", + "happy-dom": "^20.9.0", + "typescript": "^5.0.0", + "vite": "^6.4.2", + "vitest": "^3.2.4" + } +} diff --git a/examples/react-player/public/composition/index.html b/examples/react-player/public/composition/index.html new file mode 100644 index 0000000000..034cc42bf7 --- /dev/null +++ b/examples/react-player/public/composition/index.html @@ -0,0 +1,572 @@ + + + + + + Motion Blur — Velocity Showcase + + + + +
+
+ Motion Blur — Velocity Showcase + feGaussianBlur + scaleX · blur ∝ velocity +
+ + +
+
+ 3.0seconds +
+
shape
+
text
+
+
+
+
+
+ DRIFT +
+
slow
+
+ + +
+
+ 1.5seconds +
+
shape
+
text
+
+
+
+
+
+ GLIDE +
+
medium
+
+ + +
+
+ 0.7seconds +
+
shape
+
text
+
+
+
+
+
+ RUSH +
+
fast
+
+ + +
+
+ 0.35seconds +
+
shape
+
text
+
+
+
+
+
+ BLAST +
+
faster
+
+ + +
+
+ 0.2seconds +
+
shape
+
text
+
+
+
+
+
+ WARP +
+
extreme
+
+ + +
+ + diff --git a/examples/react-player/src/App.test.tsx b/examples/react-player/src/App.test.tsx new file mode 100644 index 0000000000..08679641e6 --- /dev/null +++ b/examples/react-player/src/App.test.tsx @@ -0,0 +1,49 @@ +// @vitest-environment happy-dom +import { act } from "react"; +import { createRoot, type Root } from "react-dom/client"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { App } from "./App.js"; + +// The react bindings dynamically import the player package root to register +// the custom element; stub it so the smoke test doesn't exercise the real +// player's iframe machinery in happy-dom. +vi.mock("@hyperframes/player", () => ({})); + +let container: HTMLDivElement; +let root: Root; + +beforeAll(() => { + (globalThis as Record)["IS_REACT_ACT_ENVIRONMENT"] = true; +}); + +beforeEach(() => { + container = document.createElement("div"); + document.body.appendChild(container); + root = createRoot(container); +}); + +afterEach(async () => { + await act(async () => root.unmount()); + container.remove(); +}); + +describe("App", () => { + it("renders the player wired to the demo composition", async () => { + await act(async () => root.render()); + const player = container.querySelector("hyperframes-player"); + expect(player).not.toBeNull(); + expect(player?.getAttribute("src")).toBe("/composition/index.html"); + expect(container.textContent).toContain("React playground"); + }); + + it("reflects option toggles onto the player element", async () => { + await act(async () => root.render()); + const checkboxes = [...container.querySelectorAll("input[type=checkbox]")]; + const mutedToggle = checkboxes.find((c) => c.parentElement?.textContent?.includes("Muted")); + if (!mutedToggle) throw new Error("muted toggle not rendered"); + await act(async () => { + mutedToggle.click(); + }); + expect(container.querySelector("hyperframes-player")?.hasAttribute("muted")).toBe(true); + }); +}); diff --git a/examples/react-player/src/App.tsx b/examples/react-player/src/App.tsx new file mode 100644 index 0000000000..54420be77f --- /dev/null +++ b/examples/react-player/src/App.tsx @@ -0,0 +1,130 @@ +import { HyperframesPlayer, type HyperframesPlayerHandle } from "@hyperframes/player/react"; +import { useRef, useState } from "react"; + +const COMPOSITION_SRC = "/composition/index.html"; +const RATES = [0.5, 1, 1.5, 2]; +const MAX_LOG_ENTRIES = 12; + +type LogEntry = { id: number; label: string }; + +let nextLogId = 0; + +export function App() { + const player = useRef(null); + const [duration, setDuration] = useState(0); + const [currentTime, setCurrentTime] = useState(0); + const [playing, setPlaying] = useState(false); + const [rate, setRate] = useState(1); + const [muted, setMuted] = useState(false); + const [loop, setLoop] = useState(false); + const [nativeControls, setNativeControls] = useState(false); + const [log, setLog] = useState([]); + + const logEvent = (label: string) => + setLog((prev) => [{ id: nextLogId++, label }, ...prev].slice(0, MAX_LOG_ENTRIES)); + + return ( +
+
+

+ <HyperframesPlayer> React playground +

+

+ Drives @hyperframes/player/react — props, event callbacks, and the imperative + ref handle. +

+
+ + { + setDuration(detail.duration); + logEvent(`ready — duration ${detail.duration.toFixed(2)}s`); + }} + onPlay={() => { + setPlaying(true); + logEvent("play"); + }} + onPause={() => { + setPlaying(false); + logEvent("pause"); + }} + onTimeUpdate={(detail) => setCurrentTime(detail.currentTime)} + onEnded={() => logEvent("ended")} + onError={(detail) => logEvent(`error — ${detail.message}`)} + onScenes={(detail) => logEvent(`scenes — ${detail.scenes.length} scene(s)`)} + /> + +
+ + { + const t = Number(e.target.value); + setCurrentTime(t); + player.current?.seek(t); + }} + /> + + {currentTime.toFixed(1)}s / {duration.toFixed(1)}s + +
+ +
+ + + + +
+ +
+

Events

+ {log.length === 0 ? ( +

Waiting for the composition to load…

+ ) : ( +
    + {log.map((entry) => ( +
  • {entry.label}
  • + ))} +
+ )} +
+
+ ); +} diff --git a/examples/react-player/src/main.tsx b/examples/react-player/src/main.tsx new file mode 100644 index 0000000000..15e8b83ce1 --- /dev/null +++ b/examples/react-player/src/main.tsx @@ -0,0 +1,13 @@ +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import { App } from "./App.js"; +import "./styles.css"; + +const rootEl = document.getElementById("root"); +if (!rootEl) throw new Error("#root missing from index.html"); + +createRoot(rootEl).render( + + + , +); diff --git a/examples/react-player/src/styles.css b/examples/react-player/src/styles.css new file mode 100644 index 0000000000..be74f33e5d --- /dev/null +++ b/examples/react-player/src/styles.css @@ -0,0 +1,124 @@ +* { + box-sizing: border-box; +} + +body { + margin: 0; + background: #0d0d12; + color: #e8e8f0; + font-family: ui-sans-serif, system-ui, sans-serif; +} + +.app { + max-width: 880px; + margin: 0 auto; + padding: 32px 24px 64px; + display: flex; + flex-direction: column; + gap: 20px; +} + +header h1 { + font-size: 22px; + margin: 0 0 6px; +} + +header p { + margin: 0; + color: #9a9ab0; + font-size: 14px; +} + +code { + font-family: ui-monospace, monospace; + color: #8ec7ff; +} + +.player { + border-radius: 10px; + overflow: hidden; +} + +.transport { + display: flex; + align-items: center; + gap: 14px; +} + +.transport button { + min-width: 76px; + padding: 8px 0; + border: 1px solid #2c2c3a; + border-radius: 8px; + background: #1a1a24; + color: inherit; + font-size: 14px; + cursor: pointer; +} + +.transport button:hover { + background: #23232f; +} + +.transport input[type="range"] { + flex: 1; +} + +.time { + font-variant-numeric: tabular-nums; + font-size: 13px; + color: #9a9ab0; + white-space: nowrap; +} + +.options { + display: flex; + flex-wrap: wrap; + gap: 20px; + font-size: 14px; + color: #c4c4d4; +} + +.options label { + display: flex; + align-items: center; + gap: 8px; +} + +.options select { + background: #1a1a24; + color: inherit; + border: 1px solid #2c2c3a; + border-radius: 6px; + padding: 4px 8px; +} + +.log h2 { + font-size: 14px; + text-transform: uppercase; + letter-spacing: 0.1em; + color: #9a9ab0; + margin: 0 0 8px; +} + +.log ul { + margin: 0; + padding: 0; + list-style: none; + font-family: ui-monospace, monospace; + font-size: 13px; + display: flex; + flex-direction: column; + gap: 4px; +} + +.log li { + padding: 6px 10px; + background: #14141c; + border-radius: 6px; +} + +.empty { + color: #62627a; + font-size: 13px; +} diff --git a/examples/react-player/tsconfig.json b/examples/react-player/tsconfig.json new file mode 100644 index 0000000000..4c2072a950 --- /dev/null +++ b/examples/react-player/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "moduleResolution": "bundler", + "jsx": "react-jsx", + "strict": true, + "noUncheckedIndexedAccess": true, + "esModuleInterop": true, + "skipLibCheck": true, + "noEmit": true, + "types": ["vite/client"] + }, + "include": ["src", "vite.config.ts"] +} diff --git a/examples/react-player/vite.config.ts b/examples/react-player/vite.config.ts new file mode 100644 index 0000000000..3894773c99 --- /dev/null +++ b/examples/react-player/vite.config.ts @@ -0,0 +1,10 @@ +import react from "@vitejs/plugin-react"; +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + plugins: [react()], + test: { + environment: "happy-dom", + include: ["src/**/*.test.{ts,tsx}"], + }, +}); diff --git a/package.json b/package.json index 13a2a73e3a..c3bce20614 100644 --- a/package.json +++ b/package.json @@ -6,12 +6,13 @@ "url": "https://github.com/heygen-com/hyperframes" }, "workspaces": [ - "packages/*" + "packages/*", + "examples/*" ], "type": "module", "scripts": { "dev": "bun run studio", - "build": "bun run --filter '@hyperframes/{parsers,lint,studio-server}' build && bun run --filter @hyperframes/core build && bun run --filter '@hyperframes/{core,engine,producer,player,studio,shader-transitions,aws-lambda,gcp-cloud-run,sdk}' build && bun run --filter @hyperframes/cli build && bun run --filter @hyperframes/sdk-playground build", + "build": "bun run --filter '@hyperframes/{parsers,lint,studio-server}' build && bun run --filter @hyperframes/core build && bun run --filter '@hyperframes/{core,engine,producer,player,studio,shader-transitions,aws-lambda,gcp-cloud-run,sdk}' build && bun run --filter @hyperframes/cli build && bun run --filter '@hyperframes/{sdk-playground,react-player-example}' build", "build:producer": "bun run --filter @hyperframes/producer build", "studio": "bun run --filter @hyperframes/studio dev", "build:hyperframes-runtime": "bun run --filter @hyperframes/core build:hyperframes-runtime", diff --git a/packages/player/README.md b/packages/player/README.md index 0627e4e074..8931c42704 100644 --- a/packages/player/README.md +++ b/packages/player/README.md @@ -34,10 +34,11 @@ The player loads the composition in a sandboxed iframe, auto-detects its dimensi import "@hyperframes/player"; // The custom element is now registered — use it in your markup -// React: -// Vue: +// Vue: ``` +React users should prefer the dedicated bindings — see [React](#react) below. + ### Poster image Show a static image before playback starts: @@ -132,6 +133,63 @@ player.shaderLoading; // "composition" | "player" | "none" (read/write) player.iframeElement; // HTMLIFrameElement (read-only) ``` +## React + +The `@hyperframes/player/react` subpath ships typed React bindings (React 18/19, optional peer dependency) — no manual custom-element wiring: + +```tsx +import { HyperframesPlayer } from "@hyperframes/player/react"; + +function Demo() { + return ( + console.log(`Duration: ${duration}s`)} + onEnded={() => console.log("Done!")} + style={{ maxWidth: 800, aspectRatio: "16 / 9" }} + /> + ); +} +``` + +Props are camelCase mirrors of the attributes above (`audioSrc`, `audioLocked`, `playbackRate`, `autoPlay`, `shaderCaptureScale`, `shaderLoading`, plus `className`/`style`). Other host attributes (`id`, `role`, `tabIndex`, `aria-*`, `data-*`, DOM event handlers) pass through the `elementProps` prop. + +Every player event has a callback prop with the `CustomEvent` detail unwrapped: `onReady`, `onPlay`, `onPause`, `onTimeUpdate`, `onEnded`, `onError`, `onScenes`, `onShaderTransitionState`, `onRateChange`, `onVolumeChange`, `onRuntimeProtocolError`, `onAudioOwnershipChange`, and `onPlaybackError`. A contract test scans the player sources for `dispatchEvent` sites, so the binding cannot silently drift from the element's event surface. + +Imperative control goes through a ref handle: + +```tsx +import { useRef } from "react"; +import { HyperframesPlayer, type HyperframesPlayerHandle } from "@hyperframes/player/react"; + +function Controlled({ src }: { src: string }) { + const player = useRef(null); + + return ( + <> + + + + + ); +} +``` + +The handle exposes `play()`, `pause()`, `seek()`, `stopMedia()`, the color grading API, and read-only state: `currentTime`, `duration`, `paused`, `ready`, `scenes`, `element` (the raw ``), and `iframeElement` (see "Advanced: iframe access" below). + +The component registers the custom element on mount via a dynamic import, so it is safe to render from SSR frameworks (Next.js, Remix) — the player module is only evaluated in the browser. To pre-register the element before the first player mounts, call `ensurePlayerDefined()` (browser only — throws during SSR): + +```ts +import { ensurePlayerDefined } from "@hyperframes/player/react"; + +ensurePlayerDefined(); +``` + +Handle methods are safe no-ops until the element has registered and upgraded; gate playback logic on `onReady` as you would with the raw element. + +A runnable demo app lives at [`examples/react-player`](../../examples/react-player). + ## Advanced: iframe access The composition runs inside a sandboxed `` + + `` + + poster + ); +} + +/** + * A complete server-renderable `` tag with its declarative + * shadow root, for non-React SSR (raw templates, Astro, etc.). React users get + * the same markup from ``. + */ +export function renderPlayerTagHtml( + options: PlayerSsrOptions & { className?: string; style?: string } = {}, +): string { + const attributes = [ + options.className ? ` class="${escapeAttribute(options.className)}"` : "", + options.style ? ` style="${escapeAttribute(options.style)}"` : "", + options.src ? ` src="${escapeAttribute(options.src)}"` : "", + options.poster ? ` poster="${escapeAttribute(options.poster)}"` : "", + options.width !== undefined ? ` width="${positiveDimension(options.width, 1920)}"` : "", + options.height !== undefined ? ` height="${positiveDimension(options.height, 1080)}"` : "", + ].join(""); + return ( + `` + + `` + + `` + ); +} diff --git a/packages/player/tsconfig.json b/packages/player/tsconfig.json index d1972c9fd1..7bac7ade65 100644 --- a/packages/player/tsconfig.json +++ b/packages/player/tsconfig.json @@ -3,6 +3,7 @@ "target": "ES2022", "module": "ESNext", "moduleResolution": "bundler", + "jsx": "react-jsx", "lib": ["ES2022", "DOM", "DOM.Iterable"], "declaration": true, "declarationMap": true, @@ -11,7 +12,12 @@ "esModuleInterop": true, "skipLibCheck": true, "outDir": "dist", - "rootDir": "src" + "rootDir": "src", + // The react subpath dynamic-imports the package by name (self-reference). + // Resolve it to source so tsc/dts never treat dist/ output as an input. + "paths": { + "@hyperframes/player": ["./src/hyperframes-player.ts"] + } }, "include": ["src"] } diff --git a/packages/player/tsup.config.ts b/packages/player/tsup.config.ts index 8b6f443556..89bee5044d 100644 --- a/packages/player/tsup.config.ts +++ b/packages/player/tsup.config.ts @@ -4,18 +4,34 @@ import { readFileSync } from "node:fs"; const packageVersion = JSON.parse(readFileSync(new URL("./package.json", import.meta.url), "utf8")) .version as string; -export default defineConfig({ - entry: ["src/hyperframes-player.ts", "src/slideshow/hyperframes-slideshow.ts"], - format: ["esm", "cjs", "iife"], - globalName: "HyperframesPlayer", - noExternal: ["@hyperframes/core"], - dts: true, - clean: true, - minify: true, - sourcemap: true, - define: { - __HYPERFRAMES_RUNTIME_CDN_URL__: JSON.stringify( - `https://cdn.jsdelivr.net/npm/@hyperframes/core@${packageVersion}/dist/hyperframe.runtime.iife.js`, - ), +export default defineConfig([ + { + entry: ["src/hyperframes-player.ts", "src/slideshow/hyperframes-slideshow.ts"], + format: ["esm", "cjs", "iife"], + globalName: "HyperframesPlayer", + noExternal: ["@hyperframes/core"], + dts: true, + clean: true, + minify: true, + sourcemap: true, + define: { + __HYPERFRAMES_RUNTIME_CDN_URL__: JSON.stringify( + `https://cdn.jsdelivr.net/npm/@hyperframes/core@${packageVersion}/dist/hyperframe.runtime.iife.js`, + ), + }, }, -}); + // React bindings (`@hyperframes/player/react`). Built separately so react + // stays external and the element itself is loaded via the package + // self-reference at runtime — no IIFE build, and no `clean` (the first + // config owns dist/ cleanup). + { + entry: { "react/index": "src/react/index.ts" }, + format: ["esm", "cjs"], + // The bindings load the element via the package self-reference at + // runtime; without this esbuild would inline the whole player bundle. + external: ["@hyperframes/player"], + dts: true, + minify: true, + sourcemap: true, + }, +]); diff --git a/packages/player/vitest.config.ts b/packages/player/vitest.config.ts index 98c4399003..37089f2076 100644 --- a/packages/player/vitest.config.ts +++ b/packages/player/vitest.config.ts @@ -11,6 +11,11 @@ export default defineConfig({ alias: { "@hyperframes/core/slideshow": resolve(coreRoot, "slideshow/index.ts"), "@hyperframes/core/runtime/protocol": resolve(coreRoot, "runtime/protocol.ts"), + // Self-reference used by src/react/register.ts — point it at source so + // tests (which mock it) resolve without a built dist. + "@hyperframes/player": resolve( + fileURLToPath(new URL("./src/hyperframes-player.ts", import.meta.url)), + ), }, }, test: {