esm-styles 0.3.7 → 0.3.9

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
@@ -257,7 +257,8 @@ export async function build(configPath = 'esm-styles.config.js') {
257
257
  '\n';
258
258
  await fs.writeFile(mainCssPath, mainCss, 'utf8');
259
259
  // 6. Create timestamp file
260
- 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);
261
262
  await fs.writeFile(timestampPath, `export default ${Date.now()}`, 'utf8');
262
263
  }
263
264
  // Helper for file URL import
@@ -179,6 +179,25 @@ export const isClassSelector = (key) => {
179
179
  // _foo = class, __foo = descendant class
180
180
  return key.startsWith('_');
181
181
  };
182
+ const processClass = (cls) => {
183
+ if (/^[A-Z]/.test(cls)) {
184
+ return cls;
185
+ }
186
+ // Custom kebab conversion:
187
+ // 1. Handle camelCase (e.g. myClass -> my-Class)
188
+ // 2. Handle snake_case (e.g. my_class -> my-class)
189
+ // 3. Lowercase everything (my-Class -> my-class)
190
+ // We do NOT use lodash/kebabCase because it splits numbers (i1 -> i-1), which is undesirable
191
+ return cls
192
+ .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
193
+ .replace(/_/g, '-')
194
+ .toLowerCase();
195
+ };
196
+ const transformExplicitClasses = (selector) => {
197
+ return selector.replace(/\.([a-zA-Z0-9_-]+)/g, (_, cls) => {
198
+ return '.' + processClass(cls);
199
+ });
200
+ };
182
201
  export const joinSelectorPath = (path) => {
183
202
  // Compute cartesian product of all segments
184
203
  const combos = utils.cartesianProduct(path);
@@ -187,64 +206,84 @@ export const joinSelectorPath = (path) => {
187
206
  // Check if previous part is a root selector
188
207
  const prev = idx > 0 ? parts[idx - 1] : null;
189
208
  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);
209
+ switch (true) {
210
+ case part === '*':
211
+ // Universal selector
212
+ return acc + (acc ? ' ' : '') + '*';
213
+ case part.startsWith('__'): {
214
+ const content = part.slice(2);
215
+ const match = content.match(/^([a-zA-Z0-9_-]+)(.*)$/);
216
+ let processed = '';
217
+ if (match) {
218
+ processed =
219
+ processClass(match[1]) + transformExplicitClasses(match[2]);
220
+ }
221
+ else {
222
+ processed = processClass(content);
223
+ }
224
+ return acc + (acc ? ' ' : '') + '.' + processed;
203
225
  }
204
- else {
226
+ case part.startsWith('_'): {
227
+ // Attach class directly to previous part unless prev is combinator or root
228
+ const content = part.slice(1);
229
+ const match = content.match(/^([a-zA-Z0-9_-]+)(.*)$/);
230
+ let processed = '';
231
+ if (match) {
232
+ processed =
233
+ processClass(match[1]) + transformExplicitClasses(match[2]);
234
+ }
235
+ else {
236
+ processed = processClass(content);
237
+ }
238
+ const combinators = ['>', '+', '~'];
239
+ const isPrevCombinator = prev && combinators.some((c) => prev.startsWith(c));
240
+ if (isPrevRoot || isPrevCombinator || !acc) {
241
+ return acc + (acc ? ' ' : '') + '.' + processed;
242
+ }
205
243
  // Attach directly (no space)
206
- return acc + '.' + part.slice(1);
207
- }
208
- }
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];
244
+ return acc + '.' + processed;
234
245
  }
246
+ case part.startsWith('>') ||
247
+ part.startsWith('+') ||
248
+ part.startsWith('~'):
249
+ // Combinators: always join with a space
250
+ return acc + ' ' + part;
251
+ case part.startsWith(':') ||
252
+ part.startsWith('::') ||
253
+ part.startsWith('#') ||
254
+ part.startsWith('[') ||
255
+ part.startsWith('.'):
256
+ return acc + transformExplicitClasses(part);
257
+ case isHtmlTag(part):
258
+ return acc + (acc ? ' ' : '') + part;
259
+ case startsWithHtmlTag(part):
260
+ // Handle compound selectors that start with HTML tags (e.g., 'div > *')
261
+ return acc + (acc ? ' ' : '') + transformExplicitClasses(part);
262
+ case /^[a-z][a-z0-9]*\.(.+)/.test(part) &&
263
+ isHtmlTag(part.split('.')[0]):
264
+ // If part matches 'tag.class...' and tag is an HTML tag
265
+ return acc + (acc ? ' ' : '') + transformExplicitClasses(part);
266
+ case /^[a-z][a-z0-9]*#[\w-]+$/.test(part) &&
267
+ isHtmlTag(part.split('#')[0]):
268
+ // If part matches 'tag#id' and tag is an HTML tag
269
+ // ID should technically not be kebab-cased, and regex ensures no classes.
270
+ return acc + (acc ? ' ' : '') + part;
271
+ default:
272
+ // Not a tag, not a special selector: treat as class or custom element
273
+ let processedPart = part;
274
+ const match = part.match(/^([a-zA-Z0-9_-]+)(.*)$/);
275
+ if (match) {
276
+ processedPart =
277
+ processClass(match[1]) + transformExplicitClasses(match[2]);
278
+ }
279
+ else {
280
+ processedPart = transformExplicitClasses(part);
281
+ }
282
+ // If previous part is a root selector, insert a space
283
+ if (isPrevRoot) {
284
+ return acc + ' ' + '.' + processedPart;
285
+ }
286
+ return acc + '.' + processedPart;
235
287
  }
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
288
  }, ''));
250
289
  };
@@ -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.
@@ -652,3 +670,21 @@ The supporting modules provide:
652
670
 
653
671
  - Autocomplete for available variables in most code editors
654
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.7",
3
+ "version": "0.3.9",
4
4
  "description": "A library for working with ESM styles",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",