@hyperframes/studio-server 0.7.57 → 0.7.58
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +164 -31
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -896,6 +896,21 @@ function resolveProjectFile(c, adapter, opts) {
|
|
|
896
896
|
function resolveFileMutationContext(c, adapter, operation) {
|
|
897
897
|
return resolveProjectPath(c, adapter, (id) => `/projects/${id}/file-mutations/${operation}/`);
|
|
898
898
|
}
|
|
899
|
+
function isElementPatchRequest(value) {
|
|
900
|
+
if (typeof value !== "object" || value === null) return false;
|
|
901
|
+
if (!("target" in value) || typeof value.target !== "object" || value.target === null) {
|
|
902
|
+
return false;
|
|
903
|
+
}
|
|
904
|
+
return "operations" in value && Array.isArray(value.operations) && value.operations.length > 0;
|
|
905
|
+
}
|
|
906
|
+
function isElementPatchBatchRequest(value) {
|
|
907
|
+
return typeof value === "object" && value !== null && "sourceFile" in value && typeof value.sourceFile === "string" && value.sourceFile.length > 0 && "patches" in value && Array.isArray(value.patches) && value.patches.length > 0 && value.patches.every(isElementPatchRequest);
|
|
908
|
+
}
|
|
909
|
+
function findUnsafeElementPatchBatchValues(batches) {
|
|
910
|
+
return batches.flatMap(
|
|
911
|
+
(batch) => batch.patches.flatMap((patch) => findUnsafeDomPatchValues(patch))
|
|
912
|
+
);
|
|
913
|
+
}
|
|
899
914
|
function foldElementPatches(originalContent, patches) {
|
|
900
915
|
let content = originalContent;
|
|
901
916
|
const matched = [];
|
|
@@ -906,6 +921,90 @@ function foldElementPatches(originalContent, patches) {
|
|
|
906
921
|
}
|
|
907
922
|
return { content, matched };
|
|
908
923
|
}
|
|
924
|
+
function commitElementPatchBatches(projectDir, batches, writeFile = writeFileSync4) {
|
|
925
|
+
const resolvedPaths = /* @__PURE__ */ new Set();
|
|
926
|
+
const prepared = [];
|
|
927
|
+
for (const batch of batches) {
|
|
928
|
+
const absPath = resolveWithinProject(projectDir, batch.sourceFile);
|
|
929
|
+
if (!absPath) return { error: "forbidden", sourceFile: batch.sourceFile };
|
|
930
|
+
if (resolvedPaths.has(absPath)) return { error: "duplicate", sourceFile: batch.sourceFile };
|
|
931
|
+
resolvedPaths.add(absPath);
|
|
932
|
+
let before;
|
|
933
|
+
try {
|
|
934
|
+
before = readFileSync3(absPath, "utf-8");
|
|
935
|
+
} catch {
|
|
936
|
+
return { error: "not-found", sourceFile: batch.sourceFile };
|
|
937
|
+
}
|
|
938
|
+
const folded = foldElementPatches(before, batch.patches);
|
|
939
|
+
prepared.push({
|
|
940
|
+
sourceFile: batch.sourceFile,
|
|
941
|
+
absPath,
|
|
942
|
+
before,
|
|
943
|
+
matched: folded.matched,
|
|
944
|
+
after: folded.content
|
|
945
|
+
});
|
|
946
|
+
}
|
|
947
|
+
const durable = prepared.every((file) => file.matched.every(Boolean));
|
|
948
|
+
if (!durable) {
|
|
949
|
+
return {
|
|
950
|
+
durable: false,
|
|
951
|
+
files: prepared.map((file) => ({
|
|
952
|
+
sourceFile: file.sourceFile,
|
|
953
|
+
changed: false,
|
|
954
|
+
matched: file.matched,
|
|
955
|
+
before: file.before,
|
|
956
|
+
after: file.before
|
|
957
|
+
}))
|
|
958
|
+
};
|
|
959
|
+
}
|
|
960
|
+
const files = [];
|
|
961
|
+
const attemptedWrites = [];
|
|
962
|
+
try {
|
|
963
|
+
for (const file of prepared) {
|
|
964
|
+
if (file.after === file.before) {
|
|
965
|
+
files.push({
|
|
966
|
+
sourceFile: file.sourceFile,
|
|
967
|
+
changed: false,
|
|
968
|
+
matched: file.matched,
|
|
969
|
+
before: file.before,
|
|
970
|
+
after: file.before
|
|
971
|
+
});
|
|
972
|
+
continue;
|
|
973
|
+
}
|
|
974
|
+
const backup = snapshotBeforeWrite(projectDir, file.absPath);
|
|
975
|
+
if (backup.error) {
|
|
976
|
+
throw new Error(`Failed to create backup for ${file.sourceFile}: ${backup.error}`);
|
|
977
|
+
}
|
|
978
|
+
attemptedWrites.push(file);
|
|
979
|
+
writeFile(file.absPath, file.after, "utf-8");
|
|
980
|
+
files.push({
|
|
981
|
+
sourceFile: file.sourceFile,
|
|
982
|
+
changed: true,
|
|
983
|
+
matched: file.matched,
|
|
984
|
+
before: file.before,
|
|
985
|
+
after: file.after,
|
|
986
|
+
backupPath: backupPathForResponse(projectDir, backup.backupPath)
|
|
987
|
+
});
|
|
988
|
+
}
|
|
989
|
+
} catch (error) {
|
|
990
|
+
const rollbackErrors = [];
|
|
991
|
+
for (const file of attemptedWrites.reverse()) {
|
|
992
|
+
try {
|
|
993
|
+
writeFile(file.absPath, file.before, "utf-8");
|
|
994
|
+
} catch (rollbackError) {
|
|
995
|
+
rollbackErrors.push(rollbackError);
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
if (rollbackErrors.length > 0) {
|
|
999
|
+
throw new AggregateError(
|
|
1000
|
+
[error, ...rollbackErrors],
|
|
1001
|
+
"Element patch batch failed and rollback did not complete"
|
|
1002
|
+
);
|
|
1003
|
+
}
|
|
1004
|
+
throw error;
|
|
1005
|
+
}
|
|
1006
|
+
return { durable: true, files };
|
|
1007
|
+
}
|
|
909
1008
|
function writeIfChanged(c, projectDir, filePath, absPath, original, next) {
|
|
910
1009
|
if (next === original) {
|
|
911
1010
|
return c.json({ ok: true, changed: false, content: original, path: filePath });
|
|
@@ -931,6 +1030,11 @@ function rejectUnsafeMutationValues(c, unsafeFields) {
|
|
|
931
1030
|
400
|
|
932
1031
|
);
|
|
933
1032
|
}
|
|
1033
|
+
function elementPatchBatchCommitErrorResponse(c, error, sourceFile) {
|
|
1034
|
+
if (error === "not-found") return c.json({ error, sourceFile }, 404);
|
|
1035
|
+
if (error === "forbidden") return c.json({ error, sourceFile }, 403);
|
|
1036
|
+
return c.json({ error: "duplicate source file", sourceFile }, 400);
|
|
1037
|
+
}
|
|
934
1038
|
async function parseMutationBody(c) {
|
|
935
1039
|
const body = await c.req.json().catch(() => null);
|
|
936
1040
|
if (!body?.target) {
|
|
@@ -1233,7 +1337,8 @@ function validateGsapMutationRequest(c, body) {
|
|
|
1233
1337
|
return null;
|
|
1234
1338
|
}
|
|
1235
1339
|
async function prepareGsapMutationScript(c, res, firstMutation) {
|
|
1236
|
-
|
|
1340
|
+
const beforeHtml = readFileSync3(res.absPath, "utf-8");
|
|
1341
|
+
let html = beforeHtml;
|
|
1237
1342
|
let block = extractGsapScriptBlock(html);
|
|
1238
1343
|
if (!block && (firstMutation.type === "add" || firstMutation.type === "add-with-keyframes")) {
|
|
1239
1344
|
const compId = html.match(/data-composition-id="([^"]+)"/)?.[1] ?? "main";
|
|
@@ -1265,14 +1370,14 @@ ${bootstrap}`;
|
|
|
1265
1370
|
});
|
|
1266
1371
|
}
|
|
1267
1372
|
if (!block) return c.json({ error: "no GSAP script found in file" }, 400);
|
|
1268
|
-
return { html, block };
|
|
1373
|
+
return { html, beforeHtml, block };
|
|
1269
1374
|
}
|
|
1270
1375
|
async function applyGsapMutations(c, res, mutations) {
|
|
1271
1376
|
const firstMutation = mutations[0];
|
|
1272
1377
|
if (!firstMutation) return c.json({ error: "mutations array required" }, 400);
|
|
1273
1378
|
const prepared = await prepareGsapMutationScript(c, res, firstMutation);
|
|
1274
1379
|
if (prepared instanceof Response) return prepared;
|
|
1275
|
-
const { html, block } = prepared;
|
|
1380
|
+
const { html, beforeHtml, block } = prepared;
|
|
1276
1381
|
const initialScript = block.scriptText;
|
|
1277
1382
|
const skippedSelectors = /* @__PURE__ */ new Set();
|
|
1278
1383
|
const respond = (data, status) => status ? c.json(data, status) : c.json(data);
|
|
@@ -1292,6 +1397,9 @@ async function applyGsapMutations(c, res, mutations) {
|
|
|
1292
1397
|
const changed = block.scriptText !== initialScript;
|
|
1293
1398
|
const newHtml = changed ? block.replaceScript(block.scriptText) : html;
|
|
1294
1399
|
let backupPath = null;
|
|
1400
|
+
if (readFileSync3(res.absPath, "utf-8") !== beforeHtml) {
|
|
1401
|
+
return c.json({ error: "file changed during GSAP mutation", conflict: true }, 409);
|
|
1402
|
+
}
|
|
1295
1403
|
if (changed) {
|
|
1296
1404
|
const backup = snapshotBeforeWrite(res.project.dir, res.absPath);
|
|
1297
1405
|
if (backup.error) console.warn(`Failed to create backup for ${res.filePath}: ${backup.error}`);
|
|
@@ -1303,7 +1411,7 @@ async function applyGsapMutations(c, res, mutations) {
|
|
|
1303
1411
|
changed,
|
|
1304
1412
|
mutated: changed,
|
|
1305
1413
|
parsed: parseGsapScriptAcorn(block.scriptText),
|
|
1306
|
-
before:
|
|
1414
|
+
before: beforeHtml,
|
|
1307
1415
|
after: newHtml,
|
|
1308
1416
|
scriptText: block.scriptText,
|
|
1309
1417
|
path: res.filePath,
|
|
@@ -2112,45 +2220,46 @@ function registerFileRoutes(api, adapter) {
|
|
|
2112
2220
|
backupPath: backupPathForResponse(ctx.project.dir, backup.backupPath)
|
|
2113
2221
|
});
|
|
2114
2222
|
});
|
|
2223
|
+
api.post("/projects/:id/file-mutations/patch-element-batches", async (c) => {
|
|
2224
|
+
const project = await adapter.resolveProject(c.req.param("id"));
|
|
2225
|
+
if (!project) return c.json({ error: "not found" }, 404);
|
|
2226
|
+
const body = await c.req.json().catch(() => null);
|
|
2227
|
+
if (typeof body !== "object" || body === null || !("batches" in body) || !Array.isArray(body.batches) || body.batches.length === 0 || !body.batches.every(isElementPatchBatchRequest)) {
|
|
2228
|
+
return c.json({ error: "batches with sourceFile and patches required" }, 400);
|
|
2229
|
+
}
|
|
2230
|
+
const unsafeFields = findUnsafeElementPatchBatchValues(body.batches);
|
|
2231
|
+
if (unsafeFields.length > 0) return rejectUnsafeMutationValues(c, unsafeFields);
|
|
2232
|
+
const result = commitElementPatchBatches(project.dir, body.batches);
|
|
2233
|
+
if ("error" in result) {
|
|
2234
|
+
return elementPatchBatchCommitErrorResponse(c, result.error, result.sourceFile);
|
|
2235
|
+
}
|
|
2236
|
+
return c.json(result);
|
|
2237
|
+
});
|
|
2115
2238
|
api.post("/projects/:id/file-mutations/patch-elements-batch/*", async (c) => {
|
|
2116
2239
|
const ctx = await resolveFileMutationContext(c, adapter, "patch-elements-batch");
|
|
2117
2240
|
if ("error" in ctx) return ctx.error;
|
|
2118
2241
|
const body = await c.req.json().catch(() => null);
|
|
2119
|
-
if (!body || !Array.isArray(body.patches) || body.patches.length === 0 || body.patches.
|
|
2120
|
-
(patch) => !patch?.target || !Array.isArray(patch.operations) || patch.operations.length === 0
|
|
2121
|
-
)) {
|
|
2242
|
+
if (!body || !Array.isArray(body.patches) || body.patches.length === 0 || !body.patches.every(isElementPatchRequest)) {
|
|
2122
2243
|
return c.json({ error: "patches with target and operations required" }, 400);
|
|
2123
2244
|
}
|
|
2124
|
-
const
|
|
2245
|
+
const batch = { sourceFile: ctx.filePath, patches: body.patches };
|
|
2246
|
+
const unsafeFields = findUnsafeElementPatchBatchValues([batch]);
|
|
2125
2247
|
if (unsafeFields.length > 0) {
|
|
2126
2248
|
return rejectUnsafeMutationValues(c, unsafeFields);
|
|
2127
2249
|
}
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
} catch {
|
|
2132
|
-
return c.json({ error: "not found" }, 404);
|
|
2133
|
-
}
|
|
2134
|
-
const result = foldElementPatches(originalContent, body.patches);
|
|
2135
|
-
if (result.content === originalContent) {
|
|
2136
|
-
return c.json({
|
|
2137
|
-
ok: true,
|
|
2138
|
-
changed: false,
|
|
2139
|
-
matched: result.matched,
|
|
2140
|
-
content: originalContent,
|
|
2141
|
-
path: ctx.filePath
|
|
2142
|
-
});
|
|
2250
|
+
const result = commitElementPatchBatches(ctx.project.dir, [batch]);
|
|
2251
|
+
if ("error" in result) {
|
|
2252
|
+
return elementPatchBatchCommitErrorResponse(c, result.error, result.sourceFile);
|
|
2143
2253
|
}
|
|
2144
|
-
const
|
|
2145
|
-
if (
|
|
2146
|
-
writeFileSync4(ctx.absPath, result.content, "utf-8");
|
|
2254
|
+
const file = result.files[0];
|
|
2255
|
+
if (!file) return c.json({ error: "empty element patch result" }, 500);
|
|
2147
2256
|
return c.json({
|
|
2148
2257
|
ok: true,
|
|
2149
|
-
changed:
|
|
2150
|
-
matched:
|
|
2151
|
-
content:
|
|
2152
|
-
path:
|
|
2153
|
-
backupPath:
|
|
2258
|
+
changed: file.changed,
|
|
2259
|
+
matched: file.matched,
|
|
2260
|
+
content: file.after,
|
|
2261
|
+
path: file.sourceFile,
|
|
2262
|
+
backupPath: file.backupPath
|
|
2154
2263
|
});
|
|
2155
2264
|
});
|
|
2156
2265
|
api.post("/projects/:id/file-mutations/wrap-elements/*", async (c) => {
|
|
@@ -2328,6 +2437,11 @@ function registerFileRoutes(api, adapter) {
|
|
|
2328
2437
|
const parsed = parseGsapScriptAcorn(block.scriptText);
|
|
2329
2438
|
return c.json(parsed);
|
|
2330
2439
|
});
|
|
2440
|
+
api.get("/projects/:id/gsap-mutation-capabilities", async (c) => {
|
|
2441
|
+
const project = await adapter.resolveProject(c.req.param("id"));
|
|
2442
|
+
if (!project) return c.json({ error: "not found" }, 404);
|
|
2443
|
+
return c.json({ atomicOwnershipPairs: true });
|
|
2444
|
+
});
|
|
2331
2445
|
api.post("/projects/:id/gsap-mutations/*", async (c) => {
|
|
2332
2446
|
const res = await resolveProjectPath(c, adapter, (id) => `/projects/${id}/gsap-mutations/`, {
|
|
2333
2447
|
mustExist: true
|
|
@@ -2357,6 +2471,25 @@ function registerFileRoutes(api, adapter) {
|
|
|
2357
2471
|
}
|
|
2358
2472
|
return applyGsapMutations(c, res, body.mutations);
|
|
2359
2473
|
});
|
|
2474
|
+
api.post("/projects/:id/gsap-mutation-rollback/*", async (c) => {
|
|
2475
|
+
const res = await resolveProjectPath(
|
|
2476
|
+
c,
|
|
2477
|
+
adapter,
|
|
2478
|
+
(id) => `/projects/${id}/gsap-mutation-rollback/`,
|
|
2479
|
+
{ mustExist: true }
|
|
2480
|
+
);
|
|
2481
|
+
if ("error" in res) return res.error;
|
|
2482
|
+
const body = await c.req.json().catch(() => null);
|
|
2483
|
+
if (!body || typeof body.expected !== "string" || typeof body.restore !== "string") {
|
|
2484
|
+
return c.json({ error: "expected and restore contents required" }, 400);
|
|
2485
|
+
}
|
|
2486
|
+
const current = readFileSync3(res.absPath, "utf-8");
|
|
2487
|
+
if (current !== body.expected) {
|
|
2488
|
+
return c.json({ ok: true, restored: false, conflict: true });
|
|
2489
|
+
}
|
|
2490
|
+
writeFileSync4(res.absPath, body.restore, "utf-8");
|
|
2491
|
+
return c.json({ ok: true, restored: true, conflict: false });
|
|
2492
|
+
});
|
|
2360
2493
|
}
|
|
2361
2494
|
|
|
2362
2495
|
// src/routes/preview.ts
|