Skip to content

Commit 4465a5a

Browse files
committed
refactor(core): own edit protocol contract
1 parent ec35495 commit 4465a5a

11 files changed

Lines changed: 213 additions & 17 deletions

File tree

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,12 @@
2424
"changelog:weekly": "tsx scripts/changelog-weekly.ts",
2525
"sync-schemas": "tsx scripts/sync-schemas.ts",
2626
"sync-schemas:check": "tsx scripts/sync-schemas.ts --check",
27-
"lint": "bun run check:tracked-artifacts && bun run check:workspace-contracts && oxlint . && tsx scripts/lint-skills.ts",
27+
"lint": "bun run check:tracked-artifacts && bun run check:workspace-contracts && bun run check:package-cycles && oxlint . && tsx scripts/lint-skills.ts",
2828
"lint:skills": "tsx scripts/lint-skills.ts",
2929
"lint:fix": "oxlint --fix .",
3030
"check:tracked-artifacts": "node scripts/check-tracked-artifacts.mjs",
3131
"check:workspace-contracts": "node scripts/check-workspace-contracts.mjs",
32+
"check:package-cycles": "node scripts/check-package-cycles.mjs",
3233
"format": "oxfmt .",
3334
"test": "bun run test:unit",
3435
"test:unit": "bun run --filter '*' test",
@@ -41,7 +42,7 @@
4142
"player:perf": "bun run --filter @hyperframes/player perf",
4243
"format:check": "oxfmt --check .",
4344
"knip": "knip",
44-
"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",
45+
"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",
4546
"test:skills": "node --test 'skills/**/*.test.mjs'",
4647
"generate:previews": "tsx scripts/generate-template-previews.ts",
4748
"generate:catalog-previews": "tsx scripts/generate-catalog-previews.ts",

