UNPKG

@terrazzo/parser

Version:

Parser/validator for the Design Tokens Community Group (DTCG) standard.

201 lines (187 loc) 6.43 kB
import type { DocumentNode } from '@humanwhocodes/momoa'; import { type TokenNormalized, isTokenMatch } from '@terrazzo/token-tools'; import wcmatch from 'wildcard-match'; import Logger, { type LogEntry } from '../logger.js'; import type { BuildRunnerResult, ConfigInit, TokenTransformed, TransformParams } from '../types.js'; export interface BuildRunnerOptions { sources: { filename?: URL; src: string; document: DocumentNode }[]; config: ConfigInit; logger?: Logger; } export const SINGLE_VALUE = 'SINGLE_VALUE'; export const MULTI_VALUE = 'MULTI_VALUE'; /** Validate plugin setTransform() calls for immediate feedback */ function validateTransformParams({ params, logger, pluginName, }: { params: TokenTransformed; logger: Logger; pluginName: string }) { const baseMessage: LogEntry = { group: 'plugin', label: pluginName, message: '' }; // validate value is valid for SINGLE_VALUE or MULTI_VALUE if ( !params.value || (typeof params.value !== 'string' && typeof params.value !== 'object') || Array.isArray(params.value) ) { logger.error({ ...baseMessage, message: `setTransform() value expected string or object of strings, received ${ Array.isArray(params.value) ? 'Array' : typeof params.value }`, }); } if (typeof params.value === 'object' && Object.values(params.value).some((v) => typeof v !== 'string')) { logger.error({ ...baseMessage, message: 'setTransform() value expected object of strings, received some non-string values', }); } } /** * Run build stage */ export default async function build( tokens: Record<string, TokenNormalized>, { sources, logger = new Logger(), config }: BuildRunnerOptions, ) { const formats: Record<string, TokenTransformed[]> = {}; const result: BuildRunnerResult = { outputFiles: [] }; function getTransforms(params: TransformParams) { return (formats[params.format] ?? []).filter((token) => { if (params.$type) { if (typeof params.$type === 'string' && token.token.$type !== params.$type) { return false; } else if (Array.isArray(params.$type) && !params.$type.some(($type) => token.token.$type === $type)) { return false; } } if ( params.id && params.id !== '*' && !isTokenMatch(token.token.id, Array.isArray(params.id) ? params.id : [params.id]) ) { return false; } if (params.mode && !wcmatch(params.mode)(token.mode)) { return false; } return true; }); } // transform() let transformsLocked = false; // prevent plugins from transforming after stage has ended const startTransform = performance.now(); for (const plugin of config.plugins) { if (typeof plugin.transform === 'function') { await plugin.transform({ tokens, sources, getTransforms, setTransform(id, params) { if (transformsLocked) { logger.warn({ message: 'Attempted to call setTransform() after transform step has completed.', group: 'plugin', label: plugin.name, }); return; } const token = tokens[id]!; // allow `undefined` values, but remove them here const cleanValue: TokenTransformed['value'] = typeof params.value === 'string' ? params.value : { ...(params.value as Record<string, string>) }; if (typeof cleanValue === 'object') { for (const k of Object.keys(cleanValue)) { if (cleanValue[k] === undefined) { delete cleanValue[k]; } } } validateTransformParams({ logger, params: { ...(params as any), value: cleanValue }, pluginName: plugin.name, }); // upsert if (!formats[params.format]) { formats[params.format] = []; } const foundTokenI = formats[params.format]!.findIndex( (t) => id === t.id && (!params.localID || params.localID === t.localID) && (!params.mode || params.mode === t.mode), ); if (foundTokenI === -1) { formats[params.format]!.push({ ...params, id, value: cleanValue, type: typeof cleanValue === 'string' ? SINGLE_VALUE : MULTI_VALUE, mode: params.mode || '.', token: structuredClone(token), } as TokenTransformed); } else { formats[params.format]![foundTokenI]!.value = cleanValue; formats[params.format]![foundTokenI]!.type = typeof cleanValue === 'string' ? SINGLE_VALUE : MULTI_VALUE; } }, }); } } transformsLocked = true; logger.debug({ group: 'parser', label: 'transform', message: 'transform() step', timing: performance.now() - startTransform, }); // build() const startBuild = performance.now(); for (const plugin of config.plugins) { if (typeof plugin.build === 'function') { const pluginBuildStart = performance.now(); await plugin.build({ tokens, sources, getTransforms, outputFile(filename, contents) { const resolved = new URL(filename, config.outDir); if (result.outputFiles.some((f) => new URL(f.filename, config.outDir).href === resolved.href)) { logger.error({ group: 'plugin', message: `Can’t overwrite file "${filename}"`, label: plugin.name, }); } result.outputFiles.push({ filename, contents, plugin: plugin.name, time: performance.now() - pluginBuildStart, }); }, }); } } logger.debug({ group: 'parser', label: 'build', message: 'build() step', timing: performance.now() - startBuild, }); // buildEnd() const startBuildEnd = performance.now(); for (const plugin of config.plugins) { if (typeof plugin.buildEnd === 'function') { await plugin.buildEnd({ outputFiles: structuredClone(result.outputFiles) }); } } logger.debug({ group: 'parser', label: 'build', message: 'buildEnd() step', timing: performance.now() - startBuildEnd, }); return result; }