emily-css 1.2.0-alpha.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -23,6 +23,17 @@ All notable changes to `emily-css` are documented here.
23
23
 
24
24
  ---
25
25
 
26
+ ## v1.2.1 — May 2026
27
+
28
+ **updated the full system to be more efficient**
29
+
30
+ ### Added
31
+ - updated the full system to be more efficient
32
+
33
+ ### Changed
34
+ - updated release logic
35
+
36
+ ---
26
37
  ## v1.1.1 — May 2026
27
38
 
28
39
  **updated changes and added**
package/bin/emilyui.js CHANGED
@@ -4,6 +4,21 @@ const path = require("path");
4
4
 
5
5
  const command = process.argv[2];
6
6
  const packageJson = require(path.join(__dirname, "..", "package.json"));
7
+ const usageText = `
8
+ emily-css — Config-driven CSS framework generator
9
+
10
+ Usage:
11
+ emily-css init Set up a new project
12
+ emily-css build Generate production CSS to the configured output path
13
+ emily-css watch Dev mode: rebuild on changes
14
+ emily-css doctor Scan project files for unknown EmilyCSS classes
15
+ emily-css migrate Generate a Tailwind-to-EmilyCSS migration report
16
+ --import-colours Detect Tailwind colour palettes and suggest importedPalettes config
17
+ emily-css showcase Browse components in your browser
18
+ emily-css help Full command reference
19
+
20
+ Run emily-css help for more detail.
21
+ `;
7
22
 
