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

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.
@@ -14,25 +14,25 @@
14
14
  * * <Heading1> becomes <Heading as="h1" variant="title-1">
15
15
  * * <Paragraph> becomes <Text variant="paragraph">
16
16
  * * <Link> becomes <LinkBeta>
17
+ * * <Blockquote> becomes <BlockquoteBeta>
18
+ * * <BlockquoteFooter> becomes <BlockquoteFooterBeta>
19
+ * * <UnorderedList> becomes <UnorderedListBeta>
20
+ * * <NumberedList> becomes <NumberedListBeta>
21
+ * * <ListItem> becomes <ListItemBeta>
17
22
  * * Props may need updates (e.g., different prop names)
18
23
  * * Styling classes may change
19
24
  * * Test thoroughly after migration!
20
25
  *
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
26
+
26
27
  *
27
28
  * Usage:
28
29
  * 1. Run this script in your project root
29
- * 2. Choose your migration mode (complete or import-only)
30
+ * 2. Choose your migration mode (complete)
30
31
  * 3. Update your styles as needed
31
32
  * 4. Test your application thoroughly
32
33
  *
33
34
  * Options:
34
35
  * --dry-run Show what would be changed without modifying files
35
- * --import-only Import-only migration: update import paths only
36
36
  *
37
37
  * Environment Variables:
38
38
  * TYPOGRAPHY_MIGRATION_DIRS Comma-separated list of directories to scan
