@entur/typography 1.9.13-beta.3 → 1.10.0-beta.4

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 (43) hide show
  1. package/README.md +102 -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 +21 -0
  26. package/dist/beta/LinkBeta.d.ts +16 -0
  27. package/dist/beta/Text.d.ts +21 -0
  28. package/dist/beta/index.d.ts +6 -0
  29. package/dist/beta/types.d.ts +5 -0
  30. package/dist/beta/utils.d.ts +9 -0
  31. package/dist/index.d.ts +26 -426
  32. package/dist/index.js +8 -0
  33. package/dist/styles.css +838 -2
  34. package/dist/typography.cjs.development.js +508 -0
  35. package/dist/typography.cjs.development.js.map +1 -0
  36. package/dist/typography.cjs.production.min.js +2 -0
  37. package/dist/typography.cjs.production.min.js.map +1 -0
  38. package/dist/typography.esm.js +454 -392
  39. package/dist/typography.esm.js.map +1 -1
  40. package/package.json +21 -25
  41. package/scripts/migrate-typography.js +858 -0
  42. package/dist/typography.cjs.js +0 -416
  43. package/dist/typography.cjs.js.map +0 -1
@@ -0,0 +1,858 @@
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
+ * * Props may need updates (e.g., different prop names)
18
+ * * Styling classes may change
19
+ * * Test thoroughly after migration!
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
26
+ *
27
+ * Usage:
28
+ * 1. Run this script in your project root
29
+ * 2. Choose your migration mode (complete or import-only)
30
+ * 3. Update your styles as needed
31
+ * 4. Test your application thoroughly
32
+ *
33
+ * Options:
34
+ * --dry-run Show what would be changed without modifying files
35
+ * --import-only Import-only migration: update import paths only
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
+ // =============================================================================
76
+ // šŸŽÆ MIGRATION FOLDERS CONFIGURATION
77
+ // =============================================================================
78
+ //
79
+ // EDIT THIS SECTION TO CONTROL WHICH FOLDERS ARE SCANNED
80
+ //
81
+ // ADD FOLDERS: Add new patterns to scan additional directories
82
+ // REMOVE FOLDERS: Delete patterns you don't want to scan
83
+ // CLEAR ALL: Remove all patterns to scan only what you add
84
+ //
85
+ // Examples:
86
+ // 'src/**' - Scan src folder and all subdirectories
87
+ // 'app/**' - Scan app folder and all subdirectories
88
+ // 'packages/my-app/**' - Scan specific package
89
+ // 'frontend/**' - Scan frontend directory
90
+ // 'shared/**' - Scan shared components
91
+ // 'components/**' - Scan components folder
92
+ //
93
+ // =============================================================================
94
+
95
+ const MIGRATION_FOLDERS = [
96
+ // šŸ‘‡ ADD YOUR FOLDERS HERE šŸ‘‡
97
+ 'src/**',
98
+ 'app/**',
99
+ 'apps/**',
100
+ 'components/**',
101
+ 'pages/**',
102
+ 'lib/**',
103
+ 'utils/**',
104
+ 'styles/**',
105
+ 'css/**',
106
+ 'scss/**',
107
+ // šŸ‘† ADD YOUR FOLDERS ABOVE šŸ‘†
108
+ ];
109
+
110
+ // =============================================================================
111
+
112
+ // Validate and sanitize directory input for security
113
+ function validateDirectoryPath(dir) {
114
+ return !path.isAbsolute(dir) && !dir.includes('..') && !dir.includes('~');
115
+ }
116
+
117
+ let ALLOWED_DIRECTORIES = process.env.TYPOGRAPHY_MIGRATION_DIRS
118
+ ? process.env.TYPOGRAPHY_MIGRATION_DIRS.split(',')
119
+ : MIGRATION_FOLDERS;
120
+
121
+ // Filter out potentially dangerous paths
122
+ ALLOWED_DIRECTORIES = ALLOWED_DIRECTORIES.filter(validateDirectoryPath);
123
+
124
+ if (ALLOWED_DIRECTORIES.length === 0) {
125
+ console.error(
126
+ 'āŒ Error: No valid migration directories found after security validation.',
127
+ );
128
+ console.error(
129
+ 'All directory paths must be relative and not contain ".." or "~".',
130
+ );
131
+ console.error('');
132
+ console.error('Valid examples:');
133
+ console.error(' src/**');
134
+ console.error(' app/**');
135
+ console.error(' components/**');
136
+ console.error('');
137
+ console.error('Invalid examples:');
138
+ console.error(' /absolute/path');
139
+ console.error(' ../parent/directory');
140
+ console.error(' ~/home/directory');
141
+ process.exit(1);
142
+ }
143
+
144
+ // Security: Block-list of directories to never scan
145
+ const BLOCKED_DIRECTORIES = [
146
+ '**/node_modules/**',
147
+ '**/dist/**',
148
+ '**/build/**',
149
+ '**/.git/**',
150
+ '**/coverage/**',
151
+ '**/.next/**',
152
+ '**/.nuxt/**',
153
+ '**/public/**',
154
+ '**/static/**',
155
+ '**/assets/**',
156
+ '**/images/**',
157
+ '**/fonts/**',
158
+ '**/vendor/**',
159
+ '**/temp/**',
160
+ '**/tmp/**',
161
+ ];
162
+
163
+ // Component mapping for complete migration
164
+ const COMPONENT_MAPPING = {
165
+ Heading1: { component: 'Heading', as: 'h1', variant: 'title-1' },
166
+ Heading2: { component: 'Heading', as: 'h2', variant: 'title-2' },
167
+ Heading3: { component: 'Heading', as: 'h3', variant: 'subtitle-1' },
168
+ Heading4: { component: 'Heading', as: 'h4', variant: 'subtitle-2' },
169
+ Heading5: { component: 'Heading', as: 'h5', variant: 'section-1' },
170
+ Heading6: { component: 'Heading', as: 'h6', variant: 'section-2' },
171
+ Paragraph: { component: 'Text', variant: 'paragraph' },
172
+ 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' },
176
+ SubParagraph: { component: 'Text', variant: 'subparagraph' },
177
+ Label: { component: 'Text', variant: 'label' },
178
+ EmphasizedText: { component: 'Text', variant: 'emphasized' },
179
+ CodeText: { component: 'Text', variant: 'code-text' },
180
+ Link: { component: 'LinkBeta' },
181
+ Blockquote: { component: 'BlockquoteBeta' },
182
+ };
183
+
184
+ // Props mapping for migration
185
+ const PROPS_MAPPING = {
186
+ margin: 'spacing',
187
+ };
188
+
189
+ // Spacing value mapping from old margin to new spacing
190
+ const SPACING_MAPPING = {
191
+ none: 'none',
192
+ top: 'md-top',
193
+ bottom: 'md-bottom',
194
+ left: 'md-left',
195
+ right: 'md-right',
196
+ xs: 'xs',
197
+ sm: 'sm',
198
+ md: 'md',
199
+ lg: 'lg',
200
+ xl: 'xl',
201
+ };
202
+
203
+ // Import patterns to handle
204
+ const IMPORT_PATTERNS = [
205
+ /from\s+['"`]@entur\/typography['"`]/g,
206
+ /from\s+['"`]@entur\/typography\/dist['"`]/g,
207
+ /from\s+['"`]@entur\/typography\/dist\/index['"`]/g,
208
+ /from\s+['"`]@entur\/typography\/dist\/styles\.css['"`]/g,
209
+ ];
210
+
211
+ // Parse JSX props more robustly
212
+ function parseJSXProps(propsString) {
213
+ if (!propsString || !propsString.trim()) {
214
+ return { props: {}, warnings: [] };
215
+ }
216
+
217
+ const props = {};
218
+ const warnings = [];
219
+ const MAX_ITERATIONS = 100; // Prevent infinite loops
220
+ let iterationCount = 0;
221
+
222
+ try {
223
+ // Parse props manually to handle complex cases
224
+ let remaining = propsString.trim();
225
+ let lastRemainingLength = remaining.length;
226
+
227
+ while (remaining.length > 0 && iterationCount < MAX_ITERATIONS) {
228
+ iterationCount++;
229
+
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;
236
+
237
+ // Match prop name - more efficient regex
238
+ const nameMatch = remaining.match(/^(\w+)=/);
239
+ if (!nameMatch) break;
240
+
241
+ const propName = nameMatch[1];
242
+ const matchLength = nameMatch[0].length;
243
+ remaining = remaining.substring(matchLength);
244
+
245
+ // Match prop value
246
+ if (remaining.startsWith('"') || remaining.startsWith("'")) {
247
+ // String value - use indexOf for better performance
248
+ const quote = remaining[0];
249
+ const endQuoteIndex = remaining.indexOf(quote, 1);
250
+ if (endQuoteIndex === -1) {
251
+ warnings.push(`Unterminated string in prop ${propName}`);
252
+ break;
253
+ }
254
+
255
+ const propValue = remaining.substring(1, endQuoteIndex);
256
+ props[propName] = propValue;
257
+ remaining = remaining.substring(endQuoteIndex + 1);
258
+ } else if (remaining.startsWith('{')) {
259
+ // Object value - find matching closing brace with bounds checking
260
+ let braceCount = 0;
261
+ let endIndex = -1;
262
+ const maxSearchLength = Math.min(remaining.length, 1000); // Limit search length
263
+
264
+ for (let i = 0; i < maxSearchLength; i++) {
265
+ if (remaining[i] === '{') braceCount++;
266
+ if (remaining[i] === '}') {
267
+ braceCount--;
268
+ if (braceCount === 0) {
269
+ endIndex = i;
270
+ break;
271
+ }
272
+ }
273
+ }
274
+
275
+ if (endIndex === -1) {
276
+ warnings.push(`Unterminated object in prop ${propName}`);
277
+ break;
278
+ }
279
+
280
+ const propValue = remaining.substring(1, endIndex);
281
+ props[propName] = propValue;
282
+ remaining = remaining.substring(endIndex + 1);
283
+ } else {
284
+ // Boolean prop (e.g., disabled) or invalid syntax
285
+ props[propName] = true;
286
+ break;
287
+ }
288
+
289
+ // Skip whitespace more efficiently
290
+ remaining = remaining.replace(/^\s+/, '');
291
+ }
292
+
293
+ if (iterationCount >= MAX_ITERATIONS) {
294
+ warnings.push(`Maximum parsing iterations (${MAX_ITERATIONS}) reached`);
295
+ }
296
+ } catch (error) {
297
+ warnings.push(`Failed to parse props: ${error.message}`);
298
+ }
299
+
300
+ return { props, warnings };
301
+ }
302
+
303
+ // Migrate props from old to new format
304
+ function migrateProps(props, oldComponent) {
305
+ const migratedProps = { ...props };
306
+ const warnings = [];
307
+
308
+ // Handle margin prop migration
309
+ if (props.margin) {
310
+ const newSpacing = SPACING_MAPPING[props.margin];
311
+ if (newSpacing) {
312
+ migratedProps.spacing = newSpacing;
313
+ delete migratedProps.margin;
314
+ warnings.push(
315
+ `Migrated 'margin="${props.margin}"' to 'spacing="${newSpacing}"'`,
316
+ );
317
+ } else {
318
+ // Unknown margin value - keep as is but warn
319
+ migratedProps.spacing = props.margin;
320
+ delete migratedProps.margin;
321
+ warnings.push(
322
+ `Migrated 'margin="${props.margin}"' to 'spacing="${props.margin}"' (unknown value - may need manual review)`,
323
+ );
324
+ }
325
+ }
326
+
327
+ // Handle Heading components with existing 'as' prop
328
+ if (oldComponent.startsWith('Heading') && props.as) {
329
+ const headingNumber = oldComponent.replace('Heading', '');
330
+ const expectedAs = `h${headingNumber}`;
331
+
332
+ if (props.as !== expectedAs) {
333
+ warnings.push(
334
+ `Heading component has 'as="${props.as}"' but expected 'as="${expectedAs}"' - review semantic HTML structure`,
335
+ );
336
+ }
337
+ }
338
+
339
+ // Handle style prop conflicts
340
+ if (props.style && props.margin) {
341
+ warnings.push(
342
+ `Component has both 'style' and 'margin' props - check for conflicts`,
343
+ );
344
+ }
345
+
346
+ return { props: migratedProps, warnings };
347
+ }
348
+
349
+ // Convert props object back to JSX string
350
+ function propsToString(props) {
351
+ if (!props || Object.keys(props).length === 0) {
352
+ return '';
353
+ }
354
+
355
+ return (
356
+ ' ' +
357
+ Object.entries(props)
358
+ .map(([key, value]) => {
359
+ // Handle different value types
360
+ if (typeof value === 'string' && !value.includes('{')) {
361
+ 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
368
+ return `${key}={${value}}`;
369
+ } else {
370
+ return `${key}={${value}}`;
371
+ }
372
+ })
373
+ .join(' ')
374
+ );
375
+ }
376
+
377
+ // Update imports in content
378
+ function updateImports(content) {
379
+ let updatedContent = content;
380
+ let changes = 0;
381
+
382
+ IMPORT_PATTERNS.forEach(pattern => {
383
+ const matches = content.match(pattern) || [];
384
+ changes += matches.length;
385
+ updatedContent = updatedContent.replace(pattern, `from '${BETA_IMPORT}'`);
386
+ });
387
+
388
+ return { content: updatedContent, changes };
389
+ }
390
+
391
+ // Update component usage with better prop handling
392
+ function updateComponents(content) {
393
+ let updatedContent = content;
394
+ let changes = 0;
395
+ let warnings = [];
396
+
397
+ Object.entries(COMPONENT_MAPPING).forEach(([oldComponent, mapping]) => {
398
+ // More robust regex to handle complex JSX
399
+ const componentRegex = new RegExp(`<${oldComponent}(\\s+[^>]*?)?>`, 'g');
400
+
401
+ updatedContent = updatedContent.replace(
402
+ componentRegex,
403
+ (match, propsString) => {
404
+ changes++;
405
+
406
+ // Parse existing props
407
+ const { props: existingProps, warnings: parseWarnings } =
408
+ parseJSXProps(propsString);
409
+ warnings.push(...parseWarnings);
410
+
411
+ // Migrate props
412
+ const { props: migratedProps, warnings: migrateWarnings } =
413
+ migrateProps(existingProps, oldComponent);
414
+ warnings.push(...migrateWarnings);
415
+
416
+ // Build new props from mapping
417
+ const newProps = { ...migratedProps };
418
+
419
+ // Add mapping props (but don't override existing ones)
420
+ Object.entries(mapping).forEach(([key, value]) => {
421
+ if (key !== 'component' && !newProps[key]) {
422
+ newProps[key] = value;
423
+ }
424
+ });
425
+
426
+ // Handle Heading components
427
+ if (mapping.component === 'Heading') {
428
+ const asValue = newProps.as || mapping.as;
429
+ const variantValue = newProps.variant || mapping.variant;
430
+
431
+ // Remove as and variant from props since we'll add them separately
432
+ delete newProps.as;
433
+ delete newProps.variant;
434
+
435
+ // Ensure mapping props come first
436
+ const orderedProps = {};
437
+ if (newProps.spacing) {
438
+ orderedProps.spacing = newProps.spacing;
439
+ delete newProps.spacing;
440
+ }
441
+ Object.assign(orderedProps, newProps);
442
+
443
+ const propsString = propsToString(orderedProps);
444
+ return `<Heading as="${asValue}" variant="${variantValue}"${propsString}>`;
445
+ }
446
+
447
+ // Handle other components
448
+ const componentName = mapping.component;
449
+
450
+ // Remove mapping props from newProps since they're already set
451
+ Object.keys(mapping).forEach(key => {
452
+ if (key !== 'component') {
453
+ delete newProps[key];
454
+ }
455
+ });
456
+
457
+ // Add mapping props in the correct order
458
+ const finalProps = {};
459
+ Object.entries(mapping).forEach(([key, value]) => {
460
+ if (key !== 'component') {
461
+ finalProps[key] = value;
462
+ }
463
+ });
464
+ Object.assign(finalProps, newProps);
465
+
466
+ const otherPropsString = propsToString(finalProps);
467
+ return `<${componentName}${otherPropsString}>`;
468
+ },
469
+ );
470
+
471
+ // Update closing tags
472
+ const closingTagRegex = new RegExp(`</${oldComponent}>`, 'g');
473
+ const componentName = mapping.component;
474
+ updatedContent = updatedContent.replace(
475
+ closingTagRegex,
476
+ `</${componentName}>`,
477
+ );
478
+ });
479
+
480
+ return { content: updatedContent, changes, warnings };
481
+ }
482
+
483
+ /**
484
+ * Find files matching the given pattern in allowed directories
485
+ *
486
+ * This function uses efficient glob patterns and data structures:
487
+ * - Single glob call with brace expansion instead of multiple calls
488
+ * - Set-based extension filtering for O(1) lookups
489
+ * - No array concatenation in loops
490
+ *
491
+ * @param {string} pattern - Glob pattern to match (e.g., '*.{ts,tsx,js,jsx}')
492
+ * @returns {string[]} Array of matching file paths
493
+ */
494
+ 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,
504
+ });
505
+
506
+ // Use Set for efficient deduplication and filtering
507
+ const fileExtensions = new Set([
508
+ '.ts',
509
+ '.tsx',
510
+ '.js',
511
+ '.jsx',
512
+ '.scss',
513
+ '.css',
514
+ ]);
515
+
516
+ const uniqueFiles = allFiles.filter(file => {
517
+ const ext = path.extname(file).toLowerCase();
518
+ return fileExtensions.has(ext);
519
+ });
520
+
521
+ return uniqueFiles;
522
+ }
523
+
524
+ function updateImportsAndComponents(content, strategy) {
525
+ let updatedContent = content;
526
+ let changes = 0;
527
+ let warnings = [];
528
+
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
+ }
548
+
549
+ return { content: updatedContent, changes, warnings };
550
+ }
551
+
552
+ function generateMigrationReport(files, strategy, isDryRun = false) {
553
+ const report = {
554
+ strategy,
555
+ totalFiles: files.length,
556
+ migratedFiles: 0,
557
+ totalChanges: 0,
558
+ totalWarnings: 0,
559
+ files: [],
560
+ warnings: [],
561
+ isDryRun,
562
+ };
563
+
564
+ files.forEach(file => {
565
+ try {
566
+ const content = fs.readFileSync(file, 'utf8');
567
+ const {
568
+ content: updatedContent,
569
+ changes,
570
+ warnings,
571
+ } = updateImportsAndComponents(content, strategy);
572
+
573
+ if (changes > 0) {
574
+ if (!isDryRun) {
575
+ fs.writeFileSync(file, updatedContent, 'utf8');
576
+ }
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}`));
582
+ }
583
+ } catch (error) {
584
+ report.warnings.push(`${file}: Error processing file - ${error.message}`);
585
+ }
586
+ });
587
+
588
+ return report;
589
+ }
590
+
591
+ function printReport(report) {
592
+ console.log('\nšŸŽ‰ Migration Report');
593
+ console.log('==================');
594
+ console.log(`Strategy: ${report.strategy}`);
595
+ console.log(`Total files scanned: ${report.totalFiles}`);
596
+ console.log(`Files migrated: ${report.migratedFiles}`);
597
+ console.log(`Total changes: ${report.totalChanges}`);
598
+ console.log(`Total warnings: ${report.totalWarnings}`);
599
+
600
+ if (report.files.length > 0) {
601
+ console.log('\nMigrated files:');
602
+ report.files.forEach(({ file, changes, warnings }) => {
603
+ console.log(
604
+ ` āœ… ${file} (${changes} changes${
605
+ warnings.length > 0 ? `, ${warnings.length} warnings` : ''
606
+ })`,
607
+ );
608
+ });
609
+ }
610
+
611
+ if (report.warnings.length > 0) {
612
+ console.log('\nāš ļø Warnings:');
613
+
614
+ // Group warnings by type
615
+ const marginWarnings = report.warnings.filter(w => w.includes('Migrated'));
616
+ const semanticWarnings = report.warnings.filter(w =>
617
+ w.includes('expected'),
618
+ );
619
+ const conflictWarnings = report.warnings.filter(w =>
620
+ w.includes('check for conflicts'),
621
+ );
622
+
623
+ if (marginWarnings.length > 0) {
624
+ console.log(
625
+ `\n šŸ”„ Margin → Spacing Migrations (${marginWarnings.length}):`,
626
+ );
627
+ marginWarnings.forEach(warning => console.log(` ${warning}`));
628
+ }
629
+
630
+ if (semanticWarnings.length > 0) {
631
+ console.log(`\n šŸŽÆ Semantic HTML Issues (${semanticWarnings.length}):`);
632
+ semanticWarnings.forEach(warning => console.log(` ${warning}`));
633
+ }
634
+
635
+ if (conflictWarnings.length > 0) {
636
+ console.log(`\n 🚨 Style Conflicts (${conflictWarnings.length}):`);
637
+ conflictWarnings.forEach(warning => console.log(` ${warning}`));
638
+ console.log(` → Review these components for styling conflicts`);
639
+ }
640
+
641
+ console.log('\nšŸ“‹ Summary:');
642
+ if (marginWarnings.length > 0)
643
+ console.log(
644
+ ` • ${marginWarnings.length} margin props migrated to spacing`,
645
+ );
646
+ if (semanticWarnings.length > 0)
647
+ console.log(
648
+ ` • ${semanticWarnings.length} semantic HTML issues need review`,
649
+ );
650
+ if (conflictWarnings.length > 0)
651
+ console.log(
652
+ ` • ${conflictWarnings.length} style conflicts need manual review`,
653
+ );
654
+ }
655
+ }
656
+
657
+ function showNextSteps(strategy) {
658
+ console.log('\nšŸ“ Next Steps');
659
+ console.log('=============');
660
+
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
+ }
674
+
675
+ console.log('\nāš ļø Important Notes:');
676
+ console.log('- Check warnings above for potential issues');
677
+ console.log('- Review migrated components for prop conflicts');
678
+ console.log('- Test thoroughly, especially components with custom styling');
679
+ }
680
+
681
+ function main() {
682
+ // Show help if requested
683
+ if (process.argv.includes('--help') || process.argv.includes('-h')) {
684
+ console.log('šŸŽØ Typography Migration Script');
685
+ console.log('==============================');
686
+ console.log('');
687
+ console.log('Usage:');
688
+ console.log(' # From npm package (recommended)');
689
+ console.log(' npx @entur/typography@latest migrate [options]');
690
+ console.log(' yarn dlx @entur/typography@latest migrate [options]');
691
+ console.log('');
692
+ console.log(' # Direct execution (requires glob package)');
693
+ console.log(' node scripts/migrate-typography.js [options]');
694
+ console.log('');
695
+ console.log(' # Local development');
696
+ console.log(' npm run migrate');
697
+ console.log('');
698
+ console.log('Options:');
699
+ console.log(
700
+ ' --dry-run Show what would be changed without modifying files',
701
+ );
702
+ console.log(
703
+ ' --import-only Import-only migration: update import paths only',
704
+ );
705
+
706
+ console.log(' --help, -h Show this help message');
707
+ console.log('');
708
+ console.log('Migration Modes:');
709
+ console.log(' šŸš€ Complete Mode (default): Updates everything');
710
+ 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('');
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
+ console.log('Examples:');
721
+ console.log(' # See what would be changed');
722
+ console.log(' npx @entur/typography@latest migrate --dry-run');
723
+ console.log('');
724
+ console.log(' # Complete migration: update everything (default)');
725
+ 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
+
733
+ console.log('Environment Variables:');
734
+ console.log(
735
+ ' TYPOGRAPHY_MIGRATION_DIRS Comma-separated list of directories to scan',
736
+ );
737
+ console.log(' Example: "src/**,app/**"');
738
+ console.log('');
739
+ console.log('šŸŽÆ Customizing Scan Directories:');
740
+ console.log(' Option 1: Edit MIGRATION_FOLDERS in the script (EASIEST)');
741
+ console.log(
742
+ ' Open the script and find the "MIGRATION FOLDERS CONFIGURATION" section',
743
+ );
744
+ console.log(' Add/remove folder patterns between the šŸ‘‡ and šŸ‘† markers');
745
+ console.log(' Examples: "src/**", "app/**", "packages/my-app/**"');
746
+ console.log('');
747
+ console.log(' Option 2: Set environment variable (for CI/CD)');
748
+ console.log(
749
+ ' export TYPOGRAPHY_MIGRATION_DIRS="src/**,app/**,components/**"',
750
+ );
751
+ console.log(' node scripts/migrate-typography.js');
752
+ console.log('');
753
+ console.log('Security Features:');
754
+ console.log(' - Only scans allowed directories (src/**, app/**, etc.)');
755
+ console.log(' - Never scans node_modules, dist, build, .git, etc.)');
756
+ console.log(' - Dry-run mode for safe testing');
757
+ console.log('');
758
+ process.exit(0);
759
+ }
760
+
761
+ console.log('šŸŽØ Typography Migration Script');
762
+ console.log('==============================');
763
+ console.log('');
764
+ console.log(
765
+ 'This script helps you migrate from old typography to new beta typography.',
766
+ );
767
+ console.log('');
768
+
769
+ // Find files to migrate - use a single efficient pattern
770
+ const allFiles = findFiles('*.{ts,tsx,js,jsx,scss,css}');
771
+
772
+ console.log(`Found ${allFiles.length} files to scan for typography imports.`);
773
+ console.log('');
774
+
775
+ // Security check
776
+ console.log('šŸ”’ Security: Only scanning allowed directories:');
777
+ ALLOWED_DIRECTORIES.forEach(dir => {
778
+ console.log(` āœ… ${dir}`);
779
+ });
780
+ console.log('');
781
+
782
+ // Safety check
783
+ if (allFiles.length === 0) {
784
+ console.log('āš ļø No files found to scan. This might mean:');
785
+ console.log(" - You're not in the right directory");
786
+ console.log(" - Your project structure doesn't match the allow-list");
787
+ console.log(' - You need to run this from your project root');
788
+ console.log('');
789
+ console.log('Allowed directory patterns:');
790
+ ALLOWED_DIRECTORIES.forEach(dir => console.log(` ${dir}`));
791
+ process.exit(1);
792
+ }
793
+
794
+ console.log('šŸ“ Files will be scanned in these locations:');
795
+ const scannedDirs = [
796
+ ...new Set(allFiles.map(file => path.dirname(file))),
797
+ ].slice(0, 10);
798
+
799
+ // Show relative paths safely
800
+ scannedDirs.forEach(dir => {
801
+ // Ensure we don't show absolute paths
802
+ const safeDir = path.isAbsolute(dir)
803
+ ? path.relative(process.cwd(), dir)
804
+ : dir;
805
+ console.log(` šŸ“‚ ${safeDir}`);
806
+ });
807
+
808
+ if (allFiles.length > 10) {
809
+ console.log(` ... and ${allFiles.length - 10} more files`);
810
+ }
811
+ console.log('');
812
+
813
+ // Parse command line options
814
+ const isDryRun = process.argv.includes('--dry-run');
815
+ const isImportOnly = process.argv.includes('--import-only');
816
+
817
+ if (isDryRun) {
818
+ console.log('šŸ” DRY RUN MODE: No files will be modified');
819
+ console.log('');
820
+ }
821
+
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
+ }
834
+
835
+ console.log('');
836
+
837
+ // Perform migration
838
+ const report = generateMigrationReport(
839
+ allFiles,
840
+ isImportOnly ? 'import-only' : 'complete',
841
+ isDryRun,
842
+ );
843
+ printReport(report);
844
+ showNextSteps(isImportOnly ? 'import-only' : 'complete');
845
+
846
+ console.log('\nšŸŽÆ Migration complete!');
847
+ }
848
+
849
+ if (require.main === module) {
850
+ main();
851
+ }
852
+
853
+ module.exports = {
854
+ updateImportsAndComponents,
855
+ generateMigrationReport,
856
+ COMPONENT_MAPPING,
857
+ PROPS_MAPPING,
858
+ };