@entur/typography 1.10.0-beta.9 → 2.0.0-beta.1

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 (112) hide show
  1. package/README.md +37 -17
  2. package/dist/BaseHeading.d.ts +1 -1
  3. package/dist/Blockquote.d.ts +3 -4
  4. package/dist/CodeText.d.ts +1 -1
  5. package/dist/EmphasizedText.d.ts +1 -1
  6. package/dist/Heading1.d.ts +1 -1
  7. package/dist/Heading2.d.ts +1 -1
  8. package/dist/Heading3.d.ts +1 -1
  9. package/dist/Heading4.d.ts +1 -1
  10. package/dist/Heading5.d.ts +1 -1
  11. package/dist/Heading6.d.ts +1 -1
  12. package/dist/Label.d.ts +1 -1
  13. package/dist/LeadParagraph.d.ts +1 -1
  14. package/dist/Link.d.ts +1 -1
  15. package/dist/ListItem.d.ts +1 -1
  16. package/dist/NumberedList.d.ts +1 -1
  17. package/dist/Paragraph.d.ts +1 -1
  18. package/dist/PreformattedText.d.ts +1 -1
  19. package/dist/SmallText.d.ts +1 -1
  20. package/dist/StrongText.d.ts +1 -1
  21. package/dist/SubLabel.d.ts +1 -1
  22. package/dist/SubParagraph.d.ts +1 -1
  23. package/dist/UnorderedList.d.ts +1 -1
  24. package/dist/beta/cjs/components/Blockquote.cjs +29 -0
  25. package/dist/beta/cjs/components/Blockquote.cjs.map +1 -0
  26. package/dist/beta/cjs/components/Heading.cjs +37 -0
  27. package/dist/beta/cjs/components/Heading.cjs.map +1 -0
  28. package/dist/beta/cjs/components/Link.cjs +41 -0
  29. package/dist/beta/cjs/components/Link.cjs.map +1 -0
  30. package/dist/beta/cjs/components/ListItem.cjs +33 -0
  31. package/dist/beta/cjs/components/ListItem.cjs.map +1 -0
  32. package/dist/beta/cjs/components/NumberedList.cjs +32 -0
  33. package/dist/beta/cjs/components/NumberedList.cjs.map +1 -0
  34. package/dist/beta/cjs/components/Text.cjs +36 -0
  35. package/dist/beta/cjs/components/Text.cjs.map +1 -0
  36. package/dist/beta/cjs/components/UnorderedList.cjs +29 -0
  37. package/dist/beta/cjs/components/UnorderedList.cjs.map +1 -0
  38. package/dist/beta/cjs/index.cjs +18 -0
  39. package/dist/beta/cjs/index.cjs.map +1 -0
  40. package/dist/beta/cjs/utils/utils.cjs +77 -0
  41. package/dist/beta/cjs/utils/utils.cjs.map +1 -0
  42. package/dist/beta/esm/components/Blockquote.mjs +29 -0
  43. package/dist/beta/esm/components/Blockquote.mjs.map +1 -0
  44. package/dist/beta/esm/components/Heading.mjs +37 -0
  45. package/dist/beta/esm/components/Heading.mjs.map +1 -0
  46. package/dist/beta/esm/components/Link.mjs +41 -0
  47. package/dist/beta/esm/components/Link.mjs.map +1 -0
  48. package/dist/beta/esm/components/ListItem.mjs +33 -0
  49. package/dist/beta/esm/components/ListItem.mjs.map +1 -0
  50. package/dist/beta/esm/components/NumberedList.mjs +32 -0
  51. package/dist/beta/esm/components/NumberedList.mjs.map +1 -0
  52. package/dist/beta/esm/components/Text.mjs +36 -0
  53. package/dist/beta/esm/components/Text.mjs.map +1 -0
  54. package/dist/beta/esm/components/UnorderedList.mjs +29 -0
  55. package/dist/beta/esm/components/UnorderedList.mjs.map +1 -0
  56. package/dist/beta/esm/index.mjs +18 -0
  57. package/dist/beta/esm/index.mjs.map +1 -0
  58. package/dist/beta/esm/utils/utils.mjs +77 -0
  59. package/dist/beta/esm/utils/utils.mjs.map +1 -0
  60. package/dist/beta/styles/components/heading.css +458 -0
  61. package/dist/beta/styles/components/text.css +959 -0
  62. package/dist/beta/types/components/Blockquote.d.ts +12 -0
  63. package/dist/beta/{Heading.d.ts → types/components/Heading.d.ts} +2 -3
  64. package/dist/beta/types/components/Link.d.ts +22 -0
  65. package/dist/beta/types/components/ListItem.d.ts +22 -0
  66. package/dist/beta/types/components/NumberedList.d.ts +22 -0
  67. package/dist/beta/{Text.d.ts → types/components/Text.d.ts} +2 -3
  68. package/dist/beta/types/components/UnorderedList.d.ts +20 -0
  69. package/dist/beta/types/index.d.ts +8 -0
  70. package/dist/beta/types/utils/utils.d.ts +13 -0
  71. package/dist/index.d.ts +0 -5
  72. package/dist/styles.css +162 -820
  73. package/dist/typography.cjs.js +416 -0
  74. package/dist/typography.cjs.js.map +1 -0
  75. package/dist/typography.esm.js +392 -454
  76. package/dist/typography.esm.js.map +1 -1
  77. package/fonts/Entur-Nationale-Demibold.eot +0 -0
  78. package/fonts/Entur-Nationale-Demibold.woff +0 -0
  79. package/fonts/Entur-Nationale-Demibold.woff2 +0 -0
  80. package/fonts/Entur-Nationale-DemiboldItalic.eot +0 -0
  81. package/fonts/Entur-Nationale-DemiboldItalic.woff +0 -0
  82. package/fonts/Entur-Nationale-DemiboldItalic.woff2 +0 -0
  83. package/fonts/Entur-Nationale-Italic.eot +0 -0
  84. package/fonts/Entur-Nationale-Italic.woff +0 -0
  85. package/fonts/Entur-Nationale-Italic.woff2 +0 -0
  86. package/fonts/Entur-Nationale-Light.eot +0 -0
  87. package/fonts/Entur-Nationale-Light.woff +0 -0
  88. package/fonts/Entur-Nationale-Light.woff2 +0 -0
  89. package/fonts/Entur-Nationale-LightItalic.eot +0 -0
  90. package/fonts/Entur-Nationale-LightItalic.woff +0 -0
  91. package/fonts/Entur-Nationale-LightItalic.woff2 +0 -0
  92. package/fonts/Entur-Nationale-Medium.eot +0 -0
  93. package/fonts/Entur-Nationale-Medium.woff +0 -0
  94. package/fonts/Entur-Nationale-Medium.woff2 +0 -0
  95. package/fonts/Entur-Nationale-MediumItalic.eot +0 -0
  96. package/fonts/Entur-Nationale-MediumItalic.woff +0 -0
  97. package/fonts/Entur-Nationale-MediumItalic.woff2 +0 -0
  98. package/fonts/Entur-Nationale-Regular.eot +0 -0
  99. package/fonts/Entur-Nationale-Regular.woff +0 -0
  100. package/fonts/Entur-Nationale-Regular.woff2 +0 -0
  101. package/package.json +59 -19
  102. package/scripts/migrate-typography.js +430 -178
  103. package/dist/beta/BlockquoteBeta.d.ts +0 -12
  104. package/dist/beta/LinkBeta.d.ts +0 -16
  105. package/dist/beta/index.d.ts +0 -6
  106. package/dist/beta/utils.d.ts +0 -9
  107. package/dist/index.js +0 -8
  108. package/dist/typography.cjs.development.js +0 -508
  109. package/dist/typography.cjs.development.js.map +0 -1
  110. package/dist/typography.cjs.production.min.js +0 -2
  111. package/dist/typography.cjs.production.min.js.map +0 -1
  112. /package/dist/beta/{types.d.ts → types/types.d.ts} +0 -0
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
+ * NOTICE: This script is generated using AI with human guidance.
4
5
  * Typography Migration Script
