@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.
Files changed (118) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +18 -2
  3. package/cli/bin.mjs +19 -0
  4. package/cli/bridge.ts +243 -0
  5. package/cli/check.ts +62 -0
  6. package/cli/figcraft.ts +764 -0
  7. package/cli/figcraftApply.ts +1058 -0
  8. package/cli/figcraftFile.ts +476 -0
  9. package/cli/figcraftLibrary.ts +147 -0
  10. package/cli/figcraftRecord.ts +121 -0
  11. package/cli/figcraftScene.ts +661 -0
  12. package/cli/main.ts +218 -0
  13. package/cli/members.ts +65 -0
  14. package/cli/run.ts +756 -0
  15. package/component-files.md +665 -0
  16. package/dist/code.js +24147 -0
  17. package/dist/manifest.json +23 -0
  18. package/dist/ui.html +6918 -0
  19. package/guide.md +985 -0
  20. package/main/code.ts +50 -0
  21. package/main/components/apply.ts +704 -0
  22. package/main/components/endOfRun.ts +168 -0
  23. package/main/components/errors.ts +17 -0
  24. package/main/components/fieldWriter.ts +497 -0
  25. package/main/components/file.ts +263 -0
  26. package/main/components/receipt.ts +228 -0
  27. package/main/components/resolve.ts +1193 -0
  28. package/main/components/run.ts +273 -0
  29. package/main/components/snapshot.ts +341 -0
  30. package/main/components/stamp.ts +312 -0
  31. package/main/components/update.ts +465 -0
  32. package/main/dispatcher.ts +33 -0
  33. package/main/engines/batchRunner.ts +200 -0
  34. package/main/engines/ioAdapter.ts +132 -0
  35. package/main/engines/operationExecutor.ts +512 -0
  36. package/main/engines/propertyMapper.ts +221 -0
  37. package/main/engines/queryEngine.ts +45 -0
  38. package/main/engines/styleSyncEngine.ts +444 -0
  39. package/main/engines/variableSyncEngine.ts +561 -0
  40. package/main/handlers/auditSelection.ts +60 -0
  41. package/main/handlers/boundLayers.ts +51 -0
  42. package/main/handlers/cancelOperation.ts +18 -0
  43. package/main/handlers/executeApplyBindings.ts +69 -0
  44. package/main/handlers/executeScaffold.ts +59 -0
  45. package/main/handlers/executeStyleSync.ts +121 -0
  46. package/main/handlers/executeVariableSync.ts +82 -0
  47. package/main/handlers/exportDocument.ts +114 -0
  48. package/main/handlers/getSelection.ts +43 -0
  49. package/main/handlers/index.ts +30 -0
  50. package/main/handlers/inspectCorrespondence.ts +172 -0
  51. package/main/handlers/loadConventions.ts +86 -0
  52. package/main/handlers/loadDocument.ts +253 -0
  53. package/main/handlers/lock.ts +270 -0
  54. package/main/handlers/planApply.ts +157 -0
  55. package/main/handlers/planApplyBindings.ts +79 -0
  56. package/main/handlers/planScaffold.ts +84 -0
  57. package/main/handlers/planStyleSync.ts +49 -0
  58. package/main/handlers/planVariableSync.ts +49 -0
  59. package/main/handlers/readSelectionBindings.ts +160 -0
  60. package/main/handlers/resizeUi.ts +105 -0
  61. package/main/handlers/runComponent.ts +65 -0
  62. package/main/handlers/templates.ts +462 -0
  63. package/main/handlers/tokenGraph.ts +122 -0
  64. package/main/runtimeState.ts +66 -0
  65. package/main/utils/abortController.ts +11 -0
  66. package/main/utils/boundVariables.ts +84 -0
  67. package/main/utils/buildPayloads.ts +85 -0
  68. package/main/utils/correspondence.ts +658 -0
  69. package/main/utils/designSource.ts +122 -0
  70. package/main/utils/figmaCollectionIndex.ts +120 -0
  71. package/main/utils/figmaStyleAdapter.ts +268 -0
  72. package/main/utils/figmaTokenAdapter.ts +255 -0
  73. package/main/utils/flattenDesignSystem.ts +38 -0
  74. package/main/utils/folds.ts +203 -0
  75. package/main/utils/fontStack.ts +57 -0
  76. package/main/utils/lists.ts +4 -0
  77. package/main/utils/lockStore.ts +72 -0
  78. package/main/utils/moves.ts +67 -0
  79. package/main/utils/pageRoots.ts +58 -0
  80. package/main/utils/planSymbols.ts +96 -0
  81. package/main/utils/purpose.ts +351 -0
  82. package/main/utils/recentStore.ts +166 -0
  83. package/main/utils/resolverModes.ts +271 -0
  84. package/main/utils/rootRecord.ts +137 -0
  85. package/main/utils/shardedData.ts +142 -0
  86. package/main/utils/standing.ts +106 -0
  87. package/main/utils/styleTarget.ts +51 -0
  88. package/main/utils/sum.ts +612 -0
  89. package/main/utils/symbolicIdRegistry.ts +26 -0
  90. package/main/utils/syncState.ts +133 -0
  91. package/main/utils/templateStore.ts +73 -0
  92. package/main/utils/tokenPath.ts +28 -0
  93. package/main/utils/tokenStamps.ts +91 -0
  94. package/manifest.json +15 -0
  95. package/package.json +64 -4
  96. package/reference.md +1609 -0
  97. package/shared/components/artboard.ts +126 -0
  98. package/shared/components/axes.ts +420 -0
  99. package/shared/components/bindings.ts +64 -0
  100. package/shared/components/blueprint.ts +248 -0
  101. package/shared/components/check.ts +705 -0
  102. package/shared/components/diff.ts +668 -0
  103. package/shared/components/fields.ts +404 -0
  104. package/shared/components/groups.ts +306 -0
  105. package/shared/components/index.ts +140 -0
  106. package/shared/components/members.ts +280 -0
  107. package/shared/components/model.ts +273 -0
  108. package/shared/components/order.ts +53 -0
  109. package/shared/components/read.ts +480 -0
  110. package/shared/components/stack.ts +124 -0
  111. package/shared/components/steps.ts +186 -0
  112. package/shared/components/validate.ts +548 -0
  113. package/shared/conventions.ts +28 -0
  114. package/shared/inputs.ts +40 -0
  115. package/shared/operations.ts +233 -0
  116. package/shared/pendingNames.ts +39 -0
  117. package/shared/protocol.ts +588 -0
  118. package/shared/tokenTypeMap.ts +21 -0
