@agent-native/core 0.84.30 → 0.84.34

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 (65) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +24 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/client/session-replay.ts +5 -0
  5. package/corpus/core/src/client/use-action.ts +5 -1
  6. package/corpus/core/src/server/ssr-handler.ts +12 -0
  7. package/corpus/core/src/vite/action-types-plugin.ts +6 -2
  8. package/corpus/core/src/vite/client.ts +152 -17
  9. package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +13 -1
  10. package/corpus/templates/analytics/changelog/2026-07-01-session-replays-keep-inlined-css-without-live-resource-loa.md +6 -0
  11. package/corpus/templates/design/AGENTS.md +18 -0
  12. package/corpus/templates/design/actions/apply-source-edit.ts +87 -0
  13. package/corpus/templates/design/actions/import-design-source.ts +158 -0
  14. package/corpus/templates/design/actions/list-source-files.ts +52 -0
  15. package/corpus/templates/design/actions/navigate.ts +4 -2
  16. package/corpus/templates/design/actions/preview-source-edit.ts +85 -0
  17. package/corpus/templates/design/actions/read-source-file.ts +55 -0
  18. package/corpus/templates/design/actions/resolve-selection-source.ts +101 -0
  19. package/corpus/templates/design/actions/view-screen.ts +43 -1
  20. package/corpus/templates/design/app/components/design/CodeWorkbenchHost.tsx +630 -0
  21. package/corpus/templates/design/app/components/design/DesignImportPanel.tsx +519 -0
  22. package/corpus/templates/design/app/hooks/use-navigation-state.ts +32 -6
  23. package/corpus/templates/design/app/i18n/zh-TW.ts +40 -0
  24. package/corpus/templates/design/app/i18n-data.ts +508 -0
  25. package/corpus/templates/design/app/pages/DesignEditor.tsx +171 -9
  26. package/corpus/templates/design/changelog/2026-07-01-design-can-import-figma-paste-fig-files-and-standalone-html.md +6 -0
  27. package/corpus/templates/design/changelog/2026-07-01-design-code-workspace.md +6 -0
  28. package/corpus/templates/design/package.json +3 -0
  29. package/corpus/templates/design/server/handlers/import-design-file.ts +168 -0
  30. package/corpus/templates/design/server/lib/figma-import/clipboard.ts +122 -0
  31. package/corpus/templates/design/server/lib/figma-import/decode.ts +455 -0
  32. package/corpus/templates/design/server/lib/figma-import/processor.ts +116 -0
  33. package/corpus/templates/design/server/lib/figma-import/render-html.ts +397 -0
  34. package/corpus/templates/design/server/lib/figma-import/types.ts +60 -0
  35. package/corpus/templates/design/server/lib/import-design-files.ts +314 -0
  36. package/corpus/templates/design/server/routes/api/import-design-file.post.ts +1 -0
  37. package/corpus/templates/design/server/source-workspace.ts +215 -0
  38. package/corpus/templates/design/shared/design-source-capabilities.ts +5 -5
  39. package/corpus/templates/design/shared/source-workspace.ts +149 -0
  40. package/dist/client/session-replay.d.ts +1 -0
  41. package/dist/client/session-replay.d.ts.map +1 -1
  42. package/dist/client/session-replay.js +2 -0
  43. package/dist/client/session-replay.js.map +1 -1
  44. package/dist/client/use-action.d.ts +5 -1
  45. package/dist/client/use-action.d.ts.map +1 -1
  46. package/dist/client/use-action.js.map +1 -1
  47. package/dist/collab/awareness.d.ts +2 -2
  48. package/dist/collab/awareness.d.ts.map +1 -1
  49. package/dist/collab/routes.d.ts +1 -1
  50. package/dist/notifications/routes.d.ts +3 -3
  51. package/dist/observability/routes.d.ts +3 -3
  52. package/dist/progress/routes.d.ts +1 -1
  53. package/dist/resources/handlers.d.ts +3 -3
  54. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  55. package/dist/server/ssr-handler.d.ts.map +1 -1
  56. package/dist/server/ssr-handler.js +7 -0
  57. package/dist/server/ssr-handler.js.map +1 -1
  58. package/dist/server/transcribe-voice.d.ts +1 -1
  59. package/dist/vite/action-types-plugin.d.ts.map +1 -1
  60. package/dist/vite/action-types-plugin.js +6 -2
  61. package/dist/vite/action-types-plugin.js.map +1 -1
  62. package/dist/vite/client.d.ts.map +1 -1
  63. package/dist/vite/client.js +132 -11
  64. package/dist/vite/client.js.map +1 -1
  65. package/package.json +1 -1
