emily-css 1.2.3 → 1.2.5
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 +70 -25
- package/README.md +20 -7
- package/bin/emilyui.js +26 -0
- package/package.json +7 -2
- package/src/config.js +104 -0
- package/src/constants.js +5 -0
- package/src/doctor.js +96 -52
- package/src/index.js +25 -101
- package/src/info.js +94 -0
- package/src/init.js +13 -1
- package/src/migrate.js +9 -5
- package/src/purge.js +28 -33
- package/src/purgeConfig.js +191 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,34 +1,84 @@
|
|
|
1
|
-
# Changelog
|
|
2
|
-
|
|
3
|
-
All notable changes to `emily-css` are documented here.
|
|
4
|
-
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `emily-css` are documented here.
|
|
4
|
+
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
## v1.2.
|
|
7
|
+
## v1.2.5 — May 2026
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
**Node 18+ Compatibility and Docs/CLI Alignment**
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
- fix(release): align Node support, CLI scripts, and README with current behavior
|
|
10
13
|
|
|
11
14
|
---
|
|
12
|
-
## v1.
|
|
15
|
+
## v1.3.0 -- May 2026
|
|
13
16
|
|
|
14
17
|
### Added
|
|
15
|
-
- Added
|
|
16
|
-
- Added
|
|
17
|
-
-
|
|
18
|
-
-
|
|
18
|
+
- **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`.
|
|
19
|
+
- **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:`.
|
|
20
|
+
- **`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.
|
|
21
|
+
- **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).
|
|
19
22
|
|
|
20
23
|
### Changed
|
|
21
|
-
-
|
|
22
|
-
- Improved purge class extraction for complex variant patterns and safer junk filtering.
|
|
23
|
-
- Updated README to reflect current product direction and command surface.
|
|
24
|
-
|
|
25
|
-
### Notes
|
|
26
|
-
- EmilyCSS remains CommonJS-compatible and continues to support Node 16+.
|
|
27
|
-
- ESM-only dependency major upgrades remain intentionally deferred for compatibility.
|
|
24
|
+
- `BASE_VARIANTS` in `src/constants.js` updated to include the five new variant names so `doctor` and `manifest` recognise them.
|
|
28
25
|
|
|
29
26
|
---
|
|
30
27
|
|
|
31
|
-
## v1.2.
|
|
28
|
+
## v1.2.4 — May 2026
|
|
29
|
+
|
|
30
|
+
**chore: release v1.2.4**
|
|
31
|
+
|
|
32
|
+
### Changed
|
|
33
|
+
- chore: release v1.2.4
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
# Changelog
|
|
37
|
+
|
|
38
|
+
All notable changes to `emily-css` are documented here.
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## v1.2.4 — May 2026
|
|
43
|
+
|
|
44
|
+
### Changed
|
|
45
|
+
- Extracted shared config/path helpers (`getConfig`, `getFullCssPath`, `getManifestSettings`, etc.) into `src/config.js` to eliminate duplication between `src/index.js` and `src/doctor.js`. No behaviour change.
|
|
46
|
+
- Updated `purge.js` file discovery to use `fast-glob` (already a dependency) instead of manual recursive `fs.readdirSync`, and to respect `DEFAULT_PURGE_IGNORE` from `src/constants.js` rather than hardcoded ignores.
|
|
47
|
+
- Updated Node engine requirement to `>=22.0.0`. Node 16 and 18 are EOL.
|
|
48
|
+
|
|
49
|
+
### Added
|
|
50
|
+
- `emily-css manifest` CLI command. Generates the utility/token manifest JSON to the configured output path (default: `dist/emily.manifest.json`). The manifest feature existed internally — this wires it as a standalone command.
|
|
51
|
+
|
|
52
|
+
### Notes
|
|
53
|
+
- Current runtime support is Node 22+.
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## v1.2.3 — May 2026
|
|
58
|
+
|
|
59
|
+
****
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
## v1.2.2 — May 2026
|
|
63
|
+
|
|
64
|
+
### Added
|
|
65
|
+
- Added IntelliSense JSON generation via `intellisense` config output (`dist/emily.intellisense.json` by default).
|
|
66
|
+
- Added build profiling via `emily-css build --profile` with coarse timing buckets.
|
|
67
|
+
- Added initial accessibility warnings to `emily-css doctor` (focus removal, same token text/background, and `cursor-pointer` on non-interactive elements).
|
|
68
|
+
- Added documentation stubs in `docs/` for installation, configuration, variants, accessibility, doctor, migrate, manifest, and IntelliSense.
|
|
69
|
+
|
|
70
|
+
### Changed
|
|
71
|
+
- Stabilised manifest schema metadata with explicit `schemaVersion`, package name, and package version fields.
|
|
72
|
+
- Improved purge class extraction for complex variant patterns and safer junk filtering.
|
|
73
|
+
- Updated README to reflect current product direction and command surface.
|
|
74
|
+
|
|
75
|
+
### Notes
|
|
76
|
+
- EmilyCSS remains CommonJS-compatible. Node engine requirement updated to >=22 in v1.2.4.
|
|
77
|
+
- ESM-only dependency major upgrades remain intentionally deferred for compatibility.
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## v1.2.1 — May 2026
|
|
32
82
|
|
|
33
83
|
### Changed
|
|
34
84
|
- Refactored utility generators into smaller internal modules.
|
|
@@ -397,9 +447,4 @@ All notable changes to `emily-css` are documented here.
|
|
|
397
447
|
- 11,844 utility classes generated from `emily.config.json`
|
|
398
448
|
- OKLCH colour scale generation — one hex in, 10-shade scale out
|
|
399
449
|
- Responsive variants (`sm:` `md:` `lg:` `xl:` `2xl:`)
|
|
400
|
-
- State variants (`hover:` `focus-visible:` `active:` `
|
|
401
|
-
- Purge system — strips unused classes, ~97% file size reduction
|
|
402
|
-
- Interactive setup wizard (`npx emily-css init`)
|
|
403
|
-
- 72 tests, all passing
|
|
404
|
-
|
|
405
|
-
---
|
|
450
|
+
- 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
|
-
MIT
|
|
120
|
+
MIT
|
package/bin/emilyui.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
const fs = require("fs");
|
|
3
4
|
const path = require("path");
|
|
4
5
|
|
|
5
6
|
const command = process.argv[2];
|
|
@@ -12,9 +13,11 @@ const usageText = `
|
|
|
12
13
|
emily-css build Generate production CSS to the configured output path
|
|
13
14
|
--profile Print coarse build timing information
|
|
14
15
|
emily-css watch Dev mode: rebuild on changes
|
|
16
|
+
emily-css info Show project config and CSS stats
|
|
15
17
|
emily-css doctor Scan project files for unknown EmilyCSS classes
|
|
16
18
|
emily-css migrate Generate a Tailwind-to-EmilyCSS migration report
|
|
17
19
|
--import-colours Detect Tailwind colour palettes and suggest importedPalettes config
|
|
20
|
+
emily-css manifest Generate the utility/token manifest JSON
|
|
18
21
|
emily-css showcase Browse components in your browser
|
|
19
22
|
emily-css help Full command reference
|
|
20
23
|
|
|
@@ -33,6 +36,9 @@ if (command === "init") {
|
|
|
33
36
|
require("../src/watch.js");
|
|
34
37
|
} else if (command === "showcase") {
|
|
35
38
|
require("../src/showcase.js");
|
|
39
|
+
} else if (command === "info") {
|
|
40
|
+
const { info } = require("../src/info.js");
|
|
41
|
+
info();
|
|
36
42
|
} else if (command === "doctor") {
|
|
37
43
|
const { doctor } = require("../src/doctor.js");
|
|
38
44
|
const result = doctor();
|
|
@@ -43,6 +49,19 @@ if (command === "init") {
|
|
|
43
49
|
const importColours = process.argv.includes("--import-colours");
|
|
44
50
|
const report = generateMigrationReport({ importColours });
|
|
45
51
|
console.log(formatMigrationReport(report, { importColours }));
|
|
52
|
+
} else if (command === "manifest") {
|
|
53
|
+
const { getConfig, getFullCssPath, getManifestOutputPath, ensureDirectoryForFile } = require("../src/config.js");
|
|
54
|
+
const { ensureFullFramework } = require("../src/index.js");
|
|
55
|
+
const { generateManifest } = require("../src/manifest.js");
|
|
56
|
+
const config = getConfig();
|
|
57
|
+
ensureFullFramework();
|
|
58
|
+
const fullCssPath = getFullCssPath(config);
|
|
59
|
+
const css = fs.readFileSync(fullCssPath, "utf8");
|
|
60
|
+
const manifestData = generateManifest(css, config);
|
|
61
|
+
const manifestPath = getManifestOutputPath(config);
|
|
62
|
+
ensureDirectoryForFile(manifestPath);
|
|
63
|
+
fs.writeFileSync(manifestPath, JSON.stringify(manifestData, null, 2));
|
|
64
|
+
console.log(`✓ Generated manifest: ${manifestPath}`);
|
|
46
65
|
} else if (command === "version" || command === "--version" || command === "-v") {
|
|
47
66
|
console.log(packageJson.version);
|
|
48
67
|
} else if (command === "help") {
|
|
@@ -54,9 +73,11 @@ if (command === "init") {
|
|
|
54
73
|
emily-css build Generate production CSS to the configured output path
|
|
55
74
|
--profile Print coarse build timing information
|
|
56
75
|
emily-css watch Dev mode: watch for changes and rebuild
|
|
76
|
+
emily-css info Show project config, output paths, and CSS stats
|
|
57
77
|
emily-css doctor Scan project files for unknown EmilyCSS classes
|
|
58
78
|
emily-css migrate Generate a Tailwind-to-EmilyCSS migration report
|
|
59
79
|
--import-colours Detect Tailwind colour palettes and suggest importedPalettes config
|
|
80
|
+
emily-css manifest Generate the utility/token manifest JSON
|
|
60
81
|
emily-css showcase Launch the component showcase in your browser
|
|
61
82
|
emily-css version Show installed version
|
|
62
83
|
emily-css help Show this help text
|
|
@@ -64,7 +85,12 @@ if (command === "init") {
|
|
|
64
85
|
npm scripts (added by init):
|
|
65
86
|
npm run emily:build Same as emily-css build
|
|
66
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
|
|
67
92
|
npm run emily:showcase Same as emily-css showcase
|
|
93
|
+
npm run emily:version Same as emily-css version
|
|
68
94
|
npm run emily:help Same as emily-css help
|
|
69
95
|
|
|
70
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.5",
|
|
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/config.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
function getConfigPath() {
|
|
7
|
+
return path.join(process.cwd(), 'emily.config.json');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function getConfig() {
|
|
11
|
+
const configPath = getConfigPath();
|
|
12
|
+
|
|
13
|
+
if (!fs.existsSync(configPath)) {
|
|
14
|
+
console.error('\n emily-css: No config found. Run "emily-css init" first.\n');
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function getFullCssPath(config) {
|
|
22
|
+
return path.join(process.cwd(), config.output?.fullCss || 'dist/emily.css');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function getProductionCssPath(config) {
|
|
26
|
+
return path.join(process.cwd(), config.output?.css || 'dist/emily.min.css');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function getManifestSettings(config) {
|
|
30
|
+
const manifestConfig = config.manifest;
|
|
31
|
+
|
|
32
|
+
if (manifestConfig === true) {
|
|
33
|
+
return { enabled: true, output: 'dist/emily.manifest.json' };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (manifestConfig && typeof manifestConfig === 'object') {
|
|
37
|
+
return {
|
|
38
|
+
enabled: manifestConfig.enabled === true,
|
|
39
|
+
output: manifestConfig.output || 'dist/emily.manifest.json',
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return { enabled: false, output: 'dist/emily.manifest.json' };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function getManifestOutputPath(config) {
|
|
47
|
+
const manifestSettings = getManifestSettings(config);
|
|
48
|
+
const outputPath = manifestSettings.output || 'dist/emily.manifest.json';
|
|
49
|
+
|
|
50
|
+
return path.isAbsolute(outputPath)
|
|
51
|
+
? outputPath
|
|
52
|
+
: path.join(process.cwd(), outputPath);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function getIntellisenseSettings(config) {
|
|
56
|
+
const intellisenseConfig = config.intellisense;
|
|
57
|
+
|
|
58
|
+
if (intellisenseConfig === true) {
|
|
59
|
+
return { enabled: true, output: 'dist/emily.intellisense.json' };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (intellisenseConfig && typeof intellisenseConfig === 'object') {
|
|
63
|
+
return {
|
|
64
|
+
enabled: intellisenseConfig.enabled === true,
|
|
65
|
+
output: intellisenseConfig.output || 'dist/emily.intellisense.json',
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return { enabled: false, output: 'dist/emily.intellisense.json' };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function getIntellisenseOutputPath(config) {
|
|
73
|
+
const intellisenseSettings = getIntellisenseSettings(config);
|
|
74
|
+
const outputPath = intellisenseSettings.output || 'dist/emily.intellisense.json';
|
|
75
|
+
|
|
76
|
+
return path.isAbsolute(outputPath)
|
|
77
|
+
? outputPath
|
|
78
|
+
: path.join(process.cwd(), outputPath);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function ensureDirectoryForFile(filePath) {
|
|
82
|
+
const dir = path.dirname(filePath);
|
|
83
|
+
|
|
84
|
+
if (!fs.existsSync(dir)) {
|
|
85
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function getSourceDir(config) {
|
|
90
|
+
return config.purge?.sourceDir || '.';
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
module.exports = {
|
|
94
|
+
getConfigPath,
|
|
95
|
+
getConfig,
|
|
96
|
+
getFullCssPath,
|
|
97
|
+
getProductionCssPath,
|
|
98
|
+
getManifestSettings,
|
|
99
|
+
getManifestOutputPath,
|
|
100
|
+
getIntellisenseSettings,
|
|
101
|
+
getIntellisenseOutputPath,
|
|
102
|
+
ensureDirectoryForFile,
|
|
103
|
+
getSourceDir,
|
|
104
|
+
};
|
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
|
@@ -1,54 +1,18 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
1
3
|
const fs = require("fs");
|
|
2
4
|
const path = require("path");
|
|
3
5
|
const fg = require("fast-glob");
|
|
4
6
|
const { extractClassNames } = require("./purge.js");
|
|
5
|
-
const { ensureFullFramework, generateManifest } = require("./index.js");
|
|
7
|
+
const { ensureFullFramework, generateManifest, generateColourScale } = require("./index.js");
|
|
6
8
|
const { DEFAULT_EXTENSIONS } = require("./constants.js");
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
if (!fs.existsSync(configPath)) {
|
|
16
|
-
console.error('\n emily-css: No config found. Run "emily-css init" first.\n');
|
|
17
|
-
process.exit(1);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
return JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function getFullCssPath(config) {
|
|
24
|
-
return path.join(process.cwd(), config.output?.fullCss || "dist/emily.css");
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function getManifestSettings(config) {
|
|
28
|
-
const manifestConfig = config.manifest;
|
|
29
|
-
|
|
30
|
-
if (manifestConfig === true) {
|
|
31
|
-
return { enabled: true, output: "dist/emily.manifest.json" };
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
if (manifestConfig && typeof manifestConfig === "object") {
|
|
35
|
-
return {
|
|
36
|
-
enabled: manifestConfig.enabled === true,
|
|
37
|
-
output: manifestConfig.output || "dist/emily.manifest.json",
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
return { enabled: false, output: "dist/emily.manifest.json" };
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function getManifestOutputPath(config) {
|
|
45
|
-
const manifestSettings = getManifestSettings(config);
|
|
46
|
-
const outputPath = manifestSettings.output || "dist/emily.manifest.json";
|
|
47
|
-
|
|
48
|
-
return path.isAbsolute(outputPath)
|
|
49
|
-
? outputPath
|
|
50
|
-
: path.join(process.cwd(), outputPath);
|
|
51
|
-
}
|
|
9
|
+
const { resolvePurgeConfig } = require("./purgeConfig.js");
|
|
10
|
+
const {
|
|
11
|
+
getConfig,
|
|
12
|
+
getFullCssPath,
|
|
13
|
+
getManifestSettings,
|
|
14
|
+
getManifestOutputPath,
|
|
15
|
+
} = require("./config.js");
|
|
52
16
|
|
|
53
17
|
function normaliseClassForManifest(className) {
|
|
54
18
|
if (!className || typeof className !== "string") {
|
|
@@ -148,18 +112,20 @@ function suggestClassName(className, utilitySet, variantSet) {
|
|
|
148
112
|
}
|
|
149
113
|
|
|
150
114
|
function getFilesToScan(config) {
|
|
151
|
-
const
|
|
152
|
-
const
|
|
115
|
+
const resolvedPurgeConfig = resolvePurgeConfig(config);
|
|
116
|
+
const extensions = resolvedPurgeConfig.extensions || DEFAULT_EXTENSIONS;
|
|
117
|
+
const ignore = resolvedPurgeConfig.ignore || [];
|
|
118
|
+
const sourceGlobs = resolvedPurgeConfig.sourceGlobs || [];
|
|
153
119
|
|
|
154
|
-
if (
|
|
155
|
-
return fg.sync(
|
|
120
|
+
if (sourceGlobs.length > 0) {
|
|
121
|
+
return fg.sync(sourceGlobs, {
|
|
156
122
|
ignore,
|
|
157
123
|
onlyFiles: true,
|
|
158
124
|
unique: true,
|
|
159
125
|
});
|
|
160
126
|
}
|
|
161
127
|
|
|
162
|
-
const sourceDir =
|
|
128
|
+
const sourceDir = resolvedPurgeConfig.sourceDir || ".";
|
|
163
129
|
const scanDir = path.isAbsolute(sourceDir)
|
|
164
130
|
? sourceDir
|
|
165
131
|
: path.join(process.cwd(), sourceDir);
|
|
@@ -281,6 +247,79 @@ function createAccessibilityWarnings(filePath, content) {
|
|
|
281
247
|
return warnings;
|
|
282
248
|
}
|
|
283
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
|
+
|
|
284
323
|
function doctor() {
|
|
285
324
|
const config = getConfig();
|
|
286
325
|
|
|
@@ -341,8 +380,10 @@ function doctor() {
|
|
|
341
380
|
}
|
|
342
381
|
});
|
|
343
382
|
|
|
383
|
+
warnings.push(...createContrastWarnings(config));
|
|
384
|
+
|
|
344
385
|
if (issues.length === 0 && warnings.length === 0) {
|
|
345
|
-
console.log("
|
|
386
|
+
console.log("\u2713 EmilyCSS doctor found no class issues");
|
|
346
387
|
return { ok: true, issues: [], warnings: [], exitCode: 0 };
|
|
347
388
|
}
|
|
348
389
|
|
|
@@ -384,4 +425,7 @@ module.exports = {
|
|
|
384
425
|
doctor,
|
|
385
426
|
normaliseClassForManifest,
|
|
386
427
|
suggestClassName,
|
|
428
|
+
hexToRelativeLuminance,
|
|
429
|
+
contrastRatio,
|
|
430
|
+
createContrastWarnings,
|
|
387
431
|
};
|
package/src/index.js
CHANGED
|
@@ -1,10 +1,21 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
'use strict';
|
|
3
2
|
|
|
4
3
|
const fs = require('fs');
|
|
5
4
|
const path = require('path');
|
|
6
5
|
const { generateManifest } = require('./manifest');
|
|
7
6
|
const { generateIntellisense } = require('./intellisense');
|
|
7
|
+
const {
|
|
8
|
+
getConfigPath,
|
|
9
|
+
getConfig,
|
|
10
|
+
getFullCssPath,
|
|
11
|
+
getProductionCssPath,
|
|
12
|
+
getManifestSettings,
|
|
13
|
+
getManifestOutputPath,
|
|
14
|
+
getIntellisenseSettings,
|
|
15
|
+
getIntellisenseOutputPath,
|
|
16
|
+
ensureDirectoryForFile,
|
|
17
|
+
getSourceDir,
|
|
18
|
+
} = require('./config');
|
|
8
19
|
|
|
9
20
|
|
|
10
21
|
// ============================================================================
|
|
@@ -838,12 +849,17 @@ function generateSemanticColourUtilities(semanticColours) {
|
|
|
838
849
|
|
|
839
850
|
function addAriaDataVariants(css) {
|
|
840
851
|
const variants = [
|
|
841
|
-
{ name: 'aria-expanded',
|
|
842
|
-
{ name: 'aria-selected',
|
|
843
|
-
{ name: 'aria-
|
|
844
|
-
{ name: 'aria-
|
|
845
|
-
{ name: '
|
|
846
|
-
{ 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"]' },
|
|
847
863
|
];
|
|
848
864
|
|
|
849
865
|
let variantCss = css;
|
|
@@ -1455,97 +1471,6 @@ function generatePatternComponents() {
|
|
|
1455
1471
|
// BUILD FUNCTION
|
|
1456
1472
|
// ============================================================================
|
|
1457
1473
|
|
|
1458
|
-
// ============================================================================
|
|
1459
|
-
// BUILD FUNCTION
|
|
1460
|
-
// ============================================================================
|
|
1461
|
-
|
|
1462
|
-
function getConfigPath() {
|
|
1463
|
-
return path.join(process.cwd(), 'emily.config.json');
|
|
1464
|
-
}
|
|
1465
|
-
|
|
1466
|
-
function getConfig() {
|
|
1467
|
-
const configPath = getConfigPath();
|
|
1468
|
-
|
|
1469
|
-
if (!fs.existsSync(configPath)) {
|
|
1470
|
-
console.error('\n emily-css: No config found. Run "emily-css init" first.\n');
|
|
1471
|
-
process.exit(1);
|
|
1472
|
-
}
|
|
1473
|
-
|
|
1474
|
-
return JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
1475
|
-
}
|
|
1476
|
-
|
|
1477
|
-
function getFullCssPath(config) {
|
|
1478
|
-
return path.join(process.cwd(), config.output?.fullCss || 'dist/emily.css');
|
|
1479
|
-
}
|
|
1480
|
-
|
|
1481
|
-
function getProductionCssPath(config) {
|
|
1482
|
-
return path.join(process.cwd(), config.output?.css || 'dist/emily.min.css');
|
|
1483
|
-
}
|
|
1484
|
-
|
|
1485
|
-
function getManifestSettings(config) {
|
|
1486
|
-
const manifestConfig = config.manifest;
|
|
1487
|
-
|
|
1488
|
-
if (manifestConfig === true) {
|
|
1489
|
-
return { enabled: true, output: 'dist/emily.manifest.json' };
|
|
1490
|
-
}
|
|
1491
|
-
|
|
1492
|
-
if (manifestConfig && typeof manifestConfig === 'object') {
|
|
1493
|
-
return {
|
|
1494
|
-
enabled: manifestConfig.enabled === true,
|
|
1495
|
-
output: manifestConfig.output || 'dist/emily.manifest.json',
|
|
1496
|
-
};
|
|
1497
|
-
}
|
|
1498
|
-
|
|
1499
|
-
return { enabled: false, output: 'dist/emily.manifest.json' };
|
|
1500
|
-
}
|
|
1501
|
-
|
|
1502
|
-
function getManifestOutputPath(config) {
|
|
1503
|
-
const manifestSettings = getManifestSettings(config);
|
|
1504
|
-
const outputPath = manifestSettings.output || 'dist/emily.manifest.json';
|
|
1505
|
-
|
|
1506
|
-
return path.isAbsolute(outputPath)
|
|
1507
|
-
? outputPath
|
|
1508
|
-
: path.join(process.cwd(), outputPath);
|
|
1509
|
-
}
|
|
1510
|
-
|
|
1511
|
-
function getIntellisenseSettings(config) {
|
|
1512
|
-
const intellisenseConfig = config.intellisense;
|
|
1513
|
-
|
|
1514
|
-
if (intellisenseConfig === true) {
|
|
1515
|
-
return { enabled: true, output: 'dist/emily.intellisense.json' };
|
|
1516
|
-
}
|
|
1517
|
-
|
|
1518
|
-
if (intellisenseConfig && typeof intellisenseConfig === 'object') {
|
|
1519
|
-
return {
|
|
1520
|
-
enabled: intellisenseConfig.enabled === true,
|
|
1521
|
-
output: intellisenseConfig.output || 'dist/emily.intellisense.json',
|
|
1522
|
-
};
|
|
1523
|
-
}
|
|
1524
|
-
|
|
1525
|
-
return { enabled: false, output: 'dist/emily.intellisense.json' };
|
|
1526
|
-
}
|
|
1527
|
-
|
|
1528
|
-
function getIntellisenseOutputPath(config) {
|
|
1529
|
-
const intellisenseSettings = getIntellisenseSettings(config);
|
|
1530
|
-
const outputPath = intellisenseSettings.output || 'dist/emily.intellisense.json';
|
|
1531
|
-
|
|
1532
|
-
return path.isAbsolute(outputPath)
|
|
1533
|
-
? outputPath
|
|
1534
|
-
: path.join(process.cwd(), outputPath);
|
|
1535
|
-
}
|
|
1536
|
-
|
|
1537
|
-
function ensureDirectoryForFile(filePath) {
|
|
1538
|
-
const dir = path.dirname(filePath);
|
|
1539
|
-
|
|
1540
|
-
if (!fs.existsSync(dir)) {
|
|
1541
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
1542
|
-
}
|
|
1543
|
-
}
|
|
1544
|
-
|
|
1545
|
-
function getSourceDir(config) {
|
|
1546
|
-
return config.purge?.sourceDir || '.';
|
|
1547
|
-
}
|
|
1548
|
-
|
|
1549
1474
|
function buildFullFramework() {
|
|
1550
1475
|
const config = getConfig();
|
|
1551
1476
|
|
|
@@ -1908,6 +1833,7 @@ module.exports = {
|
|
|
1908
1833
|
oklchToHex,
|
|
1909
1834
|
generateColourScale,
|
|
1910
1835
|
generateAllColours,
|
|
1836
|
+
generateFontCSS,
|
|
1911
1837
|
generateSpacing,
|
|
1912
1838
|
generateBorderUtilities,
|
|
1913
1839
|
generateColourUtilities,
|
|
@@ -1920,6 +1846,4 @@ module.exports = {
|
|
|
1920
1846
|
addAriaDataVariants,
|
|
1921
1847
|
addResponsiveVariants,
|
|
1922
1848
|
generateManifest,
|
|
1923
|
-
generateFontCSS,
|
|
1924
|
-
codeUtilities,
|
|
1925
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 = {
|
|
@@ -740,8 +741,11 @@ function migrateClasses(input, options = {}) {
|
|
|
740
741
|
}
|
|
741
742
|
|
|
742
743
|
function getFilesToScan(config, options = {}) {
|
|
743
|
-
const
|
|
744
|
-
const
|
|
744
|
+
const resolvedPurgeConfig = resolvePurgeConfig(config || {});
|
|
745
|
+
const extensions = Array.from(
|
|
746
|
+
new Set([...(resolvedPurgeConfig.extensions || MIGRATION_DEFAULT_EXTENSIONS), '.mdx']),
|
|
747
|
+
);
|
|
748
|
+
const ignore = resolvedPurgeConfig.ignore || [];
|
|
745
749
|
|
|
746
750
|
if (options.sourceGlobs && options.sourceGlobs.length > 0) {
|
|
747
751
|
return fg.sync(options.sourceGlobs, {
|
|
@@ -752,8 +756,8 @@ function getFilesToScan(config, options = {}) {
|
|
|
752
756
|
});
|
|
753
757
|
}
|
|
754
758
|
|
|
755
|
-
if (
|
|
756
|
-
return fg.sync(
|
|
759
|
+
if (resolvedPurgeConfig.sourceGlobs && resolvedPurgeConfig.sourceGlobs.length > 0) {
|
|
760
|
+
return fg.sync(resolvedPurgeConfig.sourceGlobs, {
|
|
757
761
|
ignore,
|
|
758
762
|
onlyFiles: true,
|
|
759
763
|
unique: true,
|
|
@@ -763,7 +767,7 @@ function getFilesToScan(config, options = {}) {
|
|
|
763
767
|
|
|
764
768
|
const sourceDir =
|
|
765
769
|
options.sourceDir ||
|
|
766
|
-
|
|
770
|
+
resolvedPurgeConfig.sourceDir ||
|
|
767
771
|
'.';
|
|
768
772
|
|
|
769
773
|
const scanDir = path.isAbsolute(sourceDir)
|
package/src/purge.js
CHANGED
|
@@ -1,37 +1,28 @@
|
|
|
1
|
-
|
|
1
|
+
'use strict';
|
|
2
2
|
|
|
3
3
|
const fs = require("fs");
|
|
4
4
|
const path = require("path");
|
|
5
|
-
const
|
|
5
|
+
const fg = require("fast-glob");
|
|
6
|
+
const { DEFAULT_EXTENSIONS, DEFAULT_PURGE_IGNORE } = require("./constants.js");
|
|
7
|
+
const { resolvePurgeConfig } = require("./purgeConfig.js");
|
|
6
8
|
|
|
7
9
|
function getAllFiles(dir, extensions = DEFAULT_EXTENSIONS) {
|
|
8
|
-
|
|
10
|
+
const absoluteDir = path.isAbsolute(dir) ? dir : path.resolve(dir);
|
|
11
|
+
const patterns = extensions.map((ext) => `**/*${ext}`);
|
|
12
|
+
const ignore = DEFAULT_PURGE_IGNORE.map((d) => `**/${d}/**`);
|
|
9
13
|
|
|
10
14
|
try {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
entry.name === "node_modules" ||
|
|
19
|
-
entry.name === "dist"
|
|
20
|
-
) {
|
|
21
|
-
continue;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
if (entry.isDirectory()) {
|
|
25
|
-
files = files.concat(getAllFiles(fullPath, extensions));
|
|
26
|
-
} else if (extensions.some((ext) => entry.name.endsWith(ext))) {
|
|
27
|
-
files.push(fullPath);
|
|
28
|
-
}
|
|
29
|
-
}
|
|
15
|
+
return fg.sync(patterns, {
|
|
16
|
+
cwd: absoluteDir,
|
|
17
|
+
ignore,
|
|
18
|
+
onlyFiles: true,
|
|
19
|
+
unique: true,
|
|
20
|
+
absolute: true,
|
|
21
|
+
});
|
|
30
22
|
} catch (err) {
|
|
31
|
-
console.warn(`Warning: Could not
|
|
23
|
+
console.warn(`Warning: Could not scan directory ${absoluteDir}: ${err.message}`);
|
|
24
|
+
return [];
|
|
32
25
|
}
|
|
33
|
-
|
|
34
|
-
return files;
|
|
35
26
|
}
|
|
36
27
|
|
|
37
28
|
function extractClassNames(content) {
|
|
@@ -185,24 +176,27 @@ function purgeBlock(block, usedClasses) {
|
|
|
185
176
|
}
|
|
186
177
|
|
|
187
178
|
function getFilesForPurge(scanDir, config, extensions) {
|
|
188
|
-
|
|
189
|
-
|
|
179
|
+
const resolvedPurgeConfig = resolvePurgeConfig(config);
|
|
180
|
+
const sourceGlobs = resolvedPurgeConfig.sourceGlobs || [];
|
|
190
181
|
|
|
182
|
+
if (sourceGlobs.length) {
|
|
191
183
|
console.log(`\n🔍 Scanning using sourceGlobs`);
|
|
192
|
-
|
|
184
|
+
sourceGlobs.forEach((glob) => console.log(` - ${glob}`));
|
|
193
185
|
console.log(` Extensions: ${extensions.join(", ")}`);
|
|
194
186
|
|
|
195
|
-
return fg.sync(
|
|
196
|
-
ignore:
|
|
187
|
+
return fg.sync(sourceGlobs, {
|
|
188
|
+
ignore: resolvedPurgeConfig.ignore || [],
|
|
197
189
|
onlyFiles: true,
|
|
198
190
|
unique: true,
|
|
199
191
|
});
|
|
200
192
|
}
|
|
201
193
|
|
|
202
|
-
|
|
194
|
+
const fallbackScanDir = resolvedPurgeConfig.sourceDir || scanDir;
|
|
195
|
+
|
|
196
|
+
console.log(`\n🔍 Scanning fallback directory: ${fallbackScanDir}`);
|
|
203
197
|
console.log(` Extensions: ${extensions.join(", ")}`);
|
|
204
198
|
|
|
205
|
-
return getAllFiles(
|
|
199
|
+
return getAllFiles(fallbackScanDir, extensions);
|
|
206
200
|
}
|
|
207
201
|
|
|
208
202
|
function printFileSummary(files, extensions) {
|
|
@@ -226,7 +220,8 @@ function printFileSummary(files, extensions) {
|
|
|
226
220
|
}
|
|
227
221
|
|
|
228
222
|
function purgeCSS(css, scanDir, config) {
|
|
229
|
-
const
|
|
223
|
+
const resolvedPurgeConfig = resolvePurgeConfig(config);
|
|
224
|
+
const extensions = resolvedPurgeConfig.extensions || DEFAULT_EXTENSIONS;
|
|
230
225
|
const files = getFilesForPurge(scanDir, config, extensions);
|
|
231
226
|
|
|
232
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
|
+
|