@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.
- package/README.md +449 -0
- package/bin/figma +5 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +41 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/connect.d.ts +3 -0
- package/dist/commands/connect.d.ts.map +1 -0
- package/dist/commands/connect.js +214 -0
- package/dist/commands/connect.js.map +1 -0
- package/dist/common/api.d.ts +146 -0
- package/dist/common/api.d.ts.map +1 -0
- package/dist/common/api.js +3 -0
- package/dist/common/api.js.map +1 -0
- package/dist/common/compiler.d.ts +96 -0
- package/dist/common/compiler.d.ts.map +1 -0
- package/dist/common/compiler.js +281 -0
- package/dist/common/compiler.js.map +1 -0
- package/dist/common/external.d.ts +10 -0
- package/dist/common/external.d.ts.map +1 -0
- package/dist/common/external.js +53 -0
- package/dist/common/external.js.map +1 -0
- package/dist/common/figma_connect.d.ts +24 -0
- package/dist/common/figma_connect.d.ts.map +1 -0
- package/dist/common/figma_connect.js +3 -0
- package/dist/common/figma_connect.js.map +1 -0
- package/dist/common/intrinsics.d.ts +61 -0
- package/dist/common/intrinsics.d.ts.map +1 -0
- package/dist/common/intrinsics.js +239 -0
- package/dist/common/intrinsics.js.map +1 -0
- package/dist/common/logging.d.ts +25 -0
- package/dist/common/logging.d.ts.map +1 -0
- package/dist/common/logging.js +47 -0
- package/dist/common/logging.js.map +1 -0
- package/dist/common/parser.d.ts +77 -0
- package/dist/common/parser.d.ts.map +1 -0
- package/dist/common/parser.js +736 -0
- package/dist/common/parser.js.map +1 -0
- package/dist/common/parser_template_helpers.d.ts +2 -0
- package/dist/common/parser_template_helpers.d.ts.map +1 -0
- package/dist/common/parser_template_helpers.js +53 -0
- package/dist/common/parser_template_helpers.js.map +1 -0
- package/dist/common/project.d.ts +79 -0
- package/dist/common/project.d.ts.map +1 -0
- package/dist/common/project.js +171 -0
- package/dist/common/project.js.map +1 -0
- package/dist/connect/create.d.ts +8 -0
- package/dist/connect/create.d.ts.map +1 -0
- package/dist/connect/create.js +162 -0
- package/dist/connect/create.js.map +1 -0
- package/dist/connect/delete_docs.d.ts +11 -0
- package/dist/connect/delete_docs.d.ts.map +1 -0
- package/dist/connect/delete_docs.js +60 -0
- package/dist/connect/delete_docs.js.map +1 -0
- package/dist/connect/figma_rest_api.d.ts +28 -0
- package/dist/connect/figma_rest_api.d.ts.map +1 -0
- package/dist/connect/figma_rest_api.js +19 -0
- package/dist/connect/figma_rest_api.js.map +1 -0
- package/dist/connect/helpers.d.ts +13 -0
- package/dist/connect/helpers.d.ts.map +1 -0
- package/dist/connect/helpers.js +87 -0
- package/dist/connect/helpers.js.map +1 -0
- package/dist/connect/upload.d.ts +8 -0
- package/dist/connect/upload.d.ts.map +1 -0
- package/dist/connect/upload.js +62 -0
- package/dist/connect/upload.js.map +1 -0
- package/dist/connect/validation.d.ts +3 -0
- package/dist/connect/validation.d.ts.map +1 -0
- package/dist/connect/validation.js +241 -0
- package/dist/connect/validation.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +31 -0
- package/dist/index.js.map +1 -0
- package/dist/storybook/convert.d.ts +24 -0
- package/dist/storybook/convert.d.ts.map +1 -0
- package/dist/storybook/convert.js +280 -0
- package/dist/storybook/convert.js.map +1 -0
- package/dist/storybook/external.d.ts +32 -0
- package/dist/storybook/external.d.ts.map +1 -0
- package/dist/storybook/external.js +3 -0
- package/dist/storybook/external.js.map +1 -0
- 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
|