@@ -0,0 +1,455 @@
1
+ import * as crypto from "node:crypto";
2
+ import * as zlib from "node:zlib";
3
+
4
+ import { Decompress as ZstdDecompress } from "fzstd";
5
+ import {
6
+ ByteBuffer,
7
+ compileSchema,
8
+ decodeBinarySchema,
9
+ type Schema,
10
+ } from "kiwi-schema";
11
+ import * as pako from "pako";
12
+
13
+ import type { DecodedFig, DecodedFigImage } from "./types.js";
14
+
15
+ const MAX_DECOMPRESSED_BYTES = 512 * 1024 * 1024;
16
+ const MAX_TOTAL_DECOMPRESSED_BYTES = 512 * 1024 * 1024;
17
+ const ZSTD_MAGIC = Buffer.from([0x28, 0xb5, 0x2f, 0xfd]);
18
+ const FIG_KIWI_MAGIC = Buffer.from("fig-kiwi", "utf8");
19
+ const ZIP_MAGIC = Buffer.from([0x50, 0x4b, 0x03, 0x04]);
20
+ const PNG_MAGIC = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
21
+ const JPEG_MAGIC = Buffer.from([0xff, 0xd8, 0xff]);
22
+
23
+ interface DecodedFigKiwi {
24
+ version: number;
25
+ schema: Buffer;
26
+ document: Buffer;
27
+ blobs: Buffer[];
28
+ }
29
+
30
+ interface ZipEntry {
31
+ name: string;
32
+ data: Buffer;
33
+ }
34
+
35
+ function sha1(buf: Buffer): string {
36
+ return crypto.createHash("sha1").update(buf).digest("hex");
37
+ }
38
+
39
+ function assertWithinTotalBudget(
40
+ currentBytes: number,
41
+ additionalBytes: number,
42
+ ): number {
43
+ const nextBytes = currentBytes + additionalBytes;
44
+ if (nextBytes > MAX_TOTAL_DECOMPRESSED_BYTES) {
45
+ throw new Error(
46
+ `Total decompressed Figma data exceeds size limit (${nextBytes} > ${MAX_TOTAL_DECOMPRESSED_BYTES})`,
47
+ );
48
+ }
49
+ return nextBytes;
50
+ }
51
+
52
+ export function isFigKiwiBuffer(file: Buffer): boolean {
53
+ return (
54
+ file.length >= FIG_KIWI_MAGIC.length &&
55
+ file.subarray(0, FIG_KIWI_MAGIC.length).equals(FIG_KIWI_MAGIC)
56
+ );
57
+ }
58
+
59
+ export function isZipBuffer(file: Buffer): boolean {
60
+ return file.length >= 4 && file.subarray(0, 4).equals(ZIP_MAGIC);
61
+ }
62
+
63
+ function detectImageExt(buf: Buffer): DecodedFigImage["ext"] | null {
64
+ if (buf.length >= 8 && buf.subarray(0, 8).equals(PNG_MAGIC)) return "png";
65
+ if (buf.length >= 3 && buf.subarray(0, 3).equals(JPEG_MAGIC)) return "jpg";
66
+ if (
67
+ buf.length >= 12 &&
68
+ buf.subarray(0, 4).toString("ascii") === "RIFF" &&
69
+ buf.subarray(8, 12).toString("ascii") === "WEBP"
70
+ ) {
71
+ return "webp";
72
+ }
73
+ if (buf.length >= 4 && buf.subarray(0, 4).toString("ascii") === "GIF8") {
74
+ return "gif";
75
+ }
76
+ return null;
77
+ }
78
+
79
+ function checkDecompressedSize(
80
+ buf: Buffer,
81
+ maxBytes = MAX_DECOMPRESSED_BYTES,
82
+ ): Buffer {
83
+ if (buf.length > maxBytes) {
84
+ throw new Error(
85
+ `Decompressed chunk exceeds size limit (${buf.length} > ${maxBytes})`,
86
+ );
87
+ }
88
+ return buf;
89
+ }
90
+
91
+ function decompressZstdChunk(buf: Buffer, maxOutputLength: number): Buffer {
92
+ const chunks: Buffer[] = [];
93
+ let totalBytes = 0;
94
+ const decoder = new ZstdDecompress((chunk) => {
95
+ totalBytes += chunk.length;
96
+ if (totalBytes > maxOutputLength) {
97
+ throw new Error(
98
+ `Decompressed chunk exceeds size limit (${totalBytes} > ${maxOutputLength})`,
99
+ );
100
+ }
101
+ chunks.push(Buffer.from(chunk));
102
+ });
103
+ decoder.push(buf, true);
104
+ return Buffer.concat(chunks, totalBytes);
105
+ }
106
+
107
+ function decompressPakoChunk(
108
+ buf: Buffer,
109
+ maxOutputLength: number,
110
+ raw: boolean,
111
+ ): Buffer {
112
+ const chunks: Buffer[] = [];
113
+ let totalBytes = 0;
114
+ const inflator = new pako.Inflate({ raw });
115
+ inflator.onData = (chunk) => {
116
+ totalBytes += chunk.length;
117
+ if (totalBytes > maxOutputLength) {
118
+ throw new Error(
119
+ `Decompressed chunk exceeds size limit (${totalBytes} > ${maxOutputLength})`,
120
+ );
121
+ }
122
+ chunks.push(Buffer.from(chunk));
123
+ };
124
+ inflator.push(buf, true);
125
+ if (inflator.err) {
126
+ throw new Error(inflator.msg || "Pako inflate failed");
127
+ }
128
+ return Buffer.concat(chunks, totalBytes);
129
+ }
130
+
131
+ function decompressChunk(buf: Buffer, remainingBytes: number): Buffer {
132
+ const maxOutputLength = Math.min(MAX_DECOMPRESSED_BYTES, remainingBytes);
133
+ if (maxOutputLength <= 0) {
134
+ throw new Error("Total decompressed Figma data exceeds size limit");
135
+ }
136
+ if (buf.length >= 4 && buf.subarray(0, 4).equals(ZSTD_MAGIC)) {
137
+ try {
138
+ return decompressZstdChunk(buf, maxOutputLength);
139
+ } catch (error) {
140
+ if (error instanceof Error && /size limit/i.test(error.message)) {
141
+ throw error;
142
+ }
143
+ // Some historical files carry misleading chunk headers; try zlib below.
144
+ }
145
+ }
146
+ try {
147
+ return zlib.inflateRawSync(buf, {
148
+ maxOutputLength,
149
+ });
150
+ } catch (error) {
151
+ if (error instanceof RangeError) throw error;
152
+ }
153
+ try {
154
+ return zlib.inflateSync(buf, { maxOutputLength });
155
+ } catch (error) {
156
+ if (error instanceof RangeError) throw error;
157
+ }
158
+ try {
159
+ return decompressPakoChunk(buf, maxOutputLength, true);
160
+ } catch (error) {
161
+ if (error instanceof Error && /size limit/i.test(error.message)) {
162
+ throw error;
163
+ }
164
+ // Fall through.
165
+ }
166
+ try {
167
+ return decompressPakoChunk(buf, maxOutputLength, false);
168
+ } catch (error) {
169
+ if (error instanceof Error && /size limit/i.test(error.message)) {
170
+ throw error;
171
+ }
172
+ // Fall through.
173
+ }
174
+ throw new Error("Figma chunk could not be decompressed.");
175
+ }
176
+
177
+ export function decodeKiwiContainer(file: Buffer): DecodedFigKiwi {
178
+ if (!isFigKiwiBuffer(file)) {
179
+ throw new Error("Not a fig-kiwi file (missing magic header)");
180
+ }
181
+ if (file.length < 12) {
182
+ throw new Error(
183
+ "Truncated kiwi header (file too short to contain version)",
184
+ );
185
+ }
186
+ const version = file.readUInt32LE(8);
187
+ let offset = 12;
188
+ let totalDecompressedBytes = 0;
189
+ const chunks: Buffer[] = [];
190
+ while (offset < file.length) {
191
+ if (offset + 4 > file.length) {
192
+ throw new Error(`Truncated chunk header at offset ${offset}`);
193
+ }
194
+ const length = file.readUInt32LE(offset);
195
+ offset += 4;
196
+ if (offset + length > file.length) {
197
+ throw new Error(
198
+ `Chunk extends past end of file (offset=${offset}, length=${length}, total=${file.length})`,
199
+ );
200
+ }
201
+ const compressed = file.subarray(offset, offset + length);
202
+ offset += length;
203
+ const chunk = decompressChunk(
204
+ compressed,
205
+ MAX_TOTAL_DECOMPRESSED_BYTES - totalDecompressedBytes,
206
+ );
207
+ totalDecompressedBytes = assertWithinTotalBudget(
208
+ totalDecompressedBytes,
209
+ chunk.length,
210
+ );
211
+ chunks.push(chunk);
212
+ }
213
+ if (chunks.length < 2) {
214
+ throw new Error(
215
+ `Expected at least 2 chunks (schema + document), got ${chunks.length}`,
216
+ );
217
+ }
218
+ return {
219
+ version,
220
+ schema: chunks[0]!,
221
+ document: chunks[1]!,
222
+ blobs: chunks.slice(2),
223
+ };
224
+ }
225
+
226
+ function readZip(file: Buffer): ZipEntry[] {
227
+ const eocdSignature = 0x06054b50;
228
+ const maxScan = Math.min(file.length, 65557);
229
+ let eocdOffset = -1;
230
+ for (let i = file.length - 22; i >= file.length - maxScan && i >= 0; i--) {
231
+ if (file.readUInt32LE(i) === eocdSignature) {
232
+ eocdOffset = i;
233
+ break;
234
+ }
235
+ }
236
+ if (eocdOffset < 0) throw new Error("Zip EOCD record not found");
237
+
238
+ const totalEntries = file.readUInt16LE(eocdOffset + 10);
239
+ const cdOffset = file.readUInt32LE(eocdOffset + 16);
240
+ if (cdOffset > file.length) {
241
+ throw new Error(`Central directory offset ${cdOffset} exceeds file length`);
242
+ }
243
+
244
+ const entries: ZipEntry[] = [];
245
+ let totalDecompressedBytes = 0;
246
+ let cursor = cdOffset;
247
+ for (let i = 0; i < totalEntries; i += 1) {
248
+ if (cursor + 46 > file.length) {
249
+ throw new Error(`Truncated central directory entry at offset ${cursor}`);
250
+ }
251
+ if (file.readUInt32LE(cursor) !== 0x02014b50) {
252
+ throw new Error(`Bad central directory entry signature at ${cursor}`);
253
+ }
254
+ const compressionMethod = file.readUInt16LE(cursor + 10);
255
+ const compressedSize = file.readUInt32LE(cursor + 20);
256
+ const uncompressedSize = file.readUInt32LE(cursor + 24);
257
+ const nameLen = file.readUInt16LE(cursor + 28);
258
+ const extraLen = file.readUInt16LE(cursor + 30);
259
+ const commentLen = file.readUInt16LE(cursor + 32);
260
+ const localHeaderOffset = file.readUInt32LE(cursor + 42);
261
+ const name = file
262
+ .subarray(cursor + 46, cursor + 46 + nameLen)
263
+ .toString("utf8");
264
+ cursor += 46 + nameLen + extraLen + commentLen;
265
+
266
+ const lh = localHeaderOffset;
267
+ if (lh + 30 > file.length) {
268
+ throw new Error(`Local header offset ${lh} out of bounds`);
269
+ }
270
+ if (file.readUInt32LE(lh) !== 0x04034b50) {
271
+ throw new Error(`Bad local file header signature at ${lh}`);
272
+ }
273
+ const lhNameLen = file.readUInt16LE(lh + 26);
274
+ const lhExtraLen = file.readUInt16LE(lh + 28);
275
+ const dataStart = lh + 30 + lhNameLen + lhExtraLen;
276
+ if (dataStart + compressedSize > file.length) {
277
+ throw new Error(`Compressed data for "${name}" extends past end of file`);
278
+ }
279
+ const compressed = file.subarray(dataStart, dataStart + compressedSize);
280
+
281
+ let data: Buffer;
282
+ if (compressionMethod === 0) {
283
+ totalDecompressedBytes = assertWithinTotalBudget(
284
+ totalDecompressedBytes,
285
+ compressed.length,
286
+ );
287
+ data = Buffer.from(compressed);
288
+ } else if (compressionMethod === 8) {
289
+ data = zlib.inflateRawSync(compressed, {
290
+ maxOutputLength: Math.min(
291
+ MAX_DECOMPRESSED_BYTES,
292
+ MAX_TOTAL_DECOMPRESSED_BYTES - totalDecompressedBytes,
293
+ ),
294
+ });
295
+ totalDecompressedBytes = assertWithinTotalBudget(
296
+ totalDecompressedBytes,
297
+ data.length,
298
+ );
299
+ } else {
300
+ throw new Error(
301
+ `Unsupported zip compression method ${compressionMethod} for entry "${name}"`,
302
+ );
303
+ }
304
+ if (uncompressedSize !== 0 && data.length !== uncompressedSize) {
305
+ throw new Error(
306
+ `Size mismatch for "${name}": expected ${uncompressedSize}, got ${data.length}`,
307
+ );
308
+ }
309
+ if (!name.endsWith("/")) entries.push({ name, data });
310
+ }
311
+ return entries;
312
+ }
313
+
314
+ function sanitizeForJson(value: unknown): unknown {
315
+ if (value instanceof Uint8Array) return Buffer.from(value).toString("hex");
316
+ if (typeof value === "bigint") return value.toString();
317
+ if (Array.isArray(value)) return value.map(sanitizeForJson);
318
+ if (value !== null && typeof value === "object") {
319
+ const out: Record<string, unknown> = {};
320
+ for (const [key, child] of Object.entries(value)) {
321
+ out[key] = sanitizeForJson(child);
322
+ }
323
+ return out;
324
+ }
325
+ return value;
326
+ }
327
+
328
+ function decodeKiwiDocument(
329
+ schemaBuf: Buffer,
330
+ documentBuf: Buffer,
331
+ ): unknown | null {
332
+ let schema: Schema;
333
+ try {
334
+ schema = decodeBinarySchema(schemaBuf);
335
+ } catch {
336
+ return null;
337
+ }
338
+ const rootMessage =
339
+ schema.definitions.find((definition) => definition.name === "Message")
340
+ ?.name ??
341
+ schema.definitions.find((definition) => definition.kind === "MESSAGE")
342
+ ?.name ??
343
+ null;
344
+ if (!rootMessage) return null;
345
+
346
+ let compiled: Record<string, (bb: ByteBuffer) => unknown>;
347
+ try {
348
+ compiled = compileSchema(schema);
349
+ } catch {
350
+ return null;
351
+ }
352
+ const decoder = compiled[`decode${rootMessage}`];
353
+ if (typeof decoder !== "function") return null;
354
+
355
+ try {
356
+ const view = new Uint8Array(
357
+ documentBuf.buffer,
358
+ documentBuf.byteOffset,
359
+ documentBuf.byteLength,
360
+ );
361
+ return sanitizeForJson(decoder.call(compiled, new ByteBuffer(view)));
362
+ } catch {
363
+ return null;
364
+ }
365
+ }
366
+
367
+ function collectImagesFromBlobs(blobs: Buffer[]): DecodedFigImage[] {
368
+ const seen = new Map<string, DecodedFigImage>();
369
+ for (const blob of blobs) {
370
+ if (blob.length === 0) continue;
371
+ const ext = detectImageExt(blob);
372
+ if (!ext) continue;
373
+ const hash = sha1(blob);
374
+ if (!seen.has(hash)) seen.set(hash, { hash, ext, bytes: blob });
375
+ }
376
+ return Array.from(seen.values());
377
+ }
378
+
379
+ function findThumbnail(documentBuf: Buffer, blobs: Buffer[]): Buffer | null {
380
+ const pngBlobs = blobs
381
+ .filter((blob) => blob.length >= 8 && blob.subarray(0, 8).equals(PNG_MAGIC))
382
+ .sort((a, b) => a.length - b.length);
383
+ if (pngBlobs.length > 0) return pngBlobs[0]!;
384
+
385
+ const idx = documentBuf.indexOf(PNG_MAGIC);
386
+ if (idx >= 0) {
387
+ const iend = Buffer.from([0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82]);
388
+ const end = documentBuf.indexOf(iend, idx);
389
+ if (end > idx) return documentBuf.subarray(idx, end + iend.length);
390
+ }
391
+ return null;
392
+ }
393
+
394
+ export function decodeFig(file: Buffer): DecodedFig {
395
+ if (isZipBuffer(file)) {
396
+ const entries = readZip(file);
397
+ const canvasEntry = entries.find((entry) => entry.name === "canvas.fig");
398
+ const imageEntries = entries.filter((entry) =>
399
+ entry.name.startsWith("images/"),
400
+ );
401
+
402
+ let document: unknown = null;
403
+ let version: number | undefined;
404
+ let blobs: Buffer[] = [];
405
+ if (canvasEntry) {
406
+ try {
407
+ const inner = decodeKiwiContainer(canvasEntry.data);
408
+ version = inner.version;
409
+ blobs = inner.blobs;
410
+ document = decodeKiwiDocument(inner.schema, inner.document);
411
+ } catch {
412
+ // Keep document null; caller surfaces a targeted decode error.
413
+ }
414
+ }
415
+
416
+ const images: DecodedFigImage[] = [];
417
+ const seen = new Set<string>();
418
+ for (const entry of imageEntries) {
419
+ const ext = detectImageExt(entry.data);
420
+ if (!ext) continue;
421
+ const hash = sha1(entry.data);
422
+ if (seen.has(hash)) continue;
423
+ seen.add(hash);
424
+ images.push({ hash, ext, bytes: entry.data });
425
+ }
426
+ for (const image of collectImagesFromBlobs(blobs)) {
427
+ if (seen.has(image.hash)) continue;
428
+ seen.add(image.hash);
429
+ images.push(image);
430
+ }
431
+
432
+ const thumbnailEntry = entries.find(
433
+ (entry) => entry.name === "thumbnail.png",
434
+ );
435
+ return {
436
+ format: "zip",
437
+ version,
438
+ document,
439
+ images,
440
+ thumbnail: thumbnailEntry?.data ?? null,
441
+ blobs,
442
+ };
443
+ }
444
+
445
+ const decoded = decodeKiwiContainer(file);
446
+ const document = decodeKiwiDocument(decoded.schema, decoded.document);
447
+ return {
448
+ format: "kiwi",
449
+ version: decoded.version,
450
+ document,
451
+ images: collectImagesFromBlobs(decoded.blobs),
452
+ thumbnail: findThumbnail(decoded.document, decoded.blobs),
453
+ blobs: decoded.blobs,
454
+ };
455
+ }
@@ -0,0 +1,116 @@
1
+ import { decodeFig, isFigKiwiBuffer, isZipBuffer } from "./decode.js";
2
+ import { renderFigmaHtml } from "./render-html.js";
3
+ import type {
4
+ FigmaClipboardMeta,
5
+ FigmaImportResult,
6
+ FigmaImportSelection,
7
+ } from "./types.js";
8
+
9
+ const MAX_INLINE_IMAGE_BYTES = 20 * 1024 * 1024;
10
+ const MAX_GENERATED_HTML_BYTES = 25 * 1024 * 1024;
11
+
12
+ export interface ImportFigmaBufferInput {
13
+ buffer: Buffer;
14
+ filename: string;
15
+ sourceKind: "figma-paste" | "fig-file";
16
+ selection?: FigmaImportSelection;
17
+ meta?: FigmaClipboardMeta | Record<string, unknown> | null;
18
+ }
19
+
20
+ function metaRecord(
21
+ meta: FigmaClipboardMeta | Record<string, unknown> | null | undefined,
22
+ ): Record<string, unknown> {
23
+ if (!meta) return {};
24
+ const raw =
25
+ "raw" in meta && meta.raw && typeof meta.raw === "object"
26
+ ? meta.raw
27
+ : undefined;
28
+ return {
29
+ ...(raw as Record<string, unknown> | undefined),
30
+ ...meta,
31
+ };
32
+ }
33
+
34
+ function mimeForExt(ext: string): string {
35
+ if (ext === "jpg") return "image/jpeg";
36
+ return `image/${ext}`;
37
+ }
38
+
39
+ function imageMapFromDecoded(decoded: ReturnType<typeof decodeFig>): {
40
+ imageMap: Map<string, string>;
41
+ warnings: string[];
42
+ } {
43
+ let totalBytes = 0;
44
+ const imageMap = new Map<string, string>();
45
+ const warnings: string[] = [];
46
+ for (const image of decoded.images) {
47
+ totalBytes += image.bytes.length;
48
+ if (totalBytes > MAX_INLINE_IMAGE_BYTES) {
49
+ throw new Error(
50
+ "The Figma file contains more than 20 MB of inline image assets. Export fewer frames or remove large raster images and try again.",
51
+ );
52
+ }
53
+ imageMap.set(
54
+ image.hash,
55
+ `data:${mimeForExt(image.ext)};base64,${image.bytes.toString("base64")}`,
56
+ );
57
+ }
58
+ if (decoded.images.length === 0) {
59
+ warnings.push(
60
+ "No raster image blobs were found in the Figma payload. Vector and text layers were imported; image fills may be missing.",
61
+ );
62
+ }
63
+ return { imageMap, warnings };
64
+ }
65
+
66
+ export async function importFigmaBuffer(
67
+ input: ImportFigmaBufferInput,
68
+ ): Promise<FigmaImportResult> {
69
+ if (!isFigKiwiBuffer(input.buffer) && !isZipBuffer(input.buffer)) {
70
+ throw new Error(
71
+ "The uploaded file is not a supported .fig or fig-kiwi file.",
72
+ );
73
+ }
74
+
75
+ const decoded = decodeFig(input.buffer);
76
+ if (!decoded.document) {
77
+ throw new Error(
78
+ "The Figma file decoded, but its document structure could not be read.",
79
+ );
80
+ }
81
+
82
+ const { imageMap, warnings } = imageMapFromDecoded(decoded);
83
+ const render = renderFigmaHtml({
84
+ filename: input.filename,
85
+ document: decoded.document,
86
+ imageMap,
87
+ selectionNodeId: input.selection?.nodeId,
88
+ meta: {
89
+ ...metaRecord(input.meta),
90
+ sourceKind: input.sourceKind,
91
+ format: decoded.format,
92
+ version: decoded.version,
93
+ },
94
+ });
95
+ const totalHtmlBytes = render.files.reduce(
96
+ (total, file) => total + Buffer.byteLength(file.content, "utf8"),
97
+ 0,
98
+ );
99
+ if (totalHtmlBytes > MAX_GENERATED_HTML_BYTES) {
100
+ throw new Error(
101
+ "The Figma import generated more than 25 MB of HTML. Export fewer frames or simplify large layers and try again.",
102
+ );
103
+ }
104
+
105
+ return {
106
+ files: render.files,
107
+ warnings: [...warnings, ...render.warnings],
108
+ stats: {
109
+ sourceKind: input.sourceKind,
110
+ format: decoded.format,
111
+ frameCount: render.files.length,
112
+ imageCount: decoded.images.length,
113
+ selectedNodeId: input.selection?.nodeId,
114
+ },
115
+ };
116
+ }