@astryxdesign/build 0.0.0-bootstrap.0 → 0.0.15

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/src/vite.ts ADDED
@@ -0,0 +1,427 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ import type {Plugin, UserConfig} from 'vite';
4
+ import stylexBabelPlugin from '@stylexjs/babel-plugin';
5
+ import stylex from '@stylexjs/unplugin';
6
+ import path from 'path';
7
+ import {fileURLToPath} from 'url';
8
+
9
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
10
+
11
+ const LIBRARY_PATTERN = 'node_modules/@astryxdesign/';
12
+ const STYLEX_CSS_PATH = '/virtual:stylex.css';
13
+
14
+ /**
15
+ * Browser targets for lightningcss (opt-in).
16
+ * Only needed if your StyleX version lowers light-dark() without them.
17
+ * Exported for consumers who want to opt in explicitly.
18
+ */
19
+ export const LIGHTNINGCSS_TARGETS = {
20
+ chrome: 123 << 16,
21
+ firefox: 120 << 16,
22
+ safari: (17 << 16) | (5 << 8),
23
+ };
24
+
25
+ /**
26
+ * Legacy options shape — kept for backward compatibility.
27
+ * Prefer the zero-config form: xdsStylex()
28
+ */
29
+ export interface XDSVitePluginLegacyOptions {
30
+ stylexOptions: Parameters<typeof stylex.vite>[0];
31
+ libraryPattern?: string;
32
+ /** StyleX atomic class-name prefix for Astryx library styles. @default 'astryx' */
33
+ stylexPrefix?: string;
34
+ layers?: {
35
+ library?: string;
36
+ product?: string;
37
+ };
38
+ }
39
+
40
+ export interface XDSVitePluginOptions {
41
+ /**
42
+ * Whether to enable dev mode for StyleX.
43
+ * @default process.env.NODE_ENV !== 'production'
44
+ */
45
+ dev?: boolean;
46
+
47
+ /**
48
+ * Root directory for module resolution.
49
+ * @default process.cwd()
50
+ */
51
+ rootDir?: string;
52
+
53
+ /**
54
+ * Pattern to identify Astryx library files vs product files.
55
+ * @default 'node_modules/@astryxdesign/'
56
+ */
57
+ libraryPattern?: string;
58
+
59
+ /**
60
+ * CSS layer names for the split output.
61
+ */
62
+ layers?: {
63
+ /** Layer name for Astryx library styles @default 'astryx-base' */
64
+ library?: string;
65
+ /** Layer name for product styles @default 'product' */
66
+ product?: string;
67
+ };
68
+
69
+ /**
70
+ * LightningCSS browser targets. Only needed if your StyleX version
71
+ * lowers light-dark() without them. Most recent versions preserve
72
+ * light-dark() by default.
73
+ * @default undefined (no targets set)
74
+ */
75
+ lightningcssTargets?: Record<string, number>;
76
+
77
+ /**
78
+ * StyleX atomic class-name prefix for Astryx *library* styles. The product
79
+ * build uses a distinct prefix so library and product atoms never collide
80
+ * across layers.
81
+ *
82
+ * Configurable to support the Astryx-prefix migration (P2380608025): a consumer
83
+ * can rebrand the library atom prefix to `astryx` before the final cutover.
84
+ * Defaults to `xds` so existing consumers are unaffected.
85
+ *
86
+ * @default 'astryx'
87
+ */
88
+ stylexPrefix?: string;
89
+
90
+ /**
91
+ * Extra StyleX options to merge.
92
+ */
93
+ stylexOverrides?: Record<string, unknown>;
94
+ }
95
+
96
+ /**
97
+ * Astryx Vite plugin for source builds.
98
+ *
99
+ * Provides sensible defaults for StyleX compilation with Astryx.
100
+ * Just spread into your plugins array:
101
+ *
102
+ * plugins: [...xdsStylex(), react()]
103
+ *
104
+ * Handles:
105
+ * - StyleX compilation with correct settings
106
+ * - CSS layer ordering (reset < astryx-base < astryx-theme < product)
107
+ * - resolve.alias for @astryxdesign/core source
108
+ * - optimizeDeps.exclude to prevent Vite pre-bundling Astryx
109
+ *
110
+ * @param options — optional overrides
111
+ */
112
+ export function xdsStylex(
113
+ options: XDSVitePluginOptions | XDSVitePluginLegacyOptions = {},
114
+ ): Plugin[] {
115
+ // Detect legacy API: xdsStylex({stylexOptions: {...}})
116
+ if ('stylexOptions' in options && options.stylexOptions) {
117
+ return xdsStylexLegacy(options as XDSVitePluginLegacyOptions);
118
+ }
119
+
120
+ const opts = options as XDSVitePluginOptions;
121
+ const {
122
+ dev = process.env.NODE_ENV !== 'production',
123
+ rootDir = process.cwd(),
124
+ libraryPattern = LIBRARY_PATTERN,
125
+ layers = {},
126
+ lightningcssTargets,
127
+ stylexPrefix = 'astryx',
128
+ stylexOverrides = {},
129
+ } = opts;
130
+
131
+ const libraryLayer = layers.library ?? 'astryx-base';
132
+ const productLayer = layers.product ?? 'product';
133
+
134
+ // Build StyleX options with sensible defaults
135
+ const stylexOptions: Record<string, unknown> = {
136
+ dev,
137
+ runtimeInjection: false,
138
+ treeshakeCompensation: true,
139
+ unstable_moduleResolution: {
140
+ type: 'commonJS',
141
+ rootDir,
142
+ },
143
+ ...(lightningcssTargets && {
144
+ lightningcssOptions: {targets: lightningcssTargets},
145
+ }),
146
+ ...stylexOverrides,
147
+ };
148
+
149
+ // Inject our babel wrapper as a user plugin — it runs before the
150
+ // unplugin's hardcoded StyleX instance and handles prefix routing.
151
+ const xdsBabelPlugin = path.resolve(__dirname, 'babel.js');
152
+
153
+ const basePlugin = stylex.vite({
154
+ ...(stylexOptions as any),
155
+ useCSSLayers: true,
156
+ babelConfig: {
157
+ plugins: [
158
+ [
159
+ xdsBabelPlugin,
160
+ {
161
+ ...stylexOptions,
162
+ libraryPrefix: stylexPrefix,
163
+ babelConfig: undefined,
164
+ },
165
+ ],
166
+ ],
167
+ },
168
+ });
169
+
170
+ // Layer order declaration plugin
171
+ const layerOrderPlugin: Plugin = {
172
+ name: 'xds-css-layer-order',
173
+ transformIndexHtml() {
174
+ return [
175
+ {
176
+ tag: 'style',
177
+ children: `@layer reset, ${libraryLayer}, astryx-theme, ${productLayer};`,
178
+ injectTo: 'head-prepend',
179
+ },
180
+ ];
181
+ },
182
+ };
183
+
184
+ // Config plugin — injects resolve.alias and optimizeDeps
185
+ const configPlugin: Plugin = {
186
+ name: 'xds-config',
187
+ config(): UserConfig {
188
+ // Discover all @astryxdesign/* packages to exclude from pre-bundling.
189
+ // Astryx ships as source that must be compiled by StyleX — pre-bundling
190
+ // strips stylex.create/defineVars calls and causes runtime errors.
191
+ let xdsPackages: string[] = ['@astryxdesign/core'];
192
+ try {
193
+ const fs = require('fs');
194
+ const xdsDir = path.resolve(rootDir, 'node_modules/@astryxdesign');
195
+ if (fs.existsSync(xdsDir)) {
196
+ xdsPackages = fs
197
+ .readdirSync(xdsDir)
198
+ .filter((name: string) => !name.startsWith('.'))
199
+ .map((name: string) => `@astryxdesign/${name}`);
200
+ }
201
+ } catch {
202
+ // Fallback to just @astryxdesign/core if discovery fails
203
+ }
204
+
205
+ return {
206
+ resolve: {
207
+ alias: {
208
+ '@astryxdesign/core/theme/tokens.stylex': path.resolve(
209
+ rootDir,
210
+ 'node_modules/@astryxdesign/core/src/theme/tokens.stylex.ts',
211
+ ),
212
+ '@astryxdesign/core': path.resolve(rootDir, 'node_modules/@astryxdesign/core/src'),
213
+ },
214
+ },
215
+ optimizeDeps: {
216
+ exclude: xdsPackages,
217
+ },
218
+ };
219
+ },
220
+ };
221
+
222
+ // Split-layer interceptor plugin (dev server only)
223
+ const splitLayerPlugin: Plugin = {
224
+ name: 'xds-split-layers',
225
+ configureServer(server) {
226
+ let stylexPlugin: any = null;
227
+
228
+ return () => {
229
+ for (const p of server.config.plugins.flat()) {
230
+ if ((p as any)?.__stylexGetSharedStore) {
231
+ stylexPlugin = p;
232
+ break;
233
+ }
234
+ }
235
+
236
+ server.middlewares.stack.unshift({
237
+ route: '',
238
+ handle: (req: any, res: any, next: any) => {
239
+ if (!req.url?.startsWith(STYLEX_CSS_PATH)) {
240
+ return next();
241
+ }
242
+
243
+ if (!stylexPlugin) {
244
+ res.statusCode = 200;
245
+ res.setHeader('Content-Type', 'text/css');
246
+ res.end('');
247
+ return;
248
+ }
249
+
250
+ const shared = stylexPlugin.__stylexGetSharedStore?.();
251
+ const rulesById = shared?.rulesById;
252
+
253
+ if (!rulesById || rulesById.size === 0) {
254
+ res.statusCode = 200;
255
+ res.setHeader('Content-Type', 'text/css');
256
+ res.end('');
257
+ return;
258
+ }
259
+
260
+ const libraryRules: any[] = [];
261
+ const productRules: any[] = [];
262
+
263
+ for (const [filePath, rules] of rulesById.entries()) {
264
+ if (filePath.includes(libraryPattern)) {
265
+ libraryRules.push(...rules);
266
+ } else {
267
+ productRules.push(...rules);
268
+ }
269
+ }
270
+
271
+ const libraryCss = libraryRules.length
272
+ ? stylexBabelPlugin.processStylexRules(libraryRules, {
273
+ useLayers: true,
274
+ })
275
+ : '';
276
+ const productCss = productRules.length
277
+ ? stylexBabelPlugin.processStylexRules(productRules, {
278
+ useLayers: true,
279
+ })
280
+ : '';
281
+
282
+ const parts: string[] = [];
283
+ if (libraryCss)
284
+ parts.push(`@layer ${libraryLayer} {\n${libraryCss}\n}`);
285
+ if (productCss)
286
+ parts.push(`@layer ${productLayer} {\n${productCss}\n}`);
287
+
288
+ res.statusCode = 200;
289
+ res.setHeader('Content-Type', 'text/css');
290
+ res.setHeader('Cache-Control', 'no-store');
291
+ res.end(parts.join('\n\n'));
292
+ },
293
+ });
294
+ };
295
+ },
296
+ };
297
+
298
+ return [configPlugin, layerOrderPlugin, basePlugin, splitLayerPlugin];
299
+ }
300
+
301
+ /**
302
+ * Legacy implementation — handles the old xdsStylex({stylexOptions: {...}}) API.
303
+ * Used by Storybook and other existing configs.
304
+ */
305
+ function xdsStylexLegacy(options: XDSVitePluginLegacyOptions): Plugin[] {
306
+ const {
307
+ stylexOptions,
308
+ libraryPattern = LIBRARY_PATTERN,
309
+ stylexPrefix = 'astryx',
310
+ layers = {},
311
+ } = options;
312
+
313
+ const libraryLayer = layers.library ?? 'astryx-base';
314
+ const productLayer = layers.product ?? 'product';
315
+
316
+ const xdsBabelPlugin = path.resolve(__dirname, 'babel.js');
317
+ const existingPlugins = (stylexOptions as any).babelConfig?.plugins ?? [];
318
+
319
+ const basePlugin = stylex.vite({
320
+ ...(stylexOptions as any),
321
+ useCSSLayers: true,
322
+ babelConfig: {
323
+ ...(stylexOptions as any).babelConfig,
324
+ plugins: [
325
+ [
326
+ xdsBabelPlugin,
327
+ {
328
+ ...(stylexOptions as any),
329
+ libraryPrefix: stylexPrefix,
330
+ babelConfig: undefined,
331
+ },
332
+ ],
333
+ ...existingPlugins,
334
+ ],
335
+ },
336
+ });
337
+
338
+ const layerOrderPlugin: Plugin = {
339
+ name: 'xds-css-layer-order',
340
+ transformIndexHtml() {
341
+ return [
342
+ {
343
+ tag: 'style',
344
+ children: `@layer reset, ${libraryLayer}, astryx-theme, ${productLayer};`,
345
+ injectTo: 'head-prepend',
346
+ },
347
+ ];
348
+ },
349
+ };
350
+
351
+ const splitLayerPlugin: Plugin = {
352
+ name: 'xds-split-layers',
353
+ configureServer(server) {
354
+ let stylexPlugin: any = null;
355
+
356
+ return () => {
357
+ for (const p of server.config.plugins.flat()) {
358
+ if ((p as any)?.__stylexGetSharedStore) {
359
+ stylexPlugin = p;
360
+ break;
361
+ }
362
+ }
363
+
364
+ server.middlewares.stack.unshift({
365
+ route: '',
366
+ handle: (req: any, res: any, next: any) => {
367
+ if (!req.url?.startsWith(STYLEX_CSS_PATH)) {
368
+ return next();
369
+ }
370
+
371
+ if (!stylexPlugin) {
372
+ res.statusCode = 200;
373
+ res.setHeader('Content-Type', 'text/css');
374
+ res.end('');
375
+ return;
376
+ }
377
+
378
+ const shared = stylexPlugin.__stylexGetSharedStore?.();
379
+ const rulesById = shared?.rulesById;
380
+
381
+ if (!rulesById || rulesById.size === 0) {
382
+ res.statusCode = 200;
383
+ res.setHeader('Content-Type', 'text/css');
384
+ res.end('');
385
+ return;
386
+ }
387
+
388
+ const libraryRules: any[] = [];
389
+ const productRules: any[] = [];
390
+
391
+ for (const [filePath, rules] of rulesById.entries()) {
392
+ if (filePath.includes(libraryPattern)) {
393
+ libraryRules.push(...rules);
394
+ } else {
395
+ productRules.push(...rules);
396
+ }
397
+ }
398
+
399
+ const libraryCss = libraryRules.length
400
+ ? stylexBabelPlugin.processStylexRules(libraryRules, {
401
+ useLayers: true,
402
+ })
403
+ : '';
404
+ const productCss = productRules.length
405
+ ? stylexBabelPlugin.processStylexRules(productRules, {
406
+ useLayers: true,
407
+ })
408
+ : '';
409
+
410
+ const parts: string[] = [];
411
+ if (libraryCss)
412
+ parts.push(`@layer ${libraryLayer} {\n${libraryCss}\n}`);
413
+ if (productCss)
414
+ parts.push(`@layer ${productLayer} {\n${productCss}\n}`);
415
+
416
+ res.statusCode = 200;
417
+ res.setHeader('Content-Type', 'text/css');
418
+ res.setHeader('Cache-Control', 'no-store');
419
+ res.end(parts.join('\n\n'));
420
+ },
421
+ });
422
+ };
423
+ },
424
+ };
425
+
426
+ return [layerOrderPlugin, basePlugin, splitLayerPlugin];
427
+ }