@hyperframes/core 0.7.13 → 0.7.15
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/compiler/staticGuard.js +1 -1
- package/dist/compiler/staticGuard.js.map +1 -1
- package/dist/core.types.d.ts +2 -369
- package/dist/core.types.d.ts.map +1 -1
- package/dist/core.types.js +1 -84
- package/dist/core.types.js.map +1 -1
- package/dist/generators/hyperframes.d.ts +1 -1
- package/dist/generators/hyperframes.d.ts.map +1 -1
- package/dist/generators/hyperframes.js +1 -1
- package/dist/generators/hyperframes.js.map +1 -1
- package/dist/index.d.ts +5 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -4
- package/dist/index.js.map +1 -1
- package/dist/lint/index.d.ts +2 -2
- package/dist/lint/index.d.ts.map +1 -1
- package/dist/lint/index.js +2 -1
- package/dist/lint/index.js.map +1 -1
- package/dist/parsers/gsapConstants.d.ts +2 -12
- package/dist/parsers/gsapConstants.d.ts.map +1 -1
- package/dist/parsers/gsapConstants.js +2 -110
- package/dist/parsers/gsapConstants.js.map +1 -1
- package/dist/parsers/gsapParserAcorn.d.ts +2 -48
- package/dist/parsers/gsapParserAcorn.d.ts.map +1 -1
- package/dist/parsers/gsapParserAcorn.js +2 -1057
- package/dist/parsers/gsapParserAcorn.js.map +1 -1
- package/dist/parsers/gsapParserExports.d.ts +2 -20
- package/dist/parsers/gsapParserExports.d.ts.map +1 -1
- package/dist/parsers/gsapParserExports.js +2 -8
- package/dist/parsers/gsapParserExports.js.map +1 -1
- package/dist/parsers/gsapWriterAcorn.d.ts +2 -90
- package/dist/parsers/gsapWriterAcorn.d.ts.map +1 -1
- package/dist/parsers/gsapWriterAcorn.js +2 -2016
- package/dist/parsers/gsapWriterAcorn.js.map +1 -1
- package/dist/parsers/hfIds.d.ts +2 -16
- package/dist/parsers/hfIds.d.ts.map +1 -1
- package/dist/parsers/hfIds.js +2 -127
- package/dist/parsers/hfIds.js.map +1 -1
- package/dist/parsers/springEase.d.ts +2 -28
- package/dist/parsers/springEase.d.ts.map +1 -1
- package/dist/parsers/springEase.js +2 -69
- package/dist/parsers/springEase.js.map +1 -1
- package/dist/slideshow/index.d.ts +1 -0
- package/dist/slideshow/index.d.ts.map +1 -1
- package/dist/slideshow/index.js +1 -0
- package/dist/slideshow/index.js.map +1 -1
- package/dist/studio-api/helpers/draftMarkers.d.ts +2 -10
- package/dist/studio-api/helpers/draftMarkers.d.ts.map +1 -1
- package/dist/studio-api/helpers/draftMarkers.js +2 -10
- package/dist/studio-api/helpers/draftMarkers.js.map +1 -1
- package/dist/studio-api/helpers/finiteMutation.d.ts +2 -10
- package/dist/studio-api/helpers/finiteMutation.d.ts.map +1 -1
- package/dist/studio-api/helpers/finiteMutation.js +2 -20
- package/dist/studio-api/helpers/finiteMutation.js.map +1 -1
- package/dist/studio-api/helpers/manualEditsRenderScript.d.ts +2 -12
- package/dist/studio-api/helpers/manualEditsRenderScript.d.ts.map +1 -1
- package/dist/studio-api/helpers/manualEditsRenderScript.js +2 -632
- package/dist/studio-api/helpers/manualEditsRenderScript.js.map +1 -1
- package/dist/studio-api/helpers/screenshotClip.d.ts +2 -7
- package/dist/studio-api/helpers/screenshotClip.d.ts.map +1 -1
- package/dist/studio-api/helpers/screenshotClip.js +2 -21
- package/dist/studio-api/helpers/screenshotClip.js.map +1 -1
- package/dist/studio-api/helpers/studioMotionRenderScript.d.ts +2 -8
- package/dist/studio-api/helpers/studioMotionRenderScript.d.ts.map +1 -1
- package/dist/studio-api/helpers/studioMotionRenderScript.js +2 -207
- package/dist/studio-api/helpers/studioMotionRenderScript.js.map +1 -1
- package/dist/studio-api/index.d.ts +2 -9
- package/dist/studio-api/index.d.ts.map +1 -1
- package/dist/studio-api/index.js +2 -8
- package/dist/studio-api/index.js.map +1 -1
- package/package.json +16 -18
- package/dist/generated/runtime-inline.js +0 -11
- package/dist/hyperframe.manifest.json +0 -22
- package/dist/hyperframe.runtime.mjs +0 -325
- package/dist/lint/context.d.ts +0 -17
- package/dist/lint/context.d.ts.map +0 -1
- package/dist/lint/context.js +0 -37
- package/dist/lint/context.js.map +0 -1
- package/dist/lint/hyperframeLinter.d.ts +0 -14
- package/dist/lint/hyperframeLinter.d.ts.map +0 -1
- package/dist/lint/hyperframeLinter.js +0 -132
- package/dist/lint/hyperframeLinter.js.map +0 -1
- package/dist/lint/rules/adapters.d.ts +0 -3
- package/dist/lint/rules/adapters.d.ts.map +0 -1
- package/dist/lint/rules/adapters.js +0 -40
- package/dist/lint/rules/adapters.js.map +0 -1
- package/dist/lint/rules/captions.d.ts +0 -3
- package/dist/lint/rules/captions.d.ts.map +0 -1
- package/dist/lint/rules/captions.js +0 -242
- package/dist/lint/rules/captions.js.map +0 -1
- package/dist/lint/rules/composition.d.ts +0 -5
- package/dist/lint/rules/composition.d.ts.map +0 -1
- package/dist/lint/rules/composition.js +0 -694
- package/dist/lint/rules/composition.js.map +0 -1
- package/dist/lint/rules/core.d.ts +0 -3
- package/dist/lint/rules/core.d.ts.map +0 -1
- package/dist/lint/rules/core.js +0 -432
- package/dist/lint/rules/core.js.map +0 -1
- package/dist/lint/rules/fonts.d.ts +0 -3
- package/dist/lint/rules/fonts.d.ts.map +0 -1
- package/dist/lint/rules/fonts.js +0 -205
- package/dist/lint/rules/fonts.js.map +0 -1
- package/dist/lint/rules/gsap.d.ts +0 -4
- package/dist/lint/rules/gsap.d.ts.map +0 -1
- package/dist/lint/rules/gsap.js +0 -1039
- package/dist/lint/rules/gsap.js.map +0 -1
- package/dist/lint/rules/media.d.ts +0 -3
- package/dist/lint/rules/media.d.ts.map +0 -1
- package/dist/lint/rules/media.js +0 -491
- package/dist/lint/rules/media.js.map +0 -1
- package/dist/lint/rules/slideshow.d.ts +0 -4
- package/dist/lint/rules/slideshow.d.ts.map +0 -1
- package/dist/lint/rules/slideshow.js +0 -74
- package/dist/lint/rules/slideshow.js.map +0 -1
- package/dist/lint/rules/textures.d.ts +0 -3
- package/dist/lint/rules/textures.d.ts.map +0 -1
- package/dist/lint/rules/textures.js +0 -192
- package/dist/lint/rules/textures.js.map +0 -1
- package/dist/lint/types.d.ts +0 -36
- package/dist/lint/types.d.ts.map +0 -1
- package/dist/lint/types.js +0 -2
- package/dist/lint/types.js.map +0 -1
- package/dist/lint/utils.d.ts +0 -55
- package/dist/lint/utils.d.ts.map +0 -1
- package/dist/lint/utils.js +0 -264
- package/dist/lint/utils.js.map +0 -1
- package/dist/parsers/gsapInline.d.ts +0 -47
- package/dist/parsers/gsapInline.d.ts.map +0 -1
- package/dist/parsers/gsapInline.js +0 -513
- package/dist/parsers/gsapInline.js.map +0 -1
- package/dist/parsers/gsapParser.d.ts +0 -158
- package/dist/parsers/gsapParser.d.ts.map +0 -1
- package/dist/parsers/gsapParser.js +0 -2634
- package/dist/parsers/gsapParser.js.map +0 -1
- package/dist/parsers/gsapParser.test-helpers.d.ts +0 -49
- package/dist/parsers/gsapParser.test-helpers.d.ts.map +0 -1
- package/dist/parsers/gsapParser.test-helpers.js +0 -97
- package/dist/parsers/gsapParser.test-helpers.js.map +0 -1
- package/dist/parsers/gsapSerialize.d.ts +0 -191
- package/dist/parsers/gsapSerialize.d.ts.map +0 -1
- package/dist/parsers/gsapSerialize.js +0 -410
- package/dist/parsers/gsapSerialize.js.map +0 -1
- package/dist/parsers/gsapUnroll.d.ts +0 -7
- package/dist/parsers/gsapUnroll.d.ts.map +0 -1
- package/dist/parsers/gsapUnroll.js +0 -137
- package/dist/parsers/gsapUnroll.js.map +0 -1
- package/dist/parsers/htmlParser.d.ts +0 -27
- package/dist/parsers/htmlParser.d.ts.map +0 -1
- package/dist/parsers/htmlParser.js +0 -731
- package/dist/parsers/htmlParser.js.map +0 -1
- package/dist/studio-api/createStudioApi.d.ts +0 -10
- package/dist/studio-api/createStudioApi.d.ts.map +0 -1
- package/dist/studio-api/createStudioApi.js +0 -32
- package/dist/studio-api/createStudioApi.js.map +0 -1
- package/dist/studio-api/helpers/backupJournal.d.ts +0 -9
- package/dist/studio-api/helpers/backupJournal.d.ts.map +0 -1
- package/dist/studio-api/helpers/backupJournal.js +0 -81
- package/dist/studio-api/helpers/backupJournal.js.map +0 -1
- package/dist/studio-api/helpers/hfIdPersist.d.ts +0 -11
- package/dist/studio-api/helpers/hfIdPersist.d.ts.map +0 -1
- package/dist/studio-api/helpers/hfIdPersist.js +0 -39
- package/dist/studio-api/helpers/hfIdPersist.js.map +0 -1
- package/dist/studio-api/helpers/mediaValidation.d.ts +0 -20
- package/dist/studio-api/helpers/mediaValidation.d.ts.map +0 -1
- package/dist/studio-api/helpers/mediaValidation.js +0 -56
- package/dist/studio-api/helpers/mediaValidation.js.map +0 -1
- package/dist/studio-api/helpers/mime.d.ts +0 -4
- package/dist/studio-api/helpers/mime.d.ts.map +0 -1
- package/dist/studio-api/helpers/mime.js +0 -39
- package/dist/studio-api/helpers/mime.js.map +0 -1
- package/dist/studio-api/helpers/previewAdapter.d.ts +0 -43
- package/dist/studio-api/helpers/previewAdapter.d.ts.map +0 -1
- package/dist/studio-api/helpers/previewAdapter.js +0 -118
- package/dist/studio-api/helpers/previewAdapter.js.map +0 -1
- package/dist/studio-api/helpers/projectSignature.d.ts +0 -5
- package/dist/studio-api/helpers/projectSignature.d.ts.map +0 -1
- package/dist/studio-api/helpers/projectSignature.js +0 -154
- package/dist/studio-api/helpers/projectSignature.js.map +0 -1
- package/dist/studio-api/helpers/safePath.d.ts +0 -12
- package/dist/studio-api/helpers/safePath.d.ts.map +0 -1
- package/dist/studio-api/helpers/safePath.js +0 -38
- package/dist/studio-api/helpers/safePath.js.map +0 -1
- package/dist/studio-api/helpers/sourceMutation.d.ts +0 -28
- package/dist/studio-api/helpers/sourceMutation.d.ts.map +0 -1
- package/dist/studio-api/helpers/sourceMutation.js +0 -339
- package/dist/studio-api/helpers/sourceMutation.js.map +0 -1
- package/dist/studio-api/helpers/subComposition.d.ts +0 -20
- package/dist/studio-api/helpers/subComposition.d.ts.map +0 -1
- package/dist/studio-api/helpers/subComposition.js +0 -289
- package/dist/studio-api/helpers/subComposition.js.map +0 -1
- package/dist/studio-api/helpers/waveform.d.ts +0 -4
- package/dist/studio-api/helpers/waveform.d.ts.map +0 -1
- package/dist/studio-api/helpers/waveform.js +0 -80
- package/dist/studio-api/helpers/waveform.js.map +0 -1
- package/dist/studio-api/routes/files.d.ts +0 -4
- package/dist/studio-api/routes/files.d.ts.map +0 -1
- package/dist/studio-api/routes/files.js +0 -1200
- package/dist/studio-api/routes/files.js.map +0 -1
- package/dist/studio-api/routes/fonts.d.ts +0 -3
- package/dist/studio-api/routes/fonts.d.ts.map +0 -1
- package/dist/studio-api/routes/fonts.js +0 -157
- package/dist/studio-api/routes/fonts.js.map +0 -1
- package/dist/studio-api/routes/lint.d.ts +0 -4
- package/dist/studio-api/routes/lint.d.ts.map +0 -1
- package/dist/studio-api/routes/lint.js +0 -29
- package/dist/studio-api/routes/lint.js.map +0 -1
- package/dist/studio-api/routes/preview.d.ts +0 -4
- package/dist/studio-api/routes/preview.d.ts.map +0 -1
- package/dist/studio-api/routes/preview.js +0 -331
- package/dist/studio-api/routes/preview.js.map +0 -1
- package/dist/studio-api/routes/projects.d.ts +0 -4
- package/dist/studio-api/routes/projects.d.ts.map +0 -1
- package/dist/studio-api/routes/projects.js +0 -45
- package/dist/studio-api/routes/projects.js.map +0 -1
- package/dist/studio-api/routes/registry.d.ts +0 -4
- package/dist/studio-api/routes/registry.d.ts.map +0 -1
- package/dist/studio-api/routes/registry.js +0 -31
- package/dist/studio-api/routes/registry.js.map +0 -1
- package/dist/studio-api/routes/render.d.ts +0 -4
- package/dist/studio-api/routes/render.d.ts.map +0 -1
- package/dist/studio-api/routes/render.js +0 -269
- package/dist/studio-api/routes/render.js.map +0 -1
- package/dist/studio-api/routes/storyboard.d.ts +0 -4
- package/dist/studio-api/routes/storyboard.d.ts.map +0 -1
- package/dist/studio-api/routes/storyboard.js +0 -65
- package/dist/studio-api/routes/storyboard.js.map +0 -1
- package/dist/studio-api/routes/thumbnail.d.ts +0 -4
- package/dist/studio-api/routes/thumbnail.d.ts.map +0 -1
- package/dist/studio-api/routes/thumbnail.js +0 -107
- package/dist/studio-api/routes/thumbnail.js.map +0 -1
- package/dist/studio-api/routes/waveform.d.ts +0 -4
- package/dist/studio-api/routes/waveform.d.ts.map +0 -1
- package/dist/studio-api/routes/waveform.js +0 -41
- package/dist/studio-api/routes/waveform.js.map +0 -1
- package/dist/studio-api/types.d.ts +0 -120
- package/dist/studio-api/types.d.ts.map +0 -1
- package/dist/studio-api/types.js +0 -2
- package/dist/studio-api/types.js.map +0 -1
|
@@ -1,1200 +0,0 @@
|
|
|
1
|
-
import { bodyLimit } from "hono/body-limit";
|
|
2
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync, rmSync, statSync, renameSync, readdirSync, } from "node:fs";
|
|
3
|
-
import { resolve, dirname, join } from "node:path";
|
|
4
|
-
import { isAudioFile } from "../helpers/mime.js";
|
|
5
|
-
import { generateWaveformCache } from "../helpers/waveform.js";
|
|
6
|
-
import { validateUploadedMediaBuffer } from "../helpers/mediaValidation.js";
|
|
7
|
-
import { isSafePath, resolveWithinProject } from "../helpers/safePath.js";
|
|
8
|
-
import { backupPathForResponse, snapshotBeforeWrite } from "../helpers/backupJournal.js";
|
|
9
|
-
import { findUnsafeDomPatchValues, findUnsafeMutationValues, } from "../helpers/finiteMutation.js";
|
|
10
|
-
import { classifyPropertyGroup } from "../../parsers/gsapConstants.js";
|
|
11
|
-
import { parseGsapScriptAcorn } from "../../parsers/gsapParserAcorn.js";
|
|
12
|
-
import { unrollComputedTimeline } from "../../parsers/gsapUnroll.js";
|
|
13
|
-
import { updateAnimationInScript, addAnimationToScript, removeAnimationFromScript, addKeyframeToScript, removeKeyframeFromScript, updateKeyframeInScript, convertToKeyframesFromScript, removeAllKeyframesFromScript, materializeKeyframesFromScript, unrollDynamicAnimations, setArcPathInScript, updateArcSegmentInScript, removeArcPathFromScript, addAnimationWithKeyframesToScript, splitAnimationsInScript, splitIntoPropertyGroupsFromScript, shiftPositionsInScript, scalePositionsInScript, } from "../../parsers/gsapWriterAcorn.js";
|
|
14
|
-
import { removeElementFromHtml, patchElementInHtml, probeElementInSource, splitElementInHtml, isHTMLElement, } from "../helpers/sourceMutation.js";
|
|
15
|
-
import { parseHTML } from "linkedom";
|
|
16
|
-
// ── Server cutover flag ─────────────────────────────────────────────────────
|
|
17
|
-
/**
|
|
18
|
-
* Mirror of the client STUDIO_SDK_CUTOVER_ENABLED flag for server-side writer
|
|
19
|
-
* selection. When true, the acorn writer handles GSAP mutations; otherwise the
|
|
20
|
-
* recast writer (gsapParser.ts) is used. Default false → recast.
|
|
21
|
-
*
|
|
22
|
-
* Enable with: STUDIO_SDK_CUTOVER_ENABLED=true (or =1)
|
|
23
|
-
* Mirrors the client Vite env var name so one env switch flips both sides.
|
|
24
|
-
*/
|
|
25
|
-
function isAcornGsapWriterEnabled() {
|
|
26
|
-
const val = process.env["STUDIO_SDK_CUTOVER_ENABLED"];
|
|
27
|
-
return val === "true" || val === "1";
|
|
28
|
-
}
|
|
29
|
-
/**
|
|
30
|
-
* Lazy-load gsapParser for write ops (recast-backed) — the default server writer.
|
|
31
|
-
* The read path uses the browser-safe acorn parser; this loader is only needed
|
|
32
|
-
* for the recast write path (the default when STUDIO_SDK_CUTOVER_ENABLED is off).
|
|
33
|
-
*/
|
|
34
|
-
async function loadGsapParser() {
|
|
35
|
-
return import("../../parsers/gsapParser.js");
|
|
36
|
-
}
|
|
37
|
-
/** Resolve project + safe absolute path for any project-scoped route. */
|
|
38
|
-
async function resolveProjectPath(c, adapter, pathPrefix, opts) {
|
|
39
|
-
const id = c.req.param("id");
|
|
40
|
-
const project = await adapter.resolveProject(id);
|
|
41
|
-
if (!project) {
|
|
42
|
-
return { error: c.json({ error: "not found" }, 404) };
|
|
43
|
-
}
|
|
44
|
-
const filePath = decodeURIComponent(c.req.path.replace(pathPrefix(project.id), ""));
|
|
45
|
-
if (filePath.includes("\0")) {
|
|
46
|
-
return { error: c.json({ error: "forbidden" }, 403) };
|
|
47
|
-
}
|
|
48
|
-
const absPath = resolveWithinProject(project.dir, filePath);
|
|
49
|
-
if (!absPath) {
|
|
50
|
-
return { error: c.json({ error: "forbidden" }, 403) };
|
|
51
|
-
}
|
|
52
|
-
if (opts?.mustExist && !existsSync(absPath)) {
|
|
53
|
-
return { error: c.json({ error: "not found" }, 404) };
|
|
54
|
-
}
|
|
55
|
-
return { project, filePath, absPath };
|
|
56
|
-
}
|
|
57
|
-
function resolveProjectFile(c, adapter, opts) {
|
|
58
|
-
return resolveProjectPath(c, adapter, (id) => `/projects/${id}/files/`, opts);
|
|
59
|
-
}
|
|
60
|
-
function resolveFileMutationContext(c, adapter, operation) {
|
|
61
|
-
return resolveProjectPath(c, adapter, (id) => `/projects/${id}/file-mutations/${operation}/`);
|
|
62
|
-
}
|
|
63
|
-
/** Write `next` to `absPath` only if it differs from `original`, returning a standardized change response. */
|
|
64
|
-
function writeIfChanged(c, projectDir, filePath, absPath, original, next) {
|
|
65
|
-
if (next === original) {
|
|
66
|
-
return c.json({ ok: true, changed: false, content: original, path: filePath });
|
|
67
|
-
}
|
|
68
|
-
const backup = snapshotBeforeWrite(projectDir, absPath);
|
|
69
|
-
if (backup.error)
|
|
70
|
-
console.warn(`Failed to create backup for ${filePath}: ${backup.error}`);
|
|
71
|
-
writeFileSync(absPath, next, "utf-8");
|
|
72
|
-
return c.json({
|
|
73
|
-
ok: true,
|
|
74
|
-
changed: true,
|
|
75
|
-
content: next,
|
|
76
|
-
path: filePath,
|
|
77
|
-
backupPath: backupPathForResponse(projectDir, backup.backupPath),
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
function rejectUnsafeMutationValues(c, unsafeFields) {
|
|
81
|
-
return c.json({
|
|
82
|
-
error: "mutation contains unsafe values",
|
|
83
|
-
fields: unsafeFields.map((field) => field.path),
|
|
84
|
-
unsafeValues: unsafeFields,
|
|
85
|
-
}, 400);
|
|
86
|
-
}
|
|
87
|
-
/**
|
|
88
|
-
* Parse the request body and validate that `target` is present.
|
|
89
|
-
* Returns `{ error }` if missing, or `{ target, body }` for the full parsed body.
|
|
90
|
-
*/
|
|
91
|
-
async function parseMutationBody(c) {
|
|
92
|
-
const body = (await c.req.json().catch(() => null));
|
|
93
|
-
if (!body?.target) {
|
|
94
|
-
return { error: c.json({ error: "target required" }, 400) };
|
|
95
|
-
}
|
|
96
|
-
return { target: body.target, body };
|
|
97
|
-
}
|
|
98
|
-
/** Ensure the parent directory of a path exists. */
|
|
99
|
-
function ensureDir(filePath) {
|
|
100
|
-
const dir = dirname(filePath);
|
|
101
|
-
if (!existsSync(dir))
|
|
102
|
-
mkdirSync(dir, { recursive: true });
|
|
103
|
-
}
|
|
104
|
-
/**
|
|
105
|
-
* Generate a copy name: foo.html → foo (copy).html → foo (copy 2).html
|
|
106
|
-
*/
|
|
107
|
-
function generateCopyPath(projectDir, originalPath) {
|
|
108
|
-
const ext = originalPath.includes(".") ? "." + originalPath.split(".").pop() : "";
|
|
109
|
-
const base = ext ? originalPath.slice(0, -ext.length) : originalPath;
|
|
110
|
-
// If already a copy, increment the number
|
|
111
|
-
const copyMatch = base.match(/ \(copy(?: (\d+))?\)$/);
|
|
112
|
-
const cleanBase = copyMatch ? base.slice(0, -copyMatch[0].length) : base;
|
|
113
|
-
let num = copyMatch ? (copyMatch[1] ? parseInt(copyMatch[1]) + 1 : 2) : 1;
|
|
114
|
-
let candidate = num === 1 ? `${cleanBase} (copy)${ext}` : `${cleanBase} (copy ${num})${ext}`;
|
|
115
|
-
while (existsSync(resolve(projectDir, candidate))) {
|
|
116
|
-
num++;
|
|
117
|
-
candidate = `${cleanBase} (copy ${num})${ext}`;
|
|
118
|
-
}
|
|
119
|
-
return candidate;
|
|
120
|
-
}
|
|
121
|
-
/**
|
|
122
|
-
* Walk a directory recursively and return all file paths matching a filter.
|
|
123
|
-
*/
|
|
124
|
-
function walkFiles(dir, filter) {
|
|
125
|
-
const results = [];
|
|
126
|
-
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
127
|
-
const full = join(dir, entry.name);
|
|
128
|
-
if (entry.isDirectory()) {
|
|
129
|
-
if (entry.name === "node_modules" || entry.name === ".thumbnails" || entry.name === "renders")
|
|
130
|
-
continue;
|
|
131
|
-
results.push(...walkFiles(full, filter));
|
|
132
|
-
}
|
|
133
|
-
else if (filter(entry.name)) {
|
|
134
|
-
results.push(full);
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
return results;
|
|
138
|
-
}
|
|
139
|
-
/**
|
|
140
|
-
* After a rename, update all references to the old path in project files.
|
|
141
|
-
* Scans HTML, CSS, JS, and JSON files for the old filename/path and replaces.
|
|
142
|
-
*/
|
|
143
|
-
function updateReferences(projectDir, oldPath, newPath) {
|
|
144
|
-
const textFiles = walkFiles(projectDir, (name) => /\.(html|css|js|jsx|ts|tsx|json|mjs|cjs|md|mdx)$/i.test(name));
|
|
145
|
-
let updatedCount = 0;
|
|
146
|
-
for (const file of textFiles) {
|
|
147
|
-
const content = readFileSync(file, "utf-8");
|
|
148
|
-
// Only replace full relative paths — never bare filenames, which can
|
|
149
|
-
// corrupt unrelated content (e.g. "logo.png" inside "my-logo.png").
|
|
150
|
-
if (!content.includes(oldPath))
|
|
151
|
-
continue;
|
|
152
|
-
const updated = content.split(oldPath).join(newPath);
|
|
153
|
-
if (updated !== content) {
|
|
154
|
-
writeFileSync(file, updated, "utf-8");
|
|
155
|
-
updatedCount++;
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
return updatedCount;
|
|
159
|
-
}
|
|
160
|
-
// ── GSAP script extraction ──────────────────────────────────────────────────
|
|
161
|
-
/**
|
|
162
|
-
* Parse an HTML string with linkedom, locate the inline `<script>` that
|
|
163
|
-
* contains GSAP timeline code, and return both its text content and a
|
|
164
|
-
* function that replaces that script block and serialises back to HTML.
|
|
165
|
-
*/
|
|
166
|
-
function extractGsapScriptBlock(html) {
|
|
167
|
-
const { document } = parseHTML(html);
|
|
168
|
-
const scripts = [
|
|
169
|
-
...document.querySelectorAll("script:not([src])"),
|
|
170
|
-
...Array.from(document.querySelectorAll("template")).flatMap((tmpl) => Array.from(tmpl.querySelectorAll("script:not([src])"))),
|
|
171
|
-
];
|
|
172
|
-
for (const script of scripts) {
|
|
173
|
-
const content = script.textContent || "";
|
|
174
|
-
if (content.includes("gsap.timeline") ||
|
|
175
|
-
content.includes(".set(") ||
|
|
176
|
-
content.includes(".to(")) {
|
|
177
|
-
return {
|
|
178
|
-
scriptText: content,
|
|
179
|
-
document,
|
|
180
|
-
replaceScript(newText) {
|
|
181
|
-
script.textContent = newText;
|
|
182
|
-
return document.toString();
|
|
183
|
-
},
|
|
184
|
-
};
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
return null;
|
|
188
|
-
}
|
|
189
|
-
function stripStudioEditsFromTarget(document, selector) {
|
|
190
|
-
if (!selector)
|
|
191
|
-
return 0;
|
|
192
|
-
let stripped = 0;
|
|
193
|
-
try {
|
|
194
|
-
for (const el of document.querySelectorAll(selector)) {
|
|
195
|
-
if (!isHTMLElement(el))
|
|
196
|
-
continue;
|
|
197
|
-
const htmlEl = el;
|
|
198
|
-
let touched = false;
|
|
199
|
-
// Manual path offset (--hf-studio-offset / translate) — a GSAP position tween
|
|
200
|
-
// now owns position, so the stale offset channel must go.
|
|
201
|
-
if (el.getAttribute("data-hf-studio-path-offset")) {
|
|
202
|
-
const originalTranslate = el.getAttribute("data-hf-studio-original-inline-translate");
|
|
203
|
-
htmlEl.style.removeProperty("--hf-studio-offset-x");
|
|
204
|
-
htmlEl.style.removeProperty("--hf-studio-offset-y");
|
|
205
|
-
if (originalTranslate) {
|
|
206
|
-
htmlEl.style.setProperty("translate", originalTranslate);
|
|
207
|
-
}
|
|
208
|
-
else {
|
|
209
|
-
htmlEl.style.removeProperty("translate");
|
|
210
|
-
}
|
|
211
|
-
el.removeAttribute("data-hf-studio-path-offset");
|
|
212
|
-
el.removeAttribute("data-hf-studio-original-translate");
|
|
213
|
-
el.removeAttribute("data-hf-studio-original-inline-translate");
|
|
214
|
-
touched = true;
|
|
215
|
-
}
|
|
216
|
-
// Manual rotation (--hf-studio-rotation / rotate) — likewise, a GSAP rotation
|
|
217
|
-
// set/tween now owns rotation, so clear the legacy CSS-var channel.
|
|
218
|
-
if (el.getAttribute("data-hf-studio-rotation")) {
|
|
219
|
-
const originalRotate = el.getAttribute("data-hf-studio-original-inline-rotate");
|
|
220
|
-
const originalOrigin = el.getAttribute("data-hf-studio-original-rotation-transform-origin");
|
|
221
|
-
htmlEl.style.removeProperty("--hf-studio-rotation");
|
|
222
|
-
if (originalRotate) {
|
|
223
|
-
htmlEl.style.setProperty("rotate", originalRotate);
|
|
224
|
-
}
|
|
225
|
-
else {
|
|
226
|
-
htmlEl.style.removeProperty("rotate");
|
|
227
|
-
}
|
|
228
|
-
if (originalOrigin) {
|
|
229
|
-
htmlEl.style.setProperty("transform-origin", originalOrigin);
|
|
230
|
-
}
|
|
231
|
-
else {
|
|
232
|
-
htmlEl.style.removeProperty("transform-origin");
|
|
233
|
-
}
|
|
234
|
-
el.removeAttribute("data-hf-studio-rotation");
|
|
235
|
-
el.removeAttribute("data-hf-studio-rotation-draft");
|
|
236
|
-
el.removeAttribute("data-hf-studio-original-rotate");
|
|
237
|
-
el.removeAttribute("data-hf-studio-original-inline-rotate");
|
|
238
|
-
el.removeAttribute("data-hf-studio-original-rotation-transform-origin");
|
|
239
|
-
touched = true;
|
|
240
|
-
}
|
|
241
|
-
if (touched)
|
|
242
|
-
stripped++;
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
catch {
|
|
246
|
-
// Invalid selector — skip silently.
|
|
247
|
-
}
|
|
248
|
-
return stripped;
|
|
249
|
-
}
|
|
250
|
-
// A studio path-offset (--hf-studio-offset / data-hf-studio-path-offset) and a GSAP
|
|
251
|
-
// position tween both drive translate — keeping both stacks the offsets (a gesture or
|
|
252
|
-
// drag recorded over a stale offset plays shoved off-position). When a committed tween
|
|
253
|
-
// writes a position property, the tween owns position, so the stale offset must go.
|
|
254
|
-
function keyframesWritePosition(keyframes) {
|
|
255
|
-
return keyframes.some((kf) => Object.keys(kf.properties).some((k) => classifyPropertyGroup(k) === "position"));
|
|
256
|
-
}
|
|
257
|
-
// A studio rotation edit (--hf-studio-rotation / data-hf-studio-rotation) and a GSAP
|
|
258
|
-
// rotation tween both drive rotate — keeping both stacks them. When a committed keyframe
|
|
259
|
-
// set writes a rotation property, the tween owns rotation, so the stale CSS-var channel
|
|
260
|
-
// must go (the position twin of this is `keyframesWritePosition`).
|
|
261
|
-
function keyframesWriteRotation(keyframes) {
|
|
262
|
-
return keyframes.some((kf) => Object.keys(kf.properties).some((k) => classifyPropertyGroup(k) === "rotation"));
|
|
263
|
-
}
|
|
264
|
-
function lastKeyframeOpacity(kfs) {
|
|
265
|
-
if (!kfs)
|
|
266
|
-
return undefined;
|
|
267
|
-
for (let i = kfs.keyframes.length - 1; i >= 0; i--) {
|
|
268
|
-
if ("opacity" in kfs.keyframes[i].properties)
|
|
269
|
-
return kfs.keyframes[i].properties.opacity;
|
|
270
|
-
}
|
|
271
|
-
return undefined;
|
|
272
|
-
}
|
|
273
|
-
function resolveFinalOpacity(anim) {
|
|
274
|
-
if (anim.method === "from")
|
|
275
|
-
return null;
|
|
276
|
-
const raw = anim.keyframes ? lastKeyframeOpacity(anim.keyframes) : anim.properties.opacity;
|
|
277
|
-
if (raw == null)
|
|
278
|
-
return null;
|
|
279
|
-
if (typeof raw === "string" && /^[+\-*]=/.test(raw))
|
|
280
|
-
return null;
|
|
281
|
-
const num = Number(raw);
|
|
282
|
-
return Number.isFinite(num) && num !== 0 ? num : null;
|
|
283
|
-
}
|
|
284
|
-
function bakeVisibilityOnDelete(document, anim) {
|
|
285
|
-
const opacity = resolveFinalOpacity(anim);
|
|
286
|
-
if (opacity === null)
|
|
287
|
-
return;
|
|
288
|
-
try {
|
|
289
|
-
for (const el of document.querySelectorAll(anim.targetSelector)) {
|
|
290
|
-
if (isHTMLElement(el))
|
|
291
|
-
el.style.setProperty("opacity", String(opacity));
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
catch {
|
|
295
|
-
// Invalid selector — skip silently.
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
// Mutations that can change a position tween's first keyframe (value/existence/timing)
|
|
299
|
-
// and therefore require the pre-keyframe hold-`set`s to be re-synced afterwards.
|
|
300
|
-
// `syncPositionHoldsBeforeKeyframes` rebuilds all `hf-hold` sets from scratch: it acts
|
|
301
|
-
// on every tween that has keyframes whose first percentage carries a position prop and
|
|
302
|
-
// whose start is > 0. So any mutation that creates such a tween, retargets it, or moves
|
|
303
|
-
// its start across the t=0 boundary must trigger a re-sync.
|
|
304
|
-
const HOLD_SYNC_MUTATION_TYPES = new Set([
|
|
305
|
-
"add-keyframe",
|
|
306
|
-
"update-keyframe",
|
|
307
|
-
"remove-keyframe",
|
|
308
|
-
"remove-all-keyframes",
|
|
309
|
-
"add-with-keyframes",
|
|
310
|
-
"replace-with-keyframes",
|
|
311
|
-
"convert-to-keyframes",
|
|
312
|
-
"materialize-keyframes",
|
|
313
|
-
"update-motion-path-point",
|
|
314
|
-
"add-motion-path-point",
|
|
315
|
-
"remove-motion-path-point",
|
|
316
|
-
// Authors a fresh motionPath tween whose parsed first keyframe is (0,0); if it lands
|
|
317
|
-
// at position > 0 the element snaps home at t=0 without a pre-tween hold-`set`.
|
|
318
|
-
"add-motion-path",
|
|
319
|
-
// Can move a tween's `position` (start) across the t=0 boundary, which flips whether a
|
|
320
|
-
// keyframed position tween needs a hold (started at 0 → moved later, or vice versa).
|
|
321
|
-
"update-meta",
|
|
322
|
-
// Time-shift / time-scale tweens, which can move a keyframed position tween's start
|
|
323
|
-
// across t=0, flipping hold need; stale holds are not repositioned by these ops.
|
|
324
|
-
"shift-positions",
|
|
325
|
-
"scale-positions",
|
|
326
|
-
// Retargets keyframed position tweens to a cloned element's selector; the old hold is
|
|
327
|
-
// keyed to the prior selector, so holds must be rebuilt for the new target.
|
|
328
|
-
"split-animations",
|
|
329
|
-
"delete",
|
|
330
|
-
"delete-all-for-selector",
|
|
331
|
-
]);
|
|
332
|
-
async function executeGsapMutation(body, block, respond) {
|
|
333
|
-
// When the server cutover flag is enabled, delegate to the acorn writer;
|
|
334
|
-
// otherwise use the recast writer (gsapParser.ts) as the default.
|
|
335
|
-
if (!isAcornGsapWriterEnabled()) {
|
|
336
|
-
return executeGsapMutationRecast(body, block, respond);
|
|
337
|
-
}
|
|
338
|
-
return executeGsapMutationAcorn(body, block, respond);
|
|
339
|
-
}
|
|
340
|
-
function executeGsapMutationAcorn(body, block, respond) {
|
|
341
|
-
function requireAnimation(scriptText, animationId) {
|
|
342
|
-
const parsed = parseGsapScriptAcorn(scriptText);
|
|
343
|
-
const anim = parsed.animations.find((a) => a.id === animationId);
|
|
344
|
-
if (!anim)
|
|
345
|
-
return { err: respond({ error: "animation not found" }, 404) };
|
|
346
|
-
return { anim };
|
|
347
|
-
}
|
|
348
|
-
function requireFromToAnimation(scriptText, animationId) {
|
|
349
|
-
const result = requireAnimation(scriptText, animationId);
|
|
350
|
-
if ("err" in result)
|
|
351
|
-
return result;
|
|
352
|
-
if (result.anim.method !== "fromTo")
|
|
353
|
-
return { err: respond({ error: "animation is not a fromTo" }, 400) };
|
|
354
|
-
return result;
|
|
355
|
-
}
|
|
356
|
-
switch (body.type) {
|
|
357
|
-
case "update-property":
|
|
358
|
-
case "add-property": {
|
|
359
|
-
const r = requireAnimation(block.scriptText, body.animationId);
|
|
360
|
-
if ("err" in r)
|
|
361
|
-
return r.err;
|
|
362
|
-
const val = body.type === "update-property" ? body.value : body.defaultValue;
|
|
363
|
-
return updateAnimationInScript(block.scriptText, body.animationId, {
|
|
364
|
-
properties: { ...r.anim.properties, [body.property]: val },
|
|
365
|
-
});
|
|
366
|
-
}
|
|
367
|
-
case "update-properties": {
|
|
368
|
-
const r = requireAnimation(block.scriptText, body.animationId);
|
|
369
|
-
if ("err" in r)
|
|
370
|
-
return r.err;
|
|
371
|
-
return updateAnimationInScript(block.scriptText, body.animationId, {
|
|
372
|
-
properties: { ...r.anim.properties, ...body.properties },
|
|
373
|
-
});
|
|
374
|
-
}
|
|
375
|
-
case "update-from-property":
|
|
376
|
-
case "add-from-property": {
|
|
377
|
-
const r = requireFromToAnimation(block.scriptText, body.animationId);
|
|
378
|
-
if ("err" in r)
|
|
379
|
-
return r.err;
|
|
380
|
-
const val = body.type === "update-from-property" ? body.value : body.defaultValue;
|
|
381
|
-
return updateAnimationInScript(block.scriptText, body.animationId, {
|
|
382
|
-
fromProperties: { ...(r.anim.fromProperties ?? {}), [body.property]: val },
|
|
383
|
-
});
|
|
384
|
-
}
|
|
385
|
-
case "update-meta": {
|
|
386
|
-
return updateAnimationInScript(block.scriptText, body.animationId, body.updates);
|
|
387
|
-
}
|
|
388
|
-
case "add": {
|
|
389
|
-
if (body.fromProperties && body.method !== "fromTo") {
|
|
390
|
-
return respond({ error: "fromProperties is only valid for method=fromTo" }, 400);
|
|
391
|
-
}
|
|
392
|
-
const result = addAnimationToScript(block.scriptText, {
|
|
393
|
-
targetSelector: body.targetSelector,
|
|
394
|
-
method: body.method,
|
|
395
|
-
position: body.position,
|
|
396
|
-
duration: body.duration,
|
|
397
|
-
ease: body.ease,
|
|
398
|
-
properties: body.properties,
|
|
399
|
-
fromProperties: body.fromProperties,
|
|
400
|
-
...(body.global ? { global: true } : {}),
|
|
401
|
-
});
|
|
402
|
-
return result.script;
|
|
403
|
-
}
|
|
404
|
-
case "delete": {
|
|
405
|
-
const delTarget = requireAnimation(block.scriptText, body.animationId);
|
|
406
|
-
if (!("err" in delTarget) && body.stripStudioEdits) {
|
|
407
|
-
stripStudioEditsFromTarget(block.document, delTarget.anim.targetSelector);
|
|
408
|
-
bakeVisibilityOnDelete(block.document, delTarget.anim);
|
|
409
|
-
}
|
|
410
|
-
return removeAnimationFromScript(block.scriptText, body.animationId);
|
|
411
|
-
}
|
|
412
|
-
case "delete-all-for-selector": {
|
|
413
|
-
const parsed = parseGsapScriptAcorn(block.scriptText);
|
|
414
|
-
const matching = parsed.animations.filter((a) => a.targetSelector === body.targetSelector);
|
|
415
|
-
if (matching.length === 0)
|
|
416
|
-
return block.scriptText;
|
|
417
|
-
stripStudioEditsFromTarget(block.document, body.targetSelector);
|
|
418
|
-
let script = block.scriptText;
|
|
419
|
-
for (const anim of matching.reverse()) {
|
|
420
|
-
script = removeAnimationFromScript(script, anim.id);
|
|
421
|
-
}
|
|
422
|
-
return script;
|
|
423
|
-
}
|
|
424
|
-
case "remove-property": {
|
|
425
|
-
const r = requireAnimation(block.scriptText, body.animationId);
|
|
426
|
-
if ("err" in r)
|
|
427
|
-
return r.err;
|
|
428
|
-
const filtered = { ...r.anim.properties };
|
|
429
|
-
delete filtered[body.property];
|
|
430
|
-
return updateAnimationInScript(block.scriptText, body.animationId, {
|
|
431
|
-
properties: filtered,
|
|
432
|
-
});
|
|
433
|
-
}
|
|
434
|
-
case "remove-from-property": {
|
|
435
|
-
const r = requireFromToAnimation(block.scriptText, body.animationId);
|
|
436
|
-
if ("err" in r)
|
|
437
|
-
return r.err;
|
|
438
|
-
const filtered = { ...(r.anim.fromProperties ?? {}) };
|
|
439
|
-
delete filtered[body.property];
|
|
440
|
-
return updateAnimationInScript(block.scriptText, body.animationId, {
|
|
441
|
-
fromProperties: filtered,
|
|
442
|
-
});
|
|
443
|
-
}
|
|
444
|
-
case "add-keyframe": {
|
|
445
|
-
return addKeyframeToScript(block.scriptText, body.animationId, body.percentage, body.properties, body.ease, body.backfillDefaults);
|
|
446
|
-
}
|
|
447
|
-
case "remove-keyframe": {
|
|
448
|
-
return removeKeyframeFromScript(block.scriptText, body.animationId, body.percentage);
|
|
449
|
-
}
|
|
450
|
-
case "update-keyframe": {
|
|
451
|
-
return updateKeyframeInScript(block.scriptText, body.animationId, body.percentage, body.properties, body.ease);
|
|
452
|
-
}
|
|
453
|
-
case "convert-to-keyframes": {
|
|
454
|
-
return convertToKeyframesFromScript(block.scriptText, body.animationId, body.resolvedFromValues, body.duration);
|
|
455
|
-
}
|
|
456
|
-
case "remove-all-keyframes": {
|
|
457
|
-
const preCollapse = requireAnimation(block.scriptText, body.animationId);
|
|
458
|
-
if (!("err" in preCollapse)) {
|
|
459
|
-
bakeVisibilityOnDelete(block.document, preCollapse.anim);
|
|
460
|
-
}
|
|
461
|
-
return removeAllKeyframesFromScript(block.scriptText, body.animationId);
|
|
462
|
-
}
|
|
463
|
-
case "materialize-keyframes": {
|
|
464
|
-
if (body.allElements && body.allElements.length > 0) {
|
|
465
|
-
return unrollDynamicAnimations(block.scriptText, body.animationId, body.allElements);
|
|
466
|
-
}
|
|
467
|
-
return materializeKeyframesFromScript(block.scriptText, body.animationId, body.keyframes, body.easeEach, body.resolvedSelector);
|
|
468
|
-
}
|
|
469
|
-
case "set-arc-path": {
|
|
470
|
-
return setArcPathInScript(block.scriptText, body.animationId, {
|
|
471
|
-
enabled: body.enabled,
|
|
472
|
-
autoRotate: body.autoRotate ?? false,
|
|
473
|
-
segments: body.segments ?? [],
|
|
474
|
-
});
|
|
475
|
-
}
|
|
476
|
-
case "update-arc-segment": {
|
|
477
|
-
return updateArcSegmentInScript(block.scriptText, body.animationId, body.segmentIndex, {
|
|
478
|
-
...(body.curviness !== undefined ? { curviness: body.curviness } : {}),
|
|
479
|
-
...(body.cp1 ? { cp1: body.cp1 } : {}),
|
|
480
|
-
...(body.cp2 ? { cp2: body.cp2 } : {}),
|
|
481
|
-
});
|
|
482
|
-
}
|
|
483
|
-
case "remove-arc-path": {
|
|
484
|
-
return removeArcPathFromScript(block.scriptText, body.animationId);
|
|
485
|
-
}
|
|
486
|
-
case "add-with-keyframes": {
|
|
487
|
-
const result = addAnimationWithKeyframesToScript(block.scriptText, body.targetSelector, body.position, body.duration, body.keyframes, body.ease, body.easeEach);
|
|
488
|
-
return result.script;
|
|
489
|
-
}
|
|
490
|
-
case "replace-with-keyframes": {
|
|
491
|
-
const script = removeAnimationFromScript(block.scriptText, body.animationId);
|
|
492
|
-
const added = addAnimationWithKeyframesToScript(script, body.targetSelector, body.position, body.duration, body.keyframes, body.ease);
|
|
493
|
-
return added.script;
|
|
494
|
-
}
|
|
495
|
-
case "split-animations": {
|
|
496
|
-
if (typeof body.originalId !== "string" ||
|
|
497
|
-
!body.originalId ||
|
|
498
|
-
typeof body.newId !== "string" ||
|
|
499
|
-
!body.newId ||
|
|
500
|
-
typeof body.splitTime !== "number" ||
|
|
501
|
-
!Number.isFinite(body.splitTime) ||
|
|
502
|
-
typeof body.elementStart !== "number" ||
|
|
503
|
-
!Number.isFinite(body.elementStart) ||
|
|
504
|
-
typeof body.elementDuration !== "number" ||
|
|
505
|
-
!Number.isFinite(body.elementDuration) ||
|
|
506
|
-
body.elementDuration <= 0) {
|
|
507
|
-
return respond({
|
|
508
|
-
error: "split-animations requires originalId, newId (non-empty strings), splitTime, elementStart (finite numbers), and elementDuration (positive number)",
|
|
509
|
-
}, 400);
|
|
510
|
-
}
|
|
511
|
-
return splitAnimationsInScript(block.scriptText, {
|
|
512
|
-
originalId: body.originalId,
|
|
513
|
-
newId: body.newId,
|
|
514
|
-
splitTime: body.splitTime,
|
|
515
|
-
elementStart: body.elementStart,
|
|
516
|
-
elementDuration: body.elementDuration,
|
|
517
|
-
});
|
|
518
|
-
}
|
|
519
|
-
case "split-into-property-groups": {
|
|
520
|
-
const result = splitIntoPropertyGroupsFromScript(block.scriptText, body.animationId);
|
|
521
|
-
return result.script;
|
|
522
|
-
}
|
|
523
|
-
case "unroll-timeline": {
|
|
524
|
-
return unrollComputedTimeline(block.scriptText);
|
|
525
|
-
}
|
|
526
|
-
case "shift-positions": {
|
|
527
|
-
const { targetSelector, delta } = body;
|
|
528
|
-
if (!targetSelector || !Number.isFinite(delta) || delta === 0)
|
|
529
|
-
return block.scriptText;
|
|
530
|
-
return shiftPositionsInScript(block.scriptText, targetSelector, delta);
|
|
531
|
-
}
|
|
532
|
-
case "scale-positions": {
|
|
533
|
-
const { targetSelector, oldStart, oldDuration, newStart, newDuration } = body;
|
|
534
|
-
if (!targetSelector ||
|
|
535
|
-
!Number.isFinite(oldStart) ||
|
|
536
|
-
!Number.isFinite(oldDuration) ||
|
|
537
|
-
!Number.isFinite(newStart) ||
|
|
538
|
-
!Number.isFinite(newDuration) ||
|
|
539
|
-
oldDuration <= 0 ||
|
|
540
|
-
newDuration <= 0)
|
|
541
|
-
return block.scriptText;
|
|
542
|
-
if (oldStart === newStart && oldDuration === newDuration)
|
|
543
|
-
return block.scriptText;
|
|
544
|
-
return scalePositionsInScript(block.scriptText, targetSelector, oldStart, oldDuration, newStart, newDuration);
|
|
545
|
-
}
|
|
546
|
-
default:
|
|
547
|
-
return respond({ error: `unknown mutation type: ${body.type}` }, 400);
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
async function executeGsapMutationRecast(body, block, respond) {
|
|
551
|
-
const parser = await loadGsapParser();
|
|
552
|
-
const { updateAnimationInScript, addAnimationToScript, removeAnimationFromScript, addKeyframeToScript, removeKeyframeFromScript, updateKeyframeInScript, convertToKeyframesInScript, removeAllKeyframesFromScript, materializeKeyframesInScript, unrollDynamicAnimations, setArcPathInScript, updateArcSegmentInScript, updateMotionPathPointInScript, addMotionPathPointInScript, removeMotionPathPointInScript, addMotionPathToScript, removeArcPathFromScript, addAnimationWithKeyframesToScript, splitAnimationsInScript, splitIntoPropertyGroups, } = parser;
|
|
553
|
-
function requireAnimation(scriptText, animationId) {
|
|
554
|
-
const parsed = parseGsapScriptAcorn(scriptText);
|
|
555
|
-
const anim = parsed.animations.find((a) => a.id === animationId);
|
|
556
|
-
if (!anim)
|
|
557
|
-
return { err: respond({ error: "animation not found" }, 404) };
|
|
558
|
-
return { anim };
|
|
559
|
-
}
|
|
560
|
-
function requireFromToAnimation(scriptText, animationId) {
|
|
561
|
-
const result = requireAnimation(scriptText, animationId);
|
|
562
|
-
if ("err" in result)
|
|
563
|
-
return result;
|
|
564
|
-
if (result.anim.method !== "fromTo")
|
|
565
|
-
return { err: respond({ error: "animation is not a fromTo" }, 400) };
|
|
566
|
-
return result;
|
|
567
|
-
}
|
|
568
|
-
switch (body.type) {
|
|
569
|
-
case "update-property":
|
|
570
|
-
case "add-property": {
|
|
571
|
-
const r = requireAnimation(block.scriptText, body.animationId);
|
|
572
|
-
if ("err" in r)
|
|
573
|
-
return r.err;
|
|
574
|
-
const val = body.type === "update-property" ? body.value : body.defaultValue;
|
|
575
|
-
return updateAnimationInScript(block.scriptText, body.animationId, {
|
|
576
|
-
properties: { ...r.anim.properties, [body.property]: val },
|
|
577
|
-
});
|
|
578
|
-
}
|
|
579
|
-
case "update-properties": {
|
|
580
|
-
const r = requireAnimation(block.scriptText, body.animationId);
|
|
581
|
-
if ("err" in r)
|
|
582
|
-
return r.err;
|
|
583
|
-
return updateAnimationInScript(block.scriptText, body.animationId, {
|
|
584
|
-
properties: { ...r.anim.properties, ...body.properties },
|
|
585
|
-
});
|
|
586
|
-
}
|
|
587
|
-
case "update-from-property":
|
|
588
|
-
case "add-from-property": {
|
|
589
|
-
const r = requireFromToAnimation(block.scriptText, body.animationId);
|
|
590
|
-
if ("err" in r)
|
|
591
|
-
return r.err;
|
|
592
|
-
const val = body.type === "update-from-property" ? body.value : body.defaultValue;
|
|
593
|
-
return updateAnimationInScript(block.scriptText, body.animationId, {
|
|
594
|
-
fromProperties: { ...(r.anim.fromProperties ?? {}), [body.property]: val },
|
|
595
|
-
});
|
|
596
|
-
}
|
|
597
|
-
case "update-meta": {
|
|
598
|
-
return updateAnimationInScript(block.scriptText, body.animationId, body.updates);
|
|
599
|
-
}
|
|
600
|
-
case "add": {
|
|
601
|
-
if (body.fromProperties && body.method !== "fromTo") {
|
|
602
|
-
return respond({ error: "fromProperties is only valid for method=fromTo" }, 400);
|
|
603
|
-
}
|
|
604
|
-
// A new position/rotation animation owns that channel — strip the matching
|
|
605
|
-
// legacy studio CSS var (--hf-studio-offset / --hf-studio-rotation) so it can't
|
|
606
|
-
// double with the tween, matching add-with-keyframes/replace-with-keyframes.
|
|
607
|
-
if (Object.keys(body.properties).some((k) => {
|
|
608
|
-
const group = classifyPropertyGroup(k);
|
|
609
|
-
return group === "position" || group === "rotation";
|
|
610
|
-
})) {
|
|
611
|
-
stripStudioEditsFromTarget(block.document, body.targetSelector);
|
|
612
|
-
}
|
|
613
|
-
const result = addAnimationToScript(block.scriptText, {
|
|
614
|
-
targetSelector: body.targetSelector,
|
|
615
|
-
method: body.method,
|
|
616
|
-
position: body.position,
|
|
617
|
-
duration: body.duration,
|
|
618
|
-
ease: body.ease,
|
|
619
|
-
properties: body.properties,
|
|
620
|
-
fromProperties: body.fromProperties,
|
|
621
|
-
...(body.global ? { global: true } : {}),
|
|
622
|
-
});
|
|
623
|
-
return result.script;
|
|
624
|
-
}
|
|
625
|
-
case "delete": {
|
|
626
|
-
const delTarget = requireAnimation(block.scriptText, body.animationId);
|
|
627
|
-
if (!("err" in delTarget) && body.stripStudioEdits) {
|
|
628
|
-
stripStudioEditsFromTarget(block.document, delTarget.anim.targetSelector);
|
|
629
|
-
bakeVisibilityOnDelete(block.document, delTarget.anim);
|
|
630
|
-
}
|
|
631
|
-
return removeAnimationFromScript(block.scriptText, body.animationId);
|
|
632
|
-
}
|
|
633
|
-
case "delete-all-for-selector": {
|
|
634
|
-
const parsed = parseGsapScriptAcorn(block.scriptText);
|
|
635
|
-
const matching = parsed.animations.filter((a) => a.targetSelector === body.targetSelector);
|
|
636
|
-
if (matching.length === 0)
|
|
637
|
-
return block.scriptText;
|
|
638
|
-
stripStudioEditsFromTarget(block.document, body.targetSelector);
|
|
639
|
-
let script = block.scriptText;
|
|
640
|
-
for (const anim of matching.reverse()) {
|
|
641
|
-
script = removeAnimationFromScript(script, anim.id);
|
|
642
|
-
}
|
|
643
|
-
return script;
|
|
644
|
-
}
|
|
645
|
-
case "remove-property": {
|
|
646
|
-
const r = requireAnimation(block.scriptText, body.animationId);
|
|
647
|
-
if ("err" in r)
|
|
648
|
-
return r.err;
|
|
649
|
-
const filtered = { ...r.anim.properties };
|
|
650
|
-
delete filtered[body.property];
|
|
651
|
-
return updateAnimationInScript(block.scriptText, body.animationId, {
|
|
652
|
-
properties: filtered,
|
|
653
|
-
});
|
|
654
|
-
}
|
|
655
|
-
case "remove-from-property": {
|
|
656
|
-
const r = requireFromToAnimation(block.scriptText, body.animationId);
|
|
657
|
-
if ("err" in r)
|
|
658
|
-
return r.err;
|
|
659
|
-
const filtered = { ...(r.anim.fromProperties ?? {}) };
|
|
660
|
-
delete filtered[body.property];
|
|
661
|
-
return updateAnimationInScript(block.scriptText, body.animationId, {
|
|
662
|
-
fromProperties: filtered,
|
|
663
|
-
});
|
|
664
|
-
}
|
|
665
|
-
case "add-keyframe": {
|
|
666
|
-
return addKeyframeToScript(block.scriptText, body.animationId, body.percentage, body.properties, body.ease, body.backfillDefaults);
|
|
667
|
-
}
|
|
668
|
-
case "remove-keyframe": {
|
|
669
|
-
return removeKeyframeFromScript(block.scriptText, body.animationId, body.percentage);
|
|
670
|
-
}
|
|
671
|
-
case "update-keyframe": {
|
|
672
|
-
return updateKeyframeInScript(block.scriptText, body.animationId, body.percentage, body.properties, body.ease);
|
|
673
|
-
}
|
|
674
|
-
case "convert-to-keyframes": {
|
|
675
|
-
return convertToKeyframesInScript(block.scriptText, body.animationId, body.resolvedFromValues, body.duration);
|
|
676
|
-
}
|
|
677
|
-
case "remove-all-keyframes": {
|
|
678
|
-
const preCollapse = requireAnimation(block.scriptText, body.animationId);
|
|
679
|
-
if (!("err" in preCollapse)) {
|
|
680
|
-
bakeVisibilityOnDelete(block.document, preCollapse.anim);
|
|
681
|
-
}
|
|
682
|
-
return removeAllKeyframesFromScript(block.scriptText, body.animationId);
|
|
683
|
-
}
|
|
684
|
-
case "materialize-keyframes": {
|
|
685
|
-
if (body.allElements && body.allElements.length > 0) {
|
|
686
|
-
return unrollDynamicAnimations(block.scriptText, body.animationId, body.allElements);
|
|
687
|
-
}
|
|
688
|
-
return materializeKeyframesInScript(block.scriptText, body.animationId, body.keyframes, body.easeEach, body.resolvedSelector);
|
|
689
|
-
}
|
|
690
|
-
case "set-arc-path": {
|
|
691
|
-
return setArcPathInScript(block.scriptText, body.animationId, {
|
|
692
|
-
enabled: body.enabled,
|
|
693
|
-
autoRotate: body.autoRotate ?? false,
|
|
694
|
-
segments: body.segments ?? [],
|
|
695
|
-
});
|
|
696
|
-
}
|
|
697
|
-
case "update-arc-segment": {
|
|
698
|
-
return updateArcSegmentInScript(block.scriptText, body.animationId, body.segmentIndex, {
|
|
699
|
-
...(body.curviness !== undefined ? { curviness: body.curviness } : {}),
|
|
700
|
-
...(body.cp1 ? { cp1: body.cp1 } : {}),
|
|
701
|
-
...(body.cp2 ? { cp2: body.cp2 } : {}),
|
|
702
|
-
});
|
|
703
|
-
}
|
|
704
|
-
case "update-motion-path-point": {
|
|
705
|
-
return updateMotionPathPointInScript(block.scriptText, body.animationId, body.pointIndex, {
|
|
706
|
-
x: body.x,
|
|
707
|
-
y: body.y,
|
|
708
|
-
});
|
|
709
|
-
}
|
|
710
|
-
case "add-motion-path-point": {
|
|
711
|
-
return addMotionPathPointInScript(block.scriptText, body.animationId, body.index, {
|
|
712
|
-
x: body.x,
|
|
713
|
-
y: body.y,
|
|
714
|
-
});
|
|
715
|
-
}
|
|
716
|
-
case "remove-motion-path-point": {
|
|
717
|
-
return removeMotionPathPointInScript(block.scriptText, body.animationId, body.index);
|
|
718
|
-
}
|
|
719
|
-
case "add-motion-path": {
|
|
720
|
-
const result = addMotionPathToScript(block.scriptText, body.targetSelector, body.position, body.duration, { x: body.x, y: body.y }, body.ease);
|
|
721
|
-
return result.script;
|
|
722
|
-
}
|
|
723
|
-
case "remove-arc-path": {
|
|
724
|
-
return removeArcPathFromScript(block.scriptText, body.animationId);
|
|
725
|
-
}
|
|
726
|
-
case "add-with-keyframes": {
|
|
727
|
-
if (keyframesWritePosition(body.keyframes) || keyframesWriteRotation(body.keyframes)) {
|
|
728
|
-
stripStudioEditsFromTarget(block.document, body.targetSelector);
|
|
729
|
-
}
|
|
730
|
-
const result = addAnimationWithKeyframesToScript(block.scriptText, body.targetSelector, body.position, body.duration, body.keyframes, body.ease);
|
|
731
|
-
return result.script;
|
|
732
|
-
}
|
|
733
|
-
case "replace-with-keyframes": {
|
|
734
|
-
if (keyframesWritePosition(body.keyframes) || keyframesWriteRotation(body.keyframes)) {
|
|
735
|
-
stripStudioEditsFromTarget(block.document, body.targetSelector);
|
|
736
|
-
}
|
|
737
|
-
const script = removeAnimationFromScript(block.scriptText, body.animationId);
|
|
738
|
-
const added = addAnimationWithKeyframesToScript(script, body.targetSelector, body.position, body.duration, body.keyframes, body.ease);
|
|
739
|
-
return added.script;
|
|
740
|
-
}
|
|
741
|
-
case "split-animations": {
|
|
742
|
-
if (typeof body.originalId !== "string" ||
|
|
743
|
-
!body.originalId ||
|
|
744
|
-
typeof body.newId !== "string" ||
|
|
745
|
-
!body.newId ||
|
|
746
|
-
typeof body.splitTime !== "number" ||
|
|
747
|
-
!Number.isFinite(body.splitTime) ||
|
|
748
|
-
typeof body.elementStart !== "number" ||
|
|
749
|
-
!Number.isFinite(body.elementStart) ||
|
|
750
|
-
typeof body.elementDuration !== "number" ||
|
|
751
|
-
!Number.isFinite(body.elementDuration) ||
|
|
752
|
-
body.elementDuration <= 0) {
|
|
753
|
-
return respond({
|
|
754
|
-
error: "split-animations requires originalId, newId (non-empty strings), splitTime, elementStart (finite numbers), and elementDuration (positive number)",
|
|
755
|
-
}, 400);
|
|
756
|
-
}
|
|
757
|
-
return splitAnimationsInScript(block.scriptText, {
|
|
758
|
-
originalId: body.originalId,
|
|
759
|
-
newId: body.newId,
|
|
760
|
-
splitTime: body.splitTime,
|
|
761
|
-
elementStart: body.elementStart,
|
|
762
|
-
elementDuration: body.elementDuration,
|
|
763
|
-
});
|
|
764
|
-
}
|
|
765
|
-
case "split-into-property-groups": {
|
|
766
|
-
const result = splitIntoPropertyGroups(block.scriptText, body.animationId);
|
|
767
|
-
return result.script;
|
|
768
|
-
}
|
|
769
|
-
case "unroll-timeline": {
|
|
770
|
-
return unrollComputedTimeline(block.scriptText);
|
|
771
|
-
}
|
|
772
|
-
case "shift-positions": {
|
|
773
|
-
const { targetSelector, delta } = body;
|
|
774
|
-
if (!targetSelector || !Number.isFinite(delta) || delta === 0)
|
|
775
|
-
return block.scriptText;
|
|
776
|
-
const { shiftPositionsInScript } = parser;
|
|
777
|
-
return shiftPositionsInScript(block.scriptText, targetSelector, delta);
|
|
778
|
-
}
|
|
779
|
-
case "scale-positions": {
|
|
780
|
-
const { targetSelector, oldStart, oldDuration, newStart, newDuration } = body;
|
|
781
|
-
if (!targetSelector ||
|
|
782
|
-
!Number.isFinite(oldStart) ||
|
|
783
|
-
!Number.isFinite(oldDuration) ||
|
|
784
|
-
!Number.isFinite(newStart) ||
|
|
785
|
-
!Number.isFinite(newDuration) ||
|
|
786
|
-
oldDuration <= 0 ||
|
|
787
|
-
newDuration <= 0)
|
|
788
|
-
return block.scriptText;
|
|
789
|
-
if (oldStart === newStart && oldDuration === newDuration)
|
|
790
|
-
return block.scriptText;
|
|
791
|
-
const { scalePositionsInScript } = parser;
|
|
792
|
-
return scalePositionsInScript(block.scriptText, targetSelector, oldStart, oldDuration, newStart, newDuration);
|
|
793
|
-
}
|
|
794
|
-
default:
|
|
795
|
-
return respond({ error: `unknown mutation type: ${body.type}` }, 400);
|
|
796
|
-
}
|
|
797
|
-
}
|
|
798
|
-
// ── Upload file processing ──────────────────────────────────────────────────
|
|
799
|
-
async function processUploadedFiles(formData, targetDir, projectDir) {
|
|
800
|
-
const MAX_UPLOAD_BYTES = 500 * 1024 * 1024; // 500 MB per file
|
|
801
|
-
const uploaded = [];
|
|
802
|
-
const skipped = [];
|
|
803
|
-
const invalid = [];
|
|
804
|
-
const entries = formData.entries();
|
|
805
|
-
// Derive the subdirectory prefix from targetDir relative to projectDir
|
|
806
|
-
const subDir = targetDir === projectDir ? "" : targetDir.slice(projectDir.length + 1);
|
|
807
|
-
for (const [, value] of entries) {
|
|
808
|
-
if (typeof value === "string")
|
|
809
|
-
continue;
|
|
810
|
-
// Strip path separators — browsers may include directory components
|
|
811
|
-
const name = value.name.split("/").pop()?.split("\\").pop() ?? "";
|
|
812
|
-
if (!name || name.includes("\0") || name.includes(".."))
|
|
813
|
-
continue;
|
|
814
|
-
// Reject individual files that exceed the size limit
|
|
815
|
-
if (value.size > MAX_UPLOAD_BYTES) {
|
|
816
|
-
skipped.push(name);
|
|
817
|
-
continue;
|
|
818
|
-
}
|
|
819
|
-
const destPath = resolve(targetDir, name);
|
|
820
|
-
if (!isSafePath(projectDir, destPath))
|
|
821
|
-
continue;
|
|
822
|
-
// Don't overwrite — append (2), (3), etc.
|
|
823
|
-
let finalPath = destPath;
|
|
824
|
-
let finalName = name;
|
|
825
|
-
if (existsSync(finalPath)) {
|
|
826
|
-
// Handle dotfiles correctly: .gitignore → ext="", base=".gitignore"
|
|
827
|
-
const dotIdx = name.indexOf(".", name.startsWith(".") ? 1 : 0);
|
|
828
|
-
const ext = dotIdx > 0 ? name.slice(dotIdx) : "";
|
|
829
|
-
const base = dotIdx > 0 ? name.slice(0, dotIdx) : name;
|
|
830
|
-
let n = 2;
|
|
831
|
-
const MAX_COPY_INDEX = 10000;
|
|
832
|
-
while (n < MAX_COPY_INDEX && existsSync(resolve(targetDir, `${base} (${n})${ext}`)))
|
|
833
|
-
n++;
|
|
834
|
-
if (n >= MAX_COPY_INDEX) {
|
|
835
|
-
skipped.push(name);
|
|
836
|
-
continue;
|
|
837
|
-
}
|
|
838
|
-
finalName = `${base} (${n})${ext}`;
|
|
839
|
-
finalPath = resolve(targetDir, finalName);
|
|
840
|
-
}
|
|
841
|
-
const buffer = Buffer.from(await value.arrayBuffer());
|
|
842
|
-
const validation = validateUploadedMediaBuffer(finalName, buffer);
|
|
843
|
-
if (!validation.ok) {
|
|
844
|
-
invalid.push({ name: finalName, reason: validation.reason });
|
|
845
|
-
continue;
|
|
846
|
-
}
|
|
847
|
-
writeFileSync(finalPath, buffer);
|
|
848
|
-
const relativePath = subDir ? join(subDir, finalName) : finalName;
|
|
849
|
-
uploaded.push(relativePath);
|
|
850
|
-
if (isAudioFile(finalName)) {
|
|
851
|
-
generateWaveformCache(projectDir, relativePath).catch(() => { });
|
|
852
|
-
}
|
|
853
|
-
}
|
|
854
|
-
return { uploaded, skipped, invalid };
|
|
855
|
-
}
|
|
856
|
-
// ── Route registration ──────────────────────────────────────────────────────
|
|
857
|
-
export function registerFileRoutes(api, adapter) {
|
|
858
|
-
// ── Read ──
|
|
859
|
-
api.get("/projects/:id/files/*", async (c) => {
|
|
860
|
-
const res = await resolveProjectFile(c, adapter);
|
|
861
|
-
if ("error" in res)
|
|
862
|
-
return res.error;
|
|
863
|
-
if (!existsSync(res.absPath)) {
|
|
864
|
-
if (c.req.query("optional") === "1") {
|
|
865
|
-
return c.json({ filename: res.filePath, content: "" });
|
|
866
|
-
}
|
|
867
|
-
return c.json({ error: "not found" }, 404);
|
|
868
|
-
}
|
|
869
|
-
const content = readFileSync(res.absPath, "utf-8");
|
|
870
|
-
return c.json({ filename: res.filePath, content });
|
|
871
|
-
});
|
|
872
|
-
// ── Write (overwrite) ──
|
|
873
|
-
api.put("/projects/:id/files/*", async (c) => {
|
|
874
|
-
const res = await resolveProjectFile(c, adapter);
|
|
875
|
-
if ("error" in res)
|
|
876
|
-
return res.error;
|
|
877
|
-
ensureDir(res.absPath);
|
|
878
|
-
const body = await c.req.text();
|
|
879
|
-
const backup = snapshotBeforeWrite(res.project.dir, res.absPath);
|
|
880
|
-
if (backup.error)
|
|
881
|
-
console.warn(`Failed to create backup for ${res.filePath}: ${backup.error}`);
|
|
882
|
-
writeFileSync(res.absPath, body, "utf-8");
|
|
883
|
-
return c.json({
|
|
884
|
-
ok: true,
|
|
885
|
-
path: res.filePath,
|
|
886
|
-
backupPath: backupPathForResponse(res.project.dir, backup.backupPath),
|
|
887
|
-
});
|
|
888
|
-
});
|
|
889
|
-
// ── Create (fail if exists) ──
|
|
890
|
-
api.post("/projects/:id/files/*", async (c) => {
|
|
891
|
-
const res = await resolveProjectFile(c, adapter);
|
|
892
|
-
if ("error" in res)
|
|
893
|
-
return res.error;
|
|
894
|
-
if (existsSync(res.absPath)) {
|
|
895
|
-
return c.json({ error: "already exists" }, 409);
|
|
896
|
-
}
|
|
897
|
-
ensureDir(res.absPath);
|
|
898
|
-
const body = await c.req.text().catch(() => "");
|
|
899
|
-
writeFileSync(res.absPath, body, "utf-8");
|
|
900
|
-
return c.json({ ok: true, path: res.filePath }, 201);
|
|
901
|
-
});
|
|
902
|
-
// ── Delete ──
|
|
903
|
-
api.delete("/projects/:id/files/*", async (c) => {
|
|
904
|
-
const res = await resolveProjectFile(c, adapter, { mustExist: true });
|
|
905
|
-
if ("error" in res)
|
|
906
|
-
return res.error;
|
|
907
|
-
const stat = statSync(res.absPath);
|
|
908
|
-
const backup = snapshotBeforeWrite(res.project.dir, res.absPath);
|
|
909
|
-
if (backup.error)
|
|
910
|
-
console.warn(`Failed to create backup for ${res.filePath}: ${backup.error}`);
|
|
911
|
-
if (stat.isDirectory()) {
|
|
912
|
-
rmSync(res.absPath, { recursive: true });
|
|
913
|
-
}
|
|
914
|
-
else {
|
|
915
|
-
unlinkSync(res.absPath);
|
|
916
|
-
}
|
|
917
|
-
return c.json({
|
|
918
|
-
ok: true,
|
|
919
|
-
backupPath: backupPathForResponse(res.project.dir, backup.backupPath),
|
|
920
|
-
});
|
|
921
|
-
});
|
|
922
|
-
api.post("/projects/:id/file-mutations/remove-element/*", async (c) => {
|
|
923
|
-
const ctx = await resolveFileMutationContext(c, adapter, "remove-element");
|
|
924
|
-
if ("error" in ctx)
|
|
925
|
-
return ctx.error;
|
|
926
|
-
if (!existsSync(ctx.absPath)) {
|
|
927
|
-
return c.json({ error: "not found" }, 404);
|
|
928
|
-
}
|
|
929
|
-
const parsed = await parseMutationBody(c);
|
|
930
|
-
if ("error" in parsed)
|
|
931
|
-
return parsed.error;
|
|
932
|
-
const originalContent = readFileSync(ctx.absPath, "utf-8");
|
|
933
|
-
return writeIfChanged(c, ctx.project.dir, ctx.filePath, ctx.absPath, originalContent, removeElementFromHtml(originalContent, parsed.target));
|
|
934
|
-
});
|
|
935
|
-
api.post("/projects/:id/file-mutations/split-element/*", async (c) => {
|
|
936
|
-
const ctx = await resolveFileMutationContext(c, adapter, "split-element");
|
|
937
|
-
if ("error" in ctx)
|
|
938
|
-
return ctx.error;
|
|
939
|
-
const parsed = await parseMutationBody(c);
|
|
940
|
-
if ("error" in parsed)
|
|
941
|
-
return parsed.error;
|
|
942
|
-
if (typeof parsed.body.splitTime !== "number" || !parsed.body.newId) {
|
|
943
|
-
return c.json({ error: "target, splitTime, and newId required" }, 400);
|
|
944
|
-
}
|
|
945
|
-
const fallbackTiming = typeof parsed.body.elementStart === "number" &&
|
|
946
|
-
typeof parsed.body.elementDuration === "number"
|
|
947
|
-
? { start: parsed.body.elementStart, duration: parsed.body.elementDuration }
|
|
948
|
-
: undefined;
|
|
949
|
-
let originalContent;
|
|
950
|
-
try {
|
|
951
|
-
originalContent = readFileSync(ctx.absPath, "utf-8");
|
|
952
|
-
}
|
|
953
|
-
catch {
|
|
954
|
-
return c.json({ error: "not found" }, 404);
|
|
955
|
-
}
|
|
956
|
-
const result = splitElementInHtml(originalContent, parsed.target, parsed.body.splitTime, parsed.body.newId, fallbackTiming);
|
|
957
|
-
if (!result.matched) {
|
|
958
|
-
return c.json({ ok: false, changed: false, content: originalContent, path: ctx.filePath });
|
|
959
|
-
}
|
|
960
|
-
const backup = snapshotBeforeWrite(ctx.project.dir, ctx.absPath);
|
|
961
|
-
if (backup.error)
|
|
962
|
-
console.warn(`Failed to create backup for ${ctx.filePath}: ${backup.error}`);
|
|
963
|
-
writeFileSync(ctx.absPath, result.html, "utf-8");
|
|
964
|
-
return c.json({
|
|
965
|
-
ok: true,
|
|
966
|
-
changed: true,
|
|
967
|
-
content: result.html,
|
|
968
|
-
newId: result.newId,
|
|
969
|
-
path: ctx.filePath,
|
|
970
|
-
backupPath: backupPathForResponse(ctx.project.dir, backup.backupPath),
|
|
971
|
-
});
|
|
972
|
-
});
|
|
973
|
-
api.post("/projects/:id/file-mutations/patch-element/*", async (c) => {
|
|
974
|
-
const ctx = await resolveFileMutationContext(c, adapter, "patch-element");
|
|
975
|
-
if ("error" in ctx)
|
|
976
|
-
return ctx.error;
|
|
977
|
-
const parsed = await parseMutationBody(c);
|
|
978
|
-
if ("error" in parsed)
|
|
979
|
-
return parsed.error;
|
|
980
|
-
if (!Array.isArray(parsed.body.operations) || parsed.body.operations.length === 0) {
|
|
981
|
-
return c.json({ error: "target and operations required" }, 400);
|
|
982
|
-
}
|
|
983
|
-
const unsafeFields = findUnsafeDomPatchValues(parsed.body);
|
|
984
|
-
if (unsafeFields.length > 0) {
|
|
985
|
-
return rejectUnsafeMutationValues(c, unsafeFields);
|
|
986
|
-
}
|
|
987
|
-
let originalContent;
|
|
988
|
-
try {
|
|
989
|
-
originalContent = readFileSync(ctx.absPath, "utf-8");
|
|
990
|
-
}
|
|
991
|
-
catch {
|
|
992
|
-
return c.json({ error: "not found" }, 404);
|
|
993
|
-
}
|
|
994
|
-
const { html: patched, matched } = patchElementInHtml(originalContent, parsed.target, parsed.body.operations);
|
|
995
|
-
if (patched === originalContent) {
|
|
996
|
-
return c.json({
|
|
997
|
-
ok: true,
|
|
998
|
-
changed: false,
|
|
999
|
-
matched,
|
|
1000
|
-
content: originalContent,
|
|
1001
|
-
path: ctx.filePath,
|
|
1002
|
-
});
|
|
1003
|
-
}
|
|
1004
|
-
const backup = snapshotBeforeWrite(ctx.project.dir, ctx.absPath);
|
|
1005
|
-
if (backup.error)
|
|
1006
|
-
console.warn(`Failed to create backup for ${ctx.filePath}: ${backup.error}`);
|
|
1007
|
-
writeFileSync(ctx.absPath, patched, "utf-8");
|
|
1008
|
-
return c.json({
|
|
1009
|
-
ok: true,
|
|
1010
|
-
changed: true,
|
|
1011
|
-
matched,
|
|
1012
|
-
content: patched,
|
|
1013
|
-
path: ctx.filePath,
|
|
1014
|
-
backupPath: backupPathForResponse(ctx.project.dir, backup.backupPath),
|
|
1015
|
-
});
|
|
1016
|
-
});
|
|
1017
|
-
api.post("/projects/:id/file-mutations/probe-element/*", async (c) => {
|
|
1018
|
-
const ctx = await resolveFileMutationContext(c, adapter, "probe-element");
|
|
1019
|
-
if ("error" in ctx)
|
|
1020
|
-
return ctx.error;
|
|
1021
|
-
const parsed = await parseMutationBody(c);
|
|
1022
|
-
if ("error" in parsed)
|
|
1023
|
-
return parsed.error;
|
|
1024
|
-
let content;
|
|
1025
|
-
try {
|
|
1026
|
-
content = readFileSync(ctx.absPath, "utf-8");
|
|
1027
|
-
}
|
|
1028
|
-
catch {
|
|
1029
|
-
return c.json({ exists: false });
|
|
1030
|
-
}
|
|
1031
|
-
const exists = probeElementInSource(content, parsed.target);
|
|
1032
|
-
return c.json({ exists });
|
|
1033
|
-
});
|
|
1034
|
-
// ── Rename / Move ──
|
|
1035
|
-
api.patch("/projects/:id/files/*", async (c) => {
|
|
1036
|
-
const res = await resolveProjectFile(c, adapter, { mustExist: true });
|
|
1037
|
-
if ("error" in res)
|
|
1038
|
-
return res.error;
|
|
1039
|
-
const body = (await c.req.json());
|
|
1040
|
-
if (!body.newPath || body.newPath.includes("\0")) {
|
|
1041
|
-
return c.json({ error: "newPath required" }, 400);
|
|
1042
|
-
}
|
|
1043
|
-
const newAbs = resolveWithinProject(res.project.dir, body.newPath);
|
|
1044
|
-
if (!newAbs) {
|
|
1045
|
-
return c.json({ error: "forbidden" }, 403);
|
|
1046
|
-
}
|
|
1047
|
-
if (existsSync(newAbs)) {
|
|
1048
|
-
return c.json({ error: "already exists" }, 409);
|
|
1049
|
-
}
|
|
1050
|
-
ensureDir(newAbs);
|
|
1051
|
-
renameSync(res.absPath, newAbs);
|
|
1052
|
-
// Update references to the old path across all project files
|
|
1053
|
-
const updatedFiles = updateReferences(res.project.dir, res.filePath, body.newPath);
|
|
1054
|
-
return c.json({ ok: true, path: body.newPath, updatedReferences: updatedFiles });
|
|
1055
|
-
});
|
|
1056
|
-
// ── Duplicate ──
|
|
1057
|
-
api.post("/projects/:id/duplicate-file", async (c) => {
|
|
1058
|
-
const project = await adapter.resolveProject(c.req.param("id"));
|
|
1059
|
-
if (!project)
|
|
1060
|
-
return c.json({ error: "not found" }, 404);
|
|
1061
|
-
const body = (await c.req.json());
|
|
1062
|
-
if (!body.path || body.path.includes("\0")) {
|
|
1063
|
-
return c.json({ error: "path required" }, 400);
|
|
1064
|
-
}
|
|
1065
|
-
const srcAbs = resolveWithinProject(project.dir, body.path);
|
|
1066
|
-
if (!srcAbs || !existsSync(srcAbs)) {
|
|
1067
|
-
return c.json({ error: "not found" }, 404);
|
|
1068
|
-
}
|
|
1069
|
-
const copyPath = generateCopyPath(project.dir, body.path);
|
|
1070
|
-
const destAbs = resolveWithinProject(project.dir, copyPath);
|
|
1071
|
-
if (!destAbs) {
|
|
1072
|
-
return c.json({ error: "forbidden" }, 403);
|
|
1073
|
-
}
|
|
1074
|
-
ensureDir(destAbs);
|
|
1075
|
-
writeFileSync(destAbs, readFileSync(srcAbs));
|
|
1076
|
-
return c.json({ ok: true, path: copyPath }, 201);
|
|
1077
|
-
});
|
|
1078
|
-
// ── Upload (binary assets via multipart form) ──
|
|
1079
|
-
const MAX_UPLOAD_BYTES = 500 * 1024 * 1024; // 500 MB per file
|
|
1080
|
-
api.post("/projects/:id/upload", bodyLimit({
|
|
1081
|
-
maxSize: MAX_UPLOAD_BYTES,
|
|
1082
|
-
onError: (c) => c.json({ error: "payload too large" }, 413),
|
|
1083
|
-
}), async (c) => {
|
|
1084
|
-
const project = await adapter.resolveProject(c.req.param("id"));
|
|
1085
|
-
if (!project)
|
|
1086
|
-
return c.json({ error: "not found" }, 404);
|
|
1087
|
-
// Optional subdirectory within the project (e.g. "assets/audio")
|
|
1088
|
-
const subDir = c.req.query("dir") ?? "";
|
|
1089
|
-
const targetDir = subDir ? resolveWithinProject(project.dir, subDir) : project.dir;
|
|
1090
|
-
if (!targetDir)
|
|
1091
|
-
return c.json({ error: "forbidden" }, 403);
|
|
1092
|
-
if (subDir && !existsSync(targetDir))
|
|
1093
|
-
mkdirSync(targetDir, { recursive: true });
|
|
1094
|
-
const formData = await c.req.formData();
|
|
1095
|
-
const result = await processUploadedFiles(formData, targetDir, project.dir);
|
|
1096
|
-
return c.json({ ok: true, files: result.uploaded, skipped: result.skipped, invalid: result.invalid }, 201);
|
|
1097
|
-
});
|
|
1098
|
-
// ── GSAP Animations (parse) ──
|
|
1099
|
-
api.get("/projects/:id/gsap-animations/*", async (c) => {
|
|
1100
|
-
const res = await resolveProjectPath(c, adapter, (id) => `/projects/${id}/gsap-animations/`, {
|
|
1101
|
-
mustExist: true,
|
|
1102
|
-
});
|
|
1103
|
-
if ("error" in res)
|
|
1104
|
-
return res.error;
|
|
1105
|
-
const html = readFileSync(res.absPath, "utf-8");
|
|
1106
|
-
const block = extractGsapScriptBlock(html);
|
|
1107
|
-
if (!block) {
|
|
1108
|
-
return c.json({
|
|
1109
|
-
animations: [],
|
|
1110
|
-
timelineVar: "tl",
|
|
1111
|
-
preamble: "",
|
|
1112
|
-
postamble: "",
|
|
1113
|
-
});
|
|
1114
|
-
}
|
|
1115
|
-
const parsed = parseGsapScriptAcorn(block.scriptText);
|
|
1116
|
-
return c.json(parsed);
|
|
1117
|
-
});
|
|
1118
|
-
// ── GSAP Mutations ──
|
|
1119
|
-
api.post("/projects/:id/gsap-mutations/*", async (c) => {
|
|
1120
|
-
const res = await resolveProjectPath(c, adapter, (id) => `/projects/${id}/gsap-mutations/`, {
|
|
1121
|
-
mustExist: true,
|
|
1122
|
-
});
|
|
1123
|
-
if ("error" in res)
|
|
1124
|
-
return res.error;
|
|
1125
|
-
const body = (await c.req.json().catch(() => null));
|
|
1126
|
-
if (!body || !body.type) {
|
|
1127
|
-
return c.json({ error: "mutation type required" }, 400);
|
|
1128
|
-
}
|
|
1129
|
-
const unsafeFields = findUnsafeMutationValues(body);
|
|
1130
|
-
if (unsafeFields.length > 0) {
|
|
1131
|
-
return rejectUnsafeMutationValues(c, unsafeFields);
|
|
1132
|
-
}
|
|
1133
|
-
let html = readFileSync(res.absPath, "utf-8");
|
|
1134
|
-
let block = extractGsapScriptBlock(html);
|
|
1135
|
-
if (!block && (body.type === "add" || body.type === "add-with-keyframes")) {
|
|
1136
|
-
const compId = html.match(/data-composition-id="([^"]+)"/)?.[1] ?? "main";
|
|
1137
|
-
const { GSAP_CDN } = await import("../../templates/constants.js");
|
|
1138
|
-
const gsapCdn = `<script src="${GSAP_CDN}"></script>`;
|
|
1139
|
-
const bootstrap = [
|
|
1140
|
-
gsapCdn,
|
|
1141
|
-
"<script>",
|
|
1142
|
-
"window.__timelines = window.__timelines || {};",
|
|
1143
|
-
`const tl = gsap.timeline({ paused: true });`,
|
|
1144
|
-
`window.__timelines["${compId}"] = tl;`,
|
|
1145
|
-
"</script>",
|
|
1146
|
-
].join("\n");
|
|
1147
|
-
if (html.includes("</body>")) {
|
|
1148
|
-
html = html.replace("</body>", `${bootstrap}\n</body>`);
|
|
1149
|
-
}
|
|
1150
|
-
else {
|
|
1151
|
-
html += `\n${bootstrap}`;
|
|
1152
|
-
}
|
|
1153
|
-
block = extractGsapScriptBlock(html);
|
|
1154
|
-
}
|
|
1155
|
-
if (!block) {
|
|
1156
|
-
return c.json({ error: "no GSAP script found in file" }, 400);
|
|
1157
|
-
}
|
|
1158
|
-
const respond = (data, status) =>
|
|
1159
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- bridge between generic status and Hono's literal union
|
|
1160
|
-
status ? c.json(data, status) : c.json(data);
|
|
1161
|
-
const result = await executeGsapMutation(body, block, respond);
|
|
1162
|
-
if (result instanceof Response)
|
|
1163
|
-
return result;
|
|
1164
|
-
let newScript = typeof result === "string" ? result : result.script;
|
|
1165
|
-
// Keep the "hold before first keyframe" sets in sync after any mutation that can
|
|
1166
|
-
// change a position tween's first keyframe or its existence. Without it, an
|
|
1167
|
-
// element snaps to its CSS base before the tween starts instead of holding its
|
|
1168
|
-
// first keyframe (the universal NLE behavior).
|
|
1169
|
-
if (HOLD_SYNC_MUTATION_TYPES.has(body.type)) {
|
|
1170
|
-
const parser = await loadGsapParser();
|
|
1171
|
-
newScript = parser.syncPositionHoldsBeforeKeyframes(newScript);
|
|
1172
|
-
}
|
|
1173
|
-
const changed = newScript !== block.scriptText;
|
|
1174
|
-
const newHtml = changed ? block.replaceScript(newScript) : html;
|
|
1175
|
-
let backupPath = null;
|
|
1176
|
-
if (changed) {
|
|
1177
|
-
const backup = snapshotBeforeWrite(res.project.dir, res.absPath);
|
|
1178
|
-
if (backup.error)
|
|
1179
|
-
console.warn(`Failed to create backup for ${res.filePath}: ${backup.error}`);
|
|
1180
|
-
backupPath = backupPathForResponse(res.project.dir, backup.backupPath);
|
|
1181
|
-
writeFileSync(res.absPath, newHtml, "utf-8");
|
|
1182
|
-
}
|
|
1183
|
-
const freshParsed = parseGsapScriptAcorn(newScript);
|
|
1184
|
-
const responsePayload = {
|
|
1185
|
-
ok: true,
|
|
1186
|
-
changed,
|
|
1187
|
-
parsed: freshParsed,
|
|
1188
|
-
before: html,
|
|
1189
|
-
after: newHtml,
|
|
1190
|
-
scriptText: newScript,
|
|
1191
|
-
path: res.filePath,
|
|
1192
|
-
backupPath,
|
|
1193
|
-
};
|
|
1194
|
-
if (typeof result !== "string" && result.skippedSelectors.length > 0) {
|
|
1195
|
-
responsePayload.skippedSelectors = result.skippedSelectors;
|
|
1196
|
-
}
|
|
1197
|
-
return c.json(responsePayload);
|
|
1198
|
-
});
|
|
1199
|
-
}
|
|
1200
|
-
//# sourceMappingURL=files.js.map
|