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 +8 -2
- package/dist/lib/utils/selector.js +67 -56
- package/doc/usage-guide.md +52 -15
- package/package.json +3 -1
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
|
|
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
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
};
|
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.
|
|
@@ -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.
|
|
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": {
|