UNPKG

@terrazzo/cli

Version:

CLI for managing design tokens using the Design Tokens Community Group (DTCG) standard and generating code for any platform via plugins.

190 lines 8.43 kB
import fs from 'node:fs'; import path from 'node:path'; import { pathToFileURL } from 'node:url'; import { defineConfig } from '@terrazzo/parser'; import pc from 'picocolors'; export const cwd = new URL(`${pathToFileURL(process.cwd())}/`); // trailing slash needed to interpret as directory export const DEFAULT_CONFIG_PATH = new URL('./terrazzo.config.mjs', cwd); export const DEFAULT_TOKENS_PATH = new URL('./tokens.json', cwd); export const GREEN_CHECK = pc.green('✔'); /** Load config */ export async function loadConfig({ cmd, flags, logger }) { try { let config = { tokens: [DEFAULT_TOKENS_PATH], outDir: new URL('./tokens/', cwd), plugins: [], lint: { build: { enabled: true }, rules: {} }, ignore: { tokens: [], deprecated: false }, }; let configPath; if (typeof flags.config === 'string') { if (flags.config === '') { logger.error({ group: 'config', message: 'Missing path after --config flag' }); process.exit(1); } configPath = resolveConfig(flags.config); } const resolvedConfigPath = resolveConfig(configPath); if (resolvedConfigPath) { try { const mod = await import(resolvedConfigPath); if (!mod.default) { // we format it immediately below throw new Error(`No default export found in ${path.relative(cwd.href, resolvedConfigPath)}. See https://terrazzo.dev/docs/cli for instructions.`); } config = defineConfig(mod.default, { cwd, logger }); } catch (err) { logger.error({ group: 'config', message: err.message || err }); } } else if (cmd !== 'init' && cmd !== 'check') { logger.error({ group: 'config', message: 'No config file found. Create one with `npx terrazzo init`.' }); } return { config, configPath: resolvedConfigPath }; } catch (err) { printError(err.message); process.exit(1); } } /** load tokens */ export async function loadTokens(tokenPaths, { logger }) { try { const allTokens = []; if (!Array.isArray(tokenPaths)) { logger.error({ group: 'config', message: `loadTokens: Expected array, received ${typeof tokenPaths}` }); } // if this is the default value, also check for tokens.yaml if (tokenPaths.length === 1 && tokenPaths[0].href === DEFAULT_TOKENS_PATH.href) { if (!fs.existsSync(tokenPaths[0])) { const yamlPath = new URL('./tokens.yaml', cwd); if (fs.existsSync(yamlPath)) { tokenPaths[0] = yamlPath; } else { logger.error({ group: 'config', message: `Could not locate ${path.relative(cwd.href, tokenPaths[0].href)}. To create one, run \`npx tz init\`.`, }); return; } } } // download/read for (let i = 0; i < tokenPaths.length; i++) { const filename = tokenPaths[i]; if (!(filename instanceof URL)) { logger.error({ group: 'config', message: `Expected URL, received ${filename}`, label: `loadTokens[${i}]` }); return; } else if (filename.protocol === 'http:' || filename.protocol === 'https:') { try { // if Figma URL if (filename.host === 'figma.com' || filename.host === 'www.figma.com') { const [_, fileKeyword, fileKey] = filename.pathname.split('/'); if (fileKeyword !== 'file' || !fileKey) { logger.error({ group: 'config', message: `Unexpected Figma URL. Expected "https://www.figma.com/file/:file_key/:file_name?…", received "${filename.href}"`, }); } const headers = new Headers({ Accept: '*/*', 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:123.0) Gecko/20100101 Firefox/123.0', }); if (process.env.FIGMA_ACCESS_TOKEN) { headers.set('X-FIGMA-TOKEN', process.env.FIGMA_ACCESS_TOKEN); } else { logger.warn({ group: 'config', message: 'FIGMA_ACCESS_TOKEN not set' }); } const res = await fetch(`https://api.figma.com/v1/files/${fileKey}/variables/local`, { method: 'GET', headers, }); if (res.ok) { allTokens.push({ filename, src: await res.text() }); } const message = res.status !== 404 ? JSON.stringify(await res.json(), undefined, 2) : ''; logger.error({ group: 'config', message: `Figma responded with ${res.status}${message ? `:\n${message}` : ''}`, }); break; } // otherwise, expect YAML/JSON const res = await fetch(filename, { method: 'GET', headers: { Accept: '*/*', 'User-Agent': 'Mozilla/5.0 Gecko/20100101 Firefox/123.0' }, }); allTokens.push({ filename, src: await res.text() }); } catch (err) { logger.error({ group: 'config', message: `${filename.href}: ${err}` }); return; } } else { if (fs.existsSync(filename)) { allTokens.push({ filename, src: fs.readFileSync(filename, 'utf8') }); } else { logger.error({ group: 'config', message: `Could not locate ${path.relative(cwd.href, filename.href)}. To create one, run \`npx tz init\`.`, }); return; } } } return allTokens; } catch (err) { printError(err.message); process.exit(1); } } /** Print error */ export function printError(message) { console.error(pc.red(`✗ ${message}`)); } /** Print success */ export function printSuccess(message, startTime) { console.log(`${GREEN_CHECK} ${message}${startTime ? ` ${time(startTime)}` : ''}`); } /** Resolve config */ export function resolveConfig(filename) { // --config [configpath] if (filename) { const configPath = new URL(filename, cwd); if (fs.existsSync(configPath)) { return configPath.href; // ⚠️ ESM wants "file://..." URLs on Windows & Unix. } return undefined; } // note: the order isn’t significant; just try for most-common first. // if a user has multiple files differing only by file extension, behavior is // unpredictable and that’s on them. return ['.js', '.mjs', '.cjs'] .map((ext) => new URL(`./terrazzo.config${ext}`, cwd)) .find((configPath) => fs.existsSync(configPath))?.href; // ⚠️ ESM wants "file://..." URLs on Windows & Unix.; } /** Resolve tokens.json path (for lint command) */ export function resolveTokenPath(filename, { logger }) { const tokensPath = new URL(filename, cwd); if (!fs.existsSync(tokensPath)) { logger.error({ group: 'config', message: `Could not locate ${filename}. Does the file exist?` }); } else if (!fs.statSync(tokensPath).isFile()) { logger.error({ group: 'config', message: `Expected JSON or YAML file, received ${filename}.` }); } return tokensPath; } /** Print time elapsed */ export function time(start) { const diff = performance.now() - start; return pc.dim(diff < 750 ? `${Math.round(diff)}ms` : `${(diff / 1000).toFixed(1)}s`); } //# sourceMappingURL=shared.js.map