esm-styles 0.1.2 → 0.1.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.
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/build.js ADDED
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env node
2
+ import { build } from './lib/build.js';
3
+ async function main() {
4
+ const args = process.argv.slice(2);
5
+ const configPath = args.find((arg) => !arg.startsWith('-')) || 'esm-styles.config.js';
6
+ const watch = args.includes('--watch') || args.includes('-w');
7
+ if (watch) {
8
+ console.log('[esm-styles] Watch mode is not supported internally due to ESM module caching.');
9
+ console.log('[esm-styles] Please use nodemon or a similar tool to restart the build process on file changes.');
10
+ console.log('[esm-styles] Example:');
11
+ console.log(" npx nodemon --watch <source-dir> --ext mjs --exec 'node dist/build.js <config-file>'");
12
+ process.exit(0);
13
+ }
14
+ try {
15
+ console.log('Build v0.0.11');
16
+ await build(configPath);
17
+ console.log('Build completed successfully.');
18
+ }
19
+ catch (err) {
20
+ console.error('Build failed:', err);
21
+ process.exit(1);
22
+ }
23
+ }
24
+ main();
package/dist/index.d.ts CHANGED
@@ -1,13 +1,8 @@
1
1
  /**
2
2
  * esm-styles
3
3
  *
4
- * A library for working with CSS styles in ESM
4
+ * A library for building CSS styles from ES modules
5
5
  */
6
- export * from './lib/types/index.js';
7
- export { default as getCss } from './lib/getCss.js';
8
- export { joinSelectors, cartesianSelectors } from './lib/utils/selectors.js';
9
- export { formatContentValue } from './lib/utils/content.js';
10
- export { jsKeyToCssKey, isEndValue } from './lib/utils/common.js';
11
- export { processMediaQueries } from './lib/utils/media.js';
12
- export { obj2css, prettifyCss } from './lib/utils/obj2css.js';
6
+ export * from "./lib/types/index.js";
7
+ export * from "./lib/index.js";
13
8
  export declare function greet(): string;
package/dist/index.js CHANGED
@@ -1,18 +1,11 @@
1
1
  /**
2
2
  * esm-styles
3
3
  *
4
- * A library for working with CSS styles in ESM
4
+ * A library for building CSS styles from ES modules
5
5
  */
6
6
  // Export types
7
- export * from './lib/types/index.js';
8
- // Main function export
9
- export { default as getCss } from './lib/getCss.js';
10
- // Utils exports for advanced usage
11
- export { joinSelectors, cartesianSelectors } from './lib/utils/selectors.js';
12
- export { formatContentValue } from './lib/utils/content.js';
13
- export { jsKeyToCssKey, isEndValue } from './lib/utils/common.js';
14
- export { processMediaQueries } from './lib/utils/media.js';
15
- export { obj2css, prettifyCss } from './lib/utils/obj2css.js';
7
+ export * from "./lib/types/index.js";
8
+ export * from "./lib/index.js";
16
9
  export function greet() {
17
- return 'esm-styles 0.1.2';
10
+ return "esm-styles 0.1.4";
18
11
  }
