@design-embed/plugin-figma-html 0.1.0 → 1.0.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 (59) hide show
  1. package/LICENSE +1 -1
  2. package/dist/compilers/compilerUtils.d.mts +11 -0
  3. package/dist/compilers/compilerUtils.mjs +119 -0
  4. package/dist/compilers/htmlCompiler.d.mts +6 -0
  5. package/dist/compilers/htmlCompiler.mjs +31 -0
  6. package/dist/compilers/index.d.mts +11 -0
  7. package/dist/compilers/index.mjs +17 -0
  8. package/dist/compilers/index.test.d.mts +1 -0
  9. package/dist/compilers/index.test.mjs +45 -0
  10. package/dist/compilers/reactCompiler.d.mts +6 -0
  11. package/dist/compilers/reactCompiler.mjs +47 -0
  12. package/dist/compilers/vanjsCompiler.d.mts +6 -0
  13. package/dist/compilers/vanjsCompiler.mjs +47 -0
  14. package/dist/design-embed/src/core/diagnostics/diagnostic.d.mts +16 -0
  15. package/dist/design-embed/src/core/nodes.d.mts +14 -0
  16. package/dist/design-embed/src/core/plugins/pluginApi.d.mts +34 -0
  17. package/dist/external/figmaApi.d.mts +17 -0
  18. package/dist/external/figmaApi.mjs +55 -0
  19. package/dist/external/figmaApi.test.d.mts +1 -0
  20. package/dist/external/figmaApi.test.mjs +101 -0
  21. package/dist/external/imageDownloader.d.mts +17 -0
  22. package/dist/external/imageDownloader.mjs +66 -0
  23. package/dist/external/imageDownloader.test.d.mts +1 -0
  24. package/dist/external/imageDownloader.test.mjs +42 -0
  25. package/dist/index.d.mts +8 -0
  26. package/dist/index.mjs +7 -0
  27. package/dist/plugin.d.mts +16 -0
  28. package/dist/plugin.mjs +43 -0
  29. package/dist/types.d.mts +84 -0
  30. package/package.json +11 -9
  31. package/src/plugin.ts +2 -3
  32. package/dist/compilers/compilerUtils.js +0 -182
  33. package/dist/compilers/htmlCompiler.js +0 -35
  34. package/dist/compilers/index.js +0 -17
  35. package/dist/compilers/reactCompiler.js +0 -58
  36. package/dist/compilers/vanjsCompiler.js +0 -55
  37. package/dist/external/figmaApi.js +0 -74
  38. package/dist/external/imageDownloader.js +0 -82
  39. package/dist/index.js +0 -3
  40. package/dist/plugin.js +0 -56
  41. package/node_modules/@design-embed/config/README.md +0 -5
  42. package/node_modules/@design-embed/config/dist/index.js +0 -283
  43. package/node_modules/@design-embed/config/package.json +0 -19
  44. package/node_modules/@design-embed/config/src/index.ts +0 -518
  45. package/node_modules/@design-embed/core/README.md +0 -5
  46. package/node_modules/@design-embed/core/dist/diagnostics/diagnostic.js +0 -3
  47. package/node_modules/@design-embed/core/dist/diagnostics/jsonDiagnostic.js +0 -35
  48. package/node_modules/@design-embed/core/dist/index.js +0 -351
  49. package/node_modules/@design-embed/core/dist/pipeline/checkMode.js +0 -29
  50. package/node_modules/@design-embed/core/dist/plugins/pluginApi.js +0 -1
  51. package/node_modules/@design-embed/core/dist/plugins/pluginRegistry.js +0 -25
  52. package/node_modules/@design-embed/core/package.json +0 -19
  53. package/node_modules/@design-embed/core/src/diagnostics/diagnostic.ts +0 -18
  54. package/node_modules/@design-embed/core/src/diagnostics/jsonDiagnostic.ts +0 -51
  55. package/node_modules/@design-embed/core/src/index.ts +0 -591
  56. package/node_modules/@design-embed/core/src/pipeline/checkMode.ts +0 -46
  57. package/node_modules/@design-embed/core/src/plugins/pluginApi.ts +0 -78
  58. package/node_modules/@design-embed/core/src/plugins/pluginRegistry.ts +0 -37
  59. /package/dist/{types.js → types.mjs} +0 -0
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 Jin-Woo Lee
3
+ Copyright (c) 2026 Jin Woo Lee
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
6
 
