UNPKG

@figma/code-connect

Version:

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

261 lines 13.8 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.callParser = callParser; exports.handleMessages = handleMessages; const logging_1 = require("../common/logging"); const cross_spawn_1 = require("cross-spawn"); const get_swift_parser_dir_1 = require("../parser_scripts/get_swift_parser_dir"); const fs_1 = __importDefault(require("fs")); const path_1 = __importDefault(require("path")); const get_gradlew_path_1 = require("../parser_scripts/get_gradlew_path"); const compose_errors_1 = require("../parser_scripts/compose_errors"); const temporaryInputFilePath = 'tmp/figma-code-connect-parser-io.json.tmp'; const temporaryOutputDirectoryPath = 'tmp/parser-output'; const FIRST_PARTY_PARSERS = { swift: { command: async (cwd, config) => { return `swift run --package-path ${await (0, get_swift_parser_dir_1.getSwiftParserDir)(cwd, config.xcodeprojPath, config.swiftPackagePath, config.sourcePackagesPath)} figma-swift`; }, }, compose: { command: async (cwd, config, mode) => { const gradlewPath = await (0, get_gradlew_path_1.getGradleWrapperPath)(cwd, config.gradleWrapperPath); const gradleExecutableInvocation = (0, get_gradlew_path_1.getGradleWrapperExecutablePath)(gradlewPath); const verboseFlags = config.verbose ? ' --stacktrace' : ''; if (mode === 'CREATE') { return `${gradleExecutableInvocation} -p ${gradlewPath} createCodeConnect -PfilePath=${temporaryInputFilePath}${verboseFlags} -PoutputDir=${temporaryOutputDirectoryPath}`; } else { return `${gradleExecutableInvocation} -p ${gradlewPath} parseCodeConnect -PfilePath=${temporaryInputFilePath}${verboseFlags} -PoutputDir=${temporaryOutputDirectoryPath}`; } }, temporaryInputFilePath: temporaryInputFilePath, temporaryOutputDirectoryPath: temporaryOutputDirectoryPath, }, custom: { command: async (cwd, config) => { if (!('parserCommand' in config)) { (0, logging_1.exitWithError)('No `parserCommand` specified in config. A command is required when using the `custom` parser.'); } logging_1.logger.info('Using custom parser command: ' + config.parserCommand); return config.parserCommand; }, }, __unit_test__: { command: async () => 'node parser/unit_test_parser.js', }, }; function getParser(config) { const parser = FIRST_PARTY_PARSERS[config.parser]; if (!parser) { (0, logging_1.exitWithError)(`Invalid parser specified: "${config.parser}". Valid parsers are: ${Object.keys(FIRST_PARTY_PARSERS).join(', ')}.`); } return parser; } async function callParser(config, payload, cwd) { return new Promise(async (resolve, reject) => { try { const parser = getParser(config); const configWithVerbose = { ...config, verbose: payload.verbose, }; const command = await parser.command(cwd, configWithVerbose, payload.mode); // Create temporary input file if it exists if (parser.temporaryInputFilePath) { fs_1.default.mkdirSync(path_1.default.dirname(parser.temporaryInputFilePath), { recursive: true }); fs_1.default.writeFileSync(parser.temporaryInputFilePath, JSON.stringify(payload)); } // Create temporary output directory if it exists if (parser.temporaryOutputDirectoryPath) { fs_1.default.mkdirSync(parser.temporaryOutputDirectoryPath, { recursive: true }); } logging_1.logger.debug(`Running parser: ${command}`); const commandSplit = command.split(' '); const child = (0, cross_spawn_1.spawn)(commandSplit[0], commandSplit.slice(1), { cwd, }); let stdout = ''; let stderr = ''; child.stdout.on('data', (data) => { stdout += data.toString(); }); // This handles any stderr output from the parser. // // Parsers should not generally write to stderr, and should instead return // an array of messages at the end of execution, but there are cases where // you might want to log output immediately rather than at the end of the // run - e.g. if the parser can take a while to compile first time, you // might want to inform the user immediately that it is compiling. // // To log output, the parser should write a JSON object to stderr with the // same structure as the `messages` response object, e.g. `{ "level": // "INFO", "message": "Compiling parser..." }`. // // Non-JSON output will be logged as debug messages, as this is likely to // be e.g. compiler output which the user doesn't need to see ordinarily. child.stderr.on('data', (data) => { const message = data.toString(); const trimmedMessage = message.trim(); try { const parsed = JSON.parse(trimmedMessage); handleMessages([parsed]); } catch (e) { stderr += message; logging_1.logger.debug(trimmedMessage); } }); child.on('close', (code) => { if (code !== 0) { const errorSuggestion = determineErrorSuggestionFromStderr(stderr, config.parser); if (errorSuggestion) { reject(new Error(`Parser exited with code ${code}: ${errorSuggestion}`)); } else { reject(new Error(`Parser exited with code ${code}`)); } } else { // List the files in the temporary output directory if it exists and log them if (parser.temporaryOutputDirectoryPath) { try { const outputDir = parser.temporaryOutputDirectoryPath; if (fs_1.default.existsSync(outputDir)) { const files = fs_1.default.readdirSync(outputDir); logging_1.logger.debug(`Files in temporary output directory (${outputDir}): ${files.length > 0 ? files.join(', ') : '[empty]'}`); // Combine all JSON files in the output directory into a single JSON object. // For PARSE responses: Each file has a "docs" array and a "messages" array. // For CREATE responses: Each file has a "createdFiles" array and a "messages" array. // The combined output will have a single array of the appropriate type and a single "messages" array. // Filter for .json files only const jsonFiles = files.filter((f) => f.endsWith('.json')); let allMessages = []; const uniqueDocsMap = new Map(); let allCreatedFiles = []; for (const file of jsonFiles) { const filePath = path_1.default.join(outputDir, file); try { const content = fs_1.default.readFileSync(filePath, 'utf8'); const parsed = JSON.parse(content); if (Array.isArray(parsed.docs)) { // Deduplicate docs based on figmaNode + template // The template contains the actual code example and is unique per component. // This allows multiple different code components to map to the same Figma node, // while preventing the same component from being duplicated in multi-module projects. for (const doc of parsed.docs) { const dedupeKey = JSON.stringify({ figmaNode: doc.figmaNode, template: doc.template, }); if (!uniqueDocsMap.has(dedupeKey)) { uniqueDocsMap.set(dedupeKey, doc); } } } if (Array.isArray(parsed.createdFiles)) { allCreatedFiles = allCreatedFiles.concat(parsed.createdFiles); } if (Array.isArray(parsed.messages)) { allMessages = allMessages.concat(parsed.messages); } } catch (err) { logging_1.logger.warn(`Failed to parse output file ${file}: ${err}`); } } const allDocs = Array.from(uniqueDocsMap.values()); // Return the appropriate response based on the mode if (payload.mode === 'CREATE') { resolve({ createdFiles: allCreatedFiles, messages: allMessages, }); } else { resolve({ docs: allDocs, messages: allMessages, }); } } else { logging_1.logger.debug(`Temporary output directory (${outputDir}) does not exist.`); } } catch (err) { logging_1.logger.warn(`Failed to list files in temporary output directory: ${err}`); } } else { // Assume the output is in the stdout resolve(JSON.parse(stdout)); } } if (parser.temporaryInputFilePath) { // Retain temp file and directory when verbose mode is enabled if (!payload.verbose) { fs_1.default.unlinkSync(parser.temporaryInputFilePath); // Delete parent directory if empty after removing temp file const parentDir = path_1.default.dirname(parser.temporaryInputFilePath); if (fs_1.default.readdirSync(parentDir).length === 0) { fs_1.default.rmdirSync(parentDir); } } } }); child.on('error', (e) => { reject(e); }); if (!parser.temporaryInputFilePath) { child.stdin.write(JSON.stringify(payload)); child.stdin.end(); } } catch (e) { if (payload.verbose) { console.trace(e); // Don't say to enable verbose if the user has already enabled it. (0, logging_1.exitWithError)(`Error calling parser: ${e}.`); } else { (0, logging_1.exitWithError)(`Error calling parser: ${e}. Try re-running the command with --verbose for more information.`); } } }); } function handleMessages(messages) { let hasErrors = false; messages.forEach((message) => { switch (message.level) { case 'DEBUG': logging_1.logger.debug(message.message); break; case 'INFO': logging_1.logger.info(message.message); break; case 'WARN': logging_1.logger.warn(message.message); break; case 'ERROR': logging_1.logger.error(message.message); hasErrors = true; break; } }); return { hasErrors }; } // This function is used to determine if there is a suggestion for the error based on the output // to stderr. Certain parsers return the same error code for different types of errors such as // errors from invoking the gradle wrapper for Compose. // In the future we should consider exposing a different API for having the parser return a suggestion directly. function determineErrorSuggestionFromStderr(stderr, parser) { if (parser === 'compose') { return (0, compose_errors_1.getComposeErrorSuggestion)(stderr); } return null; } //# sourceMappingURL=parser_executables.js.map