@hyperframes/studio-server 0.8.63 → 0.8.65

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
@@ -41,8 +41,15 @@ declare function createStudioApi(adapter: StudioApiAdapter): Hono;
41
41
  declare function affectsProjectSignature(projectDir: string, changedPath: string): boolean;
42
42
  /**
43
43
  * Creates a stable preview cache-busting signature for project source plus Studio manifests.
44
+ * `excluding` (project-relative paths) leaves those files out.
44
45
  */
45
- declare function createProjectSignature(projectDir: string): string;
46
+ declare function createProjectSignature(projectDir: string, excluding?: ReadonlySet<string>): string;
47
+
48
+ /**
49
+ * Compositions whose rendered frames a write at `changedPath` can change, or `null` for all
50
+ * of them: the root, assets and any file the root does not mount reach every composition.
51
+ */
52
+ declare function compositionsAffectedBy(projectDir: string, changedPath: string): string[] | null;
46
53
 
47
54
  /** Recursively walk a directory and return relative file paths. */
48
55
  declare function walkDir(dir: string, prefix?: string): string[];
@@ -160,4 +167,4 @@ type BackgroundRemovalRender = (options: {
160
167
  }>;
161
168
  declare function createBackgroundRemovalJob(opts: BackgroundRemovalJobOptions, render: BackgroundRemovalRender): MediaProcessingJobState;
162
169
 
163
- export { type AppliedFileMutation, type BackgroundRemovalRender, type DuplicateElementResult, type FileMutationInput, type FileWriteReceipt, MIME_TYPES, MediaProcessingJobState, SourceMutationTarget, StudioApiAdapter, type ThumbnailOutputDimensions, affectsProjectSignature, applyFileMutations, buildSubCompositionHtml, consumeFileWriteReceipt, createBackgroundRemovalJob, createProjectSignature, createStudioApi, duplicateElementInHtml, fileContentVersion, getMimeType, identifyFileWrite, thumbnailDeviceScaleFactor, walkDir };
170
+ export { type AppliedFileMutation, type BackgroundRemovalRender, type DuplicateElementResult, type FileMutationInput, type FileWriteReceipt, MIME_TYPES, MediaProcessingJobState, SourceMutationTarget, StudioApiAdapter, type ThumbnailOutputDimensions, affectsProjectSignature, applyFileMutations, buildSubCompositionHtml, compositionsAffectedBy, consumeFileWriteReceipt, createBackgroundRemovalJob, createProjectSignature, createStudioApi, duplicateElementInHtml, fileContentVersion, getMimeType, identifyFileWrite, thumbnailDeviceScaleFactor, walkDir };
package/dist/index.js CHANGED
@@ -217,14 +217,18 @@ async function resolveProjectAndSignature(adapter, projectId) {
217
217
  if (!project) return null;
218
218
  return { project, signature: resolveProjectSignature(adapter, project.dir) };
219
219
  }
220
- function createProjectSignature(projectDir) {
220
+ function createProjectSignature(projectDir, excluding = /* @__PURE__ */ new Set()) {
221
221
  const normalizedProjectDir = resolve(projectDir);
222
- const files = [];
223
- collectProjectSignatureFiles(normalizedProjectDir, normalizedProjectDir, files);
224
- collectProjectSignatureManifestFiles(normalizedProjectDir, files);
222
+ const collected = [];
223
+ collectProjectSignatureFiles(normalizedProjectDir, normalizedProjectDir, collected);
224
+ collectProjectSignatureManifestFiles(normalizedProjectDir, collected);
225
+ const files = collected.filter(
226
+ (entry) => !excluding.has(relative(normalizedProjectDir, entry.file).split(sep).join("/"))
227
+ );
225
228
  files.sort((a, b) => a.file.localeCompare(b.file));
226
229
  const fingerprint = createProjectFingerprint(normalizedProjectDir, files);
227
- const cached = projectSignatureCache.get(normalizedProjectDir);
230
+ const cacheKey = excluding.size ? `${normalizedProjectDir}\0${createHash("sha256").update([...excluding].sort().join("\0")).digest("hex")}` : normalizedProjectDir;
231
+ const cached = projectSignatureCache.get(cacheKey);
228
232
  if (cached?.fingerprint === fingerprint) return cached.signature;
229
233
  const hash = createHash("sha256");
230
234
  for (const entry of files) {
@@ -245,7 +249,7 @@ function createProjectSignature(projectDir) {
245
249
  hash.update("\0");
246
250
  }
247
251
  const signature = hash.digest("hex").slice(0, 24);
248
- projectSignatureCache.set(normalizedProjectDir, { fingerprint, signature });
252
+ projectSignatureCache.set(cacheKey, { fingerprint, signature });
249
253
  return signature;
250
254
  }
251
255
 
@@ -2404,14 +2408,19 @@ function registerFileRoutes(api, adapter) {
2404
2408
  if ("error" in res) return res.error;
2405
2409
  if (!existsSync3(res.absPath)) {
2406
2410
  if (c.req.query("optional") === "1") {
2407
- return c.json({ filename: res.filePath, content: "" });
2411
+ return c.json({ filename: res.filePath, content: "", missing: true });
2408
2412
  }
2409
2413
  return c.json({ error: "not found" }, 404);
2410
2414
  }
2411
2415
  const content = readFileSync5(res.absPath);
2412
2416
  const version = fileContentVersion(content);
2413
2417
  c.header("ETag", version);
2414
- return c.json({ filename: res.filePath, content: content.toString("utf-8"), version });
2418
+ return c.json({
2419
+ filename: res.filePath,
2420
+ content: content.toString("utf-8"),
2421
+ version,
2422
+ missing: false
2423
+ });
2415
2424
  });
2416
2425
  api.put("/projects/:id/files/*", async (c) => {
2417
2426
  const res = await resolveProjectFile(c, adapter);
@@ -4149,7 +4158,7 @@ function registerRenderRoutes(api, adapter) {
4149
4158
  import {
4150
4159
  existsSync as existsSync7,
4151
4160
  mkdirSync as mkdirSync5,
4152
- readFileSync as readFileSync11,
4161
+ readFileSync as readFileSync12,
4153
4162
  readdirSync as readdirSync6,
4154
4163
  renameSync as renameSync3,
4155
4164
  rmSync as rmSync4,
@@ -4160,6 +4169,63 @@ import {
4160
4169
  import { join as join11 } from "path";
4161
4170
  import { createHash as createHash4, randomUUID as randomUUID4 } from "crypto";
4162
4171
 
4172
+ // src/helpers/compositionInputs.ts
4173
+ import { readFileSync as readFileSync11 } from "fs";
4174
+ var ROOT_COMPOSITION = "index.html";
4175
+ var COMPOSITION_SRC = /\bdata-composition-src\s*=\s*(["'])(.*?)\1/g;
4176
+ function projectReader(projectDir) {
4177
+ const sources = /* @__PURE__ */ new Map();
4178
+ return (compPath) => {
4179
+ if (!sources.has(compPath)) {
4180
+ const file = resolveWithinProject(projectDir, compPath);
4181
+ let source = null;
4182
+ try {
4183
+ source = file ? readFileSync11(file, "utf-8") : null;
4184
+ } catch {
4185
+ }
4186
+ sources.set(compPath, source);
4187
+ }
4188
+ return sources.get(compPath) ?? null;
4189
+ };
4190
+ }
4191
+ function normalizeSource(src) {
4192
+ if (/^[a-z][a-z0-9+.-]*:/i.test(src) || src.startsWith("//")) return null;
4193
+ const path = src.split(/[?#]/)[0]?.replace(/^\.?\//, "") ?? "";
4194
+ return path || null;
4195
+ }
4196
+ function closureOf(read, compPath) {
4197
+ const closure = /* @__PURE__ */ new Set();
4198
+ const visit = (path) => {
4199
+ if (closure.has(path)) return;
4200
+ closure.add(path);
4201
+ for (const match of (read(path) ?? "").matchAll(COMPOSITION_SRC)) {
4202
+ const mounted = normalizeSource(match[2] ?? "");
4203
+ if (mounted) visit(mounted);
4204
+ }
4205
+ };
4206
+ visit(compPath);
4207
+ return closure;
4208
+ }
4209
+ var inputSignatures = /* @__PURE__ */ new Map();
4210
+ function compositionInputSignature(projectDir, compPath, projectSignature) {
4211
+ const key = `${projectDir}\0${compPath}`;
4212
+ const known = inputSignatures.get(key);
4213
+ if (known?.projectSignature === projectSignature) return known.inputSignature;
4214
+ const read = projectReader(projectDir);
4215
+ const inputs = closureOf(read, normalizeSource(compPath) ?? compPath).add(ROOT_COMPOSITION);
4216
+ const siblings = [...closureOf(read, ROOT_COMPOSITION)].filter((path) => !inputs.has(path));
4217
+ const inputSignature = createProjectSignature(projectDir, new Set(siblings));
4218
+ inputSignatures.set(key, { projectSignature, inputSignature });
4219
+ return inputSignature;
4220
+ }
4221
+ function compositionsAffectedBy(projectDir, changedPath) {
4222
+ const changed = changedPath.replace(/\\/g, "/");
4223
+ const read = projectReader(projectDir);
4224
+ const mounted = closureOf(read, ROOT_COMPOSITION);
4225
+ if (changed === ROOT_COMPOSITION || !mounted.has(changed)) return null;
4226
+ return [...mounted].filter((path) => closureOf(read, path).has(changed));
4227
+ }
4228
+
4163
4229
  // src/routes/thumbnailGenerationCoordinator.ts
4164
4230
  var ThumbnailGenerationCoordinator = class {
4165
4231
  constructor(concurrency = 1) {
@@ -4322,6 +4388,7 @@ function registerThumbnailRoutes(api, adapter) {
4322
4388
  c.req.path.replace(`/projects/${project.id}/thumbnail/`, "").split("?")[0] ?? ""
4323
4389
  );
4324
4390
  if (compPath && !compPath.includes(".")) compPath += ".html";
4391
+ const inputSignature = compositionInputSignature(project.dir, compPath, projectSignature);
4325
4392
  const url = new URL(c.req.url, `http://${c.req.header("host") || "localhost"}`);
4326
4393
  const rawSeekTime = url.searchParams.get("t");
4327
4394
  const parsedSeekTime = rawSeekTime == null ? Number.NaN : parseFloat(rawSeekTime);
@@ -4342,7 +4409,7 @@ function registerThumbnailRoutes(api, adapter) {
4342
4409
  let sourceKey = "";
4343
4410
  const htmlFile = join11(project.dir, compPath);
4344
4411
  if (existsSync7(htmlFile)) {
4345
- const html = readFileSync11(htmlFile, "utf-8");
4412
+ const html = readFileSync12(htmlFile, "utf-8");
4346
4413
  sourceKey = `_${createHash4("sha1").update(html).digest("hex").slice(0, 16)}`;
4347
4414
  sourceMtime = Math.round(statSync7(htmlFile).mtimeMs);
4348
4415
  if (!vpWidth) {
@@ -4355,14 +4422,14 @@ function registerThumbnailRoutes(api, adapter) {
4355
4422
  const manualEditsFile = join11(project.dir, STUDIO_MANUAL_EDITS_PATH);
4356
4423
  let manualEditsKey = "";
4357
4424
  if (existsSync7(manualEditsFile)) {
4358
- const manualEditsContent = readFileSync11(manualEditsFile, "utf-8");
4425
+ const manualEditsContent = readFileSync12(manualEditsFile, "utf-8");
4359
4426
  manualEditsKey = `_${createHash4("sha1").update(manualEditsContent).digest("hex").slice(0, 16)}`;
4360
4427
  sourceMtime = Math.max(sourceMtime, Math.round(statSync7(manualEditsFile).mtimeMs));
4361
4428
  }
4362
4429
  const motionFile = join11(project.dir, STUDIO_MOTION_PATH);
4363
4430
  let motionKey = "";
4364
4431
  if (existsSync7(motionFile)) {
4365
- const motionContent = readFileSync11(motionFile, "utf-8");
4432
+ const motionContent = readFileSync12(motionFile, "utf-8");
4366
4433
  motionKey = `_${createHash4("sha1").update(motionContent).digest("hex").slice(0, 16)}`;
4367
4434
  sourceMtime = Math.max(sourceMtime, Math.round(statSync7(motionFile).mtimeMs));
4368
4435
  }
@@ -4370,11 +4437,11 @@ function registerThumbnailRoutes(api, adapter) {
4370
4437
  const cacheDir = join11(project.dir, ".thumbnails");
4371
4438
  const selectorKey = selector ? `_${selector.replace(/[^a-zA-Z0-9_-]+/g, "_").slice(0, 80)}_${selectorIndex ?? 0}` : "";
4372
4439
  const urlVersionKey = urlVersion ? `_${urlVersion.replace(/[^a-zA-Z0-9_-]+/g, "_").slice(0, 32)}` : "";
4373
- const projectSignatureKey = `_${createHash4("sha1").update(projectSignature).digest("hex").slice(0, 16)}`;
4440
+ const inputSignatureKey = `_${createHash4("sha1").update(inputSignature).digest("hex").slice(0, 16)}`;
4374
4441
  const outputScale = outputMode === "source" ? 1 : Math.min(1, THUMBNAIL_MAX_OUTPUT_WIDTH / compW, THUMBNAIL_MAX_OUTPUT_HEIGHT / compH);
4375
4442
  const outputWidth = Math.max(1, Math.round(compW * outputScale));
4376
4443
  const outputHeight = Math.max(1, Math.round(compH * outputScale));
4377
- const cacheKey = `${THUMBNAIL_CACHE_VERSION}${urlVersionKey}${projectSignatureKey}${manualEditsKey}${motionKey}${sourceKey}_${format}_${outputMode}_${compPath.replace(/\//g, "_")}_${compW}x${compH}_${outputWidth}x${outputHeight}_${sourceMtime}_${seekTime.toFixed(2)}${selectorKey}.${format === "png" ? "png" : "jpg"}`;
4444
+ const cacheKey = `${THUMBNAIL_CACHE_VERSION}${urlVersionKey}${inputSignatureKey}${manualEditsKey}${motionKey}${sourceKey}_${format}_${outputMode}_${compPath.replace(/\//g, "_")}_${compW}x${compH}_${outputWidth}x${outputHeight}_${sourceMtime}_${seekTime.toFixed(2)}${selectorKey}.${format === "png" ? "png" : "jpg"}`;
4378
4445
  const cachePath = join11(cacheDir, cacheKey);
4379
4446
  if (!prunedCacheDirs.has(cacheDir)) {
4380
4447
  prunedCacheDirs.add(cacheDir);
@@ -4384,7 +4451,7 @@ function registerThumbnailRoutes(api, adapter) {
4384
4451
  );
4385
4452
  }
4386
4453
  if (existsSync7(cachePath)) {
4387
- return new Response(new Uint8Array(readFileSync11(cachePath)), {
4454
+ return new Response(new Uint8Array(readFileSync12(cachePath)), {
4388
4455
  headers: { "Content-Type": contentType, "Cache-Control": "no-cache" }
4389
4456
  });
4390
4457
  }
@@ -4409,8 +4476,8 @@ function registerThumbnailRoutes(api, adapter) {
4409
4476
  });
4410
4477
  if (!generated) return null;
4411
4478
  const afterGeneration = await resolveProjectAndSignature(adapter, project.id);
4412
- const freshSignature = createProjectSignature(project.dir);
4413
- if (!afterGeneration || afterGeneration.project.dir !== project.dir || afterGeneration.signature !== projectSignature || freshSignature !== projectSignature) {
4479
+ const inputsUnchanged = (signature) => compositionInputSignature(project.dir, compPath, signature) === inputSignature;
4480
+ if (afterGeneration?.project.dir !== project.dir || !inputsUnchanged(afterGeneration.signature) || !inputsUnchanged(createProjectSignature(project.dir))) {
4414
4481
  return generated;
4415
4482
  }
4416
4483
  if (!existsSync7(cacheDir)) mkdirSync5(cacheDir, { recursive: true });
@@ -4439,7 +4506,7 @@ function registerThumbnailRoutes(api, adapter) {
4439
4506
  }
4440
4507
 
4441
4508
  // src/routes/waveform.ts
4442
- import { existsSync as existsSync8, readFileSync as readFileSync12, statSync as statSync8 } from "fs";
4509
+ import { existsSync as existsSync8, readFileSync as readFileSync13, statSync as statSync8 } from "fs";
4443
4510
  import { join as join12 } from "path";
4444
4511
  function registerWaveformRoutes(api, adapter) {
4445
4512
  api.get("/projects/:id/waveform/*", async (c) => {
@@ -4455,7 +4522,7 @@ function registerWaveformRoutes(api, adapter) {
4455
4522
  const cachePath = join12(cacheDir, buildWaveformCacheKey(assetPath, stats));
4456
4523
  try {
4457
4524
  if (isWaveformCacheDirectory(cacheDir) && existsSync8(cachePath)) {
4458
- const peaks2 = JSON.parse(readFileSync12(cachePath, "utf-8"));
4525
+ const peaks2 = JSON.parse(readFileSync13(cachePath, "utf-8"));
4459
4526
  return c.json({ peaks: peaks2 });
4460
4527
  }
4461
4528
  } catch {
@@ -4941,14 +5008,14 @@ function registerMediaRoutes(api, adapter, options = {}) {
4941
5008
  }
4942
5009
 
4943
5010
  // src/routes/globalAssets.ts
4944
- import { existsSync as existsSync10, readFileSync as readFileSync13 } from "fs";
5011
+ import { existsSync as existsSync10, readFileSync as readFileSync14 } from "fs";
4945
5012
  import { homedir } from "os";
4946
5013
  import { join as join14 } from "path";
4947
5014
  function readGlobalAssets(home = homedir()) {
4948
5015
  const manifestPath = join14(home, ".media", "manifest.jsonl");
4949
5016
  if (!existsSync10(manifestPath)) return [];
4950
5017
  const out = [];
4951
- for (const line of readFileSync13(manifestPath, "utf8").split("\n")) {
5018
+ for (const line of readFileSync14(manifestPath, "utf8").split("\n")) {
4952
5019
  if (!line.trim()) continue;
4953
5020
  try {
4954
5021
  const rec = JSON.parse(line);
@@ -5108,6 +5175,7 @@ export {
5108
5175
  affectsProjectSignature,
5109
5176
  applyFileMutations,
5110
5177
  buildSubCompositionHtml,
5178
+ compositionsAffectedBy,
5111
5179
  consumeFileWriteReceipt,
5112
5180
  createBackgroundRemovalJob,
5113
5181
  createProjectSignature,