@entur/typography 1.10.0-beta.9 → 2.0.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 (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 +415 -176
  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,13 +57,19 @@
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.',
@@ -70,7 +87,7 @@ try {
70
87
 
71
88
  // Configuration
72
89
  const OLD_IMPORT = '@entur/typography';
73
- const BETA_IMPORT = '@entur/typography';
90
+ const BETA_IMPORT = '@entur/typography/beta';
74
91
 
75
92
  // Enhanced warning detection patterns - only truly problematic patterns
76
93
  const PROBLEMATIC_PATTERNS = {
@@ -123,6 +140,7 @@ const MIGRATION_FOLDERS = [
123
140
  'app/**',
124
141
  'apps/**',
125
142
  'components/**',
143
+ 'packages/**',
126
144
  'pages/**',
127
145
  'lib/**',
128
146
  'utils/**',
@@ -342,13 +360,20 @@ const COMPONENT_MAPPING = {
342
360
  Heading6: { component: 'Heading', as: 'h6', variant: 'section-2' },
343
361
  Paragraph: { component: 'Text', variant: 'paragraph' },
344
362
  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' },
363
+ SmallText: { component: 'Text', variant: 'subparagraph' },
364
+ StrongText: { component: 'Text', as: 'strong', weight: 'bold' },
365
+ SubLabel: { component: 'Text', variant: 'sublabel' },
348
366
  SubParagraph: { component: 'Text', variant: 'subparagraph' },
349
367
  Label: { component: 'Text', variant: 'label' },
350
368
  EmphasizedText: { component: 'Text', variant: 'emphasized' },
351
369
  CodeText: { component: 'Text', variant: 'code-text' },
370
+ PreformattedText: { component: 'Text', variant: 'preformatted-text' },
371
+ Link: { component: 'Link' }, // Convert Link to beta Link
372
+ Blockquote: { component: 'Blockquote' }, // Convert Blockquote to beta Blockquote
373
+ BlockquoteFooter: { component: 'BlockquoteFooter' }, // Convert BlockquoteFooter to beta BlockquoteFooter
374
+ UnorderedList: { component: 'UnorderedList' },
375
+ NumberedList: { component: 'NumberedList' },
376
+ ListItem: { component: 'ListItem' },
352
377
  };
353
378
 
354
379
  // Props mapping for migration
@@ -357,17 +382,43 @@ const PROPS_MAPPING = {
357
382
  };
358
383
 
359
384
  // Spacing value mapping from old margin to new spacing
385
+ // Based on the actual CSS classes in src/beta/styles.scss
386
+ // and the old margin prop values: "top" | "bottom" | "both" | "none"
360
387
  const SPACING_MAPPING = {
361
- none: 'none',
362
- top: 'md-top',
363
- bottom: 'md-bottom',
364
- left: 'md-left',
365
- right: 'md-right',
388
+ // Old margin values mapped to new spacing values
389
+ none: 'none', // No spacing
390
+ top: 'md-top', // Top margin only (medium size)
391
+ bottom: 'md-bottom', // Bottom margin only (medium size)
392
+ both: 'md', // Both top and bottom margins (medium size)
393
+
394
+ // Additional spacing values for more granular control
395
+ // These weren't in the old margin prop but are available in new spacing
396
+ left: 'md-left', // Left margin (medium size)
397
+ right: 'md-right', // Right margin (medium size)
398
+
399
+ // Size-based spacing (applies to both top and bottom)
366
400
  xs: 'xs',
367
401
  sm: 'sm',
368
402
  md: 'md',
369
403
  lg: 'lg',
370
404
  xl: 'xl',
405
+
406
+ // Specific directional spacing with sizes
407
+ 'xs-top': 'xs-top',
408
+ 'xs-bottom': 'xs-bottom',
409
+ 'sm-top': 'sm-top',
410
+ 'sm-bottom': 'sm-bottom',
411
+ 'md-top': 'md-top',
412
+ 'md-bottom': 'md-bottom',
413
+ 'lg-top': 'lg-top',
414
+ 'lg-bottom': 'lg-bottom',
415
+ 'xl-top': 'xl-top',
416
+ 'xl-bottom': 'xl-bottom',
417
+
418
+ // Extra small variants
419
+ xs2: 'xs2',
420
+ 'xs2-top': 'xs2-top',
421
+ 'xs2-bottom': 'xs2-bottom',
371
422
  };
372
423
 
373
424
  // Import patterns to handle
@@ -376,35 +427,40 @@ const IMPORT_PATTERNS = [
376
427
  /from\s+['"`]@entur\/typography\/dist['"`]/g,
377
428
  /from\s+['"`]@entur\/typography\/dist\/index['"`]/g,
378
429
  /from\s+['"`]@entur\/typography\/dist\/styles\.css['"`]/g,
430
+ /from\s+['"`]@entur\/typography\/styles['"`]/g,
379
431
  ];
380
432
 
381
433
  // Parse JSX props more robustly
382
434
  function parseJSXProps(propsString) {
383
435
  if (!propsString || !propsString.trim()) {
384
- return { props: {}, warnings: [] };
436
+ return { props: {}, warnings: [], spreadProps: [] };
385
437
  }
386
438
 
387
439
  const props = {};
388
440
  const warnings = [];
389
- const MAX_ITERATIONS = 100; // Prevent infinite loops
390
- let iterationCount = 0;
441
+ const spreadProps = []; // Track spread props separately
442
+ const originalSyntax = {}; // Track original JSX syntax for each prop
391
443
 
392
444
  try {
393
445
  // Parse props manually to handle complex cases
394
446
  let remaining = propsString.trim();
395
- let lastRemainingLength = remaining.length;
396
447
 
397
- while (remaining.length > 0 && iterationCount < MAX_ITERATIONS) {
398
- iterationCount++;
448
+ // First, extract all spread props
449
+ const spreadRegex = /\.\.\.\{?(\w+)\}?/g;
450
+ let spreadMatch;
451
+ while ((spreadMatch = spreadRegex.exec(remaining)) !== null) {
452
+ spreadProps.push(spreadMatch[1]);
453
+ }
399
454
 
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;
455
+ // Remove spread props from the string to parse regular props
456
+ remaining = remaining.replace(/\.\.\.\{?(\w+)\}?/g, '');
457
+
458
+ // Now parse regular props
459
+ while (remaining.trim().length > 0) {
460
+ // Skip whitespace
461
+ remaining = remaining.replace(/^\s+/, '');
406
462
 
407
- // Match prop name - more efficient regex
463
+ // Match prop name
408
464
  const nameMatch = remaining.match(/^(\w+)=/);
409
465
  if (!nameMatch) break;
410
466
 
@@ -414,7 +470,7 @@ function parseJSXProps(propsString) {
414
470
 
415
471
  // Match prop value
416
472
  if (remaining.startsWith('"') || remaining.startsWith("'")) {
417
- // String value - use indexOf for better performance
473
+ // String value
418
474
  const quote = remaining[0];
419
475
  const endQuoteIndex = remaining.indexOf(quote, 1);
420
476
  if (endQuoteIndex === -1) {
@@ -424,14 +480,14 @@ function parseJSXProps(propsString) {
424
480
 
425
481
  const propValue = remaining.substring(1, endQuoteIndex);
426
482
  props[propName] = propValue;
483
+ originalSyntax[propName] = 'string'; // Mark as string literal
427
484
  remaining = remaining.substring(endQuoteIndex + 1);
428
485
  } else if (remaining.startsWith('{')) {
429
- // Object value - find matching closing brace with bounds checking
486
+ // Object value - find matching closing brace
430
487
  let braceCount = 0;
431
488
  let endIndex = -1;
432
- const maxSearchLength = Math.min(remaining.length, 1000); // Limit search length
433
489
 
434
- for (let i = 0; i < maxSearchLength; i++) {
490
+ for (let i = 0; i < remaining.length; i++) {
435
491
  if (remaining[i] === '{') braceCount++;
436
492
  if (remaining[i] === '}') {
437
493
  braceCount--;
@@ -449,25 +505,23 @@ function parseJSXProps(propsString) {
449
505
 
450
506
  const propValue = remaining.substring(1, endIndex);
451
507
  props[propName] = propValue;
508
+ originalSyntax[propName] = 'jsx'; // Mark as JSX expression
452
509
  remaining = remaining.substring(endIndex + 1);
453
510
  } else {
454
- // Boolean prop (e.g., disabled) or invalid syntax
511
+ // Boolean prop
455
512
  props[propName] = true;
513
+ originalSyntax[propName] = 'boolean'; // Mark as boolean
456
514
  break;
457
515
  }
458
516
 
459
- // Skip whitespace more efficiently
517
+ // Skip whitespace
460
518
  remaining = remaining.replace(/^\s+/, '');
461
519
  }
462
-
463
- if (iterationCount >= MAX_ITERATIONS) {
464
- warnings.push(`Maximum parsing iterations (${MAX_ITERATIONS}) reached`);
465
- }
466
520
  } catch (error) {
467
521
  warnings.push(`Failed to parse props: ${error.message}`);
468
522
  }
469
523
 
470
- return { props, warnings };
524
+ return { props, warnings, spreadProps, originalSyntax };
471
525
  }
472
526
 
473
527
  // Migrate props from old to new format
@@ -485,11 +539,12 @@ function migrateProps(props, oldComponent) {
485
539
  `Migrated 'margin="${props.margin}"' to 'spacing="${newSpacing}"'`,
486
540
  );
487
541
  } else {
488
- // Unknown margin value - keep as is but warn
489
- migratedProps.spacing = props.margin;
542
+ // Unknown margin value - suggest alternatives
543
+ const suggestions = getSpacingSuggestions(props.margin);
544
+ migratedProps.spacing = props.margin; // Keep original value for now
490
545
  delete migratedProps.margin;
491
546
  warnings.push(
492
- `Migrated 'margin="${props.margin}"' to 'spacing="${props.margin}"' (unknown value - may need manual review)`,
547
+ `Migrated 'margin="${props.margin}"' to 'spacing="${props.margin}"' (unknown value). ${suggestions}`,
493
548
  );
494
549
  }
495
550
  }
@@ -516,8 +571,55 @@ function migrateProps(props, oldComponent) {
516
571
  return { props: migratedProps, warnings };
517
572
  }
518
573
 
574
+ // Helper function to suggest spacing alternatives for unknown margin values
575
+ function getSpacingSuggestions(unknownMargin) {
576
+ const suggestions = [];
577
+
578
+ // Check if it might be one of the old margin values
579
+ if (['top', 'bottom', 'both', 'none'].includes(unknownMargin)) {
580
+ suggestions.push(
581
+ `"${unknownMargin}" is a valid old margin value and will be migrated correctly.`,
582
+ );
583
+ return suggestions.join(' ');
584
+ }
585
+
586
+ // Check if it might be a directional value
587
+ if (
588
+ unknownMargin.includes('top') ||
589
+ unknownMargin.includes('bottom') ||
590
+ unknownMargin.includes('left') ||
591
+ unknownMargin.includes('right')
592
+ ) {
593
+ suggestions.push(
594
+ 'Consider using directional spacing like "md-top", "sm-bottom", etc.',
595
+ );
596
+ }
597
+
598
+ // Check if it might be a size value
599
+ if (['xs', 'sm', 'md', 'lg', 'xl'].includes(unknownMargin)) {
600
+ suggestions.push(
601
+ 'Consider using size-based spacing like "xs", "sm", "md", "lg", "xl".',
602
+ );
603
+ }
604
+
605
+ // Check if it might be a specific variant
606
+ if (unknownMargin.includes('xs2')) {
607
+ suggestions.push(
608
+ 'Consider using "xs2", "xs2-top", or "xs2-bottom" for extra small spacing.',
609
+ );
610
+ }
611
+
612
+ if (suggestions.length === 0) {
613
+ suggestions.push(
614
+ 'Old margin values: "none", "top", "bottom", "both". New spacing values: "xs", "sm", "md", "lg", "xl", and directional variants.',
615
+ );
616
+ }
617
+
618
+ return suggestions.join(' ');
619
+ }
620
+
519
621
  // Convert props object back to JSX string
520
- function propsToString(props) {
622
+ function propsToString(props, originalSyntax = {}) {
521
623
  if (!props || Object.keys(props).length === 0) {
522
624
  return '';
523
625
  }
@@ -526,20 +628,46 @@ function propsToString(props) {
526
628
  ' ' +
527
629
  Object.entries(props)
528
630
  .map(([key, value]) => {
529
- // Handle different value types
530
- if (typeof value === 'string' && !value.includes('{')) {
631
+ // Use original syntax information if available
632
+ if (originalSyntax[key] === 'string') {
531
633
  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}}`;
634
+ } else if (originalSyntax[key] === 'jsx') {
635
+ // Check if the JSX expression is actually a string literal (e.g., 'a' or "a")
636
+ if (
637
+ typeof value === 'string' &&
638
+ ((value.startsWith("'") &&
639
+ value.endsWith("'") &&
640
+ value.length > 1) ||
641
+ (value.startsWith('"') &&
642
+ value.endsWith('"') &&
643
+ value.length > 1))
644
+ ) {
645
+ // It's a string literal in JSX, convert to string prop
646
+ const stringValue = value.slice(1, -1); // Remove quotes
647
+ return `${key}="${stringValue}"`;
648
+ } else {
649
+ // It's a real JSX expression, keep as is
650
+ return `${key}={${value}}`;
651
+ }
652
+ } else if (originalSyntax[key] === 'boolean') {
653
+ return value ? key : '';
539
654
  } else {
540
- return `${key}={${value}}`;
655
+ // Fallback logic for when originalSyntax is not available
656
+ if (typeof value === 'string' && !value.includes('{')) {
657
+ return `${key}="${value}"`;
658
+ } else if (
659
+ typeof value === 'string' &&
660
+ value.startsWith('{') &&
661
+ value.endsWith('}')
662
+ ) {
663
+ // Already a JSX object, don't add extra braces
664
+ return `${key}={${value}}`;
665
+ } else {
666
+ return `${key}={${value}}`;
667
+ }
541
668
  }
542
669
  })
670
+ .filter(prop => prop.length > 0) // Remove empty props (like false booleans)
543
671
  .join(' ')
544
672
  );
545
673
  }
@@ -549,40 +677,113 @@ function updateImports(content) {
549
677
  let updatedContent = content;
550
678
  let changes = 0;
551
679
 
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
- });
680
+ // Handle CSS imports separately - these should stay with the main package
681
+ const cssImportPattern = /from\s+['"`]@entur\/typography\/styles['"`]/g;
682
+ if (cssImportPattern.test(content)) {
683
+ // CSS imports should remain unchanged as they're still in the main package
684
+ // No changes needed for CSS imports
685
+ }
558
686
 
559
- // Then, update destructured import names - only within @entur/typography imports
560
- // Find all import statements from @entur/typography and update component names
687
+ // Handle component imports - only migrate if they contain components that need migration
561
688
  const importRegex =
562
689
  /import\s*{([^}]+)}\s*from\s*['"']@entur\/typography['"']/g;
563
690
 
564
691
  updatedContent = updatedContent.replace(importRegex, (match, importList) => {
692
+ const components = importList
693
+ .split(',')
694
+ .map(comp => comp.trim())
695
+ .filter(comp => comp);
696
+
697
+ // Check if any of the imported components need migration
698
+ // Need to check both the full import (e.g., "Link as Li") and just the component name (e.g., "Link")
699
+ const needsMigration = components.some(comp => {
700
+ // Extract the actual component name from aliased imports
701
+ const componentName = comp.includes(' as ')
702
+ ? comp.split(' as ')[0].trim()
703
+ : comp;
704
+ return Object.keys(COMPONENT_MAPPING).includes(componentName);
705
+ });
706
+
707
+ if (!needsMigration) {
708
+ // No migration needed, keep the import as is
709
+ return match;
710
+ }
711
+
565
712
  let updatedImportList = importList;
566
713
  let hasChanges = false;
567
714
  const uniqueComponents = new Set();
715
+ const processedComponents = new Set(); // Track which components we've already processed
716
+
717
+ // First, collect all existing components that should be preserved
718
+ const existingComponents = components.filter(comp => {
719
+ // Extract the actual component name from aliased imports
720
+ const componentName = comp.includes(' as ')
721
+ ? comp.split(' as ')[0].trim()
722
+ : comp;
723
+
724
+ // Keep components that are:
725
+ // 1. Not in the migration mapping (old components), OR
726
+ // 2. Are the target components (new beta components)
727
+ const isOldComponent =
728
+ Object.keys(COMPONENT_MAPPING).includes(componentName);
729
+ const isTargetComponent = Object.values(COMPONENT_MAPPING).some(
730
+ mapping => mapping.component === componentName,
731
+ );
568
732
 
569
- // Check each component in the import list
733
+ return !isOldComponent || isTargetComponent;
734
+ });
735
+
736
+ // Then, update components that need migration
570
737
  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;
738
+ // Check if this old component exists in any form (aliased or not)
739
+ const hasComponent = components.some(comp => {
740
+ const componentName = comp.includes(' as ')
741
+ ? comp.split(' as ')[0].trim()
742
+ : comp;
743
+ return componentName === oldComponent;
744
+ });
745
+
746
+ if (hasComponent && !processedComponents.has(oldComponent)) {
747
+ // Replace the old component with the new one, preserving aliases
748
+ const componentRegex = new RegExp(`\\b${oldComponent}\\b`, 'g');
749
+ if (componentRegex.test(updatedImportList)) {
750
+ updatedImportList = updatedImportList.replace(
751
+ componentRegex,
752
+ mapping.component,
753
+ );
754
+
755
+ // Only add to uniqueComponents if the component isn't already present in aliased form
756
+ const componentAlreadyPresent = components.some(comp => {
757
+ const componentName = comp.includes(' as ')
758
+ ? comp.split(' as ')[0].trim()
759
+ : comp;
760
+ return componentName === mapping.component;
761
+ });
762
+
763
+ if (!componentAlreadyPresent) {
764
+ uniqueComponents.add(mapping.component);
765
+ }
766
+
767
+ processedComponents.add(oldComponent);
768
+ hasChanges = true;
769
+ }
579
770
  }
580
771
  });
581
772
 
582
773
  if (hasChanges) {
583
774
  changes++;
584
- // Deduplicate components and create clean import statement
585
- const finalImportList = Array.from(uniqueComponents).join(', ');
775
+ // Combine existing components with migrated components
776
+ const allComponents = [
777
+ ...existingComponents,
778
+ ...Array.from(uniqueComponents),
779
+ ];
780
+
781
+ // Filter out any empty components and deduplicate
782
+ const finalComponents = allComponents
783
+ .filter(comp => comp && comp.trim())
784
+ .filter((comp, index, arr) => arr.indexOf(comp) === index); // Remove duplicates
785
+
786
+ const finalImportList = finalComponents.join(', ');
586
787
  return `import {${finalImportList}} from '${BETA_IMPORT}'`;
587
788
  }
588
789
 
@@ -608,8 +809,12 @@ function updateComponents(content) {
608
809
  changes++;
609
810
 
610
811
  // Parse existing props
611
- const { props: existingProps, warnings: parseWarnings } =
612
- parseJSXProps(propsString);
812
+ const {
813
+ props: existingProps,
814
+ warnings: parseWarnings,
815
+ spreadProps,
816
+ originalSyntax,
817
+ } = parseJSXProps(propsString);
613
818
  warnings.push(...parseWarnings);
614
819
 
615
820
  // Migrate props
@@ -629,8 +834,46 @@ function updateComponents(content) {
629
834
 
630
835
  // Handle Heading components
631
836
  if (mapping.component === 'Heading') {
632
- const asValue = newProps.as || mapping.as;
633
- const variantValue = newProps.variant || mapping.variant;
837
+ // Preserve existing 'as' prop if it exists, otherwise use mapping default
838
+ const asValue = existingProps.as || mapping.as;
839
+ // Preserve existing 'variant' prop if it exists, otherwise use mapping default
840
+ const variantValue = existingProps.variant || mapping.variant;
841
+
842
+ // Remove as and variant from props since we'll add them separately
843
+ delete newProps.as;
844
+ delete newProps.variant;
845
+
846
+ // Create props object with as and variant, preserving original syntax
847
+ const headingProps = {
848
+ as: asValue,
849
+ variant: variantValue,
850
+ ...newProps,
851
+ };
852
+
853
+ // Create original syntax object for as and variant
854
+ const headingOriginalSyntax = {
855
+ as: originalSyntax.as || 'string',
856
+ variant: originalSyntax.variant || 'string',
857
+ ...originalSyntax,
858
+ };
859
+
860
+ const propsString = propsToString(
861
+ headingProps,
862
+ headingOriginalSyntax,
863
+ );
864
+ const spreadPropsString =
865
+ spreadProps.length > 0 ? ` {...${spreadProps.join(', ...')}}` : '';
866
+ return `<Heading${propsString}${spreadPropsString}>`;
867
+ }
868
+
869
+ // Handle Label components with special case for htmlFor prop
870
+ if (oldComponent === 'Label' && mapping.component === 'Text') {
871
+ // If htmlFor prop exists, add as="label" to ensure proper semantic HTML
872
+ // Otherwise, use existing as prop or default to undefined (no as prop)
873
+ const asValue = existingProps.htmlFor
874
+ ? 'label'
875
+ : existingProps.as || undefined;
876
+ const variantValue = existingProps.variant || mapping.variant;
634
877
 
635
878
  // Remove as and variant from props since we'll add them separately
636
879
  delete newProps.as;
@@ -644,8 +887,13 @@ function updateComponents(content) {
644
887
  }
645
888
  Object.assign(orderedProps, newProps);
646
889
 
647
- const propsString = propsToString(orderedProps);
648
- return `<Heading as="${asValue}" variant="${variantValue}"${propsString}>`;
890
+ const propsString = propsToString(orderedProps, originalSyntax);
891
+ const spreadPropsString =
892
+ spreadProps.length > 0 ? ` {...${spreadProps.join(', ...')}}` : '';
893
+
894
+ // Only add as prop if it has a value
895
+ const asProp = asValue ? ` as="${asValue}"` : '';
896
+ return `<Text${asProp} variant="${variantValue}"${propsString}${spreadPropsString}>`;
649
897
  }
650
898
 
651
899
  // Handle other components
@@ -667,8 +915,10 @@ function updateComponents(content) {
667
915
  });
668
916
  Object.assign(finalProps, newProps);
669
917
 
670
- const otherPropsString = propsToString(finalProps);
671
- return `<${componentName}${otherPropsString}>`;
918
+ const otherPropsString = propsToString(finalProps, originalSyntax);
919
+ const spreadPropsString =
920
+ spreadProps.length > 0 ? ` {...${spreadProps.join(', ...')}}` : '';
921
+ return `<${componentName}${otherPropsString}${spreadPropsString}>`;
672
922
  },
673
923
  );
674
924
 
@@ -696,15 +946,33 @@ function updateComponents(content) {
696
946
  * @returns {string[]} Array of matching file paths
697
947
  */
698
948
  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,
949
+ const allFiles = [];
950
+
951
+ // Process directory patterns
952
+ const directoryPatterns = ALLOWED_DIRECTORIES.filter(dir =>
953
+ dir.includes('**'),
954
+ );
955
+ const filePatterns = ALLOWED_DIRECTORIES.filter(dir => !dir.includes('**'));
956
+
957
+ // Handle directory patterns (e.g., src/**, app/**)
958
+ if (directoryPatterns.length > 0) {
959
+ const combinedDirPattern = `{${directoryPatterns.join(',')}}/${pattern}`;
960
+ const dirFiles = glob.sync(combinedDirPattern, {
961
+ ignore: BLOCKED_DIRECTORIES,
962
+ nodir: true,
963
+ absolute: false,
964
+ });
965
+ allFiles.push(...dirFiles);
966
+ }
967
+
968
+ // Handle file patterns (e.g., *.jsx, *.tsx)
969
+ filePatterns.forEach(filePattern => {
970
+ const files = glob.sync(filePattern, {
971
+ ignore: BLOCKED_DIRECTORIES,
972
+ nodir: true,
973
+ absolute: false,
974
+ });
975
+ allFiles.push(...files);
708
976
  });
709
977
 
710
978
  // Use Set for efficient deduplication and filtering
@@ -725,37 +993,29 @@ function findFiles(pattern) {
725
993
  return uniqueFiles;
726
994
  }
727
995
 
728
- function updateImportsAndComponents(content, strategy) {
996
+ function updateImportsAndComponents(content) {
729
997
  let updatedContent = content;
730
998
  let changes = 0;
731
999
  let warnings = [];
732
1000
 
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
- }
1001
+ // Update both imports and components
1002
+ const { content: newContent, changes: importChanges } =
1003
+ updateImports(content);
1004
+ const {
1005
+ content: finalContent,
1006
+ changes: componentChanges,
1007
+ warnings: componentWarnings,
1008
+ } = updateComponents(newContent);
1009
+ updatedContent = finalContent;
1010
+ changes = importChanges + componentChanges;
1011
+ warnings = componentWarnings;
752
1012
 
753
1013
  return { content: updatedContent, changes, warnings };
754
1014
  }
755
1015
 
756
- function generateMigrationReport(files, strategy, isDryRun = false) {
1016
+ function generateMigrationReport(files, isDryRun = false) {
757
1017
  const report = {
758
- strategy,
1018
+ strategy: 'complete',
759
1019
  totalFiles: files.length,
760
1020
  migratedFiles: 0,
761
1021
  totalChanges: 0,
@@ -776,7 +1036,7 @@ function generateMigrationReport(files, strategy, isDryRun = false) {
776
1036
  content: updatedContent,
777
1037
  changes,
778
1038
  warnings,
779
- } = updateImportsAndComponents(content, strategy);
1039
+ } = updateImportsAndComponents(content);
780
1040
 
781
1041
  // Combine migration warnings with file analysis warnings
782
1042
  const allWarnings = [...warnings, ...fileAnalysis.warnings];
@@ -1006,23 +1266,13 @@ function printReport(report) {
1006
1266
  }
1007
1267
  }
1008
1268
 
1009
- function showNextSteps(strategy) {
1269
+ function showNextSteps() {
1010
1270
  console.log('\n📝 Next Steps');
1011
1271
  console.log('=============');
1012
1272
 
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
- }
1273
+ console.log('1. 🧪 Test your application thoroughly');
1274
+ console.log('2. 🔄 Review and adjust any component props if needed');
1275
+ console.log('3. 📚 Read the migration guide on our website');
1026
1276
 
1027
1277
  console.log('\n⚠️ Important Notes:');
1028
1278
  console.log('- Check warnings above for potential issues');
@@ -1030,7 +1280,7 @@ function showNextSteps(strategy) {
1030
1280
  console.log('- Test thoroughly, especially components with custom styling');
1031
1281
  }
1032
1282
 
1033
- function main() {
1283
+ async function main() {
1034
1284
  // Show help if requested
1035
1285
  if (process.argv.includes('--help') || process.argv.includes('-h')) {
1036
1286
  console.log('🎨 Typography Migration Script');
@@ -1051,36 +1301,24 @@ function main() {
1051
1301
  console.log(
1052
1302
  ' --dry-run Show what would be changed without modifying files',
1053
1303
  );
1054
- console.log(
1055
- ' --import-only Import-only migration: update import paths only',
1056
- );
1057
-
1058
1304
  console.log(' --help, -h Show this help message');
1059
1305
  console.log('');
1060
- console.log('Migration Modes:');
1061
- console.log(' 🚀 Complete Mode (default): Updates everything');
1306
+ console.log('Migration Mode:');
1307
+ console.log(' 🚀 Complete Mode: Updates everything');
1062
1308
  console.log(' - Replaces old components with beta components');
1063
- console.log(' - May require prop/styling updates');
1309
+ console.log(' - Heading1-6 Heading with as/variant props');
1310
+ console.log(' - Text components → Text with variant props');
1311
+ console.log(' - All typography components migrate to beta versions');
1312
+ console.log(' - Import paths change to @entur/typography/beta');
1313
+ console.log(' - May require prop/styling updates (margin → spacing)');
1064
1314
  console.log(' - Test thoroughly after migration');
1065
1315
  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
1316
  console.log('Examples:');
1073
1317
  console.log(' # See what would be changed');
1074
1318
  console.log(' npx @entur/typography@latest migrate --dry-run');
1075
1319
  console.log('');
1076
1320
  console.log(' # Complete migration: update everything (default)');
1077
1321
  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
1322
 
1085
1323
  console.log('Environment Variables:');
1086
1324
  console.log(
@@ -1096,7 +1334,7 @@ function main() {
1096
1334
  console.log(' Add/remove folder patterns between the 👇 and 👆 markers');
1097
1335
  console.log(' Examples: "src/**", "app/**", "packages/my-app/**"');
1098
1336
  console.log('');
1099
- console.log(' Option 2: Set environment variable (for CI/CD)');
1337
+ console.log(' Option 2: Set environment variable');
1100
1338
  console.log(
1101
1339
  ' export TYPOGRAPHY_MIGRATION_DIRS="src/**,app/**,components/**"',
1102
1340
  );
@@ -1164,47 +1402,48 @@ function main() {
1164
1402
 
1165
1403
  // Parse command line options
1166
1404
  const isDryRun = process.argv.includes('--dry-run');
1167
- const isImportOnly = process.argv.includes('--import-only');
1168
1405
 
1169
1406
  if (isDryRun) {
1170
1407
  console.log('🔍 DRY RUN MODE: No files will be modified');
1171
1408
  console.log('');
1172
1409
  }
1173
1410
 
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
- }
1411
+ console.log('🚀 COMPLETE MIGRATION: Updating imports + component usage');
1412
+ console.log('⚠️ WARNING: This will modify your component usage!');
1413
+ console.log(' - Old components will be replaced with beta components');
1414
+ console.log(' - Import paths will change to @entur/typography/beta');
1415
+ console.log(
1416
+ ' - Link Link, Blockquote Blockquote, Lists → List components (from beta)',
1417
+ );
1418
+ console.log(
1419
+ ' - List components UnorderedList, NumberedList, ListItem (from beta)',
1420
+ );
1421
+ console.log(' - Props may change (margin → spacing)');
1422
+ console.log(' - Test thoroughly after migration');
1186
1423
 
1187
1424
  console.log('');
1188
1425
 
1189
1426
  // Perform migration
1190
- const report = generateMigrationReport(
1191
- allFiles,
1192
- isImportOnly ? 'import-only' : 'complete',
1193
- isDryRun,
1194
- );
1427
+ const report = generateMigrationReport(allFiles, isDryRun);
1195
1428
  printReport(report);
1196
- showNextSteps(isImportOnly ? 'import-only' : 'complete');
1429
+ showNextSteps();
1197
1430
 
1198
1431
  console.log('\n🎯 Migration complete!');
1199
1432
  }
1200
1433
 
1201
- if (require.main === module) {
1202
- main();
1434
+ // Check if this module is being run directly
1435
+ if (import.meta.url === `file://${process.argv[1]}`) {
1436
+ main().catch(error => {
1437
+ console.error('❌ Migration failed:', error);
1438
+ process.exit(1);
1439
+ });
1203
1440
  }
1204
1441
 
1205
- module.exports = {
1442
+ export {
1206
1443
  updateImportsAndComponents,
1207
1444
  generateMigrationReport,
1208
1445
  COMPONENT_MAPPING,
1209
1446
  PROPS_MAPPING,
1447
+ parseJSXProps,
1448
+ propsToString,
1210
1449
  };