@design-embed/plugin-figma-html 0.1.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 (41) hide show
  1. package/LICENSE +9 -0
  2. package/README.md +5 -0
  3. package/dist/compilers/compilerUtils.js +182 -0
  4. package/dist/compilers/htmlCompiler.js +35 -0
  5. package/dist/compilers/index.js +17 -0
  6. package/dist/compilers/reactCompiler.js +58 -0
  7. package/dist/compilers/vanjsCompiler.js +55 -0
  8. package/dist/external/figmaApi.js +74 -0
  9. package/dist/external/imageDownloader.js +82 -0
  10. package/dist/index.js +3 -0
  11. package/dist/plugin.js +56 -0
  12. package/dist/types.js +1 -0
  13. package/node_modules/@design-embed/config/README.md +5 -0
  14. package/node_modules/@design-embed/config/dist/index.js +283 -0
  15. package/node_modules/@design-embed/config/package.json +19 -0
  16. package/node_modules/@design-embed/config/src/index.ts +518 -0
  17. package/node_modules/@design-embed/core/README.md +5 -0
  18. package/node_modules/@design-embed/core/dist/diagnostics/diagnostic.js +3 -0
  19. package/node_modules/@design-embed/core/dist/diagnostics/jsonDiagnostic.js +35 -0
  20. package/node_modules/@design-embed/core/dist/index.js +351 -0
  21. package/node_modules/@design-embed/core/dist/pipeline/checkMode.js +29 -0
  22. package/node_modules/@design-embed/core/dist/plugins/pluginApi.js +1 -0
  23. package/node_modules/@design-embed/core/dist/plugins/pluginRegistry.js +25 -0
  24. package/node_modules/@design-embed/core/package.json +19 -0
  25. package/node_modules/@design-embed/core/src/diagnostics/diagnostic.ts +18 -0
  26. package/node_modules/@design-embed/core/src/diagnostics/jsonDiagnostic.ts +51 -0
  27. package/node_modules/@design-embed/core/src/index.ts +591 -0
  28. package/node_modules/@design-embed/core/src/pipeline/checkMode.ts +46 -0
  29. package/node_modules/@design-embed/core/src/plugins/pluginApi.ts +78 -0
  30. package/node_modules/@design-embed/core/src/plugins/pluginRegistry.ts +37 -0
  31. package/package.json +30 -0
  32. package/src/compilers/compilerUtils.ts +211 -0
  33. package/src/compilers/htmlCompiler.ts +41 -0
  34. package/src/compilers/index.ts +24 -0
  35. package/src/compilers/reactCompiler.ts +79 -0
  36. package/src/compilers/vanjsCompiler.ts +67 -0
  37. package/src/external/figmaApi.ts +142 -0
  38. package/src/external/imageDownloader.ts +136 -0
  39. package/src/index.ts +26 -0
  40. package/src/plugin.ts +76 -0
  41. package/src/types.ts +85 -0
