esm-styles 0.2.3 → 0.2.5
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/README.md +47 -13
- package/dist/lib/build.js +26 -16
- package/dist/lib/index.js +95 -17
- package/doc/usage-guide.md +8 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,8 +8,7 @@ A CSS-in-JS solution for JavaScript/TypeScript projects.
|
|
|
8
8
|
- Build CSS from organized source files with a simple CLI
|
|
9
9
|
- CSS layering support for proper style encapsulation
|
|
10
10
|
- Media query and device/theme selectors with shorthands
|
|
11
|
-
- CSS
|
|
12
|
-
- Supporting modules for easy CSS variable usage
|
|
11
|
+
- CSS variable sets for different themes and devices
|
|
13
12
|
|
|
14
13
|
## Installation
|
|
15
14
|
|
|
@@ -21,28 +20,63 @@ npm install esm-styles
|
|
|
21
20
|
|
|
22
21
|
### Basic Concept
|
|
23
22
|
|
|
24
|
-
ESM Styles lets you write
|
|
23
|
+
ESM Styles lets you write and store styles in JavaScript this way:
|
|
25
24
|
|
|
26
25
|
```js
|
|
27
|
-
//
|
|
26
|
+
// article.styles.mjs
|
|
27
|
+
import $device from './$device.mjs'
|
|
28
|
+
import $theme from './$theme.mjs'
|
|
29
|
+
import { card } from './card.styles.mjs'
|
|
30
|
+
|
|
28
31
|
export default {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
borderRadius: '4px',
|
|
32
|
+
article: {
|
|
33
|
+
display: 'flex',
|
|
34
|
+
|
|
35
|
+
card,
|
|
34
36
|
|
|
35
|
-
'
|
|
36
|
-
|
|
37
|
+
'@max-tablet': {
|
|
38
|
+
flexDirection: 'column',
|
|
37
39
|
},
|
|
38
40
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
+
button: {
|
|
42
|
+
borderRadius: $device.radii.md,
|
|
43
|
+
backgroundColor: $theme.paper.tinted,
|
|
44
|
+
|
|
45
|
+
'@dark': {
|
|
46
|
+
fontWeight: 300,
|
|
47
|
+
},
|
|
41
48
|
},
|
|
42
49
|
},
|
|
43
50
|
}
|
|
44
51
|
```
|
|
45
52
|
|
|
53
|
+
### Sample Directory Structure
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
monorepo/
|
|
57
|
+
├── package.json
|
|
58
|
+
└── packages/
|
|
59
|
+
├── app/
|
|
60
|
+
│ ├── package.json
|
|
61
|
+
│ └── src/
|
|
62
|
+
│ ├── css/
|
|
63
|
+
│ ├── styles.css
|
|
64
|
+
│ └── components/
|
|
65
|
+
│ ├── article.tsx
|
|
66
|
+
│ ├── button.tsx
|
|
67
|
+
│ └── card.tsx
|
|
68
|
+
└── styles/
|
|
69
|
+
├── $device.mjs
|
|
70
|
+
├── $theme.mjs
|
|
71
|
+
├── components/
|
|
72
|
+
│ ├── article.styles.mjs
|
|
73
|
+
│ ├── button.styles.mjs
|
|
74
|
+
│ └── card.styles.mjs
|
|
75
|
+
├── components.styles.mjs
|
|
76
|
+
├── esm-styles.config.js
|
|
77
|
+
└── package.json
|
|
78
|
+
```
|
|
79
|
+
|
|
46
80
|
### CLI Usage
|
|
47
81
|
|
|
48
82
|
Build your styles by creating a configuration file and running the CLI:
|
package/dist/lib/build.js
CHANGED
|
@@ -4,6 +4,7 @@ import { getMediaShorthands } from './utils/media-shorthand.js';
|
|
|
4
4
|
import { isEndValue } from './utils/index.js';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import fs from 'node:fs/promises';
|
|
7
|
+
// import { inspect } from 'node:util'
|
|
7
8
|
import _ from 'lodash';
|
|
8
9
|
export async function build(configPath = 'esm-styles.config.js') {
|
|
9
10
|
// --- Supporting module generation ---
|
|
@@ -19,7 +20,7 @@ export async function build(configPath = 'esm-styles.config.js') {
|
|
|
19
20
|
const sourcePath = path.join(basePath, config.sourcePath || '');
|
|
20
21
|
const outputPath = path.join(basePath, config.outputPath || '');
|
|
21
22
|
const suffix = config.sourceFilesSuffix || '.styles.mjs';
|
|
22
|
-
const
|
|
23
|
+
const floors = config.floors || [];
|
|
23
24
|
const mainCssFile = config.mainCssFile || 'styles.css';
|
|
24
25
|
// Merge media shorthands
|
|
25
26
|
const mediaShorthands = getMediaShorthands(config);
|
|
@@ -161,20 +162,30 @@ export async function build(configPath = 'esm-styles.config.js') {
|
|
|
161
162
|
await fs.writeFile(supportingModulePath, moduleContent, 'utf8');
|
|
162
163
|
}
|
|
163
164
|
}
|
|
164
|
-
// 4. Process each
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
165
|
+
// 4. Process each floor (replaces legacy layers)
|
|
166
|
+
const uniqueLayers = [];
|
|
167
|
+
// Track output files for each floor
|
|
168
|
+
const floorFiles = [];
|
|
169
|
+
for (const floor of floors) {
|
|
170
|
+
const { source, layer } = floor;
|
|
171
|
+
const inputFile = path.join(sourcePath, `${source}${suffix}`);
|
|
172
|
+
const outputFile = path.join(outputPath, `${source}.css`);
|
|
168
173
|
const fileUrl = pathToFileUrl(inputFile).href + `?update=${Date.now()}`;
|
|
169
174
|
const stylesObj = (await import(fileUrl)).default;
|
|
170
|
-
// Always call getCss for the layer object, so media queries are rendered
|
|
171
175
|
const css = getCss(stylesObj, {
|
|
172
176
|
...mediaShorthands,
|
|
173
177
|
globalRootSelector: config.globalRootSelector,
|
|
174
178
|
});
|
|
175
|
-
|
|
179
|
+
let wrappedCss = css;
|
|
180
|
+
if (layer) {
|
|
181
|
+
wrappedCss = `@layer ${layer} {\n${css}\n}`;
|
|
182
|
+
if (!uniqueLayers.includes(layer)) {
|
|
183
|
+
uniqueLayers.push(layer);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
176
186
|
await fs.writeFile(outputFile, wrappedCss, 'utf8');
|
|
177
|
-
|
|
187
|
+
floorFiles.push({ file: `${source}.css`, layer });
|
|
188
|
+
cssFiles.push({ floor: source, file: `${source}.css`, layer });
|
|
178
189
|
}
|
|
179
190
|
// 5. Create main CSS file
|
|
180
191
|
const mainCssPath = path.join(outputPath, mainCssFile);
|
|
@@ -188,14 +199,13 @@ export async function build(configPath = 'esm-styles.config.js') {
|
|
|
188
199
|
return `@import '${f.file}';`;
|
|
189
200
|
})
|
|
190
201
|
.join('\n');
|
|
191
|
-
// Compose imports for
|
|
192
|
-
const
|
|
193
|
-
const
|
|
194
|
-
.
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
const mainCss = [layerNames ? `@layer ${layerNames};` : '', layerImports, varImports]
|
|
202
|
+
// Compose imports for floors (in order)
|
|
203
|
+
const floorImports = floorFiles.map((f) => `@import '${f.file}';`).join('\n');
|
|
204
|
+
const mainCss = [
|
|
205
|
+
uniqueLayers.length ? `@layer ${uniqueLayers.join(', ')};` : '',
|
|
206
|
+
floorImports,
|
|
207
|
+
varImports,
|
|
208
|
+
]
|
|
199
209
|
.filter(Boolean)
|
|
200
210
|
.join('\n') + '\n';
|
|
201
211
|
await fs.writeFile(mainCssPath, mainCss, 'utf8');
|
package/dist/lib/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as utils from './utils/index.js';
|
|
2
2
|
import { toCssValue } from './utils/to-css-value.js';
|
|
3
|
-
function flatWalk(obj, selectorPath = [], result = { rules: [], media: {}, layers: {} }, options = {}, currentMedia = []) {
|
|
3
|
+
function flatWalk(obj, selectorPath = [], result = { rules: [], media: {}, layers: {}, containers: {} }, options = {}, currentMedia = [], currentContainers = []) {
|
|
4
4
|
const props = {};
|
|
5
5
|
for (const key in obj) {
|
|
6
6
|
const value = obj[key];
|
|
@@ -16,7 +16,7 @@ function flatWalk(obj, selectorPath = [], result = { rules: [], media: {}, layer
|
|
|
16
16
|
else if (typeof value === 'object' && value !== null) {
|
|
17
17
|
if (key.startsWith('@media ')) {
|
|
18
18
|
// Recursively walk value, collecting rules for this media block
|
|
19
|
-
const nested = flatWalk(value, selectorPath, { rules: [], media: {}, layers: {} }, options, [...currentMedia, key]);
|
|
19
|
+
const nested = flatWalk(value, selectorPath, { rules: [], media: {}, layers: {}, containers: {} }, options, [...currentMedia, key], currentContainers);
|
|
20
20
|
const mediaKey = [...currentMedia, key].join(' && ');
|
|
21
21
|
if (!result.media[mediaKey])
|
|
22
22
|
result.media[mediaKey] = [];
|
|
@@ -27,11 +27,42 @@ function flatWalk(obj, selectorPath = [], result = { rules: [], media: {}, layer
|
|
|
27
27
|
result.media[nestedMediaKey] = [];
|
|
28
28
|
result.media[nestedMediaKey].push(...nested.media[nestedMediaKey]);
|
|
29
29
|
}
|
|
30
|
+
// Merge nested containers
|
|
31
|
+
for (const nestedContainerKey in nested.containers) {
|
|
32
|
+
if (!result.containers[nestedContainerKey])
|
|
33
|
+
result.containers[nestedContainerKey] = [];
|
|
34
|
+
result.containers[nestedContainerKey].push(...nested.containers[nestedContainerKey]);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
else if (key.startsWith('@container')) {
|
|
38
|
+
// Recursively walk value, collecting rules for this container block
|
|
39
|
+
const nested = flatWalk(value, selectorPath, { rules: [], media: {}, layers: {}, containers: {} }, options, currentMedia, [...currentContainers, key]);
|
|
40
|
+
const containerKey = [...currentContainers, key].join(' && ');
|
|
41
|
+
if (!result.containers[containerKey])
|
|
42
|
+
result.containers[containerKey] = [];
|
|
43
|
+
result.containers[containerKey].push(...nested.rules);
|
|
44
|
+
// Also merge any nested containers
|
|
45
|
+
for (const nestedContainerKey in nested.containers) {
|
|
46
|
+
if (!result.containers[nestedContainerKey])
|
|
47
|
+
result.containers[nestedContainerKey] = [];
|
|
48
|
+
result.containers[nestedContainerKey].push(...nested.containers[nestedContainerKey]);
|
|
49
|
+
}
|
|
50
|
+
// Merge nested media
|
|
51
|
+
for (const nestedMediaKey in nested.media) {
|
|
52
|
+
if (!result.media[nestedMediaKey])
|
|
53
|
+
result.media[nestedMediaKey] = [];
|
|
54
|
+
result.media[nestedMediaKey].push(...nested.media[nestedMediaKey]);
|
|
55
|
+
}
|
|
30
56
|
}
|
|
31
57
|
else if (key.startsWith('@layer ')) {
|
|
32
58
|
if (!result.layers[key])
|
|
33
|
-
result.layers[key] = {
|
|
34
|
-
|
|
59
|
+
result.layers[key] = {
|
|
60
|
+
rules: [],
|
|
61
|
+
media: {},
|
|
62
|
+
layers: {},
|
|
63
|
+
containers: {},
|
|
64
|
+
};
|
|
65
|
+
flatWalk(value, selectorPath, result.layers[key], options, currentMedia, currentContainers);
|
|
35
66
|
}
|
|
36
67
|
else if (key.startsWith('@')) {
|
|
37
68
|
// Other special keys (shorthands, etc.)
|
|
@@ -39,10 +70,7 @@ function flatWalk(obj, selectorPath = [], result = { rules: [], media: {}, layer
|
|
|
39
70
|
let handled = false;
|
|
40
71
|
if (options.mediaQueries && options.mediaQueries[shorthand]) {
|
|
41
72
|
const mediaKey = `@media ${options.mediaQueries[shorthand]}`;
|
|
42
|
-
flatWalk(value, selectorPath, result, options, [
|
|
43
|
-
...currentMedia,
|
|
44
|
-
mediaKey,
|
|
45
|
-
]);
|
|
73
|
+
flatWalk(value, selectorPath, result, options, [...currentMedia, mediaKey], currentContainers);
|
|
46
74
|
handled = true;
|
|
47
75
|
}
|
|
48
76
|
if (options.selectorShorthands &&
|
|
@@ -50,20 +78,17 @@ function flatWalk(obj, selectorPath = [], result = { rules: [], media: {}, layer
|
|
|
50
78
|
const root = options.globalRootSelector || ':root';
|
|
51
79
|
for (const entry of options.selectorShorthands[shorthand]) {
|
|
52
80
|
if (entry.selector && !entry.mediaQuery) {
|
|
53
|
-
flatWalk(value, [[root + entry.selector], ...selectorPath], result, options, currentMedia);
|
|
81
|
+
flatWalk(value, [[root + entry.selector], ...selectorPath], result, options, currentMedia, currentContainers);
|
|
54
82
|
handled = true;
|
|
55
83
|
}
|
|
56
84
|
if (entry.selector && entry.mediaQuery) {
|
|
57
85
|
const mediaKey = `@media ${entry.mediaQuery}`;
|
|
58
|
-
flatWalk(value, [[root + entry.selector], ...selectorPath], result, options, [...currentMedia, mediaKey]);
|
|
86
|
+
flatWalk(value, [[root + entry.selector], ...selectorPath], result, options, [...currentMedia, mediaKey], currentContainers);
|
|
59
87
|
handled = true;
|
|
60
88
|
}
|
|
61
89
|
if (!entry.selector && entry.mediaQuery) {
|
|
62
90
|
const mediaKey = `@media ${entry.mediaQuery}`;
|
|
63
|
-
flatWalk(value, selectorPath, result, options, [
|
|
64
|
-
...currentMedia,
|
|
65
|
-
mediaKey,
|
|
66
|
-
]);
|
|
91
|
+
flatWalk(value, selectorPath, result, options, [...currentMedia, mediaKey], currentContainers);
|
|
67
92
|
handled = true;
|
|
68
93
|
}
|
|
69
94
|
}
|
|
@@ -74,14 +99,14 @@ function flatWalk(obj, selectorPath = [], result = { rules: [], media: {}, layer
|
|
|
74
99
|
}
|
|
75
100
|
else {
|
|
76
101
|
const parts = key.split(',').map((k) => k.trim());
|
|
77
|
-
flatWalk(value, [...selectorPath, parts], result, options, currentMedia);
|
|
102
|
+
flatWalk(value, [...selectorPath, parts], result, options, currentMedia, currentContainers);
|
|
78
103
|
}
|
|
79
104
|
}
|
|
80
105
|
}
|
|
81
106
|
else {
|
|
82
107
|
// Always treat as selector segment if not a special key
|
|
83
108
|
const parts = key.split(',').map((k) => k.trim());
|
|
84
|
-
flatWalk(value, [...selectorPath, parts], result, options, currentMedia);
|
|
109
|
+
flatWalk(value, [...selectorPath, parts], result, options, currentMedia, currentContainers);
|
|
85
110
|
}
|
|
86
111
|
}
|
|
87
112
|
}
|
|
@@ -184,6 +209,58 @@ function renderFlatMedia(media) {
|
|
|
184
209
|
}
|
|
185
210
|
return css;
|
|
186
211
|
}
|
|
212
|
+
function renderFlatContainers(containers) {
|
|
213
|
+
let css = '';
|
|
214
|
+
for (const key in containers) {
|
|
215
|
+
// If the key contains '&&', recursively nest the container blocks
|
|
216
|
+
const containerParts = key.split(' && ');
|
|
217
|
+
if (containerParts.length > 1) {
|
|
218
|
+
css += containerParts.reduceRight((inner, part) => `${part} {\n${inner}\n}`, renderFlatContainers({ ['']: containers[key] }));
|
|
219
|
+
}
|
|
220
|
+
else if (key === '') {
|
|
221
|
+
// Render rules directly, no block
|
|
222
|
+
// Group rules by their declarations (stringified)
|
|
223
|
+
const groups = {};
|
|
224
|
+
for (const rule of containers[key]) {
|
|
225
|
+
const declKey = JSON.stringify(rule.declarations);
|
|
226
|
+
if (!groups[declKey])
|
|
227
|
+
groups[declKey] = [];
|
|
228
|
+
groups[declKey].push(rule.selector);
|
|
229
|
+
}
|
|
230
|
+
for (const declKey in groups) {
|
|
231
|
+
const selectors = groups[declKey].join(', ');
|
|
232
|
+
const declarations = JSON.parse(declKey);
|
|
233
|
+
css += `${selectors} {\n`;
|
|
234
|
+
for (const k in declarations) {
|
|
235
|
+
css += ` ${k}: ${declarations[k]};\n`;
|
|
236
|
+
}
|
|
237
|
+
css += '}\n';
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
css += `${key} {\n`;
|
|
242
|
+
// Group rules by their declarations (stringified)
|
|
243
|
+
const groups = {};
|
|
244
|
+
for (const rule of containers[key]) {
|
|
245
|
+
const declKey = JSON.stringify(rule.declarations);
|
|
246
|
+
if (!groups[declKey])
|
|
247
|
+
groups[declKey] = [];
|
|
248
|
+
groups[declKey].push(rule.selector);
|
|
249
|
+
}
|
|
250
|
+
for (const declKey in groups) {
|
|
251
|
+
const selectors = groups[declKey].join(', ');
|
|
252
|
+
const declarations = JSON.parse(declKey);
|
|
253
|
+
css += `${selectors} {\n`;
|
|
254
|
+
for (const k in declarations) {
|
|
255
|
+
css += ` ${k}: ${declarations[k]};\n`;
|
|
256
|
+
}
|
|
257
|
+
css += '}\n';
|
|
258
|
+
}
|
|
259
|
+
css += '}\n';
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return css;
|
|
263
|
+
}
|
|
187
264
|
function mergeMedia(target, source) {
|
|
188
265
|
for (const key in source) {
|
|
189
266
|
if (!target[key])
|
|
@@ -192,7 +269,7 @@ function mergeMedia(target, source) {
|
|
|
192
269
|
}
|
|
193
270
|
}
|
|
194
271
|
export function getCss(styles, options = {}) {
|
|
195
|
-
const result = flatWalk(styles, [], { rules: [], media: {}, layers: {} }, options);
|
|
272
|
+
const result = flatWalk(styles, [], { rules: [], media: {}, layers: {}, containers: {} }, options);
|
|
196
273
|
if (typeof console !== 'undefined') {
|
|
197
274
|
// console.log(
|
|
198
275
|
// '[esm-styles] flatWalk result:',
|
|
@@ -202,6 +279,7 @@ export function getCss(styles, options = {}) {
|
|
|
202
279
|
let css = '';
|
|
203
280
|
css += renderRules(result.rules);
|
|
204
281
|
css += renderFlatMedia(result.media);
|
|
282
|
+
css += renderFlatContainers(result.containers);
|
|
205
283
|
css += renderLayers(result.layers);
|
|
206
284
|
return utils.prettifyCssString(css);
|
|
207
285
|
}
|
package/doc/usage-guide.md
CHANGED
|
@@ -73,8 +73,14 @@ export default {
|
|
|
73
73
|
outputPath: 'css',
|
|
74
74
|
sourceFilesSuffix: '.styles.mjs',
|
|
75
75
|
|
|
76
|
-
// Input layers
|
|
77
|
-
|
|
76
|
+
// Input files and their layers (order of layers matters)
|
|
77
|
+
floors: [
|
|
78
|
+
{ source: 'defaults', layer: 'defaults' }, // wrap in layer 'defaults'
|
|
79
|
+
{ source: 'components', layer: 'components' }, // wrap in layer 'components'
|
|
80
|
+
{ source: 'layout', layer: 'layout' }, // wrap in layer 'layout'
|
|
81
|
+
{ source: 'basic' }, // stay out of any layer
|
|
82
|
+
{ source: 'more-layout', layer: 'layout' }, // wrap in layer 'layout'
|
|
83
|
+
],
|
|
78
84
|
|
|
79
85
|
// Output
|
|
80
86
|
mainCssFile: 'styles.css',
|