@hyperframes/studio-server 0.7.59 → 0.7.60
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 +11 -1
- package/dist/index.js +166 -23
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.d.ts
CHANGED
|
@@ -223,6 +223,16 @@ declare function walkDir(dir: string, prefix?: string): string[];
|
|
|
223
223
|
declare const MIME_TYPES: Record<string, string>;
|
|
224
224
|
declare function getMimeType(path: string): string;
|
|
225
225
|
|
|
226
|
+
interface FileWriteReceipt {
|
|
227
|
+
path: string;
|
|
228
|
+
version: string;
|
|
229
|
+
writeToken: string;
|
|
230
|
+
}
|
|
231
|
+
/** Strong content version used as both the JSON version and HTTP ETag. */
|
|
232
|
+
declare function fileContentVersion(content: string): string;
|
|
233
|
+
/** Attach one API write's identity to the corresponding filesystem-watch echo. */
|
|
234
|
+
declare function consumeFileWriteReceipt(absPath: string): FileWriteReceipt | null;
|
|
235
|
+
|
|
226
236
|
/**
|
|
227
237
|
* Build a standalone HTML page for a sub-composition.
|
|
228
238
|
*
|
|
@@ -274,4 +284,4 @@ type BackgroundRemovalRender = (options: {
|
|
|
274
284
|
}>;
|
|
275
285
|
declare function createBackgroundRemovalJob(opts: BackgroundRemovalJobOptions, render: BackgroundRemovalRender): MediaProcessingJobState;
|
|
276
286
|
|
|
277
|
-
export { type BackgroundRemovalRender, type LintResult, MIME_TYPES, type MediaProcessingJobState, type RenderJobState, type ResolvedProject, type StudioApiAdapter, type StudioSelectionResponse, type StudioSelectionSnapshot, type StudioSelectionTextField, buildSubCompositionHtml, createBackgroundRemovalJob, createProjectSignature, createStudioApi, getMimeType, walkDir };
|
|
287
|
+
export { type BackgroundRemovalRender, type FileWriteReceipt, type LintResult, MIME_TYPES, type MediaProcessingJobState, type RenderJobState, type ResolvedProject, type StudioApiAdapter, type StudioSelectionResponse, type StudioSelectionSnapshot, type StudioSelectionTextField, buildSubCompositionHtml, consumeFileWriteReceipt, createBackgroundRemovalJob, createProjectSignature, createStudioApi, fileContentVersion, getMimeType, walkDir };
|
package/dist/index.js
CHANGED
|
@@ -288,9 +288,13 @@ function registerStoryboardRoutes(api, adapter) {
|
|
|
288
288
|
// src/routes/files.ts
|
|
289
289
|
import { bodyLimit } from "hono/body-limit";
|
|
290
290
|
import {
|
|
291
|
+
closeSync,
|
|
291
292
|
existsSync as existsSync3,
|
|
293
|
+
ftruncateSync,
|
|
294
|
+
openSync,
|
|
292
295
|
readFileSync as readFileSync4,
|
|
293
296
|
writeFileSync as writeFileSync4,
|
|
297
|
+
writeSync,
|
|
294
298
|
mkdirSync as mkdirSync3,
|
|
295
299
|
unlinkSync as unlinkSync2,
|
|
296
300
|
rmSync as rmSync2,
|
|
@@ -544,6 +548,38 @@ function pruneBackups(backupDir, backupKey, keepPerFile) {
|
|
|
544
548
|
}
|
|
545
549
|
}
|
|
546
550
|
|
|
551
|
+
// src/helpers/fileVersion.ts
|
|
552
|
+
import { createHash as createHash2, randomUUID } from "crypto";
|
|
553
|
+
var RECEIPT_TTL_MS = 1e4;
|
|
554
|
+
var receipts = /* @__PURE__ */ new Map();
|
|
555
|
+
function fileContentVersion(content) {
|
|
556
|
+
return `"sha256:${createHash2("sha256").update(content, "utf8").digest("hex")}"`;
|
|
557
|
+
}
|
|
558
|
+
function createWriteToken(requestToken) {
|
|
559
|
+
const token = requestToken?.trim();
|
|
560
|
+
return token && token.length <= 200 ? token : randomUUID();
|
|
561
|
+
}
|
|
562
|
+
function recordFileWriteReceipt(absPath, receipt) {
|
|
563
|
+
const now = Date.now();
|
|
564
|
+
const current = (receipts.get(absPath) ?? []).filter(
|
|
565
|
+
(entry) => now - entry.recordedAt < RECEIPT_TTL_MS
|
|
566
|
+
);
|
|
567
|
+
current.push({ ...receipt, recordedAt: now });
|
|
568
|
+
receipts.set(absPath, current);
|
|
569
|
+
}
|
|
570
|
+
function consumeFileWriteReceipt(absPath) {
|
|
571
|
+
const now = Date.now();
|
|
572
|
+
const current = (receipts.get(absPath) ?? []).filter(
|
|
573
|
+
(entry) => now - entry.recordedAt < RECEIPT_TTL_MS
|
|
574
|
+
);
|
|
575
|
+
const receipt = current.shift() ?? null;
|
|
576
|
+
if (current.length > 0) receipts.set(absPath, current);
|
|
577
|
+
else receipts.delete(absPath);
|
|
578
|
+
if (!receipt) return null;
|
|
579
|
+
const { path, version, writeToken } = receipt;
|
|
580
|
+
return { path, version, writeToken };
|
|
581
|
+
}
|
|
582
|
+
|
|
547
583
|
// src/helpers/finiteMutation.ts
|
|
548
584
|
function findUnsafeMutationValues(value, path = "body", options = {}) {
|
|
549
585
|
if (value === null) {
|
|
@@ -1570,9 +1606,11 @@ async function applyGsapMutations(c, res, mutations) {
|
|
|
1570
1606
|
after: newHtml,
|
|
1571
1607
|
scriptText: block.scriptText,
|
|
1572
1608
|
path: res.filePath,
|
|
1609
|
+
version: fileContentVersion(newHtml),
|
|
1573
1610
|
backupPath
|
|
1574
1611
|
};
|
|
1575
1612
|
if (skippedSelectors.size > 0) responsePayload.skippedSelectors = [...skippedSelectors];
|
|
1613
|
+
c.header("ETag", responsePayload.version);
|
|
1576
1614
|
return c.json(responsePayload);
|
|
1577
1615
|
}
|
|
1578
1616
|
function executeGsapMutationAcorn(body, block, respond) {
|
|
@@ -2233,19 +2271,111 @@ function registerFileRoutes(api, adapter) {
|
|
|
2233
2271
|
return c.json({ error: "not found" }, 404);
|
|
2234
2272
|
}
|
|
2235
2273
|
const content = readFileSync4(res.absPath, "utf-8");
|
|
2236
|
-
|
|
2274
|
+
const version = fileContentVersion(content);
|
|
2275
|
+
c.header("ETag", version);
|
|
2276
|
+
return c.json({ filename: res.filePath, content, version });
|
|
2237
2277
|
});
|
|
2238
2278
|
api.put("/projects/:id/files/*", async (c) => {
|
|
2239
2279
|
const res = await resolveProjectFile(c, adapter);
|
|
2240
2280
|
if ("error" in res) return res.error;
|
|
2241
|
-
ensureDir(res.absPath);
|
|
2242
2281
|
const body = await c.req.text();
|
|
2243
|
-
const
|
|
2244
|
-
|
|
2245
|
-
|
|
2282
|
+
const expectedVersion = c.req.header("If-Match")?.trim() ?? null;
|
|
2283
|
+
const createOnly = c.req.header("If-None-Match")?.trim() === "*";
|
|
2284
|
+
if (expectedVersion === null && !createOnly) {
|
|
2285
|
+
let currentContent = null;
|
|
2286
|
+
try {
|
|
2287
|
+
currentContent = readFileSync4(res.absPath, "utf-8");
|
|
2288
|
+
} catch (error) {
|
|
2289
|
+
if (!error || typeof error !== "object" || !("code" in error) || error.code !== "ENOENT") {
|
|
2290
|
+
throw error;
|
|
2291
|
+
}
|
|
2292
|
+
}
|
|
2293
|
+
return c.json(
|
|
2294
|
+
{
|
|
2295
|
+
error: "precondition required",
|
|
2296
|
+
path: res.filePath,
|
|
2297
|
+
currentVersion: currentContent === null ? null : fileContentVersion(currentContent),
|
|
2298
|
+
currentContent
|
|
2299
|
+
},
|
|
2300
|
+
428
|
|
2301
|
+
);
|
|
2302
|
+
}
|
|
2303
|
+
let backup = { backupPath: null };
|
|
2304
|
+
if (createOnly) {
|
|
2305
|
+
ensureDir(res.absPath);
|
|
2306
|
+
let fd;
|
|
2307
|
+
try {
|
|
2308
|
+
fd = openSync(res.absPath, "wx");
|
|
2309
|
+
} catch (error) {
|
|
2310
|
+
if (!error || typeof error !== "object" || !("code" in error) || error.code !== "EEXIST") {
|
|
2311
|
+
throw error;
|
|
2312
|
+
}
|
|
2313
|
+
const currentContent = readFileSync4(res.absPath, "utf-8");
|
|
2314
|
+
return c.json(
|
|
2315
|
+
{
|
|
2316
|
+
error: "file conflict",
|
|
2317
|
+
path: res.filePath,
|
|
2318
|
+
currentVersion: fileContentVersion(currentContent),
|
|
2319
|
+
currentContent
|
|
2320
|
+
},
|
|
2321
|
+
409
|
|
2322
|
+
);
|
|
2323
|
+
}
|
|
2324
|
+
try {
|
|
2325
|
+
writeSync(fd, body, 0, "utf-8");
|
|
2326
|
+
} finally {
|
|
2327
|
+
closeSync(fd);
|
|
2328
|
+
}
|
|
2329
|
+
} else {
|
|
2330
|
+
let fd;
|
|
2331
|
+
try {
|
|
2332
|
+
fd = openSync(res.absPath, "r+");
|
|
2333
|
+
} catch (error) {
|
|
2334
|
+
if (!error || typeof error !== "object" || !("code" in error) || error.code !== "ENOENT") {
|
|
2335
|
+
throw error;
|
|
2336
|
+
}
|
|
2337
|
+
return c.json(
|
|
2338
|
+
{
|
|
2339
|
+
error: "file conflict",
|
|
2340
|
+
path: res.filePath,
|
|
2341
|
+
currentVersion: null,
|
|
2342
|
+
currentContent: null
|
|
2343
|
+
},
|
|
2344
|
+
409
|
|
2345
|
+
);
|
|
2346
|
+
}
|
|
2347
|
+
try {
|
|
2348
|
+
const currentContent = readFileSync4(fd, "utf-8");
|
|
2349
|
+
const currentVersion = fileContentVersion(currentContent);
|
|
2350
|
+
if (expectedVersion !== currentVersion) {
|
|
2351
|
+
return c.json(
|
|
2352
|
+
{
|
|
2353
|
+
error: "file conflict",
|
|
2354
|
+
path: res.filePath,
|
|
2355
|
+
currentVersion,
|
|
2356
|
+
currentContent
|
|
2357
|
+
},
|
|
2358
|
+
409
|
|
2359
|
+
);
|
|
2360
|
+
}
|
|
2361
|
+
backup = snapshotBeforeWrite(res.project.dir, res.absPath);
|
|
2362
|
+
if (backup.error)
|
|
2363
|
+
console.warn(`Failed to create backup for ${res.filePath}: ${backup.error}`);
|
|
2364
|
+
ftruncateSync(fd, 0);
|
|
2365
|
+
writeSync(fd, body, 0, "utf-8");
|
|
2366
|
+
} finally {
|
|
2367
|
+
closeSync(fd);
|
|
2368
|
+
}
|
|
2369
|
+
}
|
|
2370
|
+
const version = fileContentVersion(body);
|
|
2371
|
+
const writeToken = createWriteToken(c.req.header("X-Hyperframes-Write-Token"));
|
|
2372
|
+
recordFileWriteReceipt(res.absPath, { path: res.filePath, version, writeToken });
|
|
2373
|
+
c.header("ETag", version);
|
|
2246
2374
|
return c.json({
|
|
2247
2375
|
ok: true,
|
|
2248
2376
|
path: res.filePath,
|
|
2377
|
+
version,
|
|
2378
|
+
writeToken,
|
|
2249
2379
|
backupPath: backupPathForResponse(res.project.dir, backup.backupPath)
|
|
2250
2380
|
});
|
|
2251
2381
|
});
|
|
@@ -2317,17 +2447,28 @@ function registerFileRoutes(api, adapter) {
|
|
|
2317
2447
|
fallbackTiming
|
|
2318
2448
|
);
|
|
2319
2449
|
if (!result.matched) {
|
|
2320
|
-
|
|
2450
|
+
const version2 = fileContentVersion(originalContent);
|
|
2451
|
+
c.header("ETag", version2);
|
|
2452
|
+
return c.json({
|
|
2453
|
+
ok: false,
|
|
2454
|
+
changed: false,
|
|
2455
|
+
content: originalContent,
|
|
2456
|
+
path: ctx.filePath,
|
|
2457
|
+
version: version2
|
|
2458
|
+
});
|
|
2321
2459
|
}
|
|
2322
2460
|
const backup = snapshotBeforeWrite(ctx.project.dir, ctx.absPath);
|
|
2323
2461
|
if (backup.error) console.warn(`Failed to create backup for ${ctx.filePath}: ${backup.error}`);
|
|
2324
2462
|
writeFileSync4(ctx.absPath, result.html, "utf-8");
|
|
2463
|
+
const version = fileContentVersion(result.html);
|
|
2464
|
+
c.header("ETag", version);
|
|
2325
2465
|
return c.json({
|
|
2326
2466
|
ok: true,
|
|
2327
2467
|
changed: true,
|
|
2328
2468
|
content: result.html,
|
|
2329
2469
|
newId: result.newId,
|
|
2330
2470
|
path: ctx.filePath,
|
|
2471
|
+
version,
|
|
2331
2472
|
backupPath: backupPathForResponse(ctx.project.dir, backup.backupPath)
|
|
2332
2473
|
});
|
|
2333
2474
|
});
|
|
@@ -2650,7 +2791,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
2650
2791
|
// src/routes/preview.ts
|
|
2651
2792
|
import { existsSync as existsSync5, readFileSync as readFileSync7, statSync as statSync2 } from "fs";
|
|
2652
2793
|
import { join as join8 } from "path";
|
|
2653
|
-
import { createHash as
|
|
2794
|
+
import { createHash as createHash3 } from "crypto";
|
|
2654
2795
|
import { injectScriptsIntoHtml, stripEmbeddedRuntimeScripts as stripEmbeddedRuntimeScripts2 } from "@hyperframes/core/compiler";
|
|
2655
2796
|
|
|
2656
2797
|
// src/helpers/subComposition.ts
|
|
@@ -3022,14 +3163,14 @@ import { ensureHfIds as ensureHfIds3 } from "@hyperframes/parsers/hf-ids";
|
|
|
3022
3163
|
// src/helpers/hfIdPersist.ts
|
|
3023
3164
|
import { ensureHfIds as ensureHfIds2 } from "@hyperframes/parsers/hf-ids";
|
|
3024
3165
|
import {
|
|
3025
|
-
closeSync,
|
|
3166
|
+
closeSync as closeSync2,
|
|
3026
3167
|
constants,
|
|
3027
3168
|
fstatSync,
|
|
3028
|
-
ftruncateSync,
|
|
3029
|
-
openSync,
|
|
3169
|
+
ftruncateSync as ftruncateSync2,
|
|
3170
|
+
openSync as openSync2,
|
|
3030
3171
|
readFileSync as readFileSync6,
|
|
3031
3172
|
writeFileSync as writeFileSync5,
|
|
3032
|
-
writeSync
|
|
3173
|
+
writeSync as writeSync2
|
|
3033
3174
|
} from "fs";
|
|
3034
3175
|
function persistHfIdsIfNeeded(filePath, html) {
|
|
3035
3176
|
const normalized = ensureHfIds2(html);
|
|
@@ -3050,7 +3191,7 @@ function persistHfIdsIfNeeded(filePath, html) {
|
|
|
3050
3191
|
function openNoFollow(filePath, flags) {
|
|
3051
3192
|
const noFollow = constants.O_NOFOLLOW ?? 0;
|
|
3052
3193
|
try {
|
|
3053
|
-
return
|
|
3194
|
+
return openSync2(filePath, flags | noFollow);
|
|
3054
3195
|
} catch {
|
|
3055
3196
|
return null;
|
|
3056
3197
|
}
|
|
@@ -3070,15 +3211,15 @@ function stampFileHfIds(filePath) {
|
|
|
3070
3211
|
const idsBefore = (html.match(/\bdata-hf-id=/g) ?? []).length;
|
|
3071
3212
|
const idsAfter = (normalized.match(/\bdata-hf-id=/g) ?? []).length;
|
|
3072
3213
|
if (writable && idsAfter > idsBefore) {
|
|
3073
|
-
|
|
3074
|
-
|
|
3214
|
+
ftruncateSync2(fd, 0);
|
|
3215
|
+
writeSync2(fd, normalized, 0, "utf-8");
|
|
3075
3216
|
}
|
|
3076
3217
|
return normalized;
|
|
3077
3218
|
} catch (err) {
|
|
3078
3219
|
console.warn("[hyperframes] stampFileHfIds: failed to stamp ids:", err);
|
|
3079
3220
|
return null;
|
|
3080
3221
|
} finally {
|
|
3081
|
-
|
|
3222
|
+
closeSync2(fd);
|
|
3082
3223
|
}
|
|
3083
3224
|
}
|
|
3084
3225
|
|
|
@@ -3234,7 +3375,7 @@ function parsePreviewVariablesParam(raw) {
|
|
|
3234
3375
|
}
|
|
3235
3376
|
function variablesEtagSalt(raw) {
|
|
3236
3377
|
if (!raw) return "";
|
|
3237
|
-
return `:vars:${
|
|
3378
|
+
return `:vars:${createHash3("sha1").update(raw).digest("hex").slice(0, 12)}`;
|
|
3238
3379
|
}
|
|
3239
3380
|
function previewVariablesFromRequest(rawVariables) {
|
|
3240
3381
|
const parse = parsePreviewVariablesParam(rawVariables);
|
|
@@ -3706,7 +3847,7 @@ function registerRenderRoutes(api, adapter) {
|
|
|
3706
3847
|
// src/routes/thumbnail.ts
|
|
3707
3848
|
import { existsSync as existsSync7, readFileSync as readFileSync10, writeFileSync as writeFileSync6, mkdirSync as mkdirSync5, statSync as statSync4 } from "fs";
|
|
3708
3849
|
import { join as join11 } from "path";
|
|
3709
|
-
import { createHash as
|
|
3850
|
+
import { createHash as createHash4 } from "crypto";
|
|
3710
3851
|
|
|
3711
3852
|
// src/helpers/manualEditsRenderScript.ts
|
|
3712
3853
|
var STUDIO_MANUAL_EDITS_PATH = ".hyperframes/studio-manual-edits.json";
|
|
@@ -4318,7 +4459,7 @@ function registerThumbnailRoutes(api, adapter) {
|
|
|
4318
4459
|
const htmlFile = join11(project.dir, compPath);
|
|
4319
4460
|
if (existsSync7(htmlFile)) {
|
|
4320
4461
|
const html = readFileSync10(htmlFile, "utf-8");
|
|
4321
|
-
sourceKey = `_${
|
|
4462
|
+
sourceKey = `_${createHash4("sha1").update(html).digest("hex").slice(0, 16)}`;
|
|
4322
4463
|
sourceMtime = Math.round(statSync4(htmlFile).mtimeMs);
|
|
4323
4464
|
if (!vpWidth) {
|
|
4324
4465
|
const wMatch = html.match(/data-width=["'](\d+)["']/);
|
|
@@ -4331,14 +4472,14 @@ function registerThumbnailRoutes(api, adapter) {
|
|
|
4331
4472
|
let manualEditsKey = "";
|
|
4332
4473
|
if (existsSync7(manualEditsFile)) {
|
|
4333
4474
|
const manualEditsContent = readFileSync10(manualEditsFile, "utf-8");
|
|
4334
|
-
manualEditsKey = `_${
|
|
4475
|
+
manualEditsKey = `_${createHash4("sha1").update(manualEditsContent).digest("hex").slice(0, 16)}`;
|
|
4335
4476
|
sourceMtime = Math.max(sourceMtime, Math.round(statSync4(manualEditsFile).mtimeMs));
|
|
4336
4477
|
}
|
|
4337
4478
|
const motionFile = join11(project.dir, STUDIO_MOTION_PATH);
|
|
4338
4479
|
let motionKey = "";
|
|
4339
4480
|
if (existsSync7(motionFile)) {
|
|
4340
4481
|
const motionContent = readFileSync10(motionFile, "utf-8");
|
|
4341
|
-
motionKey = `_${
|
|
4482
|
+
motionKey = `_${createHash4("sha1").update(motionContent).digest("hex").slice(0, 16)}`;
|
|
4342
4483
|
sourceMtime = Math.max(sourceMtime, Math.round(statSync4(motionFile).mtimeMs));
|
|
4343
4484
|
}
|
|
4344
4485
|
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}`;
|
|
@@ -4419,7 +4560,7 @@ function registerWaveformRoutes(api, adapter) {
|
|
|
4419
4560
|
}
|
|
4420
4561
|
|
|
4421
4562
|
// src/routes/fonts.ts
|
|
4422
|
-
import { closeSync as
|
|
4563
|
+
import { closeSync as closeSync3, constants as constants2, fstatSync as fstatSync2, openSync as openSync3, readSync } from "fs";
|
|
4423
4564
|
import {
|
|
4424
4565
|
collectFontFileEntries,
|
|
4425
4566
|
fontDirectories,
|
|
@@ -4526,7 +4667,7 @@ function registerFontRoutes(api) {
|
|
|
4526
4667
|
if (!located) return c.json({ error: "font not found" }, 404);
|
|
4527
4668
|
let fd;
|
|
4528
4669
|
try {
|
|
4529
|
-
fd =
|
|
4670
|
+
fd = openSync3(located.path, constants2.O_RDONLY | constants2.O_NOFOLLOW);
|
|
4530
4671
|
} catch {
|
|
4531
4672
|
return c.json({ error: "font file not accessible" }, 404);
|
|
4532
4673
|
}
|
|
@@ -4548,7 +4689,7 @@ function registerFontRoutes(api) {
|
|
|
4548
4689
|
} catch {
|
|
4549
4690
|
return c.json({ error: "failed to read font file" }, 500);
|
|
4550
4691
|
} finally {
|
|
4551
|
-
|
|
4692
|
+
closeSync3(fd);
|
|
4552
4693
|
}
|
|
4553
4694
|
});
|
|
4554
4695
|
}
|
|
@@ -5082,12 +5223,14 @@ export {
|
|
|
5082
5223
|
STUDIO_MANUAL_EDITS_PATH,
|
|
5083
5224
|
STUDIO_MOTION_PATH,
|
|
5084
5225
|
buildSubCompositionHtml,
|
|
5226
|
+
consumeFileWriteReceipt,
|
|
5085
5227
|
createBackgroundRemovalJob,
|
|
5086
5228
|
createProjectSignature,
|
|
5087
5229
|
createStudioApi,
|
|
5088
5230
|
createStudioManualEditsRenderBodyScript,
|
|
5089
5231
|
createStudioMotionRenderBodyScript,
|
|
5090
5232
|
createStudioPositionSeekReapplyScript,
|
|
5233
|
+
fileContentVersion,
|
|
5091
5234
|
getElementScreenshotClip,
|
|
5092
5235
|
getMimeType,
|
|
5093
5236
|
isSafePath,
|