5
6
  *
6
7
  * This script helps you migrate from old typography components to new beta typography.
@@ -11,28 +12,38 @@
11
12
  * - Updates import paths AND component usage
12
13
  * - Replaces old components with beta components
13
14
  * - CONSEQUENCES:
14
- * * <Heading1> becomes <Heading as="h1" variant="title-1">
15
+ * * <Heading1-6> becomes <Heading as="h1-6" variant="title-1|title-2|subtitle-1|subtitle-2|section-1|section-2">
15
16
  * * <Paragraph> becomes <Text variant="paragraph">
16
- * * <Link> becomes <LinkBeta>
17
- * * Props may need updates (e.g., different prop names)
17
+ * * <LeadParagraph> becomes <Text variant="leading">
18
+ * * <SmallText> becomes <Text variant="subparagraph">
19
+ * * <StrongText> becomes <Text as="strong" weight="bold">
20
+ * * <SubLabel> becomes <Text variant="sublabel">
21
+ * * <SubParagraph> becomes <Text variant="subparagraph">
22
+ * * <Label> becomes <Text variant="label"> (or <Text as="label" variant="label"> if htmlFor prop exists)
23
+ * * <EmphasizedText> becomes <Text variant="emphasized">
24
+ * * <CodeText> becomes <Text variant="code-text">
25
+ * * <PreformattedText> becomes <Text variant="preformatted-text">
26
+ * * <Link> becomes <Link> (from @entur/typography/beta)
27
+ * * <Blockquote> becomes <Blockquote> (from @entur/typography/beta)
28
+ * * <BlockquoteFooter> becomes <BlockquoteFooter> (from @entur/typography/beta)
29
+ * * <UnorderedList> becomes <UnorderedList> (from @entur/typography/beta)
30
+ * * <NumberedList> becomes <NumberedList> (from @entur/typography/beta)
31
+ * * <ListItem> becomes <ListItem> (from @entur/typography/beta)
32
+ * * Import paths change from @entur/typography to @entur/typography/beta
33
+ * * Props may need updates (e.g., margin becomes spacing)
18
34
  * * Styling classes may change
19
35
  * * Test thoroughly after migration!
20
36
  *
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
37
+
26
38
  *
27
39
  * Usage:
28
40
  * 1. Run this script in your project root
29
- * 2. Choose your migration mode (complete or import-only)
41
+ * 2. Choose your migration mode (complete)
30
42
  * 3. Update your styles as needed
31
43
  * 4. Test your application thoroughly
32
44
  *
33
45
  * Options:
34
46
  * --dry-run Show what would be changed without modifying files
35
- * --import-only Import-only migration: update import paths only
36
47
  *
37
48
  * Environment Variables:
38
49
  * TYPOGRAPHY_MIGRATION_DIRS Comma-separated list of directories to scan
@@ -46,31 +57,44 @@
46
57
  *
47
58
  */
48
59
 
49
- const fs = require('fs');
50
- const path = require('path');
60
+ import fs from 'fs';
61
+ import path from 'path';
62
+ import { fileURLToPath } from 'url';
63
+
64
+ // Get __dirname equivalent for ES modules
65
+ const __filename = fileURLToPath(import.meta.url);
66
+ const __dirname = path.dirname(__filename);
51
67
 
52
68
  // Check if glob is available
53
69
  let glob;
54
70
  try {
55
- glob = require('glob');
71
+ const globModule = await import('glob');
72
+ glob = globModule.default || globModule;
56
73
  } catch (error) {
57
74
  console.error(
58
75
  '❌ Error: The "glob" package is required to run this migration script.',
59
76
  );
60
77
  console.error('');
61
- console.error('Please install it:');
78
+ console.error('Please install it first:');
62
79
  console.error(' npm install glob');
63
80
  console.error(' yarn add glob');
81
+ console.error(' pnpm add glob');
64
82
  console.error('');
65
- console.error('Or use npx which will handle dependencies automatically:');
83
+ console.error('Then run the migration:');
66
84
  console.error(' npx @entur/typography@latest migrate');
85
+ console.error(' yarn dlx @entur/typography@latest migrate');
86
+ console.error('');
87
+ console.error('📚 For more information, see:');
88
+ console.error(
89
+ ' https://linje.entur.no/komponenter/ressurser/typography-migration',
90
+ );
67
91
  console.error('');
68
92
  process.exit(1);
69
93
  }
