catom 2.1.0 → 2.5.0
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/runtime/index.d.ts +90 -0
- package/dist/runtime/index.js +10 -0
- package/dist/runtime/index.js.map +1 -0
- package/dist/vite/index.d.ts +94 -0
- package/dist/vite/index.js +501 -0
- package/dist/vite/index.js.map +1 -0
- package/package.json +50 -26
- package/src/virtual.d.ts +9 -0
- package/LICENSE +0 -21
- package/README.md +0 -150
- package/babelPlugin.d.ts +0 -1
- package/babelPlugin.js +0 -1
- package/css.d.ts +0 -1
- package/css.js +0 -1
- package/dist/babelPlugin.d.ts +0 -4
- package/dist/babelPlugin.js +0 -63
- package/dist/css.d.ts +0 -5
- package/dist/css.js +0 -35
- package/dist/index.d.ts +0 -13
- package/dist/index.js +0 -11
- package/dist/plugin/astObject.d.ts +0 -3
- package/dist/plugin/astObject.js +0 -62
- package/dist/plugin/constants.d.ts +0 -17
- package/dist/plugin/constants.js +0 -9
- package/dist/plugin/cssTransform.d.ts +0 -3
- package/dist/plugin/cssTransform.js +0 -92
- package/dist/plugin/hash.d.ts +0 -1
- package/dist/plugin/hash.js +0 -50
- package/tsconfig.json +0 -14
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { Properties } from 'csstype';
|
|
2
|
+
export { Properties as CSSProperties } from 'csstype';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* CSS properties with optional pseudo selectors
|
|
6
|
+
*/
|
|
7
|
+
interface CSSPropertiesWithPseudo extends Properties {
|
|
8
|
+
/**
|
|
9
|
+
* Pseudo selector styles
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* { ':hover': { color: 'blue' } }
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
pseudo?: {
|
|
16
|
+
[selector: string]: Properties;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Input type for the css() function
|
|
21
|
+
* Supports standard CSS properties plus media and pseudo selectors
|
|
22
|
+
*/
|
|
23
|
+
interface CSSInput extends Properties {
|
|
24
|
+
/**
|
|
25
|
+
* Media query styles - can also contain pseudo selectors
|
|
26
|
+
* @example
|
|
27
|
+
* ```ts
|
|
28
|
+
* css({
|
|
29
|
+
* color: 'red',
|
|
30
|
+
* media: {
|
|
31
|
+
* '(max-width: 768px)': {
|
|
32
|
+
* color: 'blue',
|
|
33
|
+
* pseudo: { ':hover': { color: 'darkblue' } }
|
|
34
|
+
* }
|
|
35
|
+
* }
|
|
36
|
+
* })
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
media?: {
|
|
40
|
+
[query: string]: CSSPropertiesWithPseudo;
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Pseudo selector styles
|
|
44
|
+
* @example
|
|
45
|
+
* ```ts
|
|
46
|
+
* css({
|
|
47
|
+
* color: 'red',
|
|
48
|
+
* pseudo: {
|
|
49
|
+
* ':hover': { color: 'blue' },
|
|
50
|
+
* ':focus': { outline: '2px solid blue' }
|
|
51
|
+
* }
|
|
52
|
+
* })
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
pseudo?: {
|
|
56
|
+
[selector: string]: Properties;
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Define atomic CSS styles that are extracted at compile time.
|
|
61
|
+
*
|
|
62
|
+
* This function is transformed by catom/vite plugin during build.
|
|
63
|
+
* At runtime, it only serves as a type-safe placeholder.
|
|
64
|
+
*
|
|
65
|
+
* @param styles - CSS properties object with optional media and pseudo selectors
|
|
66
|
+
* @returns A string of space-separated atomic class names (at build time)
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* ```tsx
|
|
70
|
+
* import { css } from 'catom'
|
|
71
|
+
*
|
|
72
|
+
* const button = css({
|
|
73
|
+
* backgroundColor: 'blue',
|
|
74
|
+
* color: 'white',
|
|
75
|
+
* padding: '8px 16px',
|
|
76
|
+
* borderRadius: '4px',
|
|
77
|
+
* pseudo: {
|
|
78
|
+
* ':hover': { backgroundColor: 'darkblue' }
|
|
79
|
+
* }
|
|
80
|
+
* })
|
|
81
|
+
*
|
|
82
|
+
* // In your component:
|
|
83
|
+
* <button className={button}>Click me</button>
|
|
84
|
+
* ```
|
|
85
|
+
*
|
|
86
|
+
* @throws Error if called at runtime (indicates plugin misconfiguration)
|
|
87
|
+
*/
|
|
88
|
+
declare function css(_styles: CSSInput): string;
|
|
89
|
+
|
|
90
|
+
export { type CSSInput, type CSSPropertiesWithPseudo, css };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
function css(_styles) {
|
|
3
|
+
throw new Error(
|
|
4
|
+
'[catom] css() was called at runtime. This usually means the catom vite plugin is not configured correctly. Make sure to add the plugin to your vite.config.ts: import catom from "catom/vite"'
|
|
5
|
+
);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export { css };
|
|
9
|
+
//# sourceMappingURL=index.js.map
|
|
10
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/index.ts"],"names":[],"mappings":";AAmFO,SAAS,IAAI,OAAA,EAA2B;AAC7C,EAAA,MAAM,IAAI,KAAA;AAAA,IACR;AAAA,GAGF;AACF","file":"index.js","sourcesContent":["import type { Properties } from 'csstype'\n\n/**\n * CSS properties with optional pseudo selectors\n */\nexport interface CSSPropertiesWithPseudo extends Properties {\n /**\n * Pseudo selector styles\n * @example\n * ```ts\n * { ':hover': { color: 'blue' } }\n * ```\n */\n pseudo?: { [selector: string]: Properties }\n}\n\n/**\n * Input type for the css() function\n * Supports standard CSS properties plus media and pseudo selectors\n */\nexport interface CSSInput extends Properties {\n /**\n * Media query styles - can also contain pseudo selectors\n * @example\n * ```ts\n * css({\n * color: 'red',\n * media: {\n * '(max-width: 768px)': { \n * color: 'blue',\n * pseudo: { ':hover': { color: 'darkblue' } }\n * }\n * }\n * })\n * ```\n */\n media?: { [query: string]: CSSPropertiesWithPseudo }\n\n /**\n * Pseudo selector styles\n * @example\n * ```ts\n * css({\n * color: 'red',\n * pseudo: {\n * ':hover': { color: 'blue' },\n * ':focus': { outline: '2px solid blue' }\n * }\n * })\n * ```\n */\n pseudo?: { [selector: string]: Properties }\n}\n\n/**\n * Define atomic CSS styles that are extracted at compile time.\n *\n * This function is transformed by catom/vite plugin during build.\n * At runtime, it only serves as a type-safe placeholder.\n *\n * @param styles - CSS properties object with optional media and pseudo selectors\n * @returns A string of space-separated atomic class names (at build time)\n *\n * @example\n * ```tsx\n * import { css } from 'catom'\n *\n * const button = css({\n * backgroundColor: 'blue',\n * color: 'white',\n * padding: '8px 16px',\n * borderRadius: '4px',\n * pseudo: {\n * ':hover': { backgroundColor: 'darkblue' }\n * }\n * })\n *\n * // In your component:\n * <button className={button}>Click me</button>\n * ```\n *\n * @throws Error if called at runtime (indicates plugin misconfiguration)\n */\nexport function css(_styles: CSSInput): string {\n throw new Error(\n '[catom] css() was called at runtime. ' +\n 'This usually means the catom vite plugin is not configured correctly. ' +\n 'Make sure to add the plugin to your vite.config.ts: import catom from \"catom/vite\"'\n )\n}\n\n// Re-export the type for convenience\nexport type { Properties as CSSProperties } from 'csstype'\n"]}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { Properties } from 'csstype';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Input type for the css() function
|
|
5
|
+
*/
|
|
6
|
+
interface CSSInput extends Properties {
|
|
7
|
+
media?: {
|
|
8
|
+
[query: string]: Properties;
|
|
9
|
+
};
|
|
10
|
+
pseudo?: {
|
|
11
|
+
[selector: string]: Properties;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* A single atomic CSS rule
|
|
16
|
+
*/
|
|
17
|
+
interface CSSRule {
|
|
18
|
+
/** Unique class name (hash) */
|
|
19
|
+
hash: string;
|
|
20
|
+
/** CSS property in kebab-case */
|
|
21
|
+
property: string;
|
|
22
|
+
/** CSS value */
|
|
23
|
+
value: string;
|
|
24
|
+
/** Optional media query */
|
|
25
|
+
media?: string;
|
|
26
|
+
/** Optional pseudo selector (e.g., ':hover') */
|
|
27
|
+
pseudo?: string;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Plugin options
|
|
31
|
+
*/
|
|
32
|
+
interface CatomPluginOptions {
|
|
33
|
+
/**
|
|
34
|
+
* File patterns to include for transformation
|
|
35
|
+
* @default /\.[jt]sx?$/
|
|
36
|
+
*/
|
|
37
|
+
include?: string | RegExp | (string | RegExp)[];
|
|
38
|
+
/**
|
|
39
|
+
* File patterns to exclude from transformation
|
|
40
|
+
* @default /node_modules/
|
|
41
|
+
*/
|
|
42
|
+
exclude?: string | RegExp | (string | RegExp)[];
|
|
43
|
+
/**
|
|
44
|
+
* Name of the css function to transform
|
|
45
|
+
* @default 'css'
|
|
46
|
+
*/
|
|
47
|
+
functionName?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Vite plugin for zero-runtime CSS-in-JS with atomic CSS generation
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* ```ts
|
|
55
|
+
* // vite.config.ts
|
|
56
|
+
* import { defineConfig } from 'vite'
|
|
57
|
+
* import catom from 'catom/vite'
|
|
58
|
+
*
|
|
59
|
+
* export default defineConfig({
|
|
60
|
+
* plugins: [catom()]
|
|
61
|
+
* })
|
|
62
|
+
* ```
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* ```ts
|
|
66
|
+
* // In your app code
|
|
67
|
+
* import { css } from 'catom'
|
|
68
|
+
* import 'virtual:catom.css'
|
|
69
|
+
*
|
|
70
|
+
* const button = css({
|
|
71
|
+
* color: 'red',
|
|
72
|
+
* padding: '8px',
|
|
73
|
+
* pseudo: { ':hover': { color: 'blue' } }
|
|
74
|
+
* })
|
|
75
|
+
* // After transform: const button = "_a1b2c3 _d4e5f6 _g7h8i9"
|
|
76
|
+
* ```
|
|
77
|
+
*/
|
|
78
|
+
declare function catomPlugin(options?: CatomPluginOptions): {
|
|
79
|
+
name: string;
|
|
80
|
+
enforce: "pre";
|
|
81
|
+
configResolved(): void;
|
|
82
|
+
buildStart(): void;
|
|
83
|
+
resolveId(id: string): string | null;
|
|
84
|
+
load(id: string): string | null;
|
|
85
|
+
transform(this: {
|
|
86
|
+
error: (message: string) => never;
|
|
87
|
+
}, code: string, id: string): {
|
|
88
|
+
code: string;
|
|
89
|
+
map: null;
|
|
90
|
+
} | null;
|
|
91
|
+
watchChange(id: string): void;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export { type CSSInput, type CSSRule, type CatomPluginOptions, catomPlugin, catomPlugin as default };
|
|
@@ -0,0 +1,501 @@
|
|
|
1
|
+
import { createFilter } from 'vite';
|
|
2
|
+
import { parseSync } from '@swc/core';
|
|
3
|
+
|
|
4
|
+
// src/vite/index.ts
|
|
5
|
+
|
|
6
|
+
// src/core/hash.ts
|
|
7
|
+
function murmur2(str) {
|
|
8
|
+
let h = 0;
|
|
9
|
+
let k;
|
|
10
|
+
let i = 0;
|
|
11
|
+
let len = str.length;
|
|
12
|
+
for (; len >= 4; ++i, len -= 4) {
|
|
13
|
+
k = str.charCodeAt(i) & 255 | (str.charCodeAt(++i) & 255) << 8 | (str.charCodeAt(++i) & 255) << 16 | (str.charCodeAt(++i) & 255) << 24;
|
|
14
|
+
k = /* Math.imul(k, m): */
|
|
15
|
+
(k & 65535) * 1540483477 + ((k >>> 16) * 59797 << 16);
|
|
16
|
+
k ^= /* k >>> r: */
|
|
17
|
+
k >>> 24;
|
|
18
|
+
h = /* Math.imul(k, m): */
|
|
19
|
+
(k & 65535) * 1540483477 + ((k >>> 16) * 59797 << 16) ^ /* Math.imul(h, m): */
|
|
20
|
+
(h & 65535) * 1540483477 + ((h >>> 16) * 59797 << 16);
|
|
21
|
+
}
|
|
22
|
+
switch (len) {
|
|
23
|
+
case 3:
|
|
24
|
+
h ^= (str.charCodeAt(i + 2) & 255) << 16;
|
|
25
|
+
// falls through
|
|
26
|
+
case 2:
|
|
27
|
+
h ^= (str.charCodeAt(i + 1) & 255) << 8;
|
|
28
|
+
// falls through
|
|
29
|
+
case 1:
|
|
30
|
+
h ^= str.charCodeAt(i) & 255;
|
|
31
|
+
h = /* Math.imul(h, m): */
|
|
32
|
+
(h & 65535) * 1540483477 + ((h >>> 16) * 59797 << 16);
|
|
33
|
+
}
|
|
34
|
+
h ^= h >>> 13;
|
|
35
|
+
h = /* Math.imul(h, m): */
|
|
36
|
+
(h & 65535) * 1540483477 + ((h >>> 16) * 59797 << 16);
|
|
37
|
+
return ((h ^ h >>> 15) >>> 0).toString(36);
|
|
38
|
+
}
|
|
39
|
+
var PREFIX_CHARS = new Set("0123456789-".split(""));
|
|
40
|
+
function makeCSSCompatible(hash) {
|
|
41
|
+
if (PREFIX_CHARS.has(hash[0])) {
|
|
42
|
+
return `_${hash}`;
|
|
43
|
+
}
|
|
44
|
+
return hash;
|
|
45
|
+
}
|
|
46
|
+
function generateHash(identity) {
|
|
47
|
+
return makeCSSCompatible(murmur2(identity));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// src/core/css-generator.ts
|
|
51
|
+
var KEBAB_CASE_REGEX = /([a-z0-9]|(?=[A-Z]))([A-Z])/g;
|
|
52
|
+
function toKebabCase(property) {
|
|
53
|
+
return property.replace(KEBAB_CASE_REGEX, "$1-$2").toLowerCase();
|
|
54
|
+
}
|
|
55
|
+
function createRuleIdentity(property, value, media, pseudo) {
|
|
56
|
+
const mediaPrefix = media ? `@${media.trim()}` : "";
|
|
57
|
+
const pseudoPrefix = pseudo ? pseudo.trim() : "";
|
|
58
|
+
const kebabProp = toKebabCase(property);
|
|
59
|
+
return `${mediaPrefix}${pseudoPrefix}${kebabProp}:${value};`;
|
|
60
|
+
}
|
|
61
|
+
function createCSSRule(property, value, media, pseudo) {
|
|
62
|
+
const stringValue = String(value).trim();
|
|
63
|
+
const kebabProperty = toKebabCase(property.trim());
|
|
64
|
+
const identity = createRuleIdentity(property, stringValue, media, pseudo);
|
|
65
|
+
const hash = generateHash(identity);
|
|
66
|
+
return {
|
|
67
|
+
hash,
|
|
68
|
+
property: kebabProperty,
|
|
69
|
+
value: stringValue,
|
|
70
|
+
media: media?.trim(),
|
|
71
|
+
pseudo: pseudo?.trim()
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
function deduplicateRules(rules) {
|
|
75
|
+
const seen = /* @__PURE__ */ new Map();
|
|
76
|
+
for (const rule of rules) {
|
|
77
|
+
const identity = createRuleIdentity(rule.property, rule.value, rule.media, rule.pseudo);
|
|
78
|
+
if (!seen.has(identity)) {
|
|
79
|
+
seen.set(identity, rule);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return Array.from(seen.values());
|
|
83
|
+
}
|
|
84
|
+
function groupRulesByDeclaration(rules) {
|
|
85
|
+
const groups = /* @__PURE__ */ new Map();
|
|
86
|
+
for (const rule of rules) {
|
|
87
|
+
const declaration = `${rule.property}:${rule.value};`;
|
|
88
|
+
const groupKey = `${rule.media || ""}|${rule.pseudo || ""}|${declaration}`;
|
|
89
|
+
if (!groups.has(groupKey)) {
|
|
90
|
+
groups.set(groupKey, {
|
|
91
|
+
declaration,
|
|
92
|
+
hashes: /* @__PURE__ */ new Set(),
|
|
93
|
+
media: rule.media,
|
|
94
|
+
pseudo: rule.pseudo
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
groups.get(groupKey).hashes.add(rule.hash);
|
|
98
|
+
}
|
|
99
|
+
return Array.from(groups.values());
|
|
100
|
+
}
|
|
101
|
+
function generateCSS(rules) {
|
|
102
|
+
const dedupedRules = deduplicateRules(rules);
|
|
103
|
+
const regularRules = [];
|
|
104
|
+
const pseudoRules = [];
|
|
105
|
+
const mediaRules = /* @__PURE__ */ new Map();
|
|
106
|
+
for (const rule of dedupedRules) {
|
|
107
|
+
if (rule.media) {
|
|
108
|
+
const existing = mediaRules.get(rule.media) || [];
|
|
109
|
+
existing.push(rule);
|
|
110
|
+
mediaRules.set(rule.media, existing);
|
|
111
|
+
} else if (rule.pseudo) {
|
|
112
|
+
pseudoRules.push(rule);
|
|
113
|
+
} else {
|
|
114
|
+
regularRules.push(rule);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
const cssLines = [];
|
|
118
|
+
const regularGroups = groupRulesByDeclaration(regularRules);
|
|
119
|
+
for (const group of regularGroups.sort((a, b) => a.declaration.localeCompare(b.declaration))) {
|
|
120
|
+
const selectors = Array.from(group.hashes).sort().map((h) => `.${h}`).join(",\n");
|
|
121
|
+
cssLines.push(`${selectors} { ${group.declaration} }`);
|
|
122
|
+
}
|
|
123
|
+
const pseudoGroups = groupRulesByDeclaration(pseudoRules);
|
|
124
|
+
for (const group of pseudoGroups.sort((a, b) => a.declaration.localeCompare(b.declaration))) {
|
|
125
|
+
const selectors = Array.from(group.hashes).sort().map((h) => `.${h}${group.pseudo}`).join(",\n");
|
|
126
|
+
cssLines.push(`${selectors} { ${group.declaration} }`);
|
|
127
|
+
}
|
|
128
|
+
const sortedMediaQueries = Array.from(mediaRules.entries()).sort(
|
|
129
|
+
([a], [b]) => a.localeCompare(b)
|
|
130
|
+
);
|
|
131
|
+
for (const [query, rules2] of sortedMediaQueries) {
|
|
132
|
+
const mediaGroups = groupRulesByDeclaration(rules2);
|
|
133
|
+
const mediaLines = [];
|
|
134
|
+
for (const group of mediaGroups.sort((a, b) => a.declaration.localeCompare(b.declaration))) {
|
|
135
|
+
const suffix = group.pseudo || "";
|
|
136
|
+
const selectors = Array.from(group.hashes).sort().map((h) => `.${h}${suffix}`).join(",\n");
|
|
137
|
+
mediaLines.push(` ${selectors} { ${group.declaration} }`);
|
|
138
|
+
}
|
|
139
|
+
cssLines.push(`@media ${query} {
|
|
140
|
+
${mediaLines.join("\n")}
|
|
141
|
+
}`);
|
|
142
|
+
}
|
|
143
|
+
return cssLines.join("\n");
|
|
144
|
+
}
|
|
145
|
+
function isLiteral(expr) {
|
|
146
|
+
return expr.type === "StringLiteral" || expr.type === "NumericLiteral";
|
|
147
|
+
}
|
|
148
|
+
function getLiteralValue(expr) {
|
|
149
|
+
if (expr.type === "StringLiteral") {
|
|
150
|
+
return expr.value;
|
|
151
|
+
}
|
|
152
|
+
return expr.value;
|
|
153
|
+
}
|
|
154
|
+
function getPropertyKey(prop) {
|
|
155
|
+
if (prop.key.type === "Identifier") {
|
|
156
|
+
return prop.key.value;
|
|
157
|
+
}
|
|
158
|
+
if (prop.key.type === "StringLiteral") {
|
|
159
|
+
return prop.key.value;
|
|
160
|
+
}
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
function processPropertiesObject(obj, rules, media, pseudo) {
|
|
164
|
+
for (const prop of obj.properties) {
|
|
165
|
+
if (prop.type === "SpreadElement") {
|
|
166
|
+
if (prop.argument.type === "ObjectExpression") {
|
|
167
|
+
processPropertiesObject(prop.argument, rules, media, pseudo);
|
|
168
|
+
} else {
|
|
169
|
+
throw new Error(
|
|
170
|
+
`[catom] Spread elements must be object literals. Dynamic spreads are not supported at compile time.`
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
if (prop.type !== "KeyValueProperty") {
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
const keyName = getPropertyKey(prop);
|
|
179
|
+
if (!keyName) {
|
|
180
|
+
throw new Error(`[catom] Could not determine property key. Only identifiers and string literals are supported.`);
|
|
181
|
+
}
|
|
182
|
+
const value = prop.value;
|
|
183
|
+
if (keyName === "media") {
|
|
184
|
+
if (value.type !== "ObjectExpression") {
|
|
185
|
+
throw new Error(`[catom] 'media' property must be an object literal.`);
|
|
186
|
+
}
|
|
187
|
+
for (const mediaProp of value.properties) {
|
|
188
|
+
if (mediaProp.type !== "KeyValueProperty") continue;
|
|
189
|
+
const mediaQuery = getPropertyKey(mediaProp);
|
|
190
|
+
if (!mediaQuery) continue;
|
|
191
|
+
if (mediaProp.value.type !== "ObjectExpression") {
|
|
192
|
+
throw new Error(`[catom] Media query '${mediaQuery}' must contain an object literal.`);
|
|
193
|
+
}
|
|
194
|
+
processPropertiesObject(mediaProp.value, rules, mediaQuery, pseudo);
|
|
195
|
+
}
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
if (keyName === "pseudo") {
|
|
199
|
+
if (value.type !== "ObjectExpression") {
|
|
200
|
+
throw new Error(`[catom] 'pseudo' property must be an object literal.`);
|
|
201
|
+
}
|
|
202
|
+
for (const pseudoProp of value.properties) {
|
|
203
|
+
if (pseudoProp.type !== "KeyValueProperty") continue;
|
|
204
|
+
const pseudoSelector = getPropertyKey(pseudoProp);
|
|
205
|
+
if (!pseudoSelector) continue;
|
|
206
|
+
if (pseudoProp.value.type !== "ObjectExpression") {
|
|
207
|
+
throw new Error(`[catom] Pseudo selector '${pseudoSelector}' must contain an object literal.`);
|
|
208
|
+
}
|
|
209
|
+
processPropertiesObject(pseudoProp.value, rules, media, pseudoSelector);
|
|
210
|
+
}
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
let actualValue = value;
|
|
214
|
+
if (actualValue.type === "TsAsExpression") {
|
|
215
|
+
actualValue = actualValue.expression;
|
|
216
|
+
}
|
|
217
|
+
if (!isLiteral(actualValue)) {
|
|
218
|
+
throw new Error(
|
|
219
|
+
`[catom] Property '${keyName}' has a non-literal value. Only string and number literals are supported at compile time. Got: ${actualValue.type}`
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
const rule = createCSSRule(keyName, getLiteralValue(actualValue), media, pseudo);
|
|
223
|
+
rules.push(rule);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
function processCSSCall(callExpr) {
|
|
227
|
+
const rules = [];
|
|
228
|
+
if (callExpr.arguments.length === 0) {
|
|
229
|
+
return { rules: [], classNames: "" };
|
|
230
|
+
}
|
|
231
|
+
const arg = callExpr.arguments[0];
|
|
232
|
+
if (arg.expression.type !== "ObjectExpression") {
|
|
233
|
+
throw new Error(
|
|
234
|
+
`[catom] css() must be called with an object literal. Got: ${arg.expression.type}`
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
processPropertiesObject(arg.expression, rules);
|
|
238
|
+
const classNames = rules.map((r) => r.hash).join(" ");
|
|
239
|
+
return { rules, classNames };
|
|
240
|
+
}
|
|
241
|
+
var METADATA_KEYS = /* @__PURE__ */ new Set(["span", "ctxt"]);
|
|
242
|
+
function isASTNode(value) {
|
|
243
|
+
return typeof value === "object" && value !== null && typeof value.type === "string";
|
|
244
|
+
}
|
|
245
|
+
function walkAndTransform(node, functionName, allRules, replacements, visited = /* @__PURE__ */ new WeakSet()) {
|
|
246
|
+
if (!node || typeof node !== "object") return;
|
|
247
|
+
if (visited.has(node)) return;
|
|
248
|
+
visited.add(node);
|
|
249
|
+
if (isCallExpression(node)) {
|
|
250
|
+
const callee = node.callee;
|
|
251
|
+
if (callee.type === "Identifier" && callee.value === functionName) {
|
|
252
|
+
try {
|
|
253
|
+
const { rules, classNames } = processCSSCall(node);
|
|
254
|
+
allRules.push(...rules);
|
|
255
|
+
replacements.set(node.span.start, classNames);
|
|
256
|
+
} catch (error) {
|
|
257
|
+
const loc = node.span;
|
|
258
|
+
const prefix = loc ? `[${loc.start}:${loc.end}]` : "";
|
|
259
|
+
throw new Error(`${prefix} ${error.message}`);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
for (const key of Object.keys(node)) {
|
|
264
|
+
if (METADATA_KEYS.has(key)) continue;
|
|
265
|
+
const value = node[key];
|
|
266
|
+
if (Array.isArray(value)) {
|
|
267
|
+
for (const item of value) {
|
|
268
|
+
if (isASTNode(item)) {
|
|
269
|
+
walkAndTransform(item, functionName, allRules, replacements, visited);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
} else if (isASTNode(value)) {
|
|
273
|
+
walkAndTransform(value, functionName, allRules, replacements, visited);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
function isCallExpression(node) {
|
|
278
|
+
return typeof node === "object" && node !== null && node.type === "CallExpression";
|
|
279
|
+
}
|
|
280
|
+
function isTargetImport(spec, functionName) {
|
|
281
|
+
if (typeof spec !== "object" || spec === null) return false;
|
|
282
|
+
const s = spec;
|
|
283
|
+
if (s.type !== "ImportSpecifier") return false;
|
|
284
|
+
const imported = s.imported;
|
|
285
|
+
if (imported?.type === "Identifier" && imported.value === functionName) {
|
|
286
|
+
return true;
|
|
287
|
+
}
|
|
288
|
+
const local = s.local;
|
|
289
|
+
if (!imported && local?.type === "Identifier" && local.value === functionName) {
|
|
290
|
+
return true;
|
|
291
|
+
}
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
294
|
+
function findCatomImports(ast, functionName) {
|
|
295
|
+
const importsToRemove = [];
|
|
296
|
+
for (const node of ast.body) {
|
|
297
|
+
if (node.type !== "ImportDeclaration") continue;
|
|
298
|
+
const source = node.source.value;
|
|
299
|
+
if (source !== "catom") continue;
|
|
300
|
+
const specifiers = node.specifiers || [];
|
|
301
|
+
const hasCssImport = specifiers.some((spec) => isTargetImport(spec, functionName));
|
|
302
|
+
if (!hasCssImport) continue;
|
|
303
|
+
if (specifiers.length === 1) {
|
|
304
|
+
importsToRemove.push({
|
|
305
|
+
start: node.span.start - 1,
|
|
306
|
+
// SWC is 1-indexed
|
|
307
|
+
end: node.span.end - 1,
|
|
308
|
+
isFullImport: true
|
|
309
|
+
});
|
|
310
|
+
} else {
|
|
311
|
+
for (const spec of specifiers) {
|
|
312
|
+
if (isTargetImport(spec, functionName)) {
|
|
313
|
+
const s = spec;
|
|
314
|
+
importsToRemove.push({
|
|
315
|
+
start: s.span.start - 1,
|
|
316
|
+
end: s.span.end - 1,
|
|
317
|
+
isFullImport: false
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
return importsToRemove;
|
|
324
|
+
}
|
|
325
|
+
function removeImportFromCode(code, start, end, isFullImport) {
|
|
326
|
+
if (isFullImport) {
|
|
327
|
+
let lineStart = start;
|
|
328
|
+
while (lineStart > 0 && code[lineStart - 1] !== "\n") {
|
|
329
|
+
lineStart--;
|
|
330
|
+
}
|
|
331
|
+
let lineEnd = end;
|
|
332
|
+
while (lineEnd < code.length && code[lineEnd] !== "\n") {
|
|
333
|
+
lineEnd++;
|
|
334
|
+
}
|
|
335
|
+
if (code[lineEnd] === "\n") lineEnd++;
|
|
336
|
+
return code.slice(0, lineStart) + code.slice(lineEnd);
|
|
337
|
+
} else {
|
|
338
|
+
let removeEnd = end;
|
|
339
|
+
while (removeEnd < code.length && /[\s,]/.test(code[removeEnd])) {
|
|
340
|
+
removeEnd++;
|
|
341
|
+
}
|
|
342
|
+
return code.slice(0, start) + code.slice(removeEnd);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
function transformCode(code, id, functionName = "css") {
|
|
346
|
+
const isTypeScript = /\.tsx?$/.test(id);
|
|
347
|
+
const isJSX = /\.[jt]sx$/.test(id);
|
|
348
|
+
let ast;
|
|
349
|
+
try {
|
|
350
|
+
ast = parseSync(code, {
|
|
351
|
+
syntax: isTypeScript ? "typescript" : "ecmascript",
|
|
352
|
+
tsx: isJSX && isTypeScript,
|
|
353
|
+
jsx: isJSX && !isTypeScript,
|
|
354
|
+
comments: true
|
|
355
|
+
});
|
|
356
|
+
} catch {
|
|
357
|
+
return { code, cssRules: [], transformed: false };
|
|
358
|
+
}
|
|
359
|
+
const allRules = [];
|
|
360
|
+
const replacements = /* @__PURE__ */ new Map();
|
|
361
|
+
walkAndTransform(ast, functionName, allRules, replacements);
|
|
362
|
+
if (replacements.size === 0) {
|
|
363
|
+
return { code, cssRules: [], transformed: false };
|
|
364
|
+
}
|
|
365
|
+
const importsToRemove = findCatomImports(ast, functionName);
|
|
366
|
+
let result = code;
|
|
367
|
+
const modifications = [];
|
|
368
|
+
for (const [start, classNames] of replacements) {
|
|
369
|
+
const searchStart = start - 1;
|
|
370
|
+
const cssCallMatch = findCSSCallBounds(result, searchStart, functionName);
|
|
371
|
+
if (cssCallMatch) {
|
|
372
|
+
modifications.push({
|
|
373
|
+
start: cssCallMatch.start,
|
|
374
|
+
end: cssCallMatch.end,
|
|
375
|
+
replacement: JSON.stringify(classNames),
|
|
376
|
+
type: "css-call"
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
for (const imp of importsToRemove) {
|
|
381
|
+
modifications.push({
|
|
382
|
+
start: imp.start,
|
|
383
|
+
end: imp.end,
|
|
384
|
+
replacement: "",
|
|
385
|
+
type: imp.isFullImport ? "full-import" : "import-specifier"
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
modifications.sort((a, b) => b.start - a.start);
|
|
389
|
+
for (const mod of modifications) {
|
|
390
|
+
if (mod.type === "full-import") {
|
|
391
|
+
result = removeImportFromCode(result, mod.start, mod.end, true);
|
|
392
|
+
} else if (mod.type === "import-specifier") {
|
|
393
|
+
result = removeImportFromCode(result, mod.start, mod.end, false);
|
|
394
|
+
} else {
|
|
395
|
+
result = result.slice(0, mod.start) + mod.replacement + result.slice(mod.end);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
return {
|
|
399
|
+
code: result,
|
|
400
|
+
cssRules: allRules,
|
|
401
|
+
transformed: true
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
function findCSSCallBounds(code, startPos, functionName) {
|
|
405
|
+
const searchWindow = 50;
|
|
406
|
+
const searchStart = Math.max(0, startPos - searchWindow);
|
|
407
|
+
const searchEnd = Math.min(code.length, startPos + searchWindow);
|
|
408
|
+
const searchRegion = code.slice(searchStart, searchEnd);
|
|
409
|
+
const funcPattern = new RegExp(`\\b${functionName}\\s*\\(`);
|
|
410
|
+
const match = funcPattern.exec(searchRegion);
|
|
411
|
+
if (!match) return null;
|
|
412
|
+
const callStart = searchStart + match.index;
|
|
413
|
+
const parenStart = callStart + match[0].length - 1;
|
|
414
|
+
let depth = 1;
|
|
415
|
+
let i = parenStart + 1;
|
|
416
|
+
while (i < code.length && depth > 0) {
|
|
417
|
+
const char = code[i];
|
|
418
|
+
if (char === "(") depth++;
|
|
419
|
+
else if (char === ")") depth--;
|
|
420
|
+
i++;
|
|
421
|
+
}
|
|
422
|
+
if (depth !== 0) return null;
|
|
423
|
+
return { start: callStart, end: i };
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// src/vite/index.ts
|
|
427
|
+
var VIRTUAL_MODULE_ID = "virtual:catom.css";
|
|
428
|
+
var RESOLVED_VIRTUAL_MODULE_ID = "\0" + VIRTUAL_MODULE_ID;
|
|
429
|
+
function catomPlugin(options = {}) {
|
|
430
|
+
const {
|
|
431
|
+
include = /\.[jt]sx?$/,
|
|
432
|
+
exclude = /node_modules/,
|
|
433
|
+
functionName = "css"
|
|
434
|
+
} = options;
|
|
435
|
+
const moduleCSS = /* @__PURE__ */ new Map();
|
|
436
|
+
let filter;
|
|
437
|
+
return {
|
|
438
|
+
name: "vite-plugin-catom",
|
|
439
|
+
// Ensure we run before other transforms
|
|
440
|
+
enforce: "pre",
|
|
441
|
+
configResolved() {
|
|
442
|
+
filter = createFilter(include, exclude);
|
|
443
|
+
},
|
|
444
|
+
buildStart() {
|
|
445
|
+
moduleCSS.clear();
|
|
446
|
+
},
|
|
447
|
+
resolveId(id) {
|
|
448
|
+
if (id === VIRTUAL_MODULE_ID) {
|
|
449
|
+
return RESOLVED_VIRTUAL_MODULE_ID;
|
|
450
|
+
}
|
|
451
|
+
return null;
|
|
452
|
+
},
|
|
453
|
+
load(id) {
|
|
454
|
+
if (id === RESOLVED_VIRTUAL_MODULE_ID) {
|
|
455
|
+
const allRules = [];
|
|
456
|
+
for (const state of moduleCSS.values()) {
|
|
457
|
+
allRules.push(...state.rules);
|
|
458
|
+
}
|
|
459
|
+
const css = generateCSS(allRules);
|
|
460
|
+
return css;
|
|
461
|
+
}
|
|
462
|
+
return null;
|
|
463
|
+
},
|
|
464
|
+
transform(code, id) {
|
|
465
|
+
if (!filter(id)) {
|
|
466
|
+
return null;
|
|
467
|
+
}
|
|
468
|
+
if (!code.includes(functionName + "(")) {
|
|
469
|
+
return null;
|
|
470
|
+
}
|
|
471
|
+
try {
|
|
472
|
+
const result = transformCode(code, id, functionName);
|
|
473
|
+
if (!result.transformed) {
|
|
474
|
+
return null;
|
|
475
|
+
}
|
|
476
|
+
moduleCSS.set(id, {
|
|
477
|
+
rules: result.cssRules,
|
|
478
|
+
timestamp: Date.now()
|
|
479
|
+
});
|
|
480
|
+
return {
|
|
481
|
+
code: result.code,
|
|
482
|
+
map: null
|
|
483
|
+
// TODO: Add source map support
|
|
484
|
+
};
|
|
485
|
+
} catch (error) {
|
|
486
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
487
|
+
this.error(`[catom] Error transforming ${id}: ${message}`);
|
|
488
|
+
}
|
|
489
|
+
},
|
|
490
|
+
// Handle module removal (for dev server)
|
|
491
|
+
watchChange(id) {
|
|
492
|
+
if (moduleCSS.has(id)) {
|
|
493
|
+
moduleCSS.delete(id);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
export { catomPlugin, catomPlugin as default };
|
|
500
|
+
//# sourceMappingURL=index.js.map
|
|
501
|
+
//# sourceMappingURL=index.js.map
|