Skip to content
Closed
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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
9 changes: 9 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
root = true

[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
17 changes: 17 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,23 @@ jobs:
- run: pnpm install --frozen-lockfile
- run: pnpm build

lint-and-format:
name: Lint & Format
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 10
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm lint
- run: pnpm format:check

typecheck:
name: Typecheck
runs-on: ubuntu-latest
Expand Down
8 changes: 8 additions & 0 deletions .oxfmtrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"singleQuote": false,
"semi": true,
"useTabs": false,
"tabWidth": 2,
"trailingComma": "all",
"printWidth": 100
}
8 changes: 8 additions & 0 deletions .oxlintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"$schema": "https://raw.githubusercontent.com/oxc-project/oxc/main/npm/oxlint/configuration_schema.json",
"categories": {
"correctness": "error"
},
"plugins": ["react", "typescript"],
"ignorePatterns": ["dist/", "coverage/", "node_modules/"]
}
29 changes: 21 additions & 8 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ pnpm install # Install all dependencies
pnpm dev # Run the studio (composition editor)
pnpm build # Build all packages
pnpm -r typecheck # Type-check all packages
pnpm lint # Lint all packages
pnpm format:check # Check formatting
```

### Running Tests
Expand All @@ -26,21 +28,32 @@ pnpm --filter @hyperframes/engine test # Engine unit tests (vitest)
pnpm --filter @hyperframes/core test:hyperframe-runtime-ci # Runtime contract tests
```

### Linting & Formatting

```bash
pnpm lint # Run oxlint
pnpm lint:fix # Run oxlint with auto-fix
pnpm format # Format all files with oxfmt
pnpm format:check # Check formatting without writing
```

