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 +2 -1
- package/dist/lib/utils/selector.js +94 -55
- package/doc/usage-guide.md +51 -15
- package/package.json +1 -1
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
|
|
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
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
-
|
|
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 + '.' +
|
|
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
|
};
|
package/doc/usage-guide.md
CHANGED
|
@@ -144,21 +144,21 @@ export default {
|
|
|
144
144
|
|
|
145
145
|
### Configuration Properties
|
|
146
146
|
|
|
147
|
-
| Property
|
|
148
|
-
|
|
|
149
|
-
| `basePath`
|
|
150
|
-
| `sourcePath`
|
|
151
|
-
| `outputPath`
|
|
152
|
-
| `sourceFilesSuffix`
|
|
153
|
-
| `floors`
|
|
154
|
-
| `importFloors`
|
|
155
|
-
| `mainCssFile`
|
|
156
|
-
| `globalVariables`
|
|
157
|
-
| `globalRootSelector`
|
|
158
|
-
| `media`
|
|
159
|
-
| `mediaSelectors`
|
|
160
|
-
| `mediaQueries`
|
|
161
|
-
| `
|
|
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
|
+
```
|