Mercurial
view design_system/test/design_system_policy_test.js @ 270:358dd5950985
sync production config on every deploy
Always install the ignored repository .config into /etc/mrjunejune before promoting the new bundle.
Co-authored-by: Copilot <[email protected]>
| author | MrJuneJune <me@mrjunejune.com> |
|---|---|
| date | Fri, 07 Aug 2026 13:16:47 -0700 |
| parents | 60a876c4587a |
| children |
line wrap: on
line source
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 designRoot = path.join(RUNFILES, WORKSPACE, 'design_system'); const sourceRoot = path.join(designRoot, '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 listMarkdown(directory) { const markdown = []; for (const entry of fs.readdirSync(directory, { withFileTypes: true })) { const entryPath = path.join(directory, entry.name); if (entry.isDirectory()) markdown.push(...listMarkdown(entryPath)); else if (entry.name.endsWith('.md')) markdown.push(entryPath); } return markdown; } const documentationFiles = [ ...fs.readdirSync(designRoot) .filter(name => name.endsWith('.md')) .map(name => path.join(designRoot, name)), ...listMarkdown(path.join(designRoot, 'wiki')), ]; const namingFiles = [ ...files, ...documentationFiles, path.join(designRoot, 'main.c'), ]; 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 borrowedProductName = /(?:shadcn|storybook|sonner|radix|tailwind|react)/gi; const failures = []; for (const file of files) { const relative = path.relative(sourceRoot, file); const source = fs.readFileSync(file, 'utf8'); if (![ 'styles/reference.css', 'styles/tokens.css', ].includes(relative)) { 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*--(?:zenbu|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 --zenbu-sys-* 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])}`, ); } } } } const cssFiles = files.filter(file => file.endsWith('.css')); const zenbuDeclarations = new Set(); for (const file of cssFiles) { const source = fs.readFileSync(file, 'utf8'); for (const match of source.matchAll(/(--zenbu-[\w-]+)\s*:/g)) { zenbuDeclarations.add(match[1]); } } for (const file of cssFiles) { const relative = path.relative(sourceRoot, file); const source = fs.readFileSync(file, 'utf8'); for (const match of source.matchAll(/var\(\s*(--zenbu-[\w-]+)/g)) { if (!zenbuDeclarations.has(match[1])) { failures.push(`${relative} references unknown token ${match[1]}`); } } if (![ 'styles/reference.css', 'styles/semantic.css', 'styles/themes.css', 'styles/tokens.css', ].includes(relative)) { for (const match of source.matchAll( /var\(\s*(--zenbu-ref-color-[\w-]+)/g, )) { failures.push( `${relative} consumes reference color ${match[1]} directly`, ); } } if (![ 'styles/themes.css', 'styles/tokens.css', ].includes(relative) && /var\(\s*--zen-/.test(source)) { failures.push(`${relative} consumes deprecated --zen-* tokens`); } } for (const file of namingFiles) { const source = fs.readFileSync(file, 'utf8'); for (const match of source.matchAll(borrowedProductName)) { failures.push( `${path.relative(designRoot, file)} uses borrowed product name ` + JSON.stringify(match[0]), ); } } 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')}`, );