Git hooks (via [lefthook](https://github.com/evilmartians/lefthook)) run automatically after `pnpm install` and enforce linting + formatting on staged files before each commit.

## Pull Requests

- Use [conventional commit](https://www.conventionalcommits.org/) format for PR titles (e.g., `feat: add timeline export`, `fix: resolve seek overflow`)
- Use [conventional commit](https://www.conventionalcommits.org/) format for **all commits** (e.g., `feat: add timeline export`, `fix: resolve seek overflow`). Enforced by a git hook.
- CI must pass before merge (build, typecheck, tests, semantic PR title)
- PRs require at least 1 approval

## Packages

| Package | Description |
|---|---|
| `@hyperframes/core` | Types, HTML generation, runtime, linter |
| `@hyperframes/engine` | Seekable page-to-video capture engine |
| `@hyperframes/producer` | Full rendering pipeline (capture + encode) |
| `@hyperframes/studio` | Composition editor UI |
| `hyperframes` | CLI for creating, previewing, and rendering |
| Package | Description |
| ----------------------- | ------------------------------------------- |
| `@hyperframes/core` | Types, HTML generation, runtime, linter |
| `@hyperframes/engine` | Seekable page-to-video capture engine |
| `@hyperframes/producer` | Full rendering pipeline (capture + encode) |
| `@hyperframes/studio` | Composition editor UI |
| `hyperframes` | CLI for creating, previewing, and rendering |

## Releasing (Maintainers)

Expand Down
27 changes: 19 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,25 @@ npx hyperframes render # render to MP4
Define your video as HTML with data attributes:

```html
<div id="stage" data-composition-id="my-video"
data-start="0" data-width="1920" data-height="1080">
<video id="clip-1" data-start="0" data-duration="5"
data-track="0" src="intro.mp4" muted playsinline></video>
<img id="overlay" data-start="2" data-duration="3"
data-track="1" src="logo.png" />
<audio id="bg-music" data-start="0" data-duration="9"
data-track="2" data-volume="0.5" src="music.wav"></audio>
<div id="stage" data-composition-id="my-video" data-start="0" data-width="1920" data-height="1080">
<video
id="clip-1"
data-start="0"
data-duration="5"
data-track="0"
src="intro.mp4"
muted
playsinline
></video>
<img id="overlay" data-start="2" data-duration="3" data-track="1" src="logo.png" />
<audio
id="bg-music"
data-start="0"
data-duration="9"
data-track="2"
data-volume="0.5"
src="music.wav"
></audio>
</div>
```

Expand Down
2 changes: 1 addition & 1 deletion SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ We will acknowledge receipt within 48 hours and aim to provide a fix or mitigati
## Supported Versions

| Version | Supported |
|---------|-----------|
| ------- | --------- |
| 0.x | Yes |

## Scope
Expand Down
3 changes: 3 additions & 0 deletions commitlint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default {
extends: ["@commitlint/config-conventional"],
};
23 changes: 23 additions & 0 deletions knip.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { KnipConfig } from "knip";

const config: KnipConfig = {
workspaces: {
"packages/cli": {
entry: ["src/cli.ts"],
},
"packages/core": {
entry: ["src/index.ts", "src/lint/index.ts", "src/compiler/index.ts"],
},
"packages/engine": {
entry: ["src/index.ts"],
},
"packages/producer": {
entry: ["src/index.ts", "src/server.ts"],
},
"packages/studio": {
entry: ["src/index.ts", "src/styles/tailwind-preset.ts"],
},
},
};

export default config;
14 changes: 14 additions & 0 deletions lefthook.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
pre-commit:
parallel: true
commands:
lint:
glob: "*.{js,jsx,ts,tsx}"
run: npx oxlint {staged_files}
format:
glob: "*.{js,jsx,ts,tsx,json,css,md,yaml,yml}"
run: npx oxfmt --check {staged_files}

commit-msg:
commands:
commitlint:
run: npx commitlint --edit "{1}"
19 changes: 18 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,29 @@
"studio": "pnpm --filter @hyperframes/studio dev",
"build:hyperframes-runtime": "pnpm --filter @hyperframes/core build:hyperframes-runtime",
"build:hyperframes-runtime:modular": "pnpm --filter @hyperframes/core build:hyperframes-runtime:modular",
"set-version": "tsx scripts/set-version.ts"
"set-version": "tsx scripts/set-version.ts",
"lint": "oxlint .",
"lint:fix": "oxlint --fix .",
"format": "oxfmt .",
"format:check": "oxfmt --check .",
"knip": "knip",
"prepare": "lefthook install"
},
"devDependencies": {
"@commitlint/cli": "^20.5.0",
"@commitlint/config-conventional": "^20.5.0",
"@types/node": "^25.0.10",
"concurrently": "^8.2.0",
"knip": "^6.0.3",
"lefthook": "^2.1.4",
"oxfmt": "^0.41.0",
"oxlint": "^1.56.0",
"tsx": "^4.21.0",
"typescript": "^5.0.0"
},
"pnpm": {
"onlyBuiltDependencies": [
"lefthook"
]
}
}
4 changes: 2 additions & 2 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@
"name": "hyperframes",
"version": "0.1.1",
"description": "HyperFrames CLI — create, preview, and render HTML video compositions",
"type": "module",
"bin": {
"hyperframes": "./dist/cli.js"
},
"files": [
"dist"
],
"type": "module",
"scripts": {
"dev": "tsx src/cli.ts",
"build": "pnpm build:studio && tsup && pnpm build:runtime && pnpm build:copy",
Expand All @@ -30,9 +30,9 @@
"puppeteer-core": "^24.39.1"
},
"devDependencies": {
"@hyperframes/core": "workspace:*",
"@clack/prompts": "^1.1.0",
"@hono/node-server": "^1.0.0",
"@hyperframes/core": "workspace:*",
"@hyperframes/engine": "workspace:*",
"@hyperframes/producer": "workspace:*",
"@types/adm-zip": "^0.5.7",
Expand Down
28 changes: 6 additions & 22 deletions packages/cli/src/browser/manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,7 @@ import { execSync } from "node:child_process";
import { existsSync, rmSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
import {
Browser,
detectBrowserPlatform,
getInstalledBrowsers,
install,
} from "@puppeteer/browsers";
import { Browser, detectBrowserPlatform, getInstalledBrowsers, install } from "@puppeteer/browsers";

const CHROME_VERSION = "131.0.6778.85";
const CACHE_DIR = join(homedir(), ".cache", "hyperframes", "chrome");
Expand All @@ -18,11 +13,7 @@ export function setBrowserPath(path: string): void {
_browserPathOverride = path;
}

export type BrowserSource =
| "env"
| "cache"
| "system"
| "download";
export type BrowserSource = "env" | "cache" | "system" | "download";

export interface BrowserResult {
executablePath: string;
Expand Down Expand Up @@ -76,9 +67,7 @@ async function findFromCache(): Promise<BrowserResult | undefined> {
}

const installed = await getInstalledBrowsers({ cacheDir: CACHE_DIR });
const match = installed.find(
(b) => b.browser === Browser.CHROMEHEADLESSSHELL,
);
const match = installed.find((b) => b.browser === Browser.CHROMEHEADLESSSHELL);
if (match) {
return { executablePath: match.executablePath, source: "cache" };
}
Expand All @@ -93,8 +82,7 @@ function findFromSystem(): BrowserResult | undefined {
}
}

const fromWhich =
whichBinary("google-chrome") ?? whichBinary("chromium");
const fromWhich = whichBinary("google-chrome") ?? whichBinary("chromium");
if (fromWhich) {
return { executablePath: fromWhich, source: "system" };
}
Expand Down Expand Up @@ -122,17 +110,13 @@ export async function findBrowser(): Promise<BrowserResult | undefined> {
* Find or download a browser.
* Resolution: env var -> cached download -> system Chrome -> auto-download.
*/
export async function ensureBrowser(
options?: EnsureBrowserOptions,
): Promise<BrowserResult> {
export async function ensureBrowser(options?: EnsureBrowserOptions): Promise<BrowserResult> {
const existing = await findBrowser();
if (existing) return existing;

const platform = detectBrowserPlatform();
if (!platform) {
throw new Error(
`Unsupported platform: ${process.platform} ${process.arch}`,
);
throw new Error(`Unsupported platform: ${process.platform} ${process.arch}`);
}

const installed = await install({
Expand Down
26 changes: 15 additions & 11 deletions packages/cli/src/commands/benchmark.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,10 @@ const DEFAULT_CONFIGS: BenchmarkConfig[] = [
];

export default defineCommand({
meta: { name: "benchmark", description: "Run multiple render configurations and compare results" },
meta: {
name: "benchmark",
description: "Run multiple render configurations and compare results",
},
args: {
dir: { type: "positional", description: "Project directory", required: false },
runs: { type: "string", description: "Number of runs per config", default: "3" },
Expand Down Expand Up @@ -64,7 +67,9 @@ export default defineCommand({
producer = await loadProducer();
} catch {
if (jsonOutput) {
console.log(JSON.stringify({ error: "Producer module not available. Is the project built?" }));
console.log(
JSON.stringify({ error: "Producer module not available. Is the project built?" }),
);
} else {
errorBox(
"Producer module not available",
Expand Down Expand Up @@ -99,7 +104,10 @@ export default defineCommand({

for (let i = 0; i < runsPerConfig; i++) {
s?.message(`${config.label} — run ${i + 1}/${runsPerConfig}`);
const outputPath = join(benchDir, `${config.label.replace(/[^a-zA-Z0-9]/g, "_")}_run${i}.mp4`);
const outputPath = join(
benchDir,
`${config.label.replace(/[^a-zA-Z0-9]/g, "_")}_run${i}.mp4`,
);

try {
const startTime = Date.now();
Expand Down Expand Up @@ -176,12 +184,9 @@ export default defineCommand({
console.log(separator);

for (const result of results) {
const timeStr =
result.avgTime != null ? formatDuration(result.avgTime) : c.dim("failed");
const sizeStr =
result.avgSize != null ? formatBytes(result.avgSize) : c.dim("n/a");
const failStr =
result.failures > 0 ? c.warn(` (${result.failures} failed)`) : "";
const timeStr = result.avgTime != null ? formatDuration(result.avgTime) : c.dim("failed");
const sizeStr = result.avgSize != null ? formatBytes(result.avgSize) : c.dim("n/a");
const failStr = result.failures > 0 ? c.warn(` (${result.failures} failed)`) : "";

console.log(
" " +
Expand Down Expand Up @@ -215,8 +220,7 @@ export default defineCommand({
} else {
console.log("");
console.log(
c.error("\u2717") +
" All configurations failed. Ensure the rendering pipeline is set up.",
c.error("\u2717") + " All configurations failed. Ensure the rendering pipeline is set up.",
);
}

Expand Down
Loading
Loading