8
23
  if (command === "init") {
9
24
  require("../src/init.js");
@@ -50,19 +65,11 @@ if (command === "init") {
50
65
  Docs: https://emilyui.dev
51
66
  `);
52
67
  } else {
53
- console.log(`
54
- emily-css — Config-driven CSS framework generator
55
-
56
- Usage:
57
- emily-css init Set up a new project
58
- emily-css build Generate production CSS to the configured output path
59
- emily-css watch Dev mode: rebuild on changes
60
- emily-css doctor Scan project files for unknown EmilyCSS classes
61
- emily-css migrate Generate a Tailwind-to-EmilyCSS migration report
62
- --import-colours Detect Tailwind colour palettes and suggest importedPalettes config
63
- emily-css showcase Browse components in your browser
64
- emily-css help Full command reference
65
-
66
- Run emily-css help for more detail.
67
- `);
68
+ if (!command) {
69
+ console.log(usageText);
70
+ } else {
71
+ console.error(`Unknown command: ${command}`);
72
+ console.log(usageText);
73
+ process.exitCode = 1;
74
+ }
68
75
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "emily-css",
3
- "version": "1.2.0-alpha.0",
3
+ "version": "1.2.0",
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": {
@@ -0,0 +1,65 @@
1
+ const DEFAULT_EXTENSIONS = [
2
+ '.html',
3
+ '.htm',
4
+ '.twig',
5
+ '.njk',
6
+ '.liquid',
7
+ '.hbs',
8
+ '.js',
9
+ '.jsx',
10
+ '.ts',
11
+ '.tsx',
12
+ '.vue',
13
+ '.php',
14
+ '.astro',
15
+ '.svelte',
16
+ '.blade.php',
17
+ '.jinja',
18
+ '.jinja2',
19
+ '.j2',
20
+ '.md',
21
+ ];
22
+
23
+ const DEFAULT_PURGE_IGNORE = [
24
+ 'node_modules',
25
+ '.git',
26
+ '.nuxt',
27
+ '.next',
28
+ '.output',
29
+ 'dist',
30
+ 'build',
31
+ 'coverage',
32
+ '.cache',
33
+ '.vite',
34
+ ];
35
+
36
+ const DEFAULT_RESPONSIVE_VARIANTS = ['sm', 'md', 'lg', 'xl', '2xl'];
37
+
38
+ const BASE_VARIANTS = [
39
+ 'hover',
40
+ 'focus',
41
+ 'focus-within',
42
+ 'focus-visible',
43
+ 'active',
44
+ 'disabled',
45
+ 'motion-reduce',
46
+ 'motion-safe',
47
+ 'aria-expanded',
48
+ 'aria-selected',
49
+ 'aria-current',
50
+ 'aria-disabled',
51
+ 'data-open',
52
+ 'data-closed',
53
+ 'dark',
54
+ 'forced-colors',
55
+ ];
56
+
57
+ const PURGE_EXTENSIONS = [...DEFAULT_EXTENSIONS];
58
+
59
+ module.exports = {
60
+ DEFAULT_EXTENSIONS,
61
+ DEFAULT_PURGE_IGNORE,
62
+ DEFAULT_RESPONSIVE_VARIANTS,
63
+ BASE_VARIANTS,
64
+ PURGE_EXTENSIONS,
65
+ };
package/src/doctor.js CHANGED
@@ -3,28 +3,7 @@ const path = require("path");
3
3
  const fg = require("fast-glob");
4
4
  const { extractClassNames } = require("./purge.js");
5
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
- ];
6
+ const { DEFAULT_EXTENSIONS } = require("./constants.js");
28
7
 
29
8
  function getConfigPath() {
30
9
  return path.join(process.cwd(), "emily.config.json");
package/src/generators.js CHANGED
@@ -281,6 +281,7 @@ function transitionUtilities() {
281
281
  // Transforms
282
282
  function transformUtilities(spacing) {
283
283
  let css = `/* Transforms */\n`;
284
+ const composedTransform = 'translate(var(--translate-x, 0), var(--translate-y, 0)) rotate(var(--rotate, 0)) skewX(var(--skew-x, 0)) skewY(var(--skew-y, 0)) scaleX(var(--scale-x, 1)) scaleY(var(--scale-y, 1))';
284
285
 
285
286
  css += `.transform { transform: translateZ(0); }\n`;
286
287
  css += `.transform-gpu { transform: translate3d(0, 0, 0); }\n`;
@@ -289,30 +290,30 @@ function transformUtilities(spacing) {
289
290
  // Translate
290
291
  Object.entries(spacing).forEach(([key, value]) => {
291
292
  const escaped = escapeClassName(key);
292
- css += `.translate-x-${escaped} { transform: translateX(${value}); }\n`;
293
- css += `.translate-y-${escaped} { transform: translateY(${value}); }\n`;
294
- css += `.-translate-x-${escaped} { transform: translateX(-${value}); }\n`;
295
- css += `.-translate-y-${escaped} { transform: translateY(-${value}); }\n`;
293
+ css += `.translate-x-${escaped} { --translate-x: ${value}; transform: ${composedTransform}; }\n`;
294
+ css += `.translate-y-${escaped} { --translate-y: ${value}; transform: ${composedTransform}; }\n`;
295
+ css += `.-translate-x-${escaped} { --translate-x: -${value}; transform: ${composedTransform}; }\n`;
296
+ css += `.-translate-y-${escaped} { --translate-y: -${value}; transform: ${composedTransform}; }\n`;
296
297
  });
297
298
 
298
299
  // Rotate
299
300
  const rotations = [0, 1, 2, 3, 6, 12, 45, 90, 180];
300
301
  rotations.forEach(deg => {
301
- css += `.rotate-${deg} { transform: rotate(${deg}deg); }\n`;
302
- if (deg > 0) css += `.-rotate-${deg} { transform: rotate(-${deg}deg); }\n`;
302
+ css += `.rotate-${deg} { --rotate: ${deg}deg; transform: ${composedTransform}; }\n`;
303
+ if (deg > 0) css += `.-rotate-${deg} { --rotate: -${deg}deg; transform: ${composedTransform}; }\n`;
303
304
  });
304
305
 
305
306
  // Scale
306
307
  const scales = [0, 50, 75, 90, 95, 100, 110, 125, 150];
307
308
  scales.forEach(scale => {
308
- css += `.scale-${scale} { transform: scale(${scale / 100}); }\n`;
309
+ css += `.scale-${scale} { --scale-x: ${scale / 100}; --scale-y: ${scale / 100}; transform: ${composedTransform}; }\n`;
309
310
  });
310
311
 
311
312
  // Skew
312
313
  const skews = [0, 1, 2, 3];
313
314
  skews.forEach(sk => {
314
- css += `.skew-x-${sk} { transform: skewX(${sk}deg); }\n`;
315
- css += `.skew-y-${sk} { transform: skewY(${sk}deg); }\n`;
315
+ css += `.skew-x-${sk} { --skew-x: ${sk}deg; transform: ${composedTransform}; }\n`;
316
+ css += `.skew-y-${sk} { --skew-y: ${sk}deg; transform: ${composedTransform}; }\n`;
316
317
  });
317
318
 
318
319
  // Transform origin
@@ -349,18 +350,21 @@ function shadowUtilities() {
349
350
  function ringUtilities(colours) {
350
351
  let css = `/* Rings & Outlines */\n`;
351
352
 
352
- css += `.ring-0 { box-shadow: 0 0 0 0px var(--ring-color, transparent); }\n`;
353
- css += `.ring-1 { box-shadow: 0 0 0 1px var(--ring-color, transparent); }\n`;
354
- css += `.ring-2 { box-shadow: 0 0 0 2px var(--ring-color, transparent); }\n`;
353
+ css += `.ring-0 { --ring-offset-width: 0px; --ring-offset-color: #fff; --ring-color: currentColor; box-shadow: 0 0 0 var(--ring-offset-width, 0px) var(--ring-offset-color, #fff), 0 0 0 var(--ring-offset-width, 0px) transparent; }\n`;
354
+ css += `.ring-1 { --ring-offset-width: 0px; --ring-offset-color: #fff; --ring-color: currentColor; box-shadow: 0 0 0 var(--ring-offset-width, 0px) var(--ring-offset-color, #fff), 0 0 0 calc(1px + var(--ring-offset-width, 0px)) var(--ring-color, currentColor); }\n`;
355
+ css += `.ring-2 { --ring-offset-width: 0px; --ring-offset-color: #fff; --ring-color: currentColor; box-shadow: 0 0 0 var(--ring-offset-width, 0px) var(--ring-offset-color, #fff), 0 0 0 calc(2px + var(--ring-offset-width, 0px)) var(--ring-color, currentColor); }\n`;
355
356
 
356
357
  css += `.ring-offset-0 { --ring-offset-width: 0px; }\n`;
357
358
  css += `.ring-offset-2 { --ring-offset-width: 2px; }\n`;
358
359
  css += `.ring-offset-4 { --ring-offset-width: 4px; }\n`;
360
+ css += `.ring-offset-white { --ring-offset-color: #fff; }\n`;
361
+ css += `.ring-offset-black { --ring-offset-color: #000; }\n`;
359
362
 
360
363
  // Ring colours
361
364
  Object.entries(colours).forEach(([colourName, shades]) => {
362
365
  Object.entries(shades).forEach(([shade]) => {
363
366
  css += `.ring-${colourName}-${shade} { --ring-color: var(--color-${colourName}-${shade}); }\n`;
367
+ css += `.ring-offset-${colourName}-${shade} { --ring-offset-color: var(--color-${colourName}-${shade}); }\n`;
364
368
  });
365
369
  });
366
370
 
@@ -642,13 +646,10 @@ function accessibilityUtilities() {
642
646
  // Container Queries (Forward-looking)
643
647
  function containerUtilities() {
644
648
  return `/* Container Queries */
645
- @supports (container-type: inline-size) {
646
- .container-type-inline { container-type: inline-size; }
647
- @container (min-width: 20rem) { .cq-xs\\: { /* utilities */ } }
648
- @container (min-width: 28rem) { .cq-sm\\: { /* utilities */ } }
649
- @container (min-width: 36rem) { .cq-md\\: { /* utilities */ } }
650
- @container (min-width: 48rem) { .cq-lg\\: { /* utilities */ } }
651
- }
649
+ .container-type-inline { container-type: inline-size; }
650
+ .container-type-size { container-type: size; }
651
+ .container-type-normal { container-type: normal; }
652
+ .container-name-none { container-name: none; }
652
653
 
653
654
  `;
654
655
  }
package/src/index.js CHANGED
@@ -235,6 +235,7 @@ function generateCSSVariables(colours, spacing, config) {
235
235
  css += ` --color-${name}: ${hex};\n`;
236
236
  });
237
237
  }
238
+ css += ` --focus-ring-glow: color-mix(in srgb, var(--color-brand-80) 12%, transparent);\n`;
238
239
 
239
240
  // Spacing variables
240
241
  Object.entries(spacing).forEach(([key, value]) => {
@@ -1241,7 +1242,7 @@ function generatePatternComponents() {
1241
1242
  outline: 2px solid var(--color-neutral-80);
1242
1243
  outline-offset: 3px;
1243
1244
  border-color: var(--color-neutral-80);
1244
- box-shadow: 0 0 0 4px rgba(219, 39, 119, 0.1);
1245
+ box-shadow: 0 0 0 4px var(--focus-ring-glow, rgba(219, 39, 119, 0.1));
1245
1246
  }
1246
1247
 
1247
1248
  .checkbox-group,
@@ -1272,7 +1273,7 @@ function generatePatternComponents() {
1272
1273
  input[type="checkbox"]:focus {
1273
1274
  outline: 2px solid var(--color-neutral-80);
1274
1275
  outline-offset: 3px;
1275
- box-shadow: 0 0 0 4px rgba(219, 39, 119, 0.1);
1276
+ box-shadow: 0 0 0 4px var(--focus-ring-glow, rgba(219, 39, 119, 0.1));
1276
1277
  }
1277
1278
 
1278
1279
  input[type="radio"] {
@@ -1317,7 +1318,7 @@ function generatePatternComponents() {
1317
1318
  outline: 2px solid var(--color-neutral-80);
1318
1319
  outline-offset: 3px;
1319
1320
  border-radius: 50%;
1320
- box-shadow: 0 0 0 4px rgba(219, 39, 119, 0.1);
1321
+ box-shadow: 0 0 0 4px var(--focus-ring-glow, rgba(219, 39, 119, 0.1));
1321
1322
  }
1322
1323
 
1323
1324
  input[aria-invalid="true"] {
@@ -1382,7 +1383,7 @@ function generatePatternComponents() {
1382
1383
  .btn-primary:focus-visible {
1383
1384
  outline: 2px solid var(--color-neutral-80);
1384
1385
  outline-offset: 3px;
1385
- box-shadow: 0 0 0 4px rgba(219, 39, 119, 0.1);
1386
+ box-shadow: 0 0 0 4px var(--focus-ring-glow, rgba(219, 39, 119, 0.1));
1386
1387
  }
1387
1388
 
1388
1389
  .btn-secondary {
@@ -1400,7 +1401,7 @@ function generatePatternComponents() {
1400
1401
  .btn-secondary:focus-visible {
1401
1402
  outline: 2px solid var(--color-neutral-80);
1402
1403
  outline-offset: 3px;
1403
- box-shadow: 0 0 0 4px rgba(219, 39, 119, 0.1);
1404
+ box-shadow: 0 0 0 4px var(--focus-ring-glow, rgba(219, 39, 119, 0.1));
1404
1405
  }
1405
1406
 
1406
1407
  .btn-ghost {
@@ -1416,7 +1417,7 @@ function generatePatternComponents() {
1416
1417
  .btn-ghost:focus-visible {
1417
1418
  outline: 2px solid var(--color-neutral-80);
1418
1419
  outline-offset: 3px;
1419
- box-shadow: 0 0 0 4px rgba(219, 39, 119, 0.1);
1420
+ box-shadow: 0 0 0 4px var(--focus-ring-glow, rgba(219, 39, 119, 0.1));
1420
1421
  }
1421
1422
 
1422
1423
  .btn-danger {
@@ -1432,7 +1433,7 @@ function generatePatternComponents() {
1432
1433
  .btn-danger:focus-visible {
1433
1434
  outline: 2px solid var(--color-neutral-80);
1434
1435
  outline-offset: 3px;
1435
- box-shadow: 0 0 0 4px rgba(219, 39, 119, 0.1);
1436
+ box-shadow: 0 0 0 4px var(--focus-ring-glow, rgba(219, 39, 119, 0.1));
1436
1437
  }
1437
1438
 
1438
1439
  .btn-sm {
package/src/init.js CHANGED
@@ -5,24 +5,12 @@ const { Select, Input, Confirm } = require("enquirer");
5
5
  const chalk = require("chalk");
6
6
  const ora = require("ora");
7
7
  const boxen = require("boxen");
8
+ const { DEFAULT_PURGE_IGNORE, PURGE_EXTENSIONS } = require("./constants.js");
8
9
 
9
10
  // ============================================================================
10
11
  // CONSTANTS
11
12
  // ============================================================================
12
13
 
13
- const DEFAULT_PURGE_IGNORE = [
14
- "node_modules",
15
- ".git",
16
- ".nuxt",
17
- ".next",
18
- ".output",
19
- "dist",
20
- "build",
21
- "coverage",
22
- ".cache",
23
- ".vite",
24
- ];
25
-
26
14
  const COLOUR_PRESETS = {
27
15
  primary: [
28
16
  { value: "custom", label: "Enter your own hex" },
@@ -69,28 +57,6 @@ const FONT_OPTIONS = [
69
57
  { name: "system", message: "System sans-serif (no download required)" },
70
58
  ];
71
59
 
72
- const PURGE_EXTENSIONS = [
73
- ".html",
74
- ".htm",
75
- ".twig",
76
- ".njk",
77
- ".liquid",
78
- ".hbs",
79
- ".js",
80
- ".jsx",
81
- ".ts",
82
- ".tsx",
83
- ".vue",
84
- ".php",
85
- ".astro",
86
- ".svelte",
87
- ".blade.php",
88
- ".jinja",
89
- ".jinja2",
90
- ".j2",
91
- ".md",
92
- ];
93
-
94
60
  // ============================================================================
95
61
  // HELPERS
96
62
  // ============================================================================
@@ -662,7 +628,7 @@ async function init() {
662
628
 
663
629
  const baseUnitRaw = await new Input({
664
630
  name: "baseUnit",
665
- message: "Base spacing unit in px (18px = 1.125rem)",
631
+ message: "Base spacing unit in px (label/documentation only)",
666
632
  initial: "18",
667
633
  validate: function (value) {
668
634
  const parsed = Number.parseInt(value, 10);
package/src/manifest.js CHANGED
@@ -1,23 +1,8 @@
1
1
  const MANIFEST_VERSION = '1.1.0';
2
- const DEFAULT_RESPONSIVE_VARIANTS = ['sm', 'md', 'lg', 'xl', '2xl'];
3
- const BASE_VARIANTS = [
4
- 'hover',
5
- 'focus',
6
- 'focus-within',
7
- 'focus-visible',
8
- 'active',
9
- 'disabled',
10
- 'motion-reduce',
11
- 'motion-safe',
12
- 'aria-expanded',
13
- 'aria-selected',
14
- 'aria-current',
15
- 'aria-disabled',
16
- 'data-open',
17
- 'data-closed',
18
- 'dark',
19
- 'forced-colors',
20
- ];
2
+ const {
3
+ DEFAULT_RESPONSIVE_VARIANTS,
4
+ BASE_VARIANTS,
5
+ } = require('./constants.js');
21
6
 
22
7
  function parseDeclarations(block) {
23
8
  const declarations = {};
package/src/migrate.js CHANGED
@@ -5,29 +5,8 @@ const fg = require('fast-glob');
5
5
  const { extractClassNames, getAllFiles } = require('./purge.js');
6
6
  const { generateManifest } = require('./manifest.js');
7
7
  const { normaliseClassForManifest, suggestClassName } = require('./doctor.js');
8
-
9
- const DEFAULT_EXTENSIONS = [
10
- '.html',
11
- '.htm',
12
- '.twig',
13
- '.njk',
14
- '.liquid',
15
- '.hbs',
16
- '.js',
17
- '.jsx',
18
- '.ts',
19
- '.tsx',
20
- '.vue',
21
- '.php',
22
- '.astro',
23
- '.svelte',
24
- '.blade.php',
25
- '.jinja',
26
- '.jinja2',
27
- '.j2',
28
- '.md',
29
- '.mdx',
30
- ];
8
+ const { DEFAULT_EXTENSIONS } = require('./constants.js');
9
+ const MIGRATION_DEFAULT_EXTENSIONS = [...DEFAULT_EXTENSIONS, '.mdx'];
31
10
 
32
11
  const TAILWIND_MAPPINGS = {
33
12
  'text-gray-900': {
@@ -163,6 +142,150 @@ const TAILWIND_SHADE_TO_EMILY_SHADE = {
163
142
  '950': '100',
164
143
  };
165
144
 
145
+ const SINGLE_WORD_UTILITY_ALLOWLIST = new Set([
146
+ 'flex',
147
+ 'grid',
148
+ 'hidden',
149
+ 'block',
150
+ 'inline',
151
+ 'table',
152
+ 'contents',
153
+ 'flow',
154
+ 'container',
155
+ 'relative',
156
+ 'absolute',
157
+ 'fixed',
158
+ 'sticky',
159
+ 'static',
160
+ 'visible',
161
+ 'invisible',
162
+ 'uppercase',
163
+ 'lowercase',
164
+ 'capitalize',
165
+ 'truncate',
166
+ 'antialiased',
167
+ 'italic',
168
+ 'not-italic',
169
+ 'underline',
170
+ 'overline',
171
+ 'line-through',
172
+ ]);
173
+
174
+ const UTILITY_PREFIX_ALLOWLIST = new Set([
175
+ 'bg',
176
+ 'text',
177
+ 'border',
178
+ 'outline',
179
+ 'accent',
180
+ 'fill',
181
+ 'stroke',
182
+ 'ring',
183
+ 'rounded',
184
+ 'shadow',
185
+ 'font',
186
+ 'leading',
187
+ 'tracking',
188
+ 'p',
189
+ 'px',
190
+ 'py',
191
+ 'pt',
192
+ 'pr',
193
+ 'pb',
194
+ 'pl',
195
+ 'm',
196
+ 'mx',
197
+ 'my',
198
+ 'mt',
199
+ 'mr',
200
+ 'mb',
201
+ 'ml',
202
+ 'w',
203
+ 'h',
204
+ 'min-w',
205
+ 'max-w',
206
+ 'min-h',
207
+ 'max-h',
208
+ 'gap',
209
+ 'space',
210
+ 'inset',
211
+ 'top',
212
+ 'right',
213
+ 'bottom',
214
+ 'left',
215
+ 'z',
216
+ 'order',
217
+ 'col',
218
+ 'row',
219
+ 'grid-cols',
220
+ 'grid-rows',
221
+ 'justify',
222
+ 'items',
223
+ 'content',
224
+ 'self',
225
+ 'place',
226
+ 'object',
227
+ 'overflow',
228
+ 'divide',
229
+ 'cursor',
230
+ 'select',
231
+ 'duration',
232
+ 'delay',
233
+ 'ease',
234
+ 'scale',
235
+ 'rotate',
236
+ 'translate',
237
+ 'skew',
238
+ 'origin',
239
+ 'opacity',
240
+ 'basis',
241
+ 'grow',
242
+ 'shrink',
243
+ ]);
244
+
245
+ function hasUtilityLikeSyntax(className) {
246
+ if (!className || typeof className !== 'string') {
247
+ return false;
248
+ }
249
+
250
+ const variantSeparatorIndex = className.lastIndexOf(':');
251
+ if (variantSeparatorIndex !== -1) {
252
+ const baseClass = className.slice(variantSeparatorIndex + 1);
253
+ if (!baseClass) {
254
+ return false;
255
+ }
256
+ return hasUtilityLikeSyntax(baseClass);
257
+ }
258
+
259
+ if (className.startsWith('-')) {
260
+ const baseClass = className.slice(1);
261
+ return baseClass.length > 0 && hasUtilityLikeSyntax(baseClass);
262
+ }
263
+
264
+ if (SINGLE_WORD_UTILITY_ALLOWLIST.has(className)) {
265
+ return true;
266
+ }
267
+
268
+ if (
269
+ hasArbitraryValueSyntax(className) ||
270
+ className.includes('/') ||
271
+ className.includes('.') ||
272
+ className.includes('_') ||
273
+ /\d/.test(className)
274
+ ) {
275
+ return true;
276
+ }
277
+
278
+ const parts = className.split('-').filter(Boolean);
279
+
280
+ if (parts.length >= 2) {
281
+ const first = parts[0];
282
+ const firstTwo = `${parts[0]}-${parts[1]}`;
283
+ return UTILITY_PREFIX_ALLOWLIST.has(first) || UTILITY_PREFIX_ALLOWLIST.has(firstTwo);
284
+ }
285
+
286
+ return false;
287
+ }
288
+
166
289
  function getConfigPath(options = {}) {
167
290
  return options.configPath || path.join(process.cwd(), 'emily.config.json');
168
291
  }
@@ -443,12 +566,15 @@ function isLikelyUtilityClass(className) {
443
566
  if (/\s/.test(className)) return false;
444
567
  if (className.length > 120) return false;
445
568
  if (className.startsWith('--')) return false;
569
+ if (className.startsWith(':')) return false;
446
570
  if (className.startsWith('.') || className.startsWith('#') || className.startsWith('@')) return false;
447
571
  if (className.endsWith(':')) return false;
448
572
  if (className.includes('://')) return false;
449
573
  if (/[;()={},`]/.test(className)) return false;
450
574
  if (!/[a-zA-Z]/.test(className)) return false;
451
575
  if (!/^[a-zA-Z0-9:#_./\-[\]]+$/.test(className)) return false;
576
+ if (/^[a-z]+$/.test(className) && !SINGLE_WORD_UTILITY_ALLOWLIST.has(className)) return false;
577
+ if (!hasUtilityLikeSyntax(className) && !SINGLE_WORD_UTILITY_ALLOWLIST.has(className)) return false;
452
578
 
453
579
  return true;
454
580
  }
@@ -614,7 +740,7 @@ function migrateClasses(input, options = {}) {
614
740
  }
615
741
 
616
742
  function getFilesToScan(config, options = {}) {
617
- const extensions = (config && config.purge && config.purge.extensions) || DEFAULT_EXTENSIONS;
743
+ const extensions = (config && config.purge && config.purge.extensions) || MIGRATION_DEFAULT_EXTENSIONS;
618
744
  const ignore = (config && config.purge && config.purge.ignore) || [];
619
745
 
620
746
  if (options.sourceGlobs && options.sourceGlobs.length > 0) {
package/src/purge.js CHANGED
@@ -2,28 +2,7 @@
2
2
 
3
3
  const fs = require("fs");
4
4
  const path = require("path");
5
-
6
- const DEFAULT_EXTENSIONS = [
7
- ".html",
8
- ".htm",
9
- ".twig",
10
- ".njk",
11
- ".liquid",
12
- ".hbs",
13
- ".js",
14
- ".jsx",
15
- ".ts",
16
- ".tsx",
17
- ".vue",
18
- ".php",
19
- ".astro",
20
- ".svelte",
21
- ".blade.php",
22
- ".jinja",
23
- ".jinja2",
24
- ".j2",
25
- ".md",
26
- ];
5
+ const { DEFAULT_EXTENSIONS } = require("./constants.js");
27
6
 
28
7
  function getAllFiles(dir, extensions = DEFAULT_EXTENSIONS) {
29
8
  let files = [];
package/src/watch.js CHANGED
@@ -3,6 +3,10 @@ const path = require("path");
3
3
  const chokidar = require("chokidar");
4
4
  const chalk = require("chalk");
5
5
  const fg = require("fast-glob");
6
+ const {
7
+ DEFAULT_PURGE_IGNORE,
8
+ DEFAULT_EXTENSIONS,
9
+ } = require("./constants.js");
6
10
 
7
11
  const {
8
12
  buildFullFramework,
@@ -16,6 +20,7 @@ let isRunning = false;
16
20
  let pendingRun = false;
17
21
  let previousClasses = new Set();
18
22
  let hasRunOnce = false;
23
+ let activeIgnoreList = DEFAULT_PURGE_IGNORE;
19
24
 
20
25
  function readConfig() {
21
26
  const configPath = path.join(process.cwd(), "emily.config.json");
@@ -34,22 +39,26 @@ function normalisePath(filePath) {
34
39
  return filePath.replace(/\\/g, "/");
35
40
  }
36
41
 
37
- function shouldIgnore(filePath) {
38
- const normalised = normalisePath(filePath);
42
+ function normaliseIgnoreEntry(entry) {
43
+ return normalisePath(String(entry || ""))
44
+ .replace(/^\.\/+/, "")
45
+ .replace(/^\/+|\/+$/g, "");
46
+ }
39
47
 
40
- return [
41
- "node_modules/",
42
- ".git/",
43
- ".nuxt/",
44
- ".next/",
45
- ".output/",
46
- "dist/",
47
- "build/",
48
- "coverage/",
49
- ".cache/",
50
- ".vite/",
51
- ].some(
52
- (part) => normalised.includes("/" + part) || normalised.startsWith(part),
48
+ function shouldIgnore(filePath, ignoreList = DEFAULT_PURGE_IGNORE) {
49
+ if (!filePath) return false;
50
+
51
+ const normalised = normalisePath(filePath);
52
+ const normalisedIgnoreList = (Array.isArray(ignoreList) ? ignoreList : DEFAULT_PURGE_IGNORE)
53
+ .map(normaliseIgnoreEntry)
54
+ .filter(Boolean);
55
+
56
+ return normalisedIgnoreList.some(
57
+ (entry) =>
58
+ normalised === entry ||
59
+ normalised.startsWith(entry + "/") ||
60
+ normalised.includes("/" + entry + "/") ||
61
+ normalised.endsWith("/" + entry),
53
62
  );
54
63
  }
55
64
 
@@ -80,27 +89,7 @@ function getScanFiles(config) {
80
89
  }
81
90
 
82
91
  const sourceDir = config.purge?.sourceDir || ".";
83
- const extensions = config.purge?.extensions || [
84
- ".html",
85
- ".htm",
86
- ".twig",
87
- ".njk",
88
- ".liquid",
89
- ".hbs",
90
- ".js",
91
- ".jsx",
92
- ".ts",
93
- ".tsx",
94
- ".vue",
95
- ".php",
96
- ".astro",
97
- ".svelte",
98
- ".blade.php",
99
- ".jinja",
100
- ".jinja2",
101
- ".j2",
102
- ".md",
103
- ];
92
+ const extensions = config.purge?.extensions || DEFAULT_EXTENSIONS;
104
93
 
105
94
  return fg.sync(
106
95
  extensions.map((ext) => `${sourceDir.replace(/\/$/, "")}/**/*${ext}`),
@@ -115,9 +104,10 @@ function getScanFiles(config) {
115
104
  function collectUsedClasses(config) {
116
105
  const files = getScanFiles(config);
117
106
  const usedClasses = new Set();
107
+ const ignoreList = config.purge?.ignore || DEFAULT_PURGE_IGNORE;
118
108
 
119
109
  for (const file of files) {
120
- if (shouldIgnore(file)) continue;
110
+ if (shouldIgnore(file, ignoreList)) continue;
121
111
 
122
112
  try {
123
113
  const content = fs.readFileSync(file, "utf8");
@@ -210,6 +200,7 @@ function runProductionUpdate(filePath) {
210
200
 
211
201
  try {
212
202
  const config = readConfig();
203
+ activeIgnoreList = config.purge?.ignore || DEFAULT_PURGE_IGNORE;
213
204
  const normalisedFilePath = filePath ? normalisePath(filePath) : "";
214
205
  const isConfigChange = normalisedFilePath.endsWith("emily.config.json");
215
206
 
@@ -244,12 +235,13 @@ function getWatchPaths(config) {
244
235
  }
245
236
 
246
237
  function queueUpdate(filePath) {
247
- if (filePath && shouldIgnore(filePath)) return;
238
+ if (filePath && shouldIgnore(filePath, activeIgnoreList)) return;
248
239
  runProductionUpdate(filePath);
249
240
  }
250
241
 
251
242
  function runWatch() {
252
243
  const config = readConfig();
244
+ activeIgnoreList = config.purge?.ignore || DEFAULT_PURGE_IGNORE;
253
245
  const watchPaths = getWatchPaths(config);
254
246
 
255
247
  console.log("");
@@ -270,7 +262,7 @@ function runWatch() {
270
262
  runProductionUpdate();
271
263
 
272
264
  const watcher = chokidar.watch(watchPaths, {
273
- ignored: shouldIgnore,
265
+ ignored: (filePath) => shouldIgnore(filePath, activeIgnoreList),
274
266
  ignoreInitial: true,
275
267
  awaitWriteFinish: {
276
268
  stabilityThreshold: 500,
@@ -288,4 +280,4 @@ function runWatch() {
288
280
  });
289
281
  }
290
282
 
291
- runWatch();
283
+ runWatch();