packages/core/package.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,12 @@
5252
"import": "./src/editing/affordances.ts",
5353
"types": "./src/editing/affordances.ts"
5454
},
55+
"./editing/draft-markers": {
56+
"bun": "./src/editing/draftMarkers.ts",
57+
"node": "./dist/editing/draftMarkers.js",
58+
"import": "./src/editing/draftMarkers.ts",
59+
"types": "./src/editing/draftMarkers.ts"
60+
},
5561
"./variables": {
5662
"bun": "./src/variables.ts",
5763
"node": "./dist/variables.js",
@@ -278,6 +284,10 @@
278284
"import": "./dist/editing/affordances.js",
279285
"types": "./dist/editing/affordances.d.ts"
280286
},
287+
"./editing/draft-markers": {
288+
"import": "./dist/editing/draftMarkers.js",
289+
"types": "./dist/editing/draftMarkers.d.ts"
290+
},
281291
"./figma": {
282292
"import": "./dist/figma/index.js",
283293
"types": "./dist/figma/index.d.ts"
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/**
2+
* Browser-safe wire constants shared by the runtime, Studio, and studio-server.
3+
*
4+
* Keep this module dependency-free. These names are part of the edit protocol,
5+
* not server implementation details, so browser code must be able to import
6+
* them without pulling the studio-server package into its graph.
7+
*/
8+
export const STUDIO_OFFSET_X_PROP = "--hf-studio-offset-x";
9+
export const STUDIO_OFFSET_Y_PROP = "--hf-studio-offset-y";
10+
export const STUDIO_WIDTH_PROP = "--hf-studio-width";
11+
export const STUDIO_HEIGHT_PROP = "--hf-studio-height";
12+
export const STUDIO_MANUAL_EDIT_GESTURE_ATTR = "data-hf-studio-manual-edit-gesture";

packages/core/src/runtime/captionOverrides.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
* 2. `wordIndex` — fallback, DOM traversal order across .caption-group > span
1111
*/
1212

13-
export interface CaptionOverride {
13+
interface CaptionOverride {
1414
wordId?: string;
1515
wordIndex?: number;
1616
x?: number;

packages/core/src/runtime/init.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ import { createColorGradingRuntime, type RuntimeColorGradingApi } from "./colorG
3535
import { TransportClock } from "./clock";
3636
import { WebAudioTransport } from "./webAudioTransport";
3737
import { quantizeTimeToFrame } from "../inline-scripts/parityContract";
38-
import { STUDIO_MANUAL_EDIT_GESTURE_ATTR } from "../studio-api/helpers/draftMarkers";
38+
import { STUDIO_MANUAL_EDIT_GESTURE_ATTR } from "../editing/draftMarkers";
3939
import type {
4040
RuntimeDeterministicAdapter,
4141
RuntimeJson,
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
/** @deprecated Import from @hyperframes/studio-server/draft-markers */
2-
export * from "@hyperframes/studio-server/draft-markers";
1+
/** @deprecated Import from @hyperframes/core/editing/draft-markers. */
2+
export * from "../../editing/draftMarkers.js";
Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,5 @@
11
/**
2-
* Draft-marker constants shared between core's PreviewAdapter and Studio's
3-
* manual-edits code. CSS custom properties written during a drag gesture, plus
4-
* the gesture marker attribute. Exported from @hyperframes/core/studio-api/draft-markers.
2+
* Compatibility export for server consumers. The edit-protocol constants are
3+
* owned by core's dependency-free browser contract.
54
*/
6-
export const STUDIO_OFFSET_X_PROP = "--hf-studio-offset-x";
7-
export const STUDIO_OFFSET_Y_PROP = "--hf-studio-offset-y";
8-
export const STUDIO_WIDTH_PROP = "--hf-studio-width";
9-
export const STUDIO_HEIGHT_PROP = "--hf-studio-height";
10-
export const STUDIO_MANUAL_EDIT_GESTURE_ATTR = "data-hf-studio-manual-edit-gesture";
5+
export * from "@hyperframes/core/editing/draft-markers";

packages/studio/src/components/editor/manualEditsTypes.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ export {
55
STUDIO_WIDTH_PROP,
66
STUDIO_HEIGHT_PROP,
77
STUDIO_MANUAL_EDIT_GESTURE_ATTR,
8-
} from "@hyperframes/studio-server/draft-markers";
8+
} from "@hyperframes/core/editing/draft-markers";
99
export const STUDIO_ROTATION_PROP = "--hf-studio-rotation";
1010

1111
/* ── Internal DOM attribute names ─────────────────────────────────── */

packages/studio/src/player/index.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@ export { resolveIframe } from "./lib/timelineDOM";
1111

1212
// Store
1313
export { usePlayerStore, liveTime } from "./store/playerStore";
14-
// Public library surface; external consumers are invisible to the workspace analyzer.
15-
// fallow-ignore-next-line unused-exports
1614
export type { SelectElementOptions, TimelineElement, ZoomMode } from "./store/playerStore";
1715

1816
// Utils

scripts/check-package-cycles.mjs

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
#!/usr/bin/env node
2+
3+
import { existsSync, readFileSync, readdirSync } from "node:fs";
4+
import { join } from "node:path";
5+
import { fileURLToPath } from "node:url";
6+
7+
const ROOT = join(import.meta.dirname, "..");
8+
const RUNTIME_DEPENDENCY_FIELDS = ["dependencies", "optionalDependencies", "peerDependencies"];
9+
10+
/**
11+
* The deprecated @hyperframes/core/studio-api forwarding surface is public and
12+
* cannot disappear before a breaking release. Keep its one known package SCC
13+
* explicit so every new cycle still fails CI. Delete this exception together
14+
* with the forwarding surface in the next breaking release.
15+
*/
16+
export const ALLOWED_COMPATIBILITY_CYCLES = [
17+
{
18+
packages: ["@hyperframes/core", "@hyperframes/studio-server"],
19+
reason: "Deprecated core/studio-api forwarding exports; remove at the next breaking release.",
20+
},
21+
];
22+
23+
function canonicalComponent(names) {
24+
return [...names].sort().join(" -> ");
25+
}
26+
27+
export function listRuntimePackageCycles(packages) {
28+
const byName = new Map(packages.map((pkg) => [pkg.name, pkg]));
29+
const edges = new Map(
30+
// Collecting three dependency classes in one expression keeps the graph
31+
// construction declarative; focused tests cover runtime versus dev edges.
32+
// fallow-ignore-next-line complexity
33+
packages.map((pkg) => {
34+
const targets = new Set();
35+
for (const field of RUNTIME_DEPENDENCY_FIELDS) {
36+
for (const dependency of Object.keys(pkg[field] ?? {})) {
37+
if (byName.has(dependency)) targets.add(dependency);
38+
}
39+
}
40+
return [pkg.name, [...targets].sort()];
41+
}),
42+
);
43+
44+
let nextIndex = 0;
45+
const indexes = new Map();
46+
const lowLinks = new Map();
47+
const stack = [];
48+
const onStack = new Set();
49+
const components = [];
50+
51+
// Tarjan's strongly-connected-component walk is intentionally branchy; the
52+
// state transitions mirror the algorithm and are covered by focused tests.
53+
// fallow-ignore-next-line complexity
54+
function visit(name) {
55+
indexes.set(name, nextIndex);
56+
lowLinks.set(name, nextIndex);
57+
nextIndex += 1;
58+
stack.push(name);
59+
onStack.add(name);
60+
61+
for (const target of edges.get(name) ?? []) {
62+
if (!indexes.has(target)) {
63+
visit(target);
64+
lowLinks.set(name, Math.min(lowLinks.get(name), lowLinks.get(target)));
65+
} else if (onStack.has(target)) {
66+
lowLinks.set(name, Math.min(lowLinks.get(name), indexes.get(target)));
67+
}
68+
}
69+
70+
if (lowLinks.get(name) !== indexes.get(name)) return;
71+
const component = [];
72+
let member;
73+
do {
74+
member = stack.pop();
75+
onStack.delete(member);
76+
component.push(member);
77+
} while (member !== name);
78+
79+
const selfCycle = component.length === 1 && (edges.get(name) ?? []).includes(name);
80+
if (component.length > 1 || selfCycle) components.push(component.sort());
81+
}
82+
83+
for (const name of [...byName.keys()].sort()) {
84+
if (!indexes.has(name)) visit(name);
85+
}
86+
return components.sort((a, b) => canonicalComponent(a).localeCompare(canonicalComponent(b)));
87+
}
88+
89+
export function listPackageCycleIssues(packages, allowed = ALLOWED_COMPATIBILITY_CYCLES) {
90+
const allowedKeys = new Set(allowed.map((entry) => canonicalComponent(entry.packages)));
91+
return listRuntimePackageCycles(packages)
92+
.map(canonicalComponent)
93+
.filter((component) => !allowedKeys.has(component))
94+
.map((component) => `runtime workspace dependency cycle: ${component}`);
95+
}
96+
97+
export function readWorkspacePackages(root = ROOT) {
98+
return readdirSync(join(root, "packages"))
99+
.sort()
100+
.map((directory) => join(root, "packages", directory, "package.json"))
101+
.filter(existsSync)
102+
.map((path) => JSON.parse(readFileSync(path, "utf8")));
103+
}
104+
105+
function main() {
106+
const packages = readWorkspacePackages();
107+
const cycles = listRuntimePackageCycles(packages);
108+
const issues = listPackageCycleIssues(packages);
109+
if (issues.length > 0) {
110+
console.error("Package cycle violations:");
111+
issues.forEach((issue) => console.error(`- ${issue}`));
112+
process.exitCode = 1;
113+
return;
114+
}
115+
116+
const allowedKeys = new Set(
117+
ALLOWED_COMPATIBILITY_CYCLES.map((entry) => canonicalComponent(entry.packages)),
118+
);
119+
const activeCompatibilityCycles = cycles.filter((cycle) =>
120+
allowedKeys.has(canonicalComponent(cycle)),
121+
);
122+
if (activeCompatibilityCycles.length > 0) {
123+
console.log(
124+
`Package graph verified; ${activeCompatibilityCycles.length} explicit compatibility cycle remains until the next breaking release.`,
125+
);
126+
return;
127+
}
128+
console.log("Package graph verified: runtime workspace dependencies are acyclic.");
129+
}
130+
131+
if (process.argv[1] === fileURLToPath(import.meta.url)) main();

0 commit comments

Comments
 (0)