@entur/typography 1.9.13 → 1.10.0-beta.0

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.
Files changed (41) hide show
  1. package/README.md +101 -4
  2. package/dist/BaseHeading.d.ts +18 -0
  3. package/dist/Blockquote.d.ts +12 -0
  4. package/dist/CodeText.d.ts +16 -0
  5. package/dist/EmphasizedText.d.ts +20 -0
  6. package/dist/Heading1.d.ts +20 -0
  7. package/dist/Heading2.d.ts +20 -0
  8. package/dist/Heading3.d.ts +20 -0
  9. package/dist/Heading4.d.ts +20 -0
  10. package/dist/Heading5.d.ts +20 -0
  11. package/dist/Heading6.d.ts +20 -0
  12. package/dist/Label.d.ts +20 -0
  13. package/dist/LeadParagraph.d.ts +20 -0
  14. package/dist/Link.d.ts +22 -0
  15. package/dist/ListItem.d.ts +11 -0
  16. package/dist/NumberedList.d.ts +8 -0
  17. package/dist/Paragraph.d.ts +20 -0
  18. package/dist/PreformattedText.d.ts +16 -0
  19. package/dist/SmallText.d.ts +20 -0
  20. package/dist/StrongText.d.ts +20 -0
  21. package/dist/SubLabel.d.ts +20 -0
  22. package/dist/SubParagraph.d.ts +20 -0
  23. package/dist/UnorderedList.d.ts +8 -0
  24. package/dist/beta/BlockquoteBeta.d.ts +12 -0
  25. package/dist/beta/Heading.d.ts +20 -0
  26. package/dist/beta/LinkBeta.d.ts +16 -0
  27. package/dist/beta/ListItemBeta.d.ts +16 -0
  28. package/dist/beta/NumberedListBeta.d.ts +16 -0
  29. package/dist/beta/Text.d.ts +20 -0
  30. package/dist/beta/UnorderedListBeta.d.ts +14 -0
  31. package/dist/beta/index.d.ts +9 -0
  32. package/dist/beta/types.d.ts +5 -0
  33. package/dist/beta/utils.d.ts +10 -0
  34. package/dist/index.d.ts +28 -426
  35. package/dist/styles.css +1436 -0
  36. package/dist/typography.cjs.js +254 -0
  37. package/dist/typography.cjs.js.map +1 -1
  38. package/dist/typography.esm.js +255 -1
  39. package/dist/typography.esm.js.map +1 -1
  40. package/package.json +11 -8
  41. package/scripts/migrate-typography.js +1325 -0
