├── .github └── workflows │ ├── release.yaml │ └── test.yaml ├── .gitignore ├── .node-version ├── .vscode └── settings.json ├── CHANGELOG.md ├── LICENSE ├── Q&A.md ├── README.md ├── __tests__ ├── __snapshots__ │ └── plugin.spec.ts.snap ├── fixtures │ ├── empty │ │ └── main.ts │ ├── lightning-css │ │ ├── main.ts │ │ └── style.css │ ├── normal │ │ ├── blue.ts │ │ ├── main.ts │ │ ├── output.css │ │ └── red.ts │ ├── path-alias │ │ ├── main.ts │ │ ├── tokens │ │ │ └── color.stylex.ts │ │ └── tsconfig.json │ └── pxtorem │ │ └── main.ts └── plugin.spec.ts ├── client.d.ts ├── dprint.json ├── e2e ├── e2e.spec.ts ├── fixtures │ ├── qwik │ │ ├── package.json │ │ ├── src │ │ │ ├── entry.ssr.tsx │ │ │ ├── root.tsx │ │ │ └── routes │ │ │ │ └── index.tsx │ │ ├── tsconfig.json │ │ └── vite.config.mts │ ├── remix │ │ ├── app │ │ │ ├── root.tsx │ │ │ └── routes │ │ │ │ └── _index.tsx │ │ ├── package.json │ │ ├── tsconfig.json │ │ └── vite.config.mts │ ├── vite-react-spa │ │ ├── index.html │ │ ├── package.json │ │ ├── src │ │ │ └── main.tsx │ │ ├── tsconfig.json │ │ └── vite.config.mts │ ├── vite-vue-spa │ │ ├── index.html │ │ ├── package.json │ │ ├── src │ │ │ ├── app.vue │ │ │ └── main.ts │ │ └── vite.config.mts │ └── waku │ │ ├── package.json │ │ ├── src │ │ ├── components │ │ │ └── app.tsx │ │ ├── enteries.tsx │ │ └── main.tsx │ │ ├── tsconfig.json │ │ └── vite.config.mts ├── helper.ts └── package.json ├── eslint.config.js ├── examples ├── astro-demo │ ├── .vscode │ │ ├── extensions.json │ │ └── launch.json │ ├── README.md │ ├── astro.config.mjs │ ├── package.json │ ├── public │ │ └── favicon.svg │ ├── src │ │ ├── components │ │ │ ├── Card.astro │ │ │ └── style.ts │ │ ├── env.d.ts │ │ ├── layouts │ │ │ └── Layout.astro │ │ └── pages │ │ │ └── index.astro │ └── tsconfig.json ├── civet-demo │ ├── index.html │ ├── package.json │ ├── src │ │ └── main.civet │ ├── tsconfig.json │ └── vite.config.ts ├── nuxt-demo │ ├── app.vue │ ├── components │ │ ├── btn1.vue │ │ └── btn2.vue │ ├── nuxt.config.ts │ ├── package.json │ └── tsconfig.json ├── qwik-demo │ ├── .vscode │ │ ├── extensions.json │ │ ├── launch.json │ │ ├── qwik-city.code-snippets │ │ ├── qwik.code-snippets │ │ └── settings.json │ ├── README.md │ ├── package.json │ ├── public │ │ ├── favicon.svg │ │ ├── fonts │ │ │ ├── poppins-400.woff2 │ │ │ ├── poppins-500.woff2 │ │ │ └── poppins-700.woff2 │ │ ├── manifest.json │ │ └── robots.txt │ ├── src │ │ ├── components │ │ │ ├── router-head │ │ │ │ └── router-head.tsx │ │ │ └── starter │ │ │ │ ├── counter │ │ │ │ ├── counter.module.css │ │ │ │ └── counter.tsx │ │ │ │ ├── footer │ │ │ │ ├── footer.module.css │ │ │ │ └── footer.tsx │ │ │ │ ├── gauge │ │ │ │ ├── gauge.module.css │ │ │ │ └── index.tsx │ │ │ │ ├── header │ │ │ │ ├── header.module.css │ │ │ │ └── header.tsx │ │ │ │ ├── hero │ │ │ │ └── hero.tsx │ │ │ │ ├── icons │ │ │ │ └── qwik.tsx │ │ │ │ ├── infobox │ │ │ │ ├── infobox.module.css │ │ │ │ └── infobox.tsx │ │ │ │ └── next-steps │ │ │ │ ├── next-steps.module.css │ │ │ │ └── next-steps.tsx │ │ ├── entry.dev.tsx │ │ ├── entry.preview.tsx │ │ ├── entry.ssr.tsx │ │ ├── global.css │ │ ├── media │ │ │ └── thunder.png │ │ ├── root.tsx │ │ └── routes │ │ │ ├── demo │ │ │ ├── flower │ │ │ │ ├── flower.css │ │ │ │ └── index.tsx │ │ │ └── todolist │ │ │ │ ├── index.tsx │ │ │ │ └── todolist.module.css │ │ │ ├── index.tsx │ │ │ ├── layout.tsx │ │ │ ├── service-worker.ts │ │ │ └── styles.css │ ├── tsconfig.json │ └── vite.config.ts ├── remix-demo │ ├── app │ │ ├── Card.tsx │ │ ├── root.tsx │ │ ├── routes │ │ │ └── _index.tsx │ │ ├── styles │ │ │ ├── color.stylex.ts │ │ │ ├── fonts.css │ │ │ ├── index.css │ │ │ └── reset.css │ │ └── theme.stylex.ts │ ├── package.json │ ├── public │ │ └── fonts │ │ │ └── Open-Sans-Latin.woff2 │ ├── tsconfig.json │ └── vite.config.ts ├── rollup-watch │ ├── package.json │ ├── rollup.config.mjs │ └── src │ │ ├── index.ts │ │ ├── text.ts │ │ └── var.stylex.ts ├── sveltekit-demo │ ├── package.json │ ├── src │ │ ├── app.html │ │ ├── routes │ │ │ └── +page.svelte │ │ └── styles │ │ │ ├── globalTokens.stylex.ts │ │ │ └── utils.ts │ ├── svelte.config.js │ ├── tsconfig.json │ └── vite.config.js ├── vite-demo │ ├── index.html │ ├── package.json │ ├── src │ │ ├── app.vue │ │ ├── card.vue │ │ ├── global.d.ts │ │ ├── main.ts │ │ └── stylex.ts │ └── vite.config.ts ├── vite-react-demo │ ├── index.html │ ├── package.json │ ├── src │ │ ├── app.tsx │ │ ├── button.tsx │ │ ├── main.tsx │ │ ├── stylex.css │ │ ├── tokens.stylex.ts │ │ └── tokens │ │ │ └── nested.stylex.ts │ ├── themes │ │ └── other.stylex.ts │ ├── tsconfig.json │ └── vite.config.ts ├── waku-demo │ ├── package.json │ ├── src │ │ ├── components │ │ │ ├── app.tsx │ │ │ ├── client-banner.tsx │ │ │ ├── init.css │ │ │ ├── server-banner.tsx │ │ │ └── server-button.tsx │ │ ├── entries.tsx │ │ └── main.tsx │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts └── watch │ ├── package.json │ ├── src │ ├── index.ts │ ├── stylex.css │ ├── text.ts │ └── var.stylex.ts │ └── vite.config.ts ├── package.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── rollup.config.ts ├── src ├── adapter │ ├── index.ts │ └── waku.ts ├── context.ts ├── index.ts ├── interface.ts ├── plugins │ ├── build.ts │ ├── index.ts │ └── server.ts ├── shared.ts └── transformer.ts ├── tsconfig.json ├── vitest.config.mts └── yarn.lock /.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/test.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 berry 11 | run: corepack enable 12 | - uses: actions/setup-node@v3 13 | with: 14 | node-version: 22 15 | - name: Install Dependices 16 | run: pnpm install 17 | 18 | - name: Run Test 19 | run: pnpm run test 20 | 21 | - name: Report Coverage 22 | uses: codecov/codecov-action@v2 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist 3 | temp 4 | types 5 | .tmpl 6 | coverage 7 | .DS_Store 8 | .svelte-kit 9 | 10 | yarn-debug.log* 11 | yarn-error.log* 12 | 13 | .yarn/cache 14 | .yarn/install-state.gz 15 | 16 | build 17 | 18 | *.mts.timestamp-* 19 | 20 | .nuxt 21 | .output -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | v22.3.0 -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.useFlatConfig": true, 3 | "editor.defaultFormatter": "dprint.dprint", 4 | "editor.codeActionsOnSave": { 5 | "source.fixAll": "explicit" 6 | }, 7 | "editor.formatOnSave": true, 8 | "[json]": { 9 | "editor.defaultFormatter": "dprint.dprint" 10 | }, 11 | "[markdown]": { 12 | "editor.defaultFormatter": "dprint.dprint" 13 | }, 14 | "[yaml]": { 15 | "editor.defaultFormatter": "esbenp.prettier-vscode" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.7.5 2 | 3 | # Patches 4 | 5 | - Fix exports error. 6 | - Fix cleanup of styleRules in library watch mode. 7 | 8 | ## 0.7.4 9 | 10 | # Patches 11 | 12 | - Fix build watch mode can't work. 13 | 14 | ## 0.7.3 15 | 16 | # Patches 17 | 18 | - Repsect `manuallyControlCssOrder` option for hmr. 19 | - Fix hmr can't work with some case. 20 | 21 | 22 | ## 0.7.1 23 | 24 | # Patches 25 | 26 | - Fix build mode resolveId don't allowed manually. 27 | 28 | ## 0.7.0 29 | 30 | # Improve 31 | 32 | This version uses vite's internal css plugin to generate css. 33 | 34 | ## 0.6.4 35 | 36 | # Patches 37 | 38 | - Fix windows system can't match virtual file id. 39 | - Fix hijack vite's css plugin missing plugin context. 40 | 41 | ## 0.6.3 42 | 43 | # Patches 44 | 45 | - Respect `enableStylexExtend` option. 46 | 47 | ## 0.6.2 48 | 49 | # Patches 50 | 51 | - Fix side effect of bunding phase can't handle import with raw suffix. 52 | 53 | ## 0.6.1 54 | 55 | # Patches 56 | 57 | - Fix missing sync byte offset. 58 | 59 | ## 0.6.0 60 | 61 | # Features & Improves 62 | 63 | - Add new option `enableStylexExtend`. 64 | - Split project layout. 65 | - Provide rollup adapter. 66 | 67 | ## 0.5.2 68 | 69 | # Improve 70 | 71 | - Remove unnecessary babel plugins. 72 | 73 | # Patches 74 | 75 | - Fix the id of `manuallyControlCssOrder` should be unix like. 76 | - Respect `isProduction`. 77 | 78 | # Chore 79 | 80 | - Remove `es-mdoule-lexer`. 81 | - Upgrade `@stylexjs/babel-plugin` version. 82 | 83 | ## 0.5.1 84 | 85 | # Patches 86 | 87 | - Fix option `manuallyControlCssOrder` typo. 88 | 89 | ## 0.5.0 90 | 91 | # Features 92 | 93 | - Add `manuallyControlCssOrder` option 94 | 95 | # Patches 96 | 97 | - Fix development mode can't pass css with vite internal handle. 98 | 99 | ## 0.4.1 100 | 101 | # Patches 102 | 103 | - Fix no passing option `importSources` for stylex babel-plugin. 104 | 105 | ## 0.4.0 106 | 107 | # Features 108 | 109 | - Add new Option `optimizedDeps` #12 110 | 111 | ## 0.3.0 112 | 113 | # Improve and Features 114 | 115 | - Upgrade stylex compiler. 116 | - Perf patchAlias logic. 117 | - Reduce package sizes. 118 | 119 | # Patches 120 | 121 | - Fix can't load `virtual:css` in watch mode. #5 122 | 123 | ## 0.2.5 124 | 125 | # Patches 126 | 127 | - Fix transform filter don't work as expected. #1 128 | 129 | ## 0.2.4 130 | 131 | # Patches 132 | 133 | - Fix patchAlias can't work with windows system. 134 | - Fix input code include empty stylex object can't work. #10 135 | 136 | ## 0.2.2 137 | 138 | # Patches 139 | 140 | - Fix if it's `https` or `http`. 141 | 142 | ## 0.2.1 143 | 144 | # Patches 145 | 146 | - Remove `rs-module-lexer` using `es-module-lexer`. 147 | 148 | ## 0.2.0 149 | 150 | # Features & Improves 151 | 152 | - Support paths alias. #6 153 | - Rename `stylexImports` to `importSources` 154 | 155 | # Patches 156 | 157 | - Fix hmr error with nuxt. 158 | - Fix `@stylexjs/open-props` can't work. 159 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 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 | -------------------------------------------------------------------------------- /Q&A.md: -------------------------------------------------------------------------------- 1 | > Why i can't see the style work? 2 | 3 | - Normally you won't inject virtual module in your local code. But every framework has it own ideas so sometimes you can't see the style inject in your page,so you need inject them manully.Just import `import '@stylex-dev.css'` in your entry file. 4 | 5 | > What's optimizedDeps? 6 | 7 | - `optimizedDeps` is work for that which file or libraries contain stylex you need to exclude with vite. For details please see [#12](https://github.com/nonzzz/vite-plugin-stylex/issues/12). This option can help you set them up. 8 | 9 | > Should i need config for path aliase? 10 | 11 | - No, you no need to config path alias for this plugin. all alias will be automatically set. 12 | 13 | > What's manuallyControlCssOrder? 14 | 15 | - For example, css variables are defined in the entry css file but the order of stylex injection can't read them. So you can using this option to ensure the behavior. Full details see [remix-demo](./examples/remix-demo) 16 | 17 | > How can i work with RSC? 18 | 19 | - Like waku and etc. Currently like `waku`. you should declare `@stylex-dev.css` for server component. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![vite-plugin-stylex](https://socialify.git.ci/nonzzz/vite-plugin-stylex/image?description=1&font=Jost&language=1&logo=https%3A%2F%2Fvitejs.dev%2Flogo-with-shadow.png&name=1&owner=1&theme=Auto) 2 | 3 | > [!WARNING] 4 | > This is an unofficial repo. 5 | > This plugin is dedicated to providing stable stylex integration. 6 | 7 | No longer maintained Migrate to [stylex-extend](https://github.com/nonzzz/stylex-extend) 8 | 9 | ## Features 10 | 11 | - [x] CSS automatic injection 12 | - [x] Support HMR 13 | - [x] Control css order by manually 14 | - [x] Support Vite-based SSR framework 15 | 16 | ## Usage 17 | 18 | ```bash 19 | $ yarn add vite-plugin-stylex-dev -D 20 | ``` 21 | 22 | ```ts 23 | // vite.config.ts 24 | import { defineConfig } from 'vite' 25 | import { stylex } from 'vite-plugin-stylex-dev' 26 | 27 | export default defineConfig({ 28 | plugins: [stylex()] 29 | }) 30 | 31 | // then find your project entry(If you don't using manuallyControlCssOrder option) 32 | import 'virtual:stylex.css' 33 | ``` 34 | 35 | ## Options 36 | 37 | | params | type | default | description | 38 | | --------------------------- | --------------------------------------------- | ---------------------------------------------- | ---------------------------------------------------- | 39 | | `include` | `string \| RegExp \| Array` | `/\.(mjs\|js\|ts\|vue\|jsx\|tsx)(\?.*\|)$/` | Include all assets matching any of these conditions. | 40 | | `exclude` | `string \| RegExp \| Array` | `-` | Exclude all assets matching any of these conditions. | 41 | | `importSources` | `string[]` | `['stylex', '@stylexjs/stylex']` | See stylex document. | 42 | | `babelConfig` | `object` | `{}` | Babel config for stylex | 43 | | `unstable_moduleResolution` | `Record` | `{ type: 'commonJS', rootDir: process.cwd() }` | See stylex document | 44 | | `useCSSLayers` | `boolean` | `false` | See stylex document | 45 | | `optimizedDeps` | `Array` | `[]` | Work with external stylex files or libraries | 46 | | `manuallyControlCssOrder` | `boolean \|object` | `false` | control css order by manually | 47 | | `enableStylexExtend` | `boolean \| StylexExtendOptions` | `false` | see `@stylex-extend/babel-plugin` docss | 48 | 49 | ## Q & A 50 | 51 | [Q&A](./Q&A.md) 52 | 53 | ## Author 54 | 55 | Kanno 56 | 57 | ## LICENSE 58 | 59 | [MIT](./LICENSE) 60 | -------------------------------------------------------------------------------- /__tests__/__snapshots__/plugin.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`aliases 1`] = ` 4 | ":root{--x1rp20gp:#000}.x97kcnl:not(#\\#){color:var(--x1rp20gp)} 5 | " 6 | `; 7 | 8 | exports[`empty 1`] = ` 9 | "import "@stylexjs/stylex"; 10 | const { 11 | className: s 12 | } = {}; 13 | export { 14 | s as className 15 | }; 16 | " 17 | `; 18 | 19 | exports[`lightning css 1`] = ` 20 | ".x3ja3p5{color:#329b25}@media (max-width: 1024px){.x1yewo9u.x1yewo9u{color:purple}} 21 | " 22 | `; 23 | 24 | exports[`normal 1`] = ` 25 | ".xju2f9n{color:blue} 26 | .x1e2nbdu{color:red} 27 | " 28 | `; 29 | 30 | exports[`normal 2`] = ` 31 | "import * as s from "@stylexjs/stylex"; 32 | const e = { 33 | blue: { 34 | color: "xju2f9n", 35 | $$css: !0 36 | } 37 | }, o = { 38 | red: { 39 | color: "x1e2nbdu", 40 | $$css: !0 41 | } 42 | }, { 43 | className: r 44 | } = s.props(e.blue, o.red); 45 | export { 46 | r as className 47 | }; 48 | " 49 | `; 50 | 51 | exports[`normal 3`] = ` 52 | ".xju2f9n{color:#00f}.x1e2nbdu{color:red} 53 | " 54 | `; 55 | 56 | exports[`postcss 1`] = ` 57 | ".x1j61zf2{font-size:1rem} 58 | " 59 | `; 60 | 61 | exports[`ts config paths 1`] = ` 62 | ":root{--x1rp20gp:#000;} 63 | .x97kcnl:not(#\\#){color:var(--x1rp20gp)} 64 | " 65 | `; 66 | -------------------------------------------------------------------------------- /__tests__/fixtures/empty/main.ts: -------------------------------------------------------------------------------- 1 | import { create, props } from '@stylexjs/stylex' 2 | import 'virtual:stylex.css' 3 | 4 | const styles = create({ 5 | link: { 6 | padding: null 7 | } 8 | }) 9 | 10 | const { className } = props(styles.link) 11 | 12 | export { className } 13 | -------------------------------------------------------------------------------- /__tests__/fixtures/lightning-css/main.ts: -------------------------------------------------------------------------------- 1 | import { create, props } from '@stylexjs/stylex' 2 | import './style.css' 3 | 4 | const styles = create({ 5 | layout: { 6 | color: { 7 | default: 'lch(from green calc(l + 10%) c h)', 8 | '@media (--mx-1)': 'purple' 9 | } 10 | 11 | } 12 | }) 13 | 14 | export const cls = props(styles.layout) 15 | -------------------------------------------------------------------------------- /__tests__/fixtures/lightning-css/style.css: -------------------------------------------------------------------------------- 1 | @custom-media --mx-1 (width <= 1024px); 2 | 3 | @stylex-dev; -------------------------------------------------------------------------------- /__tests__/fixtures/normal/blue.ts: -------------------------------------------------------------------------------- 1 | import * as stylex from '@stylexjs/stylex' 2 | 3 | export const styles = stylex.create({ 4 | blue: { 5 | color: 'blue' 6 | } 7 | }) 8 | -------------------------------------------------------------------------------- /__tests__/fixtures/normal/main.ts: -------------------------------------------------------------------------------- 1 | import * as stylex from '@stylexjs/stylex' 2 | 3 | import * as blue from './blue' 4 | import * as red from './red' 5 | 6 | import 'virtual:stylex.css' 7 | 8 | const { className } = stylex.props(blue.styles.blue, red.styles.red) 9 | 10 | export { className } 11 | -------------------------------------------------------------------------------- /__tests__/fixtures/normal/output.css: -------------------------------------------------------------------------------- 1 | .xju2f9n{color:blue} 2 | .x1e2nbdu{color:red} -------------------------------------------------------------------------------- /__tests__/fixtures/normal/red.ts: -------------------------------------------------------------------------------- 1 | import * as stylex from '@stylexjs/stylex' 2 | 3 | export const styles = stylex.create({ 4 | red: { 5 | color: 'red' 6 | } 7 | }) 8 | -------------------------------------------------------------------------------- /__tests__/fixtures/path-alias/main.ts: -------------------------------------------------------------------------------- 1 | import { create, props } from '@stylexjs/stylex' 2 | import { colors } from '@/tokens/color.stylex' 3 | import 'virtual:stylex.css' 4 | 5 | const styles = create({ 6 | black: { 7 | color: colors.black 8 | } 9 | }) 10 | 11 | const { className } = props(styles.black) 12 | 13 | export { className } 14 | -------------------------------------------------------------------------------- /__tests__/fixtures/path-alias/tokens/color.stylex.ts: -------------------------------------------------------------------------------- 1 | import { defineVars } from '@stylexjs/stylex' 2 | 3 | export const colors = defineVars({ 4 | black: '#000' 5 | }) 6 | -------------------------------------------------------------------------------- /__tests__/fixtures/path-alias/tsconfig.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "paths": { 6 | "@/*": ["*"], 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /__tests__/fixtures/pxtorem/main.ts: -------------------------------------------------------------------------------- 1 | import * as stylex from '@stylexjs/stylex' 2 | import 'virtual:stylex.css' 3 | 4 | const styles = stylex.create({ 5 | foo: { 6 | fontSize: '16px' 7 | } 8 | }) 9 | 10 | const { className } = stylex.props(styles.foo) 11 | 12 | export { className } 13 | -------------------------------------------------------------------------------- /__tests__/plugin.spec.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { expect, test } from 'vitest' 3 | import type { UserConfig } from 'vite' 4 | import { stylex } from '../src' 5 | import type { StylexPluginOptions } from '../src' 6 | 7 | interface BuildOptions { 8 | stylex?: StylexPluginOptions 9 | vite?: UserConfig 10 | } 11 | 12 | async function build(fixture: string, opts: BuildOptions = {}) { 13 | const { stylex: stylexOptions = {}, vite: viteOptions = {} } = opts 14 | const basePath = path.join(__dirname, 'fixtures', fixture) 15 | const { build, mergeConfig } = await import('vite') 16 | const bundle = await build(mergeConfig({ 17 | build: { 18 | lib: { 19 | entry: path.join(basePath, 'main.ts'), 20 | formats: ['es'], 21 | fileName: 'bundle' 22 | }, 23 | rollupOptions: { 24 | output: { 25 | assetFileNames: 'style.[ext]' 26 | }, 27 | external: ['@stylexjs/stylex'], 28 | ...viteOptions?.build?.rollupOptions 29 | }, 30 | ...viteOptions?.build, 31 | write: false 32 | }, 33 | plugins: [...(viteOptions.plugins ?? []), stylex(stylexOptions)], 34 | logeLevel: 'silent' 35 | }, viteOptions)) 36 | if (Array.isArray(bundle)) { 37 | const chunks = bundle[0].output.filter(s => s.fileName === 'style.css' || s.fileName === 'bundle.mjs') 38 | chunks.sort((a, b) => a.fileName.endsWith('.css') && !b.fileName.endsWith('.css') ? -1 : 1) 39 | return chunks.map(s => s.type === 'asset' ? s.source : s.code) 40 | } 41 | throw new Error(`Build failed with ${fixture}`) 42 | } 43 | 44 | test('normal', async () => { 45 | const [css, js] = await build('normal', { vite: { build: { cssMinify: false } } }) 46 | expect(css).toMatchSnapshot() 47 | expect(js).toMatchSnapshot() 48 | const [css_1] = await build('normal') 49 | expect(css_1).toMatchSnapshot() 50 | }) 51 | 52 | test('postcss', async () => { 53 | const [css] = await build('pxtorem', { 54 | vite: { 55 | css: { 56 | postcss: { 57 | plugins: [(await import('postcss-pxtorem')).default] 58 | } 59 | } 60 | } 61 | }) 62 | expect(css).toMatchSnapshot() 63 | }) 64 | 65 | test('lightning css', async () => { 66 | const [css] = await build('lightning-css', { 67 | vite: { 68 | css: { 69 | transformer: 'lightningcss', 70 | lightningcss: { 71 | drafts: { 72 | customMedia: true 73 | } 74 | } 75 | } 76 | }, 77 | stylex: { 78 | manuallyControlCssOrder: { 79 | id: path.join(__dirname, 'fixtures', 'lightning-css', 'style.css') 80 | } 81 | } 82 | }) 83 | expect(css).toMatchSnapshot() 84 | }) 85 | 86 | test('aliases', async () => { 87 | const [css] = await build('path-alias', { 88 | vite: { 89 | plugins: [(await import('vite-tsconfig-paths')).default({ root: path.join(__dirname, 'fixtures', 'path-alias') })] 90 | } 91 | }) 92 | expect(css).toMatchSnapshot() 93 | }) 94 | 95 | test('ts config paths', async () => { 96 | const [css] = await build('path-alias', { 97 | vite: { 98 | resolve: { 99 | alias: { 100 | '@': './' 101 | } 102 | }, 103 | build: { 104 | minify: false 105 | } 106 | } 107 | }) 108 | expect(css).toMatchSnapshot() 109 | }) 110 | 111 | test('empty', async () => { 112 | const [css] = await build('empty') 113 | expect(css).toMatchSnapshot() 114 | }) 115 | -------------------------------------------------------------------------------- /client.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'virtual:stylex.css' { 2 | export {} 3 | } 4 | -------------------------------------------------------------------------------- /dprint.json: -------------------------------------------------------------------------------- 1 | { 2 | "lineWidth": 140, 3 | "typescript": { 4 | "semiColons": "asi", 5 | "indentWidth": 2, 6 | "quoteStyle": "preferSingle", 7 | "useTabs": false, 8 | "trailingCommas": "never", 9 | "module.sortImportDeclarations": "maintain", 10 | "importDeclaration.sortNamedImports": "maintain", 11 | "operatorPosition": "maintain", 12 | "jsx.quoteStyle": "preferDouble", 13 | "jsx.bracketPosition": "maintain", 14 | "functionDeclaration.spaceBeforeParentheses": false 15 | }, 16 | "json": {}, 17 | "markdown": {}, 18 | "excludes": ["**/node_modules", "**/*-lock.json", "yarn-4.1.1.cjs"], 19 | "plugins": [ 20 | "https://plugins.dprint.dev/typescript-0.90.5.wasm", 21 | "https://plugins.dprint.dev/json-0.19.2.wasm", 22 | "https://plugins.dprint.dev/markdown-0.17.0.wasm" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /e2e/e2e.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from 'vitest' 2 | import { Page } from 'playwright' 3 | import { createE2EServer, createWakuE2EServer } from './helper' 4 | 5 | function getColorText(s: string) { 6 | return s.match(/--color:(\w*);?/)?.[1] 7 | } 8 | 9 | async function switchColor(page: Page) { 10 | await page.waitForSelector('button') 11 | const elementHandle = await page.$('button') 12 | const textHandle = await page.$('#text') 13 | const red = await textHandle?.getAttribute('style')! 14 | expect(getColorText(red!)).toBe('red') 15 | await elementHandle!.click() 16 | await page.waitForTimeout(1000) 17 | const blue = await textHandle?.getAttribute('style')! 18 | expect(getColorText(blue!)).toBe('blue') 19 | } 20 | 21 | test('vite-react-spa', async () => { 22 | const { page, browser } = await createE2EServer('vite-react-spa') 23 | await switchColor(page) 24 | await browser.close() 25 | }) 26 | 27 | test('vite-vue-spa', async () => { 28 | const { page, browser } = await createE2EServer('vite-vue-spa') 29 | await switchColor(page) 30 | await browser.close() 31 | }) 32 | 33 | test('remix', async () => { 34 | const { page, browser } = await createE2EServer('remix') 35 | await switchColor(page) 36 | await browser.close() 37 | }) 38 | 39 | test('waku', async () => { 40 | const { cp, page, browser } = await createWakuE2EServer() 41 | await switchColor(page) 42 | await browser.close() 43 | cp.kill('SIGTERM') 44 | }) 45 | 46 | test('qwik', async () => { 47 | const { page, browser } = await createE2EServer('qwik', 'ssr') 48 | await switchColor(page) 49 | await browser.close() 50 | }) 51 | -------------------------------------------------------------------------------- /e2e/fixtures/qwik/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "qwik-e2e", 3 | "engines": { 4 | "node": ">=15.0.0" 5 | }, 6 | "private": true, 7 | "trustedDependencies": [ 8 | "sharp" 9 | ], 10 | "scripts": { 11 | "build": "qwik build", 12 | "build.client": "vite build", 13 | "build.preview": "vite build --ssr src/entry.preview.tsx", 14 | "deploy": "echo 'Run \"npm run qwik add\" to install a server adapter'", 15 | "dev": "vite --mode ssr", 16 | "dev.debug": "node --inspect-brk ./node_modules/vite/bin/vite.js --mode ssr --force", 17 | "preview": "qwik build preview && vite preview --open", 18 | "start": "vite --open --mode ssr", 19 | "qwik": "qwik" 20 | }, 21 | "devDependencies": { 22 | "@builder.io/qwik": "^1.5.7", 23 | "@builder.io/qwik-city": "^1.5.7", 24 | "vite": "^5.0.6", 25 | "vite-plugin-stylex-dev": "workspace:*", 26 | "vite-tsconfig-paths": "^4.2.1", 27 | "@stylex-extend/react": "^0.3.3", 28 | "@stylex-extend/core": "^0.3.3" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /e2e/fixtures/qwik/src/entry.ssr.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * WHAT IS THIS FILE? 3 | * 4 | * SSR entry point, in all cases the application is rendered outside the browser, this 5 | * entry point will be the common one. 6 | * 7 | * - Server (express, cloudflare...) 8 | * - npm run start 9 | * - npm run preview 10 | * - npm run build 11 | */ 12 | import { type RenderToStreamOptions, renderToStream } from '@builder.io/qwik/server' 13 | import { manifest } from '@qwik-client-manifest' 14 | import Root from './root' 15 | 16 | export default function(opts: RenderToStreamOptions) { 17 | return renderToStream(, { 18 | manifest, 19 | ...opts, 20 | // Use container attributes to set attributes on the html tag. 21 | containerAttributes: { 22 | lang: 'en-us', 23 | ...opts.containerAttributes 24 | } 25 | }) 26 | } 27 | -------------------------------------------------------------------------------- /e2e/fixtures/qwik/src/root.tsx: -------------------------------------------------------------------------------- 1 | import { component$ } from '@builder.io/qwik' 2 | import { QwikCityProvider, RouterOutlet, ServiceWorkerRegister } from '@builder.io/qwik-city' 3 | import 'virtual:stylex.css' 4 | 5 | export default component$(() => { 6 | /** 7 | * The root of a QwikCity site always start with the component, 8 | * immediately followed by the document's and . 9 | * 10 | * Don't remove the `` and `` elements. 11 | */ 12 | 13 | return ( 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | ) 24 | }) 25 | -------------------------------------------------------------------------------- /e2e/fixtures/qwik/src/routes/index.tsx: -------------------------------------------------------------------------------- 1 | import { $, component$, useSignal } from '@builder.io/qwik' 2 | import type { DocumentHead } from '@builder.io/qwik-city' 3 | import * as stylex from '@stylexjs/stylex' 4 | import { inline } from '@stylex-extend/core' 5 | 6 | export default component$(() => { 7 | const color = useSignal('red') 8 | 9 | const handleClick = $(() => { 10 | color.value = color.value === 'red' ? 'blue' : 'red' 11 | }) 12 | return ( 13 |
14 |

18 | vite-plugin-stylex-dev 19 |

20 | 21 |
22 | ) 23 | }) 24 | 25 | export const head: DocumentHead = { 26 | title: 'Welcome to Qwik', 27 | meta: [ 28 | { 29 | name: 'description', 30 | content: 'Qwik site description' 31 | } 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /e2e/fixtures/qwik/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "target": "ES2017", 5 | "module": "ES2022", 6 | "lib": ["es2022", "DOM", "WebWorker", "DOM.Iterable"], 7 | "jsx": "react-jsx", 8 | "jsxImportSource": "@builder.io/qwik", 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "resolveJsonModule": true, 12 | "moduleResolution": "node", 13 | "esModuleInterop": true, 14 | "skipLibCheck": true, 15 | "incremental": true, 16 | "isolatedModules": true, 17 | "outDir": "tmp", 18 | "noEmit": true, 19 | "types": ["node", "vite/client"], 20 | "paths": { 21 | "~/*": ["./src/*"] 22 | } 23 | }, 24 | "include": ["src", "./*.d.ts", "./*.config.ts"] 25 | } 26 | -------------------------------------------------------------------------------- /e2e/fixtures/qwik/vite.config.mts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import { qwikVite } from '@builder.io/qwik/optimizer' 3 | import { qwikCity } from '@builder.io/qwik-city/vite' 4 | import { stylex } from 'vite-plugin-stylex-dev' 5 | 6 | export default defineConfig(() => { 7 | return { 8 | plugins: [qwikCity(), qwikVite(), stylex({ enableStylexExtend: true })], 9 | dev: { 10 | headers: { 11 | 'Cache-Control': 'public, max-age=0' 12 | } 13 | } 14 | } 15 | }) 16 | -------------------------------------------------------------------------------- /e2e/fixtures/remix/app/root.tsx: -------------------------------------------------------------------------------- 1 | import { Links, Meta, Outlet, Scripts } from '@remix-run/react' 2 | import 'virtual:stylex.css' 3 | 4 | export default function App() { 5 | return ( 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /e2e/fixtures/remix/app/routes/_index.tsx: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @eslint-react/naming-convention/filename 2 | import { useState } from 'react' 3 | import type { MetaFunction } from '@remix-run/node' 4 | 5 | export const meta: MetaFunction = () => { 6 | return [ 7 | { title: 'New Remix App' }, 8 | { name: 'description', content: 'Welcome to Remix!' } 9 | ] 10 | } 11 | 12 | export default function Index() { 13 | const [color, setColor] = useState('red') 14 | 15 | return ( 16 |
17 |

vite-plugin-stylex-dev

18 | 19 |
20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /e2e/fixtures/remix/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "remix-e2e", 3 | "private": true, 4 | "sideEffects": false, 5 | "scripts": { 6 | "dev": "vite dev" 7 | }, 8 | "dependencies": { 9 | "@remix-run/node": "^2.8.1", 10 | "@remix-run/react": "^2.8.1", 11 | "@remix-run/serve": "^2.8.1", 12 | "@stylexjs/stylex": "^0.3.0", 13 | "isbot": "^3.6.8", 14 | "react": "^18.2.0", 15 | "react-dom": "^18.2.0" 16 | }, 17 | "devDependencies": { 18 | "@remix-run/dev": "^2.8.1", 19 | "@types/react": "^18.2.20", 20 | "@types/react-dom": "^18.2.7", 21 | "lightningcss": "^1.24.1", 22 | "typescript": "^5.1.6", 23 | "vite": "^5.0.6", 24 | "@stylex-extend/react": "^0.3.3", 25 | "@stylex-extend/core": "^0.3.3", 26 | "vite-plugin-stylex-dev": "workspace:*" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /e2e/fixtures/remix/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json", 3 | "compilerOptions": { 4 | "jsx": "react-jsx", 5 | "moduleResolution": "Bundler", 6 | "jsxImportSource": "@stylex-extend/react", 7 | "paths": { 8 | "~/*": ["./app/*"] 9 | }, 10 | 11 | // Remix takes care of building everything in `remix build`. 12 | "noEmit": true 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /e2e/fixtures/remix/vite.config.mts: -------------------------------------------------------------------------------- 1 | import { vitePlugin as remix } from '@remix-run/dev' 2 | import { defineConfig } from 'vite' 3 | import { stylex } from 'vite-plugin-stylex-dev' 4 | 5 | export default defineConfig({ 6 | css: { 7 | transformer: 'lightningcss', 8 | lightningcss: { 9 | drafts: { 10 | customMedia: true 11 | } 12 | } 13 | }, 14 | plugins: [ 15 | remix(), 16 | stylex({ enableStylexExtend: true }) 17 | ] 18 | }) 19 | -------------------------------------------------------------------------------- /e2e/fixtures/vite-react-spa/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 |
10 | 11 | 12 | -------------------------------------------------------------------------------- /e2e/fixtures/vite-react-spa/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vite-react-spa", 3 | "scripts": { 4 | "dev": "vite", 5 | "build": "vite build" 6 | }, 7 | "devDependencies": { 8 | "@types/react": "^18.2.45", 9 | "@types/react-dom": "^18.2.18", 10 | "@vitejs/plugin-react": "^4.2.1", 11 | "react": "^18.2.0", 12 | "react-dom": "^18.2.0", 13 | "vite": "^5.0.10", 14 | "vite-plugin-stylex-dev": "workspace:*", 15 | "@stylex-extend/react": "^0.3.3" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /e2e/fixtures/vite-react-spa/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | import 'virtual:stylex.css' 4 | 5 | export function App() { 6 | const [color, setColor] = useState('red') 7 | 8 | return ( 9 |
10 |

vite-plugin-stylex-dev

11 | 12 |
13 | ) 14 | } 15 | 16 | ReactDOM.createRoot(document.querySelector('#app')!).render( 17 | 18 | 19 | 20 | ) 21 | -------------------------------------------------------------------------------- /e2e/fixtures/vite-react-spa/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json", 3 | "compilerOptions": { 4 | "jsx": "react-jsx", 5 | "moduleResolution": "Bundler", 6 | "jsxImportSource": "@stylex-extend/react" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /e2e/fixtures/vite-react-spa/vite.config.mts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | import { stylex } from 'vite-plugin-stylex-dev' 4 | 5 | export default defineConfig({ 6 | plugins: [react(), stylex({ enableStylexExtend: true })] 7 | }) 8 | -------------------------------------------------------------------------------- /e2e/fixtures/vite-vue-spa/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 |
10 | 11 | 12 | -------------------------------------------------------------------------------- /e2e/fixtures/vite-vue-spa/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vite-vue-spa", 3 | "scripts": { 4 | "dev": "vite", 5 | "build": "vite build" 6 | }, 7 | "dependencies": { 8 | "@vitejs/plugin-vue": "4.5.1", 9 | "vite": "^5.0.6", 10 | "vite-plugin-stylex-dev": "workspace:*", 11 | "@vitejs/plugin-vue-jsx": "^4.0.0", 12 | "vue": "^3.3.10" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /e2e/fixtures/vite-vue-spa/src/app.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 30 | -------------------------------------------------------------------------------- /e2e/fixtures/vite-vue-spa/src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import App from './app.vue' 3 | import 'virtual:stylex.css' 4 | 5 | createApp(App).mount('#app') 6 | -------------------------------------------------------------------------------- /e2e/fixtures/vite-vue-spa/vite.config.mts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import vue from '@vitejs/plugin-vue' 3 | import { stylex } from 'vite-plugin-stylex-dev' 4 | 5 | export default defineConfig({ 6 | plugins: [vue(), stylex()] 7 | }) 8 | -------------------------------------------------------------------------------- /e2e/fixtures/waku/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "waku-e2e", 3 | "type": "module", 4 | "dependencies": { 5 | "@stylexjs/stylex": "workspace:*", 6 | "react": "19.0.0-rc.0", 7 | "react-dom": "19.0.0-rc.0", 8 | "react-server-dom-webpack": "19.0.0-rc.0", 9 | "waku": "0.20.2" 10 | }, 11 | "scripts": { 12 | "dev": "waku dev", 13 | "build": "waku build", 14 | "start": "waku start" 15 | }, 16 | "devDependencies": { 17 | "@types/react": "18.2.46", 18 | "@types/react-dom": "18.2.18", 19 | "typescript": "5.3.3", 20 | "vite": "5.0.10", 21 | "vite-plugin-stylex-dev": "workspace:*", 22 | "@stylex-extend/react": "^0.3.3" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /e2e/fixtures/waku/src/components/app.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | 3 | const App = () => { 4 | const [color, setColor] = useState('red') 5 | 6 | return ( 7 |
8 |

vite-plugin-stylex-dev

9 | 10 |
11 | ) 12 | } 13 | 14 | export default App 15 | -------------------------------------------------------------------------------- /e2e/fixtures/waku/src/enteries.tsx: -------------------------------------------------------------------------------- 1 | import { defineEntries } from 'waku/server' 2 | import App from './components/app' 3 | 4 | export default defineEntries( 5 | // renderEntries 6 | async () => { 7 | return { 8 | App: 9 | } 10 | }, 11 | // getBuildConfig 12 | async () => [{ pathname: '/', entries: [{ input: '' }] }], 13 | // getSsrConfig 14 | async (pathname) => { 15 | console.log(pathname) 16 | switch (pathname) { 17 | case '/': 18 | return { 19 | input: '', 20 | body: 21 | } 22 | default: 23 | return null 24 | } 25 | } 26 | ) 27 | -------------------------------------------------------------------------------- /e2e/fixtures/waku/src/main.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from 'react' 2 | import { createRoot, hydrateRoot } from 'react-dom/client' 3 | import { Root } from 'waku/client' 4 | import App from './components/app' 5 | import 'virtual:stylex.css' 6 | 7 | const rootElement = ( 8 | 9 | 10 | 11 | 12 | 13 | ) 14 | 15 | if (import.meta.env.WAKU_HYDRATE) { 16 | hydrateRoot(document.body, rootElement) 17 | } else { 18 | createRoot(document.body).render(rootElement) 19 | } 20 | -------------------------------------------------------------------------------- /e2e/fixtures/waku/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "target": "esnext", 5 | "downlevelIteration": true, 6 | "esModuleInterop": true, 7 | "module": "esnext", 8 | "moduleResolution": "bundler", 9 | "skipLibCheck": true, 10 | "noUncheckedIndexedAccess": true, 11 | "exactOptionalPropertyTypes": true, 12 | "types": ["react/experimental"], 13 | "jsx": "react-jsx", 14 | "jsxImportSource": "@stylex-extend/react" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /e2e/fixtures/waku/vite.config.mts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | 3 | import { stylex } from 'vite-plugin-stylex-dev' 4 | 5 | export default defineConfig({ 6 | ssr: { 7 | external: ['@stylexjs/stylex'] 8 | }, 9 | plugins: [stylex({ enableStylexExtend: true })] 10 | }) 11 | -------------------------------------------------------------------------------- /e2e/helper.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import child_porcess from 'child_process' 3 | import net from 'net' 4 | import { chromium } from 'playwright' 5 | 6 | export async function createChromeBrowser(port: number) { 7 | const browser = await chromium.launch() 8 | const page = await browser.newPage() 9 | const localURL = `http://localhost:${port}` 10 | page.goto(localURL) 11 | return { page, browser } 12 | } 13 | 14 | // I don't know why vite don't accept port 0 15 | export function genRandomPort() { 16 | const minPort = 5173 17 | const maxPort = 49151 18 | return Math.floor(Math.random() * (maxPort - minPort + 1)) + minPort 19 | } 20 | 21 | export async function createE2EServer(taskName: string, mode = 'development') { 22 | const configFile = path.join(__dirname, 'fixtures', taskName, 'vite.config.mts') 23 | const { createServer } = await import('vite') 24 | const server = await createServer({ 25 | configFile, 26 | server: { 27 | port: genRandomPort() 28 | }, 29 | root: path.join(__dirname, 'fixtures', taskName), 30 | mode 31 | }) 32 | await server.listen() 33 | const { page, browser } = await createChromeBrowser(server.config.server.port!) 34 | return { page, server, browser } 35 | } 36 | 37 | function waitForPort(port: number, host = 'localhost', timeout = 30000) { 38 | const startTime = Date.now() 39 | return new Promise((resolve, reject) => { 40 | const check = () => { 41 | const client = new net.Socket() 42 | client.once('connect', () => { 43 | client.end() 44 | resolve(true) 45 | }).once('error', (err) => { 46 | if (Date.now() - startTime > timeout) { 47 | reject(err) 48 | } else { 49 | setTimeout(check, 100) // Retry after 100ms 50 | } 51 | }) 52 | .connect(port, host) 53 | } 54 | check() 55 | }) 56 | } 57 | 58 | export async function createWakuE2EServer(port = genRandomPort()) { 59 | // waku don't expose cli so we should get the cli entry by two step 60 | const wakuPoint = require.resolve('waku', { paths: [__dirname] }) 61 | const waku = path.join(path.dirname(wakuPoint), 'cli.js') 62 | const cp = child_porcess.exec(`node ${waku} dev --port ${port}`, { cwd: path.join(__dirname, 'fixtures', 'waku') }) 63 | await waitForPort(port) 64 | const { page, browser } = await createChromeBrowser(port) 65 | console.log(`http://localhost:${port}`) 66 | return { cp, page, browser } 67 | } 68 | -------------------------------------------------------------------------------- /e2e/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "e2e", 3 | "private": "true" 4 | } 5 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | const { nonzzz } = require('eslint-config-kagura') 2 | const parserVue = require('vue-eslint-parser') 3 | const parserTs = require('@typescript-eslint/parser') 4 | const pluginVue = require('eslint-plugin-vue') 5 | 6 | module.exports = nonzzz({ ts: true, jsx: true, react: true }, { ignores: ['dist', 'node_modules', 'public', '**/*output.js'] }, { 7 | plugins: { 8 | vue: pluginVue 9 | }, 10 | files: ['**/*.vue'], 11 | languageOptions: { 12 | parser: parserVue, 13 | parserOptions: { 14 | ecmaFeatures: { 15 | jsx: true 16 | }, 17 | extraFileExtensions: ['.vue'], 18 | parser: parserTs, 19 | sourceType: 'module' 20 | } 21 | }, 22 | processor: pluginVue.processors['.vue'], 23 | rules: { 24 | ...pluginVue.configs.base.rules, 25 | ...pluginVue.configs.recommended.rules, 26 | 'vue/component-name-in-template-casing': ['error', 'PascalCase'], 27 | 'vue/component-options-name-casing': ['error', 'PascalCase'], 28 | 'vue/custom-event-name-casing': ['error', 'camelCase'], 29 | 'vue/define-macros-order': ['error', { 30 | order: ['defineOptions', 'defineProps', 'defineEmits', 'defineSlots'] 31 | }], 32 | 'vue/dot-location': ['error', 'property'], 33 | 'vue/dot-notation': ['error', { allowKeywords: true }], 34 | 'vue/eqeqeq': ['error', 'smart'], 35 | 'vue/html-indent': ['error', 2], 36 | 'vue/html-quotes': ['error', 'double'], 37 | 'vue/max-attributes-per-line': 'off', 38 | 'vue/multi-word-component-names': 'off', 39 | 'vue/no-dupe-keys': 'off', 40 | 'vue/no-empty-pattern': 'error', 41 | 'vue/no-extra-parens': ['error', 'functions'], 42 | 'vue/no-irregular-whitespace': 'error', 43 | 'vue/no-loss-of-precision': 'error', 44 | 'vue/no-restricted-syntax': [ 45 | 'error', 46 | 'DebuggerStatement', 47 | 'LabeledStatement', 48 | 'WithStatement' 49 | ], 50 | 'vue/no-restricted-v-bind': ['error', '/^v-/'], 51 | 'vue/no-setup-props-reactivity-loss': 'off', 52 | 'vue/no-sparse-arrays': 'error', 53 | 'vue/no-unused-refs': 'error', 54 | 'vue/no-useless-v-bind': 'error', 55 | 'vue/no-v-html': 'off', 56 | 'vue/object-shorthand': [ 57 | 'error', 58 | 'always', 59 | { 60 | avoidQuotes: true, 61 | ignoreConstructors: false 62 | } 63 | ], 64 | 'vue/prefer-separate-static-class': 'error', 65 | 'vue/prefer-template': 'error', 66 | 'vue/prop-name-casing': ['error', 'camelCase'], 67 | 'vue/require-default-prop': 'off', 68 | 'vue/require-prop-types': 'off', 69 | 'vue/space-infix-ops': 'error', 70 | 'vue/space-unary-ops': ['error', { nonwords: false, words: true }], 71 | 'vue/array-bracket-spacing': ['error', 'never'], 72 | 'vue/arrow-spacing': ['error', { after: true, before: true }], 73 | 'vue/block-spacing': ['error', 'always'], 74 | 'vue/block-tag-newline': ['error', { 75 | multiline: 'always', 76 | singleline: 'always' 77 | }], 78 | 'vue/brace-style': ['error', 'stroustrup', { allowSingleLine: true }], 79 | 'vue/comma-dangle': ['error', 'always-multiline'], 80 | 'vue/comma-spacing': ['error', { after: true, before: false }], 81 | 'vue/comma-style': ['error', 'last'], 82 | 'vue/html-comment-content-spacing': ['error', 'always', { 83 | exceptions: ['-'] 84 | }], 85 | 'vue/key-spacing': ['error', { afterColon: true, beforeColon: false }], 86 | 'vue/keyword-spacing': ['error', { after: true, before: true }], 87 | 'vue/object-curly-newline': 'off', 88 | 'vue/object-curly-spacing': ['error', 'always'], 89 | 'vue/object-property-newline': ['error', { allowMultiplePropertiesPerLine: true }], 90 | 'vue/operator-linebreak': ['error', 'before'], 91 | 'vue/padding-line-between-blocks': ['error', 'always'], 92 | 'vue/quote-props': ['error', 'consistent-as-needed'], 93 | 'vue/space-in-parens': ['error', 'never'], 94 | 'vue/template-curly-spacing': 'error' 95 | } 96 | }) 97 | -------------------------------------------------------------------------------- /examples/astro-demo/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["astro-build.astro-vscode"], 3 | "unwantedRecommendations": [] 4 | } 5 | -------------------------------------------------------------------------------- /examples/astro-demo/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "command": "./node_modules/.bin/astro dev", 6 | "name": "Development server", 7 | "request": "launch", 8 | "type": "node-terminal" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /examples/astro-demo/README.md: -------------------------------------------------------------------------------- 1 | # Astro Starter Kit: Basics 2 | 3 | ```sh 4 | npm create astro@latest -- --template basics 5 | ``` 6 | 7 | [![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/astro/tree/latest/examples/basics) 8 | [![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/basics) 9 | [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/basics/devcontainer.json) 10 | 11 | > 🧑‍🚀 **Seasoned astronaut?** Delete this file. Have fun! 12 | 13 | ![just-the-basics](https://github.com/withastro/astro/assets/2244813/a0a5533c-a856-4198-8470-2d67b1d7c554) 14 | 15 | ## 🚀 Project Structure 16 | 17 | Inside of your Astro project, you'll see the following folders and files: 18 | 19 | ```text 20 | / 21 | ├── public/ 22 | │ └── favicon.svg 23 | ├── src/ 24 | │ ├── components/ 25 | │ │ └── Card.astro 26 | │ ├── layouts/ 27 | │ │ └── Layout.astro 28 | │ └── pages/ 29 | │ └── index.astro 30 | └── package.json 31 | ``` 32 | 33 | Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name. 34 | 35 | There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components. 36 | 37 | Any static assets, like images, can be placed in the `public/` directory. 38 | 39 | ## 🧞 Commands 40 | 41 | All commands are run from the root of the project, from a terminal: 42 | 43 | | Command | Action | 44 | | :------------------------ | :----------------------------------------------- | 45 | | `npm install` | Installs dependencies | 46 | | `npm run dev` | Starts local dev server at `localhost:4321` | 47 | | `npm run build` | Build your production site to `./dist/` | 48 | | `npm run preview` | Preview your build locally, before deploying | 49 | | `npm run astro ...` | Run CLI commands like `astro add`, `astro check` | 50 | | `npm run astro -- --help` | Get help using the Astro CLI | 51 | 52 | ## 👀 Want to learn more? 53 | 54 | Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat). 55 | -------------------------------------------------------------------------------- /examples/astro-demo/astro.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'astro/config' 2 | import { stylex } from 'vite-plugin-stylex-dev' 3 | 4 | // https://astro.build/config 5 | export default defineConfig({ 6 | vite: { 7 | plugins: [stylex({ include: /\.(mjs|js|ts|vue|jsx|tsx|astro)(\?.*|)$/ })] 8 | } 9 | }) 10 | -------------------------------------------------------------------------------- /examples/astro-demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "astro-demo", 3 | "type": "module", 4 | "version": "0.0.1", 5 | "scripts": { 6 | "dev": "astro dev", 7 | "start": "astro dev", 8 | "build": "astro build", 9 | "preview": "astro preview", 10 | "astro": "astro" 11 | }, 12 | "dependencies": { 13 | "astro": "^4.1.1", 14 | "vite-plugin-stylex-dev": "workspace:*" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/astro-demo/public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | -------------------------------------------------------------------------------- /examples/astro-demo/src/components/Card.astro: -------------------------------------------------------------------------------- 1 | --- 2 | interface Props { 3 | title: string; 4 | body: string; 5 | href: string; 6 | } 7 | 8 | const { href, title, body } = Astro.props; 9 | import {styles} from './style' 10 | 11 | import stylex from '@stylexjs/stylex' 12 | 13 | --- 14 | 15 | 27 | 67 | -------------------------------------------------------------------------------- /examples/astro-demo/src/components/style.ts: -------------------------------------------------------------------------------- 1 | import stylex from '@stylexjs/stylex' 2 | 3 | export const styles = stylex.create({ 4 | Button: { 5 | color: 'pink' 6 | } 7 | }) 8 | -------------------------------------------------------------------------------- /examples/astro-demo/src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/astro-demo/src/layouts/Layout.astro: -------------------------------------------------------------------------------- 1 | --- 2 | interface Props { 3 | title: string; 4 | } 5 | 6 | const { title } = Astro.props; 7 | --- 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | {title} 18 | 19 | 20 | 21 | 22 | 23 | 52 | -------------------------------------------------------------------------------- /examples/astro-demo/src/pages/index.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Layout from '../layouts/Layout.astro'; 3 | import Card from '../components/Card.astro'; 4 | --- 5 | 6 | 7 |
8 | 36 |

Welcome to Astro

37 |

38 | To get started, open the directory src/pages in your project.
39 | Code Challenge: Tweak the "Welcome to Astro" message above. 40 |

41 | 63 |
64 |
65 | 66 | 124 | -------------------------------------------------------------------------------- /examples/astro-demo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "astro/tsconfigs/base" 3 | } 4 | -------------------------------------------------------------------------------- /examples/civet-demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 |
10 | 11 | 12 | -------------------------------------------------------------------------------- /examples/civet-demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "civet-demo", 3 | "type": "module", 4 | "scripts": { 5 | "dev": "vite" 6 | }, 7 | "devDependencies": { 8 | "@danielx/civet": "^0.6.62", 9 | "@types/react": "^18.2.45", 10 | "@types/react-dom": "^18.2.18", 11 | "@vitejs/plugin-react": "^4.2.1", 12 | "preact": "^10.19.3", 13 | "react": "^18.2.0", 14 | "react-dom": "^18.2.0", 15 | "vite": "^5.0.10", 16 | "vite-plugin-stylex-dev": "workspace:*" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/civet-demo/src/main.civet: -------------------------------------------------------------------------------- 1 | import * as stylex from @stylexjs/stylex 2 | import ReactDOM from react-dom/client 3 | import React from react 4 | import virtual:stylex.css 5 | 6 | styles := stylex.create 7 | base: 8 | margin: 0 9 | padding: 0 10 | color: 'red' 11 | fontSize: '30px' 12 | border: '1px solid green' 13 | 14 | 15 | export function App() 16 |
17 |

18 | Hello, World! 19 |

20 |
21 | 22 | ReactDOM.createRoot(document.querySelector('#app')!).render( 23 | 24 | 25 | 26 | ) 27 | -------------------------------------------------------------------------------- /examples/civet-demo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "jsx": "preserve", 5 | "lib": ["es2021","DOM"], 6 | "moduleResolution":"Bundler", 7 | "module": "ESNext", 8 | "forceConsistentCasingInFileNames": true, 9 | "resolveJsonModule": true, 10 | "esModuleInterop": true, 11 | "paths": { 12 | "react": ["./node_modules/preact/compat/"], 13 | "react-dom": ["./node_modules/preact/compat/"] 14 | } 15 | }, 16 | } -------------------------------------------------------------------------------- /examples/civet-demo/vite.config.ts: -------------------------------------------------------------------------------- 1 | import civetPlugin from '@danielx/civet/vite' 2 | import { defineConfig } from 'vite' 3 | import react from '@vitejs/plugin-react' 4 | import { stylex } from 'vite-plugin-stylex-dev' 5 | 6 | export default defineConfig({ 7 | resolve: { 8 | alias: { 9 | react: 'preact/compat', 10 | 'react-dom/test-utils': 'preact/test-utils', 11 | 'react-dom': 'preact/compat', // Must be below test-utils 12 | 'react/jsx-runtime': 'preact/jsx-runtime' 13 | } 14 | }, 15 | // Fixme @stylexjs/stylex don't respect `runtimeInjection` so we should set `dev` as false 16 | plugins: [civetPlugin({ implicitExtension: false }), react(), stylex({ include: /\.(mjs|js|ts|civet|jsx|tsx)(\?.*|)$/, dev: false })] 17 | }) 18 | -------------------------------------------------------------------------------- /examples/nuxt-demo/app.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 9 | -------------------------------------------------------------------------------- /examples/nuxt-demo/components/btn1.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 19 | 20 | 23 | -------------------------------------------------------------------------------- /examples/nuxt-demo/components/btn2.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 20 | 21 | 24 | -------------------------------------------------------------------------------- /examples/nuxt-demo/nuxt.config.ts: -------------------------------------------------------------------------------- 1 | import { stylex } from 'vite-plugin-stylex-dev' 2 | 3 | // https://nuxt.com/docs/api/configuration/nuxt-config 4 | export default defineNuxtConfig({ 5 | vite: { 6 | plugins: [stylex()] 7 | } 8 | }) 9 | -------------------------------------------------------------------------------- /examples/nuxt-demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nuxt-demo", 3 | "type": "module", 4 | "scripts": { 5 | "dev": "nuxt dev", 6 | "build": "nuxt build", 7 | "preview": "nuxt preview" 8 | }, 9 | "devDependencies": { 10 | "@nuxt/kit": "^3.8.2", 11 | "@stylexjs/stylex": "^0.3.0", 12 | "@unhead/shared": "^1.8.9", 13 | "nuxt": "^3.8.2", 14 | "ufo": "^1.3.2", 15 | "unhead": "^1.8.9", 16 | "vite-plugin-stylex-dev": "workspace:*", 17 | "vue": "^3.3.12", 18 | "vue-router": "^4.2.5" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/nuxt-demo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends":"./.nuxt/tsconfig.json" 3 | } -------------------------------------------------------------------------------- /examples/qwik-demo/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["dbaeumer.vscode-eslint", "unifiedjs.vscode-mdx"], 3 | "unwantedRecommendations": [] 4 | } 5 | -------------------------------------------------------------------------------- /examples/qwik-demo/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Launch Chrome", 9 | "request": "launch", 10 | "type": "chrome", 11 | "url": "http://localhost:5173", 12 | "webRoot": "${workspaceFolder}" 13 | }, 14 | { 15 | "type": "node", 16 | "name": "dev.debug", 17 | "request": "launch", 18 | "skipFiles": ["/**"], 19 | "cwd": "${workspaceFolder}", 20 | "program": "${workspaceFolder}/node_modules/vite/bin/vite.js", 21 | "args": ["--mode", "ssr", "--force"] 22 | } 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /examples/qwik-demo/.vscode/qwik-city.code-snippets: -------------------------------------------------------------------------------- 1 | { 2 | "onRequest": { 3 | "scope": "javascriptreact,typescriptreact", 4 | "prefix": "qonRequest", 5 | "description": "onRequest function for a route index", 6 | "body": [ 7 | "export const onRequest: RequestHandler = (request) => {", 8 | " $0", 9 | "};" 10 | ] 11 | }, 12 | "loader$": { 13 | "scope": "javascriptreact,typescriptreact", 14 | "prefix": "qloader$", 15 | "description": "loader$()", 16 | "body": ["export const $1 = routeLoader$(() => {", " $0", "});"] 17 | }, 18 | "action$": { 19 | "scope": "javascriptreact,typescriptreact", 20 | "prefix": "qaction$", 21 | "description": "action$()", 22 | "body": ["export const $1 = routeAction$((data) => {", " $0", "});"] 23 | }, 24 | "Full Page": { 25 | "scope": "javascriptreact,typescriptreact", 26 | "prefix": "qpage", 27 | "description": "Simple page component", 28 | "body": [ 29 | "import { component$ } from '@builder.io/qwik';", 30 | "", 31 | "export default component$(() => {", 32 | " $0", 33 | "});" 34 | ] 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /examples/qwik-demo/.vscode/qwik.code-snippets: -------------------------------------------------------------------------------- 1 | { 2 | "Qwik component (simple)": { 3 | "scope": "javascriptreact,typescriptreact", 4 | "prefix": "qcomponent$", 5 | "description": "Simple Qwik component", 6 | "body": [ 7 | "export const ${1:${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/}} = component$(() => {", 8 | " return <${2:div}>$4", 9 | "});" 10 | ] 11 | }, 12 | "Qwik component (props)": { 13 | "scope": "typescriptreact", 14 | "prefix": "qcomponent$ + props", 15 | "description": "Qwik component w/ props", 16 | "body": [ 17 | "export interface ${1:${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/}}Props {", 18 | " $2", 19 | "}", 20 | "", 21 | "export const $1 = component$<$1Props>((props) => {", 22 | " const ${2:count} = useSignal(0);", 23 | " return (", 24 | " <${3:div} on${4:Click}$={(ev) => {$5}}>", 25 | " $6", 26 | " ", 27 | " );", 28 | "});" 29 | ] 30 | }, 31 | "Qwik signal": { 32 | "scope": "javascriptreact,typescriptreact", 33 | "prefix": "quseSignal", 34 | "description": "useSignal() declaration", 35 | "body": ["const ${1:foo} = useSignal($2);", "$0"] 36 | }, 37 | "Qwik store": { 38 | "scope": "javascriptreact,typescriptreact", 39 | "prefix": "quseStore", 40 | "description": "useStore() declaration", 41 | "body": ["const ${1:state} = useStore({", " $2", "});", "$0"] 42 | }, 43 | "$ hook": { 44 | "scope": "javascriptreact,typescriptreact", 45 | "prefix": "q$", 46 | "description": "$() function hook", 47 | "body": ["$(() => {", " $0", "});", ""] 48 | }, 49 | "useVisibleTask": { 50 | "scope": "javascriptreact,typescriptreact", 51 | "prefix": "quseVisibleTask", 52 | "description": "useVisibleTask$() function hook", 53 | "body": ["useVisibleTask$(({ track }) => {", " $0", "});", ""] 54 | }, 55 | "useTask": { 56 | "scope": "javascriptreact,typescriptreact", 57 | "prefix": "quseTask$", 58 | "description": "useTask$() function hook", 59 | "body": [ 60 | "useTask$(({ track }) => {", 61 | " track(() => $1);", 62 | " $0", 63 | "});", 64 | "" 65 | ] 66 | }, 67 | "useResource": { 68 | "scope": "javascriptreact,typescriptreact", 69 | "prefix": "quseResource$", 70 | "description": "useResource$() declaration", 71 | "body": [ 72 | "const $1 = useResource$(({ track, cleanup }) => {", 73 | " $0", 74 | "});", 75 | "" 76 | ] 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /examples/qwik-demo/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "material-icon-theme.activeIconPack": "qwik", 3 | "emmet.includeLanguages": { 4 | "typescriptreact": "html" 5 | }, 6 | "emmet.preferences": { 7 | // to ensure closing tags are used (e.g. not just like in HTML) 8 | // https://github.com/microsoft/vscode/commit/083bf9020407ea5a91199eb1f0b373859df8d600#diff-88456bc9b7caa2f8126aea0107b4671db0f094961aaf39a7c689f890e23aaaba 9 | "output.selfClosingStyle": "xhtml" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /examples/qwik-demo/README.md: -------------------------------------------------------------------------------- 1 | # Qwik City App ⚡️ 2 | 3 | - [Qwik Docs](https://qwik.builder.io/) 4 | - [Discord](https://qwik.builder.io/chat) 5 | - [Qwik GitHub](https://github.com/BuilderIO/qwik) 6 | - [@QwikDev](https://twitter.com/QwikDev) 7 | - [Vite](https://vitejs.dev/) 8 | 9 | --- 10 | 11 | ## Project Structure 12 | 13 | This project is using Qwik with [QwikCity](https://qwik.builder.io/qwikcity/overview/). QwikCity is just an extra set of tools on top of Qwik to make it easier to build a full site, including directory-based routing, layouts, and more. 14 | 15 | Inside your project, you'll see the following directory structure: 16 | 17 | ``` 18 | ├── public/ 19 | │ └── ... 20 | └── src/ 21 | ├── components/ 22 | │ └── ... 23 | └── routes/ 24 | └── ... 25 | ``` 26 | 27 | - `src/routes`: Provides the directory-based routing, which can include a hierarchy of `layout.tsx` layout files, and an `index.tsx` file as the page. Additionally, `index.ts` files are endpoints. Please see the [routing docs](https://qwik.builder.io/qwikcity/routing/overview/) for more info. 28 | 29 | - `src/components`: Recommended directory for components. 30 | 31 | - `public`: Any static assets, like images, can be placed in the public directory. Please see the [Vite public directory](https://vitejs.dev/guide/assets.html#the-public-directory) for more info. 32 | 33 | ## Add Integrations and deployment 34 | 35 | Use the `yarn qwik add` command to add additional integrations. Some examples of integrations includes: Cloudflare, Netlify or Express Server, and the [Static Site Generator (SSG)](https://qwik.builder.io/qwikcity/guides/static-site-generation/). 36 | 37 | ```shell 38 | yarn qwik add # or `yarn qwik add` 39 | ``` 40 | 41 | ## Development 42 | 43 | Development mode uses [Vite's development server](https://vitejs.dev/). The `dev` command will server-side render (SSR) the output during development. 44 | 45 | ```shell 46 | npm start # or `yarn start` 47 | ``` 48 | 49 | > Note: during dev mode, Vite may request a significant number of `.js` files. This does not represent a Qwik production build. 50 | 51 | ## Preview 52 | 53 | The preview command will create a production build of the client modules, a production build of `src/entry.preview.tsx`, and run a local server. The preview server is only for convenience to preview a production build locally and should not be used as a production server. 54 | 55 | ```shell 56 | yarn preview # or `yarn preview` 57 | ``` 58 | 59 | ## Production 60 | 61 | The production build will generate client and server modules by running both client and server build commands. The build command will use Typescript to run a type check on the source code. 62 | 63 | ```shell 64 | yarn build # or `yarn build` 65 | ``` 66 | -------------------------------------------------------------------------------- /examples/qwik-demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-qwik-basic-starter", 3 | "description": "Demo App with Routing built-in (recommended)", 4 | "engines": { 5 | "node": ">=15.0.0" 6 | }, 7 | "private": true, 8 | "trustedDependencies": [ 9 | "sharp" 10 | ], 11 | "scripts": { 12 | "build": "qwik build", 13 | "build.client": "vite build", 14 | "build.preview": "vite build --ssr src/entry.preview.tsx", 15 | "deploy": "echo 'Run \"npm run qwik add\" to install a server adapter'", 16 | "dev": "vite --mode ssr", 17 | "dev.debug": "node --inspect-brk ./node_modules/vite/bin/vite.js --mode ssr --force", 18 | "preview": "qwik build preview && vite preview --open", 19 | "start": "vite --open --mode ssr", 20 | "qwik": "qwik" 21 | }, 22 | "devDependencies": { 23 | "@builder.io/qwik": "^1.3.0", 24 | "@builder.io/qwik-city": "^1.3.0", 25 | "vite": "^5.0.6", 26 | "vite-plugin-stylex-dev": "workspace:*", 27 | "vite-tsconfig-paths": "^4.2.1" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /examples/qwik-demo/public/favicon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/qwik-demo/public/fonts/poppins-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nonzzz/vite-plugin-stylex/bbaf0faeafecf32e171194ebb293fe47feb0a6c2/examples/qwik-demo/public/fonts/poppins-400.woff2 -------------------------------------------------------------------------------- /examples/qwik-demo/public/fonts/poppins-500.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nonzzz/vite-plugin-stylex/bbaf0faeafecf32e171194ebb293fe47feb0a6c2/examples/qwik-demo/public/fonts/poppins-500.woff2 -------------------------------------------------------------------------------- /examples/qwik-demo/public/fonts/poppins-700.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nonzzz/vite-plugin-stylex/bbaf0faeafecf32e171194ebb293fe47feb0a6c2/examples/qwik-demo/public/fonts/poppins-700.woff2 -------------------------------------------------------------------------------- /examples/qwik-demo/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/web-manifest-combined.json", 3 | "name": "qwik-project-name", 4 | "short_name": "Welcome to Qwik", 5 | "start_url": ".", 6 | "display": "standalone", 7 | "background_color": "#fff", 8 | "description": "A Qwik project app." 9 | } 10 | -------------------------------------------------------------------------------- /examples/qwik-demo/public/robots.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nonzzz/vite-plugin-stylex/bbaf0faeafecf32e171194ebb293fe47feb0a6c2/examples/qwik-demo/public/robots.txt -------------------------------------------------------------------------------- /examples/qwik-demo/src/components/router-head/router-head.tsx: -------------------------------------------------------------------------------- 1 | import { useDocumentHead, useLocation } from "@builder.io/qwik-city"; 2 | 3 | import { component$ } from "@builder.io/qwik"; 4 | 5 | /** 6 | * The RouterHead component is placed inside of the document `` element. 7 | */ 8 | export const RouterHead = component$(() => { 9 | const head = useDocumentHead(); 10 | const loc = useLocation(); 11 | 12 | return ( 13 | <> 14 | {head.title} 15 | 16 | 17 | 18 | 19 | 20 | {head.meta.map((m) => ( 21 | 22 | ))} 23 | 24 | {head.links.map((l) => ( 25 | 26 | ))} 27 | 28 | {head.styles.map((s) => ( 29 | 24 | -------------------------------------------------------------------------------- /examples/vite-demo/src/card.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 26 | -------------------------------------------------------------------------------- /examples/vite-demo/src/global.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import type { DefineComponent } from 'vue' 3 | 4 | const component: DefineComponent<{}, {}, any> 5 | export default component 6 | } 7 | -------------------------------------------------------------------------------- /examples/vite-demo/src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import App from './app.vue' 3 | import 'virtual:stylex.css' 4 | 5 | createApp(App).mount('#app') 6 | -------------------------------------------------------------------------------- /examples/vite-demo/src/stylex.ts: -------------------------------------------------------------------------------- 1 | import * as stylex from '@stylexjs/stylex' 2 | 3 | const styles = stylex.create({ 4 | normal: { 5 | color: 'red', 6 | display: 'flex', 7 | border: '1px solid blue' 8 | } 9 | }) 10 | 11 | export { styles } 12 | -------------------------------------------------------------------------------- /examples/vite-demo/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import vue from '@vitejs/plugin-vue' 3 | import Inspect from 'vite-plugin-inspect' 4 | import { stylex } from 'vite-plugin-stylex-dev' 5 | 6 | export default defineConfig({ 7 | plugins: [vue(), stylex(), Inspect({ dev: true })] 8 | }) 9 | -------------------------------------------------------------------------------- /examples/vite-react-demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 |
10 | 11 | 12 | -------------------------------------------------------------------------------- /examples/vite-react-demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vite-react-demo", 3 | "scripts": { 4 | "dev": "vite", 5 | "build": "vite build", 6 | "preview": "vite preview", 7 | "watch": "vite build --watch" 8 | }, 9 | "devDependencies": { 10 | "@types/react": "^18.2.45", 11 | "@types/react-dom": "^18.2.18", 12 | "@vitejs/plugin-react": "^4.2.1", 13 | "react": "^18.2.0", 14 | "react-dom": "^18.2.0", 15 | "react-strict-dom": "^0.0.4", 16 | "vite": "^5.0.10", 17 | "vite-plugin-stylex-dev": "workspace:*", 18 | "vite-tsconfig-paths": "^4.2.3" 19 | }, 20 | "dependencies": { 21 | "@stylexjs/open-props": "^0.3.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/vite-react-demo/src/app.tsx: -------------------------------------------------------------------------------- 1 | import { css, html } from 'react-strict-dom' 2 | import { Fragment } from 'react' 3 | import { injectGlobalStyle, inline } from '@stylex-extend/core' 4 | import { props } from '@stylexjs/stylex' 5 | import Button from './button' 6 | import { colors } from './tokens.stylex' 7 | import './stylex.css' 8 | 9 | injectGlobalStyle({ 10 | p: { 11 | color: colors.purple 12 | } 13 | }) 14 | 15 | const egStyles = css.create({ 16 | container: { borderTopWidth: 1 }, 17 | h1: { padding: 10, backgroundColor: '#eee' }, 18 | content: { padding: 10 }, 19 | div: { 20 | paddingBottom: 50, 21 | paddingTop: 50, 22 | backgroundColor: 'white' 23 | } 24 | }) 25 | 26 | function ExampleBlock() { 27 | return ( 28 | 29 | react-strict-dom 30 | Hello World 31 | 32 | ) 33 | } 34 | 35 | function App() { 36 | return ( 37 | 38 |

Hello Stylex Extend

39 | 40 | 41 |
42 | ) 43 | } 44 | export { App } 45 | -------------------------------------------------------------------------------- /examples/vite-react-demo/src/button.tsx: -------------------------------------------------------------------------------- 1 | import { colors } from '#/tokens.stylex' 2 | import { fonts } from '#/tokens/nested.stylex' 3 | import { others } from '~/other.stylex' 4 | 5 | const Button = (props: any) => ( 6 | 26 | ) 27 | 28 | export default Button 29 | -------------------------------------------------------------------------------- /examples/vite-react-demo/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | import { App } from './app' 4 | 5 | ReactDOM.createRoot(document.querySelector('#app')!).render( 6 | 7 | 8 | 9 | ) 10 | -------------------------------------------------------------------------------- /examples/vite-react-demo/src/stylex.css: -------------------------------------------------------------------------------- 1 | @custom-stylex; -------------------------------------------------------------------------------- /examples/vite-react-demo/src/tokens.stylex.ts: -------------------------------------------------------------------------------- 1 | import * as stylex from '@stylexjs/stylex' 2 | 3 | const DARK = '@media (prefers-color-scheme: dark)' 4 | 5 | export const colors = stylex.defineVars({ 6 | primary: { default: '#fff', [DARK]: '#000' }, 7 | primaryDark: { default: '#ccc', [DARK]: '#333' }, 8 | bg: { default: '#fff', [DARK]: '#000' }, 9 | white: { default: '#fff', [DARK]: '#000' }, 10 | black: { default: '#000', [DARK]: '#fff' }, 11 | purple: 'purple' 12 | }) 13 | -------------------------------------------------------------------------------- /examples/vite-react-demo/src/tokens/nested.stylex.ts: -------------------------------------------------------------------------------- 1 | import * as stylex from '@stylexjs/stylex' 2 | 3 | export const fonts = stylex.defineVars({ 4 | font: '20px' 5 | }) 6 | -------------------------------------------------------------------------------- /examples/vite-react-demo/themes/other.stylex.ts: -------------------------------------------------------------------------------- 1 | import * as stylex from '@stylexjs/stylex' 2 | 3 | export const others = stylex.defineVars({ 4 | borderRadius: '30px' 5 | }) 6 | -------------------------------------------------------------------------------- /examples/vite-react-demo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "outDir": "./dist", 5 | "jsx": "react-jsx", 6 | "esModuleInterop": true, 7 | "target": "ESNext", 8 | "module": "ESNext", 9 | "moduleResolution": "Node", 10 | "types": ["vite/client", "react", "react-dom","@stylex-extend/react"], 11 | "paths": { 12 | "#/*": ["src/*"], 13 | "~/*": ["themes/*"] 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/vite-react-demo/vite.config.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { defineConfig } from 'vite' 3 | import react from '@vitejs/plugin-react' 4 | import tsconfigPaths from 'vite-tsconfig-paths' 5 | import { stylex } from 'vite-plugin-stylex-dev' 6 | import rsdPlugin from 'react-strict-dom/babel' 7 | 8 | export default defineConfig({ 9 | // resolve: { 10 | // alias: { 11 | // '@': path.join(__dirname, 'src'), 12 | // '~': path.join(__dirname, 'themes') 13 | // } 14 | // }, 15 | plugins: [tsconfigPaths({ root: __dirname }), react(), 16 | stylex( 17 | { 18 | babelConfig: { 19 | plugins: [rsdPlugin] 20 | }, 21 | importSources: [ 22 | '@stylexjs/stylex', 23 | 'stylex', 24 | { from: 'react-strict-dom', as: 'css' } 25 | ], 26 | styleResolution: 'property-specificity', 27 | enableStylexExtend: true, 28 | manuallyControlCssOrder: { 29 | id: path.join(__dirname, 'src/stylex.css'), 30 | symbol: '@custom-stylex;' 31 | } 32 | } 33 | )] 34 | }) 35 | -------------------------------------------------------------------------------- /examples/waku-demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "waku-demo", 3 | "version": "0.1.0", 4 | "type": "module", 5 | "private": true, 6 | "scripts": { 7 | "dev": "waku dev", 8 | "build": "waku build", 9 | "start": "waku start" 10 | }, 11 | "dependencies": { 12 | "@stylexjs/stylex": "workspace:*", 13 | "react": "18.3.0-canary-c5b937576-20231219", 14 | "react-dom": "18.3.0-canary-c5b937576-20231219", 15 | "react-server-dom-webpack": "18.3.0-canary-f1039be4a-20240107", 16 | "waku": "0.20.2-alpha.0" 17 | }, 18 | "devDependencies": { 19 | "@types/react": "18.2.46", 20 | "@types/react-dom": "18.2.18", 21 | "typescript": "5.3.3", 22 | "vite": "5.0.10", 23 | "vite-plugin-stylex-dev": "workspace:*", 24 | "vite-plugin-stylex-dev-classic": "npm:vite-plugin-stylex-dev@0.7.5" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/waku-demo/src/components/app.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable stylistic/jsx-one-expression-per-line */ 2 | import { create, props } from '@stylexjs/stylex' 3 | import './init.css' 4 | import { ClientBanner } from './client-banner.js' 5 | import { ServerBanner } from './server-banner.js' 6 | 7 | import { ServerButton } from './server-button.js' 8 | 9 | const styles = create({ 10 | title: { 11 | fontSize: '26px', 12 | color: 'red' 13 | } 14 | }) 15 | 16 | const App = ({ name }: { name: string }) => { 17 | return ( 18 |
19 | Waku example 20 |

Hello {name}

21 |

This is a server component.

22 | 23 | 24 | 25 |
26 | ) 27 | } 28 | 29 | export default App 30 | -------------------------------------------------------------------------------- /examples/waku-demo/src/components/client-banner.tsx: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 'use client' 3 | 4 | import { useEffect } from 'react' 5 | import { create, props } from '@stylexjs/stylex' 6 | 7 | const styles = create({ 8 | root: { 9 | backgroundColor: 'blue', 10 | color: '#fff', 11 | padding: '10px', 12 | textAlign: 'center' 13 | } 14 | }) 15 | 16 | export const ClientBanner = () => { 17 | useEffect(() => { 18 | console.log('ClientBanner rendered!') 19 | }, []) 20 | return
This is a client banner by StyleX CSS
21 | } 22 | -------------------------------------------------------------------------------- /examples/waku-demo/src/components/init.css: -------------------------------------------------------------------------------- 1 | h3{ 2 | color: gray; 3 | } -------------------------------------------------------------------------------- /examples/waku-demo/src/components/server-banner.tsx: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import { create, props } from '@stylexjs/stylex' 4 | 5 | import '@stylex-dev.css' 6 | 7 | const styles = create({ 8 | root: { 9 | backgroundColor: 'pink', 10 | color: '#fff', 11 | padding: '20px', 12 | textAlign: 'center' 13 | } 14 | }) 15 | 16 | export const ServerBanner = () => { 17 | return
This is a server banner by StyleX CSS
18 | } 19 | -------------------------------------------------------------------------------- /examples/waku-demo/src/components/server-button.tsx: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import { create, props } from '@stylexjs/stylex' 4 | 5 | import '@stylex-dev.css' 6 | 7 | const styles = create({ 8 | root: { 9 | backgroundColor: 'purple', 10 | color: '#fff', 11 | padding: '20px', 12 | textAlign: 'center' 13 | } 14 | }) 15 | 16 | export const ServerButton = () => { 17 | return 18 | } 19 | -------------------------------------------------------------------------------- /examples/waku-demo/src/entries.tsx: -------------------------------------------------------------------------------- 1 | import { lazy } from 'react' 2 | import { defineEntries } from 'waku/server' 3 | import { Slot } from 'waku/client' 4 | 5 | const App = lazy(() => import('./components/app.js')) 6 | 7 | export default defineEntries( 8 | // renderEntries 9 | async (input) => { 10 | return { 11 | App: 12 | } 13 | }, 14 | // getBuildConfig 15 | async () => [{ pathname: '/', entries: [{ input: '' }] }], 16 | // getSsrConfig 17 | async (pathname) => { 18 | switch (pathname) { 19 | case '/': 20 | return { 21 | input: '', 22 | body: 23 | } 24 | default: 25 | return null 26 | } 27 | } 28 | ) 29 | -------------------------------------------------------------------------------- /examples/waku-demo/src/main.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from 'react' 2 | import { createRoot, hydrateRoot } from 'react-dom/client' 3 | import { Root, Slot } from 'waku/client' 4 | 5 | import 'virtual:stylex.css' 6 | 7 | const rootElement = ( 8 | 9 | 10 | 11 | 12 | 13 | ) 14 | 15 | if (import.meta.env.WAKU_HYDRATE) { 16 | hydrateRoot(document.body, rootElement) 17 | } else { 18 | createRoot(document.body).render(rootElement) 19 | } 20 | -------------------------------------------------------------------------------- /examples/waku-demo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "target": "esnext", 5 | "downlevelIteration": true, 6 | "esModuleInterop": true, 7 | "module": "nodenext", 8 | "skipLibCheck": true, 9 | "noUncheckedIndexedAccess": true, 10 | "exactOptionalPropertyTypes": true, 11 | "types": ["react/experimental"], 12 | "jsx": "react-jsx", 13 | "outDir": "./dist", 14 | "composite": true 15 | }, 16 | "include": ["./src"], 17 | "references": [ 18 | { 19 | "path": "./tsconfig.node.json" 20 | } 21 | ] 22 | } -------------------------------------------------------------------------------- /examples/waku-demo/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./dist/node", 5 | "types": ["node"] 6 | }, 7 | "include": ["vite.config.ts"] 8 | } -------------------------------------------------------------------------------- /examples/waku-demo/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | 3 | import { stylex } from 'vite-plugin-stylex-dev' 4 | import { waku } from 'vite-plugin-stylex-dev/adapter' 5 | 6 | // import { stylex as classicStylex } from 'vite-plugin-stylex-dev-classic' 7 | 8 | export default defineConfig({ 9 | ssr: { 10 | external: ['@stylexjs/stylex'] 11 | }, 12 | // plugins: [classicStylex()] 13 | plugins: [stylex({ adapter: waku })] 14 | }) 15 | -------------------------------------------------------------------------------- /examples/watch/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vite-watch-demo", 3 | "devDependencies": { 4 | "lightningcss": "^1.24.1", 5 | "vite": "^5.0.6", 6 | "vite-plugin-stylex-dev": "workspace:*" 7 | }, 8 | "scripts": { 9 | "build": "vite build --watch" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /examples/watch/src/index.ts: -------------------------------------------------------------------------------- 1 | import { create, props } from '@stylexjs/stylex' 2 | import { Text } from './text' 3 | import './stylex.css' 4 | 5 | const styles = create({ 6 | text: { 7 | color: { 8 | default: 'red', 9 | '@media (--mx-1)': 'green' 10 | } 11 | } 12 | }) 13 | 14 | export function Button() { 15 | const button = document.createElement('button') 16 | button.className = props(styles.text).className! 17 | return button 18 | } 19 | 20 | function render() { 21 | const button = Button() 22 | const text = Text() 23 | document.body.appendChild(button) 24 | document.body.appendChild(text) 25 | } 26 | 27 | render() 28 | -------------------------------------------------------------------------------- /examples/watch/src/stylex.css: -------------------------------------------------------------------------------- 1 | @custom-media --mx-1 (width <= 1024px); 2 | 3 | 4 | @stylex-dev; -------------------------------------------------------------------------------- /examples/watch/src/text.ts: -------------------------------------------------------------------------------- 1 | import { create, props } from '@stylexjs/stylex' 2 | import { colors } from './var.stylex' 3 | 4 | const styles = create({ 5 | text: { 6 | color: colors.dark, 7 | fontSize: '10px' 8 | } 9 | }) 10 | 11 | export function Text() { 12 | const button = document.createElement('div') 13 | const attrs = props(styles.text) 14 | button.className = attrs.className! 15 | button.setAttribute('style', attrs.style as unknown as string) 16 | return button 17 | } 18 | -------------------------------------------------------------------------------- /examples/watch/src/var.stylex.ts: -------------------------------------------------------------------------------- 1 | import { defineVars } from '@stylexjs/stylex' 2 | 3 | export const colors = defineVars({ 4 | primary: 'pink', 5 | secondary: 'purple', 6 | dark: '#333' 7 | }) 8 | -------------------------------------------------------------------------------- /examples/watch/vite.config.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { defineConfig } from 'vite' 3 | import { stylex } from 'vite-plugin-stylex-dev' 4 | 5 | export default defineConfig({ 6 | plugins: [stylex({ manuallyControlCssOrder: { 7 | id: path.join(__dirname, 'src/stylex.css') 8 | } 9 | })], 10 | build: { 11 | outDir: 'dist', 12 | lib: { 13 | entry: 'src/index.ts', 14 | name: 'main' 15 | } 16 | }, 17 | css: { 18 | transformer: 'lightningcss', 19 | lightningcss: { 20 | drafts: { 21 | customMedia: true 22 | } 23 | } 24 | } 25 | }) 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vite-plugin-stylex-dev", 3 | "version": "0.7.5", 4 | "description": "an unofficial stylex vite plugin", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "module": "dist/index.mjs", 8 | "workspaces": [ 9 | "examples/*" 10 | ], 11 | "exports": { 12 | ".": { 13 | "types": "./dist/index.d.ts", 14 | "import": "./dist/index.mjs", 15 | "require": "./dist/index.js" 16 | }, 17 | "./client": { 18 | "types": "./client.d.ts" 19 | }, 20 | "./adapter": { 21 | "types": "./dist/adapter/index.d.ts", 22 | "import": "./dist/adapter/index.mjs", 23 | "require": "./dist/adapter/index.js" 24 | }, 25 | "./*": "./*" 26 | }, 27 | "files": [ 28 | "dist", 29 | "README.md", 30 | "LICENSE", 31 | "client.d.ts" 32 | ], 33 | "scripts": { 34 | "dev": "rollup --config rollup.config.ts --configPlugin swc3 --watch", 35 | "build": "rollup --config rollup.config.ts --configPlugin swc3", 36 | "test": "vitest --dir __tests__", 37 | "e2e": "vitest --dir e2e" 38 | }, 39 | "keywords": [ 40 | "stylex", 41 | "experimental", 42 | "css-in-js", 43 | "vite-plugin" 44 | ], 45 | "author": "kanno", 46 | "license": "MIT", 47 | "homepage": "https://github.com/nonzzz/vite-plugin-stylex", 48 | "bugs": { 49 | "url": "https://github.com/nonzzz/vite-plugin-stylex/issues" 50 | }, 51 | "repository": { 52 | "type": "git", 53 | "url": "https://github.com/nonzzz/vite-plugin-stylex.git" 54 | }, 55 | "devDependencies": { 56 | "@stylex-extend/core": "0.3.1", 57 | "@stylex-extend/react": "0.3.1", 58 | "@stylexjs/shared": "^0.7.0", 59 | "@stylexjs/stylex": "^0.5.1", 60 | "@swc/core": "^1.4.16", 61 | "@types/babel__core": "^7.20.5", 62 | "@types/connect": "^3.4.38", 63 | "@types/node": "^20.10.4", 64 | "@types/postcss-pxtorem": "^6.0.3", 65 | "@typescript-eslint/parser": "^6.14.0", 66 | "@typescript-eslint/utils": "^6.14.0", 67 | "@vitest/coverage-v8": "^1.6.0", 68 | "dprint": "^0.46.3", 69 | "eslint": "^8.55.0", 70 | "eslint-config-kagura": "^2.2.1", 71 | "eslint-plugin-vue": "^9.19.2", 72 | "get-tsconfig": "^4.7.2", 73 | "lightningcss": "^1.24.1", 74 | "memfs": "^4.9.3", 75 | "playwright": "~1.32.3", 76 | "postcss-pxtorem": "^6.0.0", 77 | "rollup": "^4.16.0", 78 | "rollup-plugin-dts": "^6.1.1", 79 | "rollup-plugin-swc3": "^0.11.1", 80 | "typescript": "^5.5.2", 81 | "vite": "^5.3.1", 82 | "vite-plugin-inspect": "^0.8.4", 83 | "vite-tsconfig-paths": "^4.3.2", 84 | "vitest": "^1.6.0", 85 | "vue-eslint-parser": "^9.3.2" 86 | }, 87 | "dependencies": { 88 | "@babel/core": "^7.23.9", 89 | "@rollup/pluginutils": "^5.1.0", 90 | "@stylex-extend/babel-plugin": "0.3.3", 91 | "@stylexjs/babel-plugin": "^0.7.0" 92 | }, 93 | "resolutions": { 94 | "sharp": "0.32.6", 95 | "vite": "^5.3.1", 96 | "@stylexjs/stylex": "^0.7.0" 97 | }, 98 | "packageManager": "pnpm@9.4.0" 99 | } 100 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - examples/* 3 | - e2e/fixtures/* 4 | -------------------------------------------------------------------------------- /rollup.config.ts: -------------------------------------------------------------------------------- 1 | import { builtinModules, createRequire } from 'module' 2 | import { defineConfig } from 'rollup' 3 | import dts from 'rollup-plugin-dts' 4 | import { swc } from 'rollup-plugin-swc3' 5 | 6 | const _require = createRequire(import.meta.url) 7 | const { dependencies } = _require('./package.json') 8 | 9 | const external = [...Object.keys(dependencies), ...builtinModules] 10 | 11 | export default defineConfig([ 12 | { 13 | input: 'src/index.ts', 14 | external, 15 | output: [ 16 | { file: 'dist/index.mjs', format: 'esm', exports: 'named' }, 17 | { file: 'dist/index.js', format: 'cjs', exports: 'named' } 18 | ], 19 | plugins: [ 20 | swc() 21 | ] 22 | }, 23 | { 24 | input: 'src/index.ts', 25 | output: { file: 'dist/index.d.ts' }, 26 | plugins: [dts({})] 27 | }, 28 | { 29 | input: 'src/adapter/index.ts', 30 | external, 31 | output: [ 32 | { file: 'dist/adapter/index.mjs', format: 'esm', exports: 'named' }, 33 | { file: 'dist/adapter/index.js', format: 'cjs', exports: 'named' } 34 | ], 35 | plugins: [ 36 | swc() 37 | ] 38 | }, 39 | { 40 | input: 'src/adapter/index.ts', 41 | output: { file: 'dist/adapter/index.d.ts' }, 42 | plugins: [dts({})] 43 | } 44 | ]) 45 | -------------------------------------------------------------------------------- /src/adapter/index.ts: -------------------------------------------------------------------------------- 1 | export { waku } from './waku' 2 | -------------------------------------------------------------------------------- /src/adapter/waku.ts: -------------------------------------------------------------------------------- 1 | import type { Plugin, ViteDevServer } from 'vite' 2 | import type { Rule } from '@stylexjs/babel-plugin' 3 | import type { AdapterConfig } from '../interface' 4 | import { CONSTANTS } from '../plugins/server' 5 | import { parseRequest } from '../context' 6 | 7 | const STYLEX_FOR_WAKU_MARKER = '/__stylex__waku.css' 8 | 9 | const CLASSIC_WAKU_MARKER = '@stylex-dev.css' 10 | 11 | const sharedRules = new Map() 12 | 13 | // I would like know why can't sync the rules for sharedRules. It seems like got two instance of waku 14 | 15 | export function waku() { 16 | return { 17 | name: 'waku', 18 | setup(ctx, plugin) { 19 | if (ctx.env === 'build') return 20 | // @ts-expect-error 21 | ctx.vite.config.plugins = ctx.vite.config.plugins.filter(p => p.name !== 'stylex:server') 22 | 23 | let viteDevServer: ViteDevServer | null = null 24 | const entries: Set = new Set() 25 | const effects: Set = new Set() 26 | 27 | const invalidate = (ids: Set) => { 28 | for (const id of ids) { 29 | const mod = viteDevServer?.moduleGraph.getModuleById(id) 30 | if (!mod) continue 31 | viteDevServer?.reloadModule(mod) 32 | } 33 | } 34 | 35 | const self = > { 36 | configureServer(server) { 37 | viteDevServer = server 38 | 39 | viteDevServer.watcher.on('unlink', (path) => { 40 | const { original } = parseRequest(path) 41 | if (sharedRules.has(original)) { 42 | sharedRules.delete(original) 43 | // update hmr 44 | } 45 | }) 46 | }, 47 | load(id) { 48 | // Idk why i can't set css rule. waku seems like split vite plugin and 49 | // run in two instance? you can print the sharedRules and run example/waku 50 | // you can get collection one is three the other one is two. 51 | // CLASSIC_WAKU_MARKER is work for rsc. but we should take care for them. 52 | if (id === STYLEX_FOR_WAKU_MARKER || id === CLASSIC_WAKU_MARKER) { 53 | if (id === CLASSIC_WAKU_MARKER) { 54 | entries.add(id) 55 | } 56 | return { 57 | code: ctx.produceCSS(sharedRules), 58 | map: { mappings: '' } 59 | } 60 | } 61 | }, 62 | resolveId(id) { 63 | if (id === CONSTANTS.VIRTUAL_STYLEX_ID) { 64 | entries.add(STYLEX_FOR_WAKU_MARKER) 65 | return STYLEX_FOR_WAKU_MARKER 66 | } 67 | }, 68 | async transform(code, id, opt) { 69 | const result = await ctx.transform?.apply(this, [code, id, opt]) 70 | const { original } = parseRequest(id) 71 | if (typeof result === 'object' && result?.meta && 'stylex' in result.meta) { 72 | const rule: Rule[][] = [] 73 | if (result.meta.stylex.length > 0) { 74 | rule.push(result.meta.stylex) 75 | } 76 | effects.add(original) 77 | sharedRules.set(original, result.meta.stylex) 78 | invalidate(new Set([...entries])) 79 | } 80 | 81 | return result 82 | } 83 | } 84 | 85 | Object.assign(plugin, self) 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/context.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import stylexBabelPlugin from '@stylexjs/babel-plugin' 3 | import type { Rule } from '@stylexjs/babel-plugin' 4 | import { createFilter } from '@rollup/pluginutils' 5 | import { parseSync } from '@babel/core' 6 | import type { StylexExtendBabelPluginOptions } from '@stylex-extend/babel-plugin' 7 | import type { ManuallyControlCssOrder, RollupPluginContext, StylexPluginOptions } from './interface' 8 | import { error, slash } from './shared' 9 | import { CONSTANTS } from './plugins/server' 10 | 11 | export type Env = 'server' | 'build' 12 | 13 | export interface ImportSpecifier { 14 | n: string | undefined 15 | s: number 16 | e: number 17 | } 18 | export interface ParserOptions { 19 | plugins: any[] 20 | } 21 | 22 | export const defaultControlCSSOptions = { 23 | id: 'stylex.css', 24 | symbol: '@stylex-dev;' 25 | } 26 | 27 | export const defaultStylexExtendOptions = { 28 | enableInjectGlobalStyle: true, 29 | stylex: { helper: 'props' } 30 | } 31 | 32 | function handleRelativePath(from: string, to: string) { 33 | const relativePath = path.relative(path.dirname(from), to).replace(/\.\w+$/, '') 34 | return `./${slash(relativePath)}` 35 | } 36 | 37 | export function scanImportStmt(code: string, filename: string, parserOptions?: ParserOptions) { 38 | const ast = parseSync(code, { parserOpts: { plugins: parserOptions?.plugins || [] }, babelrc: false, filename })! 39 | const stmts: ImportSpecifier[] = [] 40 | for (const n of ast!.program.body) { 41 | if (n.type === 'ImportDeclaration') { 42 | const v = n.source.value 43 | if (!v) continue 44 | const { start: s, end: e } = n.source 45 | if (typeof s === 'number' && typeof e === 'number') { 46 | stmts.push({ n: v, s: s + 1, e: e - 1 }) 47 | } 48 | } 49 | } 50 | return stmts 51 | } 52 | 53 | export function parseRequest(id: string) { 54 | const [original, kind] = id.split('?') 55 | if (!kind) return { original, kind: 'native' } 56 | return { original, kind } 57 | } 58 | 59 | export class PluginContext { 60 | styleRules: Map 61 | globalStyles: Record 62 | #pluginOptions: StylexPluginOptions 63 | root: string 64 | #env: Env 65 | #rollupPluginContext: RollupPluginContext | null 66 | #stmts: ImportSpecifier[] 67 | constructor(options: StylexPluginOptions) { 68 | this.styleRules = new Map() 69 | this.#rollupPluginContext = null 70 | this.#pluginOptions = options 71 | this.#stmts = [] 72 | this.globalStyles = {} 73 | this.root = process.cwd() 74 | this.#env = process.env.NODE_ENV === 'production' ? 'build' : 'server' 75 | } 76 | 77 | get filter() { 78 | return createFilter(this.#pluginOptions.include, this.#pluginOptions.exclude) 79 | } 80 | 81 | get stylexExtendOptions(): StylexExtendBabelPluginOptions { 82 | const { enableStylexExtend } = this.#pluginOptions 83 | if (typeof enableStylexExtend === 'boolean' && enableStylexExtend) { 84 | return { ...defaultStylexExtendOptions } 85 | } 86 | if (typeof enableStylexExtend === 'object') { 87 | if (!enableStylexExtend) return {} 88 | return { ...defaultStylexExtendOptions, ...enableStylexExtend } 89 | } 90 | return {} 91 | } 92 | 93 | get stylexOptions() { 94 | return this.#pluginOptions 95 | } 96 | 97 | setupRollupPluginContext(rollupPluginContext: RollupPluginContext) { 98 | if (this.#rollupPluginContext) return 99 | this.#rollupPluginContext = rollupPluginContext 100 | } 101 | 102 | skipResolve(code: string, id: string) { 103 | if (!this.filter!(id) || id.startsWith('\0')) return false 104 | const { kind } = parseRequest(id) 105 | if (kind.includes('.css')) return false 106 | this.#stmts = scanImportStmt(code, id) 107 | let pass = false 108 | for (const stmt of this.#stmts) { 109 | const { n } = stmt 110 | if (n && this.importSources.some(i => !path.isAbsolute(n) && n.includes(typeof i === 'string' ? i : i.from))) { 111 | pass = true 112 | } 113 | } 114 | return pass 115 | } 116 | 117 | // Alough stylex/stylex-extend support translate path aliases to relative path 118 | // But it only supports tsconfig-style aliases. Now we have parsed the import stmt 119 | // So why not transform them to relative path directly? 120 | async rewriteImportStmts(code: string, id: string, stmts = this.#stmts) { 121 | let byteOffset = 0 122 | for (const stmt of stmts) { 123 | if (!stmt.n) continue 124 | if (path.isAbsolute(stmt.n) || stmt.n[0] === '.') continue 125 | if (!this.importSources.some(i => stmt.n!.includes(typeof i === 'string' ? i : i.from))) continue 126 | const resolved = await this.#rollupPluginContext!.resolve(stmt.n, id) 127 | if (resolved && resolved.id && !resolved.external) { 128 | if (resolved.id === stmt.n) continue 129 | if (CONSTANTS.RESOLVED_ID_REG.test(resolved.id)) continue 130 | if (!resolved.id.includes('node_modules')) { 131 | const next = handleRelativePath(id, resolved.id) 132 | const start = stmt.s + byteOffset 133 | const end = stmt.e + byteOffset 134 | code = code.substring(0, start) + next + code.substring(end) 135 | byteOffset += next.length - (stmt.e - stmt.s) 136 | } 137 | } 138 | } 139 | this.#stmts = [] 140 | return code 141 | } 142 | 143 | produceCSS(input = this.styleRules) { 144 | if (!input.size) return '' 145 | const { useCSSLayers } = this.stylexOptions 146 | return stylexBabelPlugin.processStylexRules([...input.values()].flat().filter(Boolean), useCSSLayers!) + '\n' + 147 | Object.values(this.globalStyles).join('\n') 148 | } 149 | 150 | destory() { 151 | this.styleRules.clear() 152 | this.root = process.cwd() 153 | this.globalStyles = {} 154 | this.#rollupPluginContext = null 155 | } 156 | 157 | // We don't recommend using NODE_ENV to determine the environment type. 158 | // Instead, we recommend using a custom environment variable. 159 | // If their are any reports about this. we can add a note to the docs. 160 | get env() { 161 | return this.#env 162 | } 163 | 164 | set env(env: Env) { 165 | this.#env = env 166 | } 167 | 168 | get importSources() { 169 | if (!this.#pluginOptions.importSources) throw error('Missing "importSources" in options') 170 | return this.#pluginOptions.importSources 171 | } 172 | 173 | get stmts() { 174 | return this.#stmts 175 | } 176 | 177 | get rollupPluginContext() { 178 | return this.#rollupPluginContext 179 | } 180 | 181 | get controlCSSByManually(): ManuallyControlCssOrder { 182 | const { manuallyControlCssOrder } = this.stylexOptions 183 | let opt = {} 184 | if (typeof manuallyControlCssOrder === 'boolean' && manuallyControlCssOrder) { 185 | opt = { ...defaultControlCSSOptions } 186 | } 187 | if (typeof manuallyControlCssOrder === 'object' && manuallyControlCssOrder) { 188 | opt = { ...defaultControlCSSOptions, ...manuallyControlCssOrder } 189 | } 190 | if ('id' in opt) { 191 | opt.id = slash(opt.id!) 192 | } 193 | return opt 194 | } 195 | 196 | get isManuallyControlCSS() { 197 | return !!this.controlCSSByManually.id 198 | } 199 | } 200 | 201 | export function createPluginContext(options: StylexPluginOptions) { 202 | return new PluginContext(options) 203 | } 204 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import type { Plugin } from 'vite' 2 | import type { Plugin as RollupPlugin } from 'rollup' 3 | import { createFilter } from '@rollup/pluginutils' 4 | import type { Rule } from '@stylexjs/babel-plugin' 5 | import type { StylexPluginOptions } from './interface' 6 | import { PluginContext, createPluginContext, parseRequest, scanImportStmt } from './context' 7 | import { transformStylex, transformStylexExtend } from './transformer' 8 | import { createForViteServer } from './plugins' 9 | 10 | type BabelConfig = StylexPluginOptions['babelConfig'] 11 | 12 | export type StylexPluginAPI = { 13 | ctx: ReturnType 14 | } 15 | 16 | const defaultBabelConfig = { 17 | plugins: [], 18 | presets: [] 19 | } 20 | 21 | const defaultOptions = { 22 | useCSSLayers: false, 23 | babelConfig: defaultBabelConfig, 24 | importSources: ['stylex', '@stylexjs/stylex'], 25 | include: /\.(mjs|js|ts|vue|jsx|tsx)(\?.*|)$/, 26 | optimizedDeps: [], 27 | manuallyControlCssOrder: false, 28 | enableStylexExtend: false 29 | } 30 | 31 | function extend(ctx: PluginContext) { 32 | const filter = createFilter(/\.[jt]sx?$/, []) 33 | return { 34 | name: 'stylex-extend', 35 | buildStart() { 36 | ctx.globalStyles = {} 37 | }, 38 | transform: { 39 | order: 'pre', 40 | async handler(code, id) { 41 | if (id.includes('/node_modules/')) return 42 | if (!filter(id)) return 43 | ctx.setupRollupPluginContext(this) 44 | const { original } = parseRequest(id) 45 | const parserOptions: string[] = [] 46 | if (!original.endsWith('.ts')) { 47 | parserOptions.push('jsx') 48 | } 49 | if (/\.tsx?$/.test(original)) { 50 | parserOptions.push('typescript') 51 | } 52 | const stmts = scanImportStmt(code, id, { plugins: parserOptions }) 53 | code = await ctx.rewriteImportStmts(code, original, stmts) 54 | const result = await transformStylexExtend(code, { 55 | filename: original, 56 | options: { parserOptions, opts: ctx.stylexExtendOptions }, 57 | env: ctx.env 58 | }) 59 | if (!result || !result.code) return 60 | if (result.metadata && 'globalStyle' in result.metadata) { 61 | ctx.globalStyles[original] = result.metadata.globalStyle as string 62 | } 63 | return { code: result.code, map: result.map } 64 | } 65 | } 66 | } 67 | } 68 | 69 | function stylex(options: StylexPluginOptions = {}) { 70 | options = { ...defaultOptions, ...options } 71 | 72 | const c = createPluginContext(options) 73 | 74 | const plugin = { 75 | name: 'stylex', 76 | api: { 77 | ctx: c 78 | }, 79 | buildStart() { 80 | if (this.meta.watchMode) { 81 | c.styleRules.clear() 82 | } 83 | }, 84 | shouldTransformCachedModule({ id, meta }) { 85 | if ('stylex' in meta && meta.stylex) { 86 | const { original } = parseRequest(id) 87 | c.styleRules.set(original, meta.stylex) 88 | } 89 | return false 90 | }, 91 | async transform(code, id) { 92 | c.setupRollupPluginContext(this) 93 | if (!c.skipResolve(code, id)) return 94 | const { original } = parseRequest(id) 95 | code = await c.rewriteImportStmts(code, original) 96 | const result = await transformStylex(code, { filename: original, env: c.env, options: c.stylexOptions }) 97 | if (!result || !result.code) return 98 | if (result.metadata && 'stylex' in result.metadata) { 99 | const rules = result.metadata.stylex as Rule[] 100 | if (rules.length) c.styleRules.set(original, rules) 101 | } 102 | return { code: result.code, map: result.map, meta: result.metadata } 103 | } 104 | } 105 | 106 | const server = createForViteServer(c, extend) 107 | server(plugin) 108 | 109 | return plugin 110 | } 111 | 112 | stylex.getPluginAPI = (plugins: readonly Plugin[]): StylexPluginAPI => plugins.find(p => p.name === 'stylex')?.api 113 | 114 | export type AdapterStylexPluginOptions = StylexPluginOptions & { 115 | filename: string 116 | } 117 | 118 | function adapter(plugin: typeof stylex, options: AdapterStylexPluginOptions): RollupPlugin { 119 | const { filename = 'stylex.css', ...rest } = options 120 | const { api, ...hooks } = plugin(rest) 121 | const { ctx } = api as StylexPluginAPI 122 | return { 123 | ...hooks, 124 | generateBundle() { 125 | this.emitFile({ fileName: filename, source: ctx.produceCSS(), type: 'asset' }) 126 | } 127 | } 128 | } 129 | 130 | export { adapter, stylex, stylex as default } 131 | 132 | export type { StylexOptions, StylexPluginOptions } from './interface' 133 | -------------------------------------------------------------------------------- /src/interface.ts: -------------------------------------------------------------------------------- 1 | import type { FilterPattern, HookHandler, Plugin, ResolvedConfig } from 'vite' 2 | import type { Options, Rule } from '@stylexjs/babel-plugin' 3 | import type { PluginItem } from '@babel/core' 4 | import type { StylexExtendBabelPluginOptions } from '@stylex-extend/babel-plugin' 5 | import { noop } from './shared' 6 | import type { Env, PluginContext } from './context' 7 | 8 | export type Mutable = { 9 | -readonly [P in keyof T]: T[P] 10 | } 11 | 12 | interface AdapterViteOptions { 13 | cssPlugins: Plugin[] 14 | config: ResolvedConfig 15 | } 16 | export interface AdapterContext { 17 | env: Env 18 | vite: AdapterViteOptions 19 | rules: PluginContext['styleRules'] 20 | useCSSLayers: boolean 21 | produceCSS: (input?: Map) => string 22 | transform: HookHandler 23 | } 24 | 25 | export interface AdapterConfig { 26 | name: string 27 | setup: (ctx: AdapterContext, plugin: Plugin) => void 28 | } 29 | 30 | export type Pretty = 31 | & { 32 | [key in keyof T]: T[key] extends (...args: any[]) => any ? (...args: Parameters) => ReturnType 33 | : T[key] & NonNullable 34 | } 35 | & NonNullable 36 | 37 | export type InternalOptions = Mutable> 38 | 39 | export type StylexOptions = Partial> 40 | 41 | export type StylexExtendOptions = Omit 42 | 43 | export interface ManuallyControlCssOrder { 44 | id?: string 45 | symbol?: string 46 | } 47 | 48 | interface InternalStylexPluginOptions extends Partial { 49 | babelConfig?: { 50 | plugins?: Array 51 | presets?: Array 52 | } 53 | useCSSLayers?: boolean 54 | include?: FilterPattern 55 | exclude?: FilterPattern 56 | /** 57 | * @experimental 58 | */ 59 | optimizedDeps?: Array 60 | /** 61 | * @experimental 62 | */ 63 | manuallyControlCssOrder?: boolean | ManuallyControlCssOrder 64 | /** 65 | * @experimental 66 | */ 67 | enableStylexExtend?: boolean | StylexExtendOptions 68 | /** 69 | * @experimental 70 | */ 71 | adapter?: () => AdapterConfig 72 | [prop: string]: unknown 73 | } 74 | 75 | export type StylexPluginOptions = Pretty 76 | 77 | export type TransformStylexOptions = Partial & { 78 | plugins: Array 79 | presets: Array 80 | dev: boolean 81 | } 82 | 83 | const transform: HookHandler = noop 84 | 85 | export type RollupPluginContext = ThisParameterType 86 | -------------------------------------------------------------------------------- /src/plugins/build.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import type { HookHandler, Plugin } from 'vite' 3 | import { PluginContext, parseRequest } from '../context' 4 | import { hijackHook } from '../shared' 5 | import { CONSTANTS, resolveId } from './server' 6 | 7 | // This implement is a relatively stable version. 8 | 9 | export function stylexBuild(plugin: Plugin, ctx: PluginContext, cssPlugins: Plugin[]) { 10 | const cssHooks = new Map<'vite:css' | 'vite:css-post' | string & ({}), HookHandler>() 11 | const entries = new Set() 12 | const self = > { 13 | enforce: 'post', 14 | resolveId(id) { 15 | const entry = resolveId(id) 16 | if (entry) { 17 | entries.add(entry) 18 | return entry 19 | } 20 | }, 21 | load(id) { 22 | const { original } = parseRequest(id) 23 | const matched = original.match(CONSTANTS.RESOLVED_ID_REG) 24 | if (matched) { 25 | return CONSTANTS.STYLEX_BUNDLE_MARK 26 | } else { 27 | if (ctx.isManuallyControlCSS && original === ctx.controlCSSByManually.id) { 28 | entries.add(id) 29 | return CONSTANTS.STYLEX_BUNDLE_MARK 30 | } 31 | } 32 | }, 33 | renderChunk: { 34 | // By declare order we can get better performance for generate styles. 35 | async handler(_, chunk) { 36 | if (!cssHooks.size) { 37 | cssPlugins.forEach((p) => { 38 | cssHooks.set(p.name, hijackHook(p, 'transform', (fn, c, args) => fn.apply(c, args), true)) 39 | }) 40 | } 41 | if (!chunk.moduleIds.some(s => entries.has(s))) { 42 | return null 43 | } 44 | for (const entry of [...entries]) { 45 | let css = ctx.produceCSS() 46 | if (!CONSTANTS.RESOLVED_ID_REG.test(entry) && ctx.isManuallyControlCSS) { 47 | const { original } = parseRequest(entry) 48 | css = ctx.isManuallyControlCSS ? fs.readFileSync(original, 'utf8').replace(ctx.controlCSSByManually.symbol!, css) : css 49 | } 50 | const res = await cssHooks.get('vite:css')?.apply(ctx.rollupPluginContext!, [css, entry]) 51 | // @ts-expect-error 52 | await cssHooks.get('vite:css-post')?.apply(ctx.rollupPluginContext!, [res?.code || '', entry]) 53 | chunk.modules[entry] = { code: null, originalLength: 0, removedExports: [], renderedExports: [], renderedLength: 0 } 54 | } 55 | return null 56 | }, 57 | order: 'pre' 58 | } 59 | } 60 | 61 | Object.assign(plugin, self) 62 | } 63 | -------------------------------------------------------------------------------- /src/plugins/index.ts: -------------------------------------------------------------------------------- 1 | import type { Plugin } from 'vite' 2 | import { PluginContext } from '../context' 3 | import { error, searchForPackageRoot, unique } from '../shared' 4 | import type { AdapterContext } from '../interface' 5 | import { stylexBuild } from './build' 6 | import { CONSTANTS, stylexServer } from './server' 7 | 8 | export function createForViteServer(ctx: PluginContext, extend: (c: PluginContext) => Plugin) { 9 | const cssPlugins: Plugin[] = [] 10 | 11 | return (plugin: Plugin) => { 12 | plugin.configResolved = function configResolved(conf) { 13 | const adapterContext: AdapterContext = { 14 | // eslint-disable-next-line prefer-spread 15 | produceCSS: (...rest: any) => ctx.produceCSS.apply(ctx, rest), 16 | transform: plugin.transform as any, 17 | vite: { cssPlugins, config: conf }, 18 | env: ctx.env, 19 | rules: ctx.styleRules, 20 | useCSSLayers: ctx.stylexOptions.useCSSLayers ?? false 21 | } 22 | 23 | ctx.env = conf.command === 'serve' ? 'server' : 'build' 24 | ctx.root = searchForPackageRoot(conf.root) 25 | const { importSources, stylexOptions } = ctx 26 | if (!stylexOptions.unstable_moduleResolution) { 27 | stylexOptions.unstable_moduleResolution = { type: 'commonJS', rootDir: ctx.root } 28 | } 29 | 30 | const optimizedDeps = unique([ 31 | ...Array.isArray(ctx.stylexOptions.optimizedDeps) ? ctx.stylexOptions.optimizedDeps : [], 32 | ...importSources.map((s: any) => typeof s === 'object' ? s.from : s), 33 | ...CONSTANTS.WELL_KNOW_LIBRARIES 34 | ]) 35 | 36 | if (ctx.env === 'server') { 37 | conf.optimizeDeps.exclude = [...optimizedDeps, ...(conf.optimizeDeps.exclude ?? [])] 38 | } 39 | if (conf.appType === 'custom') { 40 | conf.ssr.noExternal = Array.isArray(conf.ssr.noExternal) 41 | ? [...conf.ssr.noExternal, ...optimizedDeps] 42 | : conf.ssr.noExternal 43 | } 44 | cssPlugins.push(...conf.plugins.filter(p => CONSTANTS.CSS_PLUGINS.includes(p.name))) 45 | cssPlugins.sort((a, b) => a.name.length < b.name.length ? -1 : 1) 46 | const pos = conf.plugins.findIndex(p => p.name === 'stylex') 47 | if (Object.keys(ctx.stylexExtendOptions).length) { 48 | // @ts-expect-error 49 | conf.plugins.splice(pos, 0, extend(ctx)) 50 | } 51 | ctx.env === 'build' ? stylexBuild(plugin, ctx, cssPlugins) : stylexServer(plugin, ctx, cssPlugins, conf) 52 | if (typeof ctx.stylexOptions.adapter === 'function') { 53 | const adapter = ctx.stylexOptions.adapter() 54 | if (!adapter.name) { 55 | throw error('adapter missing name.') 56 | } 57 | adapter.setup(adapterContext, plugin) 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/plugins/server.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import type { HookHandler, Plugin, ResolvedConfig, Update, ViteDevServer } from 'vite' 3 | import { PluginContext, parseRequest } from '../context' 4 | import { hash, hijackHook } from '../shared' 5 | 6 | export const CONSTANTS = { 7 | WS_EVENT: 'stylex:hmr', 8 | VIRTUAL_STYLEX_ID: 'virtual:stylex.css', 9 | RESOLVED_ID_WITH_QUERY_REG: /[/\\]__stylex(_.*?)?\.css(\?.*)?$/, 10 | RESOLVED_ID_REG: /[/\\]__stylex(?:_(.*?))?\.css$/, 11 | VIRTUAL_ENTRY_ALIAS: [/^(?:virtual:)?stylex(?::(.+))?\.css(\?.*)?$/], 12 | STYLEX_BUNDLE_MARK: '@stylex__bundle__marker;', 13 | HASH_LENGTH: 6, 14 | CSS_PLUGINS: ['vite:css', 'vite:css-post'], 15 | WELL_KNOW_LIBRARIES: ['@stylexjs/open-props'] 16 | } 17 | 18 | export function resolveId(id: string) { 19 | if (id.match(CONSTANTS.RESOLVED_ID_WITH_QUERY_REG)) { 20 | return id 21 | } 22 | for (const alias of CONSTANTS.VIRTUAL_ENTRY_ALIAS) { 23 | const matched = id.match(alias) 24 | if (matched) { 25 | return '/__stylex.css' 26 | } 27 | } 28 | } 29 | 30 | let hmr = ` 31 | try { 32 | let hash = __vite__css.match(/__stylex_hash_(\\w{${CONSTANTS.HASH_LENGTH}})/) 33 | hash = hash && hash[1] 34 | if (!hash) { 35 | console.log('[vite-plugin-stylex]', 'Failed to get stylex hash, hmr might not work!') 36 | } else { 37 | await import.meta.hot.send('${CONSTANTS.WS_EVENT}', hash) 38 | } 39 | 40 | } catch (e) { 41 | console.warn('[vite-plugin0-stylex]', e) 42 | } 43 | if (!import.meta.url.include('?')) { 44 | await new Promise(r => setTimeout(r, 100)) 45 | } 46 | ` 47 | 48 | hmr = `\nif (import.meta.hot) { ${hmr} }` 49 | 50 | export function stylexServer(plugin: Plugin, ctx: PluginContext, cssPlugins: Plugin[], conf: ResolvedConfig) { 51 | const cssHooks = new Map<'vite:css' | 'vite:css-post' | string & ({}), HookHandler>() 52 | let viteDevServer: ViteDevServer | null = null 53 | let lastServerTime = Date.now() 54 | const modules = new Set() 55 | 56 | const generateCSS = () => { 57 | let css = '' 58 | const expect = modules.size 59 | for (;;) { 60 | css = ctx.produceCSS() 61 | if (expect === ctx.styleRules.size) { 62 | break 63 | } 64 | } 65 | lastServerTime = Date.now() 66 | return { css, hash: hash(css) } 67 | } 68 | 69 | const update = (ids: Set) => { 70 | if (!viteDevServer) return 71 | viteDevServer.ws.send({ 72 | type: 'update', 73 | updates: Array.from(ids).map(id => { 74 | const mod = viteDevServer?.moduleGraph.getModuleById(id) 75 | if (!mod) return null 76 | return { 77 | acceptedPath: id, 78 | path: mod.url, 79 | timestamp: lastServerTime, 80 | type: 'js-update' 81 | } as Update 82 | }).filter((s) => s !== null) as Update[] 83 | }) 84 | } 85 | 86 | let invalidateTimer: any 87 | 88 | const invalidate = (ids: Set) => { 89 | for (const id of ids) { 90 | const mod = viteDevServer?.moduleGraph.getModuleById(id) 91 | if (!mod) continue 92 | viteDevServer?.moduleGraph.invalidateModule(mod) 93 | } 94 | clearTimeout(invalidateTimer) 95 | invalidateTimer = setTimeout(() => { 96 | update(ids) 97 | }, 10) 98 | } 99 | 100 | const entries = new Set() 101 | 102 | const schedule = > { 103 | enforce: 'pre', 104 | name: 'stylex:server', 105 | apply: 'serve', 106 | resolveId(id: string) { 107 | const entry = resolveId(id) 108 | if (entry) { 109 | entries.add(entry) 110 | return entry 111 | } 112 | }, 113 | load(id: string) { 114 | const { original } = parseRequest(id) 115 | const matched = original.match(CONSTANTS.RESOLVED_ID_REG) 116 | if (matched) { 117 | const { hash, css } = generateCSS() 118 | return { 119 | code: `${css}__stylex_hash_${hash}{--:'';}`, 120 | map: { mappings: '' } 121 | } 122 | } else { 123 | if (ctx.isManuallyControlCSS && original === ctx.controlCSSByManually.id) { 124 | entries.add(id) 125 | } 126 | } 127 | }, 128 | configureServer(server) { 129 | viteDevServer = server 130 | viteDevServer.ws.on(CONSTANTS.WS_EVENT, () => { 131 | update(entries) 132 | }) 133 | } 134 | } 135 | 136 | const pos = conf.plugins.findIndex(p => p.name === 'stylex') 137 | // @ts-expect-error 138 | conf.plugins.splice(pos, 0, schedule) 139 | // 140 | hijackHook(plugin, 'transform', async (fn, c, args) => { 141 | const id = args[1] 142 | const { original } = parseRequest(id) 143 | const result = await fn.apply(c, args) 144 | if (ctx.styleRules.has(original)) { 145 | // record affect module. 146 | modules.add(original) 147 | invalidate(new Set([...entries, ...modules])) 148 | } 149 | 150 | if (ctx.isManuallyControlCSS && ctx.controlCSSByManually.id === original) { 151 | // FIXME 152 | // Find a better way pipe to vite's internal processer 153 | if (!cssHooks.size) { 154 | cssPlugins.forEach((p) => { 155 | cssHooks.set(p.name, hijackHook(p, 'transform', (fn, c, args) => fn.apply(c, args), true)) 156 | }) 157 | } 158 | const css = fs.readFileSync(ctx.controlCSSByManually.id, 'utf8').replace(ctx.controlCSSByManually.symbol!, generateCSS().css) 159 | return cssHooks.get('vite:css')?.apply(c, [css, id, args[2]]) 160 | } 161 | 162 | if (original.match(CONSTANTS.RESOLVED_ID_REG) && args[0].includes('import.meta.hot')) { 163 | const code = args[0] + hmr 164 | return { code, map: { mappings: '' } } 165 | } 166 | return result 167 | }) 168 | } 169 | -------------------------------------------------------------------------------- /src/shared.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import path from 'path' 3 | 4 | import type { HookHandler, Plugin } from 'vite' 5 | 6 | export function noop() {} 7 | 8 | export function unique(data: T[]) { 9 | return Array.from(new Set(data)) 10 | } 11 | 12 | export function error(message: string) { 13 | throw new Error(`[vite-plugin-stylex-dev]: ${message}`) 14 | } 15 | 16 | type Next = ( 17 | fn: NonNullable>, 18 | context: ThisParameterType>>, 19 | args: NonNullable>> 20 | ) => ReturnType> 21 | 22 | export function hijackHook(plugin: Plugin, name: K, next: Next): void 23 | export function hijackHook(plugin: Plugin, name: K, next: Next, executable: false): void 24 | export function hijackHook( 25 | plugin: Plugin, 26 | name: K, 27 | next: Next, 28 | executable: true 29 | ): (...args: Parameters>[2]) => ReturnType> 30 | export function hijackHook(plugin: Plugin, name: K, next: Next, executable = false) { 31 | if (!plugin[name]) throw error(`'${name}' haven't implement yet.`) 32 | const hook = plugin[name] as any 33 | if ('handler' in hook) { 34 | const fn = hook.handler 35 | hook.handler = function handler(this: any, ...args: any) { 36 | return next(fn, this, args) 37 | } 38 | if (executable) return hook.handler 39 | } else { 40 | const fn = hook 41 | plugin[name] = function handler(this: any, ...args: any) { 42 | return next(fn, this, args) 43 | } 44 | if (executable) return plugin[name] 45 | } 46 | } 47 | 48 | // MIT License 49 | // Copyright (c) Sindre Sorhus (https://sindresorhus.com) 50 | export function slash(path: string) { 51 | const isExtendedLengthPath = /^\\\\\?\\/.test(path) 52 | if (isExtendedLengthPath) return path 53 | return path.replace(/\\/g, '/') 54 | } 55 | 56 | // MIT License 57 | // Copyright (c) Vite 58 | 59 | export function tryStatSync(file: string): fs.Stats | undefined { 60 | try { 61 | // The "throwIfNoEntry" is a performance optimization for cases where the file does not exist 62 | return fs.statSync(file, { throwIfNoEntry: false }) 63 | } catch { 64 | // Ignore errors 65 | } 66 | } 67 | 68 | export function isFileReadable(filename: string): boolean { 69 | if (!tryStatSync(filename)) { 70 | return false 71 | } 72 | 73 | try { 74 | // Check if current process has read permission to the file 75 | fs.accessSync(filename, fs.constants.R_OK) 76 | 77 | return true 78 | } catch { 79 | return false 80 | } 81 | } 82 | 83 | const ROOT_FILES = ['pnpm-workspace.yaml', 'lerna.json'] 84 | 85 | // npm: https://docs.npmjs.com/cli/v7/using-npm/workspaces#installing-workspaces 86 | // yarn: https://classic.yarnpkg.com/en/docs/workspaces/#toc-how-to-use-it 87 | function hasWorkspacePackageJSON(root: string): boolean { 88 | const s = path.join(root, 'package.json') 89 | if (!isFileReadable(s)) { 90 | return false 91 | } 92 | try { 93 | const content = JSON.parse(fs.readFileSync(s, 'utf-8')) || {} 94 | return !!content.workspaces 95 | } catch { 96 | return false 97 | } 98 | } 99 | 100 | function hasRootFile(root: string): boolean { 101 | return ROOT_FILES.some((file) => fs.existsSync(path.join(root, file))) 102 | } 103 | 104 | function hasPackageJSON(root: string) { 105 | const s = path.join(root, 'package.json') 106 | return fs.existsSync(s) 107 | } 108 | 109 | /** 110 | * Search up for the nearest `package.json` 111 | */ 112 | export function searchForPackageRoot(current: string, root = current): string { 113 | if (hasPackageJSON(current)) return current 114 | 115 | const dir = path.dirname(current) 116 | // reach the fs root 117 | if (!dir || dir === current) return root 118 | 119 | return searchForPackageRoot(dir, root) 120 | } 121 | 122 | /** 123 | * Search up for the nearest workspace root 124 | */ 125 | 126 | export function searchForWorkspaceRoot( 127 | current: string, 128 | root = searchForPackageRoot(current) 129 | ): string { 130 | if (hasRootFile(current)) return current 131 | if (hasWorkspacePackageJSON(current)) return current 132 | 133 | const dir = path.dirname(current) 134 | // reach the fs root 135 | if (!dir || dir === current) return root 136 | 137 | return searchForWorkspaceRoot(dir, root) 138 | } 139 | 140 | export function hash(str: string) { 141 | let i 142 | let l 143 | let hval = 0x811C9DC5 144 | 145 | for (i = 0, l = str.length; i < l; i++) { 146 | hval ^= str.charCodeAt(i) 147 | hval += (hval << 1) + (hval << 4) + (hval << 7) + (hval << 8) + (hval << 24) 148 | } 149 | return (`00000${(hval >>> 0).toString(36)}`).slice(-6) 150 | } 151 | -------------------------------------------------------------------------------- /src/transformer.ts: -------------------------------------------------------------------------------- 1 | import { transformAsync } from '@babel/core' 2 | import stylexBabelPlugin from '@stylexjs/babel-plugin' 3 | import _stylexExtendBabelPlugin from '@stylex-extend/babel-plugin' 4 | import type { StylexExtendBabelPluginOptions } from '@stylex-extend/babel-plugin' 5 | import type { StylexPluginOptions } from './interface' 6 | import type { Env } from './context' 7 | 8 | function interopDefault(m: any) { 9 | return m.default || m 10 | } 11 | 12 | const stylexExtendBabelPlugin = interopDefault(_stylexExtendBabelPlugin) 13 | 14 | export interface TransformOptions { 15 | filename: string 16 | options: T 17 | env: Env 18 | } 19 | 20 | export interface TransformExtendOptions { 21 | opts: StylexExtendBabelPluginOptions 22 | parserOptions: any[] 23 | } 24 | 25 | export async function transformStylex(code: string, { filename, options, env }: TransformOptions) { 26 | const { babelConfig, importSources } = options 27 | return transformAsync(code, { 28 | babelrc: false, 29 | filename, 30 | presets: [], 31 | plugins: [ 32 | ...(babelConfig?.plugins || []), 33 | stylexBabelPlugin.withOptions({ 34 | ...options, 35 | dev: env === 'server', 36 | runtimeInjection: false, 37 | importSources 38 | }) 39 | ] 40 | }) 41 | } 42 | 43 | export function transformStylexExtend(code: string, { filename, options }: TransformOptions) { 44 | const { opts, parserOptions } = options 45 | return transformAsync(code, { 46 | plugins: [stylexExtendBabelPlugin.withOptions(opts)], 47 | parserOpts: { plugins: parserOptions }, 48 | babelrc: false, 49 | filename 50 | }) 51 | } 52 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "baseUrl": ".", 5 | "noImplicitThis": false, 6 | "esModuleInterop": true, 7 | "skipLibCheck": true, 8 | "target": "ES2019", 9 | "lib": ["esnext", "dom"], 10 | "resolveJsonModule": true, 11 | "module": "ESNext", 12 | "moduleResolution": "node", 13 | "outDir": "dist", 14 | "strict": true 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /vitest.config.mts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config' 2 | 3 | export default defineConfig(() => { 4 | return { 5 | test: { 6 | watch: false, 7 | // silent: true, 8 | coverage: { 9 | enabled: true, 10 | reporter: ['text', 'json', 'html'], 11 | include: ['src/**'], 12 | exclude: ['**/node_modules/**', '**/dist/**', 'src/**/interface.ts'] 13 | }, 14 | testTimeout: 8000 15 | } 16 | } 17 | }) 18 | --------------------------------------------------------------------------------