@design-embed/plugin-figma-html 0.1.0 → 1.0.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 (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 +12 -10
  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
@@ -0,0 +1,66 @@
1
+ import { join, posix } from "node:path";
2
+ import { mkdirSync, writeFileSync } from "node:fs";
3
+ //#region packages/plugin-figma-html/src/external/imageDownloader.ts
4
+ async function downloadFigmaImageFills(rootNode, outDir, options = {}) {
5
+ const targets = collectImageFillTargets(rootNode);
6
+ const uniqueTargets = Array.from(new Map(targets.map((target) => [target.imageRef, target])).values());
7
+ if (uniqueTargets.length === 0) return [];
8
+ mkdirSync(outDir, { recursive: true });
9
+ const downloadedImages = await Promise.all(uniqueTargets.map((target) => downloadImageFill(target, outDir, options)));
10
+ const publicPathByRef = new Map(downloadedImages.map((image) => [image.imageRef, image.publicPath]));
11
+ for (const target of targets) {
12
+ const fill = target.fills[target.fillIndex];
13
+ if (fill) fill.imageLocalPath = publicPathByRef.get(target.imageRef);
14
+ }
15
+ return downloadedImages;
16
+ }
17
+ function collectImageFillTargets(node) {
18
+ const targets = [];
19
+ node.fills?.forEach((fill, fillIndex, fills) => {
20
+ if (fill.type === "IMAGE" && fill.imageRef && fill.imageUrl) targets.push({
21
+ imageRef: fill.imageRef,
22
+ imageUrl: fill.imageUrl,
23
+ fills,
24
+ fillIndex
25
+ });
26
+ });
27
+ for (const child of node.children || []) targets.push(...collectImageFillTargets(child));
28
+ return targets;
29
+ }
30
+ async function downloadImageFill(target, outDir, options) {
31
+ const response = await (options.fetcher ?? fetch)(target.imageUrl);
32
+ if (!response.ok) throw new Error(`Figma image download failed for ${target.imageRef}: ${response.status} ${response.statusText}`);
33
+ const extension = extensionFromResponse(response, target.imageUrl);
34
+ const filename = `${sanitizeFilename(target.imageRef)}.${extension}`;
35
+ const filePath = join(outDir, filename);
36
+ const publicPath = posix.join(options.publicPath || outDir, filename);
37
+ writeFileSync(filePath, Buffer.from(await response.arrayBuffer()));
38
+ return {
39
+ imageRef: target.imageRef,
40
+ sourceUrl: target.imageUrl,
41
+ filePath,
42
+ publicPath
43
+ };
44
+ }
45
+ function extensionFromResponse(response, url) {
46
+ switch (response.headers.get("content-type")?.split(";")[0]) {
47
+ case "image/jpeg": return "jpg";
48
+ case "image/png": return "png";
49
+ case "image/svg+xml": return "svg";
50
+ case "image/webp": return "webp";
51
+ case "image/gif": return "gif";
52
+ }
53
+ return safeUrlPathname(url).match(/\.([a-z0-9]+)$/i)?.[1]?.toLowerCase() || "img";
54
+ }
55
+ function safeUrlPathname(url) {
56
+ try {
57
+ return new URL(url).pathname;
58
+ } catch {
59
+ return url;
60
+ }
61
+ }
62
+ function sanitizeFilename(value) {
63
+ return value.replace(/[^a-zA-Z0-9_-]/g, "_");
64
+ }
65
+ //#endregion
66
+ export { downloadFigmaImageFills };
@@ -0,0 +1 @@
1
+ export { };
@@ -0,0 +1,42 @@
1
+ import { getNodeStyles } from "../compilers/compilerUtils.mjs";
2
+ import { downloadFigmaImageFills } from "./imageDownloader.mjs";
3
+ import { join } from "node:path";
4
+ import { mkdtempSync, readFileSync, rmSync } from "node:fs";
5
+ import assert from "node:assert/strict";
6
+ import { describe, test } from "node:test";
7
+ import { tmpdir } from "node:os";
8
+ //#region packages/plugin-figma-html/src/external/imageDownloader.test.ts
9
+ describe("downloadFigmaImageFills", () => {
10
+ test("downloads image fills and attaches local paths for compilers", async () => {
11
+ const outDir = mkdtempSync(join(tmpdir(), "figma-images-"));
12
+ const node = {
13
+ name: "Hero",
14
+ type: "RECTANGLE",
15
+ fills: [{
16
+ type: "IMAGE",
17
+ scaleMode: "FILL",
18
+ imageRef: "image/ref",
19
+ imageUrl: "https://example.com/image.png"
20
+ }]
21
+ };
22
+ const fetcher = async () => new Response("image-bytes", { headers: { "content-type": "image/png" } });
23
+ try {
24
+ const downloaded = await downloadFigmaImageFills(node, outDir, {
25
+ fetcher,
26
+ publicPath: "assets"
27
+ });
28
+ assert.equal(downloaded.length, 1);
29
+ assert.equal(downloaded[0]?.publicPath, "assets/image_ref.png");
30
+ assert.equal(readFileSync(downloaded[0]?.filePath || "", "utf-8"), "image-bytes");
31
+ assert.equal(node.fills?.[0]?.imageLocalPath, "assets/image_ref.png");
32
+ assert.equal(getNodeStyles(node).backgroundImage, "url(\"assets/image_ref.png\")");
33
+ } finally {
34
+ rmSync(outDir, {
35
+ recursive: true,
36
+ force: true
37
+ });
38
+ }
39
+ });
40
+ });
41
+ //#endregion
42
+ export {};
@@ -0,0 +1,8 @@
1
+ import { CompilerMode, ExtractedParams, FigmaCompiler, FigmaNode, GeneratedFile } from "./types.mjs";
2
+ import { compileHtml } from "./compilers/htmlCompiler.mjs";
3
+ import { compileReact } from "./compilers/reactCompiler.mjs";
4
+ import { compileVanjs } from "./compilers/vanjsCompiler.mjs";
5
+ import { getCompiler, isCompilerMode } from "./compilers/index.mjs";
6
+ import { FigmaApiResponse, FigmaClientOptions, FigmaFetcher, extractParamsFromURL, fetchFigmaApiResponse, fetchFigmaNode } from "./external/figmaApi.mjs";
7
+ import { FigmaHtmlPlugin, FigmaHtmlPluginOptions } from "./plugin.mjs";
8
+ export { type CompilerMode, type ExtractedParams, type FigmaApiResponse, type FigmaClientOptions, type FigmaCompiler, type FigmaFetcher, FigmaHtmlPlugin, type FigmaHtmlPluginOptions, type FigmaNode, type GeneratedFile, compileHtml, compileReact, compileVanjs, extractParamsFromURL, fetchFigmaApiResponse, fetchFigmaNode, getCompiler, isCompilerMode };
package/dist/index.mjs ADDED
@@ -0,0 +1,7 @@
1
+ import { compileHtml } from "./compilers/htmlCompiler.mjs";
2
+ import { compileReact } from "./compilers/reactCompiler.mjs";
3
+ import { compileVanjs } from "./compilers/vanjsCompiler.mjs";
4
+ import { getCompiler, isCompilerMode } from "./compilers/index.mjs";
5
+ import { extractParamsFromURL, fetchFigmaApiResponse, fetchFigmaNode } from "./external/figmaApi.mjs";
6
+ import { FigmaHtmlPlugin } from "./plugin.mjs";
7
+ export { FigmaHtmlPlugin, compileHtml, compileReact, compileVanjs, extractParamsFromURL, fetchFigmaApiResponse, fetchFigmaNode, getCompiler, isCompilerMode };
@@ -0,0 +1,16 @@
1
+ import { SourcePlugin, SourcePluginInput, SourcePluginResult } from "./design-embed/src/core/plugins/pluginApi.mjs";
2
+
3
+ //#region packages/plugin-figma-html/src/plugin.d.ts
4
+ interface FigmaHtmlPluginOptions {
5
+ url: string;
6
+ token?: string;
7
+ assetsDir?: string;
8
+ }
9
+ declare class FigmaHtmlPlugin implements SourcePlugin {
10
+ private readonly options;
11
+ readonly name = "figma-html";
12
+ constructor(options: FigmaHtmlPluginOptions);
13
+ run(input: SourcePluginInput): Promise<SourcePluginResult>;
14
+ }
15
+ //#endregion
16
+ export { FigmaHtmlPlugin, FigmaHtmlPluginOptions };
@@ -0,0 +1,43 @@
1
+ import { compileHtml } from "./compilers/htmlCompiler.mjs";
2
+ import { extractParamsFromURL, fetchFigmaNode } from "./external/figmaApi.mjs";
3
+ import { downloadFigmaImageFills } from "./external/imageDownloader.mjs";
4
+ import { join } from "node:path";
5
+ //#region packages/plugin-figma-html/src/plugin.ts
6
+ var FigmaHtmlPlugin = class {
7
+ options;
8
+ name = "figma-html";
9
+ constructor(options) {
10
+ this.options = options;
11
+ }
12
+ async run(input) {
13
+ const { url, token: optionsToken, assetsDir = "assets" } = this.options;
14
+ const token = optionsToken ?? process.env.FIGMA_TOKEN;
15
+ if (!token) return { diagnostics: [{
16
+ code: "FIGMA_TOKEN_REQUIRED",
17
+ message: "figma-html requires a Figma token. Pass token in the FigmaHtmlPlugin constructor or set the FIGMA_TOKEN environment variable.",
18
+ severity: "error"
19
+ }] };
20
+ try {
21
+ const { fileKey, nodeId } = extractParamsFromURL(url);
22
+ const rootNode = await fetchFigmaNode(fileKey, nodeId, { token });
23
+ const downloadedImages = await downloadFigmaImageFills(rootNode, join(input.cwd, assetsDir), { publicPath: assetsDir });
24
+ const [htmlFile] = compileHtml(rootNode);
25
+ return {
26
+ html: htmlFile?.contents,
27
+ diagnostics: downloadedImages.length > 0 ? [{
28
+ code: "FIGMA_ASSETS_DOWNLOADED",
29
+ message: `Downloaded ${downloadedImages.length} image asset(s).`,
30
+ severity: "info"
31
+ }] : []
32
+ };
33
+ } catch (error) {
34
+ return { diagnostics: [{
35
+ code: "FIGMA_HTML_FAILED",
36
+ message: error instanceof Error ? error.message : String(error),
37
+ severity: "error"
38
+ }] };
39
+ }
40
+ }
41
+ };
42
+ //#endregion
43
+ export { FigmaHtmlPlugin };
@@ -0,0 +1,84 @@
1
+ //#region packages/plugin-figma-html/src/types.d.ts
2
+ interface ExtractedParams {
3
+ fileKey: string;
4
+ nodeId: string | null;
5
+ }
6
+ interface FigmaNode {
7
+ id?: string;
8
+ name?: string;
9
+ type?: string;
10
+ visible?: boolean;
11
+ characters?: string;
12
+ children?: FigmaNode[];
13
+ layoutMode?: "NONE" | "HORIZONTAL" | "VERTICAL" | "GRID" | string;
14
+ layoutSizingHorizontal?: "FIXED" | "HUG" | "FILL" | string;
15
+ layoutSizingVertical?: "FIXED" | "HUG" | "FILL" | string;
16
+ layoutWrap?: "NO_WRAP" | "WRAP" | string;
17
+ primaryAxisSizingMode?: "FIXED" | "AUTO" | string;
18
+ counterAxisSizingMode?: "FIXED" | "AUTO" | string;
19
+ primaryAxisAlignItems?: "MIN" | "CENTER" | "MAX" | "SPACE_BETWEEN" | string;
20
+ counterAxisAlignItems?: "MIN" | "CENTER" | "MAX" | "BASELINE" | string;
21
+ layoutPositioning?: "ABSOLUTE" | "AUTO" | string;
22
+ layoutAlign?: "INHERIT" | "STRETCH" | "MIN" | "CENTER" | "MAX" | string;
23
+ layoutGrow?: number;
24
+ itemSpacing?: number;
25
+ counterAxisSpacing?: number;
26
+ gridRowGap?: number;
27
+ gridColumnGap?: number;
28
+ gridColumnsSizing?: string;
29
+ gridRowsSizing?: string;
30
+ gridColumnSpan?: number;
31
+ gridRowSpan?: number;
32
+ paddingTop?: number;
33
+ paddingBottom?: number;
34
+ paddingLeft?: number;
35
+ paddingRight?: number;
36
+ absoluteBoundingBox?: {
37
+ x?: number;
38
+ y?: number;
39
+ width?: number;
40
+ height?: number;
41
+ };
42
+ fills?: Array<{
43
+ type?: string;
44
+ opacity?: number;
45
+ scaleMode?: string;
46
+ imageRef?: string;
47
+ imageUrl?: string;
48
+ imageLocalPath?: string;
49
+ color?: {
50
+ r: number;
51
+ g: number;
52
+ b: number;
53
+ };
54
+ }>;
55
+ cornerRadius?: number;
56
+ rectangleCornerRadii?: number[];
57
+ strokes?: Array<{
58
+ type?: string;
59
+ opacity?: number;
60
+ color?: {
61
+ r: number;
62
+ g: number;
63
+ b: number;
64
+ };
65
+ }>;
66
+ strokeWeight?: number;
67
+ strokeAlign?: "INSIDE" | "OUTSIDE" | "CENTER" | string;
68
+ opacity?: number;
69
+ clipsContent?: boolean;
70
+ style?: {
71
+ fontSize?: number;
72
+ fontWeight?: number | string;
73
+ fontFamily?: string;
74
+ lineHeightPx?: number;
75
+ };
76
+ }
77
+ interface GeneratedFile {
78
+ path: string;
79
+ contents: string;
80
+ }
81
+ type CompilerMode = "react" | "html" | "vanjs";
82
+ type FigmaCompiler = (node: FigmaNode) => GeneratedFile[];
83
+ //#endregion
84
+ export { CompilerMode, ExtractedParams, FigmaCompiler, FigmaNode, GeneratedFile };
package/package.json CHANGED
@@ -1,16 +1,17 @@
1
1
  {
2
2
  "name": "@design-embed/plugin-figma-html",
3
- "version": "0.1.0",
3
+ "version": "1.0.1",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "publishConfig": {
7
- "access": "public"
7
+ "access": "public",
8
+ "provenance": true
8
9
  },
9
10
  "exports": {
10
11
  ".": {
11
12
  "types": "./src/index.ts",
12
13
  "development": "./src/index.ts",
13
- "default": "./dist/index.js"
14
+ "default": "./dist/index.mjs"
14
15
  }
15
16
  },
16
17
  "files": [
@@ -19,12 +20,13 @@
19
20
  "!src/**/*.test.ts",
20
21
  "README.md"
21
22
  ],
22
- "dependencies": {
23
- "@design-embed/config": "0.1.0",
24
- "@design-embed/core": "0.1.0"
23
+ "peerDependencies": {
24
+ "design-embed": "*"
25
25
  },
26
- "bundledDependencies": [
27
- "@design-embed/config",
28
- "@design-embed/core"
29
- ]
26
+ "devDependencies": {
27
+ "design-embed": "0.2.1"
28
+ },
29
+ "scripts": {
30
+ "test": "node --conditions=development --test src/**/*.test.ts"
31
+ }
30
32
  }
package/src/plugin.ts CHANGED
@@ -1,10 +1,9 @@
1
1
  import { join } from "node:path";
2
- import type { PluginDefinition } from "@design-embed/config";
3
2
  import type {
4
3
  SourcePlugin,
5
4
  SourcePluginInput,
6
5
  SourcePluginResult,
7
- } from "@design-embed/core";
6
+ } from "design-embed";
8
7
  import { compileHtml } from "./compilers/index.ts";
9
8
  import { extractParamsFromURL, fetchFigmaNode } from "./external/figmaApi.ts";
10
9
  import { downloadFigmaImageFills } from "./external/imageDownloader.ts";
@@ -15,7 +14,7 @@ export interface FigmaHtmlPluginOptions {
15
14
  assetsDir?: string;
16
15
  }
17
16
 
18
- export class FigmaHtmlPlugin implements PluginDefinition, SourcePlugin {
17
+ export class FigmaHtmlPlugin implements SourcePlugin {
19
18
  readonly name = "figma-html";
20
19
 
21
20
  constructor(private readonly options: FigmaHtmlPluginOptions) {}
@@ -1,182 +0,0 @@
1
- export function toComponentName(name, fallback = "FigmaExport") {
2
- const cleaned = (name || "").replace(/[^a-zA-Z0-9가-힣]/g, "");
3
- if (!cleaned)
4
- return fallback;
5
- return /^[0-9]/.test(cleaned) ? `${fallback}${cleaned}` : cleaned;
6
- }
7
- export function escapeHtml(value) {
8
- return (value || "")
9
- .replace(/&/g, "&amp;")
10
- .replace(/</g, "&lt;")
11
- .replace(/>/g, "&gt;")
12
- .replace(/"/g, "&quot;")
13
- .replace(/'/g, "&#39;");
14
- }
15
- export function escapeJsString(value) {
16
- return JSON.stringify(value || "");
17
- }
18
- export function getNodeStyles(node, parent) {
19
- const styles = {};
20
- const bounds = node.absoluteBoundingBox;
21
- const parentBounds = parent?.absoluteBoundingBox;
22
- const parentUsesLayout = Boolean(parent?.layoutMode && parent.layoutMode !== "NONE");
23
- const isAbsoluteChild = Boolean(parent && (!parentUsesLayout || node.layoutPositioning === "ABSOLUTE"));
24
- if (isAbsoluteChild && bounds && parentBounds) {
25
- styles.position = "absolute";
26
- styles.left = `${Math.round((bounds.x || 0) - (parentBounds.x || 0))}px`;
27
- styles.top = `${Math.round((bounds.y || 0) - (parentBounds.y || 0))}px`;
28
- }
29
- else if (node.children?.length || node.layoutMode === "NONE") {
30
- styles.position = "relative";
31
- }
32
- if (bounds && node.layoutSizingHorizontal !== "HUG") {
33
- styles.width = `${Math.round(bounds.width || 0)}px`;
34
- }
35
- if (bounds && node.layoutSizingVertical !== "HUG") {
36
- styles.height = `${Math.round(bounds.height || 0)}px`;
37
- }
38
- if (node.layoutMode === "HORIZONTAL" || node.layoutMode === "VERTICAL") {
39
- styles.display = "flex";
40
- styles.flexDirection = node.layoutMode === "HORIZONTAL" ? "row" : "column";
41
- styles.boxSizing = "border-box";
42
- if (node.layoutWrap === "WRAP")
43
- styles.flexWrap = "wrap";
44
- if (node.itemSpacing !== undefined)
45
- styles.gap = `${Math.round(node.itemSpacing)}px`;
46
- if (node.counterAxisSpacing !== undefined && node.layoutWrap === "WRAP") {
47
- styles.rowGap = `${Math.round(node.counterAxisSpacing)}px`;
48
- }
49
- styles.justifyContent = mapPrimaryAxisAlignment(node.primaryAxisAlignItems);
50
- styles.alignItems = mapCounterAxisAlignment(node.counterAxisAlignItems);
51
- }
52
- else if (node.layoutMode === "GRID") {
53
- styles.display = "grid";
54
- styles.boxSizing = "border-box";
55
- if (node.gridColumnsSizing)
56
- styles.gridTemplateColumns = node.gridColumnsSizing;
57
- if (node.gridRowsSizing)
58
- styles.gridTemplateRows = node.gridRowsSizing;
59
- if (node.gridColumnGap !== undefined)
60
- styles.columnGap = `${Math.round(node.gridColumnGap)}px`;
61
- if (node.gridRowGap !== undefined)
62
- styles.rowGap = `${Math.round(node.gridRowGap)}px`;
63
- }
64
- if (node.layoutSizingHorizontal === "FILL" || node.layoutGrow === 1) {
65
- styles.flex = 1;
66
- styles.width = "100%";
67
- }
68
- if (node.layoutSizingVertical === "FILL") {
69
- styles.height = "100%";
70
- }
71
- if (node.layoutAlign === "STRETCH") {
72
- styles.alignSelf = "stretch";
73
- }
74
- if (node.gridColumnSpan && node.gridColumnSpan > 1) {
75
- styles.gridColumn = `span ${node.gridColumnSpan}`;
76
- }
77
- if (node.gridRowSpan && node.gridRowSpan > 1) {
78
- styles.gridRow = `span ${node.gridRowSpan}`;
79
- }
80
- if (node.paddingTop !== undefined)
81
- styles.paddingTop = `${node.paddingTop}px`;
82
- if (node.paddingBottom !== undefined)
83
- styles.paddingBottom = `${node.paddingBottom}px`;
84
- if (node.paddingLeft !== undefined)
85
- styles.paddingLeft = `${node.paddingLeft}px`;
86
- if (node.paddingRight !== undefined)
87
- styles.paddingRight = `${node.paddingRight}px`;
88
- if (node.clipsContent)
89
- styles.overflow = "hidden";
90
- if (node.opacity !== undefined && node.opacity !== 1)
91
- styles.opacity = node.opacity;
92
- const fill = node.fills?.find((item) => item.type === "SOLID" && item.color);
93
- if (fill?.color) {
94
- if (node.type === "TEXT") {
95
- styles.color = toRgba(fill.color, fill.opacity ?? 1);
96
- }
97
- else {
98
- styles.backgroundColor = toRgba(fill.color, fill.opacity ?? 1);
99
- }
100
- }
101
- const imageFill = node.fills?.find((item) => item.type === "IMAGE" && (item.imageLocalPath || item.imageUrl));
102
- const imageSource = imageFill?.imageLocalPath || imageFill?.imageUrl;
103
- if (imageSource) {
104
- styles.backgroundImage = `url("${imageSource}")`;
105
- styles.backgroundRepeat = "no-repeat";
106
- styles.backgroundPosition = "center";
107
- styles.backgroundSize = mapImageScaleMode(imageFill.scaleMode);
108
- }
109
- const stroke = node.strokes?.find((item) => item.type === "SOLID" && item.color);
110
- if (stroke?.color && node.strokeWeight) {
111
- styles.border = `${Math.round(node.strokeWeight)}px solid ${toRgba(stroke.color, stroke.opacity ?? 1)}`;
112
- styles.boxSizing = "border-box";
113
- }
114
- if (node.rectangleCornerRadii?.length === 4) {
115
- styles.borderRadius = node.rectangleCornerRadii
116
- .map((radius) => `${Math.round(radius)}px`)
117
- .join(" ");
118
- }
119
- else if (node.cornerRadius) {
120
- styles.borderRadius = `${Math.round(node.cornerRadius)}px`;
121
- }
122
- if (node.type === "TEXT") {
123
- const textStyle = node.style || {};
124
- if (textStyle.fontSize)
125
- styles.fontSize = `${textStyle.fontSize}px`;
126
- if (textStyle.fontWeight)
127
- styles.fontWeight = textStyle.fontWeight;
128
- if (textStyle.fontFamily)
129
- styles.fontFamily = `"${textStyle.fontFamily}", sans-serif`;
130
- if (textStyle.lineHeightPx)
131
- styles.lineHeight = `${Math.round(textStyle.lineHeightPx)}px`;
132
- }
133
- return styles;
134
- }
135
- export function toReactStyle(styles) {
136
- return JSON.stringify(styles).replace(/"([^"]+)":/g, "$1:");
137
- }
138
- export function toCssText(styles) {
139
- return Object.entries(styles)
140
- .map(([key, value]) => `${toKebabCase(key)}: ${value};`)
141
- .join(" ");
142
- }
143
- function toKebabCase(value) {
144
- return value.replace(/[A-Z]/g, (match) => `-${match.toLowerCase()}`);
145
- }
146
- function mapPrimaryAxisAlignment(value) {
147
- switch (value) {
148
- case "CENTER":
149
- return "center";
150
- case "MAX":
151
- return "flex-end";
152
- case "SPACE_BETWEEN":
153
- return "space-between";
154
- default:
155
- return "flex-start";
156
- }
157
- }
158
- function mapCounterAxisAlignment(value) {
159
- switch (value) {
160
- case "CENTER":
161
- return "center";
162
- case "MAX":
163
- return "flex-end";
164
- case "BASELINE":
165
- return "baseline";
166
- default:
167
- return "flex-start";
168
- }
169
- }
170
- function mapImageScaleMode(value) {
171
- switch (value) {
172
- case "FIT":
173
- return "contain";
174
- case "TILE":
175
- return "auto";
176
- default:
177
- return "cover";
178
- }
179
- }
180
- function toRgba(color, opacity) {
181
- return `rgba(${Math.round(color.r * 255)}, ${Math.round(color.g * 255)}, ${Math.round(color.b * 255)}, ${opacity})`;
182
- }
@@ -1,35 +0,0 @@
1
- import { escapeHtml, getNodeStyles, toCssText } from "./compilerUtils.js";
2
- export const compileHtml = (node) => [
3
- {
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, undefined, 2).trimEnd()}
14
- </body>
15
- </html>
16
- `,
17
- },
18
- ];
19
- function walkHtml(node, parent, depth = 0) {
20
- if (!node || node.visible === false)
21
- return "";
22
- const indent = " ".repeat(depth);
23
- const childIndent = " ".repeat(depth + 1);
24
- const name = escapeHtml(node.name || "LayoutBox");
25
- const style = escapeHtml(toCssText(getNodeStyles(node, parent)));
26
- if (node.type === "TEXT") {
27
- return `${indent}<span style="${style}" data-layer="${name}">
28
- ${childIndent}${escapeHtml(node.characters)}
29
- ${indent}</span>\n`;
30
- }
31
- const children = node.children?.map((child) => walkHtml(child, node, depth + 1)).join("") ||
32
- "";
33
- return `${indent}<div style="${style}" data-layer="${name}">
34
- ${children}${indent}</div>\n`;
35
- }
@@ -1,17 +0,0 @@
1
- import { compileHtml } from "./htmlCompiler.js";
2
- import { compileReact } from "./reactCompiler.js";
3
- import { compileVanjs } from "./vanjsCompiler.js";
4
- export { compileHtml } from "./htmlCompiler.js";
5
- export { compileReact } from "./reactCompiler.js";
6
- export { compileVanjs } from "./vanjsCompiler.js";
7
- export const compilers = {
8
- react: compileReact,
9
- html: compileHtml,
10
- vanjs: compileVanjs,
11
- };
12
- export function isCompilerMode(value) {
13
- return Boolean(value && value in compilers);
14
- }
15
- export function getCompiler(mode) {
16
- return compilers[mode];
17
- }
@@ -1,58 +0,0 @@
1
- import { escapeHtml, escapeJsString, getNodeStyles, toComponentName, toReactStyle, } from "./compilerUtils.js";
2
- export const compileReact = (node) => {
3
- const componentName = toComponentName(node.name);
4
- return [
5
- {
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
- };
23
- function walkReact(node, depth = 2) {
24
- if (!node || node.visible === false)
25
- return "";
26
- const indent = " ".repeat(depth);
27
- const childIndent = " ".repeat(depth + 1);
28
- const name = escapeHtml(node.name || "LayoutBox");
29
- const styles = toReactStyle(getNodeStyles(node));
30
- if (node.type === "TEXT") {
31
- return `${indent}<span style={${styles}} data-layer={${escapeJsString(node.name || "LayoutBox")}}>
32
- ${childIndent}{${escapeJsString(node.characters)}}
33
- ${indent}</span>\n`;
34
- }
35
- const children = node.children
36
- ?.map((child) => walkReactWithParent(child, node, depth + 1))
37
- .join("") || "";
38
- return `${indent}<div style={${styles}} data-layer="${name}">
39
- ${children}${indent}</div>\n`;
40
- }
41
- function walkReactWithParent(node, parent, depth) {
42
- if (!node || node.visible === false)
43
- return "";
44
- const indent = " ".repeat(depth);
45
- const childIndent = " ".repeat(depth + 1);
46
- const name = escapeHtml(node.name || "LayoutBox");
47
- const styles = toReactStyle(getNodeStyles(node, parent));
48
- if (node.type === "TEXT") {
49
- return `${indent}<span style={${styles}} data-layer={${escapeJsString(node.name || "LayoutBox")}}>
50
- ${childIndent}{${escapeJsString(node.characters)}}
51
- ${indent}</span>\n`;
52
- }
53
- const children = node.children
54
- ?.map((child) => walkReactWithParent(child, node, depth + 1))
55
- .join("") || "";
56
- return `${indent}<div style={${styles}} data-layer="${name}">
57
- ${children}${indent}</div>\n`;
58
- }