@idealyst/tooling 1.2.23 → 1.2.25

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,1155 @@
1
+ // src/utils/fileClassifier.ts
2
+ import * as path from "path";
3
+ var EXTENSION_PATTERNS = [
4
+ // Platform-specific component files
5
+ { pattern: /\.web\.(tsx|jsx)$/, type: "web" },
6
+ { pattern: /\.native\.(tsx|jsx)$/, type: "native" },
7
+ { pattern: /\.ios\.(tsx|jsx)$/, type: "native" },
8
+ { pattern: /\.android\.(tsx|jsx)$/, type: "native" },
9
+ // Style files (can be .ts or .tsx)
10
+ { pattern: /\.styles?\.(tsx?|jsx?)$/, type: "styles" },
11
+ // Type definition files
12
+ { pattern: /\.types?\.(ts|tsx)$/, type: "types" },
13
+ { pattern: /types\.(ts|tsx)$/, type: "types" },
14
+ { pattern: /\.d\.ts$/, type: "types" },
15
+ // Shared component files (generic .tsx/.jsx without platform suffix)
16
+ { pattern: /\.(tsx|jsx)$/, type: "shared" }
17
+ ];
18
+ var EXCLUDED_PATTERNS = [
19
+ /\.test\.(tsx?|jsx?)$/,
20
+ /\.spec\.(tsx?|jsx?)$/,
21
+ /\.stories\.(tsx?|jsx?)$/,
22
+ /\.config\.(ts|js)$/,
23
+ /index\.(ts|tsx|js|jsx)$/
24
+ ];
25
+ function classifyFile(filePath) {
26
+ const fileName = path.basename(filePath);
27
+ for (const pattern of EXCLUDED_PATTERNS) {
28
+ if (pattern.test(fileName)) {
29
+ return "other";
30
+ }
31
+ }
32
+ for (const { pattern, type } of EXTENSION_PATTERNS) {
33
+ if (pattern.test(fileName)) {
34
+ return type;
35
+ }
36
+ }
37
+ return "other";
38
+ }
39
+
40
+ // src/utils/importParser.ts
41
+ import * as ts from "typescript";
42
+
43
+ // src/rules/reactNativePrimitives.ts
44
+ var REACT_NATIVE_SOURCES = [
45
+ "react-native",
46
+ "react-native-web",
47
+ "react-native-gesture-handler",
48
+ "react-native-reanimated",
49
+ "react-native-safe-area-context",
50
+ "react-native-screens",
51
+ "react-native-svg",
52
+ "@react-native-vector-icons/material-design-icons",
53
+ "@react-native-vector-icons/common",
54
+ "@react-native-community/async-storage",
55
+ "@react-native-picker/picker",
56
+ "expo",
57
+ "expo-constants",
58
+ "expo-linking",
59
+ "expo-status-bar"
60
+ ];
61
+ var CORE_PRIMITIVES = [
62
+ {
63
+ name: "View",
64
+ source: "react-native",
65
+ platform: "react-native",
66
+ description: "Basic container component"
67
+ },
68
+ {
69
+ name: "Text",
70
+ source: "react-native",
71
+ platform: "react-native",
72
+ description: "Text display component"
73
+ },
74
+ {
75
+ name: "Image",
76
+ source: "react-native",
77
+ platform: "react-native",
78
+ description: "Image display component"
79
+ },
80
+ {
81
+ name: "ImageBackground",
82
+ source: "react-native",
83
+ platform: "react-native",
84
+ description: "Background image container"
85
+ },
86
+ {
87
+ name: "ScrollView",
88
+ source: "react-native",
89
+ platform: "react-native",
90
+ description: "Scrollable container"
91
+ },
92
+ {
93
+ name: "FlatList",
94
+ source: "react-native",
95
+ platform: "react-native",
96
+ description: "Performant list component"
97
+ },
98
+ {
99
+ name: "SectionList",
100
+ source: "react-native",
101
+ platform: "react-native",
102
+ description: "Sectioned list component"
103
+ },
104
+ {
105
+ name: "VirtualizedList",
106
+ source: "react-native",
107
+ platform: "react-native",
108
+ description: "Base virtualized list"
109
+ }
110
+ ];
111
+ var INTERACTIVE_PRIMITIVES = [
112
+ {
113
+ name: "TouchableOpacity",
114
+ source: "react-native",
115
+ platform: "react-native",
116
+ description: "Touch with opacity feedback"
117
+ },
118
+ {
119
+ name: "TouchableHighlight",
120
+ source: "react-native",
121
+ platform: "react-native",
122
+ description: "Touch with highlight feedback"
123
+ },
124
+ {
125
+ name: "TouchableWithoutFeedback",
126
+ source: "react-native",
127
+ platform: "react-native",
128
+ description: "Touch without visual feedback"
129
+ },
130
+ {
131
+ name: "TouchableNativeFeedback",
132
+ source: "react-native",
133
+ platform: "react-native",
134
+ description: "Android ripple feedback"
135
+ },
136
+ {
137
+ name: "Pressable",
138
+ source: "react-native",
139
+ platform: "react-native",
140
+ description: "Modern press handler component"
141
+ },
142
+ {
143
+ name: "Button",
144
+ source: "react-native",
145
+ platform: "react-native",
146
+ description: "Basic button component"
147
+ }
148
+ ];
149
+ var INPUT_PRIMITIVES = [
150
+ {
151
+ name: "TextInput",
152
+ source: "react-native",
153
+ platform: "react-native",
154
+ description: "Text input field"
155
+ },
156
+ {
157
+ name: "Switch",
158
+ source: "react-native",
159
+ platform: "react-native",
160
+ description: "Toggle switch component"
161
+ },
162
+ {
163
+ name: "Slider",
164
+ source: "react-native",
165
+ platform: "react-native",
166
+ description: "Slider input (deprecated)"
167
+ }
168
+ ];
169
+ var MODAL_PRIMITIVES = [
170
+ {
171
+ name: "Modal",
172
+ source: "react-native",
173
+ platform: "react-native",
174
+ description: "Modal overlay component"
175
+ },
176
+ {
177
+ name: "Alert",
178
+ source: "react-native",
179
+ platform: "react-native",
180
+ description: "Native alert dialog"
181
+ },
182
+ {
183
+ name: "ActionSheetIOS",
184
+ source: "react-native",
185
+ platform: "react-native",
186
+ description: "iOS action sheet"
187
+ },
188
+ {
189
+ name: "StatusBar",
190
+ source: "react-native",
191
+ platform: "react-native",
192
+ description: "Status bar controller"
193
+ }
194
+ ];
195
+ var ANIMATION_PRIMITIVES = [
196
+ {
197
+ name: "Animated",
198
+ source: "react-native",
199
+ platform: "react-native",
200
+ description: "Animation library namespace"
201
+ },
202
+ {
203
+ name: "Easing",
204
+ source: "react-native",
205
+ platform: "react-native",
206
+ description: "Easing functions"
207
+ },
208
+ {
209
+ name: "LayoutAnimation",
210
+ source: "react-native",
211
+ platform: "react-native",
212
+ description: "Layout animation controller"
213
+ }
214
+ ];
215
+ var PLATFORM_PRIMITIVES = [
216
+ {
217
+ name: "Platform",
218
+ source: "react-native",
219
+ platform: "react-native",
220
+ description: "Platform detection utility"
221
+ },
222
+ {
223
+ name: "Dimensions",
224
+ source: "react-native",
225
+ platform: "react-native",
226
+ description: "Screen dimensions utility"
227
+ },
228
+ {
229
+ name: "PixelRatio",
230
+ source: "react-native",
231
+ platform: "react-native",
232
+ description: "Pixel ratio utility"
233
+ },
234
+ {
235
+ name: "Appearance",
236
+ source: "react-native",
237
+ platform: "react-native",
238
+ description: "Color scheme detection"
239
+ },
240
+ {
241
+ name: "useColorScheme",
242
+ source: "react-native",
243
+ platform: "react-native",
244
+ description: "Color scheme hook"
245
+ },
246
+ {
247
+ name: "useWindowDimensions",
248
+ source: "react-native",
249
+ platform: "react-native",
250
+ description: "Window dimensions hook"
251
+ }
252
+ ];
253
+ var EVENT_PRIMITIVES = [
254
+ {
255
+ name: "BackHandler",
256
+ source: "react-native",
257
+ platform: "react-native",
258
+ description: "Android back button handler"
259
+ },
260
+ {
261
+ name: "Keyboard",
262
+ source: "react-native",
263
+ platform: "react-native",
264
+ description: "Keyboard event handler"
265
+ },
266
+ {
267
+ name: "PanResponder",
268
+ source: "react-native",
269
+ platform: "react-native",
270
+ description: "Gesture responder system"
271
+ },
272
+ {
273
+ name: "Linking",
274
+ source: "react-native",
275
+ platform: "react-native",
276
+ description: "Deep linking utility"
277
+ },
278
+ {
279
+ name: "AppState",
280
+ source: "react-native",
281
+ platform: "react-native",
282
+ description: "App lifecycle state"
283
+ }
284
+ ];
285
+ var SAFETY_PRIMITIVES = [
286
+ {
287
+ name: "SafeAreaView",
288
+ source: "react-native",
289
+ platform: "react-native",
290
+ description: "Safe area inset container"
291
+ },
292
+ {
293
+ name: "KeyboardAvoidingView",
294
+ source: "react-native",
295
+ platform: "react-native",
296
+ description: "Keyboard avoidance container"
297
+ }
298
+ ];
299
+ var ACCESSIBILITY_PRIMITIVES = [
300
+ {
301
+ name: "AccessibilityInfo",
302
+ source: "react-native",
303
+ platform: "react-native",
304
+ description: "Accessibility information API"
305
+ }
306
+ ];
307
+ var STYLE_PRIMITIVES = [
308
+ {
309
+ name: "StyleSheet",
310
+ source: "react-native",
311
+ platform: "react-native",
312
+ description: "Style sheet creator"
313
+ }
314
+ ];
315
+ var REACT_NATIVE_PRIMITIVES = [
316
+ ...CORE_PRIMITIVES,
317
+ ...INTERACTIVE_PRIMITIVES,
318
+ ...INPUT_PRIMITIVES,
319
+ ...MODAL_PRIMITIVES,
320
+ ...ANIMATION_PRIMITIVES,
321
+ ...PLATFORM_PRIMITIVES,
322
+ ...EVENT_PRIMITIVES,
323
+ ...SAFETY_PRIMITIVES,
324
+ ...ACCESSIBILITY_PRIMITIVES,
325
+ ...STYLE_PRIMITIVES
326
+ ];
327
+ var REACT_NATIVE_PRIMITIVE_NAMES = new Set(
328
+ REACT_NATIVE_PRIMITIVES.map((p) => p.name)
329
+ );
330
+ var REACT_NATIVE_RULE_SET = {
331
+ platform: "react-native",
332
+ primitives: REACT_NATIVE_PRIMITIVES,
333
+ sources: [...REACT_NATIVE_SOURCES]
334
+ };
335
+ function isReactNativePrimitive(name) {
336
+ return REACT_NATIVE_PRIMITIVE_NAMES.has(name);
337
+ }
338
+
339
+ // src/rules/reactDomPrimitives.ts
340
+ var REACT_DOM_SOURCES = [
341
+ "react-dom",
342
+ "react-dom/client",
343
+ "react-dom/server"
344
+ ];
345
+ var DOM_API_PRIMITIVES = [
346
+ {
347
+ name: "createPortal",
348
+ source: "react-dom",
349
+ platform: "react-dom",
350
+ description: "Render children into a different DOM node"
351
+ },
352
+ {
353
+ name: "flushSync",
354
+ source: "react-dom",
355
+ platform: "react-dom",
356
+ description: "Force synchronous DOM updates"
357
+ },
358
+ {
359
+ name: "createRoot",
360
+ source: "react-dom/client",
361
+ platform: "react-dom",
362
+ description: "Create a React root for rendering"
363
+ },
364
+ {
365
+ name: "hydrateRoot",
366
+ source: "react-dom/client",
367
+ platform: "react-dom",
368
+ description: "Hydrate server-rendered content"
369
+ },
370
+ {
371
+ name: "render",
372
+ source: "react-dom",
373
+ platform: "react-dom",
374
+ description: "Legacy render function (deprecated)"
375
+ },
376
+ {
377
+ name: "hydrate",
378
+ source: "react-dom",
379
+ platform: "react-dom",
380
+ description: "Legacy hydrate function (deprecated)"
381
+ },
382
+ {
383
+ name: "unmountComponentAtNode",
384
+ source: "react-dom",
385
+ platform: "react-dom",
386
+ description: "Legacy unmount function (deprecated)"
387
+ },
388
+ {
389
+ name: "findDOMNode",
390
+ source: "react-dom",
391
+ platform: "react-dom",
392
+ description: "Find DOM node (deprecated)"
393
+ }
394
+ ];
395
+ var HTML_INTRINSIC_ELEMENTS = [
396
+ // Document structure
397
+ "html",
398
+ "head",
399
+ "body",
400
+ "main",
401
+ "header",
402
+ "footer",
403
+ "nav",
404
+ "aside",
405
+ "article",
406
+ "section",
407
+ // Content sectioning
408
+ "div",
409
+ "span",
410
+ "p",
411
+ "h1",
412
+ "h2",
413
+ "h3",
414
+ "h4",
415
+ "h5",
416
+ "h6",
417
+ // Text content
418
+ "a",
419
+ "strong",
420
+ "em",
421
+ "code",
422
+ "pre",
423
+ "blockquote",
424
+ "br",
425
+ "hr",
426
+ // Lists
427
+ "ul",
428
+ "ol",
429
+ "li",
430
+ "dl",
431
+ "dt",
432
+ "dd",
433
+ // Tables
434
+ "table",
435
+ "thead",
436
+ "tbody",
437
+ "tfoot",
438
+ "tr",
439
+ "th",
440
+ "td",
441
+ "caption",
442
+ "colgroup",
443
+ "col",
444
+ // Forms
445
+ "form",
446
+ "input",
447
+ "textarea",
448
+ "select",
449
+ "option",
450
+ "optgroup",
451
+ "button",
452
+ "label",
453
+ "fieldset",
454
+ "legend",
455
+ "datalist",
456
+ "output",
457
+ "progress",
458
+ "meter",
459
+ // Media
460
+ "img",
461
+ "video",
462
+ "audio",
463
+ "source",
464
+ "track",
465
+ "picture",
466
+ "figure",
467
+ "figcaption",
468
+ "canvas",
469
+ "svg",
470
+ "iframe",
471
+ "embed",
472
+ "object",
473
+ // Interactive
474
+ "details",
475
+ "summary",
476
+ "dialog",
477
+ "menu",
478
+ // Scripting
479
+ "script",
480
+ "noscript",
481
+ "template",
482
+ "slot"
483
+ ];
484
+ var REACT_DOM_PRIMITIVES = [...DOM_API_PRIMITIVES];
485
+ var REACT_DOM_PRIMITIVE_NAMES = new Set(
486
+ REACT_DOM_PRIMITIVES.map((p) => p.name)
487
+ );
488
+ var HTML_ELEMENT_NAMES = new Set(HTML_INTRINSIC_ELEMENTS);
489
+ var REACT_DOM_RULE_SET = {
490
+ platform: "react-dom",
491
+ primitives: REACT_DOM_PRIMITIVES,
492
+ sources: [...REACT_DOM_SOURCES]
493
+ };
494
+ function isReactDomPrimitive(name) {
495
+ return REACT_DOM_PRIMITIVE_NAMES.has(name);
496
+ }
497
+
498
+ // src/utils/importParser.ts
499
+ function getPlatformForSource(source, options) {
500
+ const nativeSources = /* @__PURE__ */ new Set([
501
+ ...REACT_NATIVE_SOURCES,
502
+ ...options?.additionalNativeSources ?? []
503
+ ]);
504
+ const domSources = /* @__PURE__ */ new Set([
505
+ ...REACT_DOM_SOURCES,
506
+ ...options?.additionalDomSources ?? []
507
+ ]);
508
+ if (nativeSources.has(source)) return "react-native";
509
+ if (domSources.has(source)) return "react-dom";
510
+ if (source.startsWith("react-native")) return "react-native";
511
+ if (source.startsWith("react-dom")) return "react-dom";
512
+ return "neutral";
513
+ }
514
+ function parseImports(sourceCode, filePath = "unknown.tsx", options) {
515
+ const imports = [];
516
+ const sourceFile = ts.createSourceFile(
517
+ filePath,
518
+ sourceCode,
519
+ ts.ScriptTarget.Latest,
520
+ true,
521
+ filePath.endsWith(".tsx") || filePath.endsWith(".jsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS
522
+ );
523
+ const visit = (node) => {
524
+ if (ts.isImportDeclaration(node)) {
525
+ const importInfo = parseImportDeclaration(node, sourceFile, options);
526
+ imports.push(...importInfo);
527
+ }
528
+ if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === "require" && node.arguments.length === 1 && ts.isStringLiteral(node.arguments[0])) {
529
+ const source = node.arguments[0].text;
530
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(
531
+ node.getStart()
532
+ );
533
+ imports.push({
534
+ name: "require",
535
+ source,
536
+ platform: getPlatformForSource(source, options),
537
+ line: line + 1,
538
+ column: character + 1,
539
+ isDefault: false,
540
+ isNamespace: true,
541
+ isTypeOnly: false
542
+ });
543
+ }
544
+ ts.forEachChild(node, visit);
545
+ };
546
+ visit(sourceFile);
547
+ return imports;
548
+ }
549
+ function parseImportDeclaration(node, sourceFile, options) {
550
+ const imports = [];
551
+ if (!ts.isStringLiteral(node.moduleSpecifier)) {
552
+ return imports;
553
+ }
554
+ const source = node.moduleSpecifier.text;
555
+ const platform = getPlatformForSource(source, options);
556
+ const isTypeOnly = node.importClause?.isTypeOnly ?? false;
557
+ const importClause = node.importClause;
558
+ if (!importClause) {
559
+ return imports;
560
+ }
561
+ if (importClause.name) {
562
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(
563
+ importClause.name.getStart()
564
+ );
565
+ imports.push({
566
+ name: importClause.name.text,
567
+ source,
568
+ platform,
569
+ line: line + 1,
570
+ column: character + 1,
571
+ isDefault: true,
572
+ isNamespace: false,
573
+ isTypeOnly
574
+ });
575
+ }
576
+ const namedBindings = importClause.namedBindings;
577
+ if (namedBindings) {
578
+ if (ts.isNamespaceImport(namedBindings)) {
579
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(
580
+ namedBindings.name.getStart()
581
+ );
582
+ imports.push({
583
+ name: namedBindings.name.text,
584
+ source,
585
+ platform,
586
+ line: line + 1,
587
+ column: character + 1,
588
+ isDefault: false,
589
+ isNamespace: true,
590
+ isTypeOnly
591
+ });
592
+ } else if (ts.isNamedImports(namedBindings)) {
593
+ for (const element of namedBindings.elements) {
594
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(
595
+ element.name.getStart()
596
+ );
597
+ const importedName = element.propertyName?.text ?? element.name.text;
598
+ const localName = element.name.text;
599
+ imports.push({
600
+ name: localName,
601
+ originalName: element.propertyName ? importedName : void 0,
602
+ source,
603
+ platform,
604
+ line: line + 1,
605
+ column: character + 1,
606
+ isDefault: false,
607
+ isNamespace: false,
608
+ isTypeOnly: isTypeOnly || element.isTypeOnly
609
+ });
610
+ }
611
+ }
612
+ }
613
+ return imports;
614
+ }
615
+
616
+ // src/analyzers/platformImports.ts
617
+ var DEFAULT_OPTIONS = {
618
+ severity: "error",
619
+ additionalNativePrimitives: [],
620
+ additionalDomPrimitives: [],
621
+ ignoredPrimitives: [],
622
+ ignoredPatterns: [],
623
+ additionalNativeSources: [],
624
+ additionalDomSources: []
625
+ };
626
+ function matchesIgnoredPattern(filePath, patterns) {
627
+ if (patterns.length === 0) return false;
628
+ for (const pattern of patterns) {
629
+ const regexPattern = pattern.replace(/\*\*/g, "{{GLOBSTAR}}").replace(/\*/g, "[^/]*").replace(/{{GLOBSTAR}}/g, ".*").replace(/\?/g, ".");
630
+ const regex = new RegExp(regexPattern);
631
+ if (regex.test(filePath)) {
632
+ return true;
633
+ }
634
+ }
635
+ return false;
636
+ }
637
+ function createViolation(type, primitive, source, filePath, line, column, severity) {
638
+ const messages = {
639
+ "native-in-shared": `React Native primitive '${primitive}' from '${source}' should not be used in shared files. Use a .native.tsx file instead.`,
640
+ "dom-in-shared": `React DOM primitive '${primitive}' from '${source}' should not be used in shared files. Use a .web.tsx file instead.`,
641
+ "native-in-web": `React Native primitive '${primitive}' from '${source}' should not be used in web-specific files.`,
642
+ "dom-in-native": `React DOM primitive '${primitive}' from '${source}' should not be used in native-specific files.`
643
+ };
644
+ return {
645
+ type,
646
+ primitive,
647
+ source,
648
+ filePath,
649
+ line,
650
+ column,
651
+ message: messages[type],
652
+ severity
653
+ };
654
+ }
655
+ function isNativePrimitive(importInfo, additionalPrimitives) {
656
+ const name = importInfo.originalName ?? importInfo.name;
657
+ if (isReactNativePrimitive(name)) return true;
658
+ if (additionalPrimitives.includes(name)) return true;
659
+ const nativeSources = /* @__PURE__ */ new Set([...REACT_NATIVE_SOURCES]);
660
+ if (nativeSources.has(importInfo.source)) {
661
+ if (/^[A-Z]/.test(name)) return true;
662
+ }
663
+ return false;
664
+ }
665
+ function isDomPrimitive(importInfo, additionalPrimitives) {
666
+ const name = importInfo.originalName ?? importInfo.name;
667
+ if (isReactDomPrimitive(name)) return true;
668
+ if (additionalPrimitives.includes(name)) return true;
669
+ return false;
670
+ }
671
+ function analyzePlatformImports(filePath, sourceCode, options) {
672
+ const opts = { ...DEFAULT_OPTIONS, ...options };
673
+ const fileType = classifyFile(filePath);
674
+ const violations = [];
675
+ if (matchesIgnoredPattern(filePath, opts.ignoredPatterns)) {
676
+ return {
677
+ filePath,
678
+ fileType,
679
+ violations: [],
680
+ imports: [],
681
+ passed: true
682
+ };
683
+ }
684
+ if (fileType === "other" || fileType === "styles" || fileType === "types") {
685
+ return {
686
+ filePath,
687
+ fileType,
688
+ violations: [],
689
+ imports: [],
690
+ passed: true
691
+ };
692
+ }
693
+ const imports = parseImports(sourceCode, filePath, {
694
+ additionalNativeSources: opts.additionalNativeSources,
695
+ additionalDomSources: opts.additionalDomSources
696
+ });
697
+ const ignoredPrimitives = new Set(opts.ignoredPrimitives);
698
+ for (const imp of imports) {
699
+ if (imp.isTypeOnly) continue;
700
+ const primitiveName = imp.originalName ?? imp.name;
701
+ if (ignoredPrimitives.has(primitiveName)) continue;
702
+ switch (fileType) {
703
+ case "shared":
704
+ if (isNativePrimitive(imp, opts.additionalNativePrimitives)) {
705
+ violations.push(
706
+ createViolation(
707
+ "native-in-shared",
708
+ primitiveName,
709
+ imp.source,
710
+ filePath,
711
+ imp.line,
712
+ imp.column,
713
+ opts.severity
714
+ )
715
+ );
716
+ }
717
+ if (isDomPrimitive(imp, opts.additionalDomPrimitives)) {
718
+ violations.push(
719
+ createViolation(
720
+ "dom-in-shared",
721
+ primitiveName,
722
+ imp.source,
723
+ filePath,
724
+ imp.line,
725
+ imp.column,
726
+ opts.severity
727
+ )
728
+ );
729
+ }
730
+ break;
731
+ case "web":
732
+ if (isNativePrimitive(imp, opts.additionalNativePrimitives)) {
733
+ violations.push(
734
+ createViolation(
735
+ "native-in-web",
736
+ primitiveName,
737
+ imp.source,
738
+ filePath,
739
+ imp.line,
740
+ imp.column,
741
+ opts.severity
742
+ )
743
+ );
744
+ }
745
+ break;
746
+ case "native":
747
+ if (isDomPrimitive(imp, opts.additionalDomPrimitives)) {
748
+ violations.push(
749
+ createViolation(
750
+ "dom-in-native",
751
+ primitiveName,
752
+ imp.source,
753
+ filePath,
754
+ imp.line,
755
+ imp.column,
756
+ opts.severity
757
+ )
758
+ );
759
+ }
760
+ break;
761
+ }
762
+ }
763
+ return {
764
+ filePath,
765
+ fileType,
766
+ violations,
767
+ imports,
768
+ passed: violations.length === 0
769
+ };
770
+ }
771
+ function analyzeFiles(files, options) {
772
+ return files.map(
773
+ (file) => analyzePlatformImports(file.path, file.content, options)
774
+ );
775
+ }
776
+ function summarizeResults(results) {
777
+ const violationsByType = {
778
+ "native-in-shared": 0,
779
+ "dom-in-shared": 0,
780
+ "native-in-web": 0,
781
+ "dom-in-native": 0
782
+ };
783
+ const violationsBySeverity = {
784
+ error: 0,
785
+ warning: 0,
786
+ info: 0
787
+ };
788
+ let totalViolations = 0;
789
+ for (const result of results) {
790
+ for (const violation of result.violations) {
791
+ totalViolations++;
792
+ violationsByType[violation.type]++;
793
+ violationsBySeverity[violation.severity]++;
794
+ }
795
+ }
796
+ return {
797
+ totalFiles: results.length,
798
+ passedFiles: results.filter((r) => r.passed).length,
799
+ failedFiles: results.filter((r) => !r.passed).length,
800
+ totalViolations,
801
+ violationsByType,
802
+ violationsBySeverity
803
+ };
804
+ }
805
+ function formatViolation(violation) {
806
+ const severityPrefix = violation.severity === "error" ? "ERROR" : violation.severity === "warning" ? "WARN" : "INFO";
807
+ return `${severityPrefix}: ${violation.filePath}:${violation.line}:${violation.column} - ${violation.message}`;
808
+ }
809
+ function formatViolations(results) {
810
+ const lines = [];
811
+ for (const result of results) {
812
+ for (const violation of result.violations) {
813
+ lines.push(formatViolation(violation));
814
+ }
815
+ }
816
+ return lines;
817
+ }
818
+
819
+ // src/analyzers/componentLinter.ts
820
+ var CSS_COLOR_NAMES = /* @__PURE__ */ new Set([
821
+ "aliceblue",
822
+ "antiquewhite",
823
+ "aqua",
824
+ "aquamarine",
825
+ "azure",
826
+ "beige",
827
+ "bisque",
828
+ "black",
829
+ "blanchedalmond",
830
+ "blue",
831
+ "blueviolet",
832
+ "brown",
833
+ "burlywood",
834
+ "cadetblue",
835
+ "chartreuse",
836
+ "chocolate",
837
+ "coral",
838
+ "cornflowerblue",
839
+ "cornsilk",
840
+ "crimson",
841
+ "cyan",
842
+ "darkblue",
843
+ "darkcyan",
844
+ "darkgoldenrod",
845
+ "darkgray",
846
+ "darkgreen",
847
+ "darkgrey",
848
+ "darkkhaki",
849
+ "darkmagenta",
850
+ "darkolivegreen",
851
+ "darkorange",
852
+ "darkorchid",
853
+ "darkred",
854
+ "darksalmon",
855
+ "darkseagreen",
856
+ "darkslateblue",
857
+ "darkslategray",
858
+ "darkslategrey",
859
+ "darkturquoise",
860
+ "darkviolet",
861
+ "deeppink",
862
+ "deepskyblue",
863
+ "dimgray",
864
+ "dimgrey",
865
+ "dodgerblue",
866
+ "firebrick",
867
+ "floralwhite",
868
+ "forestgreen",
869
+ "fuchsia",
870
+ "gainsboro",
871
+ "ghostwhite",
872
+ "gold",
873
+ "goldenrod",
874
+ "gray",
875
+ "green",
876
+ "greenyellow",
877
+ "grey",
878
+ "honeydew",
879
+ "hotpink",
880
+ "indianred",
881
+ "indigo",
882
+ "ivory",
883
+ "khaki",
884
+ "lavender",
885
+ "lavenderblush",
886
+ "lawngreen",
887
+ "lemonchiffon",
888
+ "lightblue",
889
+ "lightcoral",
890
+ "lightcyan",
891
+ "lightgoldenrodyellow",
892
+ "lightgray",
893
+ "lightgreen",
894
+ "lightgrey",
895
+ "lightpink",
896
+ "lightsalmon",
897
+ "lightseagreen",
898
+ "lightskyblue",
899
+ "lightslategray",
900
+ "lightslategrey",
901
+ "lightsteelblue",
902
+ "lightyellow",
903
+ "lime",
904
+ "limegreen",
905
+ "linen",
906
+ "magenta",
907
+ "maroon",
908
+ "mediumaquamarine",
909
+ "mediumblue",
910
+ "mediumorchid",
911
+ "mediumpurple",
912
+ "mediumseagreen",
913
+ "mediumslateblue",
914
+ "mediumspringgreen",
915
+ "mediumturquoise",
916
+ "mediumvioletred",
917
+ "midnightblue",
918
+ "mintcream",
919
+ "mistyrose",
920
+ "moccasin",
921
+ "navajowhite",
922
+ "navy",
923
+ "oldlace",
924
+ "olive",
925
+ "olivedrab",
926
+ "orange",
927
+ "orangered",
928
+ "orchid",
929
+ "palegoldenrod",
930
+ "palegreen",
931
+ "paleturquoise",
932
+ "palevioletred",
933
+ "papayawhip",
934
+ "peachpuff",
935
+ "peru",
936
+ "pink",
937
+ "plum",
938
+ "powderblue",
939
+ "purple",
940
+ "rebeccapurple",
941
+ "red",
942
+ "rosybrown",
943
+ "royalblue",
944
+ "saddlebrown",
945
+ "salmon",
946
+ "sandybrown",
947
+ "seagreen",
948
+ "seashell",
949
+ "sienna",
950
+ "silver",
951
+ "skyblue",
952
+ "slateblue",
953
+ "slategray",
954
+ "slategrey",
955
+ "snow",
956
+ "springgreen",
957
+ "steelblue",
958
+ "tan",
959
+ "teal",
960
+ "thistle",
961
+ "tomato",
962
+ "turquoise",
963
+ "violet",
964
+ "wheat",
965
+ "white",
966
+ "whitesmoke",
967
+ "yellow",
968
+ "yellowgreen"
969
+ ]);
970
+ var DEFAULT_ALLOWED_COLORS = /* @__PURE__ */ new Set([
971
+ "transparent",
972
+ "inherit",
973
+ "currentColor",
974
+ "currentcolor"
975
+ ]);
976
+ var COLOR_PROPERTIES = [
977
+ "color",
978
+ "backgroundColor",
979
+ "borderColor",
980
+ "borderTopColor",
981
+ "borderRightColor",
982
+ "borderBottomColor",
983
+ "borderLeftColor",
984
+ "shadowColor",
985
+ "textDecorationColor",
986
+ "tintColor",
987
+ "overlayColor"
988
+ ];
989
+ function isHardcodedColor(value, allowedColors) {
990
+ const trimmed = value.trim().toLowerCase();
991
+ if (allowedColors.has(trimmed)) {
992
+ return false;
993
+ }
994
+ if (/^#[0-9a-f]{3,8}$/i.test(trimmed)) {
995
+ return true;
996
+ }
997
+ if (/^rgba?\s*\(/.test(trimmed)) {
998
+ return true;
999
+ }
1000
+ if (/^hsla?\s*\(/.test(trimmed)) {
1001
+ return true;
1002
+ }
1003
+ if (CSS_COLOR_NAMES.has(trimmed)) {
1004
+ return true;
1005
+ }
1006
+ return false;
1007
+ }
1008
+ function getRuleSeverity(rule, defaultSeverity) {
1009
+ if (rule === false) return null;
1010
+ if (rule === true || rule === void 0) return defaultSeverity;
1011
+ return rule;
1012
+ }
1013
+ function getLineColumn(source, index) {
1014
+ const lines = source.substring(0, index).split("\n");
1015
+ return {
1016
+ line: lines.length,
1017
+ column: lines[lines.length - 1].length + 1
1018
+ };
1019
+ }
1020
+ function lintComponent(filePath, sourceCode, options = {}) {
1021
+ const issues = [];
1022
+ const rules = options.rules || {};
1023
+ const allowedColors = /* @__PURE__ */ new Set([
1024
+ ...DEFAULT_ALLOWED_COLORS,
1025
+ ...(options.allowedColors || []).map((c) => c.toLowerCase())
1026
+ ]);
1027
+ const hardcodedColorSeverity = getRuleSeverity(rules.hardcodedColors, "warning");
1028
+ if (hardcodedColorSeverity) {
1029
+ for (const prop of COLOR_PROPERTIES) {
1030
+ const propRegex = new RegExp(`${prop}\\s*:\\s*['"]([^'"]+)['"]`, "g");
1031
+ let match;
1032
+ while ((match = propRegex.exec(sourceCode)) !== null) {
1033
+ const colorValue = match[1];
1034
+ if (isHardcodedColor(colorValue, allowedColors)) {
1035
+ const { line, column } = getLineColumn(sourceCode, match.index);
1036
+ issues.push({
1037
+ type: "hardcoded-color",
1038
+ severity: hardcodedColorSeverity,
1039
+ line,
1040
+ column,
1041
+ code: match[0],
1042
+ message: `Hardcoded color '${colorValue}' in ${prop}. Use theme colors instead.`,
1043
+ suggestion: `Use theme.colors.*, theme.intents[intent].*, or pass color via props`
1044
+ });
1045
+ }
1046
+ }
1047
+ }
1048
+ const templateColorRegex = /(?:color|backgroundColor)\s*:\s*`[^`]*#[0-9a-fA-F]{3,8}[^`]*`/g;
1049
+ let templateMatch;
1050
+ while ((templateMatch = templateColorRegex.exec(sourceCode)) !== null) {
1051
+ const { line, column } = getLineColumn(sourceCode, templateMatch.index);
1052
+ issues.push({
1053
+ type: "hardcoded-color",
1054
+ severity: hardcodedColorSeverity,
1055
+ line,
1056
+ column,
1057
+ code: templateMatch[0],
1058
+ message: `Hardcoded hex color in template literal. Use theme colors instead.`,
1059
+ suggestion: `Use theme.colors.* or theme.intents[intent].*`
1060
+ });
1061
+ }
1062
+ }
1063
+ const directPlatformSeverity = getRuleSeverity(rules.directPlatformImports, "warning");
1064
+ if (directPlatformSeverity) {
1065
+ const isSharedFile = !filePath.includes(".web.") && !filePath.includes(".native.");
1066
+ if (isSharedFile) {
1067
+ const rnImportRegex = /import\s+.*\s+from\s+['"]react-native['"]/g;
1068
+ let match;
1069
+ while ((match = rnImportRegex.exec(sourceCode)) !== null) {
1070
+ const { line, column } = getLineColumn(sourceCode, match.index);
1071
+ issues.push({
1072
+ type: "direct-platform-import",
1073
+ severity: directPlatformSeverity,
1074
+ line,
1075
+ column,
1076
+ code: match[0],
1077
+ message: `Direct import from 'react-native' in shared file. Use @idealyst/components instead.`,
1078
+ suggestion: `Import View, Text, etc. from '@idealyst/components'`
1079
+ });
1080
+ }
1081
+ }
1082
+ }
1083
+ const counts = {
1084
+ error: issues.filter((i) => i.severity === "error").length,
1085
+ warning: issues.filter((i) => i.severity === "warning").length,
1086
+ info: issues.filter((i) => i.severity === "info").length
1087
+ };
1088
+ return {
1089
+ filePath,
1090
+ issues,
1091
+ passed: counts.error === 0,
1092
+ counts
1093
+ };
1094
+ }
1095
+ function lintComponents(files, options = {}) {
1096
+ return files.map((file) => lintComponent(file.path, file.content, options));
1097
+ }
1098
+ function formatLintIssue(issue, filePath) {
1099
+ const severityPrefix = issue.severity === "error" ? "ERROR" : issue.severity === "warning" ? "WARN" : "INFO";
1100
+ let output = `${severityPrefix}: ${filePath}:${issue.line}:${issue.column} - ${issue.message}`;
1101
+ if (issue.suggestion) {
1102
+ output += `
1103
+ Suggestion: ${issue.suggestion}`;
1104
+ }
1105
+ return output;
1106
+ }
1107
+ function formatLintResults(results) {
1108
+ const lines = [];
1109
+ for (const result of results) {
1110
+ for (const issue of result.issues) {
1111
+ lines.push(formatLintIssue(issue, result.filePath));
1112
+ }
1113
+ }
1114
+ return lines;
1115
+ }
1116
+ function summarizeLintResults(results) {
1117
+ const issuesByType = {
1118
+ "hardcoded-color": 0,
1119
+ "direct-platform-import": 0
1120
+ };
1121
+ const issuesBySeverity = {
1122
+ error: 0,
1123
+ warning: 0,
1124
+ info: 0
1125
+ };
1126
+ let totalIssues = 0;
1127
+ for (const result of results) {
1128
+ for (const issue of result.issues) {
1129
+ totalIssues++;
1130
+ issuesByType[issue.type]++;
1131
+ issuesBySeverity[issue.severity]++;
1132
+ }
1133
+ }
1134
+ return {
1135
+ totalFiles: results.length,
1136
+ passedFiles: results.filter((r) => r.passed).length,
1137
+ failedFiles: results.filter((r) => !r.passed).length,
1138
+ totalIssues,
1139
+ issuesByType,
1140
+ issuesBySeverity
1141
+ };
1142
+ }
1143
+ export {
1144
+ analyzeFiles,
1145
+ analyzePlatformImports,
1146
+ formatLintIssue,
1147
+ formatLintResults,
1148
+ formatViolation,
1149
+ formatViolations,
1150
+ lintComponent,
1151
+ lintComponents,
1152
+ summarizeLintResults,
1153
+ summarizeResults
1154
+ };
1155
+ //# sourceMappingURL=index.js.map