@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,248 @@
1
+ /**
2
+ * A blueprint: one kdlet document, `<page>.blueprint.kdl`, that folds component files and artboard files into one
3
+ * page and declares what it needs.
4
+ *
5
+ * The reader reads the blueprint's own words (`page`, `tokens`, `library`, and `component` and `artboard` as
6
+ * containers) and hands each body to the readers of today's files. A body is handed over as the blueprint's own
7
+ * text with everything else blanked out: the other bodies and the blueprint's words become spaces, the braces of
8
+ * the body too, and the top-level `guard` and `define` nodes stay where they are. What is left is the body's file,
9
+ * header line first, at the lines and columns it has in the blueprint; so a blueprint is checked exactly as its
10
+ * files would be, and every problem points at the blueprint's own line.
11
+ *
12
+ * The format is `component-files.md`, *Blueprints*; the shapes are `reference.md`'s.
13
+ */
14
+
15
+ import { getLocation, parse } from '@bgotink/kdl';
16
+ import type { Node } from '@bgotink/kdl';
17
+ import { read as readDocument } from 'kdlet';
18
+ import { ARTBOARD_WORD, readArtboardFile } from './artboard.js';
19
+ import type { DesignSource } from './axes.js';
20
+ import { readComponentFile } from './index.js';
21
+ import type { ComponentFile } from './index.js';
22
+ import type { ComponentRef, Layer, Loc, Problem, SetField, Value } from './model.js';
23
+ import { report, split } from './read.js';
24
+
25
+ /** The words that are a blueprint's own. Beside them, a blueprint holds only kdlet's shared `guard` and `define`. */
26
+ export const BLUEPRINT_WORDS = ['page', 'tokens', 'library', 'component', ARTBOARD_WORD] as const;
27
+
28
+ /** Whether a file name is a blueprint's. */
29
+ export const isBlueprintFile = (fileName: string): boolean => /\.blueprint\.kdl$/.test(fileName);
30
+
31
+ /** `tokens <entry>… [name=…] [overrides=…]`: what the core's `load` is given. */
32
+ export interface BlueprintTokens {
33
+ /** One resolver document, one token file, or several token files, as written. */
34
+ entries: string[];
35
+ /** The collection's name, for token files. */
36
+ name?: string;
37
+ /** The file that holds the overrides map. */
38
+ overrides?: string;
39
+ }
40
+
41
+ export interface Blueprint {
42
+ /** Absent when the blueprint has no usable `page` line. */
43
+ page?: string;
44
+ tokens?: BlueprintTokens;
45
+ /** The `library` lines, in file order. */
46
+ libraries: string[];
47
+ /** Each `component` and `artboard` body, read as its file would be, in file order. */
48
+ bodies: ComponentFile[];
49
+ /** The blueprint's own problems and every body's, in file order, each once. */
50
+ problems: Problem[];
51
+ }
52
+
53
+ /** One component a blueprint's bodies name. */
54
+ export interface ComponentNeed {
55
+ ref: ComponentRef;
56
+ /** A `component` body of the same blueprint defines it. Never true of a key. */
57
+ satisfied: boolean;
58
+ /** Where the blueprint first names it. */
59
+ loc?: Loc;
60
+ }
61
+
62
+ export interface BlueprintNeeds {
63
+ tokens?: BlueprintTokens;
64
+ libraries: string[];
65
+ /** Every component the bodies name, once each, in the order first named. */
66
+ components: ComponentNeed[];
67
+ }
68
+
69
+ const position = (problem: Problem): number => (problem.line === undefined ? Infinity : problem.line * 100000 + (problem.column ?? 0));
70
+
71
+ const blank = (text: string): string => text.replace(/[^\r\n]/gu, ' ');
72
+
73
+ /** The blueprint's text with only `keep` left, and the braces of `body` blanked: the body's file, at the blueprint's lines. */
74
+ function fileOf(text: string, keep: Node[], body: Node): string {
75
+ const ranges: [number, number][] = [];
76
+ for (const node of keep) {
77
+ const at = getLocation(node)!;
78
+ if (node !== body || !node.children) {
79
+ ranges.push([at.start.offset, at.end.offset]);
80
+ continue;
81
+ }
82
+ const inside = getLocation(node.children!)!;
83
+ // The `{` stands just before the children, and the `}` just after them.
84
+ ranges.push([at.start.offset, inside.start.offset - 1], [inside.start.offset, inside.end.offset]);
85
+ }
86
+ ranges.sort((a, b) => a[0] - b[0]);
87
+ let out = '';
88
+ let from = 0;
89
+ for (const [start, end] of ranges) {
90
+ out += blank(text.slice(from, start)) + text.slice(start, end);
91
+ from = end;
92
+ }
93
+ return out + blank(text.slice(from));
94
+ }
95
+
96
+ /** Text in quotes, or why not. */
97
+ function text(entry: { getValue(): unknown; getTag(): string | null }): string | undefined {
98
+ const value = entry.getValue();
99
+ return entry.getTag() === null && typeof value === 'string' && value !== '' ? value : undefined;
100
+ }
101
+
102
+ /**
103
+ * Reads a blueprint's text, and each body against the design system and the inputs, as its file would be read.
104
+ *
105
+ * Every problem is reported, not the first.
106
+ */
107
+ export function readBlueprint(source: string, design?: DesignSource, inputs?: Record<string, unknown>): Blueprint {
108
+ const sink: Problem[] = [];
109
+ const blueprint: Blueprint = { libraries: [], bodies: [], problems: sink };
110
+ let nodes: Node[];
111
+ try {
112
+ nodes = parse(source, { storeLocations: true }).nodes;
113
+ } catch {
114
+ // kdlet says what is wrong with text that is not KDL, as it does for a file.
115
+ for (const problem of readDocument(source).problems)
116
+ sink.push({ severity: 'error', message: problem.message, line: problem.line, column: problem.column });
117
+ return blueprint;
118
+ }
119
+
120
+ const shared = nodes.filter((node) => node.getName() === 'guard' || node.getName() === 'define');
121
+ const bodies: Node[] = [];
122
+ let page: Node | undefined;
123
+ let tokens: Node | undefined;
124
+ for (const node of nodes) {
125
+ const word = node.getName();
126
+ if (word === 'guard' || word === 'define') continue;
127
+ if (word === 'component' || word === ARTBOARD_WORD) {
128
+ const header = (node.children?.nodes ?? []).find((child) => child.getName() === 'component' || child.getName() === ARTBOARD_WORD);
129
+ if (header)
130
+ report(
131
+ sink,
132
+ `the body of ${word} has its own "${header.getName()}" line: in a blueprint the name and its attributes go on the ${word} line, and the body holds the rest`,
133
+ header,
134
+ );
135
+ else bodies.push(node);
136
+ continue;
137
+ }
138
+ const { args, props } = split(sink, node, word);
139
+ if (word === 'page') {
140
+ if (page) report(sink, 'a blueprint builds into one page: "page" is written twice', node);
141
+ page = page ?? node;
142
+ for (const [key, entry] of props) report(sink, `page has no "${key}"`, entry);
143
+ for (const child of node.children?.nodes ?? []) report(sink, 'page takes no child nodes', child);
144
+ const name = args.length === 1 ? text(args[0]) : undefined;
145
+ if (name === undefined) report(sink, 'page needs its name as its only argument', args[0] ?? node);
146
+ else if (page === node) blueprint.page = name;
147
+ } else if (word === 'tokens') {
148
+ if (tokens) report(sink, 'a blueprint loads its tokens once: "tokens" is written twice', node);
149
+ tokens = tokens ?? node;
150
+ for (const child of node.children?.nodes ?? []) report(sink, 'tokens takes no child nodes', child);
151
+ const entries: string[] = [];
152
+ for (const arg of args) {
153
+ const entry = text(arg);
154
+ if (entry === undefined) report(sink, 'tokens takes the locations of a resolver document or of token files, in quotes', arg);
155
+ else entries.push(entry);
156
+ }
157
+ if (args.length === 0) report(sink, 'tokens needs a resolver document, a token file or several token files', node);
158
+ const loaded: BlueprintTokens = { entries };
159
+ for (const [key, entry] of props) {
160
+ const value = text(entry);
161
+ if (key !== 'name' && key !== 'overrides') report(sink, `tokens has no "${key}"; it takes name and overrides`, entry);
162
+ else if (value === undefined)
163
+ report(
164
+ sink,
165
+ `tokens: ${key} takes ${key === 'name' ? "the collection's name" : 'the location of an overrides map'}, in quotes`,
166
+ entry,
167
+ );
168
+ else if (key === 'name') loaded.name = value;
169
+ else loaded.overrides = value;
170
+ }
171
+ if (tokens === node && entries.length === args.length && entries.length > 0) blueprint.tokens = loaded;
172
+ } else if (word === 'library') {
173
+ for (const [key, entry] of props) report(sink, `library has no "${key}"`, entry);
174
+ for (const child of node.children?.nodes ?? []) report(sink, 'library takes no child nodes', child);
175
+ const name = args.length === 1 ? text(args[0]) : undefined;
176
+ if (name === undefined) report(sink, "library needs the library's name as its only argument", args[0] ?? node);
177
+ else blueprint.libraries.push(name);
178
+ } else {
179
+ report(
180
+ sink,
181
+ `"${word}" is not a word of a blueprint: it holds page, tokens, library, component and artboard, and the guard and define its bodies share`,
182
+ node,
183
+ );
184
+ }
185
+ }
186
+ if (!page) sink.push({ severity: 'error', message: 'the blueprint needs one "page <Name>"' });
187
+
188
+ const named = new Map<string, Node>();
189
+ for (const body of bodies) {
190
+ const artboard = body.getName() === ARTBOARD_WORD;
191
+ const file = fileOf(source, [...shared, body], body);
192
+ const read = artboard ? readArtboardFile(file, inputs) : readComponentFile(file, design);
193
+ blueprint.bodies.push(read);
194
+ sink.push(...read.problems);
195
+ const name = read.component?.name;
196
+ if (!name) continue;
197
+ const key = `${body.getName()}:${name}`;
198
+ if (named.has(key)) report(sink, `${body.getName()} "${name}" is written twice in the blueprint`, body);
199
+ named.set(key, body);
200
+ }
201
+
202
+ // A problem of a shared guard or define is found once per body: say it once.
203
+ const seen = new Set<string>();
204
+ const once = sink.filter((problem) => {
205
+ const key = `${problem.severity}:${problem.line}:${problem.column}:${problem.member}:${problem.message}`;
206
+ if (seen.has(key)) return false;
207
+ seen.add(key);
208
+ return true;
209
+ });
210
+ const ordered = once.map((problem, index) => ({ problem, index }));
211
+ ordered.sort((a, b) => position(a.problem) - position(b.problem) || a.index - b.index);
212
+ blueprint.problems = ordered.map((entry) => entry.problem);
213
+ return blueprint;
214
+ }
215
+
216
+ /**
217
+ * What a blueprint needs: its tokens, its libraries, and every component its bodies name, by name or by key. A name a
218
+ * `component` body of the same blueprint defines is satisfied by it. A body that does not read has no members, so it
219
+ * names nothing here.
220
+ */
221
+ export function blueprintNeeds(blueprint: Blueprint): BlueprintNeeds {
222
+ const defined = new Set<string>();
223
+ for (const body of blueprint.bodies) if (!body.artboard && body.component) defined.add(body.component.name);
224
+ const found = new Map<string, ComponentNeed>();
225
+ const add = (ref: ComponentRef, loc?: Loc): void => {
226
+ const key = 'name' in ref ? `name:${ref.name}` : `key:${ref.key}`;
227
+ if (!found.has(key)) found.set(key, { ref, satisfied: 'name' in ref && defined.has(ref.name), ...(loc ? { loc } : {}) });
228
+ };
229
+ const walk = (layer: Layer): void => {
230
+ const component = layer.fields.component as Value | undefined;
231
+ const at = layer.fieldLoc?.component ?? layer.loc;
232
+ if (component?.kind === 'plain' && typeof component.value === 'string') add({ name: component.value }, at);
233
+ else if (component?.kind === 'key') add({ key: component.key }, at);
234
+ for (const entry of Object.values((layer.fields.set as SetField | undefined) ?? {})) {
235
+ if (typeof entry.value === 'object' && 'key' in entry.value) add({ key: entry.value.key }, entry.loc ?? layer.fieldLoc?.set);
236
+ }
237
+ for (const child of layer.children) walk(child);
238
+ };
239
+ for (const body of blueprint.bodies) {
240
+ for (const property of body.component?.properties ?? []) if (property.type === 'INSTANCE_SWAP') add(property.default, property.loc);
241
+ for (const member of body.members) walk(member.tree);
242
+ }
243
+ return {
244
+ ...(blueprint.tokens ? { tokens: blueprint.tokens } : {}),
245
+ libraries: [...new Set(blueprint.libraries)],
246
+ components: [...found.values()],
247
+ };
248
+ }