@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,49 @@
|
|
|
1
|
+
import type { Command, Response } from '../../shared/protocol.js';
|
|
2
|
+
import { VariableSyncEngine } from '../engines/variableSyncEngine.js';
|
|
3
|
+
import { getState, setState } from '../runtimeState.js';
|
|
4
|
+
import { danglingSymbolWarnings } from '../utils/planSymbols.js';
|
|
5
|
+
import { figmaCollectionNameFor } from '../utils/resolverModes.js';
|
|
6
|
+
|
|
7
|
+
type PlanVariableSyncCommand = Extract<Command, { type: 'PLAN_VARIABLE_SYNC' }>;
|
|
8
|
+
|
|
9
|
+
export async function PLAN_VARIABLE_SYNC(payload: PlanVariableSyncCommand['payload']): Promise<void> {
|
|
10
|
+
const state = getState();
|
|
11
|
+
if (!state.resolver) {
|
|
12
|
+
postToUi({
|
|
13
|
+
type: 'ERROR',
|
|
14
|
+
payload: {
|
|
15
|
+
message: 'Load a resolver document before planning sync.',
|
|
16
|
+
code: 'NO_RESOLVER',
|
|
17
|
+
commandType: 'PLAN_VARIABLE_SYNC',
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const collectionName = figmaCollectionNameFor(state.resolver, payload.collectionName);
|
|
24
|
+
const engine = new VariableSyncEngine();
|
|
25
|
+
const { operations } = await engine.buildPlan(state.resolver, collectionName, state.conversionMap, {
|
|
26
|
+
allowBreaking: payload.allowBreaking === true,
|
|
27
|
+
overwriteDrift: payload.overwriteDrift === true,
|
|
28
|
+
});
|
|
29
|
+
operations.push(...danglingSymbolWarnings(operations));
|
|
30
|
+
const summary = engine.summarizePlan(operations);
|
|
31
|
+
|
|
32
|
+
setState({
|
|
33
|
+
lastPlan: operations,
|
|
34
|
+
lastPlanType: 'variable_sync',
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
postToUi({
|
|
38
|
+
type: 'PLAN_READY',
|
|
39
|
+
payload: {
|
|
40
|
+
planType: 'variable_sync',
|
|
41
|
+
operations,
|
|
42
|
+
summary,
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function postToUi(response: Response): void {
|
|
48
|
+
figma.ui.postMessage(response);
|
|
49
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the selected layers are bound to, property by property, for the
|
|
3
|
+
* Selection pane. See `reference.md`, *What the selection is bound to*.
|
|
4
|
+
*
|
|
5
|
+
* The page and file scopes walk every layer `QueryEngine` reaches; the answer
|
|
6
|
+
* is capped, and says how many it left out rather than drawing a list nobody
|
|
7
|
+
* reads to the end.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { Command, FieldBinding, PropertyBinding, Response, SelectionBindingsPayload } from '../../shared/protocol.js';
|
|
11
|
+
import { FIELDS, namesStyle } from '../../shared/components/fields.js';
|
|
12
|
+
import { applyKindOf, reportField } from '../components/fieldWriter.js';
|
|
13
|
+
import { PropertyMapper, type ExpectedBinding } from '../engines/propertyMapper.js';
|
|
14
|
+
import { QueryEngine } from '../engines/queryEngine.js';
|
|
15
|
+
import { getStamp, stampOn } from '../components/stamp.js';
|
|
16
|
+
import { getState } from '../runtimeState.js';
|
|
17
|
+
import { PAINT_PROPERTIES, boundVariablesOf } from '../utils/boundVariables.js';
|
|
18
|
+
import { loadTemplates } from '../utils/templateStore.js';
|
|
19
|
+
|
|
20
|
+
type ReadCommand = Extract<Command, { type: 'READ_SELECTION_BINDINGS' }>;
|
|
21
|
+
|
|
22
|
+
/** The most layers one answer carries. */
|
|
23
|
+
export const MAX_NODES = 200;
|
|
24
|
+
|
|
25
|
+
export async function READ_SELECTION_BINDINGS(payload: ReadCommand['payload']): Promise<void> {
|
|
26
|
+
const state = getState();
|
|
27
|
+
const all = payload.scope === 'selection' ? [...figma.currentPage.selection] : await new QueryEngine().resolve('**', payload.scope);
|
|
28
|
+
const nodes = all.slice(0, MAX_NODES);
|
|
29
|
+
|
|
30
|
+
// A variable's stamp says which token it was written for.
|
|
31
|
+
const tokenOf = new Map<string, string>();
|
|
32
|
+
const variableName = new Map<string, string>();
|
|
33
|
+
for (const variable of await figma.variables.getLocalVariablesAsync()) {
|
|
34
|
+
variableName.set(variable.id, variable.name);
|
|
35
|
+
const token = getStamp(variable)?.is.token;
|
|
36
|
+
if (token !== undefined) tokenOf.set(variable.id, token);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const mapper = state.conventions.length > 0 ? new PropertyMapper(state.conventions) : null;
|
|
40
|
+
const kept = new Map(loadTemplates().map((template) => [template.name, template]));
|
|
41
|
+
|
|
42
|
+
const out: SelectionBindingsPayload['nodes'] = [];
|
|
43
|
+
for (const node of nodes) {
|
|
44
|
+
const bound = boundVariablesOf(node);
|
|
45
|
+
const expected = new Map<string, ExpectedBinding>();
|
|
46
|
+
for (const binding of mapper ? await mapper.inferExpectedBindings(node) : []) expected.set(binding.property, binding);
|
|
47
|
+
|
|
48
|
+
const template = templateOf(node, kept);
|
|
49
|
+
const fields = new Set(template ? Object.keys(stampOn(node)?.receipt ?? {}) : []);
|
|
50
|
+
|
|
51
|
+
const names = new Set<string>([...bound.keys(), ...expected.keys()]);
|
|
52
|
+
for (const [key, property] of Object.entries(PAINT_PROPERTIES)) if (solidColor(node, key) !== null) names.add(property);
|
|
53
|
+
|
|
54
|
+
const properties: PropertyBinding[] = [...names].map((property) => {
|
|
55
|
+
const id = bound.get(property);
|
|
56
|
+
const suggestion = expected.get(property);
|
|
57
|
+
return {
|
|
58
|
+
property,
|
|
59
|
+
bound: id ? { variableName: variableName.get(id) ?? id, tokenPath: tokenOf.get(id) ?? null } : null,
|
|
60
|
+
literal: id ? null : literalOf(node, property),
|
|
61
|
+
suggestion: !id && suggestion?.exists && suggestion.variableId ? (tokenOf.get(suggestion.variableId) ?? null) : null,
|
|
62
|
+
declaredBy: template && fields.has(fieldOf(property)) ? template.name : null,
|
|
63
|
+
};
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
out.push({
|
|
67
|
+
id: node.id,
|
|
68
|
+
name: node.name,
|
|
69
|
+
kind: node.type,
|
|
70
|
+
builtFrom: template ? { template: template.name, fileName: template.fileName, kept: template.kept } : null,
|
|
71
|
+
properties,
|
|
72
|
+
fields: await fieldRows(node, variableName, tokenOf),
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
postToUi({
|
|
77
|
+
type: 'SELECTION_BINDINGS',
|
|
78
|
+
payload: {
|
|
79
|
+
scope: payload.scope,
|
|
80
|
+
readAt: new Date().toISOString(),
|
|
81
|
+
nodes: out,
|
|
82
|
+
...(all.length > nodes.length ? { more: all.length - nodes.length } : {}),
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* A row for every field the field table lists, with a token column, for the layer's kind, read as the receipt reads
|
|
89
|
+
* it (`reportField`): bound to a variable, or to a style, or plain, with what it holds where that reads as one value.
|
|
90
|
+
* A layer of no kind the table has gets none.
|
|
91
|
+
*/
|
|
92
|
+
async function fieldRows(node: SceneNode, variableName: Map<string, string>, tokenOf: Map<string, string>): Promise<FieldBinding[]> {
|
|
93
|
+
if (!['FRAME', 'COMPONENT', 'INSTANCE', 'SLOT', 'TEXT', 'LINE'].includes(node.type)) return [];
|
|
94
|
+
const kind = applyKindOf(node);
|
|
95
|
+
const rows: FieldBinding[] = [];
|
|
96
|
+
for (const [field, spec] of Object.entries(FIELDS)) {
|
|
97
|
+
if (!spec.token || !spec.nodes.includes(kind)) continue;
|
|
98
|
+
const report = reportField(node, field);
|
|
99
|
+
if (namesStyle(spec)) {
|
|
100
|
+
const style = typeof report === 'string' && report !== '' ? await figma.getStyleByIdAsync(report) : null;
|
|
101
|
+
rows.push({ field, bound: style ? { name: style.name, tokenPath: getStamp(style)?.is.token ?? null } : null });
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
const reports = (Array.isArray(report) ? report : [report]) as Array<{ variable?: string | null } | null>;
|
|
105
|
+
const id = reports.find((each) => typeof each?.variable === 'string')?.variable ?? undefined;
|
|
106
|
+
rows.push(
|
|
107
|
+
id
|
|
108
|
+
? { field, bound: { name: variableName.get(id) ?? id, tokenPath: tokenOf.get(id) ?? null } }
|
|
109
|
+
: { field, bound: null, holds: literalOf(node, PAINT_PROPERTIES[field] ?? field) },
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
return rows;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** The kept template whose set this node is in, by the set's stamp. */
|
|
116
|
+
function templateOf<T>(node: SceneNode, kept: Map<string, T>): T | undefined {
|
|
117
|
+
let current: BaseNode | null = node;
|
|
118
|
+
while (current && current.type !== 'PAGE') {
|
|
119
|
+
if (current.type === 'COMPONENT_SET') {
|
|
120
|
+
const name = stampOn(current)?.is.component;
|
|
121
|
+
return name !== undefined ? kept.get(name) : undefined;
|
|
122
|
+
}
|
|
123
|
+
current = current.parent;
|
|
124
|
+
}
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** The layer field a property is written by, in a component file's vocabulary. */
|
|
129
|
+
function fieldOf(property: string): string {
|
|
130
|
+
if (property === PAINT_PROPERTIES.fills) return 'fills';
|
|
131
|
+
if (property === PAINT_PROPERTIES.strokes) return 'strokes';
|
|
132
|
+
return property;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** The first paint's colour as hex, when it is a solid paint. */
|
|
136
|
+
function solidColor(node: SceneNode, key: string): string | null {
|
|
137
|
+
const paints = (node as unknown as Record<string, unknown>)[key];
|
|
138
|
+
if (!Array.isArray(paints) || paints.length === 0) return null;
|
|
139
|
+
const paint = paints[0] as { type?: string; color?: { r: number; g: number; b: number }; opacity?: number };
|
|
140
|
+
if (paint.type !== 'SOLID' || !paint.color) return null;
|
|
141
|
+
const byte = (channel: number) =>
|
|
142
|
+
Math.round(channel * 255)
|
|
143
|
+
.toString(16)
|
|
144
|
+
.padStart(2, '0');
|
|
145
|
+
return `#${byte(paint.color.r)}${byte(paint.color.g)}${byte(paint.color.b)}`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** What an unbound property holds, written for reading. */
|
|
149
|
+
function literalOf(node: SceneNode, property: string): string | null {
|
|
150
|
+
if (property === PAINT_PROPERTIES.fills) return solidColor(node, 'fills');
|
|
151
|
+
if (property === PAINT_PROPERTIES.strokes) return solidColor(node, 'strokes');
|
|
152
|
+
const value = (node as unknown as Record<string, unknown>)[property];
|
|
153
|
+
if (typeof value === 'number') return String(Math.round(value * 100) / 100);
|
|
154
|
+
if (typeof value === 'string') return value;
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function postToUi(response: Response): void {
|
|
159
|
+
figma.ui.postMessage(response);
|
|
160
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { Command, Response, WindowMode } from '../../shared/protocol.js';
|
|
2
|
+
|
|
3
|
+
type ResizeUiCommand = Extract<Command, { type: 'RESIZE_UI' }>;
|
|
4
|
+
|
|
5
|
+
/** Key in `figma.clientStorage`, which is per user rather than per file. */
|
|
6
|
+
export const WINDOW_SIZES_KEY = 'windowSizes';
|
|
7
|
+
/** Where a size was kept before the window had two; read once, as the narrow size. */
|
|
8
|
+
export const LEGACY_WINDOW_SIZE_KEY = 'windowSize';
|
|
9
|
+
|
|
10
|
+
export interface WindowSize {
|
|
11
|
+
width: number;
|
|
12
|
+
height: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface WindowSizes {
|
|
16
|
+
mode: WindowMode;
|
|
17
|
+
narrow: WindowSize;
|
|
18
|
+
wide: WindowSize;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Below this the tab strip wraps and the result surface has no room to be
|
|
23
|
+
* anything but a header.
|
|
24
|
+
*/
|
|
25
|
+
export const MIN_WINDOW_SIZE: WindowSize = { width: 320, height: 320 };
|
|
26
|
+
|
|
27
|
+
/** The size a window that has never been resized opens at: the narrow one, which the design is drawn at. */
|
|
28
|
+
export const DEFAULT_WINDOW_SIZE: WindowSize = { width: 460, height: 720 };
|
|
29
|
+
|
|
30
|
+
/** The wide size until someone drags it: room for the Tokens browser's columns and a detail panel. */
|
|
31
|
+
export const DEFAULT_WIDE_SIZE: WindowSize = { width: 980, height: 620 };
|
|
32
|
+
|
|
33
|
+
const DEFAULT_SIZES: WindowSizes = { mode: 'narrow', narrow: DEFAULT_WINDOW_SIZE, wide: DEFAULT_WIDE_SIZE };
|
|
34
|
+
|
|
35
|
+
function clamp(size: WindowSize): WindowSize {
|
|
36
|
+
return {
|
|
37
|
+
width: Math.max(MIN_WINDOW_SIZE.width, Math.round(size.width)),
|
|
38
|
+
height: Math.max(MIN_WINDOW_SIZE.height, Math.round(size.height)),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const isSize = (value: unknown): value is WindowSize =>
|
|
43
|
+
typeof value === 'object' &&
|
|
44
|
+
value !== null &&
|
|
45
|
+
typeof (value as WindowSize).width === 'number' &&
|
|
46
|
+
typeof (value as WindowSize).height === 'number';
|
|
47
|
+
|
|
48
|
+
export async function RESIZE_UI(payload: ResizeUiCommand['payload']): Promise<void> {
|
|
49
|
+
if ('mode' in payload) {
|
|
50
|
+
// The switch: the other remembered size, and the mode stored with it.
|
|
51
|
+
const sizes = { ...(await loadWindowSizes()), mode: payload.mode };
|
|
52
|
+
const size = sizes[payload.mode];
|
|
53
|
+
figma.ui.resize(size.width, size.height);
|
|
54
|
+
await store(sizes);
|
|
55
|
+
postToUi({ type: 'WINDOW_MODE', payload: { mode: payload.mode } });
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const size = clamp(payload);
|
|
60
|
+
figma.ui.resize(size.width, size.height);
|
|
61
|
+
|
|
62
|
+
// Only at the end of a drag, and under the mode the window is in. Persisting
|
|
63
|
+
// every frame would write to storage dozens of times for one gesture.
|
|
64
|
+
if (payload.remember) {
|
|
65
|
+
const sizes = await loadWindowSizes();
|
|
66
|
+
await store({ ...sizes, [sizes.mode]: size });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The two sizes and the mode to open in, from a previous session.
|
|
72
|
+
*
|
|
73
|
+
* Stored per user rather than in the file: a window size is a property of the
|
|
74
|
+
* screen someone is sitting at, not of the design system.
|
|
75
|
+
*/
|
|
76
|
+
export async function loadWindowSizes(): Promise<WindowSizes> {
|
|
77
|
+
try {
|
|
78
|
+
const stored: unknown = await figma.clientStorage.getAsync(WINDOW_SIZES_KEY);
|
|
79
|
+
if (typeof stored === 'object' && stored !== null) {
|
|
80
|
+
const sizes = stored as Partial<WindowSizes>;
|
|
81
|
+
return {
|
|
82
|
+
mode: sizes.mode === 'wide' ? 'wide' : 'narrow',
|
|
83
|
+
narrow: isSize(sizes.narrow) ? clamp(sizes.narrow) : DEFAULT_WINDOW_SIZE,
|
|
84
|
+
wide: isSize(sizes.wide) ? clamp(sizes.wide) : DEFAULT_WIDE_SIZE,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
const legacy: unknown = await figma.clientStorage.getAsync(LEGACY_WINDOW_SIZE_KEY);
|
|
88
|
+
if (isSize(legacy)) return { ...DEFAULT_SIZES, narrow: clamp(legacy) };
|
|
89
|
+
} catch {
|
|
90
|
+
// A window size is never worth failing to open over.
|
|
91
|
+
}
|
|
92
|
+
return DEFAULT_SIZES;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function store(sizes: WindowSizes): Promise<void> {
|
|
96
|
+
try {
|
|
97
|
+
await figma.clientStorage.setAsync(WINDOW_SIZES_KEY, sizes);
|
|
98
|
+
} catch {
|
|
99
|
+
// The window still resized; only the memory of it is lost.
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function postToUi(response: Response): void {
|
|
104
|
+
figma.ui.postMessage(response);
|
|
105
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { Command, Response } from '../../shared/protocol.js';
|
|
2
|
+
import { ComponentRunError } from '../components/errors.js';
|
|
3
|
+
import { beginRun, findStrays, finishRun, strayLine } from '../components/endOfRun.js';
|
|
4
|
+
import { checkComponents, runComponentFile } from '../components/file.js';
|
|
5
|
+
import { brokenSetProblems } from '../components/snapshot.js';
|
|
6
|
+
import { getState } from '../runtimeState.js';
|
|
7
|
+
|
|
8
|
+
type RunComponentCommand = Extract<Command, { type: 'RUN_COMPONENT' }>;
|
|
9
|
+
type CheckComponentsCommand = Extract<Command, { type: 'CHECK_COMPONENTS' }>;
|
|
10
|
+
type FinishComponentsCommand = Extract<Command, { type: 'FINISH_COMPONENTS' }>;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Runs one component file, given as text and a name: the entry the Templates pane's build uses too (`components/file.ts`).
|
|
14
|
+
*
|
|
15
|
+
* The engine stops with a `ComponentRunError` before its first write for everything it can foresee
|
|
16
|
+
* (a file that does not read, a name that does not resolve, a set that is not ours, a step that breaks
|
|
17
|
+
* instances), so those come back as an `ERROR` carrying every reason. Anything else is Figma's, and
|
|
18
|
+
* the dispatcher reports it. A plugin does not roll back: a failure in the middle of the writes leaves
|
|
19
|
+
* what was written, and the next run reconciles it.
|
|
20
|
+
*/
|
|
21
|
+
export async function RUN_COMPONENT(payload: RunComponentCommand['payload']): Promise<void> {
|
|
22
|
+
try {
|
|
23
|
+
postToUi({ type: 'COMPONENT_RUN', payload: await runComponentFile(payload.text, payload.fileName, payload.options, payload.inputs) });
|
|
24
|
+
} catch (error) {
|
|
25
|
+
if (!(error instanceof ComponentRunError)) throw error;
|
|
26
|
+
postToUi({
|
|
27
|
+
type: 'ERROR',
|
|
28
|
+
payload: { message: error.message, code: 'COMPONENT_RUN_FAILED', commandType: 'RUN_COMPONENT', problems: error.problems },
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Reads every file and says every problem of every file, and which of their sets Figma already reports as broken, writing nothing. */
|
|
34
|
+
export async function CHECK_COMPONENTS(payload: CheckComponentsCommand['payload']): Promise<void> {
|
|
35
|
+
const { errors, components } = checkComponents(
|
|
36
|
+
payload.files,
|
|
37
|
+
{ names: payload.names === true, ...(payload.inputs ? { inputs: payload.inputs } : {}) },
|
|
38
|
+
getState().resolver,
|
|
39
|
+
);
|
|
40
|
+
const problems = [...errors, ...(await brokenSetProblems(components))];
|
|
41
|
+
// The start of a run. Strays are found once, here, before anything is written: they are breaking, so a real run without the flag stops on them.
|
|
42
|
+
beginRun();
|
|
43
|
+
const strays = payload.files.length > 0 ? (await findStrays()).map(strayLine) : [];
|
|
44
|
+
if (strays.length > 0 && !payload.dryRun && !payload.allowBreaking) problems.push(...strays);
|
|
45
|
+
if (problems.length === 0)
|
|
46
|
+
return postToUi({ type: 'COMPONENTS_CHECKED', payload: { files: payload.files.length, ...(strays.length > 0 ? { strays } : {}) } });
|
|
47
|
+
postToUi({
|
|
48
|
+
type: 'ERROR',
|
|
49
|
+
payload: {
|
|
50
|
+
message: `${problems.length === 1 ? 'one problem' : `${problems.length} problems`} in the component files, and nothing was sent on`,
|
|
51
|
+
code: 'COMPONENT_RUN_FAILED',
|
|
52
|
+
commandType: 'CHECK_COMPONENTS',
|
|
53
|
+
problems,
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** The end of a run: the strays removed when allowed, and each page's sets and artboards stacked again. */
|
|
59
|
+
export async function FINISH_COMPONENTS(payload: FinishComponentsCommand['payload']): Promise<void> {
|
|
60
|
+
postToUi({ type: 'COMPONENTS_FINISHED', payload: await finishRun(payload) });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function postToUi(response: Response): void {
|
|
64
|
+
figma.ui.postMessage(response);
|
|
65
|
+
}
|