Use shared form controls and enforce UI patterns (Refs #110)
CI / checks (push) Successful in 4m51s
CI / checks (push) Successful in 4m51s
This commit is contained in:
+96
-21
@@ -1,32 +1,107 @@
|
||||
import { readdir, readFile } from 'node:fs/promises';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import path from 'node:path';
|
||||
import { parse } from 'svelte/compiler';
|
||||
|
||||
const root = fileURLToPath(new URL('../src/', import.meta.url));
|
||||
const failures = [];
|
||||
async function visit(directory) {
|
||||
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
||||
const file = path.join(directory, entry.name);
|
||||
if (entry.isDirectory()) await visit(file);
|
||||
else if (/\.(svelte|css)$/.test(entry.name) && entry.name !== 'tokens.css') {
|
||||
const source = await readFile(file, 'utf8');
|
||||
for (const match of source.matchAll(/font-size\s*:\s*[\d.]+px\b/g)) {
|
||||
const line = source.slice(0, match.index).split('\n').length;
|
||||
const nativeInputTypes = new Set([
|
||||
'checkbox',
|
||||
'radio',
|
||||
'file',
|
||||
'hidden',
|
||||
'range',
|
||||
'color',
|
||||
'button',
|
||||
'submit',
|
||||
'reset',
|
||||
'image'
|
||||
]);
|
||||
|
||||
/**
|
||||
* @param {string} source
|
||||
* @param {string} filename
|
||||
*/
|
||||
export function checkFormControls(source, filename) {
|
||||
if (
|
||||
!filename.endsWith('.svelte') ||
|
||||
/(?:^|\/)primitives\/(?:Input|Select)\.svelte$/.test(filename)
|
||||
)
|
||||
return [];
|
||||
/** @type {string[]} */
|
||||
const failures = [];
|
||||
/** @param {unknown} node */
|
||||
function visit(node) {
|
||||
if (!node || typeof node !== 'object') return;
|
||||
if ('type' in node && node.type === 'RegularElement') {
|
||||
const element = /** @type {import('svelte/compiler').AST.RegularElement} */ (node);
|
||||
const type = element.attributes.find(
|
||||
(attribute) => attribute.type === 'Attribute' && attribute.name === 'type'
|
||||
);
|
||||
const value =
|
||||
type?.type === 'Attribute' && type.value !== true
|
||||
? Array.isArray(type.value)
|
||||
? type.value[0]
|
||||
: type.value
|
||||
: undefined;
|
||||
const inputType =
|
||||
value?.type === 'Text'
|
||||
? value.data
|
||||
: value?.expression.type === 'Literal'
|
||||
? value.expression.value
|
||||
: undefined;
|
||||
if (
|
||||
element.name === 'select' ||
|
||||
(element.name === 'input' &&
|
||||
(typeof inputType !== 'string' || !nativeInputTypes.has(inputType)))
|
||||
) {
|
||||
const line = source.slice(0, element.start).split('\n').length;
|
||||
failures.push(
|
||||
`${path.relative(process.cwd(), file)}:${line}: use a shared type token (12 px minimum): ${match[0]}`
|
||||
`${filename}:${line}: use Field with ${element.name === 'select' ? 'Select' : 'Input'} instead of raw <${element.name}>`
|
||||
);
|
||||
}
|
||||
}
|
||||
for (const value of Object.values(node)) {
|
||||
if (Array.isArray(value)) value.forEach(visit);
|
||||
else if (value && typeof value === 'object') visit(value);
|
||||
}
|
||||
}
|
||||
visit(parse(source, { modern: true }).fragment);
|
||||
return failures;
|
||||
}
|
||||
await visit(root);
|
||||
const tokens = await readFile(path.join(root, 'lib/ui/styles/tokens.css'), 'utf8');
|
||||
for (const match of tokens.matchAll(
|
||||
/--text-(?:xs|sm|base|md|lg|xl|2xl|3xl|input):\s*([\d.]+)px/g
|
||||
)) {
|
||||
if (Number(match[1]) < 12) failures.push(`Type tokens must be at least 12 px: ${match[0]}`);
|
||||
|
||||
async function main() {
|
||||
const root = fileURLToPath(new URL('../src/', import.meta.url));
|
||||
/** @type {string[]} */
|
||||
const failures = [];
|
||||
/** @param {string} directory */
|
||||
async function visit(directory) {
|
||||
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
||||
const file = path.join(directory, entry.name);
|
||||
if (entry.isDirectory()) await visit(file);
|
||||
else if (/\.(svelte|css)$/.test(entry.name) && entry.name !== 'tokens.css') {
|
||||
const source = await readFile(file, 'utf8');
|
||||
const filename = path.relative(process.cwd(), file);
|
||||
failures.push(...checkFormControls(source, filename));
|
||||
for (const match of source.matchAll(/font-size\s*:\s*[\d.]+px\b/g)) {
|
||||
const line = source.slice(0, match.index).split('\n').length;
|
||||
failures.push(
|
||||
`${filename}:${line}: use a shared type token (12 px minimum): ${match[0]}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
await visit(root);
|
||||
const tokens = await readFile(path.join(root, 'lib/ui/styles/tokens.css'), 'utf8');
|
||||
for (const match of tokens.matchAll(
|
||||
/--text-(?:xs|sm|base|md|lg|xl|2xl|3xl|input):\s*([\d.]+)px/g
|
||||
)) {
|
||||
if (Number(match[1]) < 12) failures.push(`Type tokens must be at least 12 px: ${match[0]}`);
|
||||
}
|
||||
if (failures.length) {
|
||||
console.error(failures.join('\n'));
|
||||
process.exitCode = 1;
|
||||
} else console.log('UI styles: shared form controls, font sizes and 12 px minimum verified.');
|
||||
}
|
||||
if (failures.length) {
|
||||
console.error(failures.join('\n'));
|
||||
process.exitCode = 1;
|
||||
} else console.log('UI styles: shared font sizes and 12 px minimum verified.');
|
||||
|
||||
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url))
|
||||
await main();
|
||||
|
||||
Reference in New Issue
Block a user