@@ -72,6 +72,31 @@ try {
72
72
  const OLD_IMPORT = '@entur/typography';
73
73
  const BETA_IMPORT = '@entur/typography';
74
74
 
75
+ // Enhanced warning detection patterns - only truly problematic patterns
76
+ const PROBLEMATIC_PATTERNS = {
77
+ // Style conflicts that will cause issues
78
+ styleMarginConflict: /style=.*margin=/g,
79
+ styleSpacingConflict: /style=.*spacing=/g,
80
+
81
+ // Invalid HTML structure
82
+ nestedTypography: /<Text[^>]*>.*<Text[^>]*>/g,
83
+
84
+ // Accessibility issues
85
+ missingAsProps: /<Heading[^>]*>(?!.*\bas=)/g,
86
+
87
+ // Semantic HTML mismatches
88
+ semanticMismatch: /<Heading[^>]*as="([^"]*)"[^>]*variant="([^"]*)"/g,
89
+ };
90
+
91
+ // Warning severity levels
92
+ const WARNING_CATEGORIES = {
93
+ CRITICAL: 'critical', // Will break functionality
94
+ HIGH: 'high', // Likely to cause issues
95
+ MEDIUM: 'medium', // May cause styling issues
96
+ LOW: 'low', // Best practice suggestions
97
+ INFO: 'info', // Informational only
98
+ };
99
+
75
100
  // =============================================================================
76
101
  // ๐ŸŽฏ MIGRATION FOLDERS CONFIGURATION
77
102
  // =============================================================================
@@ -114,6 +139,153 @@ function validateDirectoryPath(dir) {
114
139
  return !path.isAbsolute(dir) && !dir.includes('..') && !dir.includes('~');
115
140
  }
116
141
 
142
+ // Enhanced file analysis for better warning detection - only truly problematic patterns
143
+ function analyzeFile(filePath, content) {
144
+ const analysis = {
145
+ hasStyleConflicts: false,
146
+ hasNestedTypography: false,
147
+ hasAccessibilityIssues: false,
148
+ hasSemanticMismatches: false,
149
+ lineNumbers: {},
150
+ suggestions: [],
151
+ warnings: [],
152
+ };
153
+
154
+ // Line-by-line analysis for better context
155
+ content.split('\n').forEach((line, index) => {
156
+ const lineNum = index + 1;
157
+
158
+ // Check for style conflicts (style + margin/spacing)
159
+ if (
160
+ line.match(PROBLEMATIC_PATTERNS.styleMarginConflict) ||
161
+ line.match(PROBLEMATIC_PATTERNS.styleSpacingConflict)
162
+ ) {
163
+ analysis.hasStyleConflicts = true;
164
+ analysis.lineNumbers.styleConflicts = (
165
+ analysis.lineNumbers.styleConflicts || []
166
+ ).concat(lineNum);
167
+
168
+ // Generate warning message
169
+ analysis.warnings.push(
170
+ `Line ${lineNum}: Style conflicts detected - component has both style and margin/spacing props`,
171
+ );
172
+ }
173
+
174
+ // Check for nested typography components (invalid HTML)
175
+ if (line.match(PROBLEMATIC_PATTERNS.nestedTypography)) {
176
+ analysis.hasNestedTypography = true;
177
+ analysis.lineNumbers.nestedTypography = (
178
+ analysis.lineNumbers.nestedTypography || []
179
+ ).concat(lineNum);
180
+
181
+ // Generate warning message
182
+ analysis.warnings.push(
183
+ `Line ${lineNum}: Nested typography components detected - invalid HTML structure`,
184
+ );
185
+ }
186
+
187
+ // Check for missing as props (accessibility issue)
188
+ if (line.match(PROBLEMATIC_PATTERNS.missingAsProps)) {
189
+ analysis.hasAccessibilityIssues = true;
190
+ analysis.lineNumbers.missingAsProps = (
191
+ analysis.lineNumbers.missingAsProps || []
192
+ ).concat(lineNum);
193
+
194
+ // Generate warning message
195
+ analysis.warnings.push(
196
+ `Line ${lineNum}: Missing 'as' prop - accessibility issue for Heading component`,
197
+ );
198
+ }
199
+
200
+ // Check for semantic mismatches (e.g., h1 with subtitle variant)
201
+ if (line.match(PROBLEMATIC_PATTERNS.semanticMismatch)) {
202
+ analysis.hasSemanticMismatches = true;
203
+ analysis.lineNumbers.semanticMismatches = (
204
+ analysis.lineNumbers.semanticMismatches || []
205
+ ).concat(lineNum);
206
+
207
+ // Generate warning message
208
+ analysis.warnings.push(
209
+ `Line ${lineNum}: Semantic mismatch detected - heading level and variant combination may be incorrect`,
210
+ );
211
+ }
212
+ });
213
+
214
+ return analysis;
215
+ }
216
+
217
+ // Generate enhanced warnings with context and solutions
218
+ function generateWarningWithSolution(warning, context, filePath, lineNumber) {
219
+ const severity = determineSeverity(warning);
220
+ const suggestion = generateSuggestion(warning, context);
221
+ const codeExample = generateCodeExample(warning);
222
+
223
+ return {
224
+ message: warning,
225
+ severity,
226
+ suggestion,
227
+ codeExample,
228
+ file: filePath,
229
+ line: lineNumber,
230
+ documentation: getRelevantDocs(warning),
231
+ };
232
+ }
233
+
234
+ // Determine warning severity based on content
235
+ function determineSeverity(warning) {
236
+ if (warning.includes('will break') || warning.includes('fatal'))
237
+ return WARNING_CATEGORIES.CRITICAL;
238
+ if (warning.includes('conflict') || warning.includes('override'))
239
+ return WARNING_CATEGORIES.HIGH;
240
+ if (warning.includes('may cause') || warning.includes('styling'))
241
+ return WARNING_CATEGORIES.MEDIUM;
242
+ if (warning.includes('best practice') || warning.includes('consider'))
243
+ return WARNING_CATEGORIES.LOW;
244
+ return WARNING_CATEGORIES.INFO;
245
+ }
246
+
247
+ // Generate actionable suggestions
248
+ function generateSuggestion(warning, context) {
249
+ if (warning.includes('style and margin')) {
250
+ return 'Remove the margin prop as it will be overridden by inline styles. Use spacing prop instead.';
251
+ }
252
+ if (warning.includes('missing variant')) {
253
+ return 'Add a variant prop to ensure consistent styling. Example: variant="title-1"';
254
+ }
255
+ if (warning.includes('nested typography')) {
256
+ return 'Avoid nesting Text components. Use spans or other inline elements for emphasis.';
257
+ }
258
+ if (warning.includes('deprecated margin')) {
259
+ return 'Replace margin prop with spacing prop for better consistency.';
260
+ }
261
+ return 'Review the component for potential styling conflicts.';
262
+ }
263
+
264
+ // Generate code examples for fixes
265
+ function generateCodeExample(warning) {
266
+ if (warning.includes('style and margin')) {
267
+ return '// Before: <Text style={{color: "red"}} margin="bottom">\n// After: <Text style={{color: "red"}} spacing="bottom">';
268
+ }
269
+ if (warning.includes('missing variant')) {
270
+ return '// Before: <Heading as="h1">Title</Heading>\n// After: <Heading as="h1" variant="title-1">Title</Heading>';
271
+ }
272
+ if (warning.includes('nested typography')) {
273
+ return '// Before: <Text>Hello <Text>World</Text></Text>\n// After: <Text>Hello <span>World</span></Text>';
274
+ }
275
+ return '';
276
+ }
277
+
278
+ // Get relevant documentation links
279
+ function getRelevantDocs(warning) {
280
+ if (warning.includes('variant'))
281
+ return 'https://linje.entur.no/komponenter/ressurser/typography-beta#heading-variants';
282
+ if (warning.includes('spacing'))
283
+ return 'https://linje.entur.no/komponenter/ressurser/typography-beta#spacing';
284
+ if (warning.includes('semantic'))
285
+ return 'https://linje.entur.no/komponenter/ressurser/typography-beta#semantic-html';
286
+ return 'https://linje.entur.no/komponenter/ressurser/typography-beta';
287
+ }
288
+
117
289
  let ALLOWED_DIRECTORIES = process.env.TYPOGRAPHY_MIGRATION_DIRS
118
290
  ? process.env.TYPOGRAPHY_MIGRATION_DIRS.split(',')
119
291
  : MIGRATION_FOLDERS;
@@ -170,15 +342,19 @@ const COMPONENT_MAPPING = {
170
342
  Heading6: { component: 'Heading', as: 'h6', variant: 'section-2' },
171
343
  Paragraph: { component: 'Text', variant: 'paragraph' },
172
344
  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' },
345
+ SmallText: { component: 'Text', variant: 'subparagraph' },
346
+ StrongText: { component: 'Text', as: 'strong', weight: 'bold' },
347
+ SubLabel: { component: 'Text', variant: 'sublabel' },
176
348
  SubParagraph: { component: 'Text', variant: 'subparagraph' },
177
349
  Label: { component: 'Text', variant: 'label' },
178
350
  EmphasizedText: { component: 'Text', variant: 'emphasized' },
179
351
  CodeText: { component: 'Text', variant: 'code-text' },
180
- Link: { component: 'LinkBeta' },
181
- Blockquote: { component: 'BlockquoteBeta' },
352
+ Link: { component: 'LinkBeta' }, // Convert Link to LinkBeta
353
+ Blockquote: { component: 'BlockquoteBeta' }, // Convert Blockquote to BlockquoteBeta
354
+ BlockquoteFooter: { component: 'BlockquoteFooterBeta' }, // Convert BlockquoteFooter to BlockquoteFooterBeta
355
+ UnorderedList: { component: 'UnorderedListBeta' },
356
+ NumberedList: { component: 'NumberedListBeta' },
357
+ ListItem: { component: 'ListItemBeta' },
182
358
  };
183
359
 
184
360
  // Props mapping for migration
@@ -187,17 +363,43 @@ const PROPS_MAPPING = {
187
363
  };
188
364
 
189
365
  // Spacing value mapping from old margin to new spacing
366
+ // Based on the actual CSS classes in src/beta/styles.scss
367
+ // and the old margin prop values: "top" | "bottom" | "both" | "none"
190
368
  const SPACING_MAPPING = {
191
- none: 'none',
192
- top: 'md-top',
193
- bottom: 'md-bottom',
194
- left: 'md-left',
195
- right: 'md-right',
369
+ // Old margin values mapped to new spacing values
370
+ none: 'none', // No spacing
371
+ top: 'md-top', // Top margin only (medium size)
372
+ bottom: 'md-bottom', // Bottom margin only (medium size)
373
+ both: 'md', // Both top and bottom margins (medium size)
374
+
375
+ // Additional spacing values for more granular control
376
+ // These weren't in the old margin prop but are available in new spacing
377
+ left: 'md-left', // Left margin (medium size)
378
+ right: 'md-right', // Right margin (medium size)
379
+
380
+ // Size-based spacing (applies to both top and bottom)
196
381
  xs: 'xs',
197
382
  sm: 'sm',
198
383
  md: 'md',
199
384
  lg: 'lg',
200
385
  xl: 'xl',
386
+
387
+ // Specific directional spacing with sizes
388
+ 'xs-top': 'xs-top',
389
+ 'xs-bottom': 'xs-bottom',
390
+ 'sm-top': 'sm-top',
391
+ 'sm-bottom': 'sm-bottom',
392
+ 'md-top': 'md-top',
393
+ 'md-bottom': 'md-bottom',
394
+ 'lg-top': 'lg-top',
395
+ 'lg-bottom': 'lg-bottom',
396
+ 'xl-top': 'xl-top',
397
+ 'xl-bottom': 'xl-bottom',
398
+
399
+ // Extra small variants
400
+ xs2: 'xs2',
401
+ 'xs2-top': 'xs2-top',
402
+ 'xs2-bottom': 'xs2-bottom',
201
403
  };
202
404
 
203
405
  // Import patterns to handle
@@ -211,30 +413,34 @@ const IMPORT_PATTERNS = [
211
413
  // Parse JSX props more robustly
212
414
  function parseJSXProps(propsString) {
213
415
  if (!propsString || !propsString.trim()) {
214
- return { props: {}, warnings: [] };
416
+ return { props: {}, warnings: [], spreadProps: [] };
215
417
  }
216
418
 
217
419
  const props = {};
218
420
  const warnings = [];
219
- const MAX_ITERATIONS = 100; // Prevent infinite loops
220
- let iterationCount = 0;
421
+ const spreadProps = []; // Track spread props separately
422
+ const originalSyntax = {}; // Track original JSX syntax for each prop
221
423
 
222
424
  try {
223
425
  // Parse props manually to handle complex cases
224
426
  let remaining = propsString.trim();
225
- let lastRemainingLength = remaining.length;
226
427
 
227
- while (remaining.length > 0 && iterationCount < MAX_ITERATIONS) {
228
- iterationCount++;
428
+ // First, extract all spread props
429
+ const spreadRegex = /\.\.\.\{?(\w+)\}?/g;
430
+ let spreadMatch;
431
+ while ((spreadMatch = spreadRegex.exec(remaining)) !== null) {
432
+ spreadProps.push(spreadMatch[1]);
433
+ }
229
434
 
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;
435
+ // Remove spread props from the string to parse regular props
436
+ remaining = remaining.replace(/\.\.\.\{?(\w+)\}?/g, '');
437
+
438
+ // Now parse regular props
439
+ while (remaining.trim().length > 0) {
440
+ // Skip whitespace
441
+ remaining = remaining.replace(/^\s+/, '');
236
442
 
237
- // Match prop name - more efficient regex
443
+ // Match prop name
238
444
  const nameMatch = remaining.match(/^(\w+)=/);
239
445
  if (!nameMatch) break;
240
446
 
@@ -244,7 +450,7 @@ function parseJSXProps(propsString) {
244
450
 
245
451
  // Match prop value
246
452
  if (remaining.startsWith('"') || remaining.startsWith("'")) {
247
- // String value - use indexOf for better performance
453
+ // String value
248
454
  const quote = remaining[0];
249
455
  const endQuoteIndex = remaining.indexOf(quote, 1);
250
456
  if (endQuoteIndex === -1) {
@@ -254,14 +460,14 @@ function parseJSXProps(propsString) {
254
460
 
255
461
  const propValue = remaining.substring(1, endQuoteIndex);
256
462
  props[propName] = propValue;
463
+ originalSyntax[propName] = 'string'; // Mark as string literal
257
464
  remaining = remaining.substring(endQuoteIndex + 1);
258
465
  } else if (remaining.startsWith('{')) {
259
- // Object value - find matching closing brace with bounds checking
466
+ // Object value - find matching closing brace
260
467
  let braceCount = 0;
261
468
  let endIndex = -1;
262
- const maxSearchLength = Math.min(remaining.length, 1000); // Limit search length
263
469
 
264
- for (let i = 0; i < maxSearchLength; i++) {
470
+ for (let i = 0; i < remaining.length; i++) {
265
471
  if (remaining[i] === '{') braceCount++;
266
472
  if (remaining[i] === '}') {
267
473
  braceCount--;
@@ -279,25 +485,23 @@ function parseJSXProps(propsString) {
279
485
 
280
486
  const propValue = remaining.substring(1, endIndex);
281
487
  props[propName] = propValue;
488
+ originalSyntax[propName] = 'jsx'; // Mark as JSX expression
282
489
  remaining = remaining.substring(endIndex + 1);
283
490
  } else {
284
- // Boolean prop (e.g., disabled) or invalid syntax
491
+ // Boolean prop
285
492
  props[propName] = true;
493
+ originalSyntax[propName] = 'boolean'; // Mark as boolean
286
494
  break;
287
495
  }
288
496
 
289
- // Skip whitespace more efficiently
497
+ // Skip whitespace
290
498
  remaining = remaining.replace(/^\s+/, '');
291
499
  }
292
-
293
- if (iterationCount >= MAX_ITERATIONS) {
294
- warnings.push(`Maximum parsing iterations (${MAX_ITERATIONS}) reached`);
295
- }
296
500
  } catch (error) {
297
501
  warnings.push(`Failed to parse props: ${error.message}`);
298
502
  }
299
503
 
300
- return { props, warnings };
504
+ return { props, warnings, spreadProps, originalSyntax };
301
505
  }
302
506
 
303
507
  // Migrate props from old to new format
@@ -315,11 +519,12 @@ function migrateProps(props, oldComponent) {
315
519
  `Migrated 'margin="${props.margin}"' to 'spacing="${newSpacing}"'`,
316
520
  );
317
521
  } else {
318
- // Unknown margin value - keep as is but warn
319
- migratedProps.spacing = props.margin;
522
+ // Unknown margin value - suggest alternatives
523
+ const suggestions = getSpacingSuggestions(props.margin);
524
+ migratedProps.spacing = props.margin; // Keep original value for now
320
525
  delete migratedProps.margin;
321
526
  warnings.push(
322
- `Migrated 'margin="${props.margin}"' to 'spacing="${props.margin}"' (unknown value - may need manual review)`,
527
+ `Migrated 'margin="${props.margin}"' to 'spacing="${props.margin}"' (unknown value). ${suggestions}`,
323
528
  );
324
529
  }
325
530
  }
@@ -346,8 +551,55 @@ function migrateProps(props, oldComponent) {
346
551
  return { props: migratedProps, warnings };
347
552
  }
348
553
 
554
+ // Helper function to suggest spacing alternatives for unknown margin values
555
+ function getSpacingSuggestions(unknownMargin) {
556
+ const suggestions = [];
557
+
558
+ // Check if it might be one of the old margin values
559
+ if (['top', 'bottom', 'both', 'none'].includes(unknownMargin)) {
560
+ suggestions.push(
561
+ `"${unknownMargin}" is a valid old margin value and will be migrated correctly.`,
562
+ );
563
+ return suggestions.join(' ');
564
+ }
565
+
566
+ // Check if it might be a directional value
567
+ if (
568
+ unknownMargin.includes('top') ||
569
+ unknownMargin.includes('bottom') ||
570
+ unknownMargin.includes('left') ||
571
+ unknownMargin.includes('right')
572
+ ) {
573
+ suggestions.push(
574
+ 'Consider using directional spacing like "md-top", "sm-bottom", etc.',
575
+ );
576
+ }
577
+
578
+ // Check if it might be a size value
579
+ if (['xs', 'sm', 'md', 'lg', 'xl'].includes(unknownMargin)) {
580
+ suggestions.push(
581
+ 'Consider using size-based spacing like "xs", "sm", "md", "lg", "xl".',
582
+ );
583
+ }
584
+
585
+ // Check if it might be a specific variant
586
+ if (unknownMargin.includes('xs2')) {
587
+ suggestions.push(
588
+ 'Consider using "xs2", "xs2-top", or "xs2-bottom" for extra small spacing.',
589
+ );
590
+ }
591
+
592
+ if (suggestions.length === 0) {
593
+ suggestions.push(
594
+ 'Old margin values: "none", "top", "bottom", "both". New spacing values: "xs", "sm", "md", "lg", "xl", and directional variants.',
595
+ );
596
+ }
597
+
598
+ return suggestions.join(' ');
599
+ }
600
+
349
601
  // Convert props object back to JSX string
350
- function propsToString(props) {
602
+ function propsToString(props, originalSyntax = {}) {
351
603
  if (!props || Object.keys(props).length === 0) {
352
604
  return '';
353
605
  }
@@ -356,20 +608,30 @@ function propsToString(props) {
356
608
  ' ' +
357
609
  Object.entries(props)
358
610
  .map(([key, value]) => {
359
- // Handle different value types
360
- if (typeof value === 'string' && !value.includes('{')) {
611
+ // Use original syntax information if available
612
+ if (originalSyntax[key] === 'string') {
361
613
  return `${key}="${value}"`;
362
- } else if (
363
- typeof value === 'string' &&
364
- value.startsWith('{') &&
365
- value.endsWith('}')
366
- ) {
367
- // Already a JSX object, don't add extra braces
614
+ } else if (originalSyntax[key] === 'jsx') {
368
615
  return `${key}={${value}}`;
616
+ } else if (originalSyntax[key] === 'boolean') {
617
+ return value ? key : '';
369
618
  } else {
370
- return `${key}={${value}}`;
619
+ // Fallback logic for when originalSyntax is not available
620
+ if (typeof value === 'string' && !value.includes('{')) {
621
+ return `${key}="${value}"`;
622
+ } else if (
623
+ typeof value === 'string' &&
624
+ value.startsWith('{') &&
625
+ value.endsWith('}')
626
+ ) {
627
+ // Already a JSX object, don't add extra braces
628
+ return `${key}={${value}}`;
629
+ } else {
630
+ return `${key}={${value}}`;
631
+ }
371
632
  }
372
633
  })
634
+ .filter(prop => prop.length > 0) // Remove empty props (like false booleans)
373
635
  .join(' ')
374
636
  );
375
637
  }
@@ -379,12 +641,46 @@ function updateImports(content) {
379
641
  let updatedContent = content;
380
642
  let changes = 0;
381
643
 
644
+ // First, update import paths
382
645
  IMPORT_PATTERNS.forEach(pattern => {
383
646
  const matches = content.match(pattern) || [];
384
647
  changes += matches.length;
385
648
  updatedContent = updatedContent.replace(pattern, `from '${BETA_IMPORT}'`);
386
649
  });
387
650
 
651
+ // Then, update destructured import names - only within @entur/typography imports
652
+ // Find all import statements from @entur/typography and update component names
653
+ const importRegex =
654
+ /import\s*{([^}]+)}\s*from\s*['"']@entur\/typography['"']/g;
655
+
656
+ updatedContent = updatedContent.replace(importRegex, (match, importList) => {
657
+ let updatedImportList = importList;
658
+ let hasChanges = false;
659
+ const uniqueComponents = new Set();
660
+
661
+ // Check each component in the import list
662
+ Object.entries(COMPONENT_MAPPING).forEach(([oldComponent, mapping]) => {
663
+ const componentRegex = new RegExp(`\\b${oldComponent}\\b`, 'g');
664
+ if (componentRegex.test(updatedImportList)) {
665
+ updatedImportList = updatedImportList.replace(
666
+ componentRegex,
667
+ mapping.component,
668
+ );
669
+ uniqueComponents.add(mapping.component);
670
+ hasChanges = true;
671
+ }
672
+ });
673
+
674
+ if (hasChanges) {
675
+ changes++;
676
+ // Deduplicate components and create clean import statement
677
+ const finalImportList = Array.from(uniqueComponents).join(', ');
678
+ return `import {${finalImportList}} from '${BETA_IMPORT}'`;
679
+ }
680
+
681
+ return match;
682
+ });
683
+
388
684
  return { content: updatedContent, changes };
389
685
  }
390
686
 
@@ -404,8 +700,12 @@ function updateComponents(content) {
404
700
  changes++;
405
701
 
406
702
  // Parse existing props
407
- const { props: existingProps, warnings: parseWarnings } =
408
- parseJSXProps(propsString);
703
+ const {
704
+ props: existingProps,
705
+ warnings: parseWarnings,
706
+ spreadProps,
707
+ originalSyntax,
708
+ } = parseJSXProps(propsString);
409
709
  warnings.push(...parseWarnings);
410
710
 
411
711
  // Migrate props
@@ -425,8 +725,10 @@ function updateComponents(content) {
425
725
 
426
726
  // Handle Heading components
427
727
  if (mapping.component === 'Heading') {
428
- const asValue = newProps.as || mapping.as;
429
- const variantValue = newProps.variant || mapping.variant;
728
+ // Preserve existing 'as' prop if it exists, otherwise use mapping default
729
+ const asValue = existingProps.as || mapping.as;
730
+ // Preserve existing 'variant' prop if it exists, otherwise use mapping default
731
+ const variantValue = existingProps.variant || mapping.variant;
430
732
 
431
733
  // Remove as and variant from props since we'll add them separately
432
734
  delete newProps.as;
@@ -440,8 +742,10 @@ function updateComponents(content) {
440
742
  }
441
743
  Object.assign(orderedProps, newProps);
442
744
 
443
- const propsString = propsToString(orderedProps);
444
- return `<Heading as="${asValue}" variant="${variantValue}"${propsString}>`;
745
+ const propsString = propsToString(orderedProps, originalSyntax);
746
+ const spreadPropsString =
747
+ spreadProps.length > 0 ? ` {...${spreadProps.join(', ...')}}` : '';
748
+ return `<Heading as="${asValue}" variant="${variantValue}"${propsString}${spreadPropsString}>`;
445
749
  }
446
750
 
447
751
  // Handle other components
@@ -463,8 +767,10 @@ function updateComponents(content) {
463
767
  });
464
768
  Object.assign(finalProps, newProps);
465
769
 
466
- const otherPropsString = propsToString(finalProps);
467
- return `<${componentName}${otherPropsString}>`;
770
+ const otherPropsString = propsToString(finalProps, originalSyntax);
771
+ const spreadPropsString =
772
+ spreadProps.length > 0 ? ` {...${spreadProps.join(', ...')}}` : '';
773
+ return `<${componentName}${otherPropsString}${spreadPropsString}>`;
468
774
  },
469
775
  );
470
776
 
@@ -492,15 +798,33 @@ function updateComponents(content) {
492
798
  * @returns {string[]} Array of matching file paths
493
799
  */
494
800
  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,
801
+ const allFiles = [];
802
+
803
+ // Process directory patterns
804
+ const directoryPatterns = ALLOWED_DIRECTORIES.filter(dir =>
805
+ dir.includes('**'),
806
+ );
807
+ const filePatterns = ALLOWED_DIRECTORIES.filter(dir => !dir.includes('**'));
808
+
809
+ // Handle directory patterns (e.g., src/**, app/**)
810
+ if (directoryPatterns.length > 0) {
811
+ const combinedDirPattern = `{${directoryPatterns.join(',')}}/${pattern}`;
812
+ const dirFiles = glob.sync(combinedDirPattern, {
813
+ ignore: BLOCKED_DIRECTORIES,
814
+ nodir: true,
815
+ absolute: false,
816
+ });
817
+ allFiles.push(...dirFiles);
818
+ }
819
+
820
+ // Handle file patterns (e.g., *.jsx, *.tsx)
821
+ filePatterns.forEach(filePattern => {
822
+ const files = glob.sync(filePattern, {
823
+ ignore: BLOCKED_DIRECTORIES,
824
+ nodir: true,
825
+ absolute: false,
826
+ });
827
+ allFiles.push(...files);
504
828
  });
505
829
 
506
830
  // Use Set for efficient deduplication and filtering
@@ -521,37 +845,29 @@ function findFiles(pattern) {
521
845
  return uniqueFiles;
522
846
  }
523
847
 
524
- function updateImportsAndComponents(content, strategy) {
848
+ function updateImportsAndComponents(content) {
525
849
  let updatedContent = content;
526
850
  let changes = 0;
527
851
  let warnings = [];
528
852
 
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
- }
853
+ // Update both imports and components
854
+ const { content: newContent, changes: importChanges } =
855
+ updateImports(content);
856
+ const {
857
+ content: finalContent,
858
+ changes: componentChanges,
859
+ warnings: componentWarnings,
860
+ } = updateComponents(newContent);
861
+ updatedContent = finalContent;
862
+ changes = importChanges + componentChanges;
863
+ warnings = componentWarnings;
548
864
 
549
865
  return { content: updatedContent, changes, warnings };
550
866
  }
551
867
 
552
- function generateMigrationReport(files, strategy, isDryRun = false) {
868
+ function generateMigrationReport(files, isDryRun = false) {
553
869
  const report = {
554
- strategy,
870
+ strategy: 'complete',
555
871
  totalFiles: files.length,
556
872
  migratedFiles: 0,
557
873
  totalChanges: 0,
@@ -564,21 +880,32 @@ function generateMigrationReport(files, strategy, isDryRun = false) {
564
880
  files.forEach(file => {
565
881
  try {
566
882
  const content = fs.readFileSync(file, 'utf8');
883
+
884
+ // Analyze file for problematic patterns BEFORE migration
885
+ const fileAnalysis = analyzeFile(file, content);
886
+
567
887
  const {
568
888
  content: updatedContent,
569
889
  changes,
570
890
  warnings,
571
- } = updateImportsAndComponents(content, strategy);
891
+ } = updateImportsAndComponents(content);
892
+
893
+ // Combine migration warnings with file analysis warnings
894
+ const allWarnings = [...warnings, ...fileAnalysis.warnings];
572
895
 
573
- if (changes > 0) {
896
+ if (changes > 0 || fileAnalysis.warnings.length > 0) {
574
897
  if (!isDryRun) {
575
898
  fs.writeFileSync(file, updatedContent, 'utf8');
576
899
  }
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}`));
900
+ if (changes > 0) {
901
+ report.migratedFiles++;
902
+ report.totalChanges += changes;
903
+ }
904
+ report.totalWarnings += allWarnings.length;
905
+ report.files.push({ file, changes, warnings: allWarnings });
906
+ report.warnings.push(
907
+ ...allWarnings.map(warning => `${file}: ${warning}`),
908
+ );
582
909
  }
583
910
  } catch (error) {
584
911
  report.warnings.push(`${file}: Error processing file - ${error.message}`);
@@ -620,24 +947,135 @@ function printReport(report) {
620
947
  w.includes('check for conflicts'),
621
948
  );
622
949
 
950
+ // New warning types from file analysis
951
+ const styleConflictWarnings = report.warnings.filter(
952
+ w => w.includes('style conflicts') || w.includes('style and margin'),
953
+ );
954
+ const nestedTypographyWarnings = report.warnings.filter(w =>
955
+ w.includes('nested typography'),
956
+ );
957
+ const accessibilityWarnings = report.warnings.filter(
958
+ w => w.includes('missing as prop') || w.includes('accessibility'),
959
+ );
960
+ const semanticMismatchWarnings = report.warnings.filter(w =>
961
+ w.includes('semantic mismatch'),
962
+ );
963
+
623
964
  if (marginWarnings.length > 0) {
624
965
  console.log(
625
966
  `\n ๐Ÿ”„ Margin โ†’ Spacing Migrations (${marginWarnings.length}):`,
626
967
  );
627
- marginWarnings.forEach(warning => console.log(` ${warning}`));
968
+ // Show first 5 warnings, then summarize the rest
969
+ marginWarnings
970
+ .slice(0, 5)
971
+ .forEach(warning => console.log(` ${warning}`));
972
+ if (marginWarnings.length > 5) {
973
+ console.log(
974
+ ` ... and ${marginWarnings.length - 5} more similar warnings`,
975
+ );
976
+ }
628
977
  }
629
978
 
630
979
  if (semanticWarnings.length > 0) {
631
980
  console.log(`\n ๐ŸŽฏ Semantic HTML Issues (${semanticWarnings.length}):`);
632
- semanticWarnings.forEach(warning => console.log(` ${warning}`));
981
+ // Show first 5 warnings, then summarize the rest
982
+ semanticWarnings
983
+ .slice(0, 5)
984
+ .forEach(warning => console.log(` ${warning}`));
985
+ if (semanticWarnings.length > 5) {
986
+ console.log(
987
+ ` ... and ${semanticWarnings.length - 5} more similar warnings`,
988
+ );
989
+ }
633
990
  }
634
991
 
635
992
  if (conflictWarnings.length > 0) {
636
993
  console.log(`\n ๐Ÿšจ Style Conflicts (${conflictWarnings.length}):`);
637
- conflictWarnings.forEach(warning => console.log(` ${warning}`));
994
+ // Show first 5 warnings, then summarize the rest
995
+ conflictWarnings
996
+ .slice(0, 5)
997
+ .forEach(warning => console.log(` ${warning}`));
998
+ if (conflictWarnings.length > 5) {
999
+ console.log(
1000
+ ` ... and ${conflictWarnings.length - 5} more similar warnings`,
1001
+ );
1002
+ }
638
1003
  console.log(` โ†’ Review these components for styling conflicts`);
639
1004
  }
640
1005
 
1006
+ // Display new warning types
1007
+ if (styleConflictWarnings.length > 0) {
1008
+ console.log(
1009
+ `\n ๐ŸŽจ Style + Margin Conflicts (${styleConflictWarnings.length}):`,
1010
+ );
1011
+ styleConflictWarnings
1012
+ .slice(0, 5)
1013
+ .forEach(warning => console.log(` ${warning}`));
1014
+ if (styleConflictWarnings.length > 5) {
1015
+ console.log(
1016
+ ` ... and ${
1017
+ styleConflictWarnings.length - 5
1018
+ } more similar warnings`,
1019
+ );
1020
+ }
1021
+ console.log(` โ†’ Remove margin prop when using inline styles`);
1022
+ }
1023
+
1024
+ if (nestedTypographyWarnings.length > 0) {
1025
+ console.log(
1026
+ `\n ๐Ÿšซ Nested Typography (${nestedTypographyWarnings.length}):`,
1027
+ );
1028
+ nestedTypographyWarnings
1029
+ .slice(0, 5)
1030
+ .forEach(warning => console.log(` ${warning}`));
1031
+ if (nestedTypographyWarnings.length > 5) {
1032
+ console.log(
1033
+ ` ... and ${
1034
+ nestedTypographyWarnings.length - 5
1035
+ } more similar warnings`,
1036
+ );
1037
+ }
1038
+ console.log(
1039
+ ` โ†’ Use spans or other inline elements instead of nested Text components`,
1040
+ );
1041
+ }
1042
+
1043
+ if (accessibilityWarnings.length > 0) {
1044
+ console.log(
1045
+ `\n โ™ฟ Accessibility Issues (${accessibilityWarnings.length}):`,
1046
+ );
1047
+ accessibilityWarnings
1048
+ .slice(0, 5)
1049
+ .forEach(warning => console.log(` ${warning}`));
1050
+ if (accessibilityWarnings.length > 5) {
1051
+ console.log(
1052
+ ` ... and ${
1053
+ accessibilityWarnings.length - 5
1054
+ } more similar warnings`,
1055
+ );
1056
+ }
1057
+ console.log(
1058
+ ` โ†’ Add 'as' prop to Heading components for proper semantic HTML`,
1059
+ );
1060
+ }
1061
+
1062
+ if (semanticMismatchWarnings.length > 0) {
1063
+ console.log(
1064
+ `\n ๐Ÿ” Semantic Mismatches (${semanticMismatchWarnings.length}):`,
1065
+ );
1066
+ semanticMismatchWarnings
1067
+ .slice(0, 5)
1068
+ .forEach(warning => console.log(` ${warning}`));
1069
+ if (semanticMismatchWarnings.length > 5) {
1070
+ console.log(
1071
+ ` ... and ${
1072
+ semanticMismatchWarnings.length - 5
1073
+ } more similar warnings`,
1074
+ );
1075
+ }
1076
+ console.log(` โ†’ Review heading level and variant combinations`);
1077
+ }
1078
+
641
1079
  console.log('\n๐Ÿ“‹ Summary:');
642
1080
  if (marginWarnings.length > 0)
643
1081
  console.log(
@@ -651,26 +1089,42 @@ function printReport(report) {
651
1089
  console.log(
652
1090
  ` โ€ข ${conflictWarnings.length} style conflicts need manual review`,
653
1091
  );
1092
+ if (styleConflictWarnings.length > 0)
1093
+ console.log(
1094
+ ` โ€ข ${styleConflictWarnings.length} style + margin conflicts detected`,
1095
+ );
1096
+ if (nestedTypographyWarnings.length > 0)
1097
+ console.log(
1098
+ ` โ€ข ${nestedTypographyWarnings.length} nested typography components found`,
1099
+ );
1100
+ if (accessibilityWarnings.length > 0)
1101
+ console.log(
1102
+ ` โ€ข ${accessibilityWarnings.length} accessibility issues need attention`,
1103
+ );
1104
+ if (semanticMismatchWarnings.length > 0)
1105
+ console.log(
1106
+ ` โ€ข ${semanticMismatchWarnings.length} semantic mismatches detected`,
1107
+ );
1108
+
1109
+ // Add helpful note about warning limits
1110
+ if (report.warnings.length > 15) {
1111
+ console.log(
1112
+ '\n๐Ÿ’ก Note: Only showing first 5 warnings of each type to avoid overwhelming output.',
1113
+ );
1114
+ console.log(
1115
+ ' All warnings are still logged in the migration report above.',
1116
+ );
1117
+ }
654
1118
  }
655
1119
  }
656
1120
 
657
- function showNextSteps(strategy) {
1121
+ function showNextSteps() {
658
1122
  console.log('\n๐Ÿ“ Next Steps');
659
1123
  console.log('=============');
660
1124
 
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
- }
1125
+ console.log('1. ๐Ÿงช Test your application thoroughly');
1126
+ console.log('2. ๐Ÿ”„ Review and adjust any component props if needed');
1127
+ console.log('3. ๐Ÿ“š Read the migration guide on our website');
674
1128
 
675
1129
  console.log('\nโš ๏ธ Important Notes:');
676
1130
  console.log('- Check warnings above for potential issues');
@@ -699,23 +1153,19 @@ function main() {
699
1153
  console.log(
700
1154
  ' --dry-run Show what would be changed without modifying files',
701
1155
  );
702
- console.log(
703
- ' --import-only Import-only migration: update import paths only',
704
- );
705
-
706
1156
  console.log(' --help, -h Show this help message');
707
1157
  console.log('');
708
- console.log('Migration Modes:');
709
- console.log(' ๐Ÿš€ Complete Mode (default): Updates everything');
1158
+ console.log('Migration Mode:');
1159
+ console.log(' ๐Ÿš€ Complete Mode: Updates everything');
710
1160
  console.log(' - Replaces old components with beta components');
711
- console.log(' - May require prop/styling updates');
712
- console.log(' - Test thoroughly after migration');
713
- console.log('');
1161
+ console.log(' - Heading1-6 โ†’ Heading with as/variant props');
1162
+ console.log(' - Text components โ†’ Text with variant props');
1163
+ console.log(' - Link โ†’ LinkBeta, Blockquote โ†’ BlockquoteBeta');
714
1164
  console.log(
715
- ' ๐Ÿ“ Import-Only Mode (--import-only): Only updates import paths',
1165
+ ' - Lists โ†’ UnorderedListBeta, NumberedListBeta, ListItemBeta',
716
1166
  );
717
- console.log(' - Keeps your existing component usage unchanged');
718
- console.log(' - Minimal risk, gradual migration');
1167
+ console.log(' - May require prop/styling updates');
1168
+ console.log(' - Test thoroughly after migration');
719
1169
  console.log('');
720
1170
  console.log('Examples:');
721
1171
  console.log(' # See what would be changed');
@@ -723,12 +1173,6 @@ function main() {
723
1173
  console.log('');
724
1174
  console.log(' # Complete migration: update everything (default)');
725
1175
  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
1176
 
733
1177
  console.log('Environment Variables:');
734
1178
  console.log(
@@ -744,7 +1188,7 @@ function main() {
744
1188
  console.log(' Add/remove folder patterns between the ๐Ÿ‘‡ and ๐Ÿ‘† markers');
745
1189
  console.log(' Examples: "src/**", "app/**", "packages/my-app/**"');
746
1190
  console.log('');
747
- console.log(' Option 2: Set environment variable (for CI/CD)');
1191
+ console.log(' Option 2: Set environment variable');
748
1192
  console.log(
749
1193
  ' export TYPOGRAPHY_MIGRATION_DIRS="src/**,app/**,components/**"',
750
1194
  );
@@ -812,36 +1256,30 @@ function main() {
812
1256
 
813
1257
  // Parse command line options
814
1258
  const isDryRun = process.argv.includes('--dry-run');
815
- const isImportOnly = process.argv.includes('--import-only');
816
1259
 
817
1260
  if (isDryRun) {
818
1261
  console.log('๐Ÿ” DRY RUN MODE: No files will be modified');
819
1262
  console.log('');
820
1263
  }
821
1264
 
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
- }
1265
+ console.log('๐Ÿš€ COMPLETE MIGRATION: Updating imports + component usage');
1266
+ console.log('โš ๏ธ WARNING: This will modify your component usage!');
1267
+ console.log(' - Old components will be replaced with beta components');
1268
+ console.log(
1269
+ ' - Link โ†’ LinkBeta, Blockquote โ†’ BlockquoteBeta, Lists โ†’ ListBeta components',
1270
+ );
1271
+ console.log(
1272
+ ' - List components โ†’ UnorderedListBeta, NumberedListBeta, ListItemBeta',
1273
+ );
1274
+ console.log(' - You may need to update props and styling');
1275
+ console.log(' - Test thoroughly after migration');
834
1276
 
835
1277
  console.log('');
836
1278
 
837
1279
  // Perform migration
838
- const report = generateMigrationReport(
839
- allFiles,
840
- isImportOnly ? 'import-only' : 'complete',
841
- isDryRun,
842
- );
1280
+ const report = generateMigrationReport(allFiles, isDryRun);
843
1281
  printReport(report);
844
- showNextSteps(isImportOnly ? 'import-only' : 'complete');
1282
+ showNextSteps();
845
1283
 
846
1284
  console.log('\n๐ŸŽฏ Migration complete!');
847
1285
  }