├── .github ├── FUNDING.yml └── workflows │ ├── release-pr.yml │ ├── release.yml │ └── unit-test.yml ├── .gitignore ├── .npmrc ├── .prettierrc.json ├── .stackblitzrc ├── .vscode ├── launch.json └── settings.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── docs ├── .vitepress │ ├── config.ts │ └── theme │ │ ├── index.ts │ │ └── style.css ├── features │ ├── directives.md │ ├── macros.md │ └── use-ref.md ├── index.md ├── introduction │ ├── eslint.md │ ├── getting-started.md │ ├── interop.md │ └── migration.md ├── netlify.toml ├── package.json ├── public │ └── logo.svg └── vite.config.ts ├── eslint.config.ts ├── package.json ├── packages ├── babel │ ├── package.json │ ├── src │ │ ├── index.ts │ │ ├── transform.ts │ │ └── utils.ts │ ├── test │ │ ├── interop.spec.ts │ │ └── transform.spec.ts │ └── tsdown.config.ts ├── compiler │ ├── package.json │ ├── src │ │ ├── compile.ts │ │ ├── generate.ts │ │ ├── index.ts │ │ ├── ir │ │ │ ├── component.ts │ │ │ └── index.ts │ │ ├── transform.ts │ │ ├── transforms │ │ │ ├── expression.ts │ │ │ ├── transformChildren.ts │ │ │ ├── transformElement.ts │ │ │ ├── transformTemplateRef.ts │ │ │ ├── transformText.ts │ │ │ ├── utils.ts │ │ │ ├── vBind.ts │ │ │ ├── vFor.ts │ │ │ ├── vHtml.ts │ │ │ ├── vIf.ts │ │ │ ├── vModel.ts │ │ │ ├── vOn.ts │ │ │ ├── vOnce.ts │ │ │ ├── vShow.ts │ │ │ ├── vSlot.ts │ │ │ ├── vSlots.ts │ │ │ └── vText.ts │ │ └── utils.ts │ ├── test │ │ ├── __snapshots__ │ │ │ └── compile.spec.ts.snap │ │ ├── compile.spec.ts │ │ └── transforms │ │ │ ├── __snapshots__ │ │ │ ├── expression.spec.ts.snap │ │ │ ├── transformChildren.spec.ts.snap │ │ │ ├── transformElement.spec.ts.snap │ │ │ ├── transformTemplateRef.spec.ts.snap │ │ │ ├── vBind.spec.ts.snap │ │ │ ├── vFor.spec.ts.snap │ │ │ ├── vHtml.spec.ts.snap │ │ │ ├── vIf.spec.ts.snap │ │ │ ├── vModel.spec.ts.snap │ │ │ ├── vOn.spec.ts.snap │ │ │ ├── vOnce.spec.ts.snap │ │ │ ├── vShow.spec.ts.snap │ │ │ ├── vSlot.spec.ts.snap │ │ │ ├── vSlots.spec.ts.snap │ │ │ └── vText.spec.ts.snap │ │ │ ├── _utils.ts │ │ │ ├── expression.spec.ts │ │ │ ├── transformChildren.spec.ts │ │ │ ├── transformElement.spec.ts │ │ │ ├── transformTemplateRef.spec.ts │ │ │ ├── vBind.spec.ts │ │ │ ├── vFor.spec.ts │ │ │ ├── vHtml.spec.ts │ │ │ ├── vIf.spec.ts │ │ │ ├── vModel.spec.ts │ │ │ ├── vOn.spec.ts │ │ │ ├── vOnce.spec.ts │ │ │ ├── vShow.spec.ts │ │ │ ├── vSlot.spec.ts │ │ │ ├── vSlots.spec.ts │ │ │ └── vText.spec.ts │ └── tsdown.config.ts ├── eslint │ ├── package.json │ ├── src │ │ ├── index.ts │ │ └── rules │ │ │ ├── define-style │ │ │ ├── index.ts │ │ │ └── types.ts │ │ │ ├── index.ts │ │ │ └── jsx-sort-props │ │ │ ├── index.ts │ │ │ └── types.ts │ ├── test │ │ ├── __snapshots__ │ │ │ ├── define-style.spec.ts.snap │ │ │ └── jsx-sort-props.spec.ts.snap │ │ ├── define-style.spec.ts │ │ └── jsx-sort-props.spec.ts │ └── tsdown.config.ts ├── macros │ ├── package.json │ ├── src │ │ ├── api.ts │ │ ├── astro.ts │ │ ├── core │ │ │ ├── define-component │ │ │ │ ├── await.ts │ │ │ │ ├── index.ts │ │ │ │ └── return.ts │ │ │ ├── define-expose.ts │ │ │ ├── define-model.ts │ │ │ ├── define-slots.ts │ │ │ ├── define-style.ts │ │ │ ├── helper │ │ │ │ ├── index.ts │ │ │ │ ├── use-model.ts │ │ │ │ └── with-defaults.ts │ │ │ ├── index.ts │ │ │ ├── restructure.ts │ │ │ ├── style.ts │ │ │ └── utils.ts │ │ ├── esbuild.ts │ │ ├── index.ts │ │ ├── nuxt.ts │ │ ├── options.ts │ │ ├── raw.ts │ │ ├── rolldown.ts │ │ ├── rollup.ts │ │ ├── rspack.ts │ │ ├── vite.ts │ │ ├── volar.ts │ │ ├── volar │ │ │ ├── define-component.ts │ │ │ ├── define-style.ts │ │ │ ├── global-types.ts │ │ │ ├── index.ts │ │ │ └── transform.ts │ │ └── webpack.ts │ ├── tests │ │ ├── __snapshots__ │ │ │ ├── fixtures.spec.ts.snap │ │ │ └── restructure.spec.ts.snap │ │ ├── fixtures.spec.ts │ │ ├── fixtures │ │ │ ├── define-component.tsx │ │ │ ├── define-expose.tsx │ │ │ ├── define-model.tsx │ │ │ ├── define-slots.tsx │ │ │ └── define-style.tsx │ │ └── restructure.spec.ts │ └── tsdown.config.ts └── vue-jsx-vapor │ ├── package.json │ ├── src │ ├── api.ts │ ├── astro.ts │ ├── core │ │ ├── hmr.ts │ │ ├── index.ts │ │ ├── runtime.ts │ │ ├── ssr.ts │ │ └── vue-jsx.ts │ ├── esbuild.ts │ ├── index.ts │ ├── jsx-runtime.ts │ ├── jsx-runtime │ │ ├── dom.ts │ │ └── global.d.ts │ ├── nuxt.ts │ ├── options.d.ts │ ├── raw.ts │ ├── rolldown.ts │ ├── rollup.ts │ ├── rspack.ts │ ├── unplugin.ts │ ├── vite.ts │ ├── volar.ts │ └── webpack.ts │ └── tsdown.config.ts ├── playground ├── index.html ├── main.ts ├── package.json ├── src │ ├── App.tsx │ ├── count.tsx │ ├── for.tsx │ ├── html.tsx │ ├── if.tsx │ ├── interop.tsx │ ├── model.tsx │ ├── once.tsx │ ├── show.tsx │ └── slot.tsx ├── tsconfig.json ├── vite.config.ts └── vite.js ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── tsconfig.json ├── tsdown.config.ts ├── tsm.config.ts └── vitest.config.ts /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [zhiyuanzmj] 2 | -------------------------------------------------------------------------------- /.github/workflows/release-pr.yml: -------------------------------------------------------------------------------- 1 | name: Publish Any Commit 2 | on: [pull_request] 3 | 4 | jobs: 5 | build: 6 | runs-on: ubuntu-latest 7 | 8 | steps: 9 | - name: Checkout Repo 10 | uses: actions/checkout@v4 11 | with: 12 | fetch-depth: 0 13 | 14 | - uses: pnpm/action-setup@v4.0.0 15 | 16 | - name: Setup Node.js 17 | uses: actions/setup-node@v4 18 | with: 19 | node-version: lts/* 20 | cache: pnpm 21 | 22 | - name: Install dependencies 23 | run: pnpm install 24 | 25 | - name: Build 26 | run: pnpm build 27 | 28 | - name: Publish 29 | run: pnpm dlx pkg-pr-new@0.0 publish 30 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | workflow_dispatch: {} 8 | 9 | jobs: 10 | release: 11 | runs-on: ubuntu-latest 12 | permissions: 13 | contents: write 14 | id-token: write 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | with: 19 | fetch-depth: 0 20 | 21 | - name: Install pnpm 22 | uses: pnpm/action-setup@v4.0.0 23 | 24 | - name: Set node 25 | uses: actions/setup-node@v4 26 | with: 27 | node-version: lts/* 28 | cache: pnpm 29 | registry-url: 'https://registry.npmjs.org' 30 | 31 | - run: npx changelogithub 32 | continue-on-error: true 33 | env: 34 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 35 | 36 | - name: Install Dependencies 37 | run: pnpm i 38 | 39 | - name: Build 40 | run: pnpm build 41 | 42 | - name: Publish to NPM 43 | run: pnpm -r publish --access public --no-git-checks 44 | env: 45 | NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} 46 | NPM_CONFIG_PROVENANCE: true 47 | -------------------------------------------------------------------------------- /.github/workflows/unit-test.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | pull_request: 9 | branches: 10 | - main 11 | 12 | jobs: 13 | lint: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | - name: Set node 18 | uses: actions/setup-node@v3 19 | with: 20 | node-version: 18.x 21 | 22 | - name: Setup 23 | run: npm i -g @antfu/ni 24 | 25 | - name: Install 26 | run: nci 27 | 28 | - name: Lint 29 | run: nr lint 30 | 31 | test: 32 | runs-on: ${{ matrix.os }} 33 | 34 | strategy: 35 | matrix: 36 | node: [18.x] 37 | os: 38 | - ubuntu-latest 39 | # windows-latest, TODO: hash-sum result different with linux 40 | - macos-latest 41 | 42 | fail-fast: false 43 | 44 | steps: 45 | - uses: actions/checkout@v3 46 | - name: Set node ${{ matrix.node }} 47 | uses: actions/setup-node@v3 48 | with: 49 | node-version: ${{ matrix.node }} 50 | 51 | - name: Setup 52 | run: npm i -g @antfu/ni 53 | 54 | - name: Install 55 | run: nci 56 | 57 | - name: Build 58 | run: nr build 59 | 60 | - name: Type Check 61 | run: nr typecheck 62 | 63 | - name: Test 64 | run: nr test 65 | 66 | - name: Docs Build 67 | run: nr docs:build 68 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Node template 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | *.pid.lock 15 | 16 | # Directory for instrumented libs generated by jscoverage/JSCover 17 | lib-cov 18 | 19 | # Coverage directory used by tools like istanbul 20 | coverage 21 | 22 | # nyc test coverage 23 | .nyc_output 24 | 25 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 26 | .grunt 27 | 28 | # Bower dependency directory (https://bower.io/) 29 | bower_components 30 | 31 | # node-waf configuration 32 | .lock-wscript 33 | 34 | # Compiled binary addons (https://nodejs.org/api/addons.html) 35 | build/Release 36 | 37 | # Dependency directories 38 | node_modules/ 39 | jspm_packages/ 40 | 41 | # TypeScript v1 declaration files 42 | typings/ 43 | 44 | # Optional npm cache directory 45 | .npm 46 | 47 | # Optional eslint cache 48 | .eslintcache 49 | 50 | # Optional REPL history 51 | .node_repl_history 52 | 53 | # Output of 'npm pack' 54 | *.tgz 55 | 56 | # Yarn Integrity file 57 | .yarn-integrity 58 | 59 | # dotenv environment variables file 60 | .env 61 | 62 | # parcel-bundler cache (https://parceljs.org/) 63 | .cache 64 | 65 | # next.js build output 66 | .next 67 | 68 | # nuxt.js build output 69 | .nuxt 70 | 71 | # Nuxt generate 72 | dist 73 | 74 | # vuepress build output 75 | .vuepress/dist 76 | 77 | # Serverless directories 78 | .serverless 79 | 80 | # IDE 81 | .idea 82 | 83 | # vitepress 84 | cache 85 | 86 | jsx-runtime 87 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | ignore-workspace-root-check=true 2 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true, 4 | "trailingComma": "all" 5 | } 6 | -------------------------------------------------------------------------------- /.stackblitzrc: -------------------------------------------------------------------------------- 1 | { 2 | "installDependencies": true, 3 | "startCommand": "npm run dev | npm run play" 4 | } -------------------------------------------------------------------------------- /.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 | "type": "node", 9 | "request": "launch", 10 | "name": "Vitest - Debug Current Test File", 11 | "autoAttachChildProcesses": true, 12 | "skipFiles": ["/**", "**/node_modules/**"], 13 | "program": "${workspaceRoot}/node_modules/vitest/vitest.mjs", 14 | "args": ["run", "${relativeFile}"], 15 | "smartStep": true, 16 | "console": "integratedTerminal" 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.codeActionsOnSave": { 3 | "source.fixAll.eslint": "explicit" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 zhiyuanzmj 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vue-jsx-vapor 2 | 3 | [![NPM version](https://img.shields.io/npm/v/vue-jsx-vapor?color=a1b858&label=)](https://www.npmjs.com/package/vue-jsx-vapor) 4 | 5 | Vapor Mode of Vue JSX. 6 | 7 | ## Features 8 | 9 | - ⚡️ High Performance: It has the same performance as Vue Vapor! 10 | - ⚒️ Directives: Support all build-in directives of Vue. 11 | - ✨ Macros: Support most macros of Vue, Friendly to JSX. 12 | - 🌈 Hot Module Replacement: Support functional components or defined by defineComponent. 13 | - 🦾 Type Safe: Provide Volar plugin support by install TS Macro (VSCode plugin). 14 | - ⚙️ ESLint: Provide an ESLint plugin for vue-jsx-vapor to automatically fix code. 15 | 16 | ## Installation 17 | 18 | ```bash 19 | npm i vue-jsx-vapor 20 | ``` 21 | 22 | ## Usage 23 | 24 | - [📜 Documentation](https://jsx-vapor.netlify.app/) 25 | - [🛰️ Playground](https://repl.zmjs.dev/vuejs/vue-jsx-vapor) 26 | 27 |
28 | Vite
29 | 30 | ```ts 31 | // vite.config.ts 32 | import VueJsxVapor from 'vue-jsx-vapor/vite' 33 | 34 | export default defineConfig({ 35 | plugins: [VueJsxVapor()], 36 | }) 37 | ``` 38 | 39 | Example: [`playground/`](./playground/) 40 | 41 |
42 | 43 |
44 | Rollup
45 | 46 | ```ts 47 | // rollup.config.js 48 | import VueJsxVapor from 'vue-jsx-vapor/rollup' 49 | 50 | export default { 51 | plugins: [VueJsxVapor()], 52 | } 53 | ``` 54 | 55 |
56 | 57 |
58 | Webpack
59 | 60 | ```ts 61 | // webpack.config.js 62 | module.exports = { 63 | /* ... */ 64 | plugins: [require('vue-jsx-vapor/webpack')()], 65 | } 66 | ``` 67 | 68 |
69 | 70 |
71 | Nuxt
72 | 73 | ```ts 74 | // nuxt.config.js 75 | export default defineNuxtConfig({ 76 | modules: ['vue-jsx-vapor/nuxt'], 77 | }) 78 | ``` 79 | 80 | > This module works for both Nuxt 2 and [Nuxt Vite](https://github.com/nuxt/vite) 81 | 82 |
83 | 84 |
85 | Vue CLI
86 | 87 | ```ts 88 | // vue.config.js 89 | module.exports = { 90 | configureWebpack: { 91 | plugins: [require('vue-jsx-vapor/webpack')()], 92 | }, 93 | } 94 | ``` 95 | 96 |
97 | 98 |
99 | esbuild
100 | 101 | ```ts 102 | // esbuild.config.js 103 | import { build } from 'esbuild' 104 | import VueJsxVapor from 'vue-jsx-vapor/esbuild' 105 | 106 | build({ 107 | plugins: [VueJsxVapor()], 108 | }) 109 | ``` 110 | 111 |
112 | -------------------------------------------------------------------------------- /docs/.vitepress/config.ts: -------------------------------------------------------------------------------- 1 | import { transformerTwoslash } from '@shikijs/vitepress-twoslash' 2 | import { createTwoslasher } from '@ts-macro/twoslash' 3 | import { defineConfig } from 'vitepress' 4 | import vueJsxVapor from 'vue-jsx-vapor/volar' 5 | 6 | // https://vitepress.dev/reference/site-config 7 | export default defineConfig({ 8 | title: 'Vue JSX Vapor', 9 | description: 'Vue JSX Vapor', 10 | head: [['link', { rel: 'icon', type: 'image/svg+xml', href: '/logo.svg' }]], 11 | themeConfig: { 12 | // https://vitepress.dev/reference/default-theme-config 13 | logo: '/logo.svg', 14 | nav: [ 15 | { text: 'Home', link: '/' }, 16 | { text: 'Features', link: '/features/directives' }, 17 | { text: 'Playground', link: 'https://repl.zmjs.dev/vuejs/vue-jsx-vapor' }, 18 | ], 19 | 20 | sidebar: [ 21 | { 22 | text: 'Introduction', 23 | items: [ 24 | { 25 | text: 'Getting Started', 26 | link: `/introduction/getting-started`, 27 | }, 28 | { 29 | text: 'Interop', 30 | link: `/introduction/interop`, 31 | }, 32 | { 33 | text: 'Migration', 34 | link: `/introduction/migration`, 35 | }, 36 | { 37 | text: 'ESLint', 38 | link: `/introduction/eslint`, 39 | }, 40 | ], 41 | }, 42 | { 43 | text: 'Features', 44 | items: [ 45 | { 46 | text: 'directives', 47 | link: '/features/directives', 48 | }, 49 | { 50 | text: 'macros', 51 | link: '/features/macros', 52 | }, 53 | { 54 | text: 'useRef', 55 | link: '/features/use-ref', 56 | }, 57 | ], 58 | }, 59 | ], 60 | 61 | socialLinks: [ 62 | { icon: 'discord', link: 'https://discord.gg/hMnyhpJH' }, 63 | { icon: 'github', link: 'https://github.com/vuejs/vue-jsx-vapor' }, 64 | ], 65 | }, 66 | markdown: { 67 | languages: ['js', 'ts', 'tsx'], 68 | codeTransformers: [ 69 | transformerTwoslash({ 70 | twoslasher: createTwoslasher({ 71 | compilerOptions: { 72 | jsx: 1, 73 | jsxImportSource: 'vue-jsx-vapor', 74 | customConditions: ['jsx-vapor-dev'], 75 | }, 76 | tsmCompilerOptions: { 77 | plugins: [vueJsxVapor({ macros: true })], 78 | }, 79 | }), 80 | }), 81 | ], 82 | }, 83 | }) 84 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/index.ts: -------------------------------------------------------------------------------- 1 | import TwoslashFloatingVue from '@shikijs/vitepress-twoslash/client' 2 | import DefaultTheme from 'vitepress/theme' 3 | // https://vitepress.dev/guide/custom-theme 4 | import { h } from 'vue' 5 | import type { Theme } from 'vitepress' 6 | import './style.css' 7 | import '@shikijs/vitepress-twoslash/style.css' 8 | 9 | export default { 10 | extends: DefaultTheme, 11 | Layout: () => { 12 | return h(DefaultTheme.Layout, null, { 13 | // https://vitepress.dev/guide/extending-default-theme#layout-slots 14 | }) 15 | }, 16 | enhanceApp({ app }) { 17 | app.use(TwoslashFloatingVue) 18 | }, 19 | } satisfies Theme 20 | -------------------------------------------------------------------------------- /docs/features/directives.md: -------------------------------------------------------------------------------- 1 | # Directives 2 | 3 | Vue built-in directives for JSX. 4 | 5 | | Directive | Vue | Volar | 6 | | :---------------------------: | :----------------: | :----------------: | 7 | | `v-if`, `v-else-if`, `v-else` | :white_check_mark: | :white_check_mark: | 8 | | `v-slot`, `v-slots` | :white_check_mark: | :white_check_mark: | 9 | | `v-for` | :white_check_mark: | :white_check_mark: | 10 | | `v-model` | :white_check_mark: | :white_check_mark: | 11 | | `v-html`, `v-text` | :white_check_mark: | / | 12 | | `v-once` | :white_check_mark: | / | 13 | 14 | ## Dynamic Arguments 15 | 16 | It is also possible to use a variable in a directive argument. 17 | Because JSX doesn't support `[]` keyword, use `$` instead. 18 | 19 | ## Modifiers 20 | 21 | Modifiers are special postfixes denoted by a `_`, which indicate that a directive should be bound in some special way. 22 | Because JSX doesn't support `.` keyword, we use `_` instead. 23 | 24 | ```tsx 25 |
26 | 27 |
28 | ``` 29 | 30 | ## `v-if`, `v-else-if`, `v-else` 31 | 32 | ```tsx twoslash 33 | export default ({ foo = 0 }) => { 34 | // ---cut-start--- 35 | // prettier-ignore 36 | // ---cut-end--- 37 | return ( 38 | <> 39 |
{foo}
40 | 41 |
{foo}
42 | // ^? 43 | 44 |
{foo}
45 | // ^? 46 | 47 | ) 48 | } 49 | ``` 50 | 51 | ## `v-for` 52 | 53 | ```tsx twoslash 54 | export default () => ( 55 |
56 | {item} 57 |
58 | ) 59 | ``` 60 | 61 | ## `v-slot`, `v-slots` 62 | 63 | ::: code-group 64 | 65 | ```tsx [v-slot] twoslash 66 | const Comp = () => { 67 | defineSlots<{ 68 | default: () => any 69 | slot: (scope: { bar: number }) => any 70 | slots: (scope: { baz: boolean }) => any 71 | }>() 72 | return
73 | } 74 | 75 | // ---cut-start--- 76 | // prettier-ignore 77 | // ---cut-end--- 78 | export default () => ( 79 | 80 | default slot 81 | 85 | 86 | ) 87 | ``` 88 | 89 | ```tsx [v-slots] twoslash 90 | const Comp = () => { 91 | defineSlots<{ 92 | default: () => any 93 | slot: (scope: { bar: number }) => any 94 | slots: (scope: { baz: boolean }) => any 95 | }>() 96 | return
97 | } 98 | 99 | export default () => ( 100 | <>default slot, 103 | slot: ({ bar }) => <>{bar}, 104 | }} 105 | /> 106 | ) 107 | ``` 108 | 109 | ::: 110 | 111 | ## `v-model` 112 | 113 | ```tsx twoslash 114 | import { ref } from 'vue' 115 | 116 | const Comp = () => { 117 | const model = defineModel('model') 118 | const models = defineModel('models') 119 | return
120 | } 121 | 122 | export default () => { 123 | const foo = ref('') 124 | const name = ref('model') 125 | return ( 126 | 131 | ) 132 | } 133 | ``` 134 | -------------------------------------------------------------------------------- /docs/features/use-ref.md: -------------------------------------------------------------------------------- 1 | # useRef 2 | 3 | Automatically infer type for `useRef`. It's an alias of `shallowRef`. 4 | 5 | ## Basic Usage 6 | 7 | ```tsx twoslash 8 | import { defineComponent } from 'vue' 9 | import { useRef } from 'vue-jsx-vapor' 10 | // or 11 | // import { shallowRef as useRef } from 'vue' 12 | 13 | export const Comp = defineComponent({ 14 | setup() { 15 | return { foo: 1 } 16 | }, 17 | }) 18 | 19 | export default defineComponent(() => { 20 | const comp = useRef() 21 | comp.value?.foo 22 | // ^? 23 | 24 | return ( 25 | <> 26 | 27 | 28 | ) 29 | }) 30 | ``` 31 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | # https://vitepress.dev/reference/default-theme-home-page 3 | layout: home 4 | 5 | hero: 6 | name: "Vue JSX Vapor" 7 | text: "Type-safe, Improve DX, High Performance" 8 | tagline: Supports Vapor Mode, all build-in directives and most macros of Vue. 9 | image: 10 | src: /logo.svg 11 | alt: Vue JSX Vapor 12 | actions: 13 | - theme: brand 14 | text: Get Started 15 | link: /introduction/getting-started 16 | - theme: alt 17 | text: Features 18 | link: /features/directives 19 | 20 | features: 21 | - icon: ⚒️ ️ 22 | title: Directives 23 | details: Support all build-in directives of Vue. 24 | - icon: ✨ 25 | title: Macros 26 | details: Support most macros of Vue, Friendly to JSX. 27 | - icon: 🦾 28 | title: Type Safe 29 | details: Provide Volar plugin support by install TS Macro (VSCode plugin). 30 | - icon: ⚡️ 31 | title: High Performance 32 | details: It has the same performance as Vue Vapor! 33 | - icon: 🌈 34 | title: Hot Module Replacement 35 | details: Support functional components or defined by defineComponent. 36 | - icon: ⚙️ 37 | title: ESLint 38 | details: Provide an ESLint plugin for vue-jsx-vapor to automatically fix code. 39 | --- 40 | 41 | -------------------------------------------------------------------------------- /docs/introduction/eslint.md: -------------------------------------------------------------------------------- 1 | # ESLint 2 | 3 | This is an ESLint plugin for `vue-jsx-vapor` to automatically fix code. 4 | 5 | ## Install 6 | 7 | ```sh 8 | pnpm add @vue-jsx-vapor/eslint 9 | ``` 10 | 11 | ## Setup 12 | 13 | ```ts 14 | // eslint.config.ts 15 | import vueJsxVapor from '@vue-jsx-vapor/eslint' 16 | 17 | export default [ 18 | vueJsxVapor() 19 | ] 20 | ``` 21 | 22 | ## define-style 23 | 24 | Use `prettier` to format styles in the defineStyle macro. 25 | 26 | ```ts twoslash 27 | import vueJsxVapor from '@vue-jsx-vapor/eslint' 28 | 29 | export default [ 30 | vueJsxVapor({ 31 | rules: { 32 | 'vue-jsx-vapor/define-style': ['error', { tabWidth: 2 }] 33 | } 34 | }) 35 | ] 36 | ``` 37 | 38 | ## jsx-sort-props 39 | 40 | This is a modified version of [@stylistic/jsx/jsx-sort-props](https://eslint.style/rules/jsx/jsx-sort-props), supporting custom reservedFirst and reservedLast options. 41 | 42 | ```ts twoslash 43 | import vueJsxVapor from '@vue-jsx-vapor/eslint' 44 | 45 | export default [ 46 | vueJsxVapor({ 47 | rules: { 48 | 'vue-jsx-vapor/jsx-sort-props': ['error', { 49 | reservedFirst: ['v-if', 'v-for'], 50 | reservedLast: ['v-slot'], 51 | }] 52 | } 53 | }) 54 | ] 55 | ``` 56 | 57 | ### `reservedFirst` 58 | 59 | Defaults to `['v-if', 'v-else-if', 'v-else', 'v-for', 'key', 'ref', 'v-model']` 60 | 61 | If given as an array, the array's values will override the default list of reserved props. 62 | These props will respect the order specified in the array: 63 | 64 | ```jsx 65 | // before 66 | const Before = 67 | 68 | // after 69 | const After = 70 | ``` 71 | 72 | ### `reservedLast` 73 | 74 | Defaults to `['v-slot', 'v-slots', 'v-text', 'v-html']` 75 | 76 | This can be an array option. These props must be listed after all other props. 77 | These will respect the order specified in the array: 78 | 79 | ```jsx 80 | // before 81 | const Before = 82 | 83 | // after 84 | const After = 85 | ``` 86 | -------------------------------------------------------------------------------- /docs/introduction/getting-started.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | `vue-jsx-vapor` is a Vapor Mode of `vue-jsx`. It supports all directives and most macros of Vue. 4 | 5 | We assume you are already familiar with the basic usages of Vue before you continue. 6 | 7 | ## Requirements 8 | 9 | - Vue `>= v3.6`. 10 | - VSCode extension [TS Macro](https://marketplace.visualstudio.com/items?itemName=zhiyuanzmj.vscode-ts-macro) and install `@ts-macro/tsc` instead of `tsc` to typecheck. 11 | ```json 12 | // package.json 13 | { 14 | "scripts": { 15 | "typecheck": "tsmc --noEmit" 16 | // ... 17 | } 18 | } 19 | ``` 20 | 21 | ## Install 22 | 23 | ```bash [pnpm] 24 | # plugin 25 | pnpm add vue-jsx-vapor 26 | 27 | # runtime 28 | pnpm add https://pkg.pr.new/vue@280bc48 29 | ``` 30 | 31 | The Vue Vapor runtime is not release, so we use [pkg.pr.new](https://github.com/stackblitz-labs/pkg.pr.new) to install. 32 | 33 | ## Setup 34 | 35 | ::: code-group 36 | 37 | ```ts [vite.config.ts] 38 | import { defineConfig } from 'vite' 39 | import vueJsxVapor from 'vue-jsx-vapor/vite' 40 | 41 | export default defineConfig({ 42 | plugins: [ 43 | vueJsxVapor({ 44 | macros: true, 45 | }), 46 | ], 47 | }) 48 | ``` 49 | 50 | ::: 51 | 52 | ## Typescript 53 | 54 | Due to `vue-jsx-vapor`'s support for all Vue directives and most Vue macros, the [ts-macro](https://github.com/ts-macro/ts-macro) VSCode plugin is required to enable TypeScript support when using the `vue-jsx-vapor/volar` plugin. 55 | 56 | After installing the `ts-macro` VSCode plugin, it automatically loads `vue-jsx-vapor/volar` by analyzing vite.config.ts and shared vueJsxVapor options, eliminating the need to configure tsm.config.ts. 57 | 58 | ::: code-group 59 | 60 | ```ts [tsm.config.ts] 61 | import vueJsxVapor from 'vue-jsx-vapor/volar' 62 | 63 | export default { 64 | plugins: [ 65 | vueJsxVapor({ 66 | macros: true, 67 | }), 68 | ], 69 | } 70 | ``` 71 | 72 | ::: 73 | 74 | ## Templates 75 | 76 | - [vitesse-jsx-vapor](https://github.com/zhiyuanzmj/vitesse-jsx-vapor) 77 | -------------------------------------------------------------------------------- /docs/introduction/interop.md: -------------------------------------------------------------------------------- 1 | # Interop 2 | 3 | `vue-jsx-vapor` support virtual-dom and vapor-dom co-usage. After set interop to `true`, vue-jsx-vapor will be only convert in `defineVaporComponent`'s JSX. 4 | 5 | ## Vapor in Virtual DOM 6 | 7 | [Playground](https://repl.zmjs.dev/vuejs/vapor-in-virtual-dom) 8 | 9 | ::: code-group 10 | 11 | ```ts [vite.config.ts] 12 | import vueJsx from '@vitejs/plugin-vue-jsx' 13 | import { defineConfig } from 'vite' 14 | import vueJsxVapor from 'vue-jsx-vapor/vite' 15 | 16 | export default defineConfig({ 17 | plugins: [ 18 | vueJsxVapor({ 19 | macros: true, 20 | interop: true, 21 | }), 22 | vueJsx(), 23 | ], 24 | }) 25 | ``` 26 | 27 | ```ts [main.ts] 28 | import { createApp, vaporInteropPlugin } from 'vue' 29 | import App from './App.tsx' 30 | createApp(App).use(vaporInteropPlugin).mount('#app') 31 | ``` 32 | 33 | ```tsx [App.tsx] twoslash 34 | import { 35 | computed, 36 | defineComponent, 37 | defineVaporComponent, 38 | ref, 39 | } from 'vue' 40 | import { useRef } from 'vue-jsx-vapor' 41 | 42 | const Comp = defineVaporComponent(({ count = 0 }) => { 43 | defineExpose({ 44 | double: computed(() => count * 2), 45 | }) 46 | return x 2 = 47 | }) 48 | 49 | export default defineComponent(() => { 50 | const count = ref(1) 51 | const compRef = useRef() 52 | return () => ( 53 | <> 54 | 55 | 56 | {compRef.value?.double} 57 | 58 | ) 59 | }) 60 | ``` 61 | 62 | ::: 63 | 64 | ## Virtual DOM in Vapor 65 | 66 | [Playground](https://repl.zmjs.dev/vuejs/virtual-dom-in-vapor) 67 | 68 | ::: code-group 69 | 70 | ```ts [vite.config.ts] 71 | import vueJsx from '@vitejs/plugin-vue-jsx' 72 | import { defineConfig } from 'vite' 73 | import vueJsxVapor from 'vue-jsx-vapor/vite' 74 | 75 | export default defineConfig({ 76 | plugins: [ 77 | vueJsxVapor({ 78 | macros: true, 79 | interop: true, 80 | }), 81 | vueJsx(), 82 | ], 83 | }) 84 | ``` 85 | 86 | ```ts [main.ts] 87 | import { createVaporApp, vaporInteropPlugin } from 'vue' 88 | import App from './App.tsx' 89 | createVaporApp(App).use(vaporInteropPlugin).mount('#app') 90 | ``` 91 | 92 | ```tsx [App.tsx] twoslash 93 | import { 94 | computed, 95 | defineComponent, 96 | defineVaporComponent, 97 | ref, 98 | } from 'vue' 99 | import { useRef } from 'vue-jsx-vapor' 100 | 101 | const Comp = defineVaporComponent(({ count = 0 }) => { 102 | defineExpose({ 103 | double: computed(() => count * 2), 104 | }) 105 | return x 2 = 106 | }) 107 | 108 | export default defineComponent(() => { 109 | const count = ref(1) 110 | const compRef = useRef() 111 | return () => ( 112 | <> 113 | 114 | 115 | {compRef.value?.double} 116 | 117 | ) 118 | }) 119 | ``` 120 | 121 | ::: 122 | -------------------------------------------------------------------------------- /docs/introduction/migration.md: -------------------------------------------------------------------------------- 1 | # Migration 2 | 3 | ## Migration from `vue-jsx` 4 | 5 | 1. Don't support hyphenated prop name and hyphenated component name. 6 | 2. `v-model` don't support Array Expression, use `v-model:$name$_trim={foo}` instead. 7 | 3. Don't support `v-models` directive. 8 | 4. Destructing props: 9 | 10 | > [!CAUTION] 11 | > ❌ The destructuring of props in a functional component will cause loss of reactivity. 12 | 13 | ```tsx 14 | function Comp({ foo }) { 15 | return
{foo}
16 | } 17 | 18 | export default () => { 19 | const foo = ref('foo') 20 | return 21 | } 22 | ``` 23 | 24 | #### Two Solutions 25 | 26 | 1. ✅ Pass a ref variable as prop: 27 | 28 | ```tsx 29 | function Comp({ foo }) { 30 | return
{foo.value}
31 | } 32 | 33 | export default () => { 34 | const foo = ref('foo') 35 | return 36 | } 37 | ``` 38 | 39 | 2. ✅ Set the macros option to true, then use the `defineComponent` macro for wrapping. 40 | 41 | - Setup 42 | 43 | ```ts {7} 44 | // vite.config.ts 45 | import vueJsxVapor from 'vue-jsx-vapor/vite' 46 | 47 | export default defineConfig({ 48 | plugins: [ 49 | vueJsxVapor({ 50 | macros: true, 51 | }), 52 | ] 53 | }) 54 | 55 | ``` 56 | 57 | - Usage 58 | 59 | ```tsx 60 | import { defineComponent, ref } from 'vue' 61 | 62 | const Comp = defineComponent(({ foo }) => { 63 | return <>{foo} 64 | }) 65 | // Will be convert to: 66 | const Comp = defineComponent((_props) => { 67 | return <>{_props.foo} 68 | }, { props: ['foo'] }) 69 | 70 | export default () => { 71 | const foo = ref('foo') 72 | return 73 | } 74 | ``` 75 | 76 | ## Migration from `react` 77 | 78 | Suggest using the ESLint plugin [eslint-plugin-react2vue](https://github.com/zhiyuanzmj/eslint-plugin-react2vue) for converting the React Hooks API to the Vue Composition API and Macros. 79 | 80 | ### useState 81 | 82 | ```ts 83 | // before 84 | const [foo, setFoo] = useState(count) 85 | console.log([foo, setFoo(1), setFoo]) 86 | 87 | // after 88 | const foo = ref(0) 89 | console.log([foo.value, foo.value = 1, val => foo.value = val]) 90 | ``` 91 | 92 | ### useEffect 93 | 94 | Use `watchEffect` instead of `useEffect`. 95 | 96 | ```ts 97 | // before 98 | useEffect(() => { 99 | console.log(foo) 100 | }, [foo]) 101 | 102 | // after 103 | watchEffect(() => { 104 | console.log(foo) 105 | }) 106 | ``` 107 | 108 | ### useMemo 109 | 110 | Use `computed` instead of `useMemo`. 111 | 112 | ```ts 113 | // before 114 | const double = useMemo(() => foo * 2, [foo]) 115 | console.log({ double }, [double]) 116 | 117 | // after 118 | const double = computed(() => foo * 2) 119 | console.log({ double: double.value }, [double.value]) 120 | ``` 121 | 122 | ### defineComponent 123 | 124 | Use `defineComponent` macro to support destructuring props. 125 | 126 | ```tsx 127 | // before 128 | const Comp = ({ count = 1 }) => { 129 | return
{count}
130 | } 131 | 132 | // after 133 | const Comp = defineComponent(({ count = 1 }) => { 134 | return
{count}
135 | }) 136 | ``` 137 | 138 | ### defineSlots 139 | 140 | Use `defineSlots` instead of `children` prop. 141 | 142 | ```tsx 143 | // before 144 | const Comp = ({ children }) => { 145 | return children 146 | } 147 | 148 | // after 149 | const Comp = ({ children }) => { 150 | const slots = defineSlots() 151 | return 152 | } 153 | ``` 154 | 155 | ### useCallback 156 | 157 | Remove useCallback. 158 | 159 | ```ts 160 | // before 161 | const callback = useCallback(() => { 162 | console.log(foo) 163 | }, [foo]) 164 | 165 | // after 166 | const callback = () => { 167 | console.log(foo) 168 | } 169 | ``` 170 | 171 | ### forwardRef 172 | 173 | Remove forwardRef. 174 | 175 | ```tsx 176 | // before 177 | const Comp = forwardRef(({ count }, ref) => { 178 | return
{count}
179 | }) 180 | 181 | // after 182 | const Comp = ({ count }) => { 183 | return
{count}
184 | } 185 | ``` 186 | 187 | ### useImperativeHandle 188 | 189 | Use `defineExpose` instead of `useImperativeHandle`. 190 | 191 | ```tsx 192 | // before 193 | const Comp = ({ count, ref }) => { 194 | useImperativeHandle(ref, () => { 195 | return { 196 | count: count * 2 197 | } 198 | }, [count]) 199 | return
{count}
200 | } 201 | 202 | // after 203 | const Comp = ({ count }) => { 204 | defineExpose(computed(() => { 205 | return { 206 | count: count * 2 207 | } 208 | })) 209 | return
{count}
210 | } 211 | ``` 212 | -------------------------------------------------------------------------------- /docs/netlify.toml: -------------------------------------------------------------------------------- 1 | [[rewrites]] 2 | from = "/*" 3 | to = "/:path.html" 4 | status = 200 -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docs", 3 | "private": true, 4 | "type": "module", 5 | "scripts": { 6 | "dev": "vitepress dev", 7 | "build": "vitepress build", 8 | "preview": "vitepress preview" 9 | }, 10 | "dependencies": { 11 | "@vue-jsx-vapor/eslint": "workspace:*", 12 | "vue": "catalog:", 13 | "vue-jsx-vapor": "workspace:*" 14 | }, 15 | "devDependencies": { 16 | "@shikijs/vitepress-twoslash": "^3.6.0", 17 | "@ts-macro/twoslash": "^0.0.4", 18 | "vitepress": "2.0.0-alpha.5" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /docs/public/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | 3 | export default defineConfig({ 4 | resolve: { 5 | conditions: ['jsx-vapor-dev'], 6 | }, 7 | optimizeDeps: { 8 | exclude: ['vitepress'], 9 | }, 10 | }) 11 | -------------------------------------------------------------------------------- /eslint.config.ts: -------------------------------------------------------------------------------- 1 | import { sxzz } from '@sxzz/eslint-config' 2 | import vueJsxVapor from './packages/eslint/src/index' 3 | 4 | export default [ 5 | ...(await sxzz() 6 | .removeRules( 7 | 'unicorn/filename-case', 8 | 'import/no-default-export', 9 | 'unicorn/no-new-array', 10 | 'unicorn/prefer-dom-node-remove', 11 | ) 12 | .append([ 13 | { 14 | name: 'docs', 15 | files: ['**/*.md/*.tsx'], 16 | rules: { 17 | 'no-var': 'off', 18 | 'no-mutable-exports': 'off', 19 | 'no-duplicate-imports': 'off', 20 | 'import/first': 'off', 21 | 'unused-imports/no-unused-vars': 'off', 22 | }, 23 | }, 24 | ])), 25 | vueJsxVapor({ 26 | ignores: ['**/docs/**'], 27 | }), 28 | ] 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.4.4", 3 | "packageManager": "pnpm@10.11.1", 4 | "description": "Vue JSX Vapor", 5 | "type": "module", 6 | "keywords": [ 7 | "unplugin", 8 | "vite", 9 | "webpack", 10 | "rollup", 11 | "transform", 12 | "vue-jsx", 13 | "volar", 14 | "vapor", 15 | "babel", 16 | "compiler" 17 | ], 18 | "license": "MIT", 19 | "homepage": "https://github.com/vuejs/vue-jsx-vapor#readme", 20 | "bugs": { 21 | "url": "https://github.com/vuejs/vue-jsx-vapor/issues" 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "git+https://github.com/vuejs/vue-jsx-vapor.git" 26 | }, 27 | "scripts": { 28 | "dev": "pnpm run --filter=\"./packages/*\" --parallel dev", 29 | "build": "pnpm run --filter=\"./packages/*\" build", 30 | "typecheck": "tsmc --noEmit", 31 | "lint": "eslint .", 32 | "play": "npm -C playground run dev", 33 | "test": "vitest", 34 | "release": "bumpp -r --all -x 'pnpm run changelog'", 35 | "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s", 36 | "docs:dev": "pnpm run -C ./docs dev", 37 | "docs:preview": "pnpm run -C ./docs preview", 38 | "docs:build": "pnpm run -C ./docs build" 39 | }, 40 | "devDependencies": { 41 | "@sxzz/eslint-config": "^7.0.2", 42 | "@ts-macro/tsc": "catalog:", 43 | "@types/node": "^22.15.30", 44 | "bumpp": "^10.1.1", 45 | "conventional-changelog-cli": "^5.0.0", 46 | "eslint": "^9.28.0", 47 | "tsdown": "^0.12.7", 48 | "typescript": "^5.8.3", 49 | "unplugin-raw": "^0.5.0", 50 | "vite": "catalog:", 51 | "vitest": "catalog:", 52 | "vue-jsx-vapor": "workspace:*" 53 | }, 54 | "pnpm": { 55 | "overrides": { 56 | "estree-walker": "2.0.2", 57 | "ts-macro": "catalog:" 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /packages/babel/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vue-jsx-vapor/babel", 3 | "version": "2.4.4", 4 | "description": "Vue JSX Vapor Babel Plugin", 5 | "type": "module", 6 | "keywords": [ 7 | "vue", 8 | "jsx", 9 | "vapor", 10 | "babel" 11 | ], 12 | "license": "MIT", 13 | "homepage": "https://github.com/vuejs/vue-jsx-vapor#readme", 14 | "bugs": { 15 | "url": "https://github.com/vuejs/vue-jsx-vapor/issues" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/vuejs/vue-jsx-vapor.git" 20 | }, 21 | "files": [ 22 | "dist" 23 | ], 24 | "main": "dist/index.cjs", 25 | "module": "dist/index.js", 26 | "types": "dist/index.d.ts", 27 | "exports": { 28 | ".": { 29 | "types": "./dist/index.d.ts", 30 | "jsx-vapor-dev": "./src/index.ts", 31 | "require": "./dist/index.cjs", 32 | "import": "./dist/index.js" 33 | }, 34 | "./*": "./*" 35 | }, 36 | "scripts": { 37 | "build": "tsdown", 38 | "dev": "DEV=true tsdown", 39 | "release": "bumpp && npm publish", 40 | "test": "vitest" 41 | }, 42 | "dependencies": { 43 | "@babel/core": "catalog:", 44 | "@babel/parser": "catalog:", 45 | "@babel/plugin-syntax-jsx": "catalog:", 46 | "@babel/traverse": "catalog:", 47 | "@babel/types": "catalog:", 48 | "@vue-jsx-vapor/compiler": "workspace:*", 49 | "source-map-js": "^1.2.1" 50 | }, 51 | "devDependencies": { 52 | "@types/babel__core": "catalog:", 53 | "@types/babel__generator": "^7.27.0", 54 | "@types/babel__template": "^7.4.4", 55 | "@types/babel__traverse": "^7.20.7" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /packages/babel/src/index.ts: -------------------------------------------------------------------------------- 1 | import { parse } from '@babel/parser' 2 | // @ts-ignore 3 | import _SyntaxJSX from '@babel/plugin-syntax-jsx' 4 | import { transformJSX } from './transform' 5 | import { isConditionalExpression, isJSXElement } from './utils' 6 | import type { Visitor } from '@babel/core' 7 | import type { VisitNodeFunction } from '@babel/traverse' 8 | import type { JSXElement, JSXFragment, Node } from '@babel/types' 9 | import type { CompilerOptions } from '@vue-jsx-vapor/compiler' 10 | 11 | const SyntaxJSX = _SyntaxJSX.default || _SyntaxJSX 12 | 13 | export type Options = { 14 | filename: string 15 | importSet: Set 16 | delegateEventSet: Set 17 | preambleMap: Map 18 | preambleIndex: number 19 | roots: { 20 | node: JSXElement | JSXFragment 21 | source: string 22 | inVaporComponent: boolean 23 | }[] 24 | opts: { 25 | interop?: boolean 26 | compile?: CompilerOptions 27 | } 28 | } 29 | 30 | export default (): { 31 | name: string 32 | inherits: any 33 | visitor: Visitor 34 | } => { 35 | return { 36 | name: 'Vue JSX Vapor', 37 | inherits: SyntaxJSX, 38 | visitor: { 39 | JSXElement: transformJSX, 40 | JSXFragment: transformJSX, 41 | Program: { 42 | enter: (path, state) => { 43 | state.importSet = new Set() 44 | state.delegateEventSet = new Set() 45 | state.preambleMap = new Map() 46 | state.preambleIndex = 0 47 | state.roots = [] 48 | const collectRoot: VisitNodeFunction< 49 | Node, 50 | JSXElement | JSXFragment 51 | > = (path) => { 52 | if ( 53 | !isJSXElement(path.parent) && 54 | !isConditionalExpression(path.parentPath) 55 | ) { 56 | state.roots.push({ 57 | node: path.node, 58 | source: path.getSource(), 59 | inVaporComponent: !state.opts.interop 60 | ? true 61 | : ( 62 | path.findParent( 63 | ({ node }) => 64 | node.type === 'CallExpression' && 65 | node.callee.type === 'Identifier' && 66 | ['defineVaporComponent', 'defineComponent'].includes( 67 | node.callee.name, 68 | ), 69 | ) as any 70 | )?.node.callee.name === 'defineVaporComponent', 71 | }) 72 | } 73 | } 74 | path.traverse({ 75 | JSXElement: collectRoot, 76 | JSXFragment: collectRoot, 77 | }) 78 | }, 79 | exit: (path, state) => { 80 | const { delegateEventSet, importSet, preambleMap } = state 81 | 82 | const statements: string[] = [] 83 | if (delegateEventSet.size) { 84 | statements.unshift( 85 | `_delegateEvents(${Array.from(delegateEventSet).join(', ')});`, 86 | ) 87 | } 88 | 89 | if (preambleMap.size) { 90 | let preambleResult = '' 91 | for (const [value, key] of preambleMap) { 92 | preambleResult += `const ${key} = ${value}\n` 93 | } 94 | statements.unshift(preambleResult) 95 | } 96 | 97 | const helpers = ['setNodes', 'createNodes'].filter((helper) => { 98 | const result = importSet.has(helper) 99 | result && importSet.delete(helper) 100 | return result 101 | }) 102 | if (helpers.length) { 103 | statements.unshift( 104 | `import { ${helpers.map((i) => `${i} as _${i}`).join(', ')} } from 'vue-jsx-vapor/runtime';\n`, 105 | ) 106 | } 107 | 108 | if (importSet.size) { 109 | const importResult = Array.from(importSet) 110 | .map((i) => `${i} as _${i}`) 111 | .join(', ') 112 | statements.unshift(`import { ${importResult} } from 'vue';\n`) 113 | } 114 | 115 | path.node.body.unshift( 116 | ...parse(statements.join('\n'), { 117 | sourceType: 'module', 118 | plugins: ['typescript'], 119 | }).program.body, 120 | ) 121 | }, 122 | }, 123 | }, 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /packages/babel/src/transform.ts: -------------------------------------------------------------------------------- 1 | import { parse } from '@babel/parser' 2 | import _traverse, { type VisitNodeFunction } from '@babel/traverse' 3 | import { compile } from '@vue-jsx-vapor/compiler' 4 | import { SourceMapConsumer } from 'source-map-js' 5 | import { isJSXElement } from './utils' 6 | import type { Options } from '.' 7 | import type { JSXElement, JSXFragment } from '@babel/types' 8 | 9 | // @ts-ignore 10 | const traverse = (_traverse.default || _traverse) as typeof _traverse 11 | 12 | export const transformJSX: VisitNodeFunction< 13 | Options, 14 | JSXElement | JSXFragment 15 | > = (path, state) => { 16 | if (isJSXElement(path.parent)) return 17 | 18 | const root = state.roots.shift() 19 | if (!root || !root.inVaporComponent) return 20 | 21 | const isTS = state.filename?.endsWith('tsx') 22 | let { code, helpers, preamble, map } = compile(root.node, { 23 | isTS, 24 | filename: state.filename, 25 | sourceMap: true, 26 | source: ' '.repeat(root.node.start || 0) + root.source, 27 | ...state.opts.compile, 28 | }) 29 | 30 | helpers.forEach((helper) => state.importSet.add(helper)) 31 | 32 | preamble = preamble.replaceAll( 33 | /(?<=const )t(?=(\d))/g, 34 | `_t${state.preambleIndex}`, 35 | ) 36 | code = code.replaceAll(/(?<== )t(?=\d)/g, `_t${state.preambleIndex}`) 37 | state.preambleIndex++ 38 | 39 | for (const [, key, value] of preamble.matchAll( 40 | /const (_t\d+) = (_template\(.*\))/g, 41 | )) { 42 | const result = state.preambleMap.get(value) 43 | if (result) { 44 | code = code.replaceAll(key, result) 45 | } else { 46 | state.preambleMap.set(value, key) 47 | } 48 | } 49 | 50 | for (const [, events] of preamble.matchAll(/_delegateEvents\((.*)\)/g)) { 51 | events.split(', ').forEach((event) => state.delegateEventSet.add(event)) 52 | } 53 | 54 | const ast = parse(`(() => {${code}})()`, { 55 | sourceFilename: state.filename, 56 | plugins: isTS ? ['jsx', 'typescript'] : ['jsx'], 57 | }) 58 | 59 | if (map) { 60 | const consumer = new SourceMapConsumer(map) 61 | traverse(ast, { 62 | Identifier({ node: id }) { 63 | if (!id.loc) return 64 | const originalLoc = consumer.originalPositionFor(id.loc.start) 65 | if (originalLoc.column) { 66 | id.loc.start.line = originalLoc.line 67 | id.loc.start.column = originalLoc.column 68 | id.loc.end.line = originalLoc.line 69 | id.loc.end.column = originalLoc.column + id.name.length 70 | } 71 | }, 72 | }) 73 | } 74 | 75 | path.replaceWith(ast.program.body[0]) 76 | } 77 | -------------------------------------------------------------------------------- /packages/babel/src/utils.ts: -------------------------------------------------------------------------------- 1 | import type { NodePath } from '@babel/traverse' 2 | import type { JSXElement, JSXFragment, Node } from '@babel/types' 3 | 4 | export function isConditionalExpression(path: NodePath | null): boolean { 5 | return !!( 6 | path && 7 | (path?.type === 'LogicalExpression' || 8 | path.type === 'ConditionalExpression') && 9 | (path.parent.type === 'JSXExpressionContainer' || 10 | (path.parent.type === 'ConditionalExpression' && 11 | isConditionalExpression(path.parentPath))) 12 | ) 13 | } 14 | 15 | export function isJSXElement( 16 | node?: Node | null, 17 | ): node is JSXElement | JSXFragment { 18 | return !!node && (node.type === 'JSXElement' || node.type === 'JSXFragment') 19 | } 20 | -------------------------------------------------------------------------------- /packages/babel/test/interop.spec.ts: -------------------------------------------------------------------------------- 1 | import { transformSync } from '@babel/core' 2 | import { describe, expect, test } from 'vitest' 3 | import jsx from '../src/index' 4 | 5 | describe('transform', () => { 6 | test('transform multiple components', () => { 7 | const { code } = transformSync( 8 | `const A = defineComponent(() => { 9 | defineVaporComponent(() =>
) 10 | return () =>
11 | }) 12 | const B = defineVaporComponent(() => { 13 | const C = defineComponent(() =>
) 14 | const D =
15 | return
16 | })`, 17 | { 18 | filename: 'test.tsx', 19 | plugins: [[jsx, { interop: true }]], 20 | }, 21 | )! 22 | expect(code).toMatchInlineSnapshot(` 23 | "import { template as _template } from 'vue'; 24 | const _t00 = _template("
", true); 25 | const A = defineComponent(() => { 26 | defineVaporComponent(() => (() => { 27 | const n0 = _t00(); 28 | return n0; 29 | })()); 30 | return () =>
; 31 | }); 32 | const B = defineVaporComponent(() => { 33 | const C = defineComponent(() =>
); 34 | const D = (() => { 35 | const n0 = _t00(); 36 | return n0; 37 | })(); 38 | return (() => { 39 | const n0 = _t00(); 40 | return n0; 41 | })(); 42 | });" 43 | `) 44 | }) 45 | }) 46 | -------------------------------------------------------------------------------- /packages/babel/test/transform.spec.ts: -------------------------------------------------------------------------------- 1 | import { transformSync } from '@babel/core' 2 | import { describe, expect, test } from 'vitest' 3 | import jsx from '../src/index' 4 | 5 | describe('transform', () => { 6 | test('transform multiple components', () => { 7 | const { code } = transformSync( 8 | `const a =
{Hello}
9 | const b = <>{foo?
Hello
:
World
}`, 10 | { 11 | filename: 'test.tsx', 12 | plugins: [[jsx]], 13 | }, 14 | )! 15 | expect(code).toMatchInlineSnapshot(` 16 | "import { child as _child, delegateEvents as _delegateEvents, template as _template, createIf as _createIf } from 'vue'; 17 | import { setNodes as _setNodes } from 'vue-jsx-vapor/runtime'; 18 | const _t00 = _template("
", true); 19 | const _t10 = _template("
Hello
"); 20 | const _t11 = _template("
World
"); 21 | _delegateEvents("click", "dblclick"); 22 | const a = (() => { 23 | const n0 = _t00(); 24 | const x0 = _child(n0); 25 | _setNodes(x0, () => Hello); 26 | n0.$evtclick = e => onClick(e); 27 | return n0; 28 | })(); 29 | const b = (() => { 30 | const n0 = _createIf(() => foo, () => { 31 | const n2 = _t10(); 32 | n2.$evtclick = e => onClick(e); 33 | return n2; 34 | }, () => { 35 | const n4 = _t11(); 36 | n4.$evtdblclick = e => onDblclick(e); 37 | return n4; 38 | }); 39 | return n0; 40 | })();" 41 | `) 42 | }) 43 | }) 44 | -------------------------------------------------------------------------------- /packages/babel/tsdown.config.ts: -------------------------------------------------------------------------------- 1 | import { config } from '../../tsdown.config.js' 2 | 3 | export default config() 4 | -------------------------------------------------------------------------------- /packages/compiler/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vue-jsx-vapor/compiler", 3 | "version": "2.4.4", 4 | "description": "Vue JSX Vapor Compiler", 5 | "type": "module", 6 | "keywords": [ 7 | "vue", 8 | "jsx", 9 | "vapor", 10 | "compiler" 11 | ], 12 | "license": "MIT", 13 | "homepage": "https://github.com/vuejs/vue-jsx-vapor#readme", 14 | "bugs": { 15 | "url": "https://github.com/vuejs/vue-jsx-vapor/issues" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/vuejs/vue-jsx-vapor.git" 20 | }, 21 | "files": [ 22 | "dist" 23 | ], 24 | "main": "dist/index.cjs", 25 | "module": "dist/index.js", 26 | "types": "dist/index.d.ts", 27 | "exports": { 28 | ".": { 29 | "types": "./dist/index.d.ts", 30 | "jsx-vapor-dev": "./src/index.ts", 31 | "require": "./dist/index.cjs", 32 | "import": "./dist/index.js" 33 | }, 34 | "./*": "./*" 35 | }, 36 | "scripts": { 37 | "build": "tsdown", 38 | "dev": "DEV=true tsdown", 39 | "release": "bumpp && npm publish", 40 | "test": "vitest" 41 | }, 42 | "dependencies": { 43 | "@babel/parser": "catalog:", 44 | "@babel/types": "catalog:", 45 | "@vue/compiler-dom": "catalog:", 46 | "@vue/compiler-vapor": "catalog:", 47 | "@vue/shared": "catalog:" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/compiler/src/compile.ts: -------------------------------------------------------------------------------- 1 | import { parse } from '@babel/parser' 2 | import { 3 | generate, 4 | type VaporCodegenResult as BaseVaporCodegenResult, 5 | } from '@vue/compiler-vapor' 6 | import { extend, isString } from '@vue/shared' 7 | import { customGenOperation } from './generate' 8 | 9 | import { 10 | IRNodeTypes, 11 | type HackOptions, 12 | type RootIRNode, 13 | type RootNode, 14 | } from './ir' 15 | import { 16 | transform, 17 | type DirectiveTransform, 18 | type NodeTransform, 19 | } from './transform' 20 | import { transformChildren } from './transforms/transformChildren' 21 | import { transformElement } from './transforms/transformElement' 22 | import { transformTemplateRef } from './transforms/transformTemplateRef' 23 | import { transformText } from './transforms/transformText' 24 | import { transformVBind } from './transforms/vBind' 25 | import { transformVFor } from './transforms/vFor' 26 | import { transformVHtml } from './transforms/vHtml' 27 | import { transformVIf } from './transforms/vIf' 28 | import { transformVModel } from './transforms/vModel' 29 | import { transformVOn } from './transforms/vOn' 30 | import { transformVOnce } from './transforms/vOnce' 31 | import { transformVShow } from './transforms/vShow' 32 | import { transformVSlot } from './transforms/vSlot' 33 | import { transformVSlots } from './transforms/vSlots' 34 | import { transformVText } from './transforms/vText' 35 | import type { ExpressionStatement, JSXElement, JSXFragment } from '@babel/types' 36 | import type { CompilerOptions as BaseCompilerOptions } from '@vue/compiler-dom' 37 | 38 | export { generate } 39 | 40 | export interface VaporCodegenResult 41 | extends Omit { 42 | ast: RootIRNode 43 | customHelpers: Set 44 | } 45 | 46 | // code/AST -> IR (transform) -> JS (generate) 47 | export function compile( 48 | source: JSXElement | JSXFragment | string, 49 | options: CompilerOptions = {}, 50 | ): VaporCodegenResult { 51 | const resolvedOptions = extend({}, options, { 52 | inline: true, 53 | prefixIdentifiers: false, 54 | expressionPlugins: options.expressionPlugins || ['jsx'], 55 | }) 56 | if (!resolvedOptions.source && isString(source)) { 57 | resolvedOptions.source = source 58 | } 59 | if (resolvedOptions.isTS) { 60 | const { expressionPlugins } = resolvedOptions 61 | if (!expressionPlugins.includes('typescript')) { 62 | resolvedOptions.expressionPlugins = [ 63 | ...(expressionPlugins || []), 64 | 'typescript', 65 | ] 66 | } 67 | } 68 | const root = isString(source) 69 | ? ( 70 | parse(source, { 71 | sourceType: 'module', 72 | plugins: resolvedOptions.expressionPlugins, 73 | }).program.body[0] as ExpressionStatement 74 | ).expression 75 | : source 76 | const children = 77 | root.type === 'JSXFragment' 78 | ? root.children 79 | : root.type === 'JSXElement' 80 | ? [root] 81 | : [] 82 | const ast: RootNode = { 83 | type: IRNodeTypes.ROOT, 84 | children, 85 | source: resolvedOptions.source || '', 86 | } 87 | const [nodeTransforms, directiveTransforms] = getBaseTransformPreset() 88 | 89 | const ir = transform( 90 | ast, 91 | extend({}, resolvedOptions, { 92 | nodeTransforms: [ 93 | ...nodeTransforms, 94 | ...(resolvedOptions.nodeTransforms || []), // user transforms 95 | ], 96 | directiveTransforms: extend( 97 | {}, 98 | directiveTransforms, 99 | resolvedOptions.directiveTransforms || {}, // user transforms 100 | ), 101 | }), 102 | ) 103 | 104 | return generate(ir as any, { 105 | ...resolvedOptions, 106 | customGenOperation, 107 | }) as unknown as VaporCodegenResult 108 | } 109 | 110 | export type CompilerOptions = HackOptions & { 111 | source?: string 112 | } 113 | export type TransformPreset = [ 114 | NodeTransform[], 115 | Record, 116 | ] 117 | 118 | export function getBaseTransformPreset(): TransformPreset { 119 | return [ 120 | [ 121 | transformVOnce, 122 | transformVIf, 123 | transformVFor, 124 | transformTemplateRef, 125 | transformText, 126 | transformElement, 127 | transformVSlot, 128 | transformChildren, 129 | ], 130 | { 131 | bind: transformVBind, 132 | on: transformVOn, 133 | model: transformVModel, 134 | show: transformVShow, 135 | html: transformVHtml, 136 | text: transformVText, 137 | slots: transformVSlots, 138 | }, 139 | ] 140 | } 141 | -------------------------------------------------------------------------------- /packages/compiler/src/generate.ts: -------------------------------------------------------------------------------- 1 | import { 2 | genCall, 3 | genExpression, 4 | NEWLINE, 5 | type CodeFragment, 6 | type CodegenContext, 7 | } from '@vue/compiler-vapor' 8 | import { 9 | IRNodeTypes, 10 | type CreateNodesIRNode, 11 | type OperationNode, 12 | type SetNodesIRNode, 13 | } from './ir' 14 | import type { SimpleExpressionNode } from '@vue/compiler-dom' 15 | 16 | export const customGenOperation = ( 17 | oper: OperationNode, 18 | context: CodegenContext, 19 | ) => { 20 | if (oper.type === IRNodeTypes.CREATE_NODES) { 21 | return genCreateNodes(oper, context) 22 | } else if (oper.type === IRNodeTypes.SET_NODES) { 23 | return genSetNodes(oper, context) 24 | } 25 | } 26 | 27 | export function genSetNodes( 28 | oper: SetNodesIRNode, 29 | context: CodegenContext, 30 | ): CodeFragment[] { 31 | const { helper } = context 32 | const { element, values, generated } = oper 33 | return [ 34 | NEWLINE, 35 | ...genCall( 36 | helper('setNodes'), 37 | `${generated ? 'x' : 'n'}${element}`, 38 | combineValues(values, context), 39 | ), 40 | ] 41 | } 42 | 43 | export function genCreateNodes( 44 | oper: CreateNodesIRNode, 45 | context: CodegenContext, 46 | ): CodeFragment[] { 47 | const { helper } = context 48 | const { id, values } = oper 49 | return [ 50 | NEWLINE, 51 | `const n${id} = `, 52 | ...genCall(helper('createNodes'), values && combineValues(values, context)), 53 | ] 54 | } 55 | 56 | function combineValues( 57 | values: SimpleExpressionNode[], 58 | context: CodegenContext, 59 | ): CodeFragment[] { 60 | return values.flatMap((value, i) => { 61 | const exp = genExpression(value, context) 62 | if (i > 0) { 63 | exp.unshift(', ') 64 | } 65 | return exp 66 | }) 67 | } 68 | -------------------------------------------------------------------------------- /packages/compiler/src/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | compile, 3 | generate, 4 | type CompilerOptions, 5 | type TransformPreset, 6 | } from './compile' 7 | export * from './transform' 8 | 9 | export * from './ir' 10 | 11 | export { transformText } from './transforms/transformText' 12 | export { transformElement } from './transforms/transformElement' 13 | export { transformChildren } from './transforms/transformChildren' 14 | export { transformTemplateRef } from './transforms/transformTemplateRef' 15 | export { transformVBind } from './transforms/vBind' 16 | export { transformVOn } from './transforms/vOn' 17 | export { transformVSlot } from './transforms/vSlot' 18 | export { transformVSlots } from './transforms/vSlots' 19 | export { transformVModel } from './transforms/vModel' 20 | export { transformVShow } from './transforms/vShow' 21 | export { transformVHtml } from './transforms/vHtml' 22 | export { transformVFor } from './transforms/vFor' 23 | export { transformVIf } from './transforms/vIf' 24 | export { transformVOnce } from './transforms/vOnce' 25 | export { transformVText } from './transforms/vText' 26 | -------------------------------------------------------------------------------- /packages/compiler/src/ir/component.ts: -------------------------------------------------------------------------------- 1 | import type { DirectiveTransformResult } from '../transform' 2 | import type { BlockIRNode } from './index' 3 | import type { SimpleExpressionNode } from '@vue/compiler-dom' 4 | import type { IRFor } from '@vue/compiler-vapor' 5 | 6 | // props 7 | export interface IRProp extends Omit { 8 | values: SimpleExpressionNode[] 9 | } 10 | 11 | export enum IRDynamicPropsKind { 12 | EXPRESSION, // v-bind="value" 13 | ATTRIBUTE, // v-bind:[foo]="value" 14 | } 15 | 16 | export type IRPropsStatic = IRProp[] 17 | export interface IRPropsDynamicExpression { 18 | kind: IRDynamicPropsKind.EXPRESSION 19 | value: SimpleExpressionNode 20 | handler?: boolean 21 | } 22 | export interface IRPropsDynamicAttribute extends IRProp { 23 | kind: IRDynamicPropsKind.ATTRIBUTE 24 | } 25 | export type IRProps = 26 | | IRPropsStatic 27 | | IRPropsDynamicAttribute 28 | | IRPropsDynamicExpression 29 | 30 | // slots 31 | export interface SlotBlockIRNode extends BlockIRNode { 32 | props?: SimpleExpressionNode 33 | } 34 | 35 | export enum IRSlotType { 36 | STATIC, 37 | DYNAMIC, 38 | LOOP, 39 | CONDITIONAL, 40 | EXPRESSION, 41 | } 42 | export interface IRSlotsStatic { 43 | slotType: IRSlotType.STATIC 44 | slots: Record 45 | } 46 | export interface IRSlotDynamicBasic { 47 | slotType: IRSlotType.DYNAMIC 48 | name: SimpleExpressionNode 49 | fn: SlotBlockIRNode 50 | } 51 | export interface IRSlotDynamicLoop { 52 | slotType: IRSlotType.LOOP 53 | name: SimpleExpressionNode 54 | fn: SlotBlockIRNode 55 | loop: IRFor 56 | } 57 | export interface IRSlotDynamicConditional { 58 | slotType: IRSlotType.CONDITIONAL 59 | condition: SimpleExpressionNode 60 | positive: IRSlotDynamicBasic 61 | negative?: IRSlotDynamicBasic | IRSlotDynamicConditional 62 | } 63 | export interface IRSlotsExpression { 64 | slotType: IRSlotType.EXPRESSION 65 | slots: SimpleExpressionNode 66 | } 67 | 68 | export type IRSlotDynamic = 69 | | IRSlotDynamicBasic 70 | | IRSlotDynamicLoop 71 | | IRSlotDynamicConditional 72 | export type IRSlots = IRSlotsStatic | IRSlotDynamic | IRSlotsExpression 73 | -------------------------------------------------------------------------------- /packages/compiler/src/transforms/expression.ts: -------------------------------------------------------------------------------- 1 | import { isStaticNode } from '@vue/compiler-dom' 2 | import { DynamicFlag, IRNodeTypes, type OperationNode } from '../ir' 3 | import { transformNode, type TransformContext } from '../transform' 4 | import { resolveExpression } from '../utils' 5 | import { createBranch } from './utils' 6 | import type { ConditionalExpression, LogicalExpression } from '@babel/types' 7 | 8 | export function processConditionalExpression( 9 | node: ConditionalExpression, 10 | context: TransformContext, 11 | ) { 12 | const { test, consequent, alternate } = node 13 | 14 | context.dynamic.flags |= DynamicFlag.NON_TEMPLATE | DynamicFlag.INSERT 15 | const id = context.reference() 16 | const condition = resolveExpression(test, context) 17 | const [branch, onExit] = createBranch(consequent, context) 18 | const operation: OperationNode = { 19 | type: IRNodeTypes.IF, 20 | id, 21 | condition, 22 | positive: branch, 23 | once: context.inVOnce || isStaticNode(test), 24 | } 25 | 26 | return [ 27 | () => { 28 | onExit() 29 | context.dynamic.operation = operation 30 | }, 31 | () => { 32 | const [branch, onExit] = createBranch(alternate, context) 33 | operation.negative = branch 34 | transformNode(context) 35 | onExit() 36 | }, 37 | ] 38 | } 39 | 40 | export function processLogicalExpression( 41 | node: LogicalExpression, 42 | context: TransformContext, 43 | ) { 44 | const { left, right, operator } = node 45 | 46 | context.dynamic.flags |= DynamicFlag.NON_TEMPLATE 47 | context.dynamic.flags |= DynamicFlag.INSERT 48 | 49 | const id = context.reference() 50 | const condition = resolveExpression(left, context) 51 | const [branch, onExit] = createBranch( 52 | operator === '&&' ? right : left, 53 | context, 54 | ) 55 | const operation: OperationNode = { 56 | type: IRNodeTypes.IF, 57 | id, 58 | condition, 59 | positive: branch, 60 | once: context.inVOnce, 61 | } 62 | 63 | return [ 64 | () => { 65 | onExit() 66 | context.dynamic.operation = operation 67 | }, 68 | () => { 69 | const [branch, onExit] = createBranch( 70 | operator === '&&' ? left : right, 71 | context, 72 | ) 73 | operation.negative = branch 74 | transformNode(context) 75 | onExit() 76 | }, 77 | ] 78 | } 79 | -------------------------------------------------------------------------------- /packages/compiler/src/transforms/transformChildren.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DynamicFlag, 3 | IRNodeTypes, 4 | isBlockOperation, 5 | type IRDynamicInfo, 6 | } from '../ir/index' 7 | import { 8 | transformNode, 9 | type NodeTransform, 10 | type TransformContext, 11 | } from '../transform' 12 | import { isJSXComponent, isTemplate } from '../utils' 13 | import type { Node } from '@babel/types' 14 | 15 | export const transformChildren: NodeTransform = (node, context) => { 16 | const isFragment = 17 | node.type === IRNodeTypes.ROOT || 18 | node.type === 'JSXFragment' || 19 | (node.type === 'JSXElement' && (isTemplate(node) || isJSXComponent(node))) 20 | 21 | if (node.type !== 'JSXElement' && !isFragment) return 22 | 23 | for (const [i, child] of node.children.entries()) { 24 | const childContext = context.create(child, i) 25 | transformNode(childContext) 26 | 27 | const childDynamic = childContext.dynamic 28 | 29 | if (isFragment) { 30 | childContext.reference() 31 | childContext.registerTemplate() 32 | 33 | if ( 34 | !(childDynamic.flags & DynamicFlag.NON_TEMPLATE) || 35 | childDynamic.flags & DynamicFlag.INSERT 36 | ) { 37 | context.block.returns.push(childDynamic.id!) 38 | } 39 | } else { 40 | context.childrenTemplate.push(childContext.template) 41 | } 42 | 43 | if ( 44 | childDynamic.hasDynamicChild || 45 | childDynamic.id !== undefined || 46 | childDynamic.flags & DynamicFlag.NON_TEMPLATE || 47 | childDynamic.flags & DynamicFlag.INSERT 48 | ) { 49 | context.dynamic.hasDynamicChild = true 50 | } 51 | 52 | context.dynamic.children[i] = childContext.dynamic 53 | } 54 | 55 | if (!isFragment) { 56 | processDynamicChildren(context as TransformContext) 57 | } 58 | } 59 | 60 | function processDynamicChildren(context: TransformContext) { 61 | let prevDynamics: IRDynamicInfo[] = [] 62 | let hasStaticTemplate = false 63 | const children = context.dynamic.children 64 | 65 | for (const [index, child] of children.entries()) { 66 | if (child.flags & DynamicFlag.INSERT) { 67 | prevDynamics.push(child) 68 | } 69 | 70 | if (!(child.flags & DynamicFlag.NON_TEMPLATE)) { 71 | if (prevDynamics.length) { 72 | if (hasStaticTemplate) { 73 | context.childrenTemplate[index - prevDynamics.length] = `` 74 | prevDynamics[0].flags -= DynamicFlag.NON_TEMPLATE 75 | const anchor = (prevDynamics[0].anchor = context.increaseId()) 76 | registerInsertion(prevDynamics, context, anchor) 77 | } else { 78 | registerInsertion(prevDynamics, context, -1 /* prepend */) 79 | } 80 | prevDynamics = [] 81 | } 82 | hasStaticTemplate = true 83 | } 84 | } 85 | 86 | if (prevDynamics.length) { 87 | registerInsertion(prevDynamics, context) 88 | } 89 | } 90 | 91 | function registerInsertion( 92 | dynamics: IRDynamicInfo[], 93 | context: TransformContext, 94 | anchor?: number, 95 | ) { 96 | for (const child of dynamics) { 97 | if (child.template != null) { 98 | // template node due to invalid nesting - generate actual insertion 99 | context.registerOperation({ 100 | type: IRNodeTypes.INSERT_NODE, 101 | elements: dynamics.map((child) => child.id!), 102 | parent: context.reference(), 103 | anchor, 104 | }) 105 | } else if (child.operation && isBlockOperation(child.operation)) { 106 | // block types 107 | child.operation.parent = context.reference() 108 | child.operation.anchor = anchor 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /packages/compiler/src/transforms/transformTemplateRef.ts: -------------------------------------------------------------------------------- 1 | import { IRNodeTypes } from '../ir' 2 | import { findProp, isConstantExpression, resolveExpression } from '../utils' 3 | import type { NodeTransform } from '../transform' 4 | 5 | export const transformTemplateRef: NodeTransform = (node, context) => { 6 | if (node.type !== 'JSXElement') return 7 | 8 | const dir = findProp(node, 'ref') 9 | if (!dir?.value) return 10 | 11 | context.ir.hasTemplateRef = true 12 | 13 | const value = resolveExpression(dir.value, context) 14 | 15 | return () => { 16 | const id = context.reference() 17 | const effect = !isConstantExpression(value) 18 | effect && 19 | context.registerOperation({ 20 | type: IRNodeTypes.DECLARE_OLD_REF, 21 | id, 22 | }) 23 | context.registerEffect([value], { 24 | type: IRNodeTypes.SET_TEMPLATE_REF, 25 | element: id, 26 | value, 27 | refFor: !!context.inVFor, 28 | effect, 29 | }) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/compiler/src/transforms/utils.ts: -------------------------------------------------------------------------------- 1 | import { 2 | jsxClosingFragment, 3 | jsxExpressionContainer, 4 | jsxFragment, 5 | jsxOpeningFragment, 6 | type Expression, 7 | type JSXElement, 8 | type JSXFragment, 9 | type Node, 10 | } from '@babel/types' 11 | import { createSimpleExpression } from '@vue/compiler-dom' 12 | import { 13 | DynamicFlag, 14 | IRNodeTypes, 15 | type BlockIRNode, 16 | type IRDynamicInfo, 17 | type RootNode, 18 | } from '../ir/index' 19 | import { isTemplate } from '../utils' 20 | import type { TransformContext } from '../transform' 21 | 22 | export function newDynamic(): IRDynamicInfo { 23 | return { 24 | flags: DynamicFlag.REFERENCED, 25 | children: [], 26 | } 27 | } 28 | 29 | export function newBlock(node: BlockIRNode['node']): BlockIRNode { 30 | return { 31 | type: 1 satisfies IRNodeTypes.BLOCK, 32 | node, 33 | dynamic: newDynamic(), 34 | effect: [], 35 | operation: [], 36 | returns: [], 37 | expressions: [], 38 | tempId: 0, 39 | } 40 | } 41 | 42 | export function createBranch( 43 | node: Parameters[0], 44 | context: TransformContext, 45 | isVFor?: boolean, 46 | ): [BlockIRNode, () => void] { 47 | context.node = node = wrapFragment(node) 48 | const branch: BlockIRNode = newBlock(node) 49 | const exitBlock = context.enterBlock(branch, isVFor) 50 | context.reference() 51 | return [branch, exitBlock] 52 | } 53 | 54 | export function wrapFragment(node: JSXElement | JSXFragment | Expression) { 55 | if (node.type === 'JSXFragment' || isTemplate(node)) { 56 | return node 57 | } 58 | 59 | return jsxFragment(jsxOpeningFragment(), jsxClosingFragment(), [ 60 | node.type === 'JSXElement' ? node : jsxExpressionContainer(node), 61 | ]) 62 | } 63 | 64 | export const EMPTY_EXPRESSION = createSimpleExpression('', true) 65 | 66 | export const isFragmentNode = ( 67 | node: Node | RootNode, 68 | ): node is JSXElement | JSXFragment | RootNode => 69 | node.type === IRNodeTypes.ROOT || 70 | node.type === 'JSXFragment' || 71 | (node.type === 'JSXElement' && !!isTemplate(node)) 72 | -------------------------------------------------------------------------------- /packages/compiler/src/transforms/vBind.ts: -------------------------------------------------------------------------------- 1 | import { camelize, extend } from '@vue/shared' 2 | import { resolveExpression, resolveSimpleExpression } from '../utils' 3 | import type { DirectiveTransform } from '../transform' 4 | import { isReservedProp } from './transformElement' 5 | 6 | export const transformVBind: DirectiveTransform = (dir, node, context) => { 7 | const { name, value, loc } = dir 8 | if (!loc || name.type === 'JSXNamespacedName') return 9 | 10 | const [nameString, ...modifiers] = name.name.split('_') 11 | 12 | const exp = resolveExpression(value, context) 13 | let arg = resolveSimpleExpression(nameString, true, dir.name.loc) 14 | 15 | if (arg.isStatic && isReservedProp(arg.content)) return 16 | 17 | let camel = false 18 | if (modifiers.includes('camel')) { 19 | if (arg.isStatic) { 20 | arg = extend({}, arg, { content: camelize(arg.content) }) 21 | } else { 22 | camel = true 23 | } 24 | } 25 | 26 | return { 27 | key: arg, 28 | value: exp, 29 | loc, 30 | runtimeCamelize: camel, 31 | modifier: modifiers.includes('prop') 32 | ? '.' 33 | : modifiers.includes('attr') 34 | ? '^' 35 | : undefined, 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/compiler/src/transforms/vFor.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createCompilerError, 3 | ErrorCodes, 4 | isConstantNode, 5 | type SimpleExpressionNode, 6 | } from '@vue/compiler-dom' 7 | import { DynamicFlag, IRNodeTypes } from '../ir' 8 | import { 9 | createStructuralDirectiveTransform, 10 | type NodeTransform, 11 | type TransformContext, 12 | } from '../transform' 13 | import { 14 | findProp, 15 | isJSXComponent, 16 | propToExpression, 17 | resolveExpression, 18 | resolveExpressionWithFn, 19 | resolveLocation, 20 | } from '../utils' 21 | import { createBranch } from './utils' 22 | import type { JSXAttribute, JSXElement } from '@babel/types' 23 | 24 | export const transformVFor: NodeTransform = createStructuralDirectiveTransform( 25 | 'for', 26 | processFor, 27 | ) 28 | 29 | export function processFor( 30 | node: JSXElement, 31 | dir: JSXAttribute, 32 | context: TransformContext, 33 | ) { 34 | const { value, index, key, source } = getForParseResult(dir, context) 35 | if (!source) { 36 | context.options.onError( 37 | createCompilerError( 38 | ErrorCodes.X_V_FOR_MALFORMED_EXPRESSION, 39 | resolveLocation(dir.loc, context), 40 | ), 41 | ) 42 | return 43 | } 44 | 45 | const keyProp = findProp(node, 'key') 46 | const keyProperty = keyProp && propToExpression(keyProp, context) 47 | const isComponent = isJSXComponent(node) 48 | const id = context.reference() 49 | context.dynamic.flags |= DynamicFlag.NON_TEMPLATE | DynamicFlag.INSERT 50 | const [render, exitBlock] = createBranch(node, context, true) 51 | return (): void => { 52 | exitBlock() 53 | 54 | const { parent } = context 55 | 56 | // if v-for is the only child of a parent element, it can go the fast path 57 | // when the entire list is emptied 58 | const isOnlyChild = 59 | parent && 60 | parent.block.node !== parent.node && 61 | parent.node.children.length === 1 62 | 63 | context.dynamic.operation = { 64 | type: IRNodeTypes.FOR, 65 | id, 66 | source, 67 | value, 68 | key, 69 | index, 70 | keyProp: keyProperty, 71 | render, 72 | once: context.inVOnce || !!(source.ast && isConstantNode(source.ast, {})), 73 | component: isComponent, 74 | onlyChild: !!isOnlyChild, 75 | } 76 | } 77 | } 78 | 79 | export function getForParseResult( 80 | dir: JSXAttribute, 81 | context: TransformContext, 82 | ) { 83 | let value: SimpleExpressionNode | undefined, 84 | index: SimpleExpressionNode | undefined, 85 | key: SimpleExpressionNode | undefined, 86 | source: SimpleExpressionNode | undefined 87 | if (dir.value) { 88 | if ( 89 | dir.value.type === 'JSXExpressionContainer' && 90 | dir.value.expression.type === 'BinaryExpression' 91 | ) { 92 | if (dir.value.expression.left.type === 'SequenceExpression') { 93 | const expressions = dir.value.expression.left.expressions 94 | value = 95 | expressions[0] && resolveExpressionWithFn(expressions[0], context) 96 | key = expressions[1] && resolveExpression(expressions[1], context) 97 | index = expressions[2] && resolveExpression(expressions[2], context) 98 | } else { 99 | value = resolveExpressionWithFn(dir.value.expression.left, context) 100 | } 101 | source = resolveExpression(dir.value.expression.right, context) 102 | } 103 | } else { 104 | context.options.onError( 105 | createCompilerError( 106 | ErrorCodes.X_V_FOR_NO_EXPRESSION, 107 | resolveLocation(dir.loc, context), 108 | ), 109 | ) 110 | } 111 | 112 | return { 113 | value, 114 | index, 115 | key, 116 | source, 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /packages/compiler/src/transforms/vHtml.ts: -------------------------------------------------------------------------------- 1 | import { createDOMCompilerError, DOMErrorCodes } from '@vue/compiler-dom' 2 | import { IRNodeTypes } from '../ir' 3 | import { resolveExpression, resolveLocation } from '../utils' 4 | import type { DirectiveTransform } from '../transform' 5 | import { EMPTY_EXPRESSION } from './utils' 6 | 7 | export const transformVHtml: DirectiveTransform = (dir, node, context) => { 8 | let exp 9 | const loc = resolveLocation(dir.loc, context) 10 | if (!dir.value) { 11 | context.options.onError( 12 | createDOMCompilerError(DOMErrorCodes.X_V_HTML_NO_EXPRESSION, loc), 13 | ) 14 | exp = EMPTY_EXPRESSION 15 | } else { 16 | exp = resolveExpression(dir.value, context) 17 | } 18 | if (node.children.length) { 19 | context.options.onError( 20 | createDOMCompilerError(DOMErrorCodes.X_V_HTML_WITH_CHILDREN, loc), 21 | ) 22 | context.childrenTemplate.length = 0 23 | } 24 | 25 | context.registerEffect([exp], { 26 | type: IRNodeTypes.SET_HTML, 27 | element: context.reference(), 28 | value: exp, 29 | }) 30 | } 31 | -------------------------------------------------------------------------------- /packages/compiler/src/transforms/vModel.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createCompilerError, 3 | createDOMCompilerError, 4 | createSimpleExpression, 5 | DOMErrorCodes, 6 | ErrorCodes, 7 | isMemberExpression, 8 | } from '@vue/compiler-dom' 9 | import { IRNodeTypes, type DirectiveIRNode } from '../ir' 10 | import { 11 | findProp, 12 | getText, 13 | isJSXComponent, 14 | resolveDirective, 15 | resolveLocation, 16 | } from '../utils' 17 | import type { DirectiveTransform } from '../transform' 18 | import type { JSXElement } from '@babel/types' 19 | 20 | export const transformVModel: DirectiveTransform = (_dir, node, context) => { 21 | const dir = resolveDirective(_dir, context) 22 | const { exp, arg } = dir 23 | if (!exp) { 24 | context.options.onError( 25 | createCompilerError(ErrorCodes.X_V_MODEL_NO_EXPRESSION, dir.loc), 26 | ) 27 | return 28 | } 29 | 30 | const expString = exp.content 31 | if (!expString.trim() || !isMemberExpression(exp, context.options)) { 32 | context.options.onError( 33 | createCompilerError(ErrorCodes.X_V_MODEL_MALFORMED_EXPRESSION, exp.loc), 34 | ) 35 | return 36 | } 37 | 38 | const isComponent = isJSXComponent(node) 39 | if (isComponent) { 40 | return { 41 | key: arg ? arg : createSimpleExpression('modelValue', true), 42 | value: exp, 43 | model: true, 44 | modelModifiers: dir.modifiers.map((m) => m.content), 45 | } 46 | } 47 | 48 | if (dir.arg) 49 | context.options.onError( 50 | createDOMCompilerError( 51 | DOMErrorCodes.X_V_MODEL_ARG_ON_ELEMENT, 52 | dir.arg.loc, 53 | ), 54 | ) 55 | const tag = getText(node.openingElement.name, context) 56 | const isCustomElement = context.options.isCustomElement(tag) 57 | let modelType: DirectiveIRNode['modelType'] | undefined = 'text' 58 | // TODO let runtimeDirective: VaporHelper | undefined = 'vModelText' 59 | if ( 60 | tag === 'input' || 61 | tag === 'textarea' || 62 | tag === 'select' || 63 | isCustomElement 64 | ) { 65 | if (tag === 'input' || isCustomElement) { 66 | const type = findProp(node, 'type') 67 | if (type?.value) { 68 | if (type.value.type === 'JSXExpressionContainer') { 69 | // type={foo} 70 | modelType = 'dynamic' 71 | } else if (type.value.type === 'StringLiteral') { 72 | switch (type.value.value) { 73 | case 'radio': 74 | modelType = 'radio' 75 | break 76 | case 'checkbox': 77 | modelType = 'checkbox' 78 | break 79 | case 'file': 80 | modelType = undefined 81 | context.options.onError( 82 | createDOMCompilerError( 83 | DOMErrorCodes.X_V_MODEL_ON_FILE_INPUT_ELEMENT, 84 | dir.loc, 85 | ), 86 | ) 87 | break 88 | default: 89 | // text type 90 | checkDuplicatedValue() 91 | break 92 | } 93 | } 94 | } else if (hasDynamicKeyVBind(node)) { 95 | // element has bindings with dynamic keys, which can possibly contain 96 | // "type". 97 | modelType = 'dynamic' 98 | } else { 99 | // text type 100 | checkDuplicatedValue() 101 | } 102 | } else if (tag === 'select') { 103 | modelType = 'select' 104 | } else { 105 | // textarea 106 | checkDuplicatedValue() 107 | } 108 | } else { 109 | context.options.onError( 110 | createDOMCompilerError( 111 | DOMErrorCodes.X_V_MODEL_ON_INVALID_ELEMENT, 112 | dir.loc, 113 | ), 114 | ) 115 | } 116 | 117 | if (modelType) 118 | context.registerOperation({ 119 | type: IRNodeTypes.DIRECTIVE, 120 | element: context.reference(), 121 | dir, 122 | name: 'model', 123 | modelType, 124 | builtin: true, 125 | }) 126 | 127 | function checkDuplicatedValue() { 128 | const value = findProp(node, 'value') 129 | if (value && value.value?.type !== 'StringLiteral') { 130 | context.options.onError( 131 | createDOMCompilerError( 132 | DOMErrorCodes.X_V_MODEL_UNNECESSARY_VALUE, 133 | resolveLocation(value.loc, context), 134 | ), 135 | ) 136 | } 137 | } 138 | } 139 | 140 | function hasDynamicKeyVBind(node: JSXElement): boolean { 141 | return node.openingElement.attributes.some( 142 | (p) => 143 | p.type === 'JSXSpreadAttribute' || 144 | (p.type === 'JSXAttribute' && 145 | p.name.type === 'JSXNamespacedName' && 146 | !p.name.namespace.name.startsWith('v-')), 147 | ) 148 | } 149 | -------------------------------------------------------------------------------- /packages/compiler/src/transforms/vOn.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createCompilerError, 3 | createSimpleExpression, 4 | ErrorCodes, 5 | resolveModifiers, 6 | } from '@vue/compiler-dom' 7 | import { extend, makeMap } from '@vue/shared' 8 | import { IRNodeTypes, type KeyOverride, type SetEventIRNode } from '../ir' 9 | import { 10 | isJSXComponent, 11 | resolveExpression, 12 | resolveLocation, 13 | resolveSimpleExpression, 14 | } from '../utils' 15 | import type { DirectiveTransform } from '../transform' 16 | import { EMPTY_EXPRESSION } from './utils' 17 | 18 | const delegatedEvents = /*#__PURE__*/ makeMap( 19 | 'beforeinput,click,dblclick,contextmenu,focusin,focusout,input,keydown,' + 20 | 'keyup,mousedown,mousemove,mouseout,mouseover,mouseup,pointerdown,' + 21 | 'pointermove,pointerout,pointerover,pointerup,touchend,touchmove,' + 22 | 'touchstart', 23 | ) 24 | 25 | export const transformVOn: DirectiveTransform = (dir, node, context) => { 26 | const { name, loc, value } = dir 27 | if (name.type === 'JSXNamespacedName') return 28 | const isComponent = isJSXComponent(node) 29 | 30 | const [nameString, ...modifiers] = name.name 31 | .replace(/^on([A-Z])/, (_, $1) => $1.toLowerCase()) 32 | .split('_') 33 | 34 | if (!value && !modifiers.length) { 35 | context.options.onError( 36 | createCompilerError( 37 | ErrorCodes.X_V_ON_NO_EXPRESSION, 38 | resolveLocation(loc, context), 39 | ), 40 | ) 41 | } 42 | 43 | let arg = resolveSimpleExpression(nameString, true, dir.name.loc) 44 | const exp = resolveExpression(dir.value, context) 45 | 46 | const { keyModifiers, nonKeyModifiers, eventOptionModifiers } = 47 | resolveModifiers( 48 | arg.isStatic ? `on${nameString}` : arg, 49 | modifiers.map((modifier) => createSimpleExpression(modifier)), 50 | null, 51 | resolveLocation(loc, context), 52 | ) 53 | 54 | let keyOverride: KeyOverride | undefined 55 | const isStaticClick = arg.isStatic && arg.content.toLowerCase() === 'click' 56 | 57 | // normalize click.right and click.middle since they don't actually fire 58 | if (nonKeyModifiers.includes('middle')) { 59 | if (keyOverride) { 60 | // TODO error here 61 | } 62 | if (isStaticClick) { 63 | arg = extend({}, arg, { content: 'mouseup' }) 64 | } else if (!arg.isStatic) { 65 | keyOverride = ['click', 'mouseup'] 66 | } 67 | } 68 | if (nonKeyModifiers.includes('right')) { 69 | if (isStaticClick) { 70 | arg = extend({}, arg, { content: 'contextmenu' }) 71 | } else if (!arg.isStatic) { 72 | keyOverride = ['click', 'contextmenu'] 73 | } 74 | } 75 | 76 | if (isComponent) { 77 | const handler = exp || EMPTY_EXPRESSION 78 | return { 79 | key: arg, 80 | value: handler, 81 | handler: true, 82 | handlerModifiers: { 83 | keys: keyModifiers, 84 | nonKeys: nonKeyModifiers, 85 | options: eventOptionModifiers, 86 | }, 87 | } 88 | } 89 | 90 | // Only delegate if: 91 | // - no dynamic event name 92 | // - no event option modifiers (passive, capture, once) 93 | // - is a delegatable event 94 | const delegate = 95 | arg.isStatic && !eventOptionModifiers.length && delegatedEvents(arg.content) 96 | 97 | const operation: SetEventIRNode = { 98 | type: IRNodeTypes.SET_EVENT, 99 | element: context.reference(), 100 | key: arg, 101 | value: exp, 102 | modifiers: { 103 | keys: keyModifiers, 104 | nonKeys: nonKeyModifiers, 105 | options: eventOptionModifiers, 106 | }, 107 | keyOverride, 108 | delegate, 109 | effect: !arg.isStatic, 110 | } 111 | 112 | context.registerEffect([arg], operation) 113 | } 114 | -------------------------------------------------------------------------------- /packages/compiler/src/transforms/vOnce.ts: -------------------------------------------------------------------------------- 1 | import { findProp } from '../utils' 2 | import type { NodeTransform } from '../transform' 3 | 4 | export const transformVOnce: NodeTransform = (node, context) => { 5 | if ( 6 | // !context.inSSR && 7 | node.type === 'JSXElement' && 8 | findProp(node, 'v-once') 9 | ) { 10 | context.inVOnce = true 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/compiler/src/transforms/vShow.ts: -------------------------------------------------------------------------------- 1 | import { createDOMCompilerError, DOMErrorCodes } from '@vue/compiler-dom' 2 | import { IRNodeTypes } from '../ir' 3 | import { resolveDirective } from '../utils' 4 | import type { DirectiveTransform } from '../transform' 5 | 6 | export const transformVShow: DirectiveTransform = (_dir, node, context) => { 7 | const dir = resolveDirective(_dir, context) 8 | const { exp, loc } = dir 9 | if (!exp) { 10 | context.options.onError( 11 | createDOMCompilerError(DOMErrorCodes.X_V_SHOW_NO_EXPRESSION, loc), 12 | ) 13 | return 14 | } 15 | 16 | context.registerOperation({ 17 | type: IRNodeTypes.DIRECTIVE, 18 | element: context.reference(), 19 | dir, 20 | name: 'show', 21 | builtin: true, 22 | }) 23 | } 24 | -------------------------------------------------------------------------------- /packages/compiler/src/transforms/vSlots.ts: -------------------------------------------------------------------------------- 1 | import { createCompilerError, ErrorCodes } from '@vue/compiler-dom' 2 | import { IRSlotType } from '../ir' 3 | import { isJSXComponent, resolveExpression, resolveLocation } from '../utils' 4 | import type { DirectiveTransform } from '../transform' 5 | 6 | export const transformVSlots: DirectiveTransform = (dir, node, context) => { 7 | if (!isJSXComponent(node)) return 8 | 9 | if (dir.value?.type === 'JSXExpressionContainer') { 10 | context.slots = [ 11 | { 12 | slotType: IRSlotType.EXPRESSION, 13 | slots: resolveExpression(dir.value.expression, context), 14 | }, 15 | ] 16 | 17 | if (node.children.length) { 18 | context.options.onError( 19 | createCompilerError( 20 | ErrorCodes.X_V_SLOT_MIXED_SLOT_USAGE, 21 | resolveLocation(node.children[0].loc, context), 22 | ), 23 | ) 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/compiler/src/transforms/vText.ts: -------------------------------------------------------------------------------- 1 | import { createDOMCompilerError, DOMErrorCodes } from '@vue/compiler-dom' 2 | import { isVoidTag } from '@vue/shared' 3 | import { IRNodeTypes } from '../ir' 4 | import { 5 | getLiteralExpressionValue, 6 | getText, 7 | resolveExpression, 8 | resolveLocation, 9 | } from '../utils' 10 | import type { DirectiveTransform } from '../transform' 11 | import { EMPTY_EXPRESSION } from './utils' 12 | 13 | export const transformVText: DirectiveTransform = (dir, node, context) => { 14 | let exp 15 | const loc = resolveLocation(dir.loc, context) 16 | if (!dir.value) { 17 | context.options.onError( 18 | createDOMCompilerError(DOMErrorCodes.X_V_TEXT_NO_EXPRESSION, loc), 19 | ) 20 | exp = EMPTY_EXPRESSION 21 | } else { 22 | exp = resolveExpression(dir.value, context) 23 | } 24 | if (node.children.length) { 25 | context.options.onError( 26 | createDOMCompilerError(DOMErrorCodes.X_V_TEXT_WITH_CHILDREN, loc), 27 | ) 28 | context.childrenTemplate.length = 0 29 | } 30 | 31 | // v-text on void tags do nothing 32 | if (isVoidTag(getText(node.openingElement.name, context))) { 33 | return 34 | } 35 | 36 | const literal = getLiteralExpressionValue(exp) 37 | if (literal != null) { 38 | context.childrenTemplate = [String(literal)] 39 | } else { 40 | context.childrenTemplate = [' '] 41 | context.registerOperation({ 42 | type: IRNodeTypes.GET_TEXT_CHILD, 43 | parent: context.reference(), 44 | }) 45 | context.registerEffect([exp], { 46 | type: IRNodeTypes.SET_TEXT, 47 | element: context.reference(), 48 | values: [exp], 49 | generated: true, 50 | }) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /packages/compiler/test/__snapshots__/compile.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`compile > dynamic root 1`] = ` 4 | " 5 | const n0 = _createNodes(1, 2) 6 | return n0 7 | " 8 | `; 9 | 10 | exports[`compile > dynamic root 2`] = ` 11 | " 12 | const n0 = t0() 13 | const x0 = _child(n0) 14 | _setNodes(x0, () => (a +b + c)) 15 | return n0 16 | " 17 | `; 18 | 19 | exports[`compile > expression parsing > interpolation 1`] = ` 20 | " 21 | const n0 = _createNodes(() => (a + b)) 22 | return n0 23 | " 24 | `; 25 | 26 | exports[`compile > setInsertionState > next, child and nthChild should be above the setInsertionState 1`] = ` 27 | " 28 | const n6 = t1() 29 | const n5 = _next(_child(n6)) 30 | const n7 = _nthChild(n6, 3) 31 | const p0 = _next(n7) 32 | const n4 = _child(p0) 33 | _setInsertionState(n6, n5) 34 | const n0 = _createComponent(Comp) 35 | _setInsertionState(n6, n7) 36 | const n1 = _createIf(() => (true), () => { 37 | const n3 = t0() 38 | return n3 39 | }) 40 | _renderEffect(() => _setProp(n4, "disabled", foo)) 41 | return n6 42 | " 43 | `; 44 | 45 | exports[`compile > static template 1`] = ` 46 | " 47 | const n0 = t0() 48 | return n0 49 | " 50 | `; 51 | -------------------------------------------------------------------------------- /packages/compiler/test/compile.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest' 2 | import { compile } from '../src' 3 | 4 | describe('compile', () => { 5 | test('static template', () => { 6 | const { code } = compile( 7 | `
8 |
hello
9 | 10 | 11 |
`, 12 | ) 13 | expect(code).toMatchSnapshot() 14 | }) 15 | 16 | test('dynamic root', () => { 17 | const { code } = compile(`<>{ 1 }{ 2 }`) 18 | expect(code).toMatchSnapshot() 19 | }) 20 | 21 | test('dynamic root', () => { 22 | const { code } = compile(`
{a +b + c }
`) 23 | expect(code).toMatchSnapshot() 24 | }) 25 | 26 | describe('expression parsing', () => { 27 | test('interpolation', () => { 28 | const { code } = compile(`<>{ a + b }`, { 29 | inline: true, 30 | }) 31 | expect(code).toMatchSnapshot() 32 | expect(code).contains('a + b') 33 | }) 34 | }) 35 | 36 | describe('setInsertionState', () => { 37 | test('next, child and nthChild should be above the setInsertionState', () => { 38 | const { code } = compile(` 39 |
40 |
41 | 42 |
43 |
44 |
45 |
47 |
48 | `) 49 | expect(code).toMatchSnapshot() 50 | }) 51 | }) 52 | }) 53 | -------------------------------------------------------------------------------- /packages/compiler/test/transforms/__snapshots__/expression.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`compiler: expression > conditional expression 1`] = ` 4 | "import { child as _child, setNodes as _setNodes, createNodes as _createNodes, createIf as _createIf, template as _template } from 'vue'; 5 | const t0 = _template(" ") 6 | const t1 = _template("
fail
") 7 | 8 | export function render(_ctx) { 9 | const n0 = _createIf(() => (_ctx.ok), () => { 10 | const n2 = t0() 11 | const x2 = _child(n2) 12 | _setNodes(x2, () => (_ctx.msg)) 13 | return n2 14 | }, () => { 15 | const n4 = _createIf(() => (_ctx.fail), () => { 16 | const n6 = t1() 17 | return n6 18 | }, () => { 19 | const n8 = _createNodes(null) 20 | return n8 21 | }) 22 | return n4 23 | }) 24 | return n0 25 | }" 26 | `; 27 | 28 | exports[`compiler: expression > conditional expression 2`] = ` 29 | "import { child as _child, setNodes as _setNodes, createNodes as _createNodes, createIf as _createIf, template as _template } from 'vue'; 30 | const t0 = _template(" ") 31 | const t1 = _template("
fail
") 32 | 33 | export function render(_ctx) { 34 | const n0 = _createIf(() => (_ctx.ok), () => { 35 | const n2 = t0() 36 | const x2 = _child(n2) 37 | _setNodes(x2, () => (_ctx.msg)) 38 | return n2 39 | }, () => { 40 | const n4 = _createIf(() => (_ctx.fail), () => { 41 | const n6 = t1() 42 | return n6 43 | }, () => { 44 | const n8 = _createNodes(null) 45 | return n8 46 | }) 47 | return n4 48 | }) 49 | return n0 50 | }" 51 | `; 52 | 53 | exports[`compiler: expression > conditional expression with v-once 1`] = ` 54 | " 55 | const n5 = t2() 56 | _setInsertionState(n5) 57 | const n0 = _createIf(() => (ok), () => { 58 | const n2 = t0() 59 | const x2 = _child(n2) 60 | _setNodes(x2, () => (msg)) 61 | return n2 62 | }, () => { 63 | const n4 = t1() 64 | return n4 65 | }) 66 | return n5 67 | " 68 | `; 69 | 70 | exports[`compiler: expression > logical expression 1`] = ` 71 | " 72 | const n0 = _createIf(() => (ok), () => { 73 | const n2 = t0() 74 | const x2 = _child(n2) 75 | _setNodes(x2, () => (msg)) 76 | return n2 77 | }, () => { 78 | const n4 = _createNodes(() => (ok)) 79 | return n4 80 | }) 81 | return n0 82 | " 83 | `; 84 | -------------------------------------------------------------------------------- /packages/compiler/test/transforms/__snapshots__/transformChildren.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`compiler: children transform > efficient find 1`] = ` 4 | " 5 | const n1 = t0() 6 | const n0 = _nthChild(n1, 2) 7 | const x0 = _child(n0) 8 | _setNodes(x0, () => (msg)) 9 | return n1 10 | " 11 | `; 12 | 13 | exports[`compiler: children transform > efficient traversal 1`] = ` 14 | " 15 | const n3 = t0() 16 | const p0 = _next(_child(n3)) 17 | const p1 = _next(p0) 18 | const p2 = _next(p1) 19 | const n0 = _child(p0) 20 | const n1 = _child(p1) 21 | const n2 = _child(p2) 22 | const x0 = _child(n0) 23 | _setNodes(x0, () => ({ msg: msg })) 24 | const x1 = _child(n1) 25 | _setNodes(x1, () => ({ msg: msg })) 26 | const x2 = _child(n2) 27 | _setNodes(x2, () => ({ msg: msg })) 28 | return n3 29 | " 30 | `; 31 | -------------------------------------------------------------------------------- /packages/compiler/test/transforms/__snapshots__/transformElement.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`compiler: element transform > props merging: class 1`] = ` 4 | " 5 | const n0 = _createComponent(Foo, { class: () => (["foo", { bar: isBar }]) }) 6 | return n0 7 | " 8 | `; 9 | 10 | exports[`compiler: element transform > props merging: style 1`] = ` 11 | " 12 | const n0 = _createComponent(Foo, { style: () => (["color: green", { color: 'red' }]) }) 13 | return n0 14 | " 15 | `; 16 | -------------------------------------------------------------------------------- /packages/compiler/test/transforms/__snapshots__/transformTemplateRef.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`compiler: template ref transform > dynamic ref 1`] = ` 4 | " 5 | const _setTemplateRef = _createTemplateRefSetter() 6 | const n0 = t0() 7 | let r0 8 | _renderEffect(() => r0 = _setTemplateRef(n0, foo, r0)) 9 | return n0 10 | " 11 | `; 12 | 13 | exports[`compiler: template ref transform > ref + v-for 1`] = ` 14 | " 15 | const _setTemplateRef = _createTemplateRefSetter() 16 | const n0 = _createFor(() => ([1,2,3]), (_for_item0) => { 17 | const n2 = t0() 18 | let r2 19 | _renderEffect(() => r2 = _setTemplateRef(n2, foo, r2, true)) 20 | return n2 21 | }, null, 4) 22 | return n0 23 | " 24 | `; 25 | 26 | exports[`compiler: template ref transform > ref + v-if 1`] = ` 27 | " 28 | const _setTemplateRef = _createTemplateRefSetter() 29 | const n0 = _createIf(() => (true), () => { 30 | const n2 = t0() 31 | let r2 32 | _renderEffect(() => r2 = _setTemplateRef(n2, foo, r2)) 33 | return n2 34 | }) 35 | return n0 36 | " 37 | `; 38 | 39 | exports[`compiler: template ref transform > static ref 1`] = ` 40 | " 41 | const _setTemplateRef = _createTemplateRefSetter() 42 | const n0 = t0() 43 | _setTemplateRef(n0, "foo") 44 | return n0 45 | " 46 | `; 47 | -------------------------------------------------------------------------------- /packages/compiler/test/transforms/__snapshots__/vBind.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`compiler v-bind > .attr modifier 1`] = ` 4 | " 5 | const n0 = t0() 6 | _renderEffect(() => _setAttr(n0, "foo-bar", id)) 7 | return n0 8 | " 9 | `; 10 | 11 | exports[`compiler v-bind > .camel modifier 1`] = ` 12 | " 13 | const n0 = t0() 14 | _renderEffect(() => _setProp(n0, "fooBar", id)) 15 | return n0 16 | " 17 | `; 18 | 19 | exports[`compiler v-bind > .prop modifier 1`] = ` 20 | " 21 | const n0 = t0() 22 | _renderEffect(() => _setDOMProp(n0, "fooBar", id)) 23 | return n0 24 | " 25 | `; 26 | 27 | exports[`compiler v-bind > no expression 1`] = ` 28 | " 29 | const n0 = t0() 30 | _setProp(n0, "id", true) 31 | return n0 32 | " 33 | `; 34 | 35 | exports[`compiler v-bind > with constant value 1`] = ` 36 | " 37 | const n0 = t0() 38 | _setProp(n0, "a", void 0) 39 | _setProp(n0, "b", 1 > 2) 40 | _setProp(n0, "c", 1 + 2) 41 | _setProp(n0, "d", 1 ? 2 : 3) 42 | _setProp(n0, "e", 2) 43 | _setProp(n0, "f", \`foo1\`) 44 | _setProp(n0, "g", 1) 45 | _setProp(n0, "i", true) 46 | _setProp(n0, "j", null) 47 | _setProp(n0, "l", { foo: 1 }) 48 | _setProp(n0, "n", { ...{ foo: 1 } }) 49 | _setProp(n0, "o", [1, , 3]) 50 | _setProp(n0, "p", [1, ...[2, 3]]) 51 | _setProp(n0, "q", [1, 2]) 52 | _setProp(n0, "r", /\\s+/) 53 | return n0 54 | " 55 | `; 56 | -------------------------------------------------------------------------------- /packages/compiler/test/transforms/__snapshots__/vFor.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`compiler: v-for > array de-structured value (with rest) 1`] = ` 4 | " 5 | const n0 = _createFor(() => (list), (_for_item0, _for_key0) => { 6 | const n2 = t0() 7 | const x2 = _child(n2) 8 | _setNodes(x2, () => (_for_item0.value[0] + _for_item0.value.slice(1) + _for_key0.value)) 9 | return n2 10 | }, ([id, ...other], index) => (id)) 11 | return n0 12 | " 13 | `; 14 | 15 | exports[`compiler: v-for > array de-structured value 1`] = ` 16 | " 17 | const n0 = _createFor(() => (list), (_for_item0, _for_key0) => { 18 | const n2 = t0() 19 | const x2 = _child(n2) 20 | _setNodes(x2, () => (_for_item0.value[0] + _for_item0.value[1] + _for_key0.value)) 21 | return n2 22 | }, ([id, other], index) => (id)) 23 | return n0 24 | " 25 | `; 26 | 27 | exports[`compiler: v-for > basic v-for 1`] = ` 28 | " 29 | const n0 = _createFor(() => (items), (_for_item0) => { 30 | const n2 = t0() 31 | const x2 = _child(n2) 32 | _setNodes(x2, () => (_for_item0.value)) 33 | n2.$evtclick = () => remove(_for_item0.value) 34 | return n2 35 | }, (item) => (item.id)) 36 | return n0 37 | " 38 | `; 39 | 40 | exports[`compiler: v-for > multi effect 1`] = ` 41 | " 42 | const n0 = _createFor(() => (items), (_for_item0, _for_key0) => { 43 | const n2 = t0() 44 | _renderEffect(() => { 45 | _setProp(n2, "item", _for_item0.value) 46 | _setProp(n2, "index", _for_key0.value) 47 | }) 48 | return n2 49 | }) 50 | return n0 51 | " 52 | `; 53 | 54 | exports[`compiler: v-for > nested v-for 1`] = ` 55 | " 56 | const n0 = _createFor(() => (list), (_for_item0) => { 57 | const n5 = t1() 58 | _setInsertionState(n5) 59 | const n2 = _createFor(() => (_for_item0.value), (_for_item1) => { 60 | const n4 = t0() 61 | const x4 = _child(n4) 62 | _setNodes(x4, () => (_for_item1.value+_for_item0.value)) 63 | return n4 64 | }, null, 1) 65 | return n5 66 | }) 67 | return n0 68 | " 69 | `; 70 | 71 | exports[`compiler: v-for > object de-structured value (with rest) 1`] = ` 72 | " 73 | const n0 = _createFor(() => (list), (_for_item0, _for_key0) => { 74 | const n2 = t0() 75 | const x2 = _child(n2) 76 | _setNodes(x2, () => (_for_item0.value.id + _getRestElement(_for_item0.value, ["id"]) + _for_key0.value)) 77 | return n2 78 | }, ({ id, ...other }, index) => (id)) 79 | return n0 80 | " 81 | `; 82 | 83 | exports[`compiler: v-for > object de-structured value 1`] = ` 84 | " 85 | const n0 = _createFor(() => (items), (_for_item0) => { 86 | const n2 = t0() 87 | const x2 = _child(n2) 88 | _setNodes(x2, () => (_for_item0.value.id), () => (_for_item0.value.value)) 89 | return n2 90 | }, ({ id, value }) => (id)) 91 | return n0 92 | " 93 | `; 94 | 95 | exports[`compiler: v-for > object value, key and index 1`] = ` 96 | " 97 | const n0 = _createFor(() => (items), (_for_item0, _for_key0, _for_index0) => { 98 | const n2 = t0() 99 | const x2 = _child(n2) 100 | _setNodes(x2, () => (id), () => (_for_item0.value), () => (_for_index0.value)) 101 | return n2 102 | }, (value, key, index) => (id)) 103 | return n0 104 | " 105 | `; 106 | 107 | exports[`compiler: v-for > v-for aliases w/ complex expressions 1`] = ` 108 | " 109 | const n0 = _createFor(() => (list), (_for_item0) => { 110 | const n2 = t0() 111 | const x2 = _child(n2) 112 | _setNodes(x2, () => (_for_item0.value.foo + bar + baz + _for_item0.value.baz[0] + quux)) 113 | return n2 114 | }) 115 | return n0 116 | " 117 | `; 118 | -------------------------------------------------------------------------------- /packages/compiler/test/transforms/__snapshots__/vHtml.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`v-html > should convert v-html to innerHTML 1`] = ` 4 | " 5 | const n0 = t0() 6 | _renderEffect(() => _setHtml(n0, code.value)) 7 | return n0 8 | " 9 | `; 10 | 11 | exports[`v-html > should raise error if has no expression 1`] = ` 12 | " 13 | const n0 = t0() 14 | _setHtml(n0, "") 15 | return n0 16 | " 17 | `; 18 | -------------------------------------------------------------------------------- /packages/compiler/test/transforms/__snapshots__/vIf.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`compiler: v-if > basic v-if 1`] = ` 4 | " 5 | const n0 = _createIf(() => (ok), () => { 6 | const n2 = t0() 7 | const x2 = _child(n2) 8 | _setNodes(x2, () => (msg)) 9 | return n2 10 | }) 11 | return n0 12 | " 13 | `; 14 | 15 | exports[`compiler: v-if > comment between branches 1`] = ` 16 | " 17 | const n1 = _createIf(() => ("ok"), () => { 18 | const n3 = t0() 19 | return n3 20 | }, () => _createIf(() => ("orNot"), () => { 21 | const n8 = t1() 22 | return n8 23 | }, () => { 24 | const n14 = t2() 25 | return n14 26 | }), true) 27 | const n17 = t3() 28 | return [n1, n17] 29 | " 30 | `; 31 | 32 | exports[`compiler: v-if > dedupe same template 1`] = ` 33 | " 34 | const n0 = _createIf(() => (ok), () => { 35 | const n2 = t0() 36 | return n2 37 | }) 38 | const n3 = _createIf(() => (ok), () => { 39 | const n5 = t0() 40 | return n5 41 | }) 42 | return [n0, n3] 43 | " 44 | `; 45 | 46 | exports[`compiler: v-if > template v-if 1`] = ` 47 | " 48 | const n0 = _createIf(() => (ok), () => { 49 | const n2 = t0() 50 | const n3 = t1() 51 | const n4 = t2() 52 | const x4 = _child(n4) 53 | _renderEffect(() => _setText(x4, _toDisplayString(msg))) 54 | return [n2, n3, n4] 55 | }) 56 | return n0 57 | " 58 | `; 59 | 60 | exports[`compiler: v-if > v-if + v-else 1`] = ` 61 | " 62 | const n0 = _createIf(() => (ok), () => { 63 | const n2 = t0() 64 | return n2 65 | }, () => { 66 | const n4 = t1() 67 | return n4 68 | }) 69 | return n0 70 | " 71 | `; 72 | 73 | exports[`compiler: v-if > v-if + v-else-if + v-else 1`] = ` 74 | " 75 | const n0 = _createIf(() => (ok), () => { 76 | const n2 = t0() 77 | return n2 78 | }, () => _createIf(() => (orNot), () => { 79 | const n4 = t1() 80 | return n4 81 | }, () => { 82 | const n7 = t2() 83 | return n7 84 | })) 85 | return n0 86 | " 87 | `; 88 | 89 | exports[`compiler: v-if > v-if + v-else-if 1`] = ` 90 | " 91 | const n0 = _createIf(() => (ok), () => { 92 | const n2 = t0() 93 | return n2 94 | }, () => _createIf(() => (orNot), () => { 95 | const n4 = t1() 96 | return n4 97 | })) 98 | return n0 99 | " 100 | `; 101 | -------------------------------------------------------------------------------- /packages/compiler/test/transforms/__snapshots__/vOnce.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`compiler: v-once > as root node 1`] = ` 4 | " 5 | const n0 = t0() 6 | _setProp(n0, "id", foo) 7 | return n0 8 | " 9 | `; 10 | 11 | exports[`compiler: v-once > basic 1`] = ` 12 | " 13 | const n2 = t0() 14 | const n0 = _child(n2) 15 | const n1 = _next(n0) 16 | _setNodes(n0, msg) 17 | _setClass(n1, clz) 18 | return n2 19 | " 20 | `; 21 | 22 | exports[`compiler: v-once > inside v-once 1`] = ` 23 | " 24 | const n0 = t0() 25 | return n0 26 | " 27 | `; 28 | 29 | exports[`compiler: v-once > on component 1`] = ` 30 | " 31 | const n1 = t0() 32 | _setInsertionState(n1) 33 | const n0 = _createComponent(Comp, { id: () => (foo) }, null, null, true) 34 | return n1 35 | " 36 | `; 37 | 38 | exports[`compiler: v-once > on nested plain element 1`] = ` 39 | " 40 | const n1 = t0() 41 | const n0 = _child(n1) 42 | _setProp(n0, "id", foo) 43 | return n1 44 | " 45 | `; 46 | 47 | exports[`compiler: v-once > with v-for 1`] = ` 48 | " 49 | const n0 = _createFor(() => (list), (_for_item0) => { 50 | const n2 = t0() 51 | return n2 52 | }, null, 4) 53 | return n0 54 | " 55 | `; 56 | 57 | exports[`compiler: v-once > with v-if 1`] = ` 58 | " 59 | const n0 = _createIf(() => (expr), () => { 60 | const n2 = t0() 61 | return n2 62 | }, null, true) 63 | return n0 64 | " 65 | `; 66 | 67 | exports[`compiler: v-once > with v-if/else 1`] = ` 68 | " 69 | const n0 = _createIf(() => (expr), () => { 70 | const n2 = t0() 71 | return n2 72 | }, () => { 73 | const n4 = t1() 74 | return n4 75 | }, true) 76 | return n0 77 | " 78 | `; 79 | -------------------------------------------------------------------------------- /packages/compiler/test/transforms/__snapshots__/vShow.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`compiler: v-show transform > simple expression 1`] = ` 4 | " 5 | const n0 = t0() 6 | _applyVShow(n0, () => ("foo")) 7 | return n0 8 | " 9 | `; 10 | -------------------------------------------------------------------------------- /packages/compiler/test/transforms/__snapshots__/vSlots.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`compiler: transform v-slots > basic 1`] = ` 4 | " 5 | const n0 = _createComponent(Comp, null, { 6 | $: [ 7 | { default: ({ foo })=> <>{ foo + bar } } 8 | ] 9 | }) 10 | return n0 11 | " 12 | `; 13 | -------------------------------------------------------------------------------- /packages/compiler/test/transforms/__snapshots__/vText.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`v-text > should convert v-text to setText 1`] = ` 4 | " 5 | const n0 = t0() 6 | const x0 = _child(n0) 7 | _renderEffect(() => _setText(x0, _toDisplayString(str.value))) 8 | return n0 9 | " 10 | `; 11 | 12 | exports[`v-text > should raise error and ignore children when v-text is present 1`] = ` 13 | " 14 | const n0 = t0() 15 | const x0 = _child(n0) 16 | _renderEffect(() => _setText(x0, _toDisplayString(test))) 17 | return n0 18 | " 19 | `; 20 | 21 | exports[`v-text > should raise error if has no expression 1`] = ` 22 | " 23 | const n0 = t0() 24 | return n0 25 | " 26 | `; 27 | -------------------------------------------------------------------------------- /packages/compiler/test/transforms/_utils.ts: -------------------------------------------------------------------------------- 1 | import { parse } from '@babel/parser' 2 | import { generate, transform, type CompilerOptions } from '../../src' 3 | import { customGenOperation } from '../../src/generate' 4 | import { IRNodeTypes, type RootNode } from '../../src/ir' 5 | import type { JSXElement, JSXFragment } from '@babel/types' 6 | 7 | export function makeCompile(options: CompilerOptions = {}) { 8 | return (source: string, overrideOptions: CompilerOptions = {}) => { 9 | const { 10 | body: [statement], 11 | } = parse(source, { 12 | sourceType: 'module', 13 | plugins: ['jsx', 'typescript'], 14 | }).program 15 | let children!: JSXElement[] | JSXFragment['children'] 16 | if (statement.type === 'ExpressionStatement') { 17 | children = 18 | statement.expression.type === 'JSXFragment' 19 | ? statement.expression.children 20 | : statement.expression.type === 'JSXElement' 21 | ? [statement.expression] 22 | : [] 23 | } 24 | const ast: RootNode = { 25 | type: IRNodeTypes.ROOT, 26 | children, 27 | source, 28 | } 29 | const ir = transform(ast, { 30 | expressionPlugins: ['typescript', 'jsx'], 31 | inline: true, 32 | prefixIdentifiers: false, 33 | ...options, 34 | ...overrideOptions, 35 | }) as any 36 | const { code, helpers, preamble } = generate(ir, { 37 | inline: true, 38 | prefixIdentifiers: false, 39 | ...options, 40 | ...overrideOptions, 41 | customGenOperation, 42 | }) 43 | return { ast, ir, code, helpers, preamble } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /packages/compiler/test/transforms/expression.spec.ts: -------------------------------------------------------------------------------- 1 | import { NodeTypes } from '@vue/compiler-dom' 2 | import { describe, expect, test } from 'vitest' 3 | import { 4 | IRNodeTypes, 5 | transformChildren, 6 | transformElement, 7 | transformText, 8 | transformVOnce, 9 | type IfIRNode, 10 | } from '../../src' 11 | import { makeCompile } from './_utils' 12 | 13 | const compileWithVIf = makeCompile({ 14 | nodeTransforms: [ 15 | transformVOnce, 16 | transformText, 17 | transformElement, 18 | transformChildren, 19 | ], 20 | }) 21 | 22 | describe('compiler: expression', () => { 23 | test('conditional expression', () => { 24 | const { code, helpers, ir } = compileWithVIf( 25 | `<>{ok? {msg} : fail ?
fail
: null }`, 26 | { inline: false }, 27 | ) 28 | 29 | expect(code).toMatchSnapshot() 30 | 31 | expect(helpers).contains('createIf') 32 | 33 | expect(ir.template).toEqual([' ', '
fail
']) 34 | const op = ir.block.dynamic.children[0].operation 35 | expect(op).toMatchObject({ 36 | type: IRNodeTypes.IF, 37 | id: 0, 38 | condition: { 39 | type: NodeTypes.SIMPLE_EXPRESSION, 40 | content: 'ok', 41 | isStatic: false, 42 | }, 43 | positive: { 44 | type: IRNodeTypes.BLOCK, 45 | dynamic: { 46 | children: [{ template: 0 }], 47 | }, 48 | }, 49 | }) 50 | expect(ir.block.returns).toEqual([0]) 51 | expect(ir.block.dynamic).toMatchObject({ 52 | children: [{ id: 0 }], 53 | }) 54 | 55 | expect(ir.block.effect).toEqual([]) 56 | expect((op as IfIRNode).positive.effect).lengthOf(0) 57 | 58 | expect(code).matchSnapshot() 59 | }) 60 | test('logical expression', () => { 61 | const { code, helpers, ir } = compileWithVIf( 62 | `<>{ok &&
{msg}
}`, 63 | ) 64 | 65 | expect(helpers).contains('createIf') 66 | 67 | expect(ir.template).toEqual(['
']) 68 | const op = ir.block.dynamic.children[0].operation 69 | expect(op).toMatchObject({ 70 | type: IRNodeTypes.IF, 71 | id: 0, 72 | condition: { 73 | type: NodeTypes.SIMPLE_EXPRESSION, 74 | content: 'ok', 75 | isStatic: false, 76 | }, 77 | positive: { 78 | type: IRNodeTypes.BLOCK, 79 | dynamic: { 80 | children: [{ template: 0 }], 81 | }, 82 | }, 83 | }) 84 | expect(ir.block.returns).toEqual([0]) 85 | expect(ir.block.dynamic).toMatchObject({ 86 | children: [{ id: 0 }], 87 | }) 88 | 89 | expect(ir.block.effect).toEqual([]) 90 | expect((op as IfIRNode).positive.effect).lengthOf(0) 91 | expect(code).toMatchSnapshot() 92 | }) 93 | test('conditional expression with v-once', () => { 94 | const { code, helpers, ir } = compileWithVIf( 95 | `
{ok? {msg} :
fail
}
`, 96 | ) 97 | expect(code).toMatchSnapshot() 98 | 99 | expect(helpers).contains('createIf') 100 | expect(ir.template).toEqual([ 101 | ' ', 102 | '
fail
', 103 | '
', 104 | ]) 105 | expect(ir.block.returns).toEqual([5]) 106 | expect(ir.block.dynamic).toMatchObject({ 107 | children: [{ id: 5 }], 108 | }) 109 | }) 110 | }) 111 | -------------------------------------------------------------------------------- /packages/compiler/test/transforms/transformElement.spec.ts: -------------------------------------------------------------------------------- 1 | // import { 2 | // type BindingMetadata, 3 | // NodeTypes, 4 | // } from '@vue/compiler-dom' 5 | import { describe, expect, test } from 'vitest' 6 | import { 7 | // IRDynamicPropsKind, 8 | // IRNodeTypes, 9 | transformChildren, 10 | transformElement, 11 | transformText, 12 | transformVBind, 13 | transformVOn, 14 | } from '../../src' 15 | import { makeCompile } from './_utils' 16 | 17 | const compileWithElementTransform = makeCompile({ 18 | nodeTransforms: [transformElement, transformChildren, transformText], 19 | directiveTransforms: { 20 | bind: transformVBind, 21 | on: transformVOn, 22 | }, 23 | }) 24 | 25 | describe('compiler: element transform', () => { 26 | describe('component', () => { 27 | test('import + resolve component', () => { 28 | const { code, helpers } = compileWithElementTransform(``) 29 | expect(code).toMatchInlineSnapshot(` 30 | " 31 | const n0 = _createComponent(Foo) 32 | return n0 33 | " 34 | `) 35 | expect(helpers).contains.all.keys('createComponent') 36 | }) 37 | }) 38 | 39 | test('resolve namespaced component from setup bindings (inline const)', () => { 40 | const { code, helpers } = compileWithElementTransform(``, { 41 | inline: true, 42 | }) 43 | expect(code).toMatchInlineSnapshot(` 44 | " 45 | const n0 = _createComponent(Foo.Example) 46 | return n0 47 | " 48 | `) 49 | expect(code).contains(`Foo.Example`) 50 | expect(helpers).not.toContain('resolveComponent') 51 | }) 52 | 53 | test('props merging: style', () => { 54 | const { code } = compileWithElementTransform( 55 | ``, 56 | ) 57 | expect(code).toMatchSnapshot() 58 | }) 59 | 60 | test('props merging: class', () => { 61 | const { code } = compileWithElementTransform( 62 | ``, 63 | ) 64 | expect(code).toMatchSnapshot() 65 | }) 66 | }) 67 | -------------------------------------------------------------------------------- /packages/compiler/test/transforms/transformTemplateRef.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest' 2 | import { 3 | DynamicFlag, 4 | IRNodeTypes, 5 | transformChildren, 6 | transformElement, 7 | transformTemplateRef, 8 | transformVFor, 9 | transformVIf, 10 | type ForIRNode, 11 | type IfIRNode, 12 | } from '../../src' 13 | 14 | import { makeCompile } from './_utils' 15 | 16 | const compileWithTransformRef = makeCompile({ 17 | nodeTransforms: [ 18 | transformVIf, 19 | transformVFor, 20 | transformTemplateRef, 21 | transformElement, 22 | transformChildren, 23 | ], 24 | }) 25 | 26 | describe('compiler: template ref transform', () => { 27 | test('static ref', () => { 28 | const { ir, code } = compileWithTransformRef(`
`) 29 | 30 | expect(ir.block.dynamic.children[0]).toMatchObject({ 31 | id: 0, 32 | flags: DynamicFlag.REFERENCED, 33 | }) 34 | expect(ir.template).toEqual(['
']) 35 | expect(ir.block.operation).lengthOf(1) 36 | expect(ir.block.operation[0]).toMatchObject({ 37 | type: IRNodeTypes.SET_TEMPLATE_REF, 38 | element: 0, 39 | value: { 40 | content: 'foo', 41 | isStatic: true, 42 | loc: { 43 | start: { line: 1, column: 10, offset: 9 }, 44 | end: { line: 1, column: 15, offset: 14 }, 45 | }, 46 | }, 47 | }) 48 | expect(code).matchSnapshot() 49 | expect(code).contains('const _setTemplateRef = _createTemplateRefSetter()') 50 | expect(code).contains('_setTemplateRef(n0, "foo")') 51 | }) 52 | 53 | test('dynamic ref', () => { 54 | const { ir, code } = compileWithTransformRef(`
`) 55 | 56 | expect(ir.block.dynamic.children[0]).toMatchObject({ 57 | id: 0, 58 | flags: DynamicFlag.REFERENCED, 59 | }) 60 | expect(ir.template).toEqual(['
']) 61 | expect(ir.block.operation).toMatchObject([ 62 | { 63 | type: IRNodeTypes.DECLARE_OLD_REF, 64 | id: 0, 65 | }, 66 | ]) 67 | expect(ir.block.effect).toMatchObject([ 68 | { 69 | operations: [ 70 | { 71 | type: IRNodeTypes.SET_TEMPLATE_REF, 72 | element: 0, 73 | value: { 74 | content: 'foo', 75 | isStatic: false, 76 | }, 77 | }, 78 | ], 79 | }, 80 | ]) 81 | expect(code).matchSnapshot() 82 | expect(code).contains('_setTemplateRef(n0, foo, r0)') 83 | }) 84 | 85 | test('ref + v-if', () => { 86 | const { ir, code } = compileWithTransformRef( 87 | `
`, 88 | ) 89 | expect(code).toMatchSnapshot() 90 | 91 | const op = ir.block.dynamic.children[0].operation as IfIRNode 92 | expect(op.type).toBe(IRNodeTypes.IF) 93 | 94 | const { positive } = op 95 | expect(positive.effect[0].operations).toMatchObject([ 96 | { 97 | type: IRNodeTypes.SET_TEMPLATE_REF, 98 | element: 2, 99 | value: { 100 | content: 'foo', 101 | isStatic: false, 102 | }, 103 | effect: true, 104 | }, 105 | ]) 106 | expect(code).contains('_setTemplateRef(n2, foo, r2)') 107 | }) 108 | 109 | test('ref + v-for', () => { 110 | const { ir, code } = compileWithTransformRef( 111 | `
`, 112 | ) 113 | expect(code).toMatchSnapshot() 114 | 115 | const { render } = ir.block.dynamic.children[0].operation as ForIRNode 116 | expect(render.effect[0].operations).toMatchObject([ 117 | { 118 | type: IRNodeTypes.SET_TEMPLATE_REF, 119 | element: 2, 120 | value: { 121 | content: 'foo', 122 | isStatic: false, 123 | }, 124 | refFor: true, 125 | effect: true, 126 | }, 127 | ]) 128 | expect(code).contains('_setTemplateRef(n2, foo, r2, true)') 129 | }) 130 | }) 131 | -------------------------------------------------------------------------------- /packages/compiler/test/transforms/vHtml.spec.ts: -------------------------------------------------------------------------------- 1 | import { DOMErrorCodes, NodeTypes } from '@vue/compiler-dom' 2 | import { describe, expect, test, vi } from 'vitest' 3 | import { 4 | IRNodeTypes, 5 | transformChildren, 6 | transformElement, 7 | transformVHtml, 8 | } from '../../src' 9 | import { makeCompile } from './_utils' 10 | 11 | const compileWithVHtml = makeCompile({ 12 | nodeTransforms: [transformElement, transformChildren], 13 | directiveTransforms: { 14 | html: transformVHtml, 15 | }, 16 | }) 17 | 18 | describe('v-html', () => { 19 | test('should convert v-html to innerHTML', () => { 20 | const { code, ir, helpers } = compileWithVHtml( 21 | `
`, 22 | ) 23 | 24 | expect(helpers).contains('setHtml') 25 | 26 | expect(ir.block.operation).toMatchObject([]) 27 | expect(ir.block.effect).toMatchObject([ 28 | { 29 | expressions: [ 30 | { 31 | type: NodeTypes.SIMPLE_EXPRESSION, 32 | content: 'code.value', 33 | isStatic: false, 34 | }, 35 | ], 36 | operations: [ 37 | { 38 | type: IRNodeTypes.SET_HTML, 39 | element: 0, 40 | value: { 41 | type: NodeTypes.SIMPLE_EXPRESSION, 42 | content: 'code.value', 43 | isStatic: false, 44 | }, 45 | }, 46 | ], 47 | }, 48 | ]) 49 | 50 | expect(code).matchSnapshot() 51 | }) 52 | 53 | test('should raise error and ignore children when v-html is present', () => { 54 | const onError = vi.fn() 55 | const { ir, helpers, preamble } = compileWithVHtml( 56 | `
hello
`, 57 | { 58 | onError, 59 | }, 60 | ) 61 | 62 | expect(helpers).contains('setHtml') 63 | 64 | // children should have been removed 65 | expect(ir.template).toEqual(['
']) 66 | 67 | expect(ir.block.operation).toMatchObject([]) 68 | expect(ir.block.effect).toMatchObject([ 69 | { 70 | expressions: [ 71 | { 72 | type: NodeTypes.SIMPLE_EXPRESSION, 73 | content: 'test.value', 74 | isStatic: false, 75 | }, 76 | ], 77 | operations: [ 78 | { 79 | type: IRNodeTypes.SET_HTML, 80 | element: 0, 81 | value: { 82 | type: NodeTypes.SIMPLE_EXPRESSION, 83 | content: 'test.value', 84 | isStatic: false, 85 | }, 86 | }, 87 | ], 88 | }, 89 | ]) 90 | 91 | expect(onError.mock.calls).toMatchObject([ 92 | [{ code: DOMErrorCodes.X_V_HTML_WITH_CHILDREN }], 93 | ]) 94 | 95 | // children should have been removed 96 | expect(preamble).contains('template("
", true)') 97 | }) 98 | 99 | test('should raise error if has no expression', () => { 100 | const onError = vi.fn() 101 | const { code } = compileWithVHtml(`
`, { 102 | onError, 103 | }) 104 | expect(code).matchSnapshot() 105 | expect(onError.mock.calls).toMatchObject([ 106 | [{ code: DOMErrorCodes.X_V_HTML_NO_EXPRESSION }], 107 | ]) 108 | }) 109 | }) 110 | -------------------------------------------------------------------------------- /packages/compiler/test/transforms/vShow.spec.ts: -------------------------------------------------------------------------------- 1 | import { DOMErrorCodes } from '@vue/compiler-dom' 2 | import { describe, expect, test, vi } from 'vitest' 3 | import { transformChildren, transformElement, transformVShow } from '../../src' 4 | import { makeCompile } from './_utils' 5 | 6 | const compileWithVShow = makeCompile({ 7 | nodeTransforms: [transformElement, transformChildren], 8 | directiveTransforms: { 9 | show: transformVShow, 10 | }, 11 | }) 12 | 13 | describe('compiler: v-show transform', () => { 14 | test('simple expression', () => { 15 | const { code } = compileWithVShow(`
`) 16 | expect(code).toMatchSnapshot() 17 | }) 18 | 19 | test('should raise error if has no expression', () => { 20 | const onError = vi.fn() 21 | compileWithVShow(`
`, { onError }) 22 | 23 | expect(onError).toHaveBeenCalledTimes(1) 24 | expect(onError).toHaveBeenCalledWith( 25 | expect.objectContaining({ 26 | code: DOMErrorCodes.X_V_SHOW_NO_EXPRESSION, 27 | }), 28 | ) 29 | }) 30 | }) 31 | -------------------------------------------------------------------------------- /packages/compiler/test/transforms/vSlots.spec.ts: -------------------------------------------------------------------------------- 1 | import { ErrorCodes } from '@vue/compiler-dom' 2 | import { describe, expect, test, vi } from 'vitest' 3 | import { 4 | transformChildren, 5 | transformElement, 6 | transformText, 7 | transformVBind, 8 | transformVOn, 9 | transformVSlot, 10 | transformVSlots, 11 | } from '../../src' 12 | import { makeCompile } from './_utils' 13 | 14 | const compileWithSlots = makeCompile({ 15 | nodeTransforms: [ 16 | transformText, 17 | transformElement, 18 | transformVSlot, 19 | transformChildren, 20 | ], 21 | directiveTransforms: { 22 | bind: transformVBind, 23 | on: transformVOn, 24 | slots: transformVSlots, 25 | }, 26 | }) 27 | 28 | describe('compiler: transform v-slots', () => { 29 | test('basic', () => { 30 | const { code } = compileWithSlots( 31 | ` <>{ foo + bar } }}>`, 32 | ) 33 | expect(code).toMatchSnapshot() 34 | }) 35 | 36 | test('error on invalid mixed slot usage', () => { 37 | const onError = vi.fn() 38 | const source = ` <>{ foo + bar } }}>foo` 39 | compileWithSlots(source, { onError }) 40 | const index = source.lastIndexOf('foo') 41 | expect(onError.mock.calls[0][0]).toMatchObject({ 42 | code: ErrorCodes.X_V_SLOT_MIXED_SLOT_USAGE, 43 | loc: { 44 | start: { 45 | offset: index, 46 | line: 1, 47 | column: index + 1, 48 | }, 49 | end: { 50 | offset: index + 3, 51 | line: 1, 52 | column: index + 4, 53 | }, 54 | }, 55 | }) 56 | }) 57 | }) 58 | -------------------------------------------------------------------------------- /packages/compiler/test/transforms/vText.spec.ts: -------------------------------------------------------------------------------- 1 | import { DOMErrorCodes, NodeTypes } from '@vue/compiler-dom' 2 | import { describe, expect, test, vi } from 'vitest' 3 | import { 4 | IRNodeTypes, 5 | transformChildren, 6 | transformElement, 7 | transformVText, 8 | } from '../../src' 9 | import { makeCompile } from './_utils' 10 | 11 | const compileWithVText = makeCompile({ 12 | nodeTransforms: [transformElement, transformChildren], 13 | directiveTransforms: { 14 | text: transformVText, 15 | }, 16 | }) 17 | 18 | describe('v-text', () => { 19 | test('should convert v-text to setText', () => { 20 | const { code, ir, helpers } = compileWithVText( 21 | `
`, 22 | ) 23 | 24 | expect(helpers).contains('setText') 25 | expect(ir.block.operation).toMatchObject([ 26 | { 27 | type: IRNodeTypes.GET_TEXT_CHILD, 28 | parent: 0, 29 | }, 30 | ]) 31 | 32 | expect(ir.block.effect).toMatchObject([ 33 | { 34 | expressions: [ 35 | { 36 | type: NodeTypes.SIMPLE_EXPRESSION, 37 | content: 'str.value', 38 | isStatic: false, 39 | }, 40 | ], 41 | operations: [ 42 | { 43 | type: IRNodeTypes.SET_TEXT, 44 | element: 0, 45 | values: [ 46 | { 47 | type: NodeTypes.SIMPLE_EXPRESSION, 48 | content: 'str.value', 49 | isStatic: false, 50 | }, 51 | ], 52 | }, 53 | ], 54 | }, 55 | ]) 56 | 57 | expect(code).matchSnapshot() 58 | }) 59 | 60 | test('should raise error and ignore children when v-text is present', () => { 61 | const onError = vi.fn() 62 | const { code, ir, preamble } = compileWithVText( 63 | `
hello
`, 64 | { 65 | onError, 66 | }, 67 | ) 68 | expect(onError.mock.calls).toMatchObject([ 69 | [{ code: DOMErrorCodes.X_V_TEXT_WITH_CHILDREN }], 70 | ]) 71 | 72 | // children should have been removed 73 | expect(ir.template).toEqual(['
']) 74 | 75 | expect(ir.block.effect).toMatchObject([ 76 | { 77 | expressions: [ 78 | { 79 | type: NodeTypes.SIMPLE_EXPRESSION, 80 | content: 'test', 81 | isStatic: false, 82 | }, 83 | ], 84 | operations: [ 85 | { 86 | type: IRNodeTypes.SET_TEXT, 87 | element: 0, 88 | values: [ 89 | { 90 | type: NodeTypes.SIMPLE_EXPRESSION, 91 | content: 'test', 92 | isStatic: false, 93 | }, 94 | ], 95 | }, 96 | ], 97 | }, 98 | ]) 99 | 100 | expect(code).matchSnapshot() 101 | // children should have been removed 102 | expect(preamble).contains('template("
", true)') 103 | }) 104 | 105 | test('should raise error if has no expression', () => { 106 | const onError = vi.fn() 107 | const { code } = compileWithVText(`
`, { onError }) 108 | expect(code).matchSnapshot() 109 | expect(onError.mock.calls).toMatchObject([ 110 | [{ code: DOMErrorCodes.X_V_TEXT_NO_EXPRESSION }], 111 | ]) 112 | }) 113 | }) 114 | -------------------------------------------------------------------------------- /packages/compiler/tsdown.config.ts: -------------------------------------------------------------------------------- 1 | import { config } from '../../tsdown.config.js' 2 | 3 | export default config({ 4 | entry: ['src/index.ts'], 5 | }) 6 | -------------------------------------------------------------------------------- /packages/eslint/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vue-jsx-vapor/eslint", 3 | "version": "2.4.4", 4 | "description": "Vue JSX Vapor ESLint Plugin", 5 | "type": "module", 6 | "keywords": [ 7 | "vue", 8 | "jsx", 9 | "vapor", 10 | "eslint" 11 | ], 12 | "license": "MIT", 13 | "homepage": "https://github.com/vuejs/vue-jsx-vapor#readme", 14 | "bugs": { 15 | "url": "https://github.com/vuejs/vue-jsx-vapor/issues" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/vuejs/vue-jsx-vapor.git" 20 | }, 21 | "files": [ 22 | "dist" 23 | ], 24 | "main": "dist/index.cjs", 25 | "module": "dist/index.js", 26 | "types": "dist/index.d.ts", 27 | "exports": { 28 | ".": { 29 | "types": "./dist/index.d.ts", 30 | "jsx-vapor-dev": "./src/index.ts", 31 | "require": "./dist/index.cjs", 32 | "import": "./dist/index.js" 33 | }, 34 | "./*": "./*" 35 | }, 36 | "scripts": { 37 | "build": "tsdown", 38 | "dev": "DEV=true tsdown", 39 | "release": "bumpp && npm publish", 40 | "test": "vitest" 41 | }, 42 | "dependencies": { 43 | "@prettier/sync": "^0.5.5" 44 | }, 45 | "devDependencies": { 46 | "@typescript-eslint/utils": "^8.33.1", 47 | "eslint-vitest-rule-tester": "^2.2.0" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/eslint/src/index.ts: -------------------------------------------------------------------------------- 1 | import rules, { type Rules } from './rules' 2 | import type { Linter } from 'eslint' 3 | 4 | export const plugins = { 5 | 'vue-jsx-vapor': { 6 | rules, 7 | }, 8 | } 9 | 10 | export { rules, type Rules } 11 | 12 | export default ({ rules = {}, ...options }: Linter.Config = {}) => ({ 13 | name: 'vue-jsx-vapor', 14 | plugins, 15 | rules: { 16 | 'style/jsx-sort-props': 'off', 17 | 'react/jsx-sort-props': 'off', 18 | 'vue-jsx-vapor/jsx-sort-props': rules['vue-jsx-vapor/jsx-sort-props'] || [ 19 | 'warn', 20 | { 21 | callbacksLast: true, 22 | shorthandFirst: true, 23 | reservedFirst: [ 24 | 'v-if', 25 | 'v-else-if', 26 | 'v-else', 27 | 'v-for', 28 | 'key', 29 | 'ref', 30 | 'v-model', 31 | ], 32 | reservedLast: ['v-slot', 'v-slots', 'v-text', 'v-html'], 33 | }, 34 | ], 35 | 'vue-jsx-vapor/define-style': rules['vue-jsx-vapor/define-style'] || 'warn', 36 | } satisfies Rules & Record, 37 | ...options, 38 | }) 39 | -------------------------------------------------------------------------------- /packages/eslint/src/rules/define-style/index.ts: -------------------------------------------------------------------------------- 1 | import prettier from '@prettier/sync' 2 | import type { MessageIds, RuleOptions } from './types' 3 | import type { RuleModule } from '@typescript-eslint/utils/ts-eslint' 4 | 5 | const rule: RuleModule = { 6 | defaultOptions: [ 7 | { 8 | tabWidth: 2, 9 | }, 10 | ], 11 | meta: { 12 | type: 'layout', 13 | docs: { 14 | description: 'Enforce consistent formatting in defineStyle CSS', 15 | }, 16 | fixable: 'code', 17 | messages: { 18 | 'define-style': 'Style in defineStyle should be properly formatted', 19 | 'define-style-syntax-error': 'Syntax error in defineStyle', 20 | }, 21 | schema: [ 22 | { 23 | type: 'object', 24 | properties: { 25 | tabWidth: { 26 | type: 'number', 27 | default: 2, 28 | }, 29 | }, 30 | }, 31 | ], 32 | }, 33 | create(context) { 34 | const configuration = context.options[0] || {} 35 | const tabWidth = configuration.tabWidth || 2 36 | return { 37 | CallExpression(node) { 38 | const callee = 39 | node.callee.type === 'MemberExpression' 40 | ? node.callee.object 41 | : node.callee 42 | const offset = callee.loc.start.column 43 | const parser = 44 | node.callee.type === 'MemberExpression' && 45 | node.callee.property.type === 'Identifier' 46 | ? node.callee.property.name 47 | : 'css' 48 | if (callee.type === 'Identifier' && callee.name === 'defineStyle') { 49 | const arg = node.arguments[0] 50 | 51 | if (arg?.type === 'TemplateLiteral') { 52 | const cssRaw = arg.quasis[0].value.raw 53 | 54 | let formattedCss = '' 55 | try { 56 | formattedCss = prettier.format(cssRaw, { parser, tabWidth }) 57 | } catch { 58 | return context.report({ 59 | node: arg, 60 | messageId: 'define-style-syntax-error', 61 | }) 62 | } 63 | 64 | const placeholder = ' '.repeat(offset + tabWidth) 65 | const result = `\n${placeholder}${formattedCss.slice(0, -1).replaceAll('\n', `\n${placeholder}`)}\n${' '.repeat(offset)}` 66 | if (result !== cssRaw) { 67 | context.report({ 68 | node: arg, 69 | messageId: 'define-style', 70 | fix(fixer) { 71 | return fixer.replaceTextRange( 72 | [arg.range[0] + 1, arg.range[1] - 1], 73 | result, 74 | ) 75 | }, 76 | }) 77 | } 78 | } 79 | } 80 | }, 81 | } 82 | }, 83 | } 84 | export default rule 85 | -------------------------------------------------------------------------------- /packages/eslint/src/rules/define-style/types.ts: -------------------------------------------------------------------------------- 1 | export interface DefineStyleSchema0 { 2 | tabWidth?: number 3 | } 4 | 5 | export type DefineStyleRuleOptions = [DefineStyleSchema0?] 6 | 7 | export type RuleOptions = DefineStyleRuleOptions 8 | export type MessageIds = 'define-style' | 'define-style-syntax-error' 9 | -------------------------------------------------------------------------------- /packages/eslint/src/rules/index.ts: -------------------------------------------------------------------------------- 1 | import defineStyle from './define-style' 2 | import jsxSortProps from './jsx-sort-props' 3 | import type { DefineStyleRuleOptions } from './define-style/types' 4 | import type { JsxSortPropsRuleOptions } from './jsx-sort-props/types' 5 | import type { Linter } from 'eslint' 6 | 7 | const ruleOptions = { 8 | 'jsx-sort-props': jsxSortProps, 9 | 'define-style': defineStyle, 10 | } 11 | 12 | export interface RuleOptions { 13 | 'vue-jsx-vapor/jsx-sort-props': JsxSortPropsRuleOptions 14 | 'vue-jsx-vapor/define-style': DefineStyleRuleOptions 15 | } 16 | 17 | export type Rules = Partial<{ 18 | [K in keyof RuleOptions]: 19 | | Linter.RuleSeverity 20 | | [Linter.RuleSeverity, ...RuleOptions[K]] 21 | }> 22 | 23 | export default ruleOptions 24 | -------------------------------------------------------------------------------- /packages/eslint/src/rules/jsx-sort-props/types.ts: -------------------------------------------------------------------------------- 1 | export interface JsxSortPropsSchema0 { 2 | callbacksLast?: boolean 3 | shorthandFirst?: boolean 4 | shorthandLast?: boolean 5 | multiline?: 'ignore' | 'first' | 'last' 6 | ignoreCase?: boolean 7 | noSortAlphabetically?: boolean 8 | reservedFirst?: string[] | boolean 9 | reservedLast?: string[] 10 | locale?: string 11 | } 12 | 13 | export type JsxSortPropsRuleOptions = [JsxSortPropsSchema0?] 14 | 15 | export type RuleOptions = JsxSortPropsRuleOptions 16 | export type MessageIds = 17 | | 'listIsEmpty' 18 | | 'listReservedPropsFirst' 19 | | 'listReservedPropsLast' 20 | | 'listCallbacksLast' 21 | | 'listShorthandFirst' 22 | | 'listShorthandLast' 23 | | 'listMultilineFirst' 24 | | 'listMultilineLast' 25 | | 'sortPropsByAlpha' 26 | -------------------------------------------------------------------------------- /packages/eslint/test/__snapshots__/define-style.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`define-style > basic 1`] = ` 4 | " 5 | defineStyle(\` 6 | .foo { 7 | color: red; 8 | } 9 | \`) 10 | " 11 | `; 12 | 13 | exports[`define-style > syntax error 1`] = ` 14 | " 15 | defineStyle(\` 16 | .foo { 17 | color: red 18 | background: blue; 19 | } 20 | \`) 21 | " 22 | `; 23 | -------------------------------------------------------------------------------- /packages/eslint/test/__snapshots__/jsx-sort-props.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`jsx-sort-props > basic 1`] = ` 4 | " 5 |
6 | " 7 | `; 8 | 9 | exports[`jsx-sort-props > reservedFirst 1`] = `""`; 10 | 11 | exports[`jsx-sort-props > reservedFirst and reservedLast 1`] = `" {}} v-slot:a={{ foo }} v-slot:b={{ foo }} />"`; 12 | 13 | exports[`jsx-sort-props > reservedLast 1`] = `" {}} v-slot={{ foo }} />"`; 14 | -------------------------------------------------------------------------------- /packages/eslint/test/define-style.spec.ts: -------------------------------------------------------------------------------- 1 | import { createRuleTester } from 'eslint-vitest-rule-tester' 2 | import { describe, expect, it } from 'vitest' 3 | import defineStyle from '../src/rules/define-style' 4 | 5 | describe('define-style', () => { 6 | const { invalid } = createRuleTester({ 7 | name: 'define-style', 8 | rule: defineStyle, 9 | }) 10 | 11 | it('basic', async () => { 12 | const { result } = await invalid({ 13 | code: ` 14 | defineStyle(\` .foo { color: red; } \`) 15 | `, 16 | errors: ['define-style'], 17 | }) 18 | expect(result.output).toMatchSnapshot() 19 | }) 20 | 21 | it('syntax error', async () => { 22 | const { result } = await invalid({ 23 | code: ` 24 | defineStyle(\` 25 | .foo { 26 | color: red 27 | background: blue; 28 | } 29 | \`) 30 | `, 31 | errors: ['define-style-syntax-error'], 32 | }) 33 | expect(result.output).toMatchSnapshot() 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /packages/eslint/test/jsx-sort-props.spec.ts: -------------------------------------------------------------------------------- 1 | import { createRuleTester } from 'eslint-vitest-rule-tester' 2 | import { describe, expect, it } from 'vitest' 3 | import jsxSortProps from '../src/rules/jsx-sort-props' 4 | 5 | describe('jsx-sort-props', () => { 6 | const { invalid } = createRuleTester({ 7 | name: 'jsx-sort-props', 8 | rule: jsxSortProps, 9 | parserOptions: { 10 | sourceType: 'module', 11 | ecmaFeatures: { 12 | jsx: true, 13 | }, 14 | }, 15 | }) 16 | 17 | it('basic', async () => { 18 | const { result } = await invalid({ 19 | code: ` 20 |
21 | `, 22 | errors: ['sortPropsByAlpha'], 23 | }) 24 | expect(result.output).toMatchSnapshot() 25 | }) 26 | 27 | it('reservedFirst', async () => { 28 | const { result } = await invalid({ 29 | code: '', 30 | errors: ['listReservedPropsFirst', 'listReservedPropsFirst'], 31 | options: [ 32 | { reservedFirst: ['v-if', 'v-for'], noSortAlphabetically: true }, 33 | ], 34 | }) 35 | expect(result.output).toMatchSnapshot() 36 | }) 37 | 38 | it('reservedLast', async () => { 39 | const { result } = await invalid({ 40 | code: ' {}} />', 41 | errors: ['listReservedPropsLast'], 42 | options: [{ reservedLast: ['v-slot'], callbacksLast: true }], 43 | }) 44 | expect(result.output).toMatchSnapshot() 45 | }) 46 | 47 | it('reservedFirst and reservedLast', async () => { 48 | const { result } = await invalid({ 49 | code: ' {}} />', 50 | errors: [ 51 | 'listReservedPropsFirst', 52 | 'listReservedPropsFirst', 53 | 'sortPropsByAlpha', 54 | 'listReservedPropsLast', 55 | ], 56 | options: [{ reservedFirst: ['v-model'], reservedLast: ['v-slot'] }], 57 | }) 58 | expect(result.output).toMatchSnapshot() 59 | }) 60 | }) 61 | -------------------------------------------------------------------------------- /packages/eslint/tsdown.config.ts: -------------------------------------------------------------------------------- 1 | import { config } from '../../tsdown.config.js' 2 | 3 | export default config() 4 | -------------------------------------------------------------------------------- /packages/macros/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vue-jsx-vapor/macros", 3 | "version": "2.4.4", 4 | "description": "Macros for Vue JSX Vapor", 5 | "type": "module", 6 | "keywords": [ 7 | "transform", 8 | "vue-jsx", 9 | "macros", 10 | "volar", 11 | "vapor" 12 | ], 13 | "license": "MIT", 14 | "homepage": "https://github.com/vuejs/vue-jsx-vapor#readme", 15 | "bugs": { 16 | "url": "https://github.com/vuejs/vue-jsx-vapor/issues" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/vuejs/vue-jsx-vapor.git" 21 | }, 22 | "files": [ 23 | "dist" 24 | ], 25 | "main": "dist/index.cjs", 26 | "module": "dist/index.js", 27 | "types": "dist/index.d.ts", 28 | "exports": { 29 | ".": { 30 | "types": "./dist/index.d.ts", 31 | "jsx-vapor-dev": "./src/index.ts", 32 | "require": "./dist/index.cjs", 33 | "import": "./dist/index.js" 34 | }, 35 | "./astro": { 36 | "types": "./dist/astro.d.ts", 37 | "jsx-vapor-dev": "./src/astro.ts", 38 | "require": "./dist/astro.cjs", 39 | "import": "./dist/astro.js" 40 | }, 41 | "./rspack": { 42 | "types": "./dist/rspack.d.ts", 43 | "jsx-vapor-dev": "./src/rspack.ts", 44 | "require": "./dist/rspack.cjs", 45 | "import": "./dist/rspack.js" 46 | }, 47 | "./vite": { 48 | "types": "./dist/vite.d.ts", 49 | "jsx-vapor-dev": "./src/vite.ts", 50 | "require": "./dist/vite.cjs", 51 | "import": "./dist/vite.js" 52 | }, 53 | "./webpack": { 54 | "types": "./dist/webpack.d.ts", 55 | "jsx-vapor-dev": "./src/webpack.ts", 56 | "require": "./dist/webpack.cjs", 57 | "import": "./dist/webpack.js" 58 | }, 59 | "./rollup": { 60 | "types": "./dist/rollup.d.ts", 61 | "jsx-vapor-dev": "./src/rollup.ts", 62 | "require": "./dist/rollup.cjs", 63 | "import": "./dist/rollup.js" 64 | }, 65 | "./esbuild": { 66 | "types": "./dist/esbuild.d.ts", 67 | "jsx-vapor-dev": "./src/esbuild.ts", 68 | "require": "./dist/esbuild.cjs", 69 | "import": "./dist/esbuild.js" 70 | }, 71 | "./nuxt": { 72 | "types": "./dist/nuxt.d.ts", 73 | "jsx-vapor-dev": "./src/nuxt.ts", 74 | "require": "./dist/nuxt.cjs", 75 | "import": "./dist/nuxt.js" 76 | }, 77 | "./api": { 78 | "types": "./dist/api.d.ts", 79 | "jsx-vapor-dev": "./src/api.ts", 80 | "require": "./dist/api.cjs", 81 | "import": "./dist/api.js" 82 | }, 83 | "./raw": { 84 | "types": "./dist/raw.d.ts", 85 | "jsx-vapor-dev": "./src/raw.ts", 86 | "require": "./dist/raw.cjs", 87 | "import": "./dist/raw.js" 88 | }, 89 | "./volar": { 90 | "types": "./dist/volar.d.ts", 91 | "jsx-vapor-dev": "./src/volar.ts", 92 | "require": "./dist/volar.cjs", 93 | "import": "./dist/volar.js" 94 | }, 95 | "./*": "./*" 96 | }, 97 | "scripts": { 98 | "build": "tsdown", 99 | "dev": "DEV=true tsdown" 100 | }, 101 | "peerDependencies": { 102 | "@nuxt/kit": "^3", 103 | "@nuxt/schema": "^3", 104 | "esbuild": "*", 105 | "rollup": "^3", 106 | "vite": ">=3", 107 | "webpack": "^4 || ^5" 108 | }, 109 | "peerDependenciesMeta": { 110 | "@nuxt/kit": { 111 | "optional": true 112 | }, 113 | "@nuxt/schema": { 114 | "optional": true 115 | }, 116 | "esbuild": { 117 | "optional": true 118 | }, 119 | "rollup": { 120 | "optional": true 121 | }, 122 | "vite": { 123 | "optional": true 124 | }, 125 | "webpack": { 126 | "optional": true 127 | } 128 | }, 129 | "dependencies": { 130 | "@vue-macros/common": "catalog:", 131 | "@vue/compiler-sfc": "catalog:", 132 | "hash-sum": "catalog:", 133 | "ts-macro": "catalog:", 134 | "unplugin": "catalog:" 135 | }, 136 | "devDependencies": { 137 | "@babel/types": "catalog:", 138 | "@nuxt/kit": "catalog:", 139 | "@nuxt/schema": "catalog:", 140 | "@types/hash-sum": "catalog:", 141 | "@vue-macros/test-utils": "catalog:", 142 | "vue": "catalog:" 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /packages/macros/src/api.ts: -------------------------------------------------------------------------------- 1 | export * from './core' 2 | -------------------------------------------------------------------------------- /packages/macros/src/astro.ts: -------------------------------------------------------------------------------- 1 | import type { Options } from './options' 2 | import unplugin from '.' 3 | 4 | export default (options: Options) => ({ 5 | name: 'vue-jsx-vapor', 6 | hooks: { 7 | 'astro:config:setup': (astro: any) => { 8 | astro.config.vite.plugins ||= [] 9 | astro.config.vite.plugins.push(unplugin.vite(options)) 10 | }, 11 | }, 12 | }) 13 | -------------------------------------------------------------------------------- /packages/macros/src/core/define-component/await.ts: -------------------------------------------------------------------------------- 1 | // Modified from: https://github.com/vuejs/core/blob/main/packages/compiler-sfc/src/script/topLevelAwait.ts 2 | 3 | import { 4 | importHelperFn, 5 | isFunctionType, 6 | walkAST, 7 | type MagicStringAST, 8 | } from '@vue-macros/common' 9 | import type { FunctionalNode } from '../utils' 10 | import type { AwaitExpression, Node, Statement } from '@babel/types' 11 | 12 | export function transformAwait(root: FunctionalNode, s: MagicStringAST): void { 13 | if (root.body.type !== 'BlockStatement') return 14 | let hasAwait = false 15 | for (const node of root.body.body) { 16 | if ( 17 | (node.type === 'VariableDeclaration' && !node.declare) || 18 | node.type.endsWith('Statement') 19 | ) { 20 | const scope: Statement[][] = [root.body.body] 21 | walkAST(node, { 22 | enter(child, parent) { 23 | if (isFunctionType(child)) { 24 | this.skip() 25 | } 26 | if (child.type === 'BlockStatement') { 27 | scope.push(child.body) 28 | } 29 | if (child.type === 'AwaitExpression') { 30 | hasAwait = true 31 | // if the await expression is an expression statement and 32 | // - is in the root scope 33 | // - or is not the first statement in a nested block scope 34 | // then it needs a semicolon before the generated code. 35 | const currentScope = scope.at(-1) 36 | const needsSemi = !!currentScope?.some((n, i) => { 37 | return ( 38 | (scope.length === 1 || i > 0) && 39 | n.type === 'ExpressionStatement' && 40 | n.start === child.start 41 | ) 42 | }) 43 | processAwait( 44 | s, 45 | child, 46 | needsSemi, 47 | parent!.type === 'ExpressionStatement', 48 | ) 49 | } 50 | }, 51 | leave(node: Node) { 52 | if (node.type === 'BlockStatement') scope.pop() 53 | }, 54 | }) 55 | } 56 | } 57 | 58 | if (hasAwait) { 59 | s.prependLeft(root.body.start! + 1, `\nlet __temp, __restore\n`) 60 | } 61 | } 62 | 63 | function processAwait( 64 | s: MagicStringAST, 65 | node: AwaitExpression, 66 | needSemi: boolean, 67 | isStatement: boolean, 68 | ): void { 69 | const argumentStart = 70 | node.argument.extra && node.argument.extra.parenthesized 71 | ? (node.argument.extra.parenStart as number) 72 | : node.argument.start! 73 | 74 | const argumentStr = s.slice(argumentStart, node.argument.end!) 75 | 76 | const containsNestedAwait = /\bawait\b/.test(argumentStr) 77 | 78 | s.overwrite( 79 | node.start!, 80 | argumentStart, 81 | `${needSemi ? `;` : ``}(\n ([__temp,__restore] = ${importHelperFn( 82 | s, 83 | 0, 84 | `withAsyncContext`, 85 | )}(${containsNestedAwait ? `async ` : ``}() => `, 86 | ) 87 | s.appendLeft( 88 | node.end!, 89 | `)),\n ${isStatement ? `` : `__temp = `}await __temp,\n __restore()${ 90 | isStatement ? `` : `,\n __temp` 91 | }\n)`, 92 | ) 93 | } 94 | -------------------------------------------------------------------------------- /packages/macros/src/core/define-component/return.ts: -------------------------------------------------------------------------------- 1 | import { isFunctionalNode, type FunctionalNode } from '../utils' 2 | import type { MagicStringAST } from '@vue-macros/common' 3 | 4 | export function transformReturn(root: FunctionalNode, s: MagicStringAST): void { 5 | const node = 6 | root.body.type === 'BlockStatement' 7 | ? root.body.body.find((node) => node.type === 'ReturnStatement')?.argument 8 | : root.body 9 | if (!node || isFunctionalNode(node)) return 10 | 11 | s.appendRight( 12 | node.extra?.parenthesized ? (node.extra.parenStart as number) : node.start!, 13 | '() => ', 14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /packages/macros/src/core/define-expose.ts: -------------------------------------------------------------------------------- 1 | import { importHelperFn, type MagicStringAST } from '@vue-macros/common' 2 | import type { CallExpression } from '@babel/types' 3 | 4 | export function transformDefineExpose( 5 | node: CallExpression, 6 | s: MagicStringAST, 7 | ): void { 8 | s.overwriteNode(node.callee, ';') 9 | s.appendRight( 10 | node.arguments[0]?.start || node.end! - 1, 11 | `${importHelperFn(s, 0, 'getCurrentInstance', undefined, 'vue-jsx-vapor/runtime')}().exposed = `, 12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /packages/macros/src/core/define-model.ts: -------------------------------------------------------------------------------- 1 | import { importHelperFn, type MagicStringAST } from '@vue-macros/common' 2 | import { useModelHelperId } from './helper' 3 | import type { CallExpression } from '@babel/types' 4 | 5 | export function transformDefineModel( 6 | node: CallExpression, 7 | propsName: string, 8 | s: MagicStringAST, 9 | ): void { 10 | s.overwriteNode( 11 | node.callee, 12 | importHelperFn(s, 0, 'useModel', undefined, useModelHelperId), 13 | ) 14 | s.appendRight( 15 | node.arguments[0]?.start || node.end! - 1, 16 | `${propsName}, ${ 17 | node.arguments[0]?.type !== 'StringLiteral' ? `'modelValue',` : '' 18 | }`, 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /packages/macros/src/core/define-slots.ts: -------------------------------------------------------------------------------- 1 | import { importHelperFn, type MagicStringAST } from '@vue-macros/common' 2 | import type { CallExpression } from '@babel/types' 3 | 4 | export function transformDefineSlots( 5 | node: CallExpression, 6 | s: MagicStringAST, 7 | ): void { 8 | s.overwrite( 9 | node.start!, 10 | (node.arguments[0]?.start && node.arguments[0].start - 1) || 11 | node.typeArguments?.end || 12 | node.callee.end!, 13 | `Object.assign`, 14 | ) 15 | const slots = `${importHelperFn(s, 0, 'useSlots')}()` 16 | s.appendLeft(node.end! - 1, `${node.arguments[0] ? ',' : '{}, '}${slots}`) 17 | } 18 | -------------------------------------------------------------------------------- /packages/macros/src/core/helper/index.ts: -------------------------------------------------------------------------------- 1 | export const helperPrefix = 'vue-jsx-vapor/macros' as const 2 | 3 | export const useModelHelperId = `${helperPrefix}/use-model` 4 | export { default as useModelHelperCode } from './use-model?raw' 5 | 6 | export const withDefaultsHelperId = `${helperPrefix}/with-defaults` as const 7 | export { default as withDefaultsHelperCode } from './with-defaults?raw' 8 | -------------------------------------------------------------------------------- /packages/macros/src/core/helper/use-model.ts: -------------------------------------------------------------------------------- 1 | import { customRef, watchSyncEffect, type ModelRef } from 'vue' 2 | 3 | type DefineModelOptions> = { 4 | default?: any 5 | get?: (v: T) => any 6 | set?: (v: T) => any 7 | } 8 | export function useModel< 9 | M extends PropertyKey, 10 | T extends Record, 11 | K extends keyof T, 12 | >(props: T, name: K, options?: DefineModelOptions): ModelRef 13 | export function useModel( 14 | props: Record, 15 | name: string, 16 | options: DefineModelOptions = {}, 17 | ): any { 18 | const res = customRef((track, trigger) => { 19 | let localValue: any = options && options.default 20 | let prevEmittedValue: any 21 | 22 | watchSyncEffect(() => { 23 | const propValue = props[name] 24 | if (!Object.is(prevEmittedValue, propValue)) { 25 | localValue = propValue 26 | trigger() 27 | } 28 | }) 29 | 30 | return { 31 | get() { 32 | track() 33 | return options.get ? options.get(localValue) : localValue 34 | }, 35 | 36 | set(value) { 37 | if (Object.is(value, localValue)) return 38 | localValue = value 39 | trigger() 40 | const emittedValue = (prevEmittedValue = options.set 41 | ? options.set(value) 42 | : value) 43 | for (const emit of [props[`onUpdate:${name}`]].flat()) { 44 | if (typeof emit === 'function') emit(emittedValue) 45 | } 46 | }, 47 | } 48 | }) 49 | 50 | // @ts-expect-error 51 | res[Symbol.iterator] = () => { 52 | let i = 0 53 | return { 54 | next() { 55 | if (i < 2) { 56 | return { 57 | value: i++ ? props[`${name}Modifiers`] || {} : res, 58 | done: false, 59 | } 60 | } else { 61 | return { done: true } 62 | } 63 | }, 64 | } 65 | } 66 | return res 67 | } 68 | -------------------------------------------------------------------------------- /packages/macros/src/core/helper/with-defaults.ts: -------------------------------------------------------------------------------- 1 | function resolveDefaultProps(paths: Record): any { 2 | const result: Record = {} 3 | 4 | for (const path of Object.keys(paths)) { 5 | const segments = path.split(/[.?[\]]/).filter(Boolean) 6 | let current = result 7 | 8 | for (let i = 0; i < segments.length; i++) { 9 | const segment = segments[i] 10 | if (i === segments.length - 1) { 11 | current[segment] = paths[path] 12 | } else { 13 | if (!current[segment]) { 14 | current[segment] = Number.isNaN(Number(segments[i + 1])) ? {} : [] 15 | } 16 | current = current[segment] 17 | } 18 | } 19 | } 20 | 21 | return result 22 | } 23 | 24 | export function createPropsDefaultProxy( 25 | props: Record, 26 | defaults: Record, 27 | ): Record { 28 | const defaultProps = resolveDefaultProps(defaults) 29 | const result: Record = {} 30 | 31 | for (const key of [ 32 | ...new Set([...Object.keys(props), ...Object.keys(defaultProps)]), 33 | ]) { 34 | Object.defineProperty(result, key, { 35 | enumerable: true, 36 | get: () => (props[key] === undefined ? defaultProps[key] : props[key]), 37 | }) 38 | } 39 | 40 | return result 41 | } 42 | -------------------------------------------------------------------------------- /packages/macros/src/core/style.ts: -------------------------------------------------------------------------------- 1 | import { compileStyleAsync } from '@vue/compiler-sfc' 2 | import type { OptionsResolved } from '../options' 3 | 4 | export async function transformStyle( 5 | code: string, 6 | id: string, 7 | options: OptionsResolved, 8 | ): Promise { 9 | const query = new URLSearchParams(id.split('?')[1]) 10 | const result = await compileStyleAsync({ 11 | filename: id, 12 | id: `data-v-${query.get('scopeId')}`, 13 | isProd: options.isProduction, 14 | source: code, 15 | scoped: query.get('scoped') === 'true', 16 | }) 17 | 18 | return result.code 19 | } 20 | -------------------------------------------------------------------------------- /packages/macros/src/core/utils.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | ArrowFunctionExpression, 3 | FunctionDeclaration, 4 | FunctionExpression, 5 | Node, 6 | } from '@babel/types' 7 | import type { MagicStringAST } from '@vue-macros/common' 8 | 9 | export type FunctionalNode = 10 | | FunctionDeclaration 11 | | FunctionExpression 12 | | ArrowFunctionExpression 13 | 14 | export function prependFunctionalNode( 15 | node: FunctionalNode, 16 | s: MagicStringAST, 17 | result: string, 18 | ): void { 19 | const isBlockStatement = node.body.type === 'BlockStatement' 20 | const start = node.body.extra?.parenthesized 21 | ? (node.body.extra.parenStart as number) 22 | : node.body.start! 23 | s.appendRight( 24 | start + (isBlockStatement ? 1 : 0), 25 | `${result};${!isBlockStatement ? 'return ' : ''}`, 26 | ) 27 | if (!isBlockStatement) { 28 | s.appendLeft(start, '{') 29 | s.appendRight(node.end!, '}') 30 | } 31 | } 32 | 33 | export function isFunctionalNode(node?: Node | null): node is FunctionalNode { 34 | return !!( 35 | node && 36 | (node.type === 'ArrowFunctionExpression' || 37 | node.type === 'FunctionDeclaration' || 38 | node.type === 'FunctionExpression') 39 | ) 40 | } 41 | 42 | export function getParamsStart(node: FunctionalNode, code: string): number { 43 | return node.params[0] 44 | ? node.params[0].start! 45 | : node.start! + 46 | (code.slice(node.start!, node.body.start!).match(/\(\s*\)/)?.index || 47 | 0) + 48 | 1 49 | } 50 | 51 | export function getDefaultValue(node: Node): Node { 52 | if (node.type === 'TSNonNullExpression') { 53 | return getDefaultValue(node.expression) 54 | } 55 | if (node.type === 'TSAsExpression') { 56 | return getDefaultValue(node.expression) 57 | } 58 | return node 59 | } 60 | -------------------------------------------------------------------------------- /packages/macros/src/esbuild.ts: -------------------------------------------------------------------------------- 1 | import unplugin from '.' 2 | 3 | export default unplugin.esbuild 4 | -------------------------------------------------------------------------------- /packages/macros/src/index.ts: -------------------------------------------------------------------------------- 1 | import { createUnplugin, type UnpluginInstance } from 'unplugin' 2 | import plugin from './raw' 3 | import type { Options } from './options' 4 | 5 | export * from './options' 6 | 7 | const unplugin: UnpluginInstance = createUnplugin(plugin) 8 | export default unplugin 9 | -------------------------------------------------------------------------------- /packages/macros/src/nuxt.ts: -------------------------------------------------------------------------------- 1 | import { addVitePlugin, addWebpackPlugin, defineNuxtModule } from '@nuxt/kit' 2 | import vite from './vite' 3 | import webpack from './webpack' 4 | import type { Options } from './options' 5 | import '@nuxt/schema' 6 | 7 | export interface ModuleOptions extends Options {} 8 | 9 | export default defineNuxtModule({ 10 | meta: { 11 | name: 'nuxt-vue-jsx-vapor', 12 | configKey: 'unpluginStarter', 13 | }, 14 | setup(options) { 15 | addVitePlugin(() => vite(options)) 16 | addWebpackPlugin(() => webpack(options)) 17 | 18 | // ... 19 | }, 20 | }) 21 | -------------------------------------------------------------------------------- /packages/macros/src/options.ts: -------------------------------------------------------------------------------- 1 | import { 2 | // detectVueVersion, 3 | REGEX_NODE_MODULES, 4 | REGEX_SETUP_SFC, 5 | REGEX_SRC_FILE, 6 | type BaseOptions, 7 | type MarkRequired, 8 | } from '@vue-macros/common' 9 | 10 | type DefineComponentOptions = { alias?: string[]; autoReturnFunction?: boolean } 11 | type DefineModelOptions = { alias?: string[] } 12 | type DefineExposeOptions = { alias?: string[] } 13 | type DefineSlotsOptions = { alias?: string[] } 14 | type DefineStyleOptions = { alias?: string[] } 15 | export type Options = BaseOptions & { 16 | defineComponent?: DefineComponentOptions 17 | defineModel?: DefineModelOptions 18 | defineExpose?: DefineExposeOptions 19 | defineSlots?: DefineSlotsOptions 20 | defineStyle?: DefineStyleOptions 21 | } 22 | export type OptionsResolved = MarkRequired & { 23 | defineComponent: MarkRequired 24 | defineModel: MarkRequired 25 | defineExpose: MarkRequired 26 | defineSlots: MarkRequired 27 | defineStyle: MarkRequired 28 | } 29 | 30 | export function resolveOptions(options: Options): OptionsResolved { 31 | // waiting for vue@3.6 release 32 | // const version = options.version || detectVueVersion() 33 | const version = options.version || 3.6 34 | return { 35 | include: [REGEX_SRC_FILE], 36 | exclude: [REGEX_SETUP_SFC, REGEX_NODE_MODULES], 37 | ...options, 38 | version, 39 | defineComponent: { 40 | ...options.defineComponent, 41 | alias: options.defineComponent?.alias ?? [ 42 | 'defineComponent', 43 | 'defineVaporComponent', 44 | ], 45 | }, 46 | defineModel: { alias: options.defineModel?.alias ?? ['defineModel'] }, 47 | defineSlots: { alias: options.defineSlots?.alias ?? ['defineSlots'] }, 48 | defineExpose: { alias: options.defineExpose?.alias ?? ['defineExpose'] }, 49 | defineStyle: { alias: options.defineStyle?.alias ?? ['defineStyle'] }, 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /packages/macros/src/raw.ts: -------------------------------------------------------------------------------- 1 | import { createFilter, normalizePath } from '@vue-macros/common' 2 | import { transformJsxMacros } from './core' 3 | import { 4 | helperPrefix, 5 | useModelHelperCode, 6 | useModelHelperId, 7 | withDefaultsHelperCode, 8 | withDefaultsHelperId, 9 | } from './core/helper' 10 | import { transformStyle } from './core/style' 11 | import { resolveOptions, type Options } from './options' 12 | import type { UnpluginOptions } from 'unplugin' 13 | 14 | const name = '@vue-jsx-vapor/macros' 15 | 16 | const plugin = (userOptions: Options = {}): UnpluginOptions => { 17 | const options = resolveOptions(userOptions) 18 | const filter = createFilter(options) 19 | const importMap = new Map() 20 | 21 | return { 22 | name, 23 | enforce: 'pre', 24 | 25 | resolveId(id) { 26 | if (normalizePath(id).startsWith(helperPrefix)) return id 27 | }, 28 | loadInclude(id) { 29 | return normalizePath(id).startsWith(helperPrefix) 30 | }, 31 | load(_id) { 32 | const id = normalizePath(_id) 33 | if (id === useModelHelperId) return useModelHelperCode 34 | else if (id === withDefaultsHelperId) return withDefaultsHelperCode 35 | else if (importMap.get(id)) return importMap.get(id) 36 | }, 37 | 38 | transformInclude(id) { 39 | if (importMap.get(id)) return true 40 | return filter(id) 41 | }, 42 | transform(code, id, opt?: { ssr?: boolean }) { 43 | if (opt?.ssr) { 44 | options.defineComponent.autoReturnFunction = true 45 | } 46 | if (importMap.get(id)) return transformStyle(code, id, options) 47 | return transformJsxMacros(code, id, importMap, options) 48 | }, 49 | } 50 | } 51 | export default plugin 52 | -------------------------------------------------------------------------------- /packages/macros/src/rolldown.ts: -------------------------------------------------------------------------------- 1 | import unplugin from '.' 2 | 3 | export default unplugin.rolldown 4 | -------------------------------------------------------------------------------- /packages/macros/src/rollup.ts: -------------------------------------------------------------------------------- 1 | import unplugin from '.' 2 | 3 | export default unplugin.rollup 4 | -------------------------------------------------------------------------------- /packages/macros/src/rspack.ts: -------------------------------------------------------------------------------- 1 | import unplugin from '.' 2 | 3 | export default unplugin.rspack 4 | -------------------------------------------------------------------------------- /packages/macros/src/vite.ts: -------------------------------------------------------------------------------- 1 | import unplugin from '.' 2 | 3 | export default unplugin.vite 4 | -------------------------------------------------------------------------------- /packages/macros/src/volar.ts: -------------------------------------------------------------------------------- 1 | import { createFilter } from '@vue-macros/common' 2 | import { createPlugin, type PluginReturn } from 'ts-macro' 3 | import { resolveOptions, type Options } from './options' 4 | import { getGlobalTypes, getRootMap, transformJsxMacros } from './volar/index' 5 | 6 | const plugin: PluginReturn = createPlugin( 7 | ({ ts }, userOptions = {}) => { 8 | const resolvedOptions = resolveOptions(userOptions!) 9 | const filter = createFilter(resolvedOptions) 10 | 11 | return { 12 | name: '@vue-jsx-vapor/macros', 13 | resolveVirtualCode(virtualCode) { 14 | const { filePath, codes } = virtualCode 15 | if (!filter(filePath)) return 16 | 17 | const options = { 18 | ts, 19 | ...virtualCode, 20 | ...resolvedOptions, 21 | } 22 | const rootMap = getRootMap(options) 23 | if (rootMap.size) { 24 | transformJsxMacros(rootMap, options) 25 | codes.push(getGlobalTypes(options)) 26 | } 27 | }, 28 | } 29 | }, 30 | ) 31 | export default plugin 32 | -------------------------------------------------------------------------------- /packages/macros/src/volar/define-component.ts: -------------------------------------------------------------------------------- 1 | import { allCodeFeatures, replaceRange } from 'ts-macro' 2 | import type { TransformOptions } from '.' 3 | 4 | export function transformDefineComponent( 5 | node: import('typescript').CallExpression, 6 | parent: import('typescript').Node, 7 | options: TransformOptions, 8 | ): void { 9 | const { codes, ast, ts } = options 10 | 11 | replaceRange(codes, node.arguments[0].end, node.end - 1) 12 | 13 | const componentOptions = node.arguments[1] 14 | replaceRange( 15 | codes, 16 | node.getStart(ast), 17 | node.expression.end + 1, 18 | ts.isExpressionStatement(parent) ? ';' : '', 19 | '(', 20 | [node.expression.getText(ast), node.getStart(ast), allCodeFeatures], 21 | '(() => ({}) as any, ', 22 | componentOptions 23 | ? [ 24 | componentOptions.getText(ast), 25 | componentOptions.getStart(ast), 26 | allCodeFeatures, 27 | ] 28 | : '', 29 | '), ', 30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /packages/macros/src/volar/define-style.ts: -------------------------------------------------------------------------------- 1 | import { allCodeFeatures, replaceRange, type Code } from 'ts-macro' 2 | import type { DefineStyle, TransformOptions } from '.' 3 | 4 | export function transformDefineStyle( 5 | { expression, isCssModules }: DefineStyle, 6 | index: number, 7 | options: TransformOptions, 8 | ): void { 9 | const { ts, codes, ast } = options 10 | if ( 11 | isCssModules && 12 | expression?.arguments[0] && 13 | !expression.typeArguments && 14 | ts.isTemplateLiteral(expression.arguments[0]) 15 | ) { 16 | replaceRange( 17 | codes, 18 | expression.arguments.pos - 1, 19 | expression.arguments.pos - 1, 20 | `<{`, 21 | ...parseCssClassNames( 22 | expression.arguments[0].getText(ast).slice(1, -1), 23 | ).flatMap( 24 | ({ text, offset }) => 25 | [ 26 | `\n`, 27 | [ 28 | `'${text.slice(1)}'`, 29 | `style_${index}`, 30 | expression.arguments.pos + offset + 1, 31 | { navigation: true }, 32 | ], 33 | `: string`, 34 | ] as Code[], 35 | ), 36 | '\n}>', 37 | ) 38 | } 39 | 40 | addEmbeddedCode(expression, index, options) 41 | } 42 | 43 | const commentReg = /(?<=\/\*)[\s\S]*?(?=\*\/)|(?<=\/\/)[\s\S]*?(?=\n)/g 44 | const cssClassNameReg = /(?=(\.[a-z_][-\w]*)[\s.,+~>:#)[{])/gi 45 | const fragmentReg = /(?<=\{)[^{]*(?=(? ' '.repeat(match.length)) 50 | } 51 | const matches = css.matchAll(cssClassNameReg) 52 | const result = [] 53 | for (const match of matches) { 54 | const matchText = match[1] 55 | if (matchText) { 56 | result.push({ offset: match.index, text: matchText }) 57 | } 58 | } 59 | return result 60 | } 61 | 62 | function addEmbeddedCode( 63 | expression: import('typescript').CallExpression, 64 | index: number, 65 | options: TransformOptions, 66 | ) { 67 | const { ts, ast } = options 68 | const languageId = 69 | ts.isPropertyAccessExpression(expression.expression) && 70 | ts.isIdentifier(expression.expression.name) 71 | ? expression.expression.name.text 72 | : 'css' 73 | const style = expression.arguments[0] 74 | const styleText = style 75 | .getText(ast) 76 | .slice(1, -1) 77 | .replaceAll(/\$\{.*\}/g, (str) => '_'.repeat(str.length)) 78 | options.embeddedCodes.push({ 79 | id: `style_${index}`, 80 | languageId, 81 | snapshot: { 82 | getText: (start, end) => styleText.slice(start, end), 83 | getLength: () => styleText.length, 84 | getChangeRange: () => undefined, 85 | }, 86 | mappings: [ 87 | { 88 | sourceOffsets: [style.getStart(ast) + 1], 89 | generatedOffsets: [0], 90 | lengths: [styleText.length], 91 | data: allCodeFeatures, 92 | }, 93 | ], 94 | embeddedCodes: [], 95 | }) 96 | } 97 | -------------------------------------------------------------------------------- /packages/macros/src/volar/global-types.ts: -------------------------------------------------------------------------------- 1 | import { HELPER_PREFIX } from '@vue-macros/common' 2 | import type { TransformOptions } from '.' 3 | 4 | export function getGlobalTypes(options: TransformOptions): string { 5 | const defineSlots = options.defineSlots.alias 6 | .flatMap((alias) => [ 7 | `declare function ${alias}>(): Partial;`, 8 | `declare function ${alias}>(slots: T): T;\n`, 9 | ]) 10 | .join('') 11 | const defineExpose = options.defineExpose.alias 12 | .map( 13 | (alias) => 14 | `declare function ${alias} = Record>(exposed?: Exposed): Exposed;`, 15 | ) 16 | .join('') 17 | const defineStyle = options.defineStyle.alias 18 | .map( 19 | (alias) => 20 | `declare const ${alias}: { (...args: ${HELPER_PREFIX}StyleArgs): T; scss: (...args: ${HELPER_PREFIX}StyleArgs)=> T; sass: (...args: ${HELPER_PREFIX}StyleArgs)=> T; stylus: (...args: ${HELPER_PREFIX}StyleArgs)=> T; less: (...args: ${HELPER_PREFIX}StyleArgs)=> T; postcss: (...args: ${HELPER_PREFIX}StyleArgs)=> T };\n`, 21 | ) 22 | .join('') 23 | const defineModel = options.defineModel.alias 24 | .map((alias) => 25 | alias === 'defineModel' ? 'defineModel' : `defineModel: ${alias}`, 26 | ) 27 | .join(', ') 28 | const defineComponent = options.defineComponent.alias 29 | .map((alias) => 30 | ['defineComponent', 'defineVaporComponent'].includes(alias) 31 | ? '' 32 | : `defineComponent: ${alias}`, 33 | ) 34 | .filter(Boolean) 35 | .join(', ') 36 | return ` 37 | declare const { ${defineModel}, ${defineComponent} }: typeof import('vue') 38 | ${defineSlots} 39 | ${defineExpose} 40 | ${defineStyle} 41 | type ${HELPER_PREFIX}StyleArgs = [style: string, options?: { scoped?: boolean }]; 42 | // @ts-ignore 43 | type __VLS_IsAny = 0 extends 1 & T ? true : false; type __VLS_PickNotAny = __VLS_IsAny extends true ? B : A; 44 | ` 45 | } 46 | -------------------------------------------------------------------------------- /packages/macros/src/volar/transform.ts: -------------------------------------------------------------------------------- 1 | import { HELPER_PREFIX } from '@vue-macros/common' 2 | import { replaceRange } from 'ts-macro' 3 | import { transformDefineStyle } from './define-style' 4 | import type { RootMap, TransformOptions } from '.' 5 | 6 | export function transformJsxMacros( 7 | rootMap: RootMap, 8 | options: TransformOptions, 9 | ): void { 10 | const { ts, codes, ast } = options 11 | 12 | let defineStyleIndex = 0 13 | for (const [root, macros] of rootMap) { 14 | macros.defineStyle?.forEach((defaultStyle) => 15 | transformDefineStyle(defaultStyle, defineStyleIndex++, options), 16 | ) 17 | 18 | if (!root?.body) continue 19 | 20 | const asyncModifier = root.modifiers?.find( 21 | (modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword, 22 | ) 23 | if (asyncModifier && macros.defineComponent) 24 | replaceRange(codes, asyncModifier.pos, asyncModifier.end) 25 | const result = `({}) as __VLS_PickNotAny & { __ctx: typeof ${HELPER_PREFIX}ctx }` 26 | 27 | const propsType = root.parameters[0]?.type 28 | ? String(root.parameters[0].type.getText(ast)) 29 | : '{}' 30 | replaceRange( 31 | codes, 32 | root.parameters.pos, 33 | root.parameters.pos, 34 | ts.isArrowFunction(root) && root.parameters.pos === root.pos ? '(' : '', 35 | `${HELPER_PREFIX}props: typeof ${HELPER_PREFIX}ctx.props & ${propsType}, `, 36 | `${HELPER_PREFIX}placeholder?: {}, `, 37 | `${HELPER_PREFIX}ctx = {} as Awaited>, `, 40 | `${HELPER_PREFIX}setup = (${asyncModifier ? 'async' : ''}(`, 41 | ) 42 | if (ts.isArrowFunction(root)) { 43 | replaceRange( 44 | codes, 45 | root.end, 46 | root.end, 47 | `))${root.pos === root.parameters.pos ? ')' : ''} => `, 48 | result, 49 | ) 50 | } else { 51 | replaceRange( 52 | codes, 53 | root.body.getStart(ast), 54 | root.body.getStart(ast), 55 | '=>', 56 | ) 57 | replaceRange(codes, root.end, root.end, `)){ return `, result, '}') 58 | } 59 | 60 | ts.forEachChild(root.body, (node) => { 61 | if (ts.isReturnStatement(node) && node.expression) { 62 | const props = [...(macros.defineModel ?? [])] 63 | const elements = 64 | root.parameters[0] && 65 | !root.parameters[0].type && 66 | ts.isObjectBindingPattern(root.parameters[0].name) 67 | ? root.parameters[0].name.elements 68 | : [] 69 | for (const element of elements) { 70 | if (ts.isIdentifier(element.name)) 71 | props.push( 72 | `${element.name.escapedText}${ 73 | element.initializer && 74 | ts.isNonNullExpression(element.initializer) 75 | ? ':' 76 | : '?:' 77 | } typeof ${element.name.escapedText}`, 78 | ) 79 | } 80 | 81 | const shouldWrapByCall = 82 | (ts.isArrowFunction(node.expression) || 83 | ts.isFunctionExpression(node.expression)) && 84 | macros.defineComponent 85 | replaceRange( 86 | codes, 87 | node.getStart(ast), 88 | node.expression.getStart(ast), 89 | `const ${HELPER_PREFIX}render = `, 90 | shouldWrapByCall ? '(' : '', 91 | ) 92 | replaceRange( 93 | codes, 94 | node.expression.end, 95 | node.expression.end, 96 | shouldWrapByCall ? ')()' : '', 97 | ` 98 | return { 99 | props: {} as {${props.join(', ')}}, 100 | slots: {} as ${macros.defineSlots ?? '{}'}, 101 | expose: (exposed: import('vue').ShallowUnwrapRef<${macros.defineExpose ?? '{}'}>) => {}, 102 | render: ${HELPER_PREFIX}render, 103 | }`, 104 | ) 105 | } 106 | }) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /packages/macros/src/webpack.ts: -------------------------------------------------------------------------------- 1 | import unplugin from '.' 2 | 3 | export default unplugin.webpack 4 | -------------------------------------------------------------------------------- /packages/macros/tests/__snapshots__/restructure.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`transform > reconstruct 1`] = ` 4 | "const App = (__MACROS_props, __MACROS_props1) => { 5 | function onClick(__MACROS_props){ 6 | return { foo: __MACROS_props.foo, baz: __MACROS_props1.baz.baz } 7 | }; 8 | return [ __MACROS_props[0][0][1], __MACROS_props[1].id.foo[0], __MACROS_props1.baz ] 9 | }" 10 | `; 11 | 12 | exports[`transform > reconstruct AssignmentPattern 1`] = ` 13 | " 14 | import { createPropsDefaultProxy as __MACROS_createPropsDefaultProxy } from "vue-jsx-vapor/macros/with-defaults";function App(__MACROS_props){ 15 | __MACROS_props = __MACROS_createPropsDefaultProxy(__MACROS_props, {'.foo.foo': 1, '.bar[0]': 2}); 16 | return <>{[__MACROS_props.foo.foo, __MACROS_props.bar[0]]} 17 | }" 18 | `; 19 | 20 | exports[`transform > reconstruct arrowFunctionExpression 1`] = ` 21 | "const App = (__MACROS_props) => ( 22 | <>{[__MACROS_props[0].root.foo]} 23 | )" 24 | `; 25 | 26 | exports[`transform > reconstruct default-prop 1`] = ` 27 | " 28 | import { createPropsRestProxy as __MACROS_createPropsRestProxy } from "vue"; 29 | import { createPropsDefaultProxy as __MACROS_createPropsDefaultProxy } from "vue-jsx-vapor/macros/with-defaults";function App(__MACROS_props, __MACROS_props1){ 30 | const rest = __MACROS_createPropsRestProxy(__MACROS_props, ['foo', 'baz']); 31 | __MACROS_props = __MACROS_createPropsDefaultProxy(__MACROS_props, {'.foo': 'bar'}); 32 | __MACROS_props1 = __MACROS_createPropsDefaultProxy(__MACROS_props1, {'[0]': 'foo'}); 33 | return <>{[__MACROS_props.foo, __MACROS_props.baz, rest, __MACROS_props1[0]]} 34 | }" 35 | `; 36 | 37 | exports[`transform > reconstruct rest-prop 1`] = ` 38 | " 39 | import { createPropsRestProxy as __MACROS_createPropsRestProxy } from "vue"; 40 | import { createPropsDefaultProxy as __MACROS_createPropsDefaultProxy } from "vue-jsx-vapor/macros/with-defaults";function App(__MACROS_props){ 41 | const rest = __MACROS_createPropsRestProxy(__MACROS_props, ['foo', 'bar']); 42 | __MACROS_props = __MACROS_createPropsDefaultProxy(__MACROS_props, {'.bar': 1}); 43 | return <>{[__MACROS_props.foo, __MACROS_props.bar, rest]} 44 | }" 45 | `; 46 | -------------------------------------------------------------------------------- /packages/macros/tests/fixtures.spec.ts: -------------------------------------------------------------------------------- 1 | import process from 'node:process' 2 | import { testFixtures } from '@vue-macros/test-utils' 3 | import { describe } from 'vitest' 4 | import { transformJsxMacros } from '../src/core' 5 | 6 | const options = { 7 | defineModel: { alias: ['defineModel'] }, 8 | defineSlots: { alias: ['defineSlots'] }, 9 | defineStyle: { alias: ['defineStyle'] }, 10 | defineExpose: { alias: ['defineExpose'] }, 11 | defineComponent: { alias: ['defineComponent', 'defineVaporComponent'] }, 12 | } 13 | 14 | // TODO: hash-sum's result is different on Windows and Linux 15 | const globs = 16 | process.platform === 'win32' 17 | ? import.meta.glob( 18 | ['./fixtures/**/*.tsx', '!./fixtures/**/define-style.tsx'], 19 | { 20 | eager: true, 21 | as: 'raw', 22 | }, 23 | ) 24 | : import.meta.glob('./fixtures/**/*.tsx', { 25 | eager: true, 26 | as: 'raw', 27 | }) 28 | 29 | describe('fixtures', async () => { 30 | await testFixtures( 31 | globs, 32 | (args, id, code) => 33 | transformJsxMacros(code, id, new Map(), { 34 | include: ['*.tsx'], 35 | version: 3.6, 36 | ...options, 37 | })?.code, 38 | ) 39 | }) 40 | 41 | describe('defineComponent autoReturnFunction fixtures', async () => { 42 | await testFixtures( 43 | import.meta.glob('./fixtures/**/define-component.tsx', { 44 | eager: true, 45 | as: 'raw', 46 | }), 47 | (args, id, code) => 48 | transformJsxMacros(code, id, new Map(), { 49 | include: ['*.tsx'], 50 | version: 3.6, 51 | ...options, 52 | defineComponent: { 53 | alias: ['defineComponent', 'defineVaporComponent'], 54 | autoReturnFunction: true, 55 | }, 56 | })?.code, 57 | ) 58 | }) 59 | -------------------------------------------------------------------------------- /packages/macros/tests/fixtures/define-component.tsx: -------------------------------------------------------------------------------- 1 | import { defineComponent, nextTick, unref } from 'vue' 2 | 3 | const $ = unref 4 | 5 | const Comp = defineComponent( 6 | ({ bar = 'bar'!, Comp, ...attrs }: { bar: 'bar'; baz: 'baz', Comp: any }) => { 7 | defineModel() 8 | const foo = $( 9 | defineModel('foo', { 10 | validator: (value) => { 11 | return value === 'foo' 12 | }, 13 | type: String, 14 | })!, 15 | ) 16 | return
17 | {[foo, bar, attrs.baz]} 18 | 19 |
20 | }, 21 | { name: 'Comp', props: { Comp: Object } }, 22 | ) 23 | 24 | const Comp1 = defineComponent((props: { bar: 'bar'; 'onUpdate:bar': any, comp: any }) => { 25 | const foo = defineModel('foo') 26 | return
27 | {[foo.value, props['bar'], props['onUpdate:bar']]} 28 | 29 |
30 | }) 31 | 32 | const Comp2 = defineComponent(async () => { 33 | await nextTick() 34 | let foo = await new Promise((resolve) => { 35 | setTimeout(() => resolve('foo'), 1000) 36 | }) 37 | return () =>
{foo}
38 | }) 39 | 40 | const foo = () => {} 41 | defineComponent(({ 42 | a = 0, 43 | b = 'b', 44 | c = true, 45 | d = () => {}, 46 | e = {}, 47 | f = [], 48 | g = foo, 49 | h = null, 50 | i = undefined!, 51 | }) => { 52 | return ( 53 | <> 54 | {a} 55 | {b} 56 | {c} 57 | {d} 58 | {e} 59 | {f} 60 | {g} 61 | {h} 62 | {i} 63 | 64 | ) 65 | }) -------------------------------------------------------------------------------- /packages/macros/tests/fixtures/define-expose.tsx: -------------------------------------------------------------------------------- 1 | export function Comp() { 2 | defineExpose({ 3 | foo: 1, 4 | }) 5 | return
6 | } 7 | 8 | export const Comp1 = function (props: any) { 9 | defineExpose({ 10 | foo: props.foo, 11 | }) 12 | return
13 | } 14 | 15 | export const Comp2 = ({ foo }: any) => { 16 | defineExpose({ 17 | foo, 18 | }) 19 | return
20 | } 21 | -------------------------------------------------------------------------------- /packages/macros/tests/fixtures/define-model.tsx: -------------------------------------------------------------------------------- 1 | 2 | import { defineComponent, unref } from 'vue' 3 | 4 | const $ = unref 5 | 6 | export const Comp = defineComponent(({ bar }: { bar: string }) => { 7 | const foo = defineModel('foo', { default: bar }) 8 | return
{foo.value}
9 | }) 10 | 11 | export default function () { 12 | const modelValue = $(defineModel()!) 13 | return ( 14 | 15 | {modelValue} 16 | 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /packages/macros/tests/fixtures/define-slots.tsx: -------------------------------------------------------------------------------- 1 | export const Comp = () => { 2 | const slots = defineSlots<{ 3 | default: () => any 4 | }>() 5 | return
{slots.default?.()}
6 | } 7 | 8 | export default function () { 9 | const slots = defineSlots({ 10 | default: () =>
default
, 11 | }) 12 | return
{slots.default?.()}
13 | } 14 | -------------------------------------------------------------------------------- /packages/macros/tests/fixtures/define-style.tsx: -------------------------------------------------------------------------------- 1 | import { defineComponent, ref } from 'vue' 2 | 3 | export const Comp = () => { 4 | const color = ref('red') 5 | defineStyle(` 6 | .foo { 7 | color: ${color.value}; 8 | } 9 | `) 10 | return
foo
11 | } 12 | 13 | export default defineComponent(() => { 14 | const color = ref('red') 15 | const styles = defineStyle.scss(` 16 | .bar { 17 | color: ${color.value}; 18 | .bar-baz { 19 | background: red; 20 | } 21 | } 22 | `) 23 | const { default: Default, ...slots } = defineSlots() 24 | return () => ( 25 | <> 26 |
foo
27 |
28 | bar 29 |
30 | 31 | 32 | 33 | 34 | ) 35 | }) 36 | 37 | defineStyle.scss(` 38 | .bar { 39 | color: red; 40 | } 41 | `) 42 | -------------------------------------------------------------------------------- /packages/macros/tests/restructure.spec.ts: -------------------------------------------------------------------------------- 1 | import { babelParse, MagicStringAST, walkAST } from '@vue-macros/common' 2 | import { describe, expect, test } from 'vitest' 3 | import { restructure } from '../src/api' 4 | 5 | const transformRestructure = (code: string): string => { 6 | const s = new MagicStringAST(code) 7 | const ast = babelParse(code, 'tsx') 8 | walkAST(ast, { 9 | enter(node) { 10 | if ( 11 | node.type === 'FunctionExpression' || 12 | node.type === 'ArrowFunctionExpression' || 13 | node.type === 'FunctionDeclaration' 14 | ) { 15 | restructure(s, node) 16 | } 17 | }, 18 | }) 19 | return s.toString() 20 | } 21 | 22 | describe('transform', () => { 23 | test('reconstruct', () => { 24 | const code = transformRestructure( 25 | `const App = ([[[,foo]], {id: {foo: [bar]}}], { baz }) => { 26 | function onClick({ foo }){ 27 | return { foo, baz: baz.baz } 28 | }; 29 | return [ foo, bar, baz ] 30 | }`, 31 | )! 32 | expect(code).toMatchSnapshot() 33 | }) 34 | 35 | test('reconstruct arrowFunctionExpression', () => { 36 | const code = transformRestructure( 37 | `const App = ([{root: {foo}}]) => ( 38 | <>{[foo]} 39 | )`, 40 | )! 41 | expect(code).toMatchSnapshot() 42 | }) 43 | 44 | test('reconstruct default-prop', () => { 45 | const code = transformRestructure( 46 | `function App({foo: bar = 'bar', baz: qux, ...rest}, [foo = 'foo']){ 47 | return <>{[bar, qux, rest, foo]} 48 | }`, 49 | )! 50 | expect(code).toMatchSnapshot() 51 | }) 52 | 53 | test('reconstruct rest-prop', () => { 54 | const code = transformRestructure( 55 | `function App({foo, bar = 1, ...rest}){ 56 | return <>{[foo, bar, rest]} 57 | }`, 58 | )! 59 | expect(code).toMatchSnapshot() 60 | }) 61 | 62 | test('reconstruct AssignmentPattern', () => { 63 | const code = transformRestructure( 64 | `function App({foo: {foo = 1} = ({} as any)!, bar: [bar = 2] = []}){ 65 | return <>{[foo, bar]} 66 | }`, 67 | )! 68 | expect(code).toMatchSnapshot() 69 | }) 70 | }) 71 | -------------------------------------------------------------------------------- /packages/macros/tsdown.config.ts: -------------------------------------------------------------------------------- 1 | import { config } from '../../tsdown.config.js' 2 | 3 | export default config() 4 | -------------------------------------------------------------------------------- /packages/vue-jsx-vapor/src/api.ts: -------------------------------------------------------------------------------- 1 | export * from './core' 2 | -------------------------------------------------------------------------------- /packages/vue-jsx-vapor/src/astro.ts: -------------------------------------------------------------------------------- 1 | import unplugin from './unplugin' 2 | import type { Options } from './options' 3 | 4 | export default (options: Options) => ({ 5 | name: 'vue-jsx-vapor', 6 | hooks: { 7 | 'astro:config:setup': (astro: any) => { 8 | astro.config.vite.plugins ||= [] 9 | astro.config.vite.plugins.push(unplugin.vite(options)) 10 | }, 11 | }, 12 | }) 13 | -------------------------------------------------------------------------------- /packages/vue-jsx-vapor/src/core/index.ts: -------------------------------------------------------------------------------- 1 | import { transformSync } from '@babel/core' 2 | // @ts-ignore missing type 3 | import babelTypescript from '@babel/plugin-transform-typescript' 4 | import jsx from '@vue-jsx-vapor/babel' 5 | import type { Options } from '../options' 6 | 7 | export type { Options } 8 | 9 | export function transformVueJsxVapor( 10 | code: string, 11 | id: string, 12 | options?: Options, 13 | needSourceMap = false, 14 | ) { 15 | return transformSync(code, { 16 | plugins: [ 17 | [jsx, { compile: options?.compile, interop: options?.interop }], 18 | ...(id.endsWith('.tsx') 19 | ? [[babelTypescript, { isTSX: true, allowExtensions: true }]] 20 | : []), 21 | ], 22 | filename: id, 23 | sourceMaps: needSourceMap, 24 | sourceFileName: id, 25 | babelrc: false, 26 | configFile: false, 27 | ast: true, 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /packages/vue-jsx-vapor/src/core/runtime.ts: -------------------------------------------------------------------------------- 1 | import { 2 | EffectScope, 3 | insert, 4 | isFragment, 5 | isVaporComponent, 6 | proxyRefs, 7 | remove, 8 | renderEffect, 9 | toRefs, 10 | useAttrs, 11 | VaporFragment, 12 | type Block, 13 | type GenericComponentInstance, 14 | } from 'vue' 15 | 16 | import * as Vue from 'vue' 17 | 18 | export { shallowRef as useRef } from 'vue' 19 | 20 | export function getCurrentInstance(): GenericComponentInstance | null { 21 | // @ts-ignore 22 | return Vue.currentInstance || Vue.getCurrentInstance() 23 | } 24 | 25 | /** 26 | * Returns the props of the current component instance. 27 | * 28 | * @example 29 | * ```tsx 30 | * import { useProps } from 'vue-jsx-vapor' 31 | * 32 | * defineComponent(({ foo = '' })=>{ 33 | * const props = useProps() // { foo: '' } 34 | * }) 35 | * ``` 36 | */ 37 | export function useProps() { 38 | const i = getCurrentInstance() 39 | return i!.props 40 | } 41 | 42 | /** 43 | * Returns the merged props and attrs of the current component.\ 44 | * Equivalent to `useProps()` + `useAttrs()`. 45 | * 46 | * @example 47 | * ```tsx 48 | * import { useFullProps } from 'vue-jsx-vapor' 49 | * 50 | * defineComponent((props) => { 51 | * const fullProps = useFullProps() // = useAttrs() + useProps() 52 | * }) 53 | */ 54 | export function useFullProps() { 55 | return proxyRefs({ 56 | ...toRefs(useProps()), 57 | ...toRefs(useAttrs()), 58 | }) 59 | } 60 | 61 | function createFragment( 62 | nodes: Block, 63 | anchor: Node | undefined = document.createTextNode(''), 64 | ) { 65 | const frag = new VaporFragment(nodes) 66 | frag.anchor = anchor 67 | return frag 68 | } 69 | 70 | function normalizeNode(node: any, anchor?: Node): Block { 71 | if (node instanceof Node || isFragment(node)) { 72 | anchor && (anchor.textContent = '') 73 | return node 74 | } else if (isVaporComponent(node)) { 75 | anchor && (anchor.textContent = '') 76 | return createFragment(node, anchor) 77 | } else if (Array.isArray(node)) { 78 | anchor && (anchor.textContent = '') 79 | return createFragment( 80 | node.map((i) => normalizeNode(i)), 81 | anchor, 82 | ) 83 | } else { 84 | const result = node == null || typeof node === 'boolean' ? '' : String(node) 85 | if (anchor) { 86 | anchor.textContent = result 87 | return anchor 88 | } else { 89 | return document.createTextNode(result) 90 | } 91 | } 92 | } 93 | 94 | function resolveValue(current: Block, value: any, _anchor?: Node) { 95 | const node = normalizeNode(value, _anchor) 96 | if (current) { 97 | if (isFragment(current)) { 98 | const { anchor } = current 99 | if (anchor && anchor.parentNode) { 100 | remove(current.nodes, anchor.parentNode) 101 | insert(node, anchor.parentNode, anchor) 102 | !_anchor && anchor.parentNode.removeChild(anchor) 103 | } 104 | } else if (current instanceof Node) { 105 | if (isFragment(node) && current.parentNode) { 106 | insert(node, current.parentNode, current) 107 | current.parentNode.removeChild(current) 108 | } else if (node instanceof Node) { 109 | if (current.nodeType === 3 && node.nodeType === 3) { 110 | current.textContent = node.textContent 111 | return current 112 | } else if (current.parentNode) { 113 | current.parentNode.replaceChild(node, current) 114 | } 115 | } 116 | } 117 | } 118 | return node 119 | } 120 | 121 | function resolveValues(values: any[] = [], _anchor?: Node) { 122 | const nodes: Block[] = [] 123 | const frag = createFragment(nodes, _anchor) 124 | const scopes: EffectScope[] = [] 125 | for (const [index, value] of values.entries()) { 126 | const anchor = index === values.length - 1 ? _anchor : undefined 127 | if (typeof value === 'function') { 128 | renderEffect(() => { 129 | if (scopes[index]) scopes[index].stop() 130 | scopes[index] = new EffectScope() 131 | nodes[index] = scopes[index].run(() => 132 | resolveValue(nodes[index], value(), anchor), 133 | )! 134 | }) 135 | } else { 136 | nodes[index] = resolveValue(nodes[index], value, anchor) 137 | } 138 | } 139 | return frag 140 | } 141 | 142 | export function setNodes(anchor: Node, ...values: any[]) { 143 | const resolvedValues = resolveValues(values, anchor) 144 | anchor.parentNode && insert(resolvedValues, anchor.parentNode, anchor) 145 | } 146 | 147 | export function createNodes(...values: any[]) { 148 | return resolveValues(values) 149 | } 150 | -------------------------------------------------------------------------------- /packages/vue-jsx-vapor/src/core/ssr.ts: -------------------------------------------------------------------------------- 1 | import { relative } from 'pathe' 2 | import { normalizePath } from 'unplugin-utils' 3 | import type { HotComponent } from './hmr' 4 | import type { ComponentOptions } from 'vue' 5 | 6 | export const ssrRegisterHelperId = '/__vue-jsx-ssr-register-helper' 7 | export const ssrRegisterHelperCode = 8 | `import { useSSRContext } from "vue"\n` + 9 | // the const here is just to work around the Bun bug where 10 | // Function.toString() isn't working as intended 11 | // https://github.com/oven-sh/bun/issues/9543 12 | `export const ssrRegisterHelper = ${ssrRegisterHelper.toString()}` 13 | 14 | /** 15 | * This function is serialized with toString() and evaluated as a virtual 16 | * module during SSR 17 | */ 18 | function ssrRegisterHelper(comp: ComponentOptions, filename: string) { 19 | const setup = comp.setup 20 | comp.setup = (props, ctx) => { 21 | // @ts-ignore 22 | const ssrContext = useSSRContext() 23 | ;(ssrContext.modules || (ssrContext.modules = new Set())).add(filename) 24 | if (setup) { 25 | return setup(props, ctx) 26 | } 27 | } 28 | } 29 | 30 | export function injectSSR( 31 | id: string, 32 | hotComponents: HotComponent[], 33 | root = '', 34 | ) { 35 | const normalizedId = normalizePath(relative(root, id)) 36 | let ssrInjectCode = 37 | `\nimport { ssrRegisterHelper } from "${ssrRegisterHelperId}"` + 38 | `\nconst __moduleId = ${JSON.stringify(normalizedId)}` 39 | for (const { local } of hotComponents) { 40 | ssrInjectCode += `\nssrRegisterHelper(${local}, __moduleId)` 41 | } 42 | return ssrInjectCode 43 | } 44 | -------------------------------------------------------------------------------- /packages/vue-jsx-vapor/src/core/vue-jsx.ts: -------------------------------------------------------------------------------- 1 | import { transformSync } from '@babel/core' 2 | // @ts-ignore missing type 3 | import babelTypescript from '@babel/plugin-transform-typescript' 4 | import jsx from '@vue/babel-plugin-jsx' 5 | 6 | export function transformVueJsx( 7 | code: string, 8 | id: string, 9 | needSourceMap = false, 10 | ) { 11 | const result = transformSync(code, { 12 | plugins: [ 13 | jsx, 14 | ...(id.endsWith('.tsx') 15 | ? [[babelTypescript, { isTSX: true, allowExtensions: true }]] 16 | : []), 17 | ], 18 | filename: id, 19 | sourceMaps: needSourceMap, 20 | sourceFileName: id, 21 | babelrc: false, 22 | configFile: false, 23 | }) 24 | if (result?.code) { 25 | return { 26 | code: result.code, 27 | map: result.map, 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/vue-jsx-vapor/src/esbuild.ts: -------------------------------------------------------------------------------- 1 | import unplugin from './unplugin' 2 | 3 | export default unplugin.esbuild 4 | -------------------------------------------------------------------------------- /packages/vue-jsx-vapor/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './core/runtime' 2 | 3 | export * from './jsx-runtime/dom' 4 | -------------------------------------------------------------------------------- /packages/vue-jsx-vapor/src/jsx-runtime.ts: -------------------------------------------------------------------------------- 1 | import { Fragment, h, type VNode } from 'vue' 2 | import type { NativeElements, ReservedProps } from 'vue-jsx-vapor' 3 | 4 | function jsx(type: any, props: any, key: any): ReturnType { 5 | const { children } = props 6 | delete props.children 7 | if (arguments.length > 2) { 8 | props.key = key 9 | } 10 | return h(type, props, children) 11 | } 12 | 13 | export { Fragment, jsx, jsx as jsxDEV, jsx as jsxs } 14 | 15 | declare global { 16 | // eslint-disable-next-line @typescript-eslint/no-namespace 17 | namespace JSX { 18 | export interface Element extends VNode {} 19 | export interface ElementClass { 20 | $props: {} 21 | } 22 | export interface ElementAttributesProperty { 23 | $props: {} 24 | } 25 | export interface IntrinsicElements extends NativeElements { 26 | [name: string]: any 27 | } 28 | export interface IntrinsicAttributes extends ReservedProps {} 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/vue-jsx-vapor/src/nuxt.ts: -------------------------------------------------------------------------------- 1 | import { addVitePlugin, addWebpackPlugin, defineNuxtModule } from '@nuxt/kit' 2 | import vite from './vite' 3 | import webpack from './webpack' 4 | import type { Options } from './options' 5 | import '@nuxt/schema' 6 | 7 | export interface ModuleOptions extends Options {} 8 | 9 | export default defineNuxtModule({ 10 | meta: { 11 | name: 'nuxt-vue-jsx-vapor', 12 | configKey: 'unpluginStarter', 13 | }, 14 | setup(options) { 15 | addVitePlugin(() => vite(options)) 16 | addWebpackPlugin(() => webpack(options)) 17 | 18 | // ... 19 | }, 20 | }) 21 | -------------------------------------------------------------------------------- /packages/vue-jsx-vapor/src/options.d.ts: -------------------------------------------------------------------------------- 1 | import type { CompilerOptions } from '@vue-jsx-vapor/compiler' 2 | import type { Options as MacrosOptions } from '@vue-jsx-vapor/macros' 3 | import type { FilterPattern } from 'unplugin-utils' 4 | 5 | export interface Options { 6 | // define your plugin options here 7 | include?: FilterPattern 8 | exclude?: FilterPattern 9 | interop?: boolean 10 | compile?: CompilerOptions 11 | /** @default true */ 12 | ref?: 13 | | { 14 | alias?: string[] 15 | } 16 | | boolean 17 | /** @default false */ 18 | macros?: MacrosOptions | boolean 19 | } 20 | -------------------------------------------------------------------------------- /packages/vue-jsx-vapor/src/raw.ts: -------------------------------------------------------------------------------- 1 | import macros from '@vue-jsx-vapor/macros/raw' 2 | import { transformJsxDirective } from '@vue-macros/jsx-directive/api' 3 | import { createFilter, normalizePath } from 'unplugin-utils' 4 | import { transformVueJsxVapor } from './core' 5 | import { injectHMRAndSSR } from './core/hmr' 6 | import runtimeCode from './core/runtime?raw' 7 | import { ssrRegisterHelperCode, ssrRegisterHelperId } from './core/ssr' 8 | import { transformVueJsx } from './core/vue-jsx' 9 | import type { Options } from './options' 10 | import type { UnpluginOptions } from 'unplugin' 11 | 12 | const plugin = (options: Options = {}): UnpluginOptions[] => { 13 | const transformInclude = createFilter( 14 | options?.include || /\.[cm]?[jt]sx$/, 15 | options?.exclude || /node_modules/, 16 | ) 17 | let root = '' 18 | let needHMR = false 19 | let needSourceMap = false 20 | return [ 21 | { 22 | name: 'vue-jsx-vapor', 23 | vite: { 24 | config(config) { 25 | return { 26 | // only apply esbuild to ts files 27 | // since we are handling jsx and tsx now 28 | esbuild: { 29 | include: /\.ts$/, 30 | }, 31 | define: { 32 | __VUE_OPTIONS_API__: config.define?.__VUE_OPTIONS_API__ ?? true, 33 | __VUE_PROD_DEVTOOLS__: 34 | config.define?.__VUE_PROD_DEVTOOLS__ ?? false, 35 | __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: 36 | config.define?.__VUE_PROD_HYDRATION_MISMATCH_DETAILS__ ?? false, 37 | }, 38 | } 39 | }, 40 | configResolved(config) { 41 | root = config.root 42 | needHMR = config.command === 'serve' 43 | needSourceMap = config.command === 'serve' || !!config.build.sourcemap 44 | }, 45 | }, 46 | resolveId(id) { 47 | if (id === ssrRegisterHelperId) return id 48 | if (normalizePath(id) === 'vue-jsx-vapor/runtime') return id 49 | }, 50 | loadInclude(id) { 51 | if (id === ssrRegisterHelperId) return true 52 | return normalizePath(id) === 'vue-jsx-vapor/runtime' 53 | }, 54 | load(id) { 55 | if (id === ssrRegisterHelperId) return ssrRegisterHelperCode 56 | if (normalizePath(id) === 'vue-jsx-vapor/runtime') return runtimeCode 57 | }, 58 | transformInclude, 59 | transform(code, id, opt?: { ssr?: boolean }) { 60 | const result = transformVueJsxVapor(code, id, options, needSourceMap) 61 | if (result?.code) { 62 | ;(needHMR || opt?.ssr) && 63 | injectHMRAndSSR(result, id, { ssr: opt?.ssr, root }) 64 | return { 65 | code: result.code, 66 | map: result.map, 67 | } 68 | } 69 | }, 70 | }, 71 | { 72 | name: '@vue-macros/jsx-directive', 73 | transformInclude, 74 | transform(code, id, opt?: { ssr?: boolean }) { 75 | if (options.interop || opt?.ssr) { 76 | return transformJsxDirective(code, id, { 77 | lib: 'vue', 78 | prefix: 'v-', 79 | version: 3.6, 80 | }) 81 | } 82 | }, 83 | }, 84 | { 85 | name: '@vitejs/plugin-vue-jsx', 86 | transformInclude, 87 | transform(code, id, opt?: { ssr?: boolean }) { 88 | if (options.interop || opt?.ssr) { 89 | return transformVueJsx(code, id, needSourceMap) 90 | } 91 | }, 92 | }, 93 | ...(options.macros === false 94 | ? [] 95 | : options.macros 96 | ? [macros(options.macros === true ? undefined : options.macros)] 97 | : []), 98 | ] 99 | } 100 | export default plugin 101 | -------------------------------------------------------------------------------- /packages/vue-jsx-vapor/src/rolldown.ts: -------------------------------------------------------------------------------- 1 | import { createRollupPlugin } from 'unplugin' 2 | import { unpluginFactory } from './unplugin' 3 | 4 | export default createRollupPlugin(unpluginFactory) 5 | -------------------------------------------------------------------------------- /packages/vue-jsx-vapor/src/rollup.ts: -------------------------------------------------------------------------------- 1 | import unplugin from './unplugin' 2 | 3 | export default unplugin.rollup 4 | -------------------------------------------------------------------------------- /packages/vue-jsx-vapor/src/rspack.ts: -------------------------------------------------------------------------------- 1 | import unplugin from './unplugin' 2 | 3 | export default unplugin.rspack 4 | -------------------------------------------------------------------------------- /packages/vue-jsx-vapor/src/unplugin.ts: -------------------------------------------------------------------------------- 1 | import { createUnplugin, type UnpluginFactory } from 'unplugin' 2 | import plugin from './raw' 3 | import type { Options } from './options' 4 | 5 | export type { Options } 6 | 7 | export const unpluginFactory: UnpluginFactory = ( 8 | options = {}, 9 | ) => { 10 | return plugin(options) 11 | } 12 | 13 | export const unplugin = /* #__PURE__ */ createUnplugin(unpluginFactory) 14 | 15 | export default unplugin 16 | -------------------------------------------------------------------------------- /packages/vue-jsx-vapor/src/vite.ts: -------------------------------------------------------------------------------- 1 | import unplugin from './unplugin' 2 | 3 | export default unplugin.vite 4 | -------------------------------------------------------------------------------- /packages/vue-jsx-vapor/src/volar.ts: -------------------------------------------------------------------------------- 1 | import jsxMacros from '@vue-jsx-vapor/macros/volar' 2 | import jsxDirective from '@vue-macros/volar/jsx-directive' 3 | import jsxRef from '@vue-macros/volar/jsx-ref' 4 | import { createPlugin, type PluginReturn } from 'ts-macro' 5 | import type { Options } from './options' 6 | 7 | const plugin: PluginReturn = createPlugin( 8 | (ctx, options) => { 9 | return [ 10 | jsxDirective()(ctx), 11 | options?.ref === false 12 | ? [] 13 | : jsxRef(options?.ref === true ? undefined : options?.ref)(ctx), 14 | options?.macros === false 15 | ? [] 16 | : options?.macros 17 | ? jsxMacros(options.macros === true ? undefined : options.macros)(ctx) 18 | : [], 19 | ].flat() 20 | }, 21 | ) 22 | 23 | export default plugin 24 | -------------------------------------------------------------------------------- /packages/vue-jsx-vapor/src/webpack.ts: -------------------------------------------------------------------------------- 1 | import unplugin from './unplugin' 2 | 3 | export default unplugin.webpack 4 | -------------------------------------------------------------------------------- /packages/vue-jsx-vapor/tsdown.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsdown' 2 | import { config } from '../../tsdown.config.js' 3 | 4 | export default [ 5 | config({ 6 | entry: ['./src/*.ts', '!./**.d.ts', '!./src/jsx-runtime.ts'], 7 | }), 8 | defineConfig({ 9 | entry: { index: 'src/jsx-runtime.ts' }, 10 | outDir: './jsx-runtime', 11 | format: ['esm', 'cjs'], 12 | }), 13 | ] 14 | -------------------------------------------------------------------------------- /playground/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /playground/main.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createVaporApp, 3 | // vaporInteropPlugin, 4 | } from 'vue' 5 | 6 | const modules = import.meta.glob('./src/*.tsx') 7 | const mod = ( 8 | modules[`./src${location.pathname}.tsx`] || modules['./src/App.tsx'] 9 | )() 10 | 11 | mod.then(({ default: mod }) => { 12 | const app = createVaporApp(mod) 13 | // app.use(vaporInteropPlugin). 14 | .mount('#app') 15 | 16 | // @ts-expect-error 17 | globalThis.unmount = () => { 18 | // @ts-expect-error 19 | app.unmount() 20 | } 21 | }) 22 | -------------------------------------------------------------------------------- /playground/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "playground", 3 | "version": "2.4.4", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "dev": "node vite.js --port 5174", 8 | "build": "node vite.js build" 9 | }, 10 | "devDependencies": { 11 | "vite": "catalog:", 12 | "vite-hyper-config": "^0.7.0", 13 | "vite-plugin-inspect": "^11.1.0", 14 | "vue": "catalog:", 15 | "vue-jsx-vapor": "workspace:*" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /playground/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { defineVaporComponent, ref, type Ref } from 'vue' 2 | import { useRef } from 'vue-jsx-vapor' 3 | import Count2 from './count' 4 | import For from './for' 5 | import Html from './html' 6 | import If from './if' 7 | import Model from './model' 8 | import Once from './once' 9 | import Show from './show' 10 | import Slot from './slot' 11 | 12 | export default defineVaporComponent(() => { 13 | const count = ref('1') 14 | 15 | const Count = (props: { value: string }) => { 16 | return
{props.value}
17 | } 18 | 19 | const Count1 = ({ value }: { value: Ref }) => { 20 | return
{value.value}
21 | } 22 | 23 | const compRef = useRef() 24 | 25 | return ( 26 | <> 27 |
28 | Component 29 | (count.value = e.currentTarget.value)} 32 | /> 33 | 34 | 35 | 36 | 37 | {compRef.value?.double} 38 |
39 | 40 |
41 | v-if 42 | 43 |
44 | 45 |
46 | v-for 47 | 48 |
49 | 50 |
51 | v-slot 52 | 53 |
54 | 55 |
56 | v-model 57 | 58 |
59 | 60 |
61 | v-show 62 | 63 |
64 | 65 |
66 | v-html 67 | 68 |
69 | 70 |
71 | v-once 72 | 73 |
74 | 75 | ) 76 | }) 77 | -------------------------------------------------------------------------------- /playground/src/count.tsx: -------------------------------------------------------------------------------- 1 | import { computed, defineComponent } from 'vue' 2 | 3 | export default defineComponent(({ value = '' }) => { 4 | defineExpose({ 5 | double: computed(() => +value * 2), 6 | }) 7 | return
{value}
8 | }) 9 | -------------------------------------------------------------------------------- /playground/src/for.tsx: -------------------------------------------------------------------------------- 1 | import { ref } from 'vue' 2 | 3 | export default () => { 4 | const count = ref(3) 5 | 6 | const Arr = ( 7 | <> 8 | {Array.from({ length: count.value }).map((_, index) => { 9 | if (index > 1) { 10 | return ( 11 | <> 12 |
({index}) lg 1
13 | 14 | ) 15 | } else { 16 | return [({index}) lt 1,
] 17 | } 18 | })} 19 | 20 | ) 21 | 22 | return ( 23 |
24 | 25 | 26 |
27 |
28 | map 29 | {Arr} 30 |
31 | 32 |
33 | v-for 34 |
{index}
35 |
36 |
37 |
38 | ) 39 | } 40 | -------------------------------------------------------------------------------- /playground/src/html.tsx: -------------------------------------------------------------------------------- 1 | import { ref } from 'vue' 2 | 3 | export default () => { 4 | const foo = ref('
foo
') 5 | 6 | return ( 7 | <> 8 | 9 | 10 |
11 | 12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /playground/src/if.tsx: -------------------------------------------------------------------------------- 1 | import { defineVaporComponent, ref } from 'vue' 2 | 3 | export default defineVaporComponent(() => { 4 | const count = ref(1) 5 | 6 | const Foo = () =>
2
7 | 8 | return ( 9 |
10 | 11 | 12 | 13 |
14 |
15 | expression 16 |
17 | {count.value === 1 ? ( 18 |
{1}
19 | ) : count.value === 2 ? ( 20 | 21 | ) : count.value >= 3 ? ( 22 |
lg 3: {count.value}
23 | ) : ( 24 |
lt 0: {count.value}
25 | )} 26 |
27 |
28 | 29 |
30 | v-if 31 |
32 | {/* @ts-ignore */} 33 |
34 | {count.value} 35 |
36 | 37 |
= 3}>lg 3: {count.value}
38 |
lt 0: {count.value}
39 |
40 |
41 |
42 |
43 | ) 44 | defineStyle(` 45 | .foo { 46 | color: red; 47 | } 48 | `) 49 | }) 50 | -------------------------------------------------------------------------------- /playground/src/interop.tsx: -------------------------------------------------------------------------------- 1 | import { defineComponent, defineVaporComponent, ref } from 'vue' 2 | 3 | const VaporComp = defineVaporComponent( 4 | (props) => { 5 | return ( 6 |
7 | Vapor Component: 8 | {props.model} 9 |
10 | ) 11 | }, 12 | { props: ['model'] }, 13 | ) 14 | 15 | const Comp = (props) =>
Virtual Dom Component:{props.model}
16 | 17 | export default defineComponent(() => { 18 | const model = ref('') 19 | return () => [ 20 | , 21 | , 22 | , 23 | ] 24 | }) 25 | -------------------------------------------------------------------------------- /playground/src/model.tsx: -------------------------------------------------------------------------------- 1 | import { ref } from 'vue' 2 | 3 | const Comp = () => { 4 | const model = defineModel() 5 | return 6 | } 7 | 8 | export default () => { 9 | const model = ref('model') 10 | 11 | return ( 12 | <> 13 | 14 | 15 | {model.value} 16 | 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /playground/src/once.tsx: -------------------------------------------------------------------------------- 1 | import { ref } from 'vue' 2 | 3 | export default () => { 4 | const count = ref(3) 5 | 6 | return ( 7 | <> 8 | 9 |
{count.value}
10 | 11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /playground/src/show.tsx: -------------------------------------------------------------------------------- 1 | import { ref } from 'vue' 2 | 3 | export default () => { 4 | const show = ref(false) 5 | 6 | return ( 7 | <> 8 | 9 | {String(show.value)} 10 | 11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /playground/src/slot.tsx: -------------------------------------------------------------------------------- 1 | import { ref } from 'vue' 2 | 3 | const Comp = (props: { foo: string }, { slots }) => { 4 | return ( 5 | <> 6 | {slots.default ? ( 7 | 8 | ) : ( 9 |
default slot
10 | )} 11 | 12 | ) 13 | } 14 | 15 | const slots = { 16 | default: (scope) =>
{scope.foo}
, 17 | } 18 | 19 | // eslint-disable-next-line unused-imports/no-unused-vars 20 | const slotName = ref('default') 21 | export default () => { 22 | const foo = ref('foo') 23 | return ( 24 | <> 25 | 26 |
27 |
28 | v-slots 29 | 30 | 31 |
{scope.foo}
}} 34 | /> 35 |
36 | 37 |
38 | v-slot 39 | 40 |
{scope.foo}
41 |
42 |
43 |
44 | 45 | ) 46 | } 47 | -------------------------------------------------------------------------------- /playground/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "jsx": "preserve", 5 | "jsxImportSource": "vue-jsx-vapor", 6 | "lib": ["ESNext", "DOM"], 7 | "customConditions": ["jsx-vapor-dev"], 8 | "module": "ESNext", 9 | "moduleResolution": "bundler", 10 | "resolveJsonModule": true, 11 | "types": ["vite/client"], 12 | "strict": true, 13 | "strictNullChecks": true, 14 | "esModuleInterop": true 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /playground/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import Inspect from 'vite-plugin-inspect' 3 | import VueJsxVapor from 'vue-jsx-vapor/vite' 4 | 5 | export default defineConfig({ 6 | plugins: [ 7 | VueJsxVapor({ 8 | macros: true, 9 | }), 10 | Inspect(), 11 | ], 12 | }) 13 | -------------------------------------------------------------------------------- /playground/vite.js: -------------------------------------------------------------------------------- 1 | import { startVite } from 'vite-hyper-config' 2 | 3 | startVite(undefined, { 4 | resolve: { 5 | conditions: ['jsx-vapor-dev'], 6 | }, 7 | }) 8 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - packages/* 3 | - playground 4 | - docs 5 | 6 | defines: 7 | - &babel ^7.27.4 8 | - &vue-macros ^3.0.0-beta.14 9 | - &ts-macro ^0.1.33 10 | 11 | catalog: 12 | '@babel/core': *babel 13 | '@babel/parser': *babel 14 | '@babel/plugin-syntax-jsx': ^7.27.1 15 | '@babel/plugin-transform-typescript': ^7.27.1 16 | '@babel/traverse': *babel 17 | '@babel/types': *babel 18 | '@types/babel__core': ^7.20.5 19 | 20 | '@nuxt/kit': ^3.17.5 21 | '@nuxt/schema': ^3.17.5 22 | 23 | '@vue/compiler-dom': https://pkg.pr.new/@vue/compiler-dom@280bc48 24 | '@vue/compiler-sfc': https://pkg.pr.new/@vue/compiler-sfc@280bc48 25 | '@vue/compiler-vapor': https://pkg.pr.new/@vue/compiler-vapor@280bc48 26 | '@vue/shared': https://pkg.pr.new/@vue/shared@280bc48 27 | 28 | '@vue-macros/common': *vue-macros 29 | '@vue-macros/jsx-directive': *vue-macros 30 | '@vue-macros/test-utils': *vue-macros 31 | '@vue-macros/volar': *vue-macros 32 | 33 | '@ts-macro/tsc': *ts-macro 34 | '@types/hash-sum': ^1.0.2 35 | hash-sum: ^2.0.0 36 | ts-macro: *ts-macro 37 | unplugin: ^2.3.5 38 | unplugin-utils: ^0.2.4 39 | vite: ^6.3.5 40 | vitest: ^3.2.2 41 | vue: https://pkg.pr.new/vue@280bc48 42 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "jsx": "preserve", 5 | "jsxFactory": "h", 6 | "jsxImportSource": "vue-jsx-vapor", 7 | "lib": ["esnext", "DOM"], 8 | "useDefineForClassFields": false, 9 | "customConditions": ["jsx-vapor-dev"], 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "types": ["vite/client"], 14 | "strict": true, 15 | "strictNullChecks": true, 16 | "esModuleInterop": true, 17 | "skipLibCheck": true 18 | }, 19 | "include": ["packages"], 20 | "exclude": ["**/dist", "eslint.config.ts", "playground"] 21 | } 22 | -------------------------------------------------------------------------------- /tsdown.config.ts: -------------------------------------------------------------------------------- 1 | import process from 'node:process' 2 | import { defineConfig, type Options } from 'tsdown' 3 | import Raw from 'unplugin-raw/rolldown' 4 | 5 | export const config = (options: Options = {}) => 6 | defineConfig({ 7 | entry: ['./src/*.ts', '!./**.d.ts'], 8 | clean: true, 9 | format: ['cjs', 'esm'], 10 | watch: !!process.env.DEV, 11 | dts: !process.env.DEV, 12 | external: ['vue'], 13 | define: { 14 | __DEV__: 'true', 15 | }, 16 | plugins: [Raw()], 17 | outputOptions: { 18 | exports: 'named', 19 | }, 20 | ...options, 21 | }) 22 | 23 | export default config() 24 | -------------------------------------------------------------------------------- /tsm.config.ts: -------------------------------------------------------------------------------- 1 | import vueJsxVapor from './packages/vue-jsx-vapor/src/volar' 2 | 3 | export default { 4 | plugins: [ 5 | vueJsxVapor({ 6 | macros: true, 7 | }), 8 | ], 9 | } 10 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config' 2 | 3 | export default defineConfig({ 4 | resolve: { 5 | conditions: ['jsx-vapor-dev'], 6 | }, 7 | test: { 8 | include: ['./packages/**/*.spec.ts'], 9 | }, 10 | define: { 11 | __DEV__: true, 12 | __TEST__: true, 13 | __VERSION__: '"test"', 14 | __GLOBAL__: false, 15 | __ESM_BUNDLER__: true, 16 | __ESM_BROWSER__: false, 17 | __CJS__: true, 18 | __SSR__: true, 19 | __FEATURE_OPTIONS_API__: true, 20 | __FEATURE_SUSPENSE__: true, 21 | __FEATURE_PROD_DEVTOOLS__: false, 22 | __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__: false, 23 | __COMPAT__: true, 24 | }, 25 | }) 26 | --------------------------------------------------------------------------------