@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
JavaScript
;
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