@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
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Egildo Tagliareni
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,19 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @toghen/figma
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The Figma adapter of [Toghen](https://codeberg.org/Toghen). A resolver document, resolved through [`@toghen/lib`](https://codeberg.org/Toghen/lib), becomes a Figma file's Variables and Styles: one collection per modifier, a mode per context. A component file (`.component.kdl`) becomes a component set in the same file. Running again changes only the difference.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
pnpm add -D @toghen/figma
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
The package holds three pieces:
|
|
10
|
+
|
|
11
|
+
- **The plugin**, built into `dist/` with its `manifest.json`. Import it in the Figma desktop app.
|
|
12
|
+
- **Component files**, the format a component set is built from. See [component-files.md](https://codeberg.org/Toghen/figma/src/branch/main/component-files.md).
|
|
13
|
+
- **The command line**, `toghen-figma`. It pairs with the open plugin over a local socket and runs the whole job from a repository: read and check everything, sync variables, sync styles, then build each component file.
|
|
14
|
+
|
|
15
|
+
The [guide](https://codeberg.org/Toghen/figma/src/branch/main/guide.md) goes from a fresh checkout to a synced file, and the [reference](https://codeberg.org/Toghen/figma/src/branch/main/reference.md) lists the protocol and the schemas.
|
|
16
|
+
|
|
17
|
+
## License
|
|
18
|
+
|
|
19
|
+
MIT. See [LICENSE](https://codeberg.org/Toghen/figma/src/branch/main/LICENSE).
|
package/cli/bin.mjs
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// The `toghen-figma` command. It always runs `main`: comparing this file with
|
|
3
|
+
// `process.argv[1]` breaks when the package is reached through a symlink.
|
|
4
|
+
import { readFileSync } from 'node:fs';
|
|
5
|
+
import { homedir } from 'node:os';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import { register } from 'tsx/esm/api';
|
|
8
|
+
|
|
9
|
+
register();
|
|
10
|
+
const { main } = await import('./main.ts');
|
|
11
|
+
|
|
12
|
+
process.exitCode = await main(process.argv.slice(2), {
|
|
13
|
+
// pnpm runs a package script in the package's own directory; INIT_CWD is where the person typed it.
|
|
14
|
+
cwd: process.env.INIT_CWD ?? process.cwd(),
|
|
15
|
+
readFile: (path) => readFileSync(path, 'utf8'),
|
|
16
|
+
stdout: (text) => process.stdout.write(text),
|
|
17
|
+
stderr: (text) => process.stderr.write(text),
|
|
18
|
+
pairingFile: join(homedir(), '.toghen', 'figma-pairing'),
|
|
19
|
+
});
|
package/cli/bridge.ts
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The command line's half of the bridge: a localhost socket the plugin's UI connects to.
|
|
3
|
+
*
|
|
4
|
+
* It listens on a port the plugin's manifest names, and only on the loopback addresses. The socket's
|
|
5
|
+
* origin is `null`, so it cannot tell the plugin from another page that opens the port; the two pair
|
|
6
|
+
* instead. The plugin's first message is `{ "pair": "<code>" }`, and a connection without the right code
|
|
7
|
+
* is closed with `REFUSED` and never sees a token. Once paired, messages in both directions are the plugin's
|
|
8
|
+
* own protocol (`shared/protocol.ts`), one JSON object each, and the command line asks one question at a time.
|
|
9
|
+
*
|
|
10
|
+
* The code outlives the process: each run is a new one, and the plugin reconnects to it when it starts. It is kept in
|
|
11
|
+
* a file only the person's account can read, and `pairing` is how a run reads and renews it.
|
|
12
|
+
*
|
|
13
|
+
* A run with a library holds two plugins at once, one in the library file and one in the target file: one code each,
|
|
14
|
+
* one role each, on the same port (`serve`).
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { randomInt, timingSafeEqual } from 'node:crypto';
|
|
18
|
+
import { chmodSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
19
|
+
import { createServer, type Server } from 'node:http';
|
|
20
|
+
import type { AddressInfo } from 'node:net';
|
|
21
|
+
import { dirname } from 'node:path';
|
|
22
|
+
import { WebSocketServer, type WebSocket } from 'ws';
|
|
23
|
+
import type { Command, Response } from '../shared/protocol.js';
|
|
24
|
+
|
|
25
|
+
/** The port the manifest names. A port it does not declare is refused by Figma. */
|
|
26
|
+
export const PORT = 3055;
|
|
27
|
+
/** The close code for a pairing code the command line does not know. The plugin stops trying on it. */
|
|
28
|
+
export const REFUSED = 4001;
|
|
29
|
+
/** The close code for a plugin whose place another connection with the right code took. */
|
|
30
|
+
export const REPLACED = 4002;
|
|
31
|
+
/** After this many wrong codes in one run the run stops: nothing legitimate types a code that often. */
|
|
32
|
+
const MAX_REFUSALS = 5;
|
|
33
|
+
|
|
34
|
+
const ALPHABET = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
|
|
35
|
+
|
|
36
|
+
/** Eight characters from an alphabet without look-alikes, written `XXXX-XXXX`. */
|
|
37
|
+
export function newCode(): string {
|
|
38
|
+
let code = '';
|
|
39
|
+
for (let i = 0; i < 8; i += 1) code += ALPHABET[randomInt(ALPHABET.length)];
|
|
40
|
+
return `${code.slice(0, 4)}-${code.slice(4)}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** What is typed is compared without case, dashes or spaces. */
|
|
44
|
+
const normalize = (code: string): string => code.toUpperCase().replace(/[^A-Z0-9]/g, '');
|
|
45
|
+
|
|
46
|
+
const same = (a: string, b: string): boolean => {
|
|
47
|
+
const x = Buffer.from(normalize(a));
|
|
48
|
+
const y = Buffer.from(normalize(b));
|
|
49
|
+
return x.length === y.length && timingSafeEqual(x, y);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/** The pairing code kept between runs, in a file only this account can read. */
|
|
53
|
+
export const pairing = {
|
|
54
|
+
read(file: string): string | undefined {
|
|
55
|
+
try {
|
|
56
|
+
return readFileSync(file, 'utf8').trim() || undefined;
|
|
57
|
+
} catch {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
write(file: string, code: string): void {
|
|
62
|
+
mkdirSync(dirname(file), { recursive: true, mode: 0o700 });
|
|
63
|
+
writeFileSync(file, `${code}\n`, { mode: 0o600 });
|
|
64
|
+
// The mode above applies only to a file this call created.
|
|
65
|
+
chmodSync(file, 0o600);
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/** The port is held by something else. */
|
|
70
|
+
export class PortTaken extends Error {
|
|
71
|
+
constructor(readonly port: number) {
|
|
72
|
+
super(
|
|
73
|
+
`port ${port} is taken: another toghen-figma, or another program, is listening on it. Stop that one and run again; the plugin's manifest names this port, so it cannot be changed`,
|
|
74
|
+
);
|
|
75
|
+
this.name = 'PortTaken';
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** The plugin went away, or the run was stopped, while a question was waiting. */
|
|
80
|
+
export class PluginGone extends Error {}
|
|
81
|
+
|
|
82
|
+
export interface Bridge {
|
|
83
|
+
readonly port: number;
|
|
84
|
+
/** Resolves once a plugin has paired; rejects when too many wrong codes were tried. */
|
|
85
|
+
paired: Promise<void>;
|
|
86
|
+
/**
|
|
87
|
+
* Sends a command and resolves with the first response whose type is one of `answers`, or the `ERROR` that
|
|
88
|
+
* names this command. Progress and anything else the plugin posts meanwhile goes to `onOther`.
|
|
89
|
+
*/
|
|
90
|
+
request(command: Command, answers: Response['type'][], onOther?: (response: Response) => void): Promise<Response>;
|
|
91
|
+
close(): Promise<void>;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const listen = (server: Server, port: number, host: string): Promise<void> =>
|
|
95
|
+
new Promise((resolve, reject) => {
|
|
96
|
+
server.once('error', reject);
|
|
97
|
+
server.listen(port, host, () => {
|
|
98
|
+
server.off('error', reject);
|
|
99
|
+
resolve();
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
/** One plugin's end of the bridge, whose code a run can replace while it listens (a kept code nobody came back with). */
|
|
104
|
+
export interface Role extends Bridge {
|
|
105
|
+
/** From now on this code pairs for the role, and the one before is refused. */
|
|
106
|
+
renew(code: string): void;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Starts listening, on 127.0.0.1 and, where there is one, ::1: `localhost` is either, depending on who asks.
|
|
111
|
+
*
|
|
112
|
+
* One code per role: a run with a library holds two plugins, one in each file, on the one port. A connection pairs as
|
|
113
|
+
* the role whose code it gives, and replaces the plugin that held that role. Five wrong codes in all stop every role.
|
|
114
|
+
* Closing any role closes the server, and every plugin's socket with it.
|
|
115
|
+
*/
|
|
116
|
+
export async function serve<R extends string>(port: number, codes: Record<R, string>): Promise<Record<R, Role>> {
|
|
117
|
+
interface Held {
|
|
118
|
+
code: string;
|
|
119
|
+
plugin?: WebSocket;
|
|
120
|
+
onMessage?: (response: Response) => void;
|
|
121
|
+
onGone?: (error: Error) => void;
|
|
122
|
+
markPaired: () => void;
|
|
123
|
+
refuse: (error: Error) => void;
|
|
124
|
+
paired: Promise<void>;
|
|
125
|
+
}
|
|
126
|
+
const held = {} as Record<R, Held>;
|
|
127
|
+
for (const role of Object.keys(codes) as R[]) {
|
|
128
|
+
const entry = { code: codes[role] } as Held;
|
|
129
|
+
entry.paired = new Promise<void>((resolve, reject) => {
|
|
130
|
+
entry.markPaired = resolve;
|
|
131
|
+
entry.refuse = reject;
|
|
132
|
+
});
|
|
133
|
+
entry.paired.catch(() => {});
|
|
134
|
+
held[role] = entry;
|
|
135
|
+
}
|
|
136
|
+
const all = Object.values<Held>(held);
|
|
137
|
+
let refusals = 0;
|
|
138
|
+
|
|
139
|
+
const wss = new WebSocketServer({ noServer: true, maxPayload: 256 * 1024 * 1024 });
|
|
140
|
+
wss.on('connection', (socket) => {
|
|
141
|
+
let mine: Held | undefined;
|
|
142
|
+
socket.on('message', (data) => {
|
|
143
|
+
let message: { pair?: unknown } & Partial<Response>;
|
|
144
|
+
try {
|
|
145
|
+
message = JSON.parse(data.toString());
|
|
146
|
+
} catch {
|
|
147
|
+
return socket.close(REFUSED, 'not JSON');
|
|
148
|
+
}
|
|
149
|
+
if (!mine) {
|
|
150
|
+
const { pair } = message;
|
|
151
|
+
const found = typeof pair === 'string' ? all.find((entry) => same(pair, entry.code)) : undefined;
|
|
152
|
+
if (!found) {
|
|
153
|
+
socket.close(REFUSED, 'pairing code refused');
|
|
154
|
+
refusals += 1;
|
|
155
|
+
if (refusals >= MAX_REFUSALS)
|
|
156
|
+
for (const entry of all)
|
|
157
|
+
entry.refuse(
|
|
158
|
+
new Error(
|
|
159
|
+
`${refusals} connections gave a wrong pairing code, so the run stopped. Something other than the plugin is using port ${port}`,
|
|
160
|
+
),
|
|
161
|
+
);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
mine = found;
|
|
165
|
+
found.plugin?.close(REPLACED, 'another connection paired');
|
|
166
|
+
found.plugin = socket;
|
|
167
|
+
socket.send(JSON.stringify({ paired: true }));
|
|
168
|
+
found.markPaired();
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
mine.onMessage?.(message as Response);
|
|
172
|
+
});
|
|
173
|
+
socket.on('close', () => {
|
|
174
|
+
if (!mine || mine.plugin !== socket) return;
|
|
175
|
+
mine.plugin = undefined;
|
|
176
|
+
mine.onGone?.(new PluginGone('the plugin disconnected: it was closed, or Figma lost it'));
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const servers = [createServer(), createServer()];
|
|
181
|
+
for (const server of servers)
|
|
182
|
+
server.on('upgrade', (request, socket, head) => wss.handleUpgrade(request, socket, head, (ws) => wss.emit('connection', ws, request)));
|
|
183
|
+
try {
|
|
184
|
+
await listen(servers[0], port, '127.0.0.1');
|
|
185
|
+
} catch (error) {
|
|
186
|
+
if ((error as NodeJS.ErrnoException).code === 'EADDRINUSE') throw new PortTaken(port);
|
|
187
|
+
throw error;
|
|
188
|
+
}
|
|
189
|
+
const bound = (servers[0].address() as AddressInfo).port;
|
|
190
|
+
try {
|
|
191
|
+
await listen(servers[1], bound, '::1');
|
|
192
|
+
} catch (error) {
|
|
193
|
+
// No IPv6 loopback is not a problem; a port something else holds there is.
|
|
194
|
+
if ((error as NodeJS.ErrnoException).code === 'EADDRINUSE') {
|
|
195
|
+
servers[0].close();
|
|
196
|
+
throw new PortTaken(bound);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
let closing: Promise<void> | undefined;
|
|
201
|
+
const close = (): Promise<void> =>
|
|
202
|
+
(closing ??= (async () => {
|
|
203
|
+
for (const entry of all) {
|
|
204
|
+
entry.onMessage = entry.onGone = undefined;
|
|
205
|
+
// A paired plugin is closed with the normal code, and reconnects when the next run listens.
|
|
206
|
+
entry.plugin?.close(1000);
|
|
207
|
+
}
|
|
208
|
+
for (const client of wss.clients) client.terminate();
|
|
209
|
+
wss.close();
|
|
210
|
+
await Promise.all(servers.map((server) => new Promise<void>((done) => (server.listening ? server.close(() => done()) : done()))));
|
|
211
|
+
})());
|
|
212
|
+
|
|
213
|
+
const roles = {} as Record<R, Role>;
|
|
214
|
+
for (const role of Object.keys(held) as R[]) {
|
|
215
|
+
const entry = held[role];
|
|
216
|
+
roles[role] = {
|
|
217
|
+
port: bound,
|
|
218
|
+
paired: entry.paired,
|
|
219
|
+
renew(code) {
|
|
220
|
+
entry.code = code;
|
|
221
|
+
},
|
|
222
|
+
request(command, answers, onOther) {
|
|
223
|
+
return new Promise((resolve, reject) => {
|
|
224
|
+
const plugin = entry.plugin;
|
|
225
|
+
if (!plugin || plugin.readyState !== plugin.OPEN) return reject(new PluginGone('the plugin is not connected'));
|
|
226
|
+
entry.onGone = (error) => {
|
|
227
|
+
entry.onMessage = entry.onGone = undefined;
|
|
228
|
+
reject(error);
|
|
229
|
+
};
|
|
230
|
+
entry.onMessage = (response) => {
|
|
231
|
+
const mine = response.type === 'ERROR' && response.payload.commandType === command.type;
|
|
232
|
+
if (!mine && !answers.includes(response.type)) return onOther?.(response);
|
|
233
|
+
entry.onMessage = entry.onGone = undefined;
|
|
234
|
+
resolve(response);
|
|
235
|
+
};
|
|
236
|
+
plugin.send(JSON.stringify(command));
|
|
237
|
+
});
|
|
238
|
+
},
|
|
239
|
+
close,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
return roles;
|
|
243
|
+
}
|
package/cli/check.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `toghen-figma check <entry.json>… <file>… [--name <name>] [--overrides <map.json>] [--input <document>]`: the files
|
|
3
|
+
* read against the design system, with no plugin and no Figma file.
|
|
4
|
+
*
|
|
5
|
+
* It is the plugin's own check (`checkComponents`, `main/components/file.ts`), given the resolver built here in Node, from
|
|
6
|
+
* the document the core's `load` returned, as the plugin builds it when a document is loaded. What only Figma holds,
|
|
7
|
+
* whether a variable already exists or is ambiguous between collections, is the dry run's to say.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { Resolver } from '@toghen/lib';
|
|
11
|
+
import { resolve } from 'node:path';
|
|
12
|
+
import { checkComponents } from '../main/components/file.js';
|
|
13
|
+
import { resolveSumModes } from '../main/utils/resolverModes.js';
|
|
14
|
+
import { entryLabel, readInputs, readSource, type Io, type LoadArguments } from './run.js';
|
|
15
|
+
|
|
16
|
+
export interface CheckArguments extends LoadArguments {
|
|
17
|
+
/** A resolver document, one token file, or several in order. */
|
|
18
|
+
entry: string | string[];
|
|
19
|
+
files: string[];
|
|
20
|
+
input?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Prints every problem, one per line, and returns the exit code: 0 when no file has an error, 1 otherwise. */
|
|
24
|
+
export async function check(args: CheckArguments, io: Io): Promise<number> {
|
|
25
|
+
let design: Resolver;
|
|
26
|
+
try {
|
|
27
|
+
design = new Resolver((await readSource(args.entry, io, args)).document);
|
|
28
|
+
// As the plugin does when it loads a document: the sum, which refuses a modifier with no default.
|
|
29
|
+
resolveSumModes(design);
|
|
30
|
+
} catch (error) {
|
|
31
|
+
io.stderr(`${entryLabel(args.entry)}: ${(error as Error).message}\n`);
|
|
32
|
+
return 1;
|
|
33
|
+
}
|
|
34
|
+
let inputs: Record<string, unknown> | undefined;
|
|
35
|
+
if (args.input !== undefined) {
|
|
36
|
+
try {
|
|
37
|
+
inputs = await readInputs(args.input, io);
|
|
38
|
+
} catch (error) {
|
|
39
|
+
io.stderr(`${args.input}: ${(error as Error).message}\n`);
|
|
40
|
+
return 1;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const files: { fileName: string; text: string }[] = [];
|
|
44
|
+
let unreadable = false;
|
|
45
|
+
for (const file of args.files) {
|
|
46
|
+
try {
|
|
47
|
+
files.push({ fileName: file, text: io.readFile(resolve(io.cwd, file)) });
|
|
48
|
+
} catch (error) {
|
|
49
|
+
io.stderr(`${file} cannot be read: ${(error as Error).message}\n`);
|
|
50
|
+
unreadable = true;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (unreadable) return 1;
|
|
54
|
+
|
|
55
|
+
const { errors, warnings } = checkComponents(files, { names: true, ...(inputs ? { inputs } : {}) }, design);
|
|
56
|
+
for (const line of [...errors, ...warnings]) io.stderr(`${line}\n`);
|
|
57
|
+
const noun = (n: number, word: string) => `${n} ${word}${n === 1 ? '' : 's'}`;
|
|
58
|
+
io.stdout(
|
|
59
|
+
`${noun(files.length, 'file')} read: ${errors.length === 0 ? 'no errors' : noun(errors.length, 'error')}${warnings.length > 0 ? `, ${noun(warnings.length, 'warning')}` : ''}\n`,
|
|
60
|
+
);
|
|
61
|
+
return errors.length === 0 ? 0 : 1;
|
|
62
|
+
}
|