@stylexjs/unplugin 0.17.5 → 0.18.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/lib/es/bun.mjs ADDED
@@ -0,0 +1,73 @@
1
+ import path from 'node:path';
2
+ import fsp from 'node:fs/promises';
3
+ import { unpluginFactory } from "./core.mjs";
4
+ const loaders = {
5
+ '.js': 'js',
6
+ '.jsx': 'jsx',
7
+ '.ts': 'ts',
8
+ '.tsx': 'tsx'
9
+ };
10
+ export const createStylexBunPlugin = (userOptions = {}) => {
11
+ const options = {
12
+ ...userOptions
13
+ };
14
+ if (options.dev == null) options.dev = true;
15
+ if (options.runtimeInjection == null) options.runtimeInjection = false;
16
+ if (options.useCSSLayers == null) options.useCSSLayers = true;
17
+ const plugin = unpluginFactory(options, {
18
+ framework: 'bun'
19
+ });
20
+ const cssOutput = options.bunDevCssOutput || path.resolve(process.cwd(), 'dist', 'stylex.dev.css');
21
+ let lastCss = null;
22
+ const writeCss = async () => {
23
+ const css = plugin.__stylexCollectCss?.() || '';
24
+ const next = css ? `:root { --stylex-injection: 0; }\n${css}` : ':root { --stylex-injection: 0; }';
25
+ if (next === lastCss) return;
26
+ lastCss = next;
27
+ try {
28
+ await fsp.mkdir(path.dirname(cssOutput), {
29
+ recursive: true
30
+ });
31
+ await fsp.writeFile(cssOutput, next, 'utf8');
32
+ } catch {}
33
+ };
34
+ return {
35
+ name: '@stylexjs/unplugin-bun',
36
+ async setup(build) {
37
+ if (plugin.buildStart) {
38
+ build.onStart(async () => {
39
+ await plugin.buildStart();
40
+ await writeCss();
41
+ });
42
+ } else {
43
+ build.onStart(async () => {
44
+ await writeCss();
45
+ });
46
+ }
47
+ if (plugin.buildEnd) {
48
+ build.onEnd(async () => {
49
+ await plugin.buildEnd();
50
+ await writeCss();
51
+ });
52
+ } else {
53
+ build.onEnd(async () => {
54
+ await writeCss();
55
+ });
56
+ }
57
+ build.onLoad({
58
+ filter: /\.[cm]?[jt]sx?$/
59
+ }, async args => {
60
+ const code = await Bun.file(args.path).text();
61
+ const result = plugin.transform ? await plugin.transform(code, args.path) : null;
62
+ const nextCode = result?.code ?? code;
63
+ await writeCss();
64
+ return {
65
+ contents: nextCode,
66
+ loader: loaders[path.extname(args.path)] || 'js'
67
+ };
68
+ });
69
+ }
70
+ };
71
+ };
72
+ const defaultBunPlugin = createStylexBunPlugin({});
73
+ export default defaultBunPlugin;
@@ -0,0 +1,410 @@
1
+ import { createUnplugin } from 'unplugin';
2
+ import { transformAsync } from '@babel/core';
3
+ import stylexBabelPlugin from '@stylexjs/babel-plugin';
4
+ import flowSyntaxPlugin from '@babel/plugin-syntax-flow';
5
+ import jsxSyntaxPlugin from '@babel/plugin-syntax-jsx';
6
+ import typescriptSyntaxPlugin from '@babel/plugin-syntax-typescript';
7
+ import path from 'node:path';
8
+ import fs from 'node:fs';
9
+ import fsp from 'node:fs/promises';
10
+ import { createRequire } from 'node:module';
11
+ import { transform as lightningTransform } from 'lightningcss';
12
+ import browserslist from 'browserslist';
13
+ import { browserslistToTargets } from 'lightningcss';
14
+ export function pickCssAssetFromRollupBundle(bundle, choose) {
15
+ const assets = Object.values(bundle).filter(a => a && a.type === 'asset' && typeof a.fileName === 'string' && a.fileName.endsWith('.css'));
16
+ if (assets.length === 0) return null;
17
+ if (typeof choose === 'function') {
18
+ const chosen = assets.find(a => choose(a.fileName));
19
+ if (chosen) return chosen;
20
+ }
21
+ const best = assets.find(a => /(^|\/)index\.css$/.test(a.fileName)) || assets.find(a => /(^|\/)style\.css$/.test(a.fileName));
22
+ return best || assets[0];
23
+ }
24
+ function processCollectedRulesToCSS(rules, options) {
25
+ if (!rules || rules.length === 0) return '';
26
+ const collectedCSS = stylexBabelPlugin.processStylexRules(rules, {
27
+ useLayers: !!options.useCSSLayers,
28
+ enableLTRRTLComments: options?.enableLTRRTLComments
29
+ });
30
+ const {
31
+ code
32
+ } = lightningTransform({
33
+ targets: browserslistToTargets(browserslist('>= 1%')),
34
+ ...options.lightningcssOptions,
35
+ filename: 'stylex.css',
36
+ code: Buffer.from(collectedCSS)
37
+ });
38
+ return code.toString();
39
+ }
40
+ function getAssetBaseName(asset) {
41
+ if (asset?.name && typeof asset.name === 'string') return asset.name;
42
+ const fallback = asset?.fileName ? path.basename(asset.fileName) : 'stylex.css';
43
+ const match = /^(.*?)(-[a-z0-9]{8,})?\.css$/i.exec(fallback);
44
+ if (match) return `${match[1]}.css`;
45
+ return fallback || 'stylex.css';
46
+ }
47
+ function replaceBundleReferences(bundle, oldFileName, newFileName) {
48
+ for (const item of Object.values(bundle)) {
49
+ if (!item) continue;
50
+ if (item.type === 'chunk') {
51
+ if (typeof item.code === 'string' && item.code.includes(oldFileName)) {
52
+ item.code = item.code.split(oldFileName).join(newFileName);
53
+ }
54
+ const importedCss = item.viteMetadata?.importedCss;
55
+ if (importedCss instanceof Set && importedCss.has(oldFileName)) {
56
+ importedCss.delete(oldFileName);
57
+ importedCss.add(newFileName);
58
+ } else if (Array.isArray(importedCss)) {
59
+ const next = importedCss.map(name => name === oldFileName ? newFileName : name);
60
+ item.viteMetadata.importedCss = next;
61
+ }
62
+ } else if (item.type === 'asset' && typeof item.source === 'string') {
63
+ if (item.source.includes(oldFileName)) {
64
+ item.source = item.source.split(oldFileName).join(newFileName);
65
+ }
66
+ }
67
+ }
68
+ }
69
+ export function replaceCssAssetWithHashedCopy(ctx, bundle, asset, nextSource) {
70
+ const baseName = getAssetBaseName(asset);
71
+ const referenceId = ctx.emitFile({
72
+ type: 'asset',
73
+ name: baseName,
74
+ source: nextSource
75
+ });
76
+ const nextFileName = ctx.getFileName(referenceId);
77
+ const oldFileName = asset.fileName;
78
+ if (!nextFileName || !oldFileName || nextFileName === oldFileName) {
79
+ asset.source = nextSource;
80
+ return;
81
+ }
82
+ replaceBundleReferences(bundle, oldFileName, nextFileName);
83
+ const emitted = bundle[nextFileName];
84
+ if (emitted && emitted !== asset) {
85
+ delete bundle[nextFileName];
86
+ }
87
+ asset.fileName = nextFileName;
88
+ asset.source = nextSource;
89
+ if (bundle[oldFileName] === asset) {
90
+ delete bundle[oldFileName];
91
+ }
92
+ bundle[nextFileName] = asset;
93
+ }
94
+ function readJSON(file) {
95
+ try {
96
+ const content = fs.readFileSync(file, 'utf8');
97
+ return JSON.parse(content);
98
+ } catch {
99
+ return null;
100
+ }
101
+ }
102
+ function findNearestPackageJson(startDir) {
103
+ let dir = startDir;
104
+ for (;;) {
105
+ const candidate = path.join(dir, 'package.json');
106
+ if (fs.existsSync(candidate)) return candidate;
107
+ const parent = path.dirname(dir);
108
+ if (parent === dir) break;
109
+ dir = parent;
110
+ }
111
+ return null;
112
+ }
113
+ function toPackageName(importSource) {
114
+ const source = typeof importSource === 'string' ? importSource : importSource?.from;
115
+ if (!source || source.startsWith('.') || source.startsWith('/')) return null;
116
+ if (source.startsWith('@')) {
117
+ const [scope, name] = source.split('/');
118
+ if (scope && name) return `${scope}/${name}`;
119
+ }
120
+ const [pkg] = source.split('/');
121
+ return pkg || null;
122
+ }
123
+ function hasStylexDependency(manifest, targetPackages) {
124
+ if (!manifest || typeof manifest !== 'object') return false;
125
+ const depFields = ['dependencies', 'peerDependencies', 'optionalDependencies'];
126
+ for (const field of depFields) {
127
+ const deps = manifest[field];
128
+ if (!deps || typeof deps !== 'object') continue;
129
+ for (const name of Object.keys(deps)) {
130
+ if (targetPackages.has(name)) return true;
131
+ }
132
+ }
133
+ return false;
134
+ }
135
+ function discoverStylexPackages({
136
+ importSources,
137
+ explicitPackages,
138
+ rootDir,
139
+ resolver
140
+ }) {
141
+ const targetPackages = new Set(importSources.map(toPackageName).filter(Boolean).concat(['@stylexjs/stylex']));
142
+ const found = new Set(explicitPackages || []);
143
+ const pkgJsonPath = findNearestPackageJson(rootDir);
144
+ if (!pkgJsonPath) return Array.from(found);
145
+ const pkgDir = path.dirname(pkgJsonPath);
146
+ const pkgJson = readJSON(pkgJsonPath);
147
+ if (!pkgJson) return Array.from(found);
148
+ const depFields = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies'];
149
+ const deps = new Set();
150
+ for (const field of depFields) {
151
+ const entries = pkgJson[field];
152
+ if (!entries || typeof entries !== 'object') continue;
153
+ for (const name of Object.keys(entries)) deps.add(name);
154
+ }
155
+ for (const dep of deps) {
156
+ let manifestPath = null;
157
+ try {
158
+ manifestPath = resolver.resolve(`${dep}/package.json`, {
159
+ paths: [pkgDir]
160
+ });
161
+ } catch {}
162
+ if (!manifestPath) continue;
163
+ const manifest = readJSON(manifestPath);
164
+ if (hasStylexDependency(manifest, targetPackages)) {
165
+ found.add(dep);
166
+ }
167
+ }
168
+ return Array.from(found);
169
+ }
170
+ export const unpluginFactory = (userOptions = {}, metaOptions) => {
171
+ const framework = metaOptions?.framework;
172
+ const {
173
+ dev = process.env.NODE_ENV === 'development' || process.env.BABEL_ENV === 'development',
174
+ unstable_moduleResolution = {
175
+ type: 'commonJS',
176
+ rootDir: process.cwd()
177
+ },
178
+ babelConfig: {
179
+ plugins = [],
180
+ presets = []
181
+ } = {},
182
+ importSources = ['stylex', '@stylexjs/stylex'],
183
+ useCSSLayers = false,
184
+ lightningcssOptions,
185
+ cssInjectionTarget,
186
+ externalPackages = [],
187
+ devPersistToDisk = false,
188
+ devMode = 'full',
189
+ treeshakeCompensation = ['vite', 'rollup', 'rolldown'].includes(framework),
190
+ ...stylexOptions
191
+ } = userOptions;
192
+ const stylexRulesById = new Map();
193
+ function getSharedStore() {
194
+ try {
195
+ const g = globalThis;
196
+ if (!g.__stylex_unplugin_store) {
197
+ g.__stylex_unplugin_store = {
198
+ rulesById: new Map(),
199
+ version: 0
200
+ };
201
+ }
202
+ return g.__stylex_unplugin_store;
203
+ } catch {
204
+ return {
205
+ rulesById: stylexRulesById,
206
+ version: 0
207
+ };
208
+ }
209
+ }
210
+ const nearestPkgJson = findNearestPackageJson(process.cwd());
211
+ const requireFromCwd = nearestPkgJson ? createRequire(nearestPkgJson) : createRequire(path.join(process.cwd(), 'package.json'));
212
+ const stylexPackages = discoverStylexPackages({
213
+ importSources,
214
+ explicitPackages: externalPackages,
215
+ rootDir: nearestPkgJson ? path.dirname(nearestPkgJson) : process.cwd(),
216
+ resolver: requireFromCwd
217
+ });
218
+ function findNearestNodeModules(startDir) {
219
+ let dir = startDir;
220
+ for (;;) {
221
+ const candidate = path.join(dir, 'node_modules');
222
+ if (fs.existsSync(candidate)) {
223
+ const stat = fs.statSync(candidate);
224
+ if (stat.isDirectory()) return candidate;
225
+ }
226
+ const parent = path.dirname(dir);
227
+ if (parent === dir) break;
228
+ dir = parent;
229
+ }
230
+ return null;
231
+ }
232
+ const NEAREST_NODE_MODULES = findNearestNodeModules(process.cwd());
233
+ const DISK_RULES_DIR = NEAREST_NODE_MODULES ? path.join(NEAREST_NODE_MODULES, '.stylex') : path.join(process.cwd(), 'node_modules', '.stylex');
234
+ const DISK_RULES_PATH = path.join(DISK_RULES_DIR, 'rules.json');
235
+ async function runBabelTransform(inputCode, filename, callerName) {
236
+ const result = await transformAsync(inputCode, {
237
+ babelrc: false,
238
+ filename,
239
+ presets,
240
+ plugins: [...plugins, /\.jsx?/.test(path.extname(filename)) ? flowSyntaxPlugin : [typescriptSyntaxPlugin, {
241
+ isTSX: true
242
+ }], jsxSyntaxPlugin, stylexBabelPlugin.withOptions({
243
+ ...stylexOptions,
244
+ importSources,
245
+ treeshakeCompensation,
246
+ dev,
247
+ unstable_moduleResolution
248
+ })],
249
+ caller: {
250
+ name: callerName,
251
+ supportsStaticESM: true,
252
+ supportsDynamicImport: true,
253
+ supportsTopLevelAwait: !inputCode.includes('require('),
254
+ supportsExportNamespaceFrom: true
255
+ }
256
+ });
257
+ if (!result || result.code == null) {
258
+ return {
259
+ code: inputCode,
260
+ map: null,
261
+ metadata: {}
262
+ };
263
+ }
264
+ return result;
265
+ }
266
+ function escapeReg(src) {
267
+ return src.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
268
+ }
269
+ function containsStylexImport(code, source) {
270
+ const s = escapeReg(typeof source === 'string' ? source : source.from);
271
+ const re = new RegExp(`(?:from\\s*['"]${s}['"]|import\\s*\\(\\s*['"]${s}['"]\\s*\\)|require\\s*\\(\\s*['"]${s}['"]\\s*\\)|^\\s*import\\s*['"]${s}['"])`, 'm');
272
+ return re.test(code);
273
+ }
274
+ function shouldHandle(code) {
275
+ if (!code) return false;
276
+ return importSources.some(src => containsStylexImport(code, src));
277
+ }
278
+ function resetState() {
279
+ stylexRulesById.clear();
280
+ if (devPersistToDisk) {
281
+ try {
282
+ fs.rmSync(DISK_RULES_PATH, {
283
+ force: true
284
+ });
285
+ } catch {}
286
+ }
287
+ }
288
+ function collectCss() {
289
+ const merged = new Map();
290
+ if (devPersistToDisk) {
291
+ try {
292
+ if (fs.existsSync(DISK_RULES_PATH)) {
293
+ const json = JSON.parse(fs.readFileSync(DISK_RULES_PATH, 'utf8'));
294
+ for (const [k, v] of Object.entries(json)) merged.set(k, v);
295
+ }
296
+ } catch {}
297
+ }
298
+ try {
299
+ const shared = getSharedStore().rulesById;
300
+ for (const [k, v] of shared.entries()) merged.set(k, v);
301
+ } catch {}
302
+ for (const [k, v] of stylexRulesById.entries()) merged.set(k, v);
303
+ const allRules = Array.from(merged.values()).flat();
304
+ return processCollectedRulesToCSS(allRules, {
305
+ useCSSLayers,
306
+ lightningcssOptions,
307
+ enableLTRRTLComments: stylexOptions?.enableLTRRTLComments
308
+ });
309
+ }
310
+ async function persistRulesToDisk(id, rules) {
311
+ if (!devPersistToDisk) return;
312
+ try {
313
+ let current = {};
314
+ try {
315
+ const txt = await fsp.readFile(DISK_RULES_PATH, 'utf8');
316
+ current = JSON.parse(txt);
317
+ } catch {}
318
+ if (rules && Array.isArray(rules) && rules.length > 0) {
319
+ current[id] = rules;
320
+ } else if (current[id]) {
321
+ delete current[id];
322
+ }
323
+ await fsp.writeFile(DISK_RULES_PATH, JSON.stringify(current), 'utf8');
324
+ } catch {}
325
+ }
326
+ const plugin = {
327
+ name: '@stylexjs/unplugin',
328
+ apply: (config, env) => {
329
+ try {
330
+ const command = env?.command || (typeof config === 'string' ? undefined : undefined);
331
+ if (devMode === 'off' && command === 'serve') return false;
332
+ } catch {}
333
+ return true;
334
+ },
335
+ enforce: 'pre',
336
+ buildStart() {
337
+ resetState();
338
+ },
339
+ buildEnd() {},
340
+ async transform(code, id) {
341
+ const JS_LIKE_RE = /\.[cm]?[jt]sx?(\?|$)/;
342
+ if (!JS_LIKE_RE.test(id)) return null;
343
+ if (!shouldHandle(code)) return null;
344
+ const dir = path.dirname(id);
345
+ const basename = path.basename(id);
346
+ const file = path.join(dir, basename.split('?')[0] || basename);
347
+ const result = await runBabelTransform(code, file, '@stylexjs/unplugin');
348
+ const {
349
+ metadata
350
+ } = result;
351
+ if (!stylexOptions.runtimeInjection) {
352
+ const hasRules = metadata && Array.isArray(metadata.stylex) && metadata.stylex.length > 0;
353
+ const shared = getSharedStore();
354
+ if (hasRules) {
355
+ stylexRulesById.set(id, metadata.stylex);
356
+ shared.rulesById.set(id, metadata.stylex);
357
+ shared.version++;
358
+ await persistRulesToDisk(id, metadata.stylex);
359
+ } else {
360
+ stylexRulesById.delete(id);
361
+ if (shared.rulesById.has(id)) {
362
+ shared.rulesById.delete(id);
363
+ shared.version++;
364
+ }
365
+ await persistRulesToDisk(id, []);
366
+ }
367
+ }
368
+ const ctx = this;
369
+ if (ctx && ctx.meta && ctx.meta.watchMode && typeof ctx.parse === 'function') {
370
+ try {
371
+ const ast = ctx.parse(result.code);
372
+ for (const stmt of ast.body) {
373
+ if (stmt.type === 'ImportDeclaration') {
374
+ const resolved = await ctx.resolve(stmt.source.value, id);
375
+ if (resolved && !resolved.external) {
376
+ const loaded = await ctx.load(resolved);
377
+ if (loaded && loaded.meta && 'stylex' in loaded.meta) {
378
+ stylexRulesById.set(resolved.id, loaded.meta.stylex);
379
+ }
380
+ }
381
+ }
382
+ }
383
+ } catch {}
384
+ }
385
+ return {
386
+ code: result.code,
387
+ map: result.map
388
+ };
389
+ },
390
+ shouldTransformCachedModule({
391
+ id,
392
+ meta
393
+ }) {
394
+ if (meta && 'stylex' in meta) {
395
+ stylexRulesById.set(id, meta.stylex);
396
+ }
397
+ return false;
398
+ }
399
+ };
400
+ plugin.__stylexCollectCss = collectCss;
401
+ plugin.__stylexResetState = resetState;
402
+ plugin.__stylexGetSharedStore = getSharedStore;
403
+ plugin.__stylexDevMode = devMode;
404
+ plugin.__stylexCssInjectionTarget = cssInjectionTarget;
405
+ plugin.__stylexPackages = stylexPackages;
406
+ return plugin;
407
+ };
408
+ const unpluginInstance = createUnplugin(unpluginFactory);
409
+ export default unpluginInstance;
410
+ export const unplugin = unpluginInstance;
@@ -0,0 +1,53 @@
1
+ import path from 'node:path';
2
+ import fs from 'node:fs';
3
+ import fsp from 'node:fs/promises';
4
+ import { createEsbuildPlugin } from 'unplugin';
5
+ import { unpluginFactory } from "./core.mjs";
6
+ function attachEsbuildHooks(plugin) {
7
+ return {
8
+ ...plugin,
9
+ esbuild: {
10
+ name: '@stylexjs/unplugin',
11
+ setup(build) {
12
+ build.onEnd(async result => {
13
+ try {
14
+ const css = plugin.__stylexCollectCss?.();
15
+ if (!css) return;
16
+ const initial = build.initialOptions;
17
+ const outDir = initial.outdir || (initial.outfile ? path.dirname(initial.outfile) : null);
18
+ if (!outDir) return;
19
+ let outfile = null;
20
+ const meta = result && result.metafile;
21
+ if (meta && meta.outputs) {
22
+ const outputs = Object.keys(meta.outputs);
23
+ const cssOutputs = outputs.filter(f => f.endsWith('.css'));
24
+ const pick = cssOutputs.find(f => /(^|\/)index\.css$/.test(f)) || cssOutputs.find(f => /(^|\/)style\.css$/.test(f)) || cssOutputs[0];
25
+ if (pick) outfile = path.isAbsolute(pick) ? pick : path.join(process.cwd(), pick);
26
+ } else {
27
+ try {
28
+ const files = fs.readdirSync(outDir).filter(f => f.endsWith('.css'));
29
+ const pick = files.find(f => /(^|\/)index\.css$/.test(f)) || files.find(f => /(^|\/)style\.css$/.test(f)) || files[0];
30
+ if (pick) outfile = path.join(outDir, pick);
31
+ } catch {}
32
+ }
33
+ if (!outfile) {
34
+ const fallback = path.join(outDir, 'stylex.css');
35
+ await fsp.mkdir(path.dirname(fallback), {
36
+ recursive: true
37
+ });
38
+ await fsp.writeFile(fallback, css, 'utf8');
39
+ return;
40
+ }
41
+ try {
42
+ const current = fs.readFileSync(outfile, 'utf8');
43
+ if (!current.includes(css)) {
44
+ await fsp.writeFile(outfile, current ? current + '\n' + css : css, 'utf8');
45
+ }
46
+ } catch {}
47
+ } catch {}
48
+ });
49
+ }
50
+ }
51
+ };
52
+ }
53
+ export default createEsbuildPlugin((options, metaOptions) => attachEsbuildHooks(unpluginFactory(options, metaOptions)));
@@ -0,0 +1,3 @@
1
+ import { createFarmPlugin } from 'unplugin';
2
+ import { unpluginFactory } from "./core.mjs";
3
+ export default createFarmPlugin(unpluginFactory);