@designfever/web-review-kit 0.5.0 → 0.7.0

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