70
94
 
71
95
  // Configuration
72
96
  const OLD_IMPORT = '@entur/typography';
73
- const BETA_IMPORT = '@entur/typography';
97
+ const BETA_IMPORT = '@entur/typography/beta';
74
98
 
75
99
  // Enhanced warning detection patterns - only truly problematic patterns
76
100
  const PROBLEMATIC_PATTERNS = {
@@ -123,6 +147,7 @@ const MIGRATION_FOLDERS = [
123
147
  'app/**',
124
148
  'apps/**',
125
149
  'components/**',
150
+ 'packages/**',
126
151
  'pages/**',
127
152
  'lib/**',
128
153
  'utils/**',
@@ -342,13 +367,20 @@ const COMPONENT_MAPPING = {
342
367
  Heading6: { component: 'Heading', as: 'h6', variant: 'section-2' },
343
368
  Paragraph: { component: 'Text', variant: 'paragraph' },
344
369
  LeadParagraph: { component: 'Text', variant: 'leading' },
345
- SmallText: { component: 'Text', variant: 'subparagraph', size: 's' },
346
- StrongText: { component: 'Text', variant: 'emphasized', weight: 'semibold' },
347
- SubLabel: { component: 'Text', variant: 'sublabel', size: 'xs' },
370
+ SmallText: { component: 'Text', variant: 'subparagraph' },
371
+ StrongText: { component: 'Text', as: 'strong', weight: 'bold' },
372
+ SubLabel: { component: 'Text', variant: 'sublabel' },
348
373
  SubParagraph: { component: 'Text', variant: 'subparagraph' },
349
374
  Label: { component: 'Text', variant: 'label' },
350
375
  EmphasizedText: { component: 'Text', variant: 'emphasized' },
351
376
  CodeText: { component: 'Text', variant: 'code-text' },
377
+ PreformattedText: { component: 'Text', variant: 'preformatted-text' },
378
+ Link: { component: 'Link' }, // Convert Link to beta Link
379
+ Blockquote: { component: 'Blockquote' }, // Convert Blockquote to beta Blockquote
380
+ BlockquoteFooter: { component: 'BlockquoteFooter' }, // Convert BlockquoteFooter to beta BlockquoteFooter
381
+ UnorderedList: { component: 'UnorderedList' },
382
+ NumberedList: { component: 'NumberedList' },
383
+ ListItem: { component: 'ListItem' },
352
384
  };
353
385
 
354
386
  // Props mapping for migration
@@ -357,17 +389,43 @@ const PROPS_MAPPING = {
357
389
  };
358
390
 
359
391
  // Spacing value mapping from old margin to new spacing
392
+ // Based on the actual CSS classes in src/beta/styles.scss
393
+ // and the old margin prop values: "top" | "bottom" | "both" | "none"
360
394
  const SPACING_MAPPING = {
361
- none: 'none',
362
- top: 'md-top',
363
- bottom: 'md-bottom',
364
- left: 'md-left',
365
- right: 'md-right',
395
+ // Old margin values mapped to new spacing values
396
+ none: 'none', // No spacing
397
+ top: 'md-top', // Top margin only (medium size)
398
+ bottom: 'md-bottom', // Bottom margin only (medium size)
399
+ both: 'md', // Both top and bottom margins (medium size)
400
+
401
+ // Additional spacing values for more granular control
402
+ // These weren't in the old margin prop but are available in new spacing
403
+ left: 'md-left', // Left margin (medium size)
404
+ right: 'md-right', // Right margin (medium size)
405
+
406
+ // Size-based spacing (applies to both top and bottom)
366
407
  xs: 'xs',
367
408
  sm: 'sm',
368
409
  md: 'md',
369
410
  lg: 'lg',
370
411
  xl: 'xl',
412
+
413
+ // Specific directional spacing with sizes
414
+ 'xs-top': 'xs-top',
415
+ 'xs-bottom': 'xs-bottom',
416
+ 'sm-top': 'sm-top',
417
+ 'sm-bottom': 'sm-bottom',
418
+ 'md-top': 'md-top',
419
+ 'md-bottom': 'md-bottom',
420
+ 'lg-top': 'lg-top',
421
+ 'lg-bottom': 'lg-bottom',
422
+ 'xl-top': 'xl-top',
423
+ 'xl-bottom': 'xl-bottom',
424
+
425
+ // Extra small variants
426
+ xs2: 'xs2',
427
+ 'xs2-top': 'xs2-top',
428
+ 'xs2-bottom': 'xs2-bottom',
371
429
  };
372
430
 
373
431
  // Import patterns to handle
@@ -376,35 +434,40 @@ const IMPORT_PATTERNS = [
376
434
  /from\s+['"`]@entur\/typography\/dist['"`]/g,
377
435
  /from\s+['"`]@entur\/typography\/dist\/index['"`]/g,
378
436
  /from\s+['"`]@entur\/typography\/dist\/styles\.css['"`]/g,
437
+ /from\s+['"`]@entur\/typography\/styles['"`]/g,
379
438
  ];
380
439
 
381
440
  // Parse JSX props more robustly
382
441
  function parseJSXProps(propsString) {
383
442
  if (!propsString || !propsString.trim()) {
384
- return { props: {}, warnings: [] };
443
+ return { props: {}, warnings: [], spreadProps: [] };
385
444
  }
386
445
 
387
446
  const props = {};
388
447
  const warnings = [];
389
- const MAX_ITERATIONS = 100; // Prevent infinite loops
390
- let iterationCount = 0;
448
+ const spreadProps = []; // Track spread props separately
449
+ const originalSyntax = {}; // Track original JSX syntax for each prop
391
450
 
392
451
  try {
393
452
  // Parse props manually to handle complex cases
394
453
  let remaining = propsString.trim();
395
- let lastRemainingLength = remaining.length;
396
454
 
397
- while (remaining.length > 0 && iterationCount < MAX_ITERATIONS) {
398
- iterationCount++;
455
+ // First, extract all spread props
456
+ const spreadRegex = /\.\.\.\{?(\w+)\}?/g;
457
+ let spreadMatch;
458
+ while ((spreadMatch = spreadRegex.exec(remaining)) !== null) {
459
+ spreadProps.push(spreadMatch[1]);
460
+ }
399
461
 
400
- // Safety check: if we're not making progress, break
401
- if (remaining.length >= lastRemainingLength) {
402
- warnings.push(`Parser stuck at iteration ${iterationCount}, breaking`);
403
- break;
404
- }
405
- lastRemainingLength = remaining.length;
462
+ // Remove spread props from the string to parse regular props
463
+ remaining = remaining.replace(/\.\.\.\{?(\w+)\}?/g, '');
464
+
465
+ // Now parse regular props
466
+ while (remaining.trim().length > 0) {
467
+ // Skip whitespace
468
+ remaining = remaining.replace(/^\s+/, '');
406
469
 
407
- // Match prop name - more efficient regex
470
+ // Match prop name
408
471
  const nameMatch = remaining.match(/^(\w+)=/);
409
472
  if (!nameMatch) break;
410
473
 
@@ -414,7 +477,7 @@ function parseJSXProps(propsString) {
414
477
 
415
478
  // Match prop value
416
479
  if (remaining.startsWith('"') || remaining.startsWith("'")) {
417
- // String value - use indexOf for better performance
480
+ // String value
418
481
  const quote = remaining[0];
419
482
  const endQuoteIndex = remaining.indexOf(quote, 1);
420
483
  if (endQuoteIndex === -1) {
@@ -424,14 +487,14 @@ function parseJSXProps(propsString) {
424
487
 
425
488
  const propValue = remaining.substring(1, endQuoteIndex);
426
489
  props[propName] = propValue;
490
+ originalSyntax[propName] = 'string'; // Mark as string literal
427
491
  remaining = remaining.substring(endQuoteIndex + 1);
428
492
  } else if (remaining.startsWith('{')) {
429
- // Object value - find matching closing brace with bounds checking
493
+ // Object value - find matching closing brace
430
494
  let braceCount = 0;
431
495
  let endIndex = -1;
432
- const maxSearchLength = Math.min(remaining.length, 1000); // Limit search length
433
496
 
434
- for (let i = 0; i < maxSearchLength; i++) {
497
+ for (let i = 0; i < remaining.length; i++) {
435
498
  if (remaining[i] === '{') braceCount++;
436
499
  if (remaining[i] === '}') {
437
500
  braceCount--;
@@ -449,25 +512,23 @@ function parseJSXProps(propsString) {
449
512
 
450
513
  const propValue = remaining.substring(1, endIndex);
451
514
  props[propName] = propValue;
515
+ originalSyntax[propName] = 'jsx'; // Mark as JSX expression
452
516
  remaining = remaining.substring(endIndex + 1);
453
517
  } else {
454
- // Boolean prop (e.g., disabled) or invalid syntax
518
+ // Boolean prop
455
519
  props[propName] = true;
520
+ originalSyntax[propName] = 'boolean'; // Mark as boolean
456
521
  break;
457
522
  }
458
523
 
459
- // Skip whitespace more efficiently
524
+ // Skip whitespace
460
525
  remaining = remaining.replace(/^\s+/, '');
461
526
  }
462
-
463
- if (iterationCount >= MAX_ITERATIONS) {
464
- warnings.push(`Maximum parsing iterations (${MAX_ITERATIONS}) reached`);
465
- }
466
527
  } catch (error) {
467
528
  warnings.push(`Failed to parse props: ${error.message}`);
468
529
  }
469
530
 
470
- return { props, warnings };
531
+ return { props, warnings, spreadProps, originalSyntax };
471
532
  }
472
533
 
473
534
  // Migrate props from old to new format
@@ -485,11 +546,12 @@ function migrateProps(props, oldComponent) {
485
546
  `Migrated 'margin="${props.margin}"' to 'spacing="${newSpacing}"'`,
486
547
  );
487
548
  } else {
488
- // Unknown margin value - keep as is but warn
489
- migratedProps.spacing = props.margin;
549
+ // Unknown margin value - suggest alternatives
550
+ const suggestions = getSpacingSuggestions(props.margin);
551
+ migratedProps.spacing = props.margin; // Keep original value for now
490
552
  delete migratedProps.margin;
491
553
  warnings.push(
492
- `Migrated 'margin="${props.margin}"' to 'spacing="${props.margin}"' (unknown value - may need manual review)`,
554
+ `Migrated 'margin="${props.margin}"' to 'spacing="${props.margin}"' (unknown value). ${suggestions}`,
493
555
  );
494
556
  }
495
557
  }
@@ -516,8 +578,55 @@ function migrateProps(props, oldComponent) {
516
578
  return { props: migratedProps, warnings };
517
579
  }
518
580
 
581
+ // Helper function to suggest spacing alternatives for unknown margin values
582
+ function getSpacingSuggestions(unknownMargin) {
583
+ const suggestions = [];
584
+
585
+ // Check if it might be one of the old margin values
586
+ if (['top', 'bottom', 'both', 'none'].includes(unknownMargin)) {
587
+ suggestions.push(
588
+ `"${unknownMargin}" is a valid old margin value and will be migrated correctly.`,
589
+ );
590
+ return suggestions.join(' ');
591
+ }
592
+
593
+ // Check if it might be a directional value
594
+ if (
595
+ unknownMargin.includes('top') ||
596
+ unknownMargin.includes('bottom') ||
597
+ unknownMargin.includes('left') ||
598
+ unknownMargin.includes('right')
599
+ ) {
600
+ suggestions.push(
601
+ 'Consider using directional spacing like "md-top", "sm-bottom", etc.',
602
+ );
603
+ }
604
+
605
+ // Check if it might be a size value
606
+ if (['xs', 'sm', 'md', 'lg', 'xl'].includes(unknownMargin)) {
607
+ suggestions.push(
608
+ 'Consider using size-based spacing like "xs", "sm", "md", "lg", "xl".',
609
+ );
610
+ }
611
+
612
+ // Check if it might be a specific variant
613
+ if (unknownMargin.includes('xs2')) {
614
+ suggestions.push(
615
+ 'Consider using "xs2", "xs2-top", or "xs2-bottom" for extra small spacing.',
616
+ );
617
+ }
618
+
619
+ if (suggestions.length === 0) {
620
+ suggestions.push(
621
+ 'Old margin values: "none", "top", "bottom", "both". New spacing values: "xs", "sm", "md", "lg", "xl", and directional variants.',
622
+ );
623
+ }
624
+
625
+ return suggestions.join(' ');
626
+ }
627
+
519
628
  // Convert props object back to JSX string
520
- function propsToString(props) {
629
+ function propsToString(props, originalSyntax = {}) {
521
630
  if (!props || Object.keys(props).length === 0) {
522
631
  return '';
523
632
  }
@@ -526,20 +635,46 @@ function propsToString(props) {
526
635
  ' ' +
527
636
  Object.entries(props)
528
637
  .map(([key, value]) => {
529
- // Handle different value types
530
- if (typeof value === 'string' && !value.includes('{')) {
638
+ // Use original syntax information if available
639
+ if (originalSyntax[key] === 'string') {
531
640
  return `${key}="${value}"`;
532
- } else if (
533
- typeof value === 'string' &&
534
- value.startsWith('{') &&
535
- value.endsWith('}')
536
- ) {
537
- // Already a JSX object, don't add extra braces
538
- return `${key}={${value}}`;
641
+ } else if (originalSyntax[key] === 'jsx') {
642
+ // Check if the JSX expression is actually a string literal (e.g., 'a' or "a")
643
+ if (
644
+ typeof value === 'string' &&
645
+ ((value.startsWith("'") &&
646
+ value.endsWith("'") &&
647
+ value.length > 1) ||
648
+ (value.startsWith('"') &&
649
+ value.endsWith('"') &&
650
+ value.length > 1))
651
+ ) {
652
+ // It's a string literal in JSX, convert to string prop
653
+ const stringValue = value.slice(1, -1); // Remove quotes
654
+ return `${key}="${stringValue}"`;
655
+ } else {
656
+ // It's a real JSX expression, keep as is
657
+ return `${key}={${value}}`;
658
+ }
659
+ } else if (originalSyntax[key] === 'boolean') {
660
+ return value ? key : '';
539
661
  } else {
540
- return `${key}={${value}}`;
662
+ // Fallback logic for when originalSyntax is not available
663
+ if (typeof value === 'string' && !value.includes('{')) {
664
+ return `${key}="${value}"`;
665
+ } else if (
666
+ typeof value === 'string' &&
667
+ value.startsWith('{') &&
668
+ value.endsWith('}')
669
+ ) {
670
+ // Already a JSX object, don't add extra braces
671
+ return `${key}={${value}}`;
672
+ } else {
673
+ return `${key}={${value}}`;
674
+ }
541
675
  }
542
676
  })
677
+ .filter(prop => prop.length > 0) // Remove empty props (like false booleans)
543
678
  .join(' ')
544
679
  );
545
680
  }
@@ -549,40 +684,113 @@ function updateImports(content) {
549
684
  let updatedContent = content;
550
685
  let changes = 0;
551
686
 
552
- // First, update import paths
553
- IMPORT_PATTERNS.forEach(pattern => {
554
- const matches = content.match(pattern) || [];
555
- changes += matches.length;
556
- updatedContent = updatedContent.replace(pattern, `from '${BETA_IMPORT}'`);
557
- });
687
+ // Handle CSS imports separately - these should stay with the main package
688
+ const cssImportPattern = /from\s+['"`]@entur\/typography\/styles['"`]/g;
689
+ if (cssImportPattern.test(content)) {
690
+ // CSS imports should remain unchanged as they're still in the main package
691
+ // No changes needed for CSS imports
692
+ }
558
693
 
559
- // Then, update destructured import names - only within @entur/typography imports
560
- // Find all import statements from @entur/typography and update component names
694
+ // Handle component imports - only migrate if they contain components that need migration
561
695
  const importRegex =
562
696
  /import\s*{([^}]+)}\s*from\s*['"']@entur\/typography['"']/g;
563
697
 
564
698
  updatedContent = updatedContent.replace(importRegex, (match, importList) => {
699
+ const components = importList
700
+ .split(',')
701
+ .map(comp => comp.trim())
702
+ .filter(comp => comp);
703
+
704
+ // Check if any of the imported components need migration
705
+ // Need to check both the full import (e.g., "Link as Li") and just the component name (e.g., "Link")
706
+ const needsMigration = components.some(comp => {
707
+ // Extract the actual component name from aliased imports
708
+ const componentName = comp.includes(' as ')
709
+ ? comp.split(' as ')[0].trim()
710
+ : comp;
711
+ return Object.keys(COMPONENT_MAPPING).includes(componentName);
712
+ });
713
+
714
+ if (!needsMigration) {
715
+ // No migration needed, keep the import as is
716
+ return match;
717
+ }
718
+
565
719
  let updatedImportList = importList;
566
720
  let hasChanges = false;
567
721
  const uniqueComponents = new Set();
722
+ const processedComponents = new Set(); // Track which components we've already processed
723
+
724
+ // First, collect all existing components that should be preserved
725
+ const existingComponents = components.filter(comp => {
726
+ // Extract the actual component name from aliased imports
727
+ const componentName = comp.includes(' as ')
728
+ ? comp.split(' as ')[0].trim()
729
+ : comp;
730
+
731
+ // Keep components that are:
732
+ // 1. Not in the migration mapping (old components), OR
733
+ // 2. Are the target components (new beta components)
734
+ const isOldComponent =
735
+ Object.keys(COMPONENT_MAPPING).includes(componentName);
736
+ const isTargetComponent = Object.values(COMPONENT_MAPPING).some(
737
+ mapping => mapping.component === componentName,
738
+ );
568
739
 
569
- // Check each component in the import list
740
+ return !isOldComponent || isTargetComponent;
741
+ });
742
+
743
+ // Then, update components that need migration
570
744
  Object.entries(COMPONENT_MAPPING).forEach(([oldComponent, mapping]) => {
571
- const componentRegex = new RegExp(`\\b${oldComponent}\\b`, 'g');
572
- if (componentRegex.test(updatedImportList)) {
573
- updatedImportList = updatedImportList.replace(
574
- componentRegex,
575
- mapping.component,
576
- );
577
- uniqueComponents.add(mapping.component);
578
- hasChanges = true;
745
+ // Check if this old component exists in any form (aliased or not)
746
+ const hasComponent = components.some(comp => {
747
+ const componentName = comp.includes(' as ')
748
+ ? comp.split(' as ')[0].trim()
749
+ : comp;
750
+ return componentName === oldComponent;
751
+ });
752
+
753
+ if (hasComponent && !processedComponents.has(oldComponent)) {
754
+ // Replace the old component with the new one, preserving aliases
755
+ const componentRegex = new RegExp(`\\b${oldComponent}\\b`, 'g');
756
+ if (componentRegex.test(updatedImportList)) {
757
+ updatedImportList = updatedImportList.replace(
758
+ componentRegex,
759
+ mapping.component,
760
+ );
761
+
762
+ // Only add to uniqueComponents if the component isn't already present in aliased form
763
+ const componentAlreadyPresent = components.some(comp => {
764
+ const componentName = comp.includes(' as ')
765
+ ? comp.split(' as ')[0].trim()
766
+ : comp;
767
+ return componentName === mapping.component;
768
+ });
769
+
770
+ if (!componentAlreadyPresent) {
771
+ uniqueComponents.add(mapping.component);
772
+ }
773
+
774
+ processedComponents.add(oldComponent);
775
+ hasChanges = true;
776
+ }
579
777
  }
580
778
  });
581
779
 
582
780
  if (hasChanges) {
583
781
  changes++;
584
- // Deduplicate components and create clean import statement
585
- const finalImportList = Array.from(uniqueComponents).join(', ');
782
+ // Combine existing components with migrated components
783
+ const allComponents = [
784
+ ...existingComponents,
785
+ ...Array.from(uniqueComponents),
786
+ ];
787
+
788
+ // Filter out any empty components and deduplicate
789
+ const finalComponents = allComponents
790
+ .filter(comp => comp && comp.trim())
791
+ .filter((comp, index, arr) => arr.indexOf(comp) === index); // Remove duplicates
792
+
793
+ const finalImportList = finalComponents.join(', ');
586
794
  return `import {${finalImportList}} from '${BETA_IMPORT}'`;
587
795
  }
588
796
 
@@ -608,8 +816,12 @@ function updateComponents(content) {
608
816
  changes++;
609
817
 
610
818
  // Parse existing props
611
- const { props: existingProps, warnings: parseWarnings } =
612
- parseJSXProps(propsString);
819
+ const {
820
+ props: existingProps,
821
+ warnings: parseWarnings,
822
+ spreadProps,
823
+ originalSyntax,
824
+ } = parseJSXProps(propsString);
613
825
  warnings.push(...parseWarnings);
614
826
 
615
827
  // Migrate props
@@ -629,8 +841,46 @@ function updateComponents(content) {
629
841
 
630
842
  // Handle Heading components
631
843
  if (mapping.component === 'Heading') {
632
- const asValue = newProps.as || mapping.as;
633
- const variantValue = newProps.variant || mapping.variant;
844
+ // Preserve existing 'as' prop if it exists, otherwise use mapping default
845
+ const asValue = existingProps.as || mapping.as;
846
+ // Preserve existing 'variant' prop if it exists, otherwise use mapping default
847
+ const variantValue = existingProps.variant || mapping.variant;
848
+
849
+ // Remove as and variant from props since we'll add them separately
850
+ delete newProps.as;
851
+ delete newProps.variant;
852
+
853
+ // Create props object with as and variant, preserving original syntax
854
+ const headingProps = {
855
+ as: asValue,
856
+ variant: variantValue,
857
+ ...newProps,
858
+ };
859
+
860
+ // Create original syntax object for as and variant
861
+ const headingOriginalSyntax = {
862
+ as: originalSyntax.as || 'string',
863
+ variant: originalSyntax.variant || 'string',
864
+ ...originalSyntax,
865
+ };
866
+
867
+ const propsString = propsToString(
868
+ headingProps,
869
+ headingOriginalSyntax,
870
+ );
871
+ const spreadPropsString =
872
+ spreadProps.length > 0 ? ` {...${spreadProps.join(', ...')}}` : '';
873
+ return `<Heading${propsString}${spreadPropsString}>`;
874
+ }
875
+
876
+ // Handle Label components with special case for htmlFor prop
877
+ if (oldComponent === 'Label' && mapping.component === 'Text') {
878
+ // If htmlFor prop exists, add as="label" to ensure proper semantic HTML
879
+ // Otherwise, use existing as prop or default to undefined (no as prop)
880
+ const asValue = existingProps.htmlFor
881
+ ? 'label'
882
+ : existingProps.as || undefined;
883
+ const variantValue = existingProps.variant || mapping.variant;
634
884
 
635
885
  // Remove as and variant from props since we'll add them separately
636
886
  delete newProps.as;
@@ -644,8 +894,13 @@ function updateComponents(content) {
644
894
  }
645
895
  Object.assign(orderedProps, newProps);
646
896
 
647
- const propsString = propsToString(orderedProps);
648
- return `<Heading as="${asValue}" variant="${variantValue}"${propsString}>`;
897
+ const propsString = propsToString(orderedProps, originalSyntax);
898
+ const spreadPropsString =
899
+ spreadProps.length > 0 ? ` {...${spreadProps.join(', ...')}}` : '';
900
+
901
+ // Only add as prop if it has a value
902
+ const asProp = asValue ? ` as="${asValue}"` : '';
903
+ return `<Text${asProp} variant="${variantValue}"${propsString}${spreadPropsString}>`;
649
904
  }
650
905
 
651
906
  // Handle other components
@@ -667,8 +922,10 @@ function updateComponents(content) {
667
922
  });
668
923
  Object.assign(finalProps, newProps);
669
924
 
670
- const otherPropsString = propsToString(finalProps);
671
- return `<${componentName}${otherPropsString}>`;
925
+ const otherPropsString = propsToString(finalProps, originalSyntax);
926
+ const spreadPropsString =
927
+ spreadProps.length > 0 ? ` {...${spreadProps.join(', ...')}}` : '';
928
+ return `<${componentName}${otherPropsString}${spreadPropsString}>`;
672
929
  },
673
930
  );
674
931
 
@@ -696,15 +953,33 @@ function updateComponents(content) {
696
953
  * @returns {string[]} Array of matching file paths
697
954
  */
698
955
  function findFiles(pattern) {
699
- // Create a single glob pattern that covers all allowed directories
700
- // Uses brace expansion: {src,app,components}/**/*.{ts,tsx,js,jsx}
701
- const combinedPattern = `{${ALLOWED_DIRECTORIES.join(',')}}/${pattern}`;
702
-
703
- // Use a single glob call instead of multiple calls
704
- const allFiles = glob.sync(combinedPattern, {
705
- ignore: BLOCKED_DIRECTORIES,
706
- nodir: true,
707
- absolute: false,
956
+ const allFiles = [];
957
+
958
+ // Process directory patterns
959
+ const directoryPatterns = ALLOWED_DIRECTORIES.filter(dir =>
960
+ dir.includes('**'),
961
+ );
962
+ const filePatterns = ALLOWED_DIRECTORIES.filter(dir => !dir.includes('**'));
963
+
964
+ // Handle directory patterns (e.g., src/**, app/**)
965
+ if (directoryPatterns.length > 0) {
966
+ const combinedDirPattern = `{${directoryPatterns.join(',')}}/${pattern}`;
967
+ const dirFiles = glob.sync(combinedDirPattern, {
968
+ ignore: BLOCKED_DIRECTORIES,
969
+ nodir: true,
970
+ absolute: false,
971
+ });
972
+ allFiles.push(...dirFiles);
973
+ }
974
+
975
+ // Handle file patterns (e.g., *.jsx, *.tsx)
976
+ filePatterns.forEach(filePattern => {
977
+ const files = glob.sync(filePattern, {
978
+ ignore: BLOCKED_DIRECTORIES,
979
+ nodir: true,
980
+ absolute: false,
981
+ });
982
+ allFiles.push(...files);
708
983
  });
709
984
 
710
985
  // Use Set for efficient deduplication and filtering
@@ -725,37 +1000,29 @@ function findFiles(pattern) {
725
1000
  return uniqueFiles;
726
1001
  }
727
1002
 
728
- function updateImportsAndComponents(content, strategy) {
1003
+ function updateImportsAndComponents(content) {
729
1004
  let updatedContent = content;
730
1005
  let changes = 0;
731
1006
  let warnings = [];
732
1007
 
733
- if (strategy === 'import-only') {
734
- // Only update imports
735
- const { content: newContent, changes: importChanges } =
736
- updateImports(content);
737
- updatedContent = newContent;
738
- changes = importChanges;
739
- } else if (strategy === 'complete') {
740
- // Update both imports and components
741
- const { content: newContent, changes: importChanges } =
742
- updateImports(content);
743
- const {
744
- content: finalContent,
745
- changes: componentChanges,
746
- warnings: componentWarnings,
747
- } = updateComponents(newContent);
748
- updatedContent = finalContent;
749
- changes = importChanges + componentChanges;
750
- warnings = componentWarnings;
751
- }
1008
+ // Update both imports and components
1009
+ const { content: newContent, changes: importChanges } =
1010
+ updateImports(content);
1011
+ const {
1012
+ content: finalContent,
1013
+ changes: componentChanges,
1014
+ warnings: componentWarnings,
1015
+ } = updateComponents(newContent);
1016
+ updatedContent = finalContent;
1017
+ changes = importChanges + componentChanges;
1018
+ warnings = componentWarnings;
752
1019
 
753
1020
  return { content: updatedContent, changes, warnings };
754
1021
  }
755
1022
 
756
- function generateMigrationReport(files, strategy, isDryRun = false) {
1023
+ function generateMigrationReport(files, isDryRun = false) {
757
1024
  const report = {
758
- strategy,
1025
+ strategy: 'complete',
759
1026
  totalFiles: files.length,
760
1027
  migratedFiles: 0,
761
1028
  totalChanges: 0,
@@ -776,7 +1043,7 @@ function generateMigrationReport(files, strategy, isDryRun = false) {
776
1043
  content: updatedContent,
777
1044
  changes,
778
1045
  warnings,
779
- } = updateImportsAndComponents(content, strategy);
1046
+ } = updateImportsAndComponents(content);
780
1047
 
781
1048
  // Combine migration warnings with file analysis warnings
782
1049
  const allWarnings = [...warnings, ...fileAnalysis.warnings];
@@ -1006,23 +1273,13 @@ function printReport(report) {
1006
1273
  }
1007
1274
  }
1008
1275
 
1009
- function showNextSteps(strategy) {
1276
+ function showNextSteps() {
1010
1277
  console.log('\n📝 Next Steps');
1011
1278
  console.log('=============');
1012
1279
 
1013
- if (strategy === 'import-only') {
1014
- console.log('1. Import statements updated');
1015
- console.log('2. 🔄 Update component usage manually when ready:');
1016
- Object.entries(COMPONENT_MAPPING).forEach(([old, new_]) => {
1017
- console.log(` ${old} → ${new_}`);
1018
- });
1019
- console.log('3. 🧪 Test your application');
1020
- console.log('4. 📚 Read the migration guide on our website');
1021
- } else if (strategy === 'complete') {
1022
- console.log('1. 🧪 Test your application thoroughly');
1023
- console.log('2. 🔄 Review and adjust any component props if needed');
1024
- console.log('3. 📚 Read the migration guide on our website');
1025
- }
1280
+ console.log('1. 🧪 Test your application thoroughly');
1281
+ console.log('2. 🔄 Review and adjust any component props if needed');
1282
+ console.log('3. 📚 Read the migration guide on our website');
1026
1283
 
1027
1284
  console.log('\n⚠️ Important Notes:');
1028
1285
  console.log('- Check warnings above for potential issues');
@@ -1030,7 +1287,7 @@ function showNextSteps(strategy) {
1030
1287
  console.log('- Test thoroughly, especially components with custom styling');
1031
1288
  }
1032
1289
 
1033
- function main() {
1290
+ async function main() {
1034
1291
  // Show help if requested
1035
1292
  if (process.argv.includes('--help') || process.argv.includes('-h')) {
1036
1293
  console.log('🎨 Typography Migration Script');
@@ -1047,40 +1304,34 @@ function main() {
1047
1304
  console.log(' # Local development');
1048
1305
  console.log(' npm run migrate');
1049
1306
  console.log('');
1050
- console.log('Options:');
1307
+ console.log('📋 Prerequisites:');
1308
+ console.log(' - Install glob package: npm install glob');
1051
1309
  console.log(
1052
- ' --dry-run Show what would be changed without modifying files',
1310
+ ' - Or use npx/yarn dlx which handles dependencies automatically',
1053
1311
  );
1312
+ console.log('');
1313
+ console.log('Options:');
1054
1314
  console.log(
1055
- ' --import-only Import-only migration: update import paths only',
1315
+ ' --dry-run Show what would be changed without modifying files',
1056
1316
  );
1057
-
1058
1317
  console.log(' --help, -h Show this help message');
1059
1318
  console.log('');
1060
- console.log('Migration Modes:');
1061
- console.log(' 🚀 Complete Mode (default): Updates everything');
1319
+ console.log('Migration Mode:');
1320
+ console.log(' 🚀 Complete Mode: Updates everything');
1062
1321
  console.log(' - Replaces old components with beta components');
1063
- console.log(' - May require prop/styling updates');
1322
+ console.log(' - Heading1-6 Heading with as/variant props');
1323
+ console.log(' - Text components → Text with variant props');
1324
+ console.log(' - All typography components migrate to beta versions');
1325
+ console.log(' - Import paths change to @entur/typography/beta');
1326
+ console.log(' - May require prop/styling updates (margin → spacing)');
1064
1327
  console.log(' - Test thoroughly after migration');
1065
1328
  console.log('');
1066
- console.log(
1067
- ' 📝 Import-Only Mode (--import-only): Only updates import paths',
1068
- );
1069
- console.log(' - Keeps your existing component usage unchanged');
1070
- console.log(' - Minimal risk, gradual migration');
1071
- console.log('');
1072
1329
  console.log('Examples:');
1073
1330
  console.log(' # See what would be changed');
1074
1331
  console.log(' npx @entur/typography@latest migrate --dry-run');
1075
1332
  console.log('');
1076
1333
  console.log(' # Complete migration: update everything (default)');
1077
1334
  console.log(' npx @entur/typography@latest migrate');
1078
- console.log('');
1079
- console.log(' # Import-only migration: update import paths only');
1080
- console.log(' npx @entur/typography@latest migrate --import-only');
1081
- console.log('');
1082
-
1083
- console.log('');
1084
1335
 
1085
1336
  console.log('Environment Variables:');
1086
1337
  console.log(
@@ -1096,7 +1347,7 @@ function main() {
1096
1347
  console.log(' Add/remove folder patterns between the 👇 and 👆 markers');
1097
1348
  console.log(' Examples: "src/**", "app/**", "packages/my-app/**"');
1098
1349
  console.log('');
1099
- console.log(' Option 2: Set environment variable (for CI/CD)');
1350
+ console.log(' Option 2: Set environment variable');
1100
1351
  console.log(
1101
1352
  ' export TYPOGRAPHY_MIGRATION_DIRS="src/**,app/**,components/**"',
1102
1353
  );
@@ -1164,47 +1415,48 @@ function main() {
1164
1415
 
1165
1416
  // Parse command line options
1166
1417
  const isDryRun = process.argv.includes('--dry-run');
1167
- const isImportOnly = process.argv.includes('--import-only');
1168
1418
 
1169
1419
  if (isDryRun) {
1170
1420
  console.log('🔍 DRY RUN MODE: No files will be modified');
1171
1421
  console.log('');
1172
1422
  }
1173
1423
 
1174
- if (isImportOnly) {
1175
- console.log('📝 IMPORT-ONLY MIGRATION: Updating import paths only');
1176
- console.log(' - Your component usage will remain unchanged');
1177
- console.log(' - You can update components manually later');
1178
- } else {
1179
- console.log('🚀 COMPLETE MIGRATION: Updating imports + component usage');
1180
- console.log('⚠️ WARNING: This will modify your component usage!');
1181
- console.log(' - Old components will be replaced with beta components');
1182
- console.log(' - You may need to update props and styling');
1183
- console.log(' - Test thoroughly after migration');
1184
- console.log(' (Use --import-only for import-only migration)');
1185
- }
1424
+ console.log('🚀 COMPLETE MIGRATION: Updating imports + component usage');
1425
+ console.log('⚠️ WARNING: This will modify your component usage!');
1426
+ console.log(' - Old components will be replaced with beta components');
1427
+ console.log(' - Import paths will change to @entur/typography/beta');
1428
+ console.log(
1429
+ ' - Link Link, Blockquote Blockquote, Lists → List components (from beta)',
1430
+ );
1431
+ console.log(
1432
+ ' - List components UnorderedList, NumberedList, ListItem (from beta)',
1433
+ );
1434
+ console.log(' - Props may change (margin → spacing)');
1435
+ console.log(' - Test thoroughly after migration');
1186
1436
 
1187
1437
  console.log('');
1188
1438
 
1189
1439
  // Perform migration
1190
- const report = generateMigrationReport(
1191
- allFiles,
1192
- isImportOnly ? 'import-only' : 'complete',
1193
- isDryRun,
1194
- );
1440
+ const report = generateMigrationReport(allFiles, isDryRun);
1195
1441
  printReport(report);
1196
- showNextSteps(isImportOnly ? 'import-only' : 'complete');
1442
+ showNextSteps();
1197
1443
 
1198
1444
  console.log('\n🎯 Migration complete!');
1199
1445
  }
1200
1446
 
1201
- if (require.main === module) {
1202
- main();
1447
+ // Check if this module is being run directly
1448
+ if (import.meta.url === `file://${process.argv[1]}`) {
1449
+ main().catch(error => {
1450
+ console.error('❌ Migration failed:', error);
1451
+ process.exit(1);
1452
+ });
1203
1453
  }
1204
1454
 
1205
- module.exports = {
1455
+ export {
1206
1456
  updateImportsAndComponents,
1207
1457
  generateMigrationReport,
1208
1458
  COMPONENT_MAPPING,
1209
1459
  PROPS_MAPPING,
1460
+ parseJSXProps,
1461
+ propsToString,
1210
1462
  };