@entur/typography 1.9.13 ā 1.10.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +101 -4
- package/dist/BaseHeading.d.ts +18 -0
- package/dist/Blockquote.d.ts +12 -0
- package/dist/CodeText.d.ts +16 -0
- package/dist/EmphasizedText.d.ts +20 -0
- package/dist/Heading1.d.ts +20 -0
- package/dist/Heading2.d.ts +20 -0
- package/dist/Heading3.d.ts +20 -0
- package/dist/Heading4.d.ts +20 -0
- package/dist/Heading5.d.ts +20 -0
- package/dist/Heading6.d.ts +20 -0
- package/dist/Label.d.ts +20 -0
- package/dist/LeadParagraph.d.ts +20 -0
- package/dist/Link.d.ts +22 -0
- package/dist/ListItem.d.ts +11 -0
- package/dist/NumberedList.d.ts +8 -0
- package/dist/Paragraph.d.ts +20 -0
- package/dist/PreformattedText.d.ts +16 -0
- package/dist/SmallText.d.ts +20 -0
- package/dist/StrongText.d.ts +20 -0
- package/dist/SubLabel.d.ts +20 -0
- package/dist/SubParagraph.d.ts +20 -0
- package/dist/UnorderedList.d.ts +8 -0
- package/dist/beta/BlockquoteBeta.d.ts +12 -0
- package/dist/beta/Heading.d.ts +20 -0
- package/dist/beta/LinkBeta.d.ts +16 -0
- package/dist/beta/ListItemBeta.d.ts +16 -0
- package/dist/beta/NumberedListBeta.d.ts +16 -0
- package/dist/beta/Text.d.ts +20 -0
- package/dist/beta/UnorderedListBeta.d.ts +14 -0
- package/dist/beta/index.d.ts +9 -0
- package/dist/beta/types.d.ts +5 -0
- package/dist/beta/utils.d.ts +10 -0
- package/dist/index.d.ts +28 -426
- package/dist/styles.css +1436 -0
- package/dist/typography.cjs.js +254 -0
- package/dist/typography.cjs.js.map +1 -1
- package/dist/typography.esm.js +255 -1
- package/dist/typography.esm.js.map +1 -1
- package/package.json +11 -8
- package/scripts/migrate-typography.js +1325 -0
|
@@ -0,0 +1,1325 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Typography Migration Script
|
|
5
|
+
*
|
|
6
|
+
* This script helps you migrate from old typography components to new beta typography.
|
|
7
|
+
*
|
|
8
|
+
* MIGRATION MODES:
|
|
9
|
+
*
|
|
10
|
+
* š Complete Mode (default):
|
|
11
|
+
* - Updates import paths AND component usage
|
|
12
|
+
* - Replaces old components with beta components
|
|
13
|
+
* - CONSEQUENCES:
|
|
14
|
+
* * <Heading1> becomes <Heading as="h1" variant="title-1">
|
|
15
|
+
* * <Paragraph> becomes <Text variant="paragraph">
|
|
16
|
+
* * <Link> becomes <LinkBeta>
|
|
17
|
+
* * <Blockquote> becomes <BlockquoteBeta>
|
|
18
|
+
* * <BlockquoteFooter> becomes <BlockquoteFooterBeta>
|
|
19
|
+
* * <UnorderedList> becomes <UnorderedListBeta>
|
|
20
|
+
* * <NumberedList> becomes <NumberedListBeta>
|
|
21
|
+
* * <ListItem> becomes <ListItemBeta>
|
|
22
|
+
* * Props may need updates (e.g., different prop names)
|
|
23
|
+
* * Styling classes may change
|
|
24
|
+
* * Test thoroughly after migration!
|
|
25
|
+
*
|
|
26
|
+
|
|
27
|
+
*
|
|
28
|
+
* Usage:
|
|
29
|
+
* 1. Run this script in your project root
|
|
30
|
+
* 2. Choose your migration mode (complete)
|
|
31
|
+
* 3. Update your styles as needed
|
|
32
|
+
* 4. Test your application thoroughly
|
|
33
|
+
*
|
|
34
|
+
* Options:
|
|
35
|
+
* --dry-run Show what would be changed without modifying files
|
|
36
|
+
*
|
|
37
|
+
* Environment Variables:
|
|
38
|
+
* TYPOGRAPHY_MIGRATION_DIRS Comma-separated list of directories to scan
|
|
39
|
+
* Example: "src/**,app/**"
|
|
40
|
+
*
|
|
41
|
+
* Security Features:
|
|
42
|
+
* - Only scans allowed directories (src/**, app/**, etc.)
|
|
43
|
+
* - Never scans node_modules, dist, build, .git, etc.
|
|
44
|
+
* - Dry-run mode for safe testing
|
|
45
|
+
* - Path validation prevents directory traversal attacks
|
|
46
|
+
*
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
const fs = require('fs');
|
|
50
|
+
const path = require('path');
|
|
51
|
+
|
|
52
|
+
// Check if glob is available
|
|
53
|
+
let glob;
|
|
54
|
+
try {
|
|
55
|
+
glob = require('glob');
|
|
56
|
+
} catch (error) {
|
|
57
|
+
console.error(
|
|
58
|
+
'ā Error: The "glob" package is required to run this migration script.',
|
|
59
|
+
);
|
|
60
|
+
console.error('');
|
|
61
|
+
console.error('Please install it:');
|
|
62
|
+
console.error(' npm install glob');
|
|
63
|
+
console.error(' yarn add glob');
|
|
64
|
+
console.error('');
|
|
65
|
+
console.error('Or use npx which will handle dependencies automatically:');
|
|
66
|
+
console.error(' npx @entur/typography@latest migrate');
|
|
67
|
+
console.error('');
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Configuration
|
|
72
|
+
const OLD_IMPORT = '@entur/typography';
|
|
73
|
+
const BETA_IMPORT = '@entur/typography';
|
|
74
|
+
|
|
75
|
+
// Enhanced warning detection patterns - only truly problematic patterns
|
|
76
|
+
const PROBLEMATIC_PATTERNS = {
|
|
77
|
+
// Style conflicts that will cause issues
|
|
78
|
+
styleMarginConflict: /style=.*margin=/g,
|
|
79
|
+
styleSpacingConflict: /style=.*spacing=/g,
|
|
80
|
+
|
|
81
|
+
// Invalid HTML structure
|
|
82
|
+
nestedTypography: /<Text[^>]*>.*<Text[^>]*>/g,
|
|
83
|
+
|
|
84
|
+
// Accessibility issues
|
|
85
|
+
missingAsProps: /<Heading[^>]*>(?!.*\bas=)/g,
|
|
86
|
+
|
|
87
|
+
// Semantic HTML mismatches
|
|
88
|
+
semanticMismatch: /<Heading[^>]*as="([^"]*)"[^>]*variant="([^"]*)"/g,
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// Warning severity levels
|
|
92
|
+
const WARNING_CATEGORIES = {
|
|
93
|
+
CRITICAL: 'critical', // Will break functionality
|
|
94
|
+
HIGH: 'high', // Likely to cause issues
|
|
95
|
+
MEDIUM: 'medium', // May cause styling issues
|
|
96
|
+
LOW: 'low', // Best practice suggestions
|
|
97
|
+
INFO: 'info', // Informational only
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// =============================================================================
|
|
101
|
+
// šÆ MIGRATION FOLDERS CONFIGURATION
|
|
102
|
+
// =============================================================================
|
|
103
|
+
//
|
|
104
|
+
// EDIT THIS SECTION TO CONTROL WHICH FOLDERS ARE SCANNED
|
|
105
|
+
//
|
|
106
|
+
// ADD FOLDERS: Add new patterns to scan additional directories
|
|
107
|
+
// REMOVE FOLDERS: Delete patterns you don't want to scan
|
|
108
|
+
// CLEAR ALL: Remove all patterns to scan only what you add
|
|
109
|
+
//
|
|
110
|
+
// Examples:
|
|
111
|
+
// 'src/**' - Scan src folder and all subdirectories
|
|
112
|
+
// 'app/**' - Scan app folder and all subdirectories
|
|
113
|
+
// 'packages/my-app/**' - Scan specific package
|
|
114
|
+
// 'frontend/**' - Scan frontend directory
|
|
115
|
+
// 'shared/**' - Scan shared components
|
|
116
|
+
// 'components/**' - Scan components folder
|
|
117
|
+
//
|
|
118
|
+
// =============================================================================
|
|
119
|
+
|
|
120
|
+
const MIGRATION_FOLDERS = [
|
|
121
|
+
// š ADD YOUR FOLDERS HERE š
|
|
122
|
+
'src/**',
|
|
123
|
+
'app/**',
|
|
124
|
+
'apps/**',
|
|
125
|
+
'components/**',
|
|
126
|
+
'pages/**',
|
|
127
|
+
'lib/**',
|
|
128
|
+
'utils/**',
|
|
129
|
+
'styles/**',
|
|
130
|
+
'css/**',
|
|
131
|
+
'scss/**',
|
|
132
|
+
// š ADD YOUR FOLDERS ABOVE š
|
|
133
|
+
];
|
|
134
|
+
|
|
135
|
+
// =============================================================================
|
|
136
|
+
|
|
137
|
+
// Validate and sanitize directory input for security
|
|
138
|
+
function validateDirectoryPath(dir) {
|
|
139
|
+
return !path.isAbsolute(dir) && !dir.includes('..') && !dir.includes('~');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Enhanced file analysis for better warning detection - only truly problematic patterns
|
|
143
|
+
function analyzeFile(filePath, content) {
|
|
144
|
+
const analysis = {
|
|
145
|
+
hasStyleConflicts: false,
|
|
146
|
+
hasNestedTypography: false,
|
|
147
|
+
hasAccessibilityIssues: false,
|
|
148
|
+
hasSemanticMismatches: false,
|
|
149
|
+
lineNumbers: {},
|
|
150
|
+
suggestions: [],
|
|
151
|
+
warnings: [],
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
// Line-by-line analysis for better context
|
|
155
|
+
content.split('\n').forEach((line, index) => {
|
|
156
|
+
const lineNum = index + 1;
|
|
157
|
+
|
|
158
|
+
// Check for style conflicts (style + margin/spacing)
|
|
159
|
+
if (
|
|
160
|
+
line.match(PROBLEMATIC_PATTERNS.styleMarginConflict) ||
|
|
161
|
+
line.match(PROBLEMATIC_PATTERNS.styleSpacingConflict)
|
|
162
|
+
) {
|
|
163
|
+
analysis.hasStyleConflicts = true;
|
|
164
|
+
analysis.lineNumbers.styleConflicts = (
|
|
165
|
+
analysis.lineNumbers.styleConflicts || []
|
|
166
|
+
).concat(lineNum);
|
|
167
|
+
|
|
168
|
+
// Generate warning message
|
|
169
|
+
analysis.warnings.push(
|
|
170
|
+
`Line ${lineNum}: Style conflicts detected - component has both style and margin/spacing props`,
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Check for nested typography components (invalid HTML)
|
|
175
|
+
if (line.match(PROBLEMATIC_PATTERNS.nestedTypography)) {
|
|
176
|
+
analysis.hasNestedTypography = true;
|
|
177
|
+
analysis.lineNumbers.nestedTypography = (
|
|
178
|
+
analysis.lineNumbers.nestedTypography || []
|
|
179
|
+
).concat(lineNum);
|
|
180
|
+
|
|
181
|
+
// Generate warning message
|
|
182
|
+
analysis.warnings.push(
|
|
183
|
+
`Line ${lineNum}: Nested typography components detected - invalid HTML structure`,
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Check for missing as props (accessibility issue)
|
|
188
|
+
if (line.match(PROBLEMATIC_PATTERNS.missingAsProps)) {
|
|
189
|
+
analysis.hasAccessibilityIssues = true;
|
|
190
|
+
analysis.lineNumbers.missingAsProps = (
|
|
191
|
+
analysis.lineNumbers.missingAsProps || []
|
|
192
|
+
).concat(lineNum);
|
|
193
|
+
|
|
194
|
+
// Generate warning message
|
|
195
|
+
analysis.warnings.push(
|
|
196
|
+
`Line ${lineNum}: Missing 'as' prop - accessibility issue for Heading component`,
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Check for semantic mismatches (e.g., h1 with subtitle variant)
|
|
201
|
+
if (line.match(PROBLEMATIC_PATTERNS.semanticMismatch)) {
|
|
202
|
+
analysis.hasSemanticMismatches = true;
|
|
203
|
+
analysis.lineNumbers.semanticMismatches = (
|
|
204
|
+
analysis.lineNumbers.semanticMismatches || []
|
|
205
|
+
).concat(lineNum);
|
|
206
|
+
|
|
207
|
+
// Generate warning message
|
|
208
|
+
analysis.warnings.push(
|
|
209
|
+
`Line ${lineNum}: Semantic mismatch detected - heading level and variant combination may be incorrect`,
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
return analysis;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Generate enhanced warnings with context and solutions
|
|
218
|
+
function generateWarningWithSolution(warning, context, filePath, lineNumber) {
|
|
219
|
+
const severity = determineSeverity(warning);
|
|
220
|
+
const suggestion = generateSuggestion(warning, context);
|
|
221
|
+
const codeExample = generateCodeExample(warning);
|
|
222
|
+
|
|
223
|
+
return {
|
|
224
|
+
message: warning,
|
|
225
|
+
severity,
|
|
226
|
+
suggestion,
|
|
227
|
+
codeExample,
|
|
228
|
+
file: filePath,
|
|
229
|
+
line: lineNumber,
|
|
230
|
+
documentation: getRelevantDocs(warning),
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Determine warning severity based on content
|
|
235
|
+
function determineSeverity(warning) {
|
|
236
|
+
if (warning.includes('will break') || warning.includes('fatal'))
|
|
237
|
+
return WARNING_CATEGORIES.CRITICAL;
|
|
238
|
+
if (warning.includes('conflict') || warning.includes('override'))
|
|
239
|
+
return WARNING_CATEGORIES.HIGH;
|
|
240
|
+
if (warning.includes('may cause') || warning.includes('styling'))
|
|
241
|
+
return WARNING_CATEGORIES.MEDIUM;
|
|
242
|
+
if (warning.includes('best practice') || warning.includes('consider'))
|
|
243
|
+
return WARNING_CATEGORIES.LOW;
|
|
244
|
+
return WARNING_CATEGORIES.INFO;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Generate actionable suggestions
|
|
248
|
+
function generateSuggestion(warning, context) {
|
|
249
|
+
if (warning.includes('style and margin')) {
|
|
250
|
+
return 'Remove the margin prop as it will be overridden by inline styles. Use spacing prop instead.';
|
|
251
|
+
}
|
|
252
|
+
if (warning.includes('missing variant')) {
|
|
253
|
+
return 'Add a variant prop to ensure consistent styling. Example: variant="title-1"';
|
|
254
|
+
}
|
|
255
|
+
if (warning.includes('nested typography')) {
|
|
256
|
+
return 'Avoid nesting Text components. Use spans or other inline elements for emphasis.';
|
|
257
|
+
}
|
|
258
|
+
if (warning.includes('deprecated margin')) {
|
|
259
|
+
return 'Replace margin prop with spacing prop for better consistency.';
|
|
260
|
+
}
|
|
261
|
+
return 'Review the component for potential styling conflicts.';
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Generate code examples for fixes
|
|
265
|
+
function generateCodeExample(warning) {
|
|
266
|
+
if (warning.includes('style and margin')) {
|
|
267
|
+
return '// Before: <Text style={{color: "red"}} margin="bottom">\n// After: <Text style={{color: "red"}} spacing="bottom">';
|
|
268
|
+
}
|
|
269
|
+
if (warning.includes('missing variant')) {
|
|
270
|
+
return '// Before: <Heading as="h1">Title</Heading>\n// After: <Heading as="h1" variant="title-1">Title</Heading>';
|
|
271
|
+
}
|
|
272
|
+
if (warning.includes('nested typography')) {
|
|
273
|
+
return '// Before: <Text>Hello <Text>World</Text></Text>\n// After: <Text>Hello <span>World</span></Text>';
|
|
274
|
+
}
|
|
275
|
+
return '';
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Get relevant documentation links
|
|
279
|
+
function getRelevantDocs(warning) {
|
|
280
|
+
if (warning.includes('variant'))
|
|
281
|
+
return 'https://linje.entur.no/komponenter/ressurser/typography-beta#heading-variants';
|
|
282
|
+
if (warning.includes('spacing'))
|
|
283
|
+
return 'https://linje.entur.no/komponenter/ressurser/typography-beta#spacing';
|
|
284
|
+
if (warning.includes('semantic'))
|
|
285
|
+
return 'https://linje.entur.no/komponenter/ressurser/typography-beta#semantic-html';
|
|
286
|
+
return 'https://linje.entur.no/komponenter/ressurser/typography-beta';
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
let ALLOWED_DIRECTORIES = process.env.TYPOGRAPHY_MIGRATION_DIRS
|
|
290
|
+
? process.env.TYPOGRAPHY_MIGRATION_DIRS.split(',')
|
|
291
|
+
: MIGRATION_FOLDERS;
|
|
292
|
+
|
|
293
|
+
// Filter out potentially dangerous paths
|
|
294
|
+
ALLOWED_DIRECTORIES = ALLOWED_DIRECTORIES.filter(validateDirectoryPath);
|
|
295
|
+
|
|
296
|
+
if (ALLOWED_DIRECTORIES.length === 0) {
|
|
297
|
+
console.error(
|
|
298
|
+
'ā Error: No valid migration directories found after security validation.',
|
|
299
|
+
);
|
|
300
|
+
console.error(
|
|
301
|
+
'All directory paths must be relative and not contain ".." or "~".',
|
|
302
|
+
);
|
|
303
|
+
console.error('');
|
|
304
|
+
console.error('Valid examples:');
|
|
305
|
+
console.error(' src/**');
|
|
306
|
+
console.error(' app/**');
|
|
307
|
+
console.error(' components/**');
|
|
308
|
+
console.error('');
|
|
309
|
+
console.error('Invalid examples:');
|
|
310
|
+
console.error(' /absolute/path');
|
|
311
|
+
console.error(' ../parent/directory');
|
|
312
|
+
console.error(' ~/home/directory');
|
|
313
|
+
process.exit(1);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Security: Block-list of directories to never scan
|
|
317
|
+
const BLOCKED_DIRECTORIES = [
|
|
318
|
+
'**/node_modules/**',
|
|
319
|
+
'**/dist/**',
|
|
320
|
+
'**/build/**',
|
|
321
|
+
'**/.git/**',
|
|
322
|
+
'**/coverage/**',
|
|
323
|
+
'**/.next/**',
|
|
324
|
+
'**/.nuxt/**',
|
|
325
|
+
'**/public/**',
|
|
326
|
+
'**/static/**',
|
|
327
|
+
'**/assets/**',
|
|
328
|
+
'**/images/**',
|
|
329
|
+
'**/fonts/**',
|
|
330
|
+
'**/vendor/**',
|
|
331
|
+
'**/temp/**',
|
|
332
|
+
'**/tmp/**',
|
|
333
|
+
];
|
|
334
|
+
|
|
335
|
+
// Component mapping for complete migration
|
|
336
|
+
const COMPONENT_MAPPING = {
|
|
337
|
+
Heading1: { component: 'Heading', as: 'h1', variant: 'title-1' },
|
|
338
|
+
Heading2: { component: 'Heading', as: 'h2', variant: 'title-2' },
|
|
339
|
+
Heading3: { component: 'Heading', as: 'h3', variant: 'subtitle-1' },
|
|
340
|
+
Heading4: { component: 'Heading', as: 'h4', variant: 'subtitle-2' },
|
|
341
|
+
Heading5: { component: 'Heading', as: 'h5', variant: 'section-1' },
|
|
342
|
+
Heading6: { component: 'Heading', as: 'h6', variant: 'section-2' },
|
|
343
|
+
Paragraph: { component: 'Text', variant: 'paragraph' },
|
|
344
|
+
LeadParagraph: { component: 'Text', variant: 'leading' },
|
|
345
|
+
SmallText: { component: 'Text', variant: 'subparagraph' },
|
|
346
|
+
StrongText: { component: 'Text', as: 'strong', weight: 'bold' },
|
|
347
|
+
SubLabel: { component: 'Text', variant: 'sublabel' },
|
|
348
|
+
SubParagraph: { component: 'Text', variant: 'subparagraph' },
|
|
349
|
+
Label: { component: 'Text', variant: 'label' },
|
|
350
|
+
EmphasizedText: { component: 'Text', variant: 'emphasized' },
|
|
351
|
+
CodeText: { component: 'Text', variant: 'code-text' },
|
|
352
|
+
Link: { component: 'LinkBeta' }, // Convert Link to LinkBeta
|
|
353
|
+
Blockquote: { component: 'BlockquoteBeta' }, // Convert Blockquote to BlockquoteBeta
|
|
354
|
+
BlockquoteFooter: { component: 'BlockquoteFooterBeta' }, // Convert BlockquoteFooter to BlockquoteFooterBeta
|
|
355
|
+
UnorderedList: { component: 'UnorderedListBeta' },
|
|
356
|
+
NumberedList: { component: 'NumberedListBeta' },
|
|
357
|
+
ListItem: { component: 'ListItemBeta' },
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
// Props mapping for migration
|
|
361
|
+
const PROPS_MAPPING = {
|
|
362
|
+
margin: 'spacing',
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
// Spacing value mapping from old margin to new spacing
|
|
366
|
+
// Based on the actual CSS classes in src/beta/styles.scss
|
|
367
|
+
// and the old margin prop values: "top" | "bottom" | "both" | "none"
|
|
368
|
+
const SPACING_MAPPING = {
|
|
369
|
+
// Old margin values mapped to new spacing values
|
|
370
|
+
none: 'none', // No spacing
|
|
371
|
+
top: 'md-top', // Top margin only (medium size)
|
|
372
|
+
bottom: 'md-bottom', // Bottom margin only (medium size)
|
|
373
|
+
both: 'md', // Both top and bottom margins (medium size)
|
|
374
|
+
|
|
375
|
+
// Additional spacing values for more granular control
|
|
376
|
+
// These weren't in the old margin prop but are available in new spacing
|
|
377
|
+
left: 'md-left', // Left margin (medium size)
|
|
378
|
+
right: 'md-right', // Right margin (medium size)
|
|
379
|
+
|
|
380
|
+
// Size-based spacing (applies to both top and bottom)
|
|
381
|
+
xs: 'xs',
|
|
382
|
+
sm: 'sm',
|
|
383
|
+
md: 'md',
|
|
384
|
+
lg: 'lg',
|
|
385
|
+
xl: 'xl',
|
|
386
|
+
|
|
387
|
+
// Specific directional spacing with sizes
|
|
388
|
+
'xs-top': 'xs-top',
|
|
389
|
+
'xs-bottom': 'xs-bottom',
|
|
390
|
+
'sm-top': 'sm-top',
|
|
391
|
+
'sm-bottom': 'sm-bottom',
|
|
392
|
+
'md-top': 'md-top',
|
|
393
|
+
'md-bottom': 'md-bottom',
|
|
394
|
+
'lg-top': 'lg-top',
|
|
395
|
+
'lg-bottom': 'lg-bottom',
|
|
396
|
+
'xl-top': 'xl-top',
|
|
397
|
+
'xl-bottom': 'xl-bottom',
|
|
398
|
+
|
|
399
|
+
// Extra small variants
|
|
400
|
+
xs2: 'xs2',
|
|
401
|
+
'xs2-top': 'xs2-top',
|
|
402
|
+
'xs2-bottom': 'xs2-bottom',
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
// Import patterns to handle
|
|
406
|
+
const IMPORT_PATTERNS = [
|
|
407
|
+
/from\s+['"`]@entur\/typography['"`]/g,
|
|
408
|
+
/from\s+['"`]@entur\/typography\/dist['"`]/g,
|
|
409
|
+
/from\s+['"`]@entur\/typography\/dist\/index['"`]/g,
|
|
410
|
+
/from\s+['"`]@entur\/typography\/dist\/styles\.css['"`]/g,
|
|
411
|
+
];
|
|
412
|
+
|
|
413
|
+
// Parse JSX props more robustly
|
|
414
|
+
function parseJSXProps(propsString) {
|
|
415
|
+
if (!propsString || !propsString.trim()) {
|
|
416
|
+
return { props: {}, warnings: [], spreadProps: [] };
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const props = {};
|
|
420
|
+
const warnings = [];
|
|
421
|
+
const spreadProps = []; // Track spread props separately
|
|
422
|
+
const originalSyntax = {}; // Track original JSX syntax for each prop
|
|
423
|
+
|
|
424
|
+
try {
|
|
425
|
+
// Parse props manually to handle complex cases
|
|
426
|
+
let remaining = propsString.trim();
|
|
427
|
+
|
|
428
|
+
// First, extract all spread props
|
|
429
|
+
const spreadRegex = /\.\.\.\{?(\w+)\}?/g;
|
|
430
|
+
let spreadMatch;
|
|
431
|
+
while ((spreadMatch = spreadRegex.exec(remaining)) !== null) {
|
|
432
|
+
spreadProps.push(spreadMatch[1]);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Remove spread props from the string to parse regular props
|
|
436
|
+
remaining = remaining.replace(/\.\.\.\{?(\w+)\}?/g, '');
|
|
437
|
+
|
|
438
|
+
// Now parse regular props
|
|
439
|
+
while (remaining.trim().length > 0) {
|
|
440
|
+
// Skip whitespace
|
|
441
|
+
remaining = remaining.replace(/^\s+/, '');
|
|
442
|
+
|
|
443
|
+
// Match prop name
|
|
444
|
+
const nameMatch = remaining.match(/^(\w+)=/);
|
|
445
|
+
if (!nameMatch) break;
|
|
446
|
+
|
|
447
|
+
const propName = nameMatch[1];
|
|
448
|
+
const matchLength = nameMatch[0].length;
|
|
449
|
+
remaining = remaining.substring(matchLength);
|
|
450
|
+
|
|
451
|
+
// Match prop value
|
|
452
|
+
if (remaining.startsWith('"') || remaining.startsWith("'")) {
|
|
453
|
+
// String value
|
|
454
|
+
const quote = remaining[0];
|
|
455
|
+
const endQuoteIndex = remaining.indexOf(quote, 1);
|
|
456
|
+
if (endQuoteIndex === -1) {
|
|
457
|
+
warnings.push(`Unterminated string in prop ${propName}`);
|
|
458
|
+
break;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const propValue = remaining.substring(1, endQuoteIndex);
|
|
462
|
+
props[propName] = propValue;
|
|
463
|
+
originalSyntax[propName] = 'string'; // Mark as string literal
|
|
464
|
+
remaining = remaining.substring(endQuoteIndex + 1);
|
|
465
|
+
} else if (remaining.startsWith('{')) {
|
|
466
|
+
// Object value - find matching closing brace
|
|
467
|
+
let braceCount = 0;
|
|
468
|
+
let endIndex = -1;
|
|
469
|
+
|
|
470
|
+
for (let i = 0; i < remaining.length; i++) {
|
|
471
|
+
if (remaining[i] === '{') braceCount++;
|
|
472
|
+
if (remaining[i] === '}') {
|
|
473
|
+
braceCount--;
|
|
474
|
+
if (braceCount === 0) {
|
|
475
|
+
endIndex = i;
|
|
476
|
+
break;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
if (endIndex === -1) {
|
|
482
|
+
warnings.push(`Unterminated object in prop ${propName}`);
|
|
483
|
+
break;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const propValue = remaining.substring(1, endIndex);
|
|
487
|
+
props[propName] = propValue;
|
|
488
|
+
originalSyntax[propName] = 'jsx'; // Mark as JSX expression
|
|
489
|
+
remaining = remaining.substring(endIndex + 1);
|
|
490
|
+
} else {
|
|
491
|
+
// Boolean prop
|
|
492
|
+
props[propName] = true;
|
|
493
|
+
originalSyntax[propName] = 'boolean'; // Mark as boolean
|
|
494
|
+
break;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// Skip whitespace
|
|
498
|
+
remaining = remaining.replace(/^\s+/, '');
|
|
499
|
+
}
|
|
500
|
+
} catch (error) {
|
|
501
|
+
warnings.push(`Failed to parse props: ${error.message}`);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
return { props, warnings, spreadProps, originalSyntax };
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// Migrate props from old to new format
|
|
508
|
+
function migrateProps(props, oldComponent) {
|
|
509
|
+
const migratedProps = { ...props };
|
|
510
|
+
const warnings = [];
|
|
511
|
+
|
|
512
|
+
// Handle margin prop migration
|
|
513
|
+
if (props.margin) {
|
|
514
|
+
const newSpacing = SPACING_MAPPING[props.margin];
|
|
515
|
+
if (newSpacing) {
|
|
516
|
+
migratedProps.spacing = newSpacing;
|
|
517
|
+
delete migratedProps.margin;
|
|
518
|
+
warnings.push(
|
|
519
|
+
`Migrated 'margin="${props.margin}"' to 'spacing="${newSpacing}"'`,
|
|
520
|
+
);
|
|
521
|
+
} else {
|
|
522
|
+
// Unknown margin value - suggest alternatives
|
|
523
|
+
const suggestions = getSpacingSuggestions(props.margin);
|
|
524
|
+
migratedProps.spacing = props.margin; // Keep original value for now
|
|
525
|
+
delete migratedProps.margin;
|
|
526
|
+
warnings.push(
|
|
527
|
+
`Migrated 'margin="${props.margin}"' to 'spacing="${props.margin}"' (unknown value). ${suggestions}`,
|
|
528
|
+
);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// Handle Heading components with existing 'as' prop
|
|
533
|
+
if (oldComponent.startsWith('Heading') && props.as) {
|
|
534
|
+
const headingNumber = oldComponent.replace('Heading', '');
|
|
535
|
+
const expectedAs = `h${headingNumber}`;
|
|
536
|
+
|
|
537
|
+
if (props.as !== expectedAs) {
|
|
538
|
+
warnings.push(
|
|
539
|
+
`Heading component has 'as="${props.as}"' but expected 'as="${expectedAs}"' - review semantic HTML structure`,
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// Handle style prop conflicts
|
|
545
|
+
if (props.style && props.margin) {
|
|
546
|
+
warnings.push(
|
|
547
|
+
`Component has both 'style' and 'margin' props - check for conflicts`,
|
|
548
|
+
);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
return { props: migratedProps, warnings };
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// Helper function to suggest spacing alternatives for unknown margin values
|
|
555
|
+
function getSpacingSuggestions(unknownMargin) {
|
|
556
|
+
const suggestions = [];
|
|
557
|
+
|
|
558
|
+
// Check if it might be one of the old margin values
|
|
559
|
+
if (['top', 'bottom', 'both', 'none'].includes(unknownMargin)) {
|
|
560
|
+
suggestions.push(
|
|
561
|
+
`"${unknownMargin}" is a valid old margin value and will be migrated correctly.`,
|
|
562
|
+
);
|
|
563
|
+
return suggestions.join(' ');
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// Check if it might be a directional value
|
|
567
|
+
if (
|
|
568
|
+
unknownMargin.includes('top') ||
|
|
569
|
+
unknownMargin.includes('bottom') ||
|
|
570
|
+
unknownMargin.includes('left') ||
|
|
571
|
+
unknownMargin.includes('right')
|
|
572
|
+
) {
|
|
573
|
+
suggestions.push(
|
|
574
|
+
'Consider using directional spacing like "md-top", "sm-bottom", etc.',
|
|
575
|
+
);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// Check if it might be a size value
|
|
579
|
+
if (['xs', 'sm', 'md', 'lg', 'xl'].includes(unknownMargin)) {
|
|
580
|
+
suggestions.push(
|
|
581
|
+
'Consider using size-based spacing like "xs", "sm", "md", "lg", "xl".',
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// Check if it might be a specific variant
|
|
586
|
+
if (unknownMargin.includes('xs2')) {
|
|
587
|
+
suggestions.push(
|
|
588
|
+
'Consider using "xs2", "xs2-top", or "xs2-bottom" for extra small spacing.',
|
|
589
|
+
);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
if (suggestions.length === 0) {
|
|
593
|
+
suggestions.push(
|
|
594
|
+
'Old margin values: "none", "top", "bottom", "both". New spacing values: "xs", "sm", "md", "lg", "xl", and directional variants.',
|
|
595
|
+
);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
return suggestions.join(' ');
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// Convert props object back to JSX string
|
|
602
|
+
function propsToString(props, originalSyntax = {}) {
|
|
603
|
+
if (!props || Object.keys(props).length === 0) {
|
|
604
|
+
return '';
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
return (
|
|
608
|
+
' ' +
|
|
609
|
+
Object.entries(props)
|
|
610
|
+
.map(([key, value]) => {
|
|
611
|
+
// Use original syntax information if available
|
|
612
|
+
if (originalSyntax[key] === 'string') {
|
|
613
|
+
return `${key}="${value}"`;
|
|
614
|
+
} else if (originalSyntax[key] === 'jsx') {
|
|
615
|
+
return `${key}={${value}}`;
|
|
616
|
+
} else if (originalSyntax[key] === 'boolean') {
|
|
617
|
+
return value ? key : '';
|
|
618
|
+
} else {
|
|
619
|
+
// Fallback logic for when originalSyntax is not available
|
|
620
|
+
if (typeof value === 'string' && !value.includes('{')) {
|
|
621
|
+
return `${key}="${value}"`;
|
|
622
|
+
} else if (
|
|
623
|
+
typeof value === 'string' &&
|
|
624
|
+
value.startsWith('{') &&
|
|
625
|
+
value.endsWith('}')
|
|
626
|
+
) {
|
|
627
|
+
// Already a JSX object, don't add extra braces
|
|
628
|
+
return `${key}={${value}}`;
|
|
629
|
+
} else {
|
|
630
|
+
return `${key}={${value}}`;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
})
|
|
634
|
+
.filter(prop => prop.length > 0) // Remove empty props (like false booleans)
|
|
635
|
+
.join(' ')
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// Update imports in content
|
|
640
|
+
function updateImports(content) {
|
|
641
|
+
let updatedContent = content;
|
|
642
|
+
let changes = 0;
|
|
643
|
+
|
|
644
|
+
// First, update import paths
|
|
645
|
+
IMPORT_PATTERNS.forEach(pattern => {
|
|
646
|
+
const matches = content.match(pattern) || [];
|
|
647
|
+
changes += matches.length;
|
|
648
|
+
updatedContent = updatedContent.replace(pattern, `from '${BETA_IMPORT}'`);
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
// Then, update destructured import names - only within @entur/typography imports
|
|
652
|
+
// Find all import statements from @entur/typography and update component names
|
|
653
|
+
const importRegex =
|
|
654
|
+
/import\s*{([^}]+)}\s*from\s*['"']@entur\/typography['"']/g;
|
|
655
|
+
|
|
656
|
+
updatedContent = updatedContent.replace(importRegex, (match, importList) => {
|
|
657
|
+
let updatedImportList = importList;
|
|
658
|
+
let hasChanges = false;
|
|
659
|
+
const uniqueComponents = new Set();
|
|
660
|
+
|
|
661
|
+
// First, collect all existing components that should be preserved
|
|
662
|
+
const existingComponents = importList
|
|
663
|
+
.split(',')
|
|
664
|
+
.map(comp => comp.trim())
|
|
665
|
+
.filter(comp => {
|
|
666
|
+
// Skip empty components
|
|
667
|
+
if (!comp) return false;
|
|
668
|
+
|
|
669
|
+
// Keep components that are:
|
|
670
|
+
// 1. Not in the migration mapping (old components), OR
|
|
671
|
+
// 2. Are the target components (new beta components)
|
|
672
|
+
const isOldComponent = Object.keys(COMPONENT_MAPPING).includes(comp);
|
|
673
|
+
const isTargetComponent = Object.values(COMPONENT_MAPPING).some(
|
|
674
|
+
mapping => mapping.component === comp,
|
|
675
|
+
);
|
|
676
|
+
|
|
677
|
+
return !isOldComponent || isTargetComponent;
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
// Then, update components that need migration
|
|
681
|
+
Object.entries(COMPONENT_MAPPING).forEach(([oldComponent, mapping]) => {
|
|
682
|
+
const componentRegex = new RegExp(`\\b${oldComponent}\\b`, 'g');
|
|
683
|
+
if (componentRegex.test(updatedImportList)) {
|
|
684
|
+
updatedImportList = updatedImportList.replace(
|
|
685
|
+
componentRegex,
|
|
686
|
+
mapping.component,
|
|
687
|
+
);
|
|
688
|
+
uniqueComponents.add(mapping.component);
|
|
689
|
+
hasChanges = true;
|
|
690
|
+
}
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
if (hasChanges) {
|
|
694
|
+
changes++;
|
|
695
|
+
// Combine existing components with migrated components
|
|
696
|
+
const allComponents = [
|
|
697
|
+
...existingComponents,
|
|
698
|
+
...Array.from(uniqueComponents),
|
|
699
|
+
];
|
|
700
|
+
|
|
701
|
+
// Filter out any empty components and deduplicate
|
|
702
|
+
const finalComponents = allComponents
|
|
703
|
+
.filter(comp => comp && comp.trim())
|
|
704
|
+
.filter((comp, index, arr) => arr.indexOf(comp) === index); // Remove duplicates
|
|
705
|
+
|
|
706
|
+
const finalImportList = finalComponents.join(', ');
|
|
707
|
+
return `import {${finalImportList}} from '${BETA_IMPORT}'`;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
return match;
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
return { content: updatedContent, changes };
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// Update component usage with better prop handling
|
|
717
|
+
function updateComponents(content) {
|
|
718
|
+
let updatedContent = content;
|
|
719
|
+
let changes = 0;
|
|
720
|
+
let warnings = [];
|
|
721
|
+
|
|
722
|
+
Object.entries(COMPONENT_MAPPING).forEach(([oldComponent, mapping]) => {
|
|
723
|
+
// More robust regex to handle complex JSX
|
|
724
|
+
const componentRegex = new RegExp(`<${oldComponent}(\\s+[^>]*?)?>`, 'g');
|
|
725
|
+
|
|
726
|
+
updatedContent = updatedContent.replace(
|
|
727
|
+
componentRegex,
|
|
728
|
+
(match, propsString) => {
|
|
729
|
+
changes++;
|
|
730
|
+
|
|
731
|
+
// Parse existing props
|
|
732
|
+
const {
|
|
733
|
+
props: existingProps,
|
|
734
|
+
warnings: parseWarnings,
|
|
735
|
+
spreadProps,
|
|
736
|
+
originalSyntax,
|
|
737
|
+
} = parseJSXProps(propsString);
|
|
738
|
+
warnings.push(...parseWarnings);
|
|
739
|
+
|
|
740
|
+
// Migrate props
|
|
741
|
+
const { props: migratedProps, warnings: migrateWarnings } =
|
|
742
|
+
migrateProps(existingProps, oldComponent);
|
|
743
|
+
warnings.push(...migrateWarnings);
|
|
744
|
+
|
|
745
|
+
// Build new props from mapping
|
|
746
|
+
const newProps = { ...migratedProps };
|
|
747
|
+
|
|
748
|
+
// Add mapping props (but don't override existing ones)
|
|
749
|
+
Object.entries(mapping).forEach(([key, value]) => {
|
|
750
|
+
if (key !== 'component' && !newProps[key]) {
|
|
751
|
+
newProps[key] = value;
|
|
752
|
+
}
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
// Handle Heading components
|
|
756
|
+
if (mapping.component === 'Heading') {
|
|
757
|
+
// Preserve existing 'as' prop if it exists, otherwise use mapping default
|
|
758
|
+
const asValue = existingProps.as || mapping.as;
|
|
759
|
+
// Preserve existing 'variant' prop if it exists, otherwise use mapping default
|
|
760
|
+
const variantValue = existingProps.variant || mapping.variant;
|
|
761
|
+
|
|
762
|
+
// Remove as and variant from props since we'll add them separately
|
|
763
|
+
delete newProps.as;
|
|
764
|
+
delete newProps.variant;
|
|
765
|
+
|
|
766
|
+
// Ensure mapping props come first
|
|
767
|
+
const orderedProps = {};
|
|
768
|
+
if (newProps.spacing) {
|
|
769
|
+
orderedProps.spacing = newProps.spacing;
|
|
770
|
+
delete newProps.spacing;
|
|
771
|
+
}
|
|
772
|
+
Object.assign(orderedProps, newProps);
|
|
773
|
+
|
|
774
|
+
const propsString = propsToString(orderedProps, originalSyntax);
|
|
775
|
+
const spreadPropsString =
|
|
776
|
+
spreadProps.length > 0 ? ` {...${spreadProps.join(', ...')}}` : '';
|
|
777
|
+
return `<Heading as="${asValue}" variant="${variantValue}"${propsString}${spreadPropsString}>`;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// Handle other components
|
|
781
|
+
const componentName = mapping.component;
|
|
782
|
+
|
|
783
|
+
// Remove mapping props from newProps since they're already set
|
|
784
|
+
Object.keys(mapping).forEach(key => {
|
|
785
|
+
if (key !== 'component') {
|
|
786
|
+
delete newProps[key];
|
|
787
|
+
}
|
|
788
|
+
});
|
|
789
|
+
|
|
790
|
+
// Add mapping props in the correct order
|
|
791
|
+
const finalProps = {};
|
|
792
|
+
Object.entries(mapping).forEach(([key, value]) => {
|
|
793
|
+
if (key !== 'component') {
|
|
794
|
+
finalProps[key] = value;
|
|
795
|
+
}
|
|
796
|
+
});
|
|
797
|
+
Object.assign(finalProps, newProps);
|
|
798
|
+
|
|
799
|
+
const otherPropsString = propsToString(finalProps, originalSyntax);
|
|
800
|
+
const spreadPropsString =
|
|
801
|
+
spreadProps.length > 0 ? ` {...${spreadProps.join(', ...')}}` : '';
|
|
802
|
+
return `<${componentName}${otherPropsString}${spreadPropsString}>`;
|
|
803
|
+
},
|
|
804
|
+
);
|
|
805
|
+
|
|
806
|
+
// Update closing tags
|
|
807
|
+
const closingTagRegex = new RegExp(`</${oldComponent}>`, 'g');
|
|
808
|
+
const componentName = mapping.component;
|
|
809
|
+
updatedContent = updatedContent.replace(
|
|
810
|
+
closingTagRegex,
|
|
811
|
+
`</${componentName}>`,
|
|
812
|
+
);
|
|
813
|
+
});
|
|
814
|
+
|
|
815
|
+
return { content: updatedContent, changes, warnings };
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
/**
|
|
819
|
+
* Find files matching the given pattern in allowed directories
|
|
820
|
+
*
|
|
821
|
+
* This function uses efficient glob patterns and data structures:
|
|
822
|
+
* - Single glob call with brace expansion instead of multiple calls
|
|
823
|
+
* - Set-based extension filtering for O(1) lookups
|
|
824
|
+
* - No array concatenation in loops
|
|
825
|
+
*
|
|
826
|
+
* @param {string} pattern - Glob pattern to match (e.g., '*.{ts,tsx,js,jsx}')
|
|
827
|
+
* @returns {string[]} Array of matching file paths
|
|
828
|
+
*/
|
|
829
|
+
function findFiles(pattern) {
|
|
830
|
+
const allFiles = [];
|
|
831
|
+
|
|
832
|
+
// Process directory patterns
|
|
833
|
+
const directoryPatterns = ALLOWED_DIRECTORIES.filter(dir =>
|
|
834
|
+
dir.includes('**'),
|
|
835
|
+
);
|
|
836
|
+
const filePatterns = ALLOWED_DIRECTORIES.filter(dir => !dir.includes('**'));
|
|
837
|
+
|
|
838
|
+
// Handle directory patterns (e.g., src/**, app/**)
|
|
839
|
+
if (directoryPatterns.length > 0) {
|
|
840
|
+
const combinedDirPattern = `{${directoryPatterns.join(',')}}/${pattern}`;
|
|
841
|
+
const dirFiles = glob.sync(combinedDirPattern, {
|
|
842
|
+
ignore: BLOCKED_DIRECTORIES,
|
|
843
|
+
nodir: true,
|
|
844
|
+
absolute: false,
|
|
845
|
+
});
|
|
846
|
+
allFiles.push(...dirFiles);
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// Handle file patterns (e.g., *.jsx, *.tsx)
|
|
850
|
+
filePatterns.forEach(filePattern => {
|
|
851
|
+
const files = glob.sync(filePattern, {
|
|
852
|
+
ignore: BLOCKED_DIRECTORIES,
|
|
853
|
+
nodir: true,
|
|
854
|
+
absolute: false,
|
|
855
|
+
});
|
|
856
|
+
allFiles.push(...files);
|
|
857
|
+
});
|
|
858
|
+
|
|
859
|
+
// Use Set for efficient deduplication and filtering
|
|
860
|
+
const fileExtensions = new Set([
|
|
861
|
+
'.ts',
|
|
862
|
+
'.tsx',
|
|
863
|
+
'.js',
|
|
864
|
+
'.jsx',
|
|
865
|
+
'.scss',
|
|
866
|
+
'.css',
|
|
867
|
+
]);
|
|
868
|
+
|
|
869
|
+
const uniqueFiles = allFiles.filter(file => {
|
|
870
|
+
const ext = path.extname(file).toLowerCase();
|
|
871
|
+
return fileExtensions.has(ext);
|
|
872
|
+
});
|
|
873
|
+
|
|
874
|
+
return uniqueFiles;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
function updateImportsAndComponents(content) {
|
|
878
|
+
let updatedContent = content;
|
|
879
|
+
let changes = 0;
|
|
880
|
+
let warnings = [];
|
|
881
|
+
|
|
882
|
+
// Update both imports and components
|
|
883
|
+
const { content: newContent, changes: importChanges } =
|
|
884
|
+
updateImports(content);
|
|
885
|
+
const {
|
|
886
|
+
content: finalContent,
|
|
887
|
+
changes: componentChanges,
|
|
888
|
+
warnings: componentWarnings,
|
|
889
|
+
} = updateComponents(newContent);
|
|
890
|
+
updatedContent = finalContent;
|
|
891
|
+
changes = importChanges + componentChanges;
|
|
892
|
+
warnings = componentWarnings;
|
|
893
|
+
|
|
894
|
+
return { content: updatedContent, changes, warnings };
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
function generateMigrationReport(files, isDryRun = false) {
|
|
898
|
+
const report = {
|
|
899
|
+
strategy: 'complete',
|
|
900
|
+
totalFiles: files.length,
|
|
901
|
+
migratedFiles: 0,
|
|
902
|
+
totalChanges: 0,
|
|
903
|
+
totalWarnings: 0,
|
|
904
|
+
files: [],
|
|
905
|
+
warnings: [],
|
|
906
|
+
isDryRun,
|
|
907
|
+
};
|
|
908
|
+
|
|
909
|
+
files.forEach(file => {
|
|
910
|
+
try {
|
|
911
|
+
const content = fs.readFileSync(file, 'utf8');
|
|
912
|
+
|
|
913
|
+
// Analyze file for problematic patterns BEFORE migration
|
|
914
|
+
const fileAnalysis = analyzeFile(file, content);
|
|
915
|
+
|
|
916
|
+
const {
|
|
917
|
+
content: updatedContent,
|
|
918
|
+
changes,
|
|
919
|
+
warnings,
|
|
920
|
+
} = updateImportsAndComponents(content);
|
|
921
|
+
|
|
922
|
+
// Combine migration warnings with file analysis warnings
|
|
923
|
+
const allWarnings = [...warnings, ...fileAnalysis.warnings];
|
|
924
|
+
|
|
925
|
+
if (changes > 0 || fileAnalysis.warnings.length > 0) {
|
|
926
|
+
if (!isDryRun) {
|
|
927
|
+
fs.writeFileSync(file, updatedContent, 'utf8');
|
|
928
|
+
}
|
|
929
|
+
if (changes > 0) {
|
|
930
|
+
report.migratedFiles++;
|
|
931
|
+
report.totalChanges += changes;
|
|
932
|
+
}
|
|
933
|
+
report.totalWarnings += allWarnings.length;
|
|
934
|
+
report.files.push({ file, changes, warnings: allWarnings });
|
|
935
|
+
report.warnings.push(
|
|
936
|
+
...allWarnings.map(warning => `${file}: ${warning}`),
|
|
937
|
+
);
|
|
938
|
+
}
|
|
939
|
+
} catch (error) {
|
|
940
|
+
report.warnings.push(`${file}: Error processing file - ${error.message}`);
|
|
941
|
+
}
|
|
942
|
+
});
|
|
943
|
+
|
|
944
|
+
return report;
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
function printReport(report) {
|
|
948
|
+
console.log('\nš Migration Report');
|
|
949
|
+
console.log('==================');
|
|
950
|
+
console.log(`Strategy: ${report.strategy}`);
|
|
951
|
+
console.log(`Total files scanned: ${report.totalFiles}`);
|
|
952
|
+
console.log(`Files migrated: ${report.migratedFiles}`);
|
|
953
|
+
console.log(`Total changes: ${report.totalChanges}`);
|
|
954
|
+
console.log(`Total warnings: ${report.totalWarnings}`);
|
|
955
|
+
|
|
956
|
+
if (report.files.length > 0) {
|
|
957
|
+
console.log('\nMigrated files:');
|
|
958
|
+
report.files.forEach(({ file, changes, warnings }) => {
|
|
959
|
+
console.log(
|
|
960
|
+
` ā
${file} (${changes} changes${
|
|
961
|
+
warnings.length > 0 ? `, ${warnings.length} warnings` : ''
|
|
962
|
+
})`,
|
|
963
|
+
);
|
|
964
|
+
});
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
if (report.warnings.length > 0) {
|
|
968
|
+
console.log('\nā ļø Warnings:');
|
|
969
|
+
|
|
970
|
+
// Group warnings by type
|
|
971
|
+
const marginWarnings = report.warnings.filter(w => w.includes('Migrated'));
|
|
972
|
+
const semanticWarnings = report.warnings.filter(w =>
|
|
973
|
+
w.includes('expected'),
|
|
974
|
+
);
|
|
975
|
+
const conflictWarnings = report.warnings.filter(w =>
|
|
976
|
+
w.includes('check for conflicts'),
|
|
977
|
+
);
|
|
978
|
+
|
|
979
|
+
// New warning types from file analysis
|
|
980
|
+
const styleConflictWarnings = report.warnings.filter(
|
|
981
|
+
w => w.includes('style conflicts') || w.includes('style and margin'),
|
|
982
|
+
);
|
|
983
|
+
const nestedTypographyWarnings = report.warnings.filter(w =>
|
|
984
|
+
w.includes('nested typography'),
|
|
985
|
+
);
|
|
986
|
+
const accessibilityWarnings = report.warnings.filter(
|
|
987
|
+
w => w.includes('missing as prop') || w.includes('accessibility'),
|
|
988
|
+
);
|
|
989
|
+
const semanticMismatchWarnings = report.warnings.filter(w =>
|
|
990
|
+
w.includes('semantic mismatch'),
|
|
991
|
+
);
|
|
992
|
+
|
|
993
|
+
if (marginWarnings.length > 0) {
|
|
994
|
+
console.log(
|
|
995
|
+
`\n š Margin ā Spacing Migrations (${marginWarnings.length}):`,
|
|
996
|
+
);
|
|
997
|
+
// Show first 5 warnings, then summarize the rest
|
|
998
|
+
marginWarnings
|
|
999
|
+
.slice(0, 5)
|
|
1000
|
+
.forEach(warning => console.log(` ${warning}`));
|
|
1001
|
+
if (marginWarnings.length > 5) {
|
|
1002
|
+
console.log(
|
|
1003
|
+
` ... and ${marginWarnings.length - 5} more similar warnings`,
|
|
1004
|
+
);
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
if (semanticWarnings.length > 0) {
|
|
1009
|
+
console.log(`\n šÆ Semantic HTML Issues (${semanticWarnings.length}):`);
|
|
1010
|
+
// Show first 5 warnings, then summarize the rest
|
|
1011
|
+
semanticWarnings
|
|
1012
|
+
.slice(0, 5)
|
|
1013
|
+
.forEach(warning => console.log(` ${warning}`));
|
|
1014
|
+
if (semanticWarnings.length > 5) {
|
|
1015
|
+
console.log(
|
|
1016
|
+
` ... and ${semanticWarnings.length - 5} more similar warnings`,
|
|
1017
|
+
);
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
if (conflictWarnings.length > 0) {
|
|
1022
|
+
console.log(`\n šØ Style Conflicts (${conflictWarnings.length}):`);
|
|
1023
|
+
// Show first 5 warnings, then summarize the rest
|
|
1024
|
+
conflictWarnings
|
|
1025
|
+
.slice(0, 5)
|
|
1026
|
+
.forEach(warning => console.log(` ${warning}`));
|
|
1027
|
+
if (conflictWarnings.length > 5) {
|
|
1028
|
+
console.log(
|
|
1029
|
+
` ... and ${conflictWarnings.length - 5} more similar warnings`,
|
|
1030
|
+
);
|
|
1031
|
+
}
|
|
1032
|
+
console.log(` ā Review these components for styling conflicts`);
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
// Display new warning types
|
|
1036
|
+
if (styleConflictWarnings.length > 0) {
|
|
1037
|
+
console.log(
|
|
1038
|
+
`\n šØ Style + Margin Conflicts (${styleConflictWarnings.length}):`,
|
|
1039
|
+
);
|
|
1040
|
+
styleConflictWarnings
|
|
1041
|
+
.slice(0, 5)
|
|
1042
|
+
.forEach(warning => console.log(` ${warning}`));
|
|
1043
|
+
if (styleConflictWarnings.length > 5) {
|
|
1044
|
+
console.log(
|
|
1045
|
+
` ... and ${
|
|
1046
|
+
styleConflictWarnings.length - 5
|
|
1047
|
+
} more similar warnings`,
|
|
1048
|
+
);
|
|
1049
|
+
}
|
|
1050
|
+
console.log(` ā Remove margin prop when using inline styles`);
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
if (nestedTypographyWarnings.length > 0) {
|
|
1054
|
+
console.log(
|
|
1055
|
+
`\n š« Nested Typography (${nestedTypographyWarnings.length}):`,
|
|
1056
|
+
);
|
|
1057
|
+
nestedTypographyWarnings
|
|
1058
|
+
.slice(0, 5)
|
|
1059
|
+
.forEach(warning => console.log(` ${warning}`));
|
|
1060
|
+
if (nestedTypographyWarnings.length > 5) {
|
|
1061
|
+
console.log(
|
|
1062
|
+
` ... and ${
|
|
1063
|
+
nestedTypographyWarnings.length - 5
|
|
1064
|
+
} more similar warnings`,
|
|
1065
|
+
);
|
|
1066
|
+
}
|
|
1067
|
+
console.log(
|
|
1068
|
+
` ā Use spans or other inline elements instead of nested Text components`,
|
|
1069
|
+
);
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
if (accessibilityWarnings.length > 0) {
|
|
1073
|
+
console.log(
|
|
1074
|
+
`\n āæ Accessibility Issues (${accessibilityWarnings.length}):`,
|
|
1075
|
+
);
|
|
1076
|
+
accessibilityWarnings
|
|
1077
|
+
.slice(0, 5)
|
|
1078
|
+
.forEach(warning => console.log(` ${warning}`));
|
|
1079
|
+
if (accessibilityWarnings.length > 5) {
|
|
1080
|
+
console.log(
|
|
1081
|
+
` ... and ${
|
|
1082
|
+
accessibilityWarnings.length - 5
|
|
1083
|
+
} more similar warnings`,
|
|
1084
|
+
);
|
|
1085
|
+
}
|
|
1086
|
+
console.log(
|
|
1087
|
+
` ā Add 'as' prop to Heading components for proper semantic HTML`,
|
|
1088
|
+
);
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
if (semanticMismatchWarnings.length > 0) {
|
|
1092
|
+
console.log(
|
|
1093
|
+
`\n š Semantic Mismatches (${semanticMismatchWarnings.length}):`,
|
|
1094
|
+
);
|
|
1095
|
+
semanticMismatchWarnings
|
|
1096
|
+
.slice(0, 5)
|
|
1097
|
+
.forEach(warning => console.log(` ${warning}`));
|
|
1098
|
+
if (semanticMismatchWarnings.length > 5) {
|
|
1099
|
+
console.log(
|
|
1100
|
+
` ... and ${
|
|
1101
|
+
semanticMismatchWarnings.length - 5
|
|
1102
|
+
} more similar warnings`,
|
|
1103
|
+
);
|
|
1104
|
+
}
|
|
1105
|
+
console.log(` ā Review heading level and variant combinations`);
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
console.log('\nš Summary:');
|
|
1109
|
+
if (marginWarnings.length > 0)
|
|
1110
|
+
console.log(
|
|
1111
|
+
` ⢠${marginWarnings.length} margin props migrated to spacing`,
|
|
1112
|
+
);
|
|
1113
|
+
if (semanticWarnings.length > 0)
|
|
1114
|
+
console.log(
|
|
1115
|
+
` ⢠${semanticWarnings.length} semantic HTML issues need review`,
|
|
1116
|
+
);
|
|
1117
|
+
if (conflictWarnings.length > 0)
|
|
1118
|
+
console.log(
|
|
1119
|
+
` ⢠${conflictWarnings.length} style conflicts need manual review`,
|
|
1120
|
+
);
|
|
1121
|
+
if (styleConflictWarnings.length > 0)
|
|
1122
|
+
console.log(
|
|
1123
|
+
` ⢠${styleConflictWarnings.length} style + margin conflicts detected`,
|
|
1124
|
+
);
|
|
1125
|
+
if (nestedTypographyWarnings.length > 0)
|
|
1126
|
+
console.log(
|
|
1127
|
+
` ⢠${nestedTypographyWarnings.length} nested typography components found`,
|
|
1128
|
+
);
|
|
1129
|
+
if (accessibilityWarnings.length > 0)
|
|
1130
|
+
console.log(
|
|
1131
|
+
` ⢠${accessibilityWarnings.length} accessibility issues need attention`,
|
|
1132
|
+
);
|
|
1133
|
+
if (semanticMismatchWarnings.length > 0)
|
|
1134
|
+
console.log(
|
|
1135
|
+
` ⢠${semanticMismatchWarnings.length} semantic mismatches detected`,
|
|
1136
|
+
);
|
|
1137
|
+
|
|
1138
|
+
// Add helpful note about warning limits
|
|
1139
|
+
if (report.warnings.length > 15) {
|
|
1140
|
+
console.log(
|
|
1141
|
+
'\nš” Note: Only showing first 5 warnings of each type to avoid overwhelming output.',
|
|
1142
|
+
);
|
|
1143
|
+
console.log(
|
|
1144
|
+
' All warnings are still logged in the migration report above.',
|
|
1145
|
+
);
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
function showNextSteps() {
|
|
1151
|
+
console.log('\nš Next Steps');
|
|
1152
|
+
console.log('=============');
|
|
1153
|
+
|
|
1154
|
+
console.log('1. š§Ŗ Test your application thoroughly');
|
|
1155
|
+
console.log('2. š Review and adjust any component props if needed');
|
|
1156
|
+
console.log('3. š Read the migration guide on our website');
|
|
1157
|
+
|
|
1158
|
+
console.log('\nā ļø Important Notes:');
|
|
1159
|
+
console.log('- Check warnings above for potential issues');
|
|
1160
|
+
console.log('- Review migrated components for prop conflicts');
|
|
1161
|
+
console.log('- Test thoroughly, especially components with custom styling');
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
function main() {
|
|
1165
|
+
// Show help if requested
|
|
1166
|
+
if (process.argv.includes('--help') || process.argv.includes('-h')) {
|
|
1167
|
+
console.log('šØ Typography Migration Script');
|
|
1168
|
+
console.log('==============================');
|
|
1169
|
+
console.log('');
|
|
1170
|
+
console.log('Usage:');
|
|
1171
|
+
console.log(' # From npm package (recommended)');
|
|
1172
|
+
console.log(' npx @entur/typography@latest migrate [options]');
|
|
1173
|
+
console.log(' yarn dlx @entur/typography@latest migrate [options]');
|
|
1174
|
+
console.log('');
|
|
1175
|
+
console.log(' # Direct execution (requires glob package)');
|
|
1176
|
+
console.log(' node scripts/migrate-typography.js [options]');
|
|
1177
|
+
console.log('');
|
|
1178
|
+
console.log(' # Local development');
|
|
1179
|
+
console.log(' npm run migrate');
|
|
1180
|
+
console.log('');
|
|
1181
|
+
console.log('Options:');
|
|
1182
|
+
console.log(
|
|
1183
|
+
' --dry-run Show what would be changed without modifying files',
|
|
1184
|
+
);
|
|
1185
|
+
console.log(' --help, -h Show this help message');
|
|
1186
|
+
console.log('');
|
|
1187
|
+
console.log('Migration Mode:');
|
|
1188
|
+
console.log(' š Complete Mode: Updates everything');
|
|
1189
|
+
console.log(' - Replaces old components with beta components');
|
|
1190
|
+
console.log(' - Heading1-6 ā Heading with as/variant props');
|
|
1191
|
+
console.log(' - Text components ā Text with variant props');
|
|
1192
|
+
console.log(' - Link ā LinkBeta, Blockquote ā BlockquoteBeta');
|
|
1193
|
+
console.log(
|
|
1194
|
+
' - Lists ā UnorderedListBeta, NumberedListBeta, ListItemBeta',
|
|
1195
|
+
);
|
|
1196
|
+
console.log(' - May require prop/styling updates');
|
|
1197
|
+
console.log(' - Test thoroughly after migration');
|
|
1198
|
+
console.log('');
|
|
1199
|
+
console.log('Examples:');
|
|
1200
|
+
console.log(' # See what would be changed');
|
|
1201
|
+
console.log(' npx @entur/typography@latest migrate --dry-run');
|
|
1202
|
+
console.log('');
|
|
1203
|
+
console.log(' # Complete migration: update everything (default)');
|
|
1204
|
+
console.log(' npx @entur/typography@latest migrate');
|
|
1205
|
+
|
|
1206
|
+
console.log('Environment Variables:');
|
|
1207
|
+
console.log(
|
|
1208
|
+
' TYPOGRAPHY_MIGRATION_DIRS Comma-separated list of directories to scan',
|
|
1209
|
+
);
|
|
1210
|
+
console.log(' Example: "src/**,app/**"');
|
|
1211
|
+
console.log('');
|
|
1212
|
+
console.log('šÆ Customizing Scan Directories:');
|
|
1213
|
+
console.log(' Option 1: Edit MIGRATION_FOLDERS in the script (EASIEST)');
|
|
1214
|
+
console.log(
|
|
1215
|
+
' Open the script and find the "MIGRATION FOLDERS CONFIGURATION" section',
|
|
1216
|
+
);
|
|
1217
|
+
console.log(' Add/remove folder patterns between the š and š markers');
|
|
1218
|
+
console.log(' Examples: "src/**", "app/**", "packages/my-app/**"');
|
|
1219
|
+
console.log('');
|
|
1220
|
+
console.log(' Option 2: Set environment variable');
|
|
1221
|
+
console.log(
|
|
1222
|
+
' export TYPOGRAPHY_MIGRATION_DIRS="src/**,app/**,components/**"',
|
|
1223
|
+
);
|
|
1224
|
+
console.log(' node scripts/migrate-typography.js');
|
|
1225
|
+
console.log('');
|
|
1226
|
+
console.log('Security Features:');
|
|
1227
|
+
console.log(' - Only scans allowed directories (src/**, app/**, etc.)');
|
|
1228
|
+
console.log(' - Never scans node_modules, dist, build, .git, etc.)');
|
|
1229
|
+
console.log(' - Dry-run mode for safe testing');
|
|
1230
|
+
console.log('');
|
|
1231
|
+
process.exit(0);
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
console.log('šØ Typography Migration Script');
|
|
1235
|
+
console.log('==============================');
|
|
1236
|
+
console.log('');
|
|
1237
|
+
console.log(
|
|
1238
|
+
'This script helps you migrate from old typography to new beta typography.',
|
|
1239
|
+
);
|
|
1240
|
+
console.log('');
|
|
1241
|
+
|
|
1242
|
+
// Find files to migrate - use a single efficient pattern
|
|
1243
|
+
const allFiles = findFiles('*.{ts,tsx,js,jsx,scss,css}');
|
|
1244
|
+
|
|
1245
|
+
console.log(`Found ${allFiles.length} files to scan for typography imports.`);
|
|
1246
|
+
console.log('');
|
|
1247
|
+
|
|
1248
|
+
// Security check
|
|
1249
|
+
console.log('š Security: Only scanning allowed directories:');
|
|
1250
|
+
ALLOWED_DIRECTORIES.forEach(dir => {
|
|
1251
|
+
console.log(` ā
${dir}`);
|
|
1252
|
+
});
|
|
1253
|
+
console.log('');
|
|
1254
|
+
|
|
1255
|
+
// Safety check
|
|
1256
|
+
if (allFiles.length === 0) {
|
|
1257
|
+
console.log('ā ļø No files found to scan. This might mean:');
|
|
1258
|
+
console.log(" - You're not in the right directory");
|
|
1259
|
+
console.log(" - Your project structure doesn't match the allow-list");
|
|
1260
|
+
console.log(' - You need to run this from your project root');
|
|
1261
|
+
console.log('');
|
|
1262
|
+
console.log('Allowed directory patterns:');
|
|
1263
|
+
ALLOWED_DIRECTORIES.forEach(dir => console.log(` ${dir}`));
|
|
1264
|
+
process.exit(1);
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
console.log('š Files will be scanned in these locations:');
|
|
1268
|
+
const scannedDirs = [
|
|
1269
|
+
...new Set(allFiles.map(file => path.dirname(file))),
|
|
1270
|
+
].slice(0, 10);
|
|
1271
|
+
|
|
1272
|
+
// Show relative paths safely
|
|
1273
|
+
scannedDirs.forEach(dir => {
|
|
1274
|
+
// Ensure we don't show absolute paths
|
|
1275
|
+
const safeDir = path.isAbsolute(dir)
|
|
1276
|
+
? path.relative(process.cwd(), dir)
|
|
1277
|
+
: dir;
|
|
1278
|
+
console.log(` š ${safeDir}`);
|
|
1279
|
+
});
|
|
1280
|
+
|
|
1281
|
+
if (allFiles.length > 10) {
|
|
1282
|
+
console.log(` ... and ${allFiles.length - 10} more files`);
|
|
1283
|
+
}
|
|
1284
|
+
console.log('');
|
|
1285
|
+
|
|
1286
|
+
// Parse command line options
|
|
1287
|
+
const isDryRun = process.argv.includes('--dry-run');
|
|
1288
|
+
|
|
1289
|
+
if (isDryRun) {
|
|
1290
|
+
console.log('š DRY RUN MODE: No files will be modified');
|
|
1291
|
+
console.log('');
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
console.log('š COMPLETE MIGRATION: Updating imports + component usage');
|
|
1295
|
+
console.log('ā ļø WARNING: This will modify your component usage!');
|
|
1296
|
+
console.log(' - Old components will be replaced with beta components');
|
|
1297
|
+
console.log(
|
|
1298
|
+
' - Link ā LinkBeta, Blockquote ā BlockquoteBeta, Lists ā ListBeta components',
|
|
1299
|
+
);
|
|
1300
|
+
console.log(
|
|
1301
|
+
' - List components ā UnorderedListBeta, NumberedListBeta, ListItemBeta',
|
|
1302
|
+
);
|
|
1303
|
+
console.log(' - You may need to update props and styling');
|
|
1304
|
+
console.log(' - Test thoroughly after migration');
|
|
1305
|
+
|
|
1306
|
+
console.log('');
|
|
1307
|
+
|
|
1308
|
+
// Perform migration
|
|
1309
|
+
const report = generateMigrationReport(allFiles, isDryRun);
|
|
1310
|
+
printReport(report);
|
|
1311
|
+
showNextSteps();
|
|
1312
|
+
|
|
1313
|
+
console.log('\nšÆ Migration complete!');
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
if (require.main === module) {
|
|
1317
|
+
main();
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
module.exports = {
|
|
1321
|
+
updateImportsAndComponents,
|
|
1322
|
+
generateMigrationReport,
|
|
1323
|
+
COMPONENT_MAPPING,
|
|
1324
|
+
PROPS_MAPPING,
|
|
1325
|
+
};
|