Setting the file. One moment.
Validate · Stitch::react Native · google-labs-code/stitch-skills · Skills Docs
ContentsBack to the top of the page (opens in a new tab)
scripts/ validate.js
JavaScript · 124 lines · 4 KB
15
*/
16
17 import swc from '@swc/core' ;
18 import fs from 'node:fs' ;
19 import path from 'node:path' ;
20
21 const HEX_COLOR_REGEX = /# [0-9A-Fa-f] {3,8}\b / ;
22 const RGBA_COLOR_REGEX = / ^ rgba ? \( \s * \d / ;
23 const HTML_ELEMENTS = [ 'div' , 'span' , 'p' , 'h1' , 'h2' , 'h3' , 'h4' , 'h5' , 'h6' , 'img' , 'button' , 'a' , 'input' , 'ul' , 'ol' , 'li' , 'section' , 'header' , 'footer' , 'nav' , 'main' ];
24
25 async function validateComponent ( filePath ) {
26 if ( ! filePath) {
27 console. error ( "Usage: node validate.js <path-to-component>" );
28 process. exit ( 1 );
29 }
30 try {
31 const code = fs. readFileSync (filePath, 'utf-8' );
32 const filename = path. basename (filePath);
33 const ast = await swc. parse (code, { syntax: "typescript" , tsx: true });
34 let hasInterface = false ;
35 let hasExportedInterface = false ;
36 let colorIssues = [];
37 let htmlElements = [];
38
39 console. log ( "Scanning AST..." );
40
41 const walk = ( node , parent ) => {
42 if ( ! node || typeof node !== 'object' ) return ;
43 if (Array. isArray (node)) {
44 for ( const item of node) walk (item, parent);
45 return ;
46 }
47 if ( typeof node.type !== 'string' ) return ;
48
49 if (node.type === 'TsInterfaceDeclaration' && node.id.value. endsWith ( 'Props' )) {
50 hasInterface = true ;
51 if (parent?.type === 'ExportDeclaration' ) {
52 hasExportedInterface = true ;
53 }
54 }
55
56 // Check for hardcoded hex values in strings
57 if (node.type === 'StringLiteral' && HEX_COLOR_REGEX . test (node.value)) {
58 colorIssues. push (node.value);
59 }
60
61 // Check for rgba() color strings
62 if (node.type === 'StringLiteral' && RGBA_COLOR_REGEX . test (node.value)) {
63 colorIssues. push (node.value);
64 }
65
66 // Check for HTML elements used as JSX tags
67 if (node.type === 'JSXOpeningElement' && node.name?.type === 'Identifier' ) {
68 const tagName = node.name.value;
69 if ( HTML_ELEMENTS . includes (tagName)) {
70 htmlElements. push (tagName);
71 }
72 }
73
74 for ( const key in node) {
75 if (key === 'span' ) continue ;
76 if (node[key] && typeof node[key] === 'object' ) walk (node[key], node);
77 }
78 };
79 walk (ast, null );
80
81 console. log ( `--- Validation for: ${ filename } ---` );
82
83 let valid = true ;
84
85 if (hasExportedInterface) {
86 console. log ( "PASS: Exported Props interface found." );
87 } else if (hasInterface) {
88 console. error ( "WARN: Props interface found but not exported. Add 'export' keyword." );
89 valid = false ;
90 } else {
91 console. error ( "FAIL: Missing Props interface (must end in 'Props' and be exported)." );
92 valid = false ;
93 }
94
95 if (colorIssues. length === 0 ) {
96 console. log ( "PASS: No hardcoded color values found." );
97 } else {
98 console. error ( `FAIL: Found ${ colorIssues . length } hardcoded colors. Use theme.ts instead.` );
99 colorIssues. forEach ( c => console. error ( ` - ${ c }` ));
100 valid = false ;
101 }
102
103 if (htmlElements. length === 0 ) {
104 console. log ( "PASS: No HTML elements found. Using React Native primitives." );
105 } else {
106 const unique = [ ...new Set (htmlElements)];
107 console. error ( `FAIL: Found HTML elements: ${ unique . join ( ', ' ) }. Replace with React Native components.` );
108 valid = false ;
109 }
110
111 if (valid) {
112 console. log ( " \n COMPONENT VALID." );
113 process. exit ( 0 );
114 } else {
115 console. error ( " \n VALIDATION FAILED." );
116 process. exit ( 1 );
117 }
118 } catch (err) {
119 console. error ( "ERROR:" , err.message);
120 process. exit ( 1 );
121 }
122 }
123
124 validateComponent (process.argv[ 2 ]);