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 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.3 — May 2026
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.2.2 May 2026
15
+ ## v1.3.0 -- May 2026
13
16
 
14
17
  ### Added
15
- - Added IntelliSense JSON generation via `intellisense` config output (`dist/emily.intellisense.json` by default).
16
- - Added build profiling via `emily-css build --profile` with coarse timing buckets.
17
- - Added initial accessibility warnings to `emily-css doctor` (focus removal, same token text/background, and `cursor-pointer` on non-interactive elements).
18
- - Added documentation stubs in `docs/` for installation, configuration, variants, accessibility, doctor, migrate, manifest, and IntelliSense.
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
- - Stabilised manifest schema metadata with explicit `schemaVersion`, package name, and package version fields.
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.1 — May 2026
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:` `disabled:` `dark:`)
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 16+ compatibility
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 16+
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",
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": ">=16.0.0"
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
- function getConfigPath() {
9
- return path.join(process.cwd(), "emily.config.json");
10
- }
11
-
12
- function getConfig() {
13
- const configPath = getConfigPath();
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 extensions = config?.purge?.extensions || DEFAULT_EXTENSIONS;
152
- const ignore = config?.purge?.ignore || [];
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 (config?.purge?.sourceGlobs && config.purge.sourceGlobs.length > 0) {
155
- return fg.sync(config.purge.sourceGlobs, {
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 = config?.purge?.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(" EmilyCSS doctor found no class issues");
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
- // new version
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', selector: '[aria-expanded="true"]' },
842
- { name: 'aria-selected', selector: '[aria-selected="true"]' },
843
- { name: 'aria-current', selector: '[aria-current="page"]' },
844
- { name: 'aria-disabled', selector: '[aria-disabled="true"]' },
845
- { name: 'data-open', selector: '[data-state="open"]' },
846
- { name: 'data-closed', selector: '[data-state="closed"]' },
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,js,ts}",
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 extensions = (config && config.purge && config.purge.extensions) || MIGRATION_DEFAULT_EXTENSIONS;
744
- const ignore = (config && config.purge && config.purge.ignore) || [];
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 (config && config.purge && config.purge.sourceGlobs && config.purge.sourceGlobs.length > 0) {
756
- return fg.sync(config.purge.sourceGlobs, {
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
- (config && config.purge && config.purge.sourceDir) ||
770
+ resolvedPurgeConfig.sourceDir ||
767
771
  '.';
768
772
 
769
773
  const scanDir = path.isAbsolute(sourceDir)
package/src/purge.js CHANGED
@@ -1,37 +1,28 @@
1
- // new version
1
+ 'use strict';
2
2
 
3
3
  const fs = require("fs");
4
4
  const path = require("path");
5
- const { DEFAULT_EXTENSIONS } = require("./constants.js");
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
- let files = [];
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
- const entries = fs.readdirSync(dir, { withFileTypes: true });
12
-
13
- for (const entry of entries) {
14
- const fullPath = path.join(dir, entry.name);
15
-
16
- if (
17
- entry.name.startsWith(".") ||
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 read directory ${dir}: ${err.message}`);
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
- if (config?.purge?.sourceGlobs && config.purge.sourceGlobs.length) {
189
- const fg = require("fast-glob");
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
- config.purge.sourceGlobs.forEach((glob) => console.log(` - ${glob}`));
184
+ sourceGlobs.forEach((glob) => console.log(` - ${glob}`));
193
185
  console.log(` Extensions: ${extensions.join(", ")}`);
194
186
 
195
- return fg.sync(config.purge.sourceGlobs, {
196
- ignore: config.purge.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
- console.log(`\n🔍 Scanning fallback directory: ${scanDir}`);
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(scanDir, extensions);
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 extensions = config?.purge?.extensions || DEFAULT_EXTENSIONS;
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
+