emily-css 1.1.1 → 1.2.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/src/migrate.js ADDED
@@ -0,0 +1,882 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const fg = require('fast-glob');
4
+
5
+ const { extractClassNames, getAllFiles } = require('./purge.js');
6
+ const { generateManifest } = require('./manifest.js');
7
+ const { normaliseClassForManifest, suggestClassName } = require('./doctor.js');
8
+ const { DEFAULT_EXTENSIONS } = require('./constants.js');
9
+ const MIGRATION_DEFAULT_EXTENSIONS = [...DEFAULT_EXTENSIONS, '.mdx'];
10
+
11
+ const TAILWIND_MAPPINGS = {
12
+ 'text-gray-900': {
13
+ replacement: 'text-neutral-90',
14
+ aliases: ['text-zinc-900', 'text-slate-900', 'text-stone-900', 'text-neutral-900'],
15
+ deprecated: false,
16
+ metadata: { category: 'colour' },
17
+ },
18
+ 'text-gray-700': {
19
+ replacement: 'text-neutral-70',
20
+ aliases: ['text-zinc-700', 'text-slate-700', 'text-stone-700', 'text-neutral-700'],
21
+ deprecated: false,
22
+ metadata: { category: 'colour' },
23
+ },
24
+ 'text-gray-500': {
25
+ replacement: 'text-neutral-50',
26
+ aliases: ['text-zinc-500', 'text-slate-500', 'text-stone-500', 'text-neutral-500'],
27
+ deprecated: false,
28
+ metadata: { category: 'colour' },
29
+ },
30
+ 'bg-gray-100': {
31
+ replacement: 'bg-neutral-10',
32
+ aliases: ['bg-zinc-100', 'bg-slate-100', 'bg-stone-100', 'bg-neutral-100'],
33
+ deprecated: false,
34
+ metadata: { category: 'background' },
35
+ },
36
+ 'bg-gray-900': {
37
+ replacement: 'bg-neutral-90',
38
+ aliases: ['bg-zinc-900', 'bg-slate-900', 'bg-stone-900', 'bg-neutral-900'],
39
+ deprecated: false,
40
+ metadata: { category: 'background' },
41
+ },
42
+ 'rounded-md': {
43
+ replacement: 'rounded-md',
44
+ aliases: [],
45
+ deprecated: false,
46
+ metadata: { category: 'radius' },
47
+ },
48
+ 'rounded-lg': {
49
+ replacement: 'rounded-lg',
50
+ aliases: [],
51
+ deprecated: false,
52
+ metadata: { category: 'radius' },
53
+ },
54
+ 'shadow-md': {
55
+ replacement: 'shadow-md',
56
+ aliases: [],
57
+ deprecated: false,
58
+ metadata: { category: 'shadow' },
59
+ },
60
+ flex: {
61
+ replacement: 'flex',
62
+ aliases: [],
63
+ deprecated: false,
64
+ metadata: { category: 'layout' },
65
+ },
66
+ grid: {
67
+ replacement: 'grid',
68
+ aliases: [],
69
+ deprecated: false,
70
+ metadata: { category: 'layout' },
71
+ },
72
+ hidden: {
73
+ replacement: 'hidden',
74
+ aliases: [],
75
+ deprecated: false,
76
+ metadata: { category: 'layout' },
77
+ },
78
+ block: {
79
+ replacement: 'block',
80
+ aliases: [],
81
+ deprecated: false,
82
+ metadata: { category: 'layout' },
83
+ },
84
+ 'inline-block': {
85
+ replacement: 'inline-block',
86
+ aliases: [],
87
+ deprecated: false,
88
+ metadata: { category: 'layout' },
89
+ },
90
+ };
91
+
92
+ const MIGRATION_MODE_SEMANTIC = 'semantic';
93
+ const MIGRATION_MODE_IMPORTED_PALETTES = 'imported-palettes';
94
+
95
+ const TAILWIND_COLOUR_UTILITY_PREFIXES = new Set([
96
+ 'text',
97
+ 'bg',
98
+ 'border',
99
+ 'accent',
100
+ 'fill',
101
+ 'stroke',
102
+ 'ring',
103
+ 'outline',
104
+ ]);
105
+
106
+ const TAILWIND_COLOUR_PALETTES = new Set([
107
+ 'slate',
108
+ 'gray',
109
+ 'zinc',
110
+ 'neutral',
111
+ 'stone',
112
+ 'red',
113
+ 'orange',
114
+ 'amber',
115
+ 'yellow',
116
+ 'lime',
117
+ 'green',
118
+ 'emerald',
119
+ 'teal',
120
+ 'cyan',
121
+ 'sky',
122
+ 'blue',
123
+ 'indigo',
124
+ 'violet',
125
+ 'purple',
126
+ 'fuchsia',
127
+ 'pink',
128
+ 'rose',
129
+ ]);
130
+
131
+ const TAILWIND_SHADE_TO_EMILY_SHADE = {
132
+ '50': '5',
133
+ '100': '10',
134
+ '200': '20',
135
+ '300': '30',
136
+ '400': '40',
137
+ '500': '50',
138
+ '600': '60',
139
+ '700': '70',
140
+ '800': '80',
141
+ '900': '90',
142
+ '950': '100',
143
+ };
144
+
145
+ const SINGLE_WORD_UTILITY_ALLOWLIST = new Set([
146
+ 'flex',
147
+ 'grid',
148
+ 'hidden',
149
+ 'block',
150
+ 'inline',
151
+ 'table',
152
+ 'contents',
153
+ 'flow',
154
+ 'container',
155
+ 'relative',
156
+ 'absolute',
157
+ 'fixed',
158
+ 'sticky',
159
+ 'static',
160
+ 'visible',
161
+ 'invisible',
162
+ 'uppercase',
163
+ 'lowercase',
164
+ 'capitalize',
165
+ 'truncate',
166
+ 'antialiased',
167
+ 'italic',
168
+ 'not-italic',
169
+ 'underline',
170
+ 'overline',
171
+ 'line-through',
172
+ ]);
173
+
174
+ const UTILITY_PREFIX_ALLOWLIST = new Set([
175
+ 'bg',
176
+ 'text',
177
+ 'border',
178
+ 'outline',
179
+ 'accent',
180
+ 'fill',
181
+ 'stroke',
182
+ 'ring',
183
+ 'rounded',
184
+ 'shadow',
185
+ 'font',
186
+ 'leading',
187
+ 'tracking',
188
+ 'p',
189
+ 'px',
190
+ 'py',
191
+ 'pt',
192
+ 'pr',
193
+ 'pb',
194
+ 'pl',
195
+ 'm',
196
+ 'mx',
197
+ 'my',
198
+ 'mt',
199
+ 'mr',
200
+ 'mb',
201
+ 'ml',
202
+ 'w',
203
+ 'h',
204
+ 'min-w',
205
+ 'max-w',
206
+ 'min-h',
207
+ 'max-h',
208
+ 'gap',
209
+ 'space',
210
+ 'inset',
211
+ 'top',
212
+ 'right',
213
+ 'bottom',
214
+ 'left',
215
+ 'z',
216
+ 'order',
217
+ 'col',
218
+ 'row',
219
+ 'grid-cols',
220
+ 'grid-rows',
221
+ 'justify',
222
+ 'items',
223
+ 'content',
224
+ 'self',
225
+ 'place',
226
+ 'object',
227
+ 'overflow',
228
+ 'divide',
229
+ 'cursor',
230
+ 'select',
231
+ 'duration',
232
+ 'delay',
233
+ 'ease',
234
+ 'scale',
235
+ 'rotate',
236
+ 'translate',
237
+ 'skew',
238
+ 'origin',
239
+ 'opacity',
240
+ 'basis',
241
+ 'grow',
242
+ 'shrink',
243
+ ]);
244
+
245
+ function hasUtilityLikeSyntax(className) {
246
+ if (!className || typeof className !== 'string') {
247
+ return false;
248
+ }
249
+
250
+ const variantSeparatorIndex = className.lastIndexOf(':');
251
+ if (variantSeparatorIndex !== -1) {
252
+ const baseClass = className.slice(variantSeparatorIndex + 1);
253
+ if (!baseClass) {
254
+ return false;
255
+ }
256
+ return hasUtilityLikeSyntax(baseClass);
257
+ }
258
+
259
+ if (className.startsWith('-')) {
260
+ const baseClass = className.slice(1);
261
+ return baseClass.length > 0 && hasUtilityLikeSyntax(baseClass);
262
+ }
263
+
264
+ if (SINGLE_WORD_UTILITY_ALLOWLIST.has(className)) {
265
+ return true;
266
+ }
267
+
268
+ if (
269
+ hasArbitraryValueSyntax(className) ||
270
+ className.includes('/') ||
271
+ className.includes('.') ||
272
+ className.includes('_') ||
273
+ /\d/.test(className)
274
+ ) {
275
+ return true;
276
+ }
277
+
278
+ const parts = className.split('-').filter(Boolean);
279
+
280
+ if (parts.length >= 2) {
281
+ const first = parts[0];
282
+ const firstTwo = `${parts[0]}-${parts[1]}`;
283
+ return UTILITY_PREFIX_ALLOWLIST.has(first) || UTILITY_PREFIX_ALLOWLIST.has(firstTwo);
284
+ }
285
+
286
+ return false;
287
+ }
288
+
289
+ function getConfigPath(options = {}) {
290
+ return options.configPath || path.join(process.cwd(), 'emily.config.json');
291
+ }
292
+
293
+ function readConfig(options = {}) {
294
+ if (options.config && typeof options.config === 'object') {
295
+ return options.config;
296
+ }
297
+
298
+ const configPath = getConfigPath(options);
299
+ if (!fs.existsSync(configPath)) {
300
+ return null;
301
+ }
302
+
303
+ try {
304
+ return JSON.parse(fs.readFileSync(configPath, 'utf8'));
305
+ } catch (error) {
306
+ return null;
307
+ }
308
+ }
309
+
310
+ function getManifestSettings(config) {
311
+ const manifestConfig = config && config.manifest;
312
+
313
+ if (manifestConfig === true) {
314
+ return { enabled: true, output: 'dist/emily.manifest.json' };
315
+ }
316
+
317
+ if (manifestConfig && typeof manifestConfig === 'object') {
318
+ return {
319
+ enabled: manifestConfig.enabled === true,
320
+ output: manifestConfig.output || 'dist/emily.manifest.json',
321
+ };
322
+ }
323
+
324
+ return { enabled: false, output: 'dist/emily.manifest.json' };
325
+ }
326
+
327
+ function getManifestOutputPath(config, options = {}) {
328
+ if (options.manifestPath) {
329
+ return path.isAbsolute(options.manifestPath)
330
+ ? options.manifestPath
331
+ : path.join(process.cwd(), options.manifestPath);
332
+ }
333
+
334
+ const manifestSettings = getManifestSettings(config || {});
335
+ const outputPath = manifestSettings.output || 'dist/emily.manifest.json';
336
+
337
+ return path.isAbsolute(outputPath)
338
+ ? outputPath
339
+ : path.join(process.cwd(), outputPath);
340
+ }
341
+
342
+ function getFullCssPath(config, options = {}) {
343
+ if (options.cssPath) {
344
+ return path.isAbsolute(options.cssPath)
345
+ ? options.cssPath
346
+ : path.join(process.cwd(), options.cssPath);
347
+ }
348
+
349
+ const outputPath = (config && config.output && config.output.fullCss) || 'dist/emily.css';
350
+ return path.isAbsolute(outputPath)
351
+ ? outputPath
352
+ : path.join(process.cwd(), outputPath);
353
+ }
354
+
355
+ function createEmptyManifest() {
356
+ return {
357
+ version: 'unknown',
358
+ generatedAt: new Date().toISOString(),
359
+ utilities: [],
360
+ };
361
+ }
362
+
363
+ function loadManifest(options = {}) {
364
+ const warnings = [];
365
+
366
+ if (options.manifest && Array.isArray(options.manifest.utilities)) {
367
+ return { manifest: options.manifest, warnings };
368
+ }
369
+
370
+ const config = readConfig(options) || {};
371
+ const manifestPath = getManifestOutputPath(config, options);
372
+
373
+ if (fs.existsSync(manifestPath)) {
374
+ try {
375
+ return {
376
+ manifest: JSON.parse(fs.readFileSync(manifestPath, 'utf8')),
377
+ warnings,
378
+ };
379
+ } catch (error) {
380
+ warnings.push(`Could not parse manifest at ${manifestPath}: ${error.message}`);
381
+ }
382
+ }
383
+
384
+ const fullCssPath = getFullCssPath(config, options);
385
+
386
+ if (fs.existsSync(fullCssPath)) {
387
+ try {
388
+ const css = fs.readFileSync(fullCssPath, 'utf8');
389
+ return {
390
+ manifest: generateManifest(css, config),
391
+ warnings,
392
+ };
393
+ } catch (error) {
394
+ warnings.push(`Could not generate manifest from CSS at ${fullCssPath}: ${error.message}`);
395
+ }
396
+ }
397
+
398
+ warnings.push(
399
+ 'Manifest not found. Migration report will continue without full EmilyCSS support checks.',
400
+ );
401
+
402
+ return { manifest: createEmptyManifest(), warnings };
403
+ }
404
+
405
+ function buildManifestIndexes(manifest) {
406
+ const utilities = Array.isArray(manifest && manifest.utilities) ? manifest.utilities : [];
407
+ const utilitySet = new Set();
408
+ const variantSet = new Set();
409
+
410
+ utilities.forEach((utility) => {
411
+ if (utility && utility.class) {
412
+ utilitySet.add(utility.class);
413
+ }
414
+
415
+ const variants = Array.isArray(utility && utility.variants) ? utility.variants : [];
416
+ variants.forEach((variant) => variantSet.add(variant));
417
+ });
418
+
419
+ return { utilitySet, variantSet };
420
+ }
421
+
422
+ function resolveTailwindMapping(className, mappingTable = TAILWIND_MAPPINGS) {
423
+ if (!className || typeof className !== 'string') {
424
+ return null;
425
+ }
426
+
427
+ if (mappingTable[className]) {
428
+ return {
429
+ source: className,
430
+ ...mappingTable[className],
431
+ };
432
+ }
433
+
434
+ const entries = Object.entries(mappingTable);
435
+
436
+ for (const [sourceClass, entry] of entries) {
437
+ const aliases = Array.isArray(entry.aliases) ? entry.aliases : [];
438
+ if (aliases.includes(className)) {
439
+ return {
440
+ source: sourceClass,
441
+ ...entry,
442
+ matchedAlias: className,
443
+ };
444
+ }
445
+ }
446
+
447
+ return null;
448
+ }
449
+
450
+ function buildVariantClassName(variants, baseClass) {
451
+ if (!Array.isArray(variants) || variants.length === 0) {
452
+ return baseClass;
453
+ }
454
+
455
+ return `${variants.join(':')}:${baseClass}`;
456
+ }
457
+
458
+ function getMigrationMode(options = {}) {
459
+ return options.importColours === true
460
+ ? MIGRATION_MODE_IMPORTED_PALETTES
461
+ : MIGRATION_MODE_SEMANTIC;
462
+ }
463
+
464
+ function mapTailwindPaletteToEmilyPalette(paletteName, mode) {
465
+ if (mode === MIGRATION_MODE_IMPORTED_PALETTES) {
466
+ return paletteName;
467
+ }
468
+
469
+ const semanticPaletteRemap = {
470
+ gray: 'neutral',
471
+ slate: 'neutral',
472
+ zinc: 'neutral',
473
+ stone: 'neutral',
474
+ };
475
+
476
+ return semanticPaletteRemap[paletteName] || paletteName;
477
+ }
478
+
479
+ function parseTailwindColourClass(baseClass, mode) {
480
+ if (!baseClass || typeof baseClass !== 'string') {
481
+ return null;
482
+ }
483
+
484
+ const [baseWithoutOpacity, opacitySuffix] = baseClass.split('/');
485
+ const match = baseWithoutOpacity.match(
486
+ /^([a-z-]+)-([a-z][a-z0-9-]*)-(50|100|200|300|400|500|600|700|800|900|950)$/,
487
+ );
488
+
489
+ if (!match) {
490
+ return null;
491
+ }
492
+
493
+ const utility = match[1];
494
+ const tailwindPalette = match[2];
495
+ const tailwindShade = match[3];
496
+
497
+ if (!TAILWIND_COLOUR_UTILITY_PREFIXES.has(utility)) {
498
+ return null;
499
+ }
500
+ if (!TAILWIND_COLOUR_PALETTES.has(tailwindPalette)) {
501
+ return null;
502
+ }
503
+
504
+ const emilyShade = TAILWIND_SHADE_TO_EMILY_SHADE[tailwindShade];
505
+ if (!emilyShade) {
506
+ return null;
507
+ }
508
+
509
+ const emilyPalette = mapTailwindPaletteToEmilyPalette(tailwindPalette, mode);
510
+ const emilyBaseClass = `${utility}-${emilyPalette}-${emilyShade}`;
511
+
512
+ return {
513
+ utility,
514
+ tailwindPalette,
515
+ tailwindShade,
516
+ opacitySuffix: opacitySuffix || null,
517
+ emilyPalette,
518
+ emilyShade,
519
+ emilyBaseClass,
520
+ };
521
+ }
522
+
523
+ function isSemanticColourMapping(mapping) {
524
+ const category = mapping && mapping.metadata && mapping.metadata.category;
525
+ return category === 'colour' || category === 'background';
526
+ }
527
+
528
+ function hasArbitraryValueSyntax(baseClass) {
529
+ return typeof baseClass === 'string' && /\[[^\]]+\]/.test(baseClass);
530
+ }
531
+
532
+ function buildImportedPalettesConfigBlock(importedPalettes) {
533
+ if (!Array.isArray(importedPalettes) || importedPalettes.length === 0) {
534
+ return null;
535
+ }
536
+
537
+ const grouped = {};
538
+
539
+ importedPalettes.forEach((entry) => {
540
+ if (!grouped[entry.emilyPalette]) {
541
+ grouped[entry.emilyPalette] = new Set();
542
+ }
543
+ grouped[entry.emilyPalette].add(entry.tailwindPalette);
544
+ });
545
+
546
+ const lines = ['importedPalettes: {'];
547
+
548
+ Object.keys(grouped)
549
+ .sort()
550
+ .forEach((emilyPalette) => {
551
+ const sources = Array.from(grouped[emilyPalette]).sort();
552
+ if (sources.length === 1) {
553
+ lines.push(` ${emilyPalette}: "tailwind-${sources[0]}",`);
554
+ } else {
555
+ const aliases = sources.map((source) => `"${source}"`).join(', ');
556
+ lines.push(` ${emilyPalette}: { source: "tailwind", aliases: [${aliases}] },`);
557
+ }
558
+ });
559
+
560
+ lines.push('}');
561
+ return lines.join('\n');
562
+ }
563
+
564
+ function isLikelyUtilityClass(className) {
565
+ if (!className || typeof className !== 'string') return false;
566
+ if (/\s/.test(className)) return false;
567
+ if (className.length > 120) return false;
568
+ if (className.startsWith('--')) return false;
569
+ if (className.startsWith(':')) return false;
570
+ if (className.startsWith('.') || className.startsWith('#') || className.startsWith('@')) return false;
571
+ if (className.endsWith(':')) return false;
572
+ if (className.includes('://')) return false;
573
+ if (/[;()={},`]/.test(className)) return false;
574
+ if (!/[a-zA-Z]/.test(className)) return false;
575
+ if (!/^[a-zA-Z0-9:#_./\-[\]]+$/.test(className)) return false;
576
+ if (/^[a-z]+$/.test(className) && !SINGLE_WORD_UTILITY_ALLOWLIST.has(className)) return false;
577
+ if (!hasUtilityLikeSyntax(className) && !SINGLE_WORD_UTILITY_ALLOWLIST.has(className)) return false;
578
+
579
+ return true;
580
+ }
581
+
582
+ function migrateClasses(input, options = {}) {
583
+ const mode = getMigrationMode(options);
584
+
585
+ const report = {
586
+ found: [],
587
+ supported: [],
588
+ knownTailwind: [],
589
+ unsupported: [],
590
+ arbitraryValueUtilities: [],
591
+ suggestions: [],
592
+ replacements: [],
593
+ importedPaletteMappings: [],
594
+ importedPalettes: [],
595
+ warnings: [],
596
+ };
597
+
598
+ if (typeof input !== 'string') {
599
+ report.warnings.push('migrateClasses expected a string input; received non-string content.');
600
+ return report;
601
+ }
602
+
603
+ const classSet = extractClassNames(input);
604
+ const found = Array.from(classSet).filter(isLikelyUtilityClass);
605
+
606
+ report.found = found;
607
+
608
+ const { manifest, warnings } = loadManifest(options);
609
+ report.warnings.push(...warnings);
610
+
611
+ const { utilitySet, variantSet } = buildManifestIndexes(manifest);
612
+
613
+ for (const className of found) {
614
+ const parsed = normaliseClassForManifest(className);
615
+ const variants = parsed.variants || [];
616
+ const isArbitraryValueClass = hasArbitraryValueSyntax(parsed.baseClass);
617
+ const hasUnknownVariant = variants.some((variant) => !variantSet.has(variant));
618
+ const isSupported = utilitySet.has(parsed.baseClass) && !hasUnknownVariant;
619
+
620
+ if (isSupported) {
621
+ report.supported.push(className);
622
+ }
623
+
624
+ const parsedTailwindColour = parseTailwindColourClass(parsed.baseClass, mode);
625
+ const mapping = resolveTailwindMapping(parsed.baseClass, options.mappingTable || TAILWIND_MAPPINGS);
626
+ const shouldUseImportedPaletteMapping =
627
+ mode === MIGRATION_MODE_IMPORTED_PALETTES && !!parsedTailwindColour;
628
+ const effectiveSemanticMapping =
629
+ shouldUseImportedPaletteMapping && isSemanticColourMapping(mapping) ? null : mapping;
630
+
631
+ if (isArbitraryValueClass) {
632
+ report.arbitraryValueUtilities.push(className);
633
+ if (!isSupported) {
634
+ report.unsupported.push(className);
635
+ }
636
+ continue;
637
+ }
638
+
639
+ if (shouldUseImportedPaletteMapping) {
640
+ const suggestedClass = buildVariantClassName(variants, parsedTailwindColour.emilyBaseClass);
641
+
642
+ report.importedPaletteMappings.push({
643
+ from: className,
644
+ to: suggestedClass,
645
+ utility: parsedTailwindColour.utility,
646
+ tailwindPalette: parsedTailwindColour.tailwindPalette,
647
+ tailwindShade: parsedTailwindColour.tailwindShade,
648
+ emilyPalette: parsedTailwindColour.emilyPalette,
649
+ emilyShade: parsedTailwindColour.emilyShade,
650
+ hasOpacitySuffix: parsedTailwindColour.opacitySuffix !== null,
651
+ });
652
+
653
+ report.importedPalettes.push({
654
+ tailwindPalette: parsedTailwindColour.tailwindPalette,
655
+ emilyPalette: parsedTailwindColour.emilyPalette,
656
+ });
657
+ }
658
+
659
+ if (effectiveSemanticMapping) {
660
+ report.knownTailwind.push(className);
661
+
662
+ const replacementClass = buildVariantClassName(variants, effectiveSemanticMapping.replacement);
663
+ const hasChange = replacementClass !== className;
664
+
665
+ if (hasChange) {
666
+ report.replacements.push({
667
+ from: className,
668
+ to: replacementClass,
669
+ source: effectiveSemanticMapping.source,
670
+ matchedAlias: effectiveSemanticMapping.matchedAlias || null,
671
+ deprecated: effectiveSemanticMapping.deprecated === true,
672
+ metadata: effectiveSemanticMapping.metadata || {},
673
+ });
674
+
675
+ report.suggestions.push({
676
+ className,
677
+ suggestion: replacementClass,
678
+ reason: 'tailwind-mapping',
679
+ });
680
+ }
681
+ } else if (shouldUseImportedPaletteMapping) {
682
+ const replacementClass = buildVariantClassName(variants, parsedTailwindColour.emilyBaseClass);
683
+
684
+ report.knownTailwind.push(className);
685
+ report.replacements.push({
686
+ from: className,
687
+ to: replacementClass,
688
+ source: `${parsedTailwindColour.utility}-${parsedTailwindColour.tailwindPalette}-${parsedTailwindColour.tailwindShade}`,
689
+ matchedAlias: null,
690
+ deprecated: false,
691
+ metadata: {
692
+ category: 'imported-palette-colour',
693
+ emilyPalette: parsedTailwindColour.emilyPalette,
694
+ emilyShade: parsedTailwindColour.emilyShade,
695
+ hasOpacitySuffix: parsedTailwindColour.opacitySuffix !== null,
696
+ },
697
+ });
698
+ report.suggestions.push({
699
+ className,
700
+ suggestion: replacementClass,
701
+ reason: 'imported-palette-mapping',
702
+ });
703
+ }
704
+
705
+ if (!isSupported && !effectiveSemanticMapping) {
706
+ if (shouldUseImportedPaletteMapping) {
707
+ continue;
708
+ }
709
+
710
+ report.unsupported.push(className);
711
+
712
+ if (utilitySet.size > 0) {
713
+ const suggestion = suggestClassName(className, utilitySet, variantSet);
714
+ if (suggestion) {
715
+ report.suggestions.push({
716
+ className,
717
+ suggestion,
718
+ reason: 'closest-emily-class',
719
+ });
720
+ }
721
+ }
722
+ }
723
+ }
724
+
725
+ report.found.sort();
726
+ report.supported.sort();
727
+ report.knownTailwind.sort();
728
+ report.unsupported.sort();
729
+ report.arbitraryValueUtilities.sort();
730
+ report.importedPalettes = dedupeBy(
731
+ report.importedPalettes,
732
+ (item) => `${item.tailwindPalette}->${item.emilyPalette}`,
733
+ );
734
+ report.importedPaletteMappings = dedupeBy(
735
+ report.importedPaletteMappings,
736
+ (item) => `${item.from}->${item.to}`,
737
+ );
738
+
739
+ return report;
740
+ }
741
+
742
+ function getFilesToScan(config, options = {}) {
743
+ const extensions = (config && config.purge && config.purge.extensions) || MIGRATION_DEFAULT_EXTENSIONS;
744
+ const ignore = (config && config.purge && config.purge.ignore) || [];
745
+
746
+ if (options.sourceGlobs && options.sourceGlobs.length > 0) {
747
+ return fg.sync(options.sourceGlobs, {
748
+ ignore,
749
+ onlyFiles: true,
750
+ unique: true,
751
+ absolute: true,
752
+ });
753
+ }
754
+
755
+ if (config && config.purge && config.purge.sourceGlobs && config.purge.sourceGlobs.length > 0) {
756
+ return fg.sync(config.purge.sourceGlobs, {
757
+ ignore,
758
+ onlyFiles: true,
759
+ unique: true,
760
+ absolute: true,
761
+ });
762
+ }
763
+
764
+ const sourceDir =
765
+ options.sourceDir ||
766
+ (config && config.purge && config.purge.sourceDir) ||
767
+ '.';
768
+
769
+ const scanDir = path.isAbsolute(sourceDir)
770
+ ? sourceDir
771
+ : path.join(process.cwd(), sourceDir);
772
+
773
+ if (!fs.existsSync(scanDir)) {
774
+ return [];
775
+ }
776
+
777
+ return getAllFiles(scanDir, extensions);
778
+ }
779
+
780
+ function dedupeBy(items, keyFn) {
781
+ const seen = new Set();
782
+ const result = [];
783
+
784
+ for (const item of items) {
785
+ const key = keyFn(item);
786
+ if (seen.has(key)) continue;
787
+ seen.add(key);
788
+ result.push(item);
789
+ }
790
+
791
+ return result;
792
+ }
793
+
794
+ function generateMigrationReport(options = {}) {
795
+ const config = readConfig(options) || {};
796
+ const manifestResult = loadManifest({ ...options, config });
797
+ const files = getFilesToScan(config, options);
798
+
799
+ const aggregate = {
800
+ found: new Set(),
801
+ supported: new Set(),
802
+ knownTailwind: new Set(),
803
+ unsupported: new Set(),
804
+ arbitraryValueUtilities: new Set(),
805
+ suggestions: [],
806
+ replacements: [],
807
+ importedPaletteMappings: [],
808
+ importedPalettes: [],
809
+ warnings: [...manifestResult.warnings],
810
+ };
811
+
812
+ if (files.length === 0) {
813
+ aggregate.warnings.push('No source files found for migration scan.');
814
+ }
815
+
816
+ const fileReports = [];
817
+
818
+ files.forEach((filePath) => {
819
+ try {
820
+ const content = fs.readFileSync(filePath, 'utf8');
821
+ const report = migrateClasses(content, {
822
+ ...options,
823
+ config,
824
+ manifest: manifestResult.manifest,
825
+ });
826
+
827
+ report.found.forEach((className) => aggregate.found.add(className));
828
+ report.supported.forEach((className) => aggregate.supported.add(className));
829
+ report.knownTailwind.forEach((className) => aggregate.knownTailwind.add(className));
830
+ report.unsupported.forEach((className) => aggregate.unsupported.add(className));
831
+ report.arbitraryValueUtilities.forEach((className) => aggregate.arbitraryValueUtilities.add(className));
832
+ aggregate.suggestions.push(...report.suggestions);
833
+ aggregate.replacements.push(...report.replacements);
834
+ aggregate.importedPaletteMappings.push(...report.importedPaletteMappings);
835
+ aggregate.importedPalettes.push(...report.importedPalettes);
836
+ aggregate.warnings.push(...report.warnings);
837
+
838
+ fileReports.push({
839
+ file: filePath,
840
+ report,
841
+ });
842
+ } catch (error) {
843
+ aggregate.warnings.push(`Could not read ${filePath}: ${error.message}`);
844
+ }
845
+ });
846
+
847
+ return {
848
+ files,
849
+ fileReports,
850
+ found: Array.from(aggregate.found).sort(),
851
+ supported: Array.from(aggregate.supported).sort(),
852
+ knownTailwind: Array.from(aggregate.knownTailwind).sort(),
853
+ unsupported: Array.from(aggregate.unsupported).sort(),
854
+ arbitraryValueUtilities: Array.from(aggregate.arbitraryValueUtilities).sort(),
855
+ suggestions: dedupeBy(aggregate.suggestions, (item) => `${item.className}->${item.suggestion}`),
856
+ replacements: dedupeBy(aggregate.replacements, (item) => `${item.from}->${item.to}`),
857
+ importedPaletteMappings: dedupeBy(
858
+ aggregate.importedPaletteMappings,
859
+ (item) => `${item.from}->${item.to}`,
860
+ ),
861
+ importedPalettes: dedupeBy(
862
+ aggregate.importedPalettes,
863
+ (item) => `${item.tailwindPalette}->${item.emilyPalette}`,
864
+ ),
865
+ importedPalettesConfig:
866
+ options.importColours === true
867
+ ? buildImportedPalettesConfigBlock(
868
+ dedupeBy(
869
+ aggregate.importedPalettes,
870
+ (item) => `${item.tailwindPalette}->${item.emilyPalette}`,
871
+ ),
872
+ )
873
+ : null,
874
+ warnings: Array.from(new Set(aggregate.warnings)),
875
+ };
876
+ }
877
+
878
+ module.exports = {
879
+ migrateClasses,
880
+ loadManifest,
881
+ generateMigrationReport,
882
+ };