"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.findJSXElement = findJSXElement; exports.findAndResolveImports = findAndResolveImports; exports.parseComponentMetadata = parseComponentMetadata; exports.parseRenderFunctionExpression = parseRenderFunctionExpression; exports.parseJSXRenderFunction = parseJSXRenderFunction; exports.parseValueRenderFunction = parseValueRenderFunction; exports.replacePropPlaceholders = replacePropPlaceholders; exports.getDefaultTemplate = getDefaultTemplate; exports.parseReactDoc = parseReactDoc; const typescript_1 = __importDefault(require("typescript")); const project_1 = require("../connect/project"); const label_language_mapping_1 = require("../connect/label_language_mapping"); const logging_1 = require("../common/logging"); const compiler_1 = require("../typescript/compiler"); const helpers_1 = require("../connect/helpers"); const intrinsics_1 = require("../connect/intrinsics"); const parser_template_helpers_1 = require("./parser_template_helpers"); const parser_common_1 = require("../connect/parser_common"); /** * Traverses the AST and returns the first JSX element it finds * @param node AST node * @returns */ function findJSXElement(node) { if (typescript_1.default.isJsxElement(node) || typescript_1.default.isJsxFragment(node) || typescript_1.default.isJsxSelfClosingElement(node)) { return node; } else { return typescript_1.default.forEachChild(node, findJSXElement); } } function findBlock(node) { if (typescript_1.default.isBlock(node)) { return node; } else { return typescript_1.default.forEachChild(node, findBlock); } } /** * Walks up the AST from an assignment to find the import declaration */ function findParentImportDeclaration(declaration) { let current = declaration; while (current) { if (typescript_1.default.isImportDeclaration(current)) { return current; } current = current.parent; } } function getImportsOfModule(sourceFile) { const imports = []; function visit(node) { if (typescript_1.default.isImportDeclaration(node)) { imports.push(node); } typescript_1.default.forEachChild(node, visit); } visit(sourceFile); return imports; } function resolveModuleSpecifier(program, sourceFile, importSpecifier) { const compilerHost = typescript_1.default.createCompilerHost(program.getCompilerOptions()); const moduleResolutionHost = { fileExists: compilerHost.fileExists, readFile: compilerHost.readFile, directoryExists: compilerHost.directoryExists, getCurrentDirectory: compilerHost.getCurrentDirectory, getDirectories: compilerHost.getDirectories, }; const resolvedModule = typescript_1.default.resolveModuleName(importSpecifier, sourceFile.fileName, program.getCompilerOptions(), moduleResolutionHost); return resolvedModule.resolvedModule?.resolvedFileName; } // Traverse the source file and resolve imports function findAndResolveImports(program, sourceFile) { const importSpecifierToFilePath = {}; typescript_1.default.forEachChild(sourceFile, (node) => { if (typescript_1.default.isImportDeclaration(node)) { const importSpecifier = node.moduleSpecifier.text; const resolvedFileName = resolveModuleSpecifier(program, sourceFile, importSpecifier); if (resolvedFileName) { importSpecifierToFilePath[importSpecifier] = resolvedFileName; } } }); return importSpecifierToFilePath; } /** * Finds all import statements in a file that matches the given identifiers * * @param parserContext Parser context * @param identifiers List of identifiers to find imports for * @returns */ function getSourceFilesOfImportedIdentifiers(parserContext, _identifiers) { const { sourceFile, resolvedImports } = parserContext; const importDeclarations = getImportsOfModule(sourceFile); const imports = []; const identifiers = _identifiers.map((identifier) => identifier.split('.')[0]); for (const declaration of importDeclarations) { let statement = declaration.getText(); // remove the quotation marks around the module specifier const moduleSpecifier = declaration.moduleSpecifier.getText().slice(1, -1); if (declaration.importClause) { // Default imports if (declaration.importClause.name) { const identifier = declaration.importClause.name.text; if (identifiers.includes(identifier)) { imports.push({ statement, file: resolvedImports[moduleSpecifier], moduleSpecifier, }); } } if (declaration.importClause.namedBindings) { const namedBindings = declaration.importClause.namedBindings; if (typescript_1.default.isNamedImports(namedBindings)) { // Named imports (import { x, y } from 'module') // filter out any unused imports from the statement the identifier belongs to const elements = namedBindings.elements .map((specifier) => specifier.name.text) .filter((name) => identifiers.includes(name)); if (elements.length > 0) { imports.push({ statement: statement.replace(/{.*}/s, `{ ${elements.join(', ')} }`), file: resolvedImports[moduleSpecifier], moduleSpecifier, }); } } else if (typescript_1.default.isNamespaceImport(namedBindings)) { // Namespace import (import * as name from 'module') const identifier = namedBindings.name.text; if (identifiers.includes(identifier)) { imports.push({ statement, file: resolvedImports[moduleSpecifier], moduleSpecifier, }); } } } } } return imports; } /** * Extract metadata about the referenced React component. Used by both the * Code Connect and Storybook commands. * * @param parserContext Parser context * @param componentSymbol The ts.Symbol from the metadata referencing the * component being documented * @param node The node being parsed. Used for error logging. * @returns Metadata object */ async function parseComponentMetadata(node, parserContext, silent) { const { checker, sourceFile } = parserContext; let componentSymbol = checker.getSymbolAtLocation(node); let componentSourceFile = sourceFile; let component = ''; let componentDeclaration; // Hacky fix for namespaced components, this probably doesn't work for storybook if (typescript_1.default.isPropertyAccessExpression(node)) { componentSymbol = checker.getSymbolAtLocation(node.expression); if (!componentSymbol) { throw new parser_common_1.ParserError(`Could not find symbol for component ${node.expression.getText()}`, { sourceFile, node, }); } } // Component declared in a different file if (componentSymbol && componentSymbol.declarations && (typescript_1.default.isImportSpecifier(componentSymbol.declarations[0]) || typescript_1.default.isImportClause(componentSymbol.declarations[0]))) { let importDeclaration = findParentImportDeclaration(componentSymbol.declarations[0]); if (!importDeclaration) { throw new parser_common_1.ParserError('No import statement found for component, make sure the component is imported', { sourceFile, node, }); } // The component should be imported from another file, we need to follow the // aliased symbol to get the correct function definition if (componentSymbol.flags & typescript_1.default.SymbolFlags.Alias) { componentSymbol = checker.getAliasedSymbol(componentSymbol); } if (!componentSymbol || !componentSymbol.declarations) { if (!silent) { 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.`); } return { source: '', line: 0, component: node.getText(), }; } // If we haven't found the component declaration by now, it's likely because it's // assigned to an object/namespace, for example: `export const Button = { Primary: () =>