@astryxdesign/build 0.4.7 → 0.5.0-canary.009bcb3
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/vite.mjs +155 -80
- package/package.json +1 -1
- package/src/vite.build.test.ts +179 -0
- package/src/vite.test.ts +47 -1
- package/src/vite.ts +279 -92
package/dist/vite.mjs
CHANGED
|
@@ -6,6 +6,7 @@ import stylex from "@stylexjs/unplugin";
|
|
|
6
6
|
import fs from "node:fs";
|
|
7
7
|
import path from "node:path";
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
|
+
import { createRequire } from "node:module";
|
|
9
10
|
var __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
10
11
|
var LIBRARY_PATTERN = "node_modules/@astryxdesign/";
|
|
11
12
|
var STYLEX_CSS_PATH = "/virtual:stylex.css";
|
|
@@ -119,54 +120,149 @@ function astryxStylex(options = {}) {
|
|
|
119
120
|
if (!req.url?.startsWith(STYLEX_CSS_PATH)) {
|
|
120
121
|
return next();
|
|
121
122
|
}
|
|
122
|
-
|
|
123
|
-
res.statusCode = 200;
|
|
124
|
-
res.setHeader("Content-Type", "text/css");
|
|
125
|
-
res.end("");
|
|
126
|
-
return;
|
|
127
|
-
}
|
|
128
|
-
const shared = stylexPlugin.__stylexGetSharedStore?.();
|
|
129
|
-
const rulesById = shared?.rulesById;
|
|
130
|
-
if (!rulesById || rulesById.size === 0) {
|
|
131
|
-
res.statusCode = 200;
|
|
132
|
-
res.setHeader("Content-Type", "text/css");
|
|
133
|
-
res.end("");
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
|
-
const libraryRules = [];
|
|
137
|
-
const productRules = [];
|
|
138
|
-
for (const [filePath, rules] of rulesById.entries()) {
|
|
139
|
-
if (filePath.includes(libraryPattern)) {
|
|
140
|
-
libraryRules.push(...rules);
|
|
141
|
-
} else {
|
|
142
|
-
productRules.push(...rules);
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
const libraryCss = libraryRules.length ? stylexBabelPlugin.processStylexRules(libraryRules, {
|
|
146
|
-
useLayers: true
|
|
147
|
-
}) : "";
|
|
148
|
-
const productCss = productRules.length ? stylexBabelPlugin.processStylexRules(productRules, {
|
|
149
|
-
useLayers: true
|
|
150
|
-
}) : "";
|
|
151
|
-
const parts = [];
|
|
152
|
-
if (libraryCss)
|
|
153
|
-
parts.push(`@layer ${libraryLayer} {
|
|
154
|
-
${libraryCss}
|
|
155
|
-
}`);
|
|
156
|
-
if (productCss)
|
|
157
|
-
parts.push(`@layer ${productLayer} {
|
|
158
|
-
${productCss}
|
|
159
|
-
}`);
|
|
123
|
+
const rulesById = stylexPlugin?.__stylexGetSharedStore?.()?.rulesById;
|
|
160
124
|
res.statusCode = 200;
|
|
161
125
|
res.setHeader("Content-Type", "text/css");
|
|
162
126
|
res.setHeader("Cache-Control", "no-store");
|
|
163
|
-
res.end(
|
|
127
|
+
res.end(
|
|
128
|
+
renderSplitLayers(rulesById, {
|
|
129
|
+
libraryPattern,
|
|
130
|
+
libraryLayer,
|
|
131
|
+
productLayer
|
|
132
|
+
})
|
|
133
|
+
);
|
|
164
134
|
}
|
|
165
135
|
});
|
|
166
136
|
};
|
|
167
137
|
}
|
|
168
138
|
};
|
|
169
|
-
return [
|
|
139
|
+
return [
|
|
140
|
+
configPlugin,
|
|
141
|
+
layerOrderPlugin,
|
|
142
|
+
basePlugin,
|
|
143
|
+
splitLayerPlugin,
|
|
144
|
+
buildLayerSplitPlugin(basePlugin, {
|
|
145
|
+
libraryPattern,
|
|
146
|
+
libraryLayer,
|
|
147
|
+
productLayer,
|
|
148
|
+
lightningcssOptions: stylexOptions.lightningcssOptions
|
|
149
|
+
})
|
|
150
|
+
];
|
|
151
|
+
}
|
|
152
|
+
function renderSplitLayers(rulesById, options) {
|
|
153
|
+
if (!rulesById || rulesById.size === 0) return "";
|
|
154
|
+
const libraryRules = [];
|
|
155
|
+
const productRules = [];
|
|
156
|
+
for (const [filePath, rules] of rulesById.entries()) {
|
|
157
|
+
if (filePath.includes(options.libraryPattern)) {
|
|
158
|
+
libraryRules.push(...rules);
|
|
159
|
+
} else {
|
|
160
|
+
productRules.push(...rules);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
const render = (rules, layer) => rules.length ? `@layer ${layer} {
|
|
164
|
+
${stylexBabelPlugin.processStylexRules(rules, { useLayers: true })}
|
|
165
|
+
}` : "";
|
|
166
|
+
return [
|
|
167
|
+
render(libraryRules, options.libraryLayer),
|
|
168
|
+
render(productRules, options.productLayer)
|
|
169
|
+
].filter(Boolean).join("\n\n");
|
|
170
|
+
}
|
|
171
|
+
function buildLayerSplitPlugin(basePlugin, options) {
|
|
172
|
+
const stylex2 = basePlugin;
|
|
173
|
+
let base = "/";
|
|
174
|
+
return {
|
|
175
|
+
name: "astryx-build-layer-split",
|
|
176
|
+
apply: "build",
|
|
177
|
+
enforce: "post",
|
|
178
|
+
configResolved(config) {
|
|
179
|
+
base = config.base ?? "/";
|
|
180
|
+
},
|
|
181
|
+
writeBundle(outputOptions) {
|
|
182
|
+
const rulesById = stylex2.__stylexGetSharedStore?.().rulesById;
|
|
183
|
+
if (!rulesById || rulesById.size === 0) return;
|
|
184
|
+
const merged = stylex2.__stylexCollectCss?.();
|
|
185
|
+
if (!merged) return;
|
|
186
|
+
const split = postProcessCss(
|
|
187
|
+
renderSplitLayers(rulesById, options),
|
|
188
|
+
options.lightningcssOptions
|
|
189
|
+
);
|
|
190
|
+
const outDir = outputOptions.dir ? outputOptions.dir : outputOptions.file ? path.dirname(outputOptions.file) : null;
|
|
191
|
+
if (!outDir || !fs.existsSync(outDir)) return;
|
|
192
|
+
const patched = [];
|
|
193
|
+
for (const file of listCssFiles(outDir)) {
|
|
194
|
+
const css = fs.readFileSync(file, "utf-8");
|
|
195
|
+
const at = css.lastIndexOf(merged);
|
|
196
|
+
if (at === -1) continue;
|
|
197
|
+
fs.writeFileSync(
|
|
198
|
+
file,
|
|
199
|
+
css.slice(0, at) + split + css.slice(at + merged.length)
|
|
200
|
+
);
|
|
201
|
+
patched.push(file);
|
|
202
|
+
}
|
|
203
|
+
if (patched.length === 0) {
|
|
204
|
+
this.error(
|
|
205
|
+
`astryx-build-layer-split: StyleX emitted rules but its CSS block was not found in any stylesheet under ${outDir}, so Astryx and product styles could not be separated into their cascade layers. Leaving the build unsplit would let product styles lose to a theme. This usually means the StyleX plugin version changed how it emits CSS.`
|
|
206
|
+
);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
linkOrphanStylesheets(outDir, patched, base);
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
function listCssFiles(dir) {
|
|
214
|
+
const out = [];
|
|
215
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
216
|
+
const full = path.join(dir, entry.name);
|
|
217
|
+
if (entry.isDirectory()) out.push(...listCssFiles(full));
|
|
218
|
+
else if (entry.name.endsWith(".css")) out.push(full);
|
|
219
|
+
}
|
|
220
|
+
return out;
|
|
221
|
+
}
|
|
222
|
+
function listHtmlFiles(dir) {
|
|
223
|
+
const out = [];
|
|
224
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
225
|
+
const full = path.join(dir, entry.name);
|
|
226
|
+
if (entry.isDirectory()) out.push(...listHtmlFiles(full));
|
|
227
|
+
else if (entry.name.endsWith(".html")) out.push(full);
|
|
228
|
+
}
|
|
229
|
+
return out;
|
|
230
|
+
}
|
|
231
|
+
function linkOrphanStylesheets(outDir, cssFiles, base) {
|
|
232
|
+
const pages = listHtmlFiles(outDir);
|
|
233
|
+
if (pages.length === 0) return;
|
|
234
|
+
const orphans = cssFiles.filter((css) => {
|
|
235
|
+
const name = path.basename(css);
|
|
236
|
+
return !pages.some((page) => fs.readFileSync(page, "utf-8").includes(name));
|
|
237
|
+
});
|
|
238
|
+
if (orphans.length === 0) return;
|
|
239
|
+
const links = orphans.map((css) => {
|
|
240
|
+
const href = base.replace(/\/$/, "") + "/" + path.relative(outDir, css).split(path.sep).join("/");
|
|
241
|
+
return `<link rel="stylesheet" crossorigin href="${href}">`;
|
|
242
|
+
}).join("\n ");
|
|
243
|
+
for (const page of pages) {
|
|
244
|
+
const html = fs.readFileSync(page, "utf-8");
|
|
245
|
+
if (!html.includes("</head>")) continue;
|
|
246
|
+
fs.writeFileSync(page, html.replace("</head>", ` ${links}
|
|
247
|
+
</head>`));
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
function postProcessCss(css, lightningcssOptions) {
|
|
251
|
+
if (!css) return css;
|
|
252
|
+
try {
|
|
253
|
+
const require_ = createRequire(import.meta.url);
|
|
254
|
+
const { transform, browserslistToTargets } = require_("lightningcss");
|
|
255
|
+
const browserslist = require_("browserslist");
|
|
256
|
+
const { code } = transform({
|
|
257
|
+
targets: browserslistToTargets(browserslist()),
|
|
258
|
+
...lightningcssOptions,
|
|
259
|
+
filename: "stylex.css",
|
|
260
|
+
code: Buffer.from(css)
|
|
261
|
+
});
|
|
262
|
+
return code.toString();
|
|
263
|
+
} catch {
|
|
264
|
+
return css;
|
|
265
|
+
}
|
|
170
266
|
}
|
|
171
267
|
function astryxStylexLegacy(options) {
|
|
172
268
|
const {
|
|
@@ -226,54 +322,33 @@ function astryxStylexLegacy(options) {
|
|
|
226
322
|
if (!req.url?.startsWith(STYLEX_CSS_PATH)) {
|
|
227
323
|
return next();
|
|
228
324
|
}
|
|
229
|
-
|
|
230
|
-
res.statusCode = 200;
|
|
231
|
-
res.setHeader("Content-Type", "text/css");
|
|
232
|
-
res.end("");
|
|
233
|
-
return;
|
|
234
|
-
}
|
|
235
|
-
const shared = stylexPlugin.__stylexGetSharedStore?.();
|
|
236
|
-
const rulesById = shared?.rulesById;
|
|
237
|
-
if (!rulesById || rulesById.size === 0) {
|
|
238
|
-
res.statusCode = 200;
|
|
239
|
-
res.setHeader("Content-Type", "text/css");
|
|
240
|
-
res.end("");
|
|
241
|
-
return;
|
|
242
|
-
}
|
|
243
|
-
const libraryRules = [];
|
|
244
|
-
const productRules = [];
|
|
245
|
-
for (const [filePath, rules] of rulesById.entries()) {
|
|
246
|
-
if (filePath.includes(libraryPattern)) {
|
|
247
|
-
libraryRules.push(...rules);
|
|
248
|
-
} else {
|
|
249
|
-
productRules.push(...rules);
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
const libraryCss = libraryRules.length ? stylexBabelPlugin.processStylexRules(libraryRules, {
|
|
253
|
-
useLayers: true
|
|
254
|
-
}) : "";
|
|
255
|
-
const productCss = productRules.length ? stylexBabelPlugin.processStylexRules(productRules, {
|
|
256
|
-
useLayers: true
|
|
257
|
-
}) : "";
|
|
258
|
-
const parts = [];
|
|
259
|
-
if (libraryCss)
|
|
260
|
-
parts.push(`@layer ${libraryLayer} {
|
|
261
|
-
${libraryCss}
|
|
262
|
-
}`);
|
|
263
|
-
if (productCss)
|
|
264
|
-
parts.push(`@layer ${productLayer} {
|
|
265
|
-
${productCss}
|
|
266
|
-
}`);
|
|
325
|
+
const rulesById = stylexPlugin?.__stylexGetSharedStore?.()?.rulesById;
|
|
267
326
|
res.statusCode = 200;
|
|
268
327
|
res.setHeader("Content-Type", "text/css");
|
|
269
328
|
res.setHeader("Cache-Control", "no-store");
|
|
270
|
-
res.end(
|
|
329
|
+
res.end(
|
|
330
|
+
renderSplitLayers(rulesById, {
|
|
331
|
+
libraryPattern,
|
|
332
|
+
libraryLayer,
|
|
333
|
+
productLayer
|
|
334
|
+
})
|
|
335
|
+
);
|
|
271
336
|
}
|
|
272
337
|
});
|
|
273
338
|
};
|
|
274
339
|
}
|
|
275
340
|
};
|
|
276
|
-
return [
|
|
341
|
+
return [
|
|
342
|
+
layerOrderPlugin,
|
|
343
|
+
basePlugin,
|
|
344
|
+
splitLayerPlugin,
|
|
345
|
+
buildLayerSplitPlugin(basePlugin, {
|
|
346
|
+
libraryPattern,
|
|
347
|
+
libraryLayer,
|
|
348
|
+
productLayer,
|
|
349
|
+
lightningcssOptions: stylexOptions?.lightningcssOptions
|
|
350
|
+
})
|
|
351
|
+
];
|
|
277
352
|
}
|
|
278
353
|
export {
|
|
279
354
|
LIGHTNINGCSS_TARGETS,
|
package/package.json
CHANGED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file Runs REAL Vite production builds of fixture apps and asserts on the
|
|
5
|
+
* stylesheets they emit.
|
|
6
|
+
*
|
|
7
|
+
* The unit tests in vite.test.ts check the plugin's wiring, and they passed
|
|
8
|
+
* while the build shipped a version that wrapped every StyleX rule — Astryx's
|
|
9
|
+
* and the app's alike — in the library layer. That inverts the bug rather than
|
|
10
|
+
* fixing it: product styles are supposed to outrank a theme, and wrapped in
|
|
11
|
+
* `astryx-base` they lose to one. Only the built artifact shows it, so this
|
|
12
|
+
* builds one.
|
|
13
|
+
*
|
|
14
|
+
* Two shapes, because StyleX emits through two different paths:
|
|
15
|
+
*
|
|
16
|
+
* `layer-split` the app imports a stylesheet, so the bundle has a CSS
|
|
17
|
+
* asset and StyleX appends to it in `generateBundle`
|
|
18
|
+
* `layer-split-nocss` the app imports none, so StyleX writes its own file in
|
|
19
|
+
* `writeBundle`, outside Rollup's graph
|
|
20
|
+
*
|
|
21
|
+
* The second is the one that was missing, and it hid two defects: the split did
|
|
22
|
+
* not run there at all, and the file StyleX writes is not linked by any page.
|
|
23
|
+
*
|
|
24
|
+
* What none of this can prove is which declaration actually paints — that is a
|
|
25
|
+
* cascade fact, and it lives in .github/scripts/theme-layer-cascade.js.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import {describe, it, expect, beforeAll, afterAll} from 'vitest';
|
|
29
|
+
import {build} from 'vite';
|
|
30
|
+
import react from '@vitejs/plugin-react';
|
|
31
|
+
import {mkdtempSync, readdirSync, readFileSync, rmSync} from 'node:fs';
|
|
32
|
+
import {tmpdir} from 'node:os';
|
|
33
|
+
import * as path from 'node:path';
|
|
34
|
+
import {fileURLToPath} from 'node:url';
|
|
35
|
+
import {astryxStylex} from './vite';
|
|
36
|
+
|
|
37
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
38
|
+
const REPO_ROOT = path.resolve(__dirname, '../../..');
|
|
39
|
+
const FIXTURES = path.resolve(__dirname, '../__fixtures__');
|
|
40
|
+
const CORE_SRC = path.join(REPO_ROOT, 'packages/core/src');
|
|
41
|
+
|
|
42
|
+
/** The class prefixes @astryxdesign/build/babel assigns per origin. */
|
|
43
|
+
const LIBRARY_CLASS = /\.astryx[a-z0-9]{5,}/g;
|
|
44
|
+
const PRODUCT_CLASS = /\.x[a-z0-9]{5,}/g;
|
|
45
|
+
|
|
46
|
+
type Built = {css: string; cssName: string; html: string; outDir: string};
|
|
47
|
+
|
|
48
|
+
async function buildFixture(name: string): Promise<Built> {
|
|
49
|
+
const root = path.join(FIXTURES, name);
|
|
50
|
+
const outDir = mkdtempSync(path.join(tmpdir(), `astryx-${name}-`));
|
|
51
|
+
await build({
|
|
52
|
+
root,
|
|
53
|
+
logLevel: 'error',
|
|
54
|
+
build: {outDir, emptyOutDir: true},
|
|
55
|
+
resolve: {alias: {'@astryxdesign/core': CORE_SRC}},
|
|
56
|
+
plugins: [
|
|
57
|
+
react(),
|
|
58
|
+
...astryxStylex({
|
|
59
|
+
stylexOptions: {
|
|
60
|
+
dev: false,
|
|
61
|
+
unstable_moduleResolution: {type: 'commonJS', rootDir: REPO_ROOT},
|
|
62
|
+
aliases: {
|
|
63
|
+
'@astryxdesign/core/*': [path.join(CORE_SRC, '*')],
|
|
64
|
+
'@astryxdesign/core': [CORE_SRC],
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
libraryPattern: 'packages/core/',
|
|
68
|
+
}),
|
|
69
|
+
],
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const assets = path.join(outDir, 'assets');
|
|
73
|
+
const cssName = readdirSync(assets).find(f => f.endsWith('.css'));
|
|
74
|
+
if (!cssName) throw new Error(`no stylesheet emitted into ${assets}`);
|
|
75
|
+
return {
|
|
76
|
+
css: readFileSync(path.join(assets, cssName), 'utf-8'),
|
|
77
|
+
cssName,
|
|
78
|
+
html: readFileSync(path.join(outDir, 'index.html'), 'utf-8'),
|
|
79
|
+
outDir,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** The slice of a stylesheet inside a given top-level layer block. */
|
|
84
|
+
function layerBlock(css: string, name: string): string {
|
|
85
|
+
const start = css.indexOf(`@layer ${name} {`);
|
|
86
|
+
expect(
|
|
87
|
+
start,
|
|
88
|
+
`@layer ${name} is not in the emitted stylesheet`,
|
|
89
|
+
).toBeGreaterThan(-1);
|
|
90
|
+
let depth = 0;
|
|
91
|
+
for (let i = css.indexOf('{', start); i < css.length; i++) {
|
|
92
|
+
if (css[i] === '{') depth++;
|
|
93
|
+
else if (css[i] === '}' && --depth === 0) return css.slice(start, i + 1);
|
|
94
|
+
}
|
|
95
|
+
throw new Error(`@layer ${name} is unterminated`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Both shapes must satisfy the same contract, so both run the same checks. */
|
|
99
|
+
function itSplitsCorrectly(get: () => Built) {
|
|
100
|
+
it('declares the layer order the split depends on', () => {
|
|
101
|
+
expect(get().html).toContain(
|
|
102
|
+
'@layer reset, astryx-base, astryx-theme, product;',
|
|
103
|
+
);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("puts Astryx's own rules in the library layer", () => {
|
|
107
|
+
const library = layerBlock(get().css, 'astryx-base');
|
|
108
|
+
expect(library.match(LIBRARY_CLASS)?.length ?? 0).toBeGreaterThan(50);
|
|
109
|
+
// The fixture's Button pulls in real component CSS, so a token it styles
|
|
110
|
+
// with is a fact about this block rather than about class-name shape.
|
|
111
|
+
expect(library).toContain('--radius-element');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// The regression this file exists for. The wrapped-not-split version put the
|
|
115
|
+
// app's own StyleX in astryx-base, below `astryx-theme`, so a theme could
|
|
116
|
+
// silently restyle code the theme has no business reaching.
|
|
117
|
+
it("puts the app's own rules in the product layer, not the library one", () => {
|
|
118
|
+
const {css} = get();
|
|
119
|
+
const product = layerBlock(css, 'product');
|
|
120
|
+
const library = layerBlock(css, 'astryx-base');
|
|
121
|
+
|
|
122
|
+
expect(product).toContain('11px');
|
|
123
|
+
expect(library).not.toContain('11px');
|
|
124
|
+
|
|
125
|
+
expect(product.match(LIBRARY_CLASS)).toBeNull();
|
|
126
|
+
expect(library.match(PRODUCT_CLASS)).toBeNull();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('leaves no StyleX rule outside the two layers', () => {
|
|
130
|
+
const {css} = get();
|
|
131
|
+
const outside = css
|
|
132
|
+
.replace(layerBlock(css, 'astryx-base'), '')
|
|
133
|
+
.replace(layerBlock(css, 'product'), '');
|
|
134
|
+
expect(outside).not.toContain('@layer priority');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// StyleX runs its CSS through lightningcss before emitting it. Replacing that
|
|
138
|
+
// output means running the same pass, or the build quietly loses the prefixes
|
|
139
|
+
// the original had.
|
|
140
|
+
it('keeps the vendor prefixing StyleX applies', () => {
|
|
141
|
+
expect(get().css).toContain('-webkit-');
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('links the stylesheet from the page', () => {
|
|
145
|
+
const {html, cssName} = get();
|
|
146
|
+
expect(html).toContain(cssName);
|
|
147
|
+
expect(html.split(cssName).length - 1, 'linked more than once').toBe(1);
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
describe('a production build separates Astryx and product styles by layer', () => {
|
|
152
|
+
describe('when the app imports a stylesheet of its own', () => {
|
|
153
|
+
let built: Built;
|
|
154
|
+
beforeAll(async () => {
|
|
155
|
+
built = await buildFixture('layer-split');
|
|
156
|
+
}, 180_000);
|
|
157
|
+
afterAll(
|
|
158
|
+
() => built && rmSync(built.outDir, {recursive: true, force: true}),
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
itSplitsCorrectly(() => built);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// StyleX has no bundle asset to append to here, so it writes its own file in
|
|
165
|
+
// `writeBundle` — outside Rollup's graph, which is why Vite emits no `<link>`
|
|
166
|
+
// for it. Every assertion below failed before this case was handled: the
|
|
167
|
+
// stylesheet was unsplit AND the page loaded no styles at all.
|
|
168
|
+
describe('when the app imports no stylesheet at all', () => {
|
|
169
|
+
let built: Built;
|
|
170
|
+
beforeAll(async () => {
|
|
171
|
+
built = await buildFixture('layer-split-nocss');
|
|
172
|
+
}, 180_000);
|
|
173
|
+
afterAll(
|
|
174
|
+
() => built && rmSync(built.outDir, {recursive: true, force: true}),
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
itSplitsCorrectly(() => built);
|
|
178
|
+
});
|
|
179
|
+
});
|
package/src/vite.test.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* theme layer name is fixed at `astryx-theme`.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import {describe, it, expect, beforeAll, afterAll} from 'vitest';
|
|
10
|
+
import {describe, it, expect, beforeAll, afterAll, vi} from 'vitest';
|
|
11
11
|
import {mkdtempSync, mkdirSync, rmSync} from 'node:fs';
|
|
12
12
|
import {tmpdir} from 'node:os';
|
|
13
13
|
import path from 'node:path';
|
|
@@ -53,6 +53,52 @@ describe('astryxStylex layer order (legacy API)', () => {
|
|
|
53
53
|
});
|
|
54
54
|
});
|
|
55
55
|
|
|
56
|
+
/**
|
|
57
|
+
* A production build had no equivalent of the dev server's split-layer plugin,
|
|
58
|
+
* so StyleX's `@layer priorityN` blocks sat outside the declared order and
|
|
59
|
+
* outranked `astryx-theme` — every component override a theme set was dropped.
|
|
60
|
+
* The first fix WRAPPED those blocks in the library layer instead of splitting
|
|
61
|
+
* them, which fixed the theme and broke the app: product StyleX landed below
|
|
62
|
+
* `astryx-theme` too, so a theme could silently restyle code it does not own.
|
|
63
|
+
*
|
|
64
|
+
* These pin the plugin's wiring. That the partition is right in a real build is
|
|
65
|
+
* vite.build.test.ts, and what it means for the cascade is
|
|
66
|
+
* .github/scripts/theme-layer-cascade.js — unit tests over this hook passed
|
|
67
|
+
* throughout both bugs.
|
|
68
|
+
*/
|
|
69
|
+
describe('astryxStylex build-time layer split', () => {
|
|
70
|
+
const find = (plugins: ReturnType<typeof astryxStylex>) =>
|
|
71
|
+
plugins.find(p => p.name === 'astryx-build-layer-split');
|
|
72
|
+
|
|
73
|
+
it('is present on both the modern and the legacy API', () => {
|
|
74
|
+
expect(find(astryxStylex())).toBeTruthy();
|
|
75
|
+
expect(find(astryxStylex({stylexOptions: {}}))).toBeTruthy();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('runs on a build only, after the StyleX plugin that emits the CSS', () => {
|
|
79
|
+
const plugin = find(astryxStylex());
|
|
80
|
+
expect(plugin?.apply).toBe('build');
|
|
81
|
+
expect(plugin?.enforce).toBe('post');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// The dev middleware and the build hook read the same partition, so a build
|
|
85
|
+
// that emits nothing is a build where StyleX collected nothing — never a
|
|
86
|
+
// silent pass-through of unsplit CSS.
|
|
87
|
+
it('does nothing when StyleX collected no rules', () => {
|
|
88
|
+
const plugin = find(astryxStylex());
|
|
89
|
+
const hook = (plugin as any).writeBundle;
|
|
90
|
+
const fn = typeof hook === 'function' ? hook : hook.handler;
|
|
91
|
+
const error = vi.fn();
|
|
92
|
+
expect(() =>
|
|
93
|
+
fn.call(
|
|
94
|
+
{error},
|
|
95
|
+
{dir: mkdtempSync(path.join(tmpdir(), 'astryx-empty-'))},
|
|
96
|
+
),
|
|
97
|
+
).not.toThrow();
|
|
98
|
+
expect(error).not.toHaveBeenCalled();
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
56
102
|
describe('astryxStylex optimizeDeps package discovery', () => {
|
|
57
103
|
let rootDir: string;
|
|
58
104
|
|
package/src/vite.ts
CHANGED
|
@@ -6,6 +6,7 @@ import stylex from '@stylexjs/unplugin';
|
|
|
6
6
|
import fs from 'node:fs';
|
|
7
7
|
import path from 'node:path';
|
|
8
8
|
import {fileURLToPath} from 'node:url';
|
|
9
|
+
import {createRequire} from 'node:module';
|
|
9
10
|
|
|
10
11
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
11
12
|
|
|
@@ -242,62 +243,273 @@ export function astryxStylex(
|
|
|
242
243
|
return next();
|
|
243
244
|
}
|
|
244
245
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
res.setHeader('Content-Type', 'text/css');
|
|
248
|
-
res.end('');
|
|
249
|
-
return;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
const shared = stylexPlugin.__stylexGetSharedStore?.();
|
|
253
|
-
const rulesById = shared?.rulesById;
|
|
254
|
-
|
|
255
|
-
if (!rulesById || rulesById.size === 0) {
|
|
256
|
-
res.statusCode = 200;
|
|
257
|
-
res.setHeader('Content-Type', 'text/css');
|
|
258
|
-
res.end('');
|
|
259
|
-
return;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
const libraryRules: any[] = [];
|
|
263
|
-
const productRules: any[] = [];
|
|
264
|
-
|
|
265
|
-
for (const [filePath, rules] of rulesById.entries()) {
|
|
266
|
-
if (filePath.includes(libraryPattern)) {
|
|
267
|
-
libraryRules.push(...rules);
|
|
268
|
-
} else {
|
|
269
|
-
productRules.push(...rules);
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
const libraryCss = libraryRules.length
|
|
274
|
-
? stylexBabelPlugin.processStylexRules(libraryRules, {
|
|
275
|
-
useLayers: true,
|
|
276
|
-
})
|
|
277
|
-
: '';
|
|
278
|
-
const productCss = productRules.length
|
|
279
|
-
? stylexBabelPlugin.processStylexRules(productRules, {
|
|
280
|
-
useLayers: true,
|
|
281
|
-
})
|
|
282
|
-
: '';
|
|
283
|
-
|
|
284
|
-
const parts: string[] = [];
|
|
285
|
-
if (libraryCss)
|
|
286
|
-
parts.push(`@layer ${libraryLayer} {\n${libraryCss}\n}`);
|
|
287
|
-
if (productCss)
|
|
288
|
-
parts.push(`@layer ${productLayer} {\n${productCss}\n}`);
|
|
246
|
+
const rulesById =
|
|
247
|
+
stylexPlugin?.__stylexGetSharedStore?.()?.rulesById;
|
|
289
248
|
|
|
290
249
|
res.statusCode = 200;
|
|
291
250
|
res.setHeader('Content-Type', 'text/css');
|
|
292
251
|
res.setHeader('Cache-Control', 'no-store');
|
|
293
|
-
res.end(
|
|
252
|
+
res.end(
|
|
253
|
+
renderSplitLayers(rulesById, {
|
|
254
|
+
libraryPattern,
|
|
255
|
+
libraryLayer,
|
|
256
|
+
productLayer,
|
|
257
|
+
}),
|
|
258
|
+
);
|
|
294
259
|
},
|
|
295
260
|
});
|
|
296
261
|
};
|
|
297
262
|
},
|
|
298
263
|
};
|
|
299
264
|
|
|
300
|
-
return [
|
|
265
|
+
return [
|
|
266
|
+
configPlugin,
|
|
267
|
+
layerOrderPlugin,
|
|
268
|
+
basePlugin,
|
|
269
|
+
splitLayerPlugin,
|
|
270
|
+
buildLayerSplitPlugin(basePlugin, {
|
|
271
|
+
libraryPattern,
|
|
272
|
+
libraryLayer,
|
|
273
|
+
productLayer,
|
|
274
|
+
lightningcssOptions: stylexOptions.lightningcssOptions,
|
|
275
|
+
}),
|
|
276
|
+
];
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Partition StyleX's collected rules by the source file that authored them and
|
|
281
|
+
* render each half into its cascade layer: Astryx's own styles into the library
|
|
282
|
+
* layer (below `astryx-theme`, so a theme can override them), product styles
|
|
283
|
+
* into the product layer (above it, so an app always wins).
|
|
284
|
+
*
|
|
285
|
+
* This is the one implementation. The dev middleware and the build hook differ
|
|
286
|
+
* only in when they run and what they do with the string — the partition itself
|
|
287
|
+
* must not diverge, because it did: the build shipped a version that wrapped
|
|
288
|
+
* everything in the library layer, which put product styles UNDER a theme.
|
|
289
|
+
*
|
|
290
|
+
* `rulesById` is keyed by absolute source path, which is the only place the
|
|
291
|
+
* distinction survives — by the time the CSS is text, the origin is gone.
|
|
292
|
+
*/
|
|
293
|
+
function renderSplitLayers(
|
|
294
|
+
rulesById: Map<string, unknown[]> | undefined,
|
|
295
|
+
options: {
|
|
296
|
+
libraryPattern: string;
|
|
297
|
+
libraryLayer: string;
|
|
298
|
+
productLayer: string;
|
|
299
|
+
},
|
|
300
|
+
): string {
|
|
301
|
+
if (!rulesById || rulesById.size === 0) return '';
|
|
302
|
+
|
|
303
|
+
const libraryRules: unknown[] = [];
|
|
304
|
+
const productRules: unknown[] = [];
|
|
305
|
+
|
|
306
|
+
for (const [filePath, rules] of rulesById.entries()) {
|
|
307
|
+
if (filePath.includes(options.libraryPattern)) {
|
|
308
|
+
libraryRules.push(...rules);
|
|
309
|
+
} else {
|
|
310
|
+
productRules.push(...rules);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const render = (rules: unknown[], layer: string) =>
|
|
315
|
+
rules.length
|
|
316
|
+
? `@layer ${layer} {\n${stylexBabelPlugin.processStylexRules(rules as never, {useLayers: true})}\n}`
|
|
317
|
+
: '';
|
|
318
|
+
|
|
319
|
+
return [
|
|
320
|
+
render(libraryRules, options.libraryLayer),
|
|
321
|
+
render(productRules, options.productLayer),
|
|
322
|
+
]
|
|
323
|
+
.filter(Boolean)
|
|
324
|
+
.join('\n\n');
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* The build half of the split. StyleX appends one merged block of CSS to the
|
|
329
|
+
* build's stylesheet, in its own top-level `@layer priority1…priorityN` — which
|
|
330
|
+
* is declared after `@layer reset, astryx-base, astryx-theme, product;` and so
|
|
331
|
+
* outranks every one of them. On the dev server `astryx-split-layers` re-serves
|
|
332
|
+
* the same rules already partitioned; a build had no equivalent, so a theme's
|
|
333
|
+
* component overrides were silently dropped in the built app while working in
|
|
334
|
+
* dev.
|
|
335
|
+
*
|
|
336
|
+
* This replaces that merged block with the partitioned pair. It runs in
|
|
337
|
+
* `writeBundle` because StyleX emits through two different paths depending on
|
|
338
|
+
* whether the bundle already has a stylesheet to append to (`generateBundle`)
|
|
339
|
+
* or has to write its own file (`writeBundle`) — on disk, after both, there is
|
|
340
|
+
* one case instead of two.
|
|
341
|
+
*
|
|
342
|
+
* The block is located by an exact match against StyleX's own collector rather
|
|
343
|
+
* than by looking for `@layer priority1`: a wrong guess about where the block
|
|
344
|
+
* starts silently moves rules between layers, which is the failure this exists
|
|
345
|
+
* to prevent. If the rules exist and the block cannot be found, the build
|
|
346
|
+
* fails.
|
|
347
|
+
*/
|
|
348
|
+
function buildLayerSplitPlugin(
|
|
349
|
+
basePlugin: unknown,
|
|
350
|
+
options: {
|
|
351
|
+
libraryPattern: string;
|
|
352
|
+
libraryLayer: string;
|
|
353
|
+
productLayer: string;
|
|
354
|
+
lightningcssOptions?: unknown;
|
|
355
|
+
},
|
|
356
|
+
): Plugin {
|
|
357
|
+
const stylex = basePlugin as {
|
|
358
|
+
__stylexGetSharedStore?: () => {rulesById: Map<string, unknown[]>};
|
|
359
|
+
__stylexCollectCss?: () => string;
|
|
360
|
+
};
|
|
361
|
+
let base = '/';
|
|
362
|
+
|
|
363
|
+
return {
|
|
364
|
+
name: 'astryx-build-layer-split',
|
|
365
|
+
apply: 'build',
|
|
366
|
+
enforce: 'post',
|
|
367
|
+
configResolved(config) {
|
|
368
|
+
base = config.base ?? '/';
|
|
369
|
+
},
|
|
370
|
+
writeBundle(outputOptions) {
|
|
371
|
+
const rulesById = stylex.__stylexGetSharedStore?.().rulesById;
|
|
372
|
+
if (!rulesById || rulesById.size === 0) return;
|
|
373
|
+
|
|
374
|
+
const merged = stylex.__stylexCollectCss?.();
|
|
375
|
+
if (!merged) return;
|
|
376
|
+
|
|
377
|
+
const split = postProcessCss(
|
|
378
|
+
renderSplitLayers(rulesById, options),
|
|
379
|
+
options.lightningcssOptions,
|
|
380
|
+
);
|
|
381
|
+
|
|
382
|
+
const outDir = outputOptions.dir
|
|
383
|
+
? outputOptions.dir
|
|
384
|
+
: outputOptions.file
|
|
385
|
+
? path.dirname(outputOptions.file)
|
|
386
|
+
: null;
|
|
387
|
+
// `write: false` builds keep everything in memory; there is nothing to
|
|
388
|
+
// patch and nothing was shipped, so this is not a failure.
|
|
389
|
+
if (!outDir || !fs.existsSync(outDir)) return;
|
|
390
|
+
|
|
391
|
+
const patched: string[] = [];
|
|
392
|
+
for (const file of listCssFiles(outDir)) {
|
|
393
|
+
const css = fs.readFileSync(file, 'utf-8');
|
|
394
|
+
const at = css.lastIndexOf(merged);
|
|
395
|
+
if (at === -1) continue;
|
|
396
|
+
fs.writeFileSync(
|
|
397
|
+
file,
|
|
398
|
+
css.slice(0, at) + split + css.slice(at + merged.length),
|
|
399
|
+
);
|
|
400
|
+
patched.push(file);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if (patched.length === 0) {
|
|
404
|
+
this.error(
|
|
405
|
+
'astryx-build-layer-split: StyleX emitted rules but its CSS block ' +
|
|
406
|
+
`was not found in any stylesheet under ${outDir}, so Astryx and ` +
|
|
407
|
+
'product styles could not be separated into their cascade layers. ' +
|
|
408
|
+
'Leaving the build unsplit would let product styles lose to a ' +
|
|
409
|
+
'theme. This usually means the StyleX plugin version changed how ' +
|
|
410
|
+
'it emits CSS.',
|
|
411
|
+
);
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
linkOrphanStylesheets(outDir, patched, base);
|
|
416
|
+
},
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/** Every `.css` file under a directory, recursively. */
|
|
421
|
+
function listCssFiles(dir: string): string[] {
|
|
422
|
+
const out: string[] = [];
|
|
423
|
+
for (const entry of fs.readdirSync(dir, {withFileTypes: true})) {
|
|
424
|
+
const full = path.join(dir, entry.name);
|
|
425
|
+
if (entry.isDirectory()) out.push(...listCssFiles(full));
|
|
426
|
+
else if (entry.name.endsWith('.css')) out.push(full);
|
|
427
|
+
}
|
|
428
|
+
return out;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/** Every `.html` file under a directory, recursively. */
|
|
432
|
+
function listHtmlFiles(dir: string): string[] {
|
|
433
|
+
const out: string[] = [];
|
|
434
|
+
for (const entry of fs.readdirSync(dir, {withFileTypes: true})) {
|
|
435
|
+
const full = path.join(dir, entry.name);
|
|
436
|
+
if (entry.isDirectory()) out.push(...listHtmlFiles(full));
|
|
437
|
+
else if (entry.name.endsWith('.html')) out.push(full);
|
|
438
|
+
}
|
|
439
|
+
return out;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Link a stylesheet the build wrote but no page loads.
|
|
444
|
+
*
|
|
445
|
+
* When an app imports no CSS of its own, StyleX has no bundle asset to append
|
|
446
|
+
* to, so it writes `assets/stylex.css` itself — outside Rollup's graph, which
|
|
447
|
+
* means Vite's HTML plugin never learns about it and emits no `<link>`. The app
|
|
448
|
+
* ships every Astryx style correctly split into layers, and a completely
|
|
449
|
+
* unstyled page. Importing any stylesheet hides it, which is why it survives:
|
|
450
|
+
* the moment a project has one line of its own CSS the symptom disappears.
|
|
451
|
+
*
|
|
452
|
+
* Only orphans are linked. A stylesheet already referenced by a page is Vite's,
|
|
453
|
+
* and touching it would duplicate the load. A build with no HTML at all —
|
|
454
|
+
* library mode — is left alone: its consumer imports the CSS themselves.
|
|
455
|
+
*/
|
|
456
|
+
function linkOrphanStylesheets(
|
|
457
|
+
outDir: string,
|
|
458
|
+
cssFiles: string[],
|
|
459
|
+
base: string,
|
|
460
|
+
): void {
|
|
461
|
+
const pages = listHtmlFiles(outDir);
|
|
462
|
+
if (pages.length === 0) return;
|
|
463
|
+
|
|
464
|
+
const orphans = cssFiles.filter(css => {
|
|
465
|
+
const name = path.basename(css);
|
|
466
|
+
return !pages.some(page => fs.readFileSync(page, 'utf-8').includes(name));
|
|
467
|
+
});
|
|
468
|
+
if (orphans.length === 0) return;
|
|
469
|
+
|
|
470
|
+
const links = orphans
|
|
471
|
+
.map(css => {
|
|
472
|
+
const href =
|
|
473
|
+
base.replace(/\/$/, '') +
|
|
474
|
+
'/' +
|
|
475
|
+
path.relative(outDir, css).split(path.sep).join('/');
|
|
476
|
+
return `<link rel="stylesheet" crossorigin href="${href}">`;
|
|
477
|
+
})
|
|
478
|
+
.join('\n ');
|
|
479
|
+
|
|
480
|
+
for (const page of pages) {
|
|
481
|
+
const html = fs.readFileSync(page, 'utf-8');
|
|
482
|
+
if (!html.includes('</head>')) continue;
|
|
483
|
+
fs.writeFileSync(page, html.replace('</head>', ` ${links}\n </head>`));
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* StyleX runs its collected CSS through lightningcss before emitting it, so
|
|
489
|
+
* anything replacing that output has to run the same pass or the build quietly
|
|
490
|
+
* loses the vendor prefixes and lowering the original had.
|
|
491
|
+
*
|
|
492
|
+
* lightningcss ships with both Vite and the StyleX plugin, either of which must
|
|
493
|
+
* be installed for this plugin to run at all. If it somehow is not resolvable,
|
|
494
|
+
* the unprocessed CSS is correct — just less compatible — so this degrades
|
|
495
|
+
* rather than failing the build.
|
|
496
|
+
*/
|
|
497
|
+
function postProcessCss(css: string, lightningcssOptions: unknown): string {
|
|
498
|
+
if (!css) return css;
|
|
499
|
+
try {
|
|
500
|
+
const require_ = createRequire(import.meta.url);
|
|
501
|
+
const {transform, browserslistToTargets} = require_('lightningcss');
|
|
502
|
+
const browserslist = require_('browserslist');
|
|
503
|
+
const {code} = transform({
|
|
504
|
+
targets: browserslistToTargets(browserslist()),
|
|
505
|
+
...(lightningcssOptions as object),
|
|
506
|
+
filename: 'stylex.css',
|
|
507
|
+
code: Buffer.from(css),
|
|
508
|
+
});
|
|
509
|
+
return code.toString();
|
|
510
|
+
} catch {
|
|
511
|
+
return css;
|
|
512
|
+
}
|
|
301
513
|
}
|
|
302
514
|
|
|
303
515
|
/**
|
|
@@ -370,60 +582,35 @@ function astryxStylexLegacy(options: AstryxVitePluginLegacyOptions): Plugin[] {
|
|
|
370
582
|
return next();
|
|
371
583
|
}
|
|
372
584
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
res.setHeader('Content-Type', 'text/css');
|
|
376
|
-
res.end('');
|
|
377
|
-
return;
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
const shared = stylexPlugin.__stylexGetSharedStore?.();
|
|
381
|
-
const rulesById = shared?.rulesById;
|
|
382
|
-
|
|
383
|
-
if (!rulesById || rulesById.size === 0) {
|
|
384
|
-
res.statusCode = 200;
|
|
385
|
-
res.setHeader('Content-Type', 'text/css');
|
|
386
|
-
res.end('');
|
|
387
|
-
return;
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
const libraryRules: any[] = [];
|
|
391
|
-
const productRules: any[] = [];
|
|
392
|
-
|
|
393
|
-
for (const [filePath, rules] of rulesById.entries()) {
|
|
394
|
-
if (filePath.includes(libraryPattern)) {
|
|
395
|
-
libraryRules.push(...rules);
|
|
396
|
-
} else {
|
|
397
|
-
productRules.push(...rules);
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
const libraryCss = libraryRules.length
|
|
402
|
-
? stylexBabelPlugin.processStylexRules(libraryRules, {
|
|
403
|
-
useLayers: true,
|
|
404
|
-
})
|
|
405
|
-
: '';
|
|
406
|
-
const productCss = productRules.length
|
|
407
|
-
? stylexBabelPlugin.processStylexRules(productRules, {
|
|
408
|
-
useLayers: true,
|
|
409
|
-
})
|
|
410
|
-
: '';
|
|
411
|
-
|
|
412
|
-
const parts: string[] = [];
|
|
413
|
-
if (libraryCss)
|
|
414
|
-
parts.push(`@layer ${libraryLayer} {\n${libraryCss}\n}`);
|
|
415
|
-
if (productCss)
|
|
416
|
-
parts.push(`@layer ${productLayer} {\n${productCss}\n}`);
|
|
585
|
+
const rulesById =
|
|
586
|
+
stylexPlugin?.__stylexGetSharedStore?.()?.rulesById;
|
|
417
587
|
|
|
418
588
|
res.statusCode = 200;
|
|
419
589
|
res.setHeader('Content-Type', 'text/css');
|
|
420
590
|
res.setHeader('Cache-Control', 'no-store');
|
|
421
|
-
res.end(
|
|
591
|
+
res.end(
|
|
592
|
+
renderSplitLayers(rulesById, {
|
|
593
|
+
libraryPattern,
|
|
594
|
+
libraryLayer,
|
|
595
|
+
productLayer,
|
|
596
|
+
}),
|
|
597
|
+
);
|
|
422
598
|
},
|
|
423
599
|
});
|
|
424
600
|
};
|
|
425
601
|
},
|
|
426
602
|
};
|
|
427
603
|
|
|
428
|
-
return [
|
|
604
|
+
return [
|
|
605
|
+
layerOrderPlugin,
|
|
606
|
+
basePlugin,
|
|
607
|
+
splitLayerPlugin,
|
|
608
|
+
buildLayerSplitPlugin(basePlugin, {
|
|
609
|
+
libraryPattern,
|
|
610
|
+
libraryLayer,
|
|
611
|
+
productLayer,
|
|
612
|
+
lightningcssOptions: (stylexOptions as {lightningcssOptions?: unknown})
|
|
613
|
+
?.lightningcssOptions,
|
|
614
|
+
}),
|
|
615
|
+
];
|
|
429
616
|
}
|