surshar / claude-code
Claude Code is an agentic coding tool that lives in your terminal, understands your codebase, and helps you code faster by executing routine tasks, explaining complex code, and handling git workflows - all through natural language commands.
Built by Anthropic · github.com/anthropics
The simpler GritQL approach is sufficient for catching direct <Box> inside <Text> nesting mistakes.
| @@ -1,136 +1,0 @@ | ||
| 1 | -#!/usr/bin/env npx tsx | |
| 2 | -/** | |
| 3 | - * Build-time check: Ensure <Box> is never nested inside <Text> | |
| 4 | - * | |
| 5 | - * This catches the Ink error: "<Box> can't be nested inside <Text> component" | |
| 6 | - * | |
| 7 | - * Usage: | |
| 8 | - * npx tsx scripts/check-box-in-text.ts [directory] | |
| 9 | - * # or add to package.json scripts: | |
| 10 | - * # "lint:ink": "tsx scripts/check-box-in-text.ts src" | |
| 11 | - */ | |
| 12 | - | |
| 13 | -import * as fs from "fs"; | |
| 14 | -import * as path from "path"; | |
| 15 | -import { parse } from "@babel/parser"; | |
| 16 | -import traverse from "@babel/traverse"; | |
| 17 | -import type { NodePath } from "@babel/traverse"; | |
| 18 | -import type { JSXElement, JSXOpeningElement } from "@babel/types"; | |
| 19 | - | |
| 20 | -interface Violation { | |
| 21 | - file: string; | |
| 22 | - line: number; | |
| 23 | - column: number; | |
| 24 | - message: string; | |
| 25 | -} | |
| 26 | - | |
| 27 | -function getElementName(openingElement: JSXOpeningElement): string | null { | |
| 28 | - if (openingElement.name.type === "JSXIdentifier") { | |
| 29 | - return openingElement.name.name; | |
| 30 | - } | |
| 31 | - return null; | |
| 32 | -} | |
| 33 | - | |
| 34 | -function isInsideTextElement(path: NodePath<JSXElement>): boolean { | |
| 35 | - let parent = path.parentPath; | |
| 36 | - while (parent) { | |
| 37 | - if ( | |
| 38 | - parent.isJSXElement() && | |
| 39 | - parent.node.openingElement.name.type === "JSXIdentifier" && | |
| 40 | - parent.node.openingElement.name.name === "Text" | |
| 41 | - ) { | |
| 42 | - return true; | |
| 43 | - } | |
| 44 | - parent = parent.parentPath; | |
| 45 | - } | |
| 46 | - return false; | |
| 47 | -} | |
| 48 | - | |
| 49 | -function checkFile(filePath: string): Violation[] { | |
| 50 | - const violations: Violation[] = []; | |
| 51 | - const code = fs.readFileSync(filePath, "utf-8"); | |
| 52 | - | |
| 53 | - let ast; | |
| 54 | - try { | |
| 55 | - ast = parse(code, { | |
| 56 | - sourceType: "module", | |
| 57 | - plugins: ["jsx", "typescript"], | |
| 58 | - }); | |
| 59 | - } catch { | |
| 60 | - // Skip files that can't be parsed | |
| 61 | - return violations; | |
| 62 | - } | |
| 63 | - | |
| 64 | - traverse(ast, { | |
| 65 | - JSXElement(path) { | |
| 66 | - const elementName = getElementName(path.node.openingElement); | |
| 67 | - | |
| 68 | - if (elementName === "Box" && isInsideTextElement(path)) { | |
| 69 | - const loc = path.node.loc; | |
| 70 | - violations.push({ | |
| 71 | - file: filePath, | |
| 72 | - line: loc?.start.line ?? 0, | |
| 73 | - column: loc?.start.column ?? 0, | |
| 74 | - message: "<Box> can't be nested inside <Text> component", | |
| 75 | - }); | |
| 76 | - } | |
| 77 | - }, | |
| 78 | - }); | |
| 79 | - | |
| 80 | - return violations; | |
| 81 | -} | |
| 82 | - | |
| 83 | -function findTsxFiles(dir: string): string[] { | |
| 84 | - const files: string[] = []; | |
| 85 | - | |
| 86 | - function walk(currentDir: string) { | |
| 87 | - const entries = fs.readdirSync(currentDir, { withFileTypes: true }); | |
| 88 | - for (const entry of entries) { | |
| 89 | - const fullPath = path.join(currentDir, entry.name); | |
| 90 | - if (entry.isDirectory()) { | |
| 91 | - // Skip node_modules and hidden directories | |
| 92 | - if (!entry.name.startsWith(".") && entry.name !== "node_modules") { | |
| 93 | - walk(fullPath); | |
| 94 | - } | |
| 95 | - } else if (entry.isFile() && /\.(tsx|jsx)$/.test(entry.name)) { | |
| 96 | - files.push(fullPath); | |
| 97 | - } | |
| 98 | - } | |
| 99 | - } | |
| 100 | - | |
| 101 | - walk(dir); | |
| 102 | - return files; | |
| 103 | -} | |
| 104 | - | |
| 105 | -function main() { | |
| 106 | - const targetDir = process.argv[2] || "src"; | |
| 107 | - | |
| 108 | - if (!fs.existsSync(targetDir)) { | |
| 109 | - console.error(`Directory not found: ${targetDir}`); | |
| 110 | - process.exit(1); | |
| 111 | - } | |
| 112 | - | |
| 113 | - console.log(`Checking for <Box> inside <Text> violations in ${targetDir}...`); | |
| 114 | - | |
| 115 | - const files = findTsxFiles(targetDir); | |
| 116 | - const allViolations: Violation[] = []; | |
| 117 | - | |
| 118 | - for (const file of files) { | |
| 119 | - const violations = checkFile(file); | |
| 120 | - allViolations.push(...violations); | |
| 121 | - } | |
| 122 | - | |
| 123 | - if (allViolations.length > 0) { | |
| 124 | - console.error(`\nFound ${allViolations.length} violation(s):\n`); | |
| 125 | - for (const v of allViolations) { | |
| 126 | - console.error(` ${v.file}:${v.line}:${v.column}`); | |
| 127 | - console.error(` ${v.message}\n`); | |
| 128 | - } | |
| 129 | - process.exit(1); | |
| 130 | - } | |
| 131 | - | |
| 132 | - console.log(`Checked ${files.length} files - no violations found.`); | |
| 133 | - process.exit(0); | |
| 134 | -} | |
| 135 | - | |
| 136 | -main(); |