@@ -0,0 +1,11 @@
1
+ import { FigmaNode } from "../types.mjs";
2
+
3
+ //#region packages/plugin-figma-html/src/compilers/compilerUtils.d.ts
4
+ declare function toComponentName(name: string | undefined, fallback?: string): string;
5
+ declare function escapeHtml(value: string | undefined): string;
6
+ declare function escapeJsString(value: string | undefined): string;
7
+ declare function getNodeStyles(node: FigmaNode, parent?: FigmaNode): Record<string, string | number>;
8
+ declare function toReactStyle(styles: Record<string, string | number>): string;
9
+ declare function toCssText(styles: Record<string, string | number>): string;
10
+ //#endregion
11
+ export { escapeHtml, escapeJsString, getNodeStyles, toComponentName, toCssText, toReactStyle };
@@ -0,0 +1,119 @@
1
+ //#region packages/plugin-figma-html/src/compilers/compilerUtils.ts
2
+ function toComponentName(name, fallback = "FigmaExport") {
3
+ const cleaned = (name || "").replace(/[^a-zA-Z0-9가-힣]/g, "");
4
+ if (!cleaned) return fallback;
5
+ return /^[0-9]/.test(cleaned) ? `${fallback}${cleaned}` : cleaned;
6
+ }
7
+ function escapeHtml(value) {
8
+ return (value || "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
9
+ }
10
+ function escapeJsString(value) {
11
+ return JSON.stringify(value || "");
12
+ }
13
+ function getNodeStyles(node, parent) {
14
+ const styles = {};
15
+ const bounds = node.absoluteBoundingBox;
16
+ const parentBounds = parent?.absoluteBoundingBox;
17
+ const parentUsesLayout = Boolean(parent?.layoutMode && parent.layoutMode !== "NONE");
18
+ if (Boolean(parent && (!parentUsesLayout || node.layoutPositioning === "ABSOLUTE")) && bounds && parentBounds) {
19
+ styles.position = "absolute";
20
+ styles.left = `${Math.round((bounds.x || 0) - (parentBounds.x || 0))}px`;
21
+ styles.top = `${Math.round((bounds.y || 0) - (parentBounds.y || 0))}px`;
22
+ } else if (node.children?.length || node.layoutMode === "NONE") styles.position = "relative";
23
+ if (bounds && node.layoutSizingHorizontal !== "HUG") styles.width = `${Math.round(bounds.width || 0)}px`;
24
+ if (bounds && node.layoutSizingVertical !== "HUG") styles.height = `${Math.round(bounds.height || 0)}px`;
25
+ if (node.layoutMode === "HORIZONTAL" || node.layoutMode === "VERTICAL") {
26
+ styles.display = "flex";
27
+ styles.flexDirection = node.layoutMode === "HORIZONTAL" ? "row" : "column";
28
+ styles.boxSizing = "border-box";
29
+ if (node.layoutWrap === "WRAP") styles.flexWrap = "wrap";
30
+ if (node.itemSpacing !== void 0) styles.gap = `${Math.round(node.itemSpacing)}px`;
31
+ if (node.counterAxisSpacing !== void 0 && node.layoutWrap === "WRAP") styles.rowGap = `${Math.round(node.counterAxisSpacing)}px`;
32
+ styles.justifyContent = mapPrimaryAxisAlignment(node.primaryAxisAlignItems);
33
+ styles.alignItems = mapCounterAxisAlignment(node.counterAxisAlignItems);
34
+ } else if (node.layoutMode === "GRID") {
35
+ styles.display = "grid";
36
+ styles.boxSizing = "border-box";
37
+ if (node.gridColumnsSizing) styles.gridTemplateColumns = node.gridColumnsSizing;
38
+ if (node.gridRowsSizing) styles.gridTemplateRows = node.gridRowsSizing;
39
+ if (node.gridColumnGap !== void 0) styles.columnGap = `${Math.round(node.gridColumnGap)}px`;
40
+ if (node.gridRowGap !== void 0) styles.rowGap = `${Math.round(node.gridRowGap)}px`;
41
+ }
42
+ if (node.layoutSizingHorizontal === "FILL" || node.layoutGrow === 1) {
43
+ styles.flex = 1;
44
+ styles.width = "100%";
45
+ }
46
+ if (node.layoutSizingVertical === "FILL") styles.height = "100%";
47
+ if (node.layoutAlign === "STRETCH") styles.alignSelf = "stretch";
48
+ if (node.gridColumnSpan && node.gridColumnSpan > 1) styles.gridColumn = `span ${node.gridColumnSpan}`;
49
+ if (node.gridRowSpan && node.gridRowSpan > 1) styles.gridRow = `span ${node.gridRowSpan}`;
50
+ if (node.paddingTop !== void 0) styles.paddingTop = `${node.paddingTop}px`;
51
+ if (node.paddingBottom !== void 0) styles.paddingBottom = `${node.paddingBottom}px`;
52
+ if (node.paddingLeft !== void 0) styles.paddingLeft = `${node.paddingLeft}px`;
53
+ if (node.paddingRight !== void 0) styles.paddingRight = `${node.paddingRight}px`;
54
+ if (node.clipsContent) styles.overflow = "hidden";
55
+ if (node.opacity !== void 0 && node.opacity !== 1) styles.opacity = node.opacity;
56
+ const fill = node.fills?.find((item) => item.type === "SOLID" && item.color);
57
+ if (fill?.color) if (node.type === "TEXT") styles.color = toRgba(fill.color, fill.opacity ?? 1);
58
+ else styles.backgroundColor = toRgba(fill.color, fill.opacity ?? 1);
59
+ const imageFill = node.fills?.find((item) => item.type === "IMAGE" && (item.imageLocalPath || item.imageUrl));
60
+ const imageSource = imageFill?.imageLocalPath || imageFill?.imageUrl;
61
+ if (imageSource) {
62
+ styles.backgroundImage = `url("${imageSource}")`;
63
+ styles.backgroundRepeat = "no-repeat";
64
+ styles.backgroundPosition = "center";
65
+ styles.backgroundSize = mapImageScaleMode(imageFill.scaleMode);
66
+ }
67
+ const stroke = node.strokes?.find((item) => item.type === "SOLID" && item.color);
68
+ if (stroke?.color && node.strokeWeight) {
69
+ styles.border = `${Math.round(node.strokeWeight)}px solid ${toRgba(stroke.color, stroke.opacity ?? 1)}`;
70
+ styles.boxSizing = "border-box";
71
+ }
72
+ if (node.rectangleCornerRadii?.length === 4) styles.borderRadius = node.rectangleCornerRadii.map((radius) => `${Math.round(radius)}px`).join(" ");
73
+ else if (node.cornerRadius) styles.borderRadius = `${Math.round(node.cornerRadius)}px`;
74
+ if (node.type === "TEXT") {
75
+ const textStyle = node.style || {};
76
+ if (textStyle.fontSize) styles.fontSize = `${textStyle.fontSize}px`;
77
+ if (textStyle.fontWeight) styles.fontWeight = textStyle.fontWeight;
78
+ if (textStyle.fontFamily) styles.fontFamily = `"${textStyle.fontFamily}", sans-serif`;
79
+ if (textStyle.lineHeightPx) styles.lineHeight = `${Math.round(textStyle.lineHeightPx)}px`;
80
+ }
81
+ return styles;
82
+ }
83
+ function toReactStyle(styles) {
84
+ return JSON.stringify(styles).replace(/"([^"]+)":/g, "$1:");
85
+ }
86
+ function toCssText(styles) {
87
+ return Object.entries(styles).map(([key, value]) => `${toKebabCase(key)}: ${value};`).join(" ");
88
+ }
89
+ function toKebabCase(value) {
90
+ return value.replace(/[A-Z]/g, (match) => `-${match.toLowerCase()}`);
91
+ }
92
+ function mapPrimaryAxisAlignment(value) {
93
+ switch (value) {
94
+ case "CENTER": return "center";
95
+ case "MAX": return "flex-end";
96
+ case "SPACE_BETWEEN": return "space-between";
97
+ default: return "flex-start";
98
+ }
99
+ }
100
+ function mapCounterAxisAlignment(value) {
101
+ switch (value) {
102
+ case "CENTER": return "center";
103
+ case "MAX": return "flex-end";
104
+ case "BASELINE": return "baseline";
105
+ default: return "flex-start";
106
+ }
107
+ }
108
+ function mapImageScaleMode(value) {
109
+ switch (value) {
110
+ case "FIT": return "contain";
111
+ case "TILE": return "auto";
112
+ default: return "cover";
113
+ }
114
+ }
115
+ function toRgba(color, opacity) {
116
+ return `rgba(${Math.round(color.r * 255)}, ${Math.round(color.g * 255)}, ${Math.round(color.b * 255)}, ${opacity})`;
117
+ }
118
+ //#endregion
119
+ export { escapeHtml, escapeJsString, getNodeStyles, toComponentName, toCssText, toReactStyle };
@@ -0,0 +1,6 @@
1
+ import { FigmaCompiler } from "../types.mjs";
2
+
3
+ //#region packages/plugin-figma-html/src/compilers/htmlCompiler.d.ts
4
+ declare const compileHtml: FigmaCompiler;
5
+ //#endregion
6
+ export { compileHtml };
@@ -0,0 +1,31 @@
1
+ import { escapeHtml, getNodeStyles, toCssText } from "./compilerUtils.mjs";
2
+ //#region packages/plugin-figma-html/src/compilers/htmlCompiler.ts
3
+ const compileHtml = (node) => [{
4
+ path: "index.html",
5
+ contents: `<!doctype html>
6
+ <html lang="en">
7
+ <head>
8
+ <meta charset="UTF-8" />
9
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
10
+ <title>${escapeHtml(node.name || "Figma Export")}</title>
11
+ </head>
12
+ <body>
13
+ ${walkHtml(node, void 0, 2).trimEnd()}
14
+ </body>
15
+ </html>
16
+ `
17
+ }];
18
+ function walkHtml(node, parent, depth = 0) {
19
+ if (!node || node.visible === false) return "";
20
+ const indent = " ".repeat(depth);
21
+ const childIndent = " ".repeat(depth + 1);
22
+ const name = escapeHtml(node.name || "LayoutBox");
23
+ const style = escapeHtml(toCssText(getNodeStyles(node, parent)));
24
+ if (node.type === "TEXT") return `${indent}<span style="${style}" data-layer="${name}">
25
+ ${childIndent}${escapeHtml(node.characters)}
26
+ ${indent}</span>\n`;
27
+ return `${indent}<div style="${style}" data-layer="${name}">
28
+ ${node.children?.map((child) => walkHtml(child, node, depth + 1)).join("") || ""}${indent}</div>\n`;
29
+ }
30
+ //#endregion
31
+ export { compileHtml };
@@ -0,0 +1,11 @@
1
+ import { CompilerMode, FigmaCompiler } from "../types.mjs";
2
+ import { compileHtml } from "./htmlCompiler.mjs";
3
+ import { compileReact } from "./reactCompiler.mjs";
4
+ import { compileVanjs } from "./vanjsCompiler.mjs";
5
+
6
+ //#region packages/plugin-figma-html/src/compilers/index.d.ts
7
+ declare const compilers: Record<CompilerMode, FigmaCompiler>;
8
+ declare function isCompilerMode(value: string | undefined): value is CompilerMode;
9
+ declare function getCompiler(mode: CompilerMode): FigmaCompiler;
10
+ //#endregion
11
+ export { compileHtml, compileReact, compileVanjs, compilers, getCompiler, isCompilerMode };
@@ -0,0 +1,17 @@
1
+ import { compileHtml } from "./htmlCompiler.mjs";
2
+ import { compileReact } from "./reactCompiler.mjs";
3
+ import { compileVanjs } from "./vanjsCompiler.mjs";
4
+ //#region packages/plugin-figma-html/src/compilers/index.ts
5
+ const compilers = {
6
+ react: compileReact,
7
+ html: compileHtml,
8
+ vanjs: compileVanjs
9
+ };
10
+ function isCompilerMode(value) {
11
+ return Boolean(value && value in compilers);
12
+ }
13
+ function getCompiler(mode) {
14
+ return compilers[mode];
15
+ }
16
+ //#endregion
17
+ export { compileHtml, compileReact, compileVanjs, compilers, getCompiler, isCompilerMode };
@@ -0,0 +1 @@
1
+ export { };
@@ -0,0 +1,45 @@
1
+ import { compileHtml } from "./htmlCompiler.mjs";
2
+ import { compileReact } from "./reactCompiler.mjs";
3
+ import { compileVanjs } from "./vanjsCompiler.mjs";
4
+ import { getCompiler } from "./index.mjs";
5
+ import assert from "node:assert/strict";
6
+ import { describe, test } from "node:test";
7
+ //#region packages/plugin-figma-html/src/compilers/index.test.ts
8
+ const sampleNode = {
9
+ name: "Sample Frame",
10
+ type: "FRAME",
11
+ layoutMode: "VERTICAL",
12
+ absoluteBoundingBox: {
13
+ x: 0,
14
+ y: 0,
15
+ width: 100,
16
+ height: 80
17
+ },
18
+ children: [{
19
+ name: "Title",
20
+ type: "TEXT",
21
+ characters: "Hello",
22
+ absoluteBoundingBox: {
23
+ x: 8,
24
+ y: 8,
25
+ width: 40,
26
+ height: 20
27
+ }
28
+ }]
29
+ };
30
+ describe("compiler registry", () => {
31
+ test("returns compiler functions without external dependencies", () => {
32
+ assert.equal(getCompiler("html"), compileHtml);
33
+ assert.equal(getCompiler("react"), compileReact);
34
+ assert.equal(getCompiler("vanjs"), compileVanjs);
35
+ });
36
+ });
37
+ describe("compilers", () => {
38
+ test("compile a Figma node into generated files", () => {
39
+ assert.match(compileHtml(sampleNode)[0]?.contents ?? "", /Hello/);
40
+ assert.match(compileReact(sampleNode)[0]?.contents ?? "", /SampleFrame/);
41
+ assert.deepEqual(compileVanjs(sampleNode).map((file) => file.path), ["index.html", "main.js"]);
42
+ });
43
+ });
44
+ //#endregion
45
+ export {};
@@ -0,0 +1,6 @@
1
+ import { FigmaCompiler } from "../types.mjs";
2
+
3
+ //#region packages/plugin-figma-html/src/compilers/reactCompiler.d.ts
4
+ declare const compileReact: FigmaCompiler;
5
+ //#endregion
6
+ export { compileReact };
@@ -0,0 +1,47 @@
1
+ import { escapeHtml, escapeJsString, getNodeStyles, toComponentName, toReactStyle } from "./compilerUtils.mjs";
2
+ //#region packages/plugin-figma-html/src/compilers/reactCompiler.ts
3
+ const compileReact = (node) => {
4
+ const componentName = toComponentName(node.name);
5
+ return [{
6
+ path: "FigmaComponent.tsx",
7
+ contents: `import React from 'react';
8
+
9
+ /**
10
+ * Auto-generated UI component from Figma frame: "${escapeHtml(node.name)}"
11
+ */
12
+ export const ${componentName}: React.FC = () => {
13
+ return (
14
+ ${walkReact(node).trimEnd()}
15
+ );
16
+ };
17
+
18
+ export default ${componentName};
19
+ `
20
+ }];
21
+ };
22
+ function walkReact(node, depth = 2) {
23
+ if (!node || node.visible === false) return "";
24
+ const indent = " ".repeat(depth);
25
+ const childIndent = " ".repeat(depth + 1);
26
+ const name = escapeHtml(node.name || "LayoutBox");
27
+ const styles = toReactStyle(getNodeStyles(node));
28
+ if (node.type === "TEXT") return `${indent}<span style={${styles}} data-layer={${escapeJsString(node.name || "LayoutBox")}}>
29
+ ${childIndent}{${escapeJsString(node.characters)}}
30
+ ${indent}</span>\n`;
31
+ return `${indent}<div style={${styles}} data-layer="${name}">
32
+ ${node.children?.map((child) => walkReactWithParent(child, node, depth + 1)).join("") || ""}${indent}</div>\n`;
33
+ }
34
+ function walkReactWithParent(node, parent, depth) {
35
+ if (!node || node.visible === false) return "";
36
+ const indent = " ".repeat(depth);
37
+ const childIndent = " ".repeat(depth + 1);
38
+ const name = escapeHtml(node.name || "LayoutBox");
39
+ const styles = toReactStyle(getNodeStyles(node, parent));
40
+ if (node.type === "TEXT") return `${indent}<span style={${styles}} data-layer={${escapeJsString(node.name || "LayoutBox")}}>
41
+ ${childIndent}{${escapeJsString(node.characters)}}
42
+ ${indent}</span>\n`;
43
+ return `${indent}<div style={${styles}} data-layer="${name}">
44
+ ${node.children?.map((child) => walkReactWithParent(child, node, depth + 1)).join("") || ""}${indent}</div>\n`;
45
+ }
46
+ //#endregion
47
+ export { compileReact };
@@ -0,0 +1,6 @@
1
+ import { FigmaCompiler } from "../types.mjs";
2
+
3
+ //#region packages/plugin-figma-html/src/compilers/vanjsCompiler.d.ts
4
+ declare const compileVanjs: FigmaCompiler;
5
+ //#endregion
6
+ export { compileVanjs };
@@ -0,0 +1,47 @@
1
+ import { escapeHtml, escapeJsString, getNodeStyles, toCssText } from "./compilerUtils.mjs";
2
+ //#region packages/plugin-figma-html/src/compilers/vanjsCompiler.ts
3
+ const compileVanjs = (node) => [{
4
+ path: "index.html",
5
+ contents: `<!doctype html>
6
+ <html lang="en">
7
+ <head>
8
+ <meta charset="UTF-8" />
9
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
10
+ <title>${escapeHtml(node.name || "Figma Export")}</title>
11
+ </head>
12
+ <body>
13
+ <div id="app"></div>
14
+ <script type="module" src="./main.js"><\/script>
15
+ </body>
16
+ </html>
17
+ `
18
+ }, {
19
+ path: "main.js",
20
+ contents: `import van from 'https://cdn.jsdelivr.net/npm/vanjs-core@1.5.5/src/van.min.js';
21
+
22
+ const { div, span } = van.tags;
23
+
24
+ const App = () => (
25
+ ${walkVanjs(node, void 0, 1).trimEnd()}
26
+ );
27
+
28
+ van.add(document.getElementById('app'), App());
29
+ `
30
+ }];
31
+ function walkVanjs(node, parent, depth = 0) {
32
+ if (!node || node.visible === false) return "null";
33
+ const indent = " ".repeat(depth);
34
+ const childIndent = " ".repeat(depth + 1);
35
+ const tag = node.type === "TEXT" ? "span" : "div";
36
+ const props = `{
37
+ ${childIndent}style: ${escapeJsString(toCssText(getNodeStyles(node, parent)))},
38
+ ${childIndent}"data-layer": ${escapeJsString(node.name || "LayoutBox")}
39
+ ${indent}}`;
40
+ if (node.type === "TEXT") return `${indent}${tag}(${props}, ${escapeJsString(node.characters)})\n`;
41
+ const children = (node.children || []).map((child) => walkVanjs(child, node, depth + 1)).filter((value) => value.trim() !== "null").join(",");
42
+ if (!children) return `${indent}${tag}(${props})\n`;
43
+ return `${indent}${tag}(${props},
44
+ ${children}${indent})\n`;
45
+ }
46
+ //#endregion
47
+ export { compileVanjs };
@@ -0,0 +1,16 @@
1
+ import { SourceLocation } from "../nodes.mjs";
2
+
3
+ //#region packages/design-embed/src/core/diagnostics/diagnostic.d.ts
4
+ type DiagnosticSeverity = "error" | "warning" | "info";
5
+ interface Diagnostic {
6
+ code: string;
7
+ message: string;
8
+ severity: DiagnosticSeverity;
9
+ file?: string;
10
+ source?: SourceLocation;
11
+ selector?: string;
12
+ property?: string;
13
+ details?: Record<string, unknown>;
14
+ }
15
+ //#endregion
16
+ export { Diagnostic };
@@ -0,0 +1,14 @@
1
+ //#region packages/design-embed/src/core/nodes.d.ts
2
+ /**
3
+ * Location in the source HTML file.
4
+ */
5
+ interface SourceLocation {
6
+ /** Absolute offset in characters. */
7
+ offset: number;
8
+ /** 1-based line number. */
9
+ line: number;
10
+ /** 1-based column number. */
11
+ column: number;
12
+ }
13
+ //#endregion
14
+ export { SourceLocation };
@@ -0,0 +1,34 @@
1
+ import { Diagnostic } from "../diagnostics/diagnostic.mjs";
2
+
3
+ //#region packages/design-embed/src/core/plugins/pluginApi.d.ts
4
+ /**
5
+ * Represents a file generated by the compiler.
6
+ */
7
+ interface GeneratedFile {
8
+ /** Relative path from the output directory. */
9
+ path: string;
10
+ /** File content. */
11
+ contents: string;
12
+ }
13
+ interface GeneratedAsset {
14
+ path: string;
15
+ contents?: string | Uint8Array;
16
+ sourceUrl?: string;
17
+ }
18
+ interface SourcePlugin {
19
+ name: string;
20
+ run(input: SourcePluginInput): Promise<SourcePluginResult>;
21
+ }
22
+ interface SourcePluginInput {
23
+ cwd: string;
24
+ config?: unknown;
25
+ }
26
+ interface SourcePluginResult {
27
+ html?: string;
28
+ css?: string;
29
+ assets?: GeneratedAsset[];
30
+ files?: GeneratedFile[];
31
+ diagnostics: Diagnostic[];
32
+ }
33
+ //#endregion
34
+ export { SourcePlugin, SourcePluginInput, SourcePluginResult };
@@ -0,0 +1,17 @@
1
+ import { ExtractedParams, FigmaNode } from "../types.mjs";
2
+
3
+ //#region packages/plugin-figma-html/src/external/figmaApi.d.ts
4
+ type FigmaApiResponse = unknown;
5
+ type FigmaFetcher = (input: string, init?: RequestInit) => Promise<Response>;
6
+ interface FigmaClientOptions {
7
+ token: string;
8
+ fetcher?: FigmaFetcher;
9
+ }
10
+ declare function extractParamsFromURL(input: string): ExtractedParams;
11
+ declare function fetchFigmaNode(fileKey: string, nodeId: string | null, options: FigmaClientOptions): Promise<FigmaNode>;
12
+ declare function fetchFigmaApiResponse(fileKey: string, nodeId: string | null, options: FigmaClientOptions): Promise<FigmaApiResponse>;
13
+ declare function buildFigmaNodeEndpoint(fileKey: string, nodeId: string | null): string;
14
+ declare function fetchFigmaImageFills(fileKey: string, options: FigmaClientOptions): Promise<Record<string, string>>;
15
+ declare function buildFigmaImageFillsEndpoint(fileKey: string): string;
16
+ //#endregion
17
+ export { FigmaApiResponse, FigmaClientOptions, FigmaFetcher, buildFigmaImageFillsEndpoint, buildFigmaNodeEndpoint, extractParamsFromURL, fetchFigmaApiResponse, fetchFigmaImageFills, fetchFigmaNode };
@@ -0,0 +1,55 @@
1
+ //#region packages/plugin-figma-html/src/external/figmaApi.ts
2
+ function extractParamsFromURL(input) {
3
+ const cleanInput = input.trim();
4
+ const fileKey = cleanInput.match(/figma\.com\/(?:file|design)\/([^/]+)/)?.[1] || cleanInput;
5
+ let nodeId = null;
6
+ try {
7
+ if (cleanInput.includes("figma.com")) {
8
+ const rawNodeId = new URL(cleanInput).searchParams.get("node-id");
9
+ if (rawNodeId) nodeId = rawNodeId.replace(/-/g, ":");
10
+ }
11
+ } catch {}
12
+ return {
13
+ fileKey,
14
+ nodeId
15
+ };
16
+ }
17
+ async function fetchFigmaNode(fileKey, nodeId, options) {
18
+ const data = await fetchFigmaApiResponse(fileKey, nodeId, options);
19
+ const rootNode = nodeId ? data.nodes?.[nodeId]?.document : data.document;
20
+ if (!rootNode) throw new Error(`Could not find valid element tree for Node ID: ${nodeId}`);
21
+ attachImageFillUrls(rootNode, await fetchFigmaImageFills(fileKey, options));
22
+ return rootNode;
23
+ }
24
+ async function fetchFigmaApiResponse(fileKey, nodeId, options) {
25
+ const endpoint = buildFigmaNodeEndpoint(fileKey, nodeId);
26
+ const response = await (options.fetcher ?? fetch)(endpoint, {
27
+ method: "GET",
28
+ headers: { "X-Figma-Token": options.token }
29
+ });
30
+ if (!response.ok) throw new Error(`Figma API Error: ${response.status} ${response.statusText}`);
31
+ return response.json();
32
+ }
33
+ function buildFigmaNodeEndpoint(fileKey, nodeId) {
34
+ if (!nodeId) return `https://api.figma.com/v1/files/${fileKey}?depth=2`;
35
+ return `https://api.figma.com/v1/files/${fileKey}/nodes?ids=${encodeURIComponent(nodeId)}`;
36
+ }
37
+ async function fetchFigmaImageFills(fileKey, options) {
38
+ const endpoint = buildFigmaImageFillsEndpoint(fileKey);
39
+ const response = await (options.fetcher ?? fetch)(endpoint, {
40
+ method: "GET",
41
+ headers: { "X-Figma-Token": options.token }
42
+ });
43
+ if (!response.ok) throw new Error(`Figma image fills API Error: ${response.status} ${response.statusText}`);
44
+ const data = await response.json();
45
+ return Object.fromEntries(Object.entries(data.images || {}).filter((entry) => typeof entry[1] === "string"));
46
+ }
47
+ function buildFigmaImageFillsEndpoint(fileKey) {
48
+ return `https://api.figma.com/v1/files/${fileKey}/images`;
49
+ }
50
+ function attachImageFillUrls(node, imageFills) {
51
+ for (const fill of node.fills || []) if (fill.type === "IMAGE" && fill.imageRef) fill.imageUrl = imageFills[fill.imageRef];
52
+ for (const child of node.children || []) attachImageFillUrls(child, imageFills);
53
+ }
54
+ //#endregion
55
+ export { buildFigmaImageFillsEndpoint, buildFigmaNodeEndpoint, extractParamsFromURL, fetchFigmaApiResponse, fetchFigmaImageFills, fetchFigmaNode };
@@ -0,0 +1 @@
1
+ export { };
@@ -0,0 +1,101 @@
1
+ import { buildFigmaImageFillsEndpoint, buildFigmaNodeEndpoint, extractParamsFromURL, fetchFigmaApiResponse, fetchFigmaImageFills, fetchFigmaNode } from "./figmaApi.mjs";
2
+ import assert from "node:assert/strict";
3
+ import { describe, test } from "node:test";
4
+ //#region packages/plugin-figma-html/src/external/figmaApi.test.ts
5
+ describe("extractParamsFromURL", () => {
6
+ test("extracts file key and node id from a Figma design URL", () => {
7
+ assert.deepEqual(extractParamsFromURL("https://www.figma.com/design/file123/Sample?node-id=1-2&t=abc"), {
8
+ fileKey: "file123",
9
+ nodeId: "1:2"
10
+ });
11
+ });
12
+ test("treats non-url input as a raw file key", () => {
13
+ assert.deepEqual(extractParamsFromURL("file123"), {
14
+ fileKey: "file123",
15
+ nodeId: null
16
+ });
17
+ });
18
+ });
19
+ describe("buildFigmaNodeEndpoint", () => {
20
+ test("builds root document endpoint", () => {
21
+ assert.equal(buildFigmaNodeEndpoint("file123", null), "https://api.figma.com/v1/files/file123?depth=2");
22
+ });
23
+ test("builds node endpoint with an encoded node id", () => {
24
+ assert.equal(buildFigmaNodeEndpoint("file123", "1:2"), "https://api.figma.com/v1/files/file123/nodes?ids=1%3A2");
25
+ });
26
+ });
27
+ describe("buildFigmaImageFillsEndpoint", () => {
28
+ test("builds image fills endpoint", () => {
29
+ assert.equal(buildFigmaImageFillsEndpoint("file123"), "https://api.figma.com/v1/files/file123/images");
30
+ });
31
+ });
32
+ describe("fetchFigmaNode", () => {
33
+ test("uses an injected fetcher and returns the selected node with image fill URLs", async () => {
34
+ const calls = [];
35
+ const fetcher = async (url, init) => {
36
+ calls.push([url, init]);
37
+ if (url.endsWith("/images")) return new Response(JSON.stringify({ images: { image123: "https://example.com/image.png" } }));
38
+ return new Response(JSON.stringify({ nodes: { "1:2": { document: {
39
+ id: "1:2",
40
+ name: "Button",
41
+ type: "FRAME",
42
+ fills: [{
43
+ type: "IMAGE",
44
+ imageRef: "image123"
45
+ }]
46
+ } } } }));
47
+ };
48
+ const node = await fetchFigmaNode("file123", "1:2", {
49
+ token: "token",
50
+ fetcher
51
+ });
52
+ assert.deepEqual(node, {
53
+ id: "1:2",
54
+ name: "Button",
55
+ type: "FRAME",
56
+ fills: [{
57
+ type: "IMAGE",
58
+ imageRef: "image123",
59
+ imageUrl: "https://example.com/image.png"
60
+ }]
61
+ });
62
+ assert.deepEqual(calls, [["https://api.figma.com/v1/files/file123/nodes?ids=1%3A2", {
63
+ method: "GET",
64
+ headers: { "X-Figma-Token": "token" }
65
+ }], ["https://api.figma.com/v1/files/file123/images", {
66
+ method: "GET",
67
+ headers: { "X-Figma-Token": "token" }
68
+ }]]);
69
+ });
70
+ });
71
+ describe("fetchFigmaImageFills", () => {
72
+ test("returns only successful image fill URLs", async () => {
73
+ const fetcher = async () => new Response(JSON.stringify({ images: {
74
+ image123: "https://example.com/image.png",
75
+ image456: null
76
+ } }));
77
+ assert.deepEqual(await fetchFigmaImageFills("file123", {
78
+ token: "token",
79
+ fetcher
80
+ }), { image123: "https://example.com/image.png" });
81
+ });
82
+ });
83
+ describe("fetchFigmaApiResponse", () => {
84
+ test("returns the raw API payload without extracting a document node", async () => {
85
+ const payload = {
86
+ name: "Raw file response",
87
+ document: {
88
+ id: "0:0",
89
+ name: "Document",
90
+ type: "DOCUMENT"
91
+ }
92
+ };
93
+ const fetcher = async () => new Response(JSON.stringify(payload));
94
+ assert.deepEqual(await fetchFigmaApiResponse("file123", null, {
95
+ token: "token",
96
+ fetcher
97
+ }), payload);
98
+ });
99
+ });
100
+ //#endregion
101
+ export {};
@@ -0,0 +1,17 @@
1
+ import { FigmaNode } from "../types.mjs";
2
+ import { FigmaFetcher } from "./figmaApi.mjs";
3
+
4
+ //#region packages/plugin-figma-html/src/external/imageDownloader.d.ts
5
+ interface DownloadFigmaImagesOptions {
6
+ fetcher?: FigmaFetcher;
7
+ publicPath?: string;
8
+ }
9
+ interface DownloadedFigmaImage {
10
+ imageRef: string;
11
+ sourceUrl: string;
12
+ filePath: string;
13
+ publicPath: string;
14
+ }
15
+ declare function downloadFigmaImageFills(rootNode: FigmaNode, outDir: string, options?: DownloadFigmaImagesOptions): Promise<DownloadedFigmaImage[]>;
16
+ //#endregion
17
+ export { DownloadFigmaImagesOptions, DownloadedFigmaImage, downloadFigmaImageFills };