@idealyst/tooling 1.2.20 → 1.2.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@idealyst/tooling",
3
- "version": "1.2.20",
3
+ "version": "1.2.22",
4
4
  "description": "Code analysis and validation utilities for Idealyst Framework",
5
5
  "readme": "README.md",
6
6
  "main": "src/index.ts",
@@ -121,7 +121,6 @@ function analyzeComponentDir(
121
121
  const altNames = [`${componentName}ComponentProps`, 'Props'];
122
122
  let propsInterface: ts.InterfaceDeclaration | ts.TypeAliasDeclaration | null = null;
123
123
  let interfaceDescription: string | undefined;
124
- let foundInFile: ts.SourceFile | null = null;
125
124
 
126
125
  // Search each file for the props interface
127
126
  for (const filePath of tsFiles) {
@@ -133,12 +132,10 @@ function analyzeComponentDir(
133
132
  if (ts.isInterfaceDeclaration(node) && node.name.text === propsInterfaceName) {
134
133
  propsInterface = node;
135
134
  interfaceDescription = getJSDocDescription(node);
136
- foundInFile = sourceFile;
137
135
  }
138
136
  if (ts.isTypeAliasDeclaration(node) && node.name.text === propsInterfaceName) {
139
137
  propsInterface = node;
140
138
  interfaceDescription = getJSDocDescription(node);
141
- foundInFile = sourceFile;
142
139
  }
143
140
  });
144
141
 
@@ -156,7 +153,6 @@ function analyzeComponentDir(
156
153
  if ((ts.isInterfaceDeclaration(node) || ts.isTypeAliasDeclaration(node)) && node.name.text === altName) {
157
154
  propsInterface = node;
158
155
  interfaceDescription = getJSDocDescription(node);
159
- foundInFile = sourceFile;
160
156
  }
161
157
  });
162
158
 
