@figma/code-connect 1.4.9 → 1.5.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/README.md +3 -0
- package/dist/commands/connect.d.ts +5 -0
- package/dist/commands/connect.d.ts.map +1 -1
- package/dist/commands/connect.js +55 -10
- package/dist/commands/connect.js.map +1 -1
- package/dist/commands/connect_template.d.ts.map +1 -1
- package/dist/commands/connect_template.js +2 -2
- package/dist/commands/connect_template.js.map +1 -1
- package/dist/commands/preview_utils.d.ts +103 -5
- package/dist/commands/preview_utils.d.ts.map +1 -1
- package/dist/commands/preview_utils.js +453 -76
- package/dist/commands/preview_utils.js.map +1 -1
- package/dist/commands/property_list_table.d.ts +11 -0
- package/dist/commands/property_list_table.d.ts.map +1 -0
- package/dist/commands/property_list_table.js +67 -0
- package/dist/commands/property_list_table.js.map +1 -0
- package/dist/common/updates.d.ts +4 -1
- package/dist/common/updates.d.ts.map +1 -1
- package/dist/common/updates.js +26 -5
- package/dist/common/updates.js.map +1 -1
- package/dist/connect/api.d.ts.map +1 -1
- package/dist/connect/api.js.map +1 -1
- package/dist/connect/batch_templates.d.ts +1 -1
- package/dist/connect/batch_templates.d.ts.map +1 -1
- package/dist/connect/batch_templates.js +2 -2
- package/dist/connect/batch_templates.js.map +1 -1
- package/dist/connect/migration_batch_helpers.d.ts.map +1 -1
- package/dist/connect/migration_batch_helpers.js +1 -15
- package/dist/connect/migration_batch_helpers.js.map +1 -1
- package/dist/connect/migration_helpers.d.ts +6 -0
- package/dist/connect/migration_helpers.d.ts.map +1 -1
- package/dist/connect/migration_helpers.js +66 -0
- package/dist/connect/migration_helpers.js.map +1 -1
- package/dist/connect/property_combinations.d.ts +69 -0
- package/dist/connect/property_combinations.d.ts.map +1 -0
- package/dist/connect/property_combinations.js +172 -0
- package/dist/connect/property_combinations.js.map +1 -0
- package/dist/connect/raw_template_bundler.d.ts +13 -0
- package/dist/connect/raw_template_bundler.d.ts.map +1 -0
- package/dist/connect/raw_template_bundler.js +178 -0
- package/dist/connect/raw_template_bundler.js.map +1 -0
- package/dist/connect/raw_templates.d.ts +1 -1
- package/dist/connect/raw_templates.d.ts.map +1 -1
- package/dist/connect/raw_templates.js +85 -24
- package/dist/connect/raw_templates.js.map +1 -1
- package/dist/react/parser.d.ts.map +1 -1
- package/dist/react/parser.js.map +1 -1
- package/figma-types-no-require.d.ts +8 -6
- package/figma-types.d.ts +8 -6
- package/package.json +2 -2
|
@@ -37,14 +37,21 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
37
37
|
};
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
39
|
exports.collectNodesToPreview = collectNodesToPreview;
|
|
40
|
+
exports.fetchComponentPropertyDefinitions = fetchComponentPropertyDefinitions;
|
|
41
|
+
exports.parsePropsArg = parsePropsArg;
|
|
40
42
|
exports.filterTemplatesForNodes = filterTemplatesForNodes;
|
|
41
43
|
exports.getPrettierParserMap = getPrettierParserMap;
|
|
42
44
|
exports.isPrettierParseable = isPrettierParseable;
|
|
43
45
|
exports.formatSnippet = formatSnippet;
|
|
44
46
|
exports.displayResults = displayResults;
|
|
47
|
+
exports.parseMaxCombinationsArg = parseMaxCombinationsArg;
|
|
48
|
+
exports.chunkNodeIdsForPreview = chunkNodeIdsForPreview;
|
|
49
|
+
exports.parseMissingProperty = parseMissingProperty;
|
|
50
|
+
exports.chunkOutcomeToResults = chunkOutcomeToResults;
|
|
45
51
|
exports.handlePreview = handlePreview;
|
|
46
52
|
const fs_1 = __importDefault(require("fs"));
|
|
47
53
|
const path_1 = __importDefault(require("path"));
|
|
54
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
48
55
|
const prettier = __importStar(require("prettier"));
|
|
49
56
|
const connect_1 = require("./connect");
|
|
50
57
|
const logging_1 = require("../common/logging");
|
|
@@ -52,6 +59,8 @@ const validation_1 = require("../connect/validation");
|
|
|
52
59
|
const project_1 = require("../connect/project");
|
|
53
60
|
const figma_rest_api_1 = require("../connect/figma_rest_api");
|
|
54
61
|
const fetch_1 = require("../common/fetch");
|
|
62
|
+
const property_list_table_1 = require("./property_list_table");
|
|
63
|
+
const property_combinations_1 = require("../connect/property_combinations");
|
|
55
64
|
/**
|
|
56
65
|
* Collect nodes to preview from file arguments.
|
|
57
66
|
* Matches by exact path first, then by basename.
|
|
@@ -131,6 +140,85 @@ async function collectNodesToPreview(files, allCodeConnectObjects, dir, cmd) {
|
|
|
131
140
|
}
|
|
132
141
|
return nodesToPreview;
|
|
133
142
|
}
|
|
143
|
+
/**
|
|
144
|
+
* Fetch one node's property definitions from the REST `files/:key/nodes` endpoint.
|
|
145
|
+
*/
|
|
146
|
+
async function fetchNodeSchema(baseApiUrl, fileKey, nodeId, accessToken) {
|
|
147
|
+
try {
|
|
148
|
+
const nodesUrl = `${baseApiUrl}/files/${fileKey}/nodes?ids=${nodeId}`;
|
|
149
|
+
const resp = await fetch_1.request.get(nodesUrl, { headers: (0, figma_rest_api_1.getHeaders)(accessToken) });
|
|
150
|
+
if (resp.response.status !== 200) {
|
|
151
|
+
logging_1.logger.debug(`files/nodes returned ${resp.response.status} for ${nodeId}`);
|
|
152
|
+
return { status: 'error' };
|
|
153
|
+
}
|
|
154
|
+
const node = resp.data.nodes?.[nodeId];
|
|
155
|
+
const componentSetId = node?.components?.[nodeId]?.componentSetId || undefined;
|
|
156
|
+
const defs = node?.document?.componentPropertyDefinitions;
|
|
157
|
+
if (defs && Object.keys(defs).length > 0)
|
|
158
|
+
return { status: 'ok', defs, componentSetId };
|
|
159
|
+
logging_1.logger.debug(`files/nodes returned no componentPropertyDefinitions for ${nodeId}`);
|
|
160
|
+
return { status: 'empty', componentSetId };
|
|
161
|
+
}
|
|
162
|
+
catch (err) {
|
|
163
|
+
const detail = (0, fetch_1.isFetchError)(err) ? `status ${err.response?.status}` : String(err);
|
|
164
|
+
logging_1.logger.debug(`files/nodes fetch failed for ${nodeId} (${detail})`);
|
|
165
|
+
return { status: 'error' };
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Fetch a component's property definitions from the REST `files/:key/nodes` endpoint.
|
|
170
|
+
*/
|
|
171
|
+
async function fetchComponentPropertyDefinitions(baseApiUrl, fileKey, nodeId, accessToken) {
|
|
172
|
+
const node = await fetchNodeSchema(baseApiUrl, fileKey, nodeId, accessToken);
|
|
173
|
+
if (node.status === 'error')
|
|
174
|
+
return { status: 'error' };
|
|
175
|
+
// Variant child → resolve to the parent COMPONENT_SET (whose schema carries the
|
|
176
|
+
// variant axes). Fall back to the variant's own defs if the set can't be fetched.
|
|
177
|
+
if (node.componentSetId) {
|
|
178
|
+
const set = await fetchNodeSchema(baseApiUrl, fileKey, node.componentSetId, accessToken);
|
|
179
|
+
if (set.status === 'ok')
|
|
180
|
+
return { status: 'ok', defs: set.defs };
|
|
181
|
+
logging_1.logger.debug(`Component set ${node.componentSetId} for variant ${nodeId} had no usable schema ` +
|
|
182
|
+
`(${set.status}); using the variant's own definitions`);
|
|
183
|
+
}
|
|
184
|
+
return node.status === 'ok' ? { status: 'ok', defs: node.defs } : { status: 'empty' };
|
|
185
|
+
}
|
|
186
|
+
/** Case-insensitive lookup of a `ComponentPropertyType` from a `--props` prefix token. */
|
|
187
|
+
function parsePropertyTypePrefix(token) {
|
|
188
|
+
const upper = token.trim().toUpperCase();
|
|
189
|
+
return Object.values(figma_rest_api_1.FigmaRestApi.ComponentPropertyType).includes(upper)
|
|
190
|
+
? upper
|
|
191
|
+
: undefined;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Parse a single `--props` argument into a name/value pair, with an optional
|
|
195
|
+
* `TYPE:` prefix to disambiguate properties that share a name but differ in
|
|
196
|
+
* type (e.g. a BOOLEAN and a TEXT property both named `textMsg`).
|
|
197
|
+
* Example: --props Variant=Primary "Has Icon=true" "BOOLEAN:textMsg=false"
|
|
198
|
+
*
|
|
199
|
+
* The prefix is only recognized when the token before the first `:` matches a
|
|
200
|
+
* known property type (case-insensitive); otherwise the whole name-part is kept
|
|
201
|
+
* as the property name, so real names containing `:` are preserved.
|
|
202
|
+
*/
|
|
203
|
+
function parsePropsArg(props) {
|
|
204
|
+
return props
|
|
205
|
+
.flatMap((arg) => arg.split(','))
|
|
206
|
+
.map((pair) => pair.trim())
|
|
207
|
+
.filter((pair) => pair.length > 0)
|
|
208
|
+
.map((pair) => {
|
|
209
|
+
const eq = pair.indexOf('=');
|
|
210
|
+
const namePart = (eq === -1 ? pair : pair.slice(0, eq)).trim();
|
|
211
|
+
const value = eq === -1 ? '' : pair.slice(eq + 1).trim();
|
|
212
|
+
const colon = namePart.indexOf(':');
|
|
213
|
+
if (colon !== -1) {
|
|
214
|
+
const type = parsePropertyTypePrefix(namePart.slice(0, colon));
|
|
215
|
+
if (type) {
|
|
216
|
+
return { name: namePart.slice(colon + 1).trim(), value, type };
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return { name: namePart, value };
|
|
220
|
+
});
|
|
221
|
+
}
|
|
134
222
|
/**
|
|
135
223
|
* Filter templates to only those matching the requested node IDs.
|
|
136
224
|
*/
|
|
@@ -222,20 +310,30 @@ async function formatSnippet(snippet, language) {
|
|
|
222
310
|
/**
|
|
223
311
|
* Display results with terminal colors
|
|
224
312
|
*/
|
|
313
|
+
const purple = chalk_1.default.ansi256(93);
|
|
314
|
+
const red = chalk_1.default.ansi256(196);
|
|
315
|
+
const gray = chalk_1.default.ansi256(243);
|
|
316
|
+
const yellow = chalk_1.default.ansi256(179);
|
|
225
317
|
function displayResults(results) {
|
|
226
318
|
console.log('');
|
|
227
319
|
const successCount = results.filter((r) => r.success).length;
|
|
228
320
|
const errorCount = results.filter((r) => !r.success).length;
|
|
229
|
-
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
321
|
+
// A muted note when nested children rendered as placeholders (not an error).
|
|
322
|
+
const printUnresolved = (result) => {
|
|
323
|
+
if (!result.unresolvedInstances?.length)
|
|
324
|
+
return;
|
|
325
|
+
const names = result.unresolvedInstances.map((u) => u.name || u.guid).join(', ');
|
|
326
|
+
console.log(` ${yellow(`⚠ ${result.unresolvedInstances.length} nested instance(s) not Code Connected: ${names}`)}`);
|
|
327
|
+
};
|
|
234
328
|
for (const result of results) {
|
|
329
|
+
// When expanding property combinations, label the header with the specific combination.
|
|
330
|
+
const combinationSuffix = result.propertyCombinationLabel
|
|
331
|
+
? ` ${gray(`— ${result.propertyCombinationLabel}`)}`
|
|
332
|
+
: '';
|
|
235
333
|
if (result.success && result.snippet) {
|
|
236
|
-
const componentInfo = result.component ? ` ${gray
|
|
237
|
-
console.log(`${purple}
|
|
238
|
-
console.log(` ${gray
|
|
334
|
+
const componentInfo = result.component ? ` ${gray(`→ ${result.component}`)}` : '';
|
|
335
|
+
console.log(`${purple('●')} ${chalk_1.default.bold(result.filePath)}${componentInfo}${combinationSuffix}`);
|
|
336
|
+
console.log(` ${gray(result.url)}`);
|
|
239
337
|
console.log('');
|
|
240
338
|
const indentedSnippet = result.snippet
|
|
241
339
|
.trim()
|
|
@@ -243,17 +341,30 @@ function displayResults(results) {
|
|
|
243
341
|
.map((line) => ' ' + line)
|
|
244
342
|
.join('\n');
|
|
245
343
|
console.log(indentedSnippet);
|
|
344
|
+
printUnresolved(result);
|
|
246
345
|
console.log('');
|
|
247
346
|
}
|
|
248
347
|
else {
|
|
249
|
-
const componentInfo = result.component ? ` ${gray
|
|
250
|
-
console.log(`${red}
|
|
251
|
-
console.log(` ${gray
|
|
252
|
-
console.log(` ${red
|
|
348
|
+
const componentInfo = result.component ? ` ${gray(`(${result.component})`)}` : '';
|
|
349
|
+
console.log(`${red('✕')} ${chalk_1.default.bold(result.filePath)}${componentInfo}${combinationSuffix}`);
|
|
350
|
+
console.log(` ${gray(result.url)}`);
|
|
351
|
+
console.log(` ${red('Error:')} ${result.error}`);
|
|
352
|
+
// One-line repair hint: the offending property and the real vocabulary.
|
|
353
|
+
// (Transport errors carry no vocabulary — nothing to hint.)
|
|
354
|
+
const available = result.errorDetails?.availableProperties?.map((p) => p.name).join(', ');
|
|
355
|
+
const missingProperty = result.errorDetails?.missingProperty;
|
|
356
|
+
if (missingProperty && available) {
|
|
357
|
+
console.log(` ${gray(`property "${missingProperty}" not found — available: ${available}`)}`);
|
|
358
|
+
}
|
|
359
|
+
else if (available) {
|
|
360
|
+
console.log(` ${gray(`available properties: ${available}`)}`);
|
|
361
|
+
}
|
|
362
|
+
printUnresolved(result);
|
|
253
363
|
console.log('');
|
|
254
364
|
}
|
|
255
365
|
}
|
|
256
|
-
|
|
366
|
+
const failed = errorCount > 0 ? red(`${errorCount} failed`) : gray(`${errorCount} failed`);
|
|
367
|
+
console.log(`${chalk_1.default.bold('Summary:')} ${purple(`${successCount} succeeded`)}, ${failed}`);
|
|
257
368
|
}
|
|
258
369
|
// The server caps inbound request bodies at 5MB. The figmaDocs payload
|
|
259
370
|
// (one Code Connect template per requested component) dominates the body size,
|
|
@@ -265,6 +376,22 @@ const PREVIEW_CHUNK_SIZE = 50;
|
|
|
265
376
|
// overwhelms upstream token validation and causes spurious 403 "Invalid
|
|
266
377
|
// token" responses for the chunks that queue up too long.
|
|
267
378
|
const PREVIEW_MAX_CONCURRENCY = 5;
|
|
379
|
+
// Mirrors MAX_PROPERTY_COMBINATIONS_PER_REQUEST in pixie/code_connect_preview_handler.ts.
|
|
380
|
+
// `--all` caps enumeration here and warns rather than sending a request that fails wholesale.
|
|
381
|
+
const MAX_PROPERTY_COMBINATIONS = 500;
|
|
382
|
+
function parseMaxCombinationsArg(value) {
|
|
383
|
+
if (value === undefined) {
|
|
384
|
+
return MAX_PROPERTY_COMBINATIONS;
|
|
385
|
+
}
|
|
386
|
+
const parsed = Number(value);
|
|
387
|
+
if (!Number.isInteger(parsed) || parsed < 1) {
|
|
388
|
+
(0, logging_1.exitWithError)('--max-combinations must be a positive integer');
|
|
389
|
+
}
|
|
390
|
+
if (parsed > MAX_PROPERTY_COMBINATIONS) {
|
|
391
|
+
(0, logging_1.exitWithError)(`--max-combinations cannot exceed ${MAX_PROPERTY_COMBINATIONS}`);
|
|
392
|
+
}
|
|
393
|
+
return parsed;
|
|
394
|
+
}
|
|
268
395
|
function chunkArray(arr, size) {
|
|
269
396
|
const chunks = [];
|
|
270
397
|
for (let i = 0; i < arr.length; i += size) {
|
|
@@ -272,6 +399,41 @@ function chunkArray(arr, size) {
|
|
|
272
399
|
}
|
|
273
400
|
return chunks;
|
|
274
401
|
}
|
|
402
|
+
/**
|
|
403
|
+
* Split node IDs into request chunks that respect BOTH the node-count cap
|
|
404
|
+
* (PREVIEW_CHUNK_SIZE) and, when rendering property combinations, the server's
|
|
405
|
+
* per-request combination cap (MAX_PROPERTY_COMBINATIONS).
|
|
406
|
+
*
|
|
407
|
+
* The server sums combinations across every node in a request and rejects the
|
|
408
|
+
* whole request if the total exceeds the cap, so bundling by node count alone
|
|
409
|
+
* can push several individually-valid nodes over the limit and fail them all.
|
|
410
|
+
* Each node is already truncated to <= MAX_PROPERTY_COMBINATIONS upstream, so a
|
|
411
|
+
* single node always fits (worst case: one node per chunk).
|
|
412
|
+
*/
|
|
413
|
+
function chunkNodeIdsForPreview(nodeIds, propertyCombinationsByNodeId) {
|
|
414
|
+
if (!propertyCombinationsByNodeId) {
|
|
415
|
+
return chunkArray(nodeIds, PREVIEW_CHUNK_SIZE);
|
|
416
|
+
}
|
|
417
|
+
const chunks = [];
|
|
418
|
+
let current = [];
|
|
419
|
+
let currentCombinations = 0;
|
|
420
|
+
for (const nodeId of nodeIds) {
|
|
421
|
+
const count = propertyCombinationsByNodeId[nodeId]?.length ?? 0;
|
|
422
|
+
// Close the current chunk before adding this node would breach either cap.
|
|
423
|
+
if (current.length > 0 &&
|
|
424
|
+
(current.length >= PREVIEW_CHUNK_SIZE ||
|
|
425
|
+
currentCombinations + count > MAX_PROPERTY_COMBINATIONS)) {
|
|
426
|
+
chunks.push(current);
|
|
427
|
+
current = [];
|
|
428
|
+
currentCombinations = 0;
|
|
429
|
+
}
|
|
430
|
+
current.push(nodeId);
|
|
431
|
+
currentCombinations += count;
|
|
432
|
+
}
|
|
433
|
+
if (current.length > 0)
|
|
434
|
+
chunks.push(current);
|
|
435
|
+
return chunks;
|
|
436
|
+
}
|
|
275
437
|
async function runInWaves(items, fn, maxConcurrency) {
|
|
276
438
|
const out = [];
|
|
277
439
|
for (let i = 0; i < items.length; i += maxConcurrency) {
|
|
@@ -281,14 +443,55 @@ async function runInWaves(items, fn, maxConcurrency) {
|
|
|
281
443
|
}
|
|
282
444
|
return out;
|
|
283
445
|
}
|
|
284
|
-
function sendPreviewChunk({ chunkNodeIds, requiredTemplates, baseApiUrl, fileKey, accessToken, }) {
|
|
446
|
+
function sendPreviewChunk({ chunkNodeIds, requiredTemplates, baseApiUrl, fileKey, accessToken, propertyCombinationsByNodeId, }) {
|
|
285
447
|
const chunkTemplates = filterTemplatesForNodes(chunkNodeIds, requiredTemplates);
|
|
448
|
+
// Only include property combinations for the node IDs in this chunk.
|
|
449
|
+
let renderCombinations;
|
|
450
|
+
if (propertyCombinationsByNodeId) {
|
|
451
|
+
renderCombinations = {};
|
|
452
|
+
for (const nodeId of chunkNodeIds) {
|
|
453
|
+
if (propertyCombinationsByNodeId[nodeId]) {
|
|
454
|
+
renderCombinations[nodeId] = propertyCombinationsByNodeId[nodeId];
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
286
458
|
return fetch_1.request
|
|
287
|
-
.post(`${baseApiUrl}/code_connect/preview_snippets?file_key=${fileKey}`, {
|
|
459
|
+
.post(`${baseApiUrl}/code_connect/preview_snippets?file_key=${fileKey}`, {
|
|
460
|
+
nodeIds: chunkNodeIds,
|
|
461
|
+
figmaDocs: { all: chunkTemplates },
|
|
462
|
+
...(renderCombinations && Object.keys(renderCombinations).length > 0
|
|
463
|
+
? { renderCombinations }
|
|
464
|
+
: {}),
|
|
465
|
+
}, { headers: (0, figma_rest_api_1.getHeaders)(accessToken) })
|
|
288
466
|
.then((response) => ({ chunkNodeIds, response, error: null }))
|
|
289
467
|
.catch((err) => ({ chunkNodeIds, response: null, error: err }));
|
|
290
468
|
}
|
|
291
|
-
|
|
469
|
+
/**
|
|
470
|
+
* Parse the offending property name out of a "property not found" render error,
|
|
471
|
+
* e.g. `PropertyNotFoundError: property 'Varient' not found` -> `Varient`.
|
|
472
|
+
*/
|
|
473
|
+
function parseMissingProperty(error) {
|
|
474
|
+
if (!error)
|
|
475
|
+
return undefined;
|
|
476
|
+
const match = error.match(/propert(?:y|ies)[^'"`]*['"`]([^'"`]+)['"`]/i) ?? error.match(/['"`]([^'"`]+)['"`]/);
|
|
477
|
+
return match?.[1];
|
|
478
|
+
}
|
|
479
|
+
/** Build `errorDetails` for a failed result. Undefined when there's no property context (not --all). */
|
|
480
|
+
function buildErrorDetails(error, errorKind, availableProperties) {
|
|
481
|
+
// Transport failure: network/backend problem, not a bad property — carry only the kind.
|
|
482
|
+
if (errorKind === 'transport')
|
|
483
|
+
return { kind: 'transport' };
|
|
484
|
+
if (!availableProperties)
|
|
485
|
+
return undefined;
|
|
486
|
+
const details = { availableProperties };
|
|
487
|
+
if (errorKind)
|
|
488
|
+
details.kind = errorKind;
|
|
489
|
+
const missingProperty = parseMissingProperty(error);
|
|
490
|
+
if (missingProperty)
|
|
491
|
+
details.missingProperty = missingProperty;
|
|
492
|
+
return details;
|
|
493
|
+
}
|
|
494
|
+
function chunkOutcomeToResults(outcome, nodes, fileKey, availablePropsByNodeId) {
|
|
292
495
|
const { chunkNodeIds, response, error } = outcome;
|
|
293
496
|
const results = [];
|
|
294
497
|
if (error) {
|
|
@@ -311,6 +514,8 @@ function chunkOutcomeToResults(outcome, nodes, fileKey) {
|
|
|
311
514
|
filePath: node.filePath,
|
|
312
515
|
success: false,
|
|
313
516
|
error: errorMsg,
|
|
517
|
+
// Chunk-level failure is a server/network error, not a template problem.
|
|
518
|
+
errorDetails: { kind: 'transport' },
|
|
314
519
|
});
|
|
315
520
|
}
|
|
316
521
|
}
|
|
@@ -321,39 +526,61 @@ function chunkOutcomeToResults(outcome, nodes, fileKey) {
|
|
|
321
526
|
// Track match index per nodeId so duplicate node IDs get the correct file attribution.
|
|
322
527
|
// The server returns results in the same order as the templates we sent. Each chunk's
|
|
323
528
|
// nodeIds are disjoint (we dedupe before chunking), so per-chunk indexing is correct.
|
|
529
|
+
// Property-combination results are expanded renders of one node, not one result per
|
|
530
|
+
// matching template, so they should not consume this per-template index.
|
|
324
531
|
const nodeMatchIndex = {};
|
|
325
532
|
for (const result of response.data.meta.results) {
|
|
326
533
|
const idx = nodeMatchIndex[result.nodeId] ?? 0;
|
|
327
534
|
const matchingNodes = nodes.filter((n) => n.nodeId === result.nodeId);
|
|
328
|
-
const node =
|
|
329
|
-
|
|
535
|
+
const node = result.propertyCombinationLabel
|
|
536
|
+
? matchingNodes[0]
|
|
537
|
+
: matchingNodes[idx] || matchingNodes[0];
|
|
538
|
+
if (!result.propertyCombinationLabel) {
|
|
539
|
+
nodeMatchIndex[result.nodeId] = idx + 1;
|
|
540
|
+
}
|
|
541
|
+
const error = result.error || (!result.snippet ? 'No snippet returned by server' : undefined);
|
|
542
|
+
const success = !result.error && !!result.snippet;
|
|
330
543
|
results.push({
|
|
331
544
|
url: node?.url || result.nodeUrl,
|
|
332
545
|
nodeId: result.nodeId,
|
|
333
546
|
filePath: node?.filePath || '',
|
|
334
|
-
success
|
|
547
|
+
success,
|
|
335
548
|
snippet: result.snippet,
|
|
336
549
|
language: result.language,
|
|
337
550
|
component: result.component,
|
|
338
|
-
error
|
|
551
|
+
error,
|
|
552
|
+
...(result.propertyCombinationLabel
|
|
553
|
+
? { propertyCombinationLabel: result.propertyCombinationLabel }
|
|
554
|
+
: {}),
|
|
555
|
+
...(result.unresolvedInstances?.length
|
|
556
|
+
? { unresolvedInstances: result.unresolvedInstances }
|
|
557
|
+
: {}),
|
|
558
|
+
...(success
|
|
559
|
+
? {}
|
|
560
|
+
: {
|
|
561
|
+
errorDetails: buildErrorDetails(error, result.errorKind, availablePropsByNodeId?.[result.nodeId]),
|
|
562
|
+
}),
|
|
339
563
|
});
|
|
340
564
|
}
|
|
341
565
|
return results;
|
|
342
566
|
}
|
|
343
567
|
for (const nodeId of chunkNodeIds) {
|
|
344
568
|
for (const node of nodes.filter((n) => n.nodeId === nodeId)) {
|
|
569
|
+
const error = `API request failed with status ${response.response.status}`;
|
|
345
570
|
results.push({
|
|
346
571
|
url: node.url,
|
|
347
572
|
nodeId: node.nodeId,
|
|
348
573
|
filePath: node.filePath,
|
|
349
574
|
success: false,
|
|
350
|
-
error
|
|
575
|
+
error,
|
|
576
|
+
// Non-2xx from the endpoint is a transport-level failure, not a template problem.
|
|
577
|
+
errorDetails: { kind: 'transport' },
|
|
351
578
|
});
|
|
352
579
|
}
|
|
353
580
|
}
|
|
354
581
|
return results;
|
|
355
582
|
}
|
|
356
|
-
async function previewFile({ fileKey, nodes, baseApiUrl, accessToken, dir, allCodeConnectObjects, }) {
|
|
583
|
+
async function previewFile({ fileKey, nodes, baseApiUrl, accessToken, dir, allCodeConnectObjects, propertyCombinationsByNodeId, availablePropsByNodeId, }) {
|
|
357
584
|
const allNodeIds = nodes.map((n) => n.nodeId);
|
|
358
585
|
// Deduplicate — multiple figma.connect() calls may share the same nodeId;
|
|
359
586
|
// the individual templates are sent in figmaDocs and the server iterates per-template.
|
|
@@ -362,9 +589,183 @@ async function previewFile({ fileKey, nodes, baseApiUrl, accessToken, dir, allCo
|
|
|
362
589
|
// for the same nodeId exist on the server and shouldn't be rendered individually.
|
|
363
590
|
const requestedFilePaths = new Set(nodes.map((n) => path_1.default.resolve(dir, n.filePath)));
|
|
364
591
|
const requiredTemplates = filterTemplatesForNodes(allNodeIds, allCodeConnectObjects).filter((t) => requestedFilePaths.has(path_1.default.resolve(t._codeConnectFilePath || '')));
|
|
365
|
-
const chunks =
|
|
366
|
-
const outcomes = await runInWaves(chunks, (chunkNodeIds) => sendPreviewChunk({
|
|
367
|
-
|
|
592
|
+
const chunks = chunkNodeIdsForPreview(nodeIds, propertyCombinationsByNodeId);
|
|
593
|
+
const outcomes = await runInWaves(chunks, (chunkNodeIds) => sendPreviewChunk({
|
|
594
|
+
chunkNodeIds,
|
|
595
|
+
requiredTemplates,
|
|
596
|
+
baseApiUrl,
|
|
597
|
+
fileKey,
|
|
598
|
+
accessToken,
|
|
599
|
+
propertyCombinationsByNodeId,
|
|
600
|
+
}), PREVIEW_MAX_CONCURRENCY);
|
|
601
|
+
return outcomes.flatMap((outcome) => chunkOutcomeToResults(outcome, nodes, fileKey, availablePropsByNodeId));
|
|
602
|
+
}
|
|
603
|
+
function collectAllLocalNodesToPreview(allCodeConnectObjects, dir, cmd) {
|
|
604
|
+
const nodesToCheck = [];
|
|
605
|
+
for (const doc of allCodeConnectObjects) {
|
|
606
|
+
const parsed = (0, validation_1.parseFigmaNode)(cmd.verbose, doc, true);
|
|
607
|
+
if (parsed) {
|
|
608
|
+
nodesToCheck.push({
|
|
609
|
+
fileKey: parsed.fileKey,
|
|
610
|
+
nodeId: parsed.nodeId,
|
|
611
|
+
url: doc.figmaNode,
|
|
612
|
+
filePath: path_1.default.relative(dir, doc._codeConnectFilePath || ''),
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
return nodesToCheck;
|
|
617
|
+
}
|
|
618
|
+
function groupNodesByFileKey(nodes) {
|
|
619
|
+
const nodesByFileKey = {};
|
|
620
|
+
for (const node of nodes) {
|
|
621
|
+
nodesByFileKey[node.fileKey] ??= [];
|
|
622
|
+
nodesByFileKey[node.fileKey].push(node);
|
|
623
|
+
}
|
|
624
|
+
return nodesByFileKey;
|
|
625
|
+
}
|
|
626
|
+
async function handleInspect({ nodesByFileKey, configuredApiUrl, accessToken, outputFormat, }) {
|
|
627
|
+
const items = [];
|
|
628
|
+
const seen = new Set();
|
|
629
|
+
for (const [fileKey, nodes] of Object.entries(nodesByFileKey)) {
|
|
630
|
+
const baseApiUrl = (0, figma_rest_api_1.getApiUrl)(nodes[0].url, configuredApiUrl);
|
|
631
|
+
for (const node of nodes) {
|
|
632
|
+
const dedupeKey = `${node.filePath}::${node.nodeId}`;
|
|
633
|
+
if (seen.has(dedupeKey))
|
|
634
|
+
continue;
|
|
635
|
+
seen.add(dedupeKey);
|
|
636
|
+
const result = await fetchComponentPropertyDefinitions(baseApiUrl, fileKey, node.nodeId, accessToken);
|
|
637
|
+
if (result.status === 'error') {
|
|
638
|
+
logging_1.logger.warn(`Couldn't fetch property definitions for node ${node.nodeId}`);
|
|
639
|
+
}
|
|
640
|
+
else if (result.status === 'empty') {
|
|
641
|
+
logging_1.logger.info(`Node ${node.nodeId} has no component properties or variants.`);
|
|
642
|
+
}
|
|
643
|
+
items.push({
|
|
644
|
+
filePath: node.filePath,
|
|
645
|
+
nodeId: node.nodeId,
|
|
646
|
+
availableProperties: result.status === 'ok' ? (0, property_combinations_1.toAvailableProperties)(result.defs) : [],
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
if (outputFormat === 'json') {
|
|
651
|
+
console.log(JSON.stringify(items, null, 2));
|
|
652
|
+
}
|
|
653
|
+
else {
|
|
654
|
+
(0, property_list_table_1.displayPropertyList)(items);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
function validatePreviewOptions(cmd) {
|
|
658
|
+
if (cmd.all && cmd.props) {
|
|
659
|
+
(0, logging_1.exitWithError)('Cannot combine --props and --all; use one or the other');
|
|
660
|
+
}
|
|
661
|
+
if (cmd.maxCombinations !== undefined && !cmd.all) {
|
|
662
|
+
(0, logging_1.exitWithError)('--max-combinations can only be used with --all');
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
async function buildPropertyPreviewInputs({ enabled, baseApiUrl, fileKey, nodes, accessToken, propsPairs, maxCombinations, }) {
|
|
666
|
+
if (!enabled)
|
|
667
|
+
return {};
|
|
668
|
+
const propertyCombinationsByNodeId = {};
|
|
669
|
+
const availablePropsByNodeId = {};
|
|
670
|
+
for (const nodeId of [...new Set(nodes.map((n) => n.nodeId))]) {
|
|
671
|
+
const result = await fetchComponentPropertyDefinitions(baseApiUrl, fileKey, nodeId, accessToken);
|
|
672
|
+
if (result.status === 'error') {
|
|
673
|
+
logging_1.logger.warn(`Couldn't fetch property definitions for node ${nodeId} (ensure your token has the ` +
|
|
674
|
+
`Code Connect: Write and File content: Read scopes); rendering its default property combination only`);
|
|
675
|
+
continue;
|
|
676
|
+
}
|
|
677
|
+
if (result.status === 'empty') {
|
|
678
|
+
// Not an error: the component simply has no properties or variants to vary.
|
|
679
|
+
if (propsPairs) {
|
|
680
|
+
logging_1.logger.warn(`Node ${nodeId} has no component properties or variants, so --props ` +
|
|
681
|
+
`(${propsPairs.map((p) => p.name).join(', ')}) has no effect; rendering its only property combination.`);
|
|
682
|
+
}
|
|
683
|
+
else {
|
|
684
|
+
logging_1.logger.info(`Node ${nodeId} has no component properties or variants to vary; rendering its only property combination.`);
|
|
685
|
+
}
|
|
686
|
+
continue;
|
|
687
|
+
}
|
|
688
|
+
const defs = result.defs;
|
|
689
|
+
if (propsPairs) {
|
|
690
|
+
const { propertyCombination, availableProperties, unknown, invalid, ambiguous } = (0, property_combinations_1.buildPropertyCombinationFromProps)(defs, propsPairs);
|
|
691
|
+
if (unknown.length > 0) {
|
|
692
|
+
logging_1.logger.warn(`Unknown propert${unknown.length > 1 ? 'ies' : 'y'} for node ${nodeId}: ${unknown.join(', ')}. ` +
|
|
693
|
+
`Available: ${availableProperties.map((p) => p.name).join(', ')}`);
|
|
694
|
+
}
|
|
695
|
+
for (const { name, value, options } of invalid) {
|
|
696
|
+
logging_1.logger.warn(`"${value}" is not a valid value for ${name} on node ${nodeId}. Options: ${options.join(', ')}`);
|
|
697
|
+
}
|
|
698
|
+
for (const { name, types } of ambiguous) {
|
|
699
|
+
logging_1.logger.warn(`Property "${name}" on node ${nodeId} matches multiple types (${types.join(', ')}); ` +
|
|
700
|
+
`prefix the value with a type to disambiguate, e.g. ${types[0]}:${name}=... . Skipping.`);
|
|
701
|
+
}
|
|
702
|
+
availablePropsByNodeId[nodeId] = availableProperties;
|
|
703
|
+
propertyCombinationsByNodeId[nodeId] = [propertyCombination];
|
|
704
|
+
}
|
|
705
|
+
else {
|
|
706
|
+
const { propertyCombinations, availableProperties, truncated } = (0, property_combinations_1.enumeratePropertyCombinations)(defs, {
|
|
707
|
+
maxCombinations,
|
|
708
|
+
});
|
|
709
|
+
availablePropsByNodeId[nodeId] = availableProperties;
|
|
710
|
+
if (truncated) {
|
|
711
|
+
logging_1.logger.warn(`Node ${nodeId} has ${truncated.total} property combinations, exceeding the ${truncated.cap}-combination ` +
|
|
712
|
+
`preview limit; rendering the first ${truncated.cap}. Use --props to preview a ` +
|
|
713
|
+
`specific property combination.`);
|
|
714
|
+
}
|
|
715
|
+
if (propertyCombinations.length > 0) {
|
|
716
|
+
propertyCombinationsByNodeId[nodeId] = propertyCombinations;
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
return { propertyCombinationsByNodeId, availablePropsByNodeId };
|
|
721
|
+
}
|
|
722
|
+
async function previewNodesForFile({ fileKey, nodes, configuredApiUrl, accessToken, dir, allCodeConnectObjects, renderPropertyCombinations, propsPairs, maxCombinations, }) {
|
|
723
|
+
const baseApiUrl = (0, figma_rest_api_1.getApiUrl)(nodes[0].url, configuredApiUrl);
|
|
724
|
+
const { propertyCombinationsByNodeId, availablePropsByNodeId } = await buildPropertyPreviewInputs({
|
|
725
|
+
enabled: renderPropertyCombinations,
|
|
726
|
+
baseApiUrl,
|
|
727
|
+
fileKey,
|
|
728
|
+
nodes,
|
|
729
|
+
accessToken,
|
|
730
|
+
propsPairs,
|
|
731
|
+
maxCombinations,
|
|
732
|
+
});
|
|
733
|
+
return previewFile({
|
|
734
|
+
fileKey,
|
|
735
|
+
nodes,
|
|
736
|
+
baseApiUrl,
|
|
737
|
+
accessToken,
|
|
738
|
+
dir,
|
|
739
|
+
allCodeConnectObjects,
|
|
740
|
+
propertyCombinationsByNodeId,
|
|
741
|
+
availablePropsByNodeId,
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
async function validateRenderedSnippets(results) {
|
|
745
|
+
for (const result of results) {
|
|
746
|
+
if (result.success && result.snippet) {
|
|
747
|
+
const prettierValid = await isPrettierParseable(result.snippet, result.language);
|
|
748
|
+
if (!prettierValid) {
|
|
749
|
+
// Prettier can parse this language but failed — mark as error
|
|
750
|
+
result.success = false;
|
|
751
|
+
result.error = 'Snippet has syntax errors and may not be valid code';
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
async function outputPreviewResults(results, outputFormat) {
|
|
757
|
+
if (outputFormat === 'json') {
|
|
758
|
+
console.log(JSON.stringify(results, null, 2));
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
const formattedResults = await Promise.all(results.map(async (result) => {
|
|
762
|
+
if (result.success && result.snippet) {
|
|
763
|
+
const formattedSnippet = await formatSnippet(result.snippet, result.language);
|
|
764
|
+
return { ...result, snippet: formattedSnippet };
|
|
765
|
+
}
|
|
766
|
+
return result;
|
|
767
|
+
}));
|
|
768
|
+
displayResults(formattedResults);
|
|
368
769
|
}
|
|
369
770
|
/**
|
|
370
771
|
* Handle the preview command
|
|
@@ -375,75 +776,51 @@ async function handlePreview(files, cmd) {
|
|
|
375
776
|
const projectInfo = await (0, project_1.getProjectInfo)(dir, cmd.config);
|
|
376
777
|
const accessToken = (0, connect_1.getAccessTokenOrExit)(cmd);
|
|
377
778
|
const outputFormat = cmd.output || 'table';
|
|
378
|
-
const
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
nodesToCheck = await collectNodesToPreview(files, allCodeConnectObjects, dir, cmd);
|
|
779
|
+
const configuredApiUrl = cmd.apiUrl || projectInfo.config.apiUrl;
|
|
780
|
+
if (cmd.all && (!files || files.length === 0)) {
|
|
781
|
+
(0, logging_1.exitWithError)('--all requires a specific component, e.g. `figma connect preview Button.figma.ts --all`');
|
|
382
782
|
}
|
|
383
|
-
|
|
783
|
+
const allCodeConnectObjects = await (0, connect_1.getCodeConnectObjects)(cmd, projectInfo, true);
|
|
784
|
+
const nodesToCheck = files && files.length > 0
|
|
785
|
+
? await collectNodesToPreview(files, allCodeConnectObjects, dir, cmd)
|
|
786
|
+
: collectAllLocalNodesToPreview(allCodeConnectObjects, dir, cmd);
|
|
787
|
+
if (!files || files.length === 0) {
|
|
384
788
|
logging_1.logger.info('Previewing all local Code Connect files...');
|
|
385
|
-
nodesToCheck = [];
|
|
386
|
-
for (const doc of allCodeConnectObjects) {
|
|
387
|
-
const parsed = (0, validation_1.parseFigmaNode)(cmd.verbose, doc, true);
|
|
388
|
-
if (parsed) {
|
|
389
|
-
nodesToCheck.push({
|
|
390
|
-
fileKey: parsed.fileKey,
|
|
391
|
-
nodeId: parsed.nodeId,
|
|
392
|
-
url: doc.figmaNode,
|
|
393
|
-
filePath: path_1.default.relative(dir, doc._codeConnectFilePath || ''),
|
|
394
|
-
});
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
789
|
}
|
|
398
790
|
if (nodesToCheck.length === 0) {
|
|
399
791
|
(0, logging_1.exitWithError)('No valid Code Connect files found to preview');
|
|
400
792
|
}
|
|
401
793
|
logging_1.logger.info(`Previewing ${nodesToCheck.length} component(s)...`);
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
794
|
+
const nodesByFileKey = groupNodesByFileKey(nodesToCheck);
|
|
795
|
+
if (cmd.inspect) {
|
|
796
|
+
await handleInspect({
|
|
797
|
+
nodesByFileKey,
|
|
798
|
+
configuredApiUrl,
|
|
799
|
+
accessToken,
|
|
800
|
+
outputFormat,
|
|
801
|
+
});
|
|
802
|
+
return;
|
|
409
803
|
}
|
|
804
|
+
validatePreviewOptions(cmd);
|
|
805
|
+
const maxCombinations = parseMaxCombinationsArg(cmd.maxCombinations);
|
|
806
|
+
const propsPairs = cmd.props ? parsePropsArg(cmd.props) : undefined;
|
|
410
807
|
const results = [];
|
|
411
808
|
for (const [fileKey, nodes] of Object.entries(nodesByFileKey)) {
|
|
412
|
-
const
|
|
413
|
-
const fileResults = await previewFile({
|
|
809
|
+
const fileResults = await previewNodesForFile({
|
|
414
810
|
fileKey,
|
|
415
811
|
nodes,
|
|
416
|
-
|
|
812
|
+
configuredApiUrl,
|
|
417
813
|
accessToken,
|
|
418
814
|
dir,
|
|
419
815
|
allCodeConnectObjects,
|
|
816
|
+
renderPropertyCombinations: cmd.all || !!propsPairs,
|
|
817
|
+
propsPairs,
|
|
818
|
+
maxCombinations,
|
|
420
819
|
});
|
|
421
820
|
results.push(...fileResults);
|
|
422
821
|
}
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
if (result.success && result.snippet) {
|
|
426
|
-
const prettierValid = await isPrettierParseable(result.snippet, result.language);
|
|
427
|
-
if (!prettierValid) {
|
|
428
|
-
// Prettier can parse this language but failed — mark as error
|
|
429
|
-
result.success = false;
|
|
430
|
-
result.error = 'Snippet has syntax errors and may not be valid code';
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
if (outputFormat === 'json') {
|
|
435
|
-
console.log(JSON.stringify(results, null, 2));
|
|
436
|
-
}
|
|
437
|
-
else {
|
|
438
|
-
const formattedResults = await Promise.all(results.map(async (result) => {
|
|
439
|
-
if (result.success && result.snippet) {
|
|
440
|
-
const formattedSnippet = await formatSnippet(result.snippet, result.language);
|
|
441
|
-
return { ...result, snippet: formattedSnippet };
|
|
442
|
-
}
|
|
443
|
-
return result;
|
|
444
|
-
}));
|
|
445
|
-
displayResults(formattedResults);
|
|
446
|
-
}
|
|
822
|
+
await validateRenderedSnippets(results);
|
|
823
|
+
await outputPreviewResults(results, outputFormat);
|
|
447
824
|
if (results.every((r) => !r.success)) {
|
|
448
825
|
process.exit(1);
|
|
449
826
|
}
|