24 | )
25 | }
26 | ```
27 |
28 | ## Id
29 |
30 | - [RFC](https://github.com/facebook/stylex/discussions/684) in stylex
31 |
32 | Note: This is not samiler with RFC. function `id` support pass a boolean flag. If pass `true` mean it works for `stylex-extend` self function, the default value is `flase`.
33 | If you want to use `id` with JSXAttribute `stylex` or `inline`. you should decalre a new id with `true`, Don't pass the id set to true to StyleX API itself.
34 |
35 | ### Usage
36 |
37 | ```tsx
38 | import { id } from '@stylex-extend/core'
39 | import { create, props } from '@stylexjs/js'
40 |
41 | const myId = id()
42 |
43 | const myId2 = id(true)
44 |
45 | const styles = create({
46 | parent: {
47 | [myId]: {
48 | default: 'red',
49 | ':hover': 'pink'
50 | }
51 | },
52 | child: {
53 | color: myId
54 | }
55 | })
56 |
57 | export function Component() {
58 | return (
59 |
60 |
61 |
62 | Purple
63 |
64 |
65 | )
66 | }
67 | ```
68 |
69 | ## injectGlobalStyle
70 |
71 | - unoffical API
72 |
73 | ```ts
74 | import { injectGlobalStyle } from '@stylex-extend/core'
75 | import { colors } from './colors.stylex'
76 |
77 | injectGlobalStyle({
78 | body: {
79 | fontSize: '30px',
80 | color: colors.pink,
81 | '> p': {
82 | color: 'red'
83 | }
84 | }
85 | })
86 | ```
87 |
--------------------------------------------------------------------------------
/packages/postcss/src/bundler.js:
--------------------------------------------------------------------------------
1 | const babel = require('@babel/core')
2 | const stylexBabelPlugin = require('@stylexjs/babel-plugin')
3 |
4 | // Creates a stateful bundler for processing StyleX rules using Babel.
5 | module.exports = function createBundler() {
6 | const styleXRulesMap = new Map()
7 | const globalCSS = {}
8 |
9 | // Determines if the source code should be transformed based on the presence of StyleX imports.
10 | function shouldTransform(sourceCode) {
11 | return sourceCode.includes('stylex')
12 | }
13 |
14 | // Transforms the source code using Babel, extracting StyleX rules and storing them.
15 | async function transform(id, sourceCode, babelConfig, options) {
16 | const { isDev, shouldSkipTransformError } = options
17 | const { code, map, metadata } = await babel
18 | .transformAsync(sourceCode, {
19 | filename: id,
20 | caller: {
21 | name: '@stylexjs/postcss-plugin',
22 | isDev
23 | },
24 | ...babelConfig
25 | })
26 | .catch((error) => {
27 | if (shouldSkipTransformError) {
28 | console.warn(
29 | `[@stylexjs/postcss-plugin] Failed to transform "${id}": ${error.message}`
30 | )
31 |
32 | return { code: sourceCode, map: null, metadata: {} }
33 | }
34 | throw error
35 | })
36 |
37 | const stylex = metadata.stylex
38 | if (stylex != null && stylex.length > 0) {
39 | styleXRulesMap.set(id, stylex)
40 | }
41 |
42 | const globalStyle = metadata.globalStyle
43 |
44 | if (globalStyle && globalStyle.length > 0) {
45 | globalCSS[id] = globalStyle
46 | }
47 |
48 | return { code, map, metadata }
49 | }
50 |
51 | // Removes the stored StyleX rules for the specified file.
52 | function remove(id) {
53 | styleXRulesMap.delete(id)
54 | }
55 |
56 | // Bundles all collected StyleX rules into a single CSS string.
57 | function bundle({ useCSSLayers }) {
58 | const rules = Array.from(styleXRulesMap.values()).flat()
59 |
60 | const css = stylexBabelPlugin.processStylexRules(rules, useCSSLayers) + '\n' + Object.values(globalCSS).join('\n')
61 |
62 | return css
63 | }
64 |
65 | return {
66 | shouldTransform,
67 | transform,
68 | remove,
69 | bundle
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/packages/vite/README.md:
--------------------------------------------------------------------------------
1 | # @stylex-extend/vite
2 |
3 | Experimental vite plugin
4 |
5 | ## Quick Start
6 |
7 | ### Install
8 |
9 | ```bash
10 | npm install --dev @stylex-extend/vite
11 | ```
12 |
13 | ### Usage
14 |
15 | ```ts
16 | import { stylex } from '@stylex-extend/vite'
17 | import { defineConfig } from 'vite'
18 |
19 | export default defineConfig({
20 | plugins: [
21 | // ... your plugins
22 | stylex()
23 | ]
24 | })
25 |
26 | // or using a postcss intergrate
27 |
28 | import { stylex } from '@stylex-extend/vite/postcss-ver'
29 |
30 | import { defineConfig } from 'vite'
31 |
32 | export default defineConfig({
33 | plugins: [
34 | // ... your plugins
35 | stylex({
36 | include: [],
37 | aliases: {}
38 | })
39 | ]
40 | })
41 | ```
42 |
43 | ## Options
44 |
45 | | params | type | default | description |
46 | | ---------------- | --------------------------------------------- | ------------------------------------------- | ---------------------------------------------------- |
47 | | `include` | `string \| RegExp \| Array` | `/\.(mjs\|js\|ts\|vue\|jsx\|tsx)(\?.*\|)$/` | Include all assets matching any of these conditions. |
48 | | `exclude` | `string \| RegExp \| Array` | `-` | Exclude all assets matching any of these conditions. |
49 | | `importSources` | `string[]` | `['stylex', '@stylexjs/stylex']` | See stylex document. |
50 | | `babelConfig` | `object` | `{}` | Babel config for stylex |
51 | | `useCSSLayers` | `boolean` | `false` | See stylex document |
52 | | `optimizedDeps` | `Array` | `[]` | Work with external stylex files or libraries |
53 | | `macroTransport` | `false \|object` | `'props'` | Using stylex extend macro |
54 |
55 | ## Author
56 |
57 | Kanno
58 |
59 | ## LICENSE
60 |
61 | [MIT](./LICENSE)
62 |
--------------------------------------------------------------------------------
/packages/babel-plugin/src/index.ts:
--------------------------------------------------------------------------------
1 | import type { ParserOptions, PluginObj } from '@babel/core'
2 | import { isImportDeclaration } from './ast/shared'
3 | import type { StylexExtendBabelPluginOptions } from './interface'
4 | import { Module } from './module'
5 | import type { PluginPass } from './module'
6 | import { transformId, transformInjectGlobalStyle, transformInline, transformStylexAttrs } from './visitor'
7 | import { FIELD, readImportStmt } from './visitor/imports'
8 |
9 | function declare(): PluginObj {
10 | return {
11 | name: '@stylex-extend',
12 | manipulateOptions(_, parserOpts: ParserOptions) {
13 | // https://babeljs.io/docs/babel-plugin-syntax-jsx
14 | // https://github.com/babel/babel/blob/main/packages/babel-plugin-syntax-typescript/src/index.ts
15 | if (!parserOpts.plugins) {
16 | parserOpts.plugins = []
17 | }
18 | const { plugins } = parserOpts
19 | if (
20 | plugins.some((p) => {
21 | const plugin = Array.isArray(p) ? p[0] : p
22 | return plugin === 'typescript' || plugin === 'jsx'
23 | })
24 | ) {
25 | return
26 | }
27 | plugins.push('jsx')
28 | },
29 | visitor: {
30 | Program: {
31 | enter(path, state) {
32 | const mod = new Module(path, state as PluginPass)
33 | readImportStmt(path.get('body'), mod)
34 | path.traverse({
35 | JSXAttribute(path) {
36 | transformStylexAttrs(path, mod)
37 | },
38 | CallExpression(path) {
39 | transformId(path, mod)
40 | transformInline(path, mod)
41 | transformInjectGlobalStyle(path, mod)
42 | }
43 | })
44 | },
45 | exit(path) {
46 | const body = path.get('body')
47 | for (const stmt of body) {
48 | if (isImportDeclaration(stmt)) {
49 | const s = stmt.get('source')
50 | if (s.isStringLiteral() && s.node.value === FIELD) {
51 | stmt.remove()
52 | }
53 | }
54 | }
55 | }
56 | }
57 | }
58 | }
59 | }
60 |
61 | function withOptions(options: Partial) {
62 | return [declare, options]
63 | }
64 |
65 | declare.withOptions = withOptions
66 |
67 | export type StylexExtendTransformObject = {
68 | (): PluginObj,
69 | withOptions: typeof withOptions
70 | }
71 |
72 | export default declare as unknown as StylexExtendTransformObject
73 |
74 | export type { StylexExtendBabelPluginOptions }
75 |
--------------------------------------------------------------------------------
/packages/postcss/src/index.js:
--------------------------------------------------------------------------------
1 | // Fork from https://github.com/facebook/stylex/blob/main/packages/postcss-plugin/src/index.js
2 |
3 | const postcss = require('postcss')
4 |
5 | const createBuilder = require('./builder')
6 |
7 | const PLUGIN_NAME = '@stylex-extend/postcss-plugin'
8 |
9 | const builder = createBuilder()
10 |
11 | const isDev = process.env.NODE_ENV === 'development'
12 |
13 | function plugin(options = {}) {
14 | const {
15 | cwd = process.cwd(),
16 | babelConfig = {},
17 | include,
18 | exclude: _exclude,
19 | useCSSLayers = false
20 | } = options
21 | exclude = [
22 | // Exclude type declaration files by default because it never contains any CSS rules.
23 | '**/*.d.ts',
24 | '**/*.flow',
25 | ...(_exclude ?? [])
26 | ]
27 |
28 | // Whether to skip the error when transforming StyleX rules.
29 | // Useful in watch mode where Fast Refresh can recover from errors.
30 | // Initial transform will still throw errors in watch mode to surface issues early.
31 | let shouldSkipTransformError = false
32 |
33 | return {
34 | postcssPlugin: PLUGIN_NAME,
35 | plugins: [
36 | // Processes the PostCSS root node to find and transform StyleX at-rules.
37 | async function(root, result) {
38 | const fileName = result.opts.from
39 |
40 | // Configure the builder with the provided options
41 | await builder.configure({
42 | include,
43 | exclude,
44 | cwd,
45 | babelConfig,
46 | useCSSLayers,
47 | isDev
48 | })
49 |
50 | // Find the "@stylex" at-rule
51 | const styleXAtRule = builder.findStyleXAtRule(root)
52 | if (styleXAtRule == null) {
53 | return
54 | }
55 |
56 | // Get dependencies to be watched for changes
57 | const dependencies = builder.getDependencies()
58 |
59 | // Add each dependency to the PostCSS result messages.
60 | // This watches the entire "./src" directory for "./src/**/*.{ts,tsx}"
61 | // to handle new files and deletions reliably in watch mode.
62 | for (const dependency of dependencies) {
63 | result.messages.push({
64 | plugin: PLUGIN_NAME,
65 | parent: fileName,
66 | ...dependency
67 | })
68 | }
69 |
70 | // Build and parse the CSS from collected StyleX rules
71 | const css = await builder.build({
72 | shouldSkipTransformError
73 | })
74 | const parsed = await postcss.parse(css, {
75 | from: fileName
76 | })
77 |
78 | // Replace the "@stylex" rule with the generated CSS
79 | styleXAtRule.replaceWith(parsed)
80 |
81 | result.root = root
82 |
83 | if (!shouldSkipTransformError) {
84 | // Build was successful, subsequent builds are for watch mode
85 | shouldSkipTransformError = true
86 | }
87 | }
88 | ]
89 | }
90 | }
91 |
92 | plugin.postcss = true
93 |
94 | module.exports = plugin
95 |
--------------------------------------------------------------------------------
/packages/babel-plugin/src/visitor/inline.ts:
--------------------------------------------------------------------------------
1 | // inline is same as stylex macros
2 |
3 | import { types } from '@babel/core'
4 | import type { NodePath } from '@babel/core'
5 | import { evaluateCSS, printJsAST } from '../ast/evaluate-path'
6 | import { MESSAGES } from '../ast/message'
7 | import { callExpression, findNearestTopLevelAncestor, isIdentifier, isMemberExpression, isObjectExpression, make } from '../ast/shared'
8 | import { Module } from '../module'
9 | import { APIS, insertRelativePackage } from './imports'
10 |
11 | function validateInlineMacro(
12 | path: NodePath[]
13 | ) {
14 | if (path.length > 1) { throw new Error(MESSAGES.INLINE_ONLY_ONE_ARGUMENT) }
15 | if (isObjectExpression(path[0])) {
16 | return path[0]
17 | }
18 | throw new Error(MESSAGES.INVALID_INLINE_ARGUMENT)
19 | }
20 |
21 | export type ExtendMacroKeys = 'inline' | 'injectGlobalStyle' | 'id'
22 |
23 | export function getExtendMacro(path: NodePath, mod: Module, expected: ExtendMacroKeys) {
24 | if (!path.node) { return }
25 | const callee = path.get('callee')
26 | if (isIdentifier(callee) && mod.extendImports.get(callee.node.name) === expected) {
27 | path.skip()
28 | return path
29 | }
30 | if (isMemberExpression(callee)) {
31 | const obj = callee.get('object')
32 | const prop = callee.get('property')
33 | if (isIdentifier(obj) && isIdentifier(prop)) {
34 | if (mod.extendImports.has(obj.node.name) && APIS.has(prop.node.name) && prop.node.name === expected) {
35 | path.skip()
36 | return path
37 | }
38 | }
39 | }
40 | }
41 |
42 | function insertAndReplace(
43 | path: NodePath,
44 | mod: Module,
45 | handler: (p: NodePath, applied: NodePath, expr: types.Expression[]) => void
46 | ) {
47 | const callee = getExtendMacro(path, mod, 'inline')
48 | if (callee) {
49 | const expr = validateInlineMacro(callee.get('arguments'))
50 | const { expressions, properties, into } = printJsAST(evaluateCSS(expr, mod), expr)
51 | const [create, applied] = insertRelativePackage(mod.program, mod)
52 | const declaration = make.variableDeclaration(into, callExpression(create.node, [make.objectExpression(properties)]))
53 | const nearest = findNearestTopLevelAncestor(path)
54 | nearest.insertBefore(declaration)
55 | handler(path, applied, expressions)
56 | }
57 | }
58 |
59 | // inline processing two scenes.
60 | // 1. as props/attrs function argument
61 | // 2. call it as single.
62 |
63 | export function transformInline(path: NodePath, mod: Module) {
64 | // check path
65 | if (path.parent.type === 'CallExpression') {
66 | insertAndReplace(path, mod, (p, _, expressions) => {
67 | p.replaceInline(expressions)
68 | })
69 | return
70 | }
71 | // single call
72 |
73 | insertAndReplace(path, mod, (p, applied, expressions) => {
74 | p.replaceWith(make.callExpression(applied.node, expressions))
75 | })
76 | }
77 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## 0.7.1 (2025-3-4)
2 |
3 | ### Patches
4 |
5 | - Fix windows system file resolve
6 | - Better types for macro
7 |
8 | ## 0.7.0 (2025-3-4)
9 |
10 | ### Features
11 |
12 | - Remove the call restrictions of `inline` API.
13 | - Add new packages (postcss).
14 | - vite plugin provides offical postcss binding.
15 | - Upgrade `@stylexjs/babel-plugin` from `0.9.3` to `0.11.1`
16 |
17 | ## 0.6.0 (2025-1-15)
18 |
19 | ### Features
20 |
21 | - Add new api `id`. Details see [RFC](https://github.com/facebook/stylex/discussions/684)
22 |
23 | ## 0.5.6 (2025-1-13)
24 |
25 | Fallback `unstable_moduleResolution`
26 |
27 | ## 0.5.5 (2025-1-13)
28 |
29 | - Fix `vite-plugin` sourcemap missing.
30 | - Fix `babel-plugin` should respect windows.
31 |
32 | ## v0.5.2 (2024-12-23)
33 |
34 | - Synchronize official changes to `rootdir`.
35 |
36 | ## v0.5.1 (2024-12-21)
37 |
38 | - Fix `vite-plugin` error regexp.
39 |
40 | ## v0.5.0 (2024-12-20)
41 |
42 | - Upgrade `@stylexjs` dependencies version.
43 | - Perf `@stylex-extend/core` types.
44 | - Vite plugin support translate `jsx` in SFC.
45 |
46 | ## v0.4.4 (2024-10-09)
47 |
48 | ### Patches
49 |
50 | - Fix `vite-plugin` un respect dev mode.
51 |
52 | ## v0.4.3 (2024-09-26)
53 |
54 | ### Patches
55 |
56 | - Fix `babel-plugin` unexpected css merge.
57 |
58 | ## v0.4.2 (2024-09-26)
59 |
60 | ### Patches
61 |
62 | - Fix `babel-plugin` import scan.
63 | - Fix `vite plugin` aliases error handling.
64 |
65 | ## v0.4.1 (2024-09-25)
66 |
67 | ### Patches
68 |
69 | - Fix uncapture ast kind.
70 |
71 | ## v0.4.0 (2024-09-25)
72 |
73 | ### Features
74 |
75 | - Add vite integration
76 |
77 | ## v0.3.3 (2024-06-25)
78 |
79 | ### Patches
80 |
81 | - Fix `injectGlobalStyle` can't handle template literal.
82 | - Fix `@stylex-extend/babel-plugin` can't work with esm.
83 |
84 | ## v0.3.2 (2024-05-30)
85 |
86 | ### Patches
87 |
88 | - Fix `@stylex-extend/react` types error.
89 |
90 | ## v0.3.1 (2024-05-08)
91 |
92 | ### Patches
93 |
94 | - Fix babel plugin can't handle callee.
95 |
96 | ## v0.3.0 (2024-05-08)
97 |
98 | ### Features
99 |
100 | - Add new api `inline`. Details see [RFC](https://github.com/facebook/stylex/issues/534)
101 |
102 | ## v0.2.3 (2024-04-29)
103 |
104 | ### Patches
105 |
106 | - Fix `@stylex-extend/babel-plugin` can't import not js file.
107 |
108 | ## v0.2.2 (2024-04-27)
109 |
110 | ### Patches
111 |
112 | - Fix `@stylex-extend/babel-plugin` duplicate variables.
113 |
114 | ## v0.2.1 (2024-04-18)
115 |
116 | ### Patches
117 |
118 | - Fix `@stylex-extend/babel-plugin` dynmiac variable generate.
119 |
120 | ## v0.2.0 (2024-04-17)
121 |
122 | ### Features
123 |
124 | - Expose new package `@stylex-extend/core`
125 |
126 | ## v0.1.3 (2024-04-15)
127 |
128 | ### Improve
129 |
130 | - Perf code generation.
131 |
132 | ### Patches
133 |
134 | - Fix `@stylex-extend/react` types error.
135 | - Fix `@stylex-extend/react` don't expose jsx helper.
136 | - Fix `@stylex-extend/babel-plugin` spread syntax error.
137 |
138 | ### Credits
139 |
140 | @mengdaoshizhongxinyang
141 |
142 | ## v0.1.2 (2024-04-10)
143 |
144 | ### Patches
145 |
146 | - Fix `workspace` not being replaced.
147 |
148 | ## v0.1.0 (2024-04-10)
149 |
150 | The first version
151 |
--------------------------------------------------------------------------------
/packages/vite/src/compile.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-labels */
2 | // shared with postcss-ver / normal ver
3 | // To be honest, the normal verion is better than the postcss version.
4 | // postcss ver create a new monitor to watch the project and pipe result into vite intenral graph.
5 |
6 | // Difference
7 | // normal ver will handle all aliases in plugin side.
8 | // postcss ver will handle all aliases in babel side.
9 |
10 | import type { ParserOptions, PluginItem } from '@babel/core'
11 | import { transformAsync } from '@babel/core'
12 | import extendBabelPlugin from '@stylex-extend/babel-plugin'
13 | import stylexBabelPlugin from '@stylexjs/babel-plugin'
14 | import path from 'path'
15 |
16 | type BabelParserPlugins = ParserOptions['plugins']
17 |
18 | export type TransformType = 'extend' | 'standard'
19 |
20 | export interface TransformStyleXOptions {
21 | code: string
22 | filename: string
23 | options: T extends 'extend' ? Parameters[0]
24 | : Parameters[0]
25 | parserOpts?: ParserOptions
26 | }
27 |
28 | export interface BabelConfig {
29 | plugins?: PluginItem[]
30 | presets?: PluginItem[]
31 | }
32 |
33 | const plugins = new Map<'extend' | 'standard', typeof extendBabelPlugin | typeof stylexBabelPlugin>()
34 |
35 | export function interopDefault(m: T | { default: T }): T {
36 | return (m as { default: T }).default ?? m as T
37 | }
38 |
39 | export async function getPlugin(t: T) {
40 | let plugin: typeof extendBabelPlugin | typeof stylexBabelPlugin = plugins.get(t)!
41 | if (!plugin) {
42 | try {
43 | plugin = interopDefault(t === 'extend' ? extendBabelPlugin : stylexBabelPlugin)
44 | } catch {
45 | plugin = await import(t === 'extend' ? '@stylex-extend/babel-plugin' : '@stylexjs/babel-plugin').then((
46 | m: typeof extendBabelPlugin | typeof stylexBabelPlugin
47 | ) => interopDefault(m))
48 | }
49 | plugins.set(t, plugin)
50 | }
51 | return plugin
52 | }
53 |
54 | export async function transformStyleX(t: T, opts: TransformStyleXOptions, babelConfig?: BabelConfig) {
55 | const plugin = await getPlugin(t)
56 | if (!plugin) { throw new Error('Plugin not found') }
57 | return transformAsync(opts.code, {
58 | babelrc: false,
59 | filename: opts.filename,
60 | plugins: [
61 | ...((babelConfig?.plugins) ?? []),
62 | [plugin, opts.options]
63 | ],
64 | presets: babelConfig?.presets ?? [],
65 | parserOpts: opts.parserOpts,
66 | generatorOpts: {
67 | sourceMaps: true
68 | }
69 | })
70 | }
71 |
72 | export function ensureParserOpts(id: string): BabelParserPlugins | false {
73 | const plugins: BabelParserPlugins = []
74 | const [original, ...rest] = id.split('?')
75 | const extension = path.extname(original).slice(1)
76 | if (extension === 'jsx' || extension === 'tsx') {
77 | plugins.push('jsx')
78 | }
79 | if (extension === 'ts' || extension === 'tsx') {
80 | plugins.push('typescript')
81 | }
82 | // vue&type=script&lang.tsx
83 | // vue&type=script&setup=true&lang.tsx
84 | // For vue and ...etc
85 | if (extension === 'vue') {
86 | // Check if is from unplugin-vue-router (Hard code here)
87 | for (const spec of rest) {
88 | if (spec.includes('definePage')) {
89 | return false
90 | }
91 | }
92 | loop: for (;;) {
93 | const current = rest.shift()
94 | if (!current) { break loop }
95 | const matched = current.match(/lang\.(\w+)/)
96 | if (matched) {
97 | const lang = matched[1]
98 | if (lang === 'jsx' || lang === 'tsx') {
99 | plugins.push('jsx')
100 | }
101 | if (lang === 'ts' || lang === 'tsx') {
102 | plugins.push('typescript')
103 | }
104 | break loop
105 | }
106 | }
107 | }
108 | return plugins
109 | }
110 |
111 | function getExt(p: string) {
112 | const [filename] = p.split('?', 2)
113 | return path.extname(filename).slice(1)
114 | }
115 |
116 | export function isPotentialCSSFile(id: string) {
117 | const extension = getExt(id)
118 | return extension === 'css' || (extension === 'vue' && id.includes('&lang.css')) || (extension === 'astro' && id.includes('&lang.css'))
119 | }
120 |
--------------------------------------------------------------------------------
/packages/babel-plugin/src/visitor/imports.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable react-hooks/rules-of-hooks */
2 | /* eslint-disable @eslint-react/hooks-extra/no-useless-custom-hooks */
3 | import { types } from '@babel/core'
4 | import { NodePath } from '@babel/core'
5 | import { Iter } from '../ast/evaluate-path'
6 | import { findNearestParentWithCondition, getStringLikeKindValue, isImportDeclaration, isImportSpecifier, make } from '../ast/shared'
7 | import { Module } from '../module'
8 |
9 | export const FIELD = '@stylex-extend/core'
10 | const STYLEX = '@stylexjs/stylex'
11 |
12 | export const APIS = new Set(['inline', 'injectGlobalStyle', 'id'])
13 |
14 | export function readImportStmt(stmts: NodePath[], mod: Module) {
15 | for (const stmt of stmts) {
16 | if (isImportDeclaration(stmt)) {
17 | const s = getStringLikeKindValue(stmt.get('source'))
18 | if (s === FIELD) {
19 | for (const specifier of stmt.node.specifiers) {
20 | switch (specifier.type) {
21 | case 'ImportDefaultSpecifier':
22 | case 'ImportNamespaceSpecifier':
23 | mod.extendImports.set(specifier.local.name, specifier.local.name)
24 | break
25 | case 'ImportSpecifier':
26 | if (APIS.has(getStringLikeKindValue(specifier.imported))) {
27 | mod.extendImports.set(specifier.local.name, getStringLikeKindValue(specifier.imported))
28 | }
29 | }
30 | }
31 | }
32 | }
33 | }
34 | }
35 |
36 | export interface ImportState {
37 | insert: boolean
38 | }
39 |
40 | const state = new WeakMap>()
41 | // insert relative package import stmt
42 |
43 | function useCreatingNodePath(
44 | path: NodePath,
45 | kind: K,
46 | importIdentifiers: string[]
47 | ): K extends 'local' ? NodePath[] : NodePath[] {
48 | const x = new Set(importIdentifiers)
49 | const tpl: NodePath[] = []
50 | for (const specifier of path.get('specifiers') as NodePath[]) {
51 | const s = specifier.get(kind)
52 | if (x.has(getStringLikeKindValue(specifier.get('imported')))) {
53 | // @ts-expect-error safe
54 | tpl.push(s)
55 | }
56 | }
57 | return tpl.sort((a) => {
58 | const aName = getStringLikeKindValue(a)
59 | if (aName === 'create') { return -1 }
60 | return 0
61 | }) as K extends 'local' ? NodePath[] : NodePath[]
62 | }
63 |
64 | export function insertRelativePackage(program: NodePath, mod: Module) {
65 | const { importState, importIdentifiers } = mod
66 | const { bindings } = program.scope
67 | const [create, applied] = importIdentifiers
68 |
69 | if (state.has(importState)) { return useCreatingNodePath(state.get(importState)!, 'local', importIdentifiers) }
70 | let importDeclaration: NodePath | null = null
71 | for (const { key, value } of new Iter(bindings)) {
72 | if (key === create || key === applied) {
73 | if (isImportSpecifier(value.path)) {
74 | const declaration = findNearestParentWithCondition(value.path, isImportDeclaration)
75 | if (declaration.node.source.value === STYLEX) {
76 | importDeclaration = declaration
77 | }
78 | break
79 | }
80 | }
81 | }
82 |
83 | if (importDeclaration) {
84 | const specifiers = new Set(useCreatingNodePath(importDeclaration, 'imported', importIdentifiers).map(getStringLikeKindValue))
85 | const diffs = importIdentifiers.filter((id) => !specifiers.has(id))
86 | const importSpecifiers = diffs.map((id) => make.importSpecifier(program.scope.generateUidIdentifier(id), make.identifier(id)))
87 | importDeclaration.unshiftContainer('specifiers', importSpecifiers)
88 | state.set(importState, importDeclaration)
89 | }
90 |
91 | if (!state.has(importState)) {
92 | const importSpecifiers = [
93 | make.importSpecifier(program.scope.generateUidIdentifier(create), make.identifier(create)),
94 | make.importSpecifier(program.scope.generateUidIdentifier(applied), make.identifier(applied))
95 | ]
96 | const declaration = make.importDeclaration(importSpecifiers, make.stringLiteral(STYLEX))
97 | const lastest = program.unshiftContainer('body', declaration)
98 | state.set(importState, lastest[0])
99 | }
100 |
101 | return useCreatingNodePath(state.get(importState)!, 'local', importIdentifiers)
102 | }
103 |
--------------------------------------------------------------------------------
/packages/postcss/src/builder.js:
--------------------------------------------------------------------------------
1 | const path = require('node:path')
2 | const fs = require('node:fs')
3 | const { normalize, resolve } = require('path')
4 | const { globSync } = require('fast-glob')
5 | const isGlob = require('is-glob')
6 | const globParent = require('glob-parent')
7 | const createBundler = require('./bundler')
8 |
9 | // Parses a glob pattern and extracts its base directory and pattern.
10 | // Returns an object with `base` and `glob` properties.
11 | function parseGlob(pattern) {
12 | // License: MIT
13 | // Based on:
14 | // https://github.com/chakra-ui/panda/blob/6ab003795c0b076efe6879a2e6a2a548cb96580e/packages/node/src/parse-glob.ts
15 | let glob = pattern
16 | const base = globParent(pattern)
17 |
18 | if (base !== '.') {
19 | glob = pattern.substring(base.length)
20 | if (glob.charAt(0) === '/') {
21 | glob = glob.substring(1)
22 | }
23 | }
24 |
25 | if (glob.substring(0, 2) === './') {
26 | glob = glob.substring(2)
27 | }
28 | if (glob.charAt(0) === '/') {
29 | glob = glob.substring(1)
30 | }
31 |
32 | return { base, glob }
33 | }
34 |
35 | // Parses a file path or glob pattern into a PostCSS dependency message.
36 | function parseDependency(fileOrGlob) {
37 | // License: MIT
38 | // Based on:
39 | // https://github.com/chakra-ui/panda/blob/6ab003795c0b076efe6879a2e6a2a548cb96580e/packages/node/src/parse-dependency.ts
40 | if (fileOrGlob.startsWith('!')) {
41 | return null
42 | }
43 |
44 | let message = null
45 |
46 | if (isGlob(fileOrGlob)) {
47 | const { base, glob } = parseGlob(fileOrGlob)
48 | message = { type: 'dir-dependency', dir: normalize(resolve(base)), glob }
49 | } else {
50 | message = { type: 'dependency', file: normalize(resolve(fileOrGlob)) }
51 | }
52 |
53 | return message
54 | }
55 |
56 | // Creates a builder for transforming files and bundling StyleX CSS.
57 | function createBuilder() {
58 | let config = null
59 |
60 | const bundler = createBundler()
61 |
62 | const fileModifiedMap = new Map()
63 |
64 | // Configures the builder with the provided options.
65 | function configure(options) {
66 | config = options
67 | }
68 |
69 | /// Retrieves the current configuration.
70 | function getConfig() {
71 | if (config == null) {
72 | throw new Error('Builder not configured')
73 | }
74 | return config
75 | }
76 |
77 | // Finds the `@stylex;` at-rule in the provided PostCSS root.
78 | function findStyleXAtRule(root) {
79 | let styleXAtRule = null
80 | root.walkAtRules((atRule) => {
81 | if (atRule.name === 'stylex' && !atRule.params) {
82 | styleXAtRule = atRule
83 | }
84 | })
85 | return styleXAtRule
86 | }
87 |
88 | // Retrieves all files that match the include and exclude patterns.
89 | function getFiles() {
90 | const { cwd, include, exclude } = getConfig()
91 | return globSync(include, {
92 | onlyFiles: true,
93 | ignore: exclude,
94 | cwd
95 | })
96 | }
97 |
98 | // Transforms the included files, bundles the CSS, and returns the result.
99 | async function build({ shouldSkipTransformError }) {
100 | const { cwd, babelConfig, useCSSLayers, isDev } = getConfig()
101 |
102 | const files = getFiles()
103 | const filesToTransform = []
104 |
105 | // Remove deleted files since the last build
106 | for (const file of fileModifiedMap.keys()) {
107 | if (!files.includes(file)) {
108 | fileModifiedMap.delete(file)
109 | bundler.remove(file)
110 | }
111 | }
112 |
113 | for (const file of files) {
114 | const filePath = path.resolve(cwd, file)
115 | const mtimeMs = fs.existsSync(filePath)
116 | ? fs.statSync(filePath).mtimeMs
117 | : -Infinity
118 |
119 | // Skip files that have not been modified since the last build
120 | // On first run, all files will be transformed
121 | const shouldSkip = fileModifiedMap.has(file) && mtimeMs === fileModifiedMap.get(file)
122 |
123 | if (shouldSkip) {
124 | continue
125 | }
126 |
127 | fileModifiedMap.set(file, mtimeMs)
128 | filesToTransform.push(file)
129 | }
130 |
131 | await Promise.all(
132 | filesToTransform.map((file) => {
133 | const filePath = path.resolve(cwd, file)
134 | const contents = fs.readFileSync(filePath, 'utf-8')
135 | if (!bundler.shouldTransform(contents)) {
136 | // eslint-disable-next-line array-callback-return
137 | return
138 | }
139 | return bundler.transform(filePath, contents, babelConfig, {
140 | isDev,
141 | shouldSkipTransformError
142 | })
143 | })
144 | )
145 |
146 | const css = bundler.bundle({ useCSSLayers })
147 | return css
148 | }
149 |
150 | // Retrieves the dependencies that PostCSS should watch.
151 | function getDependencies() {
152 | const { include } = getConfig()
153 | const dependencies = []
154 |
155 | for (const fileOrGlob of include) {
156 | const dependency = parseDependency(fileOrGlob)
157 | if (dependency != null) {
158 | dependencies.push(dependency)
159 | }
160 | }
161 |
162 | return dependencies
163 | }
164 |
165 | return {
166 | findStyleXAtRule,
167 | configure,
168 | build,
169 | getDependencies
170 | }
171 | }
172 |
173 | module.exports = createBuilder
174 |
--------------------------------------------------------------------------------
/.github/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | We as members, contributors, and leaders pledge to make participation in our
6 | community a harassment-free experience for everyone, regardless of age, body
7 | size, visible or invisible disability, ethnicity, sex characteristics, gender
8 | identity and expression, level of experience, education, socio-economic status,
9 | nationality, personal appearance, race, religion, or sexual identity
10 | and orientation.
11 |
12 | We pledge to act and interact in ways that contribute to an open, welcoming,
13 | diverse, inclusive, and healthy community.
14 |
15 | ## Our Standards
16 |
17 | Examples of behavior that contributes to a positive environment for our
18 | community include:
19 |
20 | - Demonstrating empathy and kindness toward other people
21 | - Being respectful of differing opinions, viewpoints, and experiences
22 | - Giving and gracefully accepting constructive feedback
23 | - Accepting responsibility and apologizing to those affected by our mistakes,
24 | and learning from the experience
25 | - Focusing on what is best not just for us as individuals, but for the
26 | overall community
27 |
28 | Examples of unacceptable behavior include:
29 |
30 | - The use of sexualized language or imagery, and sexual attention or
31 | advances of any kind
32 | - Trolling, insulting or derogatory comments, and personal or political attacks
33 | - Public or private harassment
34 | - Publishing others' private information, such as a physical or email
35 | address, without their explicit permission
36 | - Other conduct which could reasonably be considered inappropriate in a
37 | professional setting
38 |
39 | ## Enforcement Responsibilities
40 |
41 | Community leaders are responsible for clarifying and enforcing our standards of
42 | acceptable behavior and will take appropriate and fair corrective action in
43 | response to any behavior that they deem inappropriate, threatening, offensive,
44 | or harmful.
45 |
46 | Community leaders have the right and responsibility to remove, edit, or reject
47 | comments, commits, code, wiki edits, issues, and other contributions that are
48 | not aligned to this Code of Conduct, and will communicate reasons for moderation
49 | decisions when appropriate.
50 |
51 | ## Scope
52 |
53 | This Code of Conduct applies within all community spaces, and also applies when
54 | an individual is officially representing the community in public spaces.
55 | Examples of representing our community include using an official e-mail address,
56 | posting via an official social media account, or acting as an appointed
57 | representative at an online or offline event.
58 |
59 | ## Enforcement
60 |
61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
62 | reported to the community leaders responsible for enforcement at
63 | .
64 | All complaints will be reviewed and investigated promptly and fairly.
65 |
66 | All community leaders are obligated to respect the privacy and security of the
67 | reporter of any incident.
68 |
69 | ## Enforcement Guidelines
70 |
71 | Community leaders will follow these Community Impact Guidelines in determining
72 | the consequences for any action they deem in violation of this Code of Conduct:
73 |
74 | ### 1. Correction
75 |
76 | **Community Impact**: Use of inappropriate language or other behavior deemed
77 | unprofessional or unwelcome in the community.
78 |
79 | **Consequence**: A private, written warning from community leaders, providing
80 | clarity around the nature of the violation and an explanation of why the
81 | behavior was inappropriate. A public apology may be requested.
82 |
83 | ### 2. Warning
84 |
85 | **Community Impact**: A violation through a single incident or series
86 | of actions.
87 |
88 | **Consequence**: A warning with consequences for continued behavior. No
89 | interaction with the people involved, including unsolicited interaction with
90 | those enforcing the Code of Conduct, for a specified period of time. This
91 | includes avoiding interactions in community spaces as well as external channels
92 | like social media. Violating these terms may lead to a temporary or
93 | permanent ban.
94 |
95 | ### 3. Temporary Ban
96 |
97 | **Community Impact**: A serious violation of community standards, including
98 | sustained inappropriate behavior.
99 |
100 | **Consequence**: A temporary ban from any sort of interaction or public
101 | communication with the community for a specified period of time. No public or
102 | private interaction with the people involved, including unsolicited interaction
103 | with those enforcing the Code of Conduct, is allowed during this period.
104 | Violating these terms may lead to a permanent ban.
105 |
106 | ### 4. Permanent Ban
107 |
108 | **Community Impact**: Demonstrating a pattern of violation of community
109 | standards, including sustained inappropriate behavior, harassment of an
110 | individual, or aggression toward or disparagement of classes of individuals.
111 |
112 | **Consequence**: A permanent ban from any sort of public interaction within
113 | the community.
114 |
115 | ## Attribution
116 |
117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage],
118 | version 2.0, available at
119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
120 |
121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity).
122 |
123 | [homepage]: https://www.contributor-covenant.org
124 |
125 | For answers to common questions about this code of conduct, see the FAQ at
126 | https://www.contributor-covenant.org/faq. Translations are available at
127 | https://www.contributor-covenant.org/translations.
128 |
--------------------------------------------------------------------------------
/packages/babel-plugin/src/module.ts:
--------------------------------------------------------------------------------
1 | import type { NodePath, PluginPass as BabelPluginPass, types } from '@babel/core'
2 | import { moduleResolve } from '@dual-bundle/import-meta-resolve'
3 | import fs from 'fs'
4 | import path from 'path'
5 | import url from 'url'
6 | import * as v from 'valibot'
7 | import type { StylexExtendBabelPluginOptions } from './interface'
8 | import { ImportState } from './visitor/imports'
9 |
10 | const unstable_moduleResolution = {
11 | CommonJs: 'commonJS',
12 | Haste: 'haste',
13 | ExperimentalCrossFileParsing: 'experimental_crossFileParsing'
14 | } as const
15 |
16 | const schema = v.object({
17 | transport: v.optional(v.string(), 'props'),
18 | aliases: v.optional(v.record(v.string(), v.union([v.string(), v.array(v.string())])), {}),
19 | classNamePrefix: v.optional(v.string(), 'x'),
20 | unstable_moduleResolution: v.optional(
21 | v.object({
22 | type: v.enum(unstable_moduleResolution),
23 | rootDir: v.optional(v.string(), ''),
24 | themeFileExtension: v.optional(v.string(), '.stylex')
25 | }),
26 | {
27 | type: 'commonJS',
28 | rootDir: process.cwd(),
29 | themeFileExtension: '.stylex'
30 | }
31 | )
32 | })
33 |
34 | export interface PluginPass extends BabelPluginPass {
35 | file: Omit & {
36 | metadata: {
37 | globalStyle: string[]
38 | }
39 | }
40 | }
41 |
42 | export class Module {
43 | options: StylexExtendBabelPluginOptions
44 | filename: string
45 | extendImports: Map
46 | program: NodePath
47 | importState: ImportState
48 | private state: PluginPass
49 | constructor(program: NodePath, opts: PluginPass) {
50 | this.filename = opts.filename || (opts.file.opts?.sourceFileName ?? '')
51 | this.options = v.parse(schema, opts.opts)
52 | this.extendImports = new Map()
53 | this.program = program
54 | this.state = opts
55 | this.state.file.metadata.globalStyle = []
56 | this.importState = { insert: false }
57 | }
58 |
59 | addStyle(style: string) {
60 | this.state.file.metadata.globalStyle.push(style)
61 | }
62 |
63 | get importIdentifiers() {
64 | return ['create', this.options.transport] as ['create', 'props' | 'attrs']
65 | }
66 | get cwd() {
67 | return this.state.cwd
68 | }
69 |
70 | fileNameForHashing(relativePath: string) {
71 | const fileName = filePathResolver(relativePath, this.filename, this.options.aliases)
72 | const { themeFileExtension = '.stylex', type } = this.options.unstable_moduleResolution ?? {}
73 | if (!fileName || !matchFileSuffix(themeFileExtension!)(fileName) || this.options.unstable_moduleResolution == null) {
74 | return null
75 | }
76 | switch (type) {
77 | case 'haste':
78 | return path.basename(fileName)
79 | default:
80 | return getCanonicalFilePath(fileName)
81 | }
82 | }
83 | }
84 |
85 | const FILE_EXTENSIONS = ['.js', '.ts', '.tsx', '.jsx', '.mjs', '.cjs']
86 |
87 | function possibleAliasedPaths(
88 | importPath: string,
89 | aliases: StylexExtendBabelPluginOptions['aliases']
90 | ): string[] {
91 | const result = [importPath]
92 | if (aliases == null || Object.keys(aliases).length === 0) {
93 | return result
94 | }
95 | for (const [alias, _value] of Object.entries(aliases)) {
96 | const value = Array.isArray(_value) ? _value : [_value]
97 | if (alias.includes('*')) {
98 | const [before, after] = alias.split('*')
99 | if (importPath.startsWith(before) && importPath.endsWith(after)) {
100 | const replacementString = importPath.slice(
101 | before.length,
102 | after.length > 0 ? -after.length : undefined
103 | )
104 | value.forEach((v) => {
105 | result.push(v.split('*').join(replacementString))
106 | })
107 | }
108 | } else if (alias === importPath) {
109 | value.forEach((v) => {
110 | result.push(v)
111 | })
112 | }
113 | }
114 |
115 | return result
116 | }
117 |
118 | function filePathResolver(relativeFilePath: string, sourceFilePath: string, aliases: StylexExtendBabelPluginOptions['aliases']) {
119 | for (const ext of ['', ...FILE_EXTENSIONS]) {
120 | const importPathStr = relativeFilePath + ext
121 |
122 | if (relativeFilePath[0] === '.') {
123 | try {
124 | return url.fileURLToPath(moduleResolve(importPathStr, url.pathToFileURL(sourceFilePath)))
125 | } catch {
126 | continue
127 | }
128 | } else {
129 | const allAliases = possibleAliasedPaths(importPathStr, aliases)
130 | // Otherwise, try to resolve the path with aliases
131 | for (const possiblePath of allAliases) {
132 | try {
133 | const resolved = moduleResolve(url.pathToFileURL(possiblePath).href, url.pathToFileURL(path.resolve(sourceFilePath)))
134 | return url.fileURLToPath(resolved)
135 | } catch {
136 | continue
137 | }
138 | }
139 | }
140 | }
141 | // Failed to resolve the file path
142 | return null
143 | }
144 |
145 | function matchFileSuffix(allowedSuffix: string) {
146 | const merged = [...FILE_EXTENSIONS].map((s) => allowedSuffix + s)
147 | return (filename: string) => {
148 | for (const ext of merged) {
149 | if (filename.endsWith(ext)) { return true }
150 | }
151 | return filename.endsWith(allowedSuffix)
152 | }
153 | }
154 |
155 | export type PathResolverOptions = Pick
156 |
157 | // Path: https://github.com/facebook/stylex/blob/main/packages/babel-plugin/src/utils/state-manager.js
158 | // After 0.9.0 this is live
159 |
160 | export function getCanonicalFilePath(filePath: string, rootDir?: string) {
161 | const pkgNameAndPath = getPackageNameAndPath(filePath)
162 | if (pkgNameAndPath === null) {
163 | if (rootDir) {
164 | return path.relative(rootDir, filePath)
165 | }
166 | const fileName = path.relative(path.dirname(filePath), filePath)
167 | return `_unknown_path_:${fileName}`
168 | }
169 | const [packageName, packageDir] = pkgNameAndPath
170 | return `${packageName}:${path.relative(packageDir, filePath)}`
171 | }
172 |
173 | export function getPackageNameAndPath(filePath: string) {
174 | const folder = path.dirname(filePath)
175 | const hasPackageJSON = fs.existsSync(path.join(folder, 'package.json'))
176 | if (hasPackageJSON) {
177 | try {
178 | const json = JSON.parse(fs.readFileSync(path.join(folder, 'package.json'), 'utf8')) as { name: string }
179 | return [json.name, folder]
180 | } catch (error) {
181 | console.error(error)
182 | return null
183 | }
184 | }
185 | if (folder === path.parse(folder).root || folder === '') {
186 | return null
187 | }
188 | return getPackageNameAndPath(folder)
189 | }
190 |
191 | export function addFileExtension(importedFilePath: string, sourceFile: string) {
192 | if (FILE_EXTENSIONS.some((ext) => importedFilePath.endsWith(ext))) {
193 | return importedFilePath
194 | }
195 | const fileExtension = path.extname(sourceFile)
196 | // NOTE: This is unsafe. We are assuming the all files in your project
197 | // use the same file extension.
198 | // However, in a haste module system we have no way to resolve the
199 | // *actual* file to get the actual file extension used.
200 | return importedFilePath + fileExtension
201 | }
202 |
--------------------------------------------------------------------------------
/packages/babel-plugin/src/ast/shared.ts:
--------------------------------------------------------------------------------
1 | import { types } from '@babel/core'
2 | import { NodePath } from '@babel/core'
3 |
4 | export type StringLikeKindPath = NodePath
5 |
6 | export type StringLikeKind = types.StringLiteral | types.Identifier
7 |
8 | export type CalleeExpression = types.Expression | types.V8IntrinsicIdentifier
9 |
10 | export function isStringLikeKind(path: NodePath | types.Node): path is StringLikeKindPath {
11 | if (!('node' in path)) {
12 | return path.type === 'StringLiteral' || path.type === 'Identifier'
13 | }
14 | return isStringLikeKind(path.node) || isIdentifier(path)
15 | }
16 |
17 | export function isStringLiteral(path: NodePath): path is NodePath {
18 | return path.isStringLiteral()
19 | }
20 |
21 | export function isNumericLiteral(path: NodePath): path is NodePath {
22 | return path.isNumericLiteral()
23 | }
24 |
25 | export function isBooleanLiteral(path: NodePath): path is NodePath {
26 | return path.isBooleanLiteral()
27 | }
28 |
29 | export function isNullLiteral(path: NodePath): path is NodePath {
30 | return path.isNullLiteral()
31 | }
32 |
33 | export function isIdentifier(path: NodePath): path is NodePath {
34 | return path.isIdentifier()
35 | }
36 |
37 | export function isReferencedIdentifier(path: NodePath): path is NodePath {
38 | return path.isReferencedIdentifier()
39 | }
40 |
41 | export function isConditionalExpression(path: NodePath): path is NodePath {
42 | return path.isConditionalExpression()
43 | }
44 |
45 | export function isUnaryExpression(path: NodePath, opts?: object): path is NodePath {
46 | return path.isUnaryExpression(opts)
47 | }
48 |
49 | export function getStringLikeKindValue(path: StringLikeKindPath | StringLikeKind) {
50 | if (!('node' in path)) {
51 | if (path.type === 'StringLiteral') { return path.value }
52 | return path.name
53 | }
54 | return getStringLikeKindValue(path.node)
55 | }
56 |
57 | export function callExpression(callee: CalleeExpression, args: types.Expression[]) {
58 | return types.callExpression(callee, args)
59 | }
60 |
61 | export function arrowFunctionExpression(params: types.Identifier[], body: types.Expression) {
62 | return types.arrowFunctionExpression(params, body)
63 | }
64 | export function stringLiteral(value: string) {
65 | return types.stringLiteral(value)
66 | }
67 |
68 | export function objectProperty(key: types.StringLiteral | types.Identifier, value: types.Expression) {
69 | return types.objectProperty(key, value)
70 | }
71 |
72 | export function objectExpression(properties: types.ObjectProperty[]) {
73 | return types.objectExpression(properties)
74 | }
75 |
76 | export function memberExpression(object: types.Expression, property: types.PrivateName | types.Expression, computed: boolean = false) {
77 | return types.memberExpression(object, property, computed)
78 | }
79 |
80 | export function variableDeclaration(identifier: types.Identifier | string, ast: types.Expression) {
81 | return types.variableDeclaration('const', [
82 | types.variableDeclarator(typeof identifier === 'string' ? types.identifier(identifier) : identifier, ast)
83 | ])
84 | }
85 |
86 | export function isObjectExpression(path: NodePath): path is NodePath {
87 | return path.isObjectExpression()
88 | }
89 |
90 | export function isObjectProperty(path: NodePath): path is NodePath {
91 | return path.isObjectProperty()
92 | }
93 |
94 | export function isSpreadElement(path: NodePath): path is NodePath {
95 | return path.isSpreadElement()
96 | }
97 |
98 | export function isObjectMethod(path: NodePath): path is NodePath {
99 | return path.isObjectMethod()
100 | }
101 |
102 | export function isMemberExpression(path: NodePath): path is NodePath {
103 | return path.isMemberExpression()
104 | }
105 |
106 | export function isTemplateLiteral(path: NodePath): path is NodePath {
107 | return path.isTemplateLiteral()
108 | }
109 |
110 | export function isCallExpression(path: NodePath): path is NodePath {
111 | return path.isCallExpression()
112 | }
113 |
114 | export function isTopLevelCalled(
115 | path: NodePath
116 | ): path is NodePath {
117 | return types.isProgram(path.parent) || types.isExportDefaultDeclaration(path.parent) || types.isExportNamedDeclaration(path.parent)
118 | }
119 |
120 | export function isStmt(path: NodePath): path is NodePath {
121 | return path.isStatement()
122 | }
123 |
124 | export function is(condit: boolean, message: string = 'Invalid Error') {
125 | if (!condit) { throw new Error(message) }
126 | }
127 |
128 | export function isLogicalExpression(path: NodePath): path is NodePath {
129 | return path.isLogicalExpression()
130 | }
131 |
132 | export function isImportDeclaration(path: NodePath): path is NodePath {
133 | return path.isImportDeclaration()
134 | }
135 |
136 | export function isImportSpecifier(path: NodePath): path is NodePath {
137 | return path.isImportSpecifier()
138 | }
139 |
140 | export function isImportNamespaceSpecifier(path: NodePath): path is NodePath {
141 | return path.isImportNamespaceSpecifier()
142 | }
143 |
144 | export function isImportDefaultSpecifier(path: NodePath): path is NodePath {
145 | return path.isImportDefaultSpecifier()
146 | }
147 |
148 | export function findNearestParentWithCondition(
149 | path: NodePath,
150 | condition: (p: NodePath) => p is NodePath
151 | ): NodePath {
152 | if (condition(path)) { return path }
153 | if (path.parentPath == null) {
154 | throw new Error('Unexpected Path found that is not part of the AST.')
155 | }
156 | return findNearestParentWithCondition(path.parentPath, condition)
157 | }
158 |
159 | export function findNearestStatementAncestor(path: NodePath) {
160 | return findNearestParentWithCondition(path, isStmt)
161 | }
162 |
163 | export type ToplevelAncestorType = types.Program | types.ExportDefaultDeclaration | types.ExportNamedDeclaration
164 |
165 | export function findNearestTopLevelAncestor(
166 | path: NodePath
167 | ): NodePath {
168 | const ancestor = findNearestParentWithCondition(path, isTopLevelCalled) as NodePath
169 | if (isCallExpression(ancestor)) { return ancestor.parentPath as NodePath }
170 | return ancestor as NodePath
171 | }
172 |
173 | export const make = {
174 | objectProperty: (key: string, value: types.Expression, identifier?: boolean) => {
175 | return objectProperty(identifier ? types.identifier(key) : stringLiteral(key), value)
176 | },
177 | identifier: (name: string) => types.identifier(name),
178 | stringLiteral: (value: string) => stringLiteral(value),
179 | nullLiteral: () => types.nullLiteral(),
180 | numericLiteral: (value: number) => types.numericLiteral(value),
181 | callExpression: (callee: CalleeExpression, args: types.Expression[]) => callExpression(callee, args),
182 | memberExpression,
183 | logicalExpression: types.logicalExpression,
184 | arrowFunctionExpression,
185 | variableDeclaration,
186 | objectExpression,
187 | importDeclaration: types.importDeclaration,
188 | importSpecifier: types.importSpecifier
189 | }
190 |
--------------------------------------------------------------------------------
/packages/vite/src/postcss-ver.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-unused-vars */
2 | import type { TransformOptions } from '@babel/core'
3 | import { createFilter } from '@rollup/pluginutils'
4 | import type { StylexExtendBabelPluginOptions } from '@stylex-extend/babel-plugin'
5 | import fs from 'fs'
6 | import type { CSSOptions, ModuleNode, Plugin, Update, ViteDevServer } from 'vite'
7 | import { searchForWorkspaceRoot } from 'vite'
8 | import { ensureParserOpts, getPlugin, interopDefault, isPotentialCSSFile, transformStyleX } from './compile'
9 | import { CONSTANTS, WELL_KNOW_LIBS, defaultOptions, unique } from './index'
10 | import type { RollupPluginContext, StyleXOptions } from './index'
11 |
12 | // Note that postcss ver has limitations and can't handle non-js syntax. Like .vue .svelte and etc.
13 |
14 | type PostCSSAcceptedPlugin = Exclude['plugins'], undefined>[number]
15 |
16 | interface StylexPostCSSPluginOptions {
17 | cwd?: string
18 | babelConfig?: TransformOptions
19 | include?: string[]
20 | exclude?: string[]
21 | useCSSLayer: boolean
22 | }
23 |
24 | export interface StylexOptionsWithPostcss extends StyleXOptions {
25 | postcss?: {
26 | include: StylexPostCSSPluginOptions['include'],
27 | exclude: StylexPostCSSPluginOptions['exclude'],
28 | aliases: StylexExtendBabelPluginOptions['aliases']
29 | }
30 | }
31 |
32 | function getExtensionPriority(file: string) {
33 | if (/\.(js|jsx|ts|tsx|mjs|cjs)$/.test(file)) { return 0 }
34 | if (/\.css$/.test(file)) { return 1 }
35 | return 2
36 | }
37 |
38 | export function stylex(options: StylexOptionsWithPostcss = {}): Plugin[] {
39 | const cssPlugins: Plugin[] = []
40 | options = { ...defaultOptions, ...options }
41 | const { macroTransport, useCSSLayer = false, optimizedDeps: _, include, postcss: postcssConfig, exclude, babelConfig, ...rest } = options
42 | const filter = createFilter(include, exclude)
43 | const accepts: Set = new Set()
44 | const effects: Map = new Map()
45 | let globalCSS: Record = {}
46 | const servers: ViteDevServer[] = []
47 |
48 | const produceCSS = () => {
49 | return Object.values(globalCSS).join('\n')
50 | }
51 |
52 | return [
53 | {
54 | name: '@stylex-extend:postcss-resolve',
55 | enforce: 'pre',
56 | buildStart() {
57 | if (!this.meta.watchMode) {
58 | effects.clear()
59 | accepts.clear()
60 | globalCSS = {}
61 | }
62 | },
63 | async config(config) {
64 | if (!config.css) {
65 | config.css = {}
66 | }
67 | if (config.css.transformer === 'lightningcss') {
68 | throw new Error('Lightningcss is not supported by stylex-extend')
69 | }
70 | if (typeof config.css.postcss === 'string') {
71 | throw new Error('Postcss config file is not supported by stylex-extend')
72 | }
73 | // config.css.postcss.
74 | if (!config.css.postcss) {
75 | config.css.postcss = {
76 | plugins: []
77 | }
78 | }
79 |
80 | const rootDir = searchForWorkspaceRoot(config.root || process.cwd())
81 | if (!options.unstable_moduleResolution) {
82 | // For monorepo.
83 | options.unstable_moduleResolution = { type: 'commonJS', rootDir }
84 | }
85 |
86 | // @ts-expect-error ignored
87 | const postcss = await import('@stylexjs/postcss-plugin').then((m: unknown) => interopDefault(m)) as (
88 | opts: StylexPostCSSPluginOptions
89 | ) => PostCSSAcceptedPlugin
90 |
91 | const extend = await getPlugin('extend')
92 | const standard = await getPlugin('standard')
93 |
94 | const instance = postcss({
95 | include: postcssConfig?.include || [],
96 | exclude: postcssConfig?.exclude || [],
97 | useCSSLayer,
98 | cwd: rootDir,
99 | babelConfig: {
100 | parserOpts: {
101 | plugins: ['jsx', 'typescript']
102 | },
103 | plugins: [
104 | extend.withOptions({
105 | // @ts-expect-error safe
106 | unstable_moduleResolution: options.unstable_moduleResolution,
107 | // @ts-expect-error safe
108 | transport: macroTransport,
109 | classNamePrefix: options.classNamePrefix,
110 | aliases: postcssConfig?.aliases
111 | }),
112 | standard.withOptions({
113 | // @ts-expect-error safe
114 | unstable_moduleResolution: options.unstable_moduleResolution,
115 | runtimeInjection: false,
116 | classNamePrefix: options.classNamePrefix,
117 | aliases: postcssConfig?.aliases
118 | })
119 | ]
120 | }
121 | })
122 | config.css.postcss.plugins?.unshift(instance)
123 | },
124 | configResolved(config) {
125 | for (const plugin of config.plugins) {
126 | if (plugin.name === 'vite:css' || plugin.name === 'vite:css-post') {
127 | cssPlugins.push(plugin)
128 | }
129 | }
130 | cssPlugins.sort((a, b) => a.name.length - b.name.length)
131 | const optimizedDeps = unique([
132 | ...Array.isArray(options.optimizedDeps) ? options.optimizedDeps : [],
133 | ...Array.isArray(options.importSources) ? options.importSources.map((s) => typeof s === 'string' ? s : s.from) : [],
134 | ...WELL_KNOW_LIBS
135 | ])
136 | if (config.command === 'serve') {
137 | config.optimizeDeps.exclude = [...optimizedDeps, ...(config.optimizeDeps.exclude || [])]
138 | }
139 | if (config.appType === 'custom') {
140 | config.ssr.noExternal = Array.isArray(config.ssr.noExternal)
141 | ? [...config.ssr.noExternal, ...optimizedDeps]
142 | : config.ssr.noExternal
143 | }
144 | },
145 | transform(code, id) {
146 | if (isPotentialCSSFile(id) && !id.includes('node_modules')) {
147 | if (code.includes(CONSTANTS.REFERENCE_KEY)) {
148 | accepts.add(id)
149 | }
150 | }
151 | },
152 | configureServer(server) {
153 | servers.push(server)
154 | }
155 | },
156 | {
157 | name: '@stylex-extend:pre-convert',
158 | enforce: 'pre',
159 | transform: {
160 | order: 'pre',
161 | async handler(code, id) {
162 | if (macroTransport === false || id.includes('node_modules')) {
163 | return
164 | }
165 | if (!/\.[jt]sx?$/.test(id) || id.startsWith('\0')) {
166 | return
167 | }
168 |
169 | if (id in globalCSS) {
170 | delete globalCSS[id]
171 | }
172 | const plugins = ensureParserOpts(id)
173 | if (!plugins) { return }
174 | const res = await transformStyleX('extend', {
175 | code,
176 | filename: id,
177 | options: {
178 | transport: macroTransport,
179 | classNamePrefix: options.classNamePrefix,
180 | // @ts-expect-error safe
181 | unstable_moduleResolution: options.unstable_moduleResolution,
182 | aliases: postcssConfig?.aliases
183 | },
184 | parserOpts: { plugins }
185 | }, babelConfig)
186 | if (res && res.code) {
187 | if (res.metadata && CONSTANTS.STYLEX_EXTEND_META_KEY in res.metadata) {
188 | // @ts-expect-error safe
189 | globalCSS[id] = res.metadata[CONSTANTS.STYLEX_EXTEND_META_KEY] as string[]
190 | if (globalCSS[id].length) {
191 | accepts.add(id)
192 | }
193 | }
194 | return { code: res.code, map: res.map }
195 | }
196 | }
197 | }
198 | },
199 | {
200 | name: '@stylex-extend:post-convert',
201 | enforce: 'post',
202 | async transform(code, id) {
203 | if (id.includes('/node_modules/')) { return }
204 | if (!filter(id) || isPotentialCSSFile(id) || id.startsWith('\0')) {
205 | return
206 | }
207 | const res = await transformStyleX('standard', {
208 | code,
209 | filename: id,
210 | options: {
211 | ...rest,
212 | unstable_moduleResolution: options.unstable_moduleResolution,
213 | runtimeInjection: false,
214 | importSources: options.importSources,
215 | aliases: postcssConfig?.aliases
216 | }
217 | }, babelConfig)
218 | if (res && res.code) {
219 | if (res.metadata && CONSTANTS.STYLEX_META_KEY in res.metadata) {
220 | effects.set(id, true)
221 | }
222 |
223 | return { code: res.code, map: res.map }
224 | }
225 | }
226 | },
227 | {
228 | name: '@stylex-extend:flush-css',
229 | enforce: 'post',
230 | transform(code, id) {
231 | if (accepts.has(id) && servers.length > 0 && isPotentialCSSFile(id)) {
232 | // hard code replace const __vite__css__ = `...` to const __vite__css__ = ``
233 | const CSS_REGEX = /const\s+__vite__css\s*=\s*"([^"\\]*(?:\\.[^"\\]*)*)"/
234 | code = code.replace(CSS_REGEX, (_, s: string) => {
235 | return `const __vite__css = \`${s + produceCSS()}\``
236 | })
237 | return {
238 | code,
239 | map: { mappings: '' }
240 | }
241 | }
242 | },
243 | handleHotUpdate(ctx) {
244 | const { file, modules, server } = ctx
245 | if (effects.has(file)) {
246 | const cssModules = [...accepts].map((id) => server.moduleGraph.getModuleById(id)).filter(Boolean) as ModuleNode[]
247 | cssModules.sort((a, b) => {
248 | const priorityA = getExtensionPriority(a.file || '')
249 | const priorityB = getExtensionPriority(b.file || '')
250 | return priorityA - priorityB
251 | })
252 | if (cssModules.length) {
253 | cssModules.forEach((mod) => {
254 | server.moduleGraph.invalidateModule(mod)
255 | if (!server.moduleGraph.getModuleById(mod.id!)) {
256 | accepts.delete(mod.id!)
257 | }
258 | })
259 |
260 | const updates: Update[] = cssModules.map((mod) => ({
261 | type: 'js-update',
262 | path: mod.url,
263 | acceptedPath: mod.url,
264 | timestamp: Date.now()
265 | }))
266 | server.ws.send({
267 | type: 'update',
268 | updates
269 | })
270 | return [...modules, ...cssModules]
271 | }
272 | }
273 | }
274 | },
275 | {
276 | name: '@stylex-extend/vite:build-css',
277 | apply: 'build',
278 | enforce: 'pre',
279 | async renderStart() {
280 | const cssModules = [...accepts].filter((a) => isPotentialCSSFile(a))
281 | const chunks = cssModules.map((file) => {
282 | return { css: fs.readFileSync(file, 'utf8') + '\n' + produceCSS(), id: file }
283 | })
284 | for (const c of chunks) {
285 | let css = c.css
286 | for (const plugin of cssPlugins) {
287 | if (!plugin.transform) { continue }
288 | const transformHook = typeof plugin.transform === 'function' ? plugin.transform : plugin.transform.handler
289 | const ctx = {
290 | ...this,
291 | getCombinedSourcemap: () => {
292 | throw new Error('getCombinedSourcemap not implemented')
293 | }
294 | } satisfies RollupPluginContext
295 | const res = await transformHook.call(ctx, css, c.id)
296 | if (!res) { continue }
297 | if (typeof res === 'string') {
298 | css = res
299 | }
300 | if (typeof res === 'object' && res.code) {
301 | css = res.code
302 | }
303 | }
304 | }
305 | }
306 | }
307 | ]
308 | }
309 |
--------------------------------------------------------------------------------
/packages/vite/src/index.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-unsafe-assignment */
2 |
3 | /* eslint-disable @typescript-eslint/no-explicit-any */
4 | /* eslint-disable no-use-before-define */
5 | import { parseSync } from '@babel/core'
6 | import type { ParserOptions, PluginItem } from '@babel/core'
7 | import { createFilter } from '@rollup/pluginutils'
8 | import type { FilterPattern } from '@rollup/pluginutils'
9 | import { StylexExtendBabelPluginOptions } from '@stylex-extend/babel-plugin'
10 | import { xxhash } from '@stylex-extend/shared'
11 | import type { Options, Rule } from '@stylexjs/babel-plugin'
12 | import stylexBabelPlugin from '@stylexjs/babel-plugin'
13 | import path from 'path'
14 | import type { HookHandler, Plugin, Update, ViteDevServer } from 'vite'
15 | import { normalizePath, searchForWorkspaceRoot } from 'vite'
16 | import { ensureParserOpts, transformStyleX } from './compile'
17 |
18 | type UnionToIntersection = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never
19 | type LastOf = UnionToIntersection T : never> extends () => infer R ? R : never
20 |
21 | type Push = [...T, V]
22 |
23 | type UnionDeepMutable = T extends [infer L, ...infer R] ? L extends object ? [DeepMutable, ...UnionDeepMutable]
24 | : [L, ...UnionDeepMutable]
25 | : []
26 |
27 | type TuplifyUnion, N = [T] extends [never] ? true : false> = true extends N ? [] : Push>, L>
28 | type DeepMutable = {
29 | -readonly [P in keyof T]: T[P] extends object ? DeepMutable
30 | : TuplifyUnion['length'] extends 1 ? T[P]
31 | : UnionDeepMutable>[number]
32 | }
33 |
34 | type InternalOptions = DeepMutable>
35 |
36 | export interface StyleXOptions extends Partial {
37 | include?: FilterPattern
38 | exclude?: FilterPattern
39 | /**
40 | * @description For some reasons, vite can't handle cjs module resolution correctly. so pass this option to fix it.
41 | */
42 | optimizedDeps?: Array
43 | useCSSLayer?: boolean
44 | babelConfig?: {
45 | plugins?: Array,
46 | presets?: Array
47 | }
48 | /**
49 | * @default true
50 | * @description https://nonzzz.github.io/stylex-extend/
51 | */
52 | macroTransport?: StylexExtendBabelPluginOptions['transport'] | false
53 | [key: string]: any
54 | }
55 |
56 | interface ImportSpecifier {
57 | n: string | undefined
58 | s: number
59 | e: number
60 | }
61 |
62 | type BabelParserPlugins = ParserOptions['plugins']
63 |
64 | interface HMRPayload {
65 | hash: string
66 | }
67 |
68 | export type RollupPluginContext = ThisParameterType>>
69 |
70 | export const CONSTANTS = {
71 | REFERENCE_KEY: '@stylex;',
72 | STYLEX_META_KEY: 'stylex',
73 | STYLEX_EXTEND_META_KEY: 'globalStyle',
74 | VIRTUAL_STYLEX_MARK: 'virtual:stylex.css'
75 | }
76 |
77 | const WS_EVENT_TYPE = 'stylex:hmr'
78 |
79 | export const defaultOptions = {
80 | include: /\.(mjs|js|ts|vue|jsx|tsx)(\?.*|)$/,
81 | importSources: ['stylex', '@stylexjs/stylex'],
82 | macroTransport: 'props',
83 | useCSSLayer: false
84 | } satisfies StyleXOptions
85 |
86 | export const WELL_KNOW_LIBS = ['@stylexjs/open-props']
87 |
88 | export function unique(data: T[]) {
89 | return Array.from(new Set(data))
90 | }
91 |
92 | function getExt(p: string) {
93 | const [filename] = p.split('?', 2)
94 | return path.extname(filename).slice(1)
95 | }
96 |
97 | export function isPotentialCSSFile(id: string) {
98 | const extension = getExt(id)
99 | return extension === 'css' || (extension === 'vue' && id.includes('&lang.css')) || (extension === 'astro' && id.includes('&lang.css'))
100 | }
101 |
102 | class EffectModule {
103 | id: string
104 | meta: Rule[]
105 | constructor(id: string, meta: any) {
106 | this.id = id
107 | this.meta = meta
108 | }
109 | }
110 |
111 | // Vite's plugin can't handle all senarios, so we have to implement a cli to handle the rest.
112 |
113 | export function stylex(options: StyleXOptions = {}): Plugin[] {
114 | const cssPlugins: Plugin[] = []
115 | options = { ...defaultOptions, ...options }
116 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
117 | const { macroTransport, useCSSLayer, optimizedDeps: _, include, exclude, babelConfig, ...rest } = options
118 | let isBuild = false
119 | const servers: ViteDevServer[] = []
120 |
121 | const roots = new Map()
122 | let globalCSS = {}
123 | let lastHash = ''
124 |
125 | const filter = createFilter(include, exclude)
126 |
127 | const produceCSS = () => {
128 | return stylexBabelPlugin.processStylexRules(
129 | [...roots.values()]
130 | .map((r) => r.meta).flat().filter(Boolean),
131 | useCSSLayer!
132 | ) + '\n' + Object.values(globalCSS).join('\n')
133 | }
134 |
135 | // rollup private parse and es-module lexer can't parse JSX. So we have had to use babel to parse the import statements.
136 | const parseStmts = (code: string, id: string, plugins: BabelParserPlugins = []) => {
137 | const ast = parseSync(code, { filename: id, babelrc: false, parserOpts: { plugins } })
138 | const stmts: ImportSpecifier[] = []
139 | for (const n of ast!.program.body) {
140 | if (n.type === 'ImportDeclaration') {
141 | const v = n.source.value
142 | if (!v) { continue }
143 | const { start: s, end: e } = n.source
144 | if (typeof s === 'number' && typeof e === 'number') {
145 | stmts.push({ n: v, s: s + 1, e: e - 1 })
146 | }
147 | }
148 | }
149 | return stmts
150 | }
151 |
152 | const rewriteImportStmts = async (code: string, id: string, ctx: RollupPluginContext, plugins: BabelParserPlugins = []) => {
153 | const stmts = parseStmts(code, id, plugins)
154 | let i = 0
155 | for (const stmt of stmts) {
156 | const { n } = stmt
157 | if (n) {
158 | if (isPotentialCSSFile(n)) { continue }
159 | if (path.isAbsolute(n) || n[0] === '.') {
160 | continue
161 | }
162 | // respect the import sources
163 | if (!options.importSources?.some((i) => n.includes(typeof i === 'string' ? i : i.from))) {
164 | continue
165 | }
166 |
167 | const resolved = await ctx.resolve(n, id)
168 | if (resolved && resolved.id && !resolved.external) {
169 | if (resolved.id === id) {
170 | continue
171 | }
172 | if (!resolved.id.includes('node_modules')) {
173 | const p = './' + normalizePath(path.relative(path.dirname(id), resolved.id).replace(/\.\w+$/, ''))
174 | const start = stmt.s + i
175 | const end = stmt.e + i
176 | code = code.slice(0, start) + p + code.slice(end)
177 | i += p.length - (end - start)
178 | }
179 | }
180 | }
181 | }
182 | return code
183 | }
184 |
185 | // TODO: for more performance, we might need to maintain a dependency graph to invalidate the cache.
186 | const invalidate = (hmrHash?: string) => {
187 | for (const server of servers) {
188 | const updates: Update[] = []
189 | const mod = server.moduleGraph.getModuleById(CONSTANTS.VIRTUAL_STYLEX_MARK)
190 | if (!mod) { continue }
191 | const nextHash = xxhash(produceCSS())
192 | if (hmrHash) {
193 | lastHash = hmrHash
194 | }
195 | if (lastHash === nextHash) { continue }
196 | lastHash = nextHash
197 | // check if need to update the css
198 | server.moduleGraph.invalidateModule(mod)
199 | const update = {
200 | type: 'js-update',
201 | path: '/@id/' + mod.url,
202 | acceptedPath: '/@id/' + mod.url,
203 | timestamp: Date.now()
204 | } satisfies Update
205 | updates.push(update)
206 | server.ws.send({ type: 'update', updates })
207 | }
208 | }
209 |
210 | // Steps:
211 | // First, pre scan all files and collect all stylex imports as possible
212 | // Second, in serve mode generate the css from stylex and inject it to the css plugin
213 | // Third, in build mode generate the css from stylex and write it to a file
214 |
215 | return [
216 | {
217 | name: '@stylex-extend:config',
218 | enforce: 'pre',
219 | buildStart() {
220 | if (!this.meta.watchMode) {
221 | roots.clear()
222 | globalCSS = {}
223 | }
224 | },
225 | resolveId(id) {
226 | if (id === CONSTANTS.VIRTUAL_STYLEX_MARK) {
227 | return id
228 | }
229 | },
230 | load(id) {
231 | if (id === CONSTANTS.VIRTUAL_STYLEX_MARK) {
232 | return { code: produceCSS(), map: { mappings: '' } }
233 | }
234 | },
235 | configureServer(server) {
236 | servers.push(server)
237 | // vite's update for HMR are constantly changing. For better compatibility, we need to initate an update
238 | // notification from the client.
239 | server.ws.on(WS_EVENT_TYPE, (payload: HMRPayload) => {
240 | invalidate(payload.hash)
241 | })
242 | },
243 | configResolved(config) {
244 | isBuild = config.command === 'build'
245 | for (const plugin of config.plugins) {
246 | if (plugin.name === 'vite:css' || (isBuild && plugin.name === 'vite:css-post')) {
247 | cssPlugins.push(plugin)
248 | }
249 | }
250 |
251 | if (!options.unstable_moduleResolution) {
252 | // For monorepo.
253 | options.unstable_moduleResolution = { type: 'commonJS', rootDir: searchForWorkspaceRoot(config.root) }
254 | }
255 |
256 | const optimizedDeps = unique([
257 | ...Array.isArray(options.optimizedDeps) ? options.optimizedDeps : [],
258 | ...Array.isArray(options.importSources) ? options.importSources.map((s) => typeof s === 'string' ? s : s.from) : [],
259 | ...WELL_KNOW_LIBS
260 | ])
261 |
262 | if (config.command === 'serve') {
263 | config.optimizeDeps.exclude = [...optimizedDeps, ...(config.optimizeDeps.exclude || [])]
264 | }
265 | if (config.appType === 'custom') {
266 | config.ssr.noExternal = Array.isArray(config.ssr.noExternal)
267 | ? [...config.ssr.noExternal, ...optimizedDeps]
268 | : config.ssr.noExternal
269 | }
270 | }
271 | },
272 | {
273 | name: '@stylex-extend:pre-convert',
274 | enforce: 'pre',
275 | transform: {
276 | order: 'pre',
277 | async handler(code, id) {
278 | if (macroTransport === false || id.includes('/node_modules/')) { return }
279 |
280 | // convert all stylex-extend macro to stylex macro
281 | if (!/\.[jt]sx?$/.test(id) || id.startsWith('\0')) {
282 | return
283 | }
284 |
285 | const plugins = ensureParserOpts(id)
286 | if (!plugins) { return }
287 | code = await rewriteImportStmts(code, id, this, plugins)
288 |
289 | if (id in globalCSS) {
290 | // @ts-expect-error safe
291 | delete globalCSS[id]
292 | }
293 |
294 | const res = await transformStyleX('extend', {
295 | code,
296 | filename: id,
297 | options: {
298 | transport: macroTransport,
299 | classNamePrefix: options.classNamePrefix,
300 | // @ts-expect-error safe
301 | unstable_moduleResolution: options.unstable_moduleResolution
302 | },
303 | parserOpts: { plugins }
304 | }, babelConfig)
305 | if (res && res.code) {
306 | if (res.metadata && CONSTANTS.STYLEX_EXTEND_META_KEY in res.metadata) {
307 | // @ts-expect-error safe
308 | globalCSS[id] = res.metadata[CONSTANTS.STYLEX_EXTEND_META_KEY] as string[]
309 | }
310 |
311 | return { code: res.code, map: res.map }
312 | }
313 | }
314 | }
315 | },
316 | {
317 | name: '@stylex-extend/post-convert',
318 | enforce: 'post',
319 | async transform(code, id) {
320 | if (id.includes('/node_modules/')) { return }
321 | if (!filter(id) || isPotentialCSSFile(id) || id.startsWith('\0')) { return }
322 | code = await rewriteImportStmts(code, id, this)
323 | const res = await transformStyleX('standard', {
324 | code,
325 | filename: id,
326 | options: {
327 | ...rest,
328 | unstable_moduleResolution: options.unstable_moduleResolution,
329 | runtimeInjection: false,
330 | dev: !isBuild,
331 | importSources: options.importSources
332 | }
333 | }, babelConfig)
334 | if (!res) { return }
335 | if (res.metadata && CONSTANTS.STYLEX_META_KEY in res.metadata) {
336 | // @ts-expect-error safe
337 | const meta = res.metadata[CONSTANTS.STYLEX_META_KEY] as Rule[]
338 | if (meta.length) {
339 | roots.set(id, new EffectModule(id, meta))
340 | } else {
341 | roots.delete(id)
342 | }
343 | }
344 |
345 | if (res.code) { return { code: res.code, map: res.map } }
346 | }
347 | },
348 | {
349 | name: '@stylex-extend:flush-css',
350 | apply: 'serve',
351 | enforce: 'post',
352 |
353 | transform(code, id) {
354 | if (roots.has(id)) {
355 | invalidate()
356 | }
357 | const [filename] = id.split('?', 2)
358 | if (filename === CONSTANTS.VIRTUAL_STYLEX_MARK && code.includes('import.meta.hot')) {
359 | // inject client hmr notification
360 | const payload = { hash: xxhash(produceCSS()) } satisfies HMRPayload
361 | let hmr = `
362 | try {
363 | await import.meta.hot.send('${WS_EVENT_TYPE}', ${JSON.stringify(payload)})
364 | } catch (e) {
365 | console.warn('[stylex-hmr]', e)
366 | }
367 | if (!import.meta.url.includes('?')) await new Promise(resolve => setTimeout(resolve, 100))
368 | `
369 | hmr = `;(async function() {${hmr}\n})()`
370 | hmr = `\nif (import.meta.hot) {${hmr}}`
371 | return { code: code + hmr, map: { mappings: '' } }
372 | }
373 | }
374 | },
375 | {
376 | // we separate the server and build loigc. Although there will be some duplicated code, but it's worth.
377 | name: '@stylex-extend/vite:build-css',
378 | apply: 'build',
379 | enforce: 'pre',
380 | async renderStart() {
381 | let css = produceCSS()
382 | for (const plugin of cssPlugins) {
383 | if (!plugin.transform) { continue }
384 | const transformHook = typeof plugin.transform === 'function' ? plugin.transform : plugin.transform.handler
385 | const ctx = {
386 | ...this,
387 | getCombinedSourcemap: () => {
388 | throw new Error('getCombinedSourcemap not implemented')
389 | }
390 | } satisfies RollupPluginContext
391 | const res = await transformHook.call(ctx, css, CONSTANTS.VIRTUAL_STYLEX_MARK)
392 | if (!res) { continue }
393 | if (typeof res === 'string') {
394 | css = res
395 | }
396 | if (typeof res === 'object' && res.code) {
397 | css = res.code
398 | }
399 | }
400 | }
401 | }
402 | ]
403 | }
404 |
--------------------------------------------------------------------------------
/packages/babel-plugin/src/ast/evaluate-path.ts:
--------------------------------------------------------------------------------
1 | import { types } from '@babel/core'
2 | import type { NodePath } from '@babel/core'
3 | import { utils } from '@stylexjs/shared'
4 | import { MESSAGES } from '../ast/message'
5 | import {
6 | findNearestParentWithCondition,
7 | getStringLikeKindValue,
8 | isBooleanLiteral,
9 | isCallExpression,
10 | isConditionalExpression,
11 | isIdentifier,
12 | isImportDeclaration,
13 | isImportDefaultSpecifier,
14 | isImportNamespaceSpecifier,
15 | isImportSpecifier,
16 | isLogicalExpression,
17 | isMemberExpression,
18 | isNullLiteral,
19 | isNumericLiteral,
20 | isObjectExpression,
21 | isObjectMethod,
22 | isObjectProperty,
23 | isReferencedIdentifier,
24 | isSpreadElement,
25 | isStringLikeKind,
26 | isStringLiteral,
27 | isTemplateLiteral,
28 | isUnaryExpression,
29 | make
30 | } from '../ast/shared'
31 | import type { CSSObjectValue } from '../interface'
32 | import { Module } from '../module'
33 |
34 | interface EnvironmentMap {
35 | path: NodePath
36 | define: string
37 | }
38 |
39 | export interface Environment {
40 | references: Map
41 | }
42 |
43 | export interface State {
44 | confident: boolean
45 | deoptPath: NodePath | null
46 | seen: Map
47 | environment: Environment
48 | layer: number
49 | mod: Module
50 | }
51 |
52 | export interface Result {
53 | confident: boolean
54 | value: CSSObjectValue
55 | references: Map
56 | }
57 |
58 | const WITH_LOGICAL = '__logical__'
59 |
60 | export const MARK = {
61 | ref: (s: string | number) => '@__' + s,
62 | unref: (s: string) => s.slice(3),
63 | isRef: (s: string) => s.startsWith('@__')
64 | }
65 |
66 | function hash(s: string) {
67 | return 'a' + utils.hash(s)
68 | }
69 |
70 | function capitalizeFirstLetter(s: string) {
71 | return s.charAt(0).toUpperCase() + s.slice(1)
72 | }
73 |
74 | function evaluateMemberExpression(path: NodePath, state: State) {
75 | const objPath = path.get('object')
76 | const propPath = path.get('property')
77 | if (isIdentifier(objPath) && isStringLikeKind(propPath)) {
78 | const id = getStringLikeKindValue(objPath) + capitalizeFirstLetter(getStringLikeKindValue(propPath))
79 | state.environment.references.set(id, { path, define: id })
80 | return MARK.ref(id)
81 | }
82 | }
83 |
84 | function evaluateNodeToHashLike(path: NodePath, state: State) {
85 | const identifier = findNearestParentWithCondition(path, isObjectProperty).get('key')
86 | // @ts-expect-error safe
87 | const id = hash(getStringLikeKindValue(identifier))
88 | state.environment.references.set(id, { path, define: id })
89 | return MARK.ref(id)
90 | }
91 |
92 | function evaluateTemplateLiteral(path: NodePath, state: State) {
93 | const expr = path.get('expressions')
94 | if (!expr.length) {
95 | return path.node.quasis[0].value.raw
96 | }
97 | return evaluateNodeToHashLike(path, state)
98 | }
99 |
100 | function isInternalId(path: NodePath): [boolean, string | null] {
101 | if (isReferencedIdentifier(path)) {
102 | const bindings = path.scope.getBinding(path.node.name)
103 | if (!bindings) {
104 | return [false, null]
105 | }
106 | if (bindings.path.isVariableDeclarator()) {
107 | const inital = bindings.path.get('init')
108 | if (inital && inital.isObjectExpression()) {
109 | const len = inital.node.properties.length
110 | for (let i = 0; i < len; i++) {
111 | const item = inital.node.properties[i]
112 | if (item.type === 'ObjectProperty' && isStringLikeKind(item.key) && isStringLikeKind(item.value)) {
113 | if (getStringLikeKindValue(item.key) === '$id' && getStringLikeKindValue(item.value) === 'stylex-extend') {
114 | return [true, getStringLikeKindValue((inital.node.properties[1] as types.ObjectProperty).value as types.StringLiteral)]
115 | }
116 | }
117 | }
118 | }
119 | }
120 | }
121 | return [false, null]
122 | }
123 | // About difference. us evaluate will split two logic. one is evaluate object expression. another is evaluate baisc expression.
124 | // Like number, string, boolean, null, undefined, etc.
125 | // Unlike stylex. we must ensure the object expression if it's kind of object property the key must be static.
126 |
127 | function evaluate(path: NodePath, state: State): string | undefined
128 | function evaluate(path: NodePath, state: State): null
129 | function evaluate(path: NodePath, state: State): string
130 | function evaluate(path: NodePath, state: State): number
131 | function evaluate(path: NodePath, state: State): boolean
132 | function evaluate(path: NodePath, state: State): string | undefined
133 | function evaluate(path: NodePath, state: State): string
134 | function evaluate(path: NodePath, state: State): string
135 | function evaluate(path: NodePath, state: State): string | undefined
136 | function evaluate(path: NodePath, state: State): CSSObjectValue
137 | function evaluate(path: NodePath, state: State): CSSObjectValue
138 | function evaluate(path: NodePath, state: State): CSSObjectValue
139 | function evaluate(path: NodePath, state: State) {
140 | if (isNullLiteral(path)) {
141 | return null
142 | }
143 |
144 | if (isIdentifier(path)) {
145 | const [pass, id] = isInternalId(path)
146 | if (pass) {
147 | return id
148 | }
149 | const value = path.node.name
150 | if (value === 'undefined') {
151 | return undefined
152 | }
153 | state.environment.references.set(value, { path, define: value })
154 | return MARK.ref(value)
155 | }
156 |
157 | if (isStringLiteral(path) || isNumericLiteral(path) || isBooleanLiteral(path)) {
158 | return path.node.value
159 | }
160 |
161 | if (isUnaryExpression(path, { prefix: true })) {
162 | if (path.node.operator === 'void') {
163 | // we don't need to evaluate the argument to know what this will return
164 | return undefined
165 | }
166 | const args = path.get('argument')
167 | const arg = evaluate(args, state)
168 | switch (path.node.operator) {
169 | case '!':
170 | return !arg
171 | case '+':
172 | return +arg
173 | case '-':
174 | // eslint-disable-next-line @typescript-eslint/no-unsafe-unary-minus
175 | return -arg
176 | case '~':
177 | return ~arg
178 | case 'typeof':
179 | return typeof arg
180 | }
181 | return undefined
182 | }
183 |
184 | if (isMemberExpression(path)) {
185 | return evaluateMemberExpression(path, state)
186 | }
187 |
188 | if (isTemplateLiteral(path)) {
189 | return evaluateTemplateLiteral(path, state)
190 | }
191 |
192 | if (isConditionalExpression(path)) {
193 | return evaluateNodeToHashLike(path, state)
194 | }
195 |
196 | if (isCallExpression(path)) {
197 | const callee = path.get('callee')
198 | if (isMemberExpression(callee)) {
199 | const result = evaluateMemberExpression(callee, state)
200 | if (result) {
201 | const unwrapped = MARK.unref(result)
202 | state.environment.references.set(unwrapped, { path, define: unwrapped })
203 | }
204 | return result
205 | }
206 | if (isIdentifier(callee)) {
207 | const value = getStringLikeKindValue(callee)
208 | state.environment.references.set(value, { path, define: value })
209 | return MARK.ref(value)
210 | }
211 | }
212 |
213 | if (isObjectExpression(path)) {
214 | const obj: CSSObjectValue = {}
215 | const props = path.get('properties')
216 | for (const prop of props) {
217 | if (isObjectMethod(prop)) {
218 | throw new Error(MESSAGES.NOT_IMPLEMENTED)
219 | }
220 | if (isSpreadElement(prop)) {
221 | if (!state.confident) {
222 | throw new Error(MESSAGES.NO_NESTED_SPREAD)
223 | }
224 | state.confident = false
225 | const spreadExpression = evaluateForState(prop.get('argument'), state)
226 | Object.assign(obj, { [MARK.ref(state.layer)]: spreadExpression })
227 | state.confident = true
228 | state.layer++
229 | }
230 | if (isObjectProperty(prop)) {
231 | const [pass, id] = isInternalId(prop.get('key'))
232 | if (prop.node.computed && !pass) {
233 | throw new Error(MESSAGES.NO_STATIC_ATTRIBUTE)
234 | }
235 | let key: string | null = null
236 | if (id) {
237 | key = id
238 | state.layer++
239 | }
240 | if (!key && isStringLikeKind(prop.get('key'))) {
241 | // @ts-expect-error safe
242 | key = getStringLikeKindValue(prop.get('key'))
243 | }
244 | const valuePath = prop.get('value')
245 | const value = evaluate(valuePath, state)
246 | if (key) {
247 | obj[key] = value
248 | }
249 | }
250 | }
251 | return obj
252 | }
253 |
254 | if (isLogicalExpression(path)) {
255 | if (!state.confident) {
256 | state.environment.references.set(MARK.ref(state.layer), { path: path.get('left'), define: MARK.ref(state.layer) })
257 | }
258 | // stylex will evaluate all logical expr so we no need to worry about it.
259 | return evaluateForState(path.get('right'), state)
260 | }
261 | }
262 |
263 | function evaluateForState(path: NodePath, state: State) {
264 | const value = evaluate(path, state)
265 | return value
266 | }
267 |
268 | function evaluatePath(path: NodePath, mod: Module): Result {
269 | const state: State = {
270 | confident: true,
271 | deoptPath: null,
272 | layer: 0,
273 | environment: { references: new Map() },
274 | seen: new Map(),
275 | mod
276 | }
277 |
278 | return {
279 | confident: state.confident,
280 | value: evaluateForState(path, state),
281 | references: state.environment.references
282 | }
283 | }
284 |
285 | export class Iter> {
286 | private keys: string[]
287 | private data: T
288 | constructor(data: T) {
289 | this.data = data
290 | this.keys = Object.keys(data)
291 | }
292 |
293 | // dprint-ignore
294 | * [Symbol.iterator]() {
295 | for (let i = 0; i < this.keys.length; i++) {
296 | yield {
297 | key: this.keys[i],
298 | value: this.data[this.keys[i]] as T[keyof T],
299 | index: i,
300 | peek: () => this.keys[i + 1]
301 | }
302 | }
303 | }
304 | }
305 |
306 | function printCSSRule(rule: CSSObjectValue) {
307 | const iter = new Iter(rule)
308 | const properties: types.ObjectProperty[] = []
309 | const variables = new Set()
310 | let logical = false
311 | for (const { key, value } of iter) {
312 | if (key === WITH_LOGICAL) {
313 | logical = true
314 | continue
315 | }
316 | if (typeof value === 'object' && value !== null) {
317 | const [child, vars] = printCSSRule(value)
318 | properties.push(make.objectProperty(key, child))
319 | vars.forEach((v) => variables.add(v))
320 | continue
321 | }
322 | switch (typeof value) {
323 | case 'undefined':
324 | properties.push(make.objectProperty(key, make.identifier('undefined')))
325 | break
326 | case 'string': {
327 | if (value === 'undefined') {
328 | properties.push(make.objectProperty(key, make.identifier('undefined')))
329 | } else if (MARK.isRef(value)) {
330 | const unwrapped = MARK.unref(value)
331 | variables.add(unwrapped)
332 | properties.push(make.objectProperty(key, make.identifier(MARK.unref(value))))
333 | } else {
334 | properties.push(make.objectProperty(key, make.stringLiteral(value)))
335 | }
336 | break
337 | }
338 | case 'object':
339 | properties.push(make.objectProperty(key, make.nullLiteral()))
340 | break
341 | case 'number':
342 | properties.push(make.objectProperty(key, make.numericLiteral(value)))
343 | }
344 | }
345 | return [types.objectExpression(properties), variables, logical] satisfies [types.ObjectExpression, Set, boolean]
346 | }
347 |
348 | export function printJsAST(data: ReturnType, path: NodePath) {
349 | const { references, css, seens } = data
350 | // like spread kind
351 | // us reference key is look like @__{idx}
352 | // but we evaluate the right order at sortAndMergeEvaluatedResult
353 | const properties: types.ObjectProperty[] = []
354 | const expressions: types.Expression[] = []
355 | const into = path.scope.generateUidIdentifier('styles')
356 | for (let i = 0; i < css.length; i++) {
357 | const rule = css[i]
358 | const [ast, vars, logical] = printCSSRule(rule)
359 | // After stylex v0.11.0. stylex has an internal styleMap we should respect it.
360 | // see https://github.com/facebook/stylex/issues/925
361 | const expr = make.memberExpression(into, make.identifier('$' + i), false)
362 | if (vars.size) {
363 | const calleeArguments = [...vars].map((variable) => {
364 | const { path } = references.get(variable)!
365 | return path.node
366 | }) as types.Expression[]
367 | const callee = make.callExpression(expr, calleeArguments)
368 | if (logical) {
369 | expressions.push(make.logicalExpression('&&', references.get(seens[i])!.path.node as types.Expression, callee))
370 | } else {
371 | expressions.push(callee)
372 | }
373 | const func = make.arrowFunctionExpression([...vars].map((variable) => make.identifier(variable)), ast)
374 | properties.push(make.objectProperty('$' + i, func, true))
375 | continue
376 | }
377 | if (logical) {
378 | expressions.push(make.logicalExpression('&&', references.get(seens[i])!.path.node as types.Expression, expr))
379 | } else {
380 | expressions.push(expr)
381 | }
382 | properties.push(make.objectProperty('$' + i, ast, true))
383 | }
384 | return { properties, expressions, into }
385 | }
386 |
387 | export function printCssAST(data: ReturnType, mod: Module) {
388 | let str = ''
389 | const { references, css } = data
390 |
391 | const print = (s: string | number) => {
392 | str += s
393 | }
394 |
395 | const evaluateCSSVariableFromModule = (path: NodePath) => {
396 | const obj = path.get('object')
397 | const prop = path.get('property')
398 | if (isReferencedIdentifier(obj) && isIdentifier(prop)) {
399 | const binding = path.scope.getBinding(obj.node.name)
400 | const bindingPath = binding?.path
401 | if (
402 | binding && bindingPath && isImportSpecifier(bindingPath) && !isImportDefaultSpecifier(bindingPath) &&
403 | !isImportNamespaceSpecifier(bindingPath)
404 | ) {
405 | const importSpecifierPath = bindingPath
406 | const imported = importSpecifierPath.node.imported
407 | // Note: This implementation is consistent with the official.
408 | const importDeclaration = findNearestParentWithCondition(importSpecifierPath, isImportDeclaration)
409 | const hashing = mod.fileNameForHashing(importDeclaration.node.source.value)
410 | if (!hashing) {
411 | throw new Error(MESSAGES.NO_STATIC_ATTRIBUTE)
412 | }
413 | const strToHash = utils.genFileBasedIdentifier({
414 | fileName: hashing,
415 | exportName: getStringLikeKindValue(imported),
416 | key: prop.node.name
417 | })
418 | return `var(--${mod.options.classNamePrefix + utils.hash(strToHash)})`
419 | }
420 | }
421 | throw new Error(MESSAGES.NOT_IMPLEMENTED)
422 | }
423 |
424 | const evaluateLivingVariable = (value: string) => {
425 | const unwrapped = MARK.unref(value)
426 | const { path } = references.get(unwrapped)!
427 | if (isMemberExpression(path)) {
428 | return evaluateCSSVariableFromModule(path)
429 | }
430 | if (isTemplateLiteral(path)) {
431 | const { quasis } = path.node
432 | const expressions = path.get('expressions')
433 | let cap = expressions.length
434 | let str = quasis[0].value.raw
435 | while (cap) {
436 | const first = expressions.shift()!
437 | if (first && isMemberExpression(first)) {
438 | str += evaluateCSSVariableFromModule(first)
439 | }
440 | cap--
441 | }
442 | return str
443 | }
444 | throw new Error(MESSAGES.NOT_IMPLEMENTED)
445 | }
446 |
447 | const prettySelector = (selector: string) => {
448 | if (selector.charCodeAt(1) === 45) {
449 | return selector
450 | }
451 | return selector.replace(/[A-Z]|^ms/g, '-$&').toLowerCase()
452 | }
453 |
454 | const run = (rule: CSSObjectValue[] | CSSObjectValue) => {
455 | if (Array.isArray(rule)) {
456 | for (const r of rule) {
457 | run(r)
458 | }
459 | return
460 | }
461 | for (const { key: selector, value } of new Iter(rule)) {
462 | if (typeof value === 'boolean') { continue }
463 | if (typeof value === 'undefined' || typeof value === 'object' && !value) { continue }
464 | if (typeof value === 'object') {
465 | print(selector)
466 | print('{')
467 | run(value)
468 | print('}')
469 | continue
470 | }
471 | print(prettySelector(selector))
472 | print(':')
473 | if (typeof value === 'string' && MARK.isRef(value)) {
474 | print(evaluateLivingVariable(value))
475 | } else {
476 | print(value)
477 | }
478 | print(';')
479 | }
480 | }
481 |
482 | run(css)
483 |
484 | return { css: str }
485 | }
486 |
487 | function sortAndMergeEvaluatedResult(data: Result) {
488 | const { references } = data
489 |
490 | const result: CSSObjectValue[] = []
491 | const seens: Record = {}
492 | let layer = 0
493 | for (const { key, value, peek } of new Iter(data.value)) {
494 | if (!MARK.isRef(key)) {
495 | result[layer] = { ...result[layer], [key]: value }
496 | if (peek() && MARK.isRef(peek())) {
497 | layer++
498 | }
499 | continue
500 | }
501 |
502 | const next = { ...value as CSSObjectValue }
503 | if (references.has(key)) {
504 | next[WITH_LOGICAL] = true
505 | seens[layer] = key
506 | }
507 | result[layer] = next
508 |
509 | layer++
510 | }
511 | return { references, css: result, seens }
512 | }
513 |
514 | // steps:
515 | // 1. traverse object properties and evaluate each value.
516 | // 2. evaluate each node and insert into a ordered collection.
517 | // 3. try to merge the logical expression or override the style object.
518 | // 4. convert vanila collection to css object. (note is not an AST expression)
519 | // 5. convert css object to JS AST expression. (for stylex)
520 |
521 | export function evaluateCSS(path: NodePath, mod: Module) {
522 | return sortAndMergeEvaluatedResult(evaluatePath(path, mod))
523 | }
524 |
--------------------------------------------------------------------------------