@hyperframes/core 0.7.14 → 0.7.16

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.
Files changed (240) hide show
  1. package/dist/compiler/staticGuard.js +1 -1
  2. package/dist/compiler/staticGuard.js.map +1 -1
  3. package/dist/core.types.d.ts +2 -369
  4. package/dist/core.types.d.ts.map +1 -1
  5. package/dist/core.types.js +1 -84
  6. package/dist/core.types.js.map +1 -1
  7. package/dist/generated/runtime-inline.js.map +1 -1
  8. package/dist/generators/hyperframes.d.ts +1 -1
  9. package/dist/generators/hyperframes.d.ts.map +1 -1
  10. package/dist/generators/hyperframes.js +1 -1
  11. package/dist/generators/hyperframes.js.map +1 -1
  12. package/dist/hyperframe.runtime.iife.js +20 -20
  13. package/dist/index.d.ts +5 -7
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +7 -4
  16. package/dist/index.js.map +1 -1
  17. package/dist/lint/index.d.ts +2 -2
  18. package/dist/lint/index.d.ts.map +1 -1
  19. package/dist/lint/index.js +2 -1
  20. package/dist/lint/index.js.map +1 -1
  21. package/dist/parsers/gsapConstants.d.ts +2 -12
  22. package/dist/parsers/gsapConstants.d.ts.map +1 -1
  23. package/dist/parsers/gsapConstants.js +2 -110
  24. package/dist/parsers/gsapConstants.js.map +1 -1
  25. package/dist/parsers/gsapParserAcorn.d.ts +2 -48
  26. package/dist/parsers/gsapParserAcorn.d.ts.map +1 -1
  27. package/dist/parsers/gsapParserAcorn.js +2 -1057
  28. package/dist/parsers/gsapParserAcorn.js.map +1 -1
  29. package/dist/parsers/gsapParserExports.d.ts +2 -20
  30. package/dist/parsers/gsapParserExports.d.ts.map +1 -1
  31. package/dist/parsers/gsapParserExports.js +2 -8
  32. package/dist/parsers/gsapParserExports.js.map +1 -1
  33. package/dist/parsers/gsapWriterAcorn.d.ts +2 -90
  34. package/dist/parsers/gsapWriterAcorn.d.ts.map +1 -1
  35. package/dist/parsers/gsapWriterAcorn.js +2 -2016
  36. package/dist/parsers/gsapWriterAcorn.js.map +1 -1
  37. package/dist/parsers/hfIds.d.ts +2 -16
  38. package/dist/parsers/hfIds.d.ts.map +1 -1
  39. package/dist/parsers/hfIds.js +2 -127
  40. package/dist/parsers/hfIds.js.map +1 -1
  41. package/dist/parsers/springEase.d.ts +2 -28
  42. package/dist/parsers/springEase.d.ts.map +1 -1
  43. package/dist/parsers/springEase.js +2 -69
  44. package/dist/parsers/springEase.js.map +1 -1
  45. package/dist/slideshow/index.d.ts +1 -0
  46. package/dist/slideshow/index.d.ts.map +1 -1
  47. package/dist/slideshow/index.js +1 -0
  48. package/dist/slideshow/index.js.map +1 -1
  49. package/dist/studio-api/helpers/draftMarkers.d.ts +2 -10
  50. package/dist/studio-api/helpers/draftMarkers.d.ts.map +1 -1
  51. package/dist/studio-api/helpers/draftMarkers.js +2 -10
  52. package/dist/studio-api/helpers/draftMarkers.js.map +1 -1
  53. package/dist/studio-api/helpers/finiteMutation.d.ts +2 -10
  54. package/dist/studio-api/helpers/finiteMutation.d.ts.map +1 -1
  55. package/dist/studio-api/helpers/finiteMutation.js +2 -20
  56. package/dist/studio-api/helpers/finiteMutation.js.map +1 -1
  57. package/dist/studio-api/helpers/manualEditsRenderScript.d.ts +2 -12
  58. package/dist/studio-api/helpers/manualEditsRenderScript.d.ts.map +1 -1
  59. package/dist/studio-api/helpers/manualEditsRenderScript.js +2 -632
  60. package/dist/studio-api/helpers/manualEditsRenderScript.js.map +1 -1
  61. package/dist/studio-api/helpers/screenshotClip.d.ts +2 -7
  62. package/dist/studio-api/helpers/screenshotClip.d.ts.map +1 -1
  63. package/dist/studio-api/helpers/screenshotClip.js +2 -21
  64. package/dist/studio-api/helpers/screenshotClip.js.map +1 -1
  65. package/dist/studio-api/helpers/studioMotionRenderScript.d.ts +2 -8
  66. package/dist/studio-api/helpers/studioMotionRenderScript.d.ts.map +1 -1
  67. package/dist/studio-api/helpers/studioMotionRenderScript.js +2 -207
  68. package/dist/studio-api/helpers/studioMotionRenderScript.js.map +1 -1
  69. package/dist/studio-api/index.d.ts +2 -9
  70. package/dist/studio-api/index.d.ts.map +1 -1
  71. package/dist/studio-api/index.js +2 -8
  72. package/dist/studio-api/index.js.map +1 -1
  73. package/package.json +16 -18
  74. package/dist/generated/runtime-inline.js +0 -11
  75. package/dist/hyperframe.manifest.json +0 -22
  76. package/dist/hyperframe.runtime.mjs +0 -325
  77. package/dist/lint/context.d.ts +0 -17
  78. package/dist/lint/context.d.ts.map +0 -1
  79. package/dist/lint/context.js +0 -37
  80. package/dist/lint/context.js.map +0 -1
  81. package/dist/lint/hyperframeLinter.d.ts +0 -14
  82. package/dist/lint/hyperframeLinter.d.ts.map +0 -1
  83. package/dist/lint/hyperframeLinter.js +0 -132
  84. package/dist/lint/hyperframeLinter.js.map +0 -1
  85. package/dist/lint/rules/adapters.d.ts +0 -3
  86. package/dist/lint/rules/adapters.d.ts.map +0 -1
  87. package/dist/lint/rules/adapters.js +0 -40
  88. package/dist/lint/rules/adapters.js.map +0 -1
  89. package/dist/lint/rules/captions.d.ts +0 -3
  90. package/dist/lint/rules/captions.d.ts.map +0 -1
  91. package/dist/lint/rules/captions.js +0 -242
  92. package/dist/lint/rules/captions.js.map +0 -1
  93. package/dist/lint/rules/composition.d.ts +0 -5
  94. package/dist/lint/rules/composition.d.ts.map +0 -1
  95. package/dist/lint/rules/composition.js +0 -694
  96. package/dist/lint/rules/composition.js.map +0 -1
  97. package/dist/lint/rules/core.d.ts +0 -3
  98. package/dist/lint/rules/core.d.ts.map +0 -1
  99. package/dist/lint/rules/core.js +0 -432
  100. package/dist/lint/rules/core.js.map +0 -1
  101. package/dist/lint/rules/fonts.d.ts +0 -3
  102. package/dist/lint/rules/fonts.d.ts.map +0 -1
  103. package/dist/lint/rules/fonts.js +0 -205
  104. package/dist/lint/rules/fonts.js.map +0 -1
  105. package/dist/lint/rules/gsap.d.ts +0 -4
  106. package/dist/lint/rules/gsap.d.ts.map +0 -1
  107. package/dist/lint/rules/gsap.js +0 -1039
  108. package/dist/lint/rules/gsap.js.map +0 -1
  109. package/dist/lint/rules/media.d.ts +0 -3
  110. package/dist/lint/rules/media.d.ts.map +0 -1
  111. package/dist/lint/rules/media.js +0 -491
  112. package/dist/lint/rules/media.js.map +0 -1
  113. package/dist/lint/rules/slideshow.d.ts +0 -4
  114. package/dist/lint/rules/slideshow.d.ts.map +0 -1
  115. package/dist/lint/rules/slideshow.js +0 -74
  116. package/dist/lint/rules/slideshow.js.map +0 -1
  117. package/dist/lint/rules/textures.d.ts +0 -3
  118. package/dist/lint/rules/textures.d.ts.map +0 -1
  119. package/dist/lint/rules/textures.js +0 -192
  120. package/dist/lint/rules/textures.js.map +0 -1
  121. package/dist/lint/types.d.ts +0 -36
  122. package/dist/lint/types.d.ts.map +0 -1
  123. package/dist/lint/types.js +0 -2
  124. package/dist/lint/types.js.map +0 -1
  125. package/dist/lint/utils.d.ts +0 -55
  126. package/dist/lint/utils.d.ts.map +0 -1
  127. package/dist/lint/utils.js +0 -264
  128. package/dist/lint/utils.js.map +0 -1
  129. package/dist/parsers/gsapInline.d.ts +0 -47
  130. package/dist/parsers/gsapInline.d.ts.map +0 -1
  131. package/dist/parsers/gsapInline.js +0 -513
  132. package/dist/parsers/gsapInline.js.map +0 -1
  133. package/dist/parsers/gsapParser.d.ts +0 -158
  134. package/dist/parsers/gsapParser.d.ts.map +0 -1
  135. package/dist/parsers/gsapParser.js +0 -2634
  136. package/dist/parsers/gsapParser.js.map +0 -1
  137. package/dist/parsers/gsapParser.test-helpers.d.ts +0 -49
  138. package/dist/parsers/gsapParser.test-helpers.d.ts.map +0 -1
  139. package/dist/parsers/gsapParser.test-helpers.js +0 -97
  140. package/dist/parsers/gsapParser.test-helpers.js.map +0 -1
  141. package/dist/parsers/gsapSerialize.d.ts +0 -191
  142. package/dist/parsers/gsapSerialize.d.ts.map +0 -1
  143. package/dist/parsers/gsapSerialize.js +0 -410
  144. package/dist/parsers/gsapSerialize.js.map +0 -1
  145. package/dist/parsers/gsapUnroll.d.ts +0 -7
  146. package/dist/parsers/gsapUnroll.d.ts.map +0 -1
  147. package/dist/parsers/gsapUnroll.js +0 -137
  148. package/dist/parsers/gsapUnroll.js.map +0 -1
  149. package/dist/parsers/htmlParser.d.ts +0 -27
  150. package/dist/parsers/htmlParser.d.ts.map +0 -1
  151. package/dist/parsers/htmlParser.js +0 -731
  152. package/dist/parsers/htmlParser.js.map +0 -1
  153. package/dist/studio-api/createStudioApi.d.ts +0 -10
  154. package/dist/studio-api/createStudioApi.d.ts.map +0 -1
  155. package/dist/studio-api/createStudioApi.js +0 -32
  156. package/dist/studio-api/createStudioApi.js.map +0 -1
  157. package/dist/studio-api/helpers/backupJournal.d.ts +0 -9
  158. package/dist/studio-api/helpers/backupJournal.d.ts.map +0 -1
  159. package/dist/studio-api/helpers/backupJournal.js +0 -81
  160. package/dist/studio-api/helpers/backupJournal.js.map +0 -1
  161. package/dist/studio-api/helpers/hfIdPersist.d.ts +0 -11
  162. package/dist/studio-api/helpers/hfIdPersist.d.ts.map +0 -1
  163. package/dist/studio-api/helpers/hfIdPersist.js +0 -39
  164. package/dist/studio-api/helpers/hfIdPersist.js.map +0 -1
  165. package/dist/studio-api/helpers/mediaValidation.d.ts +0 -20
  166. package/dist/studio-api/helpers/mediaValidation.d.ts.map +0 -1
  167. package/dist/studio-api/helpers/mediaValidation.js +0 -56
  168. package/dist/studio-api/helpers/mediaValidation.js.map +0 -1
  169. package/dist/studio-api/helpers/mime.d.ts +0 -4
  170. package/dist/studio-api/helpers/mime.d.ts.map +0 -1
  171. package/dist/studio-api/helpers/mime.js +0 -39
  172. package/dist/studio-api/helpers/mime.js.map +0 -1
  173. package/dist/studio-api/helpers/previewAdapter.d.ts +0 -43
  174. package/dist/studio-api/helpers/previewAdapter.d.ts.map +0 -1
  175. package/dist/studio-api/helpers/previewAdapter.js +0 -118
  176. package/dist/studio-api/helpers/previewAdapter.js.map +0 -1
  177. package/dist/studio-api/helpers/projectSignature.d.ts +0 -5
  178. package/dist/studio-api/helpers/projectSignature.d.ts.map +0 -1
  179. package/dist/studio-api/helpers/projectSignature.js +0 -154
  180. package/dist/studio-api/helpers/projectSignature.js.map +0 -1
  181. package/dist/studio-api/helpers/safePath.d.ts +0 -12
  182. package/dist/studio-api/helpers/safePath.d.ts.map +0 -1
  183. package/dist/studio-api/helpers/safePath.js +0 -38
  184. package/dist/studio-api/helpers/safePath.js.map +0 -1
  185. package/dist/studio-api/helpers/sourceMutation.d.ts +0 -28
  186. package/dist/studio-api/helpers/sourceMutation.d.ts.map +0 -1
  187. package/dist/studio-api/helpers/sourceMutation.js +0 -339
  188. package/dist/studio-api/helpers/sourceMutation.js.map +0 -1
  189. package/dist/studio-api/helpers/subComposition.d.ts +0 -20
  190. package/dist/studio-api/helpers/subComposition.d.ts.map +0 -1
  191. package/dist/studio-api/helpers/subComposition.js +0 -289
  192. package/dist/studio-api/helpers/subComposition.js.map +0 -1
  193. package/dist/studio-api/helpers/waveform.d.ts +0 -4
  194. package/dist/studio-api/helpers/waveform.d.ts.map +0 -1
  195. package/dist/studio-api/helpers/waveform.js +0 -80
  196. package/dist/studio-api/helpers/waveform.js.map +0 -1
  197. package/dist/studio-api/routes/files.d.ts +0 -4
  198. package/dist/studio-api/routes/files.d.ts.map +0 -1
  199. package/dist/studio-api/routes/files.js +0 -1200
  200. package/dist/studio-api/routes/files.js.map +0 -1
  201. package/dist/studio-api/routes/fonts.d.ts +0 -3
  202. package/dist/studio-api/routes/fonts.d.ts.map +0 -1
  203. package/dist/studio-api/routes/fonts.js +0 -157
  204. package/dist/studio-api/routes/fonts.js.map +0 -1
  205. package/dist/studio-api/routes/lint.d.ts +0 -4
  206. package/dist/studio-api/routes/lint.d.ts.map +0 -1
  207. package/dist/studio-api/routes/lint.js +0 -29
  208. package/dist/studio-api/routes/lint.js.map +0 -1
  209. package/dist/studio-api/routes/preview.d.ts +0 -4
  210. package/dist/studio-api/routes/preview.d.ts.map +0 -1
  211. package/dist/studio-api/routes/preview.js +0 -331
  212. package/dist/studio-api/routes/preview.js.map +0 -1
  213. package/dist/studio-api/routes/projects.d.ts +0 -4
  214. package/dist/studio-api/routes/projects.d.ts.map +0 -1
  215. package/dist/studio-api/routes/projects.js +0 -45
  216. package/dist/studio-api/routes/projects.js.map +0 -1
  217. package/dist/studio-api/routes/registry.d.ts +0 -4
  218. package/dist/studio-api/routes/registry.d.ts.map +0 -1
  219. package/dist/studio-api/routes/registry.js +0 -31
  220. package/dist/studio-api/routes/registry.js.map +0 -1
  221. package/dist/studio-api/routes/render.d.ts +0 -4
  222. package/dist/studio-api/routes/render.d.ts.map +0 -1
  223. package/dist/studio-api/routes/render.js +0 -269
  224. package/dist/studio-api/routes/render.js.map +0 -1
  225. package/dist/studio-api/routes/storyboard.d.ts +0 -4
  226. package/dist/studio-api/routes/storyboard.d.ts.map +0 -1
  227. package/dist/studio-api/routes/storyboard.js +0 -65
  228. package/dist/studio-api/routes/storyboard.js.map +0 -1
  229. package/dist/studio-api/routes/thumbnail.d.ts +0 -4
  230. package/dist/studio-api/routes/thumbnail.d.ts.map +0 -1
  231. package/dist/studio-api/routes/thumbnail.js +0 -107
  232. package/dist/studio-api/routes/thumbnail.js.map +0 -1
  233. package/dist/studio-api/routes/waveform.d.ts +0 -4
  234. package/dist/studio-api/routes/waveform.d.ts.map +0 -1
  235. package/dist/studio-api/routes/waveform.js +0 -41
  236. package/dist/studio-api/routes/waveform.js.map +0 -1
  237. package/dist/studio-api/types.d.ts +0 -120
  238. package/dist/studio-api/types.d.ts.map +0 -1
  239. package/dist/studio-api/types.js +0 -2
  240. 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