spacehub

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

Add lint tooling to prevent <Box> inside <Text> violationsBrowse files
Add two lint mechanisms to catch the Ink error "<Box> can't be nested
inside <Text> component" at build/lint time rather than runtime:

1. Biome GritQL plugin (no-box-in-text.grit) - for Biome v2+ users
2. Build-time check script (check-box-in-text.ts) - standalone AST check

These help prevent the runtime error that occurs when Box components
are incorrectly nested inside Text components in Ink applications.
Claude authored on Dec 19, 2025 parent 5211559 · commit d5912a011dcf
2 changed files+158 -0compared with 5211559
Ascripts/check-box-in-text.ts
Ascripts/no-box-in-text.grit
@@ -1,0 +1,136 @@
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();
@@ -1,0 +1,22 @@
1+// Biome GritQL Plugin: Prevent <Box> from being nested inside <Text>
2+// This catches the Ink error: "<Box> can't be nested inside <Text> component"
3+
4+language js;
5+
6+// Match Text elements that contain Box children (direct nesting)
7+`<Text$_>$children</Text>` where {
8+ $children <: contains `<Box$_>$_</Box>`,
9+ register_diagnostic(
10+ span = $children,
11+ message = "<Box> can't be nested inside <Text> component. Use <Box> as a sibling or wrap <Text> inside <Box> instead."
12+ )
13+}
14+
15+// Also match self-closing Box inside Text
16+`<Text$_>$children</Text>` where {
17+ $children <: contains `<Box$_ />`,
18+ register_diagnostic(
19+ span = $children,
20+ message = "<Box> can't be nested inside <Text> component. Use <Box> as a sibling or wrap <Text> inside <Box> instead."
21+ )
22+}