lightningcss-plugin-css-variables 0.1.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/LICENSE +21 -0
- package/README.md +64 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +261 -0
- package/dist/index.js.map +1 -0
- package/package.json +68 -0
- package/src/calc.ts +303 -0
- package/src/index.test.ts +139 -0
- package/src/index.ts +77 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ocavue
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# lightningcss-plugin-css-variables
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/lightningcss-plugin-css-variables)
|
|
4
|
+
|
|
5
|
+
A [LightningCSS](https://lightningcss.dev/) plugin that inlines CSS custom properties at build time. Matching variable declarations are removed from the output and all `var()` references are replaced with their values.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install lightningcss-plugin-css-variables lightningcss
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```js
|
|
16
|
+
import { transform } from 'lightningcss'
|
|
17
|
+
import cssVariables from 'lightningcss-plugin-css-variables'
|
|
18
|
+
|
|
19
|
+
const { code } = transform({
|
|
20
|
+
filename: 'style.css',
|
|
21
|
+
code: Buffer.from(`
|
|
22
|
+
:root {
|
|
23
|
+
--_color: red;
|
|
24
|
+
--_spacing: 4px;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.button {
|
|
28
|
+
color: var(--_color);
|
|
29
|
+
padding: calc(var(--_spacing) * 2);
|
|
30
|
+
}
|
|
31
|
+
`),
|
|
32
|
+
visitor: cssVariables(),
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
console.log(code.toString())
|
|
36
|
+
// .button {
|
|
37
|
+
// color: red;
|
|
38
|
+
// padding: 8px;
|
|
39
|
+
// }
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
By default, all custom properties whose name starts with `--_` are inlined. Other variables are left untouched.
|
|
43
|
+
|
|
44
|
+
## Options
|
|
45
|
+
|
|
46
|
+
### `include`
|
|
47
|
+
|
|
48
|
+
A `RegExp` or an array of `RegExp` patterns to match the custom property names that should be inlined.
|
|
49
|
+
|
|
50
|
+
```js
|
|
51
|
+
cssVariables({
|
|
52
|
+
// Inline variables starting with --my-prefix-
|
|
53
|
+
include: /^--my-prefix-/,
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
cssVariables({
|
|
57
|
+
// Inline variables matching any of the patterns
|
|
58
|
+
include: [/^--_/, /^--internal-/],
|
|
59
|
+
})
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## License
|
|
63
|
+
|
|
64
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Visitor } from "lightningcss";
|
|
2
|
+
|
|
3
|
+
//#region src/index.d.ts
|
|
4
|
+
interface Options {
|
|
5
|
+
/**
|
|
6
|
+
* A regular expression or an array of regular expressions to match the custom variable names that should
|
|
7
|
+
* be inlined. By default, all custom variables that start with `--_` will be inlined.
|
|
8
|
+
*/
|
|
9
|
+
include?: RegExp | RegExp[];
|
|
10
|
+
}
|
|
11
|
+
declare function cssVariables(options?: Options): Visitor<never>;
|
|
12
|
+
//#endregion
|
|
13
|
+
export { Options, cssVariables, cssVariables as default };
|
|
14
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/index.ts"],"mappings":";;;UAIiB,OAAA;;AAAjB;;;EAKE,OAAA,GAAU,MAAA,GAAS,MAAA;AAAA;AAAA,iBAaL,YAAA,CAAa,OAAA,GAAU,OAAA,GAAU,OAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
//#region src/calc.ts
|
|
2
|
+
function isWhiteSpaceToken(token) {
|
|
3
|
+
return token.type === "token" && token.value.type === "white-space";
|
|
4
|
+
}
|
|
5
|
+
function isDelimToken(token) {
|
|
6
|
+
return token.type === "token" && token.value.type === "delim" && (token.value.value === "+" || token.value.value === "-" || token.value.value === "*" || token.value.value === "/");
|
|
7
|
+
}
|
|
8
|
+
function isLengthToken(token) {
|
|
9
|
+
return token.type === "length";
|
|
10
|
+
}
|
|
11
|
+
function isTimeToken(token) {
|
|
12
|
+
return token.type === "time";
|
|
13
|
+
}
|
|
14
|
+
function isAngleToken(token) {
|
|
15
|
+
return token.type === "angle";
|
|
16
|
+
}
|
|
17
|
+
function isNumberToken(token) {
|
|
18
|
+
return token.type === "token" && token.value.type === "number";
|
|
19
|
+
}
|
|
20
|
+
function isPercentageToken(token) {
|
|
21
|
+
return token.type === "token" && token.value.type === "percentage";
|
|
22
|
+
}
|
|
23
|
+
function isParenOpenToken(token) {
|
|
24
|
+
return token.type === "token" && token.value.type === "parenthesis-block";
|
|
25
|
+
}
|
|
26
|
+
function isParenCloseToken(token) {
|
|
27
|
+
return token.type === "token" && token.value.type === "close-parenthesis";
|
|
28
|
+
}
|
|
29
|
+
function parseCalcTokens(input) {
|
|
30
|
+
const output = [];
|
|
31
|
+
for (const token of input) {
|
|
32
|
+
if (isWhiteSpaceToken(token)) continue;
|
|
33
|
+
if (isDelimToken(token)) {
|
|
34
|
+
output.push(token);
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (isLengthToken(token)) {
|
|
38
|
+
output.push(token);
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (isTimeToken(token)) {
|
|
42
|
+
output.push(token);
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (isAngleToken(token)) {
|
|
46
|
+
output.push(token);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (isNumberToken(token)) {
|
|
50
|
+
output.push(token);
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (isPercentageToken(token)) {
|
|
54
|
+
output.push(token);
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (isParenOpenToken(token)) {
|
|
58
|
+
output.push(token);
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (isParenCloseToken(token)) {
|
|
62
|
+
output.push(token);
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
return output;
|
|
68
|
+
}
|
|
69
|
+
function toEvalResult(token) {
|
|
70
|
+
if (isLengthToken(token)) return {
|
|
71
|
+
value: token.value.value,
|
|
72
|
+
unit: token.value.unit
|
|
73
|
+
};
|
|
74
|
+
if (isTimeToken(token)) return {
|
|
75
|
+
value: token.value.value,
|
|
76
|
+
unit: token.value.type === "milliseconds" ? "ms" : "s"
|
|
77
|
+
};
|
|
78
|
+
if (isAngleToken(token)) return {
|
|
79
|
+
value: token.value.value,
|
|
80
|
+
unit: token.value.type
|
|
81
|
+
};
|
|
82
|
+
if (isNumberToken(token)) return {
|
|
83
|
+
value: token.value.value,
|
|
84
|
+
unit: null
|
|
85
|
+
};
|
|
86
|
+
if (isPercentageToken(token)) return {
|
|
87
|
+
value: token.value.value,
|
|
88
|
+
unit: "%"
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
const ANGLE_UNITS = new Set([
|
|
92
|
+
"deg",
|
|
93
|
+
"rad",
|
|
94
|
+
"grad",
|
|
95
|
+
"turn"
|
|
96
|
+
]);
|
|
97
|
+
function calcValueToToken(v) {
|
|
98
|
+
if (v.unit === null) return {
|
|
99
|
+
type: "token",
|
|
100
|
+
value: {
|
|
101
|
+
type: "number",
|
|
102
|
+
value: v.value
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
if (v.unit === "%") return {
|
|
106
|
+
type: "token",
|
|
107
|
+
value: {
|
|
108
|
+
type: "percentage",
|
|
109
|
+
value: v.value
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
if (ANGLE_UNITS.has(v.unit)) return {
|
|
113
|
+
type: "angle",
|
|
114
|
+
value: {
|
|
115
|
+
type: v.unit,
|
|
116
|
+
value: v.value
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
if (v.unit === "ms") return {
|
|
120
|
+
type: "time",
|
|
121
|
+
value: {
|
|
122
|
+
type: "milliseconds",
|
|
123
|
+
value: v.value
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
if (v.unit === "s") return {
|
|
127
|
+
type: "time",
|
|
128
|
+
value: {
|
|
129
|
+
type: "seconds",
|
|
130
|
+
value: v.value
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
return {
|
|
134
|
+
type: "length",
|
|
135
|
+
value: {
|
|
136
|
+
unit: v.unit,
|
|
137
|
+
value: v.value
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
function evaluateCalc(tokens) {
|
|
142
|
+
const calcTokens = parseCalcTokens(tokens);
|
|
143
|
+
if (calcTokens == null) return;
|
|
144
|
+
let pos = 0;
|
|
145
|
+
const peek = () => calcTokens[pos];
|
|
146
|
+
const next = () => calcTokens[pos++];
|
|
147
|
+
function expr() {
|
|
148
|
+
let left = term();
|
|
149
|
+
if (!left) return void 0;
|
|
150
|
+
let t = peek();
|
|
151
|
+
while (t && isDelimToken(t) && (t.value.value === "+" || t.value.value === "-")) {
|
|
152
|
+
const op = t.value.value;
|
|
153
|
+
next();
|
|
154
|
+
const right = term();
|
|
155
|
+
if (!right || left.unit !== right.unit) return void 0;
|
|
156
|
+
left = {
|
|
157
|
+
value: op === "+" ? left.value + right.value : left.value - right.value,
|
|
158
|
+
unit: left.unit
|
|
159
|
+
};
|
|
160
|
+
t = peek();
|
|
161
|
+
}
|
|
162
|
+
return left;
|
|
163
|
+
}
|
|
164
|
+
function term() {
|
|
165
|
+
let left = factor();
|
|
166
|
+
if (!left) return void 0;
|
|
167
|
+
let t = peek();
|
|
168
|
+
while (t && isDelimToken(t) && (t.value.value === "*" || t.value.value === "/")) {
|
|
169
|
+
const op = t.value.value;
|
|
170
|
+
next();
|
|
171
|
+
const right = factor();
|
|
172
|
+
if (!right) return void 0;
|
|
173
|
+
if (op === "*") if (left.unit === null) left = {
|
|
174
|
+
value: left.value * right.value,
|
|
175
|
+
unit: right.unit
|
|
176
|
+
};
|
|
177
|
+
else if (right.unit === null) left = {
|
|
178
|
+
value: left.value * right.value,
|
|
179
|
+
unit: left.unit
|
|
180
|
+
};
|
|
181
|
+
else return void 0;
|
|
182
|
+
else {
|
|
183
|
+
if (right.unit !== null || right.value === 0) return void 0;
|
|
184
|
+
left = {
|
|
185
|
+
value: left.value / right.value,
|
|
186
|
+
unit: left.unit
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
t = peek();
|
|
190
|
+
}
|
|
191
|
+
return left;
|
|
192
|
+
}
|
|
193
|
+
function factor() {
|
|
194
|
+
const t = peek();
|
|
195
|
+
if (!t) return void 0;
|
|
196
|
+
if (isParenOpenToken(t)) {
|
|
197
|
+
next();
|
|
198
|
+
const result = expr();
|
|
199
|
+
if (!result) return void 0;
|
|
200
|
+
const closing = peek();
|
|
201
|
+
if (!closing || !isParenCloseToken(closing)) return void 0;
|
|
202
|
+
next();
|
|
203
|
+
return result;
|
|
204
|
+
}
|
|
205
|
+
const val = toEvalResult(t);
|
|
206
|
+
if (val) {
|
|
207
|
+
next();
|
|
208
|
+
return val;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
const result = expr();
|
|
212
|
+
if (!result || pos !== calcTokens.length) return void 0;
|
|
213
|
+
return calcValueToToken(result);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
//#endregion
|
|
217
|
+
//#region src/index.ts
|
|
218
|
+
const defaultInclude = /^--_/;
|
|
219
|
+
function ensureArray(value) {
|
|
220
|
+
return Array.isArray(value) ? value : [value];
|
|
221
|
+
}
|
|
222
|
+
function warn(message) {
|
|
223
|
+
console.warn(`[lightningcss-plugin-css-variables] ${message}`);
|
|
224
|
+
}
|
|
225
|
+
function cssVariables(options) {
|
|
226
|
+
const includePatterns = ensureArray(options?.include || defaultInclude);
|
|
227
|
+
const check = (name) => {
|
|
228
|
+
return includePatterns.some((pattern) => pattern.test(name));
|
|
229
|
+
};
|
|
230
|
+
const variables = /* @__PURE__ */ new Map();
|
|
231
|
+
return {
|
|
232
|
+
Declaration: { custom(property) {
|
|
233
|
+
const name = property.name;
|
|
234
|
+
const value = property.value;
|
|
235
|
+
if (!check(name)) return;
|
|
236
|
+
if (variables.has(name)) {
|
|
237
|
+
warn(`Variable "${name}" is already defined. Ignoring duplicate declaration.`);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
variables.set(name, value);
|
|
241
|
+
return [];
|
|
242
|
+
} },
|
|
243
|
+
Variable(variable) {
|
|
244
|
+
const name = variable.name.ident;
|
|
245
|
+
if (!check(name)) return;
|
|
246
|
+
const value = variables.get(name);
|
|
247
|
+
if (value == null) {
|
|
248
|
+
warn(`Variable "${name}" is not defined. Cannot inline value.`);
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
return value;
|
|
252
|
+
},
|
|
253
|
+
FunctionExit: { calc(fn) {
|
|
254
|
+
return evaluateCalc(fn.arguments);
|
|
255
|
+
} }
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
//#endregion
|
|
260
|
+
export { cssVariables, cssVariables as default };
|
|
261
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/calc.ts","../src/index.ts"],"sourcesContent":["import type { Angle, LengthValue, Time, TokenOrValue } from 'lightningcss'\n\n// --- Token types and guards ---\n\ntype WhiteSpaceToken = {\n type: 'token'\n value: {\n type: 'white-space'\n value: string\n }\n}\n\nfunction isWhiteSpaceToken(token: TokenOrValue): token is WhiteSpaceToken {\n return token.type === 'token' && token.value.type === 'white-space'\n}\n\ntype DelimToken = {\n type: 'token'\n value: {\n type: 'delim'\n value: '+' | '-' | '*' | '/'\n }\n}\n\nfunction isDelimToken(token: TokenOrValue): token is DelimToken {\n return (\n token.type === 'token' &&\n token.value.type === 'delim' &&\n (token.value.value === '+' ||\n token.value.value === '-' ||\n token.value.value === '*' ||\n token.value.value === '/')\n )\n}\n\ntype LengthToken = {\n type: 'length'\n value: LengthValue\n}\n\nfunction isLengthToken(token: TokenOrValue): token is LengthToken {\n return token.type === 'length'\n}\n\ninterface TimeToken {\n type: 'time'\n value: Time\n}\n\nfunction isTimeToken(token: TokenOrValue): token is TimeToken {\n return token.type === 'time'\n}\n\ninterface AngleToken {\n type: 'angle'\n value: Angle\n}\n\nfunction isAngleToken(token: TokenOrValue): token is AngleToken {\n return token.type === 'angle'\n}\n\ntype NumberToken = {\n type: 'token'\n value: {\n type: 'number'\n value: number\n }\n}\n\nfunction isNumberToken(token: TokenOrValue): token is NumberToken {\n return token.type === 'token' && token.value.type === 'number'\n}\n\ntype PercentageToken = {\n type: 'token'\n value: {\n type: 'percentage'\n value: number\n }\n}\n\nfunction isPercentageToken(token: TokenOrValue): token is PercentageToken {\n return token.type === 'token' && token.value.type === 'percentage'\n}\n\ntype ParenOpenToken = {\n type: 'token'\n value: {\n type: 'parenthesis-block'\n }\n}\n\nfunction isParenOpenToken(token: TokenOrValue): token is ParenOpenToken {\n return token.type === 'token' && token.value.type === 'parenthesis-block'\n}\n\ntype ParenCloseToken = {\n type: 'token'\n value: {\n type: 'close-parenthesis'\n }\n}\n\nfunction isParenCloseToken(token: TokenOrValue): token is ParenCloseToken {\n return token.type === 'token' && token.value.type === 'close-parenthesis'\n}\n\n// --- CalcToken union and parsing ---\n\ntype CalcToken =\n | DelimToken\n | LengthToken\n | TimeToken\n | AngleToken\n | NumberToken\n | PercentageToken\n | ParenOpenToken\n | ParenCloseToken\n\nfunction parseCalcTokens(input: TokenOrValue[]): CalcToken[] | undefined {\n const output: CalcToken[] = []\n\n for (const token of input) {\n if (isWhiteSpaceToken(token)) {\n continue\n }\n if (isDelimToken(token)) {\n output.push(token)\n continue\n }\n if (isLengthToken(token)) {\n output.push(token)\n continue\n }\n if (isTimeToken(token)) {\n output.push(token)\n continue\n }\n if (isAngleToken(token)) {\n output.push(token)\n continue\n }\n if (isNumberToken(token)) {\n output.push(token)\n continue\n }\n if (isPercentageToken(token)) {\n output.push(token)\n continue\n }\n if (isParenOpenToken(token)) {\n output.push(token)\n continue\n }\n if (isParenCloseToken(token)) {\n output.push(token)\n continue\n }\n // unknown token — bail\n return\n }\n return output\n}\n\n// --- Evaluation ---\n\ninterface EvalResult {\n value: number\n unit: string | null\n}\n\nfunction toEvalResult(token: CalcToken): EvalResult | undefined {\n if (isLengthToken(token)) {\n return { value: token.value.value, unit: token.value.unit }\n }\n if (isTimeToken(token)) {\n return {\n value: token.value.value,\n unit: token.value.type === 'milliseconds' ? 'ms' : 's',\n }\n }\n if (isAngleToken(token)) {\n return { value: token.value.value, unit: token.value.type }\n }\n if (isNumberToken(token)) {\n return { value: token.value.value, unit: null }\n }\n if (isPercentageToken(token)) {\n return { value: token.value.value, unit: '%' }\n }\n return undefined\n}\n\nconst ANGLE_UNITS = new Set(['deg', 'rad', 'grad', 'turn'])\n\nfunction calcValueToToken(v: EvalResult): TokenOrValue {\n if (v.unit === null)\n return { type: 'token', value: { type: 'number', value: v.value } }\n if (v.unit === '%')\n return { type: 'token', value: { type: 'percentage', value: v.value } }\n if (ANGLE_UNITS.has(v.unit))\n return {\n type: 'angle',\n value: { type: v.unit as 'deg', value: v.value },\n }\n if (v.unit === 'ms')\n return { type: 'time', value: { type: 'milliseconds', value: v.value } }\n if (v.unit === 's')\n return { type: 'time', value: { type: 'seconds', value: v.value } }\n return {\n type: 'length',\n value: { unit: v.unit as 'px', value: v.value },\n }\n}\n\nexport function evaluateCalc(tokens: TokenOrValue[]): TokenOrValue | undefined {\n const calcTokens = parseCalcTokens(tokens)\n if (calcTokens == null) {\n return\n }\n\n let pos = 0\n const peek = () => calcTokens[pos] as CalcToken | undefined\n const next = () => calcTokens[pos++]\n\n // expr = term (('+' | '-') term)*\n function expr(): EvalResult | undefined {\n let left = term()\n if (!left) return undefined\n let t = peek()\n while (\n t &&\n isDelimToken(t) &&\n (t.value.value === '+' || t.value.value === '-')\n ) {\n const op = t.value.value\n next()\n const right = term()\n if (!right || left.unit !== right.unit) return undefined\n left = {\n value: op === '+' ? left.value + right.value : left.value - right.value,\n unit: left.unit,\n }\n t = peek()\n }\n return left\n }\n\n // term = factor (('*' | '/') factor)*\n function term(): EvalResult | undefined {\n let left = factor()\n if (!left) return undefined\n let t = peek()\n while (\n t &&\n isDelimToken(t) &&\n (t.value.value === '*' || t.value.value === '/')\n ) {\n const op = t.value.value\n next()\n const right = factor()\n if (!right) return undefined\n if (op === '*') {\n if (left.unit === null)\n left = { value: left.value * right.value, unit: right.unit }\n else if (right.unit === null)\n left = { value: left.value * right.value, unit: left.unit }\n else return undefined\n } else {\n if (right.unit !== null || right.value === 0) return undefined\n left = { value: left.value / right.value, unit: left.unit }\n }\n t = peek()\n }\n return left\n }\n\n // factor = '(' expr ')' | value\n function factor(): EvalResult | undefined {\n const t = peek()\n if (!t) return undefined\n if (isParenOpenToken(t)) {\n next()\n const result = expr()\n if (!result) return undefined\n const closing = peek()\n if (!closing || !isParenCloseToken(closing)) return undefined\n next()\n return result\n }\n const val = toEvalResult(t)\n if (val) {\n next()\n return val\n }\n return undefined\n }\n\n const result = expr()\n if (!result || pos !== calcTokens.length) return undefined\n return calcValueToToken(result)\n}\n","import type { TokenOrValue, Visitor } from 'lightningcss'\n\nimport { evaluateCalc } from './calc.ts'\n\nexport interface Options {\n /**\n * A regular expression or an array of regular expressions to match the custom variable names that should\n * be inlined. By default, all custom variables that start with `--_` will be inlined.\n */\n include?: RegExp | RegExp[]\n}\n\nconst defaultInclude = /^--_/\n\nfunction ensureArray<T>(value: T | T[]): T[] {\n return Array.isArray(value) ? value : [value]\n}\n\nfunction warn(message: string) {\n console.warn(`[lightningcss-plugin-css-variables] ${message}`)\n}\n\nexport function cssVariables(options?: Options): Visitor<never> {\n const includePatterns: RegExp[] = ensureArray(\n options?.include || defaultInclude,\n )\n\n const check = (name: string): boolean => {\n return includePatterns.some((pattern) => pattern.test(name))\n }\n\n const variables = new Map<string, TokenOrValue[]>()\n\n return {\n Declaration: {\n custom(property) {\n const name: string = property.name\n const value: TokenOrValue[] = property.value\n\n if (!check(name)) {\n return\n }\n\n if (variables.has(name)) {\n warn(\n `Variable \"${name}\" is already defined. Ignoring duplicate declaration.`,\n )\n return\n }\n\n variables.set(name, value)\n return []\n },\n },\n Variable(variable) {\n const name: string = variable.name.ident\n if (!check(name)) {\n return\n }\n\n const value = variables.get(name)\n if (value == null) {\n warn(`Variable \"${name}\" is not defined. Cannot inline value.`)\n return\n }\n\n return value\n },\n FunctionExit: {\n calc(fn: { name: string; arguments: TokenOrValue[] }) {\n return evaluateCalc(fn.arguments)\n },\n },\n }\n}\n\nexport default cssVariables\n"],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "lightningcss-plugin-css-variables",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"description": "A plugin for LightningCSS to inline CSS variables.",
|
|
6
|
+
"author": "ocavue <ocavue@gmail.com>",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"funding": "https://github.com/sponsors/ocavue",
|
|
9
|
+
"homepage": "https://github.com/ocavue/lightningcss-plugin-css-variables#readme",
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "https://github.com/ocavue/lightningcss-plugin-css-variables.git"
|
|
13
|
+
},
|
|
14
|
+
"bugs": "https://github.com/ocavue/lightningcss-plugin-css-variables/issues",
|
|
15
|
+
"keywords": [
|
|
16
|
+
"lightningcss-plugin"
|
|
17
|
+
],
|
|
18
|
+
"sideEffects": false,
|
|
19
|
+
"main": "./dist/index.js",
|
|
20
|
+
"module": "./dist/index.js",
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
22
|
+
"exports": {
|
|
23
|
+
".": {
|
|
24
|
+
"types": "./dist/index.d.ts",
|
|
25
|
+
"import": "./dist/index.js"
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"typesVersions": {
|
|
29
|
+
"*": {
|
|
30
|
+
"*": [
|
|
31
|
+
"./dist/*",
|
|
32
|
+
"./dist/index.d.ts"
|
|
33
|
+
]
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"files": [
|
|
37
|
+
"dist",
|
|
38
|
+
"src"
|
|
39
|
+
],
|
|
40
|
+
"peerDependencies": {
|
|
41
|
+
"lightningcss": "^1.31.1"
|
|
42
|
+
},
|
|
43
|
+
"peerDependenciesMeta": {
|
|
44
|
+
"lightningcss": {
|
|
45
|
+
"optional": true
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@ocavue/eslint-config": "^4.2.0",
|
|
50
|
+
"@ocavue/tsconfig": "^0.6.3",
|
|
51
|
+
"@types/node": "^22.19.11",
|
|
52
|
+
"eslint": "^10.0.2",
|
|
53
|
+
"lightningcss": "^1.31.1",
|
|
54
|
+
"prettier": "^3.8.1",
|
|
55
|
+
"tsdown": "^0.20.3",
|
|
56
|
+
"typescript": "^5.9.3",
|
|
57
|
+
"vite": "^7.3.1",
|
|
58
|
+
"vitest": "^4.0.18"
|
|
59
|
+
},
|
|
60
|
+
"scripts": {
|
|
61
|
+
"build": "tsdown",
|
|
62
|
+
"dev": "tsdown --watch",
|
|
63
|
+
"lint": "eslint .",
|
|
64
|
+
"fix": "eslint --fix . && prettier --write .",
|
|
65
|
+
"test": "vitest",
|
|
66
|
+
"typecheck": "tsc -b"
|
|
67
|
+
}
|
|
68
|
+
}
|
package/src/calc.ts
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import type { Angle, LengthValue, Time, TokenOrValue } from 'lightningcss'
|
|
2
|
+
|
|
3
|
+
// --- Token types and guards ---
|
|
4
|
+
|
|
5
|
+
type WhiteSpaceToken = {
|
|
6
|
+
type: 'token'
|
|
7
|
+
value: {
|
|
8
|
+
type: 'white-space'
|
|
9
|
+
value: string
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function isWhiteSpaceToken(token: TokenOrValue): token is WhiteSpaceToken {
|
|
14
|
+
return token.type === 'token' && token.value.type === 'white-space'
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
type DelimToken = {
|
|
18
|
+
type: 'token'
|
|
19
|
+
value: {
|
|
20
|
+
type: 'delim'
|
|
21
|
+
value: '+' | '-' | '*' | '/'
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function isDelimToken(token: TokenOrValue): token is DelimToken {
|
|
26
|
+
return (
|
|
27
|
+
token.type === 'token' &&
|
|
28
|
+
token.value.type === 'delim' &&
|
|
29
|
+
(token.value.value === '+' ||
|
|
30
|
+
token.value.value === '-' ||
|
|
31
|
+
token.value.value === '*' ||
|
|
32
|
+
token.value.value === '/')
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
type LengthToken = {
|
|
37
|
+
type: 'length'
|
|
38
|
+
value: LengthValue
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function isLengthToken(token: TokenOrValue): token is LengthToken {
|
|
42
|
+
return token.type === 'length'
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface TimeToken {
|
|
46
|
+
type: 'time'
|
|
47
|
+
value: Time
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function isTimeToken(token: TokenOrValue): token is TimeToken {
|
|
51
|
+
return token.type === 'time'
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface AngleToken {
|
|
55
|
+
type: 'angle'
|
|
56
|
+
value: Angle
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function isAngleToken(token: TokenOrValue): token is AngleToken {
|
|
60
|
+
return token.type === 'angle'
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
type NumberToken = {
|
|
64
|
+
type: 'token'
|
|
65
|
+
value: {
|
|
66
|
+
type: 'number'
|
|
67
|
+
value: number
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function isNumberToken(token: TokenOrValue): token is NumberToken {
|
|
72
|
+
return token.type === 'token' && token.value.type === 'number'
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
type PercentageToken = {
|
|
76
|
+
type: 'token'
|
|
77
|
+
value: {
|
|
78
|
+
type: 'percentage'
|
|
79
|
+
value: number
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function isPercentageToken(token: TokenOrValue): token is PercentageToken {
|
|
84
|
+
return token.type === 'token' && token.value.type === 'percentage'
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
type ParenOpenToken = {
|
|
88
|
+
type: 'token'
|
|
89
|
+
value: {
|
|
90
|
+
type: 'parenthesis-block'
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function isParenOpenToken(token: TokenOrValue): token is ParenOpenToken {
|
|
95
|
+
return token.type === 'token' && token.value.type === 'parenthesis-block'
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
type ParenCloseToken = {
|
|
99
|
+
type: 'token'
|
|
100
|
+
value: {
|
|
101
|
+
type: 'close-parenthesis'
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function isParenCloseToken(token: TokenOrValue): token is ParenCloseToken {
|
|
106
|
+
return token.type === 'token' && token.value.type === 'close-parenthesis'
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// --- CalcToken union and parsing ---
|
|
110
|
+
|
|
111
|
+
type CalcToken =
|
|
112
|
+
| DelimToken
|
|
113
|
+
| LengthToken
|
|
114
|
+
| TimeToken
|
|
115
|
+
| AngleToken
|
|
116
|
+
| NumberToken
|
|
117
|
+
| PercentageToken
|
|
118
|
+
| ParenOpenToken
|
|
119
|
+
| ParenCloseToken
|
|
120
|
+
|
|
121
|
+
function parseCalcTokens(input: TokenOrValue[]): CalcToken[] | undefined {
|
|
122
|
+
const output: CalcToken[] = []
|
|
123
|
+
|
|
124
|
+
for (const token of input) {
|
|
125
|
+
if (isWhiteSpaceToken(token)) {
|
|
126
|
+
continue
|
|
127
|
+
}
|
|
128
|
+
if (isDelimToken(token)) {
|
|
129
|
+
output.push(token)
|
|
130
|
+
continue
|
|
131
|
+
}
|
|
132
|
+
if (isLengthToken(token)) {
|
|
133
|
+
output.push(token)
|
|
134
|
+
continue
|
|
135
|
+
}
|
|
136
|
+
if (isTimeToken(token)) {
|
|
137
|
+
output.push(token)
|
|
138
|
+
continue
|
|
139
|
+
}
|
|
140
|
+
if (isAngleToken(token)) {
|
|
141
|
+
output.push(token)
|
|
142
|
+
continue
|
|
143
|
+
}
|
|
144
|
+
if (isNumberToken(token)) {
|
|
145
|
+
output.push(token)
|
|
146
|
+
continue
|
|
147
|
+
}
|
|
148
|
+
if (isPercentageToken(token)) {
|
|
149
|
+
output.push(token)
|
|
150
|
+
continue
|
|
151
|
+
}
|
|
152
|
+
if (isParenOpenToken(token)) {
|
|
153
|
+
output.push(token)
|
|
154
|
+
continue
|
|
155
|
+
}
|
|
156
|
+
if (isParenCloseToken(token)) {
|
|
157
|
+
output.push(token)
|
|
158
|
+
continue
|
|
159
|
+
}
|
|
160
|
+
// unknown token — bail
|
|
161
|
+
return
|
|
162
|
+
}
|
|
163
|
+
return output
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// --- Evaluation ---
|
|
167
|
+
|
|
168
|
+
interface EvalResult {
|
|
169
|
+
value: number
|
|
170
|
+
unit: string | null
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function toEvalResult(token: CalcToken): EvalResult | undefined {
|
|
174
|
+
if (isLengthToken(token)) {
|
|
175
|
+
return { value: token.value.value, unit: token.value.unit }
|
|
176
|
+
}
|
|
177
|
+
if (isTimeToken(token)) {
|
|
178
|
+
return {
|
|
179
|
+
value: token.value.value,
|
|
180
|
+
unit: token.value.type === 'milliseconds' ? 'ms' : 's',
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (isAngleToken(token)) {
|
|
184
|
+
return { value: token.value.value, unit: token.value.type }
|
|
185
|
+
}
|
|
186
|
+
if (isNumberToken(token)) {
|
|
187
|
+
return { value: token.value.value, unit: null }
|
|
188
|
+
}
|
|
189
|
+
if (isPercentageToken(token)) {
|
|
190
|
+
return { value: token.value.value, unit: '%' }
|
|
191
|
+
}
|
|
192
|
+
return undefined
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const ANGLE_UNITS = new Set(['deg', 'rad', 'grad', 'turn'])
|
|
196
|
+
|
|
197
|
+
function calcValueToToken(v: EvalResult): TokenOrValue {
|
|
198
|
+
if (v.unit === null)
|
|
199
|
+
return { type: 'token', value: { type: 'number', value: v.value } }
|
|
200
|
+
if (v.unit === '%')
|
|
201
|
+
return { type: 'token', value: { type: 'percentage', value: v.value } }
|
|
202
|
+
if (ANGLE_UNITS.has(v.unit))
|
|
203
|
+
return {
|
|
204
|
+
type: 'angle',
|
|
205
|
+
value: { type: v.unit as 'deg', value: v.value },
|
|
206
|
+
}
|
|
207
|
+
if (v.unit === 'ms')
|
|
208
|
+
return { type: 'time', value: { type: 'milliseconds', value: v.value } }
|
|
209
|
+
if (v.unit === 's')
|
|
210
|
+
return { type: 'time', value: { type: 'seconds', value: v.value } }
|
|
211
|
+
return {
|
|
212
|
+
type: 'length',
|
|
213
|
+
value: { unit: v.unit as 'px', value: v.value },
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export function evaluateCalc(tokens: TokenOrValue[]): TokenOrValue | undefined {
|
|
218
|
+
const calcTokens = parseCalcTokens(tokens)
|
|
219
|
+
if (calcTokens == null) {
|
|
220
|
+
return
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
let pos = 0
|
|
224
|
+
const peek = () => calcTokens[pos] as CalcToken | undefined
|
|
225
|
+
const next = () => calcTokens[pos++]
|
|
226
|
+
|
|
227
|
+
// expr = term (('+' | '-') term)*
|
|
228
|
+
function expr(): EvalResult | undefined {
|
|
229
|
+
let left = term()
|
|
230
|
+
if (!left) return undefined
|
|
231
|
+
let t = peek()
|
|
232
|
+
while (
|
|
233
|
+
t &&
|
|
234
|
+
isDelimToken(t) &&
|
|
235
|
+
(t.value.value === '+' || t.value.value === '-')
|
|
236
|
+
) {
|
|
237
|
+
const op = t.value.value
|
|
238
|
+
next()
|
|
239
|
+
const right = term()
|
|
240
|
+
if (!right || left.unit !== right.unit) return undefined
|
|
241
|
+
left = {
|
|
242
|
+
value: op === '+' ? left.value + right.value : left.value - right.value,
|
|
243
|
+
unit: left.unit,
|
|
244
|
+
}
|
|
245
|
+
t = peek()
|
|
246
|
+
}
|
|
247
|
+
return left
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// term = factor (('*' | '/') factor)*
|
|
251
|
+
function term(): EvalResult | undefined {
|
|
252
|
+
let left = factor()
|
|
253
|
+
if (!left) return undefined
|
|
254
|
+
let t = peek()
|
|
255
|
+
while (
|
|
256
|
+
t &&
|
|
257
|
+
isDelimToken(t) &&
|
|
258
|
+
(t.value.value === '*' || t.value.value === '/')
|
|
259
|
+
) {
|
|
260
|
+
const op = t.value.value
|
|
261
|
+
next()
|
|
262
|
+
const right = factor()
|
|
263
|
+
if (!right) return undefined
|
|
264
|
+
if (op === '*') {
|
|
265
|
+
if (left.unit === null)
|
|
266
|
+
left = { value: left.value * right.value, unit: right.unit }
|
|
267
|
+
else if (right.unit === null)
|
|
268
|
+
left = { value: left.value * right.value, unit: left.unit }
|
|
269
|
+
else return undefined
|
|
270
|
+
} else {
|
|
271
|
+
if (right.unit !== null || right.value === 0) return undefined
|
|
272
|
+
left = { value: left.value / right.value, unit: left.unit }
|
|
273
|
+
}
|
|
274
|
+
t = peek()
|
|
275
|
+
}
|
|
276
|
+
return left
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// factor = '(' expr ')' | value
|
|
280
|
+
function factor(): EvalResult | undefined {
|
|
281
|
+
const t = peek()
|
|
282
|
+
if (!t) return undefined
|
|
283
|
+
if (isParenOpenToken(t)) {
|
|
284
|
+
next()
|
|
285
|
+
const result = expr()
|
|
286
|
+
if (!result) return undefined
|
|
287
|
+
const closing = peek()
|
|
288
|
+
if (!closing || !isParenCloseToken(closing)) return undefined
|
|
289
|
+
next()
|
|
290
|
+
return result
|
|
291
|
+
}
|
|
292
|
+
const val = toEvalResult(t)
|
|
293
|
+
if (val) {
|
|
294
|
+
next()
|
|
295
|
+
return val
|
|
296
|
+
}
|
|
297
|
+
return undefined
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const result = expr()
|
|
301
|
+
if (!result || pos !== calcTokens.length) return undefined
|
|
302
|
+
return calcValueToToken(result)
|
|
303
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { transform } from 'lightningcss'
|
|
2
|
+
import { expect, it } from 'vitest'
|
|
3
|
+
|
|
4
|
+
import cssVariables from './index.ts'
|
|
5
|
+
|
|
6
|
+
const css = String.raw
|
|
7
|
+
|
|
8
|
+
it('should inline CSS variables start with --_', () => {
|
|
9
|
+
const input = css`
|
|
10
|
+
:root {
|
|
11
|
+
--_color: red;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
body {
|
|
15
|
+
color: var(--_color);
|
|
16
|
+
}
|
|
17
|
+
`
|
|
18
|
+
|
|
19
|
+
const output = transform({
|
|
20
|
+
filename: 'test.css',
|
|
21
|
+
code: Buffer.from(input),
|
|
22
|
+
visitor: cssVariables(),
|
|
23
|
+
}).code.toString()
|
|
24
|
+
|
|
25
|
+
expect(output).toMatchInlineSnapshot(`
|
|
26
|
+
"body {
|
|
27
|
+
color: red;
|
|
28
|
+
}
|
|
29
|
+
"
|
|
30
|
+
`)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('should not inline normal CSS variables', () => {
|
|
34
|
+
const input = css`
|
|
35
|
+
:root {
|
|
36
|
+
--color: red;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
body {
|
|
40
|
+
color: var(--color);
|
|
41
|
+
}
|
|
42
|
+
`
|
|
43
|
+
|
|
44
|
+
const output = transform({
|
|
45
|
+
filename: 'test.css',
|
|
46
|
+
code: Buffer.from(input),
|
|
47
|
+
visitor: cssVariables(),
|
|
48
|
+
}).code.toString()
|
|
49
|
+
|
|
50
|
+
expect(output).toMatchInlineSnapshot(`
|
|
51
|
+
":root {
|
|
52
|
+
--color: red;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
body {
|
|
56
|
+
color: var(--color);
|
|
57
|
+
}
|
|
58
|
+
"
|
|
59
|
+
`)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('can calculate CSS variables', () => {
|
|
63
|
+
const input = css`
|
|
64
|
+
:root {
|
|
65
|
+
--__padding-1: 1px;
|
|
66
|
+
--__padding-2: 2px;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.box1 {
|
|
70
|
+
padding: calc(var(--__padding-1) + var(--__padding-2));
|
|
71
|
+
}
|
|
72
|
+
.box2 {
|
|
73
|
+
padding: calc(var(--__padding-1) * 2 + var(--__padding-2) + 10px);
|
|
74
|
+
}
|
|
75
|
+
.box3 {
|
|
76
|
+
padding: calc(10px + 20px);
|
|
77
|
+
}
|
|
78
|
+
.box4 {
|
|
79
|
+
padding: calc((var(--__padding-1) + var(--__padding-2)) * 3);
|
|
80
|
+
}
|
|
81
|
+
.box5 {
|
|
82
|
+
padding: calc(var(--__padding-1) / 2);
|
|
83
|
+
}
|
|
84
|
+
`
|
|
85
|
+
|
|
86
|
+
const output = transform({
|
|
87
|
+
filename: 'test.css',
|
|
88
|
+
code: Buffer.from(input),
|
|
89
|
+
visitor: cssVariables(),
|
|
90
|
+
}).code.toString()
|
|
91
|
+
|
|
92
|
+
expect(output).toMatchInlineSnapshot(`
|
|
93
|
+
".box1 {
|
|
94
|
+
padding: 3px;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.box2 {
|
|
98
|
+
padding: 14px;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.box3 {
|
|
102
|
+
padding: 30px;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.box4 {
|
|
106
|
+
padding: 9px;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.box5 {
|
|
110
|
+
padding: .5px;
|
|
111
|
+
}
|
|
112
|
+
"
|
|
113
|
+
`)
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('keeps calc with mixed units as-is', () => {
|
|
117
|
+
const input = css`
|
|
118
|
+
:root {
|
|
119
|
+
--_size: 20px;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.box {
|
|
123
|
+
width: calc(100% - var(--_size));
|
|
124
|
+
}
|
|
125
|
+
`
|
|
126
|
+
|
|
127
|
+
const output = transform({
|
|
128
|
+
filename: 'test.css',
|
|
129
|
+
code: Buffer.from(input),
|
|
130
|
+
visitor: cssVariables(),
|
|
131
|
+
}).code.toString()
|
|
132
|
+
|
|
133
|
+
expect(output).toMatchInlineSnapshot(`
|
|
134
|
+
".box {
|
|
135
|
+
width: calc(100% - 20px);
|
|
136
|
+
}
|
|
137
|
+
"
|
|
138
|
+
`)
|
|
139
|
+
})
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { TokenOrValue, Visitor } from 'lightningcss'
|
|
2
|
+
|
|
3
|
+
import { evaluateCalc } from './calc.ts'
|
|
4
|
+
|
|
5
|
+
export interface Options {
|
|
6
|
+
/**
|
|
7
|
+
* A regular expression or an array of regular expressions to match the custom variable names that should
|
|
8
|
+
* be inlined. By default, all custom variables that start with `--_` will be inlined.
|
|
9
|
+
*/
|
|
10
|
+
include?: RegExp | RegExp[]
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const defaultInclude = /^--_/
|
|
14
|
+
|
|
15
|
+
function ensureArray<T>(value: T | T[]): T[] {
|
|
16
|
+
return Array.isArray(value) ? value : [value]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function warn(message: string) {
|
|
20
|
+
console.warn(`[lightningcss-plugin-css-variables] ${message}`)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function cssVariables(options?: Options): Visitor<never> {
|
|
24
|
+
const includePatterns: RegExp[] = ensureArray(
|
|
25
|
+
options?.include || defaultInclude,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
const check = (name: string): boolean => {
|
|
29
|
+
return includePatterns.some((pattern) => pattern.test(name))
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const variables = new Map<string, TokenOrValue[]>()
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
Declaration: {
|
|
36
|
+
custom(property) {
|
|
37
|
+
const name: string = property.name
|
|
38
|
+
const value: TokenOrValue[] = property.value
|
|
39
|
+
|
|
40
|
+
if (!check(name)) {
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (variables.has(name)) {
|
|
45
|
+
warn(
|
|
46
|
+
`Variable "${name}" is already defined. Ignoring duplicate declaration.`,
|
|
47
|
+
)
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
variables.set(name, value)
|
|
52
|
+
return []
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
Variable(variable) {
|
|
56
|
+
const name: string = variable.name.ident
|
|
57
|
+
if (!check(name)) {
|
|
58
|
+
return
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const value = variables.get(name)
|
|
62
|
+
if (value == null) {
|
|
63
|
+
warn(`Variable "${name}" is not defined. Cannot inline value.`)
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return value
|
|
68
|
+
},
|
|
69
|
+
FunctionExit: {
|
|
70
|
+
calc(fn: { name: string; arguments: TokenOrValue[] }) {
|
|
71
|
+
return evaluateCalc(fn.arguments)
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export default cssVariables
|