@atelier-ui/create-workspace 0.2.41 → 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.
Files changed (39) hide show
  1. package/CHANGELOG.md +33 -4
  2. package/package.json +1 -1
  3. package/src/generators/preset/files/contracts/README.md +29 -0
  4. package/src/generators/preset/files/contracts/button.contract.ts.template +20 -0
  5. package/src/generators/preset/files/contracts/types.ts.template +55 -0
  6. package/src/generators/preset/files/figma/snapshot.json +164 -0
  7. package/src/generators/preset/files/storybook/angular/atl-button.stories.ts.template +38 -0
  8. package/src/generators/preset/files/storybook/angular/main.ts.template +39 -0
  9. package/src/generators/preset/files/storybook/angular/preview.ts.template +30 -0
  10. package/src/generators/preset/files/storybook/angular/tsconfig.json +16 -0
  11. package/src/generators/preset/files/storybook/angular/vitest.config.ts.template +40 -0
  12. package/src/generators/preset/files/storybook/angular/vitest.setup.ts.template +14 -0
  13. package/src/generators/preset/files/storybook/react/atl-button.stories.tsx +31 -0
  14. package/src/generators/preset/files/storybook/react/main.ts.template +37 -0
  15. package/src/generators/preset/files/storybook/react/preview.tsx +29 -0
  16. package/src/generators/preset/files/storybook/react/vitest.config.ts.template +32 -0
  17. package/src/generators/preset/files/storybook/react/vitest.setup.ts.template +8 -0
  18. package/src/generators/preset/files/storybook/vue/atl-button.stories.ts.template +34 -0
  19. package/src/generators/preset/files/storybook/vue/main.ts.template +38 -0
  20. package/src/generators/preset/files/storybook/vue/preview.ts.template +29 -0
  21. package/src/generators/preset/files/storybook/vue/vitest.config.ts.template +32 -0
  22. package/src/generators/preset/files/storybook/vue/vitest.setup.ts.template +9 -0
  23. package/src/generators/preset/files/styles/tokens.css +59 -43
  24. package/src/generators/preset/files/tools/scripts/check-contracts.mjs +1644 -0
  25. package/src/generators/preset/files/tools/scripts/figma-snapshot-contracts.mjs +370 -0
  26. package/src/generators/preset/files/tools/scripts/lib/docgen.mjs +573 -0
  27. package/src/generators/preset/files/tools/scripts/lib/ts-eval.js +126 -0
  28. package/src/generators/preset/files/tools/scripts/preflight.mjs +220 -30
  29. package/src/generators/preset/files/tools/stylelint-rules/index.js +21 -0
  30. package/src/generators/preset/files/tools/stylelint-rules/no-primitive-token.js +362 -0
  31. package/src/generators/preset/files/tools/stylelint-rules/no-raw-color-literal.js +154 -0
  32. package/src/generators/preset/files/tools/stylelint-rules/no-token-bypass.js +497 -0
  33. package/src/generators/preset/files/tools/stylelint-rules/no-undeclared-token.js +122 -0
  34. package/src/generators/preset/files/tools/stylelint-rules/utils.js +71 -0
  35. package/src/generators/preset/preset.d.ts +1 -0
  36. package/src/generators/preset/preset.js +955 -5
  37. package/src/generators/preset/preset.js.map +1 -1
  38. package/src/generators/preset/schema.d.ts +1 -0
  39. 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
+ }