mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-01 19:42:03 +00:00
feat(core): add registry schema + TS types (#252)
* feat(core): add registry schema + TS types
PR 1/17 of the catalog system rollout. Foundation for a shadcn-style
registry with three item tiers: examples (full projects), blocks
(sub-compositions), components (effect snippets).
## What
- TS types: RegistryItem (discriminated union of ExampleItem/BlockItem/
ComponentItem), RegistryManifest, FileTarget, ItemType, FileType
- JSON Schemas: schemas/registry.json, schemas/registry-item.json
- Compile-time exhaustiveness asserts on ITEM_TYPES/FILE_TYPES so adding
to the TS union without updating the constant stops compiling
- Drift-guard test: schema enums must equal ITEM_TYPES/FILE_TYPES by
set-equality; exactly 2 distinct type enums in registry-item.json
- Public API via new ./registry export path plus re-exports from root;
schemas exposed via ./schemas/registry.json export for external tooling
## Why
- Every downstream PR (resolver, installer, hyperframes add, docs
codegen, CI previews, skill, catalog command) builds on these types
- Getting the shape right now avoids painful migrations later
## How
- Discriminated union enforces that components do not have dimensions
or duration and examples/blocks must have them (schema mirrors via
if/then/else on the type discriminant)
- target path pattern rejects .. segments, Unix absolute paths, and
Windows drive letters (defense-in-depth; CLI validates at runtime in
PR 3)
- name pattern requires alphanumeric start and end (no trailing hyphens)
- Optional metadata: version, author, license, deprecated, minCliVersion
- additionalProperties: false on nested objects (catches typos on
critical fields) but relaxed on top-level RegistryItem (allows
third-party custom metadata in PR 15 custom registries)
## Test plan
- [x] Unit tests: 11 new tests covering type guards, discriminant
narrowing, schema/TS drift guards, schema \$id sanity, optional
metadata acceptance, and compile-time checks (via @ts-expect-error)
- [x] bun run test in packages/core: 445 passed (was 434 on main,
+11 from this PR)
- [x] bunx oxfmt and bunx oxlint: clean
- [x] bun run typecheck: clean
- [ ] Manual testing: N/A (types + schemas only)
- [ ] Documentation updated: per-item doc pages land in PR 9 (codegen
from these manifests); guide updates in PR 10+
## Breaking / migration
None. Pure additive — new module, new export paths, no existing
surface touched.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(core): remove version field + hyperframes:demo file type
Address review feedback from Miguel:
- Remove `version` from RegistryItemBase + schema. Per shadcn model,
the registry is versioned by git tags, not per-item. The adversarial
review added it; the original design doc was correct.
- Remove `hyperframes:demo` from FileType union + FILE_TYPES constant
+ schema. Demo files exist on disk for the CI preview pipeline but
are NOT installed to user projects and should not appear in
registry-item.json files[]. Neither shadcn nor Remotion has a
dedicated demo file type — demos are just compositions.
- Add `required: ["type"]` to the if-condition in the schema's
allOf discriminant (Miguel's nit — makes the condition self-
contained)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(ci): add shader-transitions to Dockerfile.test
PR #251 added packages/shader-transitions/ to the workspace but didn't
update Dockerfile.test to COPY its package.json. This caused
`bun install --frozen-lockfile` to fail in the regression Docker build:
bun saw a lockfile referencing @hyperframes/shader-transitions but the
package.json wasn't present in the container, so it wanted to remove
the entry — triggering "lockfile had changes."
Verified: Docker build passes with `--no-cache` after this fix.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
cb3d94c2a5
commit
eb338ae859
@@ -72,6 +72,7 @@ COPY packages/player/package.json packages/player/package.json
|
||||
COPY packages/producer/package.json packages/producer/package.json
|
||||
COPY packages/cli/package.json packages/cli/package.json
|
||||
COPY packages/studio/package.json packages/studio/package.json
|
||||
COPY packages/shader-transitions/package.json packages/shader-transitions/package.json
|
||||
RUN bun install --frozen-lockfile
|
||||
|
||||
# Copy source
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"files": [
|
||||
"dist",
|
||||
"docs",
|
||||
"schemas",
|
||||
"README.md"
|
||||
],
|
||||
"type": "module",
|
||||
@@ -36,7 +37,13 @@
|
||||
"./text": {
|
||||
"import": "./src/text/index.ts",
|
||||
"types": "./src/text/index.ts"
|
||||
}
|
||||
},
|
||||
"./registry": {
|
||||
"import": "./src/registry/index.ts",
|
||||
"types": "./src/registry/index.ts"
|
||||
},
|
||||
"./schemas/registry.json": "./schemas/registry.json",
|
||||
"./schemas/registry-item.json": "./schemas/registry-item.json"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
@@ -61,7 +68,13 @@
|
||||
"./text": {
|
||||
"import": "./dist/text/index.js",
|
||||
"types": "./dist/text/index.d.ts"
|
||||
}
|
||||
},
|
||||
"./registry": {
|
||||
"import": "./dist/registry/index.js",
|
||||
"types": "./dist/registry/index.d.ts"
|
||||
},
|
||||
"./schemas/registry.json": "./schemas/registry.json",
|
||||
"./schemas/registry-item.json": "./schemas/registry-item.json"
|
||||
},
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts"
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://hyperframes.heygen.com/schema/registry-item.json",
|
||||
"title": "Hyperframes Registry Item",
|
||||
"description": "Manifest for a single distributable item (example, block, or component).",
|
||||
"type": "object",
|
||||
"required": ["name", "type", "title", "description", "files"],
|
||||
"properties": {
|
||||
"$schema": {
|
||||
"type": "string",
|
||||
"format": "uri"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?$",
|
||||
"description": "Item name in kebab-case, must start and end with alphanumeric."
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["hyperframes:example", "hyperframes:block", "hyperframes:component"]
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"tags": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" }
|
||||
},
|
||||
"author": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"license": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"description": "SPDX license identifier (e.g. \"Apache-2.0\", \"MIT\")."
|
||||
},
|
||||
"minCliVersion": {
|
||||
"type": "string",
|
||||
"pattern": "^\\d+\\.\\d+\\.\\d+(?:-[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?$",
|
||||
"description": "Minimum `hyperframes` CLI version required to install this item."
|
||||
},
|
||||
"deprecated": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"description": "If set, the item is deprecated; the value is the reason or migration note."
|
||||
},
|
||||
"dimensions": {
|
||||
"type": "object",
|
||||
"required": ["width", "height"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"width": { "type": "integer", "minimum": 1 },
|
||||
"height": { "type": "integer", "minimum": 1 }
|
||||
}
|
||||
},
|
||||
"duration": {
|
||||
"type": "number",
|
||||
"exclusiveMinimum": 0,
|
||||
"description": "Duration in seconds. Must be > 0."
|
||||
},
|
||||
"registryDependencies": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?$"
|
||||
}
|
||||
},
|
||||
"files": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["path", "target", "type"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"description": "Source path, relative to registry-item.json."
|
||||
},
|
||||
"target": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"description": "Destination path in the user's project, relative to project root. Must not traverse outside the project (no `..` segments, no absolute paths).",
|
||||
"not": {
|
||||
"anyOf": [
|
||||
{ "pattern": "(^|[/\\\\])\\.\\.([/\\\\]|$)" },
|
||||
{ "pattern": "^[/\\\\]" },
|
||||
{ "pattern": "^[A-Za-z]:[/\\\\]" }
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"hyperframes:composition",
|
||||
"hyperframes:asset",
|
||||
"hyperframes:snippet",
|
||||
"hyperframes:style",
|
||||
"hyperframes:timeline"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"preview": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"video": { "type": "string" },
|
||||
"poster": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"relatedSkill": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
}
|
||||
},
|
||||
"allOf": [
|
||||
{
|
||||
"if": {
|
||||
"required": ["type"],
|
||||
"properties": { "type": { "const": "hyperframes:component" } }
|
||||
},
|
||||
"then": {
|
||||
"not": {
|
||||
"anyOf": [{ "required": ["dimensions"] }, { "required": ["duration"] }]
|
||||
}
|
||||
},
|
||||
"else": {
|
||||
"required": ["dimensions", "duration"]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://hyperframes.heygen.com/schema/registry.json",
|
||||
"title": "Hyperframes Registry Manifest",
|
||||
"description": "Top-level manifest describing all items in a Hyperframes registry.",
|
||||
"type": "object",
|
||||
"required": ["name", "homepage", "items"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"$schema": {
|
||||
"type": "string",
|
||||
"format": "uri"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"description": "Registry name (e.g. \"hyperframes\")."
|
||||
},
|
||||
"homepage": {
|
||||
"type": "string",
|
||||
"format": "uri",
|
||||
"description": "Registry homepage URL."
|
||||
},
|
||||
"items": {
|
||||
"type": "array",
|
||||
"description": "Items in this registry. Each entry is a shorthand reference; the full item manifest lives at <type-dir>/<name>/registry-item.json.",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["name", "type"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?$",
|
||||
"description": "Item name in kebab-case, must start and end with alphanumeric."
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["hyperframes:example", "hyperframes:block", "hyperframes:component"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -163,3 +163,26 @@ export { createGSAPFrameAdapter } from "./adapters/gsap";
|
||||
// Text measurement
|
||||
export { fitTextFontSize } from "./text/index.js";
|
||||
export type { FitTextOptions, FitTextResult } from "./text/index.js";
|
||||
|
||||
// Registry
|
||||
export type {
|
||||
ItemType,
|
||||
FileType,
|
||||
FileTarget,
|
||||
RegistryItemDimensions,
|
||||
RegistryItemPreview,
|
||||
RegistryItem,
|
||||
ExampleItem,
|
||||
BlockItem,
|
||||
ComponentItem,
|
||||
RegistryManifestEntry,
|
||||
RegistryManifest,
|
||||
} from "./registry/index.js";
|
||||
|
||||
export {
|
||||
ITEM_TYPES,
|
||||
FILE_TYPES,
|
||||
isExampleItem,
|
||||
isBlockItem,
|
||||
isComponentItem,
|
||||
} from "./registry/index.js";
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
export type {
|
||||
ItemType,
|
||||
FileType,
|
||||
FileTarget,
|
||||
RegistryItemDimensions,
|
||||
RegistryItemPreview,
|
||||
RegistryItem,
|
||||
ExampleItem,
|
||||
BlockItem,
|
||||
ComponentItem,
|
||||
RegistryManifestEntry,
|
||||
RegistryManifest,
|
||||
} from "./types.js";
|
||||
|
||||
export { ITEM_TYPES, FILE_TYPES, isExampleItem, isBlockItem, isComponentItem } from "./types.js";
|
||||
@@ -0,0 +1,236 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import {
|
||||
FILE_TYPES,
|
||||
ITEM_TYPES,
|
||||
isBlockItem,
|
||||
isComponentItem,
|
||||
isExampleItem,
|
||||
type BlockItem,
|
||||
type ComponentItem,
|
||||
type ExampleItem,
|
||||
type FileType,
|
||||
type ItemType,
|
||||
type RegistryItem,
|
||||
type RegistryManifest,
|
||||
} from "./index.js";
|
||||
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
const schemasDir = resolve(here, "..", "..", "schemas");
|
||||
|
||||
function readSchema(name: string): Record<string, unknown> {
|
||||
const raw = readFileSync(resolve(schemasDir, name), "utf-8");
|
||||
return JSON.parse(raw) as Record<string, unknown>;
|
||||
}
|
||||
|
||||
// Walk a JSON schema and collect every `enum` array found under a property
|
||||
// with the given name. Visits each node exactly once; no cycles in parsed JSON.
|
||||
function collectEnums(schema: unknown, propName: string): string[][] {
|
||||
const results: string[][] = [];
|
||||
const visit = (node: unknown): void => {
|
||||
if (!node || typeof node !== "object") return;
|
||||
if (Array.isArray(node)) {
|
||||
node.forEach(visit);
|
||||
return;
|
||||
}
|
||||
const obj = node as Record<string, unknown>;
|
||||
const props = obj.properties;
|
||||
if (props && typeof props === "object") {
|
||||
const target = (props as Record<string, unknown>)[propName];
|
||||
if (target && typeof target === "object") {
|
||||
const e = (target as Record<string, unknown>).enum;
|
||||
if (Array.isArray(e)) results.push(e.map(String));
|
||||
}
|
||||
}
|
||||
for (const v of Object.values(obj)) visit(v);
|
||||
};
|
||||
visit(schema);
|
||||
return results;
|
||||
}
|
||||
|
||||
function setKey(values: readonly string[]): string {
|
||||
return [...values].sort().join("|");
|
||||
}
|
||||
|
||||
function setEquals(a: readonly string[], b: readonly string[]): boolean {
|
||||
return setKey(a) === setKey(b);
|
||||
}
|
||||
|
||||
describe("registry types", () => {
|
||||
const registrySchema = readSchema("registry.json");
|
||||
const registryItemSchema = readSchema("registry-item.json");
|
||||
|
||||
describe("type guards", () => {
|
||||
const baseFiles = [
|
||||
{ path: "x.html", target: "compositions/x.html", type: "hyperframes:composition" as const },
|
||||
];
|
||||
const example: ExampleItem = {
|
||||
name: "demo",
|
||||
type: "hyperframes:example",
|
||||
title: "Demo",
|
||||
description: "d",
|
||||
dimensions: { width: 1920, height: 1080 },
|
||||
duration: 5,
|
||||
files: baseFiles,
|
||||
};
|
||||
const block: BlockItem = {
|
||||
name: "demo-block",
|
||||
type: "hyperframes:block",
|
||||
title: "Demo Block",
|
||||
description: "d",
|
||||
dimensions: { width: 1080, height: 1350 },
|
||||
duration: 6,
|
||||
files: baseFiles,
|
||||
};
|
||||
const component: ComponentItem = {
|
||||
name: "demo-component",
|
||||
type: "hyperframes:component",
|
||||
title: "Demo Component",
|
||||
description: "d",
|
||||
files: baseFiles,
|
||||
};
|
||||
|
||||
it("discriminates item types", () => {
|
||||
expect(isExampleItem(example)).toBe(true);
|
||||
expect(isExampleItem(block)).toBe(false);
|
||||
expect(isBlockItem(block)).toBe(true);
|
||||
expect(isBlockItem(component)).toBe(false);
|
||||
expect(isComponentItem(component)).toBe(true);
|
||||
expect(isComponentItem(example)).toBe(false);
|
||||
});
|
||||
|
||||
it("narrows to the discriminant's shape", () => {
|
||||
const items: RegistryItem[] = [example, block, component];
|
||||
for (const item of items) {
|
||||
if (isExampleItem(item) || isBlockItem(item)) {
|
||||
// dimensions and duration are required on examples and blocks.
|
||||
expect(item.dimensions.width).toBeGreaterThan(0);
|
||||
expect(item.duration).toBeGreaterThan(0);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("constants match schema enums (drift guard)", () => {
|
||||
it("registry.json has exactly one `type` enum, equal to ITEM_TYPES", () => {
|
||||
const enums = collectEnums(registrySchema, "type");
|
||||
expect(enums).toHaveLength(1);
|
||||
expect(setEquals(enums[0]!, ITEM_TYPES)).toBe(true);
|
||||
});
|
||||
|
||||
it("registry-item.json has exactly two `type` enums: one ITEM_TYPES, one FILE_TYPES", () => {
|
||||
const enums = collectEnums(registryItemSchema, "type");
|
||||
const distinct = new Set(enums.map(setKey));
|
||||
// Two semantically distinct enums — the item's `type` and each file's `type`.
|
||||
expect(distinct.size).toBe(2);
|
||||
expect(enums.some((e) => setEquals(e, ITEM_TYPES))).toBe(true);
|
||||
expect(enums.some((e) => setEquals(e, FILE_TYPES))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("schema files", () => {
|
||||
it("registry.json has the expected $id", () => {
|
||||
expect(registrySchema.$id).toBe("https://hyperframes.heygen.com/schema/registry.json");
|
||||
});
|
||||
|
||||
it("registry-item.json has the expected $id", () => {
|
||||
expect(registryItemSchema.$id).toBe(
|
||||
"https://hyperframes.heygen.com/schema/registry-item.json",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("type-level sanity", () => {
|
||||
it("RegistryManifest accepts well-formed shape", () => {
|
||||
const m: RegistryManifest = {
|
||||
$schema: "https://hyperframes.heygen.com/schema/registry.json",
|
||||
name: "hyperframes",
|
||||
homepage: "https://hyperframes.heygen.com",
|
||||
items: [
|
||||
{ name: "warm-grain", type: "hyperframes:example" },
|
||||
{ name: "linkedin-post-card", type: "hyperframes:block" },
|
||||
{ name: "shader-wipe", type: "hyperframes:component" },
|
||||
],
|
||||
};
|
||||
expect(m.items).toHaveLength(3);
|
||||
});
|
||||
|
||||
it("ItemType and FileType are assignable from their constants", () => {
|
||||
const _it: ItemType = ITEM_TYPES[0];
|
||||
const _ft: FileType = FILE_TYPES[0];
|
||||
expect(_it).toBeDefined();
|
||||
expect(_ft).toBeDefined();
|
||||
});
|
||||
|
||||
it("components cannot carry dimensions or duration (compile-time)", () => {
|
||||
// @ts-expect-error — ComponentItem forbids `dimensions`.
|
||||
const _bad1: ComponentItem = {
|
||||
name: "bad",
|
||||
type: "hyperframes:component",
|
||||
title: "Bad",
|
||||
description: "d",
|
||||
files: [],
|
||||
dimensions: { width: 1, height: 1 },
|
||||
};
|
||||
// @ts-expect-error — ComponentItem forbids `duration`.
|
||||
const _bad2: ComponentItem = {
|
||||
name: "bad",
|
||||
type: "hyperframes:component",
|
||||
title: "Bad",
|
||||
description: "d",
|
||||
files: [],
|
||||
duration: 1,
|
||||
};
|
||||
void _bad1;
|
||||
void _bad2;
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
|
||||
it("examples and blocks require dimensions and duration (compile-time)", () => {
|
||||
// @ts-expect-error — ExampleItem requires `dimensions`.
|
||||
const _bad1: ExampleItem = {
|
||||
name: "bad",
|
||||
type: "hyperframes:example",
|
||||
title: "Bad",
|
||||
description: "d",
|
||||
duration: 5,
|
||||
files: [],
|
||||
};
|
||||
// @ts-expect-error — BlockItem requires `duration`.
|
||||
const _bad2: BlockItem = {
|
||||
name: "bad",
|
||||
type: "hyperframes:block",
|
||||
title: "Bad",
|
||||
description: "d",
|
||||
dimensions: { width: 1, height: 1 },
|
||||
files: [],
|
||||
};
|
||||
void _bad1;
|
||||
void _bad2;
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
|
||||
it("optional metadata fields are accepted (author, license, deprecated, minCliVersion)", () => {
|
||||
const item: ComponentItem = {
|
||||
name: "shader-wipe",
|
||||
type: "hyperframes:component",
|
||||
title: "Shader Wipe",
|
||||
description: "d",
|
||||
author: "heygen",
|
||||
license: "Apache-2.0",
|
||||
minCliVersion: "0.4.0",
|
||||
deprecated: "Use `shader-wipe-v2` instead.",
|
||||
files: [
|
||||
{
|
||||
path: "shader-wipe.html",
|
||||
target: "compositions/components/shader-wipe/shader-wipe.html",
|
||||
type: "hyperframes:snippet",
|
||||
},
|
||||
],
|
||||
};
|
||||
expect(item.author).toBe("heygen");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,158 @@
|
||||
// The `enum` arrays in `packages/core/schemas/registry*.json` must match
|
||||
// `ITEM_TYPES` / `FILE_TYPES` below — `types.test.ts` is the drift guard.
|
||||
|
||||
/** Top-level classification for a registry item. */
|
||||
export type ItemType = "hyperframes:example" | "hyperframes:block" | "hyperframes:component";
|
||||
|
||||
/** File-level classification, drives installer behavior. */
|
||||
export type FileType =
|
||||
| "hyperframes:composition"
|
||||
| "hyperframes:asset"
|
||||
| "hyperframes:snippet"
|
||||
| "hyperframes:style"
|
||||
| "hyperframes:timeline";
|
||||
|
||||
/** A single file to install as part of a registry item. */
|
||||
export interface FileTarget {
|
||||
/** Path to the source file, relative to the item's `registry-item.json`. */
|
||||
path: string;
|
||||
/** Destination path in the user's project, relative to the project root. */
|
||||
target: string;
|
||||
/** File type — controls how the installer treats this file. */
|
||||
type: FileType;
|
||||
}
|
||||
|
||||
export interface RegistryItemDimensions {
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
export interface RegistryItemPreview {
|
||||
/** Path or URL to the preview video (looping mp4). */
|
||||
video?: string;
|
||||
/** Path or URL to the preview poster image. */
|
||||
poster?: string;
|
||||
}
|
||||
|
||||
/** Fields common to every registry item, regardless of type. */
|
||||
interface RegistryItemBase {
|
||||
/** JSON Schema URL — `https://hyperframes.heygen.com/schema/registry-item.json`. */
|
||||
$schema?: string;
|
||||
/** Item name in kebab-case, unique within a registry. */
|
||||
name: string;
|
||||
/** Short human-readable title. */
|
||||
title: string;
|
||||
/** One-line description. */
|
||||
description: string;
|
||||
/** Filter tags (e.g. `["social", "portrait", "card"]`). */
|
||||
tags?: string[];
|
||||
/** Item author / maintainer. */
|
||||
author?: string;
|
||||
/** SPDX license identifier. */
|
||||
license?: string;
|
||||
/** Minimum `hyperframes` CLI version required to install this item (semver). */
|
||||
minCliVersion?: string;
|
||||
/** If set, the item is deprecated; the value is the reason or migration note. */
|
||||
deprecated?: string;
|
||||
/** Names of other registry items this item depends on. */
|
||||
registryDependencies?: string[];
|
||||
/** Files to install. Must be non-empty. */
|
||||
files: FileTarget[];
|
||||
/** Optional preview media. */
|
||||
preview?: RegistryItemPreview;
|
||||
/** Related skill slug (e.g. `hyperframes-captions`) — shown in docs. */
|
||||
relatedSkill?: string;
|
||||
}
|
||||
|
||||
/** Full-project example — scaffolded by `hyperframes init --example <name>`. */
|
||||
export interface ExampleItem extends RegistryItemBase {
|
||||
type: "hyperframes:example";
|
||||
/** Canvas dimensions (required for examples). */
|
||||
dimensions: RegistryItemDimensions;
|
||||
/** Duration in seconds (required for examples). */
|
||||
duration: number;
|
||||
}
|
||||
|
||||
/** Sub-composition block — installed by `hyperframes add <name>`. */
|
||||
export interface BlockItem extends RegistryItemBase {
|
||||
type: "hyperframes:block";
|
||||
/** Canvas dimensions (required for blocks — they are standalone compositions). */
|
||||
dimensions: RegistryItemDimensions;
|
||||
/** Duration in seconds (required for blocks). */
|
||||
duration: number;
|
||||
}
|
||||
|
||||
/** Effect / snippet — merged into an existing composition. */
|
||||
export interface ComponentItem extends RegistryItemBase {
|
||||
type: "hyperframes:component";
|
||||
/** Components have no intrinsic dimensions — they inherit from the host composition. */
|
||||
dimensions?: never;
|
||||
/** Components have no intrinsic duration — they inherit from the host composition. */
|
||||
duration?: never;
|
||||
}
|
||||
|
||||
/**
|
||||
* A registry item — the unit of distribution. Stored on disk as
|
||||
* `registry/<examples|blocks|components>/<name>/registry-item.json`.
|
||||
*/
|
||||
export type RegistryItem = ExampleItem | BlockItem | ComponentItem;
|
||||
|
||||
/** Shorthand reference used in the top-level `registry.json` items array. */
|
||||
export interface RegistryManifestEntry {
|
||||
name: string;
|
||||
type: ItemType;
|
||||
}
|
||||
|
||||
/** The top-level `registry.json` manifest. */
|
||||
export interface RegistryManifest {
|
||||
/** JSON Schema URL — `https://hyperframes.heygen.com/schema/registry.json`. */
|
||||
$schema?: string;
|
||||
/** Registry name (e.g. `hyperframes`). */
|
||||
name: string;
|
||||
/** Registry homepage URL. */
|
||||
homepage: string;
|
||||
/** Items in this registry. */
|
||||
items: RegistryManifestEntry[];
|
||||
}
|
||||
|
||||
// ── Constants (kept in sync with JSON Schema enums) ─────────────────────────
|
||||
|
||||
export const ITEM_TYPES = [
|
||||
"hyperframes:example",
|
||||
"hyperframes:block",
|
||||
"hyperframes:component",
|
||||
] as const satisfies readonly ItemType[];
|
||||
|
||||
export const FILE_TYPES = [
|
||||
"hyperframes:composition",
|
||||
"hyperframes:asset",
|
||||
"hyperframes:snippet",
|
||||
"hyperframes:style",
|
||||
"hyperframes:timeline",
|
||||
] as const satisfies readonly FileType[];
|
||||
|
||||
// Compile-time exhaustiveness: every member of the TS union appears in the constant.
|
||||
// If someone adds to `ItemType`/`FileType` without updating `ITEM_TYPES`/`FILE_TYPES`,
|
||||
// these lines stop compiling. (The `satisfies` above covers the other direction.)
|
||||
type _AssertItemTypesExhaustive =
|
||||
Exclude<ItemType, (typeof ITEM_TYPES)[number]> extends never ? true : never;
|
||||
type _AssertFileTypesExhaustive =
|
||||
Exclude<FileType, (typeof FILE_TYPES)[number]> extends never ? true : never;
|
||||
const _itemTypesExhaustive: _AssertItemTypesExhaustive = true;
|
||||
const _fileTypesExhaustive: _AssertFileTypesExhaustive = true;
|
||||
void _itemTypesExhaustive;
|
||||
void _fileTypesExhaustive;
|
||||
|
||||
// ── Type guards ─────────────────────────────────────────────────────────────
|
||||
|
||||
export function isExampleItem(item: RegistryItem): item is ExampleItem {
|
||||
return item.type === "hyperframes:example";
|
||||
}
|
||||
|
||||
export function isBlockItem(item: RegistryItem): item is BlockItem {
|
||||
return item.type === "hyperframes:block";
|
||||
}
|
||||
|
||||
export function isComponentItem(item: RegistryItem): item is ComponentItem {
|
||||
return item.type === "hyperframes:component";
|
||||
}
|
||||
Reference in New Issue
Block a user