@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,221 @@
1
+ import type { PropertyConvention } from '../../shared/conventions.js';
2
+ import type { Operation } from '../../shared/operations.js';
3
+ import { BOUND_BY_PARTS } from '../utils/boundVariables.js';
4
+
5
+ export interface ExpectedBinding {
6
+ convention: PropertyConvention;
7
+ variableName: string;
8
+ property: string;
9
+ exists: boolean;
10
+ variableId?: string;
11
+ isBound: boolean;
12
+ }
13
+
14
+ export interface AuditedBinding extends ExpectedBinding {
15
+ status: 'missing_variable' | 'unbound' | 'ok';
16
+ }
17
+
18
+ export interface NodeAuditResult {
19
+ nodeId: string;
20
+ nodeName: string;
21
+ bindings: AuditedBinding[];
22
+ }
23
+
24
+ async function slugFromNode(node: SceneNode): Promise<string> {
25
+ if (node.type === 'INSTANCE') {
26
+ const main = await node.getMainComponentAsync();
27
+ if (main) return main.name;
28
+ }
29
+ return node.name;
30
+ }
31
+
32
+ function variantFromNode(node: SceneNode): string {
33
+ // Asked first: a node with no such member (a frame) has nothing to read.
34
+ if (!('variantProperties' in node)) return '';
35
+ const variantProperties = (node as unknown as { variantProperties?: Record<string, string> }).variantProperties;
36
+ if (!variantProperties) return '';
37
+
38
+ const keys = Object.keys(variantProperties);
39
+ if (keys.length === 0) return '';
40
+
41
+ return keys.map((key) => variantProperties[key]).join('/');
42
+ }
43
+
44
+ async function resolvePattern(pattern: string, node: SceneNode): Promise<string> {
45
+ return (
46
+ pattern
47
+ .replace(/\{component\}/g, await slugFromNode(node))
48
+ .replace(/\{variant\}/g, variantFromNode(node))
49
+ // A node with no variants substitutes an empty string, so
50
+ // `{component}/{variant}/bg` became `Card//bg`. Figma reads `/` as a group
51
+ // separator, so that named a real variable inside an empty-named group.
52
+ .replace(/\/{2,}/g, '/')
53
+ .replace(/^\/|\/$/g, '')
54
+ );
55
+ }
56
+
57
+ function matchVariableRef(bound: unknown, variableId: string): boolean {
58
+ if (!bound) return false;
59
+
60
+ if (typeof bound === 'string') {
61
+ return bound === variableId;
62
+ }
63
+
64
+ if (Array.isArray(bound)) {
65
+ return bound.some((entry) => matchVariableRef(entry, variableId));
66
+ }
67
+
68
+ if (typeof bound === 'object') {
69
+ const record = bound as Record<string, unknown>;
70
+ const id = record.id;
71
+ const maybeVariableId = record.variableId;
72
+
73
+ if (typeof id === 'string' && id === variableId) return true;
74
+ if (typeof maybeVariableId === 'string' && maybeVariableId === variableId) return true;
75
+ }
76
+
77
+ return false;
78
+ }
79
+
80
+ function getPaintColorBindingId(node: SceneNode, property: string): unknown {
81
+ const key = property.startsWith('fills[') ? 'fills' : property.startsWith('strokes[') ? 'strokes' : null;
82
+ if (!key) return null;
83
+
84
+ const paints = (node as unknown as Record<string, unknown>)[key];
85
+ if (!Array.isArray(paints) || paints.length === 0) return null;
86
+
87
+ const first = paints[0] as Record<string, unknown>;
88
+ const boundVariables = first?.boundVariables;
89
+ if (!boundVariables || typeof boundVariables !== 'object') return null;
90
+
91
+ return (boundVariables as Record<string, unknown>).color;
92
+ }
93
+
94
+ function isPropertyBound(node: SceneNode, property: string, variableId: string): boolean {
95
+ if (property === 'fills[0].color' || property === 'strokes[0].color') {
96
+ return matchVariableRef(getPaintColorBindingId(node, property), variableId);
97
+ }
98
+
99
+ const boundVariables = (node as unknown as { boundVariables?: Record<string, unknown> }).boundVariables;
100
+ if (!boundVariables) return false;
101
+
102
+ // Figma reports a bound `cornerRadius` or `strokeWeight` on its parts only.
103
+ const parts = BOUND_BY_PARTS[property];
104
+ if (parts && boundVariables[property] === undefined) return parts.every((part) => matchVariableRef(boundVariables[part], variableId));
105
+ return matchVariableRef(boundVariables[property], variableId);
106
+ }
107
+
108
+ export class PropertyMapper {
109
+ constructor(private readonly conventions: PropertyConvention[]) {}
110
+
111
+ /**
112
+ * The file's variables, fetched once per mapper.
113
+ *
114
+ * `getLocalVariablesAsync()` returns every variable in the file, and this used
115
+ * to be called once per node *per convention* — auditing 200 nodes against 5
116
+ * conventions fetched the whole table a thousand times.
117
+ *
118
+ * Caching for the life of the mapper is what makes that safe: a mapper is
119
+ * built per handler call, so a scaffold run that creates variables gets a
120
+ * fresh one on the next command rather than reading a table from before.
121
+ */
122
+ private variablesByName: Map<string, Variable[]> | null = null;
123
+
124
+ private async findVariableByName(variableName: string, variableType?: VariableResolvedDataType): Promise<Variable | null> {
125
+ if (!this.variablesByName) {
126
+ this.variablesByName = new Map();
127
+ for (const variable of await figma.variables.getLocalVariablesAsync()) {
128
+ const existing = this.variablesByName.get(variable.name);
129
+ if (existing) {
130
+ existing.push(variable);
131
+ } else {
132
+ this.variablesByName.set(variable.name, [variable]);
133
+ }
134
+ }
135
+ }
136
+
137
+ // Names are not unique across collections, so the first match wins exactly
138
+ // as it did when this scanned the whole array in order.
139
+ for (const variable of this.variablesByName.get(variableName) ?? []) {
140
+ if (variableType && variable.resolvedType !== variableType) continue;
141
+ return variable;
142
+ }
143
+ return null;
144
+ }
145
+
146
+ async inferExpectedBindings(node: SceneNode): Promise<ExpectedBinding[]> {
147
+ const applicable = this.conventions.filter((convention) => (convention.nodeTypes as string[]).includes(node.type));
148
+
149
+ const results: ExpectedBinding[] = [];
150
+ for (const convention of applicable) {
151
+ const variableName = await resolvePattern(convention.pattern, node);
152
+ const existing = await this.findVariableByName(variableName, convention.variableType);
153
+
154
+ results.push({
155
+ convention,
156
+ variableName,
157
+ property: convention.property,
158
+ exists: !!existing,
159
+ variableId: existing?.id,
160
+ isBound: existing ? isPropertyBound(node, convention.property, existing.id) : false,
161
+ });
162
+ }
163
+
164
+ return results;
165
+ }
166
+
167
+ async auditNode(node: SceneNode): Promise<NodeAuditResult> {
168
+ const expected = await this.inferExpectedBindings(node);
169
+ const bindings: AuditedBinding[] = expected.map((binding) => ({
170
+ ...binding,
171
+ status: !binding.exists ? 'missing_variable' : !binding.isBound ? 'unbound' : 'ok',
172
+ }));
173
+
174
+ return {
175
+ nodeId: node.id,
176
+ nodeName: node.name,
177
+ bindings,
178
+ };
179
+ }
180
+
181
+ async planScaffold(node: SceneNode, collectionId: string): Promise<Operation[]> {
182
+ const bindings = await this.inferExpectedBindings(node);
183
+ const operations: Operation[] = [];
184
+
185
+ for (const binding of bindings) {
186
+ const variableId = binding.variableId ?? `$var:${binding.variableName}`;
187
+
188
+ if (!binding.exists) {
189
+ operations.push({
190
+ kind: 'CREATE_VARIABLE',
191
+ symbolicId: variableId,
192
+ collectionId,
193
+ path: binding.variableName,
194
+ variableType: binding.convention.variableType,
195
+ });
196
+ }
197
+
198
+ if (!binding.isBound) {
199
+ operations.push({
200
+ kind: 'BIND_VARIABLE',
201
+ nodeId: node.id,
202
+ property: binding.property,
203
+ variableId,
204
+ });
205
+ }
206
+ }
207
+
208
+ return operations;
209
+ }
210
+
211
+ async planApplyBindings(node: SceneNode): Promise<Operation[]> {
212
+ return (await this.inferExpectedBindings(node))
213
+ .filter((binding) => binding.exists && !binding.isBound && !!binding.variableId)
214
+ .map((binding) => ({
215
+ kind: 'BIND_VARIABLE' as const,
216
+ nodeId: node.id,
217
+ property: binding.property,
218
+ variableId: binding.variableId as string,
219
+ }));
220
+ }
221
+ }
@@ -0,0 +1,45 @@
1
+ import type { QueryScope } from '../../shared/protocol.js';
2
+ import { resolveScopeRoots } from '../utils/pageRoots.js';
3
+
4
+ function walkNodes(node: SceneNode, visitor: (node: SceneNode) => void): void {
5
+ visitor(node);
6
+ if ('children' in node) {
7
+ for (const child of node.children) {
8
+ walkNodes(child, visitor);
9
+ }
10
+ }
11
+ }
12
+
13
+ export function buildMatcher(expr: string): (name: string) => boolean {
14
+ const trimmed = expr.trim();
15
+ if (!trimmed || trimmed === '**') {
16
+ return () => true;
17
+ }
18
+
19
+ // `*` is the only wildcard this engine defines, so every other regex
20
+ // metacharacter is escaped to its literal self. `?` used to be left through:
21
+ // a query for a node genuinely named `Card?` compiled to `Card?`, which makes
22
+ // the `d` optional and quietly matched `Car` instead.
23
+ const escaped = trimmed.replace(/[.+^${}()|[\]\\?]/g, '\\$&');
24
+ const pattern = `^${escaped.replace(/\*/g, '[^/]*')}$`;
25
+ const re = new RegExp(pattern);
26
+ return (name: string) => re.test(name);
27
+ }
28
+
29
+ export class QueryEngine {
30
+ async resolve(expr: string, scope: QueryScope): Promise<SceneNode[]> {
31
+ const roots = await resolveScopeRoots(scope);
32
+ const matcher = buildMatcher(expr);
33
+ const results: SceneNode[] = [];
34
+
35
+ for (const root of roots) {
36
+ walkNodes(root, (node) => {
37
+ if (matcher(node.name)) {
38
+ results.push(node);
39
+ }
40
+ });
41
+ }
42
+
43
+ return results;
44
+ }
45
+ }