@figma/code-connect 0.1.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.
Files changed (83) hide show
  1. package/README.md +449 -0
  2. package/bin/figma +5 -0
  3. package/dist/cli.d.ts +3 -0
  4. package/dist/cli.d.ts.map +1 -0
  5. package/dist/cli.js +41 -0
  6. package/dist/cli.js.map +1 -0
  7. package/dist/commands/connect.d.ts +3 -0
  8. package/dist/commands/connect.d.ts.map +1 -0
  9. package/dist/commands/connect.js +214 -0
  10. package/dist/commands/connect.js.map +1 -0
  11. package/dist/common/api.d.ts +146 -0
  12. package/dist/common/api.d.ts.map +1 -0
  13. package/dist/common/api.js +3 -0
  14. package/dist/common/api.js.map +1 -0
  15. package/dist/common/compiler.d.ts +96 -0
  16. package/dist/common/compiler.d.ts.map +1 -0
  17. package/dist/common/compiler.js +281 -0
  18. package/dist/common/compiler.js.map +1 -0
  19. package/dist/common/external.d.ts +10 -0
  20. package/dist/common/external.d.ts.map +1 -0
  21. package/dist/common/external.js +53 -0
  22. package/dist/common/external.js.map +1 -0
  23. package/dist/common/figma_connect.d.ts +24 -0
  24. package/dist/common/figma_connect.d.ts.map +1 -0
  25. package/dist/common/figma_connect.js +3 -0
  26. package/dist/common/figma_connect.js.map +1 -0
  27. package/dist/common/intrinsics.d.ts +61 -0
  28. package/dist/common/intrinsics.d.ts.map +1 -0
  29. package/dist/common/intrinsics.js +239 -0
  30. package/dist/common/intrinsics.js.map +1 -0
  31. package/dist/common/logging.d.ts +25 -0
  32. package/dist/common/logging.d.ts.map +1 -0
  33. package/dist/common/logging.js +47 -0
  34. package/dist/common/logging.js.map +1 -0
  35. package/dist/common/parser.d.ts +77 -0
  36. package/dist/common/parser.d.ts.map +1 -0
  37. package/dist/common/parser.js +736 -0
  38. package/dist/common/parser.js.map +1 -0
  39. package/dist/common/parser_template_helpers.d.ts +2 -0
  40. package/dist/common/parser_template_helpers.d.ts.map +1 -0
  41. package/dist/common/parser_template_helpers.js +53 -0
  42. package/dist/common/parser_template_helpers.js.map +1 -0
  43. package/dist/common/project.d.ts +79 -0
  44. package/dist/common/project.d.ts.map +1 -0
  45. package/dist/common/project.js +171 -0
  46. package/dist/common/project.js.map +1 -0
  47. package/dist/connect/create.d.ts +8 -0
  48. package/dist/connect/create.d.ts.map +1 -0
  49. package/dist/connect/create.js +162 -0
  50. package/dist/connect/create.js.map +1 -0
  51. package/dist/connect/delete_docs.d.ts +11 -0
  52. package/dist/connect/delete_docs.d.ts.map +1 -0
  53. package/dist/connect/delete_docs.js +60 -0
  54. package/dist/connect/delete_docs.js.map +1 -0
  55. package/dist/connect/figma_rest_api.d.ts +28 -0
  56. package/dist/connect/figma_rest_api.d.ts.map +1 -0
  57. package/dist/connect/figma_rest_api.js +19 -0
  58. package/dist/connect/figma_rest_api.js.map +1 -0
  59. package/dist/connect/helpers.d.ts +13 -0
  60. package/dist/connect/helpers.d.ts.map +1 -0
  61. package/dist/connect/helpers.js +87 -0
  62. package/dist/connect/helpers.js.map +1 -0
  63. package/dist/connect/upload.d.ts +8 -0
  64. package/dist/connect/upload.d.ts.map +1 -0
  65. package/dist/connect/upload.js +62 -0
  66. package/dist/connect/upload.js.map +1 -0
  67. package/dist/connect/validation.d.ts +3 -0
  68. package/dist/connect/validation.d.ts.map +1 -0
  69. package/dist/connect/validation.js +241 -0
  70. package/dist/connect/validation.js.map +1 -0
  71. package/dist/index.d.ts +7 -0
  72. package/dist/index.d.ts.map +1 -0
  73. package/dist/index.js +31 -0
  74. package/dist/index.js.map +1 -0
  75. package/dist/storybook/convert.d.ts +24 -0
  76. package/dist/storybook/convert.d.ts.map +1 -0
  77. package/dist/storybook/convert.js +280 -0
  78. package/dist/storybook/convert.js.map +1 -0
  79. package/dist/storybook/external.d.ts +32 -0
  80. package/dist/storybook/external.d.ts.map +1 -0
  81. package/dist/storybook/external.js +3 -0
  82. package/dist/storybook/external.js.map +1 -0
  83. package/package.json +56 -0
