@grida/refig 0.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.
package/dist/cli.d.mts ADDED
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/cli.mjs ADDED
@@ -0,0 +1,239 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ FigmaDocument,
4
+ FigmaRenderer,
5
+ collectExportsFromDocument,
6
+ exportSettingToRenderOptions,
7
+ figFileToRestLikeDocument,
8
+ iofigma
9
+ } from "./chunk-DAHUXARL.mjs";
10
+
11
+ // cli.ts
12
+ import {
13
+ readFileSync,
14
+ readdirSync,
15
+ mkdirSync,
16
+ writeFileSync,
17
+ existsSync,
18
+ statSync
19
+ } from "fs";
20
+ import path from "path";
21
+ import { program } from "commander";
22
+ var FORMAT_SET = /* @__PURE__ */ new Set(["png", "jpeg", "webp", "pdf", "svg"]);
23
+ var DOCUMENT_JSON = "document.json";
24
+ var IMAGES_SUBDIR = "images";
25
+ function formatFromOutFile(outPath) {
26
+ const ext = path.extname(outPath).replace(/^\./, "").toLowerCase();
27
+ if (ext === "jpg") return "jpeg";
28
+ return FORMAT_SET.has(ext) ? ext : "png";
29
+ }
30
+ var EXT_BY_FORMAT = {
31
+ png: "png",
32
+ jpeg: "jpeg",
33
+ jpg: "jpeg",
34
+ svg: "svg",
35
+ pdf: "pdf"
36
+ };
37
+ function sanitizeForFilename(s) {
38
+ return String(s).replace(/[:/\\]/g, "_").replace(/\s+/g, "_") || "_";
39
+ }
40
+ function resolveInput(inputPath, explicitImagesDir) {
41
+ const resolved = path.resolve(inputPath);
42
+ const stat = statSync(resolved);
43
+ if (stat.isDirectory()) {
44
+ const documentPath = path.join(resolved, DOCUMENT_JSON);
45
+ if (!existsSync(documentPath)) {
46
+ throw new Error(
47
+ `Input directory must contain ${DOCUMENT_JSON}; not found: ${documentPath}`
48
+ );
49
+ }
50
+ const imagesDir = path.join(resolved, IMAGES_SUBDIR);
51
+ const useImagesDir = existsSync(imagesDir) && statSync(imagesDir).isDirectory() ? imagesDir : void 0;
52
+ return {
53
+ documentPath,
54
+ imagesDir: explicitImagesDir ? path.resolve(explicitImagesDir) : useImagesDir,
55
+ isRestJson: true
56
+ };
57
+ }
58
+ return {
59
+ documentPath: resolved,
60
+ imagesDir: explicitImagesDir ? path.resolve(explicitImagesDir) : void 0,
61
+ isRestJson: resolved.toLowerCase().endsWith(".json")
62
+ };
63
+ }
64
+ function readImagesFromDir(dirPath) {
65
+ const out = {};
66
+ for (const file of readdirSync(dirPath)) {
67
+ const fullPath = path.join(dirPath, file);
68
+ if (!statSync(fullPath).isFile()) continue;
69
+ const ref = path.basename(file).replace(/\.[^.]+$/, "");
70
+ if (!ref) continue;
71
+ const buf = readFileSync(fullPath);
72
+ out[ref] = new Uint8Array(buf);
73
+ }
74
+ return out;
75
+ }
76
+ function exportAllOutputBasename(nodeId, suffix, format) {
77
+ const ext = EXT_BY_FORMAT[format] ?? "png";
78
+ const safeId = sanitizeForFilename(nodeId);
79
+ const safeSuffix = sanitizeForFilename(suffix);
80
+ const name = safeSuffix ? `${safeId}_${safeSuffix}` : safeId;
81
+ return `${name}.${ext}`;
82
+ }
83
+ async function runExportAll(documentPath, outDir, imagesDir) {
84
+ const isFig = documentPath.toLowerCase().endsWith(".fig");
85
+ let document;
86
+ let items;
87
+ let rendererOptions = {};
88
+ if (isFig) {
89
+ const bytes = new Uint8Array(readFileSync(documentPath));
90
+ const figFile = iofigma.kiwi.parseFile(bytes);
91
+ const restDoc = figFileToRestLikeDocument(figFile);
92
+ items = collectExportsFromDocument(restDoc);
93
+ document = new FigmaDocument(bytes);
94
+ const imagesMap = iofigma.kiwi.extractImages(figFile.zip_files);
95
+ const images = {};
96
+ imagesMap.forEach((imgBytes, ref) => {
97
+ images[ref] = imgBytes;
98
+ });
99
+ if (Object.keys(images).length > 0) {
100
+ rendererOptions = { images };
101
+ }
102
+ } else {
103
+ const json = JSON.parse(readFileSync(documentPath, "utf8"));
104
+ document = new FigmaDocument(json);
105
+ items = collectExportsFromDocument(
106
+ document.payload
107
+ );
108
+ if (imagesDir) {
109
+ rendererOptions = { images: readImagesFromDir(imagesDir) };
110
+ }
111
+ }
112
+ if (items.length === 0) {
113
+ process.stdout.write("No nodes with export settings found.\n");
114
+ return;
115
+ }
116
+ const renderer = new FigmaRenderer(document, rendererOptions);
117
+ try {
118
+ for (const { nodeId: nid, node, setting } of items) {
119
+ const options = exportSettingToRenderOptions(node, setting);
120
+ const result = await renderer.render(nid, options);
121
+ const basename = exportAllOutputBasename(
122
+ nid,
123
+ setting.suffix,
124
+ result.format
125
+ );
126
+ const filePath = path.join(outDir, basename);
127
+ writeFileSync(filePath, Buffer.from(result.data));
128
+ process.stdout.write(
129
+ `wrote ${filePath} (${result.mimeType}, ${result.data.byteLength} bytes)
130
+ `
131
+ );
132
+ }
133
+ process.stdout.write(`Exported ${items.length} file(s) to ${outDir}
134
+ `);
135
+ } finally {
136
+ renderer.dispose();
137
+ }
138
+ }
139
+ async function runSingleNode(documentPath, nodeId, outPath, opts) {
140
+ const format = (opts.format ?? formatFromOutFile(outPath)).toLowerCase();
141
+ if (!FORMAT_SET.has(format)) {
142
+ throw new Error(`Unsupported --format "${format}"`);
143
+ }
144
+ const isJson = documentPath.toLowerCase().endsWith(".json");
145
+ const document = isJson ? new FigmaDocument(JSON.parse(readFileSync(documentPath, "utf8"))) : new FigmaDocument(new Uint8Array(readFileSync(documentPath)));
146
+ const rendererOptions = isJson && opts.imagesDir ? { images: readImagesFromDir(opts.imagesDir) } : {};
147
+ const renderer = new FigmaRenderer(document, rendererOptions);
148
+ try {
149
+ const result = await renderer.render(nodeId, {
150
+ format,
151
+ width: opts.width,
152
+ height: opts.height,
153
+ scale: opts.scale
154
+ });
155
+ mkdirSync(path.dirname(outPath), { recursive: true });
156
+ writeFileSync(outPath, Buffer.from(result.data));
157
+ process.stdout.write(
158
+ `wrote ${outPath} (${result.mimeType}, ${result.data.byteLength} bytes)
159
+ `
160
+ );
161
+ } finally {
162
+ renderer.dispose();
163
+ }
164
+ }
165
+ async function main() {
166
+ program.name("refig").description(
167
+ "Headless Figma renderer \u2014 render .fig and REST API JSON to PNG/JPEG/WebP/PDF/SVG"
168
+ ).argument(
169
+ "<input>",
170
+ "Path to .fig, JSON file (REST API response), or directory containing document.json (and optionally images/)"
171
+ ).requiredOption(
172
+ "--out <path>",
173
+ "Output file path (single node) or output directory (--export-all)"
174
+ ).option(
175
+ "--images <dir>",
176
+ "Directory of image files for REST API document (optional; not used if <input> is a dir with images/)"
177
+ ).option(
178
+ "--node <id>",
179
+ "Figma node ID to render (required unless --export-all)"
180
+ ).option(
181
+ "--export-all",
182
+ "Export every node that has exportSettings (REST JSON only)"
183
+ ).option(
184
+ "--format <fmt>",
185
+ "png | jpeg | webp | pdf | svg (single-node only; default: from --out extension)"
186
+ ).option("--width <px>", "Viewport width (single-node only)", "1024").option("--height <px>", "Viewport height (single-node only)", "1024").option("--scale <n>", "Raster scale factor (single-node only)", "1").action(
187
+ async (input, options) => {
188
+ const outPath = String(options.out ?? "").trim();
189
+ const exportAll = options.exportAll === true;
190
+ const nodeId = String(options.node ?? "").trim();
191
+ const explicitImagesDir = typeof options.images === "string" ? options.images : void 0;
192
+ if (!outPath) {
193
+ program.error("--out is required");
194
+ }
195
+ const { documentPath, imagesDir, isRestJson } = resolveInput(
196
+ input.trim(),
197
+ explicitImagesDir
198
+ );
199
+ if (exportAll) {
200
+ if (nodeId) {
201
+ program.error("--node must not be used with --export-all");
202
+ }
203
+ const outDir = path.resolve(outPath);
204
+ if (existsSync(outDir)) {
205
+ const stat = statSync(outDir);
206
+ if (!stat.isDirectory()) {
207
+ program.error(
208
+ "--out must be a directory when using --export-all"
209
+ );
210
+ }
211
+ } else {
212
+ mkdirSync(outDir, { recursive: true });
213
+ }
214
+ await runExportAll(documentPath, outDir, imagesDir);
215
+ return;
216
+ }
217
+ if (!nodeId) {
218
+ program.error("--node is required (or use --export-all)");
219
+ }
220
+ const width = Number(options.width ?? 1024);
221
+ const height = Number(options.height ?? 1024);
222
+ const scale = Number(options.scale ?? 1);
223
+ await runSingleNode(documentPath, nodeId, path.resolve(outPath), {
224
+ format: typeof options.format === "string" ? options.format : void 0,
225
+ width,
226
+ height,
227
+ scale,
228
+ imagesDir
229
+ });
230
+ }
231
+ );
232
+ await program.parseAsync(process.argv);
233
+ }
234
+ main().catch((err) => {
235
+ const message = err instanceof Error ? err.message : String(err);
236
+ process.stderr.write(`${message}
237
+ `);
238
+ process.exit(1);
239
+ });
@@ -0,0 +1,17 @@
1
+ import { FigmaRenderer } from './browser.mjs';
2
+ export { ExportItem, FigmaDocument, RefigRenderFormat, RefigRenderOptions, RefigRenderResult, RefigRendererOptions, collectExportsFromDocument, exportSettingToRenderOptions, resolveMimeType } from './browser.mjs';
3
+ import '@figma/rest-api-spec';
4
+
5
+ /**
6
+ * @grida/refig — Node.js entrypoint
7
+ *
8
+ * Re-exports the full core API and adds Node-specific helpers (file I/O).
9
+ */
10
+
11
+ declare module "./lib" {
12
+ namespace FigmaDocument {
13
+ function fromFile(filePath: string): FigmaDocument;
14
+ }
15
+ }
16
+
17
+ export { FigmaRenderer, FigmaRenderer as default };
package/dist/index.mjs ADDED
@@ -0,0 +1,30 @@
1
+ import {
2
+ FigmaDocument,
3
+ FigmaRenderer,
4
+ collectExportsFromDocument,
5
+ exportSettingToRenderOptions,
6
+ resolveMimeType
7
+ } from "./chunk-DAHUXARL.mjs";
8
+
9
+ // index.ts
10
+ import { readFileSync } from "fs";
11
+ FigmaDocument.fromFile = function fromFile(filePath) {
12
+ const normalized = filePath.trim();
13
+ if (!normalized) {
14
+ throw new Error("FigmaDocument.fromFile: path must be non-empty");
15
+ }
16
+ if (normalized.toLowerCase().endsWith(".json")) {
17
+ const text = readFileSync(normalized, "utf-8");
18
+ return new FigmaDocument(JSON.parse(text));
19
+ }
20
+ return new FigmaDocument(new Uint8Array(readFileSync(normalized)));
21
+ };
22
+ var index_default = FigmaRenderer;
23
+ export {
24
+ FigmaDocument,
25
+ FigmaRenderer,
26
+ collectExportsFromDocument,
27
+ index_default as default,
28
+ exportSettingToRenderOptions,
29
+ resolveMimeType
30
+ };
package/package.json ADDED
@@ -0,0 +1,71 @@
1
+ {
2
+ "name": "@grida/refig",
3
+ "version": "0.0.0",
4
+ "private": false,
5
+ "description": "Headless Figma renderer — render .fig and REST API JSON to PNG/JPEG/WebP/PDF/SVG",
6
+ "keywords": [
7
+ "canvas",
8
+ "cli",
9
+ "fig",
10
+ "fig-file",
11
+ "figma",
12
+ "figma-renderer",
13
+ "grida",
14
+ "headless",
15
+ "jpeg",
16
+ "pdf",
17
+ "png",
18
+ "refig",
19
+ "skia",
20
+ "svg",
21
+ "wasm",
22
+ "webp"
23
+ ],
24
+ "homepage": "https://github.com/gridaco/grida/tree/main/packages/grida-canvas-sdk-render-figma",
25
+ "bugs": "https://github.com/gridaco/grida/issues",
26
+ "license": "MIT",
27
+ "author": "softmarshmallow",
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "https://github.com/gridaco/grida.git",
31
+ "directory": "packages/grida-canvas-sdk-render-figma"
32
+ },
33
+ "bin": {
34
+ "refig": "./dist/cli.mjs"
35
+ },
36
+ "files": [
37
+ "dist"
38
+ ],
39
+ "main": "./dist/index.mjs",
40
+ "module": "./dist/index.mjs",
41
+ "types": "./dist/index.d.mts",
42
+ "exports": {
43
+ ".": {
44
+ "types": "./dist/index.d.mts",
45
+ "import": "./dist/index.mjs",
46
+ "default": "./dist/index.mjs"
47
+ },
48
+ "./browser": {
49
+ "types": "./dist/browser.d.mts",
50
+ "import": "./dist/browser.mjs",
51
+ "default": "./dist/browser.mjs"
52
+ }
53
+ },
54
+ "publishConfig": {
55
+ "access": "public"
56
+ },
57
+ "dependencies": {
58
+ "@grida/canvas-wasm": "0.90.0-canary.6",
59
+ "commander": "^12.1.0"
60
+ },
61
+ "devDependencies": {
62
+ "@figma/rest-api-spec": "0.35.0",
63
+ "fflate": "^0.8.2",
64
+ "tsup": "^8.5.0"
65
+ },
66
+ "scripts": {
67
+ "build": "tsup",
68
+ "test": "vitest run",
69
+ "typecheck": "tsc --noEmit"
70
+ }
71
+ }