UNPKG

@terrazzo/parser

Version:

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

117 lines 5.57 kB
import { pluralize } from '@terrazzo/token-tools'; import { merge } from 'merge-anything'; import { cachedLintMatcher } from './plugin-core/lib/matchers.js'; export { RECOMMENDED_CONFIG } from './plugin-core/index.js'; export default async function lintRunner({ tokens, filename, config = {}, sources, logger, }) { const { plugins = [], lint } = config; const sourceByFilename = {}; for (const source of sources) { sourceByFilename[source.filename.href] = source; } const unusedLintRules = Object.keys(lint?.rules ?? {}); const errors = []; const warnings = []; for (const plugin of plugins) { if (typeof plugin.lint === 'function') { const s = performance.now(); const linter = plugin.lint(); // oxlint-disable-next-line no-await-in-loop await Promise.all(Object.entries(linter).map(async ([id, rule]) => { if (!(id in lint.rules) || lint.rules[id] === null) { return; } // tick off used rule const unusedLintRuleI = unusedLintRules.indexOf(id); if (unusedLintRuleI !== -1) { unusedLintRules.splice(unusedLintRuleI, 1); } const [severity, options] = lint.rules[id]; if (severity === 'off') { return; } // note: this usually isn’t a Promise, but it _might_ be! await rule.create({ id, report(descriptor) { let message = ''; if (!descriptor.message && !descriptor.messageId) { logger.error({ group: 'lint', label: `${plugin.name}:${id}`, message: 'Unable to report error: missing message or messageId', }); } // handle message or messageId if (descriptor.message) { message = descriptor.message; } else { if (!(descriptor.messageId in (rule.meta?.messages ?? {}))) { logger.error({ group: 'lint', label: `${plugin.name}:${id}`, message: `messageId "${descriptor.messageId}" does not exist`, }); } message = rule.meta?.messages?.[descriptor.messageId] ?? ''; } // replace with descriptor.data (if any) if (descriptor.data && typeof descriptor.data === 'object') { for (const [k, v] of Object.entries(descriptor.data)) { // lazy formatting const formatted = ['string', 'number', 'boolean'].includes(typeof v) ? String(v) : JSON.stringify(v); message = message.replaceAll(/{{[^}]+}}/g, (inner) => { const key = inner.slice(2, -2).trim(); return key === k ? formatted : inner; }); } } (severity === 'error' ? errors : warnings).push({ group: 'lint', label: id, message, filename, node: descriptor.node, src: sourceByFilename[descriptor.filename]?.src, }); }, tokens, filename, sources, options: merge(rule.meta?.defaultOptions ?? [], rule.defaultOptions ?? [], // Note: is this the correct order to merge in? options), }); })); logger.debug({ group: 'lint', label: plugin.name, message: 'Finished', timing: performance.now() - s, }); cachedLintMatcher.reset(); // this is overly-defensive, but after a lint run, clear the state of the wc matcher cache } } const errCount = errors.length > 0 ? `${errors.length} ${pluralize(errors.length, 'error', 'errors')}` : ''; const warnCount = warnings.length > 0 ? `${warnings.length} ${pluralize(warnings.length, 'warning', 'warnings')}` : ''; if (errors.length > 0) { logger.error(...errors, { group: 'lint', label: 'lint', message: [errCount, warnCount].filter(Boolean).join(', '), }); } if (warnings.length > 0) { logger.warn(...warnings, { group: 'lint', label: 'lint', message: warnCount }); } // warn user if they have unused lint rules (they might have meant to configure something!) for (const unusedRule of unusedLintRules) { logger.warn({ group: 'lint', label: 'lint', message: `Unknown lint rule "${unusedRule}"` }); } } //# sourceMappingURL=index.js.map