mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-08-31 02:41:44 +00:00
refactor(core): own edit protocol contract
This commit is contained in:
+3
-2
@@ -24,13 +24,14 @@
|
||||
"changelog:weekly": "tsx scripts/changelog-weekly.ts",
|
||||
"sync-schemas": "tsx scripts/sync-schemas.ts",
|
||||
"sync-schemas:check": "tsx scripts/sync-schemas.ts --check",
|
||||
"lint": "bun run check:tracked-artifacts && bun run check:workspace-contracts && bun run check:gcp-cloud-run-dockerfile && oxlint . && tsx scripts/lint-skills.ts && node scripts/check-skill-mirror.mjs",
|
||||
"lint": "bun run check:tracked-artifacts && bun run check:workspace-contracts && bun run check:gcp-cloud-run-dockerfile && bun run check:package-cycles && oxlint . && tsx scripts/lint-skills.ts && node scripts/check-skill-mirror.mjs",
|
||||
"check:gcp-cloud-run-dockerfile": "bun run --cwd packages/gcp-cloud-run test:dockerfile-workspaces",
|
||||
"lint:skills": "tsx scripts/lint-skills.ts",
|
||||
"check:skill-mirror": "node scripts/check-skill-mirror.mjs",
|
||||
"lint:fix": "oxlint --fix .",
|
||||
"check:tracked-artifacts": "node scripts/check-tracked-artifacts.mjs",
|
||||
"check:workspace-contracts": "node scripts/check-workspace-contracts.mjs",
|
||||
"check:package-cycles": "node scripts/check-package-cycles.mjs",
|
||||
"format": "oxfmt .",
|
||||
"test": "bun run test:unit",
|
||||
"test:unit": "bun run --filter '*' test",
|
||||
@@ -43,7 +44,7 @@
|
||||
"player:perf": "bun run --filter @hyperframes/player perf",
|
||||
"format:check": "oxfmt --check .",
|
||||
"knip": "knip",
|
||||
"test:scripts": "node --import tsx --test scripts/check-tracked-artifacts.test.mjs scripts/check-workspace-contracts.test.mjs scripts/validate-release-channel.test.mjs scripts/draft-changelog.test.ts scripts/set-version.test.ts scripts/release-prepare.test.ts scripts/cli-options.test.ts scripts/changelog-weekly.test.ts scripts/claude-plugin-compression.test.ts scripts/studio-runtime-smoke.test.mjs scripts/verify-packed-manifests.test.mjs scripts/lint-skills.test.mjs packages/gcp-cloud-run/check-dockerfile-workspaces.test.mjs",
|
||||
"test:scripts": "node --import tsx --test scripts/check-tracked-artifacts.test.mjs scripts/check-workspace-contracts.test.mjs scripts/check-package-cycles.test.mjs scripts/validate-release-channel.test.mjs scripts/draft-changelog.test.ts scripts/set-version.test.ts scripts/release-prepare.test.ts scripts/cli-options.test.ts scripts/changelog-weekly.test.ts scripts/claude-plugin-compression.test.ts scripts/studio-runtime-smoke.test.mjs scripts/verify-packed-manifests.test.mjs scripts/lint-skills.test.mjs packages/gcp-cloud-run/check-dockerfile-workspaces.test.mjs",
|
||||
"test:skills": "node --test 'skills/**/*.test.mjs'",
|
||||
"generate:previews": "tsx scripts/generate-template-previews.ts",
|
||||
"generate:catalog-previews": "tsx scripts/generate-catalog-previews.ts",
|
||||
|
||||
@@ -52,6 +52,12 @@
|
||||
"import": "./src/editing/affordances.ts",
|
||||
"types": "./src/editing/affordances.ts"
|
||||
},
|
||||
"./editing/draft-markers": {
|
||||
"bun": "./src/editing/draftMarkers.ts",
|
||||
"node": "./dist/editing/draftMarkers.js",
|
||||
"import": "./src/editing/draftMarkers.ts",
|
||||
"types": "./src/editing/draftMarkers.ts"
|
||||
},
|
||||
"./variables": {
|
||||
"bun": "./src/variables.ts",
|
||||
"node": "./dist/variables.js",
|
||||
@@ -290,6 +296,10 @@
|
||||
"import": "./dist/editing/affordances.js",
|
||||
"types": "./dist/editing/affordances.d.ts"
|
||||
},
|
||||
"./editing/draft-markers": {
|
||||
"import": "./dist/editing/draftMarkers.js",
|
||||
"types": "./dist/editing/draftMarkers.d.ts"
|
||||
},
|
||||
"./figma": {
|
||||
"import": "./dist/figma/index.js",
|
||||
"types": "./dist/figma/index.d.ts"
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Browser-safe wire constants shared by the runtime, Studio, and studio-server.
|
||||
*
|
||||
* Keep this module dependency-free. These names are part of the edit protocol,
|
||||
* not server implementation details, so browser code must be able to import
|
||||
* them without pulling the studio-server package into its graph.
|
||||
*/
|
||||
export const STUDIO_OFFSET_X_PROP = "--hf-studio-offset-x";
|
||||
export const STUDIO_OFFSET_Y_PROP = "--hf-studio-offset-y";
|
||||
export const STUDIO_WIDTH_PROP = "--hf-studio-width";
|
||||
export const STUDIO_HEIGHT_PROP = "--hf-studio-height";
|
||||
export const STUDIO_MANUAL_EDIT_GESTURE_ATTR = "data-hf-studio-manual-edit-gesture";
|
||||
@@ -10,7 +10,7 @@
|
||||
* 2. `wordIndex` — fallback, DOM traversal order across .caption-group > span
|
||||
*/
|
||||
|
||||
export interface CaptionOverride {
|
||||
interface CaptionOverride {
|
||||
wordId?: string;
|
||||
wordIndex?: number;
|
||||
x?: number;
|
||||
|
||||
@@ -42,7 +42,7 @@ import { createColorGradingRuntime, type RuntimeColorGradingApi } from "./colorG
|
||||
import { TransportClock } from "./clock";
|
||||
import { WebAudioTransport } from "./webAudioTransport";
|
||||
import { quantizeTimeToFrame } from "../inline-scripts/parityContract";
|
||||
import { STUDIO_MANUAL_EDIT_GESTURE_ATTR } from "../studio-api/helpers/draftMarkers";
|
||||
import { STUDIO_MANUAL_EDIT_GESTURE_ATTR } from "../editing/draftMarkers";
|
||||
import type {
|
||||
RuntimeDeterministicAdapter,
|
||||
RuntimeJson,
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
/** @deprecated Import from @hyperframes/studio-server/draft-markers */
|
||||
export * from "@hyperframes/studio-server/draft-markers";
|
||||
/** @deprecated Import from @hyperframes/core/editing/draft-markers. */
|
||||
export * from "../../editing/draftMarkers.js";
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
/**
|
||||
* Draft-marker constants shared between core's PreviewAdapter and Studio's
|
||||
* manual-edits code. CSS custom properties written during a drag gesture, plus
|
||||
* the gesture marker attribute. Exported from @hyperframes/core/studio-api/draft-markers.
|
||||
* Compatibility export for server consumers. The edit-protocol constants are
|
||||
* owned by core's dependency-free browser contract.
|
||||
*/
|
||||
export const STUDIO_OFFSET_X_PROP = "--hf-studio-offset-x";
|
||||
export const STUDIO_OFFSET_Y_PROP = "--hf-studio-offset-y";
|
||||
export const STUDIO_WIDTH_PROP = "--hf-studio-width";
|
||||
export const STUDIO_HEIGHT_PROP = "--hf-studio-height";
|
||||
export const STUDIO_MANUAL_EDIT_GESTURE_ATTR = "data-hf-studio-manual-edit-gesture";
|
||||
export * from "@hyperframes/core/editing/draft-markers";
|
||||
|
||||
@@ -5,7 +5,7 @@ export {
|
||||
STUDIO_WIDTH_PROP,
|
||||
STUDIO_HEIGHT_PROP,
|
||||
STUDIO_MANUAL_EDIT_GESTURE_ATTR,
|
||||
} from "@hyperframes/studio-server/draft-markers";
|
||||
} from "@hyperframes/core/editing/draft-markers";
|
||||
export const STUDIO_ROTATION_PROP = "--hf-studio-rotation";
|
||||
|
||||
/* ── Internal DOM attribute names ─────────────────────────────────── */
|
||||
|
||||
@@ -11,8 +11,6 @@ export { resolveIframe } from "./lib/timelineDOM";
|
||||
|
||||
// Store
|
||||
export { usePlayerStore, liveTime } from "./store/playerStore";
|
||||
// Public library surface; external consumers are invisible to the workspace analyzer.
|
||||
// fallow-ignore-next-line unused-exports
|
||||
export type { SelectElementOptions, TimelineElement, ZoomMode } from "./store/playerStore";
|
||||
|
||||
// Utils
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const ROOT = join(import.meta.dirname, "..");
|
||||
const RUNTIME_DEPENDENCY_FIELDS = ["dependencies", "optionalDependencies", "peerDependencies"];
|
||||
|
||||
/**
|
||||
* The deprecated @hyperframes/core/studio-api forwarding surface is public and
|
||||
* cannot disappear before a breaking release. Keep its one known package SCC
|
||||
* explicit so every new cycle still fails CI. Delete this exception together
|
||||
* with the forwarding surface in the next breaking release.
|
||||
*/
|
||||
export const ALLOWED_COMPATIBILITY_CYCLES = [
|
||||
{
|
||||
packages: ["@hyperframes/core", "@hyperframes/studio-server"],
|
||||
reason: "Deprecated core/studio-api forwarding exports; remove at the next breaking release.",
|
||||
},
|
||||
];
|
||||
|
||||
function canonicalComponent(names) {
|
||||
return [...names].sort().join(" -> ");
|
||||
}
|
||||
|
||||
export function listRuntimePackageCycles(packages) {
|
||||
const byName = new Map(packages.map((pkg) => [pkg.name, pkg]));
|
||||
const edges = new Map(
|
||||
// Collecting three dependency classes in one expression keeps the graph
|
||||
// construction declarative; focused tests cover runtime versus dev edges.
|
||||
// fallow-ignore-next-line complexity
|
||||
packages.map((pkg) => {
|
||||
const targets = new Set();
|
||||
for (const field of RUNTIME_DEPENDENCY_FIELDS) {
|
||||
for (const dependency of Object.keys(pkg[field] ?? {})) {
|
||||
if (byName.has(dependency)) targets.add(dependency);
|
||||
}
|
||||
}
|
||||
return [pkg.name, [...targets].sort()];
|
||||
}),
|
||||
);
|
||||
|
||||
let nextIndex = 0;
|
||||
const indexes = new Map();
|
||||
const lowLinks = new Map();
|
||||
const stack = [];
|
||||
const onStack = new Set();
|
||||
const components = [];
|
||||
|
||||
// Tarjan's strongly-connected-component walk is intentionally branchy; the
|
||||
// state transitions mirror the algorithm and are covered by focused tests.
|
||||
// fallow-ignore-next-line complexity
|
||||
function visit(name) {
|
||||
indexes.set(name, nextIndex);
|
||||
lowLinks.set(name, nextIndex);
|
||||
nextIndex += 1;
|
||||
stack.push(name);
|
||||
onStack.add(name);
|
||||
|
||||
for (const target of edges.get(name) ?? []) {
|
||||
if (!indexes.has(target)) {
|
||||
visit(target);
|
||||
lowLinks.set(name, Math.min(lowLinks.get(name), lowLinks.get(target)));
|
||||
} else if (onStack.has(target)) {
|
||||
lowLinks.set(name, Math.min(lowLinks.get(name), indexes.get(target)));
|
||||
}
|
||||
}
|
||||
|
||||
if (lowLinks.get(name) !== indexes.get(name)) return;
|
||||
const component = [];
|
||||
let member;
|
||||
do {
|
||||
member = stack.pop();
|
||||
onStack.delete(member);
|
||||
component.push(member);
|
||||
} while (member !== name);
|
||||
|
||||
const selfCycle = component.length === 1 && (edges.get(name) ?? []).includes(name);
|
||||
if (component.length > 1 || selfCycle) components.push(component.sort());
|
||||
}
|
||||
|
||||
for (const name of [...byName.keys()].sort()) {
|
||||
if (!indexes.has(name)) visit(name);
|
||||
}
|
||||
return components.sort((a, b) => canonicalComponent(a).localeCompare(canonicalComponent(b)));
|
||||
}
|
||||
|
||||
export function listPackageCycleIssues(packages, allowed = ALLOWED_COMPATIBILITY_CYCLES) {
|
||||
const allowedKeys = new Set(allowed.map((entry) => canonicalComponent(entry.packages)));
|
||||
return listRuntimePackageCycles(packages)
|
||||
.map(canonicalComponent)
|
||||
.filter((component) => !allowedKeys.has(component))
|
||||
.map((component) => `runtime workspace dependency cycle: ${component}`);
|
||||
}
|
||||
|
||||
export function readWorkspacePackages(root = ROOT) {
|
||||
return readdirSync(join(root, "packages"))
|
||||
.sort()
|
||||
.map((directory) => join(root, "packages", directory, "package.json"))
|
||||
.filter(existsSync)
|
||||
.map((path) => JSON.parse(readFileSync(path, "utf8")));
|
||||
}
|
||||
|
||||
function main() {
|
||||
const packages = readWorkspacePackages();
|
||||
const cycles = listRuntimePackageCycles(packages);
|
||||
const issues = listPackageCycleIssues(packages);
|
||||
if (issues.length > 0) {
|
||||
console.error("Package cycle violations:");
|
||||
issues.forEach((issue) => console.error(`- ${issue}`));
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
const allowedKeys = new Set(
|
||||
ALLOWED_COMPATIBILITY_CYCLES.map((entry) => canonicalComponent(entry.packages)),
|
||||
);
|
||||
const activeCompatibilityCycles = cycles.filter((cycle) =>
|
||||
allowedKeys.has(canonicalComponent(cycle)),
|
||||
);
|
||||
if (activeCompatibilityCycles.length > 0) {
|
||||
console.log(
|
||||
`Package graph verified; ${activeCompatibilityCycles.length} explicit compatibility cycle remains until the next breaking release.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
console.log("Package graph verified: runtime workspace dependencies are acyclic.");
|
||||
}
|
||||
|
||||
if (process.argv[1] === fileURLToPath(import.meta.url)) main();
|
||||
@@ -0,0 +1,49 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { describe, it } from "node:test";
|
||||
import { listPackageCycleIssues, listRuntimePackageCycles } from "./check-package-cycles.mjs";
|
||||
|
||||
describe("package cycle checker", () => {
|
||||
it("accepts an acyclic runtime graph and ignores dev-only edges", () => {
|
||||
const packages = [
|
||||
{ name: "a", dependencies: { b: "workspace:*" } },
|
||||
{ name: "b", devDependencies: { a: "workspace:*" } },
|
||||
];
|
||||
assert.deepEqual(listRuntimePackageCycles(packages), []);
|
||||
});
|
||||
|
||||
it("reports a new strongly connected runtime component", () => {
|
||||
const packages = [
|
||||
{ name: "a", dependencies: { b: "workspace:*" } },
|
||||
{ name: "b", peerDependencies: { c: "workspace:*" } },
|
||||
{ name: "c", optionalDependencies: { a: "workspace:*" } },
|
||||
];
|
||||
assert.deepEqual(listPackageCycleIssues(packages, []), [
|
||||
"runtime workspace dependency cycle: a -> b -> c",
|
||||
]);
|
||||
});
|
||||
|
||||
it("permits only an exact, documented compatibility component", () => {
|
||||
const allowed = [{ packages: ["a", "b"], reason: "legacy compatibility surface" }];
|
||||
assert.deepEqual(
|
||||
listPackageCycleIssues(
|
||||
[
|
||||
{ name: "a", dependencies: { b: "workspace:*" } },
|
||||
{ name: "b", dependencies: { a: "workspace:*" } },
|
||||
],
|
||||
allowed,
|
||||
),
|
||||
[],
|
||||
);
|
||||
assert.equal(
|
||||
listPackageCycleIssues(
|
||||
[
|
||||
{ name: "a", dependencies: { b: "workspace:*" } },
|
||||
{ name: "b", dependencies: { a: "workspace:*", c: "workspace:*" } },
|
||||
{ name: "c", dependencies: { b: "workspace:*" } },
|
||||
],
|
||||
allowed,
|
||||
).length,
|
||||
1,
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user