@idealyst/tooling 1.2.3

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.
@@ -0,0 +1,395 @@
1
+ import {
2
+ AnalyzerOptions,
3
+ AnalysisResult,
4
+ FileInput,
5
+ FileType,
6
+ ImportInfo,
7
+ Severity,
8
+ Violation,
9
+ ViolationType,
10
+ } from '../types';
11
+ import { classifyFile } from '../utils/fileClassifier';
12
+ import { parseImports } from '../utils/importParser';
13
+ import {
14
+ REACT_NATIVE_PRIMITIVE_NAMES,
15
+ REACT_NATIVE_SOURCES,
16
+ isReactNativePrimitive,
17
+ } from '../rules/reactNativePrimitives';
18
+ import {
19
+ REACT_DOM_PRIMITIVE_NAMES,
20
+ REACT_DOM_SOURCES,
21
+ isReactDomPrimitive,
22
+ } from '../rules/reactDomPrimitives';
23
+
24
+ /**
25
+ * Default analyzer options
26
+ */
27
+ const DEFAULT_OPTIONS: Required<AnalyzerOptions> = {
28
+ severity: 'error',
29
+ additionalNativePrimitives: [],
30
+ additionalDomPrimitives: [],
31
+ ignoredPrimitives: [],
32
+ ignoredPatterns: [],
33
+ additionalNativeSources: [],
34
+ additionalDomSources: [],
35
+ };
36
+
37
+ /**
38
+ * Check if a file path matches any of the ignored patterns
39
+ */
40
+ function matchesIgnoredPattern(
41
+ filePath: string,
42
+ patterns: string[]
43
+ ): boolean {
44
+ if (patterns.length === 0) return false;
45
+
46
+ for (const pattern of patterns) {
47
+ // Simple glob matching - convert glob to regex
48
+ const regexPattern = pattern
49
+ .replace(/\*\*/g, '{{GLOBSTAR}}')
50
+ .replace(/\*/g, '[^/]*')
51
+ .replace(/{{GLOBSTAR}}/g, '.*')
52
+ .replace(/\?/g, '.');
53
+
54
+ const regex = new RegExp(regexPattern);
55
+ if (regex.test(filePath)) {
56
+ return true;
57
+ }
58
+ }
59
+
60
+ return false;
61
+ }
62
+
63
+ /**
64
+ * Creates a violation object
65
+ */
66
+ function createViolation(
67
+ type: ViolationType,
68
+ primitive: string,
69
+ source: string,
70
+ filePath: string,
71
+ line: number,
72
+ column: number,
73
+ severity: Severity
74
+ ): Violation {
75
+ const messages: Record<ViolationType, string> = {
76
+ 'native-in-shared': `React Native primitive '${primitive}' from '${source}' should not be used in shared files. Use a .native.tsx file instead.`,
77
+ 'dom-in-shared': `React DOM primitive '${primitive}' from '${source}' should not be used in shared files. Use a .web.tsx file instead.`,
78
+ 'native-in-web': `React Native primitive '${primitive}' from '${source}' should not be used in web-specific files.`,
79
+ 'dom-in-native': `React DOM primitive '${primitive}' from '${source}' should not be used in native-specific files.`,
80
+ };
81
+
82
+ return {
83
+ type,
84
+ primitive,
85
+ source,
86
+ filePath,
87
+ line,
88
+ column,
89
+ message: messages[type],
90
+ severity,
91
+ };
92
+ }
93
+
94
+ /**
95
+ * Check if an import should be flagged as a React Native primitive
96
+ */
97
+ function isNativePrimitive(
98
+ importInfo: ImportInfo,
99
+ additionalPrimitives: string[]
100
+ ): boolean {
101
+ const name = importInfo.originalName ?? importInfo.name;
102
+
103
+ // Check built-in primitives
104
+ if (isReactNativePrimitive(name)) return true;
105
+
106
+ // Check additional primitives
107
+ if (additionalPrimitives.includes(name)) return true;
108
+
109
+ // Check if the source is a known React Native source
110
+ const nativeSources: Set<string> = new Set([...REACT_NATIVE_SOURCES]);
111
+ if (nativeSources.has(importInfo.source)) {
112
+ // Any import from react-native that's a component (starts with uppercase)
113
+ if (/^[A-Z]/.test(name)) return true;
114
+ }
115
+
116
+ return false;
117
+ }
118
+
119
+ /**
120
+ * Check if an import should be flagged as a React DOM primitive
121
+ */
122
+ function isDomPrimitive(
123
+ importInfo: ImportInfo,
124
+ additionalPrimitives: string[]
125
+ ): boolean {
126
+ const name = importInfo.originalName ?? importInfo.name;
127
+
128
+ // Check built-in primitives
129
+ if (isReactDomPrimitive(name)) return true;
130
+
131
+ // Check additional primitives
132
+ if (additionalPrimitives.includes(name)) return true;
133
+
134
+ return false;
135
+ }
136
+
137
+ /**
138
+ * Analyze a single file for platform import violations
139
+ *
140
+ * @param filePath - Path to the file being analyzed
141
+ * @param sourceCode - The source code content
142
+ * @param options - Analyzer options
143
+ * @returns Analysis result with violations
144
+ *
145
+ * @example
146
+ * ```typescript
147
+ * const result = analyzePlatformImports(
148
+ * 'src/components/Button.tsx',
149
+ * sourceCode,
150
+ * { severity: 'error' }
151
+ * );
152
+ *
153
+ * if (result.violations.length > 0) {
154
+ * for (const v of result.violations) {
155
+ * console.error(`${v.filePath}:${v.line}:${v.column} - ${v.message}`);
156
+ * }
157
+ * }
158
+ * ```
159
+ */
160
+ export function analyzePlatformImports(
161
+ filePath: string,
162
+ sourceCode: string,
163
+ options?: AnalyzerOptions
164
+ ): AnalysisResult {
165
+ const opts = { ...DEFAULT_OPTIONS, ...options };
166
+ const fileType = classifyFile(filePath);
167
+ const violations: Violation[] = [];
168
+
169
+ // Skip ignored files
170
+ if (matchesIgnoredPattern(filePath, opts.ignoredPatterns)) {
171
+ return {
172
+ filePath,
173
+ fileType,
174
+ violations: [],
175
+ imports: [],
176
+ passed: true,
177
+ };
178
+ }
179
+
180
+ // Skip non-component files
181
+ if (fileType === 'other' || fileType === 'styles' || fileType === 'types') {
182
+ return {
183
+ filePath,
184
+ fileType,
185
+ violations: [],
186
+ imports: [],
187
+ passed: true,
188
+ };
189
+ }
190
+
191
+ // Parse imports
192
+ const imports = parseImports(sourceCode, filePath, {
193
+ additionalNativeSources: opts.additionalNativeSources,
194
+ additionalDomSources: opts.additionalDomSources,
195
+ });
196
+
197
+ // Build ignored primitives set
198
+ const ignoredPrimitives = new Set(opts.ignoredPrimitives);
199
+
200
+ // Analyze each import
201
+ for (const imp of imports) {
202
+ // Skip type-only imports
203
+ if (imp.isTypeOnly) continue;
204
+
205
+ // Skip ignored primitives
206
+ const primitiveName = imp.originalName ?? imp.name;
207
+ if (ignoredPrimitives.has(primitiveName)) continue;
208
+
209
+ // Check for violations based on file type
210
+ switch (fileType) {
211
+ case 'shared':
212
+ // Shared files should not use platform-specific imports
213
+ if (isNativePrimitive(imp, opts.additionalNativePrimitives)) {
214
+ violations.push(
215
+ createViolation(
216
+ 'native-in-shared',
217
+ primitiveName,
218
+ imp.source,
219
+ filePath,
220
+ imp.line,
221
+ imp.column,
222
+ opts.severity
223
+ )
224
+ );
225
+ }
226
+ if (isDomPrimitive(imp, opts.additionalDomPrimitives)) {
227
+ violations.push(
228
+ createViolation(
229
+ 'dom-in-shared',
230
+ primitiveName,
231
+ imp.source,
232
+ filePath,
233
+ imp.line,
234
+ imp.column,
235
+ opts.severity
236
+ )
237
+ );
238
+ }
239
+ break;
240
+
241
+ case 'web':
242
+ // Web files should not use React Native imports
243
+ if (isNativePrimitive(imp, opts.additionalNativePrimitives)) {
244
+ violations.push(
245
+ createViolation(
246
+ 'native-in-web',
247
+ primitiveName,
248
+ imp.source,
249
+ filePath,
250
+ imp.line,
251
+ imp.column,
252
+ opts.severity
253
+ )
254
+ );
255
+ }
256
+ break;
257
+
258
+ case 'native':
259
+ // Native files should not use React DOM imports
260
+ if (isDomPrimitive(imp, opts.additionalDomPrimitives)) {
261
+ violations.push(
262
+ createViolation(
263
+ 'dom-in-native',
264
+ primitiveName,
265
+ imp.source,
266
+ filePath,
267
+ imp.line,
268
+ imp.column,
269
+ opts.severity
270
+ )
271
+ );
272
+ }
273
+ break;
274
+ }
275
+ }
276
+
277
+ return {
278
+ filePath,
279
+ fileType,
280
+ violations,
281
+ imports,
282
+ passed: violations.length === 0,
283
+ };
284
+ }
285
+
286
+ /**
287
+ * Analyze multiple files for platform import violations
288
+ *
289
+ * @param files - Array of files to analyze
290
+ * @param options - Analyzer options
291
+ * @returns Array of analysis results
292
+ *
293
+ * @example
294
+ * ```typescript
295
+ * const results = analyzeFiles(
296
+ * [
297
+ * { path: 'Button.tsx', content: buttonSource },
298
+ * { path: 'Button.web.tsx', content: webSource },
299
+ * { path: 'Button.native.tsx', content: nativeSource },
300
+ * ],
301
+ * { severity: 'warning' }
302
+ * );
303
+ *
304
+ * const failed = results.filter(r => !r.passed);
305
+ * ```
306
+ */
307
+ export function analyzeFiles(
308
+ files: FileInput[],
309
+ options?: AnalyzerOptions
310
+ ): AnalysisResult[] {
311
+ return files.map((file) =>
312
+ analyzePlatformImports(file.path, file.content, options)
313
+ );
314
+ }
315
+
316
+ /**
317
+ * Get a summary of analysis results
318
+ */
319
+ export interface AnalysisSummary {
320
+ totalFiles: number;
321
+ passedFiles: number;
322
+ failedFiles: number;
323
+ totalViolations: number;
324
+ violationsByType: Record<ViolationType, number>;
325
+ violationsBySeverity: Record<Severity, number>;
326
+ }
327
+
328
+ /**
329
+ * Summarize analysis results
330
+ *
331
+ * @param results - Array of analysis results
332
+ * @returns Summary statistics
333
+ */
334
+ export function summarizeResults(results: AnalysisResult[]): AnalysisSummary {
335
+ const violationsByType: Record<ViolationType, number> = {
336
+ 'native-in-shared': 0,
337
+ 'dom-in-shared': 0,
338
+ 'native-in-web': 0,
339
+ 'dom-in-native': 0,
340
+ };
341
+
342
+ const violationsBySeverity: Record<Severity, number> = {
343
+ error: 0,
344
+ warning: 0,
345
+ info: 0,
346
+ };
347
+
348
+ let totalViolations = 0;
349
+
350
+ for (const result of results) {
351
+ for (const violation of result.violations) {
352
+ totalViolations++;
353
+ violationsByType[violation.type]++;
354
+ violationsBySeverity[violation.severity]++;
355
+ }
356
+ }
357
+
358
+ return {
359
+ totalFiles: results.length,
360
+ passedFiles: results.filter((r) => r.passed).length,
361
+ failedFiles: results.filter((r) => !r.passed).length,
362
+ totalViolations,
363
+ violationsByType,
364
+ violationsBySeverity,
365
+ };
366
+ }
367
+
368
+ /**
369
+ * Format a violation for console output
370
+ */
371
+ export function formatViolation(violation: Violation): string {
372
+ const severityPrefix =
373
+ violation.severity === 'error'
374
+ ? 'ERROR'
375
+ : violation.severity === 'warning'
376
+ ? 'WARN'
377
+ : 'INFO';
378
+
379
+ return `${severityPrefix}: ${violation.filePath}:${violation.line}:${violation.column} - ${violation.message}`;
380
+ }
381
+
382
+ /**
383
+ * Format all violations from results for console output
384
+ */
385
+ export function formatViolations(results: AnalysisResult[]): string[] {
386
+ const lines: string[] = [];
387
+
388
+ for (const result of results) {
389
+ for (const violation of result.violations) {
390
+ lines.push(formatViolation(violation));
391
+ }
392
+ }
393
+
394
+ return lines;
395
+ }
package/src/index.ts ADDED
@@ -0,0 +1,142 @@
1
+ /**
2
+ * @idealyst/tooling
3
+ *
4
+ * Code analysis and validation utilities for Idealyst Framework.
5
+ * Provides tools for babel plugins, CLI, and MCP to validate cross-platform code.
6
+ *
7
+ * Also provides component documentation generation:
8
+ * - analyzeComponents(): Generate a component registry from TypeScript source
9
+ * - analyzeTheme(): Extract theme values (intents, sizes, etc.)
10
+ * - idealystDocsPlugin(): Vite plugin for virtual module support
11
+ */
12
+
13
+ // Types
14
+ export * from './types';
15
+
16
+ // Component Documentation (also available via @idealyst/tooling/docs)
17
+ export {
18
+ analyzeComponents,
19
+ analyzeTheme,
20
+ // Babel plugin compatibility
21
+ loadThemeKeys,
22
+ resetThemeCache,
23
+ type BabelThemeKeys,
24
+ // Types
25
+ type ComponentRegistry,
26
+ type ComponentDefinition,
27
+ type PropDefinition,
28
+ type ThemeValues,
29
+ type ComponentAnalyzerOptions,
30
+ } from './analyzer';
31
+
32
+ // Vite Plugin (also available via @idealyst/tooling/vite)
33
+ export { idealystDocsPlugin, generateComponentRegistry } from './vite-plugin';
34
+ export type { IdealystDocsPluginOptions } from './analyzer/types';
35
+
36
+ // Analyzers
37
+ export {
38
+ analyzePlatformImports,
39
+ analyzeFiles,
40
+ summarizeResults,
41
+ formatViolation,
42
+ formatViolations,
43
+ type AnalysisSummary,
44
+ } from './analyzers';
45
+
46
+ // Rules
47
+ export {
48
+ // React Native
49
+ REACT_NATIVE_SOURCES,
50
+ REACT_NATIVE_PRIMITIVES,
51
+ REACT_NATIVE_PRIMITIVE_NAMES,
52
+ REACT_NATIVE_RULE_SET,
53
+ isReactNativePrimitive,
54
+ getReactNativePrimitive,
55
+ // React DOM
56
+ REACT_DOM_SOURCES,
57
+ REACT_DOM_PRIMITIVES,
58
+ REACT_DOM_PRIMITIVE_NAMES,
59
+ REACT_DOM_RULE_SET,
60
+ HTML_INTRINSIC_ELEMENTS,
61
+ HTML_ELEMENT_NAMES,
62
+ isReactDomPrimitive,
63
+ isHtmlElement,
64
+ getReactDomPrimitive,
65
+ } from './rules';
66
+
67
+ // Utilities
68
+ export {
69
+ classifyFile,
70
+ isComponentFile,
71
+ isSharedFile,
72
+ isPlatformSpecificFile,
73
+ getExpectedPlatform,
74
+ getBaseName,
75
+ parseImports,
76
+ getPlatformForSource,
77
+ filterPlatformImports,
78
+ getUniqueSources,
79
+ groupImportsBySource,
80
+ type ImportParserOptions,
81
+ } from './utils';
82
+
83
+ // =============================================================================
84
+ // Runtime Placeholders - These get replaced by the Vite plugin at build time
85
+ // =============================================================================
86
+
87
+ import type { ComponentRegistry, ComponentDefinition } from './analyzer/types';
88
+
89
+ /**
90
+ * Component registry placeholder.
91
+ * This empty object is replaced at build time by the idealystDocsPlugin
92
+ * with the actual component metadata extracted from your codebase.
93
+ *
94
+ * @example
95
+ * ```ts
96
+ * import { componentRegistry } from '@idealyst/tooling';
97
+ *
98
+ * // Access component definitions
99
+ * const buttonDef = componentRegistry['Button'];
100
+ * console.log(buttonDef.description);
101
+ * console.log(buttonDef.props);
102
+ * ```
103
+ */
104
+ export const componentRegistry: ComponentRegistry = {};
105
+
106
+ /**
107
+ * List of all component names in the registry.
108
+ * Replaced at build time by the Vite plugin.
109
+ */
110
+ export const componentNames: string[] = [];
111
+
112
+ /**
113
+ * Get components filtered by category.
114
+ * Replaced at build time by the Vite plugin.
115
+ */
116
+ export function getComponentsByCategory(category: string): string[] {
117
+ return Object.entries(componentRegistry)
118
+ .filter(([_, def]) => def.category === category)
119
+ .map(([name]) => name);
120
+ }
121
+
122
+ /**
123
+ * Get prop configuration for a component (useful for playgrounds).
124
+ * Replaced at build time by the Vite plugin.
125
+ */
126
+ export function getPropConfig(componentName: string): Record<string, any> {
127
+ const def = componentRegistry[componentName];
128
+ if (!def) return {};
129
+
130
+ return Object.entries(def.props).reduce((acc, [key, prop]) => {
131
+ if (prop.values && prop.values.length > 0) {
132
+ acc[key] = { type: 'select', options: prop.values, default: prop.default };
133
+ } else if (prop.type === 'boolean') {
134
+ acc[key] = { type: 'boolean', default: prop.default ?? false };
135
+ } else if (prop.type === 'string') {
136
+ acc[key] = { type: 'text', default: prop.default ?? '' };
137
+ } else if (prop.type === 'number') {
138
+ acc[key] = { type: 'number', default: prop.default ?? 0 };
139
+ }
140
+ return acc;
141
+ }, {} as Record<string, any>);
142
+ }
@@ -0,0 +1,2 @@
1
+ export * from './reactNativePrimitives';
2
+ export * from './reactDomPrimitives';