emily-css 1.0.29 → 1.2.0-alpha.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/CHANGELOG.md +35 -0
- package/README.md +10 -1
- package/bin/emilyui.js +17 -1
- package/package.json +1 -1
- package/src/doctor.js +293 -0
- package/src/migrate.js +756 -0
- package/src/reporters/migrationReporter.js +71 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,7 +3,42 @@
|
|
|
3
3
|
All notable changes to `emily-css` are documented here.
|
|
4
4
|
|
|
5
5
|
---
|
|
6
|
+
## v1.2.0-alpha.0 — May 2026
|
|
6
7
|
|
|
8
|
+
### Added
|
|
9
|
+
- Report-only Tailwind-to-EmilyCSS migration command: `emily-css migrate`.
|
|
10
|
+
- Default semantic migration mode for design-token aligned suggestions.
|
|
11
|
+
- Imported palette mode via `emily-css migrate --import-colours` for visual parity mapping suggestions.
|
|
12
|
+
- Detection and reporting for arbitrary value utilities during migration analysis.
|
|
13
|
+
|
|
14
|
+
### Notes
|
|
15
|
+
- Migration in this alpha is analysis-only: no source files are modified by `migrate`.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
## v1.1.0 — May 2026
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
- Added `emily-css doctor`, a manifest-powered project checker that scans configured source files and reports unknown EmilyCSS classes with suggestions.
|
|
22
|
+
- Added variant-aware class validation for responsive, state, ARIA, data-state, motion, dark, and forced-colours variants.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## v1.1.1 — May 2026
|
|
27
|
+
|
|
28
|
+
**updated changes and added**
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
- updted changes
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
## v1.1.0 — May 2026
|
|
35
|
+
|
|
36
|
+
**add utility manifest generation): chore: release v1.1.0**
|
|
37
|
+
|
|
38
|
+
### Added
|
|
39
|
+
- add utility manifest generation): chore: release v1.1.0
|
|
40
|
+
|
|
41
|
+
---
|
|
7
42
|
## v1.0.29 — May 2026
|
|
8
43
|
|
|
9
44
|
**added json manifest for future use**
|
package/README.md
CHANGED
|
@@ -53,8 +53,17 @@ npx emily-css init # Setup config + first build
|
|
|
53
53
|
npx emily-css build # Regenerate CSS
|
|
54
54
|
npx emily-css watch # Development watch mode
|
|
55
55
|
npx emily-css purge # Remove unused styles for production
|
|
56
|
+
npx emily-css migrate # Report-only Tailwind-to-EmilyCSS migration analysis
|
|
57
|
+
--import-colours # Imported palette mode (visual parity class suggestions)
|
|
56
58
|
```
|
|
57
59
|
|
|
60
|
+
## Migration (1.2.0-alpha)
|
|
61
|
+
|
|
62
|
+
- `emily-css migrate` is report-only and does not modify files.
|
|
63
|
+
- Default migration mode is semantic (`gray/slate/zinc/stone` remap toward `neutral` naming).
|
|
64
|
+
- `emily-css migrate --import-colours` enables imported palette mode for parity-oriented palette suggestions.
|
|
65
|
+
- Arbitrary value utilities (for example `w-[37px]`, `bg-[#0f172a]`) are detected and reported as unsupported.
|
|
66
|
+
|
|
58
67
|
## How Purge Works
|
|
59
68
|
|
|
60
69
|
emilyCSS scans your templates for used class names and removes everything else.
|
|
@@ -146,4 +155,4 @@ Then import the weights you need.
|
|
|
146
155
|
|
|
147
156
|
## License
|
|
148
157
|
|
|
149
|
-
MIT
|
|
158
|
+
MIT
|
package/bin/emilyui.js
CHANGED
|
@@ -14,6 +14,16 @@ if (command === "init") {
|
|
|
14
14
|
require("../src/watch.js");
|
|
15
15
|
} else if (command === "showcase") {
|
|
16
16
|
require("../src/showcase.js");
|
|
17
|
+
} else if (command === "doctor") {
|
|
18
|
+
const { doctor } = require("../src/doctor.js");
|
|
19
|
+
const result = doctor();
|
|
20
|
+
process.exitCode = result.exitCode;
|
|
21
|
+
} else if (command === "migrate") {
|
|
22
|
+
const { generateMigrationReport } = require("../src/migrate.js");
|
|
23
|
+
const { formatMigrationReport } = require("../src/reporters/migrationReporter.js");
|
|
24
|
+
const importColours = process.argv.includes("--import-colours");
|
|
25
|
+
const report = generateMigrationReport({ importColours });
|
|
26
|
+
console.log(formatMigrationReport(report, { importColours }));
|
|
17
27
|
} else if (command === "version" || command === "--version" || command === "-v") {
|
|
18
28
|
console.log(packageJson.version);
|
|
19
29
|
} else if (command === "help") {
|
|
@@ -24,6 +34,9 @@ if (command === "init") {
|
|
|
24
34
|
emily-css init Set up a new project (interactive wizard)
|
|
25
35
|
emily-css build Generate production CSS to the configured output path
|
|
26
36
|
emily-css watch Dev mode: watch for changes and rebuild
|
|
37
|
+
emily-css doctor Scan project files for unknown EmilyCSS classes
|
|
38
|
+
emily-css migrate Generate a Tailwind-to-EmilyCSS migration report
|
|
39
|
+
--import-colours Detect Tailwind colour palettes and suggest importedPalettes config
|
|
27
40
|
emily-css showcase Launch the component showcase in your browser
|
|
28
41
|
emily-css version Show installed version
|
|
29
42
|
emily-css help Show this help text
|
|
@@ -44,9 +57,12 @@ if (command === "init") {
|
|
|
44
57
|
emily-css init Set up a new project
|
|
45
58
|
emily-css build Generate production CSS to the configured output path
|
|
46
59
|
emily-css watch Dev mode: rebuild on changes
|
|
60
|
+
emily-css doctor Scan project files for unknown EmilyCSS classes
|
|
61
|
+
emily-css migrate Generate a Tailwind-to-EmilyCSS migration report
|
|
62
|
+
--import-colours Detect Tailwind colour palettes and suggest importedPalettes config
|
|
47
63
|
emily-css showcase Browse components in your browser
|
|
48
64
|
emily-css help Full command reference
|
|
49
65
|
|
|
50
66
|
Run emily-css help for more detail.
|
|
51
67
|
`);
|
|
52
|
-
}
|
|
68
|
+
}
|
package/package.json
CHANGED
package/src/doctor.js
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const fg = require("fast-glob");
|
|
4
|
+
const { extractClassNames } = require("./purge.js");
|
|
5
|
+
const { ensureFullFramework, generateManifest } = require("./index.js");
|
|
6
|
+
|
|
7
|
+
const DEFAULT_EXTENSIONS = [
|
|
8
|
+
".html",
|
|
9
|
+
".htm",
|
|
10
|
+
".twig",
|
|
11
|
+
".njk",
|
|
12
|
+
".liquid",
|
|
13
|
+
".hbs",
|
|
14
|
+
".js",
|
|
15
|
+
".jsx",
|
|
16
|
+
".ts",
|
|
17
|
+
".tsx",
|
|
18
|
+
".vue",
|
|
19
|
+
".php",
|
|
20
|
+
".astro",
|
|
21
|
+
".svelte",
|
|
22
|
+
".blade.php",
|
|
23
|
+
".jinja",
|
|
24
|
+
".jinja2",
|
|
25
|
+
".j2",
|
|
26
|
+
".md",
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
function getConfigPath() {
|
|
30
|
+
return path.join(process.cwd(), "emily.config.json");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function getConfig() {
|
|
34
|
+
const configPath = getConfigPath();
|
|
35
|
+
|
|
36
|
+
if (!fs.existsSync(configPath)) {
|
|
37
|
+
console.error('\n emily-css: No config found. Run "emily-css init" first.\n');
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function getFullCssPath(config) {
|
|
45
|
+
return path.join(process.cwd(), config.output?.fullCss || "dist/emily.css");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function getManifestSettings(config) {
|
|
49
|
+
const manifestConfig = config.manifest;
|
|
50
|
+
|
|
51
|
+
if (manifestConfig === true) {
|
|
52
|
+
return { enabled: true, output: "dist/emily.manifest.json" };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (manifestConfig && typeof manifestConfig === "object") {
|
|
56
|
+
return {
|
|
57
|
+
enabled: manifestConfig.enabled === true,
|
|
58
|
+
output: manifestConfig.output || "dist/emily.manifest.json",
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return { enabled: false, output: "dist/emily.manifest.json" };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function getManifestOutputPath(config) {
|
|
66
|
+
const manifestSettings = getManifestSettings(config);
|
|
67
|
+
const outputPath = manifestSettings.output || "dist/emily.manifest.json";
|
|
68
|
+
|
|
69
|
+
return path.isAbsolute(outputPath)
|
|
70
|
+
? outputPath
|
|
71
|
+
: path.join(process.cwd(), outputPath);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function normaliseClassForManifest(className) {
|
|
75
|
+
if (!className || typeof className !== "string") {
|
|
76
|
+
return { original: className, baseClass: "", variants: [], variant: null };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const parts = className.split(":").filter(Boolean);
|
|
80
|
+
|
|
81
|
+
if (parts.length <= 1) {
|
|
82
|
+
return {
|
|
83
|
+
original: className,
|
|
84
|
+
baseClass: className,
|
|
85
|
+
variants: [],
|
|
86
|
+
variant: null,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const baseClass = parts[parts.length - 1];
|
|
91
|
+
const variants = parts.slice(0, -1);
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
original: className,
|
|
95
|
+
baseClass,
|
|
96
|
+
variants,
|
|
97
|
+
variant: variants.join(":"),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function levenshtein(a, b) {
|
|
102
|
+
if (a === b) return 0;
|
|
103
|
+
if (!a.length) return b.length;
|
|
104
|
+
if (!b.length) return a.length;
|
|
105
|
+
|
|
106
|
+
const dp = Array.from({ length: a.length + 1 }, () => Array(b.length + 1).fill(0));
|
|
107
|
+
|
|
108
|
+
for (let i = 0; i <= a.length; i++) dp[i][0] = i;
|
|
109
|
+
for (let j = 0; j <= b.length; j++) dp[0][j] = j;
|
|
110
|
+
|
|
111
|
+
for (let i = 1; i <= a.length; i++) {
|
|
112
|
+
for (let j = 1; j <= b.length; j++) {
|
|
113
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
114
|
+
dp[i][j] = Math.min(
|
|
115
|
+
dp[i - 1][j] + 1,
|
|
116
|
+
dp[i][j - 1] + 1,
|
|
117
|
+
dp[i - 1][j - 1] + cost
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return dp[a.length][b.length];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function findClosest(target, candidates) {
|
|
126
|
+
if (!target || candidates.length === 0) return null;
|
|
127
|
+
|
|
128
|
+
let best = null;
|
|
129
|
+
let bestDistance = Infinity;
|
|
130
|
+
|
|
131
|
+
for (const candidate of candidates) {
|
|
132
|
+
const distance = levenshtein(target, candidate);
|
|
133
|
+
if (distance < bestDistance) {
|
|
134
|
+
bestDistance = distance;
|
|
135
|
+
best = candidate;
|
|
136
|
+
if (distance === 0) break;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const threshold = Math.max(2, Math.floor(target.length / 3));
|
|
141
|
+
return bestDistance <= threshold ? best : null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function suggestClassName(className, utilitySet, variantSet) {
|
|
145
|
+
const utilityList = Array.isArray(utilitySet) ? utilitySet : Array.from(utilitySet);
|
|
146
|
+
const variantList = Array.isArray(variantSet) ? variantSet : Array.from(variantSet);
|
|
147
|
+
const parsed = normaliseClassForManifest(className);
|
|
148
|
+
|
|
149
|
+
if (!parsed.variants.length) {
|
|
150
|
+
return findClosest(parsed.baseClass, utilityList);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const correctedVariants = parsed.variants.map((variant) => {
|
|
154
|
+
if (variantSet.has(variant)) return variant;
|
|
155
|
+
return findClosest(variant, variantList) || variant;
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
const correctedBase = utilitySet.has(parsed.baseClass)
|
|
159
|
+
? parsed.baseClass
|
|
160
|
+
: findClosest(parsed.baseClass, utilityList);
|
|
161
|
+
|
|
162
|
+
if (!correctedBase) return null;
|
|
163
|
+
|
|
164
|
+
const rebuilt = correctedVariants.length
|
|
165
|
+
? `${correctedVariants.join(":")}:${correctedBase}`
|
|
166
|
+
: correctedBase;
|
|
167
|
+
|
|
168
|
+
return rebuilt === className ? null : rebuilt;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function getFilesToScan(config) {
|
|
172
|
+
const extensions = config?.purge?.extensions || DEFAULT_EXTENSIONS;
|
|
173
|
+
const ignore = config?.purge?.ignore || [];
|
|
174
|
+
|
|
175
|
+
if (config?.purge?.sourceGlobs && config.purge.sourceGlobs.length > 0) {
|
|
176
|
+
return fg.sync(config.purge.sourceGlobs, {
|
|
177
|
+
ignore,
|
|
178
|
+
onlyFiles: true,
|
|
179
|
+
unique: true,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const sourceDir = config?.purge?.sourceDir || ".";
|
|
184
|
+
const scanDir = path.isAbsolute(sourceDir)
|
|
185
|
+
? sourceDir
|
|
186
|
+
: path.join(process.cwd(), sourceDir);
|
|
187
|
+
const patterns = extensions.map((ext) => `**/*${ext}`);
|
|
188
|
+
|
|
189
|
+
return fg.sync(patterns, {
|
|
190
|
+
cwd: scanDir,
|
|
191
|
+
ignore,
|
|
192
|
+
onlyFiles: true,
|
|
193
|
+
unique: true,
|
|
194
|
+
absolute: true,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function loadManifest(config, css) {
|
|
199
|
+
const manifestSettings = getManifestSettings(config);
|
|
200
|
+
const manifestOutputPath = getManifestOutputPath(config);
|
|
201
|
+
|
|
202
|
+
if (manifestSettings.enabled && fs.existsSync(manifestOutputPath)) {
|
|
203
|
+
return JSON.parse(fs.readFileSync(manifestOutputPath, "utf8"));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return generateManifest(css, config);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function doctor() {
|
|
210
|
+
const config = getConfig();
|
|
211
|
+
|
|
212
|
+
ensureFullFramework();
|
|
213
|
+
|
|
214
|
+
const fullCssPath = getFullCssPath(config);
|
|
215
|
+
if (!fs.existsSync(fullCssPath)) {
|
|
216
|
+
console.error("\nEmilyCSS doctor could not locate generated CSS.\n");
|
|
217
|
+
return { ok: false, issues: [], exitCode: 1 };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const css = fs.readFileSync(fullCssPath, "utf8");
|
|
221
|
+
const manifest = loadManifest(config, css);
|
|
222
|
+
const utilities = manifest.utilities || [];
|
|
223
|
+
const utilitySet = new Set(utilities.map((utility) => utility.class));
|
|
224
|
+
const variantSet = new Set();
|
|
225
|
+
|
|
226
|
+
utilities.forEach((utility) => {
|
|
227
|
+
(utility.variants || []).forEach((variant) => variantSet.add(variant));
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
const files = getFilesToScan(config);
|
|
231
|
+
const issues = [];
|
|
232
|
+
const suggestionCache = new Map();
|
|
233
|
+
|
|
234
|
+
files.forEach((filePath) => {
|
|
235
|
+
try {
|
|
236
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
237
|
+
const classes = extractClassNames(content);
|
|
238
|
+
|
|
239
|
+
classes.forEach((className) => {
|
|
240
|
+
const parsed = normaliseClassForManifest(className);
|
|
241
|
+
const unknownVariants = parsed.variants.filter((variant) => !variantSet.has(variant));
|
|
242
|
+
const knownBase = utilitySet.has(parsed.baseClass);
|
|
243
|
+
|
|
244
|
+
if (unknownVariants.length === 0 && knownBase) {
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (!suggestionCache.has(className)) {
|
|
249
|
+
suggestionCache.set(className, suggestClassName(className, utilitySet, variantSet));
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
issues.push({
|
|
253
|
+
file: filePath,
|
|
254
|
+
className,
|
|
255
|
+
reason: unknownVariants.length > 0 ? "unknown-variant" : "unknown-class",
|
|
256
|
+
unknownVariants,
|
|
257
|
+
suggestion: suggestionCache.get(className),
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
} catch (error) {
|
|
261
|
+
// Keep parity with purge behaviour: unreadable files are skipped.
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
if (issues.length === 0) {
|
|
266
|
+
console.log("✓ EmilyCSS doctor found no class issues");
|
|
267
|
+
return { ok: true, issues: [], exitCode: 0 };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
console.log(`EmilyCSS doctor found ${issues.length} issue${issues.length === 1 ? "" : "s"}\n`);
|
|
271
|
+
|
|
272
|
+
issues.forEach((issue) => {
|
|
273
|
+
console.log(path.relative(process.cwd(), issue.file));
|
|
274
|
+
if (issue.reason === "unknown-variant") {
|
|
275
|
+
console.log(` Unknown variant in class: ${issue.className}`);
|
|
276
|
+
} else {
|
|
277
|
+
console.log(` Unknown class: ${issue.className}`);
|
|
278
|
+
}
|
|
279
|
+
if (issue.suggestion) {
|
|
280
|
+
console.log(` Did you mean: ${issue.suggestion}?`);
|
|
281
|
+
}
|
|
282
|
+
console.log("");
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
console.log("Run `emily-css build` after fixing classes.");
|
|
286
|
+
return { ok: false, issues, exitCode: 1 };
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
module.exports = {
|
|
290
|
+
doctor,
|
|
291
|
+
normaliseClassForManifest,
|
|
292
|
+
suggestClassName,
|
|
293
|
+
};
|
package/src/migrate.js
ADDED
|
@@ -0,0 +1,756 @@
|
|
|
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
|
+
|
|
9
|
+
const DEFAULT_EXTENSIONS = [
|
|
10
|
+
'.html',
|
|
11
|
+
'.htm',
|
|
12
|
+
'.twig',
|
|
13
|
+
'.njk',
|
|
14
|
+
'.liquid',
|
|
15
|
+
'.hbs',
|
|
16
|
+
'.js',
|
|
17
|
+
'.jsx',
|
|
18
|
+
'.ts',
|
|
19
|
+
'.tsx',
|
|
20
|
+
'.vue',
|
|
21
|
+
'.php',
|
|
22
|
+
'.astro',
|
|
23
|
+
'.svelte',
|
|
24
|
+
'.blade.php',
|
|
25
|
+
'.jinja',
|
|
26
|
+
'.jinja2',
|
|
27
|
+
'.j2',
|
|
28
|
+
'.md',
|
|
29
|
+
'.mdx',
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
const TAILWIND_MAPPINGS = {
|
|
33
|
+
'text-gray-900': {
|
|
34
|
+
replacement: 'text-neutral-90',
|
|
35
|
+
aliases: ['text-zinc-900', 'text-slate-900', 'text-stone-900', 'text-neutral-900'],
|
|
36
|
+
deprecated: false,
|
|
37
|
+
metadata: { category: 'colour' },
|
|
38
|
+
},
|
|
39
|
+
'text-gray-700': {
|
|
40
|
+
replacement: 'text-neutral-70',
|
|
41
|
+
aliases: ['text-zinc-700', 'text-slate-700', 'text-stone-700', 'text-neutral-700'],
|
|
42
|
+
deprecated: false,
|
|
43
|
+
metadata: { category: 'colour' },
|
|
44
|
+
},
|
|
45
|
+
'text-gray-500': {
|
|
46
|
+
replacement: 'text-neutral-50',
|
|
47
|
+
aliases: ['text-zinc-500', 'text-slate-500', 'text-stone-500', 'text-neutral-500'],
|
|
48
|
+
deprecated: false,
|
|
49
|
+
metadata: { category: 'colour' },
|
|
50
|
+
},
|
|
51
|
+
'bg-gray-100': {
|
|
52
|
+
replacement: 'bg-neutral-10',
|
|
53
|
+
aliases: ['bg-zinc-100', 'bg-slate-100', 'bg-stone-100', 'bg-neutral-100'],
|
|
54
|
+
deprecated: false,
|
|
55
|
+
metadata: { category: 'background' },
|
|
56
|
+
},
|
|
57
|
+
'bg-gray-900': {
|
|
58
|
+
replacement: 'bg-neutral-90',
|
|
59
|
+
aliases: ['bg-zinc-900', 'bg-slate-900', 'bg-stone-900', 'bg-neutral-900'],
|
|
60
|
+
deprecated: false,
|
|
61
|
+
metadata: { category: 'background' },
|
|
62
|
+
},
|
|
63
|
+
'rounded-md': {
|
|
64
|
+
replacement: 'rounded-md',
|
|
65
|
+
aliases: [],
|
|
66
|
+
deprecated: false,
|
|
67
|
+
metadata: { category: 'radius' },
|
|
68
|
+
},
|
|
69
|
+
'rounded-lg': {
|
|
70
|
+
replacement: 'rounded-lg',
|
|
71
|
+
aliases: [],
|
|
72
|
+
deprecated: false,
|
|
73
|
+
metadata: { category: 'radius' },
|
|
74
|
+
},
|
|
75
|
+
'shadow-md': {
|
|
76
|
+
replacement: 'shadow-md',
|
|
77
|
+
aliases: [],
|
|
78
|
+
deprecated: false,
|
|
79
|
+
metadata: { category: 'shadow' },
|
|
80
|
+
},
|
|
81
|
+
flex: {
|
|
82
|
+
replacement: 'flex',
|
|
83
|
+
aliases: [],
|
|
84
|
+
deprecated: false,
|
|
85
|
+
metadata: { category: 'layout' },
|
|
86
|
+
},
|
|
87
|
+
grid: {
|
|
88
|
+
replacement: 'grid',
|
|
89
|
+
aliases: [],
|
|
90
|
+
deprecated: false,
|
|
91
|
+
metadata: { category: 'layout' },
|
|
92
|
+
},
|
|
93
|
+
hidden: {
|
|
94
|
+
replacement: 'hidden',
|
|
95
|
+
aliases: [],
|
|
96
|
+
deprecated: false,
|
|
97
|
+
metadata: { category: 'layout' },
|
|
98
|
+
},
|
|
99
|
+
block: {
|
|
100
|
+
replacement: 'block',
|
|
101
|
+
aliases: [],
|
|
102
|
+
deprecated: false,
|
|
103
|
+
metadata: { category: 'layout' },
|
|
104
|
+
},
|
|
105
|
+
'inline-block': {
|
|
106
|
+
replacement: 'inline-block',
|
|
107
|
+
aliases: [],
|
|
108
|
+
deprecated: false,
|
|
109
|
+
metadata: { category: 'layout' },
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const MIGRATION_MODE_SEMANTIC = 'semantic';
|
|
114
|
+
const MIGRATION_MODE_IMPORTED_PALETTES = 'imported-palettes';
|
|
115
|
+
|
|
116
|
+
const TAILWIND_COLOUR_UTILITY_PREFIXES = new Set([
|
|
117
|
+
'text',
|
|
118
|
+
'bg',
|
|
119
|
+
'border',
|
|
120
|
+
'accent',
|
|
121
|
+
'fill',
|
|
122
|
+
'stroke',
|
|
123
|
+
'ring',
|
|
124
|
+
'outline',
|
|
125
|
+
]);
|
|
126
|
+
|
|
127
|
+
const TAILWIND_COLOUR_PALETTES = new Set([
|
|
128
|
+
'slate',
|
|
129
|
+
'gray',
|
|
130
|
+
'zinc',
|
|
131
|
+
'neutral',
|
|
132
|
+
'stone',
|
|
133
|
+
'red',
|
|
134
|
+
'orange',
|
|
135
|
+
'amber',
|
|
136
|
+
'yellow',
|
|
137
|
+
'lime',
|
|
138
|
+
'green',
|
|
139
|
+
'emerald',
|
|
140
|
+
'teal',
|
|
141
|
+
'cyan',
|
|
142
|
+
'sky',
|
|
143
|
+
'blue',
|
|
144
|
+
'indigo',
|
|
145
|
+
'violet',
|
|
146
|
+
'purple',
|
|
147
|
+
'fuchsia',
|
|
148
|
+
'pink',
|
|
149
|
+
'rose',
|
|
150
|
+
]);
|
|
151
|
+
|
|
152
|
+
const TAILWIND_SHADE_TO_EMILY_SHADE = {
|
|
153
|
+
'50': '5',
|
|
154
|
+
'100': '10',
|
|
155
|
+
'200': '20',
|
|
156
|
+
'300': '30',
|
|
157
|
+
'400': '40',
|
|
158
|
+
'500': '50',
|
|
159
|
+
'600': '60',
|
|
160
|
+
'700': '70',
|
|
161
|
+
'800': '80',
|
|
162
|
+
'900': '90',
|
|
163
|
+
'950': '100',
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
function getConfigPath(options = {}) {
|
|
167
|
+
return options.configPath || path.join(process.cwd(), 'emily.config.json');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function readConfig(options = {}) {
|
|
171
|
+
if (options.config && typeof options.config === 'object') {
|
|
172
|
+
return options.config;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const configPath = getConfigPath(options);
|
|
176
|
+
if (!fs.existsSync(configPath)) {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
return JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
182
|
+
} catch (error) {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function getManifestSettings(config) {
|
|
188
|
+
const manifestConfig = config && config.manifest;
|
|
189
|
+
|
|
190
|
+
if (manifestConfig === true) {
|
|
191
|
+
return { enabled: true, output: 'dist/emily.manifest.json' };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (manifestConfig && typeof manifestConfig === 'object') {
|
|
195
|
+
return {
|
|
196
|
+
enabled: manifestConfig.enabled === true,
|
|
197
|
+
output: manifestConfig.output || 'dist/emily.manifest.json',
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return { enabled: false, output: 'dist/emily.manifest.json' };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function getManifestOutputPath(config, options = {}) {
|
|
205
|
+
if (options.manifestPath) {
|
|
206
|
+
return path.isAbsolute(options.manifestPath)
|
|
207
|
+
? options.manifestPath
|
|
208
|
+
: path.join(process.cwd(), options.manifestPath);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const manifestSettings = getManifestSettings(config || {});
|
|
212
|
+
const outputPath = manifestSettings.output || 'dist/emily.manifest.json';
|
|
213
|
+
|
|
214
|
+
return path.isAbsolute(outputPath)
|
|
215
|
+
? outputPath
|
|
216
|
+
: path.join(process.cwd(), outputPath);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function getFullCssPath(config, options = {}) {
|
|
220
|
+
if (options.cssPath) {
|
|
221
|
+
return path.isAbsolute(options.cssPath)
|
|
222
|
+
? options.cssPath
|
|
223
|
+
: path.join(process.cwd(), options.cssPath);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const outputPath = (config && config.output && config.output.fullCss) || 'dist/emily.css';
|
|
227
|
+
return path.isAbsolute(outputPath)
|
|
228
|
+
? outputPath
|
|
229
|
+
: path.join(process.cwd(), outputPath);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function createEmptyManifest() {
|
|
233
|
+
return {
|
|
234
|
+
version: 'unknown',
|
|
235
|
+
generatedAt: new Date().toISOString(),
|
|
236
|
+
utilities: [],
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function loadManifest(options = {}) {
|
|
241
|
+
const warnings = [];
|
|
242
|
+
|
|
243
|
+
if (options.manifest && Array.isArray(options.manifest.utilities)) {
|
|
244
|
+
return { manifest: options.manifest, warnings };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const config = readConfig(options) || {};
|
|
248
|
+
const manifestPath = getManifestOutputPath(config, options);
|
|
249
|
+
|
|
250
|
+
if (fs.existsSync(manifestPath)) {
|
|
251
|
+
try {
|
|
252
|
+
return {
|
|
253
|
+
manifest: JSON.parse(fs.readFileSync(manifestPath, 'utf8')),
|
|
254
|
+
warnings,
|
|
255
|
+
};
|
|
256
|
+
} catch (error) {
|
|
257
|
+
warnings.push(`Could not parse manifest at ${manifestPath}: ${error.message}`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const fullCssPath = getFullCssPath(config, options);
|
|
262
|
+
|
|
263
|
+
if (fs.existsSync(fullCssPath)) {
|
|
264
|
+
try {
|
|
265
|
+
const css = fs.readFileSync(fullCssPath, 'utf8');
|
|
266
|
+
return {
|
|
267
|
+
manifest: generateManifest(css, config),
|
|
268
|
+
warnings,
|
|
269
|
+
};
|
|
270
|
+
} catch (error) {
|
|
271
|
+
warnings.push(`Could not generate manifest from CSS at ${fullCssPath}: ${error.message}`);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
warnings.push(
|
|
276
|
+
'Manifest not found. Migration report will continue without full EmilyCSS support checks.',
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
return { manifest: createEmptyManifest(), warnings };
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function buildManifestIndexes(manifest) {
|
|
283
|
+
const utilities = Array.isArray(manifest && manifest.utilities) ? manifest.utilities : [];
|
|
284
|
+
const utilitySet = new Set();
|
|
285
|
+
const variantSet = new Set();
|
|
286
|
+
|
|
287
|
+
utilities.forEach((utility) => {
|
|
288
|
+
if (utility && utility.class) {
|
|
289
|
+
utilitySet.add(utility.class);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const variants = Array.isArray(utility && utility.variants) ? utility.variants : [];
|
|
293
|
+
variants.forEach((variant) => variantSet.add(variant));
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
return { utilitySet, variantSet };
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function resolveTailwindMapping(className, mappingTable = TAILWIND_MAPPINGS) {
|
|
300
|
+
if (!className || typeof className !== 'string') {
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (mappingTable[className]) {
|
|
305
|
+
return {
|
|
306
|
+
source: className,
|
|
307
|
+
...mappingTable[className],
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const entries = Object.entries(mappingTable);
|
|
312
|
+
|
|
313
|
+
for (const [sourceClass, entry] of entries) {
|
|
314
|
+
const aliases = Array.isArray(entry.aliases) ? entry.aliases : [];
|
|
315
|
+
if (aliases.includes(className)) {
|
|
316
|
+
return {
|
|
317
|
+
source: sourceClass,
|
|
318
|
+
...entry,
|
|
319
|
+
matchedAlias: className,
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return null;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function buildVariantClassName(variants, baseClass) {
|
|
328
|
+
if (!Array.isArray(variants) || variants.length === 0) {
|
|
329
|
+
return baseClass;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return `${variants.join(':')}:${baseClass}`;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function getMigrationMode(options = {}) {
|
|
336
|
+
return options.importColours === true
|
|
337
|
+
? MIGRATION_MODE_IMPORTED_PALETTES
|
|
338
|
+
: MIGRATION_MODE_SEMANTIC;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function mapTailwindPaletteToEmilyPalette(paletteName, mode) {
|
|
342
|
+
if (mode === MIGRATION_MODE_IMPORTED_PALETTES) {
|
|
343
|
+
return paletteName;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const semanticPaletteRemap = {
|
|
347
|
+
gray: 'neutral',
|
|
348
|
+
slate: 'neutral',
|
|
349
|
+
zinc: 'neutral',
|
|
350
|
+
stone: 'neutral',
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
return semanticPaletteRemap[paletteName] || paletteName;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function parseTailwindColourClass(baseClass, mode) {
|
|
357
|
+
if (!baseClass || typeof baseClass !== 'string') {
|
|
358
|
+
return null;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const [baseWithoutOpacity, opacitySuffix] = baseClass.split('/');
|
|
362
|
+
const match = baseWithoutOpacity.match(
|
|
363
|
+
/^([a-z-]+)-([a-z][a-z0-9-]*)-(50|100|200|300|400|500|600|700|800|900|950)$/,
|
|
364
|
+
);
|
|
365
|
+
|
|
366
|
+
if (!match) {
|
|
367
|
+
return null;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const utility = match[1];
|
|
371
|
+
const tailwindPalette = match[2];
|
|
372
|
+
const tailwindShade = match[3];
|
|
373
|
+
|
|
374
|
+
if (!TAILWIND_COLOUR_UTILITY_PREFIXES.has(utility)) {
|
|
375
|
+
return null;
|
|
376
|
+
}
|
|
377
|
+
if (!TAILWIND_COLOUR_PALETTES.has(tailwindPalette)) {
|
|
378
|
+
return null;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const emilyShade = TAILWIND_SHADE_TO_EMILY_SHADE[tailwindShade];
|
|
382
|
+
if (!emilyShade) {
|
|
383
|
+
return null;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const emilyPalette = mapTailwindPaletteToEmilyPalette(tailwindPalette, mode);
|
|
387
|
+
const emilyBaseClass = `${utility}-${emilyPalette}-${emilyShade}`;
|
|
388
|
+
|
|
389
|
+
return {
|
|
390
|
+
utility,
|
|
391
|
+
tailwindPalette,
|
|
392
|
+
tailwindShade,
|
|
393
|
+
opacitySuffix: opacitySuffix || null,
|
|
394
|
+
emilyPalette,
|
|
395
|
+
emilyShade,
|
|
396
|
+
emilyBaseClass,
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function isSemanticColourMapping(mapping) {
|
|
401
|
+
const category = mapping && mapping.metadata && mapping.metadata.category;
|
|
402
|
+
return category === 'colour' || category === 'background';
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function hasArbitraryValueSyntax(baseClass) {
|
|
406
|
+
return typeof baseClass === 'string' && /\[[^\]]+\]/.test(baseClass);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function buildImportedPalettesConfigBlock(importedPalettes) {
|
|
410
|
+
if (!Array.isArray(importedPalettes) || importedPalettes.length === 0) {
|
|
411
|
+
return null;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const grouped = {};
|
|
415
|
+
|
|
416
|
+
importedPalettes.forEach((entry) => {
|
|
417
|
+
if (!grouped[entry.emilyPalette]) {
|
|
418
|
+
grouped[entry.emilyPalette] = new Set();
|
|
419
|
+
}
|
|
420
|
+
grouped[entry.emilyPalette].add(entry.tailwindPalette);
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
const lines = ['importedPalettes: {'];
|
|
424
|
+
|
|
425
|
+
Object.keys(grouped)
|
|
426
|
+
.sort()
|
|
427
|
+
.forEach((emilyPalette) => {
|
|
428
|
+
const sources = Array.from(grouped[emilyPalette]).sort();
|
|
429
|
+
if (sources.length === 1) {
|
|
430
|
+
lines.push(` ${emilyPalette}: "tailwind-${sources[0]}",`);
|
|
431
|
+
} else {
|
|
432
|
+
const aliases = sources.map((source) => `"${source}"`).join(', ');
|
|
433
|
+
lines.push(` ${emilyPalette}: { source: "tailwind", aliases: [${aliases}] },`);
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
lines.push('}');
|
|
438
|
+
return lines.join('\n');
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function isLikelyUtilityClass(className) {
|
|
442
|
+
if (!className || typeof className !== 'string') return false;
|
|
443
|
+
if (/\s/.test(className)) return false;
|
|
444
|
+
if (className.length > 120) return false;
|
|
445
|
+
if (className.startsWith('--')) return false;
|
|
446
|
+
if (className.startsWith('.') || className.startsWith('#') || className.startsWith('@')) return false;
|
|
447
|
+
if (className.endsWith(':')) return false;
|
|
448
|
+
if (className.includes('://')) return false;
|
|
449
|
+
if (/[;()={},`]/.test(className)) return false;
|
|
450
|
+
if (!/[a-zA-Z]/.test(className)) return false;
|
|
451
|
+
if (!/^[a-zA-Z0-9:#_./\-[\]]+$/.test(className)) return false;
|
|
452
|
+
|
|
453
|
+
return true;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function migrateClasses(input, options = {}) {
|
|
457
|
+
const mode = getMigrationMode(options);
|
|
458
|
+
|
|
459
|
+
const report = {
|
|
460
|
+
found: [],
|
|
461
|
+
supported: [],
|
|
462
|
+
knownTailwind: [],
|
|
463
|
+
unsupported: [],
|
|
464
|
+
arbitraryValueUtilities: [],
|
|
465
|
+
suggestions: [],
|
|
466
|
+
replacements: [],
|
|
467
|
+
importedPaletteMappings: [],
|
|
468
|
+
importedPalettes: [],
|
|
469
|
+
warnings: [],
|
|
470
|
+
};
|
|
471
|
+
|
|
472
|
+
if (typeof input !== 'string') {
|
|
473
|
+
report.warnings.push('migrateClasses expected a string input; received non-string content.');
|
|
474
|
+
return report;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
const classSet = extractClassNames(input);
|
|
478
|
+
const found = Array.from(classSet).filter(isLikelyUtilityClass);
|
|
479
|
+
|
|
480
|
+
report.found = found;
|
|
481
|
+
|
|
482
|
+
const { manifest, warnings } = loadManifest(options);
|
|
483
|
+
report.warnings.push(...warnings);
|
|
484
|
+
|
|
485
|
+
const { utilitySet, variantSet } = buildManifestIndexes(manifest);
|
|
486
|
+
|
|
487
|
+
for (const className of found) {
|
|
488
|
+
const parsed = normaliseClassForManifest(className);
|
|
489
|
+
const variants = parsed.variants || [];
|
|
490
|
+
const isArbitraryValueClass = hasArbitraryValueSyntax(parsed.baseClass);
|
|
491
|
+
const hasUnknownVariant = variants.some((variant) => !variantSet.has(variant));
|
|
492
|
+
const isSupported = utilitySet.has(parsed.baseClass) && !hasUnknownVariant;
|
|
493
|
+
|
|
494
|
+
if (isSupported) {
|
|
495
|
+
report.supported.push(className);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const parsedTailwindColour = parseTailwindColourClass(parsed.baseClass, mode);
|
|
499
|
+
const mapping = resolveTailwindMapping(parsed.baseClass, options.mappingTable || TAILWIND_MAPPINGS);
|
|
500
|
+
const shouldUseImportedPaletteMapping =
|
|
501
|
+
mode === MIGRATION_MODE_IMPORTED_PALETTES && !!parsedTailwindColour;
|
|
502
|
+
const effectiveSemanticMapping =
|
|
503
|
+
shouldUseImportedPaletteMapping && isSemanticColourMapping(mapping) ? null : mapping;
|
|
504
|
+
|
|
505
|
+
if (isArbitraryValueClass) {
|
|
506
|
+
report.arbitraryValueUtilities.push(className);
|
|
507
|
+
if (!isSupported) {
|
|
508
|
+
report.unsupported.push(className);
|
|
509
|
+
}
|
|
510
|
+
continue;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
if (shouldUseImportedPaletteMapping) {
|
|
514
|
+
const suggestedClass = buildVariantClassName(variants, parsedTailwindColour.emilyBaseClass);
|
|
515
|
+
|
|
516
|
+
report.importedPaletteMappings.push({
|
|
517
|
+
from: className,
|
|
518
|
+
to: suggestedClass,
|
|
519
|
+
utility: parsedTailwindColour.utility,
|
|
520
|
+
tailwindPalette: parsedTailwindColour.tailwindPalette,
|
|
521
|
+
tailwindShade: parsedTailwindColour.tailwindShade,
|
|
522
|
+
emilyPalette: parsedTailwindColour.emilyPalette,
|
|
523
|
+
emilyShade: parsedTailwindColour.emilyShade,
|
|
524
|
+
hasOpacitySuffix: parsedTailwindColour.opacitySuffix !== null,
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
report.importedPalettes.push({
|
|
528
|
+
tailwindPalette: parsedTailwindColour.tailwindPalette,
|
|
529
|
+
emilyPalette: parsedTailwindColour.emilyPalette,
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
if (effectiveSemanticMapping) {
|
|
534
|
+
report.knownTailwind.push(className);
|
|
535
|
+
|
|
536
|
+
const replacementClass = buildVariantClassName(variants, effectiveSemanticMapping.replacement);
|
|
537
|
+
const hasChange = replacementClass !== className;
|
|
538
|
+
|
|
539
|
+
if (hasChange) {
|
|
540
|
+
report.replacements.push({
|
|
541
|
+
from: className,
|
|
542
|
+
to: replacementClass,
|
|
543
|
+
source: effectiveSemanticMapping.source,
|
|
544
|
+
matchedAlias: effectiveSemanticMapping.matchedAlias || null,
|
|
545
|
+
deprecated: effectiveSemanticMapping.deprecated === true,
|
|
546
|
+
metadata: effectiveSemanticMapping.metadata || {},
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
report.suggestions.push({
|
|
550
|
+
className,
|
|
551
|
+
suggestion: replacementClass,
|
|
552
|
+
reason: 'tailwind-mapping',
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
} else if (shouldUseImportedPaletteMapping) {
|
|
556
|
+
const replacementClass = buildVariantClassName(variants, parsedTailwindColour.emilyBaseClass);
|
|
557
|
+
|
|
558
|
+
report.knownTailwind.push(className);
|
|
559
|
+
report.replacements.push({
|
|
560
|
+
from: className,
|
|
561
|
+
to: replacementClass,
|
|
562
|
+
source: `${parsedTailwindColour.utility}-${parsedTailwindColour.tailwindPalette}-${parsedTailwindColour.tailwindShade}`,
|
|
563
|
+
matchedAlias: null,
|
|
564
|
+
deprecated: false,
|
|
565
|
+
metadata: {
|
|
566
|
+
category: 'imported-palette-colour',
|
|
567
|
+
emilyPalette: parsedTailwindColour.emilyPalette,
|
|
568
|
+
emilyShade: parsedTailwindColour.emilyShade,
|
|
569
|
+
hasOpacitySuffix: parsedTailwindColour.opacitySuffix !== null,
|
|
570
|
+
},
|
|
571
|
+
});
|
|
572
|
+
report.suggestions.push({
|
|
573
|
+
className,
|
|
574
|
+
suggestion: replacementClass,
|
|
575
|
+
reason: 'imported-palette-mapping',
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
if (!isSupported && !effectiveSemanticMapping) {
|
|
580
|
+
if (shouldUseImportedPaletteMapping) {
|
|
581
|
+
continue;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
report.unsupported.push(className);
|
|
585
|
+
|
|
586
|
+
if (utilitySet.size > 0) {
|
|
587
|
+
const suggestion = suggestClassName(className, utilitySet, variantSet);
|
|
588
|
+
if (suggestion) {
|
|
589
|
+
report.suggestions.push({
|
|
590
|
+
className,
|
|
591
|
+
suggestion,
|
|
592
|
+
reason: 'closest-emily-class',
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
report.found.sort();
|
|
600
|
+
report.supported.sort();
|
|
601
|
+
report.knownTailwind.sort();
|
|
602
|
+
report.unsupported.sort();
|
|
603
|
+
report.arbitraryValueUtilities.sort();
|
|
604
|
+
report.importedPalettes = dedupeBy(
|
|
605
|
+
report.importedPalettes,
|
|
606
|
+
(item) => `${item.tailwindPalette}->${item.emilyPalette}`,
|
|
607
|
+
);
|
|
608
|
+
report.importedPaletteMappings = dedupeBy(
|
|
609
|
+
report.importedPaletteMappings,
|
|
610
|
+
(item) => `${item.from}->${item.to}`,
|
|
611
|
+
);
|
|
612
|
+
|
|
613
|
+
return report;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function getFilesToScan(config, options = {}) {
|
|
617
|
+
const extensions = (config && config.purge && config.purge.extensions) || DEFAULT_EXTENSIONS;
|
|
618
|
+
const ignore = (config && config.purge && config.purge.ignore) || [];
|
|
619
|
+
|
|
620
|
+
if (options.sourceGlobs && options.sourceGlobs.length > 0) {
|
|
621
|
+
return fg.sync(options.sourceGlobs, {
|
|
622
|
+
ignore,
|
|
623
|
+
onlyFiles: true,
|
|
624
|
+
unique: true,
|
|
625
|
+
absolute: true,
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
if (config && config.purge && config.purge.sourceGlobs && config.purge.sourceGlobs.length > 0) {
|
|
630
|
+
return fg.sync(config.purge.sourceGlobs, {
|
|
631
|
+
ignore,
|
|
632
|
+
onlyFiles: true,
|
|
633
|
+
unique: true,
|
|
634
|
+
absolute: true,
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
const sourceDir =
|
|
639
|
+
options.sourceDir ||
|
|
640
|
+
(config && config.purge && config.purge.sourceDir) ||
|
|
641
|
+
'.';
|
|
642
|
+
|
|
643
|
+
const scanDir = path.isAbsolute(sourceDir)
|
|
644
|
+
? sourceDir
|
|
645
|
+
: path.join(process.cwd(), sourceDir);
|
|
646
|
+
|
|
647
|
+
if (!fs.existsSync(scanDir)) {
|
|
648
|
+
return [];
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
return getAllFiles(scanDir, extensions);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function dedupeBy(items, keyFn) {
|
|
655
|
+
const seen = new Set();
|
|
656
|
+
const result = [];
|
|
657
|
+
|
|
658
|
+
for (const item of items) {
|
|
659
|
+
const key = keyFn(item);
|
|
660
|
+
if (seen.has(key)) continue;
|
|
661
|
+
seen.add(key);
|
|
662
|
+
result.push(item);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
return result;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
function generateMigrationReport(options = {}) {
|
|
669
|
+
const config = readConfig(options) || {};
|
|
670
|
+
const manifestResult = loadManifest({ ...options, config });
|
|
671
|
+
const files = getFilesToScan(config, options);
|
|
672
|
+
|
|
673
|
+
const aggregate = {
|
|
674
|
+
found: new Set(),
|
|
675
|
+
supported: new Set(),
|
|
676
|
+
knownTailwind: new Set(),
|
|
677
|
+
unsupported: new Set(),
|
|
678
|
+
arbitraryValueUtilities: new Set(),
|
|
679
|
+
suggestions: [],
|
|
680
|
+
replacements: [],
|
|
681
|
+
importedPaletteMappings: [],
|
|
682
|
+
importedPalettes: [],
|
|
683
|
+
warnings: [...manifestResult.warnings],
|
|
684
|
+
};
|
|
685
|
+
|
|
686
|
+
if (files.length === 0) {
|
|
687
|
+
aggregate.warnings.push('No source files found for migration scan.');
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
const fileReports = [];
|
|
691
|
+
|
|
692
|
+
files.forEach((filePath) => {
|
|
693
|
+
try {
|
|
694
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
695
|
+
const report = migrateClasses(content, {
|
|
696
|
+
...options,
|
|
697
|
+
config,
|
|
698
|
+
manifest: manifestResult.manifest,
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
report.found.forEach((className) => aggregate.found.add(className));
|
|
702
|
+
report.supported.forEach((className) => aggregate.supported.add(className));
|
|
703
|
+
report.knownTailwind.forEach((className) => aggregate.knownTailwind.add(className));
|
|
704
|
+
report.unsupported.forEach((className) => aggregate.unsupported.add(className));
|
|
705
|
+
report.arbitraryValueUtilities.forEach((className) => aggregate.arbitraryValueUtilities.add(className));
|
|
706
|
+
aggregate.suggestions.push(...report.suggestions);
|
|
707
|
+
aggregate.replacements.push(...report.replacements);
|
|
708
|
+
aggregate.importedPaletteMappings.push(...report.importedPaletteMappings);
|
|
709
|
+
aggregate.importedPalettes.push(...report.importedPalettes);
|
|
710
|
+
aggregate.warnings.push(...report.warnings);
|
|
711
|
+
|
|
712
|
+
fileReports.push({
|
|
713
|
+
file: filePath,
|
|
714
|
+
report,
|
|
715
|
+
});
|
|
716
|
+
} catch (error) {
|
|
717
|
+
aggregate.warnings.push(`Could not read ${filePath}: ${error.message}`);
|
|
718
|
+
}
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
return {
|
|
722
|
+
files,
|
|
723
|
+
fileReports,
|
|
724
|
+
found: Array.from(aggregate.found).sort(),
|
|
725
|
+
supported: Array.from(aggregate.supported).sort(),
|
|
726
|
+
knownTailwind: Array.from(aggregate.knownTailwind).sort(),
|
|
727
|
+
unsupported: Array.from(aggregate.unsupported).sort(),
|
|
728
|
+
arbitraryValueUtilities: Array.from(aggregate.arbitraryValueUtilities).sort(),
|
|
729
|
+
suggestions: dedupeBy(aggregate.suggestions, (item) => `${item.className}->${item.suggestion}`),
|
|
730
|
+
replacements: dedupeBy(aggregate.replacements, (item) => `${item.from}->${item.to}`),
|
|
731
|
+
importedPaletteMappings: dedupeBy(
|
|
732
|
+
aggregate.importedPaletteMappings,
|
|
733
|
+
(item) => `${item.from}->${item.to}`,
|
|
734
|
+
),
|
|
735
|
+
importedPalettes: dedupeBy(
|
|
736
|
+
aggregate.importedPalettes,
|
|
737
|
+
(item) => `${item.tailwindPalette}->${item.emilyPalette}`,
|
|
738
|
+
),
|
|
739
|
+
importedPalettesConfig:
|
|
740
|
+
options.importColours === true
|
|
741
|
+
? buildImportedPalettesConfigBlock(
|
|
742
|
+
dedupeBy(
|
|
743
|
+
aggregate.importedPalettes,
|
|
744
|
+
(item) => `${item.tailwindPalette}->${item.emilyPalette}`,
|
|
745
|
+
),
|
|
746
|
+
)
|
|
747
|
+
: null,
|
|
748
|
+
warnings: Array.from(new Set(aggregate.warnings)),
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
module.exports = {
|
|
753
|
+
migrateClasses,
|
|
754
|
+
loadManifest,
|
|
755
|
+
generateMigrationReport,
|
|
756
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
function pushListSection(lines, title, items, limit = 20) {
|
|
2
|
+
if (!Array.isArray(items) || items.length === 0) {
|
|
3
|
+
return;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
lines.push('', `${title}:`);
|
|
7
|
+
items.slice(0, limit).forEach((item) => {
|
|
8
|
+
lines.push(` - ${item}`);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
if (items.length > limit) {
|
|
12
|
+
lines.push(` ...and ${items.length - limit} more`);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function formatMigrationReport(report, options = {}) {
|
|
17
|
+
const importColours = options.importColours === true;
|
|
18
|
+
const lines = [
|
|
19
|
+
'',
|
|
20
|
+
'EmilyCSS migration report',
|
|
21
|
+
` Files scanned: ${report.files.length}`,
|
|
22
|
+
` Total classes found: ${report.found.length}`,
|
|
23
|
+
` Supported EmilyCSS classes: ${report.supported.length}`,
|
|
24
|
+
` Known Tailwind classes: ${report.knownTailwind.length}`,
|
|
25
|
+
` Suggested replacements: ${report.replacements.length}`,
|
|
26
|
+
` Unsupported classes: ${report.unsupported.length}`,
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
if (importColours) {
|
|
30
|
+
lines.push(` Migration mode: imported-palettes`);
|
|
31
|
+
lines.push(` Imported palettes detected: ${report.importedPalettes.length}`);
|
|
32
|
+
} else {
|
|
33
|
+
lines.push(` Migration mode: semantic`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
pushListSection(
|
|
37
|
+
lines,
|
|
38
|
+
'Suggested replacements',
|
|
39
|
+
report.replacements.map((entry) => `${entry.from} -> ${entry.to}`),
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
pushListSection(lines, 'Unsupported classes', report.unsupported);
|
|
43
|
+
pushListSection(lines, 'Unsupported arbitrary value utilities detected', report.arbitraryValueUtilities);
|
|
44
|
+
|
|
45
|
+
if (report.warnings.length > 0) {
|
|
46
|
+
lines.push('', 'Warnings:');
|
|
47
|
+
report.warnings.forEach((warning) => {
|
|
48
|
+
lines.push(` - ${warning}`);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (importColours) {
|
|
53
|
+
pushListSection(
|
|
54
|
+
lines,
|
|
55
|
+
'Imported palette colour mappings',
|
|
56
|
+
report.importedPaletteMappings.map((entry) => `${entry.from} -> ${entry.to}`),
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
if (report.importedPalettesConfig) {
|
|
60
|
+
lines.push('', 'Suggested importedPalettes config:');
|
|
61
|
+
lines.push(report.importedPalettesConfig);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
lines.push('');
|
|
66
|
+
return lines.join('\n');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
module.exports = {
|
|
70
|
+
formatMigrationReport,
|
|
71
|
+
};
|