@@ -0,0 +1,588 @@
1
+ import type { LoadResult } from '@toghen/lib';
2
+ import type { CompactReport, RunOptions } from './components/steps.js';
3
+ import type { BlueprintTokens } from './components/blueprint.js';
4
+ import type { Operation, OperationSummary, SkipCounts } from './operations.js';
5
+
6
+ export type QueryScope = 'selection' | 'page' | 'file';
7
+ /** `load` is no plan: it is the rows a load reports (its collisions, the overrides map's unused keys), in a plan's shape so the result surface draws them. Nothing executes it. */
8
+ export type PlanType = 'variable_sync' | 'style_sync' | 'scaffold' | 'apply_bindings' | 'load' | 'discovery' | 'apply' | 'relink';
9
+
10
+ /** The layers Apply writes: frames (`FRAME`, `SLOT`), instances (`INSTANCE`: the write is an override), component definitions (`COMPONENT`, set members included). */
11
+ export type ApplyKind = 'frames' | 'instances' | 'components';
12
+
13
+ /** One shape, used twice: files in Load, Figma objects in Build. A name as written → what to use instead. */
14
+ export type Overrides = Record<string, string>;
15
+
16
+ /** What a load read, and how: the standing picture of where the document came from. */
17
+ export interface Composition {
18
+ /** A resolver document, or token files wrapped in one. */
19
+ form: 'resolver' | 'tokens';
20
+ /** As the load was given it: one path, or several in order. */
21
+ entry: string | string[];
22
+ /** Every reference whose resolution read a file: the `$ref` as written, its holder, the file read, and whether the map sent it there. */
23
+ reads: LoadResult['reads'];
24
+ /** Present only when a map was given: the map, its hash, and the keys no reference matched. */
25
+ overrides?: { map: Overrides; hash: string; unused: string[] };
26
+ /** Loaded again from a recent's stored bundle, not from the files: `reads` are those of the load that stored it. */
27
+ fromBundle?: true;
28
+ }
29
+
30
+ export type ErrorCode =
31
+ | 'INVALID_DOCUMENT'
32
+ | 'INVALID_CONVENTIONS'
33
+ | 'NO_RESOLVER'
34
+ | 'NO_PLAN'
35
+ | 'FIGMA_API_ERROR'
36
+ | 'COMPONENT_RUN_FAILED'
37
+ | 'INVALID_TEMPLATE'
38
+ | 'INVALID_PICK'
39
+ | 'OPERATION_CANCELLED'
40
+ /** Apply's query names nothing it can write: no token or group, or a field for a group. */
41
+ | 'INVALID_QUERY';
42
+
43
+ export interface ProgressPayload {
44
+ completed: number;
45
+ total: number;
46
+ currentLabel: string;
47
+ }
48
+
49
+ export interface ErrorPayload {
50
+ message: string;
51
+ code: ErrorCode;
52
+ commandType: string;
53
+ /** Every reason, one per entry, when `code` is `COMPONENT_RUN_FAILED` or `INVALID_TEMPLATE`. */
54
+ problems?: string[];
55
+ }
56
+
57
+ export interface DocumentStatePayload {
58
+ /**
59
+ * Whether there is a document at all.
60
+ *
61
+ * `GET_DOCUMENT_STATE` answers even when nothing is loaded, and the UI read
62
+ * any `DOCUMENT_STATE` as proof of a document — so opening the plugin into an
63
+ * empty session showed the loaded pane, named "Untitled", with zero tokens
64
+ * and live buttons. The question has to be answerable rather than inferred
65
+ * from a placeholder name.
66
+ */
67
+ loaded: boolean;
68
+ name: string;
69
+ /** The document's own `description`, which the Resolver spec allows at root. */
70
+ description?: string;
71
+ tokenCount: number;
72
+ modifiers: Record<string, string[]>;
73
+ /** The sum's modes: the defaults, then each other context on its own. Never the product of every combination. */
74
+ modes: string[];
75
+ /** The default context of each modifier, which export preselects: one context per modifier is chosen, and the choice is shown (R10). */
76
+ modifierDefaults: Record<string, string>;
77
+ /** The mode the document resolves to when no context is chosen: the first of `modes`. */
78
+ defaultMode: string;
79
+ /** The version the system declares for itself (`Resolver.getDeclaredVersion()`), if any. */
80
+ version?: string;
81
+ /** The file the document was read from, when the load named one. */
82
+ fileName?: string;
83
+ /** What the load read and how. Present whenever `loaded` is. */
84
+ composition?: Composition;
85
+ /** Tokens the conversion map sends to a Figma style rather than a variable. */
86
+ styleCount: number;
87
+ /**
88
+ * The collections a variable sync builds: the base first, then one per
89
+ * modifier, then one per folded part axis, each with its modes and how many variables would live in it.
90
+ * Absent when the sum cannot be built; the plan is what says why.
91
+ */
92
+ collections?: DocumentCollectionSummary[];
93
+ }
94
+
95
+ export interface DocumentCollectionSummary {
96
+ name: string;
97
+ /** The modifier it stands for; absent for the base collection. */
98
+ modifier?: string;
99
+ /** Set for the collection of a folded part axis, named after the axis. */
100
+ axis?: true;
101
+ modes: string[];
102
+ variableCount: number;
103
+ }
104
+
105
+ /**
106
+ * One entry in the recents list — what the UI draws, without the document.
107
+ *
108
+ * A filename says nothing: `toghen.resolver.json` is what every one of them is
109
+ * called. The design system's own name and version are what tell two of them
110
+ * apart.
111
+ */
112
+ export interface RecentDocumentSummary {
113
+ /**
114
+ * Identity, for replacing an entry rather than growing a second one.
115
+ *
116
+ * The document's `name`, falling back to the filename when it declares none.
117
+ * Loading an edited document is a normal working loop, not a new system.
118
+ */
119
+ key: string;
120
+ name: string;
121
+ /**
122
+ * The version the design system declares for itself, if it declares one.
123
+ *
124
+ * **Not** the document's `version` field, which the Resolver spec fixes at
125
+ * `"2025-11-01"` and reserves for breaking changes to the format — identical
126
+ * in every conforming document, and useless for telling two apart. This comes
127
+ * from `$extensions` on a set or modifier, the only places §4.3 allows them.
128
+ * See `Resolver.getDeclaredVersion()`.
129
+ */
130
+ version?: string;
131
+ description?: string;
132
+ tokenCount: number;
133
+ /** Mode names, so the shape of the system is visible without loading it. */
134
+ modes: string[];
135
+ /** Modifier names. Absent in entries kept before they were stored. */
136
+ modifiers?: string[];
137
+ /** The file it came from, kept only as a fallback label. */
138
+ fileName?: string;
139
+ /** ISO timestamp of the load this entry records. */
140
+ loadedAt: string;
141
+ }
142
+
143
+ /**
144
+ * What the **Figma file** records about having been synced — as opposed to
145
+ * `DocumentStatePayload`, which describes the JSON the user loaded.
146
+ *
147
+ * Kept apart from document state for three reasons: it is meaningful with no
148
+ * document loaded at all (a file synced last week still has an answer), it
149
+ * changes at different moments (after every execute, not on load), and folding
150
+ * it in would make every `GET_DOCUMENT_STATE` read every variable's and style's
151
+ * stamp.
152
+ */
153
+ export interface SyncStatePayload {
154
+ /** Some variable or style in this Figma file carries a Toghen stamp. */
155
+ hasRecord: boolean;
156
+ /**
157
+ * Variables and styles stamped for a token, including tokens the loaded
158
+ * document does not have. Those belong to some other document synced into the
159
+ * same file — the gap between this and `coveredCount` is information, not an
160
+ * error.
161
+ */
162
+ recordedCount: number;
163
+
164
+ /** Null when no document is loaded — there is nothing to measure against. */
165
+ documentTokenCount: number | null;
166
+ /** Document tokens stamped in every mode they hold a value for. */
167
+ coveredCount: number | null;
168
+ /** Document tokens covered in some of their modes but not all. */
169
+ partialCount: number | null;
170
+ /**
171
+ * Whose stamps they are: the leading segment of every token they name, each
172
+ * once. A lookup path starts with the document's name, so this names the
173
+ * documents that were synced into the file, answerable with none loaded.
174
+ */
175
+ recordNames: string[];
176
+ }
177
+
178
+ /**
179
+ * Where one token stands between the document and this Figma file.
180
+ *
181
+ * Every name here describes a **condition**, not an intention. `outdated` says
182
+ * the document has changed since the token was last written; it does not say
183
+ * what a sync would do about it. The plan is the only thing that speaks in the
184
+ * future tense, and two regions of one screen each predicting it is how they
185
+ * come to disagree (smell #22).
186
+ *
187
+ * - `current` — Figma holds the document's value in every mode it declares.
188
+ * - `drifted` — they differ, and the variable's stamp proves *Figma* moved
189
+ * since Toghen wrote it, so the change was made in Figma, whether or not the
190
+ * document moved too.
191
+ * - `outdated` — they differ, and Figma still holds what Toghen wrote: the
192
+ * *document* has moved.
193
+ * - `absent` — no Figma variable holds this token.
194
+ * - `adopted` — a variable of this name and type exists that Toghen did not
195
+ * write. Syncing writes into an object with a history nothing here records.
196
+ * - `blocked` — the link cannot exist: a type that varies by mode, a type no
197
+ * variable can hold, or a name taken by a variable of the wrong type.
198
+ * - `not_inspected` — backed by a Figma style, which this round does not
199
+ * compare. Shown rather than omitted, so the counts still reconcile.
200
+ */
201
+ export type CorrespondenceState = 'current' | 'drifted' | 'outdated' | 'absent' | 'adopted' | 'blocked' | 'not_inspected';
202
+
203
+ /** One mode's row inside an opened token. */
204
+ export interface ModeReport {
205
+ mode: string;
206
+ /** False when the document declares no value for this token in this mode. */
207
+ present: boolean;
208
+ /** Both sides are rendered from Figma's representation, so they compare. */
209
+ documentValue: string | null;
210
+ figmaValue: string | null;
211
+ /** Null when there is nothing to compare against — not the same as "differs". */
212
+ agrees: boolean | null;
213
+ }
214
+
215
+ export interface TokenReport {
216
+ path: string;
217
+ /** The document's `$type`, or null when the modes disagree about it. */
218
+ type: string | null;
219
+ state: CorrespondenceState;
220
+ note?: string;
221
+ variableName?: string;
222
+ modes: ModeReport[];
223
+ }
224
+
225
+ /**
226
+ * A picture of where every token stands, as of the moment it was taken.
227
+ *
228
+ * `checkedAt` is not decoration. Figma can change under the plugin with no event
229
+ * this codebase watches, so the picture is only true for the instant it was
230
+ * made — and a standing claim with no timestamp is exactly the badge that was
231
+ * caught lying in smell #22.
232
+ */
233
+ export interface InspectionPayload {
234
+ checkedAt: string;
235
+ collectionName: string;
236
+ tokens: TokenReport[];
237
+ counts: Record<CorrespondenceState, number>;
238
+ }
239
+
240
+ /** One token of the document, as the Tokens pane's browser needs it. See `reference.md`, *The token graph*. */
241
+ export interface TokenGraphEntry {
242
+ path: string;
243
+ type: string | null;
244
+ /** The path without its last segment. */
245
+ group: string;
246
+ /** Per mode of the sum, written for reading; null where the mode has no value. */
247
+ valueByMode: Record<string, string | null>;
248
+ /** Lookup paths this token's value points to, in any mode; the first hop. */
249
+ references: string[];
250
+ /** Lookup paths whose values point to this token. */
251
+ referencedBy: string[];
252
+ /** The CSS custom property lib names it by. */
253
+ codeSyntax: string;
254
+ }
255
+
256
+ export interface TokenGraphPayload {
257
+ /** The modes `valueByMode` is keyed by, the default first. */
258
+ modes: string[];
259
+ tokens: TokenGraphEntry[];
260
+ }
261
+
262
+ export interface PlanPayload {
263
+ planType: PlanType;
264
+ operations: Operation[];
265
+ summary?: PlanCounts;
266
+ }
267
+
268
+ /** A plan's counts, by the names each plan gives them. The variable and style plans add `skipsByCause`, whose sum is their `skips`. */
269
+ export interface PlanCounts {
270
+ [count: string]: number | SkipCounts | undefined;
271
+ skipsByCause?: SkipCounts;
272
+ }
273
+
274
+ export interface SelectionPayload {
275
+ ids: string[];
276
+ names: string[];
277
+ count: number;
278
+ }
279
+
280
+ export interface AuditResultPayload {
281
+ nodes: unknown[];
282
+ }
283
+
284
+ /** One bindable property of one layer. See `reference.md`, *What the selection is bound to*. */
285
+ export interface PropertyBinding {
286
+ property: string;
287
+ bound: { variableName: string; tokenPath: string | null } | null;
288
+ literal: string | null;
289
+ suggestion: string | null;
290
+ declaredBy: string | null;
291
+ }
292
+
293
+ /**
294
+ * One field of the field table on one layer: the variable it is bound to (on a paint, the first bound paint's), or the
295
+ * style it takes (`textStyle`, `effectStyle`), by name, with the token its stamp names; `null` when it is plain.
296
+ */
297
+ export interface FieldBinding {
298
+ field: string;
299
+ bound: { name: string; tokenPath: string | null } | null;
300
+ /**
301
+ * What a plain field holds, written for reading (a number, a string, a solid first paint as hex), `null` where it is
302
+ * no one value; absent on a bound field and a style field. A value Apply wrote is plain too: no stamp records it.
303
+ */
304
+ holds?: string | null;
305
+ }
306
+
307
+ export interface SelectionBindingsPayload {
308
+ scope: QueryScope;
309
+ readAt: string;
310
+ nodes: Array<{
311
+ id: string;
312
+ name: string;
313
+ kind: string;
314
+ builtFrom: { template: string; fileName: string; kept: string } | null;
315
+ properties: PropertyBinding[];
316
+ /** A row for every field the field table lists, with a token column, for the layer's kind (`applyKindOf`): bound, or plain. */
317
+ fields: FieldBinding[];
318
+ }>;
319
+ /** Layers in scope past the most one answer carries. */
320
+ more?: number;
321
+ }
322
+
323
+ export interface BoundLayersPayload {
324
+ variableName: string;
325
+ ids: string[];
326
+ pageName: string;
327
+ }
328
+
329
+ export interface TemplateSummary {
330
+ name: string;
331
+ fileName: string;
332
+ kept: string;
333
+ characters: number;
334
+ members: number;
335
+ axes: Array<{ name: string; values: string[] }>;
336
+ properties: Array<{ name: string; type: string }>;
337
+ tokenGroups: string[];
338
+ builtInto: { page: string; setId: string } | null;
339
+ /** Always null for now: nothing compares a set with its file. */
340
+ differs: boolean | null;
341
+ problems: string[];
342
+ /** A blueprint's needs, as it declares them, before anything looked them up (`LOOK_UP_NEEDS`). Absent for a component file. */
343
+ needs?: {
344
+ tokens?: BlueprintTokens;
345
+ libraries: string[];
346
+ /** Each component the bodies name, by name or as `(key)"…"`; `satisfied` when a body of the blueprint defines it. */
347
+ components: Array<{ name: string; satisfied: boolean }>;
348
+ };
349
+ }
350
+
351
+ /**
352
+ * What a look found of one kept blueprint's needs (`LOOK_UP_NEEDS`): a standing picture, dated, drawn in the Build pane.
353
+ * Found by the locator's dry pass, which imports nothing. See `reference.md`, *The Build pane's look*.
354
+ */
355
+ export interface NeedsPayload {
356
+ /** The blueprint, by the name it is kept under. */
357
+ name: string;
358
+ lookedAt: string;
359
+ /** Held against what Load loaded: the entry's file names, and whether a map was loaded with it. */
360
+ tokens?: BlueprintTokens & { state: 'loaded' | 'differs' | 'not loaded'; detail?: string };
361
+ libraries: Array<{ name: string; state: 'in the lock' | 'missing'; by?: LockedLibrary['by']; readAs?: string }>;
362
+ components: Array<{
363
+ name: string;
364
+ /** `would swap`: found, and a build would swap a stamped instance to it (a relink). `by key`: a key, not checked without importing it. */
365
+ state: 'satisfied' | 'found' | 'from a library' | 'by key' | 'would swap' | 'missing' | 'problem';
366
+ /** The declared library it comes from, when it does. */
367
+ library?: string;
368
+ detail?: string;
369
+ }>;
370
+ /** What the look could not answer: a body whose dry pass stopped, so its swaps are not known. */
371
+ notes: string[];
372
+ }
373
+
374
+ export interface TemplatesPayload {
375
+ lookedAt: string;
376
+ templates: TemplateSummary[];
377
+ }
378
+
379
+ export interface FigmaCollectionPayload {
380
+ id: string;
381
+ name: string;
382
+ modes: Array<{ modeId: string; name: string }>;
383
+ /** How many local variables the collection holds. */
384
+ variableCount: number;
385
+ }
386
+
387
+ /** The lock: what each name a file uses from a library resolved to. See `reference.md`, *The lock*. */
388
+ export interface Lock {
389
+ /** Keyed by the library's name, as Figma names it. */
390
+ libraries: Record<string, LockedLibrary>;
391
+ /** Build's map: a name as written → the name to use instead, or a key written `(key)"…"`. */
392
+ overrides?: Overrides;
393
+ }
394
+
395
+ export interface LockedLibrary {
396
+ /** The library file's key, only where a run could read it; never guessed. */
397
+ fileKey?: string;
398
+ /** FigCraft: the library's `.figcraft` file, where the run last found it, and the id the file keeps, which a move keeps. */
399
+ path?: string;
400
+ id?: string;
401
+ /** Who wrote the entry last. */
402
+ by: 'discovery' | 'build' | 'command line' | 'person';
403
+ /** A component's name as files write it → its key, and whether the key is a component set's. */
404
+ components: Record<string, { key: string; set: boolean }>;
405
+ /** A token path → the key of the style it names. */
406
+ styles: Record<string, { key: string; kind: 'TEXT' | 'EFFECT' | 'PAINT' }>;
407
+ /** `<collection>/<variable name>` → the variable's key. */
408
+ variables: Record<string, string>;
409
+ }
410
+
411
+ /** What `getPublishStatusAsync()` answers for a local component, style or variable. */
412
+ export type PublishState = 'UNPUBLISHED' | 'CURRENT' | 'CHANGED';
413
+
414
+ /**
415
+ * What a library file says of its own keys (`LIBRARY_KEYS`), each with its publish status: the lock's entry for it,
416
+ * before the command line picks who wrote it. See `reference.md`, *The lock*.
417
+ */
418
+ export interface LibraryKeys {
419
+ /** `figma.fileKey`, where it reads: a private plugin, or a development one with `enablePrivatePluginApi`. */
420
+ fileKey?: string;
421
+ /** Each component set a stamp names (and a component outside a set, if a stamp names one), by the name files write. */
422
+ components: Record<string, { key: string; set: boolean; status: PublishState }>;
423
+ /** Each text, effect and paint style the style sync stamped, by the token path its name spells. */
424
+ styles: Record<string, { key: string; kind: 'TEXT' | 'EFFECT' | 'PAINT'; status: PublishState | undefined }>;
425
+ /** Every local variable, by `<collection>/<variable name>`. */
426
+ variables: Record<string, { key: string; status: PublishState }>;
427
+ /** Lines for the run's report: what the library file could not say (a style's publish status, where Figma gave none). */
428
+ warnings?: string[];
429
+ }
430
+
431
+ export type Command =
432
+ /** With `dryRun`, for a run that writes nothing: an older build's root record is turned into stamps in memory only. */
433
+ | {
434
+ type: 'LOAD_DOCUMENT';
435
+ payload: {
436
+ /** Every file picked, by its path relative to the pick. The core reads only these. */
437
+ files: { path: string; text: string }[];
438
+ /** A resolver document, one token file, or several token files in order (the last wins). */
439
+ entry: string | string[];
440
+ /** Token files only: the collection's name. */
441
+ name?: string;
442
+ /** A `$ref` as written → the location to read instead. */
443
+ overrides?: Overrides;
444
+ dryRun?: boolean;
445
+ };
446
+ }
447
+ | { type: 'GET_RECENTS'; payload: Record<string, never> }
448
+ /** Load a document the user loaded before, from its stored copy. */
449
+ | { type: 'LOAD_RECENT'; payload: { key: string } }
450
+ | { type: 'FORGET_RECENT'; payload: { key: string } }
451
+ | { type: 'EXPORT_DOCUMENT'; payload: { includeExtensions: boolean; contexts?: Record<string, string> } }
452
+ | { type: 'GET_DOCUMENT_STATE'; payload: Record<string, never> }
453
+ | { type: 'GET_SYNC_STATE'; payload: Record<string, never> }
454
+ /**
455
+ * Read Figma back and report where every token stands.
456
+ *
457
+ * Asked for, rather than sent on load: the answer is only true for the moment
458
+ * it was taken, and paying for a full read of the file's variables when the
459
+ * user may never open the Inspector is a cost with no reader.
460
+ */
461
+ | { type: 'INSPECT_CORRESPONDENCE'; payload: Record<string, never> }
462
+ /** The document's tokens as a graph; answered with `TOKEN_GRAPH`. */
463
+ | { type: 'GET_TOKEN_GRAPH'; payload: Record<string, never> }
464
+ | { type: 'LOAD_CONVENTIONS'; payload: { json: string } }
465
+ | { type: 'GET_CONVENTIONS'; payload: Record<string, never> }
466
+ /**
467
+ * `allowBreaking` lets the plan carry the steps that lose something in Figma (the deletion of a mode its
468
+ * modifier no longer has). Left out, they are listed and not carried: the plugin's own screen never sets it.
469
+ * `overwriteDrift` writes the document's value over a mode Figma holds another value for than the last sync wrote,
470
+ * instead of leaving it as a local edit. The screen never sets it either.
471
+ */
472
+ | { type: 'PLAN_VARIABLE_SYNC'; payload: { collectionName?: string; allowBreaking?: boolean; overwriteDrift?: boolean } }
473
+ | { type: 'EXECUTE_VARIABLE_SYNC'; payload: Record<string, never> }
474
+ /** `overwriteDrift` writes the document's value over a style changed in Figma since the last sync wrote it, as for a variable. */
475
+ | { type: 'PLAN_STYLE_SYNC'; payload: { collectionName?: string; overwriteDrift?: boolean } }
476
+ | { type: 'EXECUTE_STYLE_SYNC'; payload: Record<string, never> }
477
+ | { type: 'AUDIT_SELECTION'; payload: { scope: QueryScope } }
478
+ | { type: 'PLAN_SCAFFOLD'; payload: { query: string; scope: QueryScope } }
479
+ | { type: 'EXECUTE_SCAFFOLD'; payload: Record<string, never> }
480
+ /** With `nodeId`, only that node; with `property` too, only that property. */
481
+ | { type: 'PLAN_APPLY_BINDINGS'; payload: { query: string; scope: QueryScope; nodeId?: string; property?: string } }
482
+ | { type: 'EXECUTE_APPLY_BINDINGS'; payload: Record<string, never> }
483
+ /**
484
+ * Apply: a token, or a group's tokens, on the layers of `kinds` in `scope`. `tokens` is a token's or a group's path,
485
+ * braces optional. A group's tokens reach fields by their last keys; one token takes `field`, or reaches one by its last key.
486
+ */
487
+ | { type: 'PLAN_APPLY'; payload: { tokens: string; field?: string; scope: QueryScope; kinds: ApplyKind[] } }
488
+ | { type: 'EXECUTE_APPLY'; payload: Record<string, never> }
489
+ | { type: 'READ_SELECTION_BINDINGS'; payload: { scope: QueryScope } }
490
+ | { type: 'SELECT_NODES'; payload: { ids: string[] } }
491
+ | { type: 'COUNT_BOUND_LAYERS'; payload: { variableName: string } }
492
+ | { type: 'GET_TEMPLATES'; payload: Record<string, never> }
493
+ | { type: 'KEEP_TEMPLATE'; payload: { fileName: string; text: string; replaces?: string } }
494
+ | { type: 'FORGET_TEMPLATE'; payload: { name: string } }
495
+ | { type: 'PLAN_BUILD'; payload: { name: string } }
496
+ | { type: 'EXECUTE_BUILD'; payload: { name: string; allowBreaking?: boolean } }
497
+ | { type: 'GET_FIGMA_COLLECTIONS'; payload: Record<string, never> }
498
+ /** The lock, for the Build pane. */
499
+ | { type: 'GET_LOCK'; payload: Record<string, never> }
500
+ /** Match the variables the kept files bind against the enabled libraries' variables, and write the keys found. */
501
+ | { type: 'DISCOVER_VARIABLES'; payload: Record<string, never> }
502
+ /** Write the selected instance's component, and the selected layer's library styles, under `library`. */
503
+ | { type: 'LOCK_PICK'; payload: { library: string } }
504
+ | { type: 'LOCK_FORGET'; payload: { library: string } }
505
+ | { type: 'SET_BUILD_OVERRIDES'; payload: { overrides: Overrides } }
506
+ /** Look up one kept blueprint's needs, by a dry pass of the locator that imports nothing. Answered with `NEEDS`. */
507
+ | { type: 'LOOK_UP_NEEDS'; payload: { name: string } }
508
+ /** A dry build of every kept file, reporting only the swaps a relink would make and what they need published. Answered with `PLAN_READY`, `relink`. */
509
+ | { type: 'PLAN_RELINK'; payload: Record<string, never> }
510
+ /** In a library file: its own keys and their publish status. Answered with `LIBRARY_KEYS_READ`. */
511
+ | { type: 'LIBRARY_KEYS'; payload: Record<string, never> }
512
+ /** In a target file: write one library's entry, replacing the entry whole. Answered with `LOCK`. */
513
+ | { type: 'LOCK_WRITE'; payload: { library: string; entry: LockedLibrary } }
514
+ | { type: 'GET_SELECTION'; payload: Record<string, never> }
515
+ /**
516
+ * Make Figma agree with one component file, given as its text and its name. The plugin reads,
517
+ * evaluates, validates, plans and applies; the Templates pane's build goes through the same entry.
518
+ * `inputs` is the data an artboard file reads with `(input)`: the tokens of the document the command line
519
+ * resolved, nested by path, each leaf its resolved `$value`. A component file ignores it.
520
+ */
521
+ | { type: 'RUN_COMPONENT'; payload: { text: string; fileName: string; options: RunOptions; inputs?: Record<string, unknown> } }
522
+ /**
523
+ * Read every file and say every problem of every file, writing nothing. `names` also checks the
524
+ * names each writes against the loaded design system, which a run does not: Figma resolves them.
525
+ */
526
+ | {
527
+ type: 'CHECK_COMPONENTS';
528
+ payload: {
529
+ files: Array<{ fileName: string; text: string }>;
530
+ names?: boolean;
531
+ inputs?: Record<string, unknown>;
532
+ dryRun?: boolean;
533
+ allowBreaking?: boolean;
534
+ };
535
+ }
536
+ /**
537
+ * The end of a run, once, after its last component or artboard step: removes the strays when `allowBreaking`, and stacks
538
+ * Toghen's sets and artboards again on each page the run changed one on. With `dryRun` it only says what it would restack.
539
+ */
540
+ | { type: 'FINISH_COMPONENTS'; payload: { dryRun?: boolean; allowBreaking?: boolean } }
541
+ | { type: 'CANCEL_OPERATION'; payload: Record<string, never> }
542
+ /**
543
+ * Resize the plugin window.
544
+ *
545
+ * A Figma plugin window cannot be resized by the user — the plugin has to
546
+ * offer the affordance and call `figma.ui.resize()` itself. `remember: true`
547
+ * marks the end of a drag, so the size is persisted once rather than on every
548
+ * intermediate frame.
549
+ */
550
+ | { type: 'RESIZE_UI'; payload: { width: number; height: number; remember?: boolean } | { mode: WindowMode } };
551
+
552
+ /** The window's two remembered sizes, and the switch between them. */
553
+ export type WindowMode = 'narrow' | 'wide';
554
+
555
+ export type Response =
556
+ /** `report`: the load's own rows, one per collision and per unused key of the map; absent when there are none. */
557
+ | { type: 'DOCUMENT_LOADED'; payload: DocumentStatePayload & { report?: PlanPayload } }
558
+ | { type: 'DOCUMENT_EXPORTED'; payload: { json: string; filename: string } }
559
+ | { type: 'DOCUMENT_STATE'; payload: DocumentStatePayload }
560
+ | { type: 'SYNC_STATE'; payload: SyncStatePayload }
561
+ | { type: 'CORRESPONDENCE'; payload: InspectionPayload }
562
+ | { type: 'TOKEN_GRAPH'; payload: TokenGraphPayload }
563
+ | { type: 'RECENTS'; payload: RecentDocumentSummary[] }
564
+ | { type: 'CONVENTIONS_LOADED'; payload: { count: number } }
565
+ | { type: 'PLAN_READY'; payload: PlanPayload }
566
+ | { type: 'OPERATION_PROGRESS'; payload: ProgressPayload }
567
+ | { type: 'OPERATION_COMPLETE'; payload: OperationSummary }
568
+ | { type: 'AUDIT_RESULT'; payload: AuditResultPayload }
569
+ | { type: 'SELECTION_BINDINGS'; payload: SelectionBindingsPayload }
570
+ | { type: 'BOUND_LAYERS'; payload: BoundLayersPayload }
571
+ | { type: 'TEMPLATES'; payload: TemplatesPayload }
572
+ | { type: 'WINDOW_MODE'; payload: { mode: WindowMode } }
573
+ | { type: 'FIGMA_COLLECTIONS'; payload: FigmaCollectionPayload[] }
574
+ | { type: 'LOCK'; payload: Lock }
575
+ | { type: 'NEEDS'; payload: NeedsPayload }
576
+ | { type: 'LIBRARY_KEYS_READ'; payload: LibraryKeys }
577
+ | { type: 'SELECTION_CHANGED'; payload: SelectionPayload }
578
+ /** Answer to `RUN_COMPONENT`: the engine's report, steps summarised. A run that stopped arrives as `ERROR` with `problems`. */
579
+ | { type: 'COMPONENT_RUN'; payload: { component: string; report: CompactReport } }
580
+ /**
581
+ * Answer to `CHECK_COMPONENTS` when no file has a problem. Otherwise an `ERROR` with `problems`.
582
+ * `strays` are the members outside any set, one breaking line each: present only when there are some, and, unless the
583
+ * check was for a dry run or `allowBreaking`, they are `problems` instead.
584
+ */
585
+ | { type: 'COMPONENTS_CHECKED'; payload: { files: number; strays?: string[] } }
586
+ /** Answer to `FINISH_COMPONENTS`. */
587
+ | { type: 'COMPONENTS_FINISHED'; payload: { removed: string[]; moved: string[]; notes: string[] } }
588
+ | { type: 'ERROR'; payload: ErrorPayload };
@@ -0,0 +1,21 @@
1
+ export interface TokenTypeRoute {
2
+ target: 'variable' | 'style' | 'skip';
3
+ variableType?: VariableResolvedDataType;
4
+ styleType?: 'TEXT' | 'EFFECT' | 'PAINT';
5
+ }
6
+
7
+ export type TokenTypeConversionMap = Record<string, TokenTypeRoute>;
8
+
9
+ export const defaultTokenTypeMap: TokenTypeConversionMap = {
10
+ color: { target: 'variable', variableType: 'COLOR' },
11
+ dimension: { target: 'variable', variableType: 'FLOAT' },
12
+ letterSpacing: { target: 'variable', variableType: 'FLOAT' },
13
+ number: { target: 'variable', variableType: 'FLOAT' },
14
+ fontFamily: { target: 'variable', variableType: 'STRING' },
15
+ fontWeight: { target: 'variable', variableType: 'FLOAT' },
16
+ string: { target: 'variable', variableType: 'STRING' },
17
+ boolean: { target: 'variable', variableType: 'BOOLEAN' },
18
+ typography: { target: 'style', styleType: 'TEXT' },
19
+ shadow: { target: 'style', styleType: 'EFFECT' },
20
+ gradient: { target: 'style', styleType: 'PAINT' },
21
+ };