figma-token 0.1.0 → 0.1.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.
- package/dist/index.js +214 -11
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -7,17 +7,220 @@ import { Command, InvalidArgumentError } from "commander";
|
|
|
7
7
|
// src/sync.ts
|
|
8
8
|
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
9
9
|
import { dirname } from "path";
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
10
|
+
|
|
11
|
+
// ../core/dist/index.js
|
|
12
|
+
var supportedTypes = ["color", "spacing", "radius", "borderWidth", "size", "fontSize", "opacity"];
|
|
13
|
+
var dimensionTypes = ["spacing", "radius", "borderWidth", "size", "fontSize"];
|
|
14
|
+
var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
15
|
+
var isFiniteNumber = (value) => typeof value === "number" && Number.isFinite(value);
|
|
16
|
+
var isColorValue = (value) => isRecord(value) && ["r", "g", "b", "a"].every((key) => isFiniteNumber(value[key]));
|
|
17
|
+
var words = (value) => value.replace(/%/g, " percent ").replace(/(^|[^A-Za-z0-9])-([0-9])/g, "$1 negative $2").replace(/([0-9])\.([0-9])/g, "$1 dot $2").replace(/([a-z0-9])([A-Z])/g, "$1 $2").match(/[A-Za-z0-9]+/g) ?? [];
|
|
18
|
+
function isDesignTokenArray(value) {
|
|
19
|
+
return Array.isArray(value) && value.every(
|
|
20
|
+
(token) => isRecord(token) && typeof token.name === "string" && Array.isArray(token.path) && token.path.length > 0 && token.path.every((part) => typeof part === "string") && supportedTypes.includes(token.type) && (isFiniteNumber(token.value) || isColorValue(token.value))
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
var tokenTypeFromName = (name, resolvedType) => {
|
|
24
|
+
const first = name.split("/").filter(Boolean)[0]?.toLowerCase().replace(/[-_\s]/g, "");
|
|
25
|
+
if (resolvedType === "COLOR") return "color";
|
|
26
|
+
if (resolvedType !== "FLOAT") return void 0;
|
|
27
|
+
if (first === "spacing") return "spacing";
|
|
28
|
+
if (first === "radius") return "radius";
|
|
29
|
+
if (first === "borderwidth") return "borderWidth";
|
|
30
|
+
if (first === "size") return "size";
|
|
31
|
+
if (first === "fontsize") return "fontSize";
|
|
32
|
+
if (first === "opacity") return "opacity";
|
|
33
|
+
return void 0;
|
|
34
|
+
};
|
|
35
|
+
var tokenTypeFromCollection = (collection, resolvedType) => {
|
|
36
|
+
if (resolvedType !== "FLOAT" || typeof collection?.name !== "string") return void 0;
|
|
37
|
+
const name = words(collection.name.toLowerCase());
|
|
38
|
+
if (name.includes("spacing") || name.includes("gap")) return "spacing";
|
|
39
|
+
if (name.includes("radius") || name.includes("corner") || name.includes("shape")) return "radius";
|
|
40
|
+
if (name.includes("borderwidth") || name.includes("border") && name.includes("width") || name.includes("stroke")) return "borderWidth";
|
|
41
|
+
if (name.includes("fontsize") || name.includes("font") && name.includes("size") || name.includes("text")) return "fontSize";
|
|
42
|
+
if (name.includes("size")) return "size";
|
|
43
|
+
if (name.includes("opacity") || name.includes("alpha")) return "opacity";
|
|
44
|
+
return void 0;
|
|
45
|
+
};
|
|
46
|
+
var to255 = (value) => Math.round(value <= 1 ? value * 255 : value);
|
|
47
|
+
var clamp = (min, value, max) => Math.min(max, Math.max(min, value));
|
|
48
|
+
function pickModeId(collection, values, wanted) {
|
|
49
|
+
if (wanted && wanted in values) return wanted;
|
|
50
|
+
const defaultModeId = typeof collection?.defaultModeId === "string" ? collection.defaultModeId : void 0;
|
|
51
|
+
if (defaultModeId && defaultModeId in values) return defaultModeId;
|
|
52
|
+
const modes = Array.isArray(collection?.modes) ? collection.modes.filter(isRecord) : [];
|
|
53
|
+
const firstModeId = modes.map((mode) => mode.modeId).find((modeId) => typeof modeId === "string" && modeId in values);
|
|
54
|
+
return firstModeId ?? Object.keys(values)[0];
|
|
55
|
+
}
|
|
56
|
+
function resolveValue(variables, variable, modeId, onWarning, seen = /* @__PURE__ */ new Set()) {
|
|
57
|
+
const values = isRecord(variable.valuesByMode) ? variable.valuesByMode : {};
|
|
58
|
+
const name = typeof variable.name === "string" ? variable.name : modeId;
|
|
59
|
+
if (!(modeId in values)) {
|
|
60
|
+
onWarning?.(`${name} (mode: ${modeId})`, "alias-mode-mismatch");
|
|
61
|
+
return void 0;
|
|
62
|
+
}
|
|
63
|
+
const value = values[modeId];
|
|
64
|
+
if (!isRecord(value) || value.type !== "VARIABLE_ALIAS" || typeof value.id !== "string") return value;
|
|
65
|
+
if (seen.has(value.id)) {
|
|
66
|
+
onWarning?.(name, "alias-cycle");
|
|
67
|
+
return void 0;
|
|
68
|
+
}
|
|
69
|
+
seen.add(value.id);
|
|
70
|
+
const target = variables[value.id];
|
|
71
|
+
if (!isRecord(target)) {
|
|
72
|
+
onWarning?.(name, "alias-target-missing");
|
|
73
|
+
return void 0;
|
|
74
|
+
}
|
|
75
|
+
return resolveValue(variables, target, modeId, onWarning, seen);
|
|
76
|
+
}
|
|
77
|
+
function normalizeValue(value, type) {
|
|
78
|
+
if (type === "color" && isRecord(value) && ["r", "g", "b"].every((key) => isFiniteNumber(value[key])) && (value.a === void 0 || isFiniteNumber(value.a))) {
|
|
79
|
+
return {
|
|
80
|
+
r: clamp(0, to255(value.r), 255),
|
|
81
|
+
g: clamp(0, to255(value.g), 255),
|
|
82
|
+
b: clamp(0, to255(value.b), 255),
|
|
83
|
+
a: clamp(0, typeof value.a === "number" ? value.a : 1, 1)
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
if (type !== "color" && isFiniteNumber(value)) return value;
|
|
87
|
+
return void 0;
|
|
88
|
+
}
|
|
89
|
+
function normalizeFigmaVariables(input, options = {}) {
|
|
90
|
+
if (!isRecord(input)) return [];
|
|
91
|
+
const root = isRecord(input.meta) ? input.meta : input;
|
|
92
|
+
const variables = isRecord(root.variables) ? root.variables : {};
|
|
93
|
+
const collections = isRecord(root.variableCollections) ? root.variableCollections : {};
|
|
94
|
+
const result = [];
|
|
95
|
+
for (const variable of Object.values(variables)) {
|
|
96
|
+
if (!isRecord(variable) || typeof variable.name !== "string" || variable.deletedButReferenced === true) continue;
|
|
97
|
+
const collection = typeof variable.variableCollectionId === "string" && isRecord(collections[variable.variableCollectionId]) ? collections[variable.variableCollectionId] : void 0;
|
|
98
|
+
const typeFromName = tokenTypeFromName(variable.name, variable.resolvedType);
|
|
99
|
+
const typeFromCollection = tokenTypeFromCollection(collection, variable.resolvedType);
|
|
100
|
+
const type = typeFromName ?? typeFromCollection;
|
|
101
|
+
if (!type) {
|
|
102
|
+
options.onUnsupported?.(variable.name, variable.resolvedType === "FLOAT" ? "unclassified-float" : "unsupported-type", typeof collection?.name === "string" ? collection.name : void 0);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
const path = variable.name.split("/").filter(Boolean);
|
|
106
|
+
if (!path.length) continue;
|
|
107
|
+
const values = isRecord(variable.valuesByMode) ? variable.valuesByMode : {};
|
|
108
|
+
const modeId = pickModeId(collection, values, options.modeId);
|
|
109
|
+
if (!modeId) continue;
|
|
110
|
+
const warnAlias = (name, reason) => options.onAliasWarning?.(name, reason, typeof collection?.name === "string" ? collection.name : void 0);
|
|
111
|
+
const selectedValue = values[modeId];
|
|
112
|
+
const rawValue = resolveValue(variables, variable, modeId, warnAlias);
|
|
113
|
+
const value = normalizeValue(rawValue, type);
|
|
114
|
+
if (value === void 0) {
|
|
115
|
+
if (isRecord(selectedValue) && selectedValue.type === "VARIABLE_ALIAS" && rawValue !== void 0) warnAlias(variable.name, "alias-type-mismatch");
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
const modes = Array.isArray(collection?.modes) ? collection.modes.filter(isRecord) : [];
|
|
119
|
+
const mode = modes.find((candidate) => candidate.modeId === modeId);
|
|
120
|
+
result.push({
|
|
121
|
+
name: variable.name,
|
|
122
|
+
path: typeFromName ? path : [type, ...path],
|
|
123
|
+
type,
|
|
124
|
+
value,
|
|
125
|
+
...typeof collection?.name === "string" ? { collection: collection.name } : {},
|
|
126
|
+
...typeof mode?.name === "string" ? { mode: mode.name } : {},
|
|
127
|
+
...typeof variable.description === "string" && variable.description ? { description: variable.description } : {}
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
return result.sort(compareTokens);
|
|
131
|
+
}
|
|
132
|
+
var identity = (token) => `${token.path.join("/")}\0${token.collection ?? ""}\0${token.mode ?? ""}`;
|
|
133
|
+
var compareTokens = (a, b) => (a.collection ?? "").localeCompare(b.collection ?? "") || (a.mode ?? "").localeCompare(b.mode ?? "") || a.path.join("/").localeCompare(b.path.join("/"));
|
|
134
|
+
function diffTokens(previous, current) {
|
|
135
|
+
const before = new Map(previous.map((token) => [identity(token), token]));
|
|
136
|
+
const after = new Map(current.map((token) => [identity(token), token]));
|
|
137
|
+
const diffs = [];
|
|
138
|
+
for (const [key, token] of after) {
|
|
139
|
+
const old = before.get(key);
|
|
140
|
+
if (!old) diffs.push({ type: "added", token });
|
|
141
|
+
else if (JSON.stringify(old) !== JSON.stringify(token)) diffs.push({ type: "changed", token });
|
|
142
|
+
}
|
|
143
|
+
for (const [key, token] of before) if (!after.has(key)) diffs.push({ type: "removed", token });
|
|
144
|
+
const order = { added: 0, changed: 1, removed: 2 };
|
|
145
|
+
return diffs.sort((a, b) => compareTokens(a.token, b.token) || order[a.type] - order[b.type]);
|
|
146
|
+
}
|
|
147
|
+
var renderTokensJson = (tokens) => `${JSON.stringify(tokens, null, 2)}
|
|
148
|
+
`;
|
|
149
|
+
var camelKey = (value, fallback = "default") => {
|
|
150
|
+
let key = words(value).map((word, index) => index ? word[0].toUpperCase() + word.slice(1).toLowerCase() : word.toLowerCase()).join("") || fallback;
|
|
151
|
+
if (!/^[A-Za-z_$]/.test(key)) key = `_${key}`;
|
|
152
|
+
return key;
|
|
153
|
+
};
|
|
154
|
+
var kebabKey = (value) => words(value).map((word) => word.toLowerCase()).join("-");
|
|
155
|
+
var generateVariableName = (token) => {
|
|
156
|
+
const prefix = token.type === "fontSize" ? "font-size" : token.type === "borderWidth" ? "border-width" : token.type;
|
|
157
|
+
const first = token.path[0]?.toLowerCase().replace(/[-_\s]/g, "");
|
|
158
|
+
const hasPrefix = first === prefix.replace(/-/g, "");
|
|
159
|
+
const rest = token.path.slice(hasPrefix ? 1 : 0).map(kebabKey).filter(Boolean).join("-");
|
|
160
|
+
return `${prefix}${rest ? `-${rest}` : ""}`;
|
|
161
|
+
};
|
|
162
|
+
var hex = (value) => clamp(0, Math.round(value), 255).toString(16).padStart(2, "0");
|
|
163
|
+
var colorHex = (value) => `#${hex(value.r)}${hex(value.g)}${hex(value.b)}${value.a < 1 ? hex(value.a * 255) : ""}`;
|
|
164
|
+
var formatValue = (token) => {
|
|
165
|
+
if (token.type === "color") return colorHex(token.value);
|
|
166
|
+
if (dimensionTypes.includes(token.type)) return `${token.value}px`;
|
|
167
|
+
return String(token.value);
|
|
168
|
+
};
|
|
169
|
+
var formatThemeValue = (token) => token.type === "opacity" ? token.value : formatValue(token);
|
|
170
|
+
function renderCssLike(tokens, nameFor, wrap) {
|
|
171
|
+
const lines = tokens.map((token) => `${wrap ? " " : ""}${nameFor(token)}: ${formatValue(token)};`);
|
|
172
|
+
return wrap ? `${wrap[0]}
|
|
173
|
+
${lines.join("\n")}
|
|
174
|
+
${wrap[1]}
|
|
175
|
+
` : `${lines.join("\n")}
|
|
176
|
+
`;
|
|
177
|
+
}
|
|
178
|
+
var cssName = (token) => `--${generateVariableName(token)}`;
|
|
179
|
+
var scssName = (token) => `$${generateVariableName(token)}`;
|
|
180
|
+
var renderCssVariables = (tokens) => renderCssLike(tokens, cssName, [":root {", "}"]);
|
|
181
|
+
var renderScssVariables = (tokens) => renderCssLike(tokens, scssName);
|
|
182
|
+
var renderTailwindTheme = (tokens) => renderCssLike(tokens, cssName, ["@theme {", "}"]);
|
|
183
|
+
function dtcgValue(token) {
|
|
184
|
+
if (token.type === "color") return { $type: "color", $value: colorHex(token.value) };
|
|
185
|
+
if (dimensionTypes.includes(token.type)) return { $type: "dimension", $value: { value: token.value, unit: "px" } };
|
|
186
|
+
return { $type: "number", $value: token.value };
|
|
187
|
+
}
|
|
188
|
+
function renderDtcgJson(tokens) {
|
|
189
|
+
const root = {};
|
|
190
|
+
for (const token of tokens) {
|
|
191
|
+
let cursor = root;
|
|
192
|
+
token.path.forEach((part, index) => {
|
|
193
|
+
if (index === token.path.length - 1) {
|
|
194
|
+
if (part in cursor) throw new Error(`Duplicate DTCG path: ${token.path.join(".")}`);
|
|
195
|
+
cursor[part] = dtcgValue(token);
|
|
196
|
+
} else {
|
|
197
|
+
if (part in cursor && !isRecord(cursor[part])) throw new Error(`Duplicate DTCG path: ${token.path.join(".")}`);
|
|
198
|
+
cursor = cursor[part] ?? (cursor[part] = {});
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
return `${JSON.stringify(root, null, 2)}
|
|
203
|
+
`;
|
|
204
|
+
}
|
|
205
|
+
function renderTheme(tokens, exportName = "theme") {
|
|
206
|
+
if (!/^[A-Za-z_$][\w$]*$/.test(exportName)) throw new Error(`Invalid TypeScript export name: ${exportName}`);
|
|
207
|
+
const root = {};
|
|
208
|
+
for (const token of tokens) {
|
|
209
|
+
const path = token.path.map((part) => camelKey(part, "token"));
|
|
210
|
+
let cursor = root;
|
|
211
|
+
path.forEach((part, index) => {
|
|
212
|
+
if (index === path.length - 1) {
|
|
213
|
+
if (part in cursor) throw new Error(`Duplicate theme path: ${path.join(".")}`);
|
|
214
|
+
cursor[part] = formatThemeValue(token);
|
|
215
|
+
} else {
|
|
216
|
+
if (part in cursor && !isRecord(cursor[part])) throw new Error(`Duplicate theme path: ${path.join(".")}`);
|
|
217
|
+
cursor = cursor[part] ?? (cursor[part] = {});
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
return `export const ${exportName} = ${JSON.stringify(root, null, 2)} as const;
|
|
222
|
+
`;
|
|
223
|
+
}
|
|
21
224
|
|
|
22
225
|
// src/figma/fetchFigmaVariables.ts
|
|
23
226
|
var FIGMA_API = "https://api.figma.com/v1";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "figma-token",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Export Figma Variables to design token files.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -18,11 +18,11 @@
|
|
|
18
18
|
],
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"commander": "^14.0.0",
|
|
21
|
-
"dotenv": "^17.0.0"
|
|
22
|
-
"@lee090626/core": "0.1.0"
|
|
21
|
+
"dotenv": "^17.0.0"
|
|
23
22
|
},
|
|
24
23
|
"devDependencies": {
|
|
25
|
-
"tsup": "^8.5.0"
|
|
24
|
+
"tsup": "^8.5.0",
|
|
25
|
+
"@lee090626/core": "0.1.0"
|
|
26
26
|
},
|
|
27
27
|
"scripts": {
|
|
28
28
|
"build": "tsup",
|