@@ -0,0 +1,1325 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Typography Migration Script
5
+ *
6
+ * This script helps you migrate from old typography components to new beta typography.
7
+ *
8
+ * MIGRATION MODES:
9
+ *
10
+ * šŸš€ Complete Mode (default):
11
+ * - Updates import paths AND component usage
12
+ * - Replaces old components with beta components
13
+ * - CONSEQUENCES:
14
+ * * <Heading1> becomes <Heading as="h1" variant="title-1">
15
+ * * <Paragraph> becomes <Text variant="paragraph">
16
+ * * <Link> becomes <LinkBeta>
17
+ * * <Blockquote> becomes <BlockquoteBeta>
18
+ * * <BlockquoteFooter> becomes <BlockquoteFooterBeta>
19
+ * * <UnorderedList> becomes <UnorderedListBeta>
20
+ * * <NumberedList> becomes <NumberedListBeta>
21
+ * * <ListItem> becomes <ListItemBeta>
22
+ * * Props may need updates (e.g., different prop names)
23
+ * * Styling classes may change
24
+ * * Test thoroughly after migration!
25
+ *
26
+
27
+ *
28
+ * Usage:
29
+ * 1. Run this script in your project root
30
+ * 2. Choose your migration mode (complete)
31
+ * 3. Update your styles as needed
32
+ * 4. Test your application thoroughly
33
+ *
34
+ * Options:
35
+ * --dry-run Show what would be changed without modifying files
36
+ *
37
+ * Environment Variables:
38
+ * TYPOGRAPHY_MIGRATION_DIRS Comma-separated list of directories to scan
39
+ * Example: "src/**,app/**"
40
+ *
41
+ * Security Features:
42
+ * - Only scans allowed directories (src/**, app/**, etc.)
43
+ * - Never scans node_modules, dist, build, .git, etc.
44
+ * - Dry-run mode for safe testing
45
+ * - Path validation prevents directory traversal attacks
46
+ *
47
+ */
48
+
49
+ const fs = require('fs');
50
+ const path = require('path');
51
+
52
+ // Check if glob is available
53
+ let glob;
54
+ try {
55
+ glob = require('glob');
56
+ } catch (error) {
57
+ console.error(
58
+ 'āŒ Error: The "glob" package is required to run this migration script.',
59
+ );
60
+ console.error('');
61
+ console.error('Please install it:');
62
+ console.error(' npm install glob');
63
+ console.error(' yarn add glob');
64
+ console.error('');
65
+ console.error('Or use npx which will handle dependencies automatically:');
66
+ console.error(' npx @entur/typography@latest migrate');
67
+ console.error('');
68
+ process.exit(1);
69
+ }
70
+
71
+ // Configuration
72
+ const OLD_IMPORT = '@entur/typography';
73
+ const BETA_IMPORT = '@entur/typography';
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
+
100
+ // =============================================================================
101
+ // šŸŽÆ MIGRATION FOLDERS CONFIGURATION
102
+ // =============================================================================
103
+ //
104
+ // EDIT THIS SECTION TO CONTROL WHICH FOLDERS ARE SCANNED
105
+ //
106
+ // ADD FOLDERS: Add new patterns to scan additional directories
107
+ // REMOVE FOLDERS: Delete patterns you don't want to scan
108
+ // CLEAR ALL: Remove all patterns to scan only what you add
109
+ //
110
+ // Examples:
111
+ // 'src/**' - Scan src folder and all subdirectories
112
+ // 'app/**' - Scan app folder and all subdirectories
113
+ // 'packages/my-app/**' - Scan specific package
114
+ // 'frontend/**' - Scan frontend directory
115
+ // 'shared/**' - Scan shared components
116
+ // 'components/**' - Scan components folder
117
+ //
118
+ // =============================================================================
119
+
120
+ const MIGRATION_FOLDERS = [
121
+ // šŸ‘‡ ADD YOUR FOLDERS HERE šŸ‘‡
122
+ 'src/**',
123
+ 'app/**',
124
+ 'apps/**',
125
+ 'components/**',
126
+ 'pages/**',
127
+ 'lib/**',
128
+ 'utils/**',
129
+ 'styles/**',
130
+ 'css/**',
131
+ 'scss/**',
132
+ // šŸ‘† ADD YOUR FOLDERS ABOVE šŸ‘†
133
+ ];
134
+
135
+ // =============================================================================
136
+
137
+ // Validate and sanitize directory input for security
138
+ function validateDirectoryPath(dir) {
139
+ return !path.isAbsolute(dir) && !dir.includes('..') && !dir.includes('~');
140
+ }
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
+
289
+ let ALLOWED_DIRECTORIES = process.env.TYPOGRAPHY_MIGRATION_DIRS
290
+ ? process.env.TYPOGRAPHY_MIGRATION_DIRS.split(',')
291
+ : MIGRATION_FOLDERS;
292
+
293
+ // Filter out potentially dangerous paths
294
+ ALLOWED_DIRECTORIES = ALLOWED_DIRECTORIES.filter(validateDirectoryPath);
295
+
296
+ if (ALLOWED_DIRECTORIES.length === 0) {
297
+ console.error(
298
+ 'āŒ Error: No valid migration directories found after security validation.',
299
+ );
300
+ console.error(
301
+ 'All directory paths must be relative and not contain ".." or "~".',
302
+ );
303
+ console.error('');
304
+ console.error('Valid examples:');
305
+ console.error(' src/**');
306
+ console.error(' app/**');
307
+ console.error(' components/**');
308
+ console.error('');
309
+ console.error('Invalid examples:');
310
+ console.error(' /absolute/path');
311
+ console.error(' ../parent/directory');
312
+ console.error(' ~/home/directory');
313
+ process.exit(1);
314
+ }
315
+
316
+ // Security: Block-list of directories to never scan
317
+ const BLOCKED_DIRECTORIES = [
318
+ '**/node_modules/**',
319
+ '**/dist/**',
320
+ '**/build/**',
321
+ '**/.git/**',
322
+ '**/coverage/**',
323
+ '**/.next/**',
324
+ '**/.nuxt/**',
325
+ '**/public/**',
326
+ '**/static/**',
327
+ '**/assets/**',
328
+ '**/images/**',
329
+ '**/fonts/**',
330
+ '**/vendor/**',
331
+ '**/temp/**',
332
+ '**/tmp/**',
333
+ ];
334
+
335
+ // Component mapping for complete migration
336
+ const COMPONENT_MAPPING = {
337
+ Heading1: { component: 'Heading', as: 'h1', variant: 'title-1' },
338
+ Heading2: { component: 'Heading', as: 'h2', variant: 'title-2' },
339
+ Heading3: { component: 'Heading', as: 'h3', variant: 'subtitle-1' },
340
+ Heading4: { component: 'Heading', as: 'h4', variant: 'subtitle-2' },
341
+ Heading5: { component: 'Heading', as: 'h5', variant: 'section-1' },
342
+ Heading6: { component: 'Heading', as: 'h6', variant: 'section-2' },
343
+ Paragraph: { component: 'Text', variant: 'paragraph' },
344
+ LeadParagraph: { component: 'Text', variant: 'leading' },
345
+ SmallText: { component: 'Text', variant: 'subparagraph' },
346
+ StrongText: { component: 'Text', as: 'strong', weight: 'bold' },
347
+ SubLabel: { component: 'Text', variant: 'sublabel' },
348
+ SubParagraph: { component: 'Text', variant: 'subparagraph' },
349
+ Label: { component: 'Text', variant: 'label' },
350
+ EmphasizedText: { component: 'Text', variant: 'emphasized' },
351
+ CodeText: { component: 'Text', variant: 'code-text' },
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' },
358
+ };
359
+
360
+ // Props mapping for migration
361
+ const PROPS_MAPPING = {
362
+ margin: 'spacing',
363
+ };
364
+
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"
368
+ const SPACING_MAPPING = {
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)
381
+ xs: 'xs',
382
+ sm: 'sm',
383
+ md: 'md',
384
+ lg: 'lg',
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',
403
+ };
404
+
405
+ // Import patterns to handle
406
+ const IMPORT_PATTERNS = [
407
+ /from\s+['"`]@entur\/typography['"`]/g,
408
+ /from\s+['"`]@entur\/typography\/dist['"`]/g,
409
+ /from\s+['"`]@entur\/typography\/dist\/index['"`]/g,
410
+ /from\s+['"`]@entur\/typography\/dist\/styles\.css['"`]/g,
411
+ ];
412
+
413
+ // Parse JSX props more robustly
414
+ function parseJSXProps(propsString) {
415
+ if (!propsString || !propsString.trim()) {
416
+ return { props: {}, warnings: [], spreadProps: [] };
417
+ }
418
+
419
+ const props = {};
420
+ const warnings = [];
421
+ const spreadProps = []; // Track spread props separately
422
+ const originalSyntax = {}; // Track original JSX syntax for each prop
423
+
424
+ try {
425
+ // Parse props manually to handle complex cases
426
+ let remaining = propsString.trim();
427
+
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
+ }
434
+
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+/, '');
442
+
443
+ // Match prop name
444
+ const nameMatch = remaining.match(/^(\w+)=/);
445
+ if (!nameMatch) break;
446
+
447
+ const propName = nameMatch[1];
448
+ const matchLength = nameMatch[0].length;
449
+ remaining = remaining.substring(matchLength);
450
+
451
+ // Match prop value
452
+ if (remaining.startsWith('"') || remaining.startsWith("'")) {
453
+ // String value
454
+ const quote = remaining[0];
455
+ const endQuoteIndex = remaining.indexOf(quote, 1);
456
+ if (endQuoteIndex === -1) {
457
+ warnings.push(`Unterminated string in prop ${propName}`);
458
+ break;
459
+ }
460
+
461
+ const propValue = remaining.substring(1, endQuoteIndex);
462
+ props[propName] = propValue;
463
+ originalSyntax[propName] = 'string'; // Mark as string literal
464
+ remaining = remaining.substring(endQuoteIndex + 1);
465
+ } else if (remaining.startsWith('{')) {
466
+ // Object value - find matching closing brace
467
+ let braceCount = 0;
468
+ let endIndex = -1;
469
+
470
+ for (let i = 0; i < remaining.length; i++) {
471
+ if (remaining[i] === '{') braceCount++;
472
+ if (remaining[i] === '}') {
473
+ braceCount--;
474
+ if (braceCount === 0) {
475
+ endIndex = i;
476
+ break;
477
+ }
478
+ }
479
+ }
480
+
481
+ if (endIndex === -1) {
482
+ warnings.push(`Unterminated object in prop ${propName}`);
483
+ break;
484
+ }
485
+
486
+ const propValue = remaining.substring(1, endIndex);
487
+ props[propName] = propValue;
488
+ originalSyntax[propName] = 'jsx'; // Mark as JSX expression
489
+ remaining = remaining.substring(endIndex + 1);
490
+ } else {
491
+ // Boolean prop
492
+ props[propName] = true;
493
+ originalSyntax[propName] = 'boolean'; // Mark as boolean
494
+ break;
495
+ }
496
+
497
+ // Skip whitespace
498
+ remaining = remaining.replace(/^\s+/, '');
499
+ }
500
+ } catch (error) {
501
+ warnings.push(`Failed to parse props: ${error.message}`);
502
+ }
503
+
504
+ return { props, warnings, spreadProps, originalSyntax };
505
+ }
506
+
507
+ // Migrate props from old to new format
508
+ function migrateProps(props, oldComponent) {
509
+ const migratedProps = { ...props };
510
+ const warnings = [];
511
+
512
+ // Handle margin prop migration
513
+ if (props.margin) {
514
+ const newSpacing = SPACING_MAPPING[props.margin];
515
+ if (newSpacing) {
516
+ migratedProps.spacing = newSpacing;
517
+ delete migratedProps.margin;
518
+ warnings.push(
519
+ `Migrated 'margin="${props.margin}"' to 'spacing="${newSpacing}"'`,
520
+ );
521
+ } else {
522
+ // Unknown margin value - suggest alternatives
523
+ const suggestions = getSpacingSuggestions(props.margin);
524
+ migratedProps.spacing = props.margin; // Keep original value for now
525
+ delete migratedProps.margin;
526
+ warnings.push(
527
+ `Migrated 'margin="${props.margin}"' to 'spacing="${props.margin}"' (unknown value). ${suggestions}`,
528
+ );
529
+ }
530
+ }
531
+
532
+ // Handle Heading components with existing 'as' prop
533
+ if (oldComponent.startsWith('Heading') && props.as) {
534
+ const headingNumber = oldComponent.replace('Heading', '');
535
+ const expectedAs = `h${headingNumber}`;
536
+
537
+ if (props.as !== expectedAs) {
538
+ warnings.push(
539
+ `Heading component has 'as="${props.as}"' but expected 'as="${expectedAs}"' - review semantic HTML structure`,
540
+ );
541
+ }
542
+ }
543
+
544
+ // Handle style prop conflicts
545
+ if (props.style && props.margin) {
546
+ warnings.push(
547
+ `Component has both 'style' and 'margin' props - check for conflicts`,
548
+ );
549
+ }
550
+
551
+ return { props: migratedProps, warnings };
552
+ }
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
+
601
+ // Convert props object back to JSX string
602
+ function propsToString(props, originalSyntax = {}) {
603
+ if (!props || Object.keys(props).length === 0) {
604
+ return '';
605
+ }
606
+
607
+ return (
608
+ ' ' +
609
+ Object.entries(props)
610
+ .map(([key, value]) => {
611
+ // Use original syntax information if available
612
+ if (originalSyntax[key] === 'string') {
613
+ return `${key}="${value}"`;
614
+ } else if (originalSyntax[key] === 'jsx') {
615
+ return `${key}={${value}}`;
616
+ } else if (originalSyntax[key] === 'boolean') {
617
+ return value ? key : '';
618
+ } else {
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
+ }
632
+ }
633
+ })
634
+ .filter(prop => prop.length > 0) // Remove empty props (like false booleans)
635
+ .join(' ')
636
+ );
637
+ }
638
+
639
+ // Update imports in content
640
+ function updateImports(content) {
641
+ let updatedContent = content;
642
+ let changes = 0;
643
+
644
+ // First, update import paths
645
+ IMPORT_PATTERNS.forEach(pattern => {
646
+ const matches = content.match(pattern) || [];
647
+ changes += matches.length;
648
+ updatedContent = updatedContent.replace(pattern, `from '${BETA_IMPORT}'`);
649
+ });
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
+ // First, collect all existing components that should be preserved
662
+ const existingComponents = importList
663
+ .split(',')
664
+ .map(comp => comp.trim())
665
+ .filter(comp => {
666
+ // Skip empty components
667
+ if (!comp) return false;
668
+
669
+ // Keep components that are:
670
+ // 1. Not in the migration mapping (old components), OR
671
+ // 2. Are the target components (new beta components)
672
+ const isOldComponent = Object.keys(COMPONENT_MAPPING).includes(comp);
673
+ const isTargetComponent = Object.values(COMPONENT_MAPPING).some(
674
+ mapping => mapping.component === comp,
675
+ );
676
+
677
+ return !isOldComponent || isTargetComponent;
678
+ });
679
+
680
+ // Then, update components that need migration
681
+ Object.entries(COMPONENT_MAPPING).forEach(([oldComponent, mapping]) => {
682
+ const componentRegex = new RegExp(`\\b${oldComponent}\\b`, 'g');
683
+ if (componentRegex.test(updatedImportList)) {
684
+ updatedImportList = updatedImportList.replace(
685
+ componentRegex,
686
+ mapping.component,
687
+ );
688
+ uniqueComponents.add(mapping.component);
689
+ hasChanges = true;
690
+ }
691
+ });
692
+
693
+ if (hasChanges) {
694
+ changes++;
695
+ // Combine existing components with migrated components
696
+ const allComponents = [
697
+ ...existingComponents,
698
+ ...Array.from(uniqueComponents),
699
+ ];
700
+
701
+ // Filter out any empty components and deduplicate
702
+ const finalComponents = allComponents
703
+ .filter(comp => comp && comp.trim())
704
+ .filter((comp, index, arr) => arr.indexOf(comp) === index); // Remove duplicates
705
+
706
+ const finalImportList = finalComponents.join(', ');
707
+ return `import {${finalImportList}} from '${BETA_IMPORT}'`;
708
+ }
709
+
710
+ return match;
711
+ });
712
+
713
+ return { content: updatedContent, changes };
714
+ }
715
+
716
+ // Update component usage with better prop handling
717
+ function updateComponents(content) {
718
+ let updatedContent = content;
719
+ let changes = 0;
720
+ let warnings = [];
721
+
722
+ Object.entries(COMPONENT_MAPPING).forEach(([oldComponent, mapping]) => {
723
+ // More robust regex to handle complex JSX
724
+ const componentRegex = new RegExp(`<${oldComponent}(\\s+[^>]*?)?>`, 'g');
725
+
726
+ updatedContent = updatedContent.replace(
727
+ componentRegex,
728
+ (match, propsString) => {
729
+ changes++;
730
+
731
+ // Parse existing props
732
+ const {
733
+ props: existingProps,
734
+ warnings: parseWarnings,
735
+ spreadProps,
736
+ originalSyntax,
737
+ } = parseJSXProps(propsString);
738
+ warnings.push(...parseWarnings);
739
+
740
+ // Migrate props
741
+ const { props: migratedProps, warnings: migrateWarnings } =
742
+ migrateProps(existingProps, oldComponent);
743
+ warnings.push(...migrateWarnings);
744
+
745
+ // Build new props from mapping
746
+ const newProps = { ...migratedProps };
747
+
748
+ // Add mapping props (but don't override existing ones)
749
+ Object.entries(mapping).forEach(([key, value]) => {
750
+ if (key !== 'component' && !newProps[key]) {
751
+ newProps[key] = value;
752
+ }
753
+ });
754
+
755
+ // Handle Heading components
756
+ if (mapping.component === 'Heading') {
757
+ // Preserve existing 'as' prop if it exists, otherwise use mapping default
758
+ const asValue = existingProps.as || mapping.as;
759
+ // Preserve existing 'variant' prop if it exists, otherwise use mapping default
760
+ const variantValue = existingProps.variant || mapping.variant;
761
+
762
+ // Remove as and variant from props since we'll add them separately
763
+ delete newProps.as;
764
+ delete newProps.variant;
765
+
766
+ // Ensure mapping props come first
767
+ const orderedProps = {};
768
+ if (newProps.spacing) {
769
+ orderedProps.spacing = newProps.spacing;
770
+ delete newProps.spacing;
771
+ }
772
+ Object.assign(orderedProps, newProps);
773
+
774
+ const propsString = propsToString(orderedProps, originalSyntax);
775
+ const spreadPropsString =
776
+ spreadProps.length > 0 ? ` {...${spreadProps.join(', ...')}}` : '';
777
+ return `<Heading as="${asValue}" variant="${variantValue}"${propsString}${spreadPropsString}>`;
778
+ }
779
+
780
+ // Handle other components
781
+ const componentName = mapping.component;
782
+
783
+ // Remove mapping props from newProps since they're already set
784
+ Object.keys(mapping).forEach(key => {
785
+ if (key !== 'component') {
786
+ delete newProps[key];
787
+ }
788
+ });
789
+
790
+ // Add mapping props in the correct order
791
+ const finalProps = {};
792
+ Object.entries(mapping).forEach(([key, value]) => {
793
+ if (key !== 'component') {
794
+ finalProps[key] = value;
795
+ }
796
+ });
797
+ Object.assign(finalProps, newProps);
798
+
799
+ const otherPropsString = propsToString(finalProps, originalSyntax);
800
+ const spreadPropsString =
801
+ spreadProps.length > 0 ? ` {...${spreadProps.join(', ...')}}` : '';
802
+ return `<${componentName}${otherPropsString}${spreadPropsString}>`;
803
+ },
804
+ );
805
+
806
+ // Update closing tags
807
+ const closingTagRegex = new RegExp(`</${oldComponent}>`, 'g');
808
+ const componentName = mapping.component;
809
+ updatedContent = updatedContent.replace(
810
+ closingTagRegex,
811
+ `</${componentName}>`,
812
+ );
813
+ });
814
+
815
+ return { content: updatedContent, changes, warnings };
816
+ }
817
+
818
+ /**
819
+ * Find files matching the given pattern in allowed directories
820
+ *
821
+ * This function uses efficient glob patterns and data structures:
822
+ * - Single glob call with brace expansion instead of multiple calls
823
+ * - Set-based extension filtering for O(1) lookups
824
+ * - No array concatenation in loops
825
+ *
826
+ * @param {string} pattern - Glob pattern to match (e.g., '*.{ts,tsx,js,jsx}')
827
+ * @returns {string[]} Array of matching file paths
828
+ */
829
+ function findFiles(pattern) {
830
+ const allFiles = [];
831
+
832
+ // Process directory patterns
833
+ const directoryPatterns = ALLOWED_DIRECTORIES.filter(dir =>
834
+ dir.includes('**'),
835
+ );
836
+ const filePatterns = ALLOWED_DIRECTORIES.filter(dir => !dir.includes('**'));
837
+
838
+ // Handle directory patterns (e.g., src/**, app/**)
839
+ if (directoryPatterns.length > 0) {
840
+ const combinedDirPattern = `{${directoryPatterns.join(',')}}/${pattern}`;
841
+ const dirFiles = glob.sync(combinedDirPattern, {
842
+ ignore: BLOCKED_DIRECTORIES,
843
+ nodir: true,
844
+ absolute: false,
845
+ });
846
+ allFiles.push(...dirFiles);
847
+ }
848
+
849
+ // Handle file patterns (e.g., *.jsx, *.tsx)
850
+ filePatterns.forEach(filePattern => {
851
+ const files = glob.sync(filePattern, {
852
+ ignore: BLOCKED_DIRECTORIES,
853
+ nodir: true,
854
+ absolute: false,
855
+ });
856
+ allFiles.push(...files);
857
+ });
858
+
859
+ // Use Set for efficient deduplication and filtering
860
+ const fileExtensions = new Set([
861
+ '.ts',
862
+ '.tsx',
863
+ '.js',
864
+ '.jsx',
865
+ '.scss',
866
+ '.css',
867
+ ]);
868
+
869
+ const uniqueFiles = allFiles.filter(file => {
870
+ const ext = path.extname(file).toLowerCase();
871
+ return fileExtensions.has(ext);
872
+ });
873
+
874
+ return uniqueFiles;
875
+ }
876
+
877
+ function updateImportsAndComponents(content) {
878
+ let updatedContent = content;
879
+ let changes = 0;
880
+ let warnings = [];
881
+
882
+ // Update both imports and components
883
+ const { content: newContent, changes: importChanges } =
884
+ updateImports(content);
885
+ const {
886
+ content: finalContent,
887
+ changes: componentChanges,
888
+ warnings: componentWarnings,
889
+ } = updateComponents(newContent);
890
+ updatedContent = finalContent;
891
+ changes = importChanges + componentChanges;
892
+ warnings = componentWarnings;
893
+
894
+ return { content: updatedContent, changes, warnings };
895
+ }
896
+
897
+ function generateMigrationReport(files, isDryRun = false) {
898
+ const report = {
899
+ strategy: 'complete',
900
+ totalFiles: files.length,
901
+ migratedFiles: 0,
902
+ totalChanges: 0,
903
+ totalWarnings: 0,
904
+ files: [],
905
+ warnings: [],
906
+ isDryRun,
907
+ };
908
+
909
+ files.forEach(file => {
910
+ try {
911
+ const content = fs.readFileSync(file, 'utf8');
912
+
913
+ // Analyze file for problematic patterns BEFORE migration
914
+ const fileAnalysis = analyzeFile(file, content);
915
+
916
+ const {
917
+ content: updatedContent,
918
+ changes,
919
+ warnings,
920
+ } = updateImportsAndComponents(content);
921
+
922
+ // Combine migration warnings with file analysis warnings
923
+ const allWarnings = [...warnings, ...fileAnalysis.warnings];
924
+
925
+ if (changes > 0 || fileAnalysis.warnings.length > 0) {
926
+ if (!isDryRun) {
927
+ fs.writeFileSync(file, updatedContent, 'utf8');
928
+ }
929
+ if (changes > 0) {
930
+ report.migratedFiles++;
931
+ report.totalChanges += changes;
932
+ }
933
+ report.totalWarnings += allWarnings.length;
934
+ report.files.push({ file, changes, warnings: allWarnings });
935
+ report.warnings.push(
936
+ ...allWarnings.map(warning => `${file}: ${warning}`),
937
+ );
938
+ }
939
+ } catch (error) {
940
+ report.warnings.push(`${file}: Error processing file - ${error.message}`);
941
+ }
942
+ });
943
+
944
+ return report;
945
+ }
946
+
947
+ function printReport(report) {
948
+ console.log('\nšŸŽ‰ Migration Report');
949
+ console.log('==================');
950
+ console.log(`Strategy: ${report.strategy}`);
951
+ console.log(`Total files scanned: ${report.totalFiles}`);
952
+ console.log(`Files migrated: ${report.migratedFiles}`);
953
+ console.log(`Total changes: ${report.totalChanges}`);
954
+ console.log(`Total warnings: ${report.totalWarnings}`);
955
+
956
+ if (report.files.length > 0) {
957
+ console.log('\nMigrated files:');
958
+ report.files.forEach(({ file, changes, warnings }) => {
959
+ console.log(
960
+ ` āœ… ${file} (${changes} changes${
961
+ warnings.length > 0 ? `, ${warnings.length} warnings` : ''
962
+ })`,
963
+ );
964
+ });
965
+ }
966
+
967
+ if (report.warnings.length > 0) {
968
+ console.log('\nāš ļø Warnings:');
969
+
970
+ // Group warnings by type
971
+ const marginWarnings = report.warnings.filter(w => w.includes('Migrated'));
972
+ const semanticWarnings = report.warnings.filter(w =>
973
+ w.includes('expected'),
974
+ );
975
+ const conflictWarnings = report.warnings.filter(w =>
976
+ w.includes('check for conflicts'),
977
+ );
978
+
979
+ // New warning types from file analysis
980
+ const styleConflictWarnings = report.warnings.filter(
981
+ w => w.includes('style conflicts') || w.includes('style and margin'),
982
+ );
983
+ const nestedTypographyWarnings = report.warnings.filter(w =>
984
+ w.includes('nested typography'),
985
+ );
986
+ const accessibilityWarnings = report.warnings.filter(
987
+ w => w.includes('missing as prop') || w.includes('accessibility'),
988
+ );
989
+ const semanticMismatchWarnings = report.warnings.filter(w =>
990
+ w.includes('semantic mismatch'),
991
+ );
992
+
993
+ if (marginWarnings.length > 0) {
994
+ console.log(
995
+ `\n šŸ”„ Margin → Spacing Migrations (${marginWarnings.length}):`,
996
+ );
997
+ // Show first 5 warnings, then summarize the rest
998
+ marginWarnings
999
+ .slice(0, 5)
1000
+ .forEach(warning => console.log(` ${warning}`));
1001
+ if (marginWarnings.length > 5) {
1002
+ console.log(
1003
+ ` ... and ${marginWarnings.length - 5} more similar warnings`,
1004
+ );
1005
+ }
1006
+ }
1007
+
1008
+ if (semanticWarnings.length > 0) {
1009
+ console.log(`\n šŸŽÆ Semantic HTML Issues (${semanticWarnings.length}):`);
1010
+ // Show first 5 warnings, then summarize the rest
1011
+ semanticWarnings
1012
+ .slice(0, 5)
1013
+ .forEach(warning => console.log(` ${warning}`));
1014
+ if (semanticWarnings.length > 5) {
1015
+ console.log(
1016
+ ` ... and ${semanticWarnings.length - 5} more similar warnings`,
1017
+ );
1018
+ }
1019
+ }
1020
+
1021
+ if (conflictWarnings.length > 0) {
1022
+ console.log(`\n 🚨 Style Conflicts (${conflictWarnings.length}):`);
1023
+ // Show first 5 warnings, then summarize the rest
1024
+ conflictWarnings
1025
+ .slice(0, 5)
1026
+ .forEach(warning => console.log(` ${warning}`));
1027
+ if (conflictWarnings.length > 5) {
1028
+ console.log(
1029
+ ` ... and ${conflictWarnings.length - 5} more similar warnings`,
1030
+ );
1031
+ }
1032
+ console.log(` → Review these components for styling conflicts`);
1033
+ }
1034
+
1035
+ // Display new warning types
1036
+ if (styleConflictWarnings.length > 0) {
1037
+ console.log(
1038
+ `\n šŸŽØ Style + Margin Conflicts (${styleConflictWarnings.length}):`,
1039
+ );
1040
+ styleConflictWarnings
1041
+ .slice(0, 5)
1042
+ .forEach(warning => console.log(` ${warning}`));
1043
+ if (styleConflictWarnings.length > 5) {
1044
+ console.log(
1045
+ ` ... and ${
1046
+ styleConflictWarnings.length - 5
1047
+ } more similar warnings`,
1048
+ );
1049
+ }
1050
+ console.log(` → Remove margin prop when using inline styles`);
1051
+ }
1052
+
1053
+ if (nestedTypographyWarnings.length > 0) {
1054
+ console.log(
1055
+ `\n 🚫 Nested Typography (${nestedTypographyWarnings.length}):`,
1056
+ );
1057
+ nestedTypographyWarnings
1058
+ .slice(0, 5)
1059
+ .forEach(warning => console.log(` ${warning}`));
1060
+ if (nestedTypographyWarnings.length > 5) {
1061
+ console.log(
1062
+ ` ... and ${
1063
+ nestedTypographyWarnings.length - 5
1064
+ } more similar warnings`,
1065
+ );
1066
+ }
1067
+ console.log(
1068
+ ` → Use spans or other inline elements instead of nested Text components`,
1069
+ );
1070
+ }
1071
+
1072
+ if (accessibilityWarnings.length > 0) {
1073
+ console.log(
1074
+ `\n ♿ Accessibility Issues (${accessibilityWarnings.length}):`,
1075
+ );
1076
+ accessibilityWarnings
1077
+ .slice(0, 5)
1078
+ .forEach(warning => console.log(` ${warning}`));
1079
+ if (accessibilityWarnings.length > 5) {
1080
+ console.log(
1081
+ ` ... and ${
1082
+ accessibilityWarnings.length - 5
1083
+ } more similar warnings`,
1084
+ );
1085
+ }
1086
+ console.log(
1087
+ ` → Add 'as' prop to Heading components for proper semantic HTML`,
1088
+ );
1089
+ }
1090
+
1091
+ if (semanticMismatchWarnings.length > 0) {
1092
+ console.log(
1093
+ `\n šŸ” Semantic Mismatches (${semanticMismatchWarnings.length}):`,
1094
+ );
1095
+ semanticMismatchWarnings
1096
+ .slice(0, 5)
1097
+ .forEach(warning => console.log(` ${warning}`));
1098
+ if (semanticMismatchWarnings.length > 5) {
1099
+ console.log(
1100
+ ` ... and ${
1101
+ semanticMismatchWarnings.length - 5
1102
+ } more similar warnings`,
1103
+ );
1104
+ }
1105
+ console.log(` → Review heading level and variant combinations`);
1106
+ }
1107
+
1108
+ console.log('\nšŸ“‹ Summary:');
1109
+ if (marginWarnings.length > 0)
1110
+ console.log(
1111
+ ` • ${marginWarnings.length} margin props migrated to spacing`,
1112
+ );
1113
+ if (semanticWarnings.length > 0)
1114
+ console.log(
1115
+ ` • ${semanticWarnings.length} semantic HTML issues need review`,
1116
+ );
1117
+ if (conflictWarnings.length > 0)
1118
+ console.log(
1119
+ ` • ${conflictWarnings.length} style conflicts need manual review`,
1120
+ );
1121
+ if (styleConflictWarnings.length > 0)
1122
+ console.log(
1123
+ ` • ${styleConflictWarnings.length} style + margin conflicts detected`,
1124
+ );
1125
+ if (nestedTypographyWarnings.length > 0)
1126
+ console.log(
1127
+ ` • ${nestedTypographyWarnings.length} nested typography components found`,
1128
+ );
1129
+ if (accessibilityWarnings.length > 0)
1130
+ console.log(
1131
+ ` • ${accessibilityWarnings.length} accessibility issues need attention`,
1132
+ );
1133
+ if (semanticMismatchWarnings.length > 0)
1134
+ console.log(
1135
+ ` • ${semanticMismatchWarnings.length} semantic mismatches detected`,
1136
+ );
1137
+
1138
+ // Add helpful note about warning limits
1139
+ if (report.warnings.length > 15) {
1140
+ console.log(
1141
+ '\nšŸ’” Note: Only showing first 5 warnings of each type to avoid overwhelming output.',
1142
+ );
1143
+ console.log(
1144
+ ' All warnings are still logged in the migration report above.',
1145
+ );
1146
+ }
1147
+ }
1148
+ }
1149
+
1150
+ function showNextSteps() {
1151
+ console.log('\nšŸ“ Next Steps');
1152
+ console.log('=============');
1153
+
1154
+ console.log('1. 🧪 Test your application thoroughly');
1155
+ console.log('2. šŸ”„ Review and adjust any component props if needed');
1156
+ console.log('3. šŸ“š Read the migration guide on our website');
1157
+
1158
+ console.log('\nāš ļø Important Notes:');
1159
+ console.log('- Check warnings above for potential issues');
1160
+ console.log('- Review migrated components for prop conflicts');
1161
+ console.log('- Test thoroughly, especially components with custom styling');
1162
+ }
1163
+
1164
+ function main() {
1165
+ // Show help if requested
1166
+ if (process.argv.includes('--help') || process.argv.includes('-h')) {
1167
+ console.log('šŸŽØ Typography Migration Script');
1168
+ console.log('==============================');
1169
+ console.log('');
1170
+ console.log('Usage:');
1171
+ console.log(' # From npm package (recommended)');
1172
+ console.log(' npx @entur/typography@latest migrate [options]');
1173
+ console.log(' yarn dlx @entur/typography@latest migrate [options]');
1174
+ console.log('');
1175
+ console.log(' # Direct execution (requires glob package)');
1176
+ console.log(' node scripts/migrate-typography.js [options]');
1177
+ console.log('');
1178
+ console.log(' # Local development');
1179
+ console.log(' npm run migrate');
1180
+ console.log('');
1181
+ console.log('Options:');
1182
+ console.log(
1183
+ ' --dry-run Show what would be changed without modifying files',
1184
+ );
1185
+ console.log(' --help, -h Show this help message');
1186
+ console.log('');
1187
+ console.log('Migration Mode:');
1188
+ console.log(' šŸš€ Complete Mode: Updates everything');
1189
+ console.log(' - Replaces old components with beta components');
1190
+ console.log(' - Heading1-6 → Heading with as/variant props');
1191
+ console.log(' - Text components → Text with variant props');
1192
+ console.log(' - Link → LinkBeta, Blockquote → BlockquoteBeta');
1193
+ console.log(
1194
+ ' - Lists → UnorderedListBeta, NumberedListBeta, ListItemBeta',
1195
+ );
1196
+ console.log(' - May require prop/styling updates');
1197
+ console.log(' - Test thoroughly after migration');
1198
+ console.log('');
1199
+ console.log('Examples:');
1200
+ console.log(' # See what would be changed');
1201
+ console.log(' npx @entur/typography@latest migrate --dry-run');
1202
+ console.log('');
1203
+ console.log(' # Complete migration: update everything (default)');
1204
+ console.log(' npx @entur/typography@latest migrate');
1205
+
1206
+ console.log('Environment Variables:');
1207
+ console.log(
1208
+ ' TYPOGRAPHY_MIGRATION_DIRS Comma-separated list of directories to scan',
1209
+ );
1210
+ console.log(' Example: "src/**,app/**"');
1211
+ console.log('');
1212
+ console.log('šŸŽÆ Customizing Scan Directories:');
1213
+ console.log(' Option 1: Edit MIGRATION_FOLDERS in the script (EASIEST)');
1214
+ console.log(
1215
+ ' Open the script and find the "MIGRATION FOLDERS CONFIGURATION" section',
1216
+ );
1217
+ console.log(' Add/remove folder patterns between the šŸ‘‡ and šŸ‘† markers');
1218
+ console.log(' Examples: "src/**", "app/**", "packages/my-app/**"');
1219
+ console.log('');
1220
+ console.log(' Option 2: Set environment variable');
1221
+ console.log(
1222
+ ' export TYPOGRAPHY_MIGRATION_DIRS="src/**,app/**,components/**"',
1223
+ );
1224
+ console.log(' node scripts/migrate-typography.js');
1225
+ console.log('');
1226
+ console.log('Security Features:');
1227
+ console.log(' - Only scans allowed directories (src/**, app/**, etc.)');
1228
+ console.log(' - Never scans node_modules, dist, build, .git, etc.)');
1229
+ console.log(' - Dry-run mode for safe testing');
1230
+ console.log('');
1231
+ process.exit(0);
1232
+ }
1233
+
1234
+ console.log('šŸŽØ Typography Migration Script');
1235
+ console.log('==============================');
1236
+ console.log('');
1237
+ console.log(
1238
+ 'This script helps you migrate from old typography to new beta typography.',
1239
+ );
1240
+ console.log('');
1241
+
1242
+ // Find files to migrate - use a single efficient pattern
1243
+ const allFiles = findFiles('*.{ts,tsx,js,jsx,scss,css}');
1244
+
1245
+ console.log(`Found ${allFiles.length} files to scan for typography imports.`);
1246
+ console.log('');
1247
+
1248
+ // Security check
1249
+ console.log('šŸ”’ Security: Only scanning allowed directories:');
1250
+ ALLOWED_DIRECTORIES.forEach(dir => {
1251
+ console.log(` āœ… ${dir}`);
1252
+ });
1253
+ console.log('');
1254
+
1255
+ // Safety check
1256
+ if (allFiles.length === 0) {
1257
+ console.log('āš ļø No files found to scan. This might mean:');
1258
+ console.log(" - You're not in the right directory");
1259
+ console.log(" - Your project structure doesn't match the allow-list");
1260
+ console.log(' - You need to run this from your project root');
1261
+ console.log('');
1262
+ console.log('Allowed directory patterns:');
1263
+ ALLOWED_DIRECTORIES.forEach(dir => console.log(` ${dir}`));
1264
+ process.exit(1);
1265
+ }
1266
+
1267
+ console.log('šŸ“ Files will be scanned in these locations:');
1268
+ const scannedDirs = [
1269
+ ...new Set(allFiles.map(file => path.dirname(file))),
1270
+ ].slice(0, 10);
1271
+
1272
+ // Show relative paths safely
1273
+ scannedDirs.forEach(dir => {
1274
+ // Ensure we don't show absolute paths
1275
+ const safeDir = path.isAbsolute(dir)
1276
+ ? path.relative(process.cwd(), dir)
1277
+ : dir;
1278
+ console.log(` šŸ“‚ ${safeDir}`);
1279
+ });
1280
+
1281
+ if (allFiles.length > 10) {
1282
+ console.log(` ... and ${allFiles.length - 10} more files`);
1283
+ }
1284
+ console.log('');
1285
+
1286
+ // Parse command line options
1287
+ const isDryRun = process.argv.includes('--dry-run');
1288
+
1289
+ if (isDryRun) {
1290
+ console.log('šŸ” DRY RUN MODE: No files will be modified');
1291
+ console.log('');
1292
+ }
1293
+
1294
+ console.log('šŸš€ COMPLETE MIGRATION: Updating imports + component usage');
1295
+ console.log('āš ļø WARNING: This will modify your component usage!');
1296
+ console.log(' - Old components will be replaced with beta components');
1297
+ console.log(
1298
+ ' - Link → LinkBeta, Blockquote → BlockquoteBeta, Lists → ListBeta components',
1299
+ );
1300
+ console.log(
1301
+ ' - List components → UnorderedListBeta, NumberedListBeta, ListItemBeta',
1302
+ );
1303
+ console.log(' - You may need to update props and styling');
1304
+ console.log(' - Test thoroughly after migration');
1305
+
1306
+ console.log('');
1307
+
1308
+ // Perform migration
1309
+ const report = generateMigrationReport(allFiles, isDryRun);
1310
+ printReport(report);
1311
+ showNextSteps();
1312
+
1313
+ console.log('\nšŸŽÆ Migration complete!');
1314
+ }
1315
+
1316
+ if (require.main === module) {
1317
+ main();
1318
+ }
1319
+
1320
+ module.exports = {
1321
+ updateImportsAndComponents,
1322
+ generateMigrationReport,
1323
+ COMPONENT_MAPPING,
1324
+ PROPS_MAPPING,
1325
+ };