emily-css 1.2.4 → 1.2.6
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 +30 -6
- package/README.md +19 -6
- package/bin/emilyui.js +10 -0
- package/package.json +7 -2
- package/src/constants.js +5 -0
- package/src/doctor.js +88 -7
- package/src/index.js +12 -8
- package/src/info.js +94 -0
- package/src/init.js +13 -1
- package/src/migrate.js +17 -6
- package/src/purge.js +14 -7
- package/src/purgeConfig.js +191 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,35 @@ All notable changes to `emily-css` are documented here.
|
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
+
## v1.2.6 — May 2026
|
|
8
|
+
|
|
9
|
+
**Fix migration scanner false positives by ignoring prose tokens, JS property access, and dynamic placeholders while keeping valid utility classes and arbitrary value support.**
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
- fix: tighten migration class filtering
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
## v1.2.5 — May 2026
|
|
16
|
+
|
|
17
|
+
**Node 18+ Compatibility and Docs/CLI Alignment**
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
- fix(release): align Node support, CLI scripts, and README with current behavior
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
## v1.3.0 -- May 2026
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
- **ARIA state variants**: Added `aria-checked:` variant prefix, generating CSS rules targeting `[aria-checked="true"]`. Completes the ARIA variant set alongside `aria-expanded`, `aria-selected`, `aria-current`, and `aria-disabled`.
|
|
27
|
+
- **Data-state variants**: Added `data-checked:`, `data-unchecked:`, `data-active:`, and `data-inactive:` variant prefixes targeting `[data-state="*"]` selectors. Used by Radix UI, Headless UI, and similar headless component libraries. Completes the data-state set alongside `data-open:` and `data-closed:`.
|
|
28
|
+
- **`emily-css info` command**: New CLI command. Shows a project overview without triggering a build -- version, framework detection, output paths, CSS file sizes, source globs, file count, colour/spacing/font summary.
|
|
29
|
+
- **Doctor contrast warnings**: `emily-css doctor` now checks configured brand colour shades against standard light and dark backgrounds using the WCAG relative luminance formula. Flags shade/background pairings below 4.5:1 contrast. Non-blocking (warning, not error).
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
- `BASE_VARIANTS` in `src/constants.js` updated to include the five new variant names so `doctor` and `manifest` recognise them.
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
7
36
|
## v1.2.4 — May 2026
|
|
8
37
|
|
|
9
38
|
**chore: release v1.2.4**
|
|
@@ -426,9 +455,4 @@ All notable changes to `emily-css` are documented here.
|
|
|
426
455
|
- 11,844 utility classes generated from `emily.config.json`
|
|
427
456
|
- OKLCH colour scale generation — one hex in, 10-shade scale out
|
|
428
457
|
- Responsive variants (`sm:` `md:` `lg:` `xl:` `2xl:`)
|
|
429
|
-
- State variants (`hover:` `focus-visible:` `active:` `
|
|
430
|
-
- Purge system — strips unused classes, ~97% file size reduction
|
|
431
|
-
- Interactive setup wizard (`npx emily-css init`)
|
|
432
|
-
- 72 tests, all passing
|
|
433
|
-
|
|
434
|
-
---
|
|
458
|
+
- State variants (`hover:` `focus-visible:` `active:` `disab
|
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ emilyCSS lets you define design tokens in `emily.config.json` and generate stati
|
|
|
12
12
|
- Framework-agnostic output (`dist/emily.css` and `dist/emily.min.css`)
|
|
13
13
|
- Accessibility-focused utility coverage (focus rings, visually-hidden helpers, motion-aware variants)
|
|
14
14
|
- Tooling support with manifest and IntelliSense JSON generation
|
|
15
|
-
- CommonJS package with Node
|
|
15
|
+
- CommonJS package with Node 18+ compatibility
|
|
16
16
|
|
|
17
17
|
## Install and basic workflow
|
|
18
18
|
|
|
@@ -36,19 +36,36 @@ npx emily-css init
|
|
|
36
36
|
npx emily-css build
|
|
37
37
|
npx emily-css build --profile
|
|
38
38
|
npx emily-css watch
|
|
39
|
+
npx emily-css info
|
|
39
40
|
npx emily-css doctor
|
|
40
41
|
npx emily-css migrate
|
|
41
42
|
npx emily-css migrate --import-colours
|
|
43
|
+
npx emily-css manifest
|
|
42
44
|
npx emily-css showcase
|
|
43
45
|
npx emily-css help
|
|
44
46
|
npx emily-css version
|
|
45
47
|
```
|
|
46
48
|
|
|
49
|
+
Equivalent npm scripts (when added by `emily-css init`):
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
npm run emily:build
|
|
53
|
+
npm run emily:watch
|
|
54
|
+
npm run emily:doctor
|
|
55
|
+
npm run emily:migrate
|
|
56
|
+
npm run emily:info
|
|
57
|
+
npm run emily:manifest
|
|
58
|
+
npm run emily:showcase
|
|
59
|
+
npm run emily:version
|
|
60
|
+
npm run emily:help
|
|
61
|
+
```
|
|
62
|
+
|
|
47
63
|
## Doctor and migrate
|
|
48
64
|
|
|
49
65
|
- `doctor` checks for unknown EmilyCSS classes and variants.
|
|
50
66
|
- `doctor` now also reports non-failing accessibility warnings (for example obvious focus-removal or same-token text/background patterns).
|
|
51
67
|
- `migrate` is report-only and helps plan Tailwind-to-Emily migrations without modifying files.
|
|
68
|
+
- For best migrate accuracy, generate the full framework/manifest first (`emily-css build --keep-full` or enable `manifest: true`).
|
|
52
69
|
|
|
53
70
|
## Manifest and IntelliSense JSON
|
|
54
71
|
|
|
@@ -95,13 +112,9 @@ These files are intended for tooling, audits, and editor integrations. A VSCode
|
|
|
95
112
|
## Notes on compatibility
|
|
96
113
|
|
|
97
114
|
- Package format: CommonJS
|
|
98
|
-
- Runtime support: Node
|
|
115
|
+
- Runtime support: Node 18+
|
|
99
116
|
- ESM-only major upgrades are intentionally avoided where they would break compatibility
|
|
100
117
|
|
|
101
|
-
## Documentation stubs
|
|
102
|
-
|
|
103
|
-
Starter docs are available in [`docs/`](./docs) for installation, configuration, variants, accessibility, doctor, migrate, manifest, and IntelliSense.
|
|
104
|
-
|
|
105
118
|
## License
|
|
106
119
|
|
|
107
120
|
MIT
|
package/bin/emilyui.js
CHANGED
|
@@ -13,6 +13,7 @@ const usageText = `
|
|
|
13
13
|
emily-css build Generate production CSS to the configured output path
|
|
14
14
|
--profile Print coarse build timing information
|
|
15
15
|
emily-css watch Dev mode: rebuild on changes
|
|
16
|
+
emily-css info Show project config and CSS stats
|
|
16
17
|
emily-css doctor Scan project files for unknown EmilyCSS classes
|
|
17
18
|
emily-css migrate Generate a Tailwind-to-EmilyCSS migration report
|
|
18
19
|
--import-colours Detect Tailwind colour palettes and suggest importedPalettes config
|
|
@@ -35,6 +36,9 @@ if (command === "init") {
|
|
|
35
36
|
require("../src/watch.js");
|
|
36
37
|
} else if (command === "showcase") {
|
|
37
38
|
require("../src/showcase.js");
|
|
39
|
+
} else if (command === "info") {
|
|
40
|
+
const { info } = require("../src/info.js");
|
|
41
|
+
info();
|
|
38
42
|
} else if (command === "doctor") {
|
|
39
43
|
const { doctor } = require("../src/doctor.js");
|
|
40
44
|
const result = doctor();
|
|
@@ -69,6 +73,7 @@ if (command === "init") {
|
|
|
69
73
|
emily-css build Generate production CSS to the configured output path
|
|
70
74
|
--profile Print coarse build timing information
|
|
71
75
|
emily-css watch Dev mode: watch for changes and rebuild
|
|
76
|
+
emily-css info Show project config, output paths, and CSS stats
|
|
72
77
|
emily-css doctor Scan project files for unknown EmilyCSS classes
|
|
73
78
|
emily-css migrate Generate a Tailwind-to-EmilyCSS migration report
|
|
74
79
|
--import-colours Detect Tailwind colour palettes and suggest importedPalettes config
|
|
@@ -80,7 +85,12 @@ if (command === "init") {
|
|
|
80
85
|
npm scripts (added by init):
|
|
81
86
|
npm run emily:build Same as emily-css build
|
|
82
87
|
npm run emily:watch Same as emily-css watch
|
|
88
|
+
npm run emily:doctor Same as emily-css doctor
|
|
89
|
+
npm run emily:migrate Same as emily-css migrate
|
|
90
|
+
npm run emily:info Same as emily-css info
|
|
91
|
+
npm run emily:manifest Same as emily-css manifest
|
|
83
92
|
npm run emily:showcase Same as emily-css showcase
|
|
93
|
+
npm run emily:version Same as emily-css version
|
|
84
94
|
npm run emily:help Same as emily-css help
|
|
85
95
|
|
|
86
96
|
Docs: https://emilyui.dev
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "emily-css",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.6",
|
|
4
4
|
"description": "A config-driven utility CSS framework. Define your brand once, generate the CSS.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -26,6 +26,11 @@
|
|
|
26
26
|
"ship": "node scripts/ship.js",
|
|
27
27
|
"emily:build": "emily-css build",
|
|
28
28
|
"emily:watch": "emily-css watch",
|
|
29
|
+
"emily:doctor": "emily-css doctor",
|
|
30
|
+
"emily:migrate": "emily-css migrate",
|
|
31
|
+
"emily:info": "emily-css info",
|
|
32
|
+
"emily:manifest": "emily-css manifest",
|
|
33
|
+
"emily:version": "emily-css version",
|
|
29
34
|
"emily:help": "emily-css help"
|
|
30
35
|
},
|
|
31
36
|
"keywords": [
|
|
@@ -42,7 +47,7 @@
|
|
|
42
47
|
"author": "Andy Terry",
|
|
43
48
|
"license": "MIT",
|
|
44
49
|
"engines": {
|
|
45
|
-
"node": ">=
|
|
50
|
+
"node": ">=18.0.0"
|
|
46
51
|
},
|
|
47
52
|
"devDependencies": {
|
|
48
53
|
"nodemon": "^3.1.14"
|
package/src/constants.js
CHANGED
|
@@ -46,10 +46,15 @@ const BASE_VARIANTS = [
|
|
|
46
46
|
'motion-safe',
|
|
47
47
|
'aria-expanded',
|
|
48
48
|
'aria-selected',
|
|
49
|
+
'aria-checked',
|
|
49
50
|
'aria-current',
|
|
50
51
|
'aria-disabled',
|
|
51
52
|
'data-open',
|
|
52
53
|
'data-closed',
|
|
54
|
+
'data-checked',
|
|
55
|
+
'data-unchecked',
|
|
56
|
+
'data-active',
|
|
57
|
+
'data-inactive',
|
|
53
58
|
'dark',
|
|
54
59
|
'forced-colors',
|
|
55
60
|
];
|
package/src/doctor.js
CHANGED
|
@@ -4,8 +4,9 @@ const fs = require("fs");
|
|
|
4
4
|
const path = require("path");
|
|
5
5
|
const fg = require("fast-glob");
|
|
6
6
|
const { extractClassNames } = require("./purge.js");
|
|
7
|
-
const { ensureFullFramework, generateManifest } = require("./index.js");
|
|
7
|
+
const { ensureFullFramework, generateManifest, generateColourScale } = require("./index.js");
|
|
8
8
|
const { DEFAULT_EXTENSIONS } = require("./constants.js");
|
|
9
|
+
const { resolvePurgeConfig } = require("./purgeConfig.js");
|
|
9
10
|
const {
|
|
10
11
|
getConfig,
|
|
11
12
|
getFullCssPath,
|
|
@@ -111,18 +112,20 @@ function suggestClassName(className, utilitySet, variantSet) {
|
|
|
111
112
|
}
|
|
112
113
|
|
|
113
114
|
function getFilesToScan(config) {
|
|
114
|
-
const
|
|
115
|
-
const
|
|
115
|
+
const resolvedPurgeConfig = resolvePurgeConfig(config);
|
|
116
|
+
const extensions = resolvedPurgeConfig.extensions || DEFAULT_EXTENSIONS;
|
|
117
|
+
const ignore = resolvedPurgeConfig.ignore || [];
|
|
118
|
+
const sourceGlobs = resolvedPurgeConfig.sourceGlobs || [];
|
|
116
119
|
|
|
117
|
-
if (
|
|
118
|
-
return fg.sync(
|
|
120
|
+
if (sourceGlobs.length > 0) {
|
|
121
|
+
return fg.sync(sourceGlobs, {
|
|
119
122
|
ignore,
|
|
120
123
|
onlyFiles: true,
|
|
121
124
|
unique: true,
|
|
122
125
|
});
|
|
123
126
|
}
|
|
124
127
|
|
|
125
|
-
const sourceDir =
|
|
128
|
+
const sourceDir = resolvedPurgeConfig.sourceDir || ".";
|
|
126
129
|
const scanDir = path.isAbsolute(sourceDir)
|
|
127
130
|
? sourceDir
|
|
128
131
|
: path.join(process.cwd(), sourceDir);
|
|
@@ -244,6 +247,79 @@ function createAccessibilityWarnings(filePath, content) {
|
|
|
244
247
|
return warnings;
|
|
245
248
|
}
|
|
246
249
|
|
|
250
|
+
// ─── Colour contrast helpers ──────────────────────────────────────────────────
|
|
251
|
+
|
|
252
|
+
function hexToRelativeLuminance(hex) {
|
|
253
|
+
const toLinear = (c) => {
|
|
254
|
+
const v = parseInt(c, 16) / 255;
|
|
255
|
+
return v <= 0.04045 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
|
|
256
|
+
};
|
|
257
|
+
const r = toLinear(hex.slice(1, 3));
|
|
258
|
+
const g = toLinear(hex.slice(3, 5));
|
|
259
|
+
const b = toLinear(hex.slice(5, 7));
|
|
260
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function contrastRatio(hexA, hexB) {
|
|
264
|
+
const lA = hexToRelativeLuminance(hexA);
|
|
265
|
+
const lB = hexToRelativeLuminance(hexB);
|
|
266
|
+
const lighter = Math.max(lA, lB);
|
|
267
|
+
const darker = Math.min(lA, lB);
|
|
268
|
+
return (lighter + 0.05) / (darker + 0.05);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Check configured brand colours against standard light/dark backgrounds.
|
|
272
|
+
// Shades 60–80 are the most common text-on-light use case.
|
|
273
|
+
// Shades 20–40 are the most common text-on-dark use case.
|
|
274
|
+
function createContrastWarnings(config) {
|
|
275
|
+
const warnings = [];
|
|
276
|
+
const colours = config.colours || {};
|
|
277
|
+
const LIGHT_BG = '#FAFAFA';
|
|
278
|
+
const DARK_BG = '#1A1A1A';
|
|
279
|
+
const THRESHOLD_NORMAL = 4.5;
|
|
280
|
+
|
|
281
|
+
Object.entries(colours).forEach(([name, baseHex]) => {
|
|
282
|
+
let scale;
|
|
283
|
+
try {
|
|
284
|
+
scale = generateColourScale(baseHex);
|
|
285
|
+
} catch {
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Shades typically used as text on light backgrounds
|
|
290
|
+
[60, 70, 80].forEach((shade) => {
|
|
291
|
+
const hex = scale[shade];
|
|
292
|
+
if (!hex) return;
|
|
293
|
+
const ratio = contrastRatio(hex, LIGHT_BG);
|
|
294
|
+
if (ratio < THRESHOLD_NORMAL) {
|
|
295
|
+
warnings.push({
|
|
296
|
+
file: 'emily.config.json',
|
|
297
|
+
reason: 'low-contrast-token',
|
|
298
|
+
className: `text-${name}-${shade}`,
|
|
299
|
+
message: `text-${name}-${shade} on a light background has ~${ratio.toFixed(1)}:1 contrast (WCAG AA needs 4.5:1 for normal text). Consider using a darker shade.`,
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
// Shades typically used as text on dark backgrounds
|
|
305
|
+
[20, 30, 40].forEach((shade) => {
|
|
306
|
+
const hex = scale[shade];
|
|
307
|
+
if (!hex) return;
|
|
308
|
+
const ratio = contrastRatio(hex, DARK_BG);
|
|
309
|
+
if (ratio < THRESHOLD_NORMAL) {
|
|
310
|
+
warnings.push({
|
|
311
|
+
file: 'emily.config.json',
|
|
312
|
+
reason: 'low-contrast-token',
|
|
313
|
+
className: `text-${name}-${shade}`,
|
|
314
|
+
message: `text-${name}-${shade} on a dark background has ~${ratio.toFixed(1)}:1 contrast (WCAG AA needs 4.5:1 for normal text). Consider using a lighter shade.`,
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
return warnings;
|
|
321
|
+
}
|
|
322
|
+
|
|
247
323
|
function doctor() {
|
|
248
324
|
const config = getConfig();
|
|
249
325
|
|
|
@@ -304,8 +380,10 @@ function doctor() {
|
|
|
304
380
|
}
|
|
305
381
|
});
|
|
306
382
|
|
|
383
|
+
warnings.push(...createContrastWarnings(config));
|
|
384
|
+
|
|
307
385
|
if (issues.length === 0 && warnings.length === 0) {
|
|
308
|
-
console.log("
|
|
386
|
+
console.log("\u2713 EmilyCSS doctor found no class issues");
|
|
309
387
|
return { ok: true, issues: [], warnings: [], exitCode: 0 };
|
|
310
388
|
}
|
|
311
389
|
|
|
@@ -347,4 +425,7 @@ module.exports = {
|
|
|
347
425
|
doctor,
|
|
348
426
|
normaliseClassForManifest,
|
|
349
427
|
suggestClassName,
|
|
428
|
+
hexToRelativeLuminance,
|
|
429
|
+
contrastRatio,
|
|
430
|
+
createContrastWarnings,
|
|
350
431
|
};
|
package/src/index.js
CHANGED
|
@@ -849,12 +849,17 @@ function generateSemanticColourUtilities(semanticColours) {
|
|
|
849
849
|
|
|
850
850
|
function addAriaDataVariants(css) {
|
|
851
851
|
const variants = [
|
|
852
|
-
{ name: 'aria-expanded',
|
|
853
|
-
{ name: 'aria-selected',
|
|
854
|
-
{ name: 'aria-
|
|
855
|
-
{ name: 'aria-
|
|
856
|
-
{ name: '
|
|
857
|
-
{ name: 'data-
|
|
852
|
+
{ name: 'aria-expanded', selector: '[aria-expanded="true"]' },
|
|
853
|
+
{ name: 'aria-selected', selector: '[aria-selected="true"]' },
|
|
854
|
+
{ name: 'aria-checked', selector: '[aria-checked="true"]' },
|
|
855
|
+
{ name: 'aria-current', selector: '[aria-current="page"]' },
|
|
856
|
+
{ name: 'aria-disabled', selector: '[aria-disabled="true"]' },
|
|
857
|
+
{ name: 'data-open', selector: '[data-state="open"]' },
|
|
858
|
+
{ name: 'data-closed', selector: '[data-state="closed"]' },
|
|
859
|
+
{ name: 'data-checked', selector: '[data-state="checked"]' },
|
|
860
|
+
{ name: 'data-unchecked', selector: '[data-state="unchecked"]' },
|
|
861
|
+
{ name: 'data-active', selector: '[data-state="active"]' },
|
|
862
|
+
{ name: 'data-inactive', selector: '[data-state="inactive"]' },
|
|
858
863
|
];
|
|
859
864
|
|
|
860
865
|
let variantCss = css;
|
|
@@ -1828,6 +1833,7 @@ module.exports = {
|
|
|
1828
1833
|
oklchToHex,
|
|
1829
1834
|
generateColourScale,
|
|
1830
1835
|
generateAllColours,
|
|
1836
|
+
generateFontCSS,
|
|
1831
1837
|
generateSpacing,
|
|
1832
1838
|
generateBorderUtilities,
|
|
1833
1839
|
generateColourUtilities,
|
|
@@ -1840,6 +1846,4 @@ module.exports = {
|
|
|
1840
1846
|
addAriaDataVariants,
|
|
1841
1847
|
addResponsiveVariants,
|
|
1842
1848
|
generateManifest,
|
|
1843
|
-
generateFontCSS,
|
|
1844
|
-
codeUtilities,
|
|
1845
1849
|
};
|
package/src/info.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const fg = require('fast-glob');
|
|
6
|
+
const {
|
|
7
|
+
getConfig,
|
|
8
|
+
getFullCssPath,
|
|
9
|
+
getProductionCssPath,
|
|
10
|
+
} = require('./config.js');
|
|
11
|
+
const { resolvePurgeConfig } = require('./purgeConfig.js');
|
|
12
|
+
|
|
13
|
+
function formatBytes(bytes) {
|
|
14
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
15
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function detectFramework(config) {
|
|
19
|
+
if (config.framework) return config.framework;
|
|
20
|
+
// fall back to output path heuristic
|
|
21
|
+
const outputPath = typeof config.output === 'string' ? config.output : (config.output && config.output.path) || '';
|
|
22
|
+
const out = outputPath.toLowerCase();
|
|
23
|
+
if (out.includes('public')) return 'Nuxt / Vue / Next.js';
|
|
24
|
+
return 'Static / Generic';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function getSourceGlobs(config) {
|
|
28
|
+
return resolvePurgeConfig(config).sourceGlobs;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function countSourceFiles(globs) {
|
|
32
|
+
try {
|
|
33
|
+
return fg.sync(globs, { onlyFiles: true, unique: true }).length;
|
|
34
|
+
} catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function info() {
|
|
40
|
+
let config;
|
|
41
|
+
try {
|
|
42
|
+
config = getConfig();
|
|
43
|
+
} catch (err) {
|
|
44
|
+
console.error('emily-css info: could not load emily.config.json');
|
|
45
|
+
console.error(err.message);
|
|
46
|
+
process.exitCode = 1;
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const packageJson = require(path.join(__dirname, '..', 'package.json'));
|
|
51
|
+
const fullCssPath = getFullCssPath(config);
|
|
52
|
+
const productionCssPath = getProductionCssPath(config);
|
|
53
|
+
const globs = getSourceGlobs(config);
|
|
54
|
+
const fileCount = countSourceFiles(globs);
|
|
55
|
+
|
|
56
|
+
const fullSizeStr = fs.existsSync(fullCssPath)
|
|
57
|
+
? formatBytes(fs.statSync(fullCssPath).size)
|
|
58
|
+
: 'not built';
|
|
59
|
+
|
|
60
|
+
const prodSizeStr = fs.existsSync(productionCssPath)
|
|
61
|
+
? formatBytes(fs.statSync(productionCssPath).size)
|
|
62
|
+
: 'not built';
|
|
63
|
+
|
|
64
|
+
const colourNames = config.colours ? Object.keys(config.colours) : [];
|
|
65
|
+
const semanticNames = config.semanticColours ? Object.keys(config.semanticColours) : [];
|
|
66
|
+
const colourSummary = colourNames.length
|
|
67
|
+
? colourNames.join(', ') + (semanticNames.length ? ` + ${semanticNames.length} semantic` : '')
|
|
68
|
+
: 'none';
|
|
69
|
+
|
|
70
|
+
const fontConfig = config.fontFamily || {};
|
|
71
|
+
const headingFont = typeof fontConfig === 'object' ? (fontConfig.heading || 'system') : fontConfig;
|
|
72
|
+
const bodyFont = typeof fontConfig === 'object' ? (fontConfig.body || 'system') : fontConfig;
|
|
73
|
+
|
|
74
|
+
const lines = [
|
|
75
|
+
`emily-css v${packageJson.version}`,
|
|
76
|
+
'',
|
|
77
|
+
` Project: ${detectFramework(config)}`,
|
|
78
|
+
` Config: emily.config.json`,
|
|
79
|
+
` Output: ${path.relative(process.cwd(), fullCssPath)} (full)`,
|
|
80
|
+
` ${path.relative(process.cwd(), productionCssPath)} (production)`,
|
|
81
|
+
` CSS size: ${fullSizeStr} (full) / ${prodSizeStr} (production)`,
|
|
82
|
+
` Source globs: ${globs.join(', ')}`,
|
|
83
|
+
` Files found: ${fileCount !== null ? fileCount : 'unknown'}`,
|
|
84
|
+
'',
|
|
85
|
+
` Colours: ${colourSummary}`,
|
|
86
|
+
` Spacing: ${config.spacing && config.spacing.scale ? Object.keys(config.spacing.scale).length : 0} steps`,
|
|
87
|
+
` Fonts: ${headingFont} (heading), ${bodyFont} (body)`,
|
|
88
|
+
'',
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
console.log(lines.join('\n'));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
module.exports = { info };
|
package/src/init.js
CHANGED
|
@@ -154,6 +154,11 @@ function addEmilyScriptsToPackageJson() {
|
|
|
154
154
|
const scripts = {
|
|
155
155
|
"emily:build": "emily-css build",
|
|
156
156
|
"emily:watch": "emily-css watch",
|
|
157
|
+
"emily:doctor": "emily-css doctor",
|
|
158
|
+
"emily:migrate": "emily-css migrate",
|
|
159
|
+
"emily:info": "emily-css info",
|
|
160
|
+
"emily:manifest": "emily-css manifest",
|
|
161
|
+
"emily:version": "emily-css version",
|
|
157
162
|
"emily:help": "emily-css help",
|
|
158
163
|
"emily:showcase": "emily-css showcase",
|
|
159
164
|
};
|
|
@@ -288,7 +293,7 @@ function detectProject() {
|
|
|
288
293
|
sourceDir: ".",
|
|
289
294
|
outputPath: "dist/emily.min.css",
|
|
290
295
|
sourceGlobs: [
|
|
291
|
-
"./**/*.{html,htm,twig,njk,liquid,hbs,php,astro,svelte,vue,
|
|
296
|
+
"./**/*.{html,htm,twig,njk,liquid,hbs,php,astro,svelte,vue,blade.php,jinja,jinja2,j2}",
|
|
292
297
|
],
|
|
293
298
|
linkHint: '<link rel="stylesheet" href="./dist/emily.min.css">',
|
|
294
299
|
};
|
|
@@ -325,6 +330,8 @@ function createDefaultConfig({
|
|
|
325
330
|
fullCss: "dist/emily.css",
|
|
326
331
|
},
|
|
327
332
|
|
|
333
|
+
manifest: true,
|
|
334
|
+
|
|
328
335
|
colours,
|
|
329
336
|
|
|
330
337
|
semanticColours: {
|
|
@@ -721,6 +728,11 @@ async function init() {
|
|
|
721
728
|
? "\n\nScripts added:\n" +
|
|
722
729
|
chalk.cyan(" npm run emily:build\n") +
|
|
723
730
|
chalk.cyan(" npm run emily:watch\n") +
|
|
731
|
+
chalk.cyan(" npm run emily:doctor\n") +
|
|
732
|
+
chalk.cyan(" npm run emily:migrate\n") +
|
|
733
|
+
chalk.cyan(" npm run emily:info\n") +
|
|
734
|
+
chalk.cyan(" npm run emily:manifest\n") +
|
|
735
|
+
chalk.cyan(" npm run emily:version\n") +
|
|
724
736
|
chalk.cyan(" npm run emily:showcase\n") +
|
|
725
737
|
chalk.cyan(" npm run emily:help")
|
|
726
738
|
: ""),
|
package/src/migrate.js
CHANGED
|
@@ -6,6 +6,7 @@ const { extractClassNames, getAllFiles } = require('./purge.js');
|
|
|
6
6
|
const { generateManifest } = require('./manifest.js');
|
|
7
7
|
const { normaliseClassForManifest, suggestClassName } = require('./doctor.js');
|
|
8
8
|
const { DEFAULT_EXTENSIONS } = require('./constants.js');
|
|
9
|
+
const { resolvePurgeConfig } = require('./purgeConfig.js');
|
|
9
10
|
const MIGRATION_DEFAULT_EXTENSIONS = [...DEFAULT_EXTENSIONS, '.mdx'];
|
|
10
11
|
|
|
11
12
|
const TAILWIND_MAPPINGS = {
|
|
@@ -573,6 +574,13 @@ function isLikelyUtilityClass(className) {
|
|
|
573
574
|
if (/[;()={},`]/.test(className)) return false;
|
|
574
575
|
if (!/[a-zA-Z]/.test(className)) return false;
|
|
575
576
|
if (!/^[a-zA-Z0-9:#_./\-[\]]+$/.test(className)) return false;
|
|
577
|
+
|
|
578
|
+
/*
|
|
579
|
+
* Ignore obvious prose / JS expressions
|
|
580
|
+
*/
|
|
581
|
+
if (/[.]+$/.test(className)) return false;
|
|
582
|
+
if (/^[[][^:\]]+[]]$/.test(className)) return false;
|
|
583
|
+
if (/^[a-zA-Z]+\.[a-zA-Z0-9_$]+$/.test(className)) return false;
|
|
576
584
|
if (/^[a-z]+$/.test(className) && !SINGLE_WORD_UTILITY_ALLOWLIST.has(className)) return false;
|
|
577
585
|
if (!hasUtilityLikeSyntax(className) && !SINGLE_WORD_UTILITY_ALLOWLIST.has(className)) return false;
|
|
578
586
|
|
|
@@ -740,8 +748,11 @@ function migrateClasses(input, options = {}) {
|
|
|
740
748
|
}
|
|
741
749
|
|
|
742
750
|
function getFilesToScan(config, options = {}) {
|
|
743
|
-
const
|
|
744
|
-
const
|
|
751
|
+
const resolvedPurgeConfig = resolvePurgeConfig(config || {});
|
|
752
|
+
const extensions = Array.from(
|
|
753
|
+
new Set([...(resolvedPurgeConfig.extensions || MIGRATION_DEFAULT_EXTENSIONS), '.mdx']),
|
|
754
|
+
);
|
|
755
|
+
const ignore = resolvedPurgeConfig.ignore || [];
|
|
745
756
|
|
|
746
757
|
if (options.sourceGlobs && options.sourceGlobs.length > 0) {
|
|
747
758
|
return fg.sync(options.sourceGlobs, {
|
|
@@ -752,8 +763,8 @@ function getFilesToScan(config, options = {}) {
|
|
|
752
763
|
});
|
|
753
764
|
}
|
|
754
765
|
|
|
755
|
-
if (
|
|
756
|
-
return fg.sync(
|
|
766
|
+
if (resolvedPurgeConfig.sourceGlobs && resolvedPurgeConfig.sourceGlobs.length > 0) {
|
|
767
|
+
return fg.sync(resolvedPurgeConfig.sourceGlobs, {
|
|
757
768
|
ignore,
|
|
758
769
|
onlyFiles: true,
|
|
759
770
|
unique: true,
|
|
@@ -763,7 +774,7 @@ function getFilesToScan(config, options = {}) {
|
|
|
763
774
|
|
|
764
775
|
const sourceDir =
|
|
765
776
|
options.sourceDir ||
|
|
766
|
-
|
|
777
|
+
resolvedPurgeConfig.sourceDir ||
|
|
767
778
|
'.';
|
|
768
779
|
|
|
769
780
|
const scanDir = path.isAbsolute(sourceDir)
|
|
@@ -879,4 +890,4 @@ module.exports = {
|
|
|
879
890
|
migrateClasses,
|
|
880
891
|
loadManifest,
|
|
881
892
|
generateMigrationReport,
|
|
882
|
-
};
|
|
893
|
+
};
|
package/src/purge.js
CHANGED
|
@@ -4,6 +4,7 @@ const fs = require("fs");
|
|
|
4
4
|
const path = require("path");
|
|
5
5
|
const fg = require("fast-glob");
|
|
6
6
|
const { DEFAULT_EXTENSIONS, DEFAULT_PURGE_IGNORE } = require("./constants.js");
|
|
7
|
+
const { resolvePurgeConfig } = require("./purgeConfig.js");
|
|
7
8
|
|
|
8
9
|
function getAllFiles(dir, extensions = DEFAULT_EXTENSIONS) {
|
|
9
10
|
const absoluteDir = path.isAbsolute(dir) ? dir : path.resolve(dir);
|
|
@@ -175,22 +176,27 @@ function purgeBlock(block, usedClasses) {
|
|
|
175
176
|
}
|
|
176
177
|
|
|
177
178
|
function getFilesForPurge(scanDir, config, extensions) {
|
|
178
|
-
|
|
179
|
+
const resolvedPurgeConfig = resolvePurgeConfig(config);
|
|
180
|
+
const sourceGlobs = resolvedPurgeConfig.sourceGlobs || [];
|
|
181
|
+
|
|
182
|
+
if (sourceGlobs.length) {
|
|
179
183
|
console.log(`\n🔍 Scanning using sourceGlobs`);
|
|
180
|
-
|
|
184
|
+
sourceGlobs.forEach((glob) => console.log(` - ${glob}`));
|
|
181
185
|
console.log(` Extensions: ${extensions.join(", ")}`);
|
|
182
186
|
|
|
183
|
-
return fg.sync(
|
|
184
|
-
ignore:
|
|
187
|
+
return fg.sync(sourceGlobs, {
|
|
188
|
+
ignore: resolvedPurgeConfig.ignore || [],
|
|
185
189
|
onlyFiles: true,
|
|
186
190
|
unique: true,
|
|
187
191
|
});
|
|
188
192
|
}
|
|
189
193
|
|
|
190
|
-
|
|
194
|
+
const fallbackScanDir = resolvedPurgeConfig.sourceDir || scanDir;
|
|
195
|
+
|
|
196
|
+
console.log(`\n🔍 Scanning fallback directory: ${fallbackScanDir}`);
|
|
191
197
|
console.log(` Extensions: ${extensions.join(", ")}`);
|
|
192
198
|
|
|
193
|
-
return getAllFiles(
|
|
199
|
+
return getAllFiles(fallbackScanDir, extensions);
|
|
194
200
|
}
|
|
195
201
|
|
|
196
202
|
function printFileSummary(files, extensions) {
|
|
@@ -214,7 +220,8 @@ function printFileSummary(files, extensions) {
|
|
|
214
220
|
}
|
|
215
221
|
|
|
216
222
|
function purgeCSS(css, scanDir, config) {
|
|
217
|
-
const
|
|
223
|
+
const resolvedPurgeConfig = resolvePurgeConfig(config);
|
|
224
|
+
const extensions = resolvedPurgeConfig.extensions || DEFAULT_EXTENSIONS;
|
|
218
225
|
const files = getFilesForPurge(scanDir, config, extensions);
|
|
219
226
|
|
|
220
227
|
printFileSummary(files, extensions);
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { DEFAULT_EXTENSIONS, DEFAULT_PURGE_IGNORE } = require('./constants.js');
|
|
6
|
+
|
|
7
|
+
const LEGACY_STATIC_GENERIC_GLOB = './**/*.{html,htm,twig,njk,liquid,hbs,php,astro,svelte,vue,js,ts}';
|
|
8
|
+
|
|
9
|
+
const PROJECT_PURGE_PRESETS = {
|
|
10
|
+
Nuxt: {
|
|
11
|
+
sourceDir: '.',
|
|
12
|
+
sourceGlobs: [
|
|
13
|
+
'./components/**/*.{vue,js,ts}',
|
|
14
|
+
'./pages/**/*.vue',
|
|
15
|
+
'./layouts/**/*.vue',
|
|
16
|
+
'./app.vue',
|
|
17
|
+
],
|
|
18
|
+
},
|
|
19
|
+
'Next.js': {
|
|
20
|
+
sourceDir: '.',
|
|
21
|
+
sourceGlobs: [
|
|
22
|
+
'./app/**/*.{js,jsx,ts,tsx}',
|
|
23
|
+
'./pages/**/*.{js,jsx,ts,tsx}',
|
|
24
|
+
'./components/**/*.{js,jsx,ts,tsx}',
|
|
25
|
+
'./src/**/*.{js,jsx,ts,tsx}',
|
|
26
|
+
],
|
|
27
|
+
},
|
|
28
|
+
React: {
|
|
29
|
+
sourceDir: './src',
|
|
30
|
+
sourceGlobs: [
|
|
31
|
+
'./src/**/*.{js,jsx,ts,tsx,html}',
|
|
32
|
+
'./components/**/*.{js,jsx,ts,tsx}',
|
|
33
|
+
'./public/**/*.html',
|
|
34
|
+
],
|
|
35
|
+
},
|
|
36
|
+
'Vue/Vite': {
|
|
37
|
+
sourceDir: './src',
|
|
38
|
+
sourceGlobs: [
|
|
39
|
+
'./src/**/*.{vue,js,jsx,ts,tsx,html}',
|
|
40
|
+
'./index.html',
|
|
41
|
+
],
|
|
42
|
+
},
|
|
43
|
+
Astro: {
|
|
44
|
+
sourceDir: './src',
|
|
45
|
+
sourceGlobs: [
|
|
46
|
+
'./src/**/*.{astro,html,md,mdx,js,jsx,ts,tsx,vue,svelte}',
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
Drupal: {
|
|
50
|
+
sourceDir: '.',
|
|
51
|
+
sourceGlobs: [
|
|
52
|
+
'./web/themes/custom/**/*.{twig,js,ts}',
|
|
53
|
+
'./templates/**/*.html.twig',
|
|
54
|
+
'./components/**/*.twig',
|
|
55
|
+
'./**/*.theme',
|
|
56
|
+
],
|
|
57
|
+
},
|
|
58
|
+
'Static/Generic': {
|
|
59
|
+
sourceDir: '.',
|
|
60
|
+
sourceGlobs: [
|
|
61
|
+
'./**/*.{html,htm,twig,njk,liquid,hbs,php,astro,svelte,vue,blade.php,jinja,jinja2,j2}',
|
|
62
|
+
],
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
function unique(values) {
|
|
67
|
+
return Array.from(new Set((values || []).filter(Boolean)));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function readPackageJson(cwd) {
|
|
71
|
+
const packagePath = path.join(cwd, 'package.json');
|
|
72
|
+
if (!fs.existsSync(packagePath)) return null;
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
return JSON.parse(fs.readFileSync(packagePath, 'utf8'));
|
|
76
|
+
} catch {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function hasDependency(packageJson, dependencyName) {
|
|
82
|
+
if (!packageJson) return false;
|
|
83
|
+
|
|
84
|
+
return Boolean(
|
|
85
|
+
packageJson.dependencies?.[dependencyName] ||
|
|
86
|
+
packageJson.devDependencies?.[dependencyName],
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function detectProjectType(cwd = process.cwd()) {
|
|
91
|
+
const packageJson = readPackageJson(cwd);
|
|
92
|
+
|
|
93
|
+
if (
|
|
94
|
+
fs.existsSync(path.join(cwd, 'nuxt.config.ts')) ||
|
|
95
|
+
fs.existsSync(path.join(cwd, 'nuxt.config.js')) ||
|
|
96
|
+
hasDependency(packageJson, 'nuxt')
|
|
97
|
+
) {
|
|
98
|
+
return 'Nuxt';
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (hasDependency(packageJson, 'next')) {
|
|
102
|
+
return 'Next.js';
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (hasDependency(packageJson, 'react')) {
|
|
106
|
+
return 'React';
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (
|
|
110
|
+
hasDependency(packageJson, 'vue') ||
|
|
111
|
+
fs.existsSync(path.join(cwd, 'vite.config.ts')) ||
|
|
112
|
+
fs.existsSync(path.join(cwd, 'vite.config.js'))
|
|
113
|
+
) {
|
|
114
|
+
return 'Vue/Vite';
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (
|
|
118
|
+
hasDependency(packageJson, 'astro') ||
|
|
119
|
+
fs.existsSync(path.join(cwd, 'astro.config.mjs'))
|
|
120
|
+
) {
|
|
121
|
+
return 'Astro';
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
let rootFiles = [];
|
|
125
|
+
try {
|
|
126
|
+
rootFiles = fs.readdirSync(cwd);
|
|
127
|
+
} catch {
|
|
128
|
+
return 'Static/Generic';
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const hasDrupalInfoFile = rootFiles.some((file) => file.endsWith('.info.yml'));
|
|
132
|
+
|
|
133
|
+
if (
|
|
134
|
+
hasDrupalInfoFile ||
|
|
135
|
+
fs.existsSync(path.join(cwd, 'web/core'))
|
|
136
|
+
) {
|
|
137
|
+
return 'Drupal';
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return 'Static/Generic';
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function normaliseGlob(glob) {
|
|
144
|
+
return String(glob || '').replace(/\\/g, '/').trim();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function isLegacyStaticGenericGlob(sourceGlobs) {
|
|
148
|
+
if (!Array.isArray(sourceGlobs) || sourceGlobs.length !== 1) return false;
|
|
149
|
+
const [onlyGlob] = sourceGlobs.map(normaliseGlob);
|
|
150
|
+
return onlyGlob === LEGACY_STATIC_GENERIC_GLOB;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function resolvePurgeConfig(config = {}, options = {}) {
|
|
154
|
+
const purge = config.purge || {};
|
|
155
|
+
const cwd = options.cwd || process.cwd();
|
|
156
|
+
const detectedProjectType = detectProjectType(cwd);
|
|
157
|
+
const projectType = purge.projectType || detectedProjectType;
|
|
158
|
+
const preset = PROJECT_PURGE_PRESETS[projectType] || PROJECT_PURGE_PRESETS['Static/Generic'];
|
|
159
|
+
|
|
160
|
+
const configuredSourceGlobs = Array.isArray(purge.sourceGlobs)
|
|
161
|
+
? purge.sourceGlobs.filter(Boolean)
|
|
162
|
+
: [];
|
|
163
|
+
|
|
164
|
+
let sourceGlobs = configuredSourceGlobs.length > 0
|
|
165
|
+
? configuredSourceGlobs
|
|
166
|
+
: preset.sourceGlobs;
|
|
167
|
+
|
|
168
|
+
if (projectType === 'Static/Generic' && isLegacyStaticGenericGlob(sourceGlobs)) {
|
|
169
|
+
sourceGlobs = preset.sourceGlobs;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const sourceDir = purge.sourceDir || preset.sourceDir || '.';
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
projectType,
|
|
176
|
+
sourceDir,
|
|
177
|
+
sourceGlobs,
|
|
178
|
+
ignore: unique([...DEFAULT_PURGE_IGNORE, ...(purge.ignore || [])]),
|
|
179
|
+
extensions: Array.isArray(purge.extensions) && purge.extensions.length > 0
|
|
180
|
+
? purge.extensions
|
|
181
|
+
: DEFAULT_EXTENSIONS,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
module.exports = {
|
|
186
|
+
PROJECT_PURGE_PRESETS,
|
|
187
|
+
LEGACY_STATIC_GENERIC_GLOB,
|
|
188
|
+
detectProjectType,
|
|
189
|
+
resolvePurgeConfig,
|
|
190
|
+
};
|
|
191
|
+
|