Skip to content
Open
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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,12 @@
"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 && oxlint . && tsx scripts/lint-skills.ts",
"lint": "bun run check:tracked-artifacts && bun run check:workspace-contracts && bun run check:package-cycles && oxlint . && tsx scripts/lint-skills.ts",
"lint:skills": "tsx scripts/lint-skills.ts",
"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",
Expand All @@ -41,7 +42,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",
"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",
"test:skills": "node --test 'skills/**/*.test.mjs'",
"generate:previews": "tsx scripts/generate-template-previews.ts",
"generate:catalog-previews": "tsx scripts/generate-catalog-previews.ts",
Expand Down
10 changes: 10 additions & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -278,6 +284,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"
Expand Down
12 changes: 12 additions & 0 deletions packages/core/src/editing/draftMarkers.ts
Original file line number Diff line number Diff line change
@@ -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";
2 changes: 1 addition & 1 deletion packages/core/src/runtime/captionOverrides.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/runtime/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,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,
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/studio-api/helpers/draftMarkers.ts
Original file line number Diff line number Diff line change
@@ -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";
11 changes: 3 additions & 8 deletions packages/studio-server/src/helpers/draftMarkers.ts
Original file line number Diff line number Diff line change
@@ -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";
2 changes: 1 addition & 1 deletion packages/studio/src/components/editor/manualEditsTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 ─────────────────────────────────── */
Expand Down
2 changes: 0 additions & 2 deletions packages/studio/src/player/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
131 changes: 131 additions & 0 deletions scripts/check-package-cycles.mjs
Original file line number Diff line number Diff line change
@@ -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();
49 changes: 49 additions & 0 deletions scripts/check-package-cycles.test.mjs
Original file line number Diff line number Diff line change
@@ -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,
);
});
});
Loading