@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.
Files changed (43) hide show
  1. package/.env.sample +32 -0
  2. package/README.md +35 -10
  3. package/dist/chunk-2ZLU5FTD.js +602 -0
  4. package/dist/chunk-2ZLU5FTD.js.map +1 -0
  5. package/dist/{chunk-IN36JHEU.js → chunk-RPVLRULC.js} +504 -143
  6. package/dist/chunk-RPVLRULC.js.map +1 -0
  7. package/dist/image.types-BmzkFSPX.d.cts +71 -0
  8. package/dist/image.types-BmzkFSPX.d.ts +71 -0
  9. package/dist/index.cjs +1037 -144
  10. package/dist/index.cjs.map +1 -1
  11. package/dist/index.d.cts +29 -3
  12. package/dist/index.d.ts +29 -3
  13. package/dist/index.js +50 -3
  14. package/dist/index.js.map +1 -1
  15. package/dist/react-shell.cjs +10339 -5168
  16. package/dist/react-shell.cjs.map +1 -1
  17. package/dist/react-shell.d.cts +39 -4
  18. package/dist/react-shell.d.ts +39 -4
  19. package/dist/react-shell.js +9687 -4856
  20. package/dist/react-shell.js.map +1 -1
  21. package/dist/token-Dt-ZH-YO.d.cts +88 -0
  22. package/dist/token-nJXPPdYX.d.ts +88 -0
  23. package/dist/{types-DFHHVRBc.d.cts → types-DT9Z66mV.d.cts} +13 -1
  24. package/dist/{types-DFHHVRBc.d.ts → types-DT9Z66mV.d.ts} +13 -1
  25. package/dist/vite.cjs +1144 -5
  26. package/dist/vite.cjs.map +1 -1
  27. package/dist/vite.d.cts +45 -1
  28. package/dist/vite.d.ts +45 -1
  29. package/dist/vite.js +829 -5
  30. package/dist/vite.js.map +1 -1
  31. package/docs/README.md +13 -7
  32. package/docs/adapters.md +128 -0
  33. package/docs/adaptor.sample.ts +13 -1
  34. package/docs/architecture.md +2 -1
  35. package/docs/code-review-0.6.0.md +232 -0
  36. package/docs/db-setup.md +4 -2
  37. package/docs/figma-image-mvp-0.7.0.md +330 -0
  38. package/docs/figma-overlay.md +11 -4
  39. package/docs/installation.md +42 -7
  40. package/docs/release-notes-0.7.0.md +132 -0
  41. package/docs/release-notes-0.7.1.md +34 -0
  42. package/package.json +6 -2
  43. package/dist/chunk-IN36JHEU.js.map +0 -1
