esm-styles 0.3.6 → 0.3.8

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/dist/lib/build.js CHANGED
@@ -6,6 +6,7 @@ import path from 'node:path';
6
6
  import fs from 'node:fs/promises';
7
7
  // import { inspect } from 'node:util'
8
8
  import _ from 'lodash';
9
+ import CleanCSS from 'clean-css';
9
10
  export async function build(configPath = 'esm-styles.config.js') {
10
11
  // --- Supporting module generation ---
11
12
  // Debug: log mergedSets and sets
@@ -177,7 +178,7 @@ export async function build(configPath = 'esm-styles.config.js') {
177
178
  // Track output files for each floor
178
179
  const floorFiles = [];
179
180
  for (const floor of floors) {
180
- const { source, layer, outputPath: floorOutputPath } = floor;
181
+ const { source, layer, outputPath: floorOutputPath, minify } = floor;
181
182
  const inputFile = path.join(sourcePath, `${source}${suffix}`);
182
183
  // Use floor's outputPath if provided, otherwise use default
183
184
  const floorOutputDir = floorOutputPath
@@ -201,6 +202,10 @@ export async function build(configPath = 'esm-styles.config.js') {
201
202
  }
202
203
  wrappedCss = `${comment}@layer ${layer} {\n${css}\n}`;
203
204
  }
205
+ // if floor should be minified, minify wrappedCss string
206
+ if (minify) {
207
+ wrappedCss = new CleanCSS().minify(wrappedCss).styles;
208
+ }
204
209
  await fs.writeFile(outputFile, wrappedCss, 'utf8');
205
210
  // Calculate relative path from default output directory for imports
206
211
  const relativePath = floorOutputPath
@@ -252,7 +257,8 @@ export async function build(configPath = 'esm-styles.config.js') {
252
257
  '\n';
253
258
  await fs.writeFile(mainCssPath, mainCss, 'utf8');
254
259
  // 6. Create timestamp file
255
- const timestampPath = path.join(config.basePath || '.', config.timestampOutputPath || '', 'timestamp.mjs');
260
+ const { outputPath: timestampOutputPath, extension: timestampExtension } = config.timestamp || { outputPath: '', extension: 'mjs' };
261
+ const timestampPath = path.join(config.basePath || '.', timestampOutputPath, 'timestamp.' + timestampExtension);
256
262
  await fs.writeFile(timestampPath, `export default ${Date.now()}`, 'utf8');
257
263
  }
258
264
  // Helper for file URL import
@@ -1,4 +1,5 @@
1
1
  import * as utils from './cartesian.js';
2
+ import kebabCase from 'lodash/kebabCase.js';
2
3
  const svgTags = [
3
4
  'circle',
4
5
  'ellipse',
@@ -179,6 +180,17 @@ export const isClassSelector = (key) => {
179
180
  // _foo = class, __foo = descendant class
180
181
  return key.startsWith('_');
181
182
  };
183
+ const processClass = (cls) => {
184
+ if (/^[A-Z]/.test(cls)) {
185
+ return cls;
186
+ }
187
+ return kebabCase(cls);
188
+ };
189
+ const transformExplicitClasses = (selector) => {
190
+ return selector.replace(/\.([a-zA-Z0-9_-]+)/g, (_, cls) => {
191
+ return '.' + processClass(cls);
192
+ });
193
+ };
182
194
  export const joinSelectorPath = (path) => {
183
195
  // Compute cartesian product of all segments
184
196
  const combos = utils.cartesianProduct(path);
@@ -187,64 +199,63 @@ export const joinSelectorPath = (path) => {
187
199
  // Check if previous part is a root selector
188
200
  const prev = idx > 0 ? parts[idx - 1] : null;
189
201
  const isPrevRoot = prev && (prev === ':root' || prev.startsWith(':root.'));
190
- if (part === '*') {
191
- // Universal selector
192
- return acc + (acc ? ' ' : '') + '*';
193
- }
194
- else if (part.startsWith('__')) {
195
- return acc + (acc ? ' ' : '') + '.' + part.slice(2);
196
- }
197
- else if (part.startsWith('_')) {
198
- // Attach class directly to previous part unless prev is combinator or root
199
- const combinators = ['>', '+', '~'];
200
- const isPrevCombinator = prev && combinators.some((c) => prev.startsWith(c));
201
- if (isPrevRoot || isPrevCombinator || !acc) {
202
- return acc + (acc ? ' ' : '') + '.' + part.slice(1);
203
- }
204
- else {
202
+ switch (true) {
203
+ case part === '*':
204
+ // Universal selector
205
+ return acc + (acc ? ' ' : '') + '*';
206
+ case part.startsWith('__'):
207
+ return acc + (acc ? ' ' : '') + '.' + processClass(part.slice(2));
208
+ case part.startsWith('_'): {
209
+ // Attach class directly to previous part unless prev is combinator or root
210
+ const combinators = ['>', '+', '~'];
211
+ const isPrevCombinator = prev && combinators.some((c) => prev.startsWith(c));
212
+ if (isPrevRoot || isPrevCombinator || !acc) {
213
+ return acc + (acc ? ' ' : '') + '.' + processClass(part.slice(1));
214
+ }
205
215
  // Attach directly (no space)
206
- return acc + '.' + part.slice(1);
216
+ return acc + '.' + processClass(part.slice(1));
207
217
  }
218
+ case part.startsWith('>') ||
219
+ part.startsWith('+') ||
220
+ part.startsWith('~'):
221
+ // Combinators: always join with a space
222
+ return acc + ' ' + part;
223
+ case part.startsWith(':') ||
224
+ part.startsWith('::') ||
225
+ part.startsWith('#') ||
226
+ part.startsWith('[') ||
227
+ part.startsWith('.'):
228
+ return acc + transformExplicitClasses(part);
229
+ case isHtmlTag(part):
230
+ return acc + (acc ? ' ' : '') + part;
231
+ case startsWithHtmlTag(part):
232
+ // Handle compound selectors that start with HTML tags (e.g., 'div > *')
233
+ return acc + (acc ? ' ' : '') + transformExplicitClasses(part);
234
+ case /^[a-z][a-z0-9]*\.(.+)/.test(part) &&
235
+ isHtmlTag(part.split('.')[0]):
236
+ // If part matches 'tag.class...' and tag is an HTML tag
237
+ return acc + (acc ? ' ' : '') + transformExplicitClasses(part);
238
+ case /^[a-z][a-z0-9]*#[\w-]+$/.test(part) &&
239
+ isHtmlTag(part.split('#')[0]):
240
+ // If part matches 'tag#id' and tag is an HTML tag
241
+ // ID should technically not be kebab-cased, and regex ensures no classes.
242
+ return acc + (acc ? ' ' : '') + part;
243
+ default:
244
+ // Not a tag, not a special selector: treat as class or custom element
245
+ let processedPart = part;
246
+ const match = part.match(/^([a-zA-Z0-9_-]+)(.*)$/);
247
+ if (match) {
248
+ processedPart =
249
+ processClass(match[1]) + transformExplicitClasses(match[2]);
250
+ }
251
+ else {
252
+ processedPart = transformExplicitClasses(part);
253
+ }
254
+ // If previous part is a root selector, insert a space
255
+ if (isPrevRoot) {
256
+ return acc + ' ' + '.' + processedPart;
257
+ }
258
+ return acc + '.' + processedPart;
208
259
  }
209
- else if (part.startsWith('>') ||
210
- part.startsWith('+') ||
211
- part.startsWith('~')) {
212
- // Combinators: always join with a space
213
- return acc + ' ' + part;
214
- }
215
- else if (part.startsWith(':') ||
216
- part.startsWith('::') ||
217
- part.startsWith('#') ||
218
- part.startsWith('[') ||
219
- part.startsWith('.')) {
220
- return acc + part;
221
- }
222
- else if (isHtmlTag(part)) {
223
- return acc + (acc ? ' ' : '') + part;
224
- }
225
- else if (startsWithHtmlTag(part)) {
226
- // Handle compound selectors that start with HTML tags (e.g., 'div > *')
227
- return acc + (acc ? ' ' : '') + part;
228
- }
229
- else if (/^([a-z][a-z0-9]*)\.(.+)/.test(part)) {
230
- // If part matches 'tag.class...' and tag is an HTML tag
231
- const match = part.match(/^([a-z][a-z0-9]*)\.(.+)/);
232
- if (match && isHtmlTag(match[1])) {
233
- return acc + (acc ? ' ' : '') + match[1] + '.' + match[2];
234
- }
235
- }
236
- else if (/^([a-z][a-z0-9]*)#([\w-]+)$/.test(part)) {
237
- // If part matches 'tag#id' and tag is an HTML tag
238
- const match = part.match(/^([a-z][a-z0-9]*)#([\w-]+)$/);
239
- if (match && isHtmlTag(match[1])) {
240
- return acc + (acc ? ' ' : '') + match[1] + '#' + match[2];
241
- }
242
- }
243
- // Not a tag, not a special selector: treat as class or custom element
244
- // If previous part is a root selector, insert a space
245
- if (isPrevRoot) {
246
- return acc + ' ' + '.' + part;
247
- }
248
- return acc + (acc ? '' : '') + '.' + part;
249
260
  }, ''));
250
261
  };
@@ -144,21 +144,21 @@ export default {
144
144
 
145
145
  ### Configuration Properties
146
146
 
147
- | Property | Description |
148
- | --------------------- | ------------------------------------------------------------------------- |
149
- | `basePath` | Base directory for all styles, relative to where the build command is run |
150
- | `sourcePath` | Directory inside `basePath` containing source style files |
151
- | `outputPath` | Directory inside `basePath` where output CSS files will be written |
152
- | `sourceFilesSuffix` | Suffix for source style files (default: `.styles.mjs`) |
153
- | `floors` | Array of floor configurations, defining sources, layers, and output paths |
154
- | `importFloors` | Array of floor names to include in the main CSS file |
155
- | `mainCssFile` | Name of the output CSS file that imports all layer and variable files |
156
- | `globalVariables` | Name of the file containing global CSS variables |
157
- | `globalRootSelector` | Root selector for CSS variables (default: `:root`) |
158
- | `media` | Object defining media types and their variable sets |
159
- | `mediaSelectors` | Configuration for applying media types with selectors/queries |
160
- | `mediaQueries` | Object defining shorthand names for media queries |
161
- | `timestampOutputPath` | Path where timestamp.mjs file will be written (default: `basePath`) |
147
+ | Property | Description |
148
+ | -------------------- | ------------------------------------------------------------------------------- |
149
+ | `basePath` | Base directory for all styles, relative to where the build command is run |
150
+ | `sourcePath` | Directory inside `basePath` containing source style files |
151
+ | `outputPath` | Directory inside `basePath` where output CSS files will be written |
152
+ | `sourceFilesSuffix` | Suffix for source style files (default: `.styles.mjs`) |
153
+ | `floors` | Array of floor configurations, defining sources, layers, and output paths |
154
+ | `importFloors` | Array of floor names to include in the main CSS file |
155
+ | `mainCssFile` | Name of the output CSS file that imports all layer and variable files |
156
+ | `globalVariables` | Name of the file containing global CSS variables |
157
+ | `globalRootSelector` | Root selector for CSS variables (default: `:root`) |
158
+ | `media` | Object defining media types and their variable sets |
159
+ | `mediaSelectors` | Configuration for applying media types with selectors/queries |
160
+ | `mediaQueries` | Object defining shorthand names for media queries |
161
+ | `timestamp` | Configuration for timestamp.mjs file (default: `basePath` and `.mjs` extension) |
162
162
 
163
163
  ## JS to CSS Translation Rules
164
164
 
@@ -319,6 +319,24 @@ Use commas to target multiple selectors:
319
319
  }
320
320
  ```
321
321
 
322
+ ### T9: Automatic Kebab-case Conversion
323
+
324
+ Class names in selectors are automatically converted from camelCase to kebab-case, unless they are PascalCase (starting with an uppercase letter), in which case they are preserved as-is.
325
+
326
+ ```js
327
+ {
328
+ myClass: { // Becomes .my-class
329
+ color: 'red'
330
+ },
331
+ 'myClass:hover': { // Becomes .my-class:hover
332
+ color: 'blue'
333
+ },
334
+ MyComponent: { // Becomes .MyComponent (preserved)
335
+ color: 'green'
336
+ }
337
+ }
338
+ ```
339
+
322
340
  ## Floors and Layers
323
341
 
324
342
  ESM Styles supports @layer directives through its floors configuration system.
@@ -351,6 +369,7 @@ floors: [
351
369
  { source: 'layout', layer: 'layout' },
352
370
  { source: 'utilities' }, // No layer wrapper
353
371
  { source: 'overrides', outputPath: 'special' }, // Custom output path
372
+ { source: 'minified', minify: true }, // Minify CSS file
354
373
  ]
355
374
  ```
356
375
 
@@ -651,3 +670,21 @@ The supporting modules provide:
651
670
 
652
671
  - Autocomplete for available variables in most code editors
653
672
  - The ability to see the actual values for each theme or device
673
+
674
+ ## Timestamp file
675
+
676
+ ESM Styles generate a timestamp file to track the last build time. This can be useful for caching and versioning, or to trigger HMR when in development mode.
677
+
678
+ ```js
679
+ // timestamp.mjs (generated)
680
+ export default 1767867956228
681
+ ```
682
+
683
+ ### Configuration in esm-styles.config.mjs
684
+
685
+ By default, the timestamp file is put to the root of the `basePath` directory with `.mjs` extension.
686
+
687
+ ```js
688
+ // put timestamp file to [basePath]/source folder with .ts extension
689
+ timestamp: { outputPath: 'source', extension: 'ts' },
690
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "esm-styles",
3
- "version": "0.3.6",
3
+ "version": "0.3.8",
4
4
  "description": "A library for working with ESM styles",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -35,6 +35,7 @@
35
35
  "watch": "dist/watch.js"
36
36
  },
37
37
  "devDependencies": {
38
+ "@types/clean-css": "^4.2.11",
38
39
  "@types/jest": "^29.5.14",
39
40
  "@types/js-beautify": "^1.14.3",
40
41
  "@types/lodash": "^4.17.16",
@@ -49,6 +50,7 @@
49
50
  "typescript": "^5.8.3"
50
51
  },
51
52
  "dependencies": {
53
+ "clean-css": "^5.3.3",
52
54
  "js-beautify": "^1.15.4"
53
55
  },
54
56
  "repository": {