@designfever/web-review-kit 0.6.0 → 0.7.1
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/.env.sample +32 -0
- package/README.md +35 -10
- package/dist/chunk-2ZLU5FTD.js +602 -0
- package/dist/chunk-2ZLU5FTD.js.map +1 -0
- package/dist/{chunk-IN36JHEU.js → chunk-RPVLRULC.js} +504 -143
- package/dist/chunk-RPVLRULC.js.map +1 -0
- package/dist/image.types-BmzkFSPX.d.cts +71 -0
- package/dist/image.types-BmzkFSPX.d.ts +71 -0
- package/dist/index.cjs +1037 -144
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +29 -3
- package/dist/index.d.ts +29 -3
- package/dist/index.js +50 -3
- package/dist/index.js.map +1 -1
- package/dist/react-shell.cjs +10339 -5168
- package/dist/react-shell.cjs.map +1 -1
- package/dist/react-shell.d.cts +39 -4
- package/dist/react-shell.d.ts +39 -4
- package/dist/react-shell.js +9687 -4856
- package/dist/react-shell.js.map +1 -1
- package/dist/token-Dt-ZH-YO.d.cts +88 -0
- package/dist/token-nJXPPdYX.d.ts +88 -0
- package/dist/{types-DFHHVRBc.d.cts → types-DT9Z66mV.d.cts} +13 -1
- package/dist/{types-DFHHVRBc.d.ts → types-DT9Z66mV.d.ts} +13 -1
- package/dist/vite.cjs +1144 -5
- package/dist/vite.cjs.map +1 -1
- package/dist/vite.d.cts +45 -1
- package/dist/vite.d.ts +45 -1
- package/dist/vite.js +829 -5
- package/dist/vite.js.map +1 -1
- package/docs/README.md +13 -7
- package/docs/adapters.md +128 -0
- package/docs/adaptor.sample.ts +13 -1
- package/docs/architecture.md +2 -1
- package/docs/code-review-0.6.0.md +232 -0
- package/docs/db-setup.md +4 -2
- package/docs/figma-image-mvp-0.7.0.md +330 -0
- package/docs/figma-overlay.md +11 -4
- package/docs/installation.md +42 -7
- package/docs/release-notes-0.7.0.md +132 -0
- package/docs/release-notes-0.7.1.md +34 -0
- package/package.json +6 -2
- package/dist/chunk-IN36JHEU.js.map +0 -1
|
@@ -0,0 +1,602 @@
|
|
|
1
|
+
// src/figma/parse.ts
|
|
2
|
+
var FIGMA_NODE_REF_SEPARATOR = "->";
|
|
3
|
+
function parseReviewFigmaNodeRef(value) {
|
|
4
|
+
if (typeof value !== "string") return normalizeReviewFigmaNodeRef(value);
|
|
5
|
+
const input = value.trim();
|
|
6
|
+
if (!input) return null;
|
|
7
|
+
return parseReviewFigmaNodeRefValue(input) ?? parseReviewFigmaUrl(input);
|
|
8
|
+
}
|
|
9
|
+
function requireReviewFigmaNodeRef(value) {
|
|
10
|
+
const ref = parseReviewFigmaNodeRef(value);
|
|
11
|
+
if (!ref) {
|
|
12
|
+
throw new Error("A Figma node link or fileKey->nodeId value is required.");
|
|
13
|
+
}
|
|
14
|
+
return ref;
|
|
15
|
+
}
|
|
16
|
+
function createReviewFigmaNodeValue(ref) {
|
|
17
|
+
return `${ref.fileKey}${FIGMA_NODE_REF_SEPARATOR}${ref.nodeId}`;
|
|
18
|
+
}
|
|
19
|
+
function createReviewFigmaFrameUrl(value) {
|
|
20
|
+
const ref = parseReviewFigmaNodeRef(value);
|
|
21
|
+
if (!ref) return null;
|
|
22
|
+
return `https://www.figma.com/design/${encodeURIComponent(
|
|
23
|
+
ref.fileKey
|
|
24
|
+
)}?node-id=${encodeURIComponent(ref.nodeId.replace(/:/g, "-"))}`;
|
|
25
|
+
}
|
|
26
|
+
function parseReviewFigmaNodeRefValue(value) {
|
|
27
|
+
const [fileKey, nodeId, extra] = value.split(FIGMA_NODE_REF_SEPARATOR).map((part) => part.trim());
|
|
28
|
+
if (!fileKey || !nodeId || extra !== void 0) return null;
|
|
29
|
+
return normalizeReviewFigmaNodeRef({ fileKey, nodeId });
|
|
30
|
+
}
|
|
31
|
+
function parseReviewFigmaUrl(value) {
|
|
32
|
+
let url;
|
|
33
|
+
try {
|
|
34
|
+
url = new URL(value);
|
|
35
|
+
} catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
const fileKey = getFigmaFileKey(url);
|
|
39
|
+
const nodeId = normalizeFigmaNodeId(url.searchParams.get("node-id"));
|
|
40
|
+
if (!fileKey || !nodeId) return null;
|
|
41
|
+
return { fileKey, nodeId, sourceUrl: value };
|
|
42
|
+
}
|
|
43
|
+
function getFigmaFileKey(url) {
|
|
44
|
+
if (!/(^|\.)figma\.com$/i.test(url.hostname)) return null;
|
|
45
|
+
const parts = url.pathname.split("/").filter(Boolean);
|
|
46
|
+
const fileKeyIndex = parts.findIndex(
|
|
47
|
+
(part) => ["design", "file", "proto", "board"].includes(part)
|
|
48
|
+
);
|
|
49
|
+
const fileKey = fileKeyIndex >= 0 ? parts[fileKeyIndex + 1] : null;
|
|
50
|
+
return normalizeFigmaFileKey(fileKey);
|
|
51
|
+
}
|
|
52
|
+
function normalizeReviewFigmaNodeRef(value) {
|
|
53
|
+
const fileKey = normalizeFigmaFileKey(value?.fileKey);
|
|
54
|
+
const nodeId = normalizeFigmaNodeId(value?.nodeId);
|
|
55
|
+
if (!fileKey || !nodeId) return null;
|
|
56
|
+
const sourceUrl = normalizeOptionalString(value?.sourceUrl);
|
|
57
|
+
return sourceUrl ? { fileKey, nodeId, sourceUrl } : { fileKey, nodeId };
|
|
58
|
+
}
|
|
59
|
+
function normalizeFigmaFileKey(value) {
|
|
60
|
+
return normalizeOptionalString(value)?.replace(/[?#].*$/, "") ?? null;
|
|
61
|
+
}
|
|
62
|
+
function normalizeFigmaNodeId(value) {
|
|
63
|
+
const nodeId = normalizeOptionalString(value);
|
|
64
|
+
if (!nodeId) return null;
|
|
65
|
+
if (nodeId.includes(":")) return nodeId;
|
|
66
|
+
return nodeId.replace(/-/g, ":");
|
|
67
|
+
}
|
|
68
|
+
function normalizeOptionalString(value) {
|
|
69
|
+
if (typeof value !== "string") return null;
|
|
70
|
+
return value.trim() || null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// src/figma/token.ts
|
|
74
|
+
var DEFAULT_REVIEW_FIGMA_TOKEN_ENV_KEY = "FIGMA_TOKEN";
|
|
75
|
+
var REVIEW_FIGMA_TOKEN_MISSING_CODE = "DFWR_FIGMA_TOKEN_MISSING";
|
|
76
|
+
var ReviewFigmaTokenError = class extends Error {
|
|
77
|
+
constructor(envKey = DEFAULT_REVIEW_FIGMA_TOKEN_ENV_KEY) {
|
|
78
|
+
super(
|
|
79
|
+
`Figma image rendering requires server env ${envKey}. Set ${envKey} in the dev/server environment; do not expose it as VITE_${envKey}.`
|
|
80
|
+
);
|
|
81
|
+
this.code = REVIEW_FIGMA_TOKEN_MISSING_CODE;
|
|
82
|
+
this.name = "ReviewFigmaTokenError";
|
|
83
|
+
this.envKey = envKey;
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
function readReviewFigmaToken(options = {}) {
|
|
87
|
+
if (options.enabled === false) return null;
|
|
88
|
+
const envKey = options.envKey ?? DEFAULT_REVIEW_FIGMA_TOKEN_ENV_KEY;
|
|
89
|
+
return normalizeReviewFigmaToken(options.token ?? options.env?.[envKey]);
|
|
90
|
+
}
|
|
91
|
+
function requireReviewFigmaToken(options = {}) {
|
|
92
|
+
const envKey = options.envKey ?? DEFAULT_REVIEW_FIGMA_TOKEN_ENV_KEY;
|
|
93
|
+
const token = readReviewFigmaToken(options);
|
|
94
|
+
if (!token) throw new ReviewFigmaTokenError(envKey);
|
|
95
|
+
return token;
|
|
96
|
+
}
|
|
97
|
+
function isReviewFigmaTokenError(error) {
|
|
98
|
+
if (!error || typeof error !== "object") return false;
|
|
99
|
+
return error instanceof ReviewFigmaTokenError || "code" in error && error.code === REVIEW_FIGMA_TOKEN_MISSING_CODE;
|
|
100
|
+
}
|
|
101
|
+
function normalizeReviewFigmaToken(value) {
|
|
102
|
+
if (typeof value !== "string") return null;
|
|
103
|
+
return value.trim() || null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// src/figma/render.ts
|
|
107
|
+
var DEFAULT_FIGMA_API_BASE_URL = "https://api.figma.com";
|
|
108
|
+
async function renderReviewFigmaImage(options) {
|
|
109
|
+
const token = requireReviewFigmaToken({ token: options.token });
|
|
110
|
+
const ref = requireReviewFigmaNodeRef(options.figmaUrl);
|
|
111
|
+
const renderFormat = options.format ?? "png";
|
|
112
|
+
const requestUrl = createReviewFigmaImageApiUrl({
|
|
113
|
+
apiBaseUrl: options.apiBaseUrl,
|
|
114
|
+
fileKey: ref.fileKey,
|
|
115
|
+
nodeId: ref.nodeId,
|
|
116
|
+
format: renderFormat,
|
|
117
|
+
scale: options.scale,
|
|
118
|
+
useAbsoluteBounds: options.useAbsoluteBounds
|
|
119
|
+
});
|
|
120
|
+
const fetchImage = options.fetch ?? globalThis.fetch;
|
|
121
|
+
if (!fetchImage) throw new Error("Figma image rendering requires fetch.");
|
|
122
|
+
const response = await fetchImage(requestUrl, {
|
|
123
|
+
headers: {
|
|
124
|
+
"X-Figma-Token": token
|
|
125
|
+
},
|
|
126
|
+
signal: options.signal
|
|
127
|
+
});
|
|
128
|
+
const body = await response.json().catch(() => null);
|
|
129
|
+
if (!response.ok) {
|
|
130
|
+
throw new Error(
|
|
131
|
+
body?.err || `Figma image render failed with ${response.status}`
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
const imageUrl = body?.images?.[ref.nodeId];
|
|
135
|
+
if (!imageUrl) {
|
|
136
|
+
throw new Error(`Figma image render returned no URL for ${ref.nodeId}.`);
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
fileKey: ref.fileKey,
|
|
140
|
+
nodeId: ref.nodeId,
|
|
141
|
+
figmaUrl: typeof options.figmaUrl === "string" ? options.figmaUrl : options.figmaUrl.sourceUrl,
|
|
142
|
+
imageUrl,
|
|
143
|
+
renderFormat
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
function createReviewFigmaImageApiUrl({
|
|
147
|
+
apiBaseUrl = DEFAULT_FIGMA_API_BASE_URL,
|
|
148
|
+
fileKey,
|
|
149
|
+
nodeId,
|
|
150
|
+
format = "png",
|
|
151
|
+
scale,
|
|
152
|
+
useAbsoluteBounds
|
|
153
|
+
}) {
|
|
154
|
+
const url = new URL(`/v1/images/${encodeURIComponent(fileKey)}`, apiBaseUrl);
|
|
155
|
+
url.searchParams.set("ids", nodeId);
|
|
156
|
+
url.searchParams.set("format", format);
|
|
157
|
+
if (typeof scale === "number" && Number.isFinite(scale) && scale > 0) {
|
|
158
|
+
url.searchParams.set("scale", String(scale));
|
|
159
|
+
}
|
|
160
|
+
if (useAbsoluteBounds !== void 0) {
|
|
161
|
+
url.searchParams.set("use_absolute_bounds", String(useAbsoluteBounds));
|
|
162
|
+
}
|
|
163
|
+
return url.toString();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// src/vite/figma-asset.ts
|
|
167
|
+
function parseReviewFigmaImageFormat(value) {
|
|
168
|
+
return value === "webp" || value === "png" || value === "jpg" ? value : void 0;
|
|
169
|
+
}
|
|
170
|
+
function getStoreRenderFormat(renderFormat, imageFormat) {
|
|
171
|
+
if (renderFormat === "jpg" || renderFormat === "png") return renderFormat;
|
|
172
|
+
if (imageFormat === "jpg") return "jpg";
|
|
173
|
+
return "png";
|
|
174
|
+
}
|
|
175
|
+
function getReviewFigmaImageFormatFromMimeType(mimeType) {
|
|
176
|
+
if (mimeType === "image/webp") return "webp";
|
|
177
|
+
if (mimeType === "image/png") return "png";
|
|
178
|
+
if (mimeType === "image/jpeg") return "jpg";
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
function normalizeImageMimeType(value) {
|
|
182
|
+
const mimeType = value?.split(";")[0]?.trim().toLowerCase();
|
|
183
|
+
if (mimeType === "image/jpg") return "image/jpeg";
|
|
184
|
+
if (mimeType === "image/jpeg" || mimeType === "image/png" || mimeType === "image/webp") {
|
|
185
|
+
return mimeType;
|
|
186
|
+
}
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
function createReviewFigmaAssetStorageKey(id, imageFormat) {
|
|
190
|
+
return `${id}.${getReviewFigmaAssetExtension(imageFormat)}`;
|
|
191
|
+
}
|
|
192
|
+
function createReviewFigmaAssetUrl(assetEndpoint, storageKey) {
|
|
193
|
+
return `${assetEndpoint}/${encodeURIComponent(storageKey)}`;
|
|
194
|
+
}
|
|
195
|
+
function getReviewFigmaAssetStorageKeyFromPathname(pathname, assetEndpoint) {
|
|
196
|
+
try {
|
|
197
|
+
const storageKey = decodeURIComponent(
|
|
198
|
+
pathname.slice(assetEndpoint.length + 1)
|
|
199
|
+
);
|
|
200
|
+
return isSafeReviewFigmaAssetStorageKey(storageKey) ? storageKey : null;
|
|
201
|
+
} catch {
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
function isSafeReviewFigmaAssetStorageKey(value) {
|
|
206
|
+
return /^figma_[a-z0-9_]+\.(webp|png|jpg)$/.test(value);
|
|
207
|
+
}
|
|
208
|
+
function getReviewFigmaAssetExtension(format) {
|
|
209
|
+
return format === "jpg" ? "jpg" : format;
|
|
210
|
+
}
|
|
211
|
+
function getReviewFigmaAssetMimeType(storageKey) {
|
|
212
|
+
if (storageKey.endsWith(".jpg")) return "image/jpeg";
|
|
213
|
+
if (storageKey.endsWith(".webp")) return "image/webp";
|
|
214
|
+
return "image/png";
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// src/figma/image.store.ts
|
|
218
|
+
var DEFAULT_REVIEW_FIGMA_IMAGE_STORE_ENDPOINT = "/__dfwr/figma-images";
|
|
219
|
+
function createReviewFigmaImageStoreClient(options = {}) {
|
|
220
|
+
const endpoint = options.endpoint ?? DEFAULT_REVIEW_FIGMA_IMAGE_STORE_ENDPOINT;
|
|
221
|
+
const request = createReviewFigmaImageStoreRequest(endpoint, options.fetch);
|
|
222
|
+
return {
|
|
223
|
+
listImages(target) {
|
|
224
|
+
const url = `${endpoint}?target=${encodeURIComponent(
|
|
225
|
+
JSON.stringify(target)
|
|
226
|
+
)}`;
|
|
227
|
+
return request(url);
|
|
228
|
+
},
|
|
229
|
+
async addImage(input) {
|
|
230
|
+
const nextInput = await createClientRenderedAddImageInput(
|
|
231
|
+
input,
|
|
232
|
+
options.clientRender,
|
|
233
|
+
options.fetch
|
|
234
|
+
);
|
|
235
|
+
return request(endpoint, {
|
|
236
|
+
method: "POST",
|
|
237
|
+
body: JSON.stringify(nextInput),
|
|
238
|
+
figmaToken: readReviewFigmaImageToken(
|
|
239
|
+
options.token ?? getStoredReviewFigmaImageToken
|
|
240
|
+
)
|
|
241
|
+
});
|
|
242
|
+
},
|
|
243
|
+
updateImage(id, patch) {
|
|
244
|
+
return request(`${endpoint}/${encodeURIComponent(id)}`, {
|
|
245
|
+
method: "PATCH",
|
|
246
|
+
body: JSON.stringify(patch)
|
|
247
|
+
});
|
|
248
|
+
},
|
|
249
|
+
reorderImages(input) {
|
|
250
|
+
return request(`${endpoint}/reorder`, {
|
|
251
|
+
method: "PATCH",
|
|
252
|
+
body: JSON.stringify(input)
|
|
253
|
+
});
|
|
254
|
+
},
|
|
255
|
+
deleteImage(id) {
|
|
256
|
+
return request(`${endpoint}/${encodeURIComponent(id)}`, {
|
|
257
|
+
method: "DELETE"
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
async function createClientRenderedAddImageInput(input, clientRender, fetchOption) {
|
|
263
|
+
const options = normalizeClientRenderOptions(clientRender);
|
|
264
|
+
if (!options) return input;
|
|
265
|
+
const token = readClientRenderToken(options);
|
|
266
|
+
if (!token) return input;
|
|
267
|
+
try {
|
|
268
|
+
const asset = await withTimeout(
|
|
269
|
+
createClientRenderedFigmaAsset(input.figmaUrl, token, options, fetchOption),
|
|
270
|
+
options.timeoutMs ?? 1e4
|
|
271
|
+
);
|
|
272
|
+
return {
|
|
273
|
+
...input,
|
|
274
|
+
imageFormat: asset.imageFormat,
|
|
275
|
+
asset
|
|
276
|
+
};
|
|
277
|
+
} catch {
|
|
278
|
+
return input;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
function normalizeClientRenderOptions(clientRender) {
|
|
282
|
+
if (!clientRender) return null;
|
|
283
|
+
if (clientRender === true) return {};
|
|
284
|
+
return clientRender;
|
|
285
|
+
}
|
|
286
|
+
function readClientRenderToken(options) {
|
|
287
|
+
const token = typeof options.token === "function" ? options.token() : options.token;
|
|
288
|
+
return typeof token === "string" ? token.trim() : "";
|
|
289
|
+
}
|
|
290
|
+
async function createClientRenderedFigmaAsset(figmaUrl, token, options, fetchOption) {
|
|
291
|
+
const ref = parseReviewFigmaNodeRef(figmaUrl);
|
|
292
|
+
if (!ref) throw new Error("A Figma node link is required.");
|
|
293
|
+
const requestFetch = fetchOption ?? globalThis.fetch;
|
|
294
|
+
if (!requestFetch) throw new Error("Figma client rendering requires fetch.");
|
|
295
|
+
const renderFormat = options.renderFormat ?? "png";
|
|
296
|
+
const response = await requestFetch(
|
|
297
|
+
createReviewFigmaImageApiUrl({
|
|
298
|
+
apiBaseUrl: options.apiBaseUrl,
|
|
299
|
+
fileKey: ref.fileKey,
|
|
300
|
+
nodeId: ref.nodeId,
|
|
301
|
+
format: renderFormat,
|
|
302
|
+
scale: options.renderScale,
|
|
303
|
+
useAbsoluteBounds: options.useAbsoluteBounds
|
|
304
|
+
}),
|
|
305
|
+
{
|
|
306
|
+
headers: {
|
|
307
|
+
"X-Figma-Token": token
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
);
|
|
311
|
+
const body = await response.json().catch(() => null);
|
|
312
|
+
if (!response.ok) {
|
|
313
|
+
throw new Error(body?.err || `Figma image render failed with ${response.status}`);
|
|
314
|
+
}
|
|
315
|
+
const imageUrl = body?.images?.[ref.nodeId];
|
|
316
|
+
if (!imageUrl) throw new Error(`Figma image render returned no URL for ${ref.nodeId}.`);
|
|
317
|
+
const imageResponse = await requestFetch(imageUrl);
|
|
318
|
+
if (!imageResponse.ok) {
|
|
319
|
+
throw new Error(`Figma image download failed with ${imageResponse.status}`);
|
|
320
|
+
}
|
|
321
|
+
const originalBlob = await imageResponse.blob();
|
|
322
|
+
const originalMimeType = normalizeClientImageMimeType(originalBlob.type) ?? getReviewFigmaImageMimeType(renderFormat === "jpg" ? "jpg" : "png");
|
|
323
|
+
const originalFormat = getReviewFigmaImageFormatFromMimeType(originalMimeType) ?? (renderFormat === "jpg" ? "jpg" : "png");
|
|
324
|
+
const dimensions = await readImageBlobDimensions(originalBlob);
|
|
325
|
+
const shouldConvertToWebp = options.convertToWebp ?? true;
|
|
326
|
+
const convertedBlob = shouldConvertToWebp ? await convertImageBlobToWebp(
|
|
327
|
+
originalBlob,
|
|
328
|
+
options.webpQuality ?? 0.9,
|
|
329
|
+
dimensions
|
|
330
|
+
).catch(() => null) : null;
|
|
331
|
+
const finalBlob = convertedBlob?.type === "image/webp" ? convertedBlob : originalBlob;
|
|
332
|
+
const finalMimeType = normalizeClientImageMimeType(finalBlob.type) ?? originalMimeType;
|
|
333
|
+
const finalFormat = getReviewFigmaImageFormatFromMimeType(finalMimeType) ?? originalFormat;
|
|
334
|
+
return {
|
|
335
|
+
dataUrl: await blobToDataUrl(finalBlob),
|
|
336
|
+
imageFormat: finalFormat,
|
|
337
|
+
mimeType: finalMimeType,
|
|
338
|
+
byteSize: finalBlob.size,
|
|
339
|
+
width: dimensions.width,
|
|
340
|
+
height: dimensions.height
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
async function readImageBlobDimensions(blob) {
|
|
344
|
+
const image = await loadImageBlob(blob);
|
|
345
|
+
return {
|
|
346
|
+
width: image.naturalWidth || image.width,
|
|
347
|
+
height: image.naturalHeight || image.height
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
async function convertImageBlobToWebp(blob, quality, dimensions) {
|
|
351
|
+
if (typeof document === "undefined") return null;
|
|
352
|
+
if (!dimensions.width || !dimensions.height) return null;
|
|
353
|
+
const image = await loadImageBlob(blob);
|
|
354
|
+
const canvas = document.createElement("canvas");
|
|
355
|
+
canvas.width = dimensions.width;
|
|
356
|
+
canvas.height = dimensions.height;
|
|
357
|
+
const context = canvas.getContext("2d");
|
|
358
|
+
if (!context) return null;
|
|
359
|
+
context.drawImage(image, 0, 0);
|
|
360
|
+
return new Promise((resolve) => {
|
|
361
|
+
canvas.toBlob(resolve, "image/webp", quality);
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
function loadImageBlob(blob) {
|
|
365
|
+
return new Promise((resolve, reject) => {
|
|
366
|
+
if (typeof Image === "undefined" || typeof URL === "undefined") {
|
|
367
|
+
reject(new Error("Image decoding is unavailable."));
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
const image = new Image();
|
|
371
|
+
const objectUrl = URL.createObjectURL(blob);
|
|
372
|
+
image.onload = () => {
|
|
373
|
+
URL.revokeObjectURL(objectUrl);
|
|
374
|
+
resolve(image);
|
|
375
|
+
};
|
|
376
|
+
image.onerror = () => {
|
|
377
|
+
URL.revokeObjectURL(objectUrl);
|
|
378
|
+
reject(new Error("Image decoding failed."));
|
|
379
|
+
};
|
|
380
|
+
image.src = objectUrl;
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
function blobToDataUrl(blob) {
|
|
384
|
+
return new Promise((resolve, reject) => {
|
|
385
|
+
const reader = new FileReader();
|
|
386
|
+
reader.onload = () => {
|
|
387
|
+
if (typeof reader.result === "string") {
|
|
388
|
+
resolve(reader.result);
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
reject(new Error("Blob encoding failed."));
|
|
392
|
+
};
|
|
393
|
+
reader.onerror = () => reject(reader.error ?? new Error("Blob encoding failed."));
|
|
394
|
+
reader.readAsDataURL(blob);
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
function normalizeClientImageMimeType(value) {
|
|
398
|
+
const mimeType = value?.split(";")[0]?.trim().toLowerCase();
|
|
399
|
+
if (mimeType === "image/jpg") return "image/jpeg";
|
|
400
|
+
if (mimeType === "image/jpeg" || mimeType === "image/png" || mimeType === "image/webp") {
|
|
401
|
+
return mimeType;
|
|
402
|
+
}
|
|
403
|
+
return null;
|
|
404
|
+
}
|
|
405
|
+
async function withTimeout(promise, timeoutMs) {
|
|
406
|
+
let timeoutId;
|
|
407
|
+
try {
|
|
408
|
+
return await Promise.race([
|
|
409
|
+
promise,
|
|
410
|
+
new Promise((_, reject) => {
|
|
411
|
+
timeoutId = setTimeout(
|
|
412
|
+
() => reject(new Error("Figma client rendering timed out.")),
|
|
413
|
+
timeoutMs
|
|
414
|
+
);
|
|
415
|
+
})
|
|
416
|
+
]);
|
|
417
|
+
} finally {
|
|
418
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
function getReviewFigmaImageTargetKey(target) {
|
|
422
|
+
return JSON.stringify(normalizeReviewFigmaImageTarget(target));
|
|
423
|
+
}
|
|
424
|
+
function getReviewFigmaImageMimeType(format) {
|
|
425
|
+
if (format === "jpg") return "image/jpeg";
|
|
426
|
+
if (format === "png") return "image/png";
|
|
427
|
+
return "image/webp";
|
|
428
|
+
}
|
|
429
|
+
function createReviewFigmaImageStoreRequest(endpoint, fetchOption) {
|
|
430
|
+
return async (input, init = {}) => {
|
|
431
|
+
const requestFetch = fetchOption ?? globalThis.fetch;
|
|
432
|
+
if (!requestFetch) throw new Error("Figma image store requires fetch.");
|
|
433
|
+
const figmaToken = init.figmaToken ?? "";
|
|
434
|
+
const { figmaToken: _figmaToken, ...requestInit } = init;
|
|
435
|
+
const response = await requestFetch(input, {
|
|
436
|
+
...requestInit,
|
|
437
|
+
headers: {
|
|
438
|
+
"Content-Type": "application/json",
|
|
439
|
+
...figmaToken ? { "X-Figma-Token": figmaToken } : {},
|
|
440
|
+
...requestInit.headers ?? {}
|
|
441
|
+
}
|
|
442
|
+
});
|
|
443
|
+
const text = await response.text();
|
|
444
|
+
const body = text ? JSON.parse(text) : null;
|
|
445
|
+
if (!response.ok) {
|
|
446
|
+
const message = typeof body?.error === "string" ? body.error : `Figma image store request failed: ${response.status}`;
|
|
447
|
+
throw new Error(message);
|
|
448
|
+
}
|
|
449
|
+
return body;
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
function readReviewFigmaImageToken(provider) {
|
|
453
|
+
const token = typeof provider === "function" ? provider() : provider;
|
|
454
|
+
return typeof token === "string" ? token.trim() : "";
|
|
455
|
+
}
|
|
456
|
+
function getStoredReviewFigmaImageToken() {
|
|
457
|
+
if (typeof window === "undefined") return "";
|
|
458
|
+
try {
|
|
459
|
+
return window.localStorage.getItem("figma-token") ?? "";
|
|
460
|
+
} catch {
|
|
461
|
+
return "";
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
function normalizeReviewFigmaImageTarget(target) {
|
|
465
|
+
if (target.type === "figma-node") {
|
|
466
|
+
return {
|
|
467
|
+
type: target.type,
|
|
468
|
+
projectId: target.projectId,
|
|
469
|
+
fileKey: target.fileKey,
|
|
470
|
+
nodeId: target.nodeId
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
return {
|
|
474
|
+
type: target.type,
|
|
475
|
+
projectId: target.projectId,
|
|
476
|
+
pageUrl: target.pageUrl,
|
|
477
|
+
slot: target.slot ?? "",
|
|
478
|
+
viewport: target.viewport ? {
|
|
479
|
+
label: target.viewport.label ?? "",
|
|
480
|
+
width: target.viewport.width ?? null,
|
|
481
|
+
height: target.viewport.height ?? null,
|
|
482
|
+
scope: target.viewport.scope ?? ""
|
|
483
|
+
} : null
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// src/figma/image.snapshot.ts
|
|
488
|
+
function createReviewFigmaImagesSnapshot(images, options = {}) {
|
|
489
|
+
const targetKeys = options.targets?.length ? new Set(options.targets.map(getReviewFigmaImageTargetKey)) : null;
|
|
490
|
+
return images.filter((image) => {
|
|
491
|
+
if (options.projectId && image.projectId !== options.projectId) {
|
|
492
|
+
return false;
|
|
493
|
+
}
|
|
494
|
+
if (targetKeys && !targetKeys.has(getReviewFigmaImageTargetKey(image.target))) {
|
|
495
|
+
return false;
|
|
496
|
+
}
|
|
497
|
+
return true;
|
|
498
|
+
}).map(cloneReviewFigmaImage).sort(compareReviewFigmaSnapshotImages);
|
|
499
|
+
}
|
|
500
|
+
function createReviewFigmaReleaseSnapshot({
|
|
501
|
+
images,
|
|
502
|
+
projectId,
|
|
503
|
+
releaseId,
|
|
504
|
+
label,
|
|
505
|
+
createdAt,
|
|
506
|
+
targets
|
|
507
|
+
}) {
|
|
508
|
+
return {
|
|
509
|
+
version: 1,
|
|
510
|
+
projectId,
|
|
511
|
+
...releaseId ? { releaseId } : null,
|
|
512
|
+
...label ? { label } : null,
|
|
513
|
+
createdAt: createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
514
|
+
figmaImagesSnapshot: createReviewFigmaImagesSnapshot(images, {
|
|
515
|
+
projectId,
|
|
516
|
+
targets
|
|
517
|
+
})
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
async function collectReviewFigmaReleaseSnapshot({
|
|
521
|
+
store,
|
|
522
|
+
targets,
|
|
523
|
+
...snapshotOptions
|
|
524
|
+
}) {
|
|
525
|
+
const imagesByTarget = await Promise.all(
|
|
526
|
+
targets.map((target) => store.listImages(target))
|
|
527
|
+
);
|
|
528
|
+
return createReviewFigmaReleaseSnapshot({
|
|
529
|
+
...snapshotOptions,
|
|
530
|
+
targets,
|
|
531
|
+
images: dedupeReviewFigmaImages(imagesByTarget.flat())
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
function dedupeReviewFigmaImages(images) {
|
|
535
|
+
return Array.from(new Map(images.map((image) => [image.id, image])).values());
|
|
536
|
+
}
|
|
537
|
+
function cloneReviewFigmaImage(image) {
|
|
538
|
+
return {
|
|
539
|
+
...image,
|
|
540
|
+
target: cloneReviewFigmaImageTarget(image.target)
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
function cloneReviewFigmaImageTarget(target) {
|
|
544
|
+
if (target.type === "figma-node") {
|
|
545
|
+
return {
|
|
546
|
+
type: target.type,
|
|
547
|
+
projectId: target.projectId,
|
|
548
|
+
fileKey: target.fileKey,
|
|
549
|
+
nodeId: target.nodeId
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
return {
|
|
553
|
+
type: target.type,
|
|
554
|
+
projectId: target.projectId,
|
|
555
|
+
pageUrl: target.pageUrl,
|
|
556
|
+
slot: target.slot,
|
|
557
|
+
viewport: target.viewport ? {
|
|
558
|
+
label: target.viewport.label,
|
|
559
|
+
width: target.viewport.width,
|
|
560
|
+
height: target.viewport.height,
|
|
561
|
+
scope: target.viewport.scope
|
|
562
|
+
} : void 0
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
function compareReviewFigmaSnapshotImages(a, b) {
|
|
566
|
+
return a.projectId.localeCompare(b.projectId) || getReviewFigmaImageTargetKey(a.target).localeCompare(
|
|
567
|
+
getReviewFigmaImageTargetKey(b.target)
|
|
568
|
+
) || a.order - b.order || a.createdAt.localeCompare(b.createdAt) || a.id.localeCompare(b.id);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
export {
|
|
572
|
+
parseReviewFigmaImageFormat,
|
|
573
|
+
getStoreRenderFormat,
|
|
574
|
+
getReviewFigmaImageFormatFromMimeType,
|
|
575
|
+
normalizeImageMimeType,
|
|
576
|
+
createReviewFigmaAssetStorageKey,
|
|
577
|
+
createReviewFigmaAssetUrl,
|
|
578
|
+
getReviewFigmaAssetStorageKeyFromPathname,
|
|
579
|
+
isSafeReviewFigmaAssetStorageKey,
|
|
580
|
+
getReviewFigmaAssetMimeType,
|
|
581
|
+
FIGMA_NODE_REF_SEPARATOR,
|
|
582
|
+
parseReviewFigmaNodeRef,
|
|
583
|
+
requireReviewFigmaNodeRef,
|
|
584
|
+
createReviewFigmaNodeValue,
|
|
585
|
+
createReviewFigmaFrameUrl,
|
|
586
|
+
DEFAULT_REVIEW_FIGMA_TOKEN_ENV_KEY,
|
|
587
|
+
REVIEW_FIGMA_TOKEN_MISSING_CODE,
|
|
588
|
+
ReviewFigmaTokenError,
|
|
589
|
+
readReviewFigmaToken,
|
|
590
|
+
requireReviewFigmaToken,
|
|
591
|
+
isReviewFigmaTokenError,
|
|
592
|
+
renderReviewFigmaImage,
|
|
593
|
+
createReviewFigmaImageApiUrl,
|
|
594
|
+
DEFAULT_REVIEW_FIGMA_IMAGE_STORE_ENDPOINT,
|
|
595
|
+
createReviewFigmaImageStoreClient,
|
|
596
|
+
getReviewFigmaImageTargetKey,
|
|
597
|
+
getReviewFigmaImageMimeType,
|
|
598
|
+
createReviewFigmaImagesSnapshot,
|
|
599
|
+
createReviewFigmaReleaseSnapshot,
|
|
600
|
+
collectReviewFigmaReleaseSnapshot
|
|
601
|
+
};
|
|
602
|
+
//# sourceMappingURL=chunk-2ZLU5FTD.js.map
|