@hyperframes/studio-server 0.7.46 → 0.7.47

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -149,6 +149,12 @@ interface StudioApiAdapter {
149
149
  outputResolution?: CanvasResolution;
150
150
  /** Entry file relative to projectDir (e.g. "compositions/intro.html"). Defaults to index.html. */
151
151
  composition?: string;
152
+ /**
153
+ * Composition-variable overrides ({variableId: value}), forwarded to the
154
+ * producer's RenderConfig.variables and injected as window.__hfVariables —
155
+ * the same channel `hyperframes render --variables` uses.
156
+ */
157
+ variables?: Record<string, unknown>;
152
158
  /**
153
159
  * Telemetry id of the browser user who triggered the render. Lets the
154
160
  * adapter attribute the server-emitted render_complete/render_error to
package/dist/index.js CHANGED
@@ -2200,6 +2200,19 @@ ${bootstrap}`;
2200
2200
  }
2201
2201
  block = extractGsapScriptBlock(html);
2202
2202
  }
2203
+ if (!block && (body.type === "shift-positions" || body.type === "scale-positions")) {
2204
+ return c.json({
2205
+ ok: true,
2206
+ changed: false,
2207
+ mutated: false,
2208
+ parsed: { animations: [], timelineVar: "tl", preamble: "", postamble: "" },
2209
+ before: html,
2210
+ after: html,
2211
+ scriptText: "",
2212
+ path: res.filePath,
2213
+ backupPath: null
2214
+ });
2215
+ }
2203
2216
  if (!block) {
2204
2217
  return c.json({ error: "no GSAP script found in file" }, 400);
2205
2218
  }
@@ -2228,6 +2241,7 @@ ${bootstrap}`;
2228
2241
  const responsePayload = {
2229
2242
  ok: true,
2230
2243
  changed,
2244
+ mutated: changed,
2231
2245
  parsed: freshParsed,
2232
2246
  before: html,
2233
2247
  after: newHtml,
@@ -2245,6 +2259,7 @@ ${bootstrap}`;
2245
2259
  // src/routes/preview.ts
2246
2260
  import { existsSync as existsSync5, readFileSync as readFileSync7, statSync as statSync2 } from "fs";
2247
2261
  import { join as join8 } from "path";
2262
+ import { createHash as createHash2 } from "crypto";
2248
2263
  import { injectScriptsIntoHtml, stripEmbeddedRuntimeScripts as stripEmbeddedRuntimeScripts2 } from "@hyperframes/core/compiler";
2249
2264
 
2250
2265
  // src/helpers/subComposition.ts
@@ -2316,6 +2331,9 @@ function extractTemplateInnerHtml(rawComp) {
2316
2331
  const template = doc.querySelector("template");
2317
2332
  return template ? template.innerHTML : null;
2318
2333
  }
2334
+ function escapeAttrValue(value) {
2335
+ return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;");
2336
+ }
2319
2337
  function extractElementAttrs(el) {
2320
2338
  const parts = [];
2321
2339
  for (let i = 0; i < el.attributes.length; i++) {
@@ -2323,7 +2341,7 @@ function extractElementAttrs(el) {
2323
2341
  if (attr.value === "") {
2324
2342
  parts.push(attr.name);
2325
2343
  } else {
2326
- parts.push(`${attr.name}="${attr.value}"`);
2344
+ parts.push(`${attr.name}="${escapeAttrValue(attr.value)}"`);
2327
2345
  }
2328
2346
  }
2329
2347
  return parts.join(" ");
@@ -2812,6 +2830,12 @@ function stampFileHfIds(filePath) {
2812
2830
  }
2813
2831
  }
2814
2832
 
2833
+ // src/helpers/variablesPayload.ts
2834
+ var VARIABLES_PAYLOAD_ERROR = "variables must be a JSON object of {variableId: value}";
2835
+ function isVariablesPayload(value) {
2836
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2837
+ }
2838
+
2815
2839
  // src/routes/preview.ts
2816
2840
  var PROJECT_SIGNATURE_META = "hyperframes-project-signature";
2817
2841
  var GSAP_CDN_VERSION = "3.15.0";
@@ -2934,6 +2958,40 @@ function injectGsapCdnFallback(html) {
2934
2958
  if (html.includes("<head>")) return html.replace("<head>", "<head>" + GSAP_CDN_FALLBACK_SCRIPT);
2935
2959
  return GSAP_CDN_FALLBACK_SCRIPT + html;
2936
2960
  }
2961
+ function injectPreviewVariables(html, values) {
2962
+ const json = JSON.stringify(values).replace(/</g, "\\u003c");
2963
+ const tag = `<script data-hf-preview-variables>window.__hfVariables=${json};</script>`;
2964
+ for (const pattern of [/<head[^>]*>/i, /<html[^>]*>/i, /^\s*<!doctype[^>]*>/i]) {
2965
+ const match = pattern.exec(html);
2966
+ if (match) {
2967
+ const at = match.index + match[0].length;
2968
+ return html.slice(0, at) + tag + html.slice(at);
2969
+ }
2970
+ }
2971
+ return tag + html;
2972
+ }
2973
+ function parsePreviewVariablesParam(raw) {
2974
+ if (raw === void 0 || raw === "") return { ok: true, values: null };
2975
+ let parsed;
2976
+ try {
2977
+ parsed = JSON.parse(raw);
2978
+ } catch {
2979
+ return { ok: false, error: "variables must be valid JSON" };
2980
+ }
2981
+ if (!isVariablesPayload(parsed)) {
2982
+ return { ok: false, error: VARIABLES_PAYLOAD_ERROR };
2983
+ }
2984
+ return { ok: true, values: parsed };
2985
+ }
2986
+ function variablesEtagSalt(raw) {
2987
+ if (!raw) return "";
2988
+ return `:vars:${createHash2("sha1").update(raw).digest("hex").slice(0, 12)}`;
2989
+ }
2990
+ function previewVariablesFromRequest(rawVariables) {
2991
+ const parse = parsePreviewVariablesParam(rawVariables);
2992
+ if (!parse.ok) return { error: parse.error };
2993
+ return { raw: rawVariables, values: parse.values };
2994
+ }
2937
2995
  function injectStudioPreviewAugmentations(html, adapter, projectDir, activeCompositionPath) {
2938
2996
  return injectStudioMotionScript(
2939
2997
  injectMotionPathPluginIfNeeded(
@@ -2977,8 +3035,11 @@ function registerPreviewRoutes(api, adapter) {
2977
3035
  api.get("/projects/:id/preview", async (c) => {
2978
3036
  const project = await adapter.resolveProject(c.req.param("id"));
2979
3037
  if (!project) return c.json({ error: "not found" }, 404);
3038
+ const vars = previewVariablesFromRequest(c.req.query("variables"));
3039
+ if (vars.error !== void 0) return c.json({ error: vars.error }, 400);
3040
+ const previewVariables = vars.values;
2980
3041
  const signature = resolveProjectSignature(adapter, project.dir);
2981
- const etag = `"preview:${signature}"`;
3042
+ const etag = `"preview:${signature}${variablesEtagSalt(vars.raw)}"`;
2982
3043
  const ifNoneMatch = c.req.header("If-None-Match");
2983
3044
  if (ifNoneMatch === etag) {
2984
3045
  return new Response(null, { status: 304, headers: previewCacheHeaders(etag) });
@@ -3009,6 +3070,7 @@ ${runtimeTag}`;
3009
3070
  project.dir,
3010
3071
  mainCompositionPath
3011
3072
  );
3073
+ if (previewVariables) bundled = injectPreviewVariables(bundled, previewVariables);
3012
3074
  return c.html(bundled, 200, previewCacheHeaders(etag));
3013
3075
  } catch {
3014
3076
  const fallback = resolveProjectMainHtml(project.dir, project.id);
@@ -3017,16 +3079,16 @@ ${runtimeTag}`;
3017
3079
  join8(project.dir, fallback.compositionPath),
3018
3080
  fallback.html
3019
3081
  );
3020
- return c.html(
3021
- injectStudioPreviewAugmentations(
3022
- await transformPreviewHtml(fallbackHtml, adapter, project, fallback.compositionPath),
3023
- adapter,
3024
- project.dir,
3025
- fallback.compositionPath
3026
- ),
3027
- 200,
3028
- previewCacheHeaders(etag)
3082
+ let fallbackAugmented = injectStudioPreviewAugmentations(
3083
+ await transformPreviewHtml(fallbackHtml, adapter, project, fallback.compositionPath),
3084
+ adapter,
3085
+ project.dir,
3086
+ fallback.compositionPath
3029
3087
  );
3088
+ if (previewVariables) {
3089
+ fallbackAugmented = injectPreviewVariables(fallbackAugmented, previewVariables);
3090
+ }
3091
+ return c.html(fallbackAugmented, 200, previewCacheHeaders(etag));
3030
3092
  }
3031
3093
  return c.text("not found", 404);
3032
3094
  }
@@ -3038,6 +3100,9 @@ ${runtimeTag}`;
3038
3100
  api.get("/projects/:id/preview/comp/*", async (c) => {
3039
3101
  const project = await adapter.resolveProject(c.req.param("id"));
3040
3102
  if (!project) return c.json({ error: "not found" }, 404);
3103
+ const vars = previewVariablesFromRequest(c.req.query("variables"));
3104
+ if (vars.error !== void 0) return c.json({ error: vars.error }, 400);
3105
+ const previewVariables = vars.values;
3041
3106
  const signature = resolveProjectSignature(adapter, project.dir);
3042
3107
  const compPath = decodeURIComponent(
3043
3108
  c.req.path.replace(`/projects/${project.id}/preview/comp/`, "").split("?")[0] ?? ""
@@ -3046,7 +3111,7 @@ ${runtimeTag}`;
3046
3111
  if (!compFile || !existsSync5(compFile) || !statSync2(compFile).isFile()) {
3047
3112
  return c.text("not found", 404);
3048
3113
  }
3049
- const etag = `"comp:v2:${compPath}:${signature}"`;
3114
+ const etag = `"comp:v2:${compPath}:${signature}${variablesEtagSalt(vars.raw)}"`;
3050
3115
  const ifNoneMatch = c.req.header("If-None-Match");
3051
3116
  if (ifNoneMatch === etag) {
3052
3117
  return new Response(null, { status: 304, headers: previewCacheHeaders(etag) });
@@ -3063,11 +3128,9 @@ ${runtimeTag}`;
3063
3128
  );
3064
3129
  if (!html) return c.text("not found", 404);
3065
3130
  html = ensureHfIds2(await transformPreviewHtml(html, adapter, project, compPath));
3066
- return c.html(
3067
- injectStudioPreviewAugmentations(html, adapter, project.dir, compPath),
3068
- 200,
3069
- previewCacheHeaders(etag)
3070
- );
3131
+ html = injectStudioPreviewAugmentations(html, adapter, project.dir, compPath);
3132
+ if (previewVariables) html = injectPreviewVariables(html, previewVariables);
3133
+ return c.html(html, 200, previewCacheHeaders(etag));
3071
3134
  });
3072
3135
  api.get("/projects/:id/preview/*", async (c) => {
3073
3136
  const project = await adapter.resolveProject(c.req.param("id"));
@@ -3206,6 +3269,13 @@ function registerRenderRoutes(api, adapter) {
3206
3269
  }
3207
3270
  composition = body.composition;
3208
3271
  }
3272
+ let variables;
3273
+ if (body.variables !== void 0) {
3274
+ if (!isVariablesPayload(body.variables)) {
3275
+ return c.json({ error: VARIABLES_PAYLOAD_ERROR }, 400);
3276
+ }
3277
+ variables = body.variables;
3278
+ }
3209
3279
  const now = /* @__PURE__ */ new Date();
3210
3280
  const datePart = now.toISOString().slice(0, 10);
3211
3281
  const timePart = now.toTimeString().slice(0, 8).replace(/:/g, "-");
@@ -3223,6 +3293,7 @@ function registerRenderRoutes(api, adapter) {
3223
3293
  jobId,
3224
3294
  outputResolution,
3225
3295
  composition,
3296
+ variables,
3226
3297
  distinctId: typeof body.telemetryDistinctId === "string" ? body.telemetryDistinctId : void 0
3227
3298
  });