package/dist/vite.js CHANGED
@@ -1,12 +1,814 @@
1
+ import {
2
+ DEFAULT_REVIEW_FIGMA_IMAGE_STORE_ENDPOINT,
3
+ DEFAULT_REVIEW_FIGMA_TOKEN_ENV_KEY,
4
+ ReviewFigmaTokenError,
5
+ collectReviewFigmaReleaseSnapshot,
6
+ createReviewFigmaAssetStorageKey,
7
+ createReviewFigmaAssetUrl,
8
+ createReviewFigmaImageApiUrl,
9
+ createReviewFigmaImagesSnapshot,
10
+ createReviewFigmaReleaseSnapshot,
11
+ getReviewFigmaAssetMimeType,
12
+ getReviewFigmaAssetStorageKeyFromPathname,
13
+ getReviewFigmaImageFormatFromMimeType,
14
+ getReviewFigmaImageMimeType,
15
+ getReviewFigmaImageTargetKey,
16
+ getStoreRenderFormat,
17
+ isSafeReviewFigmaAssetStorageKey,
18
+ normalizeImageMimeType,
19
+ parseReviewFigmaImageFormat,
20
+ parseReviewFigmaNodeRef,
21
+ readReviewFigmaToken,
22
+ renderReviewFigmaImage,
23
+ requireReviewFigmaToken
24
+ } from "./chunk-2ZLU5FTD.js";
25
+
26
+ // src/vite/figma-image-store.ts
27
+ import path3 from "path";
28
+ import { loadEnv } from "vite";
29
+
30
+ // src/vite/figma-image-store.server.ts
31
+ import { readFile as readFile2 } from "fs/promises";
32
+ import path2 from "path";
33
+
34
+ // src/vite/figma-image-store.image.ts
35
+ import { mkdir, readFile, rm, writeFile } from "fs/promises";
36
+ import path from "path";
37
+ function requireReviewFigmaRequestToken({
38
+ enabled,
39
+ env,
40
+ envKey,
41
+ requestToken,
42
+ token
43
+ }) {
44
+ const tokenEnvKey = envKey ?? DEFAULT_REVIEW_FIGMA_TOKEN_ENV_KEY;
45
+ const figmaToken = readReviewFigmaToken({ token, env, envKey: tokenEnvKey, enabled }) || readReviewFigmaToken({
46
+ token: requestToken,
47
+ env: {},
48
+ envKey: tokenEnvKey,
49
+ enabled
50
+ });
51
+ if (!figmaToken) throw new ReviewFigmaTokenError(tokenEnvKey);
52
+ return figmaToken;
53
+ }
54
+ async function readReviewFigmaNodeName({
55
+ apiBaseUrl,
56
+ enabled,
57
+ env,
58
+ envKey,
59
+ fetchOption,
60
+ fileKey,
61
+ nodeId,
62
+ token,
63
+ requestToken
64
+ }) {
65
+ const figmaToken = requireReviewFigmaRequestToken({
66
+ enabled,
67
+ env,
68
+ envKey,
69
+ requestToken,
70
+ token
71
+ });
72
+ const fetchNode = fetchOption ?? globalThis.fetch;
73
+ if (!fetchNode) throw new Error("Figma node name lookup requires fetch.");
74
+ const response = await fetchNode(
75
+ createReviewFigmaNodeApiUrl({ apiBaseUrl, fileKey, nodeId }),
76
+ {
77
+ headers: {
78
+ "X-Figma-Token": figmaToken
79
+ }
80
+ }
81
+ );
82
+ const body = await response.json().catch(() => null);
83
+ if (!response.ok) {
84
+ throw new Error(body?.err || `Figma node lookup failed with ${response.status}`);
85
+ }
86
+ const nodes = body?.nodes;
87
+ const node = nodes?.[nodeId] ?? Object.values(nodes ?? {})[0];
88
+ return normalizeOptionalText(node?.document?.name);
89
+ }
90
+ function createReviewFigmaNodeApiUrl({
91
+ apiBaseUrl = "https://api.figma.com",
92
+ fileKey,
93
+ nodeId
94
+ }) {
95
+ const url = new URL(
96
+ `/v1/files/${encodeURIComponent(fileKey)}/nodes`,
97
+ apiBaseUrl
98
+ );
99
+ url.searchParams.set("ids", nodeId);
100
+ return url.toString();
101
+ }
102
+ async function createReviewFigmaImage({
103
+ assetDir,
104
+ assetEndpoint,
105
+ currentImages,
106
+ env,
107
+ input,
108
+ options,
109
+ requestToken
110
+ }) {
111
+ const ref = parseReviewFigmaNodeRef(input.figmaUrl);
112
+ if (!ref) {
113
+ throw new Error("A Figma node copy link or fileKey->nodeId value is required.");
114
+ }
115
+ const id = createReviewFigmaImageId();
116
+ const explicitLabel = normalizeOptionalText(input.label);
117
+ if (input.asset) {
118
+ const cachedAsset2 = await cacheReviewFigmaProvidedImageAsset({
119
+ assetDir,
120
+ assetEndpoint,
121
+ id,
122
+ asset: input.asset,
123
+ options
124
+ });
125
+ const now2 = (/* @__PURE__ */ new Date()).toISOString();
126
+ const order2 = typeof input.order === "number" && Number.isFinite(input.order) ? input.order : getNextImageOrder(currentImages, input.target);
127
+ return {
128
+ id,
129
+ projectId: input.target.projectId,
130
+ target: input.target,
131
+ figmaUrl: input.figmaUrl,
132
+ fileKey: ref.fileKey,
133
+ nodeId: ref.nodeId,
134
+ imageUrl: cachedAsset2.imageUrl,
135
+ imageFormat: cachedAsset2.imageFormat,
136
+ mimeType: cachedAsset2.mimeType,
137
+ label: explicitLabel,
138
+ order: order2,
139
+ storageKey: cachedAsset2.storageKey,
140
+ width: input.asset.width,
141
+ height: input.asset.height,
142
+ byteSize: cachedAsset2.byteSize,
143
+ createdAt: now2,
144
+ updatedAt: now2
145
+ };
146
+ }
147
+ const nodeLabelPromise = explicitLabel ? Promise.resolve(void 0) : readReviewFigmaNodeName({
148
+ apiBaseUrl: options.apiBaseUrl,
149
+ enabled: options.enabled,
150
+ env,
151
+ envKey: options.envKey,
152
+ fetchOption: options.fetch,
153
+ fileKey: ref.fileKey,
154
+ nodeId: ref.nodeId,
155
+ token: options.token,
156
+ requestToken
157
+ }).catch(() => void 0);
158
+ const targetImageFormat = input.imageFormat ?? options.imageFormat ?? "webp";
159
+ const renderFormat = getStoreRenderFormat(options.renderFormat, targetImageFormat);
160
+ const rendered = await renderReviewFigmaImage({
161
+ figmaUrl: input.figmaUrl,
162
+ format: renderFormat,
163
+ scale: options.renderScale,
164
+ useAbsoluteBounds: options.useAbsoluteBounds,
165
+ apiBaseUrl: options.apiBaseUrl,
166
+ fetch: options.fetch,
167
+ token: requireReviewFigmaRequestToken({
168
+ enabled: options.enabled,
169
+ env,
170
+ envKey: options.envKey,
171
+ requestToken,
172
+ token: options.token
173
+ })
174
+ });
175
+ const cachedAsset = await cacheReviewFigmaImageAsset({
176
+ assetDir,
177
+ assetEndpoint,
178
+ id,
179
+ imageUrl: rendered.imageUrl,
180
+ options,
181
+ renderFormat,
182
+ targetImageFormat
183
+ });
184
+ const imageFormat = cachedAsset?.imageFormat ?? (renderFormat === "jpg" ? "jpg" : "png");
185
+ const now = (/* @__PURE__ */ new Date()).toISOString();
186
+ const order = typeof input.order === "number" && Number.isFinite(input.order) ? input.order : getNextImageOrder(currentImages, input.target);
187
+ const nodeLabel = await nodeLabelPromise;
188
+ return {
189
+ id,
190
+ projectId: input.target.projectId,
191
+ target: input.target,
192
+ figmaUrl: input.figmaUrl,
193
+ fileKey: rendered.fileKey,
194
+ nodeId: rendered.nodeId,
195
+ imageUrl: cachedAsset?.imageUrl ?? rendered.imageUrl,
196
+ imageFormat,
197
+ mimeType: cachedAsset?.mimeType ?? getReviewFigmaImageMimeType(imageFormat),
198
+ label: explicitLabel ?? nodeLabel,
199
+ order,
200
+ storageKey: cachedAsset?.storageKey,
201
+ byteSize: cachedAsset?.byteSize,
202
+ createdAt: now,
203
+ updatedAt: now
204
+ };
205
+ }
206
+ async function cacheReviewFigmaProvidedImageAsset({
207
+ assetDir,
208
+ assetEndpoint,
209
+ id,
210
+ asset,
211
+ options
212
+ }) {
213
+ const decodedAsset = decodeReviewFigmaImageAsset(asset);
214
+ const storageKey = createReviewFigmaAssetStorageKey(
215
+ id,
216
+ decodedAsset.imageFormat
217
+ );
218
+ await mkdir(assetDir, { recursive: true });
219
+ await writeFile(path.join(assetDir, storageKey), decodedAsset.data);
220
+ return {
221
+ imageUrl: createReviewFigmaAssetUrl(assetEndpoint, storageKey),
222
+ imageFormat: decodedAsset.imageFormat,
223
+ mimeType: decodedAsset.mimeType,
224
+ storageKey,
225
+ byteSize: decodedAsset.data.byteLength
226
+ };
227
+ }
228
+ function decodeReviewFigmaImageAsset(asset) {
229
+ const mimeType = normalizeImageMimeType(asset.mimeType);
230
+ if (!mimeType) throw new Error("Unsupported Figma image asset MIME type.");
231
+ const imageFormat = getReviewFigmaImageFormatFromMimeType(mimeType) ?? asset.imageFormat;
232
+ const match = /^data:([^;,]+);base64,([a-zA-Z0-9+/=\s]+)$/.exec(
233
+ asset.dataUrl
234
+ );
235
+ if (!match) throw new Error("Valid Figma image asset data URL is required.");
236
+ const dataUrlMimeType = normalizeImageMimeType(match[1]);
237
+ if (dataUrlMimeType && dataUrlMimeType !== mimeType) {
238
+ throw new Error("Figma image asset MIME type mismatch.");
239
+ }
240
+ return {
241
+ data: Buffer.from(match[2].replace(/\s/g, ""), "base64"),
242
+ imageFormat,
243
+ mimeType
244
+ };
245
+ }
246
+ async function cacheReviewFigmaImageAsset({
247
+ assetDir,
248
+ assetEndpoint,
249
+ id,
250
+ imageUrl,
251
+ options,
252
+ renderFormat,
253
+ targetImageFormat
254
+ }) {
255
+ if (options.cacheAssets === false) return null;
256
+ const asset = await downloadReviewFigmaImageAsset({
257
+ fetchOption: options.fetch,
258
+ imageUrl,
259
+ renderFormat,
260
+ targetImageFormat,
261
+ transformAsset: options.transformAsset
262
+ });
263
+ const storageKey = createReviewFigmaAssetStorageKey(id, asset.imageFormat);
264
+ await mkdir(assetDir, { recursive: true });
265
+ await writeFile(path.join(assetDir, storageKey), asset.data);
266
+ return {
267
+ imageUrl: createReviewFigmaAssetUrl(assetEndpoint, storageKey),
268
+ imageFormat: asset.imageFormat,
269
+ mimeType: asset.mimeType,
270
+ storageKey,
271
+ byteSize: asset.data.byteLength
272
+ };
273
+ }
274
+ async function downloadReviewFigmaImageAsset({
275
+ fetchOption,
276
+ imageUrl,
277
+ renderFormat,
278
+ targetImageFormat,
279
+ transformAsset
280
+ }) {
281
+ const fetchImage = fetchOption ?? globalThis.fetch;
282
+ if (!fetchImage) throw new Error("Figma image caching requires fetch.");
283
+ const response = await fetchImage(imageUrl);
284
+ if (!response.ok) {
285
+ throw new Error(`Figma image download failed with ${response.status}`);
286
+ }
287
+ const sourceMimeType = normalizeImageMimeType(response.headers.get("content-type")) ?? getReviewFigmaImageMimeType(renderFormat === "jpg" ? "jpg" : "png");
288
+ const sourceImageFormat = getReviewFigmaImageFormatFromMimeType(sourceMimeType) ?? (renderFormat === "jpg" ? "jpg" : "png");
289
+ const sourceData = new Uint8Array(await response.arrayBuffer());
290
+ const transformed = transformAsset ? await transformAsset({
291
+ data: sourceData,
292
+ imageFormat: sourceImageFormat,
293
+ mimeType: sourceMimeType,
294
+ targetFormat: targetImageFormat
295
+ }) : null;
296
+ const imageFormat = transformed?.imageFormat ?? sourceImageFormat;
297
+ const mimeType = normalizeImageMimeType(transformed?.mimeType) ?? getReviewFigmaImageMimeType(imageFormat);
298
+ const data = createBufferFromImageData(transformed?.data ?? sourceData);
299
+ return { data, imageFormat, mimeType };
300
+ }
301
+ async function deleteReviewFigmaImageAsset(assetDir, storageKey) {
302
+ if (!storageKey) return;
303
+ if (!isSafeReviewFigmaAssetStorageKey(storageKey)) return;
304
+ await rm(path.join(assetDir, storageKey), { force: true }).catch(() => null);
305
+ }
306
+ function createBufferFromImageData(data) {
307
+ return data instanceof ArrayBuffer ? Buffer.from(new Uint8Array(data)) : Buffer.from(data);
308
+ }
309
+ function listImagesForTarget(images, target) {
310
+ const targetKey = getReviewFigmaImageTargetKey(target);
311
+ return images.filter((image) => getReviewFigmaImageTargetKey(image.target) === targetKey).sort(compareReviewFigmaImages);
312
+ }
313
+ function reorderReviewFigmaImages(images, input) {
314
+ const targetKey = getReviewFigmaImageTargetKey(input.target);
315
+ const orderById = new Map(input.imageIds.map((id, index) => [id, index]));
316
+ const targetImages = listImagesForTarget(images, input.target);
317
+ const nextTargetImages = targetImages.map((image) => ({
318
+ ...image,
319
+ order: orderById.get(image.id) ?? input.imageIds.length + image.order,
320
+ updatedAt: orderById.has(image.id) ? (/* @__PURE__ */ new Date()).toISOString() : image.updatedAt
321
+ })).sort(compareReviewFigmaImages);
322
+ const nextTargetImageById = new Map(
323
+ nextTargetImages.map((image, index) => [image.id, { ...image, order: index }])
324
+ );
325
+ const allImages = images.map(
326
+ (image) => getReviewFigmaImageTargetKey(image.target) === targetKey ? nextTargetImageById.get(image.id) ?? image : image
327
+ );
328
+ return {
329
+ allImages,
330
+ targetImages: listImagesForTarget(allImages, input.target)
331
+ };
332
+ }
333
+ function updateReviewFigmaImage(images, id, patch) {
334
+ const index = images.findIndex((image) => image.id === id);
335
+ if (index < 0) return null;
336
+ const nextImage = {
337
+ ...images[index],
338
+ label: patch.label === void 0 ? images[index].label : normalizeOptionalText(patch.label),
339
+ order: typeof patch.order === "number" && Number.isFinite(patch.order) ? patch.order : images[index].order,
340
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
341
+ };
342
+ const nextImages = [...images];
343
+ nextImages[index] = nextImage;
344
+ return { image: nextImage, images: nextImages };
345
+ }
346
+ async function readReviewFigmaImageStoreFile(dataFile) {
347
+ try {
348
+ const raw = await readFile(dataFile, "utf8");
349
+ const parsed = JSON.parse(raw);
350
+ return {
351
+ version: 1,
352
+ images: Array.isArray(parsed.images) ? parsed.images.flatMap((image) => isReviewFigmaImage(image) ? [image] : []) : []
353
+ };
354
+ } catch (error) {
355
+ if (isNodeError(error) && error.code === "ENOENT") {
356
+ return { version: 1, images: [] };
357
+ }
358
+ throw error;
359
+ }
360
+ }
361
+ async function writeReviewFigmaImageStoreFile(dataFile, data) {
362
+ await mkdir(path.dirname(dataFile), { recursive: true });
363
+ await writeFile(
364
+ dataFile,
365
+ `${JSON.stringify({ version: 1, images: data.images }, null, 2)}
366
+ `,
367
+ "utf8"
368
+ );
369
+ }
370
+ function getNextImageOrder(images, target) {
371
+ const targetImages = listImagesForTarget(images, target);
372
+ return targetImages.length ? Math.max(...targetImages.map((image) => image.order)) + 1 : 0;
373
+ }
374
+ function compareReviewFigmaImages(a, b) {
375
+ return a.order - b.order || a.createdAt.localeCompare(b.createdAt);
376
+ }
377
+ function createReviewFigmaImageId() {
378
+ return `figma_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
379
+ }
380
+ function isReviewFigmaImage(value) {
381
+ if (!value || typeof value !== "object") return false;
382
+ const image = value;
383
+ return typeof image.id === "string" && typeof image.projectId === "string" && typeof image.figmaUrl === "string" && typeof image.fileKey === "string" && typeof image.nodeId === "string" && typeof image.imageUrl === "string" && typeof image.order === "number" && typeof image.createdAt === "string" && typeof image.updatedAt === "string";
384
+ }
385
+ function isNodeError(error) {
386
+ if (!error || typeof error !== "object") return false;
387
+ return "code" in error;
388
+ }
389
+ function normalizeOptionalText(value) {
390
+ if (typeof value !== "string") return void 0;
391
+ return value.trim() || void 0;
392
+ }
393
+
394
+ // src/vite/figma-image-store.server.ts
395
+ async function handleReviewFigmaImageStoreRequest({
396
+ dataFile,
397
+ assetDir,
398
+ assetEndpoint,
399
+ endpoint,
400
+ method,
401
+ options,
402
+ pathname,
403
+ requestUrl,
404
+ body,
405
+ env,
406
+ requestToken
407
+ }) {
408
+ if (method === "OPTIONS") return { status: 204, body: null };
409
+ if ((method === "GET" || method === "POST") && pathname === `${endpoint}/snapshot`) {
410
+ const input = parseReleaseSnapshotInput(body, requestUrl, options.projectId);
411
+ if (!input) return jsonError(400, "valid snapshot input is required.");
412
+ if (options.projectId && input.projectId !== options.projectId) {
413
+ return jsonError(403, "snapshot project is not allowed.");
414
+ }
415
+ if (input.targets.some((target) => !isAllowedProjectTarget(target, options.projectId))) {
416
+ return jsonError(403, "snapshot target project is not allowed.");
417
+ }
418
+ const data = await readReviewFigmaImageStoreFile(dataFile);
419
+ return {
420
+ status: 200,
421
+ body: createReviewFigmaReleaseSnapshot({
422
+ images: data.images,
423
+ projectId: input.projectId,
424
+ releaseId: input.releaseId,
425
+ label: input.label,
426
+ targets: input.targets.length > 0 ? input.targets : void 0
427
+ })
428
+ };
429
+ }
430
+ if (method === "GET" && pathname === endpoint) {
431
+ const target = parseTargetParam(requestUrl.searchParams.get("target"));
432
+ if (!target) return jsonError(400, "target query is required.");
433
+ if (!isAllowedProjectTarget(target, options.projectId)) {
434
+ return { status: 200, body: [] };
435
+ }
436
+ const data = await readReviewFigmaImageStoreFile(dataFile);
437
+ return { status: 200, body: listImagesForTarget(data.images, target) };
438
+ }
439
+ if (method === "POST" && pathname === endpoint) {
440
+ const input = parseAddImageInput(body);
441
+ if (!input) return jsonError(400, "valid add image input is required.");
442
+ if (!isAllowedProjectTarget(input.target, options.projectId)) {
443
+ return jsonError(403, "target project is not allowed.");
444
+ }
445
+ const data = await readReviewFigmaImageStoreFile(dataFile);
446
+ const image = await createReviewFigmaImage({
447
+ assetDir,
448
+ assetEndpoint,
449
+ currentImages: data.images,
450
+ env,
451
+ input,
452
+ options,
453
+ requestToken
454
+ });
455
+ data.images = [image, ...data.images];
456
+ await writeReviewFigmaImageStoreFile(dataFile, data);
457
+ return { status: 201, body: image };
458
+ }
459
+ if (method === "PATCH" && pathname === `${endpoint}/reorder`) {
460
+ const input = parseReorderImagesInput(body);
461
+ if (!input) return jsonError(400, "valid reorder input is required.");
462
+ if (!isAllowedProjectTarget(input.target, options.projectId)) {
463
+ return jsonError(403, "target project is not allowed.");
464
+ }
465
+ const data = await readReviewFigmaImageStoreFile(dataFile);
466
+ const images = reorderReviewFigmaImages(data.images, input);
467
+ data.images = images.allImages;
468
+ await writeReviewFigmaImageStoreFile(dataFile, data);
469
+ return { status: 200, body: images.targetImages };
470
+ }
471
+ const id = getEndpointItemId(pathname, endpoint);
472
+ if (id && method === "PATCH") {
473
+ const patch = parseUpdateImageInput(body);
474
+ if (!patch) return jsonError(400, "valid update patch is required.");
475
+ const data = await readReviewFigmaImageStoreFile(dataFile);
476
+ const result = updateReviewFigmaImage(data.images, id, patch);
477
+ if (!result) return jsonError(404, `Figma image not found: ${id}`);
478
+ if (!isAllowedProjectTarget(result.image.target, options.projectId)) {
479
+ return jsonError(403, "target project is not allowed.");
480
+ }
481
+ data.images = result.images;
482
+ await writeReviewFigmaImageStoreFile(dataFile, data);
483
+ return { status: 200, body: result.image };
484
+ }
485
+ if (id && method === "DELETE") {
486
+ const data = await readReviewFigmaImageStoreFile(dataFile);
487
+ const image = data.images.find((item) => item.id === id);
488
+ if (!image) return jsonError(404, `Figma image not found: ${id}`);
489
+ if (!isAllowedProjectTarget(image.target, options.projectId)) {
490
+ return jsonError(403, "target project is not allowed.");
491
+ }
492
+ data.images = data.images.filter((item) => item.id !== id);
493
+ await writeReviewFigmaImageStoreFile(dataFile, data);
494
+ await deleteReviewFigmaImageAsset(assetDir, image.storageKey);
495
+ return { status: 200, body: { ok: true } };
496
+ }
497
+ return jsonError(405, "method not allowed.");
498
+ }
499
+ async function readJsonRequestBody(req) {
500
+ if (req.method === "GET" || req.method === "DELETE") return null;
501
+ const chunks = [];
502
+ for await (const chunk of req) {
503
+ chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
504
+ }
505
+ const raw = Buffer.concat(chunks).toString("utf8").trim();
506
+ if (!raw) return null;
507
+ return JSON.parse(raw);
508
+ }
509
+ function sendJson(res, status, body) {
510
+ res.statusCode = status;
511
+ res.setHeader("Content-Type", "application/json; charset=utf-8");
512
+ if (status === 204) {
513
+ res.end();
514
+ return;
515
+ }
516
+ res.end(JSON.stringify(body ?? null));
517
+ }
518
+ async function sendReviewFigmaAsset(res, assetDir, assetEndpoint, pathname) {
519
+ const storageKey = getReviewFigmaAssetStorageKeyFromPathname(pathname, assetEndpoint);
520
+ if (!storageKey) {
521
+ sendPlainText(res, 400, "Invalid Figma image asset path.");
522
+ return;
523
+ }
524
+ try {
525
+ const data = await readFile2(path2.join(assetDir, storageKey));
526
+ res.statusCode = 200;
527
+ res.setHeader("Content-Type", getReviewFigmaAssetMimeType(storageKey));
528
+ res.setHeader("Cache-Control", "private, max-age=31536000, immutable");
529
+ res.end(data);
530
+ } catch (error) {
531
+ if (isNodeError(error) && error.code === "ENOENT") {
532
+ sendPlainText(res, 404, "Figma image asset not found.");
533
+ return;
534
+ }
535
+ sendPlainText(
536
+ res,
537
+ 500,
538
+ error instanceof Error ? error.message : "Figma image asset request failed."
539
+ );
540
+ }
541
+ }
542
+ function sendPlainText(res, status, body) {
543
+ res.statusCode = status;
544
+ res.setHeader("Content-Type", "text/plain; charset=utf-8");
545
+ res.end(body);
546
+ }
547
+ function parseTargetParam(value) {
548
+ if (!value) return null;
549
+ try {
550
+ return parseReviewFigmaImageTarget(JSON.parse(value));
551
+ } catch {
552
+ return null;
553
+ }
554
+ }
555
+ function parseAddImageInput(value) {
556
+ if (!value || typeof value !== "object") return null;
557
+ const input = value;
558
+ const target = parseReviewFigmaImageTarget(input.target);
559
+ if (!target || typeof input.figmaUrl !== "string") return null;
560
+ return {
561
+ target,
562
+ figmaUrl: input.figmaUrl,
563
+ label: typeof input.label === "string" ? input.label : void 0,
564
+ order: typeof input.order === "number" ? input.order : void 0,
565
+ imageFormat: parseReviewFigmaImageFormat(input.imageFormat),
566
+ asset: parseAddImageAssetInput(input.asset)
567
+ };
568
+ }
569
+ function parseAddImageAssetInput(value) {
570
+ if (!value || typeof value !== "object") return void 0;
571
+ const input = value;
572
+ const imageFormat = parseReviewFigmaImageFormat(input.imageFormat);
573
+ if (!imageFormat || typeof input.dataUrl !== "string" || typeof input.mimeType !== "string") {
574
+ return void 0;
575
+ }
576
+ return {
577
+ dataUrl: input.dataUrl,
578
+ imageFormat,
579
+ mimeType: input.mimeType,
580
+ byteSize: typeof input.byteSize === "number" ? input.byteSize : void 0,
581
+ width: typeof input.width === "number" ? input.width : void 0,
582
+ height: typeof input.height === "number" ? input.height : void 0
583
+ };
584
+ }
585
+ function parseUpdateImageInput(value) {
586
+ if (!value || typeof value !== "object") return null;
587
+ const input = value;
588
+ return {
589
+ label: typeof input.label === "string" ? input.label : void 0,
590
+ order: typeof input.order === "number" ? input.order : void 0
591
+ };
592
+ }
593
+ function parseReorderImagesInput(value) {
594
+ if (!value || typeof value !== "object") return null;
595
+ const input = value;
596
+ const target = parseReviewFigmaImageTarget(input.target);
597
+ if (!target || !Array.isArray(input.imageIds)) return null;
598
+ return {
599
+ target,
600
+ imageIds: input.imageIds.filter((id) => typeof id === "string")
601
+ };
602
+ }
603
+ function parseReleaseSnapshotInput(value, requestUrl, fallbackProjectId) {
604
+ const input = value && typeof value === "object" ? value : null;
605
+ const projectId = normalizeOptionalText(
606
+ typeof input?.projectId === "string" ? input.projectId : requestUrl.searchParams.get("projectId") ?? fallbackProjectId
607
+ );
608
+ if (!projectId) return null;
609
+ return {
610
+ projectId,
611
+ releaseId: normalizeOptionalText(
612
+ typeof input?.releaseId === "string" ? input.releaseId : requestUrl.searchParams.get("releaseId")
613
+ ),
614
+ label: normalizeOptionalText(
615
+ typeof input?.label === "string" ? input.label : requestUrl.searchParams.get("label")
616
+ ),
617
+ targets: parseReleaseSnapshotTargets(input?.targets, requestUrl)
618
+ };
619
+ }
620
+ function parseReleaseSnapshotTargets(value, requestUrl) {
621
+ const bodyTargets = Array.isArray(value) ? value.flatMap((target) => {
622
+ const parsed = parseReviewFigmaImageTarget(target);
623
+ return parsed ? [parsed] : [];
624
+ }) : [];
625
+ const queryTargets = requestUrl.searchParams.getAll("target").flatMap((target) => {
626
+ const parsed = parseTargetParam(target);
627
+ return parsed ? [parsed] : [];
628
+ });
629
+ const targetByKey = new Map(
630
+ [...bodyTargets, ...queryTargets].map((target) => [
631
+ getReviewFigmaImageTargetKey(target),
632
+ target
633
+ ])
634
+ );
635
+ return Array.from(targetByKey.values());
636
+ }
637
+ function parseReviewFigmaImageTarget(value) {
638
+ if (!value || typeof value !== "object") return null;
639
+ const target = value;
640
+ if (target.type === "route") {
641
+ if (typeof target.projectId !== "string" || typeof target.pageUrl !== "string") {
642
+ return null;
643
+ }
644
+ return {
645
+ type: "route",
646
+ projectId: target.projectId,
647
+ pageUrl: target.pageUrl,
648
+ viewport: target.viewport && typeof target.viewport === "object" ? target.viewport : void 0,
649
+ slot: typeof target.slot === "string" ? target.slot : void 0
650
+ };
651
+ }
652
+ if (target.type === "figma-node") {
653
+ if (typeof target.projectId !== "string" || typeof target.fileKey !== "string" || typeof target.nodeId !== "string") {
654
+ return null;
655
+ }
656
+ return {
657
+ type: "figma-node",
658
+ projectId: target.projectId,
659
+ fileKey: target.fileKey,
660
+ nodeId: target.nodeId
661
+ };
662
+ }
663
+ return null;
664
+ }
665
+ function normalizeEndpoint(endpoint) {
666
+ const normalized = endpoint.trim().replace(/\/+$/, "");
667
+ return normalized.startsWith("/") ? normalized : `/${normalized}`;
668
+ }
669
+ function getEndpointItemId(pathname, endpoint) {
670
+ if (!pathname.startsWith(`${endpoint}/`)) return null;
671
+ const value = pathname.slice(endpoint.length + 1);
672
+ if (!value || value.includes("/")) return null;
673
+ return decodeURIComponent(value);
674
+ }
675
+ function isAllowedProjectTarget(target, projectId) {
676
+ return !projectId || target.projectId === projectId;
677
+ }
678
+ function jsonError(status, error) {
679
+ return { status, body: { error } };
680
+ }
681
+
682
+ // src/vite/figma-image-store.ts
683
+ var readReviewFigmaServerToken = (options = {}) => readReviewFigmaToken({
684
+ token: options.token,
685
+ env: options.env ?? getServerEnv(),
686
+ envKey: options.envKey ?? DEFAULT_REVIEW_FIGMA_TOKEN_ENV_KEY,
687
+ enabled: options.enabled
688
+ });
689
+ var requireReviewFigmaServerToken = (options = {}) => requireReviewFigmaToken({
690
+ token: options.token,
691
+ env: options.env ?? getServerEnv(),
692
+ envKey: options.envKey ?? DEFAULT_REVIEW_FIGMA_TOKEN_ENV_KEY,
693
+ enabled: options.enabled
694
+ });
695
+ var renderReviewFigmaServerImage = (options) => {
696
+ const { token, env, envKey, enabled, ...renderOptions } = options;
697
+ const explicitToken = typeof token === "string" ? token.trim() : token;
698
+ return renderReviewFigmaImage({
699
+ ...renderOptions,
700
+ token: explicitToken || requireReviewFigmaServerToken({ env, envKey, enabled })
701
+ });
702
+ };
703
+ var reviewFigmaImageStore = (options = {}) => {
704
+ let root = "";
705
+ let dataFile = "";
706
+ let assetDir = "";
707
+ let env = {};
708
+ const enabled = options.enabled ?? true;
709
+ const endpoint = normalizeEndpoint(
710
+ options.endpoint ?? DEFAULT_REVIEW_FIGMA_IMAGE_STORE_ENDPOINT
711
+ );
712
+ const assetEndpoint = normalizeEndpoint(
713
+ options.assetEndpoint ?? `${endpoint}/assets`
714
+ );
715
+ return {
716
+ name: "df-web-review-kit-figma-image-store",
717
+ apply: "serve",
718
+ configResolved(config) {
719
+ root = config.root;
720
+ dataFile = path3.resolve(
721
+ root,
722
+ options.dataFile ?? ".df-review/figma-images.json"
723
+ );
724
+ assetDir = options.assetDir ? path3.resolve(root, options.assetDir) : path3.join(path3.dirname(dataFile), "figma-assets");
725
+ env = {
726
+ ...loadEnv(config.mode, config.envDir, ""),
727
+ ...getServerEnv(),
728
+ ...options.env ?? {}
729
+ };
730
+ },
731
+ configureServer(server) {
732
+ if (!enabled) return;
733
+ server.middlewares.use(async (req, res, next) => {
734
+ const requestUrl = new URL(req.url ?? "/", "http://localhost");
735
+ const pathname = requestUrl.pathname;
736
+ if (pathname.startsWith(`${assetEndpoint}/`)) {
737
+ await sendReviewFigmaAsset(res, assetDir, assetEndpoint, pathname);
738
+ return;
739
+ }
740
+ if (pathname !== endpoint && !pathname.startsWith(`${endpoint}/`)) {
741
+ next();
742
+ return;
743
+ }
744
+ try {
745
+ const response = await handleReviewFigmaImageStoreRequest({
746
+ dataFile,
747
+ assetDir,
748
+ assetEndpoint,
749
+ endpoint,
750
+ options,
751
+ env,
752
+ pathname,
753
+ requestUrl,
754
+ method: req.method ?? "GET",
755
+ body: await readJsonRequestBody(req),
756
+ requestToken: readRequestFigmaToken(req)
757
+ });
758
+ sendJson(res, response.status, response.body);
759
+ } catch (error) {
760
+ sendJson(res, 500, {
761
+ error: error instanceof Error ? error.message : "Figma image store request failed."
762
+ });
763
+ }
764
+ });
765
+ }
766
+ };
767
+ };
768
+ function readRequestFigmaToken(req) {
769
+ const value = req.headers?.["x-figma-token"];
770
+ const token = Array.isArray(value) ? value[0] : value;
771
+ return typeof token === "string" ? token.trim() || null : null;
772
+ }
773
+ function getServerEnv() {
774
+ const runtime = globalThis;
775
+ return runtime.process?.env ?? {};
776
+ }
777
+
1
778
  // src/vite.ts
2
779
  var VIRTUAL_JSX_DEV_RUNTIME_ID = "\0@designfever/web-review-kit/source-locator/jsx-dev-runtime";
780
+ var REVIEW_SOURCE_ENV_DEFINE_KEYS = [
781
+ ["__DF_WRK_REVIEW_SOURCE_ROOT__", "VITE_REVIEW_SOURCE_ROOT"],
782
+ ["__DF_WRK_REVIEW_SOURCE_EDITOR__", "VITE_REVIEW_SOURCE_EDITOR"],
783
+ [
784
+ "__DF_WRK_REVIEW_SOURCE_URL_TEMPLATE__",
785
+ "VITE_REVIEW_SOURCE_URL_TEMPLATE"
786
+ ]
787
+ ];
788
+ var createReviewSourceEnvReplacements = (env = {}) => {
789
+ return Object.fromEntries(
790
+ REVIEW_SOURCE_ENV_DEFINE_KEYS.map(([defineKey, envKey]) => [
791
+ defineKey,
792
+ JSON.stringify(env[envKey] ?? "")
793
+ ])
794
+ );
795
+ };
796
+ var injectReviewSourceEnv = (code, replacements) => {
797
+ let nextCode = code;
798
+ for (const [defineKey, value] of Object.entries(replacements)) {
799
+ nextCode = nextCode.split(`typeof ${defineKey}`).join(`typeof ${value}`).split(`: ${defineKey}`).join(`: ${value}`);
800
+ }
801
+ return nextCode === code ? null : nextCode;
802
+ };
3
803
  var reviewSourceLocator = (options = {}) => {
4
804
  let runtimeOptions = createRuntimeOptions(options);
805
+ let sourceEnvReplacements = createReviewSourceEnvReplacements();
5
806
  return {
6
807
  name: "df-web-review-kit-source-locator",
7
808
  enforce: "pre",
8
809
  configResolved(config) {
9
810
  runtimeOptions = createRuntimeOptions(options, config);
811
+ sourceEnvReplacements = createReviewSourceEnvReplacements(config.env);
10
812
  },
11
813
  resolveId(id, importer) {
12
814
  if (!runtimeOptions.enabled) return null;
@@ -17,12 +819,17 @@ var reviewSourceLocator = (options = {}) => {
17
819
  load(id) {
18
820
  if (id !== VIRTUAL_JSX_DEV_RUNTIME_ID) return null;
19
821
  return createJsxDevRuntime(runtimeOptions);
822
+ },
823
+ transform(code) {
824
+ const injectedCode = injectReviewSourceEnv(code, sourceEnvReplacements);
825
+ return injectedCode ? { code: injectedCode, map: null } : null;
20
826
  }
21
827
  };
22
828
  };
23
829
  var reviewDataLocator = (options = {}) => {
24
830
  let root = normalizePath(options.root ?? "");
25
831
  let enabled = options.enabled ?? false;
832
+ let sourceEnvReplacements = createReviewSourceEnvReplacements();
26
833
  const include = (options.include ?? []).map(createRuntimeMatcher);
27
834
  const exclude = (options.exclude ?? ["node_modules", "dist"]).map(
28
835
  createRuntimeMatcher
@@ -37,26 +844,34 @@ var reviewDataLocator = (options = {}) => {
37
844
  configResolved(config) {
38
845
  root = normalizePath(options.root ?? config.root ?? "");
39
846
  enabled = options.enabled ?? config.command === "serve";
847
+ sourceEnvReplacements = createReviewSourceEnvReplacements(config.env);
40
848
  },
41
849
  transform(code, id) {
850
+ const envInjectedCode = injectReviewSourceEnv(
851
+ code,
852
+ sourceEnvReplacements
853
+ );
854
+ const inputCode = envInjectedCode ?? code;
42
855
  if (!enabled) return null;
43
856
  const file = normalizePath(id.split("?")[0]);
44
857
  const relativeFile = root && file.startsWith(root + "/") ? file.slice(root.length + 1) : file;
45
858
  if (include.length > 0 && !include.some((m) => matchesPath(m, file, relativeFile)))
46
- return null;
47
- if (exclude.some((m) => matchesPath(m, file, relativeFile))) return null;
859
+ return envInjectedCode ? { code: envInjectedCode, map: null } : null;
860
+ if (exclude.some((m) => matchesPath(m, file, relativeFile))) {
861
+ return envInjectedCode ? { code: envInjectedCode, map: null } : null;
862
+ }
48
863
  const sourceFile = (options.filePath ?? "relative") === "absolute" ? file : relativeFile;
49
864
  const regex = new RegExp(componentSource, "g");
50
865
  let changed = false;
51
- const out = code.replace(
866
+ const out = inputCode.replace(
52
867
  regex,
53
868
  (_match, pre, comp, quote, name, offset) => {
54
- const line = code.slice(0, offset + pre.length).split("\n").length;
869
+ const line = inputCode.slice(0, offset + pre.length).split("\n").length;
55
870
  changed = true;
56
871
  return `${pre}${JSON.stringify(fileKey)}: ${JSON.stringify(sourceFile)}, ${JSON.stringify(lineKey)}: ${line}, ${comp}${quote}${name}${quote}`;
57
872
  }
58
873
  );
59
- return changed ? { code: out, map: null } : null;
874
+ return changed || envInjectedCode ? { code: out, map: null } : null;
60
875
  }
61
876
  };
62
877
  };
@@ -204,7 +1019,16 @@ function normalizePath(value) {
204
1019
  `;
205
1020
  }
206
1021
  export {
1022
+ collectReviewFigmaReleaseSnapshot,
1023
+ createReviewFigmaImageApiUrl,
1024
+ createReviewFigmaImagesSnapshot,
1025
+ createReviewFigmaReleaseSnapshot,
1026
+ readReviewFigmaServerToken,
1027
+ renderReviewFigmaImage,
1028
+ renderReviewFigmaServerImage,
1029
+ requireReviewFigmaServerToken,
207
1030
  reviewDataLocator,
1031
+ reviewFigmaImageStore,
208
1032
  reviewSourceLocator
209
1033
  };
210
1034
  //# sourceMappingURL=vite.js.map