From fe6efa9ac8342fd4eca024950760a40690cf83ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Pereira?= <7235666+jomifepe@users.noreply.github.com> Date: Tue, 19 May 2026 11:51:19 +0100 Subject: [PATCH 1/5] feat: use new screenshot service --- src/clients/custom-types.ts | 40 +++++++++++++++++++------------------ 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/src/clients/custom-types.ts b/src/clients/custom-types.ts index f26436a..dac6626 100644 --- a/src/clients/custom-types.ts +++ b/src/clients/custom-types.ts @@ -168,7 +168,7 @@ export async function removeSlice( }); } -const AclCreateResponseSchema = z.object({ +const ScreenshotPresignedUrlResponseSchema = z.object({ values: z.object({ url: z.string(), fields: z.record(z.string(), z.string()), @@ -177,10 +177,11 @@ const AclCreateResponseSchema = z.object({ }); const SUPPORTED_IMAGE_MIME_TYPES: Record = { - "image/png": ".png", - "image/jpeg": ".jpg", - "image/gif": ".gif", - "image/webp": ".webp", + "image/png": "png", + "image/jpeg": "jpg", + "image/gif": "gif", + "image/webp": "webp", + "image/svg+xml": "svg", }; export async function uploadScreenshot( @@ -200,29 +201,30 @@ export async function uploadScreenshot( throw new UnsupportedFileTypeError(type); } - const aclUrl = new URL("create", getAclProviderUrl(host)); - const acl = await request(aclUrl, { - headers: { Repository: repo, Authorization: `Bearer ${token}` }, - schema: AclCreateResponseSchema, + const presignedUrl = new URL("presigned-url", getScreenshotServiceUrl(host)); + presignedUrl.searchParams.set("repository", repo); + const presigned = await request(presignedUrl, { + headers: { repository: repo, Authorization: `Bearer ${token}` }, + schema: ScreenshotPresignedUrlResponseSchema, }); const extension = SUPPORTED_IMAGE_MIME_TYPES[type]; - const digest = createHash("md5") + const digest = createHash("sha1") .update(new Uint8Array(await blob.arrayBuffer())) .digest("hex"); - const key = `${repo}/shared-slices/${sliceId}/${variationId}/${digest}${extension}`; + const key = `${repo}/shared-slices/${sliceId}/${variationId}/${digest}.${extension}`; const formData = new FormData(); - for (const [field, value] of Object.entries(acl.values.fields)) { + for (const [field, value] of Object.entries(presigned.values.fields)) { formData.append(field, value); } - formData.append("key", key); - formData.append("Content-Type", type); - formData.append("file", blob); + formData.set("key", key); + formData.set("Content-Type", type); + formData.set("file", blob); - await request(acl.values.url, { method: "POST", body: formData }); + await request(presigned.values.url, { method: "POST", body: formData }); - const url = new URL(key, appendTrailingSlash(acl.imgixEndpoint)); + const url = new URL(key, appendTrailingSlash(presigned.imgixEndpoint)); url.searchParams.set("auto", "compress,format"); return url; @@ -243,6 +245,6 @@ function getCustomTypesServiceUrl(host: string): URL { return new URL(`https://customtypes.${host}/`); } -function getAclProviderUrl(host: string): URL { - return new URL(`https://acl-provider.${host}/`); +function getScreenshotServiceUrl(host: string): URL { + return new URL(`https://api.internal.${host}/screenshot/`); } From fe256d4913a625cc15ddec56c51033bf689112eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Pereira?= <7235666+jomifepe@users.noreply.github.com> Date: Tue, 26 May 2026 11:14:14 +0100 Subject: [PATCH 2/5] fix: keep extension dot in screenshot MIME type map Co-authored-by: Cursor --- src/clients/custom-types.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/clients/custom-types.ts b/src/clients/custom-types.ts index dac6626..0de1ea4 100644 --- a/src/clients/custom-types.ts +++ b/src/clients/custom-types.ts @@ -177,11 +177,11 @@ const ScreenshotPresignedUrlResponseSchema = z.object({ }); const SUPPORTED_IMAGE_MIME_TYPES: Record = { - "image/png": "png", - "image/jpeg": "jpg", - "image/gif": "gif", - "image/webp": "webp", - "image/svg+xml": "svg", + "image/png": ".png", + "image/jpeg": ".jpg", + "image/gif": ".gif", + "image/webp": ".webp", + "image/svg+xml": ".svg", }; export async function uploadScreenshot( @@ -212,7 +212,7 @@ export async function uploadScreenshot( const digest = createHash("sha1") .update(new Uint8Array(await blob.arrayBuffer())) .digest("hex"); - const key = `${repo}/shared-slices/${sliceId}/${variationId}/${digest}.${extension}`; + const key = `${repo}/shared-slices/${sliceId}/${variationId}/${digest}${extension}`; const formData = new FormData(); for (const [field, value] of Object.entries(presigned.values.fields)) { From 7ee4446a89776fc8e93ac7452e28f4775dea43d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Pereira?= <7235666+jomifepe@users.noreply.github.com> Date: Tue, 26 May 2026 11:14:32 +0100 Subject: [PATCH 3/5] fix: drop SVG from supported screenshot MIME types Co-authored-by: Cursor --- src/clients/custom-types.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/clients/custom-types.ts b/src/clients/custom-types.ts index 0de1ea4..ca1105a 100644 --- a/src/clients/custom-types.ts +++ b/src/clients/custom-types.ts @@ -181,7 +181,6 @@ const SUPPORTED_IMAGE_MIME_TYPES: Record = { "image/jpeg": ".jpg", "image/gif": ".gif", "image/webp": ".webp", - "image/svg+xml": ".svg", }; export async function uploadScreenshot( From d1442cf4f7cb13eb7c1247667ef339cc69203756 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Pereira?= <7235666+jomifepe@users.noreply.github.com> Date: Wed, 27 May 2026 10:43:45 +0100 Subject: [PATCH 4/5] feat: delete screenshots --- src/clients/custom-types.ts | 14 ++++++++++++++ src/commands/push.ts | 5 +++++ 2 files changed, 19 insertions(+) diff --git a/src/clients/custom-types.ts b/src/clients/custom-types.ts index ca1105a..f6bd316 100644 --- a/src/clients/custom-types.ts +++ b/src/clients/custom-types.ts @@ -183,6 +183,20 @@ const SUPPORTED_IMAGE_MIME_TYPES: Record = { "image/webp": ".webp", }; +export async function deleteScreenshots( + sliceId: string, + config: { repo: string; token: string | undefined; host: string }, +): Promise { + const { repo, token, host } = config; + const url = new URL("delete", getScreenshotServiceUrl(host)); + url.searchParams.set("repository", repo); + await request(url, { + method: "POST", + headers: { repository: repo, Authorization: `Bearer ${token}` }, + body: { sliceId }, + }); +} + export async function uploadScreenshot( blob: Blob, config: { diff --git a/src/commands/push.ts b/src/commands/push.ts index 7fb3871..1d204f5 100644 --- a/src/commands/push.ts +++ b/src/commands/push.ts @@ -6,6 +6,7 @@ import { getAdapter } from "../adapters"; import { getHost, getToken } from "../auth"; import { getDocumentTotalByCustomTypes } from "../clients/core"; import { + deleteScreenshots, getCustomTypes, getSlices, insertCustomType, @@ -150,6 +151,10 @@ export default createCommand(config, async ({ values }) => { } for (const id of sliceOps.delete.map((m) => m.id)) { await removeSlice(id, { repo, token, host }); + await deleteScreenshots(id, { repo, token, host }).catch((error) => { + const message = error instanceof Error ? error.message : String(error); + console.warn(`Failed to delete screenshots for slice "${id}": ${message}`); + }); } const onboardingSteps: OnboardingStep[] = []; From 221666d37a3b82f1d81b472f6f6b6e059c36d8e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Pereira?= <7235666+jomifepe@users.noreply.github.com> Date: Wed, 27 May 2026 11:14:31 +0100 Subject: [PATCH 5/5] test: delete --- test/fixtures/slice-screenshot.png | Bin 0 -> 9683 bytes test/prismic.ts | 16 +++++++ test/push.serial.test.ts | 73 ++++++++++++++++++++++++++++- 3 files changed, 87 insertions(+), 2 deletions(-) create mode 100644 test/fixtures/slice-screenshot.png diff --git a/test/fixtures/slice-screenshot.png b/test/fixtures/slice-screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..3ddc1f3166d841c48014529fa93fa78caf4ae775 GIT binary patch literal 9683 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 Xo { + const host = config.host ?? DEFAULT_HOST; + const url = new URL("files", `https://api.internal.${host}/screenshot/`); + url.searchParams.set("repository", config.repo); + const res = await fetch(url, { + headers: { Authorization: `Bearer ${config.token}` }, + }); + if (!res.ok) throw new Error(`Failed to list screenshot files: ${res.status} ${await res.text()}`); + const data = (await res.json()) as { keys: string[] }; + return data.keys; +} + +export function getScreenshotPrefix(config: RepoConfig, sliceId: string): string { + return `${config.repo}/shared-slices/${sliceId}/`; +} + export async function getWebhooks( config: RepoConfig, ): Promise<{ config: Record }[]> { diff --git a/test/push.serial.test.ts b/test/push.serial.test.ts index 2e18eae..5ed8ace 100644 --- a/test/push.serial.test.ts +++ b/test/push.serial.test.ts @@ -1,5 +1,13 @@ -import { buildCustomType, it, writeLocalCustomType } from "./it"; -import { getCustomTypes, insertCustomType } from "./prismic"; +import { fileURLToPath } from "node:url"; + +import { buildCustomType, buildSlice, it, writeLocalCustomType, writeLocalSlice } from "./it"; +import { + getCustomTypes, + getScreenshotPrefix, + getSlices, + insertCustomType, + listScreenshotFiles, +} from "./prismic"; it("supports --help", async ({ expect, prismic }) => { const { stdout, exitCode } = await prismic("push", ["--help"]); @@ -59,3 +67,64 @@ it("pushes a local edit that overwrites a remote model", async ({ const updated = remote.find((t) => t.id === customType.id); expect(updated?.label).toBe("Modified"); }); + +it("deletes a remote slice and its screenshots when removed locally", async ({ + expect, + project, + prismic, + repo, + token, + host, +}) => { + // Mirror remote into local so push only deletes the slice we remove below. + const pull = await prismic("pull", ["--repo", repo, "--force"]); + expect(pull.exitCode).toBe(0); + + const slice = buildSlice(); + await writeLocalSlice(project, slice); + + const insert = await prismic("push", ["--repo", repo]); + expect(insert.exitCode).toBe(0); + + const screenshotPath = fileURLToPath(new URL("./fixtures/slice-screenshot.png", import.meta.url)); + + const editVariation = await prismic("slice", [ + "edit-variation", + "default", + "--from-slice", + slice.id, + "--screenshot", + screenshotPath, + ]); + expect(editVariation.exitCode).toBe(0); + + const update = await prismic("push", ["--repo", repo]); + expect(update.exitCode).toBe(0); + + const screenshotPrefix = getScreenshotPrefix({ repo, token, host }, slice.id); + await expect + .poll(async () => { + const keys = await listScreenshotFiles({ repo, token, host }); + return keys.some((key) => key.startsWith(screenshotPrefix)); + }) + .toBe(true); + + const remove = await prismic("slice", ["remove", slice.id]); + expect(remove.exitCode).toBe(0); + + const pushDelete = await prismic("push", ["--repo", repo, "--force"]); + expect(pushDelete.exitCode).toBe(0); + + await expect + .poll(async () => (await getSlices({ repo, token, host })).map((s) => s.id), { + timeout: 5_000, + }) + .not.toContain(slice.id); + + await expect + .poll(async () => { + const keys = await listScreenshotFiles({ repo, token, host }); + return keys.some((key) => key.startsWith(screenshotPrefix)); + }) + .toBe(false); +});