3228
3299
  jobState.createdAt = Date.now();
@@ -3388,7 +3459,7 @@ function registerRenderRoutes(api, adapter) {
3388
3459
  // src/routes/thumbnail.ts
3389
3460
  import { existsSync as existsSync7, readFileSync as readFileSync10, writeFileSync as writeFileSync6, mkdirSync as mkdirSync5, statSync as statSync4 } from "fs";
3390
3461
  import { join as join11 } from "path";
3391
- import { createHash as createHash2 } from "crypto";
3462
+ import { createHash as createHash3 } from "crypto";
3392
3463
 
3393
3464
  // src/helpers/manualEditsRenderScript.ts
3394
3465
  var STUDIO_MANUAL_EDITS_PATH = ".hyperframes/studio-manual-edits.json";
@@ -4000,7 +4071,7 @@ function registerThumbnailRoutes(api, adapter) {
4000
4071
  const htmlFile = join11(project.dir, compPath);
4001
4072
  if (existsSync7(htmlFile)) {
4002
4073
  const html = readFileSync10(htmlFile, "utf-8");
4003
- sourceKey = `_${createHash2("sha1").update(html).digest("hex").slice(0, 16)}`;
4074
+ sourceKey = `_${createHash3("sha1").update(html).digest("hex").slice(0, 16)}`;
4004
4075
  sourceMtime = Math.round(statSync4(htmlFile).mtimeMs);
4005
4076
  if (!vpWidth) {
4006
4077
  const wMatch = html.match(/data-width=["'](\d+)["']/);
@@ -4013,14 +4084,14 @@ function registerThumbnailRoutes(api, adapter) {
4013
4084
  let manualEditsKey = "";
4014
4085
  if (existsSync7(manualEditsFile)) {
4015
4086
  const manualEditsContent = readFileSync10(manualEditsFile, "utf-8");
4016
- manualEditsKey = `_${createHash2("sha1").update(manualEditsContent).digest("hex").slice(0, 16)}`;
4087
+ manualEditsKey = `_${createHash3("sha1").update(manualEditsContent).digest("hex").slice(0, 16)}`;
4017
4088
  sourceMtime = Math.max(sourceMtime, Math.round(statSync4(manualEditsFile).mtimeMs));
4018
4089
  }
4019
4090
  const motionFile = join11(project.dir, STUDIO_MOTION_PATH);
4020
4091
  let motionKey = "";
4021
4092
  if (existsSync7(motionFile)) {
4022
4093
  const motionContent = readFileSync10(motionFile, "utf-8");
4023
- motionKey = `_${createHash2("sha1").update(motionContent).digest("hex").slice(0, 16)}`;
4094
+ motionKey = `_${createHash3("sha1").update(motionContent).digest("hex").slice(0, 16)}`;
4024
4095
  sourceMtime = Math.max(sourceMtime, Math.round(statSync4(motionFile).mtimeMs));
4025
4096
  }
4026
4097
  const previewUrl = compPath === "index.html" ? `http://${c.req.header("host")}/api/projects/${project.id}/preview` : `http://${c.req.header("host")}/api/projects/${project.id}/preview/comp/${compPath}`;