@@ -0,0 +1 @@
1
+ export declare function build(configPath?: string): Promise<void>;
@@ -0,0 +1,184 @@
1
+ import { getCss } from './index.js';
2
+ import { getCssVariables } from './utils/index.js';
3
+ import { getMediaShorthands } from './utils/media-shorthand.js';
4
+ import { isEndValue } from './utils/index.js';
5
+ import path from 'node:path';
6
+ import fs from 'node:fs/promises';
7
+ import { inspect } from 'node:util';
8
+ import _ from 'lodash';
9
+ export async function build(configPath = 'esm-styles.config.js') {
10
+ // --- Supporting module generation ---
11
+ // Debug: log mergedSets and sets
12
+ // console.log('[DEBUG] sets:', sets)
13
+ // console.log('[DEBUG] mergedSets:', inspect(mergedSets, { depth: 10 }))
14
+ // 1. Load config
15
+ const configFile = path.isAbsolute(configPath)
16
+ ? configPath
17
+ : path.resolve(process.cwd(), configPath);
18
+ const config = (await import(configFile)).default;
19
+ const basePath = path.resolve(process.cwd(), config.basePath || '.');
20
+ const sourcePath = path.join(basePath, config.sourcePath || '');
21
+ const outputPath = path.join(basePath, config.outputPath || '');
22
+ const suffix = config.sourceFilesSuffix || '.styles.mjs';
23
+ const layers = config.layers || [];
24
+ const mainCssFile = config.mainCssFile || 'styles.css';
25
+ // Merge media shorthands
26
+ const mediaShorthands = getMediaShorthands(config);
27
+ // Ensure output directory exists
28
+ await fs.mkdir(outputPath, { recursive: true });
29
+ const cssFiles = [];
30
+ // 2. Process globalVariables
31
+ if (config.globalVariables) {
32
+ const inputFile = path.join(sourcePath, `${config.globalVariables}${suffix}`);
33
+ const outputFile = path.join(outputPath, `global.css`);
34
+ const fileUrl = pathToFileUrl(inputFile).href + `?update=${Date.now()}`;
35
+ const varsObj = (await import(fileUrl)).default;
36
+ const cssVars = getCssVariables(varsObj);
37
+ const rootSelector = config.globalRootSelector || ':root';
38
+ const wrappedCss = `${rootSelector} {\n${cssVars}\n}`;
39
+ await fs.writeFile(outputFile, wrappedCss, 'utf8');
40
+ cssFiles.push({ type: 'global', file: 'global.css' });
41
+ }
42
+ // 3. Process media variable sets
43
+ if (config.media && config.mediaSelectors) {
44
+ for (const mediaType of Object.keys(config.media)) {
45
+ const sets = config.media[mediaType]; // e.g. ['light', 'twilight', 'dark']
46
+ let prevVarsObj = {};
47
+ // Collect all merged sets for supporting module
48
+ const mergedSets = {};
49
+ for (const setName of sets) {
50
+ // Inheritance: merge with previous
51
+ const inputFile = path.join(sourcePath, `${setName}${suffix}`);
52
+ const fileUrl = pathToFileUrl(inputFile).href + `?update=${Date.now()}`;
53
+ const varsObj = _.merge({}, prevVarsObj, (await import(fileUrl)).default);
54
+ prevVarsObj = varsObj;
55
+ mergedSets[setName] = _.cloneDeep(varsObj);
56
+ // For each selector config for this set
57
+ const selectorConfigs = config.mediaSelectors?.[mediaType]?.[setName] || [];
58
+ for (const selectorConfig of selectorConfigs) {
59
+ const { selector, mediaQuery, prefix } = selectorConfig;
60
+ // File name: {prefix.}{setName}.{mediaType}.css
61
+ const prefixPart = prefix ? `${prefix}.` : '';
62
+ const fileName = `${prefixPart}${setName}.${mediaType}.css`;
63
+ const outputFile = path.join(outputPath, fileName);
64
+ const cssVars = getCssVariables(varsObj);
65
+ const rootSelector = config.globalRootSelector || ':root';
66
+ let fullSelector = rootSelector;
67
+ if (selector) {
68
+ // If selector starts with combinator or pseudo, don't add space
69
+ if (/^[.:#[]./.test(selector)) {
70
+ fullSelector = `${rootSelector}${selector}`;
71
+ }
72
+ else {
73
+ fullSelector = `${rootSelector} ${selector}`;
74
+ }
75
+ }
76
+ const block = `${fullSelector} {\n${cssVars}\n}`;
77
+ await fs.writeFile(outputFile, block, 'utf8');
78
+ cssFiles.push({
79
+ type: 'media',
80
+ file: fileName,
81
+ mediaType,
82
+ setName,
83
+ mediaQuery,
84
+ });
85
+ }
86
+ }
87
+ // --- Supporting module generation ---
88
+ // Debug: log mergedSets and sets
89
+ console.log('[DEBUG] sets:', sets);
90
+ console.log('[DEBUG] mergedSets:', inspect(mergedSets, { depth: 10 }));
91
+ // Define the recursive function here so it has access to sets and mergedSets
92
+ const buildSupportingModule = (path, isRoot) => {
93
+ const allKeys = new Set();
94
+ for (const set of sets) {
95
+ const v = path.length === 0 ? mergedSets[set] : _.get(mergedSets[set], path);
96
+ if (v && typeof v === 'object' && !Array.isArray(v)) {
97
+ Object.keys(v).forEach((k) => allKeys.add(k));
98
+ }
99
+ else if (v !== undefined && !isRoot) {
100
+ allKeys.add(''); // placeholder for leaf, but only at non-root
101
+ }
102
+ }
103
+ // Debug: show allKeys and values at this path
104
+ console.log('[DEBUG] path:', path.join('.'), 'allKeys:', Array.from(allKeys));
105
+ for (const set of sets) {
106
+ const v = path.length === 0 ? mergedSets[set] : _.get(mergedSets[set], path);
107
+ console.log('[DEBUG] set:', set, 'path:', path.join('.'), 'value:', JSON.stringify(v));
108
+ }
109
+ const result = {};
110
+ for (const key of allKeys) {
111
+ if (key === '') {
112
+ // Only possible at non-root
113
+ const varName = '--' + path.map((k) => k.replace(/_/g, '-')).join('-');
114
+ const leaf = { var: varName };
115
+ for (let i = 0; i < sets.length; i++) {
116
+ const v = path.length === 0
117
+ ? mergedSets[sets[i]]
118
+ : _.get(mergedSets[sets[i]], path);
119
+ if (isEndValue(v)) {
120
+ leaf[sets[i]] = v;
121
+ }
122
+ }
123
+ if (Object.keys(leaf).length > 1) {
124
+ return leaf;
125
+ }
126
+ }
127
+ else {
128
+ const child = buildSupportingModule([...path, key], false);
129
+ if (child && Object.keys(child).length > 0) {
130
+ result[key] = child;
131
+ }
132
+ }
133
+ }
134
+ // Debug log for each recursion
135
+ console.log('[DEBUG] path:', path.join('.'), 'result:', JSON.stringify(result, null, 2));
136
+ return result;
137
+ };
138
+ const supportingModuleObj = buildSupportingModule([], true);
139
+ const supportingModulePath = path.join(sourcePath, `$${mediaType}.mjs`);
140
+ const moduleContent = `export default ${JSON.stringify(supportingModuleObj, null, 2)}\n`;
141
+ await fs.writeFile(supportingModulePath, moduleContent, 'utf8');
142
+ }
143
+ }
144
+ // 4. Process each layer (legacy, keep for now)
145
+ for (const layer of layers) {
146
+ const inputFile = path.join(sourcePath, `${layer}${suffix}`);
147
+ const outputFile = path.join(outputPath, `${layer}.css`);
148
+ const fileUrl = pathToFileUrl(inputFile).href + `?update=${Date.now()}`;
149
+ const stylesObj = (await import(fileUrl)).default;
150
+ // Always call getCss for the layer object, so media queries are rendered
151
+ const css = getCss(stylesObj, {
152
+ ...mediaShorthands,
153
+ globalRootSelector: config.globalRootSelector,
154
+ });
155
+ const wrappedCss = `@layer ${layer} {\n${css}\n}`;
156
+ await fs.writeFile(outputFile, wrappedCss, 'utf8');
157
+ cssFiles.push({ layer, file: `${layer}.css` });
158
+ }
159
+ // 5. Create main CSS file
160
+ const mainCssPath = path.join(outputPath, mainCssFile);
161
+ // Compose imports for variable sets
162
+ const varImports = cssFiles
163
+ .filter((f) => f.type === 'global' || f.type === 'media')
164
+ .map((f) => {
165
+ if (f.mediaQuery) {
166
+ return `@import '${f.file}' ${f.mediaQuery};`;
167
+ }
168
+ return `@import '${f.file}';`;
169
+ })
170
+ .join('\n');
171
+ // Compose imports for layers
172
+ const layerFiles = cssFiles.filter((f) => f.layer).map((f) => f.file);
173
+ const layerNames = cssFiles
174
+ .filter((f) => f.layer)
175
+ .map((f) => f.layer)
176
+ .join(', ');
177
+ const layerImports = layerFiles.map((f) => `@import '${f}';`).join('\n');
178
+ const mainCss = [varImports, layerNames ? `@layer ${layerNames};` : '', layerImports]
179
+ .filter(Boolean)
180
+ .join('\n') + '\n';
181
+ await fs.writeFile(mainCssPath, mainCss, 'utf8');
182
+ }
183
+ // Helper for file URL import
184
+ import { pathToFileURL as pathToFileUrl } from 'node:url';
@@ -31,8 +31,17 @@ export function getCss(object, mediaQueries = {}, mediaPrefixes = {}, auto) {
31
31
  const nodeType = determineNodeType(node, path);
32
32
  const pathParts = path.split('\\');
33
33
  const key = pathParts.pop() || '';
34
+ // Skip processing media query nodes that are nested within selectors
35
+ // as they will be handled separately in the media query section
36
+ const isNestedMediaQuery = pathParts.some((part) => part.startsWith('@media ') ||
37
+ (part.startsWith('@') &&
38
+ mediaQueries[part.replace(/^@\s*/, '')] !== undefined));
34
39
  switch (nodeType) {
35
40
  case 'selector': {
41
+ // Skip if this is inside a media query - it will be handled separately
42
+ if (isNestedMediaQuery) {
43
+ break;
44
+ }
36
45
  // Handle CSS property-value pairs
37
46
  const selector = joinSelectors(pathParts);
38
47
  // Create cartesian product of selectors for comma-separated parts
@@ -0,0 +1,3 @@
1
+ import type { CssJsObject, GetCssOptions, CssString } from './types/index.js';
2
+ export declare function getCss(styles: CssJsObject, options?: GetCssOptions): CssString;
3
+ export * from './types/index.js';
@@ -0,0 +1,211 @@
1
+ import * as utils from './utils/index.js';
2
+ function flatWalk(obj, selectorPath = [], result = { rules: [], media: {}, layers: {} }, options = {}, currentMedia = []) {
3
+ const props = {};
4
+ for (const key in obj) {
5
+ const value = obj[key];
6
+ if (utils.isEndValue(value)) {
7
+ const cssKey = utils.jsKeyToCssKey(key);
8
+ if (typeof value === 'object' &&
9
+ value !== null &&
10
+ 'var' in value &&
11
+ typeof value.var === 'string') {
12
+ // Use CSS variable reference
13
+ props[cssKey] = `var(${value.var})`;
14
+ }
15
+ else {
16
+ props[cssKey] = cssKey === 'content' ? utils.contentValue(value) : value;
17
+ }
18
+ }
19
+ else if (typeof value === 'object' && value !== null) {
20
+ if (key.startsWith('@media ')) {
21
+ // Recursively walk value, collecting rules for this media block
22
+ const nested = flatWalk(value, selectorPath, { rules: [], media: {}, layers: {} }, options, [...currentMedia, key]);
23
+ const mediaKey = [...currentMedia, key].join(' && ');
24
+ if (!result.media[mediaKey])
25
+ result.media[mediaKey] = [];
26
+ result.media[mediaKey].push(...nested.rules);
27
+ // Also merge any nested media
28
+ for (const nestedMediaKey in nested.media) {
29
+ if (!result.media[nestedMediaKey])
30
+ result.media[nestedMediaKey] = [];
31
+ result.media[nestedMediaKey].push(...nested.media[nestedMediaKey]);
32
+ }
33
+ }
34
+ else if (key.startsWith('@layer ')) {
35
+ if (!result.layers[key])
36
+ result.layers[key] = { rules: [], media: {}, layers: {} };
37
+ flatWalk(value, selectorPath, result.layers[key], options, currentMedia);
38
+ }
39
+ else if (key.startsWith('@')) {
40
+ // Other special keys (shorthands, etc.)
41
+ const shorthand = key.slice(1);
42
+ let handled = false;
43
+ if (options.mediaQueries && options.mediaQueries[shorthand]) {
44
+ const mediaKey = `@media ${options.mediaQueries[shorthand]}`;
45
+ flatWalk(value, selectorPath, result, options, [
46
+ ...currentMedia,
47
+ mediaKey,
48
+ ]);
49
+ handled = true;
50
+ }
51
+ if (options.selectorShorthands &&
52
+ options.selectorShorthands[shorthand]) {
53
+ const root = options.globalRootSelector || ':root';
54
+ for (const entry of options.selectorShorthands[shorthand]) {
55
+ if (entry.selector && !entry.mediaQuery) {
56
+ flatWalk(value, [[root + entry.selector], ...selectorPath], result, options, currentMedia);
57
+ handled = true;
58
+ }
59
+ if (entry.selector && entry.mediaQuery) {
60
+ const mediaKey = `@media ${entry.mediaQuery}`;
61
+ flatWalk(value, [[root + entry.selector], ...selectorPath], result, options, [...currentMedia, mediaKey]);
62
+ handled = true;
63
+ }
64
+ if (!entry.selector && entry.mediaQuery) {
65
+ const mediaKey = `@media ${entry.mediaQuery}`;
66
+ flatWalk(value, selectorPath, result, options, [
67
+ ...currentMedia,
68
+ mediaKey,
69
+ ]);
70
+ handled = true;
71
+ }
72
+ }
73
+ }
74
+ if (!handled) {
75
+ if (key.startsWith('@media') || key.startsWith('@layer')) {
76
+ // skip
77
+ }
78
+ else {
79
+ const parts = key.split(',').map((k) => k.trim());
80
+ flatWalk(value, [...selectorPath, parts], result, options, currentMedia);
81
+ }
82
+ }
83
+ }
84
+ else {
85
+ // Always treat as selector segment if not a special key
86
+ const parts = key.split(',').map((k) => k.trim());
87
+ flatWalk(value, [...selectorPath, parts], result, options, currentMedia);
88
+ }
89
+ }
90
+ }
91
+ if (Object.keys(props).length > 0) {
92
+ const selectors = utils.joinSelectorPath(selectorPath);
93
+ for (const selector of selectors) {
94
+ if (currentMedia.length === 0) {
95
+ result.rules.push({ selector, declarations: { ...props } });
96
+ }
97
+ else {
98
+ const mediaKey = currentMedia.join(' && ');
99
+ if (!result.media[mediaKey])
100
+ result.media[mediaKey] = [];
101
+ result.media[mediaKey].push({ selector, declarations: { ...props } });
102
+ }
103
+ }
104
+ }
105
+ return result;
106
+ }
107
+ function renderRules(rules) {
108
+ let css = '';
109
+ const groups = {};
110
+ for (const rule of rules) {
111
+ const declKey = JSON.stringify(rule.declarations);
112
+ if (!groups[declKey])
113
+ groups[declKey] = [];
114
+ groups[declKey].push(rule.selector);
115
+ }
116
+ for (const declKey in groups) {
117
+ const selectors = groups[declKey].join(', ');
118
+ const declarations = JSON.parse(declKey);
119
+ css += `${selectors} {\n`;
120
+ for (const key in declarations) {
121
+ css += ` ${key}: ${declarations[key]};\n`;
122
+ }
123
+ css += '}\n';
124
+ }
125
+ return css;
126
+ }
127
+ function renderLayers(layers) {
128
+ let css = '';
129
+ for (const key in layers) {
130
+ css += `${key} {\n`;
131
+ css += renderRules(layers[key].rules);
132
+ css += renderFlatMedia(layers[key].media);
133
+ css += renderLayers(layers[key].layers);
134
+ css += '}\n';
135
+ }
136
+ return css;
137
+ }
138
+ function renderFlatMedia(media) {
139
+ let css = '';
140
+ for (const key in media) {
141
+ // If the key contains '&&', recursively nest the media blocks
142
+ const mediaParts = key.split(' && ');
143
+ if (mediaParts.length > 1) {
144
+ css += mediaParts.reduceRight((inner, part) => `${part} {\n${inner}\n}`, renderFlatMedia({ ['']: media[key] }));
145
+ }
146
+ else if (key === '') {
147
+ // Render rules directly, no block
148
+ // Group rules by their declarations (stringified)
149
+ const groups = {};
150
+ for (const rule of media[key]) {
151
+ const declKey = JSON.stringify(rule.declarations);
152
+ if (!groups[declKey])
153
+ groups[declKey] = [];
154
+ groups[declKey].push(rule.selector);
155
+ }
156
+ for (const declKey in groups) {
157
+ const selectors = groups[declKey].join(', ');
158
+ const declarations = JSON.parse(declKey);
159
+ css += `${selectors} {\n`;
160
+ for (const k in declarations) {
161
+ css += ` ${k}: ${declarations[k]};\n`;
162
+ }
163
+ css += '}\n';
164
+ }
165
+ }
166
+ else {
167
+ css += `${key} {\n`;
168
+ // Group rules by their declarations (stringified)
169
+ const groups = {};
170
+ for (const rule of media[key]) {
171
+ const declKey = JSON.stringify(rule.declarations);
172
+ if (!groups[declKey])
173
+ groups[declKey] = [];
174
+ groups[declKey].push(rule.selector);
175
+ }
176
+ for (const declKey in groups) {
177
+ const selectors = groups[declKey].join(', ');
178
+ const declarations = JSON.parse(declKey);
179
+ css += `${selectors} {\n`;
180
+ for (const k in declarations) {
181
+ css += ` ${k}: ${declarations[k]};\n`;
182
+ }
183
+ css += '}\n';
184
+ }
185
+ css += '}\n';
186
+ }
187
+ }
188
+ return css;
189
+ }
190
+ function mergeMedia(target, source) {
191
+ for (const key in source) {
192
+ if (!target[key])
193
+ target[key] = [];
194
+ target[key].push(...source[key]);
195
+ }
196
+ }
197
+ export function getCss(styles, options = {}) {
198
+ const result = flatWalk(styles, [], { rules: [], media: {}, layers: {} }, options);
199
+ if (typeof console !== 'undefined') {
200
+ // console.log(
201
+ // '[esm-styles] flatWalk result:',
202
+ // JSON.stringify(result, null, 2)
203
+ // )
204
+ }
205
+ let css = '';
206
+ css += renderRules(result.rules);
207
+ css += renderFlatMedia(result.media);
208
+ css += renderLayers(result.layers);
209
+ return utils.prettifyCssString(css);
210
+ }
211
+ export * from './types/index.js';
@@ -1,43 +1,45 @@
1
1
  /**
2
2
  * Type definitions for the CSS in JS library
3
3
  */
4
- /**
5
- * CSS property value - can be string, number, or boolean
6
- */
7
- export type CssValue = string | number | boolean | null;
8
- /**
9
- * CSS selector or CSS property name
10
- */
11
- export type CssKey = string;
12
- /**
13
- * CSS styles object that can contain nested selectors or properties
14
- */
15
- export interface CssStyles {
16
- [key: CssKey]: CssValue | CssStyles;
17
- }
18
- /**
19
- * Named media queries configuration
20
- */
21
- export interface MediaQueries {
22
- [name: string]: string;
23
- }
24
- /**
25
- * Media prefixes configuration
26
- */
27
- export interface MediaPrefixes {
28
- [name: string]: string;
29
- }
30
- /**
31
- * Auto mode configuration for color schemes
32
- */
33
- export interface AutoConfig {
34
- [mode: string]: [string, string];
35
- }
36
- /**
37
- * Configuration for the CSS generator
38
- */
39
- export interface CssConfig {
40
- mediaQueries?: MediaQueries;
41
- mediaPrefixes?: MediaPrefixes;
42
- auto?: AutoConfig;
4
+ export type CssJsObject = Record<string, any>;
5
+ export interface GetCssOptions {
6
+ mediaQueries?: Record<string, string>;
7
+ mediaPrefixes?: Record<string, string>;
8
+ auto?: Record<string, string[]>;
9
+ globalRootSelector?: string;
10
+ selectorShorthands?: Record<string, {
11
+ selector?: string;
12
+ mediaQuery?: string;
13
+ prefix?: string;
14
+ }[]>;
43
15
  }
16
+ export type CssString = string;
17
+ export type CssPropertyValue = string | number;
18
+ export type CssRuleObject = Record<string, CssPropertyValue>;
19
+ export type CssAstNode = {
20
+ type: 'rule';
21
+ selector: string;
22
+ declarations: CssRuleObject;
23
+ } | {
24
+ type: 'group';
25
+ children: CssAstNode[];
26
+ } | {
27
+ type: 'at-rule';
28
+ name: string;
29
+ params: string;
30
+ children: CssAstNode[];
31
+ };
32
+ export type SelectorPath = string[][];
33
+ export type WalkCallback = (node: any, path: SelectorPath) => void;
34
+ export type JsKeyToCssKey = (key: string) => string;
35
+ export type IsEndValue = (value: any) => boolean;
36
+ export type JoinSelectorPath = (path: SelectorPath) => string;
37
+ export type ContentValue = (value: string) => string;
38
+ export type CartesianProduct = <T>(arrays: T[][]) => T[][];
39
+ export type PrettifyCssString = (css: string) => string;
40
+ export type MediaQueryHandler = (name: string, node: any, path: SelectorPath, options: GetCssOptions) => string;
41
+ export type LayerHandler = (name: string, node: any, path: SelectorPath, options: GetCssOptions) => string;
42
+ export type ContainerQueryHandler = (name: string, node: any, path: SelectorPath, options: GetCssOptions) => string;
43
+ export type IsHtmlTag = (key: string) => boolean;
44
+ export type IsSpecialSelector = (key: string) => boolean;
45
+ export type IsClassSelector = (key: string) => boolean;
@@ -2,3 +2,4 @@
2
2
  * Type definitions for the CSS in JS library
3
3
  */
4
4
  export {};
5
+ // End values can be string, number, array of such, or an object with a 'var' property and variable set keys.
@@ -0,0 +1 @@
1
+ export declare function cartesianProduct<T>(arrays: T[][]): T[][];
@@ -0,0 +1,7 @@
1
+ export function cartesianProduct(arrays) {
2
+ if (!arrays.length)
3
+ return [];
4
+ return arrays.reduce((a, b) => a.flatMap((d) => b.map((e) => [...d, e])), [
5
+ [],
6
+ ]);
7
+ }
@@ -1,9 +1,2 @@
1
- /**
2
- * Utilities for handling CSS content property values
3
- */
4
- /**
5
- * Converts a JavaScript string value to a valid CSS content property value
6
- * @param value - String value to convert for CSS content property
7
- * @returns CSS-compatible content value
8
- */
9
- export declare function formatContentValue(value: string | number | boolean | null): string;
1
+ import type { ContentValue } from '../types/index.js';
2
+ export declare const contentValue: ContentValue;
@@ -1,33 +1,19 @@
1
- /**
2
- * Utilities for handling CSS content property values
3
- */
4
- /**
5
- * Converts a JavaScript string value to a valid CSS content property value
6
- * @param value - String value to convert for CSS content property
7
- * @returns CSS-compatible content value
8
- */
9
- export function formatContentValue(value) {
10
- if (value === null)
11
- return 'none';
12
- const stringValue = String(value);
13
- // If it's already wrapped in quotes, return as is
14
- if (/^['"].*['"]$/.test(stringValue)) {
15
- return stringValue;
16
- }
17
- // Handle unicode and emoji characters
18
- const formattedValue = Array.from(stringValue)
19
- .map((char) => {
20
- const codePoint = char.codePointAt(0);
21
- if (!codePoint)
22
- return char;
23
- // Convert emoji and special characters to unicode escape sequences
24
- // Control characters and non-ASCII characters need escaping
25
- if (codePoint > 127 || codePoint < 32) {
26
- return `\\${codePoint.toString(16).padStart(6, '0')}`;
27
- }
28
- return char;
1
+ export const contentValue = value => {
2
+ if (typeof value !== 'string')
3
+ return value;
4
+ // If already quoted, return as is
5
+ if (/^'.*'$/.test(value) || /^".*"$/.test(value))
6
+ return value;
7
+ // Convert each character to CSS unicode escape: \00xxxx
8
+ const unicode = value
9
+ .split('')
10
+ .map(ch => {
11
+ const code = ch.codePointAt(0);
12
+ if (!code)
13
+ return '';
14
+ // CSS unicode escape: \00xxxx (4 hex digits, single backslash)
15
+ return '\\00' + code.toString(16).padStart(4, '0');
29
16
  })
30
17
  .join('');
31
- // Wrap in single quotes if not already wrapped
32
- return `'${formattedValue}'`;
33
- }
18
+ return `'${unicode}'`;
19
+ };
@@ -0,0 +1,2 @@
1
+ import type { IsEndValue } from '../types/index.js';
2
+ export declare const isEndValue: IsEndValue;
@@ -0,0 +1,16 @@
1
+ export const isEndValue = (value) => {
2
+ if (value == null)
3
+ return false;
4
+ if (typeof value === 'string' || typeof value === 'number')
5
+ return true;
6
+ if (Array.isArray(value)) {
7
+ return value.every((v) => typeof v === 'string' || typeof v === 'number');
8
+ }
9
+ if (typeof value === 'object' &&
10
+ value !== null &&
11
+ 'var' in value &&
12
+ typeof value.var === 'string') {
13
+ return true;
14
+ }
15
+ return false;
16
+ };
@@ -0,0 +1,2 @@
1
+ import type { IsEndValue } from '../types/index.js';
2
+ export declare const isEndValue: IsEndValue;
@@ -0,0 +1,10 @@
1
+ export const isEndValue = (value) => {
2
+ if (value == null)
3
+ return false;
4
+ if (typeof value === 'string' || typeof value === 'number')
5
+ return true;
6
+ if (Array.isArray(value)) {
7
+ return value.every((v) => typeof v === 'string' || typeof v === 'number');
8
+ }
9
+ return false;
10
+ };
@@ -0,0 +1,2 @@
1
+ import type { PrettifyCssString } from '../types/index.js';
2
+ export declare const prettifyCssString: PrettifyCssString;
@@ -0,0 +1,15 @@
1
+ // CSS formatting utility for getCss
2
+ import beautify from 'js-beautify';
3
+ export const prettifyCssString = (cssString) => {
4
+ // Simple pretty-print: trim and collapse extra whitespace
5
+ // return css
6
+ // .replace(/\s+/g, ' ')
7
+ // .replace(/\s*{\s*/g, ' {\n ')
8
+ // .replace(/;\s*/g, ';\n ')
9
+ // .replace(/}\s*/g, '\n}\n')
10
+ // .trim()
11
+ return beautify.css(cssString, {
12
+ indent_size: 2,
13
+ end_with_newline: true,
14
+ });
15
+ };
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Flattens a nested JS object into CSS variable declarations.
3
+ * Example:
4
+ * { colors: { paper: { normal: '#212121' } } }
5
+ * => [ '--colors-paper-normal: #212121;' ]
6
+ */
7
+ export declare function getCssVariables(obj: Record<string, any>, options?: {
8
+ prefix?: string;
9
+ }): string;
@@ -0,0 +1,24 @@
1
+ import _ from 'lodash';
2
+ /**
3
+ * Flattens a nested JS object into CSS variable declarations.
4
+ * Example:
5
+ * { colors: { paper: { normal: '#212121' } } }
6
+ * => [ '--colors-paper-normal: #212121;' ]
7
+ */
8
+ export function getCssVariables(obj, options = {}) {
9
+ const result = [];
10
+ function walk(current, path = []) {
11
+ if (_.isPlainObject(current)) {
12
+ for (const key in current) {
13
+ walk(current[key], [...path, key]);
14
+ }
15
+ }
16
+ else {
17
+ // Leaf value: build variable name
18
+ const varName = '--' + path.map(k => k.replace(/_/g, '-')).join('-');
19
+ result.push(`${varName}: ${current};`);
20
+ }
21
+ }
22
+ walk(obj);
23
+ return result.join('\n');
24
+ }
@@ -0,0 +1,8 @@
1
+ export * from './selector.js';
2
+ export * from './key.js';
3
+ export * from './content.js';
4
+ export * from './cartesian.js';
5
+ export * from './format.js';
6
+ export * from './end-value.js';
7
+ export * from './get-css-variables.js';
8
+ export * from './media-shorthand.js';
@@ -0,0 +1,8 @@
1
+ export * from './selector.js';
2
+ export * from './key.js';
3
+ export * from './content.js';
4
+ export * from './cartesian.js';
5
+ export * from './format.js';
6
+ export * from './end-value.js';
7
+ export * from './get-css-variables.js';
8
+ export * from './media-shorthand.js';
@@ -0,0 +1,2 @@
1
+ import type { JsKeyToCssKey } from '../types/index.js';
2
+ export declare const jsKeyToCssKey: JsKeyToCssKey;
@@ -0,0 +1,7 @@
1
+ export const jsKeyToCssKey = (key) => {
2
+ // Convert camelCase or PascalCase to kebab-case
3
+ return key
4
+ .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
5
+ .replace(/([A-Z])([A-Z][a-z])/g, '$1-$2')
6
+ .toLowerCase();
7
+ };
@@ -0,0 +1,12 @@
1
+ interface MediaShorthand {
2
+ selector?: string;
3
+ mediaQuery?: string;
4
+ prefix?: string;
5
+ }
6
+ export interface MediaShorthandResult {
7
+ mediaQueries: Record<string, string>;
8
+ selectorShorthands: Record<string, MediaShorthand[]>;
9
+ allShorthands: Record<string, MediaShorthand[]>;
10
+ }
11
+ export declare function getMediaShorthands(config: any): MediaShorthandResult;
12
+ export {};
@@ -0,0 +1,56 @@
1
+ export function getMediaShorthands(config) {
2
+ const mediaQueries = { ...config.mediaQueries };
3
+ const selectorShorthands = {};
4
+ const allShorthands = {};
5
+ const usedNames = new Set();
6
+ // 1. From mediaSelectors
7
+ if (config.mediaSelectors) {
8
+ for (const mediaType of Object.keys(config.mediaSelectors)) {
9
+ const variants = config.mediaSelectors[mediaType];
10
+ for (const variantName of Object.keys(variants)) {
11
+ const entries = variants[variantName];
12
+ for (const entry of entries) {
13
+ // If has mediaQuery and no selector: treat as media query
14
+ if (entry.mediaQuery && !entry.selector) {
15
+ if (mediaQueries[variantName]) {
16
+ console.warn(`[esm-styles] Warning: Duplicate media shorthand '${variantName}' in both mediaQueries and mediaSelectors. Using value from mediaQueries.`);
17
+ continue;
18
+ }
19
+ mediaQueries[variantName] = entry.mediaQuery;
20
+ usedNames.add(variantName);
21
+ allShorthands[variantName] = allShorthands[variantName] || [];
22
+ allShorthands[variantName].push({ mediaQuery: entry.mediaQuery });
23
+ }
24
+ // If has selector: treat as selector shorthand
25
+ if (entry.selector) {
26
+ selectorShorthands[variantName] =
27
+ selectorShorthands[variantName] || [];
28
+ selectorShorthands[variantName].push({
29
+ selector: entry.selector,
30
+ mediaQuery: entry.mediaQuery,
31
+ prefix: entry.prefix,
32
+ });
33
+ usedNames.add(variantName);
34
+ allShorthands[variantName] = allShorthands[variantName] || [];
35
+ allShorthands[variantName].push({
36
+ selector: entry.selector,
37
+ mediaQuery: entry.mediaQuery,
38
+ prefix: entry.prefix,
39
+ });
40
+ }
41
+ }
42
+ }
43
+ }
44
+ }
45
+ // 2. From mediaQueries (overrides)
46
+ if (config.mediaQueries) {
47
+ for (const name of Object.keys(config.mediaQueries)) {
48
+ if (usedNames.has(name)) {
49
+ console.warn(`[esm-styles] Warning: Duplicate media shorthand '${name}' in both mediaQueries and mediaSelectors. Using value from mediaQueries.`);
50
+ }
51
+ mediaQueries[name] = config.mediaQueries[name];
52
+ allShorthands[name] = [{ mediaQuery: config.mediaQueries[name] }];
53
+ }
54
+ }
55
+ return { mediaQueries, selectorShorthands, allShorthands };
56
+ }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Utilities for handling media queries
3
3
  */
4
- import { indent } from './common.js';
4
+ import { indent, jsKeyToCssKey } from './common.js';
5
5
  import { obj2css } from './obj2css.js';
6
6
  import { isEndValue } from './common.js';
7
7
  /**
@@ -36,7 +36,9 @@ export function processMediaQueries(mediaObject, mediaQueries = {}) {
36
36
  if (isEndValue(styles)) {
37
37
  return '';
38
38
  }
39
- const stylesCss = obj2css(styles);
39
+ // Process the styles to convert camelCase properties to kebab-case
40
+ const processedStyles = processStylesForMedia(styles);
41
+ const stylesCss = obj2css(processedStyles);
40
42
  if (!stylesCss.trim()) {
41
43
  return '';
42
44
  }
@@ -46,6 +48,31 @@ export function processMediaQueries(mediaObject, mediaQueries = {}) {
46
48
  // Join all media query blocks with newlines
47
49
  return mediaCssStrings.filter((css) => css).join('\n\n');
48
50
  }
51
+ /**
52
+ * Processes styles for media queries to ensure camelCase is converted to kebab-case
53
+ * @param styles - Style object to process
54
+ * @returns Processed style object with converted property names
55
+ */
56
+ function processStylesForMedia(styles) {
57
+ const result = {};
58
+ // Process each selector in the styles
59
+ Object.keys(styles).forEach((selector) => {
60
+ const selectorStyles = styles[selector];
61
+ // Skip if not an object
62
+ if (isEndValue(selectorStyles)) {
63
+ result[selector] = selectorStyles;
64
+ return;
65
+ }
66
+ const processedProps = {};
67
+ // Convert each property name to kebab-case
68
+ Object.keys(selectorStyles).forEach((prop) => {
69
+ const cssKey = jsKeyToCssKey(prop);
70
+ processedProps[cssKey] = selectorStyles[prop];
71
+ });
72
+ result[selector] = processedProps;
73
+ });
74
+ return result;
75
+ }
49
76
  /**
50
77
  * Checks if a key represents a media query
51
78
  * @param key - Key to check
@@ -0,0 +1,4 @@
1
+ import type { IsSpecialSelector, IsClassSelector } from '../types/index.js';
2
+ export declare const isSpecialSelector: IsSpecialSelector;
3
+ export declare const isClassSelector: IsClassSelector;
4
+ export declare const joinSelectorPath: (path: string[][]) => string[];
@@ -0,0 +1,186 @@
1
+ import * as utils from './cartesian.js';
2
+ // List of standard HTML tags (not exhaustive, but covers common cases)
3
+ const HTML_TAGS = new Set([
4
+ 'html',
5
+ 'body',
6
+ 'div',
7
+ 'span',
8
+ 'p',
9
+ 'a',
10
+ 'ul',
11
+ 'ol',
12
+ 'li',
13
+ 'img',
14
+ 'input',
15
+ 'textarea',
16
+ 'button',
17
+ 'form',
18
+ 'label',
19
+ 'table',
20
+ 'thead',
21
+ 'tbody',
22
+ 'tfoot',
23
+ 'tr',
24
+ 'th',
25
+ 'td',
26
+ 'section',
27
+ 'article',
28
+ 'nav',
29
+ 'header',
30
+ 'footer',
31
+ 'main',
32
+ 'aside',
33
+ 'h1',
34
+ 'h2',
35
+ 'h3',
36
+ 'h4',
37
+ 'h5',
38
+ 'h6',
39
+ 'video',
40
+ 'audio',
41
+ 'canvas',
42
+ 'svg',
43
+ 'select',
44
+ 'option',
45
+ 'blockquote',
46
+ 'pre',
47
+ 'code',
48
+ 'figure',
49
+ 'figcaption',
50
+ 'dl',
51
+ 'dt',
52
+ 'dd',
53
+ 'fieldset',
54
+ 'legend',
55
+ 'details',
56
+ 'summary',
57
+ 'iframe',
58
+ 'picture',
59
+ 'source',
60
+ 'map',
61
+ 'area',
62
+ 'meta',
63
+ 'link',
64
+ 'style',
65
+ 'script',
66
+ 'noscript',
67
+ 'b',
68
+ 'i',
69
+ 'u',
70
+ 's',
71
+ 'em',
72
+ 'strong',
73
+ 'small',
74
+ 'cite',
75
+ 'q',
76
+ 'dfn',
77
+ 'abbr',
78
+ 'data',
79
+ 'time',
80
+ 'mark',
81
+ 'ruby',
82
+ 'rt',
83
+ 'rp',
84
+ 'bdi',
85
+ 'bdo',
86
+ 'span',
87
+ 'br',
88
+ 'wbr',
89
+ 'ins',
90
+ 'del',
91
+ 'sub',
92
+ 'sup',
93
+ 'hr',
94
+ 'progress',
95
+ 'meter',
96
+ 'output',
97
+ 'details',
98
+ 'dialog',
99
+ 'menu',
100
+ 'menuitem',
101
+ 'fieldset',
102
+ 'legend',
103
+ 'datalist',
104
+ 'optgroup',
105
+ 'keygen',
106
+ 'command',
107
+ 'track',
108
+ 'embed',
109
+ 'object',
110
+ 'param',
111
+ 'col',
112
+ 'colgroup',
113
+ 'caption',
114
+ 'address',
115
+ 'applet',
116
+ 'base',
117
+ 'basefont',
118
+ 'big',
119
+ 'center',
120
+ 'dir',
121
+ 'font',
122
+ 'frame',
123
+ 'frameset',
124
+ 'isindex',
125
+ 'listing',
126
+ 'marquee',
127
+ 'multicol',
128
+ 'nextid',
129
+ 'nobr',
130
+ 'noembed',
131
+ 'noframes',
132
+ 'plaintext',
133
+ 'rb',
134
+ 'rtc',
135
+ 'strike',
136
+ 'tt',
137
+ 'xmp',
138
+ ]);
139
+ const isHtmlTag = (key) => {
140
+ // Only match if the key is a plain tag name (no underscores, no special chars)
141
+ return HTML_TAGS.has(key);
142
+ };
143
+ export const isSpecialSelector = (key) => {
144
+ // Pseudo-classes/elements, id, attribute selectors
145
+ return (key.startsWith(':') ||
146
+ key.startsWith('::') ||
147
+ key.startsWith('#') ||
148
+ key.startsWith('['));
149
+ };
150
+ export const isClassSelector = (key) => {
151
+ // _foo = class, __foo = descendant class
152
+ return key.startsWith('_');
153
+ };
154
+ export const joinSelectorPath = (path) => {
155
+ // Compute cartesian product of all segments
156
+ const combos = utils.cartesianProduct(path);
157
+ // Join each combination into a selector string
158
+ return combos.map((parts) => parts.reduce((acc, part) => {
159
+ if (part.startsWith('__')) {
160
+ return acc + (acc ? ' ' : '') + '.' + part.slice(2);
161
+ }
162
+ else if (part.startsWith('_')) {
163
+ return acc + (acc ? ' ' : '') + '.' + part.slice(1);
164
+ }
165
+ else if (part.startsWith('>') ||
166
+ part.startsWith('+') ||
167
+ part.startsWith('~')) {
168
+ // Combinators: always join with a space
169
+ return acc + ' ' + part;
170
+ }
171
+ else if (part.startsWith(':') ||
172
+ part.startsWith('::') ||
173
+ part.startsWith('#') ||
174
+ part.startsWith('[') ||
175
+ part.startsWith('.')) {
176
+ return acc + part;
177
+ }
178
+ else if (isHtmlTag(part)) {
179
+ return acc + (acc ? ' ' : '') + part;
180
+ }
181
+ else {
182
+ // Not a tag, not a special selector: treat as class
183
+ return acc + (acc ? '' : '') + '.' + part;
184
+ }
185
+ }, ''));
186
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "esm-styles",
3
- "version": "0.1.2",
3
+ "version": "0.1.5",
4
4
  "description": "A library for working with ESM styles",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -15,7 +15,8 @@
15
15
  "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
16
16
  "test:watch": "node --experimental-vm-modules node_modules/jest/bin/jest.js --watch",
17
17
  "test:coverage": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage",
18
- "prepublishOnly": "npm run build"
18
+ "prepublishOnly": "npm run build",
19
+ "watch": "node watch.js"
19
20
  },
20
21
  "keywords": [
21
22
  "esm",
@@ -27,14 +28,24 @@
27
28
  "engines": {
28
29
  "node": ">=14.16"
29
30
  },
31
+ "bin": {
32
+ "build": "dist/build.js"
33
+ },
30
34
  "devDependencies": {
31
- "@types/jest": "^29.5.11",
35
+ "@types/jest": "^29.5.14",
36
+ "@types/js-beautify": "^1.14.3",
37
+ "@types/lodash": "^4.17.16",
32
38
  "@types/node": "^20.11.0",
33
39
  "@typescript-eslint/eslint-plugin": "^6.18.1",
34
40
  "@typescript-eslint/parser": "^6.18.1",
35
41
  "eslint": "^8.56.0",
36
42
  "jest": "^29.7.0",
37
- "ts-jest": "^29.1.1",
38
- "typescript": "^5.3.3"
43
+ "lodash": "^4.17.21",
44
+ "nodemon": "^3.1.9",
45
+ "ts-jest": "^29.3.2",
46
+ "typescript": "^5.8.3"
47
+ },
48
+ "dependencies": {
49
+ "js-beautify": "^1.15.4"
39
50
  }
40
51
  }