@designfever/web-review-kit 0.7.1 → 0.7.2

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/README.md CHANGED
@@ -28,7 +28,7 @@ This package does not own internal operator tools, private admin keys, or produc
28
28
  - [Architecture and runtime logic](docs/architecture.md): core runtime, React shell, coordinate, anchor, and extension boundaries.
29
29
  - [Figma overlay](docs/figma-overlay.md): how the shell toggles a host Figma overlay.
30
30
  - [Grid overlay](docs/grid-overlay.md): how the shell toggles a host grid/helper overlay.
31
- - [Release notes 0.7.0](docs/release-notes-0.7.0.md): latest Figma image, QA field, and remote adapter loading changes.
31
+ - [Release notes 0.7.2](docs/release-notes-0.7.2.md): latest remote Figma image store changes.
32
32
 
33
33
  ## Quick Start
34
34
 
@@ -265,10 +265,12 @@ async function createClientRenderedAddImageInput(input, clientRender, fetchOptio
265
265
  const token = readClientRenderToken(options);
266
266
  if (!token) return input;
267
267
  try {
268
- const asset = await withTimeout(
269
- createClientRenderedFigmaAsset(input.figmaUrl, token, options, fetchOption),
270
- options.timeoutMs ?? 1e4
271
- );
268
+ const asset = await createReviewFigmaClientRenderedAsset({
269
+ ...options,
270
+ fetch: fetchOption,
271
+ figmaUrl: input.figmaUrl,
272
+ token
273
+ });
272
274
  return {
273
275
  ...input,
274
276
  imageFormat: asset.imageFormat,
@@ -340,6 +342,17 @@ async function createClientRenderedFigmaAsset(figmaUrl, token, options, fetchOpt
340
342
  height: dimensions.height
341
343
  };
342
344
  }
345
+ function createReviewFigmaClientRenderedAsset({
346
+ fetch: fetchOption,
347
+ figmaUrl,
348
+ token,
349
+ ...options
350
+ }) {
351
+ return withTimeout(
352
+ createClientRenderedFigmaAsset(figmaUrl, token, options, fetchOption),
353
+ options.timeoutMs ?? 1e4
354
+ );
355
+ }
343
356
  async function readImageBlobDimensions(blob) {
344
357
  const image = await loadImageBlob(blob);
345
358
  return {
@@ -593,10 +606,11 @@ export {
593
606
  createReviewFigmaImageApiUrl,
594
607
  DEFAULT_REVIEW_FIGMA_IMAGE_STORE_ENDPOINT,
595
608
  createReviewFigmaImageStoreClient,
609
+ createReviewFigmaClientRenderedAsset,
596
610
  getReviewFigmaImageTargetKey,
597
611
  getReviewFigmaImageMimeType,
598
612
  createReviewFigmaImagesSnapshot,
599
613
  createReviewFigmaReleaseSnapshot,
600
614
  collectReviewFigmaReleaseSnapshot
601
615
  };
602
- //# sourceMappingURL=chunk-2ZLU5FTD.js.map
616
+ //# sourceMappingURL=chunk-K26WLRRP.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/figma/parse.ts","../src/figma/token.ts","../src/figma/render.ts","../src/vite/figma-asset.ts","../src/figma/image.store.ts","../src/figma/image.snapshot.ts"],"sourcesContent":["export type ReviewFigmaNodeRef = {\n fileKey: string;\n nodeId: string;\n sourceUrl?: string;\n};\n\nexport const FIGMA_NODE_REF_SEPARATOR = '->';\n\nexport function parseReviewFigmaNodeRef(\n value: string | ReviewFigmaNodeRef\n): ReviewFigmaNodeRef | null {\n if (typeof value !== 'string') return normalizeReviewFigmaNodeRef(value);\n\n const input = value.trim();\n if (!input) return null;\n\n return parseReviewFigmaNodeRefValue(input) ?? parseReviewFigmaUrl(input);\n}\n\nexport function requireReviewFigmaNodeRef(\n value: string | ReviewFigmaNodeRef\n): ReviewFigmaNodeRef {\n const ref = parseReviewFigmaNodeRef(value);\n if (!ref) {\n throw new Error('A Figma node link or fileKey->nodeId value is required.');\n }\n return ref;\n}\n\nexport function createReviewFigmaNodeValue(ref: ReviewFigmaNodeRef) {\n return `${ref.fileKey}${FIGMA_NODE_REF_SEPARATOR}${ref.nodeId}`;\n}\n\nexport function createReviewFigmaFrameUrl(\n value: string | ReviewFigmaNodeRef\n) {\n const ref = parseReviewFigmaNodeRef(value);\n if (!ref) return null;\n\n return `https://www.figma.com/design/${encodeURIComponent(\n ref.fileKey\n )}?node-id=${encodeURIComponent(ref.nodeId.replace(/:/g, '-'))}`;\n}\n\nfunction parseReviewFigmaNodeRefValue(value: string) {\n const [fileKey, nodeId, extra] = value\n .split(FIGMA_NODE_REF_SEPARATOR)\n .map((part) => part.trim());\n\n if (!fileKey || !nodeId || extra !== undefined) return null;\n\n return normalizeReviewFigmaNodeRef({ fileKey, nodeId });\n}\n\nfunction parseReviewFigmaUrl(value: string) {\n let url: URL;\n try {\n url = new URL(value);\n } catch {\n return null;\n }\n\n const fileKey = getFigmaFileKey(url);\n const nodeId = normalizeFigmaNodeId(url.searchParams.get('node-id'));\n if (!fileKey || !nodeId) return null;\n\n return { fileKey, nodeId, sourceUrl: value };\n}\n\nfunction getFigmaFileKey(url: URL) {\n if (!/(^|\\.)figma\\.com$/i.test(url.hostname)) return null;\n\n const parts = url.pathname.split('/').filter(Boolean);\n const fileKeyIndex = parts.findIndex((part) =>\n ['design', 'file', 'proto', 'board'].includes(part)\n );\n const fileKey = fileKeyIndex >= 0 ? parts[fileKeyIndex + 1] : null;\n\n return normalizeFigmaFileKey(fileKey);\n}\n\nfunction normalizeReviewFigmaNodeRef(\n value: ReviewFigmaNodeRef | null | undefined\n) {\n const fileKey = normalizeFigmaFileKey(value?.fileKey);\n const nodeId = normalizeFigmaNodeId(value?.nodeId);\n if (!fileKey || !nodeId) return null;\n const sourceUrl = normalizeOptionalString(value?.sourceUrl);\n\n return sourceUrl ? { fileKey, nodeId, sourceUrl } : { fileKey, nodeId };\n}\n\nfunction normalizeFigmaFileKey(value: string | null | undefined) {\n return normalizeOptionalString(value)?.replace(/[?#].*$/, '') ?? null;\n}\n\nfunction normalizeFigmaNodeId(value: string | null | undefined) {\n const nodeId = normalizeOptionalString(value);\n if (!nodeId) return null;\n if (nodeId.includes(':')) return nodeId;\n return nodeId.replace(/-/g, ':');\n}\n\nfunction normalizeOptionalString(value: string | null | undefined) {\n if (typeof value !== 'string') return null;\n return value.trim() || null;\n}\n","export const DEFAULT_REVIEW_FIGMA_TOKEN_ENV_KEY = 'FIGMA_TOKEN';\nexport const REVIEW_FIGMA_TOKEN_MISSING_CODE = 'DFWR_FIGMA_TOKEN_MISSING';\n\nexport type ReviewFigmaTokenEnv = Record<\n string,\n string | null | undefined\n>;\n\nexport type ReviewFigmaTokenOptions = {\n token?: string | null;\n env?: ReviewFigmaTokenEnv;\n envKey?: string;\n enabled?: boolean;\n};\n\nexport class ReviewFigmaTokenError extends Error {\n readonly code = REVIEW_FIGMA_TOKEN_MISSING_CODE;\n readonly envKey: string;\n\n constructor(envKey = DEFAULT_REVIEW_FIGMA_TOKEN_ENV_KEY) {\n super(\n `Figma image rendering requires server env ${envKey}. Set ${envKey} in the dev/server environment; do not expose it as VITE_${envKey}.`\n );\n this.name = 'ReviewFigmaTokenError';\n this.envKey = envKey;\n }\n}\n\nexport function readReviewFigmaToken(\n options: ReviewFigmaTokenOptions = {}\n): string | null {\n if (options.enabled === false) return null;\n\n const envKey = options.envKey ?? DEFAULT_REVIEW_FIGMA_TOKEN_ENV_KEY;\n return normalizeReviewFigmaToken(options.token ?? options.env?.[envKey]);\n}\n\nexport function requireReviewFigmaToken(\n options: ReviewFigmaTokenOptions = {}\n): string {\n const envKey = options.envKey ?? DEFAULT_REVIEW_FIGMA_TOKEN_ENV_KEY;\n const token = readReviewFigmaToken(options);\n\n if (!token) throw new ReviewFigmaTokenError(envKey);\n return token;\n}\n\nexport function isReviewFigmaTokenError(\n error: unknown\n): error is ReviewFigmaTokenError {\n if (!error || typeof error !== 'object') return false;\n\n return (\n error instanceof ReviewFigmaTokenError ||\n ('code' in error && error.code === REVIEW_FIGMA_TOKEN_MISSING_CODE)\n );\n}\n\nfunction normalizeReviewFigmaToken(value: string | null | undefined) {\n if (typeof value !== 'string') return null;\n return value.trim() || null;\n}\n","import {\n requireReviewFigmaNodeRef,\n type ReviewFigmaNodeRef,\n} from './parse';\nimport { requireReviewFigmaToken } from './token';\n\nexport type ReviewFigmaRenderFormat = 'png' | 'jpg' | 'svg' | 'pdf';\n\nexport type ReviewFigmaImageRenderOptions = {\n figmaUrl: string | ReviewFigmaNodeRef;\n token?: string | null;\n format?: ReviewFigmaRenderFormat;\n scale?: number;\n useAbsoluteBounds?: boolean;\n apiBaseUrl?: string;\n fetch?: typeof fetch;\n signal?: AbortSignal;\n};\n\nexport type ReviewFigmaRenderedImage = {\n fileKey: string;\n nodeId: string;\n figmaUrl?: string;\n imageUrl: string;\n renderFormat: ReviewFigmaRenderFormat;\n};\n\ntype FigmaImageResponse = {\n err?: string | null;\n images?: Record<string, string | null | undefined>;\n};\n\nconst DEFAULT_FIGMA_API_BASE_URL = 'https://api.figma.com';\n\nexport async function renderReviewFigmaImage(\n options: ReviewFigmaImageRenderOptions\n): Promise<ReviewFigmaRenderedImage> {\n const token = requireReviewFigmaToken({ token: options.token });\n const ref = requireReviewFigmaNodeRef(options.figmaUrl);\n const renderFormat = options.format ?? 'png';\n const requestUrl = createReviewFigmaImageApiUrl({\n apiBaseUrl: options.apiBaseUrl,\n fileKey: ref.fileKey,\n nodeId: ref.nodeId,\n format: renderFormat,\n scale: options.scale,\n useAbsoluteBounds: options.useAbsoluteBounds,\n });\n const fetchImage = options.fetch ?? globalThis.fetch;\n if (!fetchImage) throw new Error('Figma image rendering requires fetch.');\n\n const response = await fetchImage(requestUrl, {\n headers: {\n 'X-Figma-Token': token,\n },\n signal: options.signal,\n });\n const body = (await response.json().catch(() => null)) as\n | FigmaImageResponse\n | null;\n\n if (!response.ok) {\n throw new Error(\n body?.err || `Figma image render failed with ${response.status}`\n );\n }\n\n const imageUrl = body?.images?.[ref.nodeId];\n if (!imageUrl) {\n throw new Error(`Figma image render returned no URL for ${ref.nodeId}.`);\n }\n\n return {\n fileKey: ref.fileKey,\n nodeId: ref.nodeId,\n figmaUrl: typeof options.figmaUrl === 'string'\n ? options.figmaUrl\n : options.figmaUrl.sourceUrl,\n imageUrl,\n renderFormat,\n };\n}\n\nexport function createReviewFigmaImageApiUrl({\n apiBaseUrl = DEFAULT_FIGMA_API_BASE_URL,\n fileKey,\n nodeId,\n format = 'png',\n scale,\n useAbsoluteBounds,\n}: {\n apiBaseUrl?: string;\n fileKey: string;\n nodeId: string;\n format?: ReviewFigmaRenderFormat;\n scale?: number;\n useAbsoluteBounds?: boolean;\n}) {\n const url = new URL(`/v1/images/${encodeURIComponent(fileKey)}`, apiBaseUrl);\n url.searchParams.set('ids', nodeId);\n url.searchParams.set('format', format);\n\n if (typeof scale === 'number' && Number.isFinite(scale) && scale > 0) {\n url.searchParams.set('scale', String(scale));\n }\n if (useAbsoluteBounds !== undefined) {\n url.searchParams.set('use_absolute_bounds', String(useAbsoluteBounds));\n }\n\n return url.toString();\n}\n","// Pure helpers for Figma image asset format conversion and storage-key handling.\n// Extracted from src/vite.ts to keep the plugin entry focused on request flow.\nimport type { ReviewFigmaImageFormat } from '../figma/image.types';\nimport type { ReviewFigmaRenderFormat } from '../figma/render';\n\nexport function parseReviewFigmaImageFormat(value: unknown) {\n return value === 'webp' || value === 'png' || value === 'jpg'\n ? value\n : undefined;\n}\n\nexport function getStoreRenderFormat(\n renderFormat: ReviewFigmaRenderFormat | undefined,\n imageFormat: ReviewFigmaImageFormat | undefined\n): Extract<ReviewFigmaRenderFormat, 'png' | 'jpg'> {\n if (renderFormat === 'jpg' || renderFormat === 'png') return renderFormat;\n if (imageFormat === 'jpg') return 'jpg';\n return 'png';\n}\n\nexport function getReviewFigmaImageFormatFromMimeType(\n mimeType: string\n): ReviewFigmaImageFormat | null {\n if (mimeType === 'image/webp') return 'webp';\n if (mimeType === 'image/png') return 'png';\n if (mimeType === 'image/jpeg') return 'jpg';\n return null;\n}\n\nexport function normalizeImageMimeType(value: string | null | undefined) {\n const mimeType = value?.split(';')[0]?.trim().toLowerCase();\n if (mimeType === 'image/jpg') return 'image/jpeg';\n if (\n mimeType === 'image/jpeg' ||\n mimeType === 'image/png' ||\n mimeType === 'image/webp'\n ) {\n return mimeType;\n }\n return null;\n}\n\nexport function createReviewFigmaAssetStorageKey(\n id: string,\n imageFormat: ReviewFigmaImageFormat\n) {\n return `${id}.${getReviewFigmaAssetExtension(imageFormat)}`;\n}\n\nexport function createReviewFigmaAssetUrl(\n assetEndpoint: string,\n storageKey: string\n) {\n return `${assetEndpoint}/${encodeURIComponent(storageKey)}`;\n}\n\nexport function getReviewFigmaAssetStorageKeyFromPathname(\n pathname: string,\n assetEndpoint: string\n) {\n try {\n const storageKey = decodeURIComponent(\n pathname.slice(assetEndpoint.length + 1)\n );\n return isSafeReviewFigmaAssetStorageKey(storageKey) ? storageKey : null;\n } catch {\n return null;\n }\n}\n\nexport function isSafeReviewFigmaAssetStorageKey(value: string) {\n return /^figma_[a-z0-9_]+\\.(webp|png|jpg)$/.test(value);\n}\n\nfunction getReviewFigmaAssetExtension(format: ReviewFigmaImageFormat) {\n return format === 'jpg' ? 'jpg' : format;\n}\n\nexport function getReviewFigmaAssetMimeType(storageKey: string) {\n if (storageKey.endsWith('.jpg')) return 'image/jpeg';\n if (storageKey.endsWith('.webp')) return 'image/webp';\n return 'image/png';\n}\n","import type {\n AddReviewFigmaImageInput,\n ReorderReviewFigmaImagesInput,\n ReviewFigmaImage,\n ReviewFigmaImageAssetInput,\n ReviewFigmaImageFormat,\n ReviewFigmaImageStore,\n ReviewFigmaImageTarget,\n} from './image.types';\nimport { getReviewFigmaImageFormatFromMimeType } from '../vite/figma-asset';\nimport { parseReviewFigmaNodeRef } from './parse';\nimport {\n createReviewFigmaImageApiUrl,\n type ReviewFigmaRenderFormat,\n} from './render';\n\nexport const DEFAULT_REVIEW_FIGMA_IMAGE_STORE_ENDPOINT =\n '/__dfwr/figma-images';\n\ntype ReviewFigmaImageTokenProvider =\n | string\n | null\n | undefined\n | (() => string | null | undefined);\n\nexport type ReviewFigmaImageStoreClientOptions = {\n endpoint?: string;\n fetch?: typeof fetch;\n token?: ReviewFigmaImageTokenProvider;\n clientRender?: boolean | ReviewFigmaImageClientRenderOptions;\n};\n\nexport type ReviewFigmaImageClientRenderOptions = {\n token?: string | null | (() => string | null | undefined);\n apiBaseUrl?: string;\n renderFormat?: Extract<ReviewFigmaRenderFormat, 'png' | 'jpg'>;\n renderScale?: number;\n useAbsoluteBounds?: boolean;\n convertToWebp?: boolean;\n webpQuality?: number;\n timeoutMs?: number;\n};\n\nexport type CreateReviewFigmaClientRenderedAssetOptions =\n ReviewFigmaImageClientRenderOptions & {\n figmaUrl: string;\n token: string;\n fetch?: typeof fetch;\n };\n\nexport function createReviewFigmaImageStoreClient(\n options: ReviewFigmaImageStoreClientOptions = {}\n): ReviewFigmaImageStore {\n const endpoint =\n options.endpoint ?? DEFAULT_REVIEW_FIGMA_IMAGE_STORE_ENDPOINT;\n const request = createReviewFigmaImageStoreRequest(endpoint, options.fetch);\n\n return {\n listImages(target) {\n const url = `${endpoint}?target=${encodeURIComponent(\n JSON.stringify(target)\n )}`;\n return request<ReviewFigmaImage[]>(url);\n },\n async addImage(input) {\n const nextInput = await createClientRenderedAddImageInput(\n input,\n options.clientRender,\n options.fetch\n );\n return request<ReviewFigmaImage>(endpoint, {\n method: 'POST',\n body: JSON.stringify(nextInput),\n figmaToken: readReviewFigmaImageToken(\n options.token ?? getStoredReviewFigmaImageToken\n ),\n });\n },\n updateImage(id, patch) {\n return request<ReviewFigmaImage>(`${endpoint}/${encodeURIComponent(id)}`, {\n method: 'PATCH',\n body: JSON.stringify(patch),\n });\n },\n reorderImages(input) {\n return request<ReviewFigmaImage[]>(`${endpoint}/reorder`, {\n method: 'PATCH',\n body: JSON.stringify(input),\n });\n },\n deleteImage(id) {\n return request<void>(`${endpoint}/${encodeURIComponent(id)}`, {\n method: 'DELETE',\n });\n },\n };\n}\n\nasync function createClientRenderedAddImageInput(\n input: AddReviewFigmaImageInput,\n clientRender: ReviewFigmaImageStoreClientOptions['clientRender'],\n fetchOption: typeof fetch | undefined\n): Promise<AddReviewFigmaImageInput> {\n const options = normalizeClientRenderOptions(clientRender);\n if (!options) return input;\n\n const token = readClientRenderToken(options);\n if (!token) return input;\n\n try {\n const asset = await createReviewFigmaClientRenderedAsset({\n ...options,\n fetch: fetchOption,\n figmaUrl: input.figmaUrl,\n token,\n });\n return {\n ...input,\n imageFormat: asset.imageFormat,\n asset,\n };\n } catch {\n return input;\n }\n}\n\nfunction normalizeClientRenderOptions(\n clientRender: ReviewFigmaImageStoreClientOptions['clientRender']\n): ReviewFigmaImageClientRenderOptions | null {\n if (!clientRender) return null;\n if (clientRender === true) return {};\n return clientRender;\n}\n\nfunction readClientRenderToken(options: ReviewFigmaImageClientRenderOptions) {\n const token =\n typeof options.token === 'function' ? options.token() : options.token;\n return typeof token === 'string' ? token.trim() : '';\n}\n\nasync function createClientRenderedFigmaAsset(\n figmaUrl: string,\n token: string,\n options: ReviewFigmaImageClientRenderOptions,\n fetchOption: typeof fetch | undefined\n): Promise<ReviewFigmaImageAssetInput> {\n const ref = parseReviewFigmaNodeRef(figmaUrl);\n if (!ref) throw new Error('A Figma node link is required.');\n\n const requestFetch = fetchOption ?? globalThis.fetch;\n if (!requestFetch) throw new Error('Figma client rendering requires fetch.');\n\n const renderFormat = options.renderFormat ?? 'png';\n const response = await requestFetch(\n createReviewFigmaImageApiUrl({\n apiBaseUrl: options.apiBaseUrl,\n fileKey: ref.fileKey,\n nodeId: ref.nodeId,\n format: renderFormat,\n scale: options.renderScale,\n useAbsoluteBounds: options.useAbsoluteBounds,\n }),\n {\n headers: {\n 'X-Figma-Token': token,\n },\n }\n );\n const body = (await response.json().catch(() => null)) as\n | { err?: string | null; images?: Record<string, string | null | undefined> }\n | null;\n if (!response.ok) {\n throw new Error(body?.err || `Figma image render failed with ${response.status}`);\n }\n\n const imageUrl = body?.images?.[ref.nodeId];\n if (!imageUrl) throw new Error(`Figma image render returned no URL for ${ref.nodeId}.`);\n\n const imageResponse = await requestFetch(imageUrl);\n if (!imageResponse.ok) {\n throw new Error(`Figma image download failed with ${imageResponse.status}`);\n }\n\n const originalBlob = await imageResponse.blob();\n const originalMimeType =\n normalizeClientImageMimeType(originalBlob.type) ??\n getReviewFigmaImageMimeType(renderFormat === 'jpg' ? 'jpg' : 'png');\n const originalFormat =\n getReviewFigmaImageFormatFromMimeType(originalMimeType) ??\n (renderFormat === 'jpg' ? 'jpg' : 'png');\n const dimensions = await readImageBlobDimensions(originalBlob);\n const shouldConvertToWebp = options.convertToWebp ?? true;\n const convertedBlob = shouldConvertToWebp\n ? await convertImageBlobToWebp(\n originalBlob,\n options.webpQuality ?? 0.9,\n dimensions\n ).catch(() => null)\n : null;\n const finalBlob =\n convertedBlob?.type === 'image/webp' ? convertedBlob : originalBlob;\n const finalMimeType =\n normalizeClientImageMimeType(finalBlob.type) ?? originalMimeType;\n const finalFormat =\n getReviewFigmaImageFormatFromMimeType(finalMimeType) ?? originalFormat;\n\n return {\n dataUrl: await blobToDataUrl(finalBlob),\n imageFormat: finalFormat,\n mimeType: finalMimeType,\n byteSize: finalBlob.size,\n width: dimensions.width,\n height: dimensions.height,\n };\n}\n\nexport function createReviewFigmaClientRenderedAsset({\n fetch: fetchOption,\n figmaUrl,\n token,\n ...options\n}: CreateReviewFigmaClientRenderedAssetOptions): Promise<ReviewFigmaImageAssetInput> {\n return withTimeout(\n createClientRenderedFigmaAsset(figmaUrl, token, options, fetchOption),\n options.timeoutMs ?? 10000\n );\n}\n\nasync function readImageBlobDimensions(blob: Blob) {\n const image = await loadImageBlob(blob);\n return {\n width: image.naturalWidth || image.width,\n height: image.naturalHeight || image.height,\n };\n}\n\nasync function convertImageBlobToWebp(\n blob: Blob,\n quality: number,\n dimensions: { width: number; height: number }\n) {\n if (typeof document === 'undefined') return null;\n if (!dimensions.width || !dimensions.height) return null;\n\n const image = await loadImageBlob(blob);\n const canvas = document.createElement('canvas');\n canvas.width = dimensions.width;\n canvas.height = dimensions.height;\n const context = canvas.getContext('2d');\n if (!context) return null;\n\n context.drawImage(image, 0, 0);\n\n return new Promise<Blob | null>((resolve) => {\n canvas.toBlob(resolve, 'image/webp', quality);\n });\n}\n\nfunction loadImageBlob(blob: Blob) {\n return new Promise<HTMLImageElement>((resolve, reject) => {\n if (typeof Image === 'undefined' || typeof URL === 'undefined') {\n reject(new Error('Image decoding is unavailable.'));\n return;\n }\n\n const image = new Image();\n const objectUrl = URL.createObjectURL(blob);\n image.onload = () => {\n URL.revokeObjectURL(objectUrl);\n resolve(image);\n };\n image.onerror = () => {\n URL.revokeObjectURL(objectUrl);\n reject(new Error('Image decoding failed.'));\n };\n image.src = objectUrl;\n });\n}\n\nfunction blobToDataUrl(blob: Blob) {\n return new Promise<string>((resolve, reject) => {\n const reader = new FileReader();\n reader.onload = () => {\n if (typeof reader.result === 'string') {\n resolve(reader.result);\n return;\n }\n reject(new Error('Blob encoding failed.'));\n };\n reader.onerror = () => reject(reader.error ?? new Error('Blob encoding failed.'));\n reader.readAsDataURL(blob);\n });\n}\n\nfunction normalizeClientImageMimeType(value: string | null | undefined) {\n const mimeType = value?.split(';')[0]?.trim().toLowerCase();\n if (mimeType === 'image/jpg') return 'image/jpeg';\n if (\n mimeType === 'image/jpeg' ||\n mimeType === 'image/png' ||\n mimeType === 'image/webp'\n ) {\n return mimeType;\n }\n return null;\n}\n\nasync function withTimeout<T>(promise: Promise<T>, timeoutMs: number) {\n let timeoutId: ReturnType<typeof setTimeout> | undefined;\n try {\n return await Promise.race([\n promise,\n new Promise<T>((_, reject) => {\n timeoutId = setTimeout(\n () => reject(new Error('Figma client rendering timed out.')),\n timeoutMs\n );\n }),\n ]);\n } finally {\n if (timeoutId) clearTimeout(timeoutId);\n }\n}\n\nexport function getReviewFigmaImageTargetKey(target: ReviewFigmaImageTarget) {\n return JSON.stringify(normalizeReviewFigmaImageTarget(target));\n}\n\nexport function getReviewFigmaImageMimeType(\n format: ReviewFigmaImageFormat\n) {\n if (format === 'jpg') return 'image/jpeg';\n if (format === 'png') return 'image/png';\n return 'image/webp';\n}\n\ntype ReviewFigmaImageStoreRequestInit = RequestInit & {\n figmaToken?: string;\n};\n\nfunction createReviewFigmaImageStoreRequest(\n endpoint: string,\n fetchOption: typeof fetch | undefined\n) {\n return async <T>(\n input: string,\n init: ReviewFigmaImageStoreRequestInit = {}\n ) => {\n const requestFetch = fetchOption ?? globalThis.fetch;\n if (!requestFetch) throw new Error('Figma image store requires fetch.');\n const figmaToken = init.figmaToken ?? '';\n const { figmaToken: _figmaToken, ...requestInit } = init;\n\n const response = await requestFetch(input, {\n ...requestInit,\n headers: {\n 'Content-Type': 'application/json',\n ...(figmaToken ? { 'X-Figma-Token': figmaToken } : {}),\n ...(requestInit.headers ?? {}),\n },\n });\n const text = await response.text();\n const body = text ? JSON.parse(text) : null;\n\n if (!response.ok) {\n const message =\n typeof body?.error === 'string'\n ? body.error\n : `Figma image store request failed: ${response.status}`;\n throw new Error(message);\n }\n\n return body as T;\n };\n}\n\nfunction readReviewFigmaImageToken(provider: ReviewFigmaImageTokenProvider) {\n const token = typeof provider === 'function' ? provider() : provider;\n return typeof token === 'string' ? token.trim() : '';\n}\n\nfunction getStoredReviewFigmaImageToken() {\n if (typeof window === 'undefined') return '';\n\n try {\n return window.localStorage.getItem('figma-token') ?? '';\n } catch {\n return '';\n }\n}\n\nfunction normalizeReviewFigmaImageTarget(target: ReviewFigmaImageTarget) {\n if (target.type === 'figma-node') {\n return {\n type: target.type,\n projectId: target.projectId,\n fileKey: target.fileKey,\n nodeId: target.nodeId,\n };\n }\n\n return {\n type: target.type,\n projectId: target.projectId,\n pageUrl: target.pageUrl,\n slot: target.slot ?? '',\n viewport: target.viewport\n ? {\n label: target.viewport.label ?? '',\n width: target.viewport.width ?? null,\n height: target.viewport.height ?? null,\n scope: target.viewport.scope ?? '',\n }\n : null,\n };\n}\n\nexport type { ReorderReviewFigmaImagesInput };\n","import type {\n ReviewFigmaImage,\n ReviewFigmaImageStore,\n ReviewFigmaImageTarget,\n} from './image.types';\nimport { getReviewFigmaImageTargetKey } from './image.store';\n\nexport type ReviewFigmaImagesSnapshot = ReviewFigmaImage[];\n\nexport type ReviewFigmaReleaseSnapshot = {\n version: 1;\n projectId: string;\n releaseId?: string;\n label?: string;\n createdAt: string;\n figmaImagesSnapshot: ReviewFigmaImagesSnapshot;\n};\n\nexport type CreateReviewFigmaImagesSnapshotOptions = {\n projectId?: string;\n targets?: readonly ReviewFigmaImageTarget[];\n};\n\nexport type CreateReviewFigmaReleaseSnapshotOptions =\n CreateReviewFigmaImagesSnapshotOptions & {\n images: readonly ReviewFigmaImage[];\n projectId: string;\n releaseId?: string;\n label?: string;\n createdAt?: string;\n };\n\nexport type CollectReviewFigmaReleaseSnapshotOptions = Omit<\n CreateReviewFigmaReleaseSnapshotOptions,\n 'images'\n> & {\n store: ReviewFigmaImageStore;\n targets: readonly ReviewFigmaImageTarget[];\n};\n\nexport function createReviewFigmaImagesSnapshot(\n images: readonly ReviewFigmaImage[],\n options: CreateReviewFigmaImagesSnapshotOptions = {}\n): ReviewFigmaImagesSnapshot {\n const targetKeys = options.targets?.length\n ? new Set(options.targets.map(getReviewFigmaImageTargetKey))\n : null;\n\n return images\n .filter((image) => {\n if (options.projectId && image.projectId !== options.projectId) {\n return false;\n }\n if (\n targetKeys &&\n !targetKeys.has(getReviewFigmaImageTargetKey(image.target))\n ) {\n return false;\n }\n return true;\n })\n .map(cloneReviewFigmaImage)\n .sort(compareReviewFigmaSnapshotImages);\n}\n\nexport function createReviewFigmaReleaseSnapshot({\n images,\n projectId,\n releaseId,\n label,\n createdAt,\n targets,\n}: CreateReviewFigmaReleaseSnapshotOptions): ReviewFigmaReleaseSnapshot {\n return {\n version: 1,\n projectId,\n ...(releaseId ? { releaseId } : null),\n ...(label ? { label } : null),\n createdAt: createdAt ?? new Date().toISOString(),\n figmaImagesSnapshot: createReviewFigmaImagesSnapshot(images, {\n projectId,\n targets,\n }),\n };\n}\n\nexport async function collectReviewFigmaReleaseSnapshot({\n store,\n targets,\n ...snapshotOptions\n}: CollectReviewFigmaReleaseSnapshotOptions): Promise<ReviewFigmaReleaseSnapshot> {\n const imagesByTarget = await Promise.all(\n targets.map((target) => store.listImages(target))\n );\n\n return createReviewFigmaReleaseSnapshot({\n ...snapshotOptions,\n targets,\n images: dedupeReviewFigmaImages(imagesByTarget.flat()),\n });\n}\n\nfunction dedupeReviewFigmaImages(images: readonly ReviewFigmaImage[]) {\n return Array.from(new Map(images.map((image) => [image.id, image])).values());\n}\n\nfunction cloneReviewFigmaImage(image: ReviewFigmaImage): ReviewFigmaImage {\n return {\n ...image,\n target: cloneReviewFigmaImageTarget(image.target),\n };\n}\n\nfunction cloneReviewFigmaImageTarget(\n target: ReviewFigmaImageTarget\n): ReviewFigmaImageTarget {\n if (target.type === 'figma-node') {\n return {\n type: target.type,\n projectId: target.projectId,\n fileKey: target.fileKey,\n nodeId: target.nodeId,\n };\n }\n\n return {\n type: target.type,\n projectId: target.projectId,\n pageUrl: target.pageUrl,\n slot: target.slot,\n viewport: target.viewport\n ? {\n label: target.viewport.label,\n width: target.viewport.width,\n height: target.viewport.height,\n scope: target.viewport.scope,\n }\n : undefined,\n };\n}\n\nfunction compareReviewFigmaSnapshotImages(\n a: ReviewFigmaImage,\n b: ReviewFigmaImage\n) {\n return (\n a.projectId.localeCompare(b.projectId) ||\n getReviewFigmaImageTargetKey(a.target).localeCompare(\n getReviewFigmaImageTargetKey(b.target)\n ) ||\n a.order - b.order ||\n a.createdAt.localeCompare(b.createdAt) ||\n a.id.localeCompare(b.id)\n );\n}\n"],"mappings":"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","names":[]}
package/dist/index.cjs CHANGED
@@ -22,6 +22,7 @@ var index_exports = {};
22
22
  __export(index_exports, {
23
23
  DEFAULT_REVIEW_FIGMA_IMAGE_FORMAT: () => DEFAULT_REVIEW_FIGMA_IMAGE_FORMAT,
24
24
  DEFAULT_REVIEW_FIGMA_IMAGE_STORE_ENDPOINT: () => DEFAULT_REVIEW_FIGMA_IMAGE_STORE_ENDPOINT,
25
+ DEFAULT_REVIEW_FIGMA_REMOTE_IMAGES_TABLE: () => DEFAULT_REVIEW_FIGMA_REMOTE_IMAGES_TABLE,
25
26
  DEFAULT_REVIEW_FIGMA_TOKEN_ENV_KEY: () => DEFAULT_REVIEW_FIGMA_TOKEN_ENV_KEY,
26
27
  DEFAULT_REVIEW_VIEWPORTS: () => DEFAULT_REVIEW_VIEWPORTS,
27
28
  FIGMA_NODE_REF_SEPARATOR: () => FIGMA_NODE_REF_SEPARATOR,
@@ -29,6 +30,7 @@ __export(index_exports, {
29
30
  REVIEW_WORKFLOW_STATUS_OPTIONS: () => REVIEW_WORKFLOW_STATUS_OPTIONS,
30
31
  ReviewFigmaTokenError: () => ReviewFigmaTokenError,
31
32
  collectReviewFigmaReleaseSnapshot: () => collectReviewFigmaReleaseSnapshot,
33
+ createRemoteReviewFigmaImageStore: () => createRemoteReviewFigmaImageStore,
32
34
  createReviewFigmaFrameUrl: () => createReviewFigmaFrameUrl,
33
35
  createReviewFigmaImageStoreClient: () => createReviewFigmaImageStoreClient,
34
36
  createReviewFigmaImagesSnapshot: () => createReviewFigmaImagesSnapshot,
@@ -580,10 +582,12 @@ async function createClientRenderedAddImageInput(input, clientRender, fetchOptio
580
582
  const token = readClientRenderToken(options);
581
583
  if (!token) return input;
582
584
  try {
583
- const asset = await withTimeout(
584
- createClientRenderedFigmaAsset(input.figmaUrl, token, options, fetchOption),
585
- options.timeoutMs ?? 1e4
586
- );
585
+ const asset = await createReviewFigmaClientRenderedAsset({
586
+ ...options,
587
+ fetch: fetchOption,
588
+ figmaUrl: input.figmaUrl,
589
+ token
590
+ });
587
591
  return {
588
592
  ...input,
589
593
  imageFormat: asset.imageFormat,
@@ -655,6 +659,17 @@ async function createClientRenderedFigmaAsset(figmaUrl, token, options, fetchOpt
655
659
  height: dimensions.height
656
660
  };
657
661
  }
662
+ function createReviewFigmaClientRenderedAsset({
663
+ fetch: fetchOption,
664
+ figmaUrl,
665
+ token,
666
+ ...options
667
+ }) {
668
+ return withTimeout(
669
+ createClientRenderedFigmaAsset(figmaUrl, token, options, fetchOption),
670
+ options.timeoutMs ?? 1e4
671
+ );
672
+ }
658
673
  async function readImageBlobDimensions(blob) {
659
674
  const image = await loadImageBlob(blob);
660
675
  return {
@@ -799,6 +814,300 @@ function normalizeReviewFigmaImageTarget(target) {
799
814
  };
800
815
  }
801
816
 
817
+ // src/figma/remote.image.store.ts
818
+ var DEFAULT_REVIEW_FIGMA_REMOTE_IMAGES_TABLE = "review_figma_images";
819
+ var DEFAULT_REVIEW_FIGMA_REMOTE_UPLOAD_TIMEOUT_MS = 3e4;
820
+ var FIGMA_TOKEN_STORAGE_KEY = "figma-token";
821
+ function createRemoteReviewFigmaImageStore({
822
+ client,
823
+ uploadEndpoint,
824
+ table = DEFAULT_REVIEW_FIGMA_REMOTE_IMAGES_TABLE,
825
+ fetch: fetchOption,
826
+ token,
827
+ clientRender = true,
828
+ uploadTimeoutMs = DEFAULT_REVIEW_FIGMA_REMOTE_UPLOAD_TIMEOUT_MS
829
+ }) {
830
+ const listImages = async (target) => {
831
+ const targetKey = getReviewFigmaImageTargetKey(target);
832
+ const { data, error } = await client.from(table).select("*").eq("project_id", target.projectId).eq("target_key", targetKey).order("sort_order", { ascending: true }).order("created_at", { ascending: true });
833
+ if (error) throwRemoteStoreError(error, "list Figma images");
834
+ return (data ?? []).map(rowToFigmaImage);
835
+ };
836
+ return {
837
+ listImages,
838
+ async addImage(input) {
839
+ const ref = parseReviewFigmaNodeRef(input.figmaUrl);
840
+ if (!ref) {
841
+ throw new Error("A Figma node link or fileKey->nodeId value is required.");
842
+ }
843
+ const id = createFigmaImageId();
844
+ const asset = await resolveFigmaImageAsset({
845
+ input,
846
+ token,
847
+ clientRender,
848
+ fetch: fetchOption
849
+ });
850
+ const uploaded = await uploadFigmaAsset({
851
+ asset,
852
+ endpoint: uploadEndpoint,
853
+ fetch: fetchOption,
854
+ imageId: id,
855
+ projectId: input.target.projectId,
856
+ timeoutMs: uploadTimeoutMs
857
+ });
858
+ const order = typeof input.order === "number" && Number.isFinite(input.order) ? input.order : await getNextImageOrder(listImages, input.target);
859
+ const now = (/* @__PURE__ */ new Date()).toISOString();
860
+ const row = createImageRow({
861
+ asset,
862
+ fileKey: ref.fileKey,
863
+ id,
864
+ imageUrl: uploaded.imageUrl,
865
+ input,
866
+ nodeId: ref.nodeId,
867
+ now,
868
+ order,
869
+ storageKey: uploaded.storageKey
870
+ });
871
+ const { data, error } = await client.from(table).insert(row).select("*").single();
872
+ if (error) throwRemoteStoreError(error, "insert Figma image");
873
+ return rowToFigmaImage(data);
874
+ },
875
+ async updateImage(id, patch) {
876
+ const { data, error } = await client.from(table).update(createUpdatePatch(patch)).eq("id", id).select("*").single();
877
+ if (error) throwRemoteStoreError(error, "update Figma image");
878
+ return rowToFigmaImage(data);
879
+ },
880
+ async reorderImages(input) {
881
+ const current = await listImages(input.target);
882
+ const currentIds = new Set(current.map((image) => image.id));
883
+ const nextIds = [
884
+ ...input.imageIds.filter((id) => currentIds.has(id)),
885
+ ...current.map((image) => image.id).filter((id) => !input.imageIds.includes(id))
886
+ ];
887
+ const targetKey = getReviewFigmaImageTargetKey(input.target);
888
+ const updatedAt = (/* @__PURE__ */ new Date()).toISOString();
889
+ const results = await Promise.all(
890
+ nextIds.map(
891
+ (id, index) => client.from(table).update({ sort_order: index, updated_at: updatedAt }).eq("id", id).eq("project_id", input.target.projectId).eq("target_key", targetKey)
892
+ )
893
+ );
894
+ const failed = results.find((result) => result.error);
895
+ if (failed?.error) throwRemoteStoreError(failed.error, "reorder Figma images");
896
+ return listImages(input.target);
897
+ },
898
+ async deleteImage(id) {
899
+ const { error } = await client.from(table).delete().eq("id", id);
900
+ if (error) throwRemoteStoreError(error, "delete Figma image");
901
+ }
902
+ };
903
+ }
904
+ async function resolveFigmaImageAsset({
905
+ input,
906
+ token,
907
+ clientRender,
908
+ fetch
909
+ }) {
910
+ if (input.asset) return input.asset;
911
+ if (!clientRender) {
912
+ throw new Error("Figma image asset is required when clientRender is disabled.");
913
+ }
914
+ const renderOptions = clientRender === true ? {} : clientRender;
915
+ const figmaToken = readRemoteFigmaToken(renderOptions.token ?? token);
916
+ if (!figmaToken) throw new Error("Figma token is required.");
917
+ return createReviewFigmaClientRenderedAsset({
918
+ ...renderOptions,
919
+ fetch,
920
+ figmaUrl: input.figmaUrl,
921
+ token: figmaToken
922
+ });
923
+ }
924
+ async function uploadFigmaAsset({
925
+ asset,
926
+ endpoint,
927
+ fetch: fetchOption,
928
+ imageId,
929
+ projectId,
930
+ timeoutMs
931
+ }) {
932
+ const requestFetch = fetchOption ?? globalThis.fetch;
933
+ if (!requestFetch) throw new Error("Figma asset upload requires fetch.");
934
+ const { blob, mimeType } = decodeAssetDataUrl(asset);
935
+ const response = await withTimeout2(
936
+ requestFetch(
937
+ createAssetUploadUrl(endpoint, {
938
+ imageFormat: asset.imageFormat,
939
+ imageId,
940
+ projectId
941
+ }),
942
+ {
943
+ method: "POST",
944
+ headers: {
945
+ "Content-Type": mimeType
946
+ },
947
+ body: blob
948
+ }
949
+ ),
950
+ timeoutMs,
951
+ "Figma asset upload timed out."
952
+ );
953
+ const data = await response.json().catch(() => null);
954
+ if (!response.ok) {
955
+ throw new Error(
956
+ readString(data?.error) || `Figma asset upload failed with ${response.status}`
957
+ );
958
+ }
959
+ const storageKey = readString(data?.storageKey) ?? readString(data?.r2Key);
960
+ const imageUrl = readString(data?.imageUrl) ?? readString(data?.publicUrl);
961
+ if (!storageKey || !imageUrl) {
962
+ throw new Error("Figma asset upload response is missing image URL.");
963
+ }
964
+ return { storageKey, imageUrl };
965
+ }
966
+ function createAssetUploadUrl(endpoint, params) {
967
+ const base = typeof window !== "undefined" ? window.location.origin : "http://localhost";
968
+ const url = new URL(endpoint, base);
969
+ url.searchParams.set("projectId", params.projectId);
970
+ url.searchParams.set("imageId", params.imageId);
971
+ url.searchParams.set("imageFormat", params.imageFormat);
972
+ return url.toString();
973
+ }
974
+ function createImageRow({
975
+ asset,
976
+ fileKey,
977
+ id,
978
+ imageUrl,
979
+ input,
980
+ nodeId,
981
+ now,
982
+ order,
983
+ storageKey
984
+ }) {
985
+ const target = input.target;
986
+ const targetKey = getReviewFigmaImageTargetKey(target);
987
+ return {
988
+ id,
989
+ project_id: target.projectId,
990
+ target_key: targetKey,
991
+ target,
992
+ target_type: target.type,
993
+ page_url: target.type === "route" ? target.pageUrl : null,
994
+ viewport_label: target.type === "route" ? target.viewport?.label ?? null : null,
995
+ viewport_width: target.type === "route" ? target.viewport?.width ?? null : null,
996
+ viewport_height: target.type === "route" ? target.viewport?.height ?? null : null,
997
+ viewport_scope: target.type === "route" ? target.viewport?.scope ?? null : null,
998
+ slot: target.type === "route" ? target.slot ?? null : null,
999
+ figma_url: input.figmaUrl,
1000
+ file_key: fileKey,
1001
+ node_id: nodeId,
1002
+ image_url: imageUrl,
1003
+ image_format: asset.imageFormat,
1004
+ mime_type: asset.mimeType,
1005
+ storage_key: storageKey,
1006
+ label: normalizeOptionalText(input.label) ?? null,
1007
+ sort_order: order,
1008
+ width: asset.width ?? null,
1009
+ height: asset.height ?? null,
1010
+ byte_size: asset.byteSize ?? null,
1011
+ created_at: now,
1012
+ updated_at: now
1013
+ };
1014
+ }
1015
+ function createUpdatePatch(patch) {
1016
+ return {
1017
+ ...patch.label !== void 0 ? { label: normalizeOptionalText(patch.label) ?? null } : {},
1018
+ ...typeof patch.order === "number" && Number.isFinite(patch.order) ? { sort_order: patch.order } : {},
1019
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
1020
+ };
1021
+ }
1022
+ async function getNextImageOrder(listImages, target) {
1023
+ const images = await listImages(target);
1024
+ return images.length ? Math.max(...images.map((image) => image.order)) + 1 : 0;
1025
+ }
1026
+ function rowToFigmaImage(row) {
1027
+ return {
1028
+ id: row.id,
1029
+ projectId: row.project_id,
1030
+ target: row.target,
1031
+ figmaUrl: row.figma_url,
1032
+ fileKey: row.file_key,
1033
+ nodeId: row.node_id,
1034
+ imageUrl: row.image_url,
1035
+ imageFormat: row.image_format,
1036
+ mimeType: row.mime_type,
1037
+ label: row.label ?? void 0,
1038
+ order: row.sort_order,
1039
+ storageKey: row.storage_key,
1040
+ width: row.width ?? void 0,
1041
+ height: row.height ?? void 0,
1042
+ byteSize: row.byte_size ?? void 0,
1043
+ createdAt: row.created_at,
1044
+ updatedAt: row.updated_at
1045
+ };
1046
+ }
1047
+ function decodeAssetDataUrl(asset) {
1048
+ const match = asset.dataUrl.match(/^data:([^;,]+);base64,(.*)$/);
1049
+ if (!match) throw new Error("Valid Figma image asset data URL is required.");
1050
+ const mimeType = match[1]?.trim();
1051
+ if (!mimeType || mimeType !== asset.mimeType) {
1052
+ throw new Error("Figma image asset MIME type mismatch.");
1053
+ }
1054
+ const binary = globalThis.atob(match[2] ?? "");
1055
+ const bytes = new Uint8Array(binary.length);
1056
+ for (let index = 0; index < binary.length; index += 1) {
1057
+ bytes[index] = binary.charCodeAt(index);
1058
+ }
1059
+ return {
1060
+ blob: new Blob([bytes], { type: mimeType }),
1061
+ mimeType
1062
+ };
1063
+ }
1064
+ async function withTimeout2(promise, timeoutMs, message) {
1065
+ let timeoutId;
1066
+ try {
1067
+ return await Promise.race([
1068
+ promise,
1069
+ new Promise((_, reject) => {
1070
+ timeoutId = setTimeout(() => reject(new Error(message)), timeoutMs);
1071
+ })
1072
+ ]);
1073
+ } finally {
1074
+ if (timeoutId) clearTimeout(timeoutId);
1075
+ }
1076
+ }
1077
+ function readRemoteFigmaToken(provider) {
1078
+ const token = typeof provider === "function" ? provider() : provider;
1079
+ if (typeof token === "string" && token.trim()) return token.trim();
1080
+ return readStoredFigmaToken();
1081
+ }
1082
+ function readStoredFigmaToken() {
1083
+ if (typeof window === "undefined") return "";
1084
+ try {
1085
+ return window.localStorage.getItem(FIGMA_TOKEN_STORAGE_KEY)?.trim() || "";
1086
+ } catch {
1087
+ return "";
1088
+ }
1089
+ }
1090
+ function createFigmaImageId() {
1091
+ const id = globalThis.crypto?.randomUUID?.() ?? Math.random().toString(36).slice(2);
1092
+ return `figma_${id.replace(/[^A-Za-z0-9_-]+/g, "-")}`;
1093
+ }
1094
+ function normalizeOptionalText(value) {
1095
+ if (typeof value !== "string") return void 0;
1096
+ return value.trim() || void 0;
1097
+ }
1098
+ function readString(value) {
1099
+ return typeof value === "string" && value.trim() ? value.trim() : null;
1100
+ }
1101
+ function throwRemoteStoreError(error, action) {
1102
+ const source = error;
1103
+ const message = readString(source.message) ?? `Remote Figma image ${action} failed.`;
1104
+ const code = readString(source.code);
1105
+ const details = readString(source.details);
1106
+ const hint = readString(source.hint);
1107
+ const suffix = [code, details, hint].filter(Boolean).join(" / ");
1108
+ throw new Error(suffix ? `${message} (${suffix})` : message);
1109
+ }
1110
+
802
1111
  // src/figma/image.snapshot.ts
803
1112
  function createReviewFigmaImagesSnapshot(images, options = {}) {
804
1113
  const targetKeys = options.targets?.length ? new Set(options.targets.map(getReviewFigmaImageTargetKey)) : null;
@@ -5381,6 +5690,7 @@ function createNoopController() {
5381
5690
  0 && (module.exports = {
5382
5691
  DEFAULT_REVIEW_FIGMA_IMAGE_FORMAT,
5383
5692
  DEFAULT_REVIEW_FIGMA_IMAGE_STORE_ENDPOINT,
5693
+ DEFAULT_REVIEW_FIGMA_REMOTE_IMAGES_TABLE,
5384
5694
  DEFAULT_REVIEW_FIGMA_TOKEN_ENV_KEY,
5385
5695
  DEFAULT_REVIEW_VIEWPORTS,
5386
5696
  FIGMA_NODE_REF_SEPARATOR,
@@ -5388,6 +5698,7 @@ function createNoopController() {
5388
5698
  REVIEW_WORKFLOW_STATUS_OPTIONS,
5389
5699
  ReviewFigmaTokenError,
5390
5700
  collectReviewFigmaReleaseSnapshot,
5701
+ createRemoteReviewFigmaImageStore,
5391
5702
  createReviewFigmaFrameUrl,
5392
5703
  createReviewFigmaImageStoreClient,
5393
5704
  createReviewFigmaImagesSnapshot,