const readability = { create(context) { const filename = context.filename; function checkParameters(node) { const last = node.params.at(-1); if ( last?.type === "Identifier" && !last.optional && last.typeAnnotation?.typeAnnotation.type === "TSUnionType" && last.typeAnnotation.typeAnnotation.types.some( type => type.type === "TSUndefinedKeyword", ) ) context.report({ messageId: "optional", node: last }); } return { ArrowFunctionExpression: checkParameters, CallExpression(node) { if ( node.callee.type === "MemberExpression" && ["refine", "superRefine"].includes(node.callee.property.name) && !filename.endsWith(".schema.ts") ) context.report({ messageId: "schema", node }); }, FunctionDeclaration(node) { checkParameters(node); if (node.id?.name?.endsWith?.("ViewModel")) context.report({ messageId: "vmName", node: node.id }); }, FunctionExpression: checkParameters, ImportDeclaration(node) { if ( node.source.value === "zod" && node.importKind !== "type" && !filename.endsWith(".schema.ts") ) context.report({ messageId: "schema", node }); }, JSXOpeningElement(node) { if ( !filename.includes("/packages/icons/") && node.name.type === "JSXIdentifier" && /^(?:svg|path|circle|ellipse|rect|polygon|polyline|line|g|defs|use|symbol)$/.test( node.name.name, ) ) context.report({ messageId: "svg", node }); }, TSUnionType(node) { const complex = node.types.length > 2 || node.types.some(type => type.type === "TSTypeLiteral"); if ( complex && node.types.some( (type, index) => index > 0 && type.loc.start.line === node.types[index - 1].loc.end.line, ) ) context.report({ messageId: "union", node }); }, VariableDeclarator(node) { if ( !/^[A-Z]/.test(node.id.name ?? "") || node.init?.type !== "ArrowFunctionExpression" || node.init.body.type !== "BlockStatement" ) return; for (const statement of node.init.body.body) { if (statement.type === "FunctionDeclaration") context.report({ messageId: "helper", node: statement }); else if (statement.type === "VariableDeclaration") { for (const declaration of statement.declarations) { if ( ["ArrowFunctionExpression", "FunctionExpression"].includes( declaration.init?.type, ) ) context.report({ messageId: "helper", node: declaration }); } } } }, }; }, meta: { messages: { helper: "Move helpers outside components; actions belong to the ViewModel.", optional: "Use an optional trailing parameter instead of a union with undefined.", schema: "Declare Zod schemas in a dedicated *.schema.ts file.", svg: "Own SVG markup in packages/icons, and import its public component.", union: "Put complex union members on separate lines.", vmName: "Name custom hooks use*Vm, not use*ViewModel.", }, schema: [], type: "suggestion", }, }; function isComponent(node) { const pending = [node]; while (pending.length > 0) { const child = pending.pop(); if (!child) continue; switch (child.type) { case "JSXElement": { return true; } case "JSXFragment": { return true; } case "JSXExpressionContainer": { pending.push(child.expression); continue; } case "LogicalExpression": { pending.push(child.right); continue; } case "ConditionalExpression": { pending.push(child.consequent, child.alternate); continue; } } } return false; } const jsxSpacing = { create(context) { function check(node) { let previous; for (const child of node.children) { if (child.type === "JSXText" && !child.value.trim()) continue; if (isComponent(child)) { if (previous) { const gap = context.sourceCode.text.slice( previous.range[1], child.range[0], ); if (!/\n\s*\n/.test(gap)) context.report({ fix: fixer => fixer.insertTextAfter(previous, "\n"), messageId: "space", node: child, }); } previous = child; } else previous = undefined; } } return { JSXElement: check, JSXFragment: check }; }, meta: { fixable: "whitespace", messages: { space: "Separate sibling JSX components with a blank line." }, schema: [], type: "layout", }, }; export default { rules: { "jsx-spacing": jsxSpacing, readability } };