@hyperframes/studio-server 0.8.133 → 0.8.135
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.js +731 -704
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -159,10 +159,10 @@ function registerProjectRoutes(api, adapter) {
|
|
|
159
159
|
import { bodyLimit } from "hono/body-limit";
|
|
160
160
|
import {
|
|
161
161
|
closeSync,
|
|
162
|
-
existsSync as
|
|
162
|
+
existsSync as existsSync4,
|
|
163
163
|
lstatSync as lstatSync2,
|
|
164
164
|
openSync,
|
|
165
|
-
readFileSync as
|
|
165
|
+
readFileSync as readFileSync5,
|
|
166
166
|
readlinkSync,
|
|
167
167
|
unlinkSync as unlinkSync2,
|
|
168
168
|
rmSync as rmSync3,
|
|
@@ -172,7 +172,7 @@ import {
|
|
|
172
172
|
readdirSync as readdirSync2,
|
|
173
173
|
realpathSync
|
|
174
174
|
} from "fs";
|
|
175
|
-
import { resolve as resolve2, dirname as dirname3, join as
|
|
175
|
+
import { resolve as resolve2, dirname as dirname3, join as join6 } from "path";
|
|
176
176
|
|
|
177
177
|
// src/helpers/mime.ts
|
|
178
178
|
var MIME_TYPES = {
|
|
@@ -216,8 +216,504 @@ function getMimeType(path) {
|
|
|
216
216
|
const ext = path.slice(path.lastIndexOf(".")).toLowerCase();
|
|
217
217
|
return MIME_TYPES[ext] || "application/octet-stream";
|
|
218
218
|
}
|
|
219
|
-
function isAudioFile(name) {
|
|
220
|
-
return (getMimeType(name) ?? "").startsWith("audio/");
|
|
219
|
+
function isAudioFile(name) {
|
|
220
|
+
return (getMimeType(name) ?? "").startsWith("audio/");
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// src/helpers/compositionInputs.ts
|
|
224
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
225
|
+
import { createHash } from "crypto";
|
|
226
|
+
import { parseHTML as parseHTML3 } from "linkedom";
|
|
227
|
+
|
|
228
|
+
// src/helpers/subComposition.ts
|
|
229
|
+
import { existsSync, readFileSync } from "fs";
|
|
230
|
+
import { join as join2 } from "path";
|
|
231
|
+
import { parseHTML } from "linkedom";
|
|
232
|
+
import {
|
|
233
|
+
rewriteAssetPaths,
|
|
234
|
+
rewriteCssAssetUrls,
|
|
235
|
+
rewriteInlineStyleAssetUrls
|
|
236
|
+
} from "@hyperframes/core";
|
|
237
|
+
import {
|
|
238
|
+
deferScriptsUntilFonts,
|
|
239
|
+
RUNTIME_BOOTSTRAP_ATTR,
|
|
240
|
+
stripEmbeddedRuntimeScripts
|
|
241
|
+
} from "@hyperframes/core/compiler";
|
|
242
|
+
import { isFullHtmlDocument } from "@hyperframes/core/compiler/html-document";
|
|
243
|
+
import { gsapCdnDist } from "@hyperframes/core/gsap-cdn";
|
|
244
|
+
function rewriteRelativePaths(root, compPath, projectDir) {
|
|
245
|
+
const assetExists = (path) => existsSync(join2(projectDir, path));
|
|
246
|
+
rewriteAssetPaths(
|
|
247
|
+
root.querySelectorAll("[src], [href]"),
|
|
248
|
+
compPath,
|
|
249
|
+
(el, attr) => el.getAttribute(attr),
|
|
250
|
+
(el, attr, value) => el.setAttribute(attr, value),
|
|
251
|
+
assetExists
|
|
252
|
+
);
|
|
253
|
+
rewriteInlineStyleAssetUrls(
|
|
254
|
+
root.querySelectorAll("[style]"),
|
|
255
|
+
compPath,
|
|
256
|
+
(el) => el.getAttribute("style"),
|
|
257
|
+
(el, value) => el.setAttribute("style", value),
|
|
258
|
+
assetExists
|
|
259
|
+
);
|
|
260
|
+
for (const styleEl of root.querySelectorAll("style")) {
|
|
261
|
+
styleEl.textContent = rewriteCssAssetUrls(styleEl.textContent || "", compPath, assetExists);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
function escapeLeadingDigitIdent(id) {
|
|
265
|
+
return `\\${id.charCodeAt(0).toString(16)} ${id.slice(1)}`;
|
|
266
|
+
}
|
|
267
|
+
var REGEXP_SPECIALS = /[.*+?^${}()|[\]\\]/g;
|
|
268
|
+
function fixDigitLeadingIdSelectors(root) {
|
|
269
|
+
const digitIds = /* @__PURE__ */ new Set();
|
|
270
|
+
for (const el of root.querySelectorAll("[id]")) {
|
|
271
|
+
const id = el.getAttribute("id");
|
|
272
|
+
if (id && /^\d/.test(id)) digitIds.add(id);
|
|
273
|
+
}
|
|
274
|
+
if (digitIds.size === 0) return;
|
|
275
|
+
for (const styleEl of root.querySelectorAll("style")) {
|
|
276
|
+
let css = styleEl.textContent || "";
|
|
277
|
+
for (const id of digitIds) {
|
|
278
|
+
const pattern = new RegExp(`#${id.replace(REGEXP_SPECIALS, "\\$&")}(?![\\w-])`, "g");
|
|
279
|
+
css = css.replace(pattern, `#${escapeLeadingDigitIdent(id)}`);
|
|
280
|
+
}
|
|
281
|
+
styleEl.textContent = css;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
function extractFullDocumentParts(rawHtml, compPath, projectDir) {
|
|
285
|
+
const { document: doc } = parseHTML(rawHtml);
|
|
286
|
+
const rewriteTargets = [doc.head, doc.body].filter(Boolean);
|
|
287
|
+
for (const target of rewriteTargets) {
|
|
288
|
+
rewriteRelativePaths(target, compPath, projectDir);
|
|
289
|
+
}
|
|
290
|
+
fixDigitLeadingIdSelectors(doc);
|
|
291
|
+
const headContent = doc.head?.innerHTML ?? "";
|
|
292
|
+
const bodyContent = doc.body?.innerHTML ?? "";
|
|
293
|
+
const htmlEl = doc.documentElement;
|
|
294
|
+
const htmlAttrs = extractElementAttrs(htmlEl);
|
|
295
|
+
const bodyAttrs = doc.body ? extractElementAttrs(doc.body) : "";
|
|
296
|
+
return { headContent, bodyContent, htmlAttrs, bodyAttrs };
|
|
297
|
+
}
|
|
298
|
+
function extractTemplateInnerHtml(rawComp) {
|
|
299
|
+
const { document: doc } = parseHTML(rawComp);
|
|
300
|
+
const template = doc.querySelector("template");
|
|
301
|
+
return template ? template.innerHTML : null;
|
|
302
|
+
}
|
|
303
|
+
function escapeAttrValue(value) {
|
|
304
|
+
return value.replace(/&/g, "&").replace(/"/g, """);
|
|
305
|
+
}
|
|
306
|
+
function extractElementAttrs(el) {
|
|
307
|
+
const parts = [];
|
|
308
|
+
for (let i = 0; i < el.attributes.length; i++) {
|
|
309
|
+
const attr = el.attributes[i];
|
|
310
|
+
if (attr.value === "") {
|
|
311
|
+
parts.push(attr.name);
|
|
312
|
+
} else {
|
|
313
|
+
parts.push(`${attr.name}="${escapeAttrValue(attr.value)}"`);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return parts.join(" ");
|
|
317
|
+
}
|
|
318
|
+
var NON_RENDERED_TAGS = /* @__PURE__ */ new Set(["SCRIPT", "STYLE", "LINK", "META", "TEMPLATE", "NOSCRIPT"]);
|
|
319
|
+
function promoteTemplateCompositionId(rawComp, body) {
|
|
320
|
+
let templateCompositionId;
|
|
321
|
+
for (const tag of rawComp.matchAll(/<template\b[^>]*/gi)) {
|
|
322
|
+
const id = /\bdata-composition-id\s*=\s*["']([^"']+)["']/i.exec(tag[0] ?? "")?.[1];
|
|
323
|
+
if (id) {
|
|
324
|
+
templateCompositionId = id;
|
|
325
|
+
break;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
if (!templateCompositionId) return;
|
|
329
|
+
if (body.querySelector("[data-composition-id]")) return;
|
|
330
|
+
const root = Array.from(body.children).find((el) => !NON_RENDERED_TAGS.has(el.tagName));
|
|
331
|
+
root?.setAttribute("data-composition-id", templateCompositionId);
|
|
332
|
+
}
|
|
333
|
+
function tagRootCompositionFile(bodyHtml, compPath) {
|
|
334
|
+
const match = bodyHtml.match(/<[a-zA-Z][^>]*\bdata-composition-id=/);
|
|
335
|
+
if (match?.index == null) return bodyHtml;
|
|
336
|
+
const tagEnd = bodyHtml.indexOf(">", match.index);
|
|
337
|
+
if (tagEnd === -1) return bodyHtml;
|
|
338
|
+
if (bodyHtml.slice(match.index, tagEnd).includes("data-composition-file")) return bodyHtml;
|
|
339
|
+
return bodyHtml.slice(0, tagEnd) + ` data-composition-file="${compPath}"` + bodyHtml.slice(tagEnd);
|
|
340
|
+
}
|
|
341
|
+
function rootHeadContent(rootHtml) {
|
|
342
|
+
return rootHtml.match(/<head[^>]*>([\s\S]*?)<\/head>/i)?.[1] ?? "";
|
|
343
|
+
}
|
|
344
|
+
function buildSubCompositionHtml(projectDir, compPath, runtimeUrl, baseHref, rawOverride) {
|
|
345
|
+
const compFile = join2(projectDir, compPath);
|
|
346
|
+
if (!existsSync(compFile)) return null;
|
|
347
|
+
const rawComp = rawOverride ?? readFileSync(compFile, "utf-8");
|
|
348
|
+
let compHeadContent = "";
|
|
349
|
+
let rewrittenContent;
|
|
350
|
+
let htmlAttrs = "";
|
|
351
|
+
let bodyAttrs = "";
|
|
352
|
+
const templateInner = extractTemplateInnerHtml(rawComp);
|
|
353
|
+
if (templateInner != null) {
|
|
354
|
+
const { document: contentDoc } = parseHTML(
|
|
355
|
+
`<!DOCTYPE html><html><head></head><body>${templateInner}</body></html>`
|
|
356
|
+
);
|
|
357
|
+
rewriteRelativePaths(contentDoc, compPath, projectDir);
|
|
358
|
+
fixDigitLeadingIdSelectors(contentDoc);
|
|
359
|
+
promoteTemplateCompositionId(rawComp, contentDoc.body);
|
|
360
|
+
rewrittenContent = contentDoc.body.innerHTML || templateInner;
|
|
361
|
+
} else if (isFullHtmlDocument(rawComp)) {
|
|
362
|
+
const parts = extractFullDocumentParts(rawComp, compPath, projectDir);
|
|
363
|
+
compHeadContent = parts.headContent;
|
|
364
|
+
rewrittenContent = parts.bodyContent;
|
|
365
|
+
htmlAttrs = parts.htmlAttrs;
|
|
366
|
+
bodyAttrs = parts.bodyAttrs;
|
|
367
|
+
} else {
|
|
368
|
+
const { document: contentDoc } = parseHTML(
|
|
369
|
+
`<!DOCTYPE html><html><head></head><body>${rawComp}</body></html>`
|
|
370
|
+
);
|
|
371
|
+
rewriteRelativePaths(contentDoc, compPath, projectDir);
|
|
372
|
+
fixDigitLeadingIdSelectors(contentDoc);
|
|
373
|
+
rewrittenContent = contentDoc.body.innerHTML || rawComp;
|
|
374
|
+
}
|
|
375
|
+
rewrittenContent = stripEmbeddedRuntimeScripts(rewrittenContent);
|
|
376
|
+
const { document: scriptsDoc } = parseHTML(
|
|
377
|
+
`<!DOCTYPE html><html><head></head><body>${rewrittenContent}</body></html>`
|
|
378
|
+
);
|
|
379
|
+
deferScriptsUntilFonts(scriptsDoc);
|
|
380
|
+
rewrittenContent = scriptsDoc.body.innerHTML;
|
|
381
|
+
rewrittenContent = tagRootCompositionFile(rewrittenContent, compPath);
|
|
382
|
+
const indexPath = join2(projectDir, "index.html");
|
|
383
|
+
let headContent = "";
|
|
384
|
+
if (existsSync(indexPath)) headContent = rootHeadContent(readFileSync(indexPath, "utf-8"));
|
|
385
|
+
if (baseHref && !hasBaseElement(headContent)) {
|
|
386
|
+
headContent = `<base href="${baseHref}">
|
|
387
|
+
${headContent}`;
|
|
388
|
+
}
|
|
389
|
+
if (compHeadContent) headContent += `
|
|
390
|
+
${compHeadContent}`;
|
|
391
|
+
headContent = stripEmbeddedRuntimeScripts(headContent);
|
|
392
|
+
headContent += `
|
|
393
|
+
<script ${RUNTIME_BOOTSTRAP_ATTR}="1" src="${runtimeUrl}"></script>`;
|
|
394
|
+
if (!headContent.includes("gsap")) {
|
|
395
|
+
headContent += `
|
|
396
|
+
<script src="${gsapCdnDist()}gsap.min.js"></script>`;
|
|
397
|
+
}
|
|
398
|
+
const htmlOpen = htmlAttrs ? `<html ${htmlAttrs}>` : "<html>";
|
|
399
|
+
const bodyOpen = bodyAttrs ? `<body ${bodyAttrs}>` : "<body>";
|
|
400
|
+
return `<!DOCTYPE html>
|
|
401
|
+
${htmlOpen}
|
|
402
|
+
<head>
|
|
403
|
+
${headContent}
|
|
404
|
+
</head>
|
|
405
|
+
${bodyOpen}
|
|
406
|
+
<script>window.__timelines=window.__timelines||{};</script>
|
|
407
|
+
${rewrittenContent}
|
|
408
|
+
</body>
|
|
409
|
+
</html>`;
|
|
410
|
+
}
|
|
411
|
+
function hasBaseElement(html) {
|
|
412
|
+
return parseHTML(html).document.querySelector("base") !== null;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// src/helpers/compositionInsertion.ts
|
|
416
|
+
import { existsSync as existsSync2 } from "fs";
|
|
417
|
+
import { readProjectFile } from "@hyperframes/parsers/asset-resolution";
|
|
418
|
+
import { randomUUID } from "crypto";
|
|
419
|
+
import { dirname, relative, resolve, sep } from "path";
|
|
420
|
+
import { parseHTML as parseHTML2 } from "linkedom";
|
|
421
|
+
import { ProjectRootMissingError } from "@hyperframes/core";
|
|
422
|
+
var CompositionInsertionError = class extends Error {
|
|
423
|
+
constructor(message, status) {
|
|
424
|
+
super(message);
|
|
425
|
+
this.status = status;
|
|
426
|
+
}
|
|
427
|
+
status;
|
|
428
|
+
};
|
|
429
|
+
function descendants(root, selector) {
|
|
430
|
+
const found = Array.from(root.querySelectorAll(selector));
|
|
431
|
+
for (const template of root.querySelectorAll("template")) {
|
|
432
|
+
found.push(...descendants(template, selector));
|
|
433
|
+
}
|
|
434
|
+
return [...new Set(found)];
|
|
435
|
+
}
|
|
436
|
+
function compositionRoot(source) {
|
|
437
|
+
const document = parseHTML2(source).document;
|
|
438
|
+
const root = descendants(document, "[data-composition-id]")[0];
|
|
439
|
+
if (!root) throw new CompositionInsertionError("Composition source has no root", 400);
|
|
440
|
+
return { document, root };
|
|
441
|
+
}
|
|
442
|
+
function positiveAttribute(root, ...names) {
|
|
443
|
+
for (const name of names) {
|
|
444
|
+
const value = Number.parseFloat(root.getAttribute(name) ?? "");
|
|
445
|
+
if (Number.isFinite(value) && value > 0) return value;
|
|
446
|
+
}
|
|
447
|
+
throw new CompositionInsertionError(`Composition source has no valid ${names[0]}`, 400);
|
|
448
|
+
}
|
|
449
|
+
function canonicalProjectPath(projectDir, candidate) {
|
|
450
|
+
if (!candidate) {
|
|
451
|
+
if (folderGone(projectDir)) throw new ProjectRootMissingError(projectDir);
|
|
452
|
+
throw new CompositionInsertionError("Composition source escapes the project", 400);
|
|
453
|
+
}
|
|
454
|
+
if (!existsSync2(candidate)) {
|
|
455
|
+
throw new CompositionInsertionError("Composition source was not found", 404);
|
|
456
|
+
}
|
|
457
|
+
const canonical = realpath(candidate);
|
|
458
|
+
if (!isSafePath(realpath(projectDir), canonical)) {
|
|
459
|
+
throw new CompositionInsertionError("Composition source escapes the project", 400);
|
|
460
|
+
}
|
|
461
|
+
return canonical;
|
|
462
|
+
}
|
|
463
|
+
function readCompositionSource(file) {
|
|
464
|
+
const read = readProjectFile(file);
|
|
465
|
+
if (read.kind === "folder") {
|
|
466
|
+
throw new CompositionInsertionError("Composition source is a folder, not an HTML file", 400);
|
|
467
|
+
}
|
|
468
|
+
if (read.kind === "missing") {
|
|
469
|
+
throw new CompositionInsertionError("Composition source was not found", 404);
|
|
470
|
+
}
|
|
471
|
+
return read.text;
|
|
472
|
+
}
|
|
473
|
+
function validateSourcePath(sourcePath) {
|
|
474
|
+
if (!sourcePath.trim() || sourcePath.includes("\0") || /^[a-z]+:/i.test(sourcePath)) {
|
|
475
|
+
throw new CompositionInsertionError("Invalid composition source path", 400);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
function canonicalProjectFile(projectDir, sourcePath) {
|
|
479
|
+
validateSourcePath(sourcePath);
|
|
480
|
+
return canonicalProjectPath(projectDir, resolveWithinProject(projectDir, sourcePath));
|
|
481
|
+
}
|
|
482
|
+
function canonicalDependency(projectDir, ownerAbs, sourcePath) {
|
|
483
|
+
validateSourcePath(sourcePath);
|
|
484
|
+
return canonicalProjectPath(
|
|
485
|
+
projectDir,
|
|
486
|
+
resolveWithinProject(projectDir, relative(projectDir, resolve(dirname(ownerAbs), sourcePath)))
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
function validateDependencyGraph(projectDir, targetAbs, sourceAbs) {
|
|
490
|
+
const visited = /* @__PURE__ */ new Set();
|
|
491
|
+
const visiting = /* @__PURE__ */ new Set();
|
|
492
|
+
const visit = (file) => {
|
|
493
|
+
if (file === targetAbs) {
|
|
494
|
+
throw new CompositionInsertionError("Composition insertion would create a cycle", 400);
|
|
495
|
+
}
|
|
496
|
+
if (visiting.has(file)) {
|
|
497
|
+
throw new CompositionInsertionError("Composition dependency cycle detected", 400);
|
|
498
|
+
}
|
|
499
|
+
if (visited.has(file)) return;
|
|
500
|
+
visiting.add(file);
|
|
501
|
+
const source = readCompositionSource(file);
|
|
502
|
+
const { document } = compositionRoot(source);
|
|
503
|
+
for (const host of descendants(document, "[data-composition-src]")) {
|
|
504
|
+
const dependency = host.getAttribute("data-composition-src");
|
|
505
|
+
if (dependency) {
|
|
506
|
+
visit(canonicalDependency(projectDir, file, dependency));
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
visiting.delete(file);
|
|
510
|
+
visited.add(file);
|
|
511
|
+
};
|
|
512
|
+
visit(sourceAbs);
|
|
513
|
+
}
|
|
514
|
+
function numberAttribute(element, name, fallback = 0) {
|
|
515
|
+
const value = Number.parseFloat(element.getAttribute(name) ?? "");
|
|
516
|
+
return Number.isFinite(value) ? value : fallback;
|
|
517
|
+
}
|
|
518
|
+
function rangesOverlap(start, duration, other) {
|
|
519
|
+
const otherStart = numberAttribute(other, "data-start");
|
|
520
|
+
const otherDuration = numberAttribute(other, "data-duration");
|
|
521
|
+
return start < otherStart + otherDuration && otherStart < start + duration;
|
|
522
|
+
}
|
|
523
|
+
function resolveTrack(root, desiredTrack, start, duration) {
|
|
524
|
+
const clips = descendants(root, "[data-start][data-duration]").filter(
|
|
525
|
+
(element) => element !== root && element.parentElement?.closest("[data-composition-id]") === root
|
|
526
|
+
);
|
|
527
|
+
const tracks = [...new Set(clips.map((clip) => numberAttribute(clip, "data-track-index")))].sort(
|
|
528
|
+
(a, b) => a - b
|
|
529
|
+
);
|
|
530
|
+
const isFree = (track) => !clips.some(
|
|
531
|
+
(clip) => numberAttribute(clip, "data-track-index") === track && rangesOverlap(start, duration, clip)
|
|
532
|
+
);
|
|
533
|
+
if (isFree(desiredTrack)) return desiredTrack;
|
|
534
|
+
const row = tracks.indexOf(desiredTrack);
|
|
535
|
+
for (let index = row - 1; index >= 0; index--) {
|
|
536
|
+
const track = tracks[index];
|
|
537
|
+
if (track !== void 0 && isFree(track)) return track;
|
|
538
|
+
}
|
|
539
|
+
for (let index = Math.max(0, row + 1); index < tracks.length; index++) {
|
|
540
|
+
const track = tracks[index];
|
|
541
|
+
if (track !== void 0 && isFree(track)) return track;
|
|
542
|
+
}
|
|
543
|
+
return Math.max(desiredTrack, ...tracks, -1) + 1;
|
|
544
|
+
}
|
|
545
|
+
function uniqueHostId(root, base) {
|
|
546
|
+
const ids = /* @__PURE__ */ new Set([
|
|
547
|
+
...descendants(root, "[id]").map((element) => element.id),
|
|
548
|
+
...descendants(root, "[data-composition-id]").flatMap((element) => {
|
|
549
|
+
const id = element.getAttribute("data-composition-id");
|
|
550
|
+
return id ? [id] : [];
|
|
551
|
+
})
|
|
552
|
+
]);
|
|
553
|
+
if (!ids.has(base)) return base;
|
|
554
|
+
let suffix = 2;
|
|
555
|
+
while (ids.has(`${base}_${suffix}`)) suffix += 1;
|
|
556
|
+
return `${base}_${suffix}`;
|
|
557
|
+
}
|
|
558
|
+
function relativeSourcePath(targetAbs, sourceAbs) {
|
|
559
|
+
return relative(dirname(targetAbs), sourceAbs).split(sep).join("/");
|
|
560
|
+
}
|
|
561
|
+
function insertCompositionIntoSource(input) {
|
|
562
|
+
const targetAbs = canonicalProjectFile(input.projectDir, input.targetPath);
|
|
563
|
+
const sourceAbs = canonicalProjectFile(input.projectDir, input.sourcePath);
|
|
564
|
+
validateDependencyGraph(input.projectDir, targetAbs, sourceAbs);
|
|
565
|
+
const source = readCompositionSource(sourceAbs);
|
|
566
|
+
const sourceComposition = compositionRoot(source).root;
|
|
567
|
+
const duration = positiveAttribute(
|
|
568
|
+
sourceComposition,
|
|
569
|
+
"data-composition-duration",
|
|
570
|
+
"data-duration"
|
|
571
|
+
);
|
|
572
|
+
const width = positiveAttribute(sourceComposition, "data-width");
|
|
573
|
+
const height = positiveAttribute(sourceComposition, "data-height");
|
|
574
|
+
const { document, root } = compositionRoot(input.parentSource);
|
|
575
|
+
const parentDuration = positiveAttribute(root, "data-duration", "data-composition-duration");
|
|
576
|
+
const base = (sourceComposition.getAttribute("data-composition-id") ?? "composition").replace(/[^a-zA-Z0-9_-]+/g, "-").replace(/^-+|-+$/g, "") || "composition";
|
|
577
|
+
const hostId = uniqueHostId(root, base);
|
|
578
|
+
const track = resolveTrack(
|
|
579
|
+
root,
|
|
580
|
+
Math.max(0, Math.round(input.desiredTrack)),
|
|
581
|
+
input.start,
|
|
582
|
+
duration
|
|
583
|
+
);
|
|
584
|
+
const zIndex = Math.max(
|
|
585
|
+
0,
|
|
586
|
+
...descendants(root, "[style]").map((element) => {
|
|
587
|
+
const match = /(?:^|;)\s*z-index\s*:\s*(-?\d+)/i.exec(element.getAttribute("style") ?? "");
|
|
588
|
+
return match?.[1] ? Number.parseInt(match[1], 10) : 0;
|
|
589
|
+
})
|
|
590
|
+
) + 1;
|
|
591
|
+
const host = document.createElement("div");
|
|
592
|
+
host.id = hostId;
|
|
593
|
+
host.className = "clip";
|
|
594
|
+
host.setAttribute("data-hf-id", `hf-${randomUUID()}`);
|
|
595
|
+
host.setAttribute("data-composition-id", hostId);
|
|
596
|
+
host.setAttribute("data-composition-src", relativeSourcePath(targetAbs, sourceAbs));
|
|
597
|
+
host.setAttribute("data-start", String(Math.round(input.start * 100) / 100));
|
|
598
|
+
host.setAttribute("data-duration", String(duration));
|
|
599
|
+
host.setAttribute("data-playback-start", "0");
|
|
600
|
+
host.setAttribute("data-track-index", String(track));
|
|
601
|
+
host.setAttribute("data-width", String(width));
|
|
602
|
+
host.setAttribute("data-height", String(height));
|
|
603
|
+
host.setAttribute(
|
|
604
|
+
"style",
|
|
605
|
+
`position: absolute; left: 0px; top: 0px; width: ${width}px; height: ${height}px; z-index: ${zIndex}`
|
|
606
|
+
);
|
|
607
|
+
root.appendChild(host);
|
|
608
|
+
if (input.start + duration > parentDuration) {
|
|
609
|
+
const name = root.hasAttribute("data-duration") ? "data-duration" : "data-composition-duration";
|
|
610
|
+
root.setAttribute(name, String(Math.round((input.start + duration) * 100) / 100));
|
|
611
|
+
}
|
|
612
|
+
return { html: document.toString(), hostId, track, duration };
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// src/helpers/compositionInputs.ts
|
|
616
|
+
var ROOT_COMPOSITION = "index.html";
|
|
617
|
+
var COMPOSITION_SRC = /\bdata-composition-src\s*=\s*(["'])(.*?)\1/g;
|
|
618
|
+
function projectReader(projectDir) {
|
|
619
|
+
const sources = /* @__PURE__ */ new Map();
|
|
620
|
+
return (compPath) => {
|
|
621
|
+
if (!sources.has(compPath)) {
|
|
622
|
+
const file = resolveWithinProject(projectDir, compPath);
|
|
623
|
+
let source = null;
|
|
624
|
+
try {
|
|
625
|
+
source = file ? readFileSync2(file, "utf-8") : null;
|
|
626
|
+
} catch {
|
|
627
|
+
}
|
|
628
|
+
sources.set(compPath, source);
|
|
629
|
+
}
|
|
630
|
+
return sources.get(compPath) ?? null;
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
function normalizeSource(src) {
|
|
634
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(src) || src.startsWith("//")) return null;
|
|
635
|
+
const path = src.split(/[?#]/)[0]?.replace(/^\.?\//, "") ?? "";
|
|
636
|
+
return path || null;
|
|
637
|
+
}
|
|
638
|
+
function closureOf(read, compPath) {
|
|
639
|
+
const closure = /* @__PURE__ */ new Set();
|
|
640
|
+
const visit = (path) => {
|
|
641
|
+
if (closure.has(path)) return;
|
|
642
|
+
closure.add(path);
|
|
643
|
+
for (const match of (read(path) ?? "").matchAll(COMPOSITION_SRC)) {
|
|
644
|
+
const mounted = normalizeSource(match[2] ?? "");
|
|
645
|
+
if (mounted) visit(mounted);
|
|
646
|
+
}
|
|
647
|
+
};
|
|
648
|
+
visit(compPath);
|
|
649
|
+
return closure;
|
|
650
|
+
}
|
|
651
|
+
function lengthIsTimeline(projectDir, compPath) {
|
|
652
|
+
const read = projectReader(projectDir);
|
|
653
|
+
const documents = /* @__PURE__ */ new Map();
|
|
654
|
+
const elements = (path, selector) => {
|
|
655
|
+
let document = documents.get(path);
|
|
656
|
+
if (!document) documents.set(path, document = parseHTML3(read(path) ?? "").document);
|
|
657
|
+
return descendants(document, selector);
|
|
658
|
+
};
|
|
659
|
+
const comp = normalizeSource(compPath) ?? compPath;
|
|
660
|
+
const unsized = (path) => {
|
|
661
|
+
const [root] = elements(path, "[data-composition-id]");
|
|
662
|
+
return Boolean(root && !root.hasAttribute("data-duration"));
|
|
663
|
+
};
|
|
664
|
+
if (unsized(ROOT_COMPOSITION)) return true;
|
|
665
|
+
const hosts = [...closureOf(read, ROOT_COMPOSITION)].flatMap(
|
|
666
|
+
(path) => elements(path, "[data-composition-src]").filter(
|
|
667
|
+
(host) => normalizeSource(host.getAttribute("data-composition-src") ?? "") === comp
|
|
668
|
+
)
|
|
669
|
+
);
|
|
670
|
+
if (comp === ROOT_COMPOSITION || hosts.length === 0) return unsized(comp);
|
|
671
|
+
return hosts.some(
|
|
672
|
+
(host) => !host.hasAttribute("data-duration") && !host.hasAttribute("data-end")
|
|
673
|
+
);
|
|
674
|
+
}
|
|
675
|
+
var inputSignatures = /* @__PURE__ */ new Map();
|
|
676
|
+
var rootMemos = /* @__PURE__ */ new Map();
|
|
677
|
+
var sha1 = (text2) => createHash("sha1").update(text2).digest("hex");
|
|
678
|
+
function rootHashes(read) {
|
|
679
|
+
const source = read(ROOT_COMPOSITION) ?? "";
|
|
680
|
+
return { root: sha1(source), head: sha1(rootHeadContent(source)) };
|
|
681
|
+
}
|
|
682
|
+
function compositionInputSignature(projectDir, compPath, projectSignature) {
|
|
683
|
+
const key = `${projectDir}\0${compPath}`;
|
|
684
|
+
const known = inputSignatures.get(key);
|
|
685
|
+
if (known?.projectSignature === projectSignature) return known.inputSignature;
|
|
686
|
+
const read = projectReader(projectDir);
|
|
687
|
+
const comp = normalizeSource(compPath) ?? compPath;
|
|
688
|
+
const inputs = closureOf(read, comp).add(ROOT_COMPOSITION);
|
|
689
|
+
const excluded = new Set(
|
|
690
|
+
[...closureOf(read, ROOT_COMPOSITION)].filter((path) => !inputs.has(path))
|
|
691
|
+
);
|
|
692
|
+
let inputSignature;
|
|
693
|
+
if (comp === ROOT_COMPOSITION) {
|
|
694
|
+
inputSignature = createProjectSignature(projectDir, excluded);
|
|
695
|
+
} else {
|
|
696
|
+
const hashes = rootHashes(read);
|
|
697
|
+
if (!rootMemos.has(projectDir)) rootMemos.set(projectDir, { ...hashes, affected: null });
|
|
698
|
+
inputSignature = `${createProjectSignature(projectDir, excluded.add(ROOT_COMPOSITION))}:${hashes.head}`;
|
|
699
|
+
}
|
|
700
|
+
inputSignatures.set(key, { projectSignature, inputSignature });
|
|
701
|
+
return inputSignature;
|
|
702
|
+
}
|
|
703
|
+
function compositionsAffectedBy(projectDir, changedPath) {
|
|
704
|
+
const changed = changedPath.replace(/\\/g, "/");
|
|
705
|
+
const read = projectReader(projectDir);
|
|
706
|
+
if (changed === ROOT_COMPOSITION) {
|
|
707
|
+
const now = rootHashes(read);
|
|
708
|
+
const memo = rootMemos.get(projectDir);
|
|
709
|
+
if (memo?.root === now.root) return memo.affected;
|
|
710
|
+
const affected = memo?.head === now.head ? [ROOT_COMPOSITION] : null;
|
|
711
|
+
rootMemos.set(projectDir, { ...now, affected });
|
|
712
|
+
return affected;
|
|
713
|
+
}
|
|
714
|
+
const mounted = closureOf(read, ROOT_COMPOSITION);
|
|
715
|
+
if (!mounted.has(changed)) return null;
|
|
716
|
+
return [...mounted].filter((path) => closureOf(read, path).has(changed));
|
|
221
717
|
}
|
|
222
718
|
|
|
223
719
|
// src/routes/files.ts
|
|
@@ -226,7 +722,7 @@ import { createFileAtomically, replaceFileAtomically as replaceFileAtomically2 }
|
|
|
226
722
|
// src/helpers/waveform.ts
|
|
227
723
|
import { spawn } from "child_process";
|
|
228
724
|
import {
|
|
229
|
-
existsSync,
|
|
725
|
+
existsSync as existsSync3,
|
|
230
726
|
writeFileSync,
|
|
231
727
|
mkdirSync,
|
|
232
728
|
lstatSync,
|
|
@@ -235,7 +731,7 @@ import {
|
|
|
235
731
|
rmSync,
|
|
236
732
|
statSync
|
|
237
733
|
} from "fs";
|
|
238
|
-
import { dirname, join as
|
|
734
|
+
import { dirname as dirname2, join as join3 } from "path";
|
|
239
735
|
import { findFfBinary } from "@hyperframes/parsers/ff-binaries";
|
|
240
736
|
var SAMPLE_RATE = 4e3;
|
|
241
737
|
var PEAK_COUNT = 4e3;
|
|
@@ -302,12 +798,12 @@ function decodeAudioPeaks(audioPath) {
|
|
|
302
798
|
});
|
|
303
799
|
}
|
|
304
800
|
async function generateWaveformCache(projectDir, assetPath) {
|
|
305
|
-
const audioPath =
|
|
306
|
-
if (!
|
|
801
|
+
const audioPath = join3(projectDir, assetPath);
|
|
802
|
+
if (!existsSync3(audioPath)) return;
|
|
307
803
|
const stats = statSync(audioPath);
|
|
308
|
-
const cacheDir =
|
|
309
|
-
const cachePath =
|
|
310
|
-
if (isWaveformCacheDirectory(cacheDir) &&
|
|
804
|
+
const cacheDir = join3(projectDir, ".waveform-cache");
|
|
805
|
+
const cachePath = join3(cacheDir, buildWaveformCacheKey(assetPath, stats));
|
|
806
|
+
if (isWaveformCacheDirectory(cacheDir) && existsSync3(cachePath)) return;
|
|
311
807
|
const peaks = await decodeAudioPeaks(audioPath);
|
|
312
808
|
writeWaveformCache(cachePath, peaks);
|
|
313
809
|
}
|
|
@@ -315,7 +811,7 @@ function isWaveformCacheDirectory(cacheDir) {
|
|
|
315
811
|
return lstatSync(cacheDir, { throwIfNoEntry: false })?.isDirectory() ?? false;
|
|
316
812
|
}
|
|
317
813
|
function writeWaveformCache(cachePath, peaks) {
|
|
318
|
-
const cacheDir =
|
|
814
|
+
const cacheDir = dirname2(cachePath);
|
|
319
815
|
try {
|
|
320
816
|
mkdirSync(cacheDir, { mode: 448 });
|
|
321
817
|
} catch (error) {
|
|
@@ -324,9 +820,9 @@ function writeWaveformCache(cachePath, peaks) {
|
|
|
324
820
|
if (!isWaveformCacheDirectory(cacheDir)) {
|
|
325
821
|
throw new Error("Waveform cache must be a directory, not a symlink");
|
|
326
822
|
}
|
|
327
|
-
const stagingDir = mkdtempSync(
|
|
823
|
+
const stagingDir = mkdtempSync(join3(cacheDir, ".waveform-"));
|
|
328
824
|
try {
|
|
329
|
-
const stagingPath =
|
|
825
|
+
const stagingPath = join3(stagingDir, "peaks.json");
|
|
330
826
|
writeFileSync(stagingPath, JSON.stringify(peaks), { flag: "wx" });
|
|
331
827
|
renameSync(stagingPath, cachePath);
|
|
332
828
|
} finally {
|
|
@@ -341,7 +837,7 @@ function writeWaveformCache(cachePath, peaks) {
|
|
|
341
837
|
import { spawnSync } from "child_process";
|
|
342
838
|
import { mkdtempSync as mkdtempSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
343
839
|
import { tmpdir } from "os";
|
|
344
|
-
import { basename, join as
|
|
840
|
+
import { basename, join as join4 } from "path";
|
|
345
841
|
import { FFPROBE_PATH_ENV, findFfBinary as findFfBinary2 } from "@hyperframes/parsers/ff-binaries";
|
|
346
842
|
var VIDEO_EXT = /\.(mp4|webm|mov|mkv|avi|m4v|mxf|mts|m2ts|ts)$/i;
|
|
347
843
|
var AUDIO_EXT = /\.(mp3|wav|ogg|m4a|aac|flac)$/i;
|
|
@@ -392,8 +888,8 @@ function checkStreams(stdout, isVideo, isAudio) {
|
|
|
392
888
|
}
|
|
393
889
|
}
|
|
394
890
|
function validateUploadedMediaBuffer(fileName, buffer, runner = defaultRunner) {
|
|
395
|
-
const tempDir = mkdtempSync2(
|
|
396
|
-
const tempPath =
|
|
891
|
+
const tempDir = mkdtempSync2(join4(tmpdir(), "hyperframes-upload-"));
|
|
892
|
+
const tempPath = join4(tempDir, basename(fileName));
|
|
397
893
|
try {
|
|
398
894
|
writeFileSync2(tempPath, buffer);
|
|
399
895
|
return validateUploadedMedia(tempPath, runner);
|
|
@@ -403,9 +899,9 @@ function validateUploadedMediaBuffer(fileName, buffer, runner = defaultRunner) {
|
|
|
403
899
|
}
|
|
404
900
|
|
|
405
901
|
// src/helpers/backupJournal.ts
|
|
406
|
-
import { readdirSync, readFileSync, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
|
|
902
|
+
import { readdirSync, readFileSync as readFileSync3, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
|
|
407
903
|
import { Buffer as Buffer2 } from "buffer";
|
|
408
|
-
import { join as
|
|
904
|
+
import { join as join5, relative as relative2 } from "path";
|
|
409
905
|
var DEFAULT_KEEP_PER_FILE = 10;
|
|
410
906
|
function backupKeyForPath(path) {
|
|
411
907
|
return Buffer2.from(path, "utf-8").toString("base64url");
|
|
@@ -415,16 +911,16 @@ function timestampPrefix() {
|
|
|
415
911
|
}
|
|
416
912
|
function backupPathForResponse(projectDir, backupPath) {
|
|
417
913
|
if (!backupPath) return null;
|
|
418
|
-
const rel =
|
|
914
|
+
const rel = relative2(projectDir, backupPath);
|
|
419
915
|
if (!rel || rel.startsWith("..")) return null;
|
|
420
916
|
return rel.split("\\").join("/");
|
|
421
917
|
}
|
|
422
918
|
function snapshotBeforeWrite(projectDir, absPath, options = {}) {
|
|
423
919
|
if (!isSafePath(projectDir, absPath)) return { backupPath: null };
|
|
424
920
|
try {
|
|
425
|
-
const content =
|
|
426
|
-
const relativePath =
|
|
427
|
-
const backupDir =
|
|
921
|
+
const content = readFileSync3(absPath);
|
|
922
|
+
const relativePath = relative2(projectDir, absPath);
|
|
923
|
+
const backupDir = join5(projectDir, ".hyperframes", "backup");
|
|
428
924
|
mkdirWithinProject(projectDir, backupDir);
|
|
429
925
|
const backupKey = backupKeyForPath(relativePath);
|
|
430
926
|
const backupPath = nextBackupPath(backupDir, backupKey);
|
|
@@ -441,18 +937,18 @@ function snapshotBeforeWrite(projectDir, absPath, options = {}) {
|
|
|
441
937
|
}
|
|
442
938
|
function nextBackupPath(backupDir, backupKey) {
|
|
443
939
|
const base = `${timestampPrefix()}-${backupKey}`;
|
|
444
|
-
let candidate =
|
|
940
|
+
let candidate = join5(backupDir, base);
|
|
445
941
|
let counter = 2;
|
|
446
942
|
while (true) {
|
|
447
943
|
try {
|
|
448
|
-
|
|
944
|
+
readFileSync3(candidate);
|
|
449
945
|
} catch (error) {
|
|
450
946
|
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
|
|
451
947
|
return candidate;
|
|
452
948
|
}
|
|
453
949
|
throw error;
|
|
454
950
|
}
|
|
455
|
-
candidate =
|
|
951
|
+
candidate = join5(backupDir, `${base}-${counter}`);
|
|
456
952
|
counter += 1;
|
|
457
953
|
}
|
|
458
954
|
}
|
|
@@ -460,7 +956,7 @@ function pruneBackups(backupDir, backupKey, keepPerFile) {
|
|
|
460
956
|
const keep = Math.max(1, Math.floor(keepPerFile));
|
|
461
957
|
const suffix = `-${backupKey}`;
|
|
462
958
|
const numberedSuffix = new RegExp(`-${backupKey}-\\d+$`);
|
|
463
|
-
const matches = readdirSync(backupDir).filter((name) => name.endsWith(suffix) || numberedSuffix.test(name)).map((name) =>
|
|
959
|
+
const matches = readdirSync(backupDir).filter((name) => name.endsWith(suffix) || numberedSuffix.test(name)).map((name) => join5(backupDir, name)).sort((a, b) => {
|
|
464
960
|
return b.localeCompare(a);
|
|
465
961
|
});
|
|
466
962
|
for (const file of matches.slice(keep)) {
|
|
@@ -481,17 +977,17 @@ function replaceWithProjectDirMissing(c, hostHeaders) {
|
|
|
481
977
|
}
|
|
482
978
|
|
|
483
979
|
// src/helpers/applyFileMutations.ts
|
|
484
|
-
import { readFileSync as
|
|
980
|
+
import { readFileSync as readFileSync4, statSync as statSync2 } from "fs";
|
|
485
981
|
import { replaceFileAtomically } from "@hyperframes/core/atomic-file";
|
|
486
982
|
function applyFileMutations(projectDir, mutations, requestToken, writeFile = (path, content) => replaceFileAtomically(path, content, statSync2(path).mode)) {
|
|
487
983
|
const prepared = mutations.map((mutation) => ({
|
|
488
984
|
...mutation,
|
|
489
|
-
before: mutation.before ??
|
|
985
|
+
before: mutation.before ?? readFileSync4(mutation.absPath, "utf-8")
|
|
490
986
|
}));
|
|
491
987
|
const results = [];
|
|
492
988
|
const attempted = [];
|
|
493
989
|
for (const mutation of prepared)
|
|
494
|
-
assertExpectedVersion(mutation,
|
|
990
|
+
assertExpectedVersion(mutation, readFileSync4(mutation.absPath, "utf-8"));
|
|
495
991
|
try {
|
|
496
992
|
for (const mutation of prepared) {
|
|
497
993
|
results.push(applyOneMutation(projectDir, mutation, requestToken, writeFile, attempted));
|
|
@@ -501,7 +997,7 @@ function applyFileMutations(projectDir, mutations, requestToken, writeFile = (pa
|
|
|
501
997
|
const rollbackErrors = [];
|
|
502
998
|
for (const mutation of attempted.reverse()) {
|
|
503
999
|
try {
|
|
504
|
-
if (!mutation.written ||
|
|
1000
|
+
if (!mutation.written || readFileSync4(mutation.absPath, "utf-8") === mutation.after)
|
|
505
1001
|
writeFile(mutation.absPath, mutation.before, "utf-8");
|
|
506
1002
|
clearFileWriteReceipt(mutation.absPath, mutation.version, mutation.writeToken);
|
|
507
1003
|
} catch (rollbackError) {
|
|
@@ -518,7 +1014,7 @@ function applyFileMutations(projectDir, mutations, requestToken, writeFile = (pa
|
|
|
518
1014
|
}
|
|
519
1015
|
}
|
|
520
1016
|
function applyOneMutation(projectDir, mutation, requestToken, writeFile, attempted) {
|
|
521
|
-
const current =
|
|
1017
|
+
const current = readFileSync4(mutation.absPath, "utf-8");
|
|
522
1018
|
assertExpectedVersion(mutation, current);
|
|
523
1019
|
if (mutation.after === mutation.before) {
|
|
524
1020
|
return {
|
|
@@ -532,7 +1028,7 @@ function applyOneMutation(projectDir, mutation, requestToken, writeFile, attempt
|
|
|
532
1028
|
}
|
|
533
1029
|
const backup = snapshotBeforeWrite(projectDir, mutation.absPath);
|
|
534
1030
|
if (backup.error) throw new Error(`backup failed: ${backup.error}`);
|
|
535
|
-
assertExpectedVersion(mutation,
|
|
1031
|
+
assertExpectedVersion(mutation, readFileSync4(mutation.absPath, "utf-8"));
|
|
536
1032
|
const before = current;
|
|
537
1033
|
const version = fileContentVersion(mutation.after);
|
|
538
1034
|
const writeToken = createWriteToken(requestToken);
|
|
@@ -541,295 +1037,96 @@ function applyOneMutation(projectDir, mutation, requestToken, writeFile, attempt
|
|
|
541
1037
|
writeFile(mutation.absPath, mutation.after, "utf-8");
|
|
542
1038
|
attempt.written = true;
|
|
543
1039
|
recordFileWriteReceipt(mutation.absPath, {
|
|
544
|
-
path: mutation.sourceFile,
|
|
545
|
-
version,
|
|
546
|
-
writeToken,
|
|
547
|
-
overwrote: before
|
|
548
|
-
});
|
|
549
|
-
return {
|
|
550
|
-
...mutation,
|
|
551
|
-
before,
|
|
552
|
-
changed: true,
|
|
553
|
-
backupPath: backupPathForResponse(projectDir, backup.backupPath),
|
|
554
|
-
version,
|
|
555
|
-
writeToken
|
|
556
|
-
};
|
|
557
|
-
}
|
|
558
|
-
var FileChangedError = class extends Error {
|
|
559
|
-
constructor(sourceFile) {
|
|
560
|
-
super("file changed since the timeline was read");
|
|
561
|
-
this.sourceFile = sourceFile;
|
|
562
|
-
}
|
|
563
|
-
sourceFile;
|
|
564
|
-
};
|
|
565
|
-
function assertExpectedVersion(mutation, current) {
|
|
566
|
-
const stale = mutation.expectedVersion === void 0 ? current !== mutation.before : fileContentVersion(current) !== mutation.expectedVersion;
|
|
567
|
-
if (stale) throw new FileChangedError(mutation.sourceFile);
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
// src/routes/files.ts
|
|
571
|
-
import { classifyPropertyGroup } from "@hyperframes/parsers/gsap-constants";
|
|
572
|
-
import { findTimelineScript, parseGsapScriptAcorn } from "@hyperframes/parsers/gsap-parser-acorn";
|
|
573
|
-
import { unrollComputedTimeline } from "@hyperframes/parsers";
|
|
574
|
-
import {
|
|
575
|
-
updateAnimationInScript,
|
|
576
|
-
addAnimationToScript,
|
|
577
|
-
removeAnimationFromScript,
|
|
578
|
-
addKeyframeToScript,
|
|
579
|
-
removeKeyframeFromScript,
|
|
580
|
-
moveKeyframeInScript,
|
|
581
|
-
resizeKeyframedTweenInScript,
|
|
582
|
-
updateKeyframeInScript,
|
|
583
|
-
convertToKeyframesFromScript,
|
|
584
|
-
removeAllKeyframesFromScript,
|
|
585
|
-
materializeKeyframesFromScript,
|
|
586
|
-
unrollDynamicAnimations,
|
|
587
|
-
setArcPathInScript,
|
|
588
|
-
updateArcSegmentInScript,
|
|
589
|
-
updateMotionPathPointInScript,
|
|
590
|
-
addMotionPathPointInScript,
|
|
591
|
-
removeMotionPathPointInScript,
|
|
592
|
-
addMotionPathToScript,
|
|
593
|
-
removeArcPathFromScript,
|
|
594
|
-
addAnimationWithKeyframesToScript,
|
|
595
|
-
replaceTweenWithKeyframesInScript,
|
|
596
|
-
splitAnimationsInScript,
|
|
597
|
-
splitIntoPropertyGroupsFromScript,
|
|
598
|
-
retimeClipTweensInScript,
|
|
599
|
-
dedupePositionWritesInScript,
|
|
600
|
-
syncPositionHoldsBeforeKeyframes,
|
|
601
|
-
clipQueryRoot
|
|
602
|
-
} from "@hyperframes/parsers/gsap-writer-acorn";
|
|
603
|
-
|
|
604
|
-
// src/helpers/studioFontFace.ts
|
|
605
|
-
var FONT_STYLE_RE = /<style\b[^>]*data-hf-studio-fonts=(["'])true\1[^>]*>([\s\S]*?)<\/style>/i;
|
|
606
|
-
var ONE_FONT_FACE_RULE = /^@font-face \{(?:[^{}<"'\\]|"(?:[^"\\<\n\r\f]|\\[^<\n\r\f])*"|'(?:[^'\\<\n\r\f]|\\[^<\n\r\f])*')*\}$/;
|
|
607
|
-
function isStudioFontFaceCss(css) {
|
|
608
|
-
return typeof css === "string" && ONE_FONT_FACE_RULE.test(css);
|
|
609
|
-
}
|
|
610
|
-
function ensureStudioFontFaceCss(html, css) {
|
|
611
|
-
if (html.includes(css)) return html;
|
|
612
|
-
const block = FONT_STYLE_RE.exec(html);
|
|
613
|
-
if (block) {
|
|
614
|
-
const next = `${(block[2] ?? "").trim()}
|
|
615
|
-
${css}`.trim();
|
|
616
|
-
return html.replace(block[0], () => `<style data-hf-studio-fonts="true">
|
|
617
|
-
${next}
|
|
618
|
-
</style>`);
|
|
619
|
-
}
|
|
620
|
-
const styleTag = `<style data-hf-studio-fonts="true">
|
|
621
|
-
${css}
|
|
622
|
-
</style>`;
|
|
623
|
-
if (/<\/head>/i.test(html)) return html.replace(/<\/head>/i, () => ` ${styleTag}
|
|
624
|
-
</head>`);
|
|
625
|
-
return `${styleTag}
|
|
626
|
-
${html}`;
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
// src/routes/files.ts
|
|
630
|
-
import { parseHTML as parseHTML2 } from "linkedom";
|
|
631
|
-
import { ensureHfIds } from "@hyperframes/parsers/hf-ids";
|
|
632
|
-
|
|
633
|
-
// src/helpers/compositionInsertion.ts
|
|
634
|
-
import { existsSync as existsSync2 } from "fs";
|
|
635
|
-
import { readProjectFile } from "@hyperframes/parsers/asset-resolution";
|
|
636
|
-
import { randomUUID } from "crypto";
|
|
637
|
-
import { dirname as dirname2, relative as relative2, resolve, sep } from "path";
|
|
638
|
-
import { parseHTML } from "linkedom";
|
|
639
|
-
import { ProjectRootMissingError } from "@hyperframes/core";
|
|
640
|
-
var CompositionInsertionError = class extends Error {
|
|
641
|
-
constructor(message, status) {
|
|
642
|
-
super(message);
|
|
643
|
-
this.status = status;
|
|
644
|
-
}
|
|
645
|
-
status;
|
|
646
|
-
};
|
|
647
|
-
function descendants(root, selector) {
|
|
648
|
-
const found = Array.from(root.querySelectorAll(selector));
|
|
649
|
-
for (const template of root.querySelectorAll("template")) {
|
|
650
|
-
found.push(...descendants(template, selector));
|
|
651
|
-
}
|
|
652
|
-
return [...new Set(found)];
|
|
653
|
-
}
|
|
654
|
-
function compositionRoot(source) {
|
|
655
|
-
const document = parseHTML(source).document;
|
|
656
|
-
const root = descendants(document, "[data-composition-id]")[0];
|
|
657
|
-
if (!root) throw new CompositionInsertionError("Composition source has no root", 400);
|
|
658
|
-
return { document, root };
|
|
659
|
-
}
|
|
660
|
-
function positiveAttribute(root, ...names) {
|
|
661
|
-
for (const name of names) {
|
|
662
|
-
const value = Number.parseFloat(root.getAttribute(name) ?? "");
|
|
663
|
-
if (Number.isFinite(value) && value > 0) return value;
|
|
664
|
-
}
|
|
665
|
-
throw new CompositionInsertionError(`Composition source has no valid ${names[0]}`, 400);
|
|
666
|
-
}
|
|
667
|
-
function canonicalProjectPath(projectDir, candidate) {
|
|
668
|
-
if (!candidate) {
|
|
669
|
-
if (folderGone(projectDir)) throw new ProjectRootMissingError(projectDir);
|
|
670
|
-
throw new CompositionInsertionError("Composition source escapes the project", 400);
|
|
671
|
-
}
|
|
672
|
-
if (!existsSync2(candidate)) {
|
|
673
|
-
throw new CompositionInsertionError("Composition source was not found", 404);
|
|
674
|
-
}
|
|
675
|
-
const canonical = realpath(candidate);
|
|
676
|
-
if (!isSafePath(realpath(projectDir), canonical)) {
|
|
677
|
-
throw new CompositionInsertionError("Composition source escapes the project", 400);
|
|
678
|
-
}
|
|
679
|
-
return canonical;
|
|
680
|
-
}
|
|
681
|
-
function readCompositionSource(file) {
|
|
682
|
-
const read = readProjectFile(file);
|
|
683
|
-
if (read.kind === "folder") {
|
|
684
|
-
throw new CompositionInsertionError("Composition source is a folder, not an HTML file", 400);
|
|
685
|
-
}
|
|
686
|
-
if (read.kind === "missing") {
|
|
687
|
-
throw new CompositionInsertionError("Composition source was not found", 404);
|
|
688
|
-
}
|
|
689
|
-
return read.text;
|
|
690
|
-
}
|
|
691
|
-
function validateSourcePath(sourcePath) {
|
|
692
|
-
if (!sourcePath.trim() || sourcePath.includes("\0") || /^[a-z]+:/i.test(sourcePath)) {
|
|
693
|
-
throw new CompositionInsertionError("Invalid composition source path", 400);
|
|
694
|
-
}
|
|
695
|
-
}
|
|
696
|
-
function canonicalProjectFile(projectDir, sourcePath) {
|
|
697
|
-
validateSourcePath(sourcePath);
|
|
698
|
-
return canonicalProjectPath(projectDir, resolveWithinProject(projectDir, sourcePath));
|
|
699
|
-
}
|
|
700
|
-
function canonicalDependency(projectDir, ownerAbs, sourcePath) {
|
|
701
|
-
validateSourcePath(sourcePath);
|
|
702
|
-
return canonicalProjectPath(
|
|
703
|
-
projectDir,
|
|
704
|
-
resolveWithinProject(projectDir, relative2(projectDir, resolve(dirname2(ownerAbs), sourcePath)))
|
|
705
|
-
);
|
|
706
|
-
}
|
|
707
|
-
function validateDependencyGraph(projectDir, targetAbs, sourceAbs) {
|
|
708
|
-
const visited = /* @__PURE__ */ new Set();
|
|
709
|
-
const visiting = /* @__PURE__ */ new Set();
|
|
710
|
-
const visit = (file) => {
|
|
711
|
-
if (file === targetAbs) {
|
|
712
|
-
throw new CompositionInsertionError("Composition insertion would create a cycle", 400);
|
|
713
|
-
}
|
|
714
|
-
if (visiting.has(file)) {
|
|
715
|
-
throw new CompositionInsertionError("Composition dependency cycle detected", 400);
|
|
716
|
-
}
|
|
717
|
-
if (visited.has(file)) return;
|
|
718
|
-
visiting.add(file);
|
|
719
|
-
const source = readCompositionSource(file);
|
|
720
|
-
const { document } = compositionRoot(source);
|
|
721
|
-
for (const host of descendants(document, "[data-composition-src]")) {
|
|
722
|
-
const dependency = host.getAttribute("data-composition-src");
|
|
723
|
-
if (dependency) {
|
|
724
|
-
visit(canonicalDependency(projectDir, file, dependency));
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
|
-
visiting.delete(file);
|
|
728
|
-
visited.add(file);
|
|
729
|
-
};
|
|
730
|
-
visit(sourceAbs);
|
|
731
|
-
}
|
|
732
|
-
function numberAttribute(element, name, fallback = 0) {
|
|
733
|
-
const value = Number.parseFloat(element.getAttribute(name) ?? "");
|
|
734
|
-
return Number.isFinite(value) ? value : fallback;
|
|
735
|
-
}
|
|
736
|
-
function rangesOverlap(start, duration, other) {
|
|
737
|
-
const otherStart = numberAttribute(other, "data-start");
|
|
738
|
-
const otherDuration = numberAttribute(other, "data-duration");
|
|
739
|
-
return start < otherStart + otherDuration && otherStart < start + duration;
|
|
740
|
-
}
|
|
741
|
-
function resolveTrack(root, desiredTrack, start, duration) {
|
|
742
|
-
const clips = descendants(root, "[data-start][data-duration]").filter(
|
|
743
|
-
(element) => element !== root && element.parentElement?.closest("[data-composition-id]") === root
|
|
744
|
-
);
|
|
745
|
-
const tracks = [...new Set(clips.map((clip) => numberAttribute(clip, "data-track-index")))].sort(
|
|
746
|
-
(a, b) => a - b
|
|
747
|
-
);
|
|
748
|
-
const isFree = (track) => !clips.some(
|
|
749
|
-
(clip) => numberAttribute(clip, "data-track-index") === track && rangesOverlap(start, duration, clip)
|
|
750
|
-
);
|
|
751
|
-
if (isFree(desiredTrack)) return desiredTrack;
|
|
752
|
-
const row = tracks.indexOf(desiredTrack);
|
|
753
|
-
for (let index = row - 1; index >= 0; index--) {
|
|
754
|
-
const track = tracks[index];
|
|
755
|
-
if (track !== void 0 && isFree(track)) return track;
|
|
756
|
-
}
|
|
757
|
-
for (let index = Math.max(0, row + 1); index < tracks.length; index++) {
|
|
758
|
-
const track = tracks[index];
|
|
759
|
-
if (track !== void 0 && isFree(track)) return track;
|
|
760
|
-
}
|
|
761
|
-
return Math.max(desiredTrack, ...tracks, -1) + 1;
|
|
1040
|
+
path: mutation.sourceFile,
|
|
1041
|
+
version,
|
|
1042
|
+
writeToken,
|
|
1043
|
+
overwrote: before
|
|
1044
|
+
});
|
|
1045
|
+
return {
|
|
1046
|
+
...mutation,
|
|
1047
|
+
before,
|
|
1048
|
+
changed: true,
|
|
1049
|
+
backupPath: backupPathForResponse(projectDir, backup.backupPath),
|
|
1050
|
+
version,
|
|
1051
|
+
writeToken
|
|
1052
|
+
};
|
|
762
1053
|
}
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
while (ids.has(`${base}_${suffix}`)) suffix += 1;
|
|
774
|
-
return `${base}_${suffix}`;
|
|
1054
|
+
var FileChangedError = class extends Error {
|
|
1055
|
+
constructor(sourceFile) {
|
|
1056
|
+
super("file changed since the timeline was read");
|
|
1057
|
+
this.sourceFile = sourceFile;
|
|
1058
|
+
}
|
|
1059
|
+
sourceFile;
|
|
1060
|
+
};
|
|
1061
|
+
function assertExpectedVersion(mutation, current) {
|
|
1062
|
+
const stale = mutation.expectedVersion === void 0 ? current !== mutation.before : fileContentVersion(current) !== mutation.expectedVersion;
|
|
1063
|
+
if (stale) throw new FileChangedError(mutation.sourceFile);
|
|
775
1064
|
}
|
|
776
|
-
|
|
777
|
-
|
|
1065
|
+
|
|
1066
|
+
// src/routes/files.ts
|
|
1067
|
+
import { classifyPropertyGroup } from "@hyperframes/parsers/gsap-constants";
|
|
1068
|
+
import { findTimelineScript, parseGsapScriptAcorn } from "@hyperframes/parsers/gsap-parser-acorn";
|
|
1069
|
+
import { unrollComputedTimeline } from "@hyperframes/parsers";
|
|
1070
|
+
import {
|
|
1071
|
+
updateAnimationInScript,
|
|
1072
|
+
addAnimationToScript,
|
|
1073
|
+
removeAnimationFromScript,
|
|
1074
|
+
addKeyframeToScript,
|
|
1075
|
+
removeKeyframeFromScript,
|
|
1076
|
+
moveKeyframeInScript,
|
|
1077
|
+
resizeKeyframedTweenInScript,
|
|
1078
|
+
updateKeyframeInScript,
|
|
1079
|
+
convertToKeyframesFromScript,
|
|
1080
|
+
removeAllKeyframesFromScript,
|
|
1081
|
+
materializeKeyframesFromScript,
|
|
1082
|
+
unrollDynamicAnimations,
|
|
1083
|
+
setArcPathInScript,
|
|
1084
|
+
updateArcSegmentInScript,
|
|
1085
|
+
updateMotionPathPointInScript,
|
|
1086
|
+
addMotionPathPointInScript,
|
|
1087
|
+
removeMotionPathPointInScript,
|
|
1088
|
+
addMotionPathToScript,
|
|
1089
|
+
removeArcPathFromScript,
|
|
1090
|
+
addAnimationWithKeyframesToScript,
|
|
1091
|
+
replaceTweenWithKeyframesInScript,
|
|
1092
|
+
splitAnimationsInScript,
|
|
1093
|
+
splitIntoPropertyGroupsFromScript,
|
|
1094
|
+
retimeClipTweensInScript,
|
|
1095
|
+
dedupePositionWritesInScript,
|
|
1096
|
+
syncPositionHoldsBeforeKeyframes,
|
|
1097
|
+
trimTrailingKeyframeSpans,
|
|
1098
|
+
clipQueryRoot
|
|
1099
|
+
} from "@hyperframes/parsers/gsap-writer-acorn";
|
|
1100
|
+
|
|
1101
|
+
// src/helpers/studioFontFace.ts
|
|
1102
|
+
var FONT_STYLE_RE = /<style\b[^>]*data-hf-studio-fonts=(["'])true\1[^>]*>([\s\S]*?)<\/style>/i;
|
|
1103
|
+
var ONE_FONT_FACE_RULE = /^@font-face \{(?:[^{}<"'\\]|"(?:[^"\\<\n\r\f]|\\[^<\n\r\f])*"|'(?:[^'\\<\n\r\f]|\\[^<\n\r\f])*')*\}$/;
|
|
1104
|
+
function isStudioFontFaceCss(css) {
|
|
1105
|
+
return typeof css === "string" && ONE_FONT_FACE_RULE.test(css);
|
|
778
1106
|
}
|
|
779
|
-
function
|
|
780
|
-
|
|
781
|
-
const
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
"data-duration"
|
|
789
|
-
);
|
|
790
|
-
const width = positiveAttribute(sourceComposition, "data-width");
|
|
791
|
-
const height = positiveAttribute(sourceComposition, "data-height");
|
|
792
|
-
const { document, root } = compositionRoot(input.parentSource);
|
|
793
|
-
const parentDuration = positiveAttribute(root, "data-duration", "data-composition-duration");
|
|
794
|
-
const base = (sourceComposition.getAttribute("data-composition-id") ?? "composition").replace(/[^a-zA-Z0-9_-]+/g, "-").replace(/^-+|-+$/g, "") || "composition";
|
|
795
|
-
const hostId = uniqueHostId(root, base);
|
|
796
|
-
const track = resolveTrack(
|
|
797
|
-
root,
|
|
798
|
-
Math.max(0, Math.round(input.desiredTrack)),
|
|
799
|
-
input.start,
|
|
800
|
-
duration
|
|
801
|
-
);
|
|
802
|
-
const zIndex = Math.max(
|
|
803
|
-
0,
|
|
804
|
-
...descendants(root, "[style]").map((element) => {
|
|
805
|
-
const match = /(?:^|;)\s*z-index\s*:\s*(-?\d+)/i.exec(element.getAttribute("style") ?? "");
|
|
806
|
-
return match?.[1] ? Number.parseInt(match[1], 10) : 0;
|
|
807
|
-
})
|
|
808
|
-
) + 1;
|
|
809
|
-
const host = document.createElement("div");
|
|
810
|
-
host.id = hostId;
|
|
811
|
-
host.className = "clip";
|
|
812
|
-
host.setAttribute("data-hf-id", `hf-${randomUUID()}`);
|
|
813
|
-
host.setAttribute("data-composition-id", hostId);
|
|
814
|
-
host.setAttribute("data-composition-src", relativeSourcePath(targetAbs, sourceAbs));
|
|
815
|
-
host.setAttribute("data-start", String(Math.round(input.start * 100) / 100));
|
|
816
|
-
host.setAttribute("data-duration", String(duration));
|
|
817
|
-
host.setAttribute("data-playback-start", "0");
|
|
818
|
-
host.setAttribute("data-track-index", String(track));
|
|
819
|
-
host.setAttribute("data-width", String(width));
|
|
820
|
-
host.setAttribute("data-height", String(height));
|
|
821
|
-
host.setAttribute(
|
|
822
|
-
"style",
|
|
823
|
-
`position: absolute; left: 0px; top: 0px; width: ${width}px; height: ${height}px; z-index: ${zIndex}`
|
|
824
|
-
);
|
|
825
|
-
root.appendChild(host);
|
|
826
|
-
if (input.start + duration > parentDuration) {
|
|
827
|
-
const name = root.hasAttribute("data-duration") ? "data-duration" : "data-composition-duration";
|
|
828
|
-
root.setAttribute(name, String(Math.round((input.start + duration) * 100) / 100));
|
|
1107
|
+
function ensureStudioFontFaceCss(html, css) {
|
|
1108
|
+
if (html.includes(css)) return html;
|
|
1109
|
+
const block = FONT_STYLE_RE.exec(html);
|
|
1110
|
+
if (block) {
|
|
1111
|
+
const next = `${(block[2] ?? "").trim()}
|
|
1112
|
+
${css}`.trim();
|
|
1113
|
+
return html.replace(block[0], () => `<style data-hf-studio-fonts="true">
|
|
1114
|
+
${next}
|
|
1115
|
+
</style>`);
|
|
829
1116
|
}
|
|
830
|
-
|
|
1117
|
+
const styleTag = `<style data-hf-studio-fonts="true">
|
|
1118
|
+
${css}
|
|
1119
|
+
</style>`;
|
|
1120
|
+
if (/<\/head>/i.test(html)) return html.replace(/<\/head>/i, () => ` ${styleTag}
|
|
1121
|
+
</head>`);
|
|
1122
|
+
return `${styleTag}
|
|
1123
|
+
${html}`;
|
|
831
1124
|
}
|
|
832
1125
|
|
|
1126
|
+
// src/routes/files.ts
|
|
1127
|
+
import { parseHTML as parseHTML4 } from "linkedom";
|
|
1128
|
+
import { ensureHfIds } from "@hyperframes/parsers/hf-ids";
|
|
1129
|
+
|
|
833
1130
|
// src/routes/gsapMutationCapabilities.ts
|
|
834
1131
|
var differential = (family) => ({
|
|
835
1132
|
family,
|
|
@@ -946,7 +1243,7 @@ async function resolveProjectPath(c, adapter, route, opts) {
|
|
|
946
1243
|
}
|
|
947
1244
|
return { error: c.json({ error: "forbidden", why: "outside_project" }, 403) };
|
|
948
1245
|
}
|
|
949
|
-
if (opts?.mustExist && !
|
|
1246
|
+
if (opts?.mustExist && !existsSync4(absPath)) {
|
|
950
1247
|
return { error: c.json({ error: "not found" }, 404) };
|
|
951
1248
|
}
|
|
952
1249
|
return { project, filePath, absPath };
|
|
@@ -1012,7 +1309,7 @@ function projectHtmlIds(projectDir) {
|
|
|
1012
1309
|
if (!rel.endsWith(".html") || isInHiddenOrVendorDir(rel)) continue;
|
|
1013
1310
|
let html;
|
|
1014
1311
|
try {
|
|
1015
|
-
html =
|
|
1312
|
+
html = readFileSync5(join6(projectDir, rel), "utf-8");
|
|
1016
1313
|
} catch {
|
|
1017
1314
|
continue;
|
|
1018
1315
|
}
|
|
@@ -1041,7 +1338,7 @@ function foldAndCommitElementPatchBatches(projectDir, batches, writeFile, reques
|
|
|
1041
1338
|
resolvedPaths.add(absPath);
|
|
1042
1339
|
let before;
|
|
1043
1340
|
try {
|
|
1044
|
-
before =
|
|
1341
|
+
before = readFileSync5(absPath, "utf-8");
|
|
1045
1342
|
} catch {
|
|
1046
1343
|
return { error: "not-found", sourceFile: batch.sourceFile };
|
|
1047
1344
|
}
|
|
@@ -1097,7 +1394,7 @@ function commitElementPatchBatchesWithReceipts(c, projectDir, batches) {
|
|
|
1097
1394
|
);
|
|
1098
1395
|
}
|
|
1099
1396
|
function writeFileWithReceipt(c, filePath, absPath, html) {
|
|
1100
|
-
const overwrote =
|
|
1397
|
+
const overwrote = readFileSync5(absPath);
|
|
1101
1398
|
replaceFileAtomically2(absPath, html, statSync3(absPath).mode);
|
|
1102
1399
|
const version = fileContentVersion(html);
|
|
1103
1400
|
const writeToken = createWriteToken(c.req.header("X-Hyperframes-Write-Token"));
|
|
@@ -1107,7 +1404,7 @@ function writeFileWithReceipt(c, filePath, absPath, html) {
|
|
|
1107
1404
|
function writeMutationResult(c, projectDir, filePath, absPath, html, original) {
|
|
1108
1405
|
const backup = snapshotBeforeWrite(projectDir, absPath);
|
|
1109
1406
|
if (backup.error) return c.json({ error: `backup failed: ${backup.error}` }, 500);
|
|
1110
|
-
if (
|
|
1407
|
+
if (readFileSync5(absPath, "utf-8") !== original) {
|
|
1111
1408
|
return c.json({ error: "file changed", conflict: true, path: filePath }, 409);
|
|
1112
1409
|
}
|
|
1113
1410
|
const { version } = writeFileWithReceipt(c, filePath, absPath, html);
|
|
@@ -1162,7 +1459,7 @@ function generateCopyPath(projectDir, originalPath) {
|
|
|
1162
1459
|
const cleanBase = copyMatch ? base.slice(0, -copyMatch[0].length) : base;
|
|
1163
1460
|
let num = copyMatch ? copyMatch[1] ? parseInt(copyMatch[1]) + 1 : 2 : 1;
|
|
1164
1461
|
let candidate = num === 1 ? `${cleanBase} (copy)${ext}` : `${cleanBase} (copy ${num})${ext}`;
|
|
1165
|
-
while (
|
|
1462
|
+
while (existsSync4(resolve2(projectDir, candidate))) {
|
|
1166
1463
|
num++;
|
|
1167
1464
|
candidate = `${cleanBase} (copy ${num})${ext}`;
|
|
1168
1465
|
}
|
|
@@ -1171,7 +1468,7 @@ function generateCopyPath(projectDir, originalPath) {
|
|
|
1171
1468
|
function walkFiles(dir, filter) {
|
|
1172
1469
|
const results = [];
|
|
1173
1470
|
for (const entry of readableEntries(dir)) {
|
|
1174
|
-
const full =
|
|
1471
|
+
const full = join6(dir, entry.name);
|
|
1175
1472
|
if (entry.isDirectory()) {
|
|
1176
1473
|
if (entry.name === "node_modules" || entry.name === ".thumbnails" || entry.name === "renders" || entry.name === ".transcode-cache")
|
|
1177
1474
|
continue;
|
|
@@ -1196,7 +1493,7 @@ function readableEntries(dir) {
|
|
|
1196
1493
|
}
|
|
1197
1494
|
function readableText(file) {
|
|
1198
1495
|
try {
|
|
1199
|
-
return
|
|
1496
|
+
return readFileSync5(file, "utf-8");
|
|
1200
1497
|
} catch (error) {
|
|
1201
1498
|
if (skippable(error)) return null;
|
|
1202
1499
|
throw error;
|
|
@@ -1254,7 +1551,7 @@ function occurrences(text2, part) {
|
|
|
1254
1551
|
}
|
|
1255
1552
|
function projectPaths(root, dir = root, prefix = "", ancestors = /* @__PURE__ */ new Set()) {
|
|
1256
1553
|
return readableEntries(dir).flatMap((entry) => {
|
|
1257
|
-
const [path, full] = [`${prefix}${entry.name}`,
|
|
1554
|
+
const [path, full] = [`${prefix}${entry.name}`, join6(dir, entry.name)];
|
|
1258
1555
|
const real = entry.name === "node_modules" ? null : directoryReal(entry, full);
|
|
1259
1556
|
if (real === null || ancestors.has(real) || !isSafePath(root, full)) return [path];
|
|
1260
1557
|
return [path, ...projectPaths(root, full, `${path}/`, /* @__PURE__ */ new Set([...ancestors, real]))];
|
|
@@ -1287,7 +1584,7 @@ function updateReferences(projectDir, oldPath, newPath, isDirectory) {
|
|
|
1287
1584
|
return updatedCount;
|
|
1288
1585
|
}
|
|
1289
1586
|
function extractGsapScriptBlock(html) {
|
|
1290
|
-
const { document } =
|
|
1587
|
+
const { document } = parseHTML4(ensureHfIds(html));
|
|
1291
1588
|
const scripts = [
|
|
1292
1589
|
...document.querySelectorAll("script:not([src])"),
|
|
1293
1590
|
...Array.from(document.querySelectorAll("template")).flatMap(
|
|
@@ -1562,7 +1859,7 @@ function containsRawGsapExpression(value) {
|
|
|
1562
1859
|
return Object.values(value).some(containsRawGsapExpression);
|
|
1563
1860
|
}
|
|
1564
1861
|
async function prepareGsapMutationScript(c, res, firstMutation) {
|
|
1565
|
-
const beforeHtml =
|
|
1862
|
+
const beforeHtml = readFileSync5(res.absPath, "utf-8");
|
|
1566
1863
|
let html = beforeHtml;
|
|
1567
1864
|
let block = extractGsapScriptBlock(html);
|
|
1568
1865
|
if (!block && (firstMutation.type === "add" || firstMutation.type === "add-with-keyframes")) {
|
|
@@ -1637,6 +1934,7 @@ async function applyGsapMutations(c, res, mutations) {
|
|
|
1637
1934
|
const prepared = await prepareGsapMutationScript(c, res, firstMutation);
|
|
1638
1935
|
if (prepared instanceof Response) return prepared;
|
|
1639
1936
|
const { html, beforeHtml, block } = prepared;
|
|
1937
|
+
let lengthIsAuthored;
|
|
1640
1938
|
const initialScript = block.scriptText;
|
|
1641
1939
|
const skippedSelectors = /* @__PURE__ */ new Set();
|
|
1642
1940
|
const respond = (data, status) => status ? c.json(data, status) : c.json(data);
|
|
@@ -1668,7 +1966,9 @@ async function applyGsapMutations(c, res, mutations) {
|
|
|
1668
1966
|
for (const selector of result.skippedSelectors) skippedSelectors.add(selector);
|
|
1669
1967
|
}
|
|
1670
1968
|
if (HOLD_SYNC_MUTATION_TYPES.has(mutation.type)) {
|
|
1671
|
-
|
|
1969
|
+
lengthIsAuthored ??= !lengthIsTimeline(res.project.dir, res.filePath);
|
|
1970
|
+
if (lengthIsAuthored) newScript = trimTrailingKeyframeSpans(previousScript, newScript);
|
|
1971
|
+
newScript = writer === "acorn" ? syncPositionHoldsBeforeKeyframes(newScript, previousScript) : (await loadGsapParser()).syncPositionHoldsBeforeKeyframes(newScript, previousScript);
|
|
1672
1972
|
}
|
|
1673
1973
|
mutationChanges.push(newScript !== previousScript);
|
|
1674
1974
|
block.scriptText = newScript;
|
|
@@ -1676,7 +1976,7 @@ async function applyGsapMutations(c, res, mutations) {
|
|
|
1676
1976
|
const changed = block.scriptText !== initialScript;
|
|
1677
1977
|
const newHtml = changed ? block.replaceScript(block.scriptText) : html;
|
|
1678
1978
|
let backupPath = null;
|
|
1679
|
-
if (
|
|
1979
|
+
if (readFileSync5(res.absPath, "utf-8") !== beforeHtml) {
|
|
1680
1980
|
return c.json({ error: "file changed during GSAP mutation", conflict: true }, 409);
|
|
1681
1981
|
}
|
|
1682
1982
|
if (changed) {
|
|
@@ -2367,7 +2667,7 @@ async function foldAtomicCutFile(c, file, absPath, before, writer) {
|
|
|
2367
2667
|
for (const selector of result.skippedSelectors) skippedSelectors.add(selector);
|
|
2368
2668
|
}
|
|
2369
2669
|
if (script !== block.scriptText) {
|
|
2370
|
-
script = writer === "acorn" ? syncPositionHoldsBeforeKeyframes(script) : (await loadGsapParser()).syncPositionHoldsBeforeKeyframes(script);
|
|
2670
|
+
script = writer === "acorn" ? syncPositionHoldsBeforeKeyframes(script, block.scriptText) : (await loadGsapParser()).syncPositionHoldsBeforeKeyframes(script, block.scriptText);
|
|
2371
2671
|
after = block.replaceScript(script);
|
|
2372
2672
|
}
|
|
2373
2673
|
}
|
|
@@ -2406,9 +2706,9 @@ async function processUploadedFiles(formData, targetDir, projectDir) {
|
|
|
2406
2706
|
const base = dotIdx > 0 ? name.slice(0, dotIdx) : name;
|
|
2407
2707
|
const MAX_COPY_INDEX = 1e4;
|
|
2408
2708
|
let n = 1;
|
|
2409
|
-
if (
|
|
2709
|
+
if (existsSync4(finalPath)) {
|
|
2410
2710
|
n = 2;
|
|
2411
|
-
while (n < MAX_COPY_INDEX &&
|
|
2711
|
+
while (n < MAX_COPY_INDEX && existsSync4(resolve2(targetDir, `${base} (${n})${ext}`))) n++;
|
|
2412
2712
|
if (n >= MAX_COPY_INDEX) {
|
|
2413
2713
|
skipped.push(name);
|
|
2414
2714
|
continue;
|
|
@@ -2442,7 +2742,7 @@ async function processUploadedFiles(formData, targetDir, projectDir) {
|
|
|
2442
2742
|
if (n >= MAX_COPY_INDEX) skipped.push(name);
|
|
2443
2743
|
continue;
|
|
2444
2744
|
}
|
|
2445
|
-
const relativePath = subDir ?
|
|
2745
|
+
const relativePath = subDir ? join6(subDir, finalName) : finalName;
|
|
2446
2746
|
uploaded.push(relativePath);
|
|
2447
2747
|
if (validation.unchecked) unchecked.push({ name: finalName, reason: validation.unchecked });
|
|
2448
2748
|
if (isAudioFile(finalName)) {
|
|
@@ -2475,7 +2775,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
2475
2775
|
if (stat2.size > MAX_TEXT_READ_BYTES) {
|
|
2476
2776
|
return c.json({ error: "too large to read as text", why: "too_large" }, 413);
|
|
2477
2777
|
}
|
|
2478
|
-
const content =
|
|
2778
|
+
const content = readFileSync5(fd);
|
|
2479
2779
|
const version = fileContentVersion(content);
|
|
2480
2780
|
c.header("ETag", version);
|
|
2481
2781
|
if (content.subarray(0, GIT_BINARY_SNIFF_BYTES).includes(0)) {
|
|
@@ -2500,7 +2800,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
2500
2800
|
if (expectedVersion === null && !createOnly) {
|
|
2501
2801
|
let currentContent = null;
|
|
2502
2802
|
try {
|
|
2503
|
-
currentContent =
|
|
2803
|
+
currentContent = readFileSync5(res.absPath);
|
|
2504
2804
|
} catch (error) {
|
|
2505
2805
|
if (!error || typeof error !== "object" || !("code" in error) || error.code !== "ENOENT") {
|
|
2506
2806
|
throw error;
|
|
@@ -2526,7 +2826,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
2526
2826
|
if (!error || typeof error !== "object" || !("code" in error) || error.code !== "EEXIST") {
|
|
2527
2827
|
throw error;
|
|
2528
2828
|
}
|
|
2529
|
-
const currentContent =
|
|
2829
|
+
const currentContent = readFileSync5(res.absPath);
|
|
2530
2830
|
return c.json(
|
|
2531
2831
|
{
|
|
2532
2832
|
error: "file conflict",
|
|
@@ -2556,7 +2856,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
2556
2856
|
);
|
|
2557
2857
|
}
|
|
2558
2858
|
try {
|
|
2559
|
-
const currentContent =
|
|
2859
|
+
const currentContent = readFileSync5(fd);
|
|
2560
2860
|
overwrote = currentContent;
|
|
2561
2861
|
const currentVersion = fileContentVersion(currentContent);
|
|
2562
2862
|
if (expectedVersion !== currentVersion) {
|
|
@@ -2632,7 +2932,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
2632
2932
|
}
|
|
2633
2933
|
let before;
|
|
2634
2934
|
try {
|
|
2635
|
-
before =
|
|
2935
|
+
before = readFileSync5(ctx.absPath, "utf-8");
|
|
2636
2936
|
} catch (error) {
|
|
2637
2937
|
if (!error || typeof error !== "object" || !("code" in error) || error.code !== "ENOENT") {
|
|
2638
2938
|
throw error;
|
|
@@ -2661,7 +2961,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
2661
2961
|
}
|
|
2662
2962
|
const backup = snapshotBeforeWrite(ctx.project.dir, ctx.absPath);
|
|
2663
2963
|
if (backup.error) return c.json({ error: `backup failed: ${backup.error}` }, 500);
|
|
2664
|
-
const current =
|
|
2964
|
+
const current = readFileSync5(ctx.absPath, "utf-8");
|
|
2665
2965
|
if (current !== before) {
|
|
2666
2966
|
const currentVersion2 = fileContentVersion(current);
|
|
2667
2967
|
return c.json({ error: "file conflict", currentVersion: currentVersion2, currentContent: current }, 409);
|
|
@@ -2689,12 +2989,12 @@ function registerFileRoutes(api, adapter) {
|
|
|
2689
2989
|
api.post("/projects/:id/file-mutations/remove-element/*", async (c) => {
|
|
2690
2990
|
const ctx = await resolveFileMutationContext(c, adapter, "remove-element");
|
|
2691
2991
|
if ("error" in ctx) return ctx.error;
|
|
2692
|
-
if (!
|
|
2992
|
+
if (!existsSync4(ctx.absPath)) {
|
|
2693
2993
|
return c.json({ error: "not found" }, 404);
|
|
2694
2994
|
}
|
|
2695
2995
|
const parsed = await parseMutationBody(c);
|
|
2696
2996
|
if ("error" in parsed) return parsed.error;
|
|
2697
|
-
const originalContent =
|
|
2997
|
+
const originalContent = readFileSync5(ctx.absPath, "utf-8");
|
|
2698
2998
|
return writeIfChanged(
|
|
2699
2999
|
c,
|
|
2700
3000
|
ctx.project.dir,
|
|
@@ -2707,7 +3007,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
2707
3007
|
api.post("/projects/:id/file-mutations/remove-elements/*", async (c) => {
|
|
2708
3008
|
const ctx = await resolveFileMutationContext(c, adapter, "remove-elements");
|
|
2709
3009
|
if ("error" in ctx) return ctx.error;
|
|
2710
|
-
if (!
|
|
3010
|
+
if (!existsSync4(ctx.absPath)) {
|
|
2711
3011
|
return c.json({ error: "not found" }, 404);
|
|
2712
3012
|
}
|
|
2713
3013
|
const body = await c.req.json().catch(() => null);
|
|
@@ -2715,7 +3015,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
2715
3015
|
if (!Array.isArray(targets) || targets.length === 0) {
|
|
2716
3016
|
return c.json({ error: "targets required" }, 400);
|
|
2717
3017
|
}
|
|
2718
|
-
const originalContent =
|
|
3018
|
+
const originalContent = readFileSync5(ctx.absPath, "utf-8");
|
|
2719
3019
|
return writeIfChanged(
|
|
2720
3020
|
c,
|
|
2721
3021
|
ctx.project.dir,
|
|
@@ -2751,7 +3051,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
2751
3051
|
seen.add(absPath);
|
|
2752
3052
|
let before;
|
|
2753
3053
|
try {
|
|
2754
|
-
before =
|
|
3054
|
+
before = readFileSync5(absPath, "utf-8");
|
|
2755
3055
|
} catch {
|
|
2756
3056
|
return c.json({ error: `not found: ${file.path}` }, 404);
|
|
2757
3057
|
}
|
|
@@ -2778,7 +3078,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
2778
3078
|
prepared.push(folded);
|
|
2779
3079
|
}
|
|
2780
3080
|
for (const file of prepared) {
|
|
2781
|
-
const current =
|
|
3081
|
+
const current = readFileSync5(file.absPath, "utf-8");
|
|
2782
3082
|
if (current !== file.before) {
|
|
2783
3083
|
return c.json(
|
|
2784
3084
|
{
|
|
@@ -2821,7 +3121,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
2821
3121
|
const conflicts = [];
|
|
2822
3122
|
for (const file of written.reverse()) {
|
|
2823
3123
|
try {
|
|
2824
|
-
const current =
|
|
3124
|
+
const current = readFileSync5(file.absPath, "utf-8");
|
|
2825
3125
|
if (current !== file.after) {
|
|
2826
3126
|
conflicts.push(file.path);
|
|
2827
3127
|
continue;
|
|
@@ -2869,7 +3169,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
2869
3169
|
const fallbackTiming = typeof parsed.body.elementStart === "number" && typeof parsed.body.elementDuration === "number" ? { start: parsed.body.elementStart, duration: parsed.body.elementDuration } : void 0;
|
|
2870
3170
|
let originalContent;
|
|
2871
3171
|
try {
|
|
2872
|
-
originalContent =
|
|
3172
|
+
originalContent = readFileSync5(ctx.absPath, "utf-8");
|
|
2873
3173
|
} catch {
|
|
2874
3174
|
return c.json({ error: "not found" }, 404);
|
|
2875
3175
|
}
|
|
@@ -2931,7 +3231,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
2931
3231
|
for (let attempt = 1; ; attempt += 1) {
|
|
2932
3232
|
let originalContent;
|
|
2933
3233
|
try {
|
|
2934
|
-
originalContent =
|
|
3234
|
+
originalContent = readFileSync5(ctx.absPath, "utf-8");
|
|
2935
3235
|
} catch {
|
|
2936
3236
|
return c.json({ error: "not found" }, 404);
|
|
2937
3237
|
}
|
|
@@ -3043,7 +3343,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
3043
3343
|
}
|
|
3044
3344
|
let originalContent;
|
|
3045
3345
|
try {
|
|
3046
|
-
originalContent =
|
|
3346
|
+
originalContent = readFileSync5(ctx.absPath, "utf-8");
|
|
3047
3347
|
} catch {
|
|
3048
3348
|
return c.json({ error: "not found" }, 404);
|
|
3049
3349
|
}
|
|
@@ -3099,7 +3399,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
3099
3399
|
).map((entry) => ({ target: entry.target, track: entry.track }));
|
|
3100
3400
|
let originalContent;
|
|
3101
3401
|
try {
|
|
3102
|
-
originalContent =
|
|
3402
|
+
originalContent = readFileSync5(ctx.absPath, "utf-8");
|
|
3103
3403
|
} catch {
|
|
3104
3404
|
return c.json({ error: "not found" }, 404);
|
|
3105
3405
|
}
|
|
@@ -3128,7 +3428,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
3128
3428
|
if ("error" in parsed) return parsed.error;
|
|
3129
3429
|
let content;
|
|
3130
3430
|
try {
|
|
3131
|
-
content =
|
|
3431
|
+
content = readFileSync5(ctx.absPath, "utf-8");
|
|
3132
3432
|
} catch {
|
|
3133
3433
|
return c.json({ exists: false });
|
|
3134
3434
|
}
|
|
@@ -3142,7 +3442,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
3142
3442
|
if (!Array.isArray(body?.targets)) return c.json({ error: "targets required" }, 400);
|
|
3143
3443
|
let content;
|
|
3144
3444
|
try {
|
|
3145
|
-
content =
|
|
3445
|
+
content = readFileSync5(ctx.absPath, "utf-8");
|
|
3146
3446
|
} catch {
|
|
3147
3447
|
return c.json({ exists: body.targets.map(() => false) });
|
|
3148
3448
|
}
|
|
@@ -3159,7 +3459,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
3159
3459
|
if (!newAbs) {
|
|
3160
3460
|
return c.json({ error: "forbidden" }, 403);
|
|
3161
3461
|
}
|
|
3162
|
-
if (
|
|
3462
|
+
if (existsSync4(newAbs)) {
|
|
3163
3463
|
return c.json({ error: "already exists" }, 409);
|
|
3164
3464
|
}
|
|
3165
3465
|
const isDirectory = statSync3(res.absPath, { throwIfNoEntry: false })?.isDirectory() ?? false;
|
|
@@ -3176,7 +3476,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
3176
3476
|
return c.json({ error: "path required" }, 400);
|
|
3177
3477
|
}
|
|
3178
3478
|
const srcAbs = resolveWithinProject(project.dir, body.path);
|
|
3179
|
-
if (!srcAbs || !
|
|
3479
|
+
if (!srcAbs || !existsSync4(srcAbs)) {
|
|
3180
3480
|
return c.json({ error: "not found" }, 404);
|
|
3181
3481
|
}
|
|
3182
3482
|
const copyPath = generateCopyPath(project.dir, body.path);
|
|
@@ -3186,7 +3486,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
3186
3486
|
}
|
|
3187
3487
|
ensureDir(project.dir, destAbs);
|
|
3188
3488
|
try {
|
|
3189
|
-
createFileAtomically(destAbs,
|
|
3489
|
+
createFileAtomically(destAbs, readFileSync5(srcAbs));
|
|
3190
3490
|
} catch (error) {
|
|
3191
3491
|
if (!error || typeof error !== "object" || !("code" in error) || error.code !== "EEXIST") {
|
|
3192
3492
|
throw error;
|
|
@@ -3230,7 +3530,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
3230
3530
|
mustExist: true
|
|
3231
3531
|
});
|
|
3232
3532
|
if ("error" in res) return res.error;
|
|
3233
|
-
const html =
|
|
3533
|
+
const html = readFileSync5(res.absPath, "utf-8");
|
|
3234
3534
|
const etag = `${fileContentVersion(html).slice(0, -1)}:gsap-animations:v1"`;
|
|
3235
3535
|
const headers = { ETag: etag, "Cache-Control": "no-cache" };
|
|
3236
3536
|
if (c.req.header("If-None-Match") === etag) return new Response(null, { status: 304, headers });
|
|
@@ -3253,256 +3553,67 @@ function registerFileRoutes(api, adapter) {
|
|
|
3253
3553
|
const res = await resolveProjectPath(c, adapter, "gsap-mutations", {
|
|
3254
3554
|
mustExist: true,
|
|
3255
3555
|
pin: true
|
|
3256
|
-
});
|
|
3257
|
-
if ("error" in res) return res.error;
|
|
3258
|
-
const body = await c.req.json().catch(() => null);
|
|
3259
|
-
if (!body) return c.json({ error: "mutation type required" }, 400);
|
|
3260
|
-
const error = validateGsapMutationRequest(c, body);
|
|
3261
|
-
if (error) return error;
|
|
3262
|
-
return applyGsapMutations(c, res, [body]);
|
|
3263
|
-
});
|
|
3264
|
-
api.post("/projects/:id/gsap-mutations-batch/*", async (c) => {
|
|
3265
|
-
const res = await resolveProjectPath(c, adapter, "gsap-mutations-batch", {
|
|
3266
|
-
mustExist: true,
|
|
3267
|
-
pin: true
|
|
3268
|
-
});
|
|
3269
|
-
if ("error" in res) return res.error;
|
|
3270
|
-
const body = await c.req.json().catch(() => null);
|
|
3271
|
-
if (!body || !Array.isArray(body.mutations) || body.mutations.length === 0) {
|
|
3272
|
-
return c.json({ error: "mutations array required" }, 400);
|
|
3273
|
-
}
|
|
3274
|
-
for (const mutation of body.mutations) {
|
|
3275
|
-
const error = validateGsapMutationRequest(c, mutation);
|
|
3276
|
-
if (error) return error;
|
|
3277
|
-
}
|
|
3278
|
-
return applyGsapMutations(c, res, body.mutations);
|
|
3279
|
-
});
|
|
3280
|
-
api.post("/projects/:id/gsap-mutation-rollback/*", async (c) => {
|
|
3281
|
-
const res = await resolveProjectPath(c, adapter, "gsap-mutation-rollback", {
|
|
3282
|
-
mustExist: true,
|
|
3283
|
-
pin: true
|
|
3284
|
-
});
|
|
3285
|
-
if ("error" in res) return res.error;
|
|
3286
|
-
const body = await c.req.json().catch(() => null);
|
|
3287
|
-
if (!body || typeof body.expected !== "string" || typeof body.restore !== "string") {
|
|
3288
|
-
return c.json({ error: "expected and restore contents required" }, 400);
|
|
3289
|
-
}
|
|
3290
|
-
const current = readFileSync3(res.absPath, "utf-8");
|
|
3291
|
-
if (current !== body.expected) {
|
|
3292
|
-
return c.json({ ok: true, restored: false, conflict: true });
|
|
3293
|
-
}
|
|
3294
|
-
replaceFileAtomically2(res.absPath, body.restore, statSync3(res.absPath).mode);
|
|
3295
|
-
return c.json({ ok: true, restored: true, conflict: false });
|
|
3296
|
-
});
|
|
3297
|
-
}
|
|
3298
|
-
|
|
3299
|
-
// src/routes/preview.ts
|
|
3300
|
-
import { parseHTML as parseHTML5 } from "linkedom";
|
|
3301
|
-
import { createReadStream, existsSync as existsSync5, readFileSync as readFileSync5, statSync as statSync4 } from "fs";
|
|
3302
|
-
import { Readable } from "stream";
|
|
3303
|
-
import { join as join8, resolve as resolve4 } from "path";
|
|
3304
|
-
import { createHash } from "crypto";
|
|
3305
|
-
import {
|
|
3306
|
-
AFTER_FONTS_SCRIPT_TYPE,
|
|
3307
|
-
addScenePartsManifest,
|
|
3308
|
-
RUNTIME_BOOTSTRAP_ATTR as RUNTIME_BOOTSTRAP_ATTR2,
|
|
3309
|
-
insertBeforeCloseTag as insertBeforeCloseTag2,
|
|
3310
|
-
insertRuntimeTag,
|
|
3311
|
-
stripEmbeddedRuntimeScripts as stripEmbeddedRuntimeScripts2
|
|
3312
|
-
} from "@hyperframes/core/compiler";
|
|
3313
|
-
import { STUDIO_PREVIEW_MARK_META } from "@hyperframes/core/studio-preview-mark";
|
|
3314
|
-
import { gsapCdnDist as gsapCdnDist2, motionPathPluginUrl } from "@hyperframes/core/gsap-cdn";
|
|
3315
|
-
import { findStartTags as findStartTags2, injectTagsAtHeadStart } from "@hyperframes/core/compiler/html-document";
|
|
3316
|
-
import { isWithinProjectRoot } from "@hyperframes/parsers/asset-resolution";
|
|
3317
|
-
|
|
3318
|
-
// src/helpers/subComposition.ts
|
|
3319
|
-
import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
|
|
3320
|
-
import { join as join6 } from "path";
|
|
3321
|
-
import { parseHTML as parseHTML3 } from "linkedom";
|
|
3322
|
-
import {
|
|
3323
|
-
rewriteAssetPaths,
|
|
3324
|
-
rewriteCssAssetUrls,
|
|
3325
|
-
rewriteInlineStyleAssetUrls
|
|
3326
|
-
} from "@hyperframes/core";
|
|
3327
|
-
import {
|
|
3328
|
-
deferScriptsUntilFonts,
|
|
3329
|
-
RUNTIME_BOOTSTRAP_ATTR,
|
|
3330
|
-
stripEmbeddedRuntimeScripts
|
|
3331
|
-
} from "@hyperframes/core/compiler";
|
|
3332
|
-
import { isFullHtmlDocument } from "@hyperframes/core/compiler/html-document";
|
|
3333
|
-
import { gsapCdnDist } from "@hyperframes/core/gsap-cdn";
|
|
3334
|
-
function rewriteRelativePaths(root, compPath, projectDir) {
|
|
3335
|
-
const assetExists = (path) => existsSync4(join6(projectDir, path));
|
|
3336
|
-
rewriteAssetPaths(
|
|
3337
|
-
root.querySelectorAll("[src], [href]"),
|
|
3338
|
-
compPath,
|
|
3339
|
-
(el, attr) => el.getAttribute(attr),
|
|
3340
|
-
(el, attr, value) => el.setAttribute(attr, value),
|
|
3341
|
-
assetExists
|
|
3342
|
-
);
|
|
3343
|
-
rewriteInlineStyleAssetUrls(
|
|
3344
|
-
root.querySelectorAll("[style]"),
|
|
3345
|
-
compPath,
|
|
3346
|
-
(el) => el.getAttribute("style"),
|
|
3347
|
-
(el, value) => el.setAttribute("style", value),
|
|
3348
|
-
assetExists
|
|
3349
|
-
);
|
|
3350
|
-
for (const styleEl of root.querySelectorAll("style")) {
|
|
3351
|
-
styleEl.textContent = rewriteCssAssetUrls(styleEl.textContent || "", compPath, assetExists);
|
|
3352
|
-
}
|
|
3353
|
-
}
|
|
3354
|
-
function escapeLeadingDigitIdent(id) {
|
|
3355
|
-
return `\\${id.charCodeAt(0).toString(16)} ${id.slice(1)}`;
|
|
3356
|
-
}
|
|
3357
|
-
var REGEXP_SPECIALS = /[.*+?^${}()|[\]\\]/g;
|
|
3358
|
-
function fixDigitLeadingIdSelectors(root) {
|
|
3359
|
-
const digitIds = /* @__PURE__ */ new Set();
|
|
3360
|
-
for (const el of root.querySelectorAll("[id]")) {
|
|
3361
|
-
const id = el.getAttribute("id");
|
|
3362
|
-
if (id && /^\d/.test(id)) digitIds.add(id);
|
|
3363
|
-
}
|
|
3364
|
-
if (digitIds.size === 0) return;
|
|
3365
|
-
for (const styleEl of root.querySelectorAll("style")) {
|
|
3366
|
-
let css = styleEl.textContent || "";
|
|
3367
|
-
for (const id of digitIds) {
|
|
3368
|
-
const pattern = new RegExp(`#${id.replace(REGEXP_SPECIALS, "\\$&")}(?![\\w-])`, "g");
|
|
3369
|
-
css = css.replace(pattern, `#${escapeLeadingDigitIdent(id)}`);
|
|
3556
|
+
});
|
|
3557
|
+
if ("error" in res) return res.error;
|
|
3558
|
+
const body = await c.req.json().catch(() => null);
|
|
3559
|
+
if (!body) return c.json({ error: "mutation type required" }, 400);
|
|
3560
|
+
const error = validateGsapMutationRequest(c, body);
|
|
3561
|
+
if (error) return error;
|
|
3562
|
+
return applyGsapMutations(c, res, [body]);
|
|
3563
|
+
});
|
|
3564
|
+
api.post("/projects/:id/gsap-mutations-batch/*", async (c) => {
|
|
3565
|
+
const res = await resolveProjectPath(c, adapter, "gsap-mutations-batch", {
|
|
3566
|
+
mustExist: true,
|
|
3567
|
+
pin: true
|
|
3568
|
+
});
|
|
3569
|
+
if ("error" in res) return res.error;
|
|
3570
|
+
const body = await c.req.json().catch(() => null);
|
|
3571
|
+
if (!body || !Array.isArray(body.mutations) || body.mutations.length === 0) {
|
|
3572
|
+
return c.json({ error: "mutations array required" }, 400);
|
|
3370
3573
|
}
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
function extractFullDocumentParts(rawHtml, compPath, projectDir) {
|
|
3375
|
-
const { document: doc } = parseHTML3(rawHtml);
|
|
3376
|
-
const rewriteTargets = [doc.head, doc.body].filter(Boolean);
|
|
3377
|
-
for (const target of rewriteTargets) {
|
|
3378
|
-
rewriteRelativePaths(target, compPath, projectDir);
|
|
3379
|
-
}
|
|
3380
|
-
fixDigitLeadingIdSelectors(doc);
|
|
3381
|
-
const headContent = doc.head?.innerHTML ?? "";
|
|
3382
|
-
const bodyContent = doc.body?.innerHTML ?? "";
|
|
3383
|
-
const htmlEl = doc.documentElement;
|
|
3384
|
-
const htmlAttrs = extractElementAttrs(htmlEl);
|
|
3385
|
-
const bodyAttrs = doc.body ? extractElementAttrs(doc.body) : "";
|
|
3386
|
-
return { headContent, bodyContent, htmlAttrs, bodyAttrs };
|
|
3387
|
-
}
|
|
3388
|
-
function extractTemplateInnerHtml(rawComp) {
|
|
3389
|
-
const { document: doc } = parseHTML3(rawComp);
|
|
3390
|
-
const template = doc.querySelector("template");
|
|
3391
|
-
return template ? template.innerHTML : null;
|
|
3392
|
-
}
|
|
3393
|
-
function escapeAttrValue(value) {
|
|
3394
|
-
return value.replace(/&/g, "&").replace(/"/g, """);
|
|
3395
|
-
}
|
|
3396
|
-
function extractElementAttrs(el) {
|
|
3397
|
-
const parts = [];
|
|
3398
|
-
for (let i = 0; i < el.attributes.length; i++) {
|
|
3399
|
-
const attr = el.attributes[i];
|
|
3400
|
-
if (attr.value === "") {
|
|
3401
|
-
parts.push(attr.name);
|
|
3402
|
-
} else {
|
|
3403
|
-
parts.push(`${attr.name}="${escapeAttrValue(attr.value)}"`);
|
|
3574
|
+
for (const mutation of body.mutations) {
|
|
3575
|
+
const error = validateGsapMutationRequest(c, mutation);
|
|
3576
|
+
if (error) return error;
|
|
3404
3577
|
}
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3578
|
+
return applyGsapMutations(c, res, body.mutations);
|
|
3579
|
+
});
|
|
3580
|
+
api.post("/projects/:id/gsap-mutation-rollback/*", async (c) => {
|
|
3581
|
+
const res = await resolveProjectPath(c, adapter, "gsap-mutation-rollback", {
|
|
3582
|
+
mustExist: true,
|
|
3583
|
+
pin: true
|
|
3584
|
+
});
|
|
3585
|
+
if ("error" in res) return res.error;
|
|
3586
|
+
const body = await c.req.json().catch(() => null);
|
|
3587
|
+
if (!body || typeof body.expected !== "string" || typeof body.restore !== "string") {
|
|
3588
|
+
return c.json({ error: "expected and restore contents required" }, 400);
|
|
3416
3589
|
}
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
}
|
|
3423
|
-
|
|
3424
|
-
const match = bodyHtml.match(/<[a-zA-Z][^>]*\bdata-composition-id=/);
|
|
3425
|
-
if (match?.index == null) return bodyHtml;
|
|
3426
|
-
const tagEnd = bodyHtml.indexOf(">", match.index);
|
|
3427
|
-
if (tagEnd === -1) return bodyHtml;
|
|
3428
|
-
if (bodyHtml.slice(match.index, tagEnd).includes("data-composition-file")) return bodyHtml;
|
|
3429
|
-
return bodyHtml.slice(0, tagEnd) + ` data-composition-file="${compPath}"` + bodyHtml.slice(tagEnd);
|
|
3430
|
-
}
|
|
3431
|
-
function rootHeadContent(rootHtml) {
|
|
3432
|
-
return rootHtml.match(/<head[^>]*>([\s\S]*?)<\/head>/i)?.[1] ?? "";
|
|
3433
|
-
}
|
|
3434
|
-
function buildSubCompositionHtml(projectDir, compPath, runtimeUrl, baseHref, rawOverride) {
|
|
3435
|
-
const compFile = join6(projectDir, compPath);
|
|
3436
|
-
if (!existsSync4(compFile)) return null;
|
|
3437
|
-
const rawComp = rawOverride ?? readFileSync4(compFile, "utf-8");
|
|
3438
|
-
let compHeadContent = "";
|
|
3439
|
-
let rewrittenContent;
|
|
3440
|
-
let htmlAttrs = "";
|
|
3441
|
-
let bodyAttrs = "";
|
|
3442
|
-
const templateInner = extractTemplateInnerHtml(rawComp);
|
|
3443
|
-
if (templateInner != null) {
|
|
3444
|
-
const { document: contentDoc } = parseHTML3(
|
|
3445
|
-
`<!DOCTYPE html><html><head></head><body>${templateInner}</body></html>`
|
|
3446
|
-
);
|
|
3447
|
-
rewriteRelativePaths(contentDoc, compPath, projectDir);
|
|
3448
|
-
fixDigitLeadingIdSelectors(contentDoc);
|
|
3449
|
-
promoteTemplateCompositionId(rawComp, contentDoc.body);
|
|
3450
|
-
rewrittenContent = contentDoc.body.innerHTML || templateInner;
|
|
3451
|
-
} else if (isFullHtmlDocument(rawComp)) {
|
|
3452
|
-
const parts = extractFullDocumentParts(rawComp, compPath, projectDir);
|
|
3453
|
-
compHeadContent = parts.headContent;
|
|
3454
|
-
rewrittenContent = parts.bodyContent;
|
|
3455
|
-
htmlAttrs = parts.htmlAttrs;
|
|
3456
|
-
bodyAttrs = parts.bodyAttrs;
|
|
3457
|
-
} else {
|
|
3458
|
-
const { document: contentDoc } = parseHTML3(
|
|
3459
|
-
`<!DOCTYPE html><html><head></head><body>${rawComp}</body></html>`
|
|
3460
|
-
);
|
|
3461
|
-
rewriteRelativePaths(contentDoc, compPath, projectDir);
|
|
3462
|
-
fixDigitLeadingIdSelectors(contentDoc);
|
|
3463
|
-
rewrittenContent = contentDoc.body.innerHTML || rawComp;
|
|
3464
|
-
}
|
|
3465
|
-
rewrittenContent = stripEmbeddedRuntimeScripts(rewrittenContent);
|
|
3466
|
-
const { document: scriptsDoc } = parseHTML3(
|
|
3467
|
-
`<!DOCTYPE html><html><head></head><body>${rewrittenContent}</body></html>`
|
|
3468
|
-
);
|
|
3469
|
-
deferScriptsUntilFonts(scriptsDoc);
|
|
3470
|
-
rewrittenContent = scriptsDoc.body.innerHTML;
|
|
3471
|
-
rewrittenContent = tagRootCompositionFile(rewrittenContent, compPath);
|
|
3472
|
-
const indexPath = join6(projectDir, "index.html");
|
|
3473
|
-
let headContent = "";
|
|
3474
|
-
if (existsSync4(indexPath)) headContent = rootHeadContent(readFileSync4(indexPath, "utf-8"));
|
|
3475
|
-
if (baseHref && !hasBaseElement(headContent)) {
|
|
3476
|
-
headContent = `<base href="${baseHref}">
|
|
3477
|
-
${headContent}`;
|
|
3478
|
-
}
|
|
3479
|
-
if (compHeadContent) headContent += `
|
|
3480
|
-
${compHeadContent}`;
|
|
3481
|
-
headContent = stripEmbeddedRuntimeScripts(headContent);
|
|
3482
|
-
headContent += `
|
|
3483
|
-
<script ${RUNTIME_BOOTSTRAP_ATTR}="1" src="${runtimeUrl}"></script>`;
|
|
3484
|
-
if (!headContent.includes("gsap")) {
|
|
3485
|
-
headContent += `
|
|
3486
|
-
<script src="${gsapCdnDist()}gsap.min.js"></script>`;
|
|
3487
|
-
}
|
|
3488
|
-
const htmlOpen = htmlAttrs ? `<html ${htmlAttrs}>` : "<html>";
|
|
3489
|
-
const bodyOpen = bodyAttrs ? `<body ${bodyAttrs}>` : "<body>";
|
|
3490
|
-
return `<!DOCTYPE html>
|
|
3491
|
-
${htmlOpen}
|
|
3492
|
-
<head>
|
|
3493
|
-
${headContent}
|
|
3494
|
-
</head>
|
|
3495
|
-
${bodyOpen}
|
|
3496
|
-
<script>window.__timelines=window.__timelines||{};</script>
|
|
3497
|
-
${rewrittenContent}
|
|
3498
|
-
</body>
|
|
3499
|
-
</html>`;
|
|
3500
|
-
}
|
|
3501
|
-
function hasBaseElement(html) {
|
|
3502
|
-
return parseHTML3(html).document.querySelector("base") !== null;
|
|
3590
|
+
const current = readFileSync5(res.absPath, "utf-8");
|
|
3591
|
+
if (current !== body.expected) {
|
|
3592
|
+
return c.json({ ok: true, restored: false, conflict: true });
|
|
3593
|
+
}
|
|
3594
|
+
replaceFileAtomically2(res.absPath, body.restore, statSync3(res.absPath).mode);
|
|
3595
|
+
return c.json({ ok: true, restored: true, conflict: false });
|
|
3596
|
+
});
|
|
3503
3597
|
}
|
|
3504
3598
|
|
|
3505
3599
|
// src/routes/preview.ts
|
|
3600
|
+
import { parseHTML as parseHTML6 } from "linkedom";
|
|
3601
|
+
import { createReadStream, existsSync as existsSync5, readFileSync as readFileSync6, statSync as statSync4 } from "fs";
|
|
3602
|
+
import { Readable } from "stream";
|
|
3603
|
+
import { join as join8, resolve as resolve4 } from "path";
|
|
3604
|
+
import { createHash as createHash2 } from "crypto";
|
|
3605
|
+
import {
|
|
3606
|
+
AFTER_FONTS_SCRIPT_TYPE,
|
|
3607
|
+
addScenePartsManifest,
|
|
3608
|
+
RUNTIME_BOOTSTRAP_ATTR as RUNTIME_BOOTSTRAP_ATTR2,
|
|
3609
|
+
insertBeforeCloseTag as insertBeforeCloseTag2,
|
|
3610
|
+
insertRuntimeTag,
|
|
3611
|
+
stripEmbeddedRuntimeScripts as stripEmbeddedRuntimeScripts2
|
|
3612
|
+
} from "@hyperframes/core/compiler";
|
|
3613
|
+
import { STUDIO_PREVIEW_MARK_META } from "@hyperframes/core/studio-preview-mark";
|
|
3614
|
+
import { gsapCdnDist as gsapCdnDist2, motionPathPluginUrl } from "@hyperframes/core/gsap-cdn";
|
|
3615
|
+
import { findStartTags as findStartTags2, injectTagsAtHeadStart } from "@hyperframes/core/compiler/html-document";
|
|
3616
|
+
import { isWithinProjectRoot } from "@hyperframes/parsers/asset-resolution";
|
|
3506
3617
|
import { ensureHfIds as ensureHfIds2 } from "@hyperframes/parsers/hf-ids";
|
|
3507
3618
|
|
|
3508
3619
|
// src/helpers/previewReads.ts
|
|
@@ -3606,7 +3717,7 @@ function injectPreviewVariables(html, values) {
|
|
|
3606
3717
|
|
|
3607
3718
|
// src/helpers/lazyPreviewImages.ts
|
|
3608
3719
|
import { findStartTags } from "@hyperframes/core/compiler/html-document";
|
|
3609
|
-
import { parseHTML as
|
|
3720
|
+
import { parseHTML as parseHTML5 } from "linkedom";
|
|
3610
3721
|
import { STUDIO_PREVIEW_LAZY_ATTR } from "@hyperframes/core/studio-preview-mark";
|
|
3611
3722
|
function startsAfterZero(el) {
|
|
3612
3723
|
for (let node = el; node; node = node.parentElement) {
|
|
@@ -3617,7 +3728,7 @@ function startsAfterZero(el) {
|
|
|
3617
3728
|
var hasLoading = (img) => Array.from(img.attributes).some((attr) => attr.name.toLowerCase() === "loading");
|
|
3618
3729
|
function lazyPreviewImages(html) {
|
|
3619
3730
|
if (!/<!doctype|<html[\s>]/i.test(html)) return html;
|
|
3620
|
-
const images = [...
|
|
3731
|
+
const images = [...parseHTML5(html).document.querySelectorAll("img")];
|
|
3621
3732
|
const tags = findStartTags(html, "img");
|
|
3622
3733
|
if (tags.length !== images.length) return html;
|
|
3623
3734
|
const parts = [];
|
|
@@ -3653,7 +3764,7 @@ function readStudioMotionManifestContent(projectDir) {
|
|
|
3653
3764
|
const manifestPath = join8(projectDir, STUDIO_MOTION_PATH);
|
|
3654
3765
|
if (!existsSync5(manifestPath)) return "";
|
|
3655
3766
|
try {
|
|
3656
|
-
return
|
|
3767
|
+
return readFileSync6(manifestPath, "utf-8");
|
|
3657
3768
|
} catch {
|
|
3658
3769
|
return "";
|
|
3659
3770
|
}
|
|
@@ -3708,7 +3819,7 @@ function isGsapCoreUrl(src) {
|
|
|
3708
3819
|
return URL.canParse(src, base) && /\/gsap(\.min)?\.js$/i.test(new URL(src, base).pathname);
|
|
3709
3820
|
}
|
|
3710
3821
|
function findGsapCoreScript(html) {
|
|
3711
|
-
const scripts = [...
|
|
3822
|
+
const scripts = [...parseHTML6(html).document.querySelectorAll("script")];
|
|
3712
3823
|
const starts = findStartTags2(html, "script");
|
|
3713
3824
|
if (starts.length !== scripts.length) return null;
|
|
3714
3825
|
const at = scripts.findIndex(
|
|
@@ -3795,7 +3906,7 @@ function parsePreviewVariablesParam(raw) {
|
|
|
3795
3906
|
}
|
|
3796
3907
|
function variablesEtagSalt(raw) {
|
|
3797
3908
|
if (!raw) return "";
|
|
3798
|
-
return `:vars:${
|
|
3909
|
+
return `:vars:${createHash2("sha1").update(raw).digest("hex").slice(0, 12)}`;
|
|
3799
3910
|
}
|
|
3800
3911
|
function previewVariablesFromRequest(rawVariables) {
|
|
3801
3912
|
const parse2 = parsePreviewVariablesParam(rawVariables);
|
|
@@ -3835,13 +3946,13 @@ function resolveProjectMainHtml(projectDir, projectId) {
|
|
|
3835
3946
|
recordPreviewRead(projectDir, blockHtmlPath);
|
|
3836
3947
|
if (existsSync5(indexPath)) {
|
|
3837
3948
|
return {
|
|
3838
|
-
html:
|
|
3949
|
+
html: readFileSync6(indexPath, "utf-8"),
|
|
3839
3950
|
compositionPath: "index.html"
|
|
3840
3951
|
};
|
|
3841
3952
|
}
|
|
3842
3953
|
if (existsSync5(blockHtmlPath)) {
|
|
3843
3954
|
return {
|
|
3844
|
-
html:
|
|
3955
|
+
html: readFileSync6(blockHtmlPath, "utf-8"),
|
|
3845
3956
|
compositionPath: `${projectId}.html`
|
|
3846
3957
|
};
|
|
3847
3958
|
}
|
|
@@ -3972,7 +4083,7 @@ ${etag}`;
|
|
|
3972
4083
|
function pinSubCompHfIds(compFile, compPath) {
|
|
3973
4084
|
if (!/\.html?$/i.test(compPath)) return void 0;
|
|
3974
4085
|
try {
|
|
3975
|
-
return ensureHfIds2(
|
|
4086
|
+
return ensureHfIds2(readFileSync6(compFile, "utf-8"));
|
|
3976
4087
|
} catch {
|
|
3977
4088
|
return null;
|
|
3978
4089
|
}
|
|
@@ -3991,7 +4102,7 @@ ${etag}`;
|
|
|
3991
4102
|
if (!compFile || !existsSync5(compFile) || !statSync4(compFile).isFile()) {
|
|
3992
4103
|
return c.text("not found", 404);
|
|
3993
4104
|
}
|
|
3994
|
-
const compPathHash =
|
|
4105
|
+
const compPathHash = createHash2("sha1").update(compPath).digest("hex");
|
|
3995
4106
|
const capture = c.req.query(PREVIEW_CAPTURE_PARAM) !== void 0;
|
|
3996
4107
|
const etag = `"comp:v2:${compPathHash}:${signature}${variablesEtagSalt(vars.raw)}${capture ? ":capture" : ""}"`;
|
|
3997
4108
|
const ifNoneMatch = c.req.header("If-None-Match");
|
|
@@ -4083,7 +4194,7 @@ ${etag}`;
|
|
|
4083
4194
|
recordProxyRequest();
|
|
4084
4195
|
servedContentType = PROXY_VARIANT_CONFIG[proxyVariant].contentType;
|
|
4085
4196
|
}
|
|
4086
|
-
const textBuffer = isText ? Buffer.from(
|
|
4197
|
+
const textBuffer = isText ? Buffer.from(readFileSync6(file, "utf-8"), "utf-8") : null;
|
|
4087
4198
|
const totalSize = textBuffer ? textBuffer.length : statSync4(servedPath).size;
|
|
4088
4199
|
const bodyFor = (start, end) => textBuffer ? new Uint8Array(textBuffer.subarray(start, end + 1)) : (
|
|
4089
4200
|
// Node's web stream type and the DOM one do not overlap for tsc on
|
|
@@ -4127,7 +4238,7 @@ ${etag}`;
|
|
|
4127
4238
|
}
|
|
4128
4239
|
|
|
4129
4240
|
// src/routes/lint.ts
|
|
4130
|
-
import { readFileSync as
|
|
4241
|
+
import { readFileSync as readFileSync7 } from "fs";
|
|
4131
4242
|
import { join as join9 } from "path";
|
|
4132
4243
|
async function collectProjectFindings(adapter, projectDir) {
|
|
4133
4244
|
const findings = [];
|
|
@@ -4146,7 +4257,7 @@ async function lintUncoveredHtml(adapter, projectDir, htmlFiles, coveredFiles) {
|
|
|
4146
4257
|
const findings = [];
|
|
4147
4258
|
for (const file of htmlFiles) {
|
|
4148
4259
|
if (coveredFiles.has(file)) continue;
|
|
4149
|
-
const content =
|
|
4260
|
+
const content = readFileSync7(join9(projectDir, file), "utf-8");
|
|
4150
4261
|
const result = await adapter.lint(content, { filePath: file, isSubComposition: true });
|
|
4151
4262
|
for (const finding of result?.findings ?? []) {
|
|
4152
4263
|
findings.push({ ...finding, file });
|
|
@@ -4179,7 +4290,7 @@ function registerLintRoutes(api, adapter) {
|
|
|
4179
4290
|
|
|
4180
4291
|
// src/routes/render.ts
|
|
4181
4292
|
import { streamSSE } from "hono/streaming";
|
|
4182
|
-
import { existsSync as existsSync6, readFileSync as
|
|
4293
|
+
import { existsSync as existsSync6, readFileSync as readFileSync8, unlinkSync as unlinkSync3, readdirSync as readdirSync3, statSync as statSync5 } from "fs";
|
|
4183
4294
|
import { join as join10 } from "path";
|
|
4184
4295
|
import { VALID_CANVAS_RESOLUTIONS } from "@hyperframes/parsers";
|
|
4185
4296
|
import { formatRenderOutputTimestamp, parseFps } from "@hyperframes/core";
|
|
@@ -4319,7 +4430,7 @@ function registerRenderRoutes(api, adapter) {
|
|
|
4319
4430
|
}
|
|
4320
4431
|
const contentType = renderContentType(job.outputPath);
|
|
4321
4432
|
const filename = job.outputPath.split("/").pop() ?? `render.mp4`;
|
|
4322
|
-
const content =
|
|
4433
|
+
const content = readFileSync8(job.outputPath);
|
|
4323
4434
|
return new Response(content, {
|
|
4324
4435
|
headers: {
|
|
4325
4436
|
"Content-Type": contentType,
|
|
@@ -4337,7 +4448,7 @@ function registerRenderRoutes(api, adapter) {
|
|
|
4337
4448
|
}
|
|
4338
4449
|
const contentType = renderContentType(job.outputPath);
|
|
4339
4450
|
const filename = job.outputPath.split("/").pop() ?? `render.mp4`;
|
|
4340
|
-
const content =
|
|
4451
|
+
const content = readFileSync8(job.outputPath);
|
|
4341
4452
|
return new Response(content, {
|
|
4342
4453
|
headers: {
|
|
4343
4454
|
"Content-Type": contentType,
|
|
@@ -4370,7 +4481,7 @@ function registerRenderRoutes(api, adapter) {
|
|
|
4370
4481
|
if (!fp) return c.json({ error: "forbidden" }, 403);
|
|
4371
4482
|
if (!existsSync6(fp)) return c.json({ error: "not found" }, 404);
|
|
4372
4483
|
const contentType = renderContentType(fp);
|
|
4373
|
-
const content =
|
|
4484
|
+
const content = readFileSync8(fp);
|
|
4374
4485
|
return new Response(content, {
|
|
4375
4486
|
headers: {
|
|
4376
4487
|
"Content-Type": contentType,
|
|
@@ -4395,7 +4506,7 @@ function registerRenderRoutes(api, adapter) {
|
|
|
4395
4506
|
let audioLoweredDb;
|
|
4396
4507
|
if (existsSync6(metaPath)) {
|
|
4397
4508
|
try {
|
|
4398
|
-
const meta = JSON.parse(
|
|
4509
|
+
const meta = JSON.parse(readFileSync8(metaPath, "utf-8"));
|
|
4399
4510
|
if (meta.status === "failed" && !existsSync6(fp)) status = "failed";
|
|
4400
4511
|
if (meta.durationMs) durationMs = meta.durationMs;
|
|
4401
4512
|
if (typeof meta.audioLoweredDb === "number") audioLoweredDb = meta.audioLoweredDb;
|
|
@@ -4430,7 +4541,7 @@ function registerRenderRoutes(api, adapter) {
|
|
|
4430
4541
|
// src/routes/imageThumbnail.ts
|
|
4431
4542
|
import { HTTPException } from "hono/http-exception";
|
|
4432
4543
|
import { open, stat } from "fs/promises";
|
|
4433
|
-
import { createHash as
|
|
4544
|
+
import { createHash as createHash3 } from "crypto";
|
|
4434
4545
|
|
|
4435
4546
|
// src/routes/thumbnailGenerationCoordinator.ts
|
|
4436
4547
|
var ThumbnailGenerationCoordinator = class {
|
|
@@ -4612,7 +4723,7 @@ function registerImageThumbnailRoutes(api, adapter) {
|
|
|
4612
4723
|
api.get("/projects/:id/image-thumbnail/*", async (c) => {
|
|
4613
4724
|
try {
|
|
4614
4725
|
const { path, info } = await resolveSource(adapter, c.req.param("id"), c.req.url);
|
|
4615
|
-
const key =
|
|
4726
|
+
const key = createHash3("sha256").update(JSON.stringify([path, info.dev, info.ino, info.size, info.mtimeMs, info.ctimeMs])).digest("hex");
|
|
4616
4727
|
const etag = `"${key}"`;
|
|
4617
4728
|
c.header("ETag", etag);
|
|
4618
4729
|
c.header("Cache-Control", "private, no-cache");
|
|
@@ -4653,90 +4764,6 @@ import {
|
|
|
4653
4764
|
} from "fs";
|
|
4654
4765
|
import { join as join11 } from "path";
|
|
4655
4766
|
import { createHash as createHash4, randomUUID as randomUUID2 } from "crypto";
|
|
4656
|
-
|
|
4657
|
-
// src/helpers/compositionInputs.ts
|
|
4658
|
-
import { readFileSync as readFileSync8 } from "fs";
|
|
4659
|
-
import { createHash as createHash3 } from "crypto";
|
|
4660
|
-
var ROOT_COMPOSITION = "index.html";
|
|
4661
|
-
var COMPOSITION_SRC = /\bdata-composition-src\s*=\s*(["'])(.*?)\1/g;
|
|
4662
|
-
function projectReader(projectDir) {
|
|
4663
|
-
const sources = /* @__PURE__ */ new Map();
|
|
4664
|
-
return (compPath) => {
|
|
4665
|
-
if (!sources.has(compPath)) {
|
|
4666
|
-
const file = resolveWithinProject(projectDir, compPath);
|
|
4667
|
-
let source = null;
|
|
4668
|
-
try {
|
|
4669
|
-
source = file ? readFileSync8(file, "utf-8") : null;
|
|
4670
|
-
} catch {
|
|
4671
|
-
}
|
|
4672
|
-
sources.set(compPath, source);
|
|
4673
|
-
}
|
|
4674
|
-
return sources.get(compPath) ?? null;
|
|
4675
|
-
};
|
|
4676
|
-
}
|
|
4677
|
-
function normalizeSource(src) {
|
|
4678
|
-
if (/^[a-z][a-z0-9+.-]*:/i.test(src) || src.startsWith("//")) return null;
|
|
4679
|
-
const path = src.split(/[?#]/)[0]?.replace(/^\.?\//, "") ?? "";
|
|
4680
|
-
return path || null;
|
|
4681
|
-
}
|
|
4682
|
-
function closureOf(read, compPath) {
|
|
4683
|
-
const closure = /* @__PURE__ */ new Set();
|
|
4684
|
-
const visit = (path) => {
|
|
4685
|
-
if (closure.has(path)) return;
|
|
4686
|
-
closure.add(path);
|
|
4687
|
-
for (const match of (read(path) ?? "").matchAll(COMPOSITION_SRC)) {
|
|
4688
|
-
const mounted = normalizeSource(match[2] ?? "");
|
|
4689
|
-
if (mounted) visit(mounted);
|
|
4690
|
-
}
|
|
4691
|
-
};
|
|
4692
|
-
visit(compPath);
|
|
4693
|
-
return closure;
|
|
4694
|
-
}
|
|
4695
|
-
var inputSignatures = /* @__PURE__ */ new Map();
|
|
4696
|
-
var rootMemos = /* @__PURE__ */ new Map();
|
|
4697
|
-
var sha1 = (text2) => createHash3("sha1").update(text2).digest("hex");
|
|
4698
|
-
function rootHashes(read) {
|
|
4699
|
-
const source = read(ROOT_COMPOSITION) ?? "";
|
|
4700
|
-
return { root: sha1(source), head: sha1(rootHeadContent(source)) };
|
|
4701
|
-
}
|
|
4702
|
-
function compositionInputSignature(projectDir, compPath, projectSignature) {
|
|
4703
|
-
const key = `${projectDir}\0${compPath}`;
|
|
4704
|
-
const known = inputSignatures.get(key);
|
|
4705
|
-
if (known?.projectSignature === projectSignature) return known.inputSignature;
|
|
4706
|
-
const read = projectReader(projectDir);
|
|
4707
|
-
const comp = normalizeSource(compPath) ?? compPath;
|
|
4708
|
-
const inputs = closureOf(read, comp).add(ROOT_COMPOSITION);
|
|
4709
|
-
const excluded = new Set(
|
|
4710
|
-
[...closureOf(read, ROOT_COMPOSITION)].filter((path) => !inputs.has(path))
|
|
4711
|
-
);
|
|
4712
|
-
let inputSignature;
|
|
4713
|
-
if (comp === ROOT_COMPOSITION) {
|
|
4714
|
-
inputSignature = createProjectSignature(projectDir, excluded);
|
|
4715
|
-
} else {
|
|
4716
|
-
const hashes = rootHashes(read);
|
|
4717
|
-
if (!rootMemos.has(projectDir)) rootMemos.set(projectDir, { ...hashes, affected: null });
|
|
4718
|
-
inputSignature = `${createProjectSignature(projectDir, excluded.add(ROOT_COMPOSITION))}:${hashes.head}`;
|
|
4719
|
-
}
|
|
4720
|
-
inputSignatures.set(key, { projectSignature, inputSignature });
|
|
4721
|
-
return inputSignature;
|
|
4722
|
-
}
|
|
4723
|
-
function compositionsAffectedBy(projectDir, changedPath) {
|
|
4724
|
-
const changed = changedPath.replace(/\\/g, "/");
|
|
4725
|
-
const read = projectReader(projectDir);
|
|
4726
|
-
if (changed === ROOT_COMPOSITION) {
|
|
4727
|
-
const now = rootHashes(read);
|
|
4728
|
-
const memo = rootMemos.get(projectDir);
|
|
4729
|
-
if (memo?.root === now.root) return memo.affected;
|
|
4730
|
-
const affected = memo?.head === now.head ? [ROOT_COMPOSITION] : null;
|
|
4731
|
-
rootMemos.set(projectDir, { ...now, affected });
|
|
4732
|
-
return affected;
|
|
4733
|
-
}
|
|
4734
|
-
const mounted = closureOf(read, ROOT_COMPOSITION);
|
|
4735
|
-
if (!mounted.has(changed)) return null;
|
|
4736
|
-
return [...mounted].filter((path) => closureOf(read, path).has(changed));
|
|
4737
|
-
}
|
|
4738
|
-
|
|
4739
|
-
// src/routes/thumbnail.ts
|
|
4740
4767
|
var THUMBNAIL_CACHE_VERSION = "v5";
|
|
4741
4768
|
var THUMBNAIL_MAX_OUTPUT_WIDTH = 240;
|
|
4742
4769
|
var THUMBNAIL_MAX_OUTPUT_HEIGHT = 135;
|