@atelier-ui/create-workspace 0.2.42 → 0.2.43
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/CHANGELOG.md +29 -4
- package/package.json +1 -1
- package/src/generators/preset/files/contracts/README.md +29 -0
- package/src/generators/preset/files/contracts/button.contract.ts.template +20 -0
- package/src/generators/preset/files/contracts/types.ts.template +55 -0
- package/src/generators/preset/files/figma/snapshot.json +164 -0
- package/src/generators/preset/files/storybook/angular/atl-button.stories.ts.template +38 -0
- package/src/generators/preset/files/storybook/angular/main.ts.template +39 -0
- package/src/generators/preset/files/storybook/angular/preview.ts.template +30 -0
- package/src/generators/preset/files/storybook/angular/tsconfig.json +16 -0
- package/src/generators/preset/files/storybook/angular/vitest.config.ts.template +40 -0
- package/src/generators/preset/files/storybook/angular/vitest.setup.ts.template +14 -0
- package/src/generators/preset/files/storybook/react/atl-button.stories.tsx +31 -0
- package/src/generators/preset/files/storybook/react/main.ts.template +37 -0
- package/src/generators/preset/files/storybook/react/preview.tsx +29 -0
- package/src/generators/preset/files/storybook/react/vitest.config.ts.template +32 -0
- package/src/generators/preset/files/storybook/react/vitest.setup.ts.template +8 -0
- package/src/generators/preset/files/storybook/vue/atl-button.stories.ts.template +34 -0
- package/src/generators/preset/files/storybook/vue/main.ts.template +38 -0
- package/src/generators/preset/files/storybook/vue/preview.ts.template +29 -0
- package/src/generators/preset/files/storybook/vue/vitest.config.ts.template +32 -0
- package/src/generators/preset/files/storybook/vue/vitest.setup.ts.template +9 -0
- package/src/generators/preset/files/styles/tokens.css +59 -43
- package/src/generators/preset/files/tools/scripts/check-contracts.mjs +1644 -0
- package/src/generators/preset/files/tools/scripts/figma-snapshot-contracts.mjs +370 -0
- package/src/generators/preset/files/tools/scripts/lib/docgen.mjs +573 -0
- package/src/generators/preset/files/tools/scripts/lib/ts-eval.js +126 -0
- package/src/generators/preset/files/tools/scripts/preflight.mjs +163 -31
- package/src/generators/preset/files/tools/stylelint-rules/index.js +21 -0
- package/src/generators/preset/files/tools/stylelint-rules/no-primitive-token.js +362 -0
- package/src/generators/preset/files/tools/stylelint-rules/no-raw-color-literal.js +154 -0
- package/src/generators/preset/files/tools/stylelint-rules/no-token-bypass.js +497 -0
- package/src/generators/preset/files/tools/stylelint-rules/no-undeclared-token.js +122 -0
- package/src/generators/preset/files/tools/stylelint-rules/utils.js +71 -0
- package/src/generators/preset/preset.d.ts +1 -0
- package/src/generators/preset/preset.js +955 -5
- package/src/generators/preset/preset.js.map +1 -1
- package/src/generators/preset/schema.d.ts +1 -0
- package/src/generators/preset/schema.json +5 -0
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* figma-snapshot-contracts.mjs
|
|
4
|
+
*
|
|
5
|
+
* A snapshot generator whose roster is the contracts, not a hand-maintained
|
|
6
|
+
* master list. `tools/scripts/figma-snapshot.mjs` (the monorepo's own refresh
|
|
7
|
+
* step, ADR-0019) hardcodes all 43 Atelier master node ids; that list has no
|
|
8
|
+
* meaning outside this repo. This script instead reads whichever
|
|
9
|
+
* `<name>.contract.ts` files exist under the contracts directory (same
|
|
10
|
+
* resolution as `check-contracts.mjs`, Step 1 of ADR-0121 S4) and, for each
|
|
11
|
+
* one's `figmaNodeId`, writes exactly the master-entry fields `check-contracts.mjs`
|
|
12
|
+
* consumes: `selector`, `nodeId`, `name`, `description`, `variantAxes`,
|
|
13
|
+
* `properties`, `variants`.
|
|
14
|
+
*
|
|
15
|
+
* Deliberately does NOT import tools/scripts/figma-snapshot.mjs — the MCP
|
|
16
|
+
* client boilerplate and the `.mcp.json`-version-resolver below are copied
|
|
17
|
+
* from it, not shared, so this file stands alone when copied into a
|
|
18
|
+
* scaffolded workspace (ADR-0123's "the scaffold ships the check" — this is
|
|
19
|
+
* its Figma-refresh half).
|
|
20
|
+
*
|
|
21
|
+
* node tools/scripts/figma-snapshot-contracts.mjs --file <figmaFileKey> [--out <path>]
|
|
22
|
+
* node tools/scripts/figma-snapshot-contracts.mjs --file <figmaFileKey> --dry-run
|
|
23
|
+
*
|
|
24
|
+
* Requirements (same as figma-snapshot.mjs): Figma Desktop running with the
|
|
25
|
+
* file open and the figma-console Desktop Bridge plugin connected. Fails
|
|
26
|
+
* loud (exit 2) if the bridge is not connected — never a silent/empty write.
|
|
27
|
+
*
|
|
28
|
+
* --dry-run prints the roster (selector -> nodeId) and the figma_execute
|
|
29
|
+
* plugin code that would run for the first master, then exits 0 WITHOUT
|
|
30
|
+
* connecting to Figma at all — the only mode this script's own test suite
|
|
31
|
+
* can exercise in an environment with no Figma Desktop Bridge.
|
|
32
|
+
*/
|
|
33
|
+
import { writeFileSync, readFileSync, existsSync } from 'node:fs';
|
|
34
|
+
import { fileURLToPath } from 'node:url';
|
|
35
|
+
import { dirname, resolve, join } from 'node:path';
|
|
36
|
+
import { createRequire } from 'node:module';
|
|
37
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
38
|
+
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
39
|
+
|
|
40
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
41
|
+
// Portable the same way check-contracts.mjs is (ADR-0121 S4 Step 1): derived
|
|
42
|
+
// from THIS script's own location, so a byte-identical copy dropped into
|
|
43
|
+
// <scaffold>/tools/scripts/ defaults to the scaffold's own tree.
|
|
44
|
+
const ROOT = resolve(__dirname, '../..');
|
|
45
|
+
const CWD = process.cwd();
|
|
46
|
+
const MCP_CONFIG_PATH = resolve(ROOT, '.mcp.json');
|
|
47
|
+
|
|
48
|
+
// ts-eval.js is required relative to THIS script's own directory (like
|
|
49
|
+
// check-contracts.mjs does) so the canonical script + lib/ts-eval.js pair
|
|
50
|
+
// works unmodified wherever it is copied.
|
|
51
|
+
const require = createRequire(import.meta.url);
|
|
52
|
+
const { parseExportedVars } = require('./lib/ts-eval.js');
|
|
53
|
+
|
|
54
|
+
// ─── CLI args ───────────────────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
function parseArgs(argv) {
|
|
57
|
+
const out = { file: null, out: null, contracts: null, dryRun: false };
|
|
58
|
+
for (let i = 0; i < argv.length; i++) {
|
|
59
|
+
const a = argv[i];
|
|
60
|
+
if (a === '--file') out.file = argv[++i];
|
|
61
|
+
else if (a === '--out') out.out = argv[++i];
|
|
62
|
+
else if (a === '--contracts') out.contracts = argv[++i];
|
|
63
|
+
else if (a === '--dry-run') out.dryRun = true;
|
|
64
|
+
}
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const args = parseArgs(process.argv.slice(2));
|
|
69
|
+
|
|
70
|
+
if (!args.file) {
|
|
71
|
+
console.error(
|
|
72
|
+
'Usage: node tools/scripts/figma-snapshot-contracts.mjs --file <figmaFileKey> [--out <path>] [--contracts <dir>] [--dry-run]',
|
|
73
|
+
);
|
|
74
|
+
process.exit(2);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ─── Contracts-dir resolution (same precedence as check-contracts.mjs's
|
|
78
|
+
// Step 1: CLI flag > contracts.config.json at the cwd root > monorepo
|
|
79
|
+
// default) ──────────────────────────────────────────────────────────────
|
|
80
|
+
const CONFIG_PATH = join(CWD, 'contracts.config.json');
|
|
81
|
+
let fileConfig = null;
|
|
82
|
+
if (existsSync(CONFIG_PATH)) {
|
|
83
|
+
try {
|
|
84
|
+
fileConfig = JSON.parse(readFileSync(CONFIG_PATH, 'utf-8'));
|
|
85
|
+
} catch (e) {
|
|
86
|
+
console.error(`${CONFIG_PATH}: invalid JSON (${e.message})`);
|
|
87
|
+
process.exit(2);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const CONTRACTS_DIR = args.contracts
|
|
92
|
+
? resolve(CWD, args.contracts)
|
|
93
|
+
: fileConfig?.contracts
|
|
94
|
+
? resolve(CWD, fileConfig.contracts)
|
|
95
|
+
: join(ROOT, 'libs/spec/src/contracts');
|
|
96
|
+
|
|
97
|
+
const OUT_PATH = args.out
|
|
98
|
+
? resolve(CWD, args.out)
|
|
99
|
+
: join(ROOT, 'tools/figma/snapshot.json');
|
|
100
|
+
|
|
101
|
+
// ─── The roster: every contract's { component, figmaNodeId } ──────────────
|
|
102
|
+
|
|
103
|
+
function loadRoster(contractsDir) {
|
|
104
|
+
const fs = require('node:fs');
|
|
105
|
+
const path = require('node:path');
|
|
106
|
+
const files = fs
|
|
107
|
+
.readdirSync(contractsDir)
|
|
108
|
+
.filter((f) => f.endsWith('.contract.ts'))
|
|
109
|
+
.sort();
|
|
110
|
+
const roster = [];
|
|
111
|
+
for (const file of files) {
|
|
112
|
+
const full = path.join(contractsDir, file);
|
|
113
|
+
const vars = parseExportedVars(full);
|
|
114
|
+
const contract = vars.contract;
|
|
115
|
+
if (
|
|
116
|
+
!contract ||
|
|
117
|
+
typeof contract !== 'object' ||
|
|
118
|
+
!contract.component ||
|
|
119
|
+
!contract.figmaNodeId
|
|
120
|
+
) {
|
|
121
|
+
console.error(
|
|
122
|
+
`${file}: 'contract' did not evaluate to a static { component, figmaNodeId, ... } object — skipped.`,
|
|
123
|
+
);
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
roster.push({
|
|
127
|
+
selector: contract.component,
|
|
128
|
+
nodeId: contract.figmaNodeId,
|
|
129
|
+
file,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
return roster;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const roster = loadRoster(CONTRACTS_DIR);
|
|
136
|
+
if (roster.length === 0) {
|
|
137
|
+
console.error(
|
|
138
|
+
`No contracts found under ${CONTRACTS_DIR} — nothing to snapshot.`,
|
|
139
|
+
);
|
|
140
|
+
process.exit(2);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ─── The figma_execute plugin code, one master per call ────────────────────
|
|
144
|
+
// Modelled on figma-snapshot.mjs's own probe: componentPropertyDefinitions
|
|
145
|
+
// for `properties`/`variantAxes`, and each variant child's `name` (the
|
|
146
|
+
// "axis=value, axis=value" convention) for `variants`. A single node lookup
|
|
147
|
+
// per master rather than the monorepo probe's whole-file walk — this script
|
|
148
|
+
// has a roster of one to a handful of contracts, not 43 masters plus every
|
|
149
|
+
// child part.
|
|
150
|
+
function pluginCodeFor(nodeId) {
|
|
151
|
+
return `
|
|
152
|
+
await figma.loadAllPagesAsync();
|
|
153
|
+
const node = await figma.getNodeByIdAsync('${nodeId}');
|
|
154
|
+
if (!node) return null;
|
|
155
|
+
const isSet = node.type === 'COMPONENT_SET';
|
|
156
|
+
const properties = {};
|
|
157
|
+
const variantAxes = {};
|
|
158
|
+
for (const [key, def] of Object.entries(node.componentPropertyDefinitions || {})) {
|
|
159
|
+
properties[key] = def.type;
|
|
160
|
+
if (def.type === 'VARIANT' && Array.isArray(def.variantOptions)) {
|
|
161
|
+
variantAxes[key] = def.variantOptions;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
function parseVariantName(name) {
|
|
165
|
+
if (!name || name.indexOf('=') === -1) return null;
|
|
166
|
+
const out = {};
|
|
167
|
+
for (const part of name.split(',')) {
|
|
168
|
+
const kv = part.split('=');
|
|
169
|
+
const k = (kv[0] || '').trim();
|
|
170
|
+
const v = kv[1] === undefined ? undefined : kv[1].trim();
|
|
171
|
+
if (k && v !== undefined) out[k] = v;
|
|
172
|
+
}
|
|
173
|
+
return Object.keys(out).length ? out : null;
|
|
174
|
+
}
|
|
175
|
+
const variants = isSet
|
|
176
|
+
? (node.children || []).map((c) => parseVariantName(c.name)).filter(Boolean)
|
|
177
|
+
: [];
|
|
178
|
+
return {
|
|
179
|
+
name: node.name,
|
|
180
|
+
description: node.description || '',
|
|
181
|
+
properties,
|
|
182
|
+
variantAxes,
|
|
183
|
+
variants,
|
|
184
|
+
};
|
|
185
|
+
`;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ─── --dry-run: print the roster and the plugin code, never connect ───────
|
|
189
|
+
|
|
190
|
+
if (args.dryRun) {
|
|
191
|
+
console.log(`Contracts dir: ${CONTRACTS_DIR}`);
|
|
192
|
+
console.log(`Would write: ${OUT_PATH}`);
|
|
193
|
+
console.log(`\nRoster (selector -> nodeId):`);
|
|
194
|
+
for (const { selector, nodeId, file } of roster) {
|
|
195
|
+
console.log(` ${selector.padEnd(24)} -> ${nodeId} (${file})`);
|
|
196
|
+
}
|
|
197
|
+
console.log(
|
|
198
|
+
`\nfigma_execute plugin code (shown for the first master, ${roster[0].selector} / ${roster[0].nodeId} — the same shape runs once per master, with 'nodeId' substituted):`,
|
|
199
|
+
);
|
|
200
|
+
console.log(pluginCodeFor(roster[0].nodeId));
|
|
201
|
+
console.log(`\n(dry run — no connection made, nothing written)`);
|
|
202
|
+
process.exit(0);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ─── Resolve the pinned figma-console-mcp package spec from .mcp.json ─────
|
|
206
|
+
// Copied from figma-snapshot.mjs (ADR-0110: pin the server the skills
|
|
207
|
+
// hardcode) — deliberately not imported, so this file stands alone.
|
|
208
|
+
|
|
209
|
+
function resolveFigmaConsolePackageSpec() {
|
|
210
|
+
let config;
|
|
211
|
+
try {
|
|
212
|
+
config = JSON.parse(readFileSync(MCP_CONFIG_PATH, 'utf8'));
|
|
213
|
+
} catch (err) {
|
|
214
|
+
throw new Error(
|
|
215
|
+
`could not read/parse ${MCP_CONFIG_PATH}: ${err?.message ?? err}`,
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
const serverArgs = config?.mcpServers?.['figma-console']?.args;
|
|
219
|
+
const spec = Array.isArray(serverArgs)
|
|
220
|
+
? serverArgs.find((a) => /^figma-console-mcp@/.test(a))
|
|
221
|
+
: undefined;
|
|
222
|
+
if (!spec) {
|
|
223
|
+
throw new Error(
|
|
224
|
+
`${MCP_CONFIG_PATH} has no mcpServers['figma-console'].args entry matching ` +
|
|
225
|
+
`/^figma-console-mcp@/ — refusing to fall back to @latest (ADR-0110 pins this server).`,
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
return spec;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function versionFromPackageSpec(spec) {
|
|
232
|
+
const idx = String(spec).lastIndexOf('@');
|
|
233
|
+
return idx > 0 ? String(spec).slice(idx + 1) : null;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function isConnected(status) {
|
|
237
|
+
return Boolean(
|
|
238
|
+
status?.connected ||
|
|
239
|
+
status?.plugin?.connected ||
|
|
240
|
+
status?.details?.plugin?.connected ||
|
|
241
|
+
status?.probeResult?.success ||
|
|
242
|
+
status?.setup?.probeResult?.success ||
|
|
243
|
+
status?.setup?.valid ||
|
|
244
|
+
status?.transport?.websocket?.available,
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async function call(client, name, toolArgs) {
|
|
249
|
+
const res = await client.callTool({ name, arguments: toolArgs });
|
|
250
|
+
const text = res?.content?.find((c) => c.type === 'text')?.text ?? '';
|
|
251
|
+
try {
|
|
252
|
+
return JSON.parse(text);
|
|
253
|
+
} catch {
|
|
254
|
+
throw new Error(`${name} returned non-JSON output`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
main().catch((err) => {
|
|
259
|
+
console.error(`✗ figma:snapshot-contracts failed: ${err?.message ?? err}`);
|
|
260
|
+
process.exit(2);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
async function main() {
|
|
264
|
+
const client = new Client(
|
|
265
|
+
{ name: 'atelier-figma-snapshot-contracts', version: '1.0.0' },
|
|
266
|
+
{ capabilities: {} },
|
|
267
|
+
);
|
|
268
|
+
const figmaConsolePackageSpec = resolveFigmaConsolePackageSpec();
|
|
269
|
+
const transport = new StdioClientTransport({
|
|
270
|
+
command: 'npx',
|
|
271
|
+
args: ['-y', figmaConsolePackageSpec],
|
|
272
|
+
env: { ...process.env },
|
|
273
|
+
});
|
|
274
|
+
await client.connect(transport);
|
|
275
|
+
const reportedServerVersion = client.getServerVersion?.();
|
|
276
|
+
|
|
277
|
+
try {
|
|
278
|
+
// Probe the bridge — fail loud if the plugin is not connected. Same
|
|
279
|
+
// retry shape as figma-snapshot.mjs: the plugin attaches ~0.5s after a
|
|
280
|
+
// freshly-spawned server starts.
|
|
281
|
+
let status = null;
|
|
282
|
+
for (let attempt = 0; attempt < 10; attempt++) {
|
|
283
|
+
status = await call(client, 'figma_get_status', { probe: true });
|
|
284
|
+
if (isConnected(status)) break;
|
|
285
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
286
|
+
}
|
|
287
|
+
if (!isConnected(status)) {
|
|
288
|
+
console.error(
|
|
289
|
+
'✗ Figma Desktop Bridge not connected. Open Figma Desktop with the file and the\n' +
|
|
290
|
+
' figma-console Desktop Bridge plugin running, and ensure no other MCP client holds\n' +
|
|
291
|
+
' the bridge, then re-run this script.',
|
|
292
|
+
);
|
|
293
|
+
process.exit(2);
|
|
294
|
+
}
|
|
295
|
+
const declaredServerVersion = versionFromPackageSpec(
|
|
296
|
+
figmaConsolePackageSpec,
|
|
297
|
+
);
|
|
298
|
+
const serverVersion =
|
|
299
|
+
reportedServerVersion?.version ??
|
|
300
|
+
status?.serverVersion ??
|
|
301
|
+
status?.details?.serverVersion ??
|
|
302
|
+
(declaredServerVersion
|
|
303
|
+
? `${declaredServerVersion} (declared in .mcp.json; server did not report)`
|
|
304
|
+
: null);
|
|
305
|
+
|
|
306
|
+
// ─── Verify --file actually names the open file ────────────────────────
|
|
307
|
+
// The Bridge reads whichever file is open in Figma Desktop, regardless of
|
|
308
|
+
// what --file claims — figma-snapshot.mjs never had to check this because it
|
|
309
|
+
// always targets the hardcoded Atelier FILE_KEY, but this script's whole
|
|
310
|
+
// point is a participant's own duplicate, so the open file cannot be
|
|
311
|
+
// assumed to match. figma.fileKey is the ground truth reported from inside
|
|
312
|
+
// the plugin sandbox itself.
|
|
313
|
+
const openFile = (
|
|
314
|
+
await call(client, 'figma_execute', {
|
|
315
|
+
code: `
|
|
316
|
+
await figma.loadAllPagesAsync();
|
|
317
|
+
return { fileKey: figma.fileKey ?? null, fileName: figma.root.name };
|
|
318
|
+
`,
|
|
319
|
+
timeout: 10000,
|
|
320
|
+
})
|
|
321
|
+
)?.result;
|
|
322
|
+
if (openFile?.fileKey && openFile.fileKey !== args.file) {
|
|
323
|
+
console.error(
|
|
324
|
+
`✗ --file ${args.file} does not match the file open in Figma Desktop ` +
|
|
325
|
+
`(${openFile.fileKey}, "${openFile.fileName}"). Open the file you meant to ` +
|
|
326
|
+
`snapshot, or pass --file ${openFile.fileKey}, and re-run.`,
|
|
327
|
+
);
|
|
328
|
+
process.exit(2);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const components = [];
|
|
332
|
+
for (const { selector, nodeId } of roster) {
|
|
333
|
+
const result = (
|
|
334
|
+
await call(client, 'figma_execute', {
|
|
335
|
+
code: pluginCodeFor(nodeId),
|
|
336
|
+
timeout: 15000,
|
|
337
|
+
})
|
|
338
|
+
)?.result;
|
|
339
|
+
if (!result) {
|
|
340
|
+
console.warn(`⚠ skipped ${selector} (${nodeId}): node not found`);
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
components.push({
|
|
344
|
+
selector,
|
|
345
|
+
nodeId,
|
|
346
|
+
name: result.name,
|
|
347
|
+
description: result.description ?? '',
|
|
348
|
+
variantAxes: result.variantAxes ?? {},
|
|
349
|
+
properties: result.properties ?? {},
|
|
350
|
+
variants: result.variants ?? [],
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (components.length === 0) throw new Error('no components captured');
|
|
355
|
+
|
|
356
|
+
const snapshot = {
|
|
357
|
+
meta: {
|
|
358
|
+
fileKey: args.file,
|
|
359
|
+
generatedAt: new Date().toISOString(),
|
|
360
|
+
serverVersion,
|
|
361
|
+
generator: 'figma-snapshot-contracts',
|
|
362
|
+
},
|
|
363
|
+
components,
|
|
364
|
+
};
|
|
365
|
+
writeFileSync(OUT_PATH, JSON.stringify(snapshot, null, 2) + '\n');
|
|
366
|
+
console.log(`✓ wrote ${components.length} master(s) to ${OUT_PATH}`);
|
|
367
|
+
} finally {
|
|
368
|
+
await client.close();
|
|
369
|
+
}
|
|
370
|
+
}
|