UNPKG

@terrazzo/parser

Version:

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

286 lines 12.3 kB
import { CachedWildcardMatcher } from '@terrazzo/token-tools'; import Logger, {} from '../logger.js'; export const SINGLE_VALUE = 'SINGLE_VALUE'; export const MULTI_VALUE = 'MULTI_VALUE'; /** Validate plugin setTransform() calls for immediate feedback */ function validateTransformParams({ params, logger, pluginName, }) { const baseMessage = { 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', }); } } const FALLBACK_PERMUTATION_ID = JSON.stringify({ tzMode: '*' }); // Important: getTransforms() will likely be called hundreds of times, and creating wcmatch instances is expensive! // Use this cached matcher to significantly speed up matching const cachedMatcher = new CachedWildcardMatcher(); /** Run build stage */ export default async function build(tokens, { resolver, sources, logger = new Logger(), config }) { const formats = {}; const result = { outputFiles: [] }; function createGetTransforms(plugin) { return function getTransforms(params) { if (!params?.format) { logger.warn({ group: 'plugin', label: plugin, message: '"format" missing from getTransforms(), no tokens returned.', }); return []; } const isLegacyModes = params.input && Object.keys(params.input).length === 1 && 'tzMode' in params.input; const permutationID = params.input && !isLegacyModes ? resolver.getPermutationID(params.input) : FALLBACK_PERMUTATION_ID; const mode = params.mode || (isLegacyModes && params.input.tzMode) || undefined; // Optimization: don’t create wildcard matcher if single token IDs are requested—it’s slow and pointless const singleTokenID = (typeof params.id === 'string' && tokens[params.id]?.id) || (Array.isArray(params.id) && params.id.length === 1 && tokens[params.id[0]]?.id) || undefined; const $type = (typeof params.$type === 'string' && [params.$type]) || (Array.isArray(params.$type) && params.$type) || undefined; const idMatcher = params.id && !singleTokenID && !isFullWildcard(params.id) ? cachedMatcher.tokenIDMatch(params.id) : null; const modeMatcher = mode && mode !== '.' && !isFullWildcard(mode) ? cachedMatcher.match(mode) : null; return (formats[params.format]?.[permutationID] ?? []).filter((token) => { if ((singleTokenID && token.id !== singleTokenID) || (idMatcher && !idMatcher(token.id))) { return false; } if (params.$type && !$type?.some((value) => token.token.$type === value)) { return false; } if ((mode === '.' && token.mode !== '.') || (modeMatcher && !modeMatcher(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') { const pt = performance.now(); await plugin.transform({ context: { logger }, tokens, sources, getTransforms: createGetTransforms(plugin.name), // oxlint-disable-next-line no-loop-func 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]; if (!token) { logger.error({ group: 'plugin', label: plugin.name, message: `No token "${id}"` }); } const isLegacyModes = params.input && Object.keys(params.input).length === 1 && 'tzMode' in params.input; const permutationID = params.input && !isLegacyModes ? resolver.getPermutationID(params.input) : FALLBACK_PERMUTATION_ID; const mode = params.mode || (isLegacyModes && params.input.tzMode) || undefined; const cleanValue = typeof params.value === 'string' ? params.value : { ...params.value }; validateTransformParams({ logger, params: { ...params, value: cleanValue }, pluginName: plugin.name, }); // upsert if (!formats[params.format]) { formats[params.format] = { [FALLBACK_PERMUTATION_ID]: [] }; } if (!formats[params.format][permutationID]) { formats[params.format][permutationID] = []; } const foundTokenI = formats[params.format][permutationID].findIndex((t) => id === t.id && (!params.localID || params.localID === t.localID) && (!mode || t.mode === mode)); if (foundTokenI === -1) { const transformedToken = { ...params, id, value: cleanValue, type: typeof cleanValue === 'string' ? SINGLE_VALUE : MULTI_VALUE, mode: mode || '.', token: makeReadOnlyToken(token), permutationID, input: JSON.parse(permutationID), }; // backwards compat: upconvert mode into "tzMode" modifier. This // allows newer plugins to use resolver syntax without disrupting // older plugins. formats[params.format][permutationID].push(transformedToken); // If this is a “default” permutation, this should also be duplicated in the global space if (params.input && Object.keys(params.input).length === 0 && permutationID !== FALLBACK_PERMUTATION_ID) { formats[params.format][FALLBACK_PERMUTATION_ID].push(transformedToken); } } else { formats[params.format][permutationID][foundTokenI].value = cleanValue; formats[params.format][permutationID][foundTokenI].type = typeof cleanValue === 'string' ? SINGLE_VALUE : MULTI_VALUE; } }, resolver, }); logger.debug({ group: 'plugin', label: plugin.name, message: 'transform()', timing: performance.now() - pt, }); } } transformsLocked = true; logger.debug({ group: 'parser', label: 'transform', message: 'All plugins finished transform()', timing: performance.now() - startTransform, }); // build() const startBuild = performance.now(); await Promise.all(config.plugins.map(async (plugin) => { if (typeof plugin.build === 'function') { const pb = performance.now(); await plugin.build({ context: { logger }, tokens, sources, getTransforms: createGetTransforms(plugin.name), resolver, 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() - pb, }); }, }); logger.debug({ group: 'plugin', label: plugin.name, message: 'build()', timing: performance.now() - pb, }); } })); logger.debug({ group: 'parser', label: 'build', message: 'All plugins finished build()', timing: performance.now() - startBuild, }); cachedMatcher.reset(); // Garbage collect wildcard matches accumulated in the build process. This is needed in watch mode especially and long sessions. // buildEnd() const startBuildEnd = performance.now(); await Promise.all(config.plugins.map(async (plugin) => await plugin.buildEnd?.({ context: { logger }, tokens, getTransforms: createGetTransforms(plugin.name), sources, outputFiles: structuredClone(result.outputFiles), }))); logger.debug({ group: 'parser', label: 'build', message: 'buildEnd() step', timing: performance.now() - startBuildEnd, }); return result; } function isFullWildcard(value) { return ((typeof value === 'string' && (value === '*' || value === '**')) || (Array.isArray(value) && value.some((v) => v === '*' || v === '**'))); } /** Generate getters for transformed tokens. Reduces memory usage while improving accuracy. Provides some safety for read-only values. */ function makeReadOnlyToken(token) { return { get id() { return token.id; }, get $value() { return token.$value; }, get $type() { return token.$type; }, get $description() { return token.$description; }, get $deprecated() { return token.$deprecated; }, get $extends() { return token.$extends; }, get $extensions() { return token.$extensions; }, get mode() { return token.mode; }, get originalValue() { return token.originalValue; }, get aliasChain() { return token.aliasChain; }, get aliasOf() { return token.aliasOf; }, get partialAliasOf() { return token.partialAliasOf; }, get aliasedBy() { return token.aliasedBy; }, get group() { return token.group; }, get source() { return token.source; }, get jsonID() { return token.jsonID; }, get dependencies() { return token.dependencies; }, }; } //# sourceMappingURL=index.js.map