Mercurial
diff design_system/test/design_system_policy_test.js @ 254:2b6e732087ff
[ui] Add complete native component catalog
Co-authored-by: Copilot <[email protected]>
| author | MrJuneJune <me@mrjunejune.com> |
|---|---|
| date | Tue, 04 Aug 2026 15:12:09 -0700 |
| parents | |
| children | 60a876c4587a |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/design_system/test/design_system_policy_test.js Tue Aug 04 15:12:09 2026 -0700 @@ -0,0 +1,311 @@ +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); + +const RUNFILES = process.env.JS_BINARY__RUNFILES; +const WORKSPACE = process.env.JS_BINARY__WORKSPACE; +assert.ok(RUNFILES); +assert.ok(WORKSPACE); + +const sourceRoot = path.join(RUNFILES, WORKSPACE, 'design_system/src'); +const files = []; + +function walk(directory) { + for (const entry of fs.readdirSync(directory, { withFileTypes: true })) { + const entryPath = path.join(directory, entry.name); + if (entry.isDirectory()) walk(entryPath); + else if (/\.(?:css|html|js)$/.test(entry.name)) files.push(entryPath); + } +} + +walk(sourceRoot); + +function validateCallableDocumentation( + relative, + source, + index, + name, + parameterSource, + failures, +) { + const prefix = source.slice(0, index); + const comment = prefix.match(/\/\*\*([\s\S]*?)\*\/\s*$/); + const body = comment?.[1] || ''; + const parameters = parameterSource + .split(',') + .map(parameter => parameter.trim() + .replace(/^\.\.\./, '') + .split('=')[0].trim()) + .filter(Boolean); + if (!/@return\s+\{[^}]+\}/.test(body)) { + failures.push(`${relative} exported callable ${name} lacks @return`); + } + const documented = [...body.matchAll( + /@param\s+\{[^}]+\}\s+([A-Za-z0-9_$]+)/g, + )].map(parameter => parameter[1]); + for (const parameter of parameters) { + if (!documented.includes(parameter)) { + failures.push( + `${relative} exported callable ${name} lacks @param for ${parameter}`, + ); + } + } +} + +const rawColor = new RegExp([ + '#[0-9a-fA-F]{3,8}\\b', + '\\b(?:rgb|rgba|hsl|hsla|hwb|lab|lch|oklab|oklch)\\s*\\(', +].join('|'), 'gi'); +const forbiddenGlyph = /[×←→↑↓+◆◇✓✔✕✖★☆⚠]/gu; +const pictographic = /\p{Extended_Pictographic}/gu; +const failures = []; + +for (const file of files) { + const relative = path.relative(sourceRoot, file); + const source = fs.readFileSync(file, 'utf8'); + + if (relative !== 'styles/tokens.css') { + for (const match of source.matchAll(rawColor)) { + failures.push( + `${relative}:${source.slice(0, match.index).split('\n').length} ` + + `raw color ${JSON.stringify(match[0])}`, + ); + } + if (relative.endsWith('.css')) { + const css = source.replace(/\/\*[\s\S]*?\*\//g, ''); + const allowedWords = new Set([ + 'none', + 'transparent', + 'currentcolor', + 'inherit', + 'initial', + 'unset', + 'revert', + 'solid', + 'dashed', + 'dotted', + 'double', + 'groove', + 'ridge', + 'inset', + 'outset', + 'canvas', + 'canvastext', + 'linktext', + 'visitedtext', + 'activetext', + 'buttonface', + 'buttontext', + 'field', + 'fieldtext', + 'highlight', + 'highlighttext', + 'graytext', + 'mark', + 'marktext', + 'selecteditem', + 'selecteditemtext', + 'accentcolor', + 'accentcolortext', + 'color-mix', + 'in', + 'srgb', + 'linear-gradient', + 'radial-gradient', + 'conic-gradient', + 'to', + 'top', + 'right', + 'bottom', + 'left', + 'at', + 'circle', + 'ellipse', + 'px', + 'rem', + 'em', + 'vh', + 'vw', + 'vi', + 'vb', + 'vmin', + 'vmax', + 'deg', + 'rad', + 'turn', + ]); + for (const declaration of css.matchAll( + /(^|[;{])\s*([-\w]+)\s*:\s*([^;{}]+)/gm, + )) { + const property = declaration[2].toLowerCase(); + const value = declaration[3]; + const colorProperty = [ + 'accent-color', + 'background', + 'background-color', + 'box-shadow', + 'caret-color', + 'color', + 'fill', + 'outline', + 'outline-color', + 'scrollbar-color', + 'stroke', + 'text-shadow', + ].includes(property) || + (property.startsWith('border') && + !/(?:collapse|image|radius|spacing|style|width)/.test(property)); + if (!colorProperty) continue; + const withoutTokens = value.replace( + /var\(\s*--zen-[\w-]+\s*\)/g, + '', + ); + const words = withoutTokens.toLowerCase() + .match(/[a-z][a-z-]*/g) || []; + if (words.some(word => !allowedWords.has(word)) && + !/^\s*(?:0|none|transparent|currentColor|inherit)\s*$/i.test( + value, + )) { + failures.push( + `${relative}:${css.slice(0, declaration.index).split('\n').length} ` + + `${property} must use a --zen-* token`, + ); + } + } + } + } + + for (const pattern of [forbiddenGlyph, pictographic]) { + for (const match of source.matchAll(pattern)) { + failures.push( + `${relative}:${source.slice(0, match.index).split('\n').length} ` + + `icon glyph ${JSON.stringify(match[0])}; use <zen-icon>`, + ); + } + } + if (relative !== 'components/icon.js' && + /<svg\b|createElementNS\([^)]*["']svg["']/i.test(source)) { + failures.push(`${relative} defines SVG outside components/icon.js`); + } + if (relative.endsWith('.js')) { + if (/\bimport\s*\/[/*]/.test(source)) { + failures.push(`${relative} places comments inside import syntax`); + } + const importPattern = + /(?:import\s+(?:[^'"]+\s+from\s+)?|export\s+[^'"]+\s+from\s+)["']([^"']+)["']/g; + for (const match of source.matchAll(importPattern)) { + if (!match[1].startsWith('.')) { + failures.push( + `${relative} imports external runtime ${JSON.stringify(match[1])}`, + ); + } + } + if (/\bimport\s*\(/.test(source)) { + failures.push(`${relative} uses forbidden dynamic import()`); + } + } + if (relative.endsWith('.css') && + /@font-face|(?:font|font-family)\s*:[^;]*(?:icon|awesome|material)/i.test( + source, + )) { + failures.push(`${relative} declares an icon font`); + } + if (relative.endsWith('.css')) { + for (const match of source.matchAll( + /@import\s+(?:url\(\s*)?["']?([^"')\s;]+)["']?\s*\)?/g, + )) { + if (!match[1].startsWith('.')) { + failures.push( + `${relative} imports external stylesheet ${JSON.stringify(match[1])}`, + ); + } + } + } +} + +for (const file of files.filter(file => + file.endsWith('.js') +)) { + const relative = path.relative(sourceRoot, file); + const source = fs.readFileSync(file, 'utf8'); + const declaration = + /^export\s+(?:default\s+)?(?:async\s+)?(class|const|let|var|function)\s+([A-Za-z0-9_]+)/gm; + for (const match of source.matchAll(declaration)) { + const prefix = source.slice(0, match.index); + const comment = prefix.match(/\/\*\*([\s\S]*?)\*\/\s*$/); + const line = prefix.split('\n').length; + if (!comment) { + failures.push(`${relative}:${line} ${match[2]} lacks Google-style JSDoc`); + continue; + } + const body = comment[1]; + if (match[1] === 'class' && !/@extends\s+\{[^}]+\}/.test(body)) { + failures.push(`${relative}:${line} ${match[2]} lacks @extends`); + } else if (['const', 'let', 'var'].includes(match[1]) && + !/@(?:const|type)\b/.test(body)) { + failures.push(`${relative}:${line} ${match[2]} lacks @const/@type`); + } else if (match[1] === 'function' && + (!/@param\s+\{[^}]+\}/.test(body) || + !/@return\s+\{[^}]+\}/.test(body))) { + failures.push(`${relative}:${line} ${match[2]} lacks @param/@return`); + } else if (match[1] === 'function') { + const signature = source.slice(match.index).match( + /^export\s+(?:default\s+)?(?:async\s+)?function\s+[A-Za-z0-9_]+\s*\(([^)]*)\)/, + ); + const parameters = (signature?.[1] || '') + .split(',') + .map(parameter => parameter.trim() + .replace(/^\.\.\./, '') + .split('=')[0].trim()) + .filter(Boolean); + const documented = [...body.matchAll( + /@param\s+\{[^}]+\}\s+([A-Za-z0-9_$]+)/g, + )].map(parameter => parameter[1]); + for (const parameter of parameters) { + if (!documented.includes(parameter)) { + failures.push( + `${relative}:${line} ${match[2]} lacks @param for ${parameter}`, + ); + } + } + } + } + for (const match of source.matchAll( + /^export\s+const\s+([A-Za-z0-9_]+)\s*=\s*(?:async\s*)?(?:\(([^)]*)\)|([A-Za-z0-9_$]+))\s*=>/gm, + )) { + validateCallableDocumentation( + relative, + source, + match.index, + match[1], + match[2] || match[3] || '', + failures, + ); + } + for (const match of source.matchAll( + /^export\s+const\s+([A-Za-z0-9_]+)\s*=\s*(?:async\s*)?function(?:\s+[A-Za-z0-9_]+)?\s*\(([^)]*)\)/gm, + )) { + validateCallableDocumentation( + relative, + source, + match.index, + match[1], + match[2], + failures, + ); + } + for (const match of source.matchAll( + /export\s*\{[\s\S]*?\}\s*(?!from\b)(?:;|$)/g, + )) { + failures.push( + `${relative}:${source.slice(0, match.index).split('\n').length} ` + + "uses an undocumented local export list", + ); + } +} + +assert.deepEqual( + failures, + [], + `Design-system policy violations:\n${failures.join('\n')}`, +);