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