UNPKG

@figma/code-connect

Version:

A tool for connecting your design system components in code with your design system in Figma

131 lines 5.71 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.extractSignature = extractSignature; exports.extractSignatureFromProject = extractSignatureFromProject; const path_1 = __importDefault(require("path")); const ts_morph_1 = require("ts-morph"); const tsconfig_1 = require("../../common/tsconfig"); const DEFAULT_COMPONENT = 'DefaultComponent'; const REACT_INTERFACE_NAMES = ['HTMLAttributes', 'Attributes', 'AriaAttributes', 'DOMAttributes']; const IGNORE_REACT_PROPS = ['ref', 'key']; /** * Initializing a ts-morph project can take time so lets cache it. * We are assuming the underlying project does not change during the course of the CLI run. */ let cachedTsMorphProject; function extractSignature({ nameToFind, sourceFilePath, }) { if (!cachedTsMorphProject) { const tsConfigFilePath = (0, tsconfig_1.findTsConfigPath)(path_1.default.dirname(sourceFilePath)); const options = tsConfigFilePath ? { tsConfigFilePath } : { compilerOptions: { target: ts_morph_1.ScriptTarget.ESNext, module: ts_morph_1.ts.ModuleKind.CommonJS, jsx: ts_morph_1.ts.JsxEmit.React, esModuleInterop: true, skipLibCheck: true, lib: ['ES2021'], strict: true, rootDir: 'src', }, }; cachedTsMorphProject = new ts_morph_1.Project(options); } return extractSignatureFromProject({ tsMorphProject: cachedTsMorphProject, sourceFilePath, nameToFind, }); } function extractSignatureFromProject({ tsMorphProject, sourceFilePath, nameToFind, }) { tsMorphProject.addSourceFileAtPath(sourceFilePath); const signatureSourcePath = path_1.default.join(path_1.default.dirname(sourceFilePath), 'extracted_signature.ts'); const signatureSourceFile = tsMorphProject.createSourceFile(signatureSourcePath); const filename = path_1.default.parse(sourceFilePath).name.split('.')[0]; if (nameToFind === 'default') { signatureSourceFile.addImportDeclaration({ defaultImport: DEFAULT_COMPONENT, moduleSpecifier: `./${filename}`, }); } else { signatureSourceFile.addImportDeclaration({ namedImports: [nameToFind], moduleSpecifier: `./${filename}`, }); } /** * Flatten the component's props type by creating a virtual TS source file * and adding this type alias, which has the effect of flattening any inherited * types down to a single type, then extract the final type from this new type */ signatureSourceFile.addTypeAlias({ name: 'ExtractFinalType', typeParameters: ['T'], type: 'T extends infer R ? { [K in keyof R]: R[K] } : never', }); const typeAlias = signatureSourceFile.addTypeAlias({ name: '__FinalType', type: `ExtractFinalType<React.ComponentProps<typeof ${nameToFind === 'default' ? DEFAULT_COMPONENT : nameToFind}>>`, }); let type = typeAlias.getType(); // For simplicity, if top-level type is a union, use the first type if (type.isUnion()) { type = type.getUnionTypes()[0]; } if (!type.isObject()) { tsMorphProject.removeSourceFile(signatureSourceFile); throw new Error('Props not an object: ' + type.getText()); } else { const props = type.getProperties(); const result = {}; for (const prop of props) { const propString = getPropString(tsMorphProject, prop, signatureSourceFile); if (propString) { result[prop.getName()] = propString; } } tsMorphProject.removeSourceFile(signatureSourceFile); return result; } } function getPropString(tsMorphProject, prop, sourceFile) { if (IGNORE_REACT_PROPS.includes(prop.getName())) { return null; } const [declaration] = prop.getDeclarations(); const parent = declaration.getParentOrThrow().compilerNode; if (ts_morph_1.ts.isInterfaceDeclaration(parent)) { const parentInterfaceName = parent.name.getText(); // Skip props that are inherited from React types if (REACT_INTERFACE_NAMES.includes(parentInterfaceName)) { return null; } if (parent.heritageClauses && parent.heritageClauses[0].getText().includes('HTMLAttributes')) { // If interface extends HTMLAttributes, only allow props defined in interface // TODO will fail if extends multiple interfaces and prop defined in one of them - could recursively check if (!parent.members.some((m) => m.name?.getText() === prop.getName())) { return null; } } } const compilerProp = prop.compilerSymbol; const checker = tsMorphProject.getTypeChecker().compilerObject; const propType = prop.getTypeAtLocation(sourceFile).compilerType; let propTypeString = checker.typeToString(propType); if (propType.isUnion()) { // Get the types of the union const unionTypes = propType.types; // Map each type to its string representation and join them with a | propTypeString = unionTypes.map((type) => checker.typeToString(type)).join(' | '); } return compilerProp.flags & ts_morph_1.ts.SymbolFlags.Optional ? `?${propTypeString.replace(/undefined \| /g, '')}` : propTypeString; } //# sourceMappingURL=signature_extraction.js.map