@toghen/figma 0.0.0-stage → 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.
- package/LICENSE +21 -0
- package/README.md +18 -2
- package/cli/bin.mjs +19 -0
- package/cli/bridge.ts +243 -0
- package/cli/check.ts +62 -0
- package/cli/figcraft.ts +764 -0
- package/cli/figcraftApply.ts +1058 -0
- package/cli/figcraftFile.ts +476 -0
- package/cli/figcraftLibrary.ts +147 -0
- package/cli/figcraftRecord.ts +121 -0
- package/cli/figcraftScene.ts +661 -0
- package/cli/main.ts +218 -0
- package/cli/members.ts +65 -0
- package/cli/run.ts +756 -0
- package/component-files.md +665 -0
- package/dist/code.js +24147 -0
- package/dist/manifest.json +23 -0
- package/dist/ui.html +6918 -0
- package/guide.md +985 -0
- package/main/code.ts +50 -0
- package/main/components/apply.ts +704 -0
- package/main/components/endOfRun.ts +168 -0
- package/main/components/errors.ts +17 -0
- package/main/components/fieldWriter.ts +497 -0
- package/main/components/file.ts +263 -0
- package/main/components/receipt.ts +228 -0
- package/main/components/resolve.ts +1193 -0
- package/main/components/run.ts +273 -0
- package/main/components/snapshot.ts +341 -0
- package/main/components/stamp.ts +312 -0
- package/main/components/update.ts +465 -0
- package/main/dispatcher.ts +33 -0
- package/main/engines/batchRunner.ts +200 -0
- package/main/engines/ioAdapter.ts +132 -0
- package/main/engines/operationExecutor.ts +512 -0
- package/main/engines/propertyMapper.ts +221 -0
- package/main/engines/queryEngine.ts +45 -0
- package/main/engines/styleSyncEngine.ts +444 -0
- package/main/engines/variableSyncEngine.ts +561 -0
- package/main/handlers/auditSelection.ts +60 -0
- package/main/handlers/boundLayers.ts +51 -0
- package/main/handlers/cancelOperation.ts +18 -0
- package/main/handlers/executeApplyBindings.ts +69 -0
- package/main/handlers/executeScaffold.ts +59 -0
- package/main/handlers/executeStyleSync.ts +121 -0
- package/main/handlers/executeVariableSync.ts +82 -0
- package/main/handlers/exportDocument.ts +114 -0
- package/main/handlers/getSelection.ts +43 -0
- package/main/handlers/index.ts +30 -0
- package/main/handlers/inspectCorrespondence.ts +172 -0
- package/main/handlers/loadConventions.ts +86 -0
- package/main/handlers/loadDocument.ts +253 -0
- package/main/handlers/lock.ts +270 -0
- package/main/handlers/planApply.ts +157 -0
- package/main/handlers/planApplyBindings.ts +79 -0
- package/main/handlers/planScaffold.ts +84 -0
- package/main/handlers/planStyleSync.ts +49 -0
- package/main/handlers/planVariableSync.ts +49 -0
- package/main/handlers/readSelectionBindings.ts +160 -0
- package/main/handlers/resizeUi.ts +105 -0
- package/main/handlers/runComponent.ts +65 -0
- package/main/handlers/templates.ts +462 -0
- package/main/handlers/tokenGraph.ts +122 -0
- package/main/runtimeState.ts +66 -0
- package/main/utils/abortController.ts +11 -0
- package/main/utils/boundVariables.ts +84 -0
- package/main/utils/buildPayloads.ts +85 -0
- package/main/utils/correspondence.ts +658 -0
- package/main/utils/designSource.ts +122 -0
- package/main/utils/figmaCollectionIndex.ts +120 -0
- package/main/utils/figmaStyleAdapter.ts +268 -0
- package/main/utils/figmaTokenAdapter.ts +255 -0
- package/main/utils/flattenDesignSystem.ts +38 -0
- package/main/utils/folds.ts +203 -0
- package/main/utils/fontStack.ts +57 -0
- package/main/utils/lists.ts +4 -0
- package/main/utils/lockStore.ts +72 -0
- package/main/utils/moves.ts +67 -0
- package/main/utils/pageRoots.ts +58 -0
- package/main/utils/planSymbols.ts +96 -0
- package/main/utils/purpose.ts +351 -0
- package/main/utils/recentStore.ts +166 -0
- package/main/utils/resolverModes.ts +271 -0
- package/main/utils/rootRecord.ts +137 -0
- package/main/utils/shardedData.ts +142 -0
- package/main/utils/standing.ts +106 -0
- package/main/utils/styleTarget.ts +51 -0
- package/main/utils/sum.ts +612 -0
- package/main/utils/symbolicIdRegistry.ts +26 -0
- package/main/utils/syncState.ts +133 -0
- package/main/utils/templateStore.ts +73 -0
- package/main/utils/tokenPath.ts +28 -0
- package/main/utils/tokenStamps.ts +91 -0
- package/manifest.json +15 -0
- package/package.json +64 -4
- package/reference.md +1609 -0
- package/shared/components/artboard.ts +126 -0
- package/shared/components/axes.ts +420 -0
- package/shared/components/bindings.ts +64 -0
- package/shared/components/blueprint.ts +248 -0
- package/shared/components/check.ts +705 -0
- package/shared/components/diff.ts +668 -0
- package/shared/components/fields.ts +404 -0
- package/shared/components/groups.ts +306 -0
- package/shared/components/index.ts +140 -0
- package/shared/components/members.ts +280 -0
- package/shared/components/model.ts +273 -0
- package/shared/components/order.ts +53 -0
- package/shared/components/read.ts +480 -0
- package/shared/components/stack.ts +124 -0
- package/shared/components/steps.ts +186 -0
- package/shared/components/validate.ts +548 -0
- package/shared/conventions.ts +28 -0
- package/shared/inputs.ts +40 -0
- package/shared/operations.ts +233 -0
- package/shared/pendingNames.ts +39 -0
- package/shared/protocol.ts +588 -0
- package/shared/tokenTypeMap.ts +21 -0
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
import type { Operation, StyleBinding } from '../../shared/operations.js';
|
|
2
|
+
import { FIELDS } from '../../shared/components/fields.js';
|
|
3
|
+
import { resolvedFor, writeToken } from '../components/fieldWriter.js';
|
|
4
|
+
import { getStamp, receiptOf, setStamp, strip } from '../components/stamp.js';
|
|
5
|
+
import { reportOf, reportedScopes, stampWritten } from '../utils/tokenStamps.js';
|
|
6
|
+
|
|
7
|
+
type BindableNode = SceneNode & {
|
|
8
|
+
setBoundVariable?: (field: string, variable: Variable | null) => void;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
function applyTextStyleProps(style: TextStyle, props: Record<string, unknown>): void {
|
|
12
|
+
if (typeof props.fontFamily === 'string' && typeof props.fontStyle === 'string') {
|
|
13
|
+
style.fontName = {
|
|
14
|
+
family: props.fontFamily,
|
|
15
|
+
style: props.fontStyle,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (typeof props.fontSize === 'number') {
|
|
20
|
+
style.fontSize = props.fontSize;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (props.lineHeight && typeof props.lineHeight === 'object') {
|
|
24
|
+
style.lineHeight = props.lineHeight as LineHeight;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (props.letterSpacing && typeof props.letterSpacing === 'object') {
|
|
28
|
+
style.letterSpacing = props.letterSpacing as LetterSpacing;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (typeof props.textCase === 'string') {
|
|
32
|
+
style.textCase = props.textCase as TextCase;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (typeof props.textDecoration === 'string') {
|
|
36
|
+
style.textDecoration = props.textDecoration as TextDecoration;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function applyEffectStyleProps(style: EffectStyle, props: Record<string, unknown>): void {
|
|
41
|
+
if (Array.isArray(props.effects)) {
|
|
42
|
+
style.effects = props.effects as ReadonlyArray<Effect>;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function applyPaintStyleProps(style: PaintStyle, props: Record<string, unknown>): void {
|
|
47
|
+
if (Array.isArray(props.paints)) {
|
|
48
|
+
style.paints = props.paints as ReadonlyArray<Paint>;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function applyStyleProps(style: BaseStyle, props: Record<string, unknown>): void {
|
|
53
|
+
switch (style.type) {
|
|
54
|
+
case 'TEXT':
|
|
55
|
+
applyTextStyleProps(style as TextStyle, props);
|
|
56
|
+
return;
|
|
57
|
+
case 'EFFECT':
|
|
58
|
+
applyEffectStyleProps(style as EffectStyle, props);
|
|
59
|
+
return;
|
|
60
|
+
case 'PAINT':
|
|
61
|
+
applyPaintStyleProps(style as PaintStyle, props);
|
|
62
|
+
return;
|
|
63
|
+
default:
|
|
64
|
+
throw new Error(`Unsupported style type: ${(style as BaseStyle).type}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Every text style field a style plan may bind — the table in `styleSyncEngine.ts`. */
|
|
69
|
+
const BINDABLE_TEXT_FIELDS: readonly VariableBindableTextField[] = [
|
|
70
|
+
'fontFamily',
|
|
71
|
+
'fontStyle',
|
|
72
|
+
'fontWeight',
|
|
73
|
+
'fontSize',
|
|
74
|
+
'letterSpacing',
|
|
75
|
+
'lineHeight',
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
type BoundField = readonly [StyleBinding, Variable];
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Fetches every variable a style binds to. Called before the style is touched,
|
|
82
|
+
* so a missing variable fails the operation with nothing written — and, on a
|
|
83
|
+
* create, with no empty style left behind.
|
|
84
|
+
*/
|
|
85
|
+
async function variablesFor(bindings: readonly StyleBinding[] = []): Promise<BoundField[]> {
|
|
86
|
+
return Promise.all(bindings.map(async (binding): Promise<BoundField> => [binding, await getVariableById(binding.variableId)]));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Writes a style's props, then binds its fields. */
|
|
90
|
+
function writeStyle(style: BaseStyle, props: Record<string, unknown>, bound: readonly BoundField[], existing: boolean): void {
|
|
91
|
+
if (style.type === 'TEXT') {
|
|
92
|
+
const text = style as TextStyle;
|
|
93
|
+
// A bound field ignores a written value, so a member that is no longer a
|
|
94
|
+
// reference would go on following the old variable. Clear, then write.
|
|
95
|
+
if (existing) {
|
|
96
|
+
for (const field of BINDABLE_TEXT_FIELDS) text.setBoundVariable(field, null);
|
|
97
|
+
}
|
|
98
|
+
applyTextStyleProps(text, props);
|
|
99
|
+
for (const [binding, variable] of bound) {
|
|
100
|
+
text.setBoundVariable(binding.field as VariableBindableTextField, variable);
|
|
101
|
+
}
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (style.type === 'EFFECT' && bound.length > 0) {
|
|
106
|
+
// Assigning `effects` replaces every earlier binding along with the layers,
|
|
107
|
+
// so unlike a text style there is nothing to clear first.
|
|
108
|
+
const effects = [...(props.effects as Effect[])];
|
|
109
|
+
for (const [binding, variable] of bound) {
|
|
110
|
+
const index = binding.index ?? 0;
|
|
111
|
+
if (!effects[index]) {
|
|
112
|
+
throw new Error(`Effect style has no layer ${index} to bind ${binding.field} to.`);
|
|
113
|
+
}
|
|
114
|
+
effects[index] = figma.variables.setBoundVariableForEffect(effects[index], binding.field as VariableBindableEffectField, variable);
|
|
115
|
+
}
|
|
116
|
+
(style as EffectStyle).effects = effects;
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
applyStyleProps(style, props);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Node types that are not part of the scene graph and can never be bound to.
|
|
125
|
+
*
|
|
126
|
+
* `getNodeByIdAsync` resolves any node in the document, including the document
|
|
127
|
+
* root and pages, so the result has to be narrowed before it is treated as a
|
|
128
|
+
* `SceneNode`. The old code asserted `as SceneNode` outright, which would have
|
|
129
|
+
* let a page id through to a property binding.
|
|
130
|
+
*/
|
|
131
|
+
const NON_SCENE_NODE_TYPES: ReadonlySet<string> = new Set(['DOCUMENT', 'PAGE']);
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Resolves a node id to a scene node.
|
|
135
|
+
*
|
|
136
|
+
* Uses the **async** accessor deliberately. `figma.getNodeById` is deprecated
|
|
137
|
+
* and, per the Plugin API typings, *"will throw an exception if the plugin
|
|
138
|
+
* manifest contains `documentAccess: dynamic-page`"* — which this plugin's
|
|
139
|
+
* manifest does. Every `BIND_VARIABLE` and `UNBIND_VARIABLE` therefore failed in
|
|
140
|
+
* a real Figma file, while the tests passed because no mock defined the method
|
|
141
|
+
* and no test reached the binding branch.
|
|
142
|
+
*/
|
|
143
|
+
async function getSceneNodeById(nodeId: string): Promise<SceneNode> {
|
|
144
|
+
const node = await figma.getNodeByIdAsync(nodeId);
|
|
145
|
+
if (!node) {
|
|
146
|
+
throw new Error(`Node not found: ${nodeId}`);
|
|
147
|
+
}
|
|
148
|
+
if (node.removed) {
|
|
149
|
+
throw new Error(`Node has been removed: ${nodeId}`);
|
|
150
|
+
}
|
|
151
|
+
if (NON_SCENE_NODE_TYPES.has(node.type)) {
|
|
152
|
+
throw new Error(`Node ${nodeId} is a ${node.type}, which cannot be bound to.`);
|
|
153
|
+
}
|
|
154
|
+
return node as SceneNode;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function getVariableById(variableId: string): Promise<Variable> {
|
|
158
|
+
const variable = await figma.variables.getVariableByIdAsync(variableId);
|
|
159
|
+
if (!variable) {
|
|
160
|
+
throw new Error(`Variable not found: ${variableId}`);
|
|
161
|
+
}
|
|
162
|
+
return variable;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function bindPaintColor(node: SceneNode, property: 'fills[0].color' | 'strokes[0].color', variable: Variable | null): void {
|
|
166
|
+
const key = property.startsWith('fills[') ? 'fills' : 'strokes';
|
|
167
|
+
const paints = (node as unknown as Record<string, unknown>)[key];
|
|
168
|
+
|
|
169
|
+
if (!Array.isArray(paints) || paints.length === 0) {
|
|
170
|
+
throw new Error(`Property ${property} requires at least one paint on node ${node.id}`);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const firstPaint = paints[0] as Paint;
|
|
174
|
+
if (firstPaint.type !== 'SOLID') {
|
|
175
|
+
throw new Error(`Property ${property} requires first paint to be SOLID on node ${node.id}`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const bound = figma.variables.setBoundVariableForPaint(firstPaint, 'color', variable);
|
|
179
|
+
(node as unknown as Record<string, unknown>)[key] = [bound, ...paints.slice(1)] as Paint[];
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function bindNodeProperty(node: SceneNode, property: string, variable: Variable | null): Promise<void> {
|
|
183
|
+
if (property === 'fills[0].color' || property === 'strokes[0].color') {
|
|
184
|
+
bindPaintColor(node, property, variable);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const bindable = node as BindableNode;
|
|
189
|
+
if (!bindable.setBoundVariable) {
|
|
190
|
+
throw new Error(`Node ${node.id} does not support variable binding for property ${property}`);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// A field of the field table is bound by the field writer, as a build and Apply bind it: one binding path.
|
|
194
|
+
// A convention names its variable, not a token, so the variable stands for the token.
|
|
195
|
+
if (variable && Object.prototype.hasOwnProperty.call(FIELDS, property) && FIELDS[property].token?.bind?.call === 'field') {
|
|
196
|
+
await writeToken(node, property, variable.name, resolvedFor(variable));
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
bindable.setBoundVariable(property, variable);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Writes one operation to a file and answers the id of what it made or changed. */
|
|
204
|
+
export type OperationWriter = (op: Operation) => Promise<string | undefined>;
|
|
205
|
+
|
|
206
|
+
let writer: OperationWriter | undefined;
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Sends every operation to another file than Figma's: the command line's FigCraft target
|
|
210
|
+
* (`cli/figcraft.ts`) writes through FigCraft's commands. `undefined` puts Figma's API back.
|
|
211
|
+
*/
|
|
212
|
+
export function writeOperationsWith(next: OperationWriter | undefined): void {
|
|
213
|
+
writer = next;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Writes one operation, to Figma or through the writer, then stamps what it wrote. The stamping reads the object back
|
|
218
|
+
* through `figma`, Figma's own or the one the writer's target answers with, so both targets stamp the same fields.
|
|
219
|
+
*/
|
|
220
|
+
export async function executeOperation(op: Operation): Promise<string | undefined> {
|
|
221
|
+
const id = writer ? await writer(op) : await writeToFigma(op);
|
|
222
|
+
await stampAfter(op, id);
|
|
223
|
+
return id;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async function writeToFigma(op: Operation): Promise<string | undefined> {
|
|
227
|
+
switch (op.kind) {
|
|
228
|
+
case 'CREATE_COLLECTION': {
|
|
229
|
+
if (op.modes.length === 0) {
|
|
230
|
+
throw new Error(`CREATE_COLLECTION requires at least one mode: ${op.name}`);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// The one internally multi-step operation, and so the only one that needs
|
|
234
|
+
// to be transactional. A new collection always arrives with exactly one
|
|
235
|
+
// mode, so this is rename-then-add-the-rest.
|
|
236
|
+
//
|
|
237
|
+
// If an addMode hits the plan's mode cap partway through, the collection
|
|
238
|
+
// must not survive: it would exist by name, so the next run would take the
|
|
239
|
+
// "already exists" branch, emit ADD_MODE, and hit the same cap forever —
|
|
240
|
+
// leaving an empty collection the user has to find and delete by hand,
|
|
241
|
+
// with nothing saying why.
|
|
242
|
+
const collection = figma.variables.createVariableCollection(op.name);
|
|
243
|
+
try {
|
|
244
|
+
collection.renameMode(collection.modes[0].modeId, op.modes[0]);
|
|
245
|
+
for (let i = 1; i < op.modes.length; i += 1) {
|
|
246
|
+
collection.addMode(op.modes[i]);
|
|
247
|
+
}
|
|
248
|
+
} catch (error) {
|
|
249
|
+
collection.remove();
|
|
250
|
+
throw new Error(
|
|
251
|
+
`Could not create collection "${op.name}" with ${op.modes.length} modes: ` +
|
|
252
|
+
`${error instanceof Error ? error.message : String(error)}`,
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
return collection.id;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
case 'RENAME_VARIABLE': {
|
|
259
|
+
const variable = await figma.variables.getVariableByIdAsync(op.variableId);
|
|
260
|
+
if (!variable) {
|
|
261
|
+
throw new Error(`Variable not found: ${op.variableId}`);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
variable.name = op.name;
|
|
265
|
+
return variable.id;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
case 'SET_CODE_SYNTAX': {
|
|
269
|
+
const variable = await figma.variables.getVariableByIdAsync(op.variableId);
|
|
270
|
+
if (!variable) {
|
|
271
|
+
throw new Error(`Variable not found: ${op.variableId}`);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
variable.setVariableCodeSyntax(op.platform, op.value);
|
|
275
|
+
return variable.id;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
case 'ADD_MODE': {
|
|
279
|
+
const collection = await figma.variables.getVariableCollectionByIdAsync(op.collectionId);
|
|
280
|
+
if (!collection) {
|
|
281
|
+
throw new Error(`Collection not found: ${op.collectionId}`);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
collection.addMode(op.modeName);
|
|
285
|
+
return collection.id;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
case 'DELETE_MODE': {
|
|
289
|
+
const collection = await figma.variables.getVariableCollectionByIdAsync(op.collectionId);
|
|
290
|
+
if (!collection) {
|
|
291
|
+
throw new Error(`Collection not found: ${op.collectionId}`);
|
|
292
|
+
}
|
|
293
|
+
// The default mode is the first, and Figma would promote the next one if it went: the plan never names it, and neither does this.
|
|
294
|
+
if (collection.modes[0]?.modeId === op.modeId) {
|
|
295
|
+
throw new Error(`The default mode "${op.modeName}" of collection "${op.collectionName}" is never deleted.`);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
collection.removeMode(op.modeId);
|
|
299
|
+
return collection.id;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
case 'DELETE_VARIABLE': {
|
|
303
|
+
const variable = await figma.variables.getVariableByIdAsync(op.variableId);
|
|
304
|
+
if (!variable) {
|
|
305
|
+
throw new Error(`Variable not found: ${op.variableId}`);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
variable.remove();
|
|
309
|
+
return op.variableId;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
case 'CREATE_VARIABLE': {
|
|
313
|
+
const collection = await figma.variables.getVariableCollectionByIdAsync(op.collectionId);
|
|
314
|
+
if (!collection) {
|
|
315
|
+
throw new Error(`Collection not found: ${op.collectionId}`);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const variable = figma.variables.createVariable(op.path, collection, op.variableType);
|
|
319
|
+
if (op.scopes) variable.scopes = op.scopes;
|
|
320
|
+
return variable.id;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
case 'SET_VARIABLE_SCOPES': {
|
|
324
|
+
const variable = await figma.variables.getVariableByIdAsync(op.variableId);
|
|
325
|
+
if (!variable) {
|
|
326
|
+
throw new Error(`Variable not found: ${op.variableId}`);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
variable.scopes = op.scopes;
|
|
330
|
+
return variable.id;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
case 'SET_VARIABLE_VALUE': {
|
|
334
|
+
const variable = await figma.variables.getVariableByIdAsync(op.variableId);
|
|
335
|
+
if (!variable) {
|
|
336
|
+
throw new Error(`Variable not found: ${op.variableId}`);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
variable.setValueForMode(op.modeId, op.value);
|
|
340
|
+
return variable.id;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Nothing to write: the stamp is written after (`stampAfter`).
|
|
344
|
+
case 'STAMP':
|
|
345
|
+
return op.id;
|
|
346
|
+
|
|
347
|
+
case 'BIND_VARIABLE': {
|
|
348
|
+
const node = await getSceneNodeById(op.nodeId);
|
|
349
|
+
const variable = await getVariableById(op.variableId);
|
|
350
|
+
await bindNodeProperty(node, op.property, variable);
|
|
351
|
+
return op.variableId;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
case 'UNBIND_VARIABLE': {
|
|
355
|
+
const node = await getSceneNodeById(op.nodeId);
|
|
356
|
+
await bindNodeProperty(node, op.property, null);
|
|
357
|
+
return undefined;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
case 'APPLY_TOKEN': {
|
|
361
|
+
// Answers the variable when the field was bound whole; nothing when it, or part of it, was written as the value in `op.context`.
|
|
362
|
+
const node = await getSceneNodeById(op.nodeId);
|
|
363
|
+
const variable = op.variableId === undefined ? undefined : await getVariableById(op.variableId);
|
|
364
|
+
const atDefaults = op.value !== undefined && op.context !== undefined ? { value: op.value, context: op.context } : undefined;
|
|
365
|
+
const written = await writeToken(node, op.field, op.path, resolvedFor(variable, atDefaults));
|
|
366
|
+
return written.how === 'bound' ? written.variable.id : undefined;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
case 'CREATE_STYLE': {
|
|
370
|
+
const bound = await variablesFor(op.bindings);
|
|
371
|
+
const style = (() => {
|
|
372
|
+
switch (op.styleType) {
|
|
373
|
+
case 'TEXT':
|
|
374
|
+
return figma.createTextStyle() as BaseStyle;
|
|
375
|
+
case 'EFFECT':
|
|
376
|
+
return figma.createEffectStyle() as BaseStyle;
|
|
377
|
+
case 'PAINT':
|
|
378
|
+
return figma.createPaintStyle() as BaseStyle;
|
|
379
|
+
default:
|
|
380
|
+
throw new Error(`Unsupported styleType: ${op.styleType}`);
|
|
381
|
+
}
|
|
382
|
+
})();
|
|
383
|
+
|
|
384
|
+
style.name = op.name;
|
|
385
|
+
writeStyle(style, op.props, bound, false);
|
|
386
|
+
return style.id;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
case 'UPDATE_STYLE': {
|
|
390
|
+
const style = await figma.getStyleByIdAsync(op.styleId);
|
|
391
|
+
if (!style) {
|
|
392
|
+
throw new Error(`Style not found: ${op.styleId}`);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
writeStyle(style, op.props, await variablesFor(op.bindings), true);
|
|
396
|
+
return style.id;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
case 'SKIP':
|
|
400
|
+
case 'WARNING':
|
|
401
|
+
case 'BREAKING':
|
|
402
|
+
case 'NOTE':
|
|
403
|
+
case 'SWAP_COMPONENT':
|
|
404
|
+
throw new Error(`No executor for operation kind: ${op.kind}`);
|
|
405
|
+
|
|
406
|
+
default: {
|
|
407
|
+
const _exhaustive: never = op;
|
|
408
|
+
throw new Error(`Unhandled operation kind: ${String(_exhaustive)}`);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Stamps what `op` just wrote: each write is read back and stamped on the object, what the target reports rather than
|
|
415
|
+
* what was sent, so a later read compares exactly though Figma stores a number less precisely than the document.
|
|
416
|
+
* `id` is what the write answered: the id of a variable or style it made.
|
|
417
|
+
*/
|
|
418
|
+
async function stampAfter(op: Operation, id: string | undefined): Promise<void> {
|
|
419
|
+
switch (op.kind) {
|
|
420
|
+
case 'DELETE_VARIABLE':
|
|
421
|
+
for (const id of op.strip ?? []) {
|
|
422
|
+
const copy = await figma.variables.getVariableByIdAsync(id);
|
|
423
|
+
if (copy) strip(copy);
|
|
424
|
+
}
|
|
425
|
+
return;
|
|
426
|
+
case 'CREATE_VARIABLE':
|
|
427
|
+
case 'SET_VARIABLE_SCOPES':
|
|
428
|
+
case 'SET_VARIABLE_VALUE': {
|
|
429
|
+
const token = op.kind === 'SET_VARIABLE_SCOPES' ? op.path : op.tokenPath;
|
|
430
|
+
if (token === undefined) return;
|
|
431
|
+
const variableId = op.kind === 'CREATE_VARIABLE' ? id : op.variableId;
|
|
432
|
+
const variable = variableId === undefined ? null : await figma.variables.getVariableByIdAsync(variableId);
|
|
433
|
+
if (!variable) throw new Error(`Variable not found: ${variableId}`);
|
|
434
|
+
// A new variable is stamped as soon as it exists, so a run that stops before its values still finds it by its stamp next time.
|
|
435
|
+
const reported =
|
|
436
|
+
op.kind === 'SET_VARIABLE_VALUE'
|
|
437
|
+
? { [op.modeId]: variable.valuesByMode[op.modeId] }
|
|
438
|
+
: op.scopes
|
|
439
|
+
? { scopes: reportedScopes(variable.scopes) }
|
|
440
|
+
: {};
|
|
441
|
+
stampWritten(variable, token, reported);
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
case 'STAMP': {
|
|
445
|
+
const object = op.target === 'variable' ? await figma.variables.getVariableByIdAsync(op.id) : await figma.getStyleByIdAsync(op.id);
|
|
446
|
+
const stamp = object ? getStamp(object) : undefined;
|
|
447
|
+
if (!object || !stamp) {
|
|
448
|
+
throw new Error(`${op.target === 'variable' ? 'Variable' : 'Style'} not found: ${op.id}`);
|
|
449
|
+
}
|
|
450
|
+
// The same stamp, with the object's own id.
|
|
451
|
+
setStamp(object, stamp);
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
case 'CREATE_STYLE':
|
|
455
|
+
case 'UPDATE_STYLE': {
|
|
456
|
+
const styleId = op.kind === 'UPDATE_STYLE' ? op.styleId : id;
|
|
457
|
+
const style = styleId === undefined ? null : await figma.getStyleByIdAsync(styleId);
|
|
458
|
+
if (!style) throw new Error(`Style not found: ${styleId}`);
|
|
459
|
+
// A style is written whole, so its stamp is replaced whole, whatever token it named before.
|
|
460
|
+
setStamp(style, { is: { token: op.tokenPath }, receipt: receiptOf(reportOf(style)) });
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
default:
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
export function labelForOperation(op: Operation): string {
|
|
469
|
+
switch (op.kind) {
|
|
470
|
+
case 'CREATE_COLLECTION':
|
|
471
|
+
return `Creating collection "${op.name}"`;
|
|
472
|
+
case 'ADD_MODE':
|
|
473
|
+
return `Adding mode "${op.modeName}" to "${op.collectionName}"`;
|
|
474
|
+
case 'DELETE_MODE':
|
|
475
|
+
return `Deleting mode "${op.modeName}" of "${op.collectionName}"`;
|
|
476
|
+
case 'DELETE_VARIABLE':
|
|
477
|
+
return `Deleting variable "${op.variableName}" of "${op.collectionName}"`;
|
|
478
|
+
case 'CREATE_VARIABLE':
|
|
479
|
+
return `Creating variable "${op.path}"`;
|
|
480
|
+
case 'SET_VARIABLE_VALUE':
|
|
481
|
+
return op.label;
|
|
482
|
+
case 'RENAME_VARIABLE':
|
|
483
|
+
return `Renaming variable to "${op.name}"`;
|
|
484
|
+
case 'SET_CODE_SYNTAX':
|
|
485
|
+
return `Writing code syntax ${op.value}`;
|
|
486
|
+
case 'SET_VARIABLE_SCOPES':
|
|
487
|
+
return `Writing scopes of "${op.path}"`;
|
|
488
|
+
case 'STAMP':
|
|
489
|
+
return `Stamping "${op.path}" with its id`;
|
|
490
|
+
case 'BIND_VARIABLE':
|
|
491
|
+
return `Binding ${op.property} on node ${op.nodeId}`;
|
|
492
|
+
case 'UNBIND_VARIABLE':
|
|
493
|
+
return `Unbinding ${op.property} on node ${op.nodeId}`;
|
|
494
|
+
case 'APPLY_TOKEN':
|
|
495
|
+
return `Applying ${op.path} to ${op.field} of "${op.nodeName}"`;
|
|
496
|
+
case 'CREATE_STYLE':
|
|
497
|
+
return `Creating ${op.styleType} style "${op.name}"`;
|
|
498
|
+
case 'UPDATE_STYLE':
|
|
499
|
+
return `Updating style "${op.tokenPath}"`;
|
|
500
|
+
case 'SKIP':
|
|
501
|
+
return `Skipping ${op.path}`;
|
|
502
|
+
case 'WARNING':
|
|
503
|
+
case 'BREAKING':
|
|
504
|
+
case 'NOTE':
|
|
505
|
+
case 'SWAP_COMPONENT':
|
|
506
|
+
return op.message;
|
|
507
|
+
default: {
|
|
508
|
+
const _exhaustive: never = op;
|
|
509
|
+
return String(_exhaustive);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|