@astryxdesign/build 0.7.6-canary.f4abc97 → 0.7.6-canary.f52dbbe
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 +2 -0
- package/dist/undefinedVariables.d.ts +32 -0
- package/dist/vite.mjs +107 -10
- package/package.json +1 -1
- package/src/undefinedVariables.ts +57 -0
- package/src/vite.build.test.ts +141 -12
- package/src/vite.test.ts +55 -0
- package/src/vite.ts +145 -6
package/README.md
CHANGED
|
@@ -239,6 +239,8 @@ Wraps `@stylexjs/unplugin` and intercepts the dev CSS endpoint (`/virtual:stylex
|
|
|
239
239
|
|
|
240
240
|
Unsupported declarations fail the build by default with StyleX's replacement guidance. Set `propertyValidationMode` explicitly in `stylexOverrides` (modern API) or `stylexOptions` (legacy API) only when a migration needs the upstream `warn` or `silent` behavior.
|
|
241
241
|
|
|
242
|
+
A production build also checks the StyleX stylesheet for generated custom properties (`--<prefix><hash>`) that it reads but no stylesheet in the build declares, which browsers drop: a `.stylex.ts` export read in a style that is not a `defineVars`/`defineConsts` value, or one whose module never reached the build. It warns, naming the variable and the rule that reads it. Only stylesheets count as declarations, so a property the app sets at runtime (an inline style) is reported too: give it a name outside the StyleX class-name prefixes.
|
|
243
|
+
|
|
242
244
|
---
|
|
243
245
|
|
|
244
246
|
## Advanced Options
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file undefinedVariables.ts
|
|
3
|
+
* @input A stylesheet's text, the StyleX class-name prefixes, and the other
|
|
4
|
+
* stylesheets the build emits
|
|
5
|
+
* @output undefinedGeneratedVariables, the generated custom properties the
|
|
6
|
+
* stylesheet reads but never declares
|
|
7
|
+
* @position Internal to @astryxdesign/build: astryxStylex (vite.ts) runs it
|
|
8
|
+
* on a production stylesheet and warns about what it finds. Not exported
|
|
9
|
+
* from any package entry.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* The StyleX-generated custom properties `css` reads that it never declares.
|
|
13
|
+
*
|
|
14
|
+
* StyleX names the variables it generates `--<prefix><hash>` (a theme
|
|
15
|
+
* variable, or a constant waiting for its value). One that is read but never
|
|
16
|
+
* declared is a value that compiled to a variable nothing sets, so browsers
|
|
17
|
+
* drop the declaration or take its fallback: a constant whose defining module
|
|
18
|
+
* never reached the build, or a plain export read from a `.stylex.ts` file as
|
|
19
|
+
* if it were a token. The dynamic-style variables StyleX sets inline
|
|
20
|
+
* (`--<prefix>-<property>`) and component variables with a dash after the
|
|
21
|
+
* prefix are not generated names and are never reported.
|
|
22
|
+
*
|
|
23
|
+
* Only a hash-shaped name counts as generated: the prefix and four to seven
|
|
24
|
+
* base-36 characters (StyleX's hash is a 32-bit value in base 36, shorter than
|
|
25
|
+
* four characters about once in a hundred thousand). An app's own short names
|
|
26
|
+
* (`--xl`) are never reported, and a property declared in any stylesheet the
|
|
27
|
+
* build emits (`others`, the app's own CSS) is declared.
|
|
28
|
+
*
|
|
29
|
+
* SYNC: scripts/build-css.mjs `undefinedStyleXVariables` is the same check
|
|
30
|
+
* for the prebuilt package stylesheets.
|
|
31
|
+
*/
|
|
32
|
+
export declare function undefinedGeneratedVariables(css: string, prefixes: readonly string[], others?: readonly string[]): string[];
|
package/dist/vite.mjs
CHANGED
|
@@ -8,6 +8,26 @@ import path from "node:path";
|
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
9
|
import { createRequire } from "node:module";
|
|
10
10
|
import { createHash } from "node:crypto";
|
|
11
|
+
|
|
12
|
+
// src/undefinedVariables.ts
|
|
13
|
+
function undefinedGeneratedVariables(css, prefixes, others = []) {
|
|
14
|
+
const names = prefixes.map((prefix) => prefix.replace(/[^a-zA-Z0-9_-]/g, "")).filter(Boolean).join("|");
|
|
15
|
+
const declared = new Set(
|
|
16
|
+
[css, ...others].flatMap(
|
|
17
|
+
(text) => [...text.matchAll(/(--[A-Za-z0-9_-]+)\s*:/g)].map((match) => match[1])
|
|
18
|
+
)
|
|
19
|
+
);
|
|
20
|
+
const read = new Set(
|
|
21
|
+
[
|
|
22
|
+
...css.matchAll(
|
|
23
|
+
new RegExp(`var\\(\\s*(--(?:${names})[a-z0-9]{4,7})\\s*[,)]`, "g")
|
|
24
|
+
)
|
|
25
|
+
].map((match) => match[1])
|
|
26
|
+
);
|
|
27
|
+
return [...read].filter((name) => !declared.has(name)).sort();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// src/vite.ts
|
|
11
31
|
var __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
12
32
|
var LIBRARY_PATTERN = "node_modules/@astryxdesign/";
|
|
13
33
|
var STYLEX_CSS_PATH = "/virtual:stylex.css";
|
|
@@ -32,6 +52,10 @@ function astryxStylex(options = {}) {
|
|
|
32
52
|
} = opts;
|
|
33
53
|
const libraryLayer = layers.library ?? "astryx-base";
|
|
34
54
|
const productLayer = layers.product ?? "product";
|
|
55
|
+
const classNamePrefixes = [
|
|
56
|
+
stylexPrefix,
|
|
57
|
+
stylexOverrides.classNamePrefix ?? "x"
|
|
58
|
+
];
|
|
35
59
|
const overrideLightningcssOptions = stylexOverrides.lightningcssOptions;
|
|
36
60
|
const stylexOptions = {
|
|
37
61
|
dev,
|
|
@@ -148,7 +172,8 @@ function astryxStylex(options = {}) {
|
|
|
148
172
|
renderSplitLayers(rulesById, {
|
|
149
173
|
libraryPattern,
|
|
150
174
|
libraryLayer,
|
|
151
|
-
productLayer
|
|
175
|
+
productLayer,
|
|
176
|
+
classNamePrefixes
|
|
152
177
|
})
|
|
153
178
|
);
|
|
154
179
|
}
|
|
@@ -165,7 +190,8 @@ function astryxStylex(options = {}) {
|
|
|
165
190
|
libraryPattern,
|
|
166
191
|
libraryLayer,
|
|
167
192
|
productLayer,
|
|
168
|
-
lightningcssOptions: stylexOptions.lightningcssOptions
|
|
193
|
+
lightningcssOptions: stylexOptions.lightningcssOptions,
|
|
194
|
+
classNamePrefixes
|
|
169
195
|
})
|
|
170
196
|
];
|
|
171
197
|
}
|
|
@@ -173,21 +199,57 @@ function renderSplitLayers(rulesById, options) {
|
|
|
173
199
|
if (!rulesById || rulesById.size === 0) return "";
|
|
174
200
|
const libraryRules = [];
|
|
175
201
|
const productRules = [];
|
|
202
|
+
const collectedConstants = [];
|
|
176
203
|
for (const [filePath, rules] of rulesById.entries()) {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
204
|
+
const own = filePath.includes(options.libraryPattern) ? libraryRules : productRules;
|
|
205
|
+
for (const rule of rules) {
|
|
206
|
+
if (isConstantRule(rule)) {
|
|
207
|
+
collectedConstants.push(rule);
|
|
208
|
+
} else {
|
|
209
|
+
own.push(rule);
|
|
210
|
+
}
|
|
181
211
|
}
|
|
182
212
|
}
|
|
213
|
+
const constantRules = constantRulesForEveryPrefix(
|
|
214
|
+
collectedConstants,
|
|
215
|
+
options.classNamePrefixes
|
|
216
|
+
);
|
|
183
217
|
const render = (rules, layer) => rules.length ? `@layer ${layer} {
|
|
184
|
-
${stylexBabelPlugin.processStylexRules(rules, { useLayers: true })}
|
|
218
|
+
${stylexBabelPlugin.processStylexRules([...constantRules, ...rules], { useLayers: true })}
|
|
185
219
|
}` : "";
|
|
186
220
|
return [
|
|
187
221
|
render(libraryRules, options.libraryLayer),
|
|
188
222
|
render(productRules, options.productLayer)
|
|
189
223
|
].filter(Boolean).join("\n\n");
|
|
190
224
|
}
|
|
225
|
+
function constantRulesForEveryPrefix(rules, prefixes) {
|
|
226
|
+
const ordered = [...new Set(prefixes)].filter(Boolean).sort((a, b) => b.length - a.length);
|
|
227
|
+
const out = [];
|
|
228
|
+
const keys = /* @__PURE__ */ new Set();
|
|
229
|
+
const add = (rule, key) => {
|
|
230
|
+
if (keys.has(key)) return;
|
|
231
|
+
keys.add(key);
|
|
232
|
+
const [, body, ...rest] = rule;
|
|
233
|
+
out.push([key, { ...body, constKey: key }, ...rest]);
|
|
234
|
+
};
|
|
235
|
+
for (const rule of rules) {
|
|
236
|
+
const key = String(rule[1].constKey);
|
|
237
|
+
add(rule, key);
|
|
238
|
+
for (const prefix of ordered) {
|
|
239
|
+
const match = new RegExp(`^(|.*-)${prefix}([0-9a-z]+)$`).exec(key);
|
|
240
|
+
if (match == null) continue;
|
|
241
|
+
for (const other of ordered) {
|
|
242
|
+
add(rule, `${match[1]}${other}${match[2]}`);
|
|
243
|
+
}
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return out;
|
|
248
|
+
}
|
|
249
|
+
function isConstantRule(rule) {
|
|
250
|
+
const body = Array.isArray(rule) ? rule[1] : void 0;
|
|
251
|
+
return body?.constKey != null && body?.constVal != null;
|
|
252
|
+
}
|
|
191
253
|
function buildLayerSplitPlugin(basePlugin, options) {
|
|
192
254
|
const stylex2 = basePlugin;
|
|
193
255
|
let base = "/";
|
|
@@ -198,7 +260,7 @@ function buildLayerSplitPlugin(basePlugin, options) {
|
|
|
198
260
|
configResolved(config) {
|
|
199
261
|
base = config.base ?? "/";
|
|
200
262
|
},
|
|
201
|
-
writeBundle(outputOptions) {
|
|
263
|
+
writeBundle(outputOptions, bundle) {
|
|
202
264
|
const rulesById = stylex2.__stylexGetSharedStore?.().rulesById;
|
|
203
265
|
if (!rulesById || rulesById.size === 0) return;
|
|
204
266
|
const merged = stylex2.__stylexCollectCss?.();
|
|
@@ -207,6 +269,24 @@ function buildLayerSplitPlugin(basePlugin, options) {
|
|
|
207
269
|
renderSplitLayers(rulesById, options),
|
|
208
270
|
options.lightningcssOptions
|
|
209
271
|
);
|
|
272
|
+
const emittedCss = Object.values(bundle).filter(
|
|
273
|
+
(output) => output.type === "asset" && output.fileName.endsWith(".css")
|
|
274
|
+
).map(
|
|
275
|
+
(asset) => typeof asset.source === "string" ? asset.source : Buffer.from(asset.source).toString("utf8")
|
|
276
|
+
);
|
|
277
|
+
const undeclared = undefinedGeneratedVariables(
|
|
278
|
+
split,
|
|
279
|
+
options.classNamePrefixes,
|
|
280
|
+
emittedCss
|
|
281
|
+
);
|
|
282
|
+
if (undeclared.length > 0) {
|
|
283
|
+
this.warn(
|
|
284
|
+
`astryx-build-layer-split: the StyleX stylesheet reads ${undeclared.length} generated custom propert${undeclared.length === 1 ? "y" : "ies"} that no stylesheet in the build declares:
|
|
285
|
+
` + undeclared.map(
|
|
286
|
+
(name) => ` ${name}, read by: ${rulesReading(split, name).join(" ") || "(rule not found)"}`
|
|
287
|
+
).join("\n") + "\nWith no declaration, browsers drop the declarations that read it. A `.stylex.ts` export read in a style must be a `defineVars`/`defineConsts` value whose module reaches the build; a property the app sets at runtime (an inline style) needs a name outside the StyleX class-name prefixes."
|
|
288
|
+
);
|
|
289
|
+
}
|
|
210
290
|
const outDir = outputOptions.dir ? outputOptions.dir : outputOptions.file ? path.dirname(outputOptions.file) : null;
|
|
211
291
|
if (!outDir || !fs.existsSync(outDir)) return;
|
|
212
292
|
const containingFiles = [];
|
|
@@ -247,6 +327,9 @@ function buildLayerSplitPlugin(basePlugin, options) {
|
|
|
247
327
|
}
|
|
248
328
|
};
|
|
249
329
|
}
|
|
330
|
+
function rulesReading(css, name) {
|
|
331
|
+
return [...css.matchAll(/[^{}]+\{[^{}]*\}/g)].map((match) => match[0].trim().replace(/\s+/g, " ")).filter((rule) => rule.includes(`var(${name}`)).slice(0, 3);
|
|
332
|
+
}
|
|
250
333
|
function listCssFiles(dir) {
|
|
251
334
|
const out = [];
|
|
252
335
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
@@ -310,12 +393,24 @@ function astryxStylexLegacy(options) {
|
|
|
310
393
|
const legacyLightningcssOptions = stylexOptions.lightningcssOptions;
|
|
311
394
|
const validatedStylexOptions = {
|
|
312
395
|
propertyValidationMode: "throw",
|
|
396
|
+
// The unplugin applies its Vite default (true) only to its own StyleX
|
|
397
|
+
// instance, which runs after the babel wrapper below has already compiled
|
|
398
|
+
// every StyleX call. Without it here, a file that reads only `defineConsts`
|
|
399
|
+
// from a `.stylex.ts` module loses that import once `stylex.create` is
|
|
400
|
+
// compiled (the TypeScript transform drops it as unused), the module is
|
|
401
|
+
// never loaded, and its constants never reach the CSS: a media-query key
|
|
402
|
+
// fails the build and a value ships as an undefined custom property.
|
|
403
|
+
treeshakeCompensation: true,
|
|
313
404
|
...stylexOptions,
|
|
314
405
|
lightningcssOptions: {
|
|
315
406
|
targets: LIGHTNINGCSS_TARGETS,
|
|
316
407
|
...legacyLightningcssOptions
|
|
317
408
|
}
|
|
318
409
|
};
|
|
410
|
+
const classNamePrefixes = [
|
|
411
|
+
stylexPrefix,
|
|
412
|
+
validatedStylexOptions.classNamePrefix ?? "x"
|
|
413
|
+
];
|
|
319
414
|
const astryxBabelPlugin = path.resolve(__dirname, "../src/babel.js");
|
|
320
415
|
const existingPlugins = validatedStylexOptions.babelConfig?.plugins ?? [];
|
|
321
416
|
const basePlugin = stylex.vite({
|
|
@@ -373,7 +468,8 @@ function astryxStylexLegacy(options) {
|
|
|
373
468
|
renderSplitLayers(rulesById, {
|
|
374
469
|
libraryPattern,
|
|
375
470
|
libraryLayer,
|
|
376
|
-
productLayer
|
|
471
|
+
productLayer,
|
|
472
|
+
classNamePrefixes
|
|
377
473
|
})
|
|
378
474
|
);
|
|
379
475
|
}
|
|
@@ -389,7 +485,8 @@ function astryxStylexLegacy(options) {
|
|
|
389
485
|
libraryPattern,
|
|
390
486
|
libraryLayer,
|
|
391
487
|
productLayer,
|
|
392
|
-
lightningcssOptions: validatedStylexOptions.lightningcssOptions
|
|
488
|
+
lightningcssOptions: validatedStylexOptions.lightningcssOptions,
|
|
489
|
+
classNamePrefixes
|
|
393
490
|
})
|
|
394
491
|
];
|
|
395
492
|
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file undefinedVariables.ts
|
|
5
|
+
* @input A stylesheet's text, the StyleX class-name prefixes, and the other
|
|
6
|
+
* stylesheets the build emits
|
|
7
|
+
* @output undefinedGeneratedVariables, the generated custom properties the
|
|
8
|
+
* stylesheet reads but never declares
|
|
9
|
+
* @position Internal to @astryxdesign/build: astryxStylex (vite.ts) runs it
|
|
10
|
+
* on a production stylesheet and warns about what it finds. Not exported
|
|
11
|
+
* from any package entry.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The StyleX-generated custom properties `css` reads that it never declares.
|
|
16
|
+
*
|
|
17
|
+
* StyleX names the variables it generates `--<prefix><hash>` (a theme
|
|
18
|
+
* variable, or a constant waiting for its value). One that is read but never
|
|
19
|
+
* declared is a value that compiled to a variable nothing sets, so browsers
|
|
20
|
+
* drop the declaration or take its fallback: a constant whose defining module
|
|
21
|
+
* never reached the build, or a plain export read from a `.stylex.ts` file as
|
|
22
|
+
* if it were a token. The dynamic-style variables StyleX sets inline
|
|
23
|
+
* (`--<prefix>-<property>`) and component variables with a dash after the
|
|
24
|
+
* prefix are not generated names and are never reported.
|
|
25
|
+
*
|
|
26
|
+
* Only a hash-shaped name counts as generated: the prefix and four to seven
|
|
27
|
+
* base-36 characters (StyleX's hash is a 32-bit value in base 36, shorter than
|
|
28
|
+
* four characters about once in a hundred thousand). An app's own short names
|
|
29
|
+
* (`--xl`) are never reported, and a property declared in any stylesheet the
|
|
30
|
+
* build emits (`others`, the app's own CSS) is declared.
|
|
31
|
+
*
|
|
32
|
+
* SYNC: scripts/build-css.mjs `undefinedStyleXVariables` is the same check
|
|
33
|
+
* for the prebuilt package stylesheets.
|
|
34
|
+
*/
|
|
35
|
+
export function undefinedGeneratedVariables(
|
|
36
|
+
css: string,
|
|
37
|
+
prefixes: readonly string[],
|
|
38
|
+
others: readonly string[] = [],
|
|
39
|
+
): string[] {
|
|
40
|
+
const names = prefixes
|
|
41
|
+
.map(prefix => prefix.replace(/[^a-zA-Z0-9_-]/g, ''))
|
|
42
|
+
.filter(Boolean)
|
|
43
|
+
.join('|');
|
|
44
|
+
const declared = new Set(
|
|
45
|
+
[css, ...others].flatMap(text =>
|
|
46
|
+
[...text.matchAll(/(--[A-Za-z0-9_-]+)\s*:/g)].map(match => match[1]),
|
|
47
|
+
),
|
|
48
|
+
);
|
|
49
|
+
const read = new Set(
|
|
50
|
+
[
|
|
51
|
+
...css.matchAll(
|
|
52
|
+
new RegExp(`var\\(\\s*(--(?:${names})[a-z0-9]{4,7})\\s*[,)]`, 'g'),
|
|
53
|
+
),
|
|
54
|
+
].map(match => match[1]),
|
|
55
|
+
);
|
|
56
|
+
return [...read].filter(name => !declared.has(name)).sort();
|
|
57
|
+
}
|
package/src/vite.build.test.ts
CHANGED
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
*/
|
|
32
32
|
|
|
33
33
|
import {describe, it, expect, beforeAll, afterAll} from 'vitest';
|
|
34
|
-
import {build} from 'vite';
|
|
34
|
+
import {build, createLogger} from 'vite';
|
|
35
35
|
import react from '@vitejs/plugin-react';
|
|
36
36
|
import {
|
|
37
37
|
mkdirSync,
|
|
@@ -70,6 +70,21 @@ type Built = {
|
|
|
70
70
|
async function buildFixture(
|
|
71
71
|
name: string,
|
|
72
72
|
htmlNames: string[] = ['index.html'],
|
|
73
|
+
{
|
|
74
|
+
libraryPattern = 'packages/core/',
|
|
75
|
+
zeroConfig = false,
|
|
76
|
+
stylexOverrides,
|
|
77
|
+
warnings = [],
|
|
78
|
+
}: {
|
|
79
|
+
/** Which source paths land in the library layer. */
|
|
80
|
+
libraryPattern?: string;
|
|
81
|
+
/** Use `astryxStylex({...})` instead of the legacy `{stylexOptions}` form. */
|
|
82
|
+
zeroConfig?: boolean;
|
|
83
|
+
/** Zero-config form only: `stylexOverrides`. */
|
|
84
|
+
stylexOverrides?: Record<string, unknown>;
|
|
85
|
+
/** Receives every warning the build logs. */
|
|
86
|
+
warnings?: string[];
|
|
87
|
+
} = {},
|
|
73
88
|
): Promise<Built> {
|
|
74
89
|
const root = path.join(FIXTURES, name);
|
|
75
90
|
const outDir = mkdtempSync(path.join(tmpdir(), `astryx-${name}-`));
|
|
@@ -77,6 +92,11 @@ async function buildFixture(
|
|
|
77
92
|
await build({
|
|
78
93
|
root,
|
|
79
94
|
logLevel: 'error',
|
|
95
|
+
customLogger: {
|
|
96
|
+
...createLogger('error'),
|
|
97
|
+
warn: (message: string) => void warnings.push(message),
|
|
98
|
+
warnOnce: (message: string) => void warnings.push(message),
|
|
99
|
+
},
|
|
80
100
|
build: {
|
|
81
101
|
outDir,
|
|
82
102
|
emptyOutDir: true,
|
|
@@ -94,17 +114,27 @@ async function buildFixture(
|
|
|
94
114
|
resolve: {alias: {'@astryxdesign/core': CORE_SRC}},
|
|
95
115
|
plugins: [
|
|
96
116
|
react(),
|
|
97
|
-
...
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
117
|
+
...(zeroConfig
|
|
118
|
+
? astryxStylex({
|
|
119
|
+
dev: false,
|
|
120
|
+
rootDir: REPO_ROOT,
|
|
121
|
+
libraryPattern,
|
|
122
|
+
stylexOverrides,
|
|
123
|
+
})
|
|
124
|
+
: astryxStylex({
|
|
125
|
+
stylexOptions: {
|
|
126
|
+
dev: false,
|
|
127
|
+
unstable_moduleResolution: {
|
|
128
|
+
type: 'commonJS',
|
|
129
|
+
rootDir: REPO_ROOT,
|
|
130
|
+
},
|
|
131
|
+
aliases: {
|
|
132
|
+
'@astryxdesign/core/*': [path.join(CORE_SRC, '*')],
|
|
133
|
+
'@astryxdesign/core': [CORE_SRC],
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
libraryPattern,
|
|
137
|
+
})),
|
|
108
138
|
],
|
|
109
139
|
});
|
|
110
140
|
} catch (error) {
|
|
@@ -338,6 +368,105 @@ describe('a production build separates Astryx and product styles by layer', () =
|
|
|
338
368
|
});
|
|
339
369
|
});
|
|
340
370
|
|
|
371
|
+
// `defineConsts` values are compiled into the reading file as placeholders,
|
|
372
|
+
// `var(--<prefix><hash>)`, and the defining file contributes the values; the
|
|
373
|
+
// final CSS pass substitutes one for the other. Both halves must reach that
|
|
374
|
+
// pass. Storybook builds through the legacy `{stylexOptions}` form, whose
|
|
375
|
+
// StyleX instance ran without `treeshakeCompensation`: once `stylex.create`
|
|
376
|
+
// was compiled, nothing used the import at runtime, the TypeScript transform
|
|
377
|
+
// dropped it, and the defining module was never loaded. A media-query key was
|
|
378
|
+
// left as `var(--x…){…}` and failed the build ("Invalid empty selector"); a
|
|
379
|
+
// value shipped as a custom property nothing defines. And a constant defined
|
|
380
|
+
// on one side of the layer split and read on the other was substituted only
|
|
381
|
+
// within its own side, and one an Astryx package defines (compiled with the
|
|
382
|
+
// library class-name prefix) never matched an app file's placeholder (written
|
|
383
|
+
// with the product prefix).
|
|
384
|
+
describe('a production build resolves defineConsts imported across files', () => {
|
|
385
|
+
/** A constant reference StyleX left unsubstituted. */
|
|
386
|
+
const PLACEHOLDER = /var\(--(?:astryx|x)[a-z0-9]{4,}\)/;
|
|
387
|
+
|
|
388
|
+
for (const zeroConfig of [false, true]) {
|
|
389
|
+
describe(
|
|
390
|
+
zeroConfig ? 'zero-config form' : 'legacy stylexOptions form (Storybook)',
|
|
391
|
+
() => {
|
|
392
|
+
let built: Built;
|
|
393
|
+
beforeAll(async () => {
|
|
394
|
+
built = await buildFixture('stylex-consts', ['index.html'], {
|
|
395
|
+
libraryPattern: '__fixtures__/stylex-consts/library/',
|
|
396
|
+
zeroConfig,
|
|
397
|
+
});
|
|
398
|
+
}, 180_000);
|
|
399
|
+
afterAll(
|
|
400
|
+
() => built && rmSync(built.outDir, {recursive: true, force: true}),
|
|
401
|
+
);
|
|
402
|
+
|
|
403
|
+
it('substitutes a constant read as a value', () => {
|
|
404
|
+
expect(built.css).toMatch(/min-height:\s*52px/);
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
it('substitutes a constant read as a media-query key', () => {
|
|
408
|
+
expect(built.css).toMatch(
|
|
409
|
+
/@media \((?:max-width: ?599px|width ?<= ?599px)\)\s*\{[^}]*transition-property:\s*none/,
|
|
410
|
+
);
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
it('substitutes a library-side constant read from the product side', () => {
|
|
414
|
+
expect(built.css).toMatch(/padding-top:\s*13px/);
|
|
415
|
+
expect(built.css).toMatch(
|
|
416
|
+
/@media \((?:min-width: ?900px|width ?>= ?900px)\)\s*\{[^}]*max-width:\s*640px/,
|
|
417
|
+
);
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
it('substitutes a constant an Astryx package defines under the library prefix', () => {
|
|
421
|
+
expect(built.css).toMatch(/padding-bottom:\s*17px/);
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
it('leaves no constant placeholder behind', () => {
|
|
425
|
+
expect(built.css).not.toMatch(PLACEHOLDER);
|
|
426
|
+
});
|
|
427
|
+
},
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
describe('a production build checks the custom properties its stylesheet reads', () => {
|
|
433
|
+
const MESSAGE =
|
|
434
|
+
/reads 1 generated custom property that no stylesheet in the build declares[\s\S]*--x[a-z0-9]+, read by: [^\n]*row-gap/;
|
|
435
|
+
|
|
436
|
+
it('warns, naming the variable and the rule that reads it', async () => {
|
|
437
|
+
const warnings: string[] = [];
|
|
438
|
+
const built = await buildFixture('stylex-undefined-var', ['index.html'], {
|
|
439
|
+
warnings,
|
|
440
|
+
});
|
|
441
|
+
rmSync(built.outDir, {recursive: true, force: true});
|
|
442
|
+
expect(warnings.join('\n')).toMatch(MESSAGE);
|
|
443
|
+
}, 180_000);
|
|
444
|
+
|
|
445
|
+
it('checks the product prefix a zero-config build sets in stylexOverrides', async () => {
|
|
446
|
+
const warnings: string[] = [];
|
|
447
|
+
const built = await buildFixture('stylex-undefined-var', ['index.html'], {
|
|
448
|
+
zeroConfig: true,
|
|
449
|
+
stylexOverrides: {classNamePrefix: 'app'},
|
|
450
|
+
warnings,
|
|
451
|
+
});
|
|
452
|
+
rmSync(built.outDir, {recursive: true, force: true});
|
|
453
|
+
expect(warnings.join('\n')).toMatch(
|
|
454
|
+
/reads 1 generated custom property that no stylesheet in the build declares[\s\S]*--app[a-z0-9]+, read by: [^\n]*row-gap/,
|
|
455
|
+
);
|
|
456
|
+
}, 180_000);
|
|
457
|
+
|
|
458
|
+
it("never reports an app's own custom properties, declared in its own CSS", async () => {
|
|
459
|
+
const warnings: string[] = [];
|
|
460
|
+
const built = await buildFixture('stylex-app-vars', ['index.html'], {
|
|
461
|
+
warnings,
|
|
462
|
+
});
|
|
463
|
+
rmSync(built.outDir, {recursive: true, force: true});
|
|
464
|
+
expect(warnings.join('\n')).not.toMatch(
|
|
465
|
+
/no stylesheet in the build declares/,
|
|
466
|
+
);
|
|
467
|
+
}, 180_000);
|
|
468
|
+
});
|
|
469
|
+
|
|
341
470
|
describe('a production build validates StyleX declarations', () => {
|
|
342
471
|
it('fails with longhand guidance instead of dropping a border shorthand', async () => {
|
|
343
472
|
await expect(buildFixture('invalid-border')).rejects.toThrow(
|
package/src/vite.test.ts
CHANGED
|
@@ -14,6 +14,7 @@ import path from 'node:path';
|
|
|
14
14
|
import {fileURLToPath, pathToFileURL} from 'node:url';
|
|
15
15
|
import stylex from '@stylexjs/unplugin';
|
|
16
16
|
import {astryxStylex, LIGHTNINGCSS_TARGETS} from './vite';
|
|
17
|
+
import {undefinedGeneratedVariables} from './undefinedVariables';
|
|
17
18
|
|
|
18
19
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
19
20
|
|
|
@@ -272,3 +273,57 @@ describe('astryxStylex optimizeDeps package discovery', () => {
|
|
|
272
273
|
}
|
|
273
274
|
});
|
|
274
275
|
});
|
|
276
|
+
|
|
277
|
+
describe('undefinedGeneratedVariables', () => {
|
|
278
|
+
it('reports a generated variable that is read but never declared', () => {
|
|
279
|
+
expect(
|
|
280
|
+
undefinedGeneratedVariables(
|
|
281
|
+
'.a{min-height:var(--astryx1abc2)}.b{color:var(--x9zz9z, red)}',
|
|
282
|
+
['astryx', 'x'],
|
|
283
|
+
),
|
|
284
|
+
).toEqual(['--astryx1abc2', '--x9zz9z']);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it('accepts a variable declared anywhere in the stylesheet', () => {
|
|
288
|
+
expect(
|
|
289
|
+
undefinedGeneratedVariables(
|
|
290
|
+
':root{--astryx1abc2:4px}.a{padding:var(--astryx1abc2)}',
|
|
291
|
+
['astryx', 'x'],
|
|
292
|
+
),
|
|
293
|
+
).toEqual([]);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it('ignores dynamic-style and component variables, which carry a dash', () => {
|
|
297
|
+
expect(
|
|
298
|
+
undefinedGeneratedVariables(
|
|
299
|
+
'.a{width:var(--x-width)}.b{padding:var(--astryx-card-padding,8px)}',
|
|
300
|
+
['astryx', 'x'],
|
|
301
|
+
),
|
|
302
|
+
).toEqual([]);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it('counts a declaration in any other stylesheet the build emits', () => {
|
|
306
|
+
expect(
|
|
307
|
+
undefinedGeneratedVariables(
|
|
308
|
+
'.a{padding:var(--xlarge)}',
|
|
309
|
+
['astryx', 'x'],
|
|
310
|
+
[':root{--xlarge:24px}'],
|
|
311
|
+
),
|
|
312
|
+
).toEqual([]);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it('reports only hash-shaped names: four to seven characters after the prefix', () => {
|
|
316
|
+
expect(
|
|
317
|
+
undefinedGeneratedVariables(
|
|
318
|
+
'.a{margin:var(--xl)}.b{gap:var(--xs1)}.c{inset:var(--x1abc2de)}.d{top:var(--x1abc2def)}',
|
|
319
|
+
['astryx', 'x'],
|
|
320
|
+
),
|
|
321
|
+
).toEqual(['--x1abc2de']);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it('checks only the prefixes it is given', () => {
|
|
325
|
+
expect(
|
|
326
|
+
undefinedGeneratedVariables('.a{color:var(--x1abc2)}', ['astryx']),
|
|
327
|
+
).toEqual([]);
|
|
328
|
+
});
|
|
329
|
+
});
|
package/src/vite.ts
CHANGED
|
@@ -8,6 +8,7 @@ import path from 'node:path';
|
|
|
8
8
|
import {fileURLToPath} from 'node:url';
|
|
9
9
|
import {createRequire} from 'node:module';
|
|
10
10
|
import {createHash} from 'node:crypto';
|
|
11
|
+
import {undefinedGeneratedVariables} from './undefinedVariables';
|
|
11
12
|
|
|
12
13
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
13
14
|
|
|
@@ -136,6 +137,12 @@ export function astryxStylex(
|
|
|
136
137
|
|
|
137
138
|
const libraryLayer = layers.library ?? 'astryx-base';
|
|
138
139
|
const productLayer = layers.product ?? 'product';
|
|
140
|
+
// Library files compile with `stylexPrefix`, product files with StyleX's
|
|
141
|
+
// `classNamePrefix` (the babel wrapper routes them).
|
|
142
|
+
const classNamePrefixes = [
|
|
143
|
+
stylexPrefix,
|
|
144
|
+
(stylexOverrides as {classNamePrefix?: string}).classNamePrefix ?? 'x',
|
|
145
|
+
];
|
|
139
146
|
|
|
140
147
|
const overrideLightningcssOptions = (
|
|
141
148
|
stylexOverrides as {lightningcssOptions?: Record<string, unknown>}
|
|
@@ -284,6 +291,7 @@ export function astryxStylex(
|
|
|
284
291
|
libraryPattern,
|
|
285
292
|
libraryLayer,
|
|
286
293
|
productLayer,
|
|
294
|
+
classNamePrefixes,
|
|
287
295
|
}),
|
|
288
296
|
);
|
|
289
297
|
},
|
|
@@ -302,6 +310,7 @@ export function astryxStylex(
|
|
|
302
310
|
libraryLayer,
|
|
303
311
|
productLayer,
|
|
304
312
|
lightningcssOptions: stylexOptions.lightningcssOptions,
|
|
313
|
+
classNamePrefixes,
|
|
305
314
|
}),
|
|
306
315
|
];
|
|
307
316
|
}
|
|
@@ -330,24 +339,43 @@ function renderSplitLayers(
|
|
|
330
339
|
libraryPattern: string;
|
|
331
340
|
libraryLayer: string;
|
|
332
341
|
productLayer: string;
|
|
342
|
+
/** The StyleX class-name prefixes in play: library, then product. */
|
|
343
|
+
classNamePrefixes: readonly string[];
|
|
333
344
|
},
|
|
334
345
|
): string {
|
|
335
346
|
if (!rulesById || rulesById.size === 0) return '';
|
|
336
347
|
|
|
337
348
|
const libraryRules: unknown[] = [];
|
|
338
349
|
const productRules: unknown[] = [];
|
|
350
|
+
// `defineConsts` values. A file that reads a constant compiles it to a
|
|
351
|
+
// placeholder, `var(--<prefix><hash>)`, and `processStylexRules` substitutes
|
|
352
|
+
// the value from the defining file's rules, but only among the rules it is
|
|
353
|
+
// given. The defining file and the reading one can sit on opposite sides of
|
|
354
|
+
// the split, so every layer gets every constant, under every class-name
|
|
355
|
+
// prefix (see constantRulesForEveryPrefix). Constant rules emit no CSS of
|
|
356
|
+
// their own, so repeating them adds nothing to the output.
|
|
357
|
+
const collectedConstants: unknown[] = [];
|
|
339
358
|
|
|
340
359
|
for (const [filePath, rules] of rulesById.entries()) {
|
|
341
|
-
|
|
342
|
-
libraryRules
|
|
343
|
-
|
|
344
|
-
|
|
360
|
+
const own = filePath.includes(options.libraryPattern)
|
|
361
|
+
? libraryRules
|
|
362
|
+
: productRules;
|
|
363
|
+
for (const rule of rules) {
|
|
364
|
+
if (isConstantRule(rule)) {
|
|
365
|
+
collectedConstants.push(rule);
|
|
366
|
+
} else {
|
|
367
|
+
own.push(rule);
|
|
368
|
+
}
|
|
345
369
|
}
|
|
346
370
|
}
|
|
347
371
|
|
|
372
|
+
const constantRules = constantRulesForEveryPrefix(
|
|
373
|
+
collectedConstants,
|
|
374
|
+
options.classNamePrefixes,
|
|
375
|
+
);
|
|
348
376
|
const render = (rules: unknown[], layer: string) =>
|
|
349
377
|
rules.length
|
|
350
|
-
? `@layer ${layer} {\n${stylexBabelPlugin.processStylexRules(rules as never, {useLayers: true})}\n}`
|
|
378
|
+
? `@layer ${layer} {\n${stylexBabelPlugin.processStylexRules([...constantRules, ...rules] as never, {useLayers: true})}\n}`
|
|
351
379
|
: '';
|
|
352
380
|
|
|
353
381
|
return [
|
|
@@ -358,6 +386,56 @@ function renderSplitLayers(
|
|
|
358
386
|
.join('\n\n');
|
|
359
387
|
}
|
|
360
388
|
|
|
389
|
+
/**
|
|
390
|
+
* Each `defineConsts` rule once under every class-name prefix in play.
|
|
391
|
+
*
|
|
392
|
+
* StyleX keys a constant by its defining file's prefix, `<prefix><hash>` (or
|
|
393
|
+
* `<name>-<prefix><hash>` with debug class names), where the hash depends on
|
|
394
|
+
* the export and key alone, and a file that reads it writes the placeholder
|
|
395
|
+
* with its own prefix. An Astryx package compiles with the library prefix and
|
|
396
|
+
* an app with the product one, so an app reading a package's constant writes
|
|
397
|
+
* `var(--x<hash>)` while the package's rule is keyed `astryx<hash>`; the
|
|
398
|
+
* copies make every prefix's placeholder resolve. A key StyleX took verbatim
|
|
399
|
+
* (`'--name'`) carries no prefix and is kept as it is.
|
|
400
|
+
*/
|
|
401
|
+
function constantRulesForEveryPrefix(
|
|
402
|
+
rules: readonly unknown[],
|
|
403
|
+
prefixes: readonly string[],
|
|
404
|
+
): unknown[] {
|
|
405
|
+
const ordered = [...new Set(prefixes)]
|
|
406
|
+
.filter(Boolean)
|
|
407
|
+
.sort((a, b) => b.length - a.length);
|
|
408
|
+
const out: unknown[] = [];
|
|
409
|
+
const keys = new Set<string>();
|
|
410
|
+
const add = (rule: unknown[], key: string) => {
|
|
411
|
+
if (keys.has(key)) return;
|
|
412
|
+
keys.add(key);
|
|
413
|
+
const [, body, ...rest] = rule;
|
|
414
|
+
out.push([key, {...(body as object), constKey: key}, ...rest]);
|
|
415
|
+
};
|
|
416
|
+
for (const rule of rules as unknown[][]) {
|
|
417
|
+
const key = String((rule[1] as {constKey: unknown}).constKey);
|
|
418
|
+
add(rule, key);
|
|
419
|
+
for (const prefix of ordered) {
|
|
420
|
+
const match = new RegExp(`^(|.*-)${prefix}([0-9a-z]+)$`).exec(key);
|
|
421
|
+
if (match == null) continue;
|
|
422
|
+
for (const other of ordered) {
|
|
423
|
+
add(rule, `${match[1]}${other}${match[2]}`);
|
|
424
|
+
}
|
|
425
|
+
break;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
return out;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/** A `defineConsts` value as StyleX collects it: a rule with `constKey`/`constVal`. */
|
|
432
|
+
function isConstantRule(rule: unknown): boolean {
|
|
433
|
+
const body = Array.isArray(rule)
|
|
434
|
+
? (rule[1] as {constKey?: unknown; constVal?: unknown} | undefined)
|
|
435
|
+
: undefined;
|
|
436
|
+
return body?.constKey != null && body?.constVal != null;
|
|
437
|
+
}
|
|
438
|
+
|
|
361
439
|
/**
|
|
362
440
|
* The build half of the split. StyleX appends one merged block of CSS to the
|
|
363
441
|
* build's stylesheet, in its own top-level `@layer priority1…priorityN` — which
|
|
@@ -387,6 +465,8 @@ function buildLayerSplitPlugin(
|
|
|
387
465
|
libraryLayer: string;
|
|
388
466
|
productLayer: string;
|
|
389
467
|
lightningcssOptions?: unknown;
|
|
468
|
+
/** The StyleX class-name prefixes in play: library, then product. */
|
|
469
|
+
classNamePrefixes: readonly string[];
|
|
390
470
|
},
|
|
391
471
|
): Plugin {
|
|
392
472
|
const stylex = basePlugin as {
|
|
@@ -402,7 +482,7 @@ function buildLayerSplitPlugin(
|
|
|
402
482
|
configResolved(config) {
|
|
403
483
|
base = config.base ?? '/';
|
|
404
484
|
},
|
|
405
|
-
writeBundle(outputOptions) {
|
|
485
|
+
writeBundle(outputOptions, bundle) {
|
|
406
486
|
const rulesById = stylex.__stylexGetSharedStore?.().rulesById;
|
|
407
487
|
if (!rulesById || rulesById.size === 0) return;
|
|
408
488
|
|
|
@@ -414,6 +494,42 @@ function buildLayerSplitPlugin(
|
|
|
414
494
|
options.lightningcssOptions,
|
|
415
495
|
);
|
|
416
496
|
|
|
497
|
+
// An app's own stylesheets declare its own custom properties.
|
|
498
|
+
const emittedCss = Object.values(bundle)
|
|
499
|
+
.filter(
|
|
500
|
+
(output): output is Extract<typeof output, {type: 'asset'}> =>
|
|
501
|
+
output.type === 'asset' && output.fileName.endsWith('.css'),
|
|
502
|
+
)
|
|
503
|
+
.map(asset =>
|
|
504
|
+
typeof asset.source === 'string'
|
|
505
|
+
? asset.source
|
|
506
|
+
: Buffer.from(asset.source).toString('utf8'),
|
|
507
|
+
);
|
|
508
|
+
const undeclared = undefinedGeneratedVariables(
|
|
509
|
+
split,
|
|
510
|
+
options.classNamePrefixes,
|
|
511
|
+
emittedCss,
|
|
512
|
+
);
|
|
513
|
+
if (undeclared.length > 0) {
|
|
514
|
+
this.warn(
|
|
515
|
+
'astryx-build-layer-split: the StyleX stylesheet reads ' +
|
|
516
|
+
`${undeclared.length} generated custom ` +
|
|
517
|
+
`propert${undeclared.length === 1 ? 'y' : 'ies'} ` +
|
|
518
|
+
'that no stylesheet in the build declares:\n' +
|
|
519
|
+
undeclared
|
|
520
|
+
.map(
|
|
521
|
+
name =>
|
|
522
|
+
` ${name}, read by: ${rulesReading(split, name).join(' ') || '(rule not found)'}`,
|
|
523
|
+
)
|
|
524
|
+
.join('\n') +
|
|
525
|
+
'\nWith no declaration, browsers drop the declarations that read ' +
|
|
526
|
+
'it. A `.stylex.ts` export read in a style must be a ' +
|
|
527
|
+
'`defineVars`/`defineConsts` value whose module reaches the build; ' +
|
|
528
|
+
'a property the app sets at runtime (an inline style) needs a name ' +
|
|
529
|
+
'outside the StyleX class-name prefixes.',
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
|
|
417
533
|
const outDir = outputOptions.dir
|
|
418
534
|
? outputOptions.dir
|
|
419
535
|
: outputOptions.file
|
|
@@ -472,6 +588,14 @@ function buildLayerSplitPlugin(
|
|
|
472
588
|
};
|
|
473
589
|
}
|
|
474
590
|
|
|
591
|
+
/** The rules of `css` that read `name`, for the failure message. */
|
|
592
|
+
function rulesReading(css: string, name: string): string[] {
|
|
593
|
+
return [...css.matchAll(/[^{}]+\{[^{}]*\}/g)]
|
|
594
|
+
.map(match => match[0].trim().replace(/\s+/g, ' '))
|
|
595
|
+
.filter(rule => rule.includes(`var(${name}`))
|
|
596
|
+
.slice(0, 3);
|
|
597
|
+
}
|
|
598
|
+
|
|
475
599
|
/** Every `.css` file under a directory, recursively. */
|
|
476
600
|
function listCssFiles(dir: string): string[] {
|
|
477
601
|
const out: string[] = [];
|
|
@@ -586,12 +710,25 @@ function astryxStylexLegacy(options: AstryxVitePluginLegacyOptions): Plugin[] {
|
|
|
586
710
|
).lightningcssOptions;
|
|
587
711
|
const validatedStylexOptions = {
|
|
588
712
|
propertyValidationMode: 'throw',
|
|
713
|
+
// The unplugin applies its Vite default (true) only to its own StyleX
|
|
714
|
+
// instance, which runs after the babel wrapper below has already compiled
|
|
715
|
+
// every StyleX call. Without it here, a file that reads only `defineConsts`
|
|
716
|
+
// from a `.stylex.ts` module loses that import once `stylex.create` is
|
|
717
|
+
// compiled (the TypeScript transform drops it as unused), the module is
|
|
718
|
+
// never loaded, and its constants never reach the CSS: a media-query key
|
|
719
|
+
// fails the build and a value ships as an undefined custom property.
|
|
720
|
+
treeshakeCompensation: true,
|
|
589
721
|
...(stylexOptions as any),
|
|
590
722
|
lightningcssOptions: {
|
|
591
723
|
targets: LIGHTNINGCSS_TARGETS,
|
|
592
724
|
...legacyLightningcssOptions,
|
|
593
725
|
},
|
|
594
726
|
};
|
|
727
|
+
const classNamePrefixes = [
|
|
728
|
+
stylexPrefix,
|
|
729
|
+
(validatedStylexOptions as {classNamePrefix?: string}).classNamePrefix ??
|
|
730
|
+
'x',
|
|
731
|
+
];
|
|
595
732
|
const astryxBabelPlugin = path.resolve(__dirname, 'babel.js');
|
|
596
733
|
const existingPlugins = validatedStylexOptions.babelConfig?.plugins ?? [];
|
|
597
734
|
|
|
@@ -658,6 +795,7 @@ function astryxStylexLegacy(options: AstryxVitePluginLegacyOptions): Plugin[] {
|
|
|
658
795
|
libraryPattern,
|
|
659
796
|
libraryLayer,
|
|
660
797
|
productLayer,
|
|
798
|
+
classNamePrefixes,
|
|
661
799
|
}),
|
|
662
800
|
);
|
|
663
801
|
},
|
|
@@ -677,6 +815,7 @@ function astryxStylexLegacy(options: AstryxVitePluginLegacyOptions): Plugin[] {
|
|
|
677
815
|
lightningcssOptions: (
|
|
678
816
|
validatedStylexOptions as {lightningcssOptions?: unknown}
|
|
679
817
|
).lightningcssOptions,
|
|
818
|
+
classNamePrefixes,
|
|
680
819
|
}),
|
|
681
820
|
];
|
|
682
821
|
}
|