emily-css 1.0.29 → 1.1.1

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
@@ -3,7 +3,30 @@
3
3
  All notable changes to `emily-css` are documented here.
4
4
 
5
5
  ---
6
+ ## v1.1.0 — May 2026
6
7
 
8
+ ### Added
9
+ - Added `emily-css doctor`, a manifest-powered project checker that scans configured source files and reports unknown EmilyCSS classes with suggestions.
10
+ - Added variant-aware class validation for responsive, state, ARIA, data-state, motion, dark, and forced-colours variants.
11
+
12
+ ---
13
+
14
+ ## v1.1.1 — May 2026
15
+
16
+ **updated changes and added**
17
+
18
+ ### Added
19
+ - updted changes
20
+
21
+ ---
22
+ ## v1.1.0 — May 2026
23
+
24
+ **add utility manifest generation): chore: release v1.1.0**
25
+
26
+ ### Added
27
+ - add utility manifest generation): chore: release v1.1.0
28
+
29
+ ---
7
30
  ## v1.0.29 — May 2026
8
31
 
9
32
  **added json manifest for future use**
package/bin/emilyui.js CHANGED
@@ -14,6 +14,10 @@ if (command === "init") {
14
14
  require("../src/watch.js");
15
15
  } else if (command === "showcase") {
16
16
  require("../src/showcase.js");
17
+ } else if (command === "doctor") {
18
+ const { doctor } = require("../src/doctor.js");
19
+ const result = doctor();
20
+ process.exitCode = result.exitCode;
17
21
  } else if (command === "version" || command === "--version" || command === "-v") {
18
22
  console.log(packageJson.version);
19
23
  } else if (command === "help") {
@@ -24,6 +28,7 @@ if (command === "init") {
24
28
  emily-css init Set up a new project (interactive wizard)
25
29
  emily-css build Generate production CSS to the configured output path
26
30
  emily-css watch Dev mode: watch for changes and rebuild
31
+ emily-css doctor Scan project files for unknown EmilyCSS classes
27
32
  emily-css showcase Launch the component showcase in your browser
28
33
  emily-css version Show installed version
29
34
  emily-css help Show this help text
@@ -44,9 +49,10 @@ if (command === "init") {
44
49
  emily-css init Set up a new project
45
50
  emily-css build Generate production CSS to the configured output path
46
51
  emily-css watch Dev mode: rebuild on changes
52
+ emily-css doctor Scan project files for unknown EmilyCSS classes
47
53
  emily-css showcase Browse components in your browser
48
54
  emily-css help Full command reference
49
55
 
50
56
  Run emily-css help for more detail.
51
57
  `);
52
- }
58
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "emily-css",
3
- "version": "1.0.29",
3
+ "version": "1.1.1",
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": {
package/src/doctor.js ADDED
@@ -0,0 +1,293 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const fg = require("fast-glob");
4
+ const { extractClassNames } = require("./purge.js");
5
+ const { ensureFullFramework, generateManifest } = require("./index.js");
6
+
7
+ const DEFAULT_EXTENSIONS = [
8
+ ".html",
9
+ ".htm",
10
+ ".twig",
11
+ ".njk",
12
+ ".liquid",
13
+ ".hbs",
14
+ ".js",
15
+ ".jsx",
16
+ ".ts",
17
+ ".tsx",
18
+ ".vue",
19
+ ".php",
20
+ ".astro",
21
+ ".svelte",
22
+ ".blade.php",
23
+ ".jinja",
24
+ ".jinja2",
25
+ ".j2",
26
+ ".md",
27
+ ];
28
+
29
+ function getConfigPath() {
30
+ return path.join(process.cwd(), "emily.config.json");
31
+ }
32
+
33
+ function getConfig() {
34
+ const configPath = getConfigPath();
35
+
36
+ if (!fs.existsSync(configPath)) {
37
+ console.error('\n emily-css: No config found. Run "emily-css init" first.\n');
38
+ process.exit(1);
39
+ }
40
+
41
+ return JSON.parse(fs.readFileSync(configPath, "utf8"));
42
+ }
43
+
44
+ function getFullCssPath(config) {
45
+ return path.join(process.cwd(), config.output?.fullCss || "dist/emily.css");
46
+ }
47
+
48
+ function getManifestSettings(config) {
49
+ const manifestConfig = config.manifest;
50
+
51
+ if (manifestConfig === true) {
52
+ return { enabled: true, output: "dist/emily.manifest.json" };
53
+ }
54
+
55
+ if (manifestConfig && typeof manifestConfig === "object") {
56
+ return {
57
+ enabled: manifestConfig.enabled === true,
58
+ output: manifestConfig.output || "dist/emily.manifest.json",
59
+ };
60
+ }
61
+
62
+ return { enabled: false, output: "dist/emily.manifest.json" };
63
+ }
64
+
65
+ function getManifestOutputPath(config) {
66
+ const manifestSettings = getManifestSettings(config);
67
+ const outputPath = manifestSettings.output || "dist/emily.manifest.json";
68
+
69
+ return path.isAbsolute(outputPath)
70
+ ? outputPath
71
+ : path.join(process.cwd(), outputPath);
72
+ }
73
+
74
+ function normaliseClassForManifest(className) {
75
+ if (!className || typeof className !== "string") {
76
+ return { original: className, baseClass: "", variants: [], variant: null };
77
+ }
78
+
79
+ const parts = className.split(":").filter(Boolean);
80
+
81
+ if (parts.length <= 1) {
82
+ return {
83
+ original: className,
84
+ baseClass: className,
85
+ variants: [],
86
+ variant: null,
87
+ };
88
+ }
89
+
90
+ const baseClass = parts[parts.length - 1];
91
+ const variants = parts.slice(0, -1);
92
+
93
+ return {
94
+ original: className,
95
+ baseClass,
96
+ variants,
97
+ variant: variants.join(":"),
98
+ };
99
+ }
100
+
101
+ function levenshtein(a, b) {
102
+ if (a === b) return 0;
103
+ if (!a.length) return b.length;
104
+ if (!b.length) return a.length;
105
+
106
+ const dp = Array.from({ length: a.length + 1 }, () => Array(b.length + 1).fill(0));
107
+
108
+ for (let i = 0; i <= a.length; i++) dp[i][0] = i;
109
+ for (let j = 0; j <= b.length; j++) dp[0][j] = j;
110
+
111
+ for (let i = 1; i <= a.length; i++) {
112
+ for (let j = 1; j <= b.length; j++) {
113
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
114
+ dp[i][j] = Math.min(
115
+ dp[i - 1][j] + 1,
116
+ dp[i][j - 1] + 1,
117
+ dp[i - 1][j - 1] + cost
118
+ );
119
+ }
120
+ }
121
+
122
+ return dp[a.length][b.length];
123
+ }
124
+
125
+ function findClosest(target, candidates) {
126
+ if (!target || candidates.length === 0) return null;
127
+
128
+ let best = null;
129
+ let bestDistance = Infinity;
130
+
131
+ for (const candidate of candidates) {
132
+ const distance = levenshtein(target, candidate);
133
+ if (distance < bestDistance) {
134
+ bestDistance = distance;
135
+ best = candidate;
136
+ if (distance === 0) break;
137
+ }
138
+ }
139
+
140
+ const threshold = Math.max(2, Math.floor(target.length / 3));
141
+ return bestDistance <= threshold ? best : null;
142
+ }
143
+
144
+ function suggestClassName(className, utilitySet, variantSet) {
145
+ const utilityList = Array.isArray(utilitySet) ? utilitySet : Array.from(utilitySet);
146
+ const variantList = Array.isArray(variantSet) ? variantSet : Array.from(variantSet);
147
+ const parsed = normaliseClassForManifest(className);
148
+
149
+ if (!parsed.variants.length) {
150
+ return findClosest(parsed.baseClass, utilityList);
151
+ }
152
+
153
+ const correctedVariants = parsed.variants.map((variant) => {
154
+ if (variantSet.has(variant)) return variant;
155
+ return findClosest(variant, variantList) || variant;
156
+ });
157
+
158
+ const correctedBase = utilitySet.has(parsed.baseClass)
159
+ ? parsed.baseClass
160
+ : findClosest(parsed.baseClass, utilityList);
161
+
162
+ if (!correctedBase) return null;
163
+
164
+ const rebuilt = correctedVariants.length
165
+ ? `${correctedVariants.join(":")}:${correctedBase}`
166
+ : correctedBase;
167
+
168
+ return rebuilt === className ? null : rebuilt;
169
+ }
170
+
171
+ function getFilesToScan(config) {
172
+ const extensions = config?.purge?.extensions || DEFAULT_EXTENSIONS;
173
+ const ignore = config?.purge?.ignore || [];
174
+
175
+ if (config?.purge?.sourceGlobs && config.purge.sourceGlobs.length > 0) {
176
+ return fg.sync(config.purge.sourceGlobs, {
177
+ ignore,
178
+ onlyFiles: true,
179
+ unique: true,
180
+ });
181
+ }
182
+
183
+ const sourceDir = config?.purge?.sourceDir || ".";
184
+ const scanDir = path.isAbsolute(sourceDir)
185
+ ? sourceDir
186
+ : path.join(process.cwd(), sourceDir);
187
+ const patterns = extensions.map((ext) => `**/*${ext}`);
188
+
189
+ return fg.sync(patterns, {
190
+ cwd: scanDir,
191
+ ignore,
192
+ onlyFiles: true,
193
+ unique: true,
194
+ absolute: true,
195
+ });
196
+ }
197
+
198
+ function loadManifest(config, css) {
199
+ const manifestSettings = getManifestSettings(config);
200
+ const manifestOutputPath = getManifestOutputPath(config);
201
+
202
+ if (manifestSettings.enabled && fs.existsSync(manifestOutputPath)) {
203
+ return JSON.parse(fs.readFileSync(manifestOutputPath, "utf8"));
204
+ }
205
+
206
+ return generateManifest(css, config);
207
+ }
208
+
209
+ function doctor() {
210
+ const config = getConfig();
211
+
212
+ ensureFullFramework();
213
+
214
+ const fullCssPath = getFullCssPath(config);
215
+ if (!fs.existsSync(fullCssPath)) {
216
+ console.error("\nEmilyCSS doctor could not locate generated CSS.\n");
217
+ return { ok: false, issues: [], exitCode: 1 };
218
+ }
219
+
220
+ const css = fs.readFileSync(fullCssPath, "utf8");
221
+ const manifest = loadManifest(config, css);
222
+ const utilities = manifest.utilities || [];
223
+ const utilitySet = new Set(utilities.map((utility) => utility.class));
224
+ const variantSet = new Set();
225
+
226
+ utilities.forEach((utility) => {
227
+ (utility.variants || []).forEach((variant) => variantSet.add(variant));
228
+ });
229
+
230
+ const files = getFilesToScan(config);
231
+ const issues = [];
232
+ const suggestionCache = new Map();
233
+
234
+ files.forEach((filePath) => {
235
+ try {
236
+ const content = fs.readFileSync(filePath, "utf8");
237
+ const classes = extractClassNames(content);
238
+
239
+ classes.forEach((className) => {
240
+ const parsed = normaliseClassForManifest(className);
241
+ const unknownVariants = parsed.variants.filter((variant) => !variantSet.has(variant));
242
+ const knownBase = utilitySet.has(parsed.baseClass);
243
+
244
+ if (unknownVariants.length === 0 && knownBase) {
245
+ return;
246
+ }
247
+
248
+ if (!suggestionCache.has(className)) {
249
+ suggestionCache.set(className, suggestClassName(className, utilitySet, variantSet));
250
+ }
251
+
252
+ issues.push({
253
+ file: filePath,
254
+ className,
255
+ reason: unknownVariants.length > 0 ? "unknown-variant" : "unknown-class",
256
+ unknownVariants,
257
+ suggestion: suggestionCache.get(className),
258
+ });
259
+ });
260
+ } catch (error) {
261
+ // Keep parity with purge behaviour: unreadable files are skipped.
262
+ }
263
+ });
264
+
265
+ if (issues.length === 0) {
266
+ console.log("✓ EmilyCSS doctor found no class issues");
267
+ return { ok: true, issues: [], exitCode: 0 };
268
+ }
269
+
270
+ console.log(`EmilyCSS doctor found ${issues.length} issue${issues.length === 1 ? "" : "s"}\n`);
271
+
272
+ issues.forEach((issue) => {
273
+ console.log(path.relative(process.cwd(), issue.file));
274
+ if (issue.reason === "unknown-variant") {
275
+ console.log(` Unknown variant in class: ${issue.className}`);
276
+ } else {
277
+ console.log(` Unknown class: ${issue.className}`);
278
+ }
279
+ if (issue.suggestion) {
280
+ console.log(` Did you mean: ${issue.suggestion}?`);
281
+ }
282
+ console.log("");
283
+ });
284
+
285
+ console.log("Run `emily-css build` after fixing classes.");
286
+ return { ok: false, issues, exitCode: 1 };
287
+ }
288
+
289
+ module.exports = {
290
+ doctor,
291
+ normaliseClassForManifest,
292
+ suggestClassName,
293
+ };