@figma/code-connect
Version:
A tool for connecting your design system components in code with your design system in Figma
149 lines • 6.68 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.isRawTemplate = isRawTemplate;
exports.parseRawFile = parseRawFile;
const fs_1 = __importDefault(require("fs"));
const typescript_1 = __importDefault(require("typescript"));
const label_language_mapping_1 = require("./label_language_mapping");
const helpers_1 = require("./helpers");
/**
* Returns true if the file content looks like a raw template file, i.e. its
* leading comment block contains a `// url=`, `// component=`, or `// source=`
* directive. Used to distinguish raw `.figma.ts` templates from React/HTML
* Code Connect files that share the same extension.
*/
function isRawTemplate(content) {
for (const line of content.split('\n')) {
const trimmed = line.trim();
if (trimmed === '' || trimmed.startsWith('//')) {
if (/^\/\/\s*(url|component|source)=/.test(trimmed)) {
return true;
}
continue;
}
// First non-comment, non-blank line reached without finding a known directive — not a raw template
break;
}
return false;
}
function transpileTypeScriptTemplate(filePath, fileContent) {
// Convert ESM import of 'figma' to require syntax
// Supports: import figma from 'figma'
const figmaImportRegex = /^import\s+figma\s+from\s+['"]figma['"]\s*;?\s*$/m;
if (figmaImportRegex.test(fileContent)) {
fileContent = fileContent.replace(figmaImportRegex, "const figma = require('figma')");
}
// Detect any remaining non-type import statements. Type-only imports (`import type`)
// are erased by the TS compiler and are fine. Regular imports (except the figma
// import we just converted) are not supported in Phase 1 (no bundling), so we show
// a helpful error.
const importRegex = /^import\s+(?!type\s)/m;
if (importRegex.test(fileContent)) {
const match = fileContent.match(/^(import\s+.+)$/m);
const importLine = match ? match[1] : 'import ...';
throw new Error(`TypeScript template files only support importing from 'figma'.\n` +
`Found in ${filePath}:\n` +
` ${importLine}\n\n` +
`Use "const figma = require('figma')" or "import figma from 'figma'" to access the Figma API.\n` +
`Other module imports will be supported in a future version.`);
}
const result = typescript_1.default.transpileModule(fileContent, {
compilerOptions: {
module: typescript_1.default.ModuleKind.ESNext,
target: typescript_1.default.ScriptTarget.ES2021,
removeComments: false,
},
});
return result.outputText;
}
/**
* Extracts metadata fields (url, component, source) from the leading comments
* of a raw template file. This must be done before transpilation because
* TypeScript can remove comments that appear before type-only imports.
*/
function extractMetadataFields(fileContent) {
const lines = fileContent.split('\n');
const fields = {};
let templateStartLine = 0;
// Parse consecutive comment lines at the start of the file
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
// Match pattern: // fieldName=value
const match = line.match(/^\/\/\s*(\w+)=(.+)$/);
if (match) {
const [, fieldName, fieldValue] = match;
const normalizedFieldName = fieldName.toLowerCase();
if (normalizedFieldName === 'url' ||
normalizedFieldName === 'component' ||
normalizedFieldName === 'source') {
fields[normalizedFieldName] = fieldValue.trim();
}
templateStartLine = i + 1;
}
else if (line === '' || line.startsWith('//')) {
// Allow blank lines or other comments, but don't increment template start
continue;
}
else {
// First non-comment line found
break;
}
}
return { fields, templateStartLine };
}
function parseRawFile(filePath, label, config, dir) {
let fileContent = fs_1.default.readFileSync(filePath, 'utf-8');
// Extract metadata fields BEFORE transpilation to avoid losing comments
// that appear before type-only imports (which TypeScript erases)
const { fields, templateStartLine } = extractMetadataFields(fileContent);
if (filePath.endsWith('.ts')) {
fileContent = transpileTypeScriptTemplate(filePath, fileContent);
}
if (!fields.url) {
throw new Error(`Missing required url field in ${filePath}. Please add a // url=... comment to the top of the file.`);
}
let figmaNodeUrl = fields.url;
// Extract template from the transpiled content, starting at the line
// where the metadata comments ended in the original source
const transpiledLines = fileContent.split('\n');
let templateStartIndex = 0;
// Skip lines until we've passed the original metadata comment section
for (let i = 0; i < transpiledLines.length; i++) {
const line = transpiledLines[i].trim();
// Skip blank lines and comments at the start
if (line === '' || line.startsWith('//')) {
continue;
}
// First non-comment line in transpiled output
templateStartIndex = i;
break;
}
const template = transpiledLines.slice(templateStartIndex).join('\n');
// Apply documentUrlSubstitutions if provided
if (config?.documentUrlSubstitutions) {
figmaNodeUrl = (0, helpers_1.applyDocumentUrlSubstitutions)(figmaNodeUrl, config.documentUrlSubstitutions);
}
// Determine effective label from parameter, config, or default
const effectiveLabel = label || config?.label || label_language_mapping_1.CodeConnectLabel.Code;
const language = config?.language || (0, label_language_mapping_1.getInferredLanguageForRaw)(effectiveLabel);
return {
figmaNode: figmaNodeUrl,
component: fields.component,
template,
// nestable by default unless user specifies in template
// (templateData.nestable AND template.metadata.nestable need to
// be true for instance to be nested)
templateData: { nestable: true, isParserless: true },
language,
label: effectiveLabel,
source: fields.source || '',
sourceLocation: { line: -1 },
metadata: {
cliVersion: require('../../package.json').version,
},
};
}
//# sourceMappingURL=raw_templates.js.map