@@ -0,0 +1,136 @@
1
+ import { mkdirSync, writeFileSync } from "node:fs";
2
+ import { join, posix } from "node:path";
3
+ import type { FigmaNode } from "../types.ts";
4
+ import type { FigmaFetcher } from "./figmaApi.ts";
5
+
6
+ export interface DownloadFigmaImagesOptions {
7
+ fetcher?: FigmaFetcher;
8
+ publicPath?: string;
9
+ }
10
+
11
+ export interface DownloadedFigmaImage {
12
+ imageRef: string;
13
+ sourceUrl: string;
14
+ filePath: string;
15
+ publicPath: string;
16
+ }
17
+
18
+ interface ImageFillTarget {
19
+ imageRef: string;
20
+ imageUrl: string;
21
+ fills: NonNullable<FigmaNode["fills"]>;
22
+ fillIndex: number;
23
+ }
24
+
25
+ export async function downloadFigmaImageFills(
26
+ rootNode: FigmaNode,
27
+ outDir: string,
28
+ options: DownloadFigmaImagesOptions = {},
29
+ ): Promise<DownloadedFigmaImage[]> {
30
+ const targets = collectImageFillTargets(rootNode);
31
+ const uniqueTargets = Array.from(
32
+ new Map(targets.map((target) => [target.imageRef, target])).values(),
33
+ );
34
+
35
+ if (uniqueTargets.length === 0) return [];
36
+
37
+ mkdirSync(outDir, { recursive: true });
38
+
39
+ const downloadedImages = await Promise.all(
40
+ uniqueTargets.map((target) => downloadImageFill(target, outDir, options)),
41
+ );
42
+ const publicPathByRef = new Map(
43
+ downloadedImages.map((image) => [image.imageRef, image.publicPath]),
44
+ );
45
+
46
+ for (const target of targets) {
47
+ const fill = target.fills[target.fillIndex];
48
+ if (fill) {
49
+ fill.imageLocalPath = publicPathByRef.get(target.imageRef);
50
+ }
51
+ }
52
+
53
+ return downloadedImages;
54
+ }
55
+
56
+ function collectImageFillTargets(node: FigmaNode): ImageFillTarget[] {
57
+ const targets: ImageFillTarget[] = [];
58
+
59
+ node.fills?.forEach((fill, fillIndex, fills) => {
60
+ if (fill.type === "IMAGE" && fill.imageRef && fill.imageUrl) {
61
+ targets.push({
62
+ imageRef: fill.imageRef,
63
+ imageUrl: fill.imageUrl,
64
+ fills,
65
+ fillIndex,
66
+ });
67
+ }
68
+ });
69
+
70
+ for (const child of node.children || []) {
71
+ targets.push(...collectImageFillTargets(child));
72
+ }
73
+
74
+ return targets;
75
+ }
76
+
77
+ async function downloadImageFill(
78
+ target: ImageFillTarget,
79
+ outDir: string,
80
+ options: DownloadFigmaImagesOptions,
81
+ ): Promise<DownloadedFigmaImage> {
82
+ const fetcher = options.fetcher ?? fetch;
83
+ const response = await fetcher(target.imageUrl);
84
+
85
+ if (!response.ok) {
86
+ throw new Error(
87
+ `Figma image download failed for ${target.imageRef}: ${response.status} ${response.statusText}`,
88
+ );
89
+ }
90
+
91
+ const extension = extensionFromResponse(response, target.imageUrl);
92
+ const filename = `${sanitizeFilename(target.imageRef)}.${extension}`;
93
+ const filePath = join(outDir, filename);
94
+ const publicPath = posix.join(options.publicPath || outDir, filename);
95
+
96
+ writeFileSync(filePath, Buffer.from(await response.arrayBuffer()));
97
+
98
+ return {
99
+ imageRef: target.imageRef,
100
+ sourceUrl: target.imageUrl,
101
+ filePath,
102
+ publicPath,
103
+ };
104
+ }
105
+
106
+ function extensionFromResponse(response: Response, url: string): string {
107
+ const contentType = response.headers.get("content-type")?.split(";")[0];
108
+ switch (contentType) {
109
+ case "image/jpeg":
110
+ return "jpg";
111
+ case "image/png":
112
+ return "png";
113
+ case "image/svg+xml":
114
+ return "svg";
115
+ case "image/webp":
116
+ return "webp";
117
+ case "image/gif":
118
+ return "gif";
119
+ }
120
+
121
+ const pathname = safeUrlPathname(url);
122
+ const extension = pathname.match(/\.([a-z0-9]+)$/i)?.[1]?.toLowerCase();
123
+ return extension || "img";
124
+ }
125
+
126
+ function safeUrlPathname(url: string): string {
127
+ try {
128
+ return new URL(url).pathname;
129
+ } catch {
130
+ return url;
131
+ }
132
+ }
133
+
134
+ function sanitizeFilename(value: string): string {
135
+ return value.replace(/[^a-zA-Z0-9_-]/g, "_");
136
+ }
package/src/index.ts ADDED
@@ -0,0 +1,26 @@
1
+ export {
2
+ compileHtml,
3
+ compileReact,
4
+ compileVanjs,
5
+ getCompiler,
6
+ isCompilerMode,
7
+ } from "./compilers/index.ts";
8
+ export type {
9
+ FigmaApiResponse,
10
+ FigmaClientOptions,
11
+ FigmaFetcher,
12
+ } from "./external/figmaApi.ts";
13
+ export {
14
+ extractParamsFromURL,
15
+ fetchFigmaApiResponse,
16
+ fetchFigmaNode,
17
+ } from "./external/figmaApi.ts";
18
+ export type { FigmaHtmlPluginOptions } from "./plugin.ts";
19
+ export { FigmaHtmlPlugin } from "./plugin.ts";
20
+ export type {
21
+ CompilerMode,
22
+ ExtractedParams,
23
+ FigmaCompiler,
24
+ FigmaNode,
25
+ GeneratedFile,
26
+ } from "./types.ts";
package/src/plugin.ts ADDED
@@ -0,0 +1,76 @@
1
+ import { join } from "node:path";
2
+ import type { PluginDefinition } from "@design-embed/config";
3
+ import type {
4
+ SourcePlugin,
5
+ SourcePluginInput,
6
+ SourcePluginResult,
7
+ } from "@design-embed/core";
8
+ import { compileHtml } from "./compilers/index.ts";
9
+ import { extractParamsFromURL, fetchFigmaNode } from "./external/figmaApi.ts";
10
+ import { downloadFigmaImageFills } from "./external/imageDownloader.ts";
11
+
12
+ export interface FigmaHtmlPluginOptions {
13
+ url: string;
14
+ token?: string;
15
+ assetsDir?: string;
16
+ }
17
+
18
+ export class FigmaHtmlPlugin implements PluginDefinition, SourcePlugin {
19
+ readonly name = "figma-html";
20
+
21
+ constructor(private readonly options: FigmaHtmlPluginOptions) {}
22
+
23
+ async run(input: SourcePluginInput): Promise<SourcePluginResult> {
24
+ const { url, token: optionsToken, assetsDir = "assets" } = this.options;
25
+ const token = optionsToken ?? process.env.FIGMA_TOKEN;
26
+
27
+ if (!token) {
28
+ return {
29
+ diagnostics: [
30
+ {
31
+ code: "FIGMA_TOKEN_REQUIRED",
32
+ message:
33
+ "figma-html requires a Figma token. Pass token in the FigmaHtmlPlugin constructor or set the FIGMA_TOKEN environment variable.",
34
+ severity: "error",
35
+ },
36
+ ],
37
+ };
38
+ }
39
+
40
+ try {
41
+ const { fileKey, nodeId } = extractParamsFromURL(url);
42
+ const rootNode = await fetchFigmaNode(fileKey, nodeId, { token });
43
+ const downloadedImages = await downloadFigmaImageFills(
44
+ rootNode,
45
+ join(input.cwd, assetsDir),
46
+ { publicPath: assetsDir },
47
+ );
48
+ const [htmlFile] = compileHtml(rootNode);
49
+
50
+ return {
51
+ html: htmlFile?.contents,
52
+ diagnostics:
53
+ downloadedImages.length > 0
54
+ ? [
55
+ {
56
+ code: "FIGMA_ASSETS_DOWNLOADED",
57
+ message: `Downloaded ${downloadedImages.length} image asset(s).`,
58
+ severity: "info",
59
+ },
60
+ ]
61
+ : [],
62
+ };
63
+ } catch (error) {
64
+ const message = error instanceof Error ? error.message : String(error);
65
+ return {
66
+ diagnostics: [
67
+ {
68
+ code: "FIGMA_HTML_FAILED",
69
+ message,
70
+ severity: "error",
71
+ },
72
+ ],
73
+ };
74
+ }
75
+ }
76
+ }
package/src/types.ts ADDED
@@ -0,0 +1,85 @@
1
+ export interface ExtractedParams {
2
+ fileKey: string;
3
+ nodeId: string | null;
4
+ }
5
+
6
+ export 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
+
78
+ export interface GeneratedFile {
79
+ path: string;
80
+ contents: string;
81
+ }
82
+
83
+ export type CompilerMode = "react" | "html" | "vanjs";
84
+
85
+ export type FigmaCompiler = (node: FigmaNode) => GeneratedFile[];