@entur/typography 1.10.0-beta.1 โ†’ 1.10.0-beta.10

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.
@@ -18,21 +18,16 @@
18
18
  * * Styling classes may change
19
19
  * * Test thoroughly after migration!
20
20
  *
21
- * ๐Ÿ“ Import-Only Mode (--import-only):
22
- * - Only updates import paths from '@entur/typography' to '@entur/typography'
23
- * - Keeps your existing component usage unchanged
24
- * - Minimal risk, allows gradual migration
25
- * - You can manually update components later
21
+
26
22
  *
27
23
  * Usage:
28
24
  * 1. Run this script in your project root
29
- * 2. Choose your migration mode (complete or import-only)
25
+ * 2. Choose your migration mode (complete)
30
26
  * 3. Update your styles as needed
31
27
  * 4. Test your application thoroughly
32
28
  *
33
29
  * Options:
34
30
  * --dry-run Show what would be changed without modifying files
35
- * --import-only Import-only migration: update import paths only
36
31
  *
37
32
  * Environment Variables:
38
33
  * TYPOGRAPHY_MIGRATION_DIRS Comma-separated list of directories to scan
@@ -72,6 +67,31 @@ try {
72
67
  const OLD_IMPORT = '@entur/typography';
73
68
  const BETA_IMPORT = '@entur/typography';
74
69
 
70
+ // Enhanced warning detection patterns - only truly problematic patterns
71
+ const PROBLEMATIC_PATTERNS = {
72
+ // Style conflicts that will cause issues
73
+ styleMarginConflict: /style=.*margin=/g,
74
+ styleSpacingConflict: /style=.*spacing=/g,
75
+
76
+ // Invalid HTML structure
77
+ nestedTypography: /<Text[^>]*>.*<Text[^>]*>/g,
78
+
79
+ // Accessibility issues
80
+ missingAsProps: /<Heading[^>]*>(?!.*\bas=)/g,
81
+
82
+ // Semantic HTML mismatches
83
+ semanticMismatch: /<Heading[^>]*as="([^"]*)"[^>]*variant="([^"]*)"/g,
84
+ };
85
+
86
+ // Warning severity levels
87
+ const WARNING_CATEGORIES = {
88
+ CRITICAL: 'critical', // Will break functionality
89
+ HIGH: 'high', // Likely to cause issues
90
+ MEDIUM: 'medium', // May cause styling issues
91
+ LOW: 'low', // Best practice suggestions
92
+ INFO: 'info', // Informational only
93
+ };
94
+
75
95
  // =============================================================================
76
96
  // ๐ŸŽฏ MIGRATION FOLDERS CONFIGURATION
77
97
  // =============================================================================
@@ -114,6 +134,153 @@ function validateDirectoryPath(dir) {
114
134
  return !path.isAbsolute(dir) && !dir.includes('..') && !dir.includes('~');
115
135
  }
116
136
 
137
+ // Enhanced file analysis for better warning detection - only truly problematic patterns
138
+ function analyzeFile(filePath, content) {
139
+ const analysis = {
140
+ hasStyleConflicts: false,
141
+ hasNestedTypography: false,
142
+ hasAccessibilityIssues: false,
143
+ hasSemanticMismatches: false,
144
+ lineNumbers: {},
145
+ suggestions: [],
146
+ warnings: [],
147
+ };
148
+
149
+ // Line-by-line analysis for better context
150
+ content.split('\n').forEach((line, index) => {
151
+ const lineNum = index + 1;
152
+
153
+ // Check for style conflicts (style + margin/spacing)
154
+ if (
155
+ line.match(PROBLEMATIC_PATTERNS.styleMarginConflict) ||
156
+ line.match(PROBLEMATIC_PATTERNS.styleSpacingConflict)
157
+ ) {
158
+ analysis.hasStyleConflicts = true;
159
+ analysis.lineNumbers.styleConflicts = (
160
+ analysis.lineNumbers.styleConflicts || []
161
+ ).concat(lineNum);
162
+
163
+ // Generate warning message
164
+ analysis.warnings.push(
165
+ `Line ${lineNum}: Style conflicts detected - component has both style and margin/spacing props`,
166
+ );
167
+ }
168
+
169
+ // Check for nested typography components (invalid HTML)
170
+ if (line.match(PROBLEMATIC_PATTERNS.nestedTypography)) {
171
+ analysis.hasNestedTypography = true;
172
+ analysis.lineNumbers.nestedTypography = (
173
+ analysis.lineNumbers.nestedTypography || []
174
+ ).concat(lineNum);
175
+
176
+ // Generate warning message
177
+ analysis.warnings.push(
178
+ `Line ${lineNum}: Nested typography components detected - invalid HTML structure`,
179
+ );
180
+ }
181
+
182
+ // Check for missing as props (accessibility issue)
183
+ if (line.match(PROBLEMATIC_PATTERNS.missingAsProps)) {
184
+ analysis.hasAccessibilityIssues = true;
185
+ analysis.lineNumbers.missingAsProps = (
186
+ analysis.lineNumbers.missingAsProps || []
187
+ ).concat(lineNum);
188
+
189
+ // Generate warning message
190
+ analysis.warnings.push(
191
+ `Line ${lineNum}: Missing 'as' prop - accessibility issue for Heading component`,
192
+ );
193
+ }
194
+
195
+ // Check for semantic mismatches (e.g., h1 with subtitle variant)
196
+ if (line.match(PROBLEMATIC_PATTERNS.semanticMismatch)) {
197
+ analysis.hasSemanticMismatches = true;
198
+ analysis.lineNumbers.semanticMismatches = (
199
+ analysis.lineNumbers.semanticMismatches || []
200
+ ).concat(lineNum);
201
+
202
+ // Generate warning message
203
+ analysis.warnings.push(
204
+ `Line ${lineNum}: Semantic mismatch detected - heading level and variant combination may be incorrect`,
205
+ );
206
+ }
207
+ });
208
+
209
+ return analysis;
210
+ }
211
+
212
+ // Generate enhanced warnings with context and solutions
213
+ function generateWarningWithSolution(warning, context, filePath, lineNumber) {
214
+ const severity = determineSeverity(warning);
215
+ const suggestion = generateSuggestion(warning, context);
216
+ const codeExample = generateCodeExample(warning);
217
+
218
+ return {
219
+ message: warning,
220
+ severity,
221
+ suggestion,
222
+ codeExample,
223
+ file: filePath,
224
+ line: lineNumber,
225
+ documentation: getRelevantDocs(warning),
226
+ };
227
+ }
228
+
229
+ // Determine warning severity based on content
230
+ function determineSeverity(warning) {
231
+ if (warning.includes('will break') || warning.includes('fatal'))
232
+ return WARNING_CATEGORIES.CRITICAL;
233
+ if (warning.includes('conflict') || warning.includes('override'))
234
+ return WARNING_CATEGORIES.HIGH;
235
+ if (warning.includes('may cause') || warning.includes('styling'))
236
+ return WARNING_CATEGORIES.MEDIUM;
237
+ if (warning.includes('best practice') || warning.includes('consider'))
238
+ return WARNING_CATEGORIES.LOW;
239
+ return WARNING_CATEGORIES.INFO;
240
+ }
241
+
242
+ // Generate actionable suggestions
243
+ function generateSuggestion(warning, context) {
244
+ if (warning.includes('style and margin')) {
245
+ return 'Remove the margin prop as it will be overridden by inline styles. Use spacing prop instead.';
246
+ }
247
+ if (warning.includes('missing variant')) {
248
+ return 'Add a variant prop to ensure consistent styling. Example: variant="title-1"';
249
+ }
250
+ if (warning.includes('nested typography')) {
251
+ return 'Avoid nesting Text components. Use spans or other inline elements for emphasis.';
252
+ }
253
+ if (warning.includes('deprecated margin')) {
254
+ return 'Replace margin prop with spacing prop for better consistency.';
255
+ }
256
+ return 'Review the component for potential styling conflicts.';
257
+ }
258
+
259
+ // Generate code examples for fixes
260
+ function generateCodeExample(warning) {
261
+ if (warning.includes('style and margin')) {
262
+ return '// Before: <Text style={{color: "red"}} margin="bottom">\n// After: <Text style={{color: "red"}} spacing="bottom">';
263
+ }
264
+ if (warning.includes('missing variant')) {
265
+ return '// Before: <Heading as="h1">Title</Heading>\n// After: <Heading as="h1" variant="title-1">Title</Heading>';
266
+ }
267
+ if (warning.includes('nested typography')) {
268
+ return '// Before: <Text>Hello <Text>World</Text></Text>\n// After: <Text>Hello <span>World</span></Text>';
269
+ }
270
+ return '';
271
+ }
272
+
273
+ // Get relevant documentation links
274
+ function getRelevantDocs(warning) {
275
+ if (warning.includes('variant'))
276
+ return 'https://linje.entur.no/komponenter/ressurser/typography-beta#heading-variants';
277
+ if (warning.includes('spacing'))
278
+ return 'https://linje.entur.no/komponenter/ressurser/typography-beta#spacing';
279
+ if (warning.includes('semantic'))
280
+ return 'https://linje.entur.no/komponenter/ressurser/typography-beta#semantic-html';
281
+ return 'https://linje.entur.no/komponenter/ressurser/typography-beta';
282
+ }
283
+
117
284
  let ALLOWED_DIRECTORIES = process.env.TYPOGRAPHY_MIGRATION_DIRS
118
285
  ? process.env.TYPOGRAPHY_MIGRATION_DIRS.split(',')
119
286
  : MIGRATION_FOLDERS;
@@ -170,15 +337,14 @@ const COMPONENT_MAPPING = {
170
337
  Heading6: { component: 'Heading', as: 'h6', variant: 'section-2' },
171
338
  Paragraph: { component: 'Text', variant: 'paragraph' },
172
339
  LeadParagraph: { component: 'Text', variant: 'leading' },
173
- SmallText: { component: 'Text', variant: 'subparagraph', size: 's' },
174
- StrongText: { component: 'Text', variant: 'emphasized', weight: 'semibold' },
175
- SubLabel: { component: 'Text', variant: 'sublabel', size: 'xs' },
340
+ SmallText: { component: 'Text', variant: 'subparagraph' },
341
+ StrongText: { component: 'Text', as: 'strong', weight: 'bold' },
342
+ SubLabel: { component: 'Text', variant: 'sublabel' },
176
343
  SubParagraph: { component: 'Text', variant: 'subparagraph' },
177
344
  Label: { component: 'Text', variant: 'label' },
178
345
  EmphasizedText: { component: 'Text', variant: 'emphasized' },
179
346
  CodeText: { component: 'Text', variant: 'code-text' },
180
- Link: { component: 'LinkBeta' },
181
- Blockquote: { component: 'BlockquoteBeta' },
347
+ Link: { component: 'LinkBeta' }, // Convert Link to LinkBeta
182
348
  };
183
349
 
184
350
  // Props mapping for migration
@@ -187,17 +353,43 @@ const PROPS_MAPPING = {
187
353
  };
188
354
 
189
355
  // Spacing value mapping from old margin to new spacing
356
+ // Based on the actual CSS classes in src/beta/styles.scss
357
+ // and the old margin prop values: "top" | "bottom" | "both" | "none"
190
358
  const SPACING_MAPPING = {
191
- none: 'none',
192
- top: 'md-top',
193
- bottom: 'md-bottom',
194
- left: 'md-left',
195
- right: 'md-right',
359
+ // Old margin values mapped to new spacing values
360
+ none: 'none', // No spacing
361
+ top: 'md-top', // Top margin only (medium size)
362
+ bottom: 'md-bottom', // Bottom margin only (medium size)
363
+ both: 'md', // Both top and bottom margins (medium size)
364
+
365
+ // Additional spacing values for more granular control
366
+ // These weren't in the old margin prop but are available in new spacing
367
+ left: 'md-left', // Left margin (medium size)
368
+ right: 'md-right', // Right margin (medium size)
369
+
370
+ // Size-based spacing (applies to both top and bottom)
196
371
  xs: 'xs',
197
372
  sm: 'sm',
198
373
  md: 'md',
199
374
  lg: 'lg',
200
375
  xl: 'xl',
376
+
377
+ // Specific directional spacing with sizes
378
+ 'xs-top': 'xs-top',
379
+ 'xs-bottom': 'xs-bottom',
380
+ 'sm-top': 'sm-top',
381
+ 'sm-bottom': 'sm-bottom',
382
+ 'md-top': 'md-top',
383
+ 'md-bottom': 'md-bottom',
384
+ 'lg-top': 'lg-top',
385
+ 'lg-bottom': 'lg-bottom',
386
+ 'xl-top': 'xl-top',
387
+ 'xl-bottom': 'xl-bottom',
388
+
389
+ // Extra small variants
390
+ xs2: 'xs2',
391
+ 'xs2-top': 'xs2-top',
392
+ 'xs2-bottom': 'xs2-bottom',
201
393
  };
202
394
 
203
395
  // Import patterns to handle
@@ -211,30 +403,33 @@ const IMPORT_PATTERNS = [
211
403
  // Parse JSX props more robustly
212
404
  function parseJSXProps(propsString) {
213
405
  if (!propsString || !propsString.trim()) {
214
- return { props: {}, warnings: [] };
406
+ return { props: {}, warnings: [], spreadProps: [] };
215
407
  }
216
408
 
217
409
  const props = {};
218
410
  const warnings = [];
219
- const MAX_ITERATIONS = 100; // Prevent infinite loops
220
- let iterationCount = 0;
411
+ const spreadProps = []; // Track spread props separately
221
412
 
222
413
  try {
223
414
  // Parse props manually to handle complex cases
224
415
  let remaining = propsString.trim();
225
- let lastRemainingLength = remaining.length;
226
416
 
227
- while (remaining.length > 0 && iterationCount < MAX_ITERATIONS) {
228
- iterationCount++;
417
+ // First, extract all spread props
418
+ const spreadRegex = /\.\.\.\{?(\w+)\}?/g;
419
+ let spreadMatch;
420
+ while ((spreadMatch = spreadRegex.exec(remaining)) !== null) {
421
+ spreadProps.push(spreadMatch[1]);
422
+ }
229
423
 
230
- // Safety check: if we're not making progress, break
231
- if (remaining.length >= lastRemainingLength) {
232
- warnings.push(`Parser stuck at iteration ${iterationCount}, breaking`);
233
- break;
234
- }
235
- lastRemainingLength = remaining.length;
424
+ // Remove spread props from the string to parse regular props
425
+ remaining = remaining.replace(/\.\.\.\{?(\w+)\}?/g, '');
426
+
427
+ // Now parse regular props
428
+ while (remaining.trim().length > 0) {
429
+ // Skip whitespace
430
+ remaining = remaining.replace(/^\s+/, '');
236
431
 
237
- // Match prop name - more efficient regex
432
+ // Match prop name
238
433
  const nameMatch = remaining.match(/^(\w+)=/);
239
434
  if (!nameMatch) break;
240
435
 
@@ -244,7 +439,7 @@ function parseJSXProps(propsString) {
244
439
 
245
440
  // Match prop value
246
441
  if (remaining.startsWith('"') || remaining.startsWith("'")) {
247
- // String value - use indexOf for better performance
442
+ // String value
248
443
  const quote = remaining[0];
249
444
  const endQuoteIndex = remaining.indexOf(quote, 1);
250
445
  if (endQuoteIndex === -1) {
@@ -256,12 +451,11 @@ function parseJSXProps(propsString) {
256
451
  props[propName] = propValue;
257
452
  remaining = remaining.substring(endQuoteIndex + 1);
258
453
  } else if (remaining.startsWith('{')) {
259
- // Object value - find matching closing brace with bounds checking
454
+ // Object value - find matching closing brace
260
455
  let braceCount = 0;
261
456
  let endIndex = -1;
262
- const maxSearchLength = Math.min(remaining.length, 1000); // Limit search length
263
457
 
264
- for (let i = 0; i < maxSearchLength; i++) {
458
+ for (let i = 0; i < remaining.length; i++) {
265
459
  if (remaining[i] === '{') braceCount++;
266
460
  if (remaining[i] === '}') {
267
461
  braceCount--;
@@ -281,23 +475,19 @@ function parseJSXProps(propsString) {
281
475
  props[propName] = propValue;
282
476
  remaining = remaining.substring(endIndex + 1);
283
477
  } else {
284
- // Boolean prop (e.g., disabled) or invalid syntax
478
+ // Boolean prop
285
479
  props[propName] = true;
286
480
  break;
287
481
  }
288
482
 
289
- // Skip whitespace more efficiently
483
+ // Skip whitespace
290
484
  remaining = remaining.replace(/^\s+/, '');
291
485
  }
292
-
293
- if (iterationCount >= MAX_ITERATIONS) {
294
- warnings.push(`Maximum parsing iterations (${MAX_ITERATIONS}) reached`);
295
- }
296
486
  } catch (error) {
297
487
  warnings.push(`Failed to parse props: ${error.message}`);
298
488
  }
299
489
 
300
- return { props, warnings };
490
+ return { props, warnings, spreadProps };
301
491
  }
302
492
 
303
493
  // Migrate props from old to new format
@@ -315,11 +505,12 @@ function migrateProps(props, oldComponent) {
315
505
  `Migrated 'margin="${props.margin}"' to 'spacing="${newSpacing}"'`,
316
506
  );
317
507
  } else {
318
- // Unknown margin value - keep as is but warn
319
- migratedProps.spacing = props.margin;
508
+ // Unknown margin value - suggest alternatives
509
+ const suggestions = getSpacingSuggestions(props.margin);
510
+ migratedProps.spacing = props.margin; // Keep original value for now
320
511
  delete migratedProps.margin;
321
512
  warnings.push(
322
- `Migrated 'margin="${props.margin}"' to 'spacing="${props.margin}"' (unknown value - may need manual review)`,
513
+ `Migrated 'margin="${props.margin}"' to 'spacing="${props.margin}"' (unknown value). ${suggestions}`,
323
514
  );
324
515
  }
325
516
  }
@@ -346,6 +537,53 @@ function migrateProps(props, oldComponent) {
346
537
  return { props: migratedProps, warnings };
347
538
  }
348
539
 
540
+ // Helper function to suggest spacing alternatives for unknown margin values
541
+ function getSpacingSuggestions(unknownMargin) {
542
+ const suggestions = [];
543
+
544
+ // Check if it might be one of the old margin values
545
+ if (['top', 'bottom', 'both', 'none'].includes(unknownMargin)) {
546
+ suggestions.push(
547
+ `"${unknownMargin}" is a valid old margin value and will be migrated correctly.`,
548
+ );
549
+ return suggestions.join(' ');
550
+ }
551
+
552
+ // Check if it might be a directional value
553
+ if (
554
+ unknownMargin.includes('top') ||
555
+ unknownMargin.includes('bottom') ||
556
+ unknownMargin.includes('left') ||
557
+ unknownMargin.includes('right')
558
+ ) {
559
+ suggestions.push(
560
+ 'Consider using directional spacing like "md-top", "sm-bottom", etc.',
561
+ );
562
+ }
563
+
564
+ // Check if it might be a size value
565
+ if (['xs', 'sm', 'md', 'lg', 'xl'].includes(unknownMargin)) {
566
+ suggestions.push(
567
+ 'Consider using size-based spacing like "xs", "sm", "md", "lg", "xl".',
568
+ );
569
+ }
570
+
571
+ // Check if it might be a specific variant
572
+ if (unknownMargin.includes('xs2')) {
573
+ suggestions.push(
574
+ 'Consider using "xs2", "xs2-top", or "xs2-bottom" for extra small spacing.',
575
+ );
576
+ }
577
+
578
+ if (suggestions.length === 0) {
579
+ suggestions.push(
580
+ 'Old margin values: "none", "top", "bottom", "both". New spacing values: "xs", "sm", "md", "lg", "xl", and directional variants.',
581
+ );
582
+ }
583
+
584
+ return suggestions.join(' ');
585
+ }
586
+
349
587
  // Convert props object back to JSX string
350
588
  function propsToString(props) {
351
589
  if (!props || Object.keys(props).length === 0) {
@@ -379,12 +617,46 @@ function updateImports(content) {
379
617
  let updatedContent = content;
380
618
  let changes = 0;
381
619
 
620
+ // First, update import paths
382
621
  IMPORT_PATTERNS.forEach(pattern => {
383
622
  const matches = content.match(pattern) || [];
384
623
  changes += matches.length;
385
624
  updatedContent = updatedContent.replace(pattern, `from '${BETA_IMPORT}'`);
386
625
  });
387
626
 
627
+ // Then, update destructured import names - only within @entur/typography imports
628
+ // Find all import statements from @entur/typography and update component names
629
+ const importRegex =
630
+ /import\s*{([^}]+)}\s*from\s*['"']@entur\/typography['"']/g;
631
+
632
+ updatedContent = updatedContent.replace(importRegex, (match, importList) => {
633
+ let updatedImportList = importList;
634
+ let hasChanges = false;
635
+ const uniqueComponents = new Set();
636
+
637
+ // Check each component in the import list
638
+ Object.entries(COMPONENT_MAPPING).forEach(([oldComponent, mapping]) => {
639
+ const componentRegex = new RegExp(`\\b${oldComponent}\\b`, 'g');
640
+ if (componentRegex.test(updatedImportList)) {
641
+ updatedImportList = updatedImportList.replace(
642
+ componentRegex,
643
+ mapping.component,
644
+ );
645
+ uniqueComponents.add(mapping.component);
646
+ hasChanges = true;
647
+ }
648
+ });
649
+
650
+ if (hasChanges) {
651
+ changes++;
652
+ // Deduplicate components and create clean import statement
653
+ const finalImportList = Array.from(uniqueComponents).join(', ');
654
+ return `import {${finalImportList}} from '${BETA_IMPORT}'`;
655
+ }
656
+
657
+ return match;
658
+ });
659
+
388
660
  return { content: updatedContent, changes };
389
661
  }
390
662
 
@@ -404,8 +676,11 @@ function updateComponents(content) {
404
676
  changes++;
405
677
 
406
678
  // Parse existing props
407
- const { props: existingProps, warnings: parseWarnings } =
408
- parseJSXProps(propsString);
679
+ const {
680
+ props: existingProps,
681
+ warnings: parseWarnings,
682
+ spreadProps,
683
+ } = parseJSXProps(propsString);
409
684
  warnings.push(...parseWarnings);
410
685
 
411
686
  // Migrate props
@@ -425,8 +700,10 @@ function updateComponents(content) {
425
700
 
426
701
  // Handle Heading components
427
702
  if (mapping.component === 'Heading') {
428
- const asValue = newProps.as || mapping.as;
429
- const variantValue = newProps.variant || mapping.variant;
703
+ // Preserve existing 'as' prop if it exists, otherwise use mapping default
704
+ const asValue = existingProps.as || mapping.as;
705
+ // Preserve existing 'variant' prop if it exists, otherwise use mapping default
706
+ const variantValue = existingProps.variant || mapping.variant;
430
707
 
431
708
  // Remove as and variant from props since we'll add them separately
432
709
  delete newProps.as;
@@ -441,7 +718,9 @@ function updateComponents(content) {
441
718
  Object.assign(orderedProps, newProps);
442
719
 
443
720
  const propsString = propsToString(orderedProps);
444
- return `<Heading as="${asValue}" variant="${variantValue}"${propsString}>`;
721
+ const spreadPropsString =
722
+ spreadProps.length > 0 ? ` {...${spreadProps.join(', ...')}}` : '';
723
+ return `<Heading as="${asValue}" variant="${variantValue}"${propsString}${spreadPropsString}>`;
445
724
  }
446
725
 
447
726
  // Handle other components
@@ -464,7 +743,9 @@ function updateComponents(content) {
464
743
  Object.assign(finalProps, newProps);
465
744
 
466
745
  const otherPropsString = propsToString(finalProps);
467
- return `<${componentName}${otherPropsString}>`;
746
+ const spreadPropsString =
747
+ spreadProps.length > 0 ? ` {...${spreadProps.join(', ...')}}` : '';
748
+ return `<${componentName}${otherPropsString}${spreadPropsString}>`;
468
749
  },
469
750
  );
470
751
 
@@ -492,15 +773,33 @@ function updateComponents(content) {
492
773
  * @returns {string[]} Array of matching file paths
493
774
  */
494
775
  function findFiles(pattern) {
495
- // Create a single glob pattern that covers all allowed directories
496
- // Uses brace expansion: {src,app,components}/**/*.{ts,tsx,js,jsx}
497
- const combinedPattern = `{${ALLOWED_DIRECTORIES.join(',')}}/${pattern}`;
498
-
499
- // Use a single glob call instead of multiple calls
500
- const allFiles = glob.sync(combinedPattern, {
501
- ignore: BLOCKED_DIRECTORIES,
502
- nodir: true,
503
- absolute: false,
776
+ const allFiles = [];
777
+
778
+ // Process directory patterns
779
+ const directoryPatterns = ALLOWED_DIRECTORIES.filter(dir =>
780
+ dir.includes('**'),
781
+ );
782
+ const filePatterns = ALLOWED_DIRECTORIES.filter(dir => !dir.includes('**'));
783
+
784
+ // Handle directory patterns (e.g., src/**, app/**)
785
+ if (directoryPatterns.length > 0) {
786
+ const combinedDirPattern = `{${directoryPatterns.join(',')}}/${pattern}`;
787
+ const dirFiles = glob.sync(combinedDirPattern, {
788
+ ignore: BLOCKED_DIRECTORIES,
789
+ nodir: true,
790
+ absolute: false,
791
+ });
792
+ allFiles.push(...dirFiles);
793
+ }
794
+
795
+ // Handle file patterns (e.g., *.jsx, *.tsx)
796
+ filePatterns.forEach(filePattern => {
797
+ const files = glob.sync(filePattern, {
798
+ ignore: BLOCKED_DIRECTORIES,
799
+ nodir: true,
800
+ absolute: false,
801
+ });
802
+ allFiles.push(...files);
504
803
  });
505
804
 
506
805
  // Use Set for efficient deduplication and filtering
@@ -521,37 +820,29 @@ function findFiles(pattern) {
521
820
  return uniqueFiles;
522
821
  }
523
822
 
524
- function updateImportsAndComponents(content, strategy) {
823
+ function updateImportsAndComponents(content) {
525
824
  let updatedContent = content;
526
825
  let changes = 0;
527
826
  let warnings = [];
528
827
 
529
- if (strategy === 'import-only') {
530
- // Only update imports
531
- const { content: newContent, changes: importChanges } =
532
- updateImports(content);
533
- updatedContent = newContent;
534
- changes = importChanges;
535
- } else if (strategy === 'complete') {
536
- // Update both imports and components
537
- const { content: newContent, changes: importChanges } =
538
- updateImports(content);
539
- const {
540
- content: finalContent,
541
- changes: componentChanges,
542
- warnings: componentWarnings,
543
- } = updateComponents(newContent);
544
- updatedContent = finalContent;
545
- changes = importChanges + componentChanges;
546
- warnings = componentWarnings;
547
- }
828
+ // Update both imports and components
829
+ const { content: newContent, changes: importChanges } =
830
+ updateImports(content);
831
+ const {
832
+ content: finalContent,
833
+ changes: componentChanges,
834
+ warnings: componentWarnings,
835
+ } = updateComponents(newContent);
836
+ updatedContent = finalContent;
837
+ changes = importChanges + componentChanges;
838
+ warnings = componentWarnings;
548
839
 
549
840
  return { content: updatedContent, changes, warnings };
550
841
  }
551
842
 
552
- function generateMigrationReport(files, strategy, isDryRun = false) {
843
+ function generateMigrationReport(files, isDryRun = false) {
553
844
  const report = {
554
- strategy,
845
+ strategy: 'complete',
555
846
  totalFiles: files.length,
556
847
  migratedFiles: 0,
557
848
  totalChanges: 0,
@@ -564,21 +855,32 @@ function generateMigrationReport(files, strategy, isDryRun = false) {
564
855
  files.forEach(file => {
565
856
  try {
566
857
  const content = fs.readFileSync(file, 'utf8');
858
+
859
+ // Analyze file for problematic patterns BEFORE migration
860
+ const fileAnalysis = analyzeFile(file, content);
861
+
567
862
  const {
568
863
  content: updatedContent,
569
864
  changes,
570
865
  warnings,
571
- } = updateImportsAndComponents(content, strategy);
866
+ } = updateImportsAndComponents(content);
572
867
 
573
- if (changes > 0) {
868
+ // Combine migration warnings with file analysis warnings
869
+ const allWarnings = [...warnings, ...fileAnalysis.warnings];
870
+
871
+ if (changes > 0 || fileAnalysis.warnings.length > 0) {
574
872
  if (!isDryRun) {
575
873
  fs.writeFileSync(file, updatedContent, 'utf8');
576
874
  }
577
- report.migratedFiles++;
578
- report.totalChanges += changes;
579
- report.totalWarnings += warnings.length;
580
- report.files.push({ file, changes, warnings });
581
- report.warnings.push(...warnings.map(warning => `${file}: ${warning}`));
875
+ if (changes > 0) {
876
+ report.migratedFiles++;
877
+ report.totalChanges += changes;
878
+ }
879
+ report.totalWarnings += allWarnings.length;
880
+ report.files.push({ file, changes, warnings: allWarnings });
881
+ report.warnings.push(
882
+ ...allWarnings.map(warning => `${file}: ${warning}`),
883
+ );
582
884
  }
583
885
  } catch (error) {
584
886
  report.warnings.push(`${file}: Error processing file - ${error.message}`);
@@ -620,24 +922,135 @@ function printReport(report) {
620
922
  w.includes('check for conflicts'),
621
923
  );
622
924
 
925
+ // New warning types from file analysis
926
+ const styleConflictWarnings = report.warnings.filter(
927
+ w => w.includes('style conflicts') || w.includes('style and margin'),
928
+ );
929
+ const nestedTypographyWarnings = report.warnings.filter(w =>
930
+ w.includes('nested typography'),
931
+ );
932
+ const accessibilityWarnings = report.warnings.filter(
933
+ w => w.includes('missing as prop') || w.includes('accessibility'),
934
+ );
935
+ const semanticMismatchWarnings = report.warnings.filter(w =>
936
+ w.includes('semantic mismatch'),
937
+ );
938
+
623
939
  if (marginWarnings.length > 0) {
624
940
  console.log(
625
941
  `\n ๐Ÿ”„ Margin โ†’ Spacing Migrations (${marginWarnings.length}):`,
626
942
  );
627
- marginWarnings.forEach(warning => console.log(` ${warning}`));
943
+ // Show first 5 warnings, then summarize the rest
944
+ marginWarnings
945
+ .slice(0, 5)
946
+ .forEach(warning => console.log(` ${warning}`));
947
+ if (marginWarnings.length > 5) {
948
+ console.log(
949
+ ` ... and ${marginWarnings.length - 5} more similar warnings`,
950
+ );
951
+ }
628
952
  }
629
953
 
630
954
  if (semanticWarnings.length > 0) {
631
955
  console.log(`\n ๐ŸŽฏ Semantic HTML Issues (${semanticWarnings.length}):`);
632
- semanticWarnings.forEach(warning => console.log(` ${warning}`));
956
+ // Show first 5 warnings, then summarize the rest
957
+ semanticWarnings
958
+ .slice(0, 5)
959
+ .forEach(warning => console.log(` ${warning}`));
960
+ if (semanticWarnings.length > 5) {
961
+ console.log(
962
+ ` ... and ${semanticWarnings.length - 5} more similar warnings`,
963
+ );
964
+ }
633
965
  }
634
966
 
635
967
  if (conflictWarnings.length > 0) {
636
968
  console.log(`\n ๐Ÿšจ Style Conflicts (${conflictWarnings.length}):`);
637
- conflictWarnings.forEach(warning => console.log(` ${warning}`));
969
+ // Show first 5 warnings, then summarize the rest
970
+ conflictWarnings
971
+ .slice(0, 5)
972
+ .forEach(warning => console.log(` ${warning}`));
973
+ if (conflictWarnings.length > 5) {
974
+ console.log(
975
+ ` ... and ${conflictWarnings.length - 5} more similar warnings`,
976
+ );
977
+ }
638
978
  console.log(` โ†’ Review these components for styling conflicts`);
639
979
  }
640
980
 
981
+ // Display new warning types
982
+ if (styleConflictWarnings.length > 0) {
983
+ console.log(
984
+ `\n ๐ŸŽจ Style + Margin Conflicts (${styleConflictWarnings.length}):`,
985
+ );
986
+ styleConflictWarnings
987
+ .slice(0, 5)
988
+ .forEach(warning => console.log(` ${warning}`));
989
+ if (styleConflictWarnings.length > 5) {
990
+ console.log(
991
+ ` ... and ${
992
+ styleConflictWarnings.length - 5
993
+ } more similar warnings`,
994
+ );
995
+ }
996
+ console.log(` โ†’ Remove margin prop when using inline styles`);
997
+ }
998
+
999
+ if (nestedTypographyWarnings.length > 0) {
1000
+ console.log(
1001
+ `\n ๐Ÿšซ Nested Typography (${nestedTypographyWarnings.length}):`,
1002
+ );
1003
+ nestedTypographyWarnings
1004
+ .slice(0, 5)
1005
+ .forEach(warning => console.log(` ${warning}`));
1006
+ if (nestedTypographyWarnings.length > 5) {
1007
+ console.log(
1008
+ ` ... and ${
1009
+ nestedTypographyWarnings.length - 5
1010
+ } more similar warnings`,
1011
+ );
1012
+ }
1013
+ console.log(
1014
+ ` โ†’ Use spans or other inline elements instead of nested Text components`,
1015
+ );
1016
+ }
1017
+
1018
+ if (accessibilityWarnings.length > 0) {
1019
+ console.log(
1020
+ `\n โ™ฟ Accessibility Issues (${accessibilityWarnings.length}):`,
1021
+ );
1022
+ accessibilityWarnings
1023
+ .slice(0, 5)
1024
+ .forEach(warning => console.log(` ${warning}`));
1025
+ if (accessibilityWarnings.length > 5) {
1026
+ console.log(
1027
+ ` ... and ${
1028
+ accessibilityWarnings.length - 5
1029
+ } more similar warnings`,
1030
+ );
1031
+ }
1032
+ console.log(
1033
+ ` โ†’ Add 'as' prop to Heading components for proper semantic HTML`,
1034
+ );
1035
+ }
1036
+
1037
+ if (semanticMismatchWarnings.length > 0) {
1038
+ console.log(
1039
+ `\n ๐Ÿ” Semantic Mismatches (${semanticMismatchWarnings.length}):`,
1040
+ );
1041
+ semanticMismatchWarnings
1042
+ .slice(0, 5)
1043
+ .forEach(warning => console.log(` ${warning}`));
1044
+ if (semanticMismatchWarnings.length > 5) {
1045
+ console.log(
1046
+ ` ... and ${
1047
+ semanticMismatchWarnings.length - 5
1048
+ } more similar warnings`,
1049
+ );
1050
+ }
1051
+ console.log(` โ†’ Review heading level and variant combinations`);
1052
+ }
1053
+
641
1054
  console.log('\n๐Ÿ“‹ Summary:');
642
1055
  if (marginWarnings.length > 0)
643
1056
  console.log(
@@ -651,26 +1064,42 @@ function printReport(report) {
651
1064
  console.log(
652
1065
  ` โ€ข ${conflictWarnings.length} style conflicts need manual review`,
653
1066
  );
1067
+ if (styleConflictWarnings.length > 0)
1068
+ console.log(
1069
+ ` โ€ข ${styleConflictWarnings.length} style + margin conflicts detected`,
1070
+ );
1071
+ if (nestedTypographyWarnings.length > 0)
1072
+ console.log(
1073
+ ` โ€ข ${nestedTypographyWarnings.length} nested typography components found`,
1074
+ );
1075
+ if (accessibilityWarnings.length > 0)
1076
+ console.log(
1077
+ ` โ€ข ${accessibilityWarnings.length} accessibility issues need attention`,
1078
+ );
1079
+ if (semanticMismatchWarnings.length > 0)
1080
+ console.log(
1081
+ ` โ€ข ${semanticMismatchWarnings.length} semantic mismatches detected`,
1082
+ );
1083
+
1084
+ // Add helpful note about warning limits
1085
+ if (report.warnings.length > 15) {
1086
+ console.log(
1087
+ '\n๐Ÿ’ก Note: Only showing first 5 warnings of each type to avoid overwhelming output.',
1088
+ );
1089
+ console.log(
1090
+ ' All warnings are still logged in the migration report above.',
1091
+ );
1092
+ }
654
1093
  }
655
1094
  }
656
1095
 
657
- function showNextSteps(strategy) {
1096
+ function showNextSteps() {
658
1097
  console.log('\n๐Ÿ“ Next Steps');
659
1098
  console.log('=============');
660
1099
 
661
- if (strategy === 'import-only') {
662
- console.log('1. โœ… Import statements updated');
663
- console.log('2. ๐Ÿ”„ Update component usage manually when ready:');
664
- Object.entries(COMPONENT_MAPPING).forEach(([old, new_]) => {
665
- console.log(` ${old} โ†’ ${new_}`);
666
- });
667
- console.log('3. ๐Ÿงช Test your application');
668
- console.log('4. ๐Ÿ“š Read the migration guide on our website');
669
- } else if (strategy === 'complete') {
670
- console.log('1. ๐Ÿงช Test your application thoroughly');
671
- console.log('2. ๐Ÿ”„ Review and adjust any component props if needed');
672
- console.log('3. ๐Ÿ“š Read the migration guide on our website');
673
- }
1100
+ console.log('1. ๐Ÿงช Test your application thoroughly');
1101
+ console.log('2. ๐Ÿ”„ Review and adjust any component props if needed');
1102
+ console.log('3. ๐Ÿ“š Read the migration guide on our website');
674
1103
 
675
1104
  console.log('\nโš ๏ธ Important Notes:');
676
1105
  console.log('- Check warnings above for potential issues');
@@ -699,36 +1128,20 @@ function main() {
699
1128
  console.log(
700
1129
  ' --dry-run Show what would be changed without modifying files',
701
1130
  );
702
- console.log(
703
- ' --import-only Import-only migration: update import paths only',
704
- );
705
-
706
1131
  console.log(' --help, -h Show this help message');
707
1132
  console.log('');
708
- console.log('Migration Modes:');
709
- console.log(' ๐Ÿš€ Complete Mode (default): Updates everything');
1133
+ console.log('Migration Mode:');
1134
+ console.log(' ๐Ÿš€ Complete Mode: Updates everything');
710
1135
  console.log(' - Replaces old components with beta components');
711
1136
  console.log(' - May require prop/styling updates');
712
1137
  console.log(' - Test thoroughly after migration');
713
1138
  console.log('');
714
- console.log(
715
- ' ๐Ÿ“ Import-Only Mode (--import-only): Only updates import paths',
716
- );
717
- console.log(' - Keeps your existing component usage unchanged');
718
- console.log(' - Minimal risk, gradual migration');
719
- console.log('');
720
1139
  console.log('Examples:');
721
1140
  console.log(' # See what would be changed');
722
1141
  console.log(' npx @entur/typography@latest migrate --dry-run');
723
1142
  console.log('');
724
1143
  console.log(' # Complete migration: update everything (default)');
725
1144
  console.log(' npx @entur/typography@latest migrate');
726
- console.log('');
727
- console.log(' # Import-only migration: update import paths only');
728
- console.log(' npx @entur/typography@latest migrate --import-only');
729
- console.log('');
730
-
731
- console.log('');
732
1145
 
733
1146
  console.log('Environment Variables:');
734
1147
  console.log(
@@ -812,36 +1225,24 @@ function main() {
812
1225
 
813
1226
  // Parse command line options
814
1227
  const isDryRun = process.argv.includes('--dry-run');
815
- const isImportOnly = process.argv.includes('--import-only');
816
1228
 
817
1229
  if (isDryRun) {
818
1230
  console.log('๐Ÿ” DRY RUN MODE: No files will be modified');
819
1231
  console.log('');
820
1232
  }
821
1233
 
822
- if (isImportOnly) {
823
- console.log('๐Ÿ“ IMPORT-ONLY MIGRATION: Updating import paths only');
824
- console.log(' - Your component usage will remain unchanged');
825
- console.log(' - You can update components manually later');
826
- } else {
827
- console.log('๐Ÿš€ COMPLETE MIGRATION: Updating imports + component usage');
828
- console.log('โš ๏ธ WARNING: This will modify your component usage!');
829
- console.log(' - Old components will be replaced with beta components');
830
- console.log(' - You may need to update props and styling');
831
- console.log(' - Test thoroughly after migration');
832
- console.log(' (Use --import-only for import-only migration)');
833
- }
1234
+ console.log('๐Ÿš€ COMPLETE MIGRATION: Updating imports + component usage');
1235
+ console.log('โš ๏ธ WARNING: This will modify your component usage!');
1236
+ console.log(' - Old components will be replaced with beta components');
1237
+ console.log(' - You may need to update props and styling');
1238
+ console.log(' - Test thoroughly after migration');
834
1239
 
835
1240
  console.log('');
836
1241
 
837
1242
  // Perform migration
838
- const report = generateMigrationReport(
839
- allFiles,
840
- isImportOnly ? 'import-only' : 'complete',
841
- isDryRun,
842
- );
1243
+ const report = generateMigrationReport(allFiles, isDryRun);
843
1244
  printReport(report);
844
- showNextSteps(isImportOnly ? 'import-only' : 'complete');
1245
+ showNextSteps();
845
1246
 
846
1247
  console.log('\n๐ŸŽฏ Migration complete!');
847
1248
  }