emily-css 1.2.0 → 1.2.2

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/src/index.js CHANGED
@@ -4,6 +4,7 @@
4
4
  const fs = require('fs');
5
5
  const path = require('path');
6
6
  const { generateManifest } = require('./manifest');
7
+ const { generateIntellisense } = require('./intellisense');
7
8
 
8
9
 
9
10
  // ============================================================================
@@ -1507,6 +1508,32 @@ function getManifestOutputPath(config) {
1507
1508
  : path.join(process.cwd(), outputPath);
1508
1509
  }
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
+
1510
1537
  function ensureDirectoryForFile(filePath) {
1511
1538
  const dir = path.dirname(filePath);
1512
1539
 
@@ -1729,15 +1756,30 @@ ${bodyFont}`;
1729
1756
  fs.writeFileSync(fullCssPath, css);
1730
1757
 
1731
1758
  const manifestSettings = getManifestSettings(config);
1759
+ const intellisenseSettings = getIntellisenseSettings(config);
1760
+ const shouldGenerateManifestData =
1761
+ manifestSettings.enabled || intellisenseSettings.enabled;
1762
+ const manifestData = shouldGenerateManifestData
1763
+ ? generateManifest(css, config)
1764
+ : null;
1765
+
1732
1766
  if (manifestSettings.enabled) {
1733
1767
  const manifestPath = getManifestOutputPath(config);
1734
- const manifest = generateManifest(css, config);
1735
1768
 
1736
1769
  ensureDirectoryForFile(manifestPath);
1737
- fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
1770
+ fs.writeFileSync(manifestPath, JSON.stringify(manifestData, null, 2));
1738
1771
  console.log(`✓ Generated manifest: ${manifestPath}`);
1739
1772
  }
1740
1773
 
1774
+ if (intellisenseSettings.enabled) {
1775
+ const intellisensePath = getIntellisenseOutputPath(config);
1776
+ const intellisense = generateIntellisense(manifestData);
1777
+
1778
+ ensureDirectoryForFile(intellisensePath);
1779
+ fs.writeFileSync(intellisensePath, JSON.stringify(intellisense, null, 2));
1780
+ console.log(`✓ Generated IntelliSense: ${intellisensePath}`);
1781
+ }
1782
+
1741
1783
  console.log(`✓ Generated CSS: ${fullCssPath}`);
1742
1784
  console.log(`✓ File size: ${(css.length / 1024).toFixed(2)} KB (unminified)`);
1743
1785
  console.log('Full framework build complete');
@@ -1753,11 +1795,16 @@ function minify(css) {
1753
1795
  .trim();
1754
1796
  }
1755
1797
 
1756
- function buildProductionCss() {
1798
+ function buildProductionCss(options = {}) {
1757
1799
  const config = getConfig();
1758
1800
  const sourceDir = getSourceDir(config);
1759
1801
  const fullCssPath = getFullCssPath(config);
1760
1802
  const productionCssPath = getProductionCssPath(config);
1803
+ const profile = {
1804
+ purge: 0,
1805
+ minify: 0,
1806
+ write: 0,
1807
+ };
1761
1808
 
1762
1809
  if (!fs.existsSync(fullCssPath)) {
1763
1810
  buildFullFramework();
@@ -1765,11 +1812,18 @@ function buildProductionCss() {
1765
1812
 
1766
1813
  const { purgeCSS } = require('./purge.js');
1767
1814
  const css = fs.readFileSync(fullCssPath, 'utf8');
1815
+ const purgeStart = Date.now();
1768
1816
  const purged = purgeCSS(css, sourceDir, config);
1817
+ profile.purge = Date.now() - purgeStart;
1818
+
1819
+ const minifyStart = Date.now();
1769
1820
  const minified = minify(purged);
1821
+ profile.minify = Date.now() - minifyStart;
1770
1822
 
1823
+ const writeStart = Date.now();
1771
1824
  ensureDirectoryForFile(productionCssPath);
1772
1825
  fs.writeFileSync(productionCssPath, minified);
1826
+ profile.write = Date.now() - writeStart;
1773
1827
 
1774
1828
  return {
1775
1829
  css,
@@ -1779,6 +1833,7 @@ function buildProductionCss() {
1779
1833
  outputSize: Buffer.byteLength(minified, 'utf8'),
1780
1834
  outputPath: productionCssPath,
1781
1835
  fullCssPath,
1836
+ profile: options.profile ? profile : undefined,
1782
1837
  };
1783
1838
  }
1784
1839
 
@@ -1800,11 +1855,14 @@ function ensureFullFramework() {
1800
1855
  }
1801
1856
 
1802
1857
  function build(options = {}) {
1858
+ const totalStart = Date.now();
1859
+ const frameworkStart = Date.now();
1803
1860
  ensureFullFramework();
1861
+ const frameworkMs = Date.now() - frameworkStart;
1804
1862
 
1805
1863
  const config = getConfig();
1806
1864
  const fullCssPath = getFullCssPath(config);
1807
- const result = buildProductionCss();
1865
+ const result = buildProductionCss({ profile: options.profile });
1808
1866
 
1809
1867
  console.log('\u2713 Generated production CSS: ' + path.relative(process.cwd(), result.outputPath));
1810
1868
  console.log('\u2713 File size: ' + (result.outputSize / 1024).toFixed(2) + ' KB');
@@ -1818,12 +1876,27 @@ function build(options = {}) {
1818
1876
  }
1819
1877
  }
1820
1878
 
1879
+ if (options.profile) {
1880
+ const timings = result.profile || { purge: 0, minify: 0, write: 0 };
1881
+ const totalMs = Date.now() - totalStart;
1882
+
1883
+ console.log('\nEmilyCSS build profile\n');
1884
+ console.log('Framework: ' + frameworkMs + 'ms');
1885
+ console.log('Purge: ' + timings.purge + 'ms');
1886
+ console.log('Minify: ' + timings.minify + 'ms');
1887
+ console.log('Write: ' + timings.write + 'ms');
1888
+ console.log('Total: ' + totalMs + 'ms');
1889
+ }
1890
+
1821
1891
  console.log('Build complete');
1822
1892
  }
1823
1893
 
1824
1894
  if (require.main === module) {
1825
1895
  const args = process.argv.slice(2);
1826
- build({ keepFull: args.includes('--keep-full') });
1896
+ build({
1897
+ keepFull: args.includes('--keep-full'),
1898
+ profile: args.includes('--profile'),
1899
+ });
1827
1900
  }
1828
1901
 
1829
1902
  module.exports = {
@@ -0,0 +1,27 @@
1
+ function toIntellisenseUtility(utility) {
2
+ return {
3
+ class: utility.class || null,
4
+ category: utility.category || null,
5
+ property: utility.property || null,
6
+ value: utility.value || null,
7
+ token: utility.token || null,
8
+ variants: Array.isArray(utility.variants) ? utility.variants : [],
9
+ };
10
+ }
11
+
12
+ function generateIntellisense(manifest) {
13
+ const utilities = Array.isArray(manifest && manifest.utilities)
14
+ ? manifest.utilities
15
+ : [];
16
+
17
+ return {
18
+ version: "1",
19
+ generatedAt:
20
+ (manifest && manifest.generatedAt) || new Date().toISOString(),
21
+ utilities: utilities.map(toIntellisenseUtility),
22
+ };
23
+ }
24
+
25
+ module.exports = {
26
+ generateIntellisense,
27
+ };
package/src/manifest.js CHANGED
@@ -1,4 +1,5 @@
1
1
  const MANIFEST_VERSION = '1.1.0';
2
+ const packageJson = require('../package.json');
2
3
  const {
3
4
  DEFAULT_RESPONSIVE_VARIANTS,
4
5
  BASE_VARIANTS,
@@ -38,18 +39,76 @@ function getTokenFromDeclarations(declarations) {
38
39
  return null;
39
40
  }
40
41
 
41
- function isSimpleBaseClassSelector(selector) {
42
- if (!selector || !selector.startsWith('.')) return false;
43
- if (selector.includes(' ')) return false;
44
- if (selector.includes(',')) return false;
45
- if (selector.includes('[')) return false;
46
- if (selector.includes(':')) return false;
47
- if (selector.includes('::')) return false;
48
- if (selector.includes('>')) return false;
49
- if (selector.includes('+')) return false;
50
- if (selector.includes('~')) return false;
51
-
52
- return true;
42
+ function readLeadingClassSelector(selector) {
43
+ if (!selector || !selector.startsWith('.')) return null;
44
+
45
+ let classSelector = '.';
46
+
47
+ for (let i = 1; i < selector.length; i++) {
48
+ const char = selector[i];
49
+
50
+ if (char === '\\' && i + 1 < selector.length) {
51
+ classSelector += selector[i] + selector[i + 1];
52
+ i++;
53
+ continue;
54
+ }
55
+
56
+ if (
57
+ char === ' ' ||
58
+ char === '\t' ||
59
+ char === '\n' ||
60
+ char === '\r' ||
61
+ char === ',' ||
62
+ char === '>' ||
63
+ char === '+' ||
64
+ char === '~' ||
65
+ char === ':' ||
66
+ char === '['
67
+ ) {
68
+ break;
69
+ }
70
+
71
+ classSelector += char;
72
+ }
73
+
74
+ return classSelector.length > 1 ? classSelector : null;
75
+ }
76
+
77
+ function extractManifestClassSelectors(selector) {
78
+ if (typeof selector !== 'string' || selector.trim().length === 0) {
79
+ return [];
80
+ }
81
+
82
+ const classSelectors = [];
83
+ const seen = new Set();
84
+ const selectorParts = selector
85
+ .split(',')
86
+ .map((part) => part.trim())
87
+ .filter(Boolean);
88
+
89
+ selectorParts.forEach((part) => {
90
+ const classSelector = readLeadingClassSelector(part);
91
+ if (!classSelector) return;
92
+ if (classSelector.includes('\\:')) return;
93
+
94
+ const remainder = part.slice(classSelector.length).trimStart();
95
+
96
+ // Expanded pseudo-state / pseudo-element and attribute selectors are not
97
+ // base utilities in the manifest (for example .hover\:x:hover, .x[...]).
98
+ if (
99
+ remainder.startsWith(':') ||
100
+ remainder.startsWith('::') ||
101
+ remainder.startsWith('[')
102
+ ) {
103
+ return;
104
+ }
105
+
106
+ if (seen.has(classSelector)) return;
107
+ seen.add(classSelector);
108
+ classSelectors.push(classSelector);
109
+ });
110
+
111
+ return classSelectors;
53
112
  }
54
113
 
55
114
  function inferCategory(className, property) {
@@ -249,7 +308,10 @@ function getVariants(config) {
249
308
 
250
309
  function generateManifest(css, config = {}) {
251
310
  const manifest = {
252
- version: MANIFEST_VERSION,
311
+ schemaVersion: '1',
312
+ package: packageJson.name || 'emily-css',
313
+ version: packageJson.version || '0.0.0',
314
+ manifestVersion: MANIFEST_VERSION,
253
315
  generatedAt: new Date().toISOString(),
254
316
  utilities: [],
255
317
  };
@@ -266,21 +328,25 @@ function generateManifest(css, config = {}) {
266
328
  while ((ruleMatch = ruleRegex.exec(cleanedCss)) !== null) {
267
329
  const selector = ruleMatch[1].trim();
268
330
  const declarationBlock = ruleMatch[2].trim();
269
-
270
- if (!isSimpleBaseClassSelector(selector)) continue;
331
+ const classSelectors = extractManifestClassSelectors(selector);
332
+ if (classSelectors.length === 0) continue;
271
333
 
272
334
  const { declarations, firstProperty, firstValue } = parseDeclarations(declarationBlock);
273
335
  if (!firstProperty) continue;
274
336
 
275
- manifest.utilities.push({
276
- class: normalizeClassName(selector),
277
- category: inferCategory(normalizeClassName(selector), firstProperty),
278
- property: firstProperty,
279
- value: firstValue,
280
- token: getTokenFromDeclarations(declarations),
281
- declarations,
282
- variants,
283
- source: 'generated-css',
337
+ classSelectors.forEach((classSelector) => {
338
+ const className = normalizeClassName(classSelector);
339
+
340
+ manifest.utilities.push({
341
+ class: className,
342
+ category: inferCategory(className, firstProperty),
343
+ property: firstProperty,
344
+ value: firstValue,
345
+ token: getTokenFromDeclarations(declarations),
346
+ declarations,
347
+ variants,
348
+ source: 'generated-css',
349
+ });
284
350
  });
285
351
  }
286
352
 
package/src/purge.js CHANGED
@@ -36,23 +36,48 @@ function getAllFiles(dir, extensions = DEFAULT_EXTENSIONS) {
36
36
 
37
37
  function extractClassNames(content) {
38
38
  const classNames = new Set();
39
- const classRegex = /(?:class|className)\s*=\s*["']([^"']+)["']/g;
39
+
40
+ function isLikelyClassToken(token) {
41
+ if (!token || typeof token !== "string") return false;
42
+ if (token.length > 120) return false;
43
+ if (token.includes("://")) return false;
44
+ if (token.startsWith(".") || token.startsWith("#") || token.startsWith("@")) return false;
45
+ if (token.endsWith(":")) return false;
46
+ if (/[(){};,`$]/.test(token)) return false;
47
+ if (!/^[a-zA-Z0-9:#_./\-[\]]+$/.test(token)) return false;
48
+ if (!/[a-zA-Z]/.test(token)) return false;
49
+
50
+ return true;
51
+ }
52
+
53
+ function addClassToken(token) {
54
+ if (!token) return;
55
+
56
+ const cleaned = token.trim().replace(/^['"`]+|['"`]+$/g, "");
57
+ if (!cleaned) return;
58
+ if (!isLikelyClassToken(cleaned)) return;
59
+
60
+ classNames.add(cleaned);
61
+ }
62
+
63
+ const classRegex = /(?:class|className)\s*=\s*(["'])([\s\S]*?)\1/g;
40
64
  let match;
41
65
 
42
66
  while ((match = classRegex.exec(content)) !== null) {
43
- const classes = match[1].split(/\s+/);
44
- classes.forEach((cls) => {
45
- if (cls.trim()) classNames.add(cls.trim());
46
- });
67
+ const classes = match[2].split(/\s+/);
68
+ classes.forEach(addClassToken);
47
69
  }
48
70
 
49
- const vueRegex = /(?::class|class\.|v-bind:class)\s*=\s*["'{]([^"'}]+)["'}]/g;
50
- while ((match = vueRegex.exec(content)) !== null) {
51
- const classes = match[1].split(/[\s,]+/);
52
- classes.forEach((cls) => {
53
- const cleaned = cls.replace(/['"`{}"]/g, "").trim();
54
- if (cleaned) classNames.add(cleaned);
55
- });
71
+ const vueBindingRegex = /(?:\:class|v-bind:class)\s*=\s*(["'])([\s\S]*?)\1/g;
72
+ const vueObjectKeyRegex = /['"`]([^'"`]+)['"`]\s*:/g;
73
+
74
+ while ((match = vueBindingRegex.exec(content)) !== null) {
75
+ const bindingContent = match[2];
76
+ let keyMatch;
77
+
78
+ while ((keyMatch = vueObjectKeyRegex.exec(bindingContent)) !== null) {
79
+ addClassToken(keyMatch[1]);
80
+ }
56
81
  }
57
82
 
58
83
  const templateStringRegex = /`([^`]+)`/g;
@@ -62,14 +87,9 @@ function extractClassNames(content) {
62
87
 
63
88
  possibleClasses.forEach((cls) => {
64
89
  const cleaned = cls.trim();
65
-
66
- if (
67
- cleaned &&
68
- /^[a-zA-Z0-9:_./-]+$/.test(cleaned) &&
69
- /[-:]/.test(cleaned)
70
- ) {
71
- classNames.add(cleaned);
72
- }
90
+ if (!cleaned) return;
91
+ if (!/[-:]/.test(cleaned)) return;
92
+ addClassToken(cleaned);
73
93
  });
74
94
  }
75
95
 
@@ -149,7 +149,7 @@
149
149
  </div>
150
150
  </section>
151
151
 
152
- <section id="buttons" class="bg-white rounded-lg shadow-md border border-neutral-20 p-6 mb-6 background-neutral-100">
152
+ <section id="buttons" class="bg-white rounded-lg shadow-md border border-neutral-20 p-6 mb-6">
153
153
  <h2 class="text-2xl font-bold mb-4">Buttons</h2>
154
154
 
155
155
  <div class="flex flex-wrap gap-3">