├── .github ├── CODE_OF_CONDUCT.md └── workflows │ ├── deploy-site.yaml │ ├── publish.yaml │ ├── release.yaml │ └── tests.yaml ├── .gitignore ├── .node-version ├── .vscode └── settings.json ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── docs ├── .vitepress │ ├── config.mts │ └── theme │ │ └── index.ts ├── guide │ ├── api.md │ ├── configuration.md │ └── reference.md ├── index.md ├── integrations │ ├── overflow.md │ └── vite.md └── package.json ├── dprint.json ├── eslint.config.js ├── examples ├── vite-postcss-demo │ ├── index.html │ ├── package.json │ ├── src │ │ ├── app.tsx │ │ ├── component │ │ │ └── aliases.tsx │ │ ├── main.tsx │ │ ├── stylex.css │ │ └── themes │ │ │ └── colors.stylex.ts │ ├── tsconfig.json │ └── vite.config.ts ├── vite-react-demo │ ├── index.html │ ├── package.json │ ├── src │ │ ├── include.tsx │ │ └── main.tsx │ ├── tsconfig.json │ └── vite.config.ts └── vite-vue-demo │ ├── index.html │ ├── package.json │ ├── src │ ├── colors.stylex.ts │ ├── lang.vue │ ├── main.tsx │ └── pages │ │ └── xx.vue │ ├── tsconfig.json │ └── vite.config.ts ├── package.json ├── packages ├── babel-plugin │ ├── PRINCIPLE.md │ ├── README.md │ ├── __tests__ │ │ ├── fixtures │ │ │ ├── id │ │ │ │ ├── transform │ │ │ │ │ ├── code.js │ │ │ │ │ └── output.js │ │ │ │ └── with-inline │ │ │ │ │ ├── code.js │ │ │ │ │ └── output.js │ │ │ ├── inject-global-style │ │ │ │ ├── code.js │ │ │ │ ├── expression.stylex.js │ │ │ │ └── output.js │ │ │ ├── inline-macro │ │ │ │ ├── args │ │ │ │ │ ├── code.js │ │ │ │ │ └── output.js │ │ │ │ ├── called-multiple │ │ │ │ │ ├── code.js │ │ │ │ │ └── output.js │ │ │ │ ├── merged │ │ │ │ │ ├── code.js │ │ │ │ │ └── output.js │ │ │ │ ├── single-call │ │ │ │ │ ├── code.js │ │ │ │ │ └── output.js │ │ │ │ └── static-prop │ │ │ │ │ ├── code.js │ │ │ │ │ └── output.js │ │ │ └── jsx-attributes │ │ │ │ ├── define-css-variable │ │ │ │ ├── code.js │ │ │ │ └── output.js │ │ │ │ ├── dynamic-nested-prop │ │ │ │ ├── code.js │ │ │ │ └── output.js │ │ │ │ ├── dynamic-same-variable │ │ │ │ ├── code.js │ │ │ │ └── output.js │ │ │ │ ├── dynamic-single-prop │ │ │ │ ├── code.js │ │ │ │ └── output.js │ │ │ │ ├── dynamic-spread-prop │ │ │ │ ├── code.js │ │ │ │ └── output.js │ │ │ │ ├── keyframes-prop │ │ │ │ ├── code.js │ │ │ │ └── output.js │ │ │ │ ├── static-nested-prop │ │ │ │ ├── code.js │ │ │ │ └── output.js │ │ │ │ ├── static-single-prop │ │ │ │ ├── code.js │ │ │ │ └── output.js │ │ │ │ └── static-spread-prop │ │ │ │ ├── code.js │ │ │ │ └── output.js │ │ └── stylex-macro.spec.ts │ ├── package.json │ ├── rollup.config.mts │ ├── src │ │ ├── ast │ │ │ ├── evaluate-path.ts │ │ │ ├── message.ts │ │ │ └── shared.ts │ │ ├── index.ts │ │ ├── interface.ts │ │ ├── module.ts │ │ └── visitor │ │ │ ├── global-style.ts │ │ │ ├── id.ts │ │ │ ├── imports.ts │ │ │ ├── index.ts │ │ │ ├── inline.ts │ │ │ └── jsx-attribute.ts │ └── tsconfig.json ├── core │ ├── README.md │ ├── package.json │ ├── rollup.config.mts │ └── src │ │ └── index.ts ├── postcss │ ├── README.md │ ├── package.json │ └── src │ │ ├── builder.js │ │ ├── bundler.js │ │ └── index.js ├── react │ ├── README.md │ ├── package.json │ └── src │ │ ├── index.d.ts │ │ ├── index.js │ │ ├── jsx-dev-runtime.d.ts │ │ ├── jsx-dev-runtime.js │ │ ├── jsx-dev-runtime.mjs │ │ ├── jsx-namespace.d.ts │ │ ├── jsx-runtime.d.ts │ │ ├── jsx-runtime.js │ │ └── jsx-runtime.mjs ├── shared │ ├── README.md │ ├── package.json │ ├── rollup.config.mts │ ├── src │ │ ├── css-type.ts │ │ ├── hash.ts │ │ └── index.ts │ └── tsconfig.json ├── vite │ ├── README.md │ ├── client.d.ts │ ├── package.json │ ├── rollup.config.mts │ ├── src │ │ ├── compile.ts │ │ ├── index.ts │ │ └── postcss-ver.ts │ └── tsconfig.json └── vue │ ├── README.md │ ├── index.d.ts │ ├── index.js │ └── package.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── tsconfig.json └── vitest.config.ts /.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 | -------------------------------------------------------------------------------- /.github/workflows/deploy-site.yaml: -------------------------------------------------------------------------------- 1 | name: Deploy Site 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | 7 | jobs: 8 | deploy-site: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | - name: Install Dependencies 13 | run: make 14 | - uses: actions/setup-node@v3 15 | with: 16 | node-version: 22.9.0 17 | - name: Build site 18 | run: make build-site 19 | 20 | - name: Deploy site 21 | uses: JamesIves/github-pages-deploy-action@v4.4.3 22 | with: 23 | branch: gh-page 24 | folder: docs/.vitepress/dist 25 | single-commit: true 26 | clean: true 27 | -------------------------------------------------------------------------------- /.github/workflows/publish.yaml: -------------------------------------------------------------------------------- 1 | name: publish 2 | on: 3 | push: 4 | tags: ["v*"] 5 | 6 | jobs: 7 | publish: 8 | runs-on: ubuntu-latest 9 | permissions: 10 | contents: read 11 | id-token: write 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: actions/setup-node@v4 15 | with: 16 | node-version: 22.9.0 17 | registry-url: "https://registry.npmjs.org" 18 | - name: Install Dependices 19 | run: make 20 | - name: Pack and Publish 21 | run: | 22 | make publish-all 23 | env: 24 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 25 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: releaser 2 | 3 | on: 4 | push: 5 | tags: ["v*"] 6 | 7 | jobs: 8 | releaser: 9 | permissions: 10 | contents: write 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: Extra Changelog 15 | run: | 16 | CHANGELOG=$(awk -v ver=$(awk -F'"' '/"version": ".+"/{ print $4; exit; }' package.json) '/^## / { if (p) { exit }; if ($2 == ver) { p=1; next} } p' CHANGELOG.md) 17 | echo "CHANGELOG<> $GITHUB_ENV 18 | echo "$CHANGELOG" >> $GITHUB_ENV 19 | echo "EOF" >> $GITHUB_ENV 20 | - name: Github Releaser 21 | uses: actions/create-release@v1 22 | env: 23 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 24 | with: 25 | tag_name: ${{ github.ref }} 26 | body: ${{ env.CHANGELOG }} 27 | draft: false 28 | prerelease: false 29 | -------------------------------------------------------------------------------- /.github/workflows/tests.yaml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v4 10 | - name: Install pnpm 11 | run: corepack enable 12 | - uses: actions/setup-node@v3 13 | with: 14 | node-version: 22.9.0 15 | - name: Install Dependices 16 | run: make 17 | - name: Prepare Dependencies 18 | run: make build-all 19 | 20 | - name: Run Test 21 | run: pnpm test 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .DS_Store 4 | **/.vitepress/cache 5 | **/.vitepress/dist 6 | typed-router.d.ts -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | v22.9.0 -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.useFlatConfig": true, 3 | "editor.defaultFormatter": "dprint.dprint", 4 | "dprint.path": "./node_modules/.bin/dprint", 5 | "editor.codeActionsOnSave": { 6 | "source.fixAll": "explicit" 7 | }, 8 | "editor.formatOnSave": true, 9 | "[json]": { 10 | "editor.defaultFormatter": "dprint.dprint" 11 | }, 12 | "[markdown]": { 13 | "editor.defaultFormatter": "dprint.dprint" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 kanno 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | currentDir = $(CURDIR) 2 | 3 | install: 4 | @echo "Setup pnpm package manager..." 5 | npm install -g corepack@latest --force 6 | corepack enable 7 | pnpm install 8 | 9 | build-all: build-shared build-core build-babel-plugin build-vite 10 | 11 | build-babel-plugin: 12 | @echo "Building babel-plugin package..." 13 | $(eval currentDir = $(CURDIR)/packages/babel-plugin) 14 | -rm -rf $(currentDir)/dist 15 | pnpm -C $(currentDir) run build 16 | 17 | build-core: build-shared 18 | @echo "Building core package..." 19 | $(eval currentDir = $(CURDIR)/packages/core) 20 | -rm -rf $(currentDir)/dist 21 | pnpm -C $(currentDir) run build 22 | 23 | 24 | build-vite: build-babel-plugin 25 | @echo "Building vite package..." 26 | $(eval currentDir = $(CURDIR)/packages/vite) 27 | -rm -rf $(currentDir)/dist 28 | pnpm -C $(currentDir) run build 29 | 30 | build-shared: 31 | @echo "Building shared package..." 32 | $(eval currentDir = $(CURDIR)/packages/shared) 33 | -rm -rf $(currentDir)/dist 34 | pnpm -C $(currentDir) run build 35 | 36 | publish-all: build-all 37 | @echo "Publishing packages..." 38 | pnpm -r publish --no-git-checks --access public 39 | 40 | test: 41 | pnpm -r run test 42 | 43 | cleanup-suite: 44 | @echo "Cleanup babel plugin ouput suite..." 45 | $(eval currentDir = $(CURDIR)/packages/babel-plugin/__tests__) 46 | find $(currentDir) -type f -name "output.js" -exec rm -f {} \; 47 | 48 | lint: 49 | @echo "Linting code..." 50 | pnpm exec eslint --fix "packages/**/*{.ts,.tsx,.js}" 51 | 52 | format: 53 | @echo "formatting code..." 54 | pnpm exec dprint fmt 55 | 56 | dev-site: 57 | @echo "Starting dev site..." 58 | pnpm -C $(currentDir)/docs run dev 59 | 60 | build-site: 61 | @echo "Building site..." 62 | pnpm -C $(currentDir)/docs run build -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | 5 | 6 | 7 |

8 | 9 | ### Document 10 | 11 | [website](https://nonzzz.github.io/stylex-extend/) 12 | 13 | ### Sponsors 14 | 15 |

16 | 17 | 18 | 19 |

20 | 21 | ### Author 22 | 23 | Kanno 24 | 25 | ### License 26 | 27 | [MIT](./LICENSE) 28 | -------------------------------------------------------------------------------- /docs/.vitepress/config.mts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitepress' 2 | import { groupIconMdPlugin, groupIconVitePlugin } from 'vitepress-plugin-group-icons' 3 | 4 | // https://vitepress.dev/reference/site-config 5 | export default defineConfig({ 6 | base: '/stylex-extend/', 7 | title: 'stylex-extend', 8 | description: 'An unoffical stylexjs experimental project', 9 | cleanUrls: true, 10 | themeConfig: { 11 | // https://vitepress.dev/reference/default-theme-config 12 | nav: [ 13 | { text: 'Guide', link: '/guide/reference', activeMatch: '/guide/' }, 14 | { text: 'Integrations', link: '/integrations/overflow', activeMatch: '/integrations/' } 15 | ], 16 | sidebar: [ 17 | { 18 | text: 'Guide', 19 | items: [ 20 | { text: 'Reference', link: '/guide/reference' }, 21 | { text: 'Configuration', link: '/guide/configuration' }, 22 | { text: 'APIs', link: '/guide/api' } 23 | ] 24 | }, 25 | { 26 | text: 'Integrations', 27 | items: [ 28 | { text: 'Overflow', link: '/integrations/overflow' }, 29 | { text: 'Vite', link: '/integrations/vite' } 30 | ] 31 | } 32 | ], 33 | socialLinks: [ 34 | { icon: 'github', link: 'https://github.com/nonzzz/stylex-extend' } 35 | ], 36 | footer: { 37 | message: 'Released under the MIT License.', 38 | copyright: 'Copyright © 2024-present Kanno' 39 | }, 40 | lastUpdated: { 41 | text: 'Last Modified', 42 | formatOptions: { 43 | dateStyle: 'short', 44 | timeStyle: 'medium' 45 | } 46 | } 47 | }, 48 | markdown: { 49 | config(md) { 50 | md.use(groupIconMdPlugin) 51 | } 52 | }, 53 | vite: { 54 | plugins: [ 55 | groupIconVitePlugin() 56 | ] 57 | } 58 | }) 59 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/index.ts: -------------------------------------------------------------------------------- 1 | import Theme from 'vitepress/theme' 2 | import 'virtual:group-icons.css' 3 | 4 | export default Theme 5 | -------------------------------------------------------------------------------- /docs/guide/api.md: -------------------------------------------------------------------------------- 1 | # APIs 2 | 3 | ## Inline 4 | 5 | - [RFC](https://github.com/facebook/stylex/issues/534#issuecomment-2121745213) in StyleX 6 | 7 | ### Usage 8 | 9 | ```tsx 10 | import { inline } from '@stylex-extend/core' 11 | import { create, props } from '@stylexjs/js' 12 | 13 | const styles = create({ 14 | summary: { 15 | color: 'pink' 16 | } 17 | }) 18 | 19 | export function Component() { 20 | return ( 21 |
22 | 23 |
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 | -------------------------------------------------------------------------------- /docs/guide/configuration.md: -------------------------------------------------------------------------------- 1 | # Configuration 2 | 3 | Currently it only provide [vite integration](../integrations/vite.md). If you use other tools, you can also integrate them manually. 4 | 5 | The following are the useful packages included in the project 6 | 7 | ## @stylex-extend/babel-plugin 8 | 9 | ### Configuration Options 10 | 11 |

12 | 13 | #### transport 14 | 15 | - Type: `Transport` 16 | - Default: `true` 17 | 18 | Specifying this in config will translate JSXAttribute `stylex`. 19 | 20 | ```ts 21 | export type Transport = 'props' | 'attrs' 22 | ``` 23 | 24 | #### aliases 25 | 26 | - Type: `Record` 27 | - Default: `{}` 28 | 29 | Allows you to alias project directories to absolute paths, making it easier to import modules. 30 | 31 | ## @stylex-extend/core 32 | 33 | Provides a collection of experimental APIs. 34 | 35 | ## @stylex-extend/react 36 | 37 | Provide LSP support for react jsx. 38 | 39 | ## @stylex-extend/vue 40 | 41 | Provide LSP support for vue jsx. 42 | -------------------------------------------------------------------------------- /docs/guide/reference.md: -------------------------------------------------------------------------------- 1 | # Welcome to stylex-extend 2 | 3 | Stylex-extend is an unofficial [`StyleX`](https://stylexjs.com/) experimental project. 4 | 5 | It allows you to use some unreleased experimental features without having to worry about feature to migrations. 6 | 7 | ## Reference 8 | 9 | - [Configuration](./configuration.md) 10 | - [APIs](./api.md) 11 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | # https://vitepress.dev/reference/default-theme-home-page 3 | layout: home 4 | 5 | hero: 6 | name: "stylex-extend" 7 | text: "An unofficial stylexjs experimental project" 8 | actions: 9 | - theme: brand 10 | text: Get started 11 | link: /guide/reference 12 | - theme: alt 13 | text: View on Github 14 | link: https://github.com/nonzzz/stylex-extend 15 | --- 16 | -------------------------------------------------------------------------------- /docs/integrations/overflow.md: -------------------------------------------------------------------------------- 1 | # Integrations 2 | 3 | ## Frameworks / Tools 4 | -------------------------------------------------------------------------------- /docs/integrations/vite.md: -------------------------------------------------------------------------------- 1 | # Vite Plugin 2 | 3 | ## Install 4 | 5 | :::code-group 6 | 7 | ```bash [pnpm] 8 | pnpm add -D @stylex-extend/core @stylex-extend/vite 9 | ``` 10 | 11 | ```bash [yarn] 12 | yarn add -D @stylex-extend/core @stylex-extend/vite 13 | ``` 14 | 15 | ```bash [npm] 16 | npm install -D @stylex-extend/core @stylex-extend/vite 17 | ``` 18 | 19 | ::: 20 | 21 | Install the plugin: 22 | 23 | ```ts 24 | // vite.config.ts 25 | import { stylex } from '@stylex-extend/vite' 26 | import { defineConfig } from 'vite' 27 | 28 | export default defineConfig({ 29 | plugins: [ 30 | stylex() 31 | ] 32 | }) 33 | ``` 34 | 35 | ## Usage 36 | 37 | ```ts 38 | // in your application entry file. 39 | import 'virtual:stylex.css' 40 | ``` 41 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "document", 3 | "private": true, 4 | "scripts": { 5 | "dev": "vitepress dev", 6 | "build": "vitepress build", 7 | "preview": "vitepress preview" 8 | }, 9 | "devDependencies": { 10 | "vitepress": "^1.3.4" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /dprint.json: -------------------------------------------------------------------------------- 1 | { 2 | "lineWidth": 140, 3 | "extends": "https://dprint.nonzzz.moe/dprint.json", 4 | "excludes": ["**/dist", "**/*/output.js", "packages/**/*.d.ts"] 5 | } 6 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | const { nonzzz } = require('eslint-config-kagura') 2 | const { react } = require('@eslint-sukka/react') 3 | 4 | module.exports = nonzzz( 5 | { typescript: true }, 6 | ...react(), 7 | { 8 | ignores: [ 9 | '**/node_modules', 10 | '**/dist', 11 | '**/*/output.js', 12 | '**/*/*.d.ts', 13 | '**/analysis' 14 | ] 15 | } 16 | ) 17 | -------------------------------------------------------------------------------- /examples/vite-postcss-demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 |

10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/vite-postcss-demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vite-react-demo", 3 | "type": "module", 4 | "scripts": { 5 | "dev": "vite", 6 | "build": "vite build", 7 | "preview": "vite preview" 8 | }, 9 | "devDependencies": { 10 | "@stylex-extend/react": "workspace:*", 11 | "@types/react": "^18.2.72", 12 | "@types/react-dom": "^18.2.22", 13 | "@vitejs/plugin-react": "^4.2.1", 14 | "react": "^18.2.0", 15 | "react-dom": "^18.2.0", 16 | "vite": "^5.2.6" 17 | }, 18 | "license": "MIT" 19 | } 20 | -------------------------------------------------------------------------------- /examples/vite-postcss-demo/src/app.tsx: -------------------------------------------------------------------------------- 1 | import { injectGlobalStyle } from '@stylex-extend/core' 2 | import React, { useState } from 'react' 3 | import { Aliases } from '~/component/aliases' 4 | import { colors } from '~/themes/colors.stylex' 5 | 6 | injectGlobalStyle({ 7 | body: { 8 | margin: 0, 9 | border: '1px solid pink', 10 | backgroundColor: colors.gray 11 | } 12 | }) 13 | 14 | interface ButtonProps { 15 | color: string 16 | onClick: () => void 17 | } 18 | 19 | function Button(props: React.PropsWithChildren) { 20 | return
{props.children}
21 | } 22 | 23 | export function App() { 24 | const [color, setColor] = useState('red') 25 | 26 | return ( 27 |
28 |
29 | With macro 30 |

31 | Blue 32 | Green 33 |

34 |
35 | 36 | 37 |
38 | ) 39 | } 40 | -------------------------------------------------------------------------------- /examples/vite-postcss-demo/src/component/aliases.tsx: -------------------------------------------------------------------------------- 1 | export function Aliases() { 2 | return
I'm aliases
3 | } 4 | -------------------------------------------------------------------------------- /examples/vite-postcss-demo/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | import './stylex.css' 4 | import { App } from './app' 5 | 6 | ReactDOM.createRoot(document.querySelector('#app')).render( 7 | 8 | 9 | 10 | ) 11 | -------------------------------------------------------------------------------- /examples/vite-postcss-demo/src/stylex.css: -------------------------------------------------------------------------------- 1 | @stylex; 2 | -------------------------------------------------------------------------------- /examples/vite-postcss-demo/src/themes/colors.stylex.ts: -------------------------------------------------------------------------------- 1 | import { defineVars } from '@stylexjs/stylex' 2 | 3 | export const colors = defineVars({ 4 | blue: 'blue', 5 | gray: '#f5f5f5' 6 | }) 7 | -------------------------------------------------------------------------------- /examples/vite-postcss-demo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "lib": ["DOM"], 5 | "esModuleInterop": true, 6 | "jsx": "react-jsx", 7 | "moduleResolution": "Node", 8 | "types": ["vite/client", "react", "react-dom"], 9 | "jsxImportSource": "@stylex-extend/react", 10 | "baseUrl": ".", 11 | "paths": { 12 | "~/*": ["./src/*"] 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /examples/vite-postcss-demo/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { stylex } from '@stylex-extend/vite/postcss-ver' 2 | import react from '@vitejs/plugin-react' 3 | import path from 'path' 4 | import url from 'url' 5 | import { defineConfig } from 'vite' 6 | 7 | const __filename = url.fileURLToPath(import.meta.url) 8 | 9 | const __dirname = path.dirname(__filename) 10 | 11 | export default defineConfig({ 12 | resolve: { 13 | alias: { 14 | '~': path.resolve(__dirname, 'src') 15 | } 16 | }, 17 | plugins: [ 18 | react(), 19 | stylex({ 20 | mactroTransport: 'props', 21 | postcss: { 22 | include: ['examples/vite-postcss-demo/src/**/*.{ts,tsx}'], 23 | aliases: { 24 | '~/*': [path.join(__dirname, 'src/*')] 25 | } 26 | } 27 | }) 28 | ] 29 | }) 30 | -------------------------------------------------------------------------------- /examples/vite-react-demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/vite-react-demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vite-react-demo", 3 | "type": "module", 4 | "scripts": { 5 | "dev": "vite", 6 | "build": "vite build", 7 | "preview": "vite preview" 8 | }, 9 | "devDependencies": { 10 | "@stylex-extend/react": "workspace:*", 11 | "@types/react": "^18.2.72", 12 | "@types/react-dom": "^18.2.22", 13 | "@vitejs/plugin-react": "^4.2.1", 14 | "react": "^18.2.0", 15 | "react-dom": "^18.2.0", 16 | "vite": "^5.2.6" 17 | }, 18 | "license": "MIT" 19 | } 20 | -------------------------------------------------------------------------------- /examples/vite-react-demo/src/include.tsx: -------------------------------------------------------------------------------- 1 | import { inline } from '@stylex-extend/core' 2 | import { create, props } from '@stylexjs/stylex' 3 | 4 | function include(args: ReturnType) { 5 | return args 6 | } 7 | 8 | const classical = include(inline({ color: 'lightskyblue' })) 9 | 10 | const x = create({ 11 | normal: { 12 | color: 'pink' 13 | } 14 | }) 15 | 16 | export function Include() { 17 | return
Include
18 | } 19 | -------------------------------------------------------------------------------- /examples/vite-react-demo/src/main.tsx: -------------------------------------------------------------------------------- 1 | import { id } from '@stylex-extend/core' 2 | import * as stylex from '@stylexjs/stylex' 3 | import React, { useState } from 'react' 4 | import ReactDOM from 'react-dom/client' 5 | import 'virtual:stylex.css' 6 | import { Include } from './include' 7 | 8 | interface ButtonProps { 9 | color: string 10 | onClick: () => void 11 | } 12 | 13 | const myId = id() 14 | const myId2 = id(true) 15 | 16 | const basic = stylex.create({ 17 | base: { 18 | [myId]: { 19 | default: 'green', 20 | ':hover': 'purple' 21 | } 22 | }, 23 | font: { 24 | [myId]: { 25 | default: '20px' 26 | } 27 | } 28 | }) 29 | 30 | function Button(props: React.PropsWithChildren) { 31 | return
{props.children}
32 | } 33 | 34 | export function App() { 35 | const [color, setColor] = useState('red') 36 | 37 | return ( 38 |
39 |
40 | This is a Base Case 41 |

42 | Green Text 43 | 44 | Large Font 45 | 46 |

47 |
48 |
49 | With macro 50 |

51 | Text 52 | 53 | Purple 54 | Green 55 | 56 |

57 |
58 | 59 | 60 |
61 | ) 62 | } 63 | 64 | ReactDOM.createRoot(document.querySelector('#app')!).render( 65 | 66 | 67 | 68 | ) 69 | -------------------------------------------------------------------------------- /examples/vite-react-demo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "lib": ["DOM"], 5 | "esModuleInterop": true, 6 | "jsx": "react-jsx", 7 | "moduleResolution": "Node", 8 | "types": ["vite/client", "react", "react-dom"], 9 | "jsxImportSource": "@stylex-extend/react" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /examples/vite-react-demo/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { stylex } from '@stylex-extend/vite' 2 | import react from '@vitejs/plugin-react' 3 | import { defineConfig } from 'vite' 4 | 5 | export default defineConfig({ 6 | plugins: [ 7 | react(), 8 | stylex() 9 | ] 10 | }) 11 | -------------------------------------------------------------------------------- /examples/vite-vue-demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/vite-vue-demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vite-vue-demo", 3 | "type": "module", 4 | "scripts": { 5 | "dev": "vite", 6 | "build": "vite build", 7 | "preview": "vite preview" 8 | }, 9 | "devDependencies": { 10 | "@stylex-extend/vue": "workspace:*", 11 | "@vitejs/plugin-vue": "^5.0.4", 12 | "@vitejs/plugin-vue-jsx": "^3.1.0", 13 | "vite": "^5.2.6", 14 | "vue": "^3.4.21", 15 | "vue-router": "^4.5.0", 16 | "unplugin-vue-router": "^0.10.9" 17 | }, 18 | "license": "MIT" 19 | } 20 | -------------------------------------------------------------------------------- /examples/vite-vue-demo/src/colors.stylex.ts: -------------------------------------------------------------------------------- 1 | import { defineVars } from '@stylexjs/stylex' 2 | 3 | export const colors = defineVars({ 4 | purple: 'purple' 5 | }) 6 | -------------------------------------------------------------------------------- /examples/vite-vue-demo/src/lang.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 21 | -------------------------------------------------------------------------------- /examples/vite-vue-demo/src/main.tsx: -------------------------------------------------------------------------------- 1 | import { injectGlobalStyle } from '@stylex-extend/core' 2 | import { keyframes } from '@stylexjs/stylex' 3 | import { createApp, defineComponent } from 'vue' 4 | import { RouterView, createRouter, createWebHistory, useRouter } from 'vue-router' 5 | import { routes } from 'vue-router/auto-routes' 6 | import { colors } from './colors.stylex' 7 | import Lang from './lang.vue' 8 | import 'virtual:stylex.css' 9 | 10 | // import 'stylex.css' 11 | 12 | console.log(colors) 13 | 14 | const pulse = keyframes({ 15 | '0%': { transform: 'scale(1)' }, 16 | '50%': { transform: 'scale(0.5)' }, 17 | '100%': { transform: 'scale(1)' } 18 | }) 19 | 20 | const A = defineComponent(() => { 21 | return () =>
123
22 | }) 23 | 24 | const color = 'purple' 25 | 26 | export { colors } 27 | 28 | injectGlobalStyle({ 29 | p: { 30 | color: colors.purple 31 | } 32 | }) 33 | 34 | const App = defineComponent({ 35 | setup() { 36 | // eslint-disable-next-line react-hooks/rules-of-hooks 37 | const router = useRouter() 38 | const handleClick = () => { 39 | router.push({ name: '/xx' }).catch(console.error) 40 | } 41 | 42 | return () => ( 43 | <> 44 |
45 | 456 46 |
47 |

text

48 | 56 | 57 | 58 | 61 | 62 | 63 | ) 64 | } 65 | }) 66 | 67 | const router = createRouter({ 68 | history: createWebHistory(), 69 | routes 70 | }) 71 | 72 | createApp(App).use(router).mount('#app') 73 | -------------------------------------------------------------------------------- /examples/vite-vue-demo/src/pages/xx.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 23 | -------------------------------------------------------------------------------- /examples/vite-vue-demo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "lib": ["DOM", "DOM.Iterable"], 5 | "esModuleInterop": true, 6 | "types": ["vite/client", "@stylex-extend/vue"], 7 | "jsx": "preserve", 8 | "jsxImportSource": "vue" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /examples/vite-vue-demo/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { stylex } from '@stylex-extend/vite' 2 | import vue from '@vitejs/plugin-vue' 3 | import jsx from '@vitejs/plugin-vue-jsx' 4 | import VueRouter from 'unplugin-vue-router/vite' 5 | import { defineConfig } from 'vite' 6 | 7 | export default defineConfig({ 8 | plugins: [VueRouter(), vue(), jsx(), stylex({ macroTransport: 'attrs', useCSSLayer: true, macroTransformOrder: 'post' })], 9 | optimizeDeps: { 10 | exclude: ['@stylex-extend/core'] 11 | } 12 | }) 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stylex-extend", 3 | "description": "An unofficial stylexjs extension", 4 | "version": "0.7.1", 5 | "scripts": { 6 | "test": "pnpm -r run test" 7 | }, 8 | "author": "kanno", 9 | "license": "MIT", 10 | "packageManager": "pnpm@9.0.1", 11 | "devDependencies": { 12 | "@eslint-sukka/react": "^6.12.0", 13 | "@stylex-extend/babel-plugin": "workspace:*", 14 | "@rollup/plugin-esm-shim": "^0.1.8", 15 | "@rollup/plugin-node-resolve": "^16.0.0", 16 | "rollup": "^4.34.9", 17 | "rollup-plugin-swc3": "^0.12.1", 18 | "@swc/core": "^1.11.5", 19 | "rollup-plugin-dts": "^6.1.1", 20 | "@stylex-extend/core": "file:./packages/core", 21 | "@stylex-extend/shared": "workspace:*", 22 | "@stylex-extend/vite": "workspace:*", 23 | "@stylexjs/babel-plugin": "0.11.1", 24 | "@stylexjs/stylex": "0.11.1", 25 | "@types/node": "^20.11.30", 26 | "dprint": "^0.45.1", 27 | "eslint": "^9.16.0", 28 | "eslint-config-kagura": "^3.0.1", 29 | "typescript": "^5.4.3", 30 | "vite": "^6.2.0", 31 | "vitepress-plugin-group-icons": "^1.1.0", 32 | "vitest": "^3.0.7" 33 | }, 34 | "pnpm": { 35 | "overrides": { 36 | "is-core-module": "npm:@nolyfill/is-core-module@^1", 37 | "vite": "^6.2.0" 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /packages/babel-plugin/PRINCIPLE.md: -------------------------------------------------------------------------------- 1 | # Principle 2 | 3 | Welcome to `@stylex-extend/babel-plugin`. This is an unofficial `@stylexjs` extension. We support using `JSXAttribute` syntax to define inline style and etc. 4 | 5 | All of features are based on the `@stylexjs` RFC or some interesting ideas. 6 | 7 | ## Features 8 | 9 | > JSXAttribute 10 | 11 | ```jsx 12 | // acceptable syntax 13 | 14 | const color = 'red' 15 | 16 | function font(unit) { 17 | return ... 18 | } 19 | 20 | function Component(props) { 21 | 22 | return
33 | } 34 | 35 | // We don't support overly complex syntax. don't write nested spreads syntax. 36 | ``` 37 | -------------------------------------------------------------------------------- /packages/babel-plugin/README.md: -------------------------------------------------------------------------------- 1 | # @stylex-extend/babel-plugin 2 | 3 | ## Quick Start 4 | 5 | ### Install 6 | 7 | ```bash 8 | yarn add @stylex-extend/babel-plugin 9 | ``` 10 | 11 | ### Usage 12 | 13 | ```js 14 | // .babelrc.js 15 | 16 | module.exports = { 17 | plugins: ['@stylex-extend/babel-plugin', '@stylexjs/babel-plugin'] 18 | } 19 | ``` 20 | -------------------------------------------------------------------------------- /packages/babel-plugin/__tests__/fixtures/id/transform/code.js: -------------------------------------------------------------------------------- 1 | import { id } from '@stylex-extend/core' 2 | import { create } from '@stylexjs/stylex' 3 | import stylex from '@stylexjs/stylex' 4 | 5 | const myId = id() 6 | 7 | const styles = create({ 8 | base: { 9 | [myId]: { 10 | default: 'red' 11 | } 12 | }, 13 | variant: { 14 | color: myId 15 | } 16 | }) 17 | 18 | export function Component() { 19 | return ( 20 |
23 |
24 |
25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /packages/babel-plugin/__tests__/fixtures/id/transform/output.js: -------------------------------------------------------------------------------- 1 | import { create } from "@stylexjs/stylex"; 2 | import stylex from "@stylexjs/stylex"; 3 | const myId = "var(--lw3x18)"; 4 | const styles = create({ 5 | base: { 6 | [myId]: { 7 | default: "red", 8 | }, 9 | }, 10 | variant: { 11 | color: myId, 12 | }, 13 | }); 14 | export function Component() { 15 | return ( 16 |
17 |
18 |
19 | ); 20 | } -------------------------------------------------------------------------------- /packages/babel-plugin/__tests__/fixtures/id/with-inline/code.js: -------------------------------------------------------------------------------- 1 | import { id, inline } from '@stylex-extend/core' 2 | import { props } from '@stylexjs/stylex' 3 | 4 | const myId = id(true) 5 | 6 | export function Component() { 7 | return ( 8 |
9 |

10 | First pagination 11 | 12 | Nested Text 13 | 14 |

15 |
16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /packages/babel-plugin/__tests__/fixtures/id/with-inline/output.js: -------------------------------------------------------------------------------- 1 | import { create as _create, props } from "@stylexjs/stylex"; 2 | const myId = { 3 | $id: "stylex-extend", 4 | value: "var(--1ulkty)", 5 | }; 6 | const _styles = _create({ 7 | $0: { 8 | "var(--1ulkty)": { 9 | default: "red", 10 | }, 11 | }, 12 | }); 13 | const _styles2 = _create({ 14 | $0: { 15 | color: "var(--1ulkty)", 16 | "var(--1ulkty)": { 17 | default: "28px", 18 | }, 19 | }, 20 | }); 21 | const _styles3 = _create({ 22 | $0: { 23 | fontSize: "var(--1ulkty)", 24 | }, 25 | }); 26 | export function Component() { 27 | return ( 28 |
29 |

30 | First pagination 31 | Nested Text 32 |

33 |
34 | ); 35 | } -------------------------------------------------------------------------------- /packages/babel-plugin/__tests__/fixtures/inject-global-style/code.js: -------------------------------------------------------------------------------- 1 | import { injectGlobalStyle } from '@stylex-extend/core' 2 | import { expression } from './expression.stylex' 3 | 4 | export const styles = injectGlobalStyle({ 5 | html: { 6 | fontSize: expression.font, 7 | padding: 0, 8 | border: `1px solid ${expression.red}` 9 | } 10 | }) 11 | -------------------------------------------------------------------------------- /packages/babel-plugin/__tests__/fixtures/inject-global-style/expression.stylex.js: -------------------------------------------------------------------------------- 1 | import { defineVars } from '@stylexjs/stylex' 2 | 3 | export const expression = defineVars({ 4 | font: '13px', 5 | red: 'red' 6 | }) 7 | -------------------------------------------------------------------------------- /packages/babel-plugin/__tests__/fixtures/inject-global-style/output.js: -------------------------------------------------------------------------------- 1 | import { expression } from "./expression.stylex"; 2 | export const styles = ""; -------------------------------------------------------------------------------- /packages/babel-plugin/__tests__/fixtures/inline-macro/args/code.js: -------------------------------------------------------------------------------- 1 | import { inline } from '@stylex-extend/core' 2 | 3 | function fn(arg) { 4 | console.log(arg) 5 | } 6 | 7 | fn(inline({ color: 'purple' })) 8 | -------------------------------------------------------------------------------- /packages/babel-plugin/__tests__/fixtures/inline-macro/args/output.js: -------------------------------------------------------------------------------- 1 | import { create as _create, props as _props } from "@stylexjs/stylex"; 2 | function fn(arg) { 3 | console.log(arg); 4 | } 5 | const _styles = _create({ 6 | $0: { 7 | color: "purple", 8 | }, 9 | }); 10 | fn(_styles.$0); -------------------------------------------------------------------------------- /packages/babel-plugin/__tests__/fixtures/inline-macro/called-multiple/code.js: -------------------------------------------------------------------------------- 1 | import { inline } from '@stylex-extend/core' 2 | import { create } from '@stylexjs/stylex' 3 | import stylex from '@stylexjs/stylex' 4 | 5 | const styles = create({ 6 | base: { 7 | color: 'red' 8 | } 9 | }) 10 | 11 | export function Component() { 12 | return ( 13 |
22 |
23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /packages/babel-plugin/__tests__/fixtures/inline-macro/called-multiple/output.js: -------------------------------------------------------------------------------- 1 | import { props as _props, create } from "@stylexjs/stylex"; 2 | import stylex from "@stylexjs/stylex"; 3 | const styles = create({ 4 | base: { 5 | color: "red", 6 | }, 7 | }); 8 | const _styles = create({ 9 | $0: { 10 | font: "16px", 11 | }, 12 | }); 13 | const _styles2 = create({ 14 | $0: { 15 | color: "pink", 16 | display: "flex", 17 | }, 18 | }); 19 | export function Component() { 20 | return
; 21 | } -------------------------------------------------------------------------------- /packages/babel-plugin/__tests__/fixtures/inline-macro/merged/code.js: -------------------------------------------------------------------------------- 1 | import { inline } from '@stylex-extend/core' 2 | import { props } from '@stylexjs/stylex' 3 | import { create } from '@stylexjs/stylex' 4 | 5 | function include(inlined) { 6 | return inlined 7 | } 8 | 9 | const inlined = include(inline({ color: 'purple' })) 10 | 11 | export const styles = create({ 12 | base: { 13 | color: 'red', 14 | fontSize: '16px', 15 | ...inlined 16 | } 17 | }) 18 | 19 | export const c = props(styles.base) 20 | -------------------------------------------------------------------------------- /packages/babel-plugin/__tests__/fixtures/inline-macro/merged/output.js: -------------------------------------------------------------------------------- 1 | import { create as _create, props } from "@stylexjs/stylex"; 2 | import { create } from "@stylexjs/stylex"; 3 | function include(inlined) { 4 | return inlined; 5 | } 6 | const _styles = _create({ 7 | $0: { 8 | color: "purple", 9 | }, 10 | }); 11 | const inlined = include(_styles.$0); 12 | export const styles = create({ 13 | base: { 14 | color: "red", 15 | fontSize: "16px", 16 | ...inlined, 17 | }, 18 | }); 19 | export const c = props(styles.base); -------------------------------------------------------------------------------- /packages/babel-plugin/__tests__/fixtures/inline-macro/single-call/code.js: -------------------------------------------------------------------------------- 1 | import { inline } from '@stylex-extend/core' 2 | 3 | export const s = inline({ color: 'red' }) 4 | -------------------------------------------------------------------------------- /packages/babel-plugin/__tests__/fixtures/inline-macro/single-call/output.js: -------------------------------------------------------------------------------- 1 | import { create as _create, props as _props } from "@stylexjs/stylex"; 2 | const _styles = _create({ 3 | $0: { 4 | color: "red", 5 | }, 6 | }); 7 | export const s = _props(_styles.$0); -------------------------------------------------------------------------------- /packages/babel-plugin/__tests__/fixtures/inline-macro/static-prop/code.js: -------------------------------------------------------------------------------- 1 | import { inline } from '@stylex-extend/core' 2 | import { create } from '@stylexjs/stylex' 3 | import stylex from '@stylexjs/stylex' 4 | 5 | const styles = create({ 6 | base: { 7 | color: 'red' 8 | } 9 | }) 10 | 11 | export function Component(props) { 12 | return ( 13 |
28 |
29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /packages/babel-plugin/__tests__/fixtures/inline-macro/static-prop/output.js: -------------------------------------------------------------------------------- 1 | import { props as _props, create } from "@stylexjs/stylex"; 2 | import stylex from "@stylexjs/stylex"; 3 | const styles = create({ 4 | base: { 5 | color: "red", 6 | }, 7 | }); 8 | const _styles = create({ 9 | $0: (propsHoverColor, propsActiveColor) => ({ 10 | font: "16px", 11 | display: "inline-flex", 12 | color: { 13 | defualt: "red", 14 | ":hover": propsHoverColor, 15 | ":active": propsActiveColor, 16 | }, 17 | backgroundColor: propsHoverColor, 18 | }), 19 | }); 20 | export function Component(props) { 21 | return ( 22 |
28 | ); 29 | } -------------------------------------------------------------------------------- /packages/babel-plugin/__tests__/fixtures/jsx-attributes/define-css-variable/code.js: -------------------------------------------------------------------------------- 1 | function t(s) { 2 | return s 3 | } 4 | 5 | const ctx = { t } 6 | 7 | export function Component(props) { 8 | const size = '16px' 9 | return ( 10 |
21 |
22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /packages/babel-plugin/__tests__/fixtures/jsx-attributes/define-css-variable/output.js: -------------------------------------------------------------------------------- 1 | import { create as _create, props as _props } from "@stylexjs/stylex"; 2 | function t(s) { 3 | return s; 4 | } 5 | const ctx = { 6 | t, 7 | }; 8 | const _styles = _create({ 9 | $0: (size, t, ctxT, a1c2mtth) => ({ 10 | "--font-size-unit": size, 11 | display: t, 12 | textAlign: ctxT, 13 | color: { 14 | default: null, 15 | "@media (max-width: 600px)": a1c2mtth, 16 | }, 17 | }), 18 | }); 19 | export function Component(props) { 20 | const size = "16px"; 21 | return ( 22 |
32 | ); 33 | } -------------------------------------------------------------------------------- /packages/babel-plugin/__tests__/fixtures/jsx-attributes/dynamic-nested-prop/code.js: -------------------------------------------------------------------------------- 1 | export function Component(props) { 2 | const color = 'pink' 3 | 4 | return ( 5 |
19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /packages/babel-plugin/__tests__/fixtures/jsx-attributes/dynamic-nested-prop/output.js: -------------------------------------------------------------------------------- 1 | import { create as _create, props as _props } from "@stylexjs/stylex"; 2 | const _styles = _create({ 3 | $0: (color, propsDisplay, propsNormalRadius, propsMaxRadius) => ({ 4 | color: color, 5 | fontSize: "20px", 6 | display: { 7 | display: "flex", 8 | "@media (max-width: 600px)": propsDisplay, 9 | }, 10 | borderRadius: { 11 | default: propsNormalRadius, 12 | "@media (max-width: 600px)": propsMaxRadius, 13 | }, 14 | }), 15 | }); 16 | export function Component(props) { 17 | const color = "pink"; 18 | return ( 19 |
24 | ); 25 | } -------------------------------------------------------------------------------- /packages/babel-plugin/__tests__/fixtures/jsx-attributes/dynamic-same-variable/code.js: -------------------------------------------------------------------------------- 1 | export function Component(props) { 2 | const color = 'pink' 3 | 4 | return ( 5 |
16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /packages/babel-plugin/__tests__/fixtures/jsx-attributes/dynamic-same-variable/output.js: -------------------------------------------------------------------------------- 1 | import { create as _create, props as _props } from "@stylexjs/stylex"; 2 | const _styles = _create({ 3 | $0: (color, propsOtherColor) => ({ 4 | color: { 5 | default: "purple", 6 | ":hover": color, 7 | ":focus": color, 8 | ":media (max-width: 600px)": propsOtherColor, 9 | ":active": color, 10 | }, 11 | }), 12 | }); 13 | export function Component(props) { 14 | const color = "pink"; 15 | return
; 16 | } -------------------------------------------------------------------------------- /packages/babel-plugin/__tests__/fixtures/jsx-attributes/dynamic-single-prop/code.js: -------------------------------------------------------------------------------- 1 | const color = 'pink' 2 | 3 | export function Component(props) { 4 | const bottom = '10px' 5 | return
6 | } 7 | -------------------------------------------------------------------------------- /packages/babel-plugin/__tests__/fixtures/jsx-attributes/dynamic-single-prop/output.js: -------------------------------------------------------------------------------- 1 | import { create as _create, props as _props } from "@stylexjs/stylex"; 2 | const color = "pink"; 3 | const _styles = _create({ 4 | $0: (color, amashst, aomo79q) => ({ 5 | color: color, 6 | fontSize: "20px", 7 | display: amashst, 8 | padding: aomo79q, 9 | }), 10 | }); 11 | export function Component(props) { 12 | const bottom = "10px"; 13 | return ( 14 |
23 | ); 24 | } -------------------------------------------------------------------------------- /packages/babel-plugin/__tests__/fixtures/jsx-attributes/dynamic-spread-prop/code.js: -------------------------------------------------------------------------------- 1 | const visible = true 2 | 3 | export function Component(props) { 4 | const { color, flex } = props 5 | return ( 6 |
26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /packages/babel-plugin/__tests__/fixtures/jsx-attributes/dynamic-spread-prop/output.js: -------------------------------------------------------------------------------- 1 | import { create as _create, props as _props } from "@stylexjs/stylex"; 2 | const visible = true; 3 | const _styles = _create({ 4 | $0: (color) => ({ 5 | color: color, 6 | }), 7 | $1: { 8 | display: "flex", 9 | }, 10 | $2: (propsFontSize, propsFontSizeActive, propsColor, flex) => ({ 11 | fontSize: { 12 | default: "18px", 13 | "@media (max-width: 768px)": "20px", 14 | ":hover": propsFontSize, 15 | ":active": propsFontSizeActive, 16 | ":focus": propsFontSize, 17 | }, 18 | color: { 19 | default: "pink", 20 | hover: propsColor, 21 | }, 22 | display: flex, 23 | }), 24 | }); 25 | export function Component(props) { 26 | const { color, flex } = props; 27 | return ( 28 |
36 | ); 37 | } -------------------------------------------------------------------------------- /packages/babel-plugin/__tests__/fixtures/jsx-attributes/keyframes-prop/code.js: -------------------------------------------------------------------------------- 1 | const pulse = stylex.keyframes({ 2 | '0%': { transform: 'scale(1)' }, 3 | '50%': { transform: 'scale(1.1)' }, 4 | '100%': { transform: 'scale(1)' } 5 | }) 6 | 7 | export function Component() { 8 | return ( 9 |
26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /packages/babel-plugin/__tests__/fixtures/jsx-attributes/keyframes-prop/output.js: -------------------------------------------------------------------------------- 1 | import { create as _create, props as _props } from "@stylexjs/stylex"; 2 | const pulse = stylex.keyframes({ 3 | "0%": { 4 | transform: "scale(1)", 5 | }, 6 | "50%": { 7 | transform: "scale(1.1)", 8 | }, 9 | "100%": { 10 | transform: "scale(1)", 11 | }, 12 | }); 13 | const _styles = _create({ 14 | $0: { 15 | color: "red", 16 | }, 17 | $1: { 18 | display: "flex", 19 | }, 20 | $2: (pulse) => ({ 21 | pulse: { 22 | animationName: pulse, 23 | animationDuration: "1s", 24 | animationIterationCount: "infinite", 25 | }, 26 | }), 27 | $3: { 28 | fontSize: { 29 | default: "18px", 30 | "@media (max-width: 768px)": "20px", 31 | }, 32 | }, 33 | }); 34 | export function Component() { 35 | return ( 36 |
37 | ); 38 | } -------------------------------------------------------------------------------- /packages/babel-plugin/__tests__/fixtures/jsx-attributes/static-nested-prop/code.js: -------------------------------------------------------------------------------- 1 |
15 | -------------------------------------------------------------------------------- /packages/babel-plugin/__tests__/fixtures/jsx-attributes/static-nested-prop/output.js: -------------------------------------------------------------------------------- 1 | import { create as _create, props as _props } from "@stylexjs/stylex"; 2 | const _styles = _create({ 3 | $0: { 4 | color: "red", 5 | fontSize: { 6 | default: "16px", 7 | "@media (min-width: 768px)": "20px", 8 | }, 9 | textAlign: { 10 | default: "center", 11 | }, 12 | display: "flex", 13 | lineHeight: 1.5, 14 | }, 15 | }); 16 |
; -------------------------------------------------------------------------------- /packages/babel-plugin/__tests__/fixtures/jsx-attributes/static-single-prop/code.js: -------------------------------------------------------------------------------- 1 |
7 | -------------------------------------------------------------------------------- /packages/babel-plugin/__tests__/fixtures/jsx-attributes/static-single-prop/output.js: -------------------------------------------------------------------------------- 1 | import { create as _create, props as _props } from "@stylexjs/stylex"; 2 | const _styles = _create({ 3 | $0: { 4 | color: "red", 5 | fontSize: "16px", 6 | }, 7 | }); 8 |
; -------------------------------------------------------------------------------- /packages/babel-plugin/__tests__/fixtures/jsx-attributes/static-spread-prop/code.js: -------------------------------------------------------------------------------- 1 | export const Component = () => { 2 | return ( 3 |
15 | ) 16 | } 17 | 18 | export const Component2 = () => { 19 | const ok = true 20 | return ( 21 |
32 | ) 33 | } 34 | 35 | export const Component3 = () => { 36 | const ok = true 37 | const ok2 = true 38 | return ( 39 |
54 | ) 55 | } 56 | -------------------------------------------------------------------------------- /packages/babel-plugin/__tests__/fixtures/jsx-attributes/static-spread-prop/output.js: -------------------------------------------------------------------------------- 1 | import { create as _create, props as _props } from "@stylexjs/stylex"; 2 | const _styles = _create({ 3 | $0: { 4 | color: "red", 5 | }, 6 | $1: { 7 | display: "flex", 8 | }, 9 | $2: { 10 | fontSize: { 11 | default: "18px", 12 | "@media (max-width: 768px)": "20px", 13 | }, 14 | }, 15 | }); 16 | export const Component = () => { 17 | return
; 18 | }; 19 | const _styles2 = _create({ 20 | $0: { 21 | display: "flex", 22 | }, 23 | $1: { 24 | fontSize: { 25 | default: "18px", 26 | "@media (max-width: 768px)": "20px", 27 | }, 28 | }, 29 | }); 30 | export const Component2 = () => { 31 | const ok = true; 32 | return
; 33 | }; 34 | const _styles3 = _create({ 35 | $0: { 36 | color: "red", 37 | }, 38 | $1: { 39 | display: "flex", 40 | }, 41 | $2: { 42 | fontSize: { 43 | default: "18px", 44 | "@media (max-width: 768px)": "20px", 45 | }, 46 | }, 47 | $3: { 48 | color: "blue", 49 | }, 50 | $4: { 51 | color: "yellow", 52 | }, 53 | $5: { 54 | boxSizing: "box", 55 | }, 56 | }); 57 | export const Component3 = () => { 58 | const ok = true; 59 | const ok2 = true; 60 | return ( 61 |
71 | ); 72 | }; -------------------------------------------------------------------------------- /packages/babel-plugin/__tests__/stylex-macro.spec.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | 3 | import { pluginTester } from 'babel-plugin-tester' 4 | import plugin from '../src' 5 | 6 | pluginTester({ 7 | plugin, 8 | pluginName: 'stylex-extend', 9 | fixtures: path.join(__dirname, 'fixtures'), 10 | babelOptions: { 11 | parserOpts: { 12 | plugins: ['jsx'] 13 | } 14 | } 15 | }) 16 | -------------------------------------------------------------------------------- /packages/babel-plugin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@stylex-extend/babel-plugin", 3 | "version": "0.7.1", 4 | "description": "A friendly stylex babel plugin extension.", 5 | "module": "./dist/index.mjs", 6 | "main": "./dist/index.js", 7 | "types": "./dist/index.d.ts", 8 | "scripts": { 9 | "test": "vitest", 10 | "dev": "rollup --config rollup.config.mts --configPlugin swc3 --watch", 11 | "build": "rollup --config rollup.config.mts --configPlugin swc3" 12 | }, 13 | "license": "MIT", 14 | "repository": "https://github.com/nonzzz/stylex-extend.git", 15 | "exports": { 16 | ".": { 17 | "types": "./dist/index.d.ts", 18 | "import": "./dist/index.mjs", 19 | "require": "./dist/index.js" 20 | }, 21 | "./package.json": "./package.json" 22 | }, 23 | "devDependencies": { 24 | "@types/babel__core": "^7.20.5", 25 | "@types/stylis": "^4.2.5", 26 | "valibot": "^0.42.0", 27 | "babel-plugin-tester": "^11.0.4" 28 | }, 29 | "dependencies": { 30 | "@babel/core": "^7.23.9", 31 | "@stylexjs/shared": "0.9.3", 32 | "@stylex-extend/shared": "workspace:*", 33 | "stylis": "^4.3.1", 34 | "@dual-bundle/import-meta-resolve": "^4.1.0" 35 | }, 36 | "files": [ 37 | "dist", 38 | "README.md" 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /packages/babel-plugin/rollup.config.mts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unsafe-member-access */ 2 | /* eslint-disable @typescript-eslint/no-unsafe-argument */ 3 | import shim from '@rollup/plugin-esm-shim' 4 | import { nodeResolve } from '@rollup/plugin-node-resolve' 5 | import { builtinModules, createRequire } from 'module' 6 | import { defineConfig } from 'rollup' 7 | import { dts } from 'rollup-plugin-dts' 8 | import { swc } from 'rollup-plugin-swc3' 9 | 10 | // https://www.typescriptlang.org/tsconfig/#preserveSymlinks 11 | 12 | const _require = createRequire(import.meta.url) 13 | 14 | const external = [ 15 | ...builtinModules, 16 | ...Object.keys(_require('./package.json').dependencies) 17 | ] 18 | 19 | export default defineConfig( 20 | [ 21 | { 22 | input: 'src/index.ts', 23 | output: [ 24 | { file: 'dist/index.js', format: 'cjs' }, 25 | { file: 'dist/index.mjs', format: 'es' } 26 | ], 27 | plugins: [nodeResolve(), swc(), shim()], 28 | external 29 | }, 30 | { 31 | input: 'src/index.ts', 32 | output: { file: 'dist/index.d.ts', format: 'es' }, 33 | plugins: [dts({ compilerOptions: { preserveSymlinks: false } })], 34 | external 35 | } 36 | ] 37 | ) 38 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /packages/babel-plugin/src/ast/message.ts: -------------------------------------------------------------------------------- 1 | export const MESSAGES = { 2 | NOT_IMPLEMENTED: 'Not implemented.', 3 | INVALID_CSS_AST_KIND: 'Only accept a style object.', 4 | INVALID_JSX_ELEMENT: 'Invalid JSX element.', 5 | DUPLICATE_STYLEX_ATTR: 'Duplicate stylex attribute.', 6 | NO_STATIC_ATTRIBUTE: 'Only static attribute is allowed in style object.', 7 | NO_NESTED_SPREAD: 'Nested spread syntax is not allowed in style object.', 8 | ONLY_LOGICAL_AND: 'Only logical and operator is allowed in spread element.', 9 | INVALID_SPREAD_SIDE: 'Only object expression is allowed on the right side of the spread element.', 10 | INVALID_ATTRS_KIND: "Only object expression is allowed for jsx attribute 'stylex'", 11 | INLINE_ONLY_ONE_ARGUMENT: 'function inline() only accept one argument.', 12 | GLOBAL_STYLE_ONLY_ONE_ARGUMENT: 'function injectGlobalStyle() only accept one argument.', 13 | IMPORT_EXTEND_PKG_ERROR: "'@stylex-extend/core' only support named import.", 14 | INVALID_FILE: 'Invalid file path', 15 | ONLY_TOP_LEVEL_INJECT_GLOBAL_STYLE: 'function injectGlobalStyle() must be called at the top level of the module.', 16 | INVALID_CSS_TOKEN: 'Invalid css token.', 17 | INVALID_INLINE_ARGUMENT: 'Invalid inline argument.', 18 | INVALID_ID_ARGUMENT: 'Invalid id argument.', 19 | ONLY_TOP_LEVEL_ID: 'function id() must be called at the top level of the module.' 20 | } 21 | -------------------------------------------------------------------------------- /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/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/babel-plugin/src/interface.ts: -------------------------------------------------------------------------------- 1 | export type Transport = 'props' | 'attrs' | (string & {}) 2 | 3 | export interface StylexBindingMeta { 4 | helper: 'props' | 'attrs' | (string & {}) 5 | } 6 | 7 | export interface ModuleResolutionCommonJS { 8 | type: 'commonJS' | 'haste' 9 | rootDir?: string 10 | themeFileExtension?: null | undefined | string 11 | } 12 | 13 | export interface MOduleResolutionHaste { 14 | type: 'haste' 15 | themeFileExtension?: null | undefined | string 16 | } 17 | 18 | export interface ModuleResolutionExperimentalCrossFileParsing { 19 | type: 'experimental_crossFileParsing' 20 | rootDir?: string 21 | themeFileExtension?: null | undefined | string 22 | } 23 | 24 | export type ModuleResolution = ModuleResolutionCommonJS | MOduleResolutionHaste | ModuleResolutionExperimentalCrossFileParsing 25 | 26 | export interface StylexExtendBabelPluginOptions { 27 | transport?: Transport 28 | /** 29 | * @see {@link https://stylexjs.com/docs/api/configuration/babel-plugin/#aliases} 30 | */ 31 | aliases?: Record 32 | /** 33 | * @default 'x' 34 | * @see {@link https://stylexjs.com/docs/api/configuration/babel-plugin/#classnameprefix} 35 | */ 36 | classNamePrefix?: string 37 | /** 38 | * @see {@link https://stylexjs.com/docs/api/configuration/babel-plugin/#unstable_moduleresolution} 39 | */ 40 | unstable_moduleResolution?: ModuleResolution 41 | } 42 | 43 | export type CSSObjectValue = { 44 | [key: string]: CSSObjectValue | number | string | undefined | null | boolean 45 | } 46 | -------------------------------------------------------------------------------- /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/visitor/global-style.ts: -------------------------------------------------------------------------------- 1 | import { types } from '@babel/core' 2 | import type { NodePath } from '@babel/core' 3 | import { compile, serialize, stringify } from 'stylis' 4 | import { evaluateCSS, printCssAST } from '../ast/evaluate-path' 5 | import { MESSAGES } from '../ast/message' 6 | import { findNearestStatementAncestor, isObjectExpression, isTopLevelCalled } from '../ast/shared' 7 | import { Module } from '../module' 8 | import { getExtendMacro } from './inline' 9 | 10 | function validateInjectGlobalStyleMacro( 11 | path: NodePath[], 12 | path2: NodePath 13 | ) { 14 | if (!isTopLevelCalled(path2)) { throw new Error(MESSAGES.ONLY_TOP_LEVEL_INJECT_GLOBAL_STYLE) } 15 | if (path.length > 1) { throw new Error(MESSAGES.GLOBAL_STYLE_ONLY_ONE_ARGUMENT) } 16 | if (isObjectExpression(path[0])) { 17 | return path[0] 18 | } 19 | throw new Error(MESSAGES.INVALID_CSS_AST_KIND) 20 | } 21 | 22 | export function transformInjectGlobalStyle(path: NodePath, mod: Module) { 23 | const callee = getExtendMacro(path, mod, 'injectGlobalStyle') 24 | if (callee) { 25 | const expr = validateInjectGlobalStyleMacro(callee.get('arguments'), findNearestStatementAncestor(path)) 26 | const { css } = printCssAST(evaluateCSS(expr, mod), mod) 27 | path.replaceWith(types.stringLiteral('')) 28 | const result = serialize(compile(css), stringify) 29 | mod.addStyle(result) 30 | return result 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/babel-plugin/src/visitor/id.ts: -------------------------------------------------------------------------------- 1 | import { types } from '@babel/core' 2 | import type { NodePath } from '@babel/core' 3 | import { xxhash } from '@stylex-extend/shared' 4 | import { MESSAGES } from '../ast/message' 5 | import { findNearestStatementAncestor, isBooleanLiteral, isTopLevelCalled, make } from '../ast/shared' 6 | import { Module, getCanonicalFilePath } from '../module' 7 | import { getExtendMacro } from './inline' 8 | 9 | function validateIdMacro( 10 | path: NodePath[], 11 | path2: NodePath 12 | ) { 13 | if (!path.length) { return '' } 14 | if (!isTopLevelCalled(path2)) { throw new Error(MESSAGES.ONLY_TOP_LEVEL_ID) } 15 | if (isBooleanLiteral(path[0])) { 16 | return path[0].node.value 17 | } 18 | throw new Error(MESSAGES.INVALID_ID_ARGUMENT) 19 | } 20 | 21 | function generateIdExpression(id: string) { 22 | const value = types.stringLiteral(`var(--${id})`) 23 | const specialID = types.stringLiteral('stylex-extend') 24 | return types.objectExpression([ 25 | types.objectProperty(make.identifier('$id'), specialID), 26 | types.objectProperty(make.identifier('value'), value) 27 | ]) 28 | } 29 | 30 | // https://github.com/facebook/stylex/discussions/684 31 | // the generate id should be a css variable. 32 | // const myId = id() => { $id: 'stylex-extend', value: xxhash } 33 | export function transformId(path: NodePath, mod: Module) { 34 | const callee = getExtendMacro(path, mod, 'id') 35 | if (callee) { 36 | const isVariant = validateIdMacro(callee.get('arguments'), findNearestStatementAncestor(callee)) 37 | const id = xxhash(getCanonicalFilePath(mod.filename, mod.cwd)) 38 | if (isVariant) { 39 | path.replaceWith(generateIdExpression(id)) 40 | return 41 | } 42 | path.replaceWith(types.stringLiteral(`var(--${id})`)) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /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/babel-plugin/src/visitor/index.ts: -------------------------------------------------------------------------------- 1 | export { transformInjectGlobalStyle } from './global-style' 2 | export { transformId } from './id' 3 | export { transformInline } from './inline' 4 | export { transformStylexAttrs } from './jsx-attribute' 5 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /packages/babel-plugin/src/visitor/jsx-attribute.ts: -------------------------------------------------------------------------------- 1 | import { NodePath, types } from '@babel/core' 2 | import { evaluateCSS, printJsAST } from '../ast/evaluate-path' 3 | import { MESSAGES } from '../ast/message' 4 | import { callExpression, findNearestParentWithCondition, findNearestTopLevelAncestor, isObjectExpression, make } from '../ast/shared' 5 | import { Module } from '../module' 6 | import { insertRelativePackage } from './imports' 7 | 8 | const X = 'stylex' 9 | 10 | function validateJSXAtrributes( 11 | path: NodePath, 12 | path2: NodePath 13 | ) { 14 | if (!isObjectExpression(path2)) { throw new Error(MESSAGES.INVALID_ATTRS_KIND) } 15 | const nearestOpeningElement = findNearestParentWithCondition(path, (p) => p.isJSXOpeningElement()) 16 | if (!nearestOpeningElement) { throw new Error(MESSAGES.INVALID_JSX_ELEMENT) } 17 | const attrs = nearestOpeningElement.get('attributes').filter((p) => p.isJSXAttribute() && p.node.name.name === X) 18 | if (attrs.length > 1) { throw new Error(MESSAGES.DUPLICATE_STYLEX_ATTR) } 19 | return path2 20 | } 21 | 22 | export function transformStylexAttrs(path: NodePath, mod: Module) { 23 | const { node } = path 24 | const value = path.get('value') 25 | if (node.name.name === X && value.isJSXExpressionContainer()) { 26 | const expr = validateJSXAtrributes(path, value.get('expression')) 27 | const { properties, expressions, into } = printJsAST(evaluateCSS(expr, mod), expr) 28 | const [create, applied] = insertRelativePackage(mod.program, mod) 29 | const declaration = make.variableDeclaration(into, callExpression(create.node, [make.objectExpression(properties)])) 30 | const nearest = findNearestTopLevelAncestor(path) 31 | nearest.insertBefore(declaration) 32 | path.replaceWith(types.jsxSpreadAttribute(callExpression(applied.node, expressions))) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/babel-plugin/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "paths": { 6 | "@/*": ["src/*"] 7 | }, 8 | "jsx": "react-jsx", 9 | "types": ["node", "vitest/globals"] 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/core/README.md: -------------------------------------------------------------------------------- 1 | # @stylex-extend/core 2 | 3 | ## Quick Start 4 | 5 | ### Install 6 | 7 | ```bash 8 | yarn add @stylex-extend/core 9 | ``` 10 | 11 | ### Usage 12 | 13 | ```js 14 | import { injectGlobalStyle } from '@styex-extend/css' 15 | 16 | const styles = css({ 17 | p: { 18 | color: 'red' 19 | } 20 | }) 21 | ``` 22 | -------------------------------------------------------------------------------- /packages/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@stylex-extend/core", 3 | "main": "dist/index.js", 4 | "types": "dist/index.d.ts", 5 | "module": "dist/index.mjs", 6 | "version": "0.7.1", 7 | "license": "MIT", 8 | "repository": "https://github.com/nonzzz/stylex-extend.git", 9 | "scripts": { 10 | "build": "rollup --config rollup.config.mts --configPlugin swc3" 11 | }, 12 | "exports": { 13 | ".": { 14 | "types": "./dist/index.d.ts", 15 | "import": "./dist/index.mjs", 16 | "require": "./dist/index.js" 17 | }, 18 | "./package.json": "./package.json" 19 | }, 20 | "dependencies": { 21 | "@stylex-extend/shared": "workspace:*" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/core/rollup.config.mts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unsafe-member-access */ 2 | /* eslint-disable @typescript-eslint/no-unsafe-argument */ 3 | import { builtinModules, createRequire } from 'module' 4 | import { defineConfig } from 'rollup' 5 | import { dts } from 'rollup-plugin-dts' 6 | import { swc } from 'rollup-plugin-swc3' 7 | 8 | const _require = createRequire(import.meta.url) 9 | 10 | const external = [ 11 | ...builtinModules, 12 | ...Object.keys(_require('./package.json').dependencies) 13 | ] 14 | 15 | export default defineConfig( 16 | [ 17 | { 18 | input: 'src/index.ts', 19 | output: [ 20 | { file: 'dist/index.js', format: 'cjs' }, 21 | { file: 'dist/index.mjs', format: 'es' } 22 | ], 23 | plugins: [swc()], 24 | external 25 | }, 26 | { 27 | input: 'src/index.ts', 28 | output: { file: 'dist/index.d.ts', format: 'es' }, 29 | plugins: [dts({ compilerOptions: { preserveSymlinks: false } })], 30 | external 31 | } 32 | ] 33 | ) 34 | -------------------------------------------------------------------------------- /packages/core/src/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | import type { CSSObject, StylexCSS } from '@stylex-extend/shared' 3 | import type { StyleXArray, CompiledStyles, InlineStyles } from "@stylexjs/stylex/lib/StyleXTypes"; 4 | 5 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 6 | export type ReadonlyParmater) => any> = T extends (...args: ReadonlyArray) => any ? P 7 | : never 8 | 9 | export type StylexAttrsParamter = StyleXArray< 10 | | (null | undefined | CompiledStyles) 11 | | boolean 12 | | Readonly<[CompiledStyles, InlineStyles]> 13 | > 14 | 15 | export function injectGlobalStyle(..._: Array>): string { 16 | throw new Error("'injectGlobalStyle' calls should be compiled away.") 17 | } 18 | 19 | export function inline(_: CSSObject): StylexAttrsParamter { 20 | throw new Error("'inline' calls should be compiled away.") 21 | } 22 | 23 | export function id(_?: boolean): string { 24 | throw new Error('`id` calls should be compiled away.') 25 | } 26 | 27 | function createWhenAPI(errorMessage: string) { 28 | return (selector: string, pseudo?: string): string => { 29 | throw new Error(errorMessage) 30 | } 31 | } 32 | 33 | /** 34 | * @description This has not yet been implemented. I think `id` can handle most scenarios. 35 | */ 36 | export const when = { 37 | // a b 38 | ancestor: createWhenAPI("'when.ancestor' calls should be compiled away."), 39 | // a:has(b) 40 | descendent: createWhenAPI("'when.descendent' calls should be compiled away."), 41 | // a + b 42 | sibling: createWhenAPI("'when.sibling' calls should be compiled away.") 43 | } 44 | -------------------------------------------------------------------------------- /packages/postcss/README.md: -------------------------------------------------------------------------------- 1 | # @stylex-extend/postcss 2 | 3 | ## Quick Start 4 | 5 | ### Install 6 | 7 | ```bash 8 | yarn add @stylex-extend/postcss autoprefixer -D 9 | ``` 10 | 11 | ### Usage 12 | 13 | ```js 14 | // .babelrc.js 15 | 16 | module.exports = { 17 | plugins: ['@stylex-extend/babel-plugin', '@stylexjs/babel-plugin'] 18 | } 19 | ``` 20 | 21 | Add the following to your postcss.config.js 22 | 23 | ```js 24 | module.exports = { 25 | plugins: { 26 | '@stylex-extend/postcss': { 27 | include: ['src/**/*.{js,jsx,ts,tsx}'] 28 | }, 29 | autoprefixer: {} 30 | } 31 | } 32 | ``` 33 | -------------------------------------------------------------------------------- /packages/postcss/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@stylex-extend/postcss", 3 | "version": "0.7.1", 4 | "main": "src/index.js", 5 | "license": "MIT", 6 | "dependencies": { 7 | "@stylexjs/babel-plugin": "0.11.1", 8 | "@stylex-extend/babel-plugin": "workspace:*", 9 | "@babel/core": "^7.25.2", 10 | "postcss": "^8.4.49", 11 | "fast-glob": "^3.3.2", 12 | "glob-parent": "^6.0.2", 13 | "is-glob": "^4.0.3" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/react/README.md: -------------------------------------------------------------------------------- 1 | # @stylex-extend/react 2 | -------------------------------------------------------------------------------- /packages/react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@stylex-extend/react", 3 | "main": "./src/index.js", 4 | "module": "./src/index.js", 5 | "types": "./src/index.d.ts", 6 | "version": "0.7.1", 7 | "repository": "https://github.com/nonzzz/stylex-extend.git", 8 | "license": "MIT", 9 | "devDependencies": { 10 | "@types/react": "^18.2.69" 11 | }, 12 | "dependencies": { 13 | "@stylex-extend/shared": "workspace:*" 14 | }, 15 | "exports": { 16 | ".": "./src/index.js", 17 | "./jsx-runtime": { 18 | "import": "./src/jsx-runtime.mjs", 19 | "require": "./src/jsx-runtime.js", 20 | "types": "./src/jsx-runtime.d.ts" 21 | }, 22 | "./jsx-dev-runtime": { 23 | "import": "./src/jsx-dev-runtime.mjs", 24 | "require": "./src/jsx-dev-runtime.js", 25 | "types": "./src/jsx-dev-runtime.d.ts" 26 | }, 27 | "./*": "./src/*", 28 | "./package.json": "./package.json" 29 | }, 30 | "typesVersions": { 31 | "*": { 32 | "*": [ 33 | "./src/*.d.ts" 34 | ] 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/react/src/index.d.ts: -------------------------------------------------------------------------------- 1 | import {} from 'react' 2 | import type { StylexProperty } from '@stylex-extend/shared' 3 | 4 | declare module 'react' { 5 | interface Attributes { 6 | stylex?: StylexProperty 7 | } 8 | } 9 | export {} 10 | -------------------------------------------------------------------------------- /packages/react/src/index.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nonzzz/stylex-extend/464f420bed4b3e1c311af0ca8b0cd18a99bc51fa/packages/react/src/index.js -------------------------------------------------------------------------------- /packages/react/src/jsx-dev-runtime.d.ts: -------------------------------------------------------------------------------- 1 | export { StyledJSX as JSX } from './jsx-namespace' 2 | -------------------------------------------------------------------------------- /packages/react/src/jsx-dev-runtime.js: -------------------------------------------------------------------------------- 1 | const { jsxDEV, Fragment } = require('react/jsx-dev-runtime') 2 | 3 | module.exports = { 4 | jsxDEV, 5 | Fragment 6 | } 7 | -------------------------------------------------------------------------------- /packages/react/src/jsx-dev-runtime.mjs: -------------------------------------------------------------------------------- 1 | export { Fragment, jsxDEV } from 'react/jsx-dev-runtime' 2 | -------------------------------------------------------------------------------- /packages/react/src/jsx-namespace.d.ts: -------------------------------------------------------------------------------- 1 | import 'react' 2 | import type { StylexProperty } from '@stylex-extend/shared' 3 | 4 | type ReactJSXLibraryManagedAttributes = JSX.LibraryManagedAttributes 5 | type ReactJSXIntrinsicElements = JSX.IntrinsicElements 6 | 7 | type ReactJSXElement = JSX.Element 8 | type ReactJSXElementClass = JSX.ElementClass 9 | type ReactJSXElementAttributesProperty = JSX.ElementAttributesProperty 10 | type ReactJSXElementChildrenAttribute = JSX.ElementChildrenAttribute 11 | type ReactJSXLibraryManagedAttributes = JSX.LibraryManagedAttributes 12 | type ReactJSXIntrinsicAttributes = JSX.IntrinsicAttributes 13 | type ReactJSXIntrinsicClassAttributes = JSX.IntrinsicClassAttributes 14 | type ReactJSXIntrinsicElements = JSX.IntrinsicElements 15 | type ReactJSXElementType = string | React.JSXElementConstructor 16 | 17 | export namespace StyledJSX { 18 | export type LibraryManagedAttributes = { stylex?: StylexProperty } & ReactJSXLibraryManagedAttributes 19 | 20 | export type IntrinsicElements = { 21 | [K in keyof ReactJSXIntrinsicElements]: ReactJSXIntrinsicElements[K] & { 22 | stylex?: StylexProperty 23 | } 24 | } 25 | 26 | export type ElementType = ReactJSXElementType 27 | export interface Element extends ReactJSXElement {} 28 | export interface ElementClass extends ReactJSXElementClass {} 29 | export interface ElementAttributesProperty extends ReactJSXElementAttributesProperty {} 30 | export interface ElementChildrenAttribute extends ReactJSXElementChildrenAttribute {} 31 | 32 | export interface IntrinsicAttributes extends ReactJSXIntrinsicAttributes {} 33 | export interface IntrinsicClassAttributes extends ReactJSXIntrinsicClassAttributes {} 34 | } 35 | -------------------------------------------------------------------------------- /packages/react/src/jsx-runtime.d.ts: -------------------------------------------------------------------------------- 1 | export { StyledJSX as JSX } from './jsx-namespace' 2 | -------------------------------------------------------------------------------- /packages/react/src/jsx-runtime.js: -------------------------------------------------------------------------------- 1 | const { jsx, Fragment, jsxs } = require('react/jsx-runtime') 2 | 3 | module.exports = { 4 | jsx, 5 | Fragment, 6 | jsxs 7 | } 8 | -------------------------------------------------------------------------------- /packages/react/src/jsx-runtime.mjs: -------------------------------------------------------------------------------- 1 | export { Fragment, jsx, jsxs } from 'react/jsx-runtime' 2 | -------------------------------------------------------------------------------- /packages/shared/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nonzzz/stylex-extend/464f420bed4b3e1c311af0ca8b0cd18a99bc51fa/packages/shared/README.md -------------------------------------------------------------------------------- /packages/shared/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@stylex-extend/shared", 3 | "description": "Shared utilities for StyleX Extend", 4 | "version": "0.7.1", 5 | "license": "MIT", 6 | "main": "./dist/index.js", 7 | "module": "./dist/index.mjs", 8 | "types": "./dist/index.d.ts", 9 | "repository": "https://github.com/nonzzz/stylex-extend.git", 10 | "files": [ 11 | "dist", 12 | "README.md" 13 | ], 14 | "scripts": { 15 | "dev": "rollup --config rollup.config.mts --configPlugin swc3 --watch", 16 | "build": "rollup --config rollup.config.mts --configPlugin swc3" 17 | }, 18 | "exports": { 19 | ".": { 20 | "types": "./dist/index.d.ts", 21 | "import": "./dist/index.mjs", 22 | "require": "./dist/index.js" 23 | }, 24 | "./package.json": "./package.json" 25 | }, 26 | "dependencies": { 27 | "csstype": "^3.1.3" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/shared/rollup.config.mts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unsafe-member-access */ 2 | /* eslint-disable @typescript-eslint/no-unsafe-argument */ 3 | import { builtinModules, createRequire } from 'module' 4 | import { defineConfig } from 'rollup' 5 | import { dts } from 'rollup-plugin-dts' 6 | import { swc } from 'rollup-plugin-swc3' 7 | 8 | // https://www.typescriptlang.org/tsconfig/#preserveSymlinks 9 | 10 | const _require = createRequire(import.meta.url) 11 | 12 | const external = [ 13 | ...builtinModules, 14 | ...Object.keys(_require('./package.json').dependencies) 15 | ] 16 | 17 | export default defineConfig( 18 | [ 19 | { 20 | input: 'src/index.ts', 21 | output: [ 22 | { file: 'dist/index.js', format: 'cjs' }, 23 | { file: 'dist/index.mjs', format: 'es' } 24 | ], 25 | plugins: [swc()], 26 | external 27 | }, 28 | { 29 | input: 'src/index.ts', 30 | output: { file: 'dist/index.d.ts', format: 'es' }, 31 | plugins: [dts({ compilerOptions: { preserveSymlinks: false } })], 32 | external 33 | } 34 | ] 35 | ) 36 | -------------------------------------------------------------------------------- /packages/shared/src/css-type.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import * as CSS from 'csstype' 3 | 4 | export type CSSProperties = CSS.PropertiesFallback 5 | 6 | type CSSPropertiesKey = (keyof CSSProperties) | CSS.AtRules | CSS.Pseudos | (string & {}) 7 | 8 | type SelectOrAtRules = CSS.AtRules | CSS.Pseudos | (string & {}) 9 | 10 | type CSSPropertiesMoreValue = { 11 | default?: T 12 | } & Partial> 13 | 14 | export type CSSPropertiesWithMultiValues = { 15 | [K in CSSPropertiesKey]?: K extends keyof CSSProperties ? (CSSProperties[K] | CSSPropertiesMoreValue) 16 | : CSSPropertiesWithMultiValues | (string & {}) | number 17 | } 18 | 19 | export type CSSObject = CSSPropertiesWithMultiValues 20 | 21 | export type StylexProperty = CSSObject | ((...args: any[]) => CSSObject) 22 | 23 | type CSSPropertiesAndSubKey = (keyof CSSProperties) | CSS.AtRules | `&${CSS.Pseudos}` | (string & {}) 24 | 25 | export type StylexCSSObject = { 26 | [K in CSSPropertiesAndSubKey]?: K extends keyof CSSProperties ? CSSProperties[K] 27 | : StylexCSSObject | (string & {}) | ((...args: any[]) => StylexCSSObject) | number 28 | } 29 | 30 | export type StylexCSS = StylexCSSObject | ((...args: any[]) => StylexCSSObject) 31 | -------------------------------------------------------------------------------- /packages/shared/src/hash.ts: -------------------------------------------------------------------------------- 1 | export function xxhash(str: string) { 2 | let i 3 | let l 4 | let hval = 0x811C9DC5 5 | 6 | for (i = 0, l = str.length; i < l; i++) { 7 | hval ^= str.charCodeAt(i) 8 | hval += (hval << 1) + (hval << 4) + (hval << 7) + (hval << 8) + (hval << 24) 9 | } 10 | return (`00000${(hval >>> 0).toString(36)}`).slice(-6) 11 | } 12 | 13 | const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' 14 | 15 | function seededRandom(seed: number): () => number { 16 | return function() { 17 | const x = Math.sin(seed++) * 10000 18 | return x - Math.floor(x) 19 | } 20 | } 21 | 22 | export function randomAlpha() { 23 | let r = '' 24 | const seed = Math.floor(Math.random() * 1000000) 25 | const random = seededRandom(seed) 26 | for (let i = 0; i < 10; i++) { 27 | const index = Math.floor(random() * chars.length) 28 | r += chars[index] 29 | } 30 | return r 31 | } 32 | -------------------------------------------------------------------------------- /packages/shared/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './css-type' 2 | export * from './hash' 3 | -------------------------------------------------------------------------------- /packages/shared/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /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/vite/client.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'virtual:stylex.css' { 2 | export {} 3 | } 4 | -------------------------------------------------------------------------------- /packages/vite/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@stylex-extend/vite", 3 | "version": "0.7.1", 4 | "main": "./dist/index.js", 5 | "module": "./dist/index.mjs", 6 | "files": [ 7 | "dist", 8 | "client.d.ts", 9 | "README.md" 10 | ], 11 | "scripts": { 12 | "dev": "rollup --config rollup.config.mts --configPlugin swc3 --watch", 13 | "build": "rollup --config rollup.config.mts --configPlugin swc3" 14 | }, 15 | "exports": { 16 | ".": { 17 | "types": "./dist/index.d.ts", 18 | "import": "./dist/index.mjs", 19 | "require": "./dist/index.js" 20 | }, 21 | "./package.json": "./package.json", 22 | "./client": "./client.d.ts", 23 | "./postcss-ver": { 24 | "import": "./dist/postcss-ver.mjs", 25 | "require": "./dist/postcss-ver.js", 26 | "types": "./dist/postcss-ver.d.ts" 27 | } 28 | }, 29 | "dependencies": { 30 | "@stylexjs/babel-plugin": "0.11.1", 31 | "@stylex-extend/babel-plugin": "workspace:*", 32 | "@rollup/pluginutils": "^5.1.0", 33 | "@babel/core": "^7.25.2", 34 | "@stylex-extend/shared": "workspace:*", 35 | "postcss-load-config": "^6.0.1", 36 | "@stylexjs/postcss-plugin": "0.11.1" 37 | }, 38 | "devDependencies": { 39 | "@types/babel__core": "^7.20.5" 40 | }, 41 | "repository": "https://github.com/nonzzz/stylex-extend.git", 42 | "keywords": [ 43 | "stylex", 44 | "experimental", 45 | "css-in-js", 46 | "vite-plugin" 47 | ] 48 | } 49 | -------------------------------------------------------------------------------- /packages/vite/rollup.config.mts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unsafe-member-access */ 2 | /* eslint-disable @typescript-eslint/no-unsafe-argument */ 3 | import shim from '@rollup/plugin-esm-shim' 4 | import { builtinModules, createRequire } from 'module' 5 | import { defineConfig } from 'rollup' 6 | import { dts } from 'rollup-plugin-dts' 7 | import { swc } from 'rollup-plugin-swc3' 8 | 9 | // https://www.typescriptlang.org/tsconfig/#preserveSymlinks 10 | 11 | const _require = createRequire(import.meta.url) 12 | 13 | const external = [ 14 | ...builtinModules, 15 | ...Object.keys(_require('./package.json').dependencies) 16 | ] 17 | 18 | export default defineConfig( 19 | [ 20 | { 21 | input: ['src/index.ts', 'src/postcss-ver.ts'], 22 | output: [ 23 | { 24 | dir: 'dist', 25 | format: 'cjs', 26 | entryFileNames: '[name].js' 27 | }, 28 | { 29 | dir: 'dist', 30 | format: 'es', 31 | entryFileNames: '[name].mjs', 32 | chunkFileNames: '[name]-[hash].mjs' 33 | } 34 | ], 35 | plugins: [swc(), shim()], 36 | external 37 | }, 38 | { 39 | input: 'src/index.ts', 40 | output: { file: 'dist/index.d.ts', format: 'es' }, 41 | plugins: [dts({ compilerOptions: { preserveSymlinks: false } })], 42 | external 43 | }, 44 | { 45 | input: 'src/postcss-ver.ts', 46 | output: { file: 'dist/postcss-ver.d.ts', format: 'es' }, 47 | plugins: [dts({ compilerOptions: { preserveSymlinks: false } })], 48 | external 49 | } 50 | ] 51 | ) 52 | -------------------------------------------------------------------------------- /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/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/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/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "paths": { 6 | "@/*": ["src/*"] 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/vue/README.md: -------------------------------------------------------------------------------- 1 | # @stylex-extend/vue 2 | -------------------------------------------------------------------------------- /packages/vue/index.d.ts: -------------------------------------------------------------------------------- 1 | import {} from 'vue' 2 | import { StylexProperty } from '@stylex-extend/shared' 3 | 4 | declare module 'vue' { 5 | interface AriaAttributes { 6 | stylex?: StylexProperty 7 | } 8 | interface ComponentCustomProps { 9 | stylex?: StylexProperty 10 | } 11 | } 12 | 13 | export {} 14 | -------------------------------------------------------------------------------- /packages/vue/index.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nonzzz/stylex-extend/464f420bed4b3e1c311af0ca8b0cd18a99bc51fa/packages/vue/index.js -------------------------------------------------------------------------------- /packages/vue/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@stylex-extend/vue", 3 | "main": "./index.js", 4 | "module": "./index.js", 5 | "types": "./index.d.ts", 6 | "version": "0.7.1", 7 | "license": "MIT", 8 | "repository": "https://github.com/nonzzz/stylex-extend.git", 9 | "devDependencies": { 10 | "vue": "^3.4.21" 11 | }, 12 | "dependencies": { 13 | "@stylex-extend/shared": "workspace:*" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - "packages/*" 3 | - "examples/*" 4 | - "docs" 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "outDir": "dist", 5 | "noImplicitThis": true, 6 | "esModuleInterop": true, 7 | "skipLibCheck": true, 8 | "resolveJsonModule": true, 9 | "target": "ESNext", 10 | "module": "ESNext", 11 | "lib": ["ESNext"], 12 | "moduleResolution": "Bundler", 13 | "jsx": "react-jsx", 14 | "strict": true, 15 | "types": ["vitest/globals"] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config' 2 | 3 | export default defineConfig({ 4 | test: { 5 | include: ['**/__tests__/**/*.{ts,tsx}'], 6 | exclude: ['**/__tests__/**/fixtures/**'], 7 | globals: true, 8 | watch: false 9 | } 10 | }) 11 | --------------------------------------------------------------------------------