@@ -0,0 +1,736 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.parse = exports.getDefaultTemplate = exports.parseRenderFunction = exports.isFigmaConnectFile = exports.parseComponentMetadata = exports.parsePropsObject = exports.InternalError = exports.ParserError = void 0;
7
+ const typescript_1 = __importDefault(require("typescript"));
8
+ const project_1 = require("./project");
9
+ const logging_1 = require("./logging");
10
+ const compiler_1 = require("./compiler");
11
+ const intrinsics_1 = require("./intrinsics");
12
+ const parser_template_helpers_1 = require("./parser_template_helpers");
13
+ class ParserError extends Error {
14
+ constructor(message, context) {
15
+ super(message);
16
+ this.name = 'ParserError';
17
+ this.sourceFileName = context?.sourceFile.fileName || '';
18
+ this.sourceFilePosition =
19
+ context && context.node
20
+ ? getPositionInSourceFile(context.node, context.sourceFile) || null
21
+ : null;
22
+ }
23
+ toString() {
24
+ let msg = `${(0, logging_1.highlight)((0, logging_1.error)(this.name))}: ${this.message}\n`;
25
+ if (this.sourceFileName && this.sourceFilePosition) {
26
+ msg += ` -> ${(0, logging_1.reset)(this.sourceFileName)}:${this.sourceFilePosition.line}:${this.sourceFilePosition.character}\n`;
27
+ }
28
+ return msg;
29
+ }
30
+ toDebugString() {
31
+ return this.toString() + `\n ${this.stack}`;
32
+ }
33
+ }
34
+ exports.ParserError = ParserError;
35
+ class InternalError extends ParserError {
36
+ constructor(message) {
37
+ super(message);
38
+ this.name = 'InternalError';
39
+ }
40
+ }
41
+ exports.InternalError = InternalError;
42
+ /**
43
+ * Traverses the AST and returns the first JSX element it finds
44
+ * @param node AST node
45
+ * @returns
46
+ */
47
+ function findJSXElement(node) {
48
+ if (typescript_1.default.isJsxElement(node) || typescript_1.default.isJsxFragment(node) || typescript_1.default.isJsxSelfClosingElement(node)) {
49
+ return node;
50
+ }
51
+ else {
52
+ return typescript_1.default.forEachChild(node, findJSXElement);
53
+ }
54
+ }
55
+ function findBlock(node) {
56
+ if (typescript_1.default.isBlock(node)) {
57
+ return node;
58
+ }
59
+ else {
60
+ return typescript_1.default.forEachChild(node, findBlock);
61
+ }
62
+ }
63
+ function findDescendants(node, cb) {
64
+ const matches = [];
65
+ function visit(node) {
66
+ if (cb(node)) {
67
+ matches.push(node);
68
+ }
69
+ typescript_1.default.forEachChild(node, visit);
70
+ }
71
+ visit(node);
72
+ return matches;
73
+ }
74
+ function getPositionInSourceFile(node, sourceFile) {
75
+ return sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
76
+ }
77
+ /**
78
+ * Walks up the AST from an assignment to find the import declaration
79
+ */
80
+ function findParentImportDeclaration(declaration) {
81
+ let current = declaration;
82
+ while (current) {
83
+ if (typescript_1.default.isImportDeclaration(current)) {
84
+ return current;
85
+ }
86
+ current = current.parent;
87
+ }
88
+ }
89
+ function getImportsOfModule(sourceFile) {
90
+ const imports = [];
91
+ function visit(node) {
92
+ if (typescript_1.default.isImportDeclaration(node)) {
93
+ imports.push(node);
94
+ }
95
+ typescript_1.default.forEachChild(node, visit);
96
+ }
97
+ visit(sourceFile);
98
+ return imports;
99
+ }
100
+ /**
101
+ * Finds all import statements in a file that matches the given identifiers
102
+ *
103
+ * @param parserContext Parser context
104
+ * @param identifiers List of identifiers to find imports for
105
+ * @returns
106
+ */
107
+ function getImportsForIdentifiers({ sourceFile }, _identifiers) {
108
+ const importDeclarations = getImportsOfModule(sourceFile);
109
+ const imports = [];
110
+ const identifiers = _identifiers.map((identifier) => identifier.split('.')[0]);
111
+ for (const declaration of importDeclarations) {
112
+ let statement = declaration.getText();
113
+ const file = declaration.getSourceFile();
114
+ if (declaration.importClause) {
115
+ // Default imports
116
+ if (declaration.importClause.name) {
117
+ const identifier = declaration.importClause.name.text;
118
+ if (identifiers.includes(identifier)) {
119
+ imports.push({
120
+ statement,
121
+ file: file.fileName,
122
+ });
123
+ }
124
+ }
125
+ if (declaration.importClause.namedBindings) {
126
+ const namedBindings = declaration.importClause.namedBindings;
127
+ if (typescript_1.default.isNamedImports(namedBindings)) {
128
+ // Named imports (import { x, y } from 'module')
129
+ // filter out any unused imports from the statement the identifier belongs to
130
+ const elements = namedBindings.elements
131
+ .map((specifier) => specifier.name.text)
132
+ .filter((name) => identifiers.includes(name));
133
+ if (elements.length > 0) {
134
+ imports.push({
135
+ statement: statement.replace(/{.*}/s, `{ ${elements.join(', ')} }`),
136
+ file: file.fileName,
137
+ });
138
+ }
139
+ }
140
+ else if (typescript_1.default.isNamespaceImport(namedBindings)) {
141
+ // Namespace import (import * as name from 'module')
142
+ const identifier = namedBindings.name.text;
143
+ if (identifiers.includes(identifier)) {
144
+ imports.push({
145
+ statement,
146
+ file: file.fileName,
147
+ });
148
+ }
149
+ }
150
+ }
151
+ }
152
+ }
153
+ return imports;
154
+ }
155
+ /**
156
+ * Parsers the `props` field of a `Figma.connect()` call, returning a mapping of
157
+ * prop names to their respective intrinsic types
158
+ *
159
+ * @param objectLiteral An object literal expression
160
+ * @param parserContext Parser context
161
+ * @returns
162
+ */
163
+ function parsePropsObject(objectLiteral, parserContext) {
164
+ const { sourceFile, checker } = parserContext;
165
+ return (0, compiler_1.convertObjectLiteralToJs)(objectLiteral, sourceFile, checker, (valueNode) => {
166
+ if (typescript_1.default.isCallExpression(valueNode)) {
167
+ return (0, intrinsics_1.parseIntrinsic)(valueNode, parserContext);
168
+ }
169
+ });
170
+ }
171
+ exports.parsePropsObject = parsePropsObject;
172
+ /**
173
+ * Extract metadata about the referenced React component. Used by both the
174
+ * Figmadoc and Storybook commands.
175
+ *
176
+ * @param parserContext Parser context
177
+ * @param componentSymbol The ts.Symbol from the metadata referencing the
178
+ * component being documented
179
+ * @param node The node being parsed. Used for error logging.
180
+ * @returns Metadata object
181
+ */
182
+ async function parseComponentMetadata(node, { checker, sourceFile }) {
183
+ let componentSymbol = checker.getSymbolAtLocation(node);
184
+ let componentSourceFile = sourceFile;
185
+ let component = '';
186
+ let componentDeclaration;
187
+ // Hacky fix for namespaced components, this probably doesn't work for storybook
188
+ if (typescript_1.default.isPropertyAccessExpression(node)) {
189
+ componentSymbol = checker.getSymbolAtLocation(node.expression);
190
+ if (!componentSymbol) {
191
+ throw new ParserError(`Could not find symbol for component ${node.expression.getText()}`, {
192
+ sourceFile,
193
+ node,
194
+ });
195
+ }
196
+ }
197
+ // Component declared in a different file
198
+ if (componentSymbol &&
199
+ componentSymbol.declarations &&
200
+ (typescript_1.default.isImportSpecifier(componentSymbol.declarations[0]) ||
201
+ typescript_1.default.isImportClause(componentSymbol.declarations[0]))) {
202
+ let importDeclaration = findParentImportDeclaration(componentSymbol.declarations[0]);
203
+ if (!importDeclaration) {
204
+ throw new ParserError('No import statement found for component, make sure the component is imported', {
205
+ sourceFile,
206
+ node,
207
+ });
208
+ }
209
+ // The component should be imported from another file, we need to follow the
210
+ // aliased symbol to get the correct function definition
211
+ if (componentSymbol.flags & typescript_1.default.SymbolFlags.Alias) {
212
+ componentSymbol = checker.getAliasedSymbol(componentSymbol);
213
+ }
214
+ if (!componentSymbol || !componentSymbol.declarations) {
215
+ logging_1.logger.warn(`Import for ${node.getText()} could not be resolved, make sure that your \`include\` globs in \`figma.config.json\` matches the component source file (in addition to the Code Connect file). If you're using path aliases, make sure to include the same aliases in \`figma.config.json\` with the \`paths\` option.`);
216
+ return {
217
+ source: '',
218
+ line: 0,
219
+ component: node.getText(),
220
+ };
221
+ }
222
+ // If we haven't found the component declaration by now, it's likely because it's
223
+ // assigned to an object/namespace, for example: `export const Button = { Primary: () => <button /> }`,
224
+ // so we need to find the function declaration by traversing the AST in that file.
225
+ if (!typescript_1.default.isFunctionDeclaration(componentSymbol.declarations[0])) {
226
+ const sourceFile = componentSymbol.declarations[0].getSourceFile();
227
+ (0, compiler_1.bfsFindNode)(sourceFile, sourceFile, (node) => {
228
+ if ((typescript_1.default.isFunctionDeclaration(node) || typescript_1.default.isVariableDeclaration(node)) &&
229
+ node.name &&
230
+ componentSymbol?.name &&
231
+ node.name.getText() === componentSymbol.name) {
232
+ componentSymbol = checker.getSymbolAtLocation(node.name);
233
+ return true;
234
+ }
235
+ return false;
236
+ });
237
+ }
238
+ componentDeclaration = componentSymbol.declarations[0];
239
+ componentSourceFile = componentDeclaration.getSourceFile();
240
+ }
241
+ else {
242
+ componentDeclaration = componentSymbol?.declarations?.[0];
243
+ }
244
+ const source = componentSourceFile.fileName;
245
+ if (!source) {
246
+ throw new InternalError(`Could not find source file for component ${component} - is this file included in the directory passed to \`figma connect <dir>\`?`);
247
+ }
248
+ if (!componentDeclaration) {
249
+ throw new ParserError(`Could not find declaration for component ${component}`, {
250
+ sourceFile,
251
+ node,
252
+ });
253
+ }
254
+ const line = getPositionInSourceFile(componentDeclaration, componentSourceFile).line;
255
+ if (line === undefined) {
256
+ throw new InternalError(`Could not determine line number for component ${componentDeclaration.getStart(sourceFile)}`);
257
+ }
258
+ return {
259
+ source,
260
+ line,
261
+ component: node.getText(),
262
+ };
263
+ }
264
+ exports.parseComponentMetadata = parseComponentMetadata;
265
+ /**
266
+ * Checks if an AST node is a `Figma.connect()` call
267
+ *
268
+ * @param node AST node
269
+ * @param sourceFile Source file
270
+ * @returns True if the node is a `Figma.connect()` call
271
+ */
272
+ function isFigmaConnectCall(node, sourceFile) {
273
+ return (typescript_1.default.isCallExpression(node) && node.expression.getText(sourceFile).includes(intrinsics_1.FIGMA_CONNECT_CALL));
274
+ }
275
+ /**
276
+ * Checks if a file contains figmadoc by looking for the `Figma.connect()` function call
277
+ *
278
+ * @param program
279
+ * @param file
280
+ * @returns
281
+ */
282
+ function isFigmaConnectFile(program, file) {
283
+ // We don't support Figmadoc in JSX and this throws an error if we let it proceed
284
+ if (!file.endsWith('.tsx')) {
285
+ return false;
286
+ }
287
+ const sourceFile = program.getSourceFile(file);
288
+ if (!sourceFile) {
289
+ throw new InternalError(`Could not find source file for ${file}`);
290
+ }
291
+ return (findDescendants(sourceFile, (node) => {
292
+ if (isFigmaConnectCall(node, sourceFile)) {
293
+ return true;
294
+ }
295
+ return false;
296
+ }).length > 0);
297
+ }
298
+ exports.isFigmaConnectFile = isFigmaConnectFile;
299
+ /**
300
+ * Parses the `links` field of a `Figma.connect()` call
301
+ *
302
+ * @param linksArray an ArrayLiteralExpression
303
+ * @param parserContext Parser context
304
+ * @returns An array of link objects
305
+ */
306
+ function parseLinks(linksArray, parserContext) {
307
+ const { sourceFile } = parserContext;
308
+ const links = [];
309
+ for (const element of linksArray.elements) {
310
+ (0, compiler_1.assertIsObjectLiteralExpression)(element, sourceFile, `'links' must be an array literal with objects of the format { name: string, url: string }`);
311
+ const name = (0, compiler_1.parsePropertyOfType)({
312
+ objectLiteralNode: element,
313
+ propertyName: 'name',
314
+ predicate: typescript_1.default.isStringLiteral,
315
+ parserContext,
316
+ required: true,
317
+ errorMessage: "The 'name' property must be a string literal",
318
+ });
319
+ const url = (0, compiler_1.parsePropertyOfType)({
320
+ objectLiteralNode: element,
321
+ propertyName: 'url',
322
+ predicate: typescript_1.default.isStringLiteral,
323
+ parserContext,
324
+ required: true,
325
+ errorMessage: "The 'url' property must be a string literal",
326
+ });
327
+ if (name && url) {
328
+ links.push({ name: (0, compiler_1.stripQuotes)(name), url: (0, compiler_1.stripQuotes)(url) });
329
+ }
330
+ }
331
+ return links;
332
+ }
333
+ function parseVariant(variantMap, sourceFile, checker) {
334
+ return (0, compiler_1.convertObjectLiteralToJs)(variantMap, sourceFile, checker, (valueNode) => {
335
+ if (!typescript_1.default.isObjectLiteralElement(valueNode) &&
336
+ !typescript_1.default.isStringLiteral(valueNode) &&
337
+ !typescript_1.default.isNumericLiteral(valueNode) &&
338
+ valueNode.kind !== typescript_1.default.SyntaxKind.TrueKeyword &&
339
+ valueNode.kind !== typescript_1.default.SyntaxKind.FalseKeyword) {
340
+ throw new ParserError(`Invalid value for variant, got: ${valueNode.getText()}`, {
341
+ node: valueNode,
342
+ sourceFile,
343
+ });
344
+ }
345
+ });
346
+ }
347
+ /**
348
+ * Parses the render function passed to `Figma.connect()`, extracting the code and
349
+ * any import statements matching the JSX elements used in the function body
350
+ *
351
+ * @param exp A function or arrow function expression
352
+ * @param parserContext Parser context
353
+ * @param propMappings Prop mappings object as returned by parseProps
354
+ *
355
+ * @returns The code of the render function and a list of imports
356
+ */
357
+ function parseRenderFunction(exp, parserContext, propMappings) {
358
+ const { sourceFile } = parserContext;
359
+ let jsx = findJSXElement(exp);
360
+ let exampleCode;
361
+ if (exp.parameters.length > 1) {
362
+ throw new ParserError(`Expected a single props parameter for the render function, got ${exp.parameters.length} parameters`, { sourceFile, node: exp });
363
+ }
364
+ const propsParameter = exp.parameters[0];
365
+ // Keep track of any props which are referenced in the example so that we can
366
+ // insert the appropriate `figma.properties` call in the JS template
367
+ const referencedProps = new Set();
368
+ function createPropPlaceholder(name, node, wrapInJsxExpression = false) {
369
+ const mappedProp = propMappings && propMappings[name];
370
+ if (!mappedProp) {
371
+ throw new ParserError(`Could not find prop mapping for ${name} in the props object`, {
372
+ sourceFile,
373
+ node,
374
+ });
375
+ }
376
+ referencedProps.add(name);
377
+ const callExpression = typescript_1.default.factory.createCallExpression(typescript_1.default.factory.createIdentifier('__PROP__'), undefined, [typescript_1.default.factory.createStringLiteral(name)]);
378
+ if (wrapInJsxExpression) {
379
+ return typescript_1.default.factory.createJsxExpression(undefined, callExpression);
380
+ }
381
+ else {
382
+ return callExpression;
383
+ }
384
+ }
385
+ // Find all property access expressions in the function body and replace them
386
+ // with a function call like `__PROP__("propName")`, so that we can easily
387
+ // find them in the next step to convert them into
388
+ // `${_fcc_renderReactProp(...)` in the template string.
389
+ //
390
+ // Doing it this way means we can normalize the different ways in which props
391
+ // can be accessed using the compiler API, which is much easier than using a
392
+ // regex, then in the next step we can use a simple regex to convert that into
393
+ // the template string.
394
+ if (propsParameter && jsx) {
395
+ jsx = typescript_1.default.transform(jsx, [
396
+ (context) => (rootNode) => {
397
+ function visit(node) {
398
+ // `props.` notation
399
+ if (typescript_1.default.isIdentifier(propsParameter.name) &&
400
+ typescript_1.default.isPropertyAccessExpression(node) &&
401
+ node.expression.getText().startsWith(propsParameter.name.getText())) {
402
+ const name = node.name.getText();
403
+ return createPropPlaceholder(name, node);
404
+ }
405
+ // `props[""]` notation
406
+ if (typescript_1.default.isIdentifier(propsParameter.name) &&
407
+ typescript_1.default.isElementAccessExpression(node) &&
408
+ node.expression.getText().startsWith(propsParameter.name.getText()) &&
409
+ typescript_1.default.isStringLiteral(node.argumentExpression)) {
410
+ const name = (0, compiler_1.stripQuotes)(node.argumentExpression);
411
+ return createPropPlaceholder(name, node);
412
+ }
413
+ // object destructuring references
414
+ if (typescript_1.default.isObjectBindingPattern(propsParameter.name) &&
415
+ typescript_1.default.isJsxExpression(node) &&
416
+ node.expression &&
417
+ propsParameter.name.elements.find((el) => el.name.getText() === node.expression?.getText())) {
418
+ const name = node.expression.getText();
419
+ return createPropPlaceholder(name, node, true);
420
+ }
421
+ // Replaces {...props} with all the prop mapped props we know about,
422
+ // e.g. <Button {...props} /> becomes:
423
+ // <Button prop1={__PROP__("prop1")} prop2={__PROP__("prop2")} />.
424
+ if (typescript_1.default.isJsxSpreadAttribute(node) &&
425
+ typescript_1.default.isIdentifier(node.expression) &&
426
+ node.expression.getText() === propsParameter.name.getText()) {
427
+ const props = propMappings
428
+ ? Object.keys(propMappings).map((prop) => {
429
+ return typescript_1.default.factory.createJsxAttribute(typescript_1.default.factory.createIdentifier(prop), createPropPlaceholder(prop, node, true));
430
+ })
431
+ : [];
432
+ if (propMappings) {
433
+ for (const key of Object.keys(propMappings)) {
434
+ referencedProps.add(key);
435
+ }
436
+ }
437
+ return props;
438
+ }
439
+ return typescript_1.default.visitEachChild(node, visit, context);
440
+ }
441
+ return typescript_1.default.visitNode(rootNode, visit);
442
+ },
443
+ ]).transformed[0];
444
+ }
445
+ const printer = typescript_1.default.createPrinter();
446
+ const block = findBlock(exp);
447
+ let nestable = false;
448
+ if (jsx && (!block || (block && block.statements.length <= 1))) {
449
+ // The function body is a single JSX element
450
+ exampleCode = printer.printNode(typescript_1.default.EmitHint.Unspecified, jsx, sourceFile);
451
+ nestable = true;
452
+ }
453
+ else if (block) {
454
+ // The function body has more stuff in it, so we wrap it in a function
455
+ // expression that returns the JSX element. Why not just print the exact function passed
456
+ // to `render`? Because the parameters to that function are not actually referenced in the
457
+ // rendered code snippet in Figma - they're mapped to values on the Figma instance.
458
+ const functionName = 'Example';
459
+ const functionExpression = typescript_1.default.factory.createFunctionExpression(undefined, undefined, typescript_1.default.factory.createIdentifier(functionName), [], undefined, undefined, typescript_1.default.factory.createBlock([
460
+ ...block.statements.filter((s) => !typescript_1.default.isReturnStatement(s)),
461
+ typescript_1.default.factory.createReturnStatement(jsx),
462
+ ], true));
463
+ const printer = typescript_1.default.createPrinter();
464
+ exampleCode = printer.printNode(typescript_1.default.EmitHint.Unspecified, functionExpression, sourceFile);
465
+ }
466
+ else {
467
+ throw new ParserError(`Expected a single JSX element or a block statement in the render function, got ${exp.getText()}`, { sourceFile, node: exp });
468
+ }
469
+ let templateCode = '';
470
+ // Replace React prop placeholders we inserted above (like
471
+ // `reactPropName={__PROP__("figmaPropName")}`) with calls to
472
+ // _fcc_renderReactProp, which renders them correctly (see
473
+ // parser_template_helpers.ts)
474
+ exampleCode = exampleCode.replace(
475
+ // match " reactPropName={__PROP__("figmaPropName")}" and extract the names
476
+ / ([A-Za-z0-9]+)=\{__PROP__\("([A-Za-z0-9]+)"\)\}/g, (_match, reactPropName, figmaPropName) => {
477
+ return `\${_fcc_renderReactProp('${reactPropName}', ${figmaPropName})}`;
478
+ });
479
+ // Replace React children placeholders like `${__PROP__("propName")}` with
480
+ // `${propName}`. These never need special treatment based on their type.
481
+ exampleCode = exampleCode.replace(/\{__PROP__\("([A-Za-z]+)"\)\}/g, '${$1}');
482
+ // Generate the template code
483
+ // Inject React-specific template helper functions
484
+ templateCode = (0, parser_template_helpers_1.getParsedTemplateHelpersString)() + '\n\n';
485
+ // Require the template API
486
+ templateCode += `const figma = require('figma')\n\n`;
487
+ // Then we output `const propName = figma.properties.<kind>('propName')` calls
488
+ // for each referenced prop, so these are accessible to the template code.
489
+ if (propMappings && referencedProps.size > 0) {
490
+ referencedProps.forEach((prop) => {
491
+ const propMapping = propMappings[prop];
492
+ if (!propMapping) {
493
+ throw new ParserError(`Could not find prop mapping for ${prop}`, {
494
+ sourceFile,
495
+ node: exp,
496
+ });
497
+ }
498
+ templateCode += `const ${prop} = ${(0, intrinsics_1.intrinsicToString)(propMapping)}\n`;
499
+ });
500
+ templateCode += '\n';
501
+ }
502
+ // Finally, output the example code
503
+ templateCode += `export default figma.tsx\`${exampleCode}\`\n`;
504
+ // Find all JSX elements in the function body and extract their import
505
+ // statements
506
+ const jsxTags = findDescendants(exp, (element) => typescript_1.default.isJsxElement(element) || typescript_1.default.isJsxSelfClosingElement(element)).map(compiler_1.getTagName);
507
+ const imports = getImportsForIdentifiers(parserContext, jsxTags);
508
+ return {
509
+ code: templateCode,
510
+ imports,
511
+ nestable,
512
+ };
513
+ }
514
+ exports.parseRenderFunction = parseRenderFunction;
515
+ /**
516
+ * This wrapper function ensures that property names are type checked in case
517
+ * we make changes to the `Figma.connect()` interface.
518
+ */
519
+ function makeConfigPropertyParser({ key, predicate, errorMessage, }) {
520
+ return (configArg, parserContext) => {
521
+ if (!configArg) {
522
+ return undefined;
523
+ }
524
+ return (0, compiler_1.parsePropertyOfType)({
525
+ objectLiteralNode: configArg,
526
+ propertyName: key,
527
+ predicate,
528
+ parserContext,
529
+ required: false,
530
+ errorMessage,
531
+ });
532
+ };
533
+ }
534
+ function makeFunctionArgumentParser({ index, predicate, required, errorMessage, }) {
535
+ return (node, parserContext) => {
536
+ return (0, compiler_1.parseFunctionArgument)(node, parserContext, index, predicate, required, errorMessage);
537
+ };
538
+ }
539
+ function makeArgParser() {
540
+ return {
541
+ parseComponent: makeFunctionArgumentParser({
542
+ index: 0,
543
+ predicate: (0, compiler_1.isOneOf)([typescript_1.default.isIdentifier, typescript_1.default.isPropertyAccessExpression]),
544
+ required: true,
545
+ errorMessage: `\`${intrinsics_1.FIGMA_CONNECT_CALL}\` must be called with a reference to a Component as the first argument. Example usage:
546
+ \`${intrinsics_1.FIGMA_CONNECT_CALL}(Button, 'https://www.figma.com/file/123?node-id=1-1')\``,
547
+ }),
548
+ parseFigmaNodeUrl: (node, parserContext) => {
549
+ const { checker } = parserContext;
550
+ const invalidTypeErrorMsg = `The second argument to ${intrinsics_1.FIGMA_CONNECT_CALL}() must be a string literal (the URL of the Figma node). Example usage:
551
+ \`${intrinsics_1.FIGMA_CONNECT_CALL}(Button, 'https://www.figma.com/file/123?node-id=1-1')\``;
552
+ const index = 1;
553
+ const required = true;
554
+ let arg = (0, compiler_1.parseFunctionArgument)(node, parserContext, index, (0, compiler_1.isOneOf)([typescript_1.default.isIdentifier, typescript_1.default.isStringLiteral]), required, invalidTypeErrorMsg);
555
+ if (arg && typescript_1.default.isIdentifier(arg)) {
556
+ const symbol = checker.getSymbolAtLocation(arg);
557
+ if (symbol) {
558
+ const decl = symbol.valueDeclaration;
559
+ if (decl &&
560
+ typescript_1.default.isVariableDeclaration(decl) &&
561
+ decl.initializer &&
562
+ typescript_1.default.isStringLiteral(decl.initializer)) {
563
+ arg = decl.initializer;
564
+ }
565
+ }
566
+ }
567
+ // If we followed the identifier to its declaration and it's not a string literal,
568
+ // throw an error
569
+ if (!arg || !typescript_1.default.isStringLiteral(arg)) {
570
+ throw new ParserError(invalidTypeErrorMsg, {
571
+ node: arg,
572
+ sourceFile: parserContext.sourceFile,
573
+ });
574
+ }
575
+ return arg;
576
+ },
577
+ parseConfig: makeFunctionArgumentParser({
578
+ index: 2,
579
+ predicate: typescript_1.default.isObjectLiteralExpression,
580
+ required: false,
581
+ errorMessage: `The third argument to ${intrinsics_1.FIGMA_CONNECT_CALL}() must be an object literal. Example usage:
582
+ \`${intrinsics_1.FIGMA_CONNECT_CALL}(Button, 'https://www.figma.com/file/123?node-id=1-1', { render: () => <Button /> })\``,
583
+ }),
584
+ parseProps: makeConfigPropertyParser({
585
+ key: 'props',
586
+ predicate: typescript_1.default.isObjectLiteralExpression,
587
+ errorMessage: `The 'props' property must be an object literal. Example usage:
588
+ \`${intrinsics_1.FIGMA_CONNECT_CALL}(Button, 'https://www.figma.com/file/123?node-id=1-1', {
589
+ props: {
590
+ disabled: Figma.boolean('Disabled'),
591
+ text: Figma.string('TextContent'),
592
+ }
593
+ })\``,
594
+ }),
595
+ parseExample: makeConfigPropertyParser({
596
+ key: 'example',
597
+ predicate: (0, compiler_1.isOneOf)([typescript_1.default.isArrowFunction, typescript_1.default.isFunctionExpression]),
598
+ errorMessage: `The 'example' property must be an inline function or arrow function. Example usage:
599
+ \`${intrinsics_1.FIGMA_CONNECT_CALL}(Button, 'https://www.figma.com/file/123?node-id=1-1', {
600
+ render: () => <Button />
601
+ })\``,
602
+ }),
603
+ parseVariant: makeConfigPropertyParser({
604
+ key: 'variant',
605
+ predicate: typescript_1.default.isObjectLiteralExpression,
606
+ errorMessage: `The 'variant' property must be an object literal. Example usage:
607
+ \`${intrinsics_1.FIGMA_CONNECT_CALL}(Button, 'https://www.figma.com/file/123?node-id=1-1', {
608
+ variant: {
609
+ "Has Icon": true
610
+ }
611
+ })\``,
612
+ }),
613
+ parseLinks: makeConfigPropertyParser({
614
+ key: 'links',
615
+ predicate: typescript_1.default.isArrayLiteralExpression,
616
+ errorMessage: `The 'links' property must be an array literal. Example usage:
617
+ \`${intrinsics_1.FIGMA_CONNECT_CALL}(Button, 'https://www.figma.com/file/123?node-id=1-1', {
618
+ links: [
619
+ { name: 'Storybook', url: 'https://storybook.com' }
620
+ ]
621
+ })\``,
622
+ }),
623
+ };
624
+ }
625
+ function getDefaultTemplate(componentMetadata) {
626
+ const example = `<${componentMetadata.component} />`;
627
+ return `const figma = require("figma")\n\nexport default figma.tsx\`${example}\``;
628
+ }
629
+ exports.getDefaultTemplate = getDefaultTemplate;
630
+ async function parseDoc(node, parserContext, repoUrl) {
631
+ const { checker, sourceFile, config } = parserContext;
632
+ // Parse the arguments to the `Figma.connect()` call
633
+ const args = makeArgParser();
634
+ // The ones with ! are definitely defined because their parser fn has required: true,
635
+ // but I couldn't work out how to model that in TypeScript
636
+ const componentArg = args.parseComponent(node, parserContext);
637
+ const figmaNodeUrlArg = args.parseFigmaNodeUrl(node, parserContext);
638
+ const configObjArg = args.parseConfig(node, parserContext);
639
+ const propsArg = configObjArg && args.parseProps(configObjArg, parserContext);
640
+ const exampleArg = configObjArg && args.parseExample(configObjArg, parserContext);
641
+ const variantArg = configObjArg && args.parseVariant(configObjArg, parserContext);
642
+ const linksArg = configObjArg && args.parseLinks(configObjArg, parserContext);
643
+ let figmaNode = (0, compiler_1.stripQuotes)(figmaNodeUrlArg);
644
+ if (config?.documentUrlSubstitutions) {
645
+ Object.entries(config.documentUrlSubstitutions).forEach(([from, to]) => {
646
+ figmaNode = figmaNode.replace(from, to);
647
+ });
648
+ }
649
+ const metadata = await parseComponentMetadata(componentArg, parserContext);
650
+ const props = propsArg ? parsePropsObject(propsArg, parserContext) : undefined;
651
+ const render = exampleArg ? parseRenderFunction(exampleArg, parserContext, props) : undefined;
652
+ const variant = variantArg ? parseVariant(variantArg, sourceFile, checker) : undefined;
653
+ const links = linksArg ? parseLinks(linksArg, parserContext) : undefined;
654
+ // If no template function was provided, construct one and add the import
655
+ // statement for the component
656
+ let imports = render?.imports ?? getImportsForIdentifiers(parserContext, [metadata.component]);
657
+ const template = render?.code ?? getDefaultTemplate(metadata);
658
+ if (imports.length === 0) {
659
+ // If no imports were found, it might mean that the component is not imported, or
660
+ // that the `figma.connect` call is in the same file as the component. In the latter
661
+ // case - we'll want to generate one
662
+ const fileName = metadata.source.split('/').pop()?.split('.')[0];
663
+ imports = [
664
+ {
665
+ statement: `import { ${metadata.component} } from './${fileName}'`,
666
+ file: sourceFile.fileName,
667
+ },
668
+ ];
669
+ }
670
+ const resolvedImports = imports.map((imp) => {
671
+ if (config) {
672
+ const resolvedPath = (0, project_1.resolveImportPath)(imp.file, config);
673
+ if (resolvedPath) {
674
+ return imp.statement.replace(/['"]([\.\/a-zA-Z0-9]*)['"]/, `'${resolvedPath}'`);
675
+ }
676
+ }
677
+ return imp.statement;
678
+ }) ?? [];
679
+ if (resolvedImports.length === 0) {
680
+ logging_1.logger.warn(`The import statement for ${metadata.component} could not be automatically resolved, make sure the component is imported (if not colocating) and that the path mappings are correct in your figma.config.json`);
681
+ }
682
+ return {
683
+ figmaNode,
684
+ label: 'React',
685
+ language: 'typescript',
686
+ component: metadata.component,
687
+ source: (0, project_1.getRemoteFileUrl)(metadata.source, repoUrl),
688
+ sourceLocation: { line: metadata.line },
689
+ variant,
690
+ template,
691
+ templateData: {
692
+ // TODO: `props` here is currently only used for validation purposes,
693
+ // we should eventually remove it from the JSON payload
694
+ props,
695
+ imports: resolvedImports,
696
+ // If there's no render function, the default example is always nestable
697
+ nestable: render ? render.nestable : true,
698
+ },
699
+ links,
700
+ metadata: {
701
+ cliVersion: require('../../package.json').version,
702
+ },
703
+ };
704
+ }
705
+ async function parse(program, file, repoUrl, config, debug) {
706
+ const sourceFile = program.getSourceFile(file);
707
+ if (!sourceFile) {
708
+ throw new InternalError(`Could not find source file for ${file}`);
709
+ }
710
+ const parserContext = {
711
+ checker: program.getTypeChecker(),
712
+ sourceFile,
713
+ config,
714
+ };
715
+ const figmadocs = [];
716
+ const nodes = [parserContext.sourceFile];
717
+ while (nodes.length > 0) {
718
+ const node = nodes.shift();
719
+ if (isFigmaConnectCall(node, parserContext.sourceFile)) {
720
+ const doc = await parseDoc(node, parserContext, repoUrl);
721
+ if (doc) {
722
+ figmadocs.push(doc);
723
+ }
724
+ }
725
+ nodes.push(...node.getChildren(parserContext.sourceFile));
726
+ }
727
+ if (figmadocs.length === 0) {
728
+ throw new ParserError(`Didn't find any calls to Figmadoc()`, {
729
+ sourceFile: parserContext.sourceFile,
730
+ node: parserContext.sourceFile,
731
+ });
732
+ }
733
+ return figmadocs;
734
+ }
735
+ exports.parse = parse;
736
+ //# sourceMappingURL=parser.js.map