@@ -385,7 +381,7 @@ function analyzeProperty(
385
381
  function extractPropValues(
386
382
  type: ts.Type,
387
383
  typeString: string,
388
- typeChecker: ts.TypeChecker,
384
+ _typeChecker: ts.TypeChecker,
389
385
  themeValues: ThemeValues
390
386
  ): string[] {
391
387
  // Handle theme-derived types
@@ -0,0 +1,397 @@
1
+ import { Severity } from '../types';
2
+
3
+ /**
4
+ * Types of linting issues the component linter can detect.
5
+ *
6
+ * These are issues that TypeScript cannot catch - primarily style/pattern issues
7
+ * that are syntactically valid but violate Idealyst conventions.
8
+ */
9
+ export type LintIssueType =
10
+ | 'hardcoded-color' // Using color strings like '#fff', 'red', 'rgb()'
11
+ | 'direct-platform-import'; // Importing directly from react-native in shared file
12
+
13
+ /**
14
+ * A single lint issue found during analysis
15
+ */
16
+ export interface LintIssue {
17
+ /** Type of lint issue */
18
+ type: LintIssueType;
19
+ /** Severity level */
20
+ severity: Severity;
21
+ /** Line number where issue occurred */
22
+ line: number;
23
+ /** Column number where issue occurred */
24
+ column: number;
25
+ /** The problematic code snippet */
26
+ code: string;
27
+ /** Human-readable message describing the issue */
28
+ message: string;
29
+ /** Suggested fix (if available) */
30
+ suggestion?: string;
31
+ }
32
+
33
+ /**
34
+ * Result of linting a component file
35
+ */
36
+ export interface LintResult {
37
+ /** Path to the analyzed file */
38
+ filePath: string;
39
+ /** List of issues found */
40
+ issues: LintIssue[];
41
+ /** Whether the file passed linting (no errors) */
42
+ passed: boolean;
43
+ /** Count of issues by severity */
44
+ counts: {
45
+ error: number;
46
+ warning: number;
47
+ info: number;
48
+ };
49
+ }
50
+
51
+ /**
52
+ * Options for the component linter
53
+ */
54
+ export interface ComponentLinterOptions {
55
+ /**
56
+ * Which rules to enable (all enabled by default)
57
+ */
58
+ rules?: {
59
+ /** Detect hardcoded color values like '#fff', 'red', 'rgb()' */
60
+ hardcodedColors?: boolean | Severity;
61
+ /** Detect direct imports from 'react-native' in shared files */
62
+ directPlatformImports?: boolean | Severity;
63
+ };
64
+
65
+ /**
66
+ * Glob patterns for files to ignore
67
+ */
68
+ ignoredPatterns?: string[];
69
+
70
+ /**
71
+ * Color values that are allowed (e.g., 'transparent', 'inherit')
72
+ * @default ['transparent', 'inherit', 'currentColor']
73
+ */
74
+ allowedColors?: string[];
75
+ }
76
+
77
+ // Common CSS color names that indicate hardcoded colors
78
+ const CSS_COLOR_NAMES = new Set([
79
+ 'aliceblue', 'antiquewhite', 'aqua', 'aquamarine', 'azure', 'beige', 'bisque',
80
+ 'black', 'blanchedalmond', 'blue', 'blueviolet', 'brown', 'burlywood', 'cadetblue',
81
+ 'chartreuse', 'chocolate', 'coral', 'cornflowerblue', 'cornsilk', 'crimson', 'cyan',
82
+ 'darkblue', 'darkcyan', 'darkgoldenrod', 'darkgray', 'darkgreen', 'darkgrey',
83
+ 'darkkhaki', 'darkmagenta', 'darkolivegreen', 'darkorange', 'darkorchid', 'darkred',
84
+ 'darksalmon', 'darkseagreen', 'darkslateblue', 'darkslategray', 'darkslategrey',
85
+ 'darkturquoise', 'darkviolet', 'deeppink', 'deepskyblue', 'dimgray', 'dimgrey',
86
+ 'dodgerblue', 'firebrick', 'floralwhite', 'forestgreen', 'fuchsia', 'gainsboro',
87
+ 'ghostwhite', 'gold', 'goldenrod', 'gray', 'green', 'greenyellow', 'grey',
88
+ 'honeydew', 'hotpink', 'indianred', 'indigo', 'ivory', 'khaki', 'lavender',
89
+ 'lavenderblush', 'lawngreen', 'lemonchiffon', 'lightblue', 'lightcoral', 'lightcyan',
90
+ 'lightgoldenrodyellow', 'lightgray', 'lightgreen', 'lightgrey', 'lightpink',
91
+ 'lightsalmon', 'lightseagreen', 'lightskyblue', 'lightslategray', 'lightslategrey',
92
+ 'lightsteelblue', 'lightyellow', 'lime', 'limegreen', 'linen', 'magenta', 'maroon',
93
+ 'mediumaquamarine', 'mediumblue', 'mediumorchid', 'mediumpurple', 'mediumseagreen',
94
+ 'mediumslateblue', 'mediumspringgreen', 'mediumturquoise', 'mediumvioletred',
95
+ 'midnightblue', 'mintcream', 'mistyrose', 'moccasin', 'navajowhite', 'navy',
96
+ 'oldlace', 'olive', 'olivedrab', 'orange', 'orangered', 'orchid', 'palegoldenrod',
97
+ 'palegreen', 'paleturquoise', 'palevioletred', 'papayawhip', 'peachpuff', 'peru',
98
+ 'pink', 'plum', 'powderblue', 'purple', 'rebeccapurple', 'red', 'rosybrown',
99
+ 'royalblue', 'saddlebrown', 'salmon', 'sandybrown', 'seagreen', 'seashell', 'sienna',
100
+ 'silver', 'skyblue', 'slateblue', 'slategray', 'slategrey', 'snow', 'springgreen',
101
+ 'steelblue', 'tan', 'teal', 'thistle', 'tomato', 'turquoise', 'violet', 'wheat',
102
+ 'white', 'whitesmoke', 'yellow', 'yellowgreen',
103
+ ]);
104
+
105
+ // Colors that are generally safe to use
106
+ const DEFAULT_ALLOWED_COLORS = new Set([
107
+ 'transparent',
108
+ 'inherit',
109
+ 'currentColor',
110
+ 'currentcolor',
111
+ ]);
112
+
113
+ // Style properties that typically use colors
114
+ const COLOR_PROPERTIES = [
115
+ 'color',
116
+ 'backgroundColor',
117
+ 'borderColor',
118
+ 'borderTopColor',
119
+ 'borderRightColor',
120
+ 'borderBottomColor',
121
+ 'borderLeftColor',
122
+ 'shadowColor',
123
+ 'textDecorationColor',
124
+ 'tintColor',
125
+ 'overlayColor',
126
+ ];
127
+
128
+ /**
129
+ * Check if a string is a hardcoded color value
130
+ */
131
+ function isHardcodedColor(value: string, allowedColors: Set<string>): boolean {
132
+ const trimmed = value.trim().toLowerCase();
133
+
134
+ // Check if it's an allowed color
135
+ if (allowedColors.has(trimmed)) {
136
+ return false;
137
+ }
138
+
139
+ // Hex color: #fff, #ffffff, #ffffffff
140
+ if (/^#[0-9a-f]{3,8}$/i.test(trimmed)) {
141
+ return true;
142
+ }
143
+
144
+ // RGB/RGBA: rgb(0,0,0), rgba(0,0,0,0.5)
145
+ if (/^rgba?\s*\(/.test(trimmed)) {
146
+ return true;
147
+ }
148
+
149
+ // HSL/HSLA: hsl(0,0%,0%), hsla(0,0%,0%,0.5)
150
+ if (/^hsla?\s*\(/.test(trimmed)) {
151
+ return true;
152
+ }
153
+
154
+ // CSS named color
155
+ if (CSS_COLOR_NAMES.has(trimmed)) {
156
+ return true;
157
+ }
158
+
159
+ return false;
160
+ }
161
+
162
+ /**
163
+ * Get the severity for a rule, handling boolean and severity values
164
+ */
165
+ function getRuleSeverity(
166
+ rule: boolean | Severity | undefined,
167
+ defaultSeverity: Severity
168
+ ): Severity | null {
169
+ if (rule === false) return null;
170
+ if (rule === true || rule === undefined) return defaultSeverity;
171
+ return rule;
172
+ }
173
+
174
+ /**
175
+ * Parse source code and find line/column for a match index
176
+ */
177
+ function getLineColumn(source: string, index: number): { line: number; column: number } {
178
+ const lines = source.substring(0, index).split('\n');
179
+ return {
180
+ line: lines.length,
181
+ column: lines[lines.length - 1].length + 1,
182
+ };
183
+ }
184
+
185
+ /**
186
+ * Lint a component file for Idealyst-specific issues.
187
+ *
188
+ * Detects issues that TypeScript cannot catch:
189
+ * - Hardcoded colors (TypeScript sees these as valid strings)
190
+ * - Direct react-native imports in shared files (valid TS, but breaks web)
191
+ *
192
+ * @param filePath - Path to the file being analyzed
193
+ * @param sourceCode - The source code content
194
+ * @param options - Linter options
195
+ * @returns Lint result with issues found
196
+ *
197
+ * @example
198
+ * ```typescript
199
+ * const result = lintComponent(
200
+ * 'src/components/MyButton.tsx',
201
+ * sourceCode,
202
+ * { rules: { hardcodedColors: 'error' } }
203
+ * );
204
+ *
205
+ * if (!result.passed) {
206
+ * for (const issue of result.issues) {
207
+ * console.error(`${issue.line}:${issue.column} - ${issue.message}`);
208
+ * }
209
+ * }
210
+ * ```
211
+ */
212
+ export function lintComponent(
213
+ filePath: string,
214
+ sourceCode: string,
215
+ options: ComponentLinterOptions = {}
216
+ ): LintResult {
217
+ const issues: LintIssue[] = [];
218
+ const rules = options.rules || {};
219
+
220
+ const allowedColors = new Set([
221
+ ...DEFAULT_ALLOWED_COLORS,
222
+ ...(options.allowedColors || []).map(c => c.toLowerCase()),
223
+ ]);
224
+
225
+ // Rule: Hardcoded colors
226
+ const hardcodedColorSeverity = getRuleSeverity(rules.hardcodedColors, 'warning');
227
+ if (hardcodedColorSeverity) {
228
+ // Match color properties with string values
229
+ for (const prop of COLOR_PROPERTIES) {
230
+ // Match: backgroundColor: '#fff' or backgroundColor: "red"
231
+ const propRegex = new RegExp(`${prop}\\s*:\\s*['"]([^'"]+)['"]`, 'g');
232
+ let match;
233
+ while ((match = propRegex.exec(sourceCode)) !== null) {
234
+ const colorValue = match[1];
235
+ if (isHardcodedColor(colorValue, allowedColors)) {
236
+ const { line, column } = getLineColumn(sourceCode, match.index);
237
+ issues.push({
238
+ type: 'hardcoded-color',
239
+ severity: hardcodedColorSeverity,
240
+ line,
241
+ column,
242
+ code: match[0],
243
+ message: `Hardcoded color '${colorValue}' in ${prop}. Use theme colors instead.`,
244
+ suggestion: `Use theme.colors.*, theme.intents[intent].*, or pass color via props`,
245
+ });
246
+ }
247
+ }
248
+ }
249
+
250
+ // Also check for color/backgroundColor in template literals
251
+ const templateColorRegex = /(?:color|backgroundColor)\s*:\s*`[^`]*#[0-9a-fA-F]{3,8}[^`]*`/g;
252
+ let templateMatch;
253
+ while ((templateMatch = templateColorRegex.exec(sourceCode)) !== null) {
254
+ const { line, column } = getLineColumn(sourceCode, templateMatch.index);
255
+ issues.push({
256
+ type: 'hardcoded-color',
257
+ severity: hardcodedColorSeverity,
258
+ line,
259
+ column,
260
+ code: templateMatch[0],
261
+ message: `Hardcoded hex color in template literal. Use theme colors instead.`,
262
+ suggestion: `Use theme.colors.* or theme.intents[intent].*`,
263
+ });
264
+ }
265
+ }
266
+
267
+ // Rule: Direct platform imports in shared files
268
+ const directPlatformSeverity = getRuleSeverity(rules.directPlatformImports, 'warning');
269
+ if (directPlatformSeverity) {
270
+ // Only check shared files (not .web.tsx or .native.tsx)
271
+ const isSharedFile = !filePath.includes('.web.') && !filePath.includes('.native.');
272
+ if (isSharedFile) {
273
+ // Check for direct react-native imports
274
+ const rnImportRegex = /import\s+.*\s+from\s+['"]react-native['"]/g;
275
+ let match;
276
+ while ((match = rnImportRegex.exec(sourceCode)) !== null) {
277
+ const { line, column } = getLineColumn(sourceCode, match.index);
278
+ issues.push({
279
+ type: 'direct-platform-import',
280
+ severity: directPlatformSeverity,
281
+ line,
282
+ column,
283
+ code: match[0],
284
+ message: `Direct import from 'react-native' in shared file. Use @idealyst/components instead.`,
285
+ suggestion: `Import View, Text, etc. from '@idealyst/components'`,
286
+ });
287
+ }
288
+ }
289
+ }
290
+
291
+ // Calculate counts
292
+ const counts = {
293
+ error: issues.filter(i => i.severity === 'error').length,
294
+ warning: issues.filter(i => i.severity === 'warning').length,
295
+ info: issues.filter(i => i.severity === 'info').length,
296
+ };
297
+
298
+ return {
299
+ filePath,
300
+ issues,
301
+ passed: counts.error === 0,
302
+ counts,
303
+ };
304
+ }
305
+
306
+ /**
307
+ * Lint multiple component files
308
+ *
309
+ * @param files - Array of files to lint
310
+ * @param options - Linter options
311
+ * @returns Array of lint results
312
+ */
313
+ export function lintComponents(
314
+ files: Array<{ path: string; content: string }>,
315
+ options: ComponentLinterOptions = {}
316
+ ): LintResult[] {
317
+ return files.map(file => lintComponent(file.path, file.content, options));
318
+ }
319
+
320
+ /**
321
+ * Format a lint issue for console output
322
+ */
323
+ export function formatLintIssue(issue: LintIssue, filePath: string): string {
324
+ const severityPrefix = issue.severity === 'error'
325
+ ? 'ERROR'
326
+ : issue.severity === 'warning'
327
+ ? 'WARN'
328
+ : 'INFO';
329
+
330
+ let output = `${severityPrefix}: ${filePath}:${issue.line}:${issue.column} - ${issue.message}`;
331
+ if (issue.suggestion) {
332
+ output += `\n Suggestion: ${issue.suggestion}`;
333
+ }
334
+ return output;
335
+ }
336
+
337
+ /**
338
+ * Format all lint results for console output
339
+ */
340
+ export function formatLintResults(results: LintResult[]): string[] {
341
+ const lines: string[] = [];
342
+
343
+ for (const result of results) {
344
+ for (const issue of result.issues) {
345
+ lines.push(formatLintIssue(issue, result.filePath));
346
+ }
347
+ }
348
+
349
+ return lines;
350
+ }
351
+
352
+ /**
353
+ * Summary of lint results
354
+ */
355
+ export interface LintSummary {
356
+ totalFiles: number;
357
+ passedFiles: number;
358
+ failedFiles: number;
359
+ totalIssues: number;
360
+ issuesByType: Record<LintIssueType, number>;
361
+ issuesBySeverity: Record<Severity, number>;
362
+ }
363
+
364
+ /**
365
+ * Summarize lint results
366
+ */
367
+ export function summarizeLintResults(results: LintResult[]): LintSummary {
368
+ const issuesByType: Record<LintIssueType, number> = {
369
+ 'hardcoded-color': 0,
370
+ 'direct-platform-import': 0,
371
+ };
372
+
373
+ const issuesBySeverity: Record<Severity, number> = {
374
+ error: 0,
375
+ warning: 0,
376
+ info: 0,
377
+ };
378
+
379
+ let totalIssues = 0;
380
+
381
+ for (const result of results) {
382
+ for (const issue of result.issues) {
383
+ totalIssues++;
384
+ issuesByType[issue.type]++;
385
+ issuesBySeverity[issue.severity]++;
386
+ }
387
+ }
388
+
389
+ return {
390
+ totalFiles: results.length,
391
+ passedFiles: results.filter(r => r.passed).length,
392
+ failedFiles: results.filter(r => !r.passed).length,
393
+ totalIssues,
394
+ issuesByType,
395
+ issuesBySeverity,
396
+ };
397
+ }
@@ -1 +1,2 @@
1
1
  export * from './platformImports';
2
+ export * from './componentLinter';
@@ -2,7 +2,6 @@ import {
2
2
  AnalyzerOptions,
3
3
  AnalysisResult,
4
4
  FileInput,
5
- FileType,
6
5
  ImportInfo,
7
6
  Severity,
8
7
  Violation,
@@ -11,13 +10,10 @@ import {
11
10
  import { classifyFile } from '../utils/fileClassifier';
12
11
  import { parseImports } from '../utils/importParser';
13
12
  import {
14
- REACT_NATIVE_PRIMITIVE_NAMES,
15
13
  REACT_NATIVE_SOURCES,
16
14
  isReactNativePrimitive,
17
15
  } from '../rules/reactNativePrimitives';
18
16
  import {
19
- REACT_DOM_PRIMITIVE_NAMES,
20
- REACT_DOM_SOURCES,
21
17
  isReactDomPrimitive,
22
18
  } from '../rules/reactDomPrimitives';
23
19
 
package/src/index.ts CHANGED
@@ -37,12 +37,24 @@ export type { IdealystDocsPluginOptions } from './analyzer/types';
37
37
 
38
38
  // Analyzers
39
39
  export {
40
+ // Platform import analysis
40
41
  analyzePlatformImports,
41
42
  analyzeFiles,
42
43
  summarizeResults,
43
44
  formatViolation,
44
45
  formatViolations,
45
46
  type AnalysisSummary,
47
+ // Component linting (catches issues TypeScript can't)
48
+ lintComponent,
49
+ lintComponents,
50
+ formatLintIssue,
51
+ formatLintResults,
52
+ summarizeLintResults,
53
+ type LintIssueType,
54
+ type LintIssue,
55
+ type LintResult,
56
+ type LintSummary,
57
+ type ComponentLinterOptions,
46
58
  } from './analyzers';
47
59
 
48
60
  // Rules
@@ -86,7 +98,7 @@ export {
86
98
  // Runtime Placeholders - These get replaced by the Vite plugin at build time
87
99
  // =============================================================================
88
100
 
89
- import type { ComponentRegistry, ComponentDefinition } from './analyzer/types';
101
+ import type { ComponentRegistry } from './analyzer/types';
90
102
 
91
103
  /**
92
104
  * Component registry placeholder.
@@ -25,7 +25,7 @@
25
25
  * ```
26
26
  */
27
27
 
28
- import type { Plugin, ViteDevServer } from 'vite';
28
+ import type { Plugin } from 'vite';
29
29
  import * as fs from 'fs';
30
30
  import * as path from 'path';
31
31
  import { analyzeComponents } from './analyzer';
@@ -36,7 +36,6 @@ import type { IdealystDocsPluginOptions, ComponentRegistry } from './analyzer/ty
36
36
  */
37
37
  export function idealystDocsPlugin(options: IdealystDocsPluginOptions): Plugin {
38
38
  let registry: ComponentRegistry | null = null;
39
- let server: ViteDevServer | null = null;
40
39
 
41
40
  const { debug = false, output, outputPath } = options;
42
41
 
@@ -83,10 +82,6 @@ export function idealystDocsPlugin(options: IdealystDocsPluginOptions): Plugin {
83
82
  return {
84
83
  name: 'idealyst-docs',
85
84
 
86
- configureServer(_server) {
87
- server = _server;
88
- },
89
-
90
85
  // Transform @idealyst/tooling to inject the actual registry
91
86
  transform(code, id) {
92
87
  // Check if this is the tooling package's index file