@toghen/figma 0.0.0-stage → 0.1.1
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 +1067 -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 +24161 -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 +565 -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,18 @@
|
|
|
1
|
+
import { getState, setState } from '../runtimeState.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Stops the run in progress, and leaves the plan alone.
|
|
5
|
+
*
|
|
6
|
+
* Clearing `lastPlan` here raced the handler still executing it: by the time the
|
|
7
|
+
* run reported what it had managed to apply, the plan describing the rest was
|
|
8
|
+
* already gone. Plan lifecycle belongs to the execute handler, which clears it
|
|
9
|
+
* when the run finishes.
|
|
10
|
+
*
|
|
11
|
+
* Nothing needs to be rewound. A mode is recorded only once its value has
|
|
12
|
+
* actually landed, so re-planning *is* the resume — the next plan contains
|
|
13
|
+
* exactly what this run did not finish.
|
|
14
|
+
*/
|
|
15
|
+
export async function CANCEL_OPERATION(): Promise<void> {
|
|
16
|
+
getState().activeOperation?.abort();
|
|
17
|
+
setState({ activeOperation: null });
|
|
18
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { PlanType, Response } from '../../shared/protocol.js';
|
|
2
|
+
import { runOperations } from '../engines/batchRunner.js';
|
|
3
|
+
import { getState, setState } from '../runtimeState.js';
|
|
4
|
+
import { SimpleAbortController } from '../utils/abortController.js';
|
|
5
|
+
|
|
6
|
+
function postToUi(message: Response): void {
|
|
7
|
+
figma.ui.postMessage(message);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Runs the last plan, when it is one of `planType`: the conventions' bindings, or Apply's tokens. */
|
|
11
|
+
async function executePlan(planType: PlanType, commandType: string, missing: string): Promise<void> {
|
|
12
|
+
const state = getState();
|
|
13
|
+
if (!state.lastPlan || state.lastPlanType !== planType) {
|
|
14
|
+
postToUi({
|
|
15
|
+
type: 'ERROR',
|
|
16
|
+
payload: {
|
|
17
|
+
message: missing,
|
|
18
|
+
code: 'NO_PLAN',
|
|
19
|
+
commandType,
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const controller = new SimpleAbortController();
|
|
26
|
+
setState({ activeOperation: controller });
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const summary = await runOperations(state.lastPlan, {
|
|
30
|
+
signal: controller.signal,
|
|
31
|
+
onProgress: (completed, total, currentLabel) => {
|
|
32
|
+
postToUi({
|
|
33
|
+
type: 'OPERATION_PROGRESS',
|
|
34
|
+
payload: { completed, total, currentLabel },
|
|
35
|
+
});
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
setState({
|
|
40
|
+
activeOperation: null,
|
|
41
|
+
lastPlan: null,
|
|
42
|
+
lastPlanType: null,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
postToUi({
|
|
46
|
+
type: 'OPERATION_COMPLETE',
|
|
47
|
+
payload: summary,
|
|
48
|
+
});
|
|
49
|
+
} catch (error) {
|
|
50
|
+
setState({ activeOperation: null });
|
|
51
|
+
postToUi({
|
|
52
|
+
type: 'ERROR',
|
|
53
|
+
payload: {
|
|
54
|
+
message: error instanceof Error ? error.message : String(error),
|
|
55
|
+
code: 'FIGMA_API_ERROR',
|
|
56
|
+
commandType,
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function EXECUTE_APPLY_BINDINGS(): Promise<void> {
|
|
63
|
+
return executePlan('apply_bindings', 'EXECUTE_APPLY_BINDINGS', 'Create an apply-bindings plan before executing.');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Apply's execution: every `APPLY_TOKEN` written through the field writer. No stamp is written. */
|
|
67
|
+
export function EXECUTE_APPLY(): Promise<void> {
|
|
68
|
+
return executePlan('apply', 'EXECUTE_APPLY', 'Create an apply plan before executing.');
|
|
69
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { Response } from '../../shared/protocol.js';
|
|
2
|
+
import { runOperations } from '../engines/batchRunner.js';
|
|
3
|
+
import { getState, setState } from '../runtimeState.js';
|
|
4
|
+
import { SimpleAbortController } from '../utils/abortController.js';
|
|
5
|
+
|
|
6
|
+
function postToUi(message: Response): void {
|
|
7
|
+
figma.ui.postMessage(message);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function EXECUTE_SCAFFOLD(): Promise<void> {
|
|
11
|
+
const state = getState();
|
|
12
|
+
if (!state.lastPlan || state.lastPlanType !== 'scaffold') {
|
|
13
|
+
postToUi({
|
|
14
|
+
type: 'ERROR',
|
|
15
|
+
payload: {
|
|
16
|
+
message: 'Create a scaffold plan before executing.',
|
|
17
|
+
code: 'NO_PLAN',
|
|
18
|
+
commandType: 'EXECUTE_SCAFFOLD',
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const controller = new SimpleAbortController();
|
|
25
|
+
setState({ activeOperation: controller });
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const summary = await runOperations(state.lastPlan, {
|
|
29
|
+
signal: controller.signal,
|
|
30
|
+
onProgress: (completed, total, currentLabel) => {
|
|
31
|
+
postToUi({
|
|
32
|
+
type: 'OPERATION_PROGRESS',
|
|
33
|
+
payload: { completed, total, currentLabel },
|
|
34
|
+
});
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
setState({
|
|
39
|
+
activeOperation: null,
|
|
40
|
+
lastPlan: null,
|
|
41
|
+
lastPlanType: null,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
postToUi({
|
|
45
|
+
type: 'OPERATION_COMPLETE',
|
|
46
|
+
payload: summary,
|
|
47
|
+
});
|
|
48
|
+
} catch (error) {
|
|
49
|
+
setState({ activeOperation: null });
|
|
50
|
+
postToUi({
|
|
51
|
+
type: 'ERROR',
|
|
52
|
+
payload: {
|
|
53
|
+
message: error instanceof Error ? error.message : String(error),
|
|
54
|
+
code: 'FIGMA_API_ERROR',
|
|
55
|
+
commandType: 'EXECUTE_SCAFFOLD',
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import type { Operation } from '../../shared/operations.js';
|
|
2
|
+
import type { Response } from '../../shared/protocol.js';
|
|
3
|
+
import { runOperations } from '../engines/batchRunner.js';
|
|
4
|
+
import { getState, setState } from '../runtimeState.js';
|
|
5
|
+
import { buildSyncStatePayload } from '../utils/syncState.js';
|
|
6
|
+
import { SimpleAbortController } from '../utils/abortController.js';
|
|
7
|
+
|
|
8
|
+
function postToUi(message: Response): void {
|
|
9
|
+
figma.ui.postMessage(message);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Loads the fonts the plan's text styles name. A font that was listed and
|
|
14
|
+
* cannot be loaded costs its styles, not the run: each becomes a skip, as a
|
|
15
|
+
* style with no family to load is skipped at plan time.
|
|
16
|
+
*/
|
|
17
|
+
async function preloadFontsForPlan(ops: readonly Operation[]): Promise<Operation[]> {
|
|
18
|
+
const fontOf = (op: Operation): FontName | null => {
|
|
19
|
+
if (op.kind !== 'CREATE_STYLE' && op.kind !== 'UPDATE_STYLE') return null;
|
|
20
|
+
const props = op.props as Record<string, unknown>;
|
|
21
|
+
if (typeof props.fontFamily !== 'string' || !props.fontFamily) return null;
|
|
22
|
+
return { family: props.fontFamily, style: typeof props.fontStyle === 'string' ? props.fontStyle : 'Regular' };
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const refs = [
|
|
26
|
+
...new Set(
|
|
27
|
+
ops
|
|
28
|
+
.map(fontOf)
|
|
29
|
+
.filter((font) => font !== null)
|
|
30
|
+
.map((font) => JSON.stringify(font)),
|
|
31
|
+
),
|
|
32
|
+
];
|
|
33
|
+
const failed = new Set<string>();
|
|
34
|
+
await Promise.all(refs.map((ref) => figma.loadFontAsync(JSON.parse(ref) as FontName).catch(() => void failed.add(ref))));
|
|
35
|
+
|
|
36
|
+
return ops.map((op) => {
|
|
37
|
+
const font = fontOf(op);
|
|
38
|
+
if (!font || !failed.has(JSON.stringify(font)) || (op.kind !== 'CREATE_STYLE' && op.kind !== 'UPDATE_STYLE')) return op;
|
|
39
|
+
return {
|
|
40
|
+
kind: 'SKIP',
|
|
41
|
+
reason: `"${op.tokenPath}" has no family Figma can load: "${font.family}" is listed but could not be loaded.`,
|
|
42
|
+
path: op.tokenPath,
|
|
43
|
+
cause: 'font-not-loaded',
|
|
44
|
+
reported: true,
|
|
45
|
+
};
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function EXECUTE_STYLE_SYNC(): Promise<void> {
|
|
50
|
+
const state = getState();
|
|
51
|
+
|
|
52
|
+
if (!state.resolver) {
|
|
53
|
+
postToUi({
|
|
54
|
+
type: 'ERROR',
|
|
55
|
+
payload: {
|
|
56
|
+
message: 'Load a resolver document before executing style sync.',
|
|
57
|
+
code: 'NO_RESOLVER',
|
|
58
|
+
commandType: 'EXECUTE_STYLE_SYNC',
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (!state.lastPlan || state.lastPlanType !== 'style_sync') {
|
|
65
|
+
postToUi({
|
|
66
|
+
type: 'ERROR',
|
|
67
|
+
payload: {
|
|
68
|
+
message: 'Create a style sync plan before executing.',
|
|
69
|
+
code: 'NO_PLAN',
|
|
70
|
+
commandType: 'EXECUTE_STYLE_SYNC',
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const controller = new SimpleAbortController();
|
|
77
|
+
setState({ activeOperation: controller });
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
const plan = await preloadFontsForPlan(state.lastPlan);
|
|
81
|
+
|
|
82
|
+
const summary = await runOperations(plan, {
|
|
83
|
+
signal: controller.signal,
|
|
84
|
+
onProgress: (completed, total, currentLabel) => {
|
|
85
|
+
postToUi({
|
|
86
|
+
type: 'OPERATION_PROGRESS',
|
|
87
|
+
payload: { completed, total, currentLabel },
|
|
88
|
+
});
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// What the run applied is already stamped on each style it wrote, from what Figma reported back.
|
|
93
|
+
setState({
|
|
94
|
+
activeOperation: null,
|
|
95
|
+
lastPlan: null,
|
|
96
|
+
lastPlanType: null,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
postToUi({
|
|
100
|
+
type: 'OPERATION_COMPLETE',
|
|
101
|
+
payload: summary,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// The stamps just moved. A run that changes them and leaves the header
|
|
105
|
+
// reading the old numbers is the defect this reporting exists to close.
|
|
106
|
+
postToUi({
|
|
107
|
+
type: 'SYNC_STATE',
|
|
108
|
+
payload: await buildSyncStatePayload(state.resolver, state.conversionMap),
|
|
109
|
+
});
|
|
110
|
+
} catch (error) {
|
|
111
|
+
setState({ activeOperation: null });
|
|
112
|
+
postToUi({
|
|
113
|
+
type: 'ERROR',
|
|
114
|
+
payload: {
|
|
115
|
+
message: error instanceof Error ? error.message : String(error),
|
|
116
|
+
code: 'FIGMA_API_ERROR',
|
|
117
|
+
commandType: 'EXECUTE_STYLE_SYNC',
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { SimpleAbortController } from '../utils/abortController.js';
|
|
2
|
+
import type { Response } from '../../shared/protocol.js';
|
|
3
|
+
import { runOperations } from '../engines/batchRunner.js';
|
|
4
|
+
import { getState, setState } from '../runtimeState.js';
|
|
5
|
+
import { buildSyncStatePayload } from '../utils/syncState.js';
|
|
6
|
+
|
|
7
|
+
function postToUi(message: Response): void {
|
|
8
|
+
figma.ui.postMessage(message);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function EXECUTE_VARIABLE_SYNC(): Promise<void> {
|
|
12
|
+
const state = getState();
|
|
13
|
+
|
|
14
|
+
if (!state.resolver) {
|
|
15
|
+
postToUi({
|
|
16
|
+
type: 'ERROR',
|
|
17
|
+
payload: {
|
|
18
|
+
message: 'Load a resolver document before executing sync.',
|
|
19
|
+
code: 'NO_RESOLVER',
|
|
20
|
+
commandType: 'EXECUTE_VARIABLE_SYNC',
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (!state.lastPlan || state.lastPlanType !== 'variable_sync') {
|
|
27
|
+
postToUi({
|
|
28
|
+
type: 'ERROR',
|
|
29
|
+
payload: {
|
|
30
|
+
message: 'Create a variable sync plan before executing.',
|
|
31
|
+
code: 'NO_PLAN',
|
|
32
|
+
commandType: 'EXECUTE_VARIABLE_SYNC',
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const controller = new SimpleAbortController();
|
|
39
|
+
setState({ activeOperation: controller });
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const summary = await runOperations(state.lastPlan, {
|
|
43
|
+
signal: controller.signal,
|
|
44
|
+
onProgress: (completed, total, currentLabel) => {
|
|
45
|
+
postToUi({
|
|
46
|
+
type: 'OPERATION_PROGRESS',
|
|
47
|
+
payload: { completed, total, currentLabel },
|
|
48
|
+
});
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// What the run applied is already stamped on each variable it wrote, from what Figma reported back, so a run
|
|
53
|
+
// that stopped halfway resumes: the next plan holds exactly the modes that did not land.
|
|
54
|
+
setState({
|
|
55
|
+
activeOperation: null,
|
|
56
|
+
lastPlan: null,
|
|
57
|
+
lastPlanType: null,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
postToUi({
|
|
61
|
+
type: 'OPERATION_COMPLETE',
|
|
62
|
+
payload: summary,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// The stamps just moved. A run that changes them and leaves the header
|
|
66
|
+
// reading the old numbers is the defect this reporting exists to close.
|
|
67
|
+
postToUi({
|
|
68
|
+
type: 'SYNC_STATE',
|
|
69
|
+
payload: await buildSyncStatePayload(state.resolver, state.conversionMap),
|
|
70
|
+
});
|
|
71
|
+
} catch (error) {
|
|
72
|
+
setState({ activeOperation: null });
|
|
73
|
+
postToUi({
|
|
74
|
+
type: 'ERROR',
|
|
75
|
+
payload: {
|
|
76
|
+
message: error instanceof Error ? error.message : String(error),
|
|
77
|
+
code: 'FIGMA_API_ERROR',
|
|
78
|
+
commandType: 'EXECUTE_VARIABLE_SYNC',
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import type { ResolverInput } from '@toghen/lib';
|
|
2
|
+
import type { Command, Response } from '../../shared/protocol.js';
|
|
3
|
+
import { exportToJSON } from '../engines/ioAdapter.js';
|
|
4
|
+
import type { Stamp } from '../components/stamp.js';
|
|
5
|
+
import { getState } from '../runtimeState.js';
|
|
6
|
+
import { indexFigmaCollections } from '../utils/figmaCollectionIndex.js';
|
|
7
|
+
import { flatMap } from '../utils/lists.js';
|
|
8
|
+
import { localStyles } from '../utils/tokenStamps.js';
|
|
9
|
+
|
|
10
|
+
type ExportDocumentCommand = Extract<Command, { type: 'EXPORT_DOCUMENT' }>;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Writes the resolved tokens out as DTCG JSON, for one combination of contexts.
|
|
14
|
+
*
|
|
15
|
+
* Figma styles and this format are both single-mode, so a document with several
|
|
16
|
+
* contexts cannot round-trip through here. The combination is chosen
|
|
17
|
+
* deliberately: one context per modifier, each defaulting to the modifier's
|
|
18
|
+
* default and shown on the screen before the command runs. It is resolved on its
|
|
19
|
+
* own (`resolver.resolveToDesignSystem(input)`), never as part of the product of
|
|
20
|
+
* every combination, which a large document cannot finish. The combination
|
|
21
|
+
* appears in the filename, and the contexts left out are named to the user.
|
|
22
|
+
*/
|
|
23
|
+
export async function EXPORT_DOCUMENT(payload: ExportDocumentCommand['payload']): Promise<void> {
|
|
24
|
+
const { resolver } = getState();
|
|
25
|
+
|
|
26
|
+
if (!resolver) {
|
|
27
|
+
postToUi({
|
|
28
|
+
type: 'ERROR',
|
|
29
|
+
payload: {
|
|
30
|
+
message: 'Load a resolver document before exporting.',
|
|
31
|
+
code: 'NO_RESOLVER',
|
|
32
|
+
commandType: 'EXPORT_DOCUMENT',
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const declared = resolver.getModifiers();
|
|
40
|
+
const modifiers = declared.map((modifier) => modifier.name);
|
|
41
|
+
const asked = payload.contexts ?? {};
|
|
42
|
+
for (const name of Object.keys(asked)) {
|
|
43
|
+
if (!modifiers.includes(name)) {
|
|
44
|
+
throw new Error(`"${name}" is not one of this document's modifiers (${modifiers.join(', ') || 'none'}).`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const input: ResolverInput = {};
|
|
49
|
+
for (const { name, contexts, default: fallback } of declared) {
|
|
50
|
+
const context = asked[name] ?? fallback;
|
|
51
|
+
if (context === undefined || !contexts.includes(context)) {
|
|
52
|
+
throw new Error(`"${context}" is not one of the contexts of "${name}" (${contexts.join(', ')}).`);
|
|
53
|
+
}
|
|
54
|
+
input[name] = context;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const ds = resolver.resolveToDesignSystem(input);
|
|
58
|
+
const mode = resolver.getModeName(input);
|
|
59
|
+
|
|
60
|
+
const json = exportToJSON(ds, payload.includeExtensions ? await stampsByToken() : new Map(), {
|
|
61
|
+
includeExtensions: payload.includeExtensions,
|
|
62
|
+
// Where the tokens came from. The resolver document can say which
|
|
63
|
+
// revision of the system it describes; without carrying it here, the
|
|
64
|
+
// export said only which combination it held.
|
|
65
|
+
documentVersion: resolver.getDeclaredVersion(),
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const omitted = flatMap(declared, ({ name, contexts }) => contexts.filter((context) => context !== input[name]));
|
|
69
|
+
if (omitted.length > 0) {
|
|
70
|
+
figma.notify(
|
|
71
|
+
`Exported ${mode}. ${omitted.join(', ')} ${omitted.length === 1 ? 'is' : 'are'} ` +
|
|
72
|
+
`not in this file — DTCG token files hold one context of each modifier.`,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const baseName = resolver.name?.trim() || 'resolver';
|
|
77
|
+
// A mode name joins the contexts with "/", which a filename cannot hold.
|
|
78
|
+
const filename = omitted.length > 0 ? `${baseName}.${mode.replace(/\//g, '.')}.json` : `${baseName}.json`;
|
|
79
|
+
|
|
80
|
+
postToUi({
|
|
81
|
+
type: 'DOCUMENT_EXPORTED',
|
|
82
|
+
payload: { json, filename },
|
|
83
|
+
});
|
|
84
|
+
} catch (error) {
|
|
85
|
+
postToUi({
|
|
86
|
+
type: 'ERROR',
|
|
87
|
+
payload: {
|
|
88
|
+
message: error instanceof Error ? error.message : String(error),
|
|
89
|
+
code: 'INVALID_DOCUMENT',
|
|
90
|
+
commandType: 'EXPORT_DOCUMENT',
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* The stamp each variable and style Toghen wrote in the file carries, by the token it names, without its id: a target's
|
|
98
|
+
* ids stay in the file they belong to, out of the token file. A person's copy is not Toghen's and is not among them.
|
|
99
|
+
*/
|
|
100
|
+
async function stampsByToken(): Promise<Map<string, Stamp>> {
|
|
101
|
+
const stamps = new Map<string, Stamp>();
|
|
102
|
+
const add = (stamp: Stamp | undefined): void => {
|
|
103
|
+
if (stamp?.is.token === undefined) return;
|
|
104
|
+
const { id: _id, ...rest } = stamp;
|
|
105
|
+
stamps.set(stamp.is.token, rest);
|
|
106
|
+
};
|
|
107
|
+
for (const stamped of Object.values((await indexFigmaCollections()).variablesByToken)) for (const { stamp } of stamped) add(stamp);
|
|
108
|
+
for (const { stamp } of await localStyles()) add(stamp);
|
|
109
|
+
return stamps;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function postToUi(message: Response): void {
|
|
113
|
+
figma.ui.postMessage(message);
|
|
114
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { Response } from '../../shared/protocol.js';
|
|
2
|
+
import { buildSelectionPayload } from '../utils/buildPayloads.js';
|
|
3
|
+
import { indexFigmaCollections } from '../utils/figmaCollectionIndex.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Answers with the current selection.
|
|
7
|
+
*
|
|
8
|
+
* The UI hears about selection *changes* as they happen, so this covers the one
|
|
9
|
+
* case that event cannot: a panel opening and needing to know what is selected
|
|
10
|
+
* now, rather than waiting for the user to click something else.
|
|
11
|
+
*/
|
|
12
|
+
export async function GET_SELECTION(): Promise<void> {
|
|
13
|
+
const response: Response = {
|
|
14
|
+
type: 'SELECTION_CHANGED',
|
|
15
|
+
payload: buildSelectionPayload(figma.currentPage.selection),
|
|
16
|
+
};
|
|
17
|
+
figma.ui.postMessage(response);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Answers with the file's variable collections and their modes.
|
|
22
|
+
*
|
|
23
|
+
* The System pane matches them by name against the collections the document
|
|
24
|
+
* would build, and shows how many variables each holds beside how many the
|
|
25
|
+
* document would put there.
|
|
26
|
+
*/
|
|
27
|
+
export async function GET_FIGMA_COLLECTIONS(): Promise<void> {
|
|
28
|
+
const index = await indexFigmaCollections();
|
|
29
|
+
const variableCount: Record<string, number> = {};
|
|
30
|
+
for (const variable of Object.values(index.variableById)) {
|
|
31
|
+
variableCount[variable.variableCollectionId] = (variableCount[variable.variableCollectionId] ?? 0) + 1;
|
|
32
|
+
}
|
|
33
|
+
const response: Response = {
|
|
34
|
+
type: 'FIGMA_COLLECTIONS',
|
|
35
|
+
payload: Object.keys(index.byId).map((id) => ({
|
|
36
|
+
id,
|
|
37
|
+
name: index.byId[id].name,
|
|
38
|
+
modes: index.byId[id].modes,
|
|
39
|
+
variableCount: variableCount[id] ?? 0,
|
|
40
|
+
})),
|
|
41
|
+
};
|
|
42
|
+
figma.ui.postMessage(response);
|
|
43
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export {
|
|
2
|
+
LOAD_DOCUMENT,
|
|
3
|
+
GET_DOCUMENT_STATE,
|
|
4
|
+
GET_SYNC_STATE,
|
|
5
|
+
GET_RECENTS,
|
|
6
|
+
LOAD_RECENT,
|
|
7
|
+
FORGET_RECENT,
|
|
8
|
+
} from './loadDocument.js';
|
|
9
|
+
export { INSPECT_CORRESPONDENCE } from './inspectCorrespondence.js';
|
|
10
|
+
export { GET_TOKEN_GRAPH } from './tokenGraph.js';
|
|
11
|
+
export { LOAD_CONVENTIONS, GET_CONVENTIONS } from './loadConventions.js';
|
|
12
|
+
export { PLAN_VARIABLE_SYNC } from './planVariableSync.js';
|
|
13
|
+
export { EXECUTE_VARIABLE_SYNC } from './executeVariableSync.js';
|
|
14
|
+
export { PLAN_STYLE_SYNC } from './planStyleSync.js';
|
|
15
|
+
export { EXECUTE_STYLE_SYNC } from './executeStyleSync.js';
|
|
16
|
+
export { EXPORT_DOCUMENT } from './exportDocument.js';
|
|
17
|
+
export { AUDIT_SELECTION } from './auditSelection.js';
|
|
18
|
+
export { PLAN_SCAFFOLD } from './planScaffold.js';
|
|
19
|
+
export { EXECUTE_SCAFFOLD } from './executeScaffold.js';
|
|
20
|
+
export { PLAN_APPLY_BINDINGS } from './planApplyBindings.js';
|
|
21
|
+
export { EXECUTE_APPLY_BINDINGS, EXECUTE_APPLY } from './executeApplyBindings.js';
|
|
22
|
+
export { PLAN_APPLY } from './planApply.js';
|
|
23
|
+
export { READ_SELECTION_BINDINGS } from './readSelectionBindings.js';
|
|
24
|
+
export { SELECT_NODES, COUNT_BOUND_LAYERS } from './boundLayers.js';
|
|
25
|
+
export { GET_TEMPLATES, KEEP_TEMPLATE, FORGET_TEMPLATE, PLAN_BUILD, EXECUTE_BUILD, LOOK_UP_NEEDS, PLAN_RELINK } from './templates.js';
|
|
26
|
+
export { CANCEL_OPERATION } from './cancelOperation.js';
|
|
27
|
+
export { GET_SELECTION, GET_FIGMA_COLLECTIONS } from './getSelection.js';
|
|
28
|
+
export { RESIZE_UI } from './resizeUi.js';
|
|
29
|
+
export { RUN_COMPONENT, CHECK_COMPONENTS, FINISH_COMPONENTS } from './runComponent.js';
|
|
30
|
+
export { GET_LOCK, DISCOVER_VARIABLES, LOCK_PICK, LOCK_FORGET, SET_BUILD_OVERRIDES, LIBRARY_KEYS, LOCK_WRITE } from './lock.js';
|