@hyperframes/studio-server 0.8.71 → 0.8.73
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +3 -1
- package/dist/index.js +63 -46
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.d.ts
CHANGED
|
@@ -90,6 +90,8 @@ declare const PREVIEW_BUNDLE_OPTIONS: {
|
|
|
90
90
|
declare const MIME_TYPES: Record<string, string>;
|
|
91
91
|
declare function getMimeType(path: string): string;
|
|
92
92
|
|
|
93
|
+
declare function requestSubPath(url: string, route: string): string;
|
|
94
|
+
|
|
93
95
|
interface FileWriteReceipt {
|
|
94
96
|
path: string;
|
|
95
97
|
version: string;
|
|
@@ -183,4 +185,4 @@ type BackgroundRemovalRender = (options: {
|
|
|
183
185
|
}>;
|
|
184
186
|
declare function createBackgroundRemovalJob(opts: BackgroundRemovalJobOptions, render: BackgroundRemovalRender): MediaProcessingJobState;
|
|
185
187
|
|
|
186
|
-
export { type AppliedFileMutation, type BackgroundRemovalRender, type DuplicateElementResult, type FileMutationInput, type FileWriteReceipt, MIME_TYPES, MediaProcessingJobState, PREVIEW_BUNDLE_OPTIONS, SourceMutationTarget, StudioApiAdapter, type ThumbnailOutputDimensions, affectsProjectSignature, applyFileMutations, buildSubCompositionHtml, compositionsAffectedBy, consumeFileWriteReceipt, createBackgroundRemovalJob, createProjectSignature, createStudioApi, duplicateElementInHtml, fileContentVersion, getMimeType, identifyFileWrite, settledFileTag, thumbnailDeviceScaleFactor, walkDir };
|
|
188
|
+
export { type AppliedFileMutation, type BackgroundRemovalRender, type DuplicateElementResult, type FileMutationInput, type FileWriteReceipt, MIME_TYPES, MediaProcessingJobState, PREVIEW_BUNDLE_OPTIONS, SourceMutationTarget, StudioApiAdapter, type ThumbnailOutputDimensions, affectsProjectSignature, applyFileMutations, buildSubCompositionHtml, compositionsAffectedBy, consumeFileWriteReceipt, createBackgroundRemovalJob, createProjectSignature, createStudioApi, duplicateElementInHtml, fileContentVersion, getMimeType, identifyFileWrite, requestSubPath, settledFileTag, thumbnailDeviceScaleFactor, walkDir };
|
package/dist/index.js
CHANGED
|
@@ -1018,6 +1018,14 @@ function resolveGsapWriter(env) {
|
|
|
1018
1018
|
throw new Error(`Invalid ${GSAP_WRITER_MIGRATION.flag}=${configured}; expected recast or acorn`);
|
|
1019
1019
|
}
|
|
1020
1020
|
|
|
1021
|
+
// src/helpers/requestSubPath.ts
|
|
1022
|
+
function requestSubPath(url, route) {
|
|
1023
|
+
const routeSegments = route.split("/");
|
|
1024
|
+
const segments = new URL(url).pathname.split("/");
|
|
1025
|
+
const start = segments.indexOf(routeSegments[0] ?? "");
|
|
1026
|
+
return decodeURIComponent(segments.slice(start + routeSegments.length).join("/"));
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1021
1029
|
// src/routes/files.ts
|
|
1022
1030
|
async function loadGsapParser() {
|
|
1023
1031
|
return import("@hyperframes/parsers/gsap-parser-recast");
|
|
@@ -1040,7 +1048,7 @@ function isDanglingSymlinkInProject(projectDir, lexicalPath) {
|
|
|
1040
1048
|
return true;
|
|
1041
1049
|
}
|
|
1042
1050
|
}
|
|
1043
|
-
async function resolveProjectPath(c, adapter,
|
|
1051
|
+
async function resolveProjectPath(c, adapter, route, opts) {
|
|
1044
1052
|
const id = c.req.param("id");
|
|
1045
1053
|
const project = await adapter.resolveProject(id);
|
|
1046
1054
|
if (!project) {
|
|
@@ -1051,7 +1059,7 @@ async function resolveProjectPath(c, adapter, pathPrefix, opts) {
|
|
|
1051
1059
|
error: c.json({ error: "not found", why: "project_dir_missing" }, 404)
|
|
1052
1060
|
};
|
|
1053
1061
|
}
|
|
1054
|
-
const filePath =
|
|
1062
|
+
const filePath = requestSubPath(c.req.url, `projects/:id/${route}`);
|
|
1055
1063
|
if (filePath.includes("\0")) {
|
|
1056
1064
|
return { error: c.json({ error: "forbidden", why: "nul" }, 403) };
|
|
1057
1065
|
}
|
|
@@ -1068,10 +1076,10 @@ async function resolveProjectPath(c, adapter, pathPrefix, opts) {
|
|
|
1068
1076
|
return { project, filePath, absPath };
|
|
1069
1077
|
}
|
|
1070
1078
|
function resolveProjectFile(c, adapter, opts) {
|
|
1071
|
-
return resolveProjectPath(c, adapter,
|
|
1079
|
+
return resolveProjectPath(c, adapter, "files", opts);
|
|
1072
1080
|
}
|
|
1073
1081
|
function resolveFileMutationContext(c, adapter, operation) {
|
|
1074
|
-
return resolveProjectPath(c, adapter,
|
|
1082
|
+
return resolveProjectPath(c, adapter, `file-mutations/${operation}`);
|
|
1075
1083
|
}
|
|
1076
1084
|
function isOptionalInteger(value) {
|
|
1077
1085
|
return value === void 0 || Number.isInteger(value);
|
|
@@ -3142,7 +3150,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
3142
3150
|
}
|
|
3143
3151
|
);
|
|
3144
3152
|
api.get("/projects/:id/gsap-animations/*", async (c) => {
|
|
3145
|
-
const res = await resolveProjectPath(c, adapter,
|
|
3153
|
+
const res = await resolveProjectPath(c, adapter, "gsap-animations", {
|
|
3146
3154
|
mustExist: true
|
|
3147
3155
|
});
|
|
3148
3156
|
if ("error" in res) return res.error;
|
|
@@ -3165,7 +3173,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
3165
3173
|
return c.json({ atomicOwnershipPairs: true });
|
|
3166
3174
|
});
|
|
3167
3175
|
api.post("/projects/:id/gsap-mutations/*", async (c) => {
|
|
3168
|
-
const res = await resolveProjectPath(c, adapter,
|
|
3176
|
+
const res = await resolveProjectPath(c, adapter, "gsap-mutations", {
|
|
3169
3177
|
mustExist: true
|
|
3170
3178
|
});
|
|
3171
3179
|
if ("error" in res) return res.error;
|
|
@@ -3176,12 +3184,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
3176
3184
|
return applyGsapMutations(c, res, [body]);
|
|
3177
3185
|
});
|
|
3178
3186
|
api.post("/projects/:id/gsap-mutations-batch/*", async (c) => {
|
|
3179
|
-
const res = await resolveProjectPath(
|
|
3180
|
-
c,
|
|
3181
|
-
adapter,
|
|
3182
|
-
(id) => `/projects/${id}/gsap-mutations-batch/`,
|
|
3183
|
-
{ mustExist: true }
|
|
3184
|
-
);
|
|
3187
|
+
const res = await resolveProjectPath(c, adapter, "gsap-mutations-batch", { mustExist: true });
|
|
3185
3188
|
if ("error" in res) return res.error;
|
|
3186
3189
|
const body = await c.req.json().catch(() => null);
|
|
3187
3190
|
if (!body || !Array.isArray(body.mutations) || body.mutations.length === 0) {
|
|
@@ -3194,12 +3197,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
3194
3197
|
return applyGsapMutations(c, res, body.mutations);
|
|
3195
3198
|
});
|
|
3196
3199
|
api.post("/projects/:id/gsap-mutation-rollback/*", async (c) => {
|
|
3197
|
-
const res = await resolveProjectPath(
|
|
3198
|
-
c,
|
|
3199
|
-
adapter,
|
|
3200
|
-
(id) => `/projects/${id}/gsap-mutation-rollback/`,
|
|
3201
|
-
{ mustExist: true }
|
|
3202
|
-
);
|
|
3200
|
+
const res = await resolveProjectPath(c, adapter, "gsap-mutation-rollback", { mustExist: true });
|
|
3203
3201
|
if ("error" in res) return res.error;
|
|
3204
3202
|
const body = await c.req.json().catch(() => null);
|
|
3205
3203
|
if (!body || typeof body.expected !== "string" || typeof body.restore !== "string") {
|
|
@@ -3766,9 +3764,7 @@ ${runtimeTag}`;
|
|
|
3766
3764
|
const vars = previewVariablesFromRequest(c.req.query("variables"));
|
|
3767
3765
|
if (vars.error !== void 0) return c.json({ error: vars.error }, 400);
|
|
3768
3766
|
const previewVariables = vars.values;
|
|
3769
|
-
const compPath =
|
|
3770
|
-
c.req.path.replace(`/projects/${project.id}/preview/comp/`, "").split("?")[0] ?? ""
|
|
3771
|
-
);
|
|
3767
|
+
const compPath = requestSubPath(c.req.url, "projects/:id/preview/comp");
|
|
3772
3768
|
const compFile = resolveWithinProject(project.dir, compPath);
|
|
3773
3769
|
if (!compFile || !existsSync5(compFile) || !statSync5(compFile).isFile()) {
|
|
3774
3770
|
return c.text("not found", 404);
|
|
@@ -3802,9 +3798,7 @@ ${runtimeTag}`;
|
|
|
3802
3798
|
api.get("/projects/:id/preview/*", async (c) => {
|
|
3803
3799
|
const project = await adapter.resolveProject(c.req.param("id"));
|
|
3804
3800
|
if (!project) return c.json({ error: "not found" }, 404);
|
|
3805
|
-
const subPath =
|
|
3806
|
-
c.req.path.replace(`/projects/${project.id}/preview/`, "").split("?")[0] ?? ""
|
|
3807
|
-
);
|
|
3801
|
+
const subPath = requestSubPath(c.req.url, "projects/:id/preview");
|
|
3808
3802
|
const candidate = resolve4(project.dir, subPath);
|
|
3809
3803
|
const file = isWithinProjectRoot(project.dir, candidate) ? candidate : null;
|
|
3810
3804
|
if (!file) {
|
|
@@ -4198,7 +4192,10 @@ function registerRenderRoutes(api, adapter) {
|
|
|
4198
4192
|
|
|
4199
4193
|
// src/routes/thumbnail.ts
|
|
4200
4194
|
import {
|
|
4195
|
+
closeSync as closeSync3,
|
|
4201
4196
|
existsSync as existsSync7,
|
|
4197
|
+
fstatSync as fstatSync3,
|
|
4198
|
+
openSync as openSync3,
|
|
4202
4199
|
mkdirSync as mkdirSync5,
|
|
4203
4200
|
readFileSync as readFileSync12,
|
|
4204
4201
|
readdirSync as readdirSync6,
|
|
@@ -4285,10 +4282,10 @@ var ThumbnailGenerationCoordinator = class {
|
|
|
4285
4282
|
if (signal.aborted) return Promise.reject(new DOMException("Aborted", "AbortError"));
|
|
4286
4283
|
let entry = this.entries.get(key);
|
|
4287
4284
|
if (!entry) {
|
|
4288
|
-
let
|
|
4285
|
+
let resolve6;
|
|
4289
4286
|
let reject;
|
|
4290
4287
|
const promise = new Promise((resolvePromise, rejectPromise) => {
|
|
4291
|
-
|
|
4288
|
+
resolve6 = resolvePromise;
|
|
4292
4289
|
reject = rejectPromise;
|
|
4293
4290
|
});
|
|
4294
4291
|
entry = {
|
|
@@ -4298,7 +4295,7 @@ var ThumbnailGenerationCoordinator = class {
|
|
|
4298
4295
|
state: "queued",
|
|
4299
4296
|
work,
|
|
4300
4297
|
promise,
|
|
4301
|
-
resolve:
|
|
4298
|
+
resolve: resolve6,
|
|
4302
4299
|
reject
|
|
4303
4300
|
};
|
|
4304
4301
|
this.entries.set(key, entry);
|
|
@@ -4312,7 +4309,7 @@ var ThumbnailGenerationCoordinator = class {
|
|
|
4312
4309
|
return /* @__PURE__ */ new Set([...this.entries.keys(), ...[...this.activeEntries].map((entry) => entry.key)]);
|
|
4313
4310
|
}
|
|
4314
4311
|
lease(entry, signal) {
|
|
4315
|
-
return new Promise((
|
|
4312
|
+
return new Promise((resolve6, reject) => {
|
|
4316
4313
|
let released = false;
|
|
4317
4314
|
const release = () => {
|
|
4318
4315
|
if (released) return;
|
|
@@ -4336,7 +4333,7 @@ var ThumbnailGenerationCoordinator = class {
|
|
|
4336
4333
|
entry.promise.then(
|
|
4337
4334
|
(value) => {
|
|
4338
4335
|
release();
|
|
4339
|
-
|
|
4336
|
+
resolve6(value);
|
|
4340
4337
|
},
|
|
4341
4338
|
(reason) => {
|
|
4342
4339
|
release();
|
|
@@ -4418,6 +4415,22 @@ function writeThumbnailAtomically(path, buffer) {
|
|
|
4418
4415
|
rmSync4(temporaryPath, { force: true });
|
|
4419
4416
|
}
|
|
4420
4417
|
}
|
|
4418
|
+
function readCompositionSource(file) {
|
|
4419
|
+
let fd;
|
|
4420
|
+
try {
|
|
4421
|
+
fd = openSync3(file, "r");
|
|
4422
|
+
} catch (err) {
|
|
4423
|
+
if (err.code === "ENOENT") return "missing";
|
|
4424
|
+
throw err;
|
|
4425
|
+
}
|
|
4426
|
+
try {
|
|
4427
|
+
const stat = fstatSync3(fd);
|
|
4428
|
+
if (!stat.isFile()) return "not-a-file";
|
|
4429
|
+
return { html: readFileSync12(fd, "utf-8"), mtimeMs: stat.mtimeMs };
|
|
4430
|
+
} finally {
|
|
4431
|
+
closeSync3(fd);
|
|
4432
|
+
}
|
|
4433
|
+
}
|
|
4421
4434
|
function registerThumbnailRoutes(api, adapter) {
|
|
4422
4435
|
api.get("/projects/:id/thumbnail/*", async (c) => {
|
|
4423
4436
|
if (!adapter.generateThumbnail) {
|
|
@@ -4426,10 +4439,11 @@ function registerThumbnailRoutes(api, adapter) {
|
|
|
4426
4439
|
const resolved = await resolveProjectAndSignature(adapter, c.req.param("id"));
|
|
4427
4440
|
if (!resolved) return c.json({ error: "not found" }, 404);
|
|
4428
4441
|
const { project, signature: projectSignature } = resolved;
|
|
4429
|
-
let compPath =
|
|
4430
|
-
c.req.path.replace(`/projects/${project.id}/thumbnail/`, "").split("?")[0] ?? ""
|
|
4431
|
-
);
|
|
4442
|
+
let compPath = requestSubPath(c.req.url, "projects/:id/thumbnail");
|
|
4432
4443
|
if (compPath && !compPath.includes(".")) compPath += ".html";
|
|
4444
|
+
const htmlFile = resolveWithinProject(project.dir, compPath);
|
|
4445
|
+
const source = htmlFile ? readCompositionSource(htmlFile) : "not-a-file";
|
|
4446
|
+
if (source === "not-a-file") return c.json({ error: "not found" }, 404);
|
|
4433
4447
|
const inputSignature = compositionInputSignature(project.dir, compPath, projectSignature);
|
|
4434
4448
|
const url = new URL(c.req.url, `http://${c.req.header("host") || "localhost"}`);
|
|
4435
4449
|
const rawSeekTime = url.searchParams.get("t");
|
|
@@ -4449,11 +4463,10 @@ function registerThumbnailRoutes(api, adapter) {
|
|
|
4449
4463
|
let compH = vpHeight || 1080;
|
|
4450
4464
|
let sourceMtime = 0;
|
|
4451
4465
|
let sourceKey = "";
|
|
4452
|
-
|
|
4453
|
-
|
|
4454
|
-
const html = readFileSync12(htmlFile, "utf-8");
|
|
4466
|
+
if (source !== "missing") {
|
|
4467
|
+
const { html } = source;
|
|
4455
4468
|
sourceKey = `_${createHash4("sha1").update(html).digest("hex").slice(0, 16)}`;
|
|
4456
|
-
sourceMtime = Math.round(
|
|
4469
|
+
sourceMtime = Math.round(source.mtimeMs);
|
|
4457
4470
|
if (!vpWidth) {
|
|
4458
4471
|
const wMatch = html.match(/data-width=["'](\d+)["']/);
|
|
4459
4472
|
const hMatch = html.match(/data-height=["'](\d+)["']/);
|
|
@@ -4475,7 +4488,8 @@ function registerThumbnailRoutes(api, adapter) {
|
|
|
4475
4488
|
motionKey = `_${createHash4("sha1").update(motionContent).digest("hex").slice(0, 16)}`;
|
|
4476
4489
|
sourceMtime = Math.max(sourceMtime, Math.round(statSync7(motionFile).mtimeMs));
|
|
4477
4490
|
}
|
|
4478
|
-
const
|
|
4491
|
+
const projectUrl = `http://${c.req.header("host")}/api/projects/${encodeURIComponent(project.id)}`;
|
|
4492
|
+
const previewUrl = compPath === "index.html" ? `${projectUrl}/preview` : `${projectUrl}/preview/comp/${compPath.split("/").map(encodeURIComponent).join("/")}`;
|
|
4479
4493
|
const cacheDir = join11(project.dir, ".thumbnails");
|
|
4480
4494
|
const selectorKey = selector ? `_${selector.replace(/[^a-zA-Z0-9_-]+/g, "_").slice(0, 80)}_${selectorIndex ?? 0}` : "";
|
|
4481
4495
|
const urlVersionKey = urlVersion ? `_${urlVersion.replace(/[^a-zA-Z0-9_-]+/g, "_").slice(0, 32)}` : "";
|
|
@@ -4549,15 +4563,17 @@ function registerThumbnailRoutes(api, adapter) {
|
|
|
4549
4563
|
|
|
4550
4564
|
// src/routes/waveform.ts
|
|
4551
4565
|
import { existsSync as existsSync8, readFileSync as readFileSync13, statSync as statSync8 } from "fs";
|
|
4552
|
-
import { join as join12 } from "path";
|
|
4566
|
+
import { join as join12, resolve as resolve5 } from "path";
|
|
4567
|
+
import { isWithinProjectRoot as isWithinProjectRoot2 } from "@hyperframes/parsers/asset-resolution";
|
|
4553
4568
|
function registerWaveformRoutes(api, adapter) {
|
|
4554
4569
|
api.get("/projects/:id/waveform/*", async (c) => {
|
|
4555
4570
|
const project = await adapter.resolveProject(c.req.param("id"));
|
|
4556
4571
|
if (!project) return c.json({ error: "not found" }, 404);
|
|
4557
|
-
const assetPath =
|
|
4558
|
-
|
|
4559
|
-
)
|
|
4560
|
-
|
|
4572
|
+
const assetPath = requestSubPath(c.req.url, "projects/:id/waveform");
|
|
4573
|
+
const audioPath = resolve5(project.dir, assetPath);
|
|
4574
|
+
if (!isWithinProjectRoot2(project.dir, audioPath)) {
|
|
4575
|
+
return c.json({ error: "file not found" }, 404);
|
|
4576
|
+
}
|
|
4561
4577
|
const stats = statSync8(audioPath, { throwIfNoEntry: false });
|
|
4562
4578
|
if (!stats) return c.json({ error: "file not found" }, 404);
|
|
4563
4579
|
const cacheDir = join12(project.dir, ".waveform-cache");
|
|
@@ -4584,7 +4600,7 @@ function registerWaveformRoutes(api, adapter) {
|
|
|
4584
4600
|
}
|
|
4585
4601
|
|
|
4586
4602
|
// src/routes/fonts.ts
|
|
4587
|
-
import { closeSync as
|
|
4603
|
+
import { closeSync as closeSync4, constants as constants2, fstatSync as fstatSync4, openSync as openSync4, readSync } from "fs";
|
|
4588
4604
|
import {
|
|
4589
4605
|
collectFontFileEntries,
|
|
4590
4606
|
fontDirectories,
|
|
@@ -4691,12 +4707,12 @@ function registerFontRoutes(api) {
|
|
|
4691
4707
|
if (!located) return c.json({ error: "font not found" }, 404);
|
|
4692
4708
|
let fd;
|
|
4693
4709
|
try {
|
|
4694
|
-
fd =
|
|
4710
|
+
fd = openSync4(located.path, constants2.O_RDONLY | constants2.O_NOFOLLOW);
|
|
4695
4711
|
} catch {
|
|
4696
4712
|
return c.json({ error: "font file not accessible" }, 404);
|
|
4697
4713
|
}
|
|
4698
4714
|
try {
|
|
4699
|
-
const stat =
|
|
4715
|
+
const stat = fstatSync4(fd);
|
|
4700
4716
|
if (stat.size > SYSTEM_FONT_SIZE_LIMIT) {
|
|
4701
4717
|
return c.json({ error: "font file too large" }, 413);
|
|
4702
4718
|
}
|
|
@@ -4713,7 +4729,7 @@ function registerFontRoutes(api) {
|
|
|
4713
4729
|
} catch {
|
|
4714
4730
|
return c.json({ error: "failed to read font file" }, 500);
|
|
4715
4731
|
} finally {
|
|
4716
|
-
|
|
4732
|
+
closeSync4(fd);
|
|
4717
4733
|
}
|
|
4718
4734
|
});
|
|
4719
4735
|
}
|
|
@@ -5237,6 +5253,7 @@ export {
|
|
|
5237
5253
|
parseSourceDocument,
|
|
5238
5254
|
patchElementInHtml,
|
|
5239
5255
|
removeElementFromHtml,
|
|
5256
|
+
requestSubPath,
|
|
5240
5257
|
settledFileTag,
|
|
5241
5258
|
splitElementInHtml,
|
|
5242
5259
|
thumbnailDeviceScaleFactor,
|