esm-styles 0.3.12 → 0.4.1

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 CHANGED
@@ -181,6 +181,11 @@ export default {
181
181
  hover: '(hover: hover)',
182
182
  // ...whatever you want
183
183
  },
184
+
185
+ // Import aliases (optional) - replace long relative paths with short prefixes
186
+ aliases: {
187
+ '@': '.', // resolves relative to sourcePath
188
+ },
184
189
  }
185
190
  ```
186
191
 
@@ -451,6 +456,50 @@ The build process wraps floors in their respective layers and generates a main C
451
456
 
452
457
  Missing variables in one theme automatically inherit from the previous theme in the configuration.
453
458
 
459
+ ### Import Aliases
460
+
461
+ Simplify imports in your style files by configuring path aliases:
462
+
463
+ ```js
464
+ // esm-styles.config.js
465
+ export default {
466
+ // ...
467
+ aliases: {
468
+ '@': '.', // @ resolves to sourcePath
469
+ '@components': './components',
470
+ },
471
+ }
472
+ ```
473
+
474
+ Then use in your style files:
475
+
476
+ ```js
477
+ // Before (relative paths)
478
+ import $theme from '../../$theme.mjs'
479
+ import { button } from '../components/button.styles.mjs'
480
+
481
+ // After (with aliases)
482
+ import $theme from '@/$theme.mjs'
483
+ import { button } from '@components/button.styles.mjs'
484
+ ```
485
+
486
+ Alias paths are resolved relative to the `sourcePath` directory. This feature uses esbuild internally for fast module resolution.
487
+
488
+ #### IDE Support for Aliases
489
+
490
+ To enable Cmd+click navigation and IntelliSense for aliased imports, create a `jsconfig.json` in your styles source directory with matching path mappings:
491
+
492
+ ```json
493
+ {
494
+ "compilerOptions": {
495
+ "baseUrl": ".",
496
+ "paths": {
497
+ "@/*": ["./*"]
498
+ }
499
+ }
500
+ }
501
+ ```
502
+
454
503
  ## Additional documentation
455
504
 
456
505
  For humans: [doc/usage-guide.md](doc/usage-guide.md)
package/dist/lib/build.js CHANGED
@@ -7,6 +7,64 @@ import fs from 'node:fs/promises';
7
7
  // import { inspect } from 'node:util'
8
8
  import _ from 'lodash';
9
9
  import * as esbuild from 'esbuild';
10
+ /**
11
+ * Create an esbuild plugin for path alias resolution.
12
+ * Handles prefixes like '@/' -> './source/'
13
+ */
14
+ function createAliasPlugin(aliases, sourcePath) {
15
+ // Sort aliases by length (longest first) to match most specific first
16
+ const sortedAliases = Object.entries(aliases).sort(([a], [b]) => b.length - a.length);
17
+ return {
18
+ name: 'esm-styles-alias',
19
+ setup(build) {
20
+ // Match any import that starts with one of our alias prefixes
21
+ const aliasPattern = new RegExp(`^(${sortedAliases.map(([key]) => escapeRegex(key)).join('|')})(/.*)?$`);
22
+ build.onResolve({ filter: aliasPattern }, (args) => {
23
+ for (const [alias, target] of sortedAliases) {
24
+ if (args.path === alias || args.path.startsWith(alias + '/')) {
25
+ const rest = args.path.slice(alias.length); // includes leading '/' or empty
26
+ const resolvedTarget = path.resolve(sourcePath, target);
27
+ const resolvedPath = path.join(resolvedTarget, rest);
28
+ return { path: resolvedPath };
29
+ }
30
+ }
31
+ return null;
32
+ });
33
+ },
34
+ };
35
+ }
36
+ /**
37
+ * Escape special regex characters in a string
38
+ */
39
+ function escapeRegex(str) {
40
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
41
+ }
42
+ /**
43
+ * Import a module with alias resolution using esbuild.
44
+ * Falls back to direct import when no aliases are configured.
45
+ */
46
+ async function importWithAliases(filePath, aliases, sourcePath) {
47
+ // If no aliases configured, use direct import with cache-busting
48
+ if (!aliases || Object.keys(aliases).length === 0) {
49
+ const fileUrl = pathToFileUrl(filePath).href + `?update=${Date.now()}`;
50
+ return import(fileUrl);
51
+ }
52
+ // Use esbuild to bundle with alias resolution via plugin
53
+ const result = await esbuild.build({
54
+ entryPoints: [filePath],
55
+ bundle: true,
56
+ write: false,
57
+ format: 'esm',
58
+ platform: 'node',
59
+ plugins: [createAliasPlugin(aliases, sourcePath)],
60
+ // Prevent esbuild from trying to resolve node_modules
61
+ packages: 'external',
62
+ });
63
+ // Import from data URL to avoid filesystem caching
64
+ const code = result.outputFiles[0].text;
65
+ const dataUrl = `data:text/javascript;base64,${Buffer.from(code).toString('base64')}`;
66
+ return import(dataUrl);
67
+ }
10
68
  export async function build(configPath = 'esm-styles.config.js') {
11
69
  // --- Supporting module generation ---
12
70
  // Debug: log mergedSets and sets
@@ -23,6 +81,7 @@ export async function build(configPath = 'esm-styles.config.js') {
23
81
  const suffix = config.sourceFilesSuffix || '.styles.mjs';
24
82
  const floors = config.floors || [];
25
83
  const mainCssFile = config.mainCssFile || 'styles.css';
84
+ const aliases = config.aliases;
26
85
  // Helper function to generate CSS comment header
27
86
  const generateCssComment = (sourceName) => {
28
87
  const normalizedBasePath = (config.basePath || '.').replace(/^\.*\//, '');
@@ -38,8 +97,8 @@ export async function build(configPath = 'esm-styles.config.js') {
38
97
  if (config.globalVariables) {
39
98
  const inputFile = path.join(sourcePath, `${config.globalVariables}${suffix}`);
40
99
  const outputFile = path.join(outputPath, `global.css`);
41
- const fileUrl = pathToFileUrl(inputFile).href + `?update=${Date.now()}`;
42
- const varsObj = (await import(fileUrl)).default;
100
+ const varsObj = (await importWithAliases(inputFile, aliases, sourcePath))
101
+ .default;
43
102
  const cssVars = getCssVariables(varsObj);
44
103
  const rootSelector = config.globalRootSelector || ':root';
45
104
  const comment = generateCssComment(config.globalVariables);
@@ -57,8 +116,7 @@ export async function build(configPath = 'esm-styles.config.js') {
57
116
  for (const setName of sets) {
58
117
  // Inheritance: merge with previous
59
118
  const inputFile = path.join(sourcePath, `${setName}${suffix}`);
60
- const fileUrl = pathToFileUrl(inputFile).href + `?update=${Date.now()}`;
61
- const varsObj = _.merge({}, prevVarsObj, (await import(fileUrl)).default);
119
+ const varsObj = _.merge({}, prevVarsObj, (await importWithAliases(inputFile, aliases, sourcePath)).default);
62
120
  prevVarsObj = varsObj;
63
121
  mergedSets[setName] = _.cloneDeep(varsObj);
64
122
  // For each selector config for this set
@@ -187,8 +245,8 @@ export async function build(configPath = 'esm-styles.config.js') {
187
245
  // Ensure the output directory exists
188
246
  await fs.mkdir(floorOutputDir, { recursive: true });
189
247
  const outputFile = path.join(floorOutputDir, `${source}.css`);
190
- const fileUrl = pathToFileUrl(inputFile).href + `?update=${Date.now()}`;
191
- const stylesObj = (await import(fileUrl)).default;
248
+ const stylesObj = (await importWithAliases(inputFile, aliases, sourcePath))
249
+ .default;
192
250
  const css = getCss(stylesObj, {
193
251
  ...mediaShorthands,
194
252
  globalRootSelector: config.globalRootSelector,
@@ -0,0 +1,183 @@
1
+ /**
2
+ * ESM Styles - TypeScript definitions for style objects
3
+ *
4
+ * Provides type safety and autocompletion for CSS properties
5
+ * while maintaining the flexible, nested syntax of esm-styles.
6
+ */
7
+ import type { Properties } from 'csstype';
8
+ /**
9
+ * CSS variable reference object (as generated by $theme/$device modules)
10
+ */
11
+ export interface CSSVariableRef {
12
+ var: string;
13
+ name?: string;
14
+ [key: string]: string | undefined;
15
+ }
16
+ /**
17
+ * Value that can be used for any CSS property
18
+ */
19
+ export type CSSValue = string | number | CSSVariableRef;
20
+ /**
21
+ * CSS Properties with support for CSS variable references
22
+ * Using csstype's Properties as base with extended value types
23
+ */
24
+ export type CSSProperties = {
25
+ [K in keyof Properties]?: Properties[K] | CSSValue;
26
+ };
27
+ /**
28
+ * Common pseudo-class and pseudo-element keys for better autocompletion
29
+ */
30
+ export type CommonPseudos = ':hover' | ':active' | ':focus' | ':focus-visible' | ':focus-within' | ':visited' | ':disabled' | ':enabled' | ':checked' | ':first-child' | ':last-child' | ':nth-child' | ':not' | ':has' | '::before' | '::after' | '::placeholder' | '::selection' | '::first-line' | '::first-letter';
31
+ /**
32
+ * Complete style object type.
33
+ *
34
+ * Due to TypeScript limitations with index signatures,
35
+ * we use a permissive approach that still provides
36
+ * autocompletion for CSS properties while allowing
37
+ * nested selectors.
38
+ *
39
+ * The tradeoff: typos in CSS property names won't be caught,
40
+ * but you get full autocompletion when typing.
41
+ */
42
+ export type StyleObject = CSSProperties & {
43
+ [K in CommonPseudos]?: StyleObject;
44
+ } & {
45
+ [selector: string]: StyleObject | CSSValue | string | undefined;
46
+ };
47
+ /**
48
+ * Root-level stylesheet with named selectors
49
+ */
50
+ export type StyleSheet = {
51
+ [selector: string]: StyleObject;
52
+ };
53
+ /**
54
+ * Strict CSS properties without nested selectors.
55
+ * Use this for leaf-level style objects where you want
56
+ * full type checking and no nested selectors are needed.
57
+ */
58
+ export type StrictCSSProperties = {
59
+ [K in keyof Properties]?: Properties[K] | CSSValue;
60
+ };
61
+ /**
62
+ * Strict style object with explicit nested selector type.
63
+ * Provides better type checking at the cost of more verbose syntax.
64
+ */
65
+ export interface StrictStyleObject extends StrictCSSProperties {
66
+ ':hover'?: StrictStyleObject;
67
+ ':active'?: StrictStyleObject;
68
+ ':focus'?: StrictStyleObject;
69
+ ':focus-visible'?: StrictStyleObject;
70
+ ':disabled'?: StrictStyleObject;
71
+ '::before'?: StrictStyleObject;
72
+ '::after'?: StrictStyleObject;
73
+ '::placeholder'?: StrictStyleObject;
74
+ }
75
+ /**
76
+ * Define a typed stylesheet with autocompletion.
77
+ *
78
+ * @example
79
+ * ```ts
80
+ * import { defineStyles } from 'esm-styles'
81
+ * import $theme from './$theme'
82
+ *
83
+ * export default defineStyles({
84
+ * button: {
85
+ * backgroundColor: $theme.paper.bright,
86
+ * padding: '12px 24px',
87
+ *
88
+ * ':hover': {
89
+ * opacity: 0.9,
90
+ * },
91
+ *
92
+ * '@dark': {
93
+ * backgroundColor: $theme.paper.tinted,
94
+ * },
95
+ *
96
+ * __icon: {
97
+ * width: '20px',
98
+ * },
99
+ * },
100
+ * })
101
+ * ```
102
+ */
103
+ export declare function defineStyles<T extends StyleSheet>(styles: T): T;
104
+ /**
105
+ * Define a component style for named export.
106
+ * Use this in component files that are imported into floor modules.
107
+ *
108
+ * @example
109
+ * ```ts
110
+ * // button.styles.ts
111
+ * import { defineComponent } from 'esm-styles'
112
+ * import $theme from './$theme'
113
+ *
114
+ * export const button = defineComponent({
115
+ * padding: '12px 24px',
116
+ * backgroundColor: $theme.paper.bright,
117
+ *
118
+ * ':hover': { opacity: 0.9 },
119
+ * '@dark': { backgroundColor: $theme.paper.tinted },
120
+ *
121
+ * primary: { backgroundColor: 'blue' },
122
+ * __icon: { width: '20px' },
123
+ * })
124
+ * ```
125
+ *
126
+ * ```ts
127
+ * // components.styles.ts (floor module)
128
+ * import { defineStyles } from 'esm-styles'
129
+ * import { button } from './button.styles'
130
+ *
131
+ * export default defineStyles({ button })
132
+ * ```
133
+ */
134
+ export declare function defineComponent<T extends StyleObject>(styles: T): T;
135
+ /**
136
+ * Define a reusable style mixin (for spreading into other styles).
137
+ *
138
+ * @example
139
+ * ```ts
140
+ * import { defineMixin } from 'esm-styles'
141
+ *
142
+ * export const flexCenter = defineMixin({
143
+ * display: 'flex',
144
+ * alignItems: 'center',
145
+ * justifyContent: 'center',
146
+ * })
147
+ *
148
+ * // Usage:
149
+ * export const card = defineComponent({
150
+ * ...flexCenter,
151
+ * padding: '24px',
152
+ * })
153
+ * ```
154
+ */
155
+ export declare function defineMixin<T extends StyleObject>(mixin: T): T;
156
+ /**
157
+ * Define strict CSS properties (no nesting, full type checking).
158
+ * Use for simple style objects where you want maximum type safety.
159
+ *
160
+ * @example
161
+ * ```ts
162
+ * const buttonBase = defineStrict({
163
+ * display: 'inline-flex',
164
+ * padding: '12px 24px',
165
+ * // backgroundColr: 'red', // ❌ TypeScript error!
166
+ * })
167
+ * ```
168
+ */
169
+ export declare function defineStrict<T extends StrictCSSProperties>(styles: T): T;
170
+ /**
171
+ * Leaf token with CSS variable reference
172
+ */
173
+ export interface ThemeToken extends CSSVariableRef {
174
+ var: string;
175
+ name: string;
176
+ }
177
+ /**
178
+ * Nested theme tokens structure
179
+ */
180
+ export type ThemeTokens = {
181
+ [key: string]: ThemeToken | ThemeTokens;
182
+ };
183
+ export type { Properties, Pseudos } from 'csstype';
@@ -0,0 +1,112 @@
1
+ /**
2
+ * ESM Styles - TypeScript definitions for style objects
3
+ *
4
+ * Provides type safety and autocompletion for CSS properties
5
+ * while maintaining the flexible, nested syntax of esm-styles.
6
+ */
7
+ // ============================================
8
+ // Helper Functions
9
+ // ============================================
10
+ /**
11
+ * Define a typed stylesheet with autocompletion.
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * import { defineStyles } from 'esm-styles'
16
+ * import $theme from './$theme'
17
+ *
18
+ * export default defineStyles({
19
+ * button: {
20
+ * backgroundColor: $theme.paper.bright,
21
+ * padding: '12px 24px',
22
+ *
23
+ * ':hover': {
24
+ * opacity: 0.9,
25
+ * },
26
+ *
27
+ * '@dark': {
28
+ * backgroundColor: $theme.paper.tinted,
29
+ * },
30
+ *
31
+ * __icon: {
32
+ * width: '20px',
33
+ * },
34
+ * },
35
+ * })
36
+ * ```
37
+ */
38
+ export function defineStyles(styles) {
39
+ return styles;
40
+ }
41
+ /**
42
+ * Define a component style for named export.
43
+ * Use this in component files that are imported into floor modules.
44
+ *
45
+ * @example
46
+ * ```ts
47
+ * // button.styles.ts
48
+ * import { defineComponent } from 'esm-styles'
49
+ * import $theme from './$theme'
50
+ *
51
+ * export const button = defineComponent({
52
+ * padding: '12px 24px',
53
+ * backgroundColor: $theme.paper.bright,
54
+ *
55
+ * ':hover': { opacity: 0.9 },
56
+ * '@dark': { backgroundColor: $theme.paper.tinted },
57
+ *
58
+ * primary: { backgroundColor: 'blue' },
59
+ * __icon: { width: '20px' },
60
+ * })
61
+ * ```
62
+ *
63
+ * ```ts
64
+ * // components.styles.ts (floor module)
65
+ * import { defineStyles } from 'esm-styles'
66
+ * import { button } from './button.styles'
67
+ *
68
+ * export default defineStyles({ button })
69
+ * ```
70
+ */
71
+ export function defineComponent(styles) {
72
+ return styles;
73
+ }
74
+ /**
75
+ * Define a reusable style mixin (for spreading into other styles).
76
+ *
77
+ * @example
78
+ * ```ts
79
+ * import { defineMixin } from 'esm-styles'
80
+ *
81
+ * export const flexCenter = defineMixin({
82
+ * display: 'flex',
83
+ * alignItems: 'center',
84
+ * justifyContent: 'center',
85
+ * })
86
+ *
87
+ * // Usage:
88
+ * export const card = defineComponent({
89
+ * ...flexCenter,
90
+ * padding: '24px',
91
+ * })
92
+ * ```
93
+ */
94
+ export function defineMixin(mixin) {
95
+ return mixin;
96
+ }
97
+ /**
98
+ * Define strict CSS properties (no nesting, full type checking).
99
+ * Use for simple style objects where you want maximum type safety.
100
+ *
101
+ * @example
102
+ * ```ts
103
+ * const buttonBase = defineStrict({
104
+ * display: 'inline-flex',
105
+ * padding: '12px 24px',
106
+ * // backgroundColr: 'red', // ❌ TypeScript error!
107
+ * })
108
+ * ```
109
+ */
110
+ export function defineStrict(styles) {
111
+ return styles;
112
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "esm-styles",
3
- "version": "0.3.12",
3
+ "version": "0.4.1",
4
4
  "description": "A library for working with ESM styles",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -49,6 +49,7 @@
49
49
  "typescript": "^5.8.3"
50
50
  },
51
51
  "dependencies": {
52
+ "csstype": "^3.2.3",
52
53
  "esbuild": "^0.27.2",
53
54
  "js-beautify": "^1.15.4"
54
55
  },