├── README.md ├── packages ├── jsx-explorer │ ├── README.md │ ├── src │ │ ├── env.d.ts │ │ ├── editor.worker.ts │ │ ├── index.css │ │ ├── options.tsx │ │ └── index.ts │ ├── vite.config.js │ ├── vite.config.ts │ ├── index.html │ └── package.json ├── babel-plugin-resolve-type │ ├── README.md │ ├── package.json │ ├── test │ │ ├── __snapshots__ │ │ │ └── resolve-type.test.tsx.snap │ │ └── resolve-type.test.tsx │ └── src │ │ └── index.ts ├── babel-plugin-jsx │ ├── test │ │ ├── setup.ts │ │ ├── __snapshots__ │ │ │ ├── resolve-type.test.tsx.snap │ │ │ └── snapshot.test.ts.snap │ │ ├── resolve-type.test.tsx │ │ ├── v-models.test.tsx │ │ ├── v-model.test.tsx │ │ ├── snapshot.test.ts │ │ └── index.test.tsx │ ├── src │ │ ├── slotFlags.ts │ │ ├── patchFlags.ts │ │ ├── interface.ts │ │ ├── sugar-fragment.ts │ │ ├── parseDirectives.ts │ │ ├── index.ts │ │ ├── utils.ts │ │ └── transform-vue-jsx.ts │ ├── package.json │ ├── README-zh_CN.md │ └── README.md └── babel-helper-vue-transform-on │ ├── README.md │ ├── index.d.mts │ ├── index.mjs │ └── package.json ├── pnpm-workspace.yaml ├── netlify.toml ├── eslint.config.js ├── .github ├── workflows │ ├── unit-test.yml │ ├── release.yml │ ├── emoji-helper.yml │ ├── release-commit.yml │ └── issue-reply.yml ├── renovate.json5 ├── ISSUE_TEMPLATE │ ├── question.md │ └── bug_report.md └── PULL_REQUEST_TEMPLATE.md ├── tsdown.config.ts ├── tsconfig.json ├── vitest.config.ts ├── LICENSE ├── package.json ├── .gitignore └── CHANGELOG.md /README.md: -------------------------------------------------------------------------------- 1 | packages/babel-plugin-jsx/README.md -------------------------------------------------------------------------------- /packages/jsx-explorer/README.md: -------------------------------------------------------------------------------- 1 | # JSX Explorer 2 | -------------------------------------------------------------------------------- /packages/babel-plugin-resolve-type/README.md: -------------------------------------------------------------------------------- 1 | # babel-plugin-resolve-type 2 | -------------------------------------------------------------------------------- /packages/jsx-explorer/src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /packages/babel-plugin-jsx/test/setup.ts: -------------------------------------------------------------------------------- 1 | import 'regenerator-runtime/runtime' 2 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - packages/* 3 | catalog: 4 | vue: ^3.5.25 5 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build.environment] 2 | NODE_VERSION = "lts/*" 3 | 4 | [build] 5 | command = "pnpm run build && pnpm run build:playground" 6 | publish = "packages/jsx-explorer/dist" 7 | -------------------------------------------------------------------------------- /packages/babel-helper-vue-transform-on/README.md: -------------------------------------------------------------------------------- 1 | # @vue/babel-helper-vue-transform-on 2 | 3 | A package used internally by vue jsx transformer to transform events. 4 | 5 | on: { click: xx } --> onClick: xx 6 | -------------------------------------------------------------------------------- /packages/babel-helper-vue-transform-on/index.d.mts: -------------------------------------------------------------------------------- 1 | declare function transformOn( 2 | obj: Record, 3 | ): Record<`on${string}`, any> 4 | 5 | export default transformOn 6 | export { transformOn as 'module.exports' } 7 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import { sxzz } from '@sxzz/eslint-config' 2 | 3 | export default sxzz( 4 | {}, 5 | { 6 | rules: { 7 | 'import/no-default-export': 'off', 8 | 'unicorn/filename-case': 'off', 9 | }, 10 | }, 11 | ) 12 | -------------------------------------------------------------------------------- /.github/workflows/unit-test.yml: -------------------------------------------------------------------------------- 1 | name: Unit Test 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | permissions: {} 10 | 11 | jobs: 12 | unit-test: 13 | uses: sxzz/workflows/.github/workflows/unit-test.yml@v1 14 | -------------------------------------------------------------------------------- /packages/babel-helper-vue-transform-on/index.mjs: -------------------------------------------------------------------------------- 1 | function transformOn(obj) { 2 | const result = {} 3 | Object.keys(obj).forEach((evt) => { 4 | result[`on${evt[0].toUpperCase()}${evt.slice(1)}`] = obj[evt] 5 | }) 6 | return result 7 | } 8 | 9 | export default transformOn 10 | export { transformOn as 'module.exports' } 11 | -------------------------------------------------------------------------------- /.github/renovate.json5: -------------------------------------------------------------------------------- 1 | { 2 | $schema: 'https://docs.renovatebot.com/renovate-schema.json', 3 | extends: [ 4 | 'github>haoqunjiang/renovate-presets:npm.json5', 5 | 'schedule:monthly', 6 | ], 7 | packageRules: [ 8 | { 9 | depTypeList: ['peerDependencies'], 10 | enabled: false, 11 | }, 12 | ], 13 | } 14 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | release: 10 | uses: sxzz/workflows/.github/workflows/release.yml@v1 11 | with: 12 | publish: true 13 | build: pnpm run build 14 | permissions: 15 | contents: write 16 | id-token: write 17 | -------------------------------------------------------------------------------- /packages/jsx-explorer/vite.config.js: -------------------------------------------------------------------------------- 1 | import VueJSX from '@vitejs/plugin-vue-jsx' 2 | import { defaultClientConditions, defineConfig } from 'vite' 3 | export default defineConfig({ 4 | resolve: { 5 | conditions: ['dev', ...defaultClientConditions], 6 | }, 7 | define: { 8 | 'process.env.BABEL_TYPES_8_BREAKING': 'false', 9 | }, 10 | plugins: [VueJSX()], 11 | }) 12 | -------------------------------------------------------------------------------- /packages/jsx-explorer/vite.config.ts: -------------------------------------------------------------------------------- 1 | import VueJSX from '@vitejs/plugin-vue-jsx' 2 | import { defaultClientConditions, defineConfig } from 'vite' 3 | 4 | export default defineConfig({ 5 | resolve: { 6 | conditions: ['dev', ...defaultClientConditions], 7 | }, 8 | define: { 9 | 'process.env.BABEL_TYPES_8_BREAKING': 'false', 10 | }, 11 | plugins: [VueJSX()], 12 | }) 13 | -------------------------------------------------------------------------------- /.github/workflows/emoji-helper.yml: -------------------------------------------------------------------------------- 1 | name: Emoji Helper 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | emoji: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions-cool/emoji-helper@040b841cb25e2e6f50151c73b5ce12fee57019d2 # v1.0.0 12 | with: 13 | type: release 14 | emoji: '+1, laugh, heart, hooray, rocket, eyes' 15 | -------------------------------------------------------------------------------- /.github/workflows/release-commit.yml: -------------------------------------------------------------------------------- 1 | name: Publish Any Commit 2 | on: [push, pull_request] 3 | 4 | permissions: {} 5 | 6 | jobs: 7 | release: 8 | if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository 9 | uses: sxzz/workflows/.github/workflows/release-commit.yml@v1 10 | with: 11 | packages: "'./packages/*'" 12 | compact: true 13 | -------------------------------------------------------------------------------- /tsdown.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsdown' 2 | 3 | export default defineConfig({ 4 | workspace: [ 5 | './packages/babel-plugin-jsx', 6 | './packages/babel-plugin-resolve-type', 7 | ], 8 | entry: ['src/index.ts'], 9 | dts: { oxc: true }, 10 | target: 'node20.19', 11 | platform: 'neutral', 12 | inlineOnly: [], 13 | exports: { 14 | devExports: 'dev', 15 | }, 16 | publint: 'ci-only', 17 | }) 18 | -------------------------------------------------------------------------------- /packages/jsx-explorer/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Vue JSX Explorer 7 | 8 | 9 | 10 |
11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /packages/jsx-explorer/src/editor.worker.ts: -------------------------------------------------------------------------------- 1 | import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker' 2 | import tsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker' 3 | 4 | // @ts-ignore 5 | globalThis.MonacoEnvironment = { 6 | globalAPI: true, 7 | getWorker(_: any, label: string) { 8 | if (label === 'typescript' || label === 'javascript') { 9 | return new tsWorker() 10 | } 11 | return new editorWorker() 12 | }, 13 | } 14 | -------------------------------------------------------------------------------- /packages/babel-plugin-jsx/test/__snapshots__/resolve-type.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`resolve type > runtime props > basic 1`] = ` 4 | "import { createVNode as _createVNode } from "vue"; 5 | interface Props { 6 | foo?: string; 7 | } 8 | const App = defineComponent((props: Props) => _createVNode("div", null, null), { 9 | props: { 10 | foo: { 11 | type: String, 12 | required: false 13 | } 14 | }, 15 | name: "App" 16 | });" 17 | `; 18 | -------------------------------------------------------------------------------- /packages/babel-helper-vue-transform-on/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vue/babel-helper-vue-transform-on", 3 | "type": "module", 4 | "version": "2.0.1", 5 | "description": "to help transform on", 6 | "author": "Amour1688 ", 7 | "license": "MIT", 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/vuejs/babel-plugin-jsx.git", 11 | "directory": "packages/babel-helper-vue-transform-on" 12 | }, 13 | "exports": { 14 | ".": "./index.mjs", 15 | "./package.json": "./package.json" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: '❓ Question or need help' 3 | about: Question or need help 4 | title: '[Question] Help' 5 | labels: 'question' 6 | assignees: '' 7 | --- 8 | 9 | ### 🧐 Problem Description 10 | 11 | 12 | 13 | ### 💻 Sample code 14 | 15 | 16 | 17 | ### 🚑 Other information 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /packages/jsx-explorer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vue/jsx-explorer", 3 | "type": "module", 4 | "version": "2.0.1", 5 | "private": true, 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "@babel/standalone": "^7.28.5", 13 | "@vue/babel-plugin-jsx": "workspace:*", 14 | "assert": "^2.1.0", 15 | "monaco-editor": "^0.55.1", 16 | "vue": "catalog:" 17 | }, 18 | "devDependencies": { 19 | "@types/babel__standalone": "^7.1.9", 20 | "@vitejs/plugin-vue-jsx": "^5.1.2", 21 | "vite": "npm:rolldown-vite@latest" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "jsx": "preserve", 5 | "jsxImportSource": "vue", 6 | "lib": ["es2023", "DOM"], 7 | "moduleDetection": "force", 8 | "customConditions": ["dev"], 9 | "module": "preserve", 10 | "moduleResolution": "bundler", 11 | "resolveJsonModule": true, 12 | "types": ["vitest/globals"], 13 | "strict": true, 14 | "noUnusedLocals": true, 15 | "declaration": true, 16 | "noEmit": true, 17 | "esModuleInterop": true, 18 | "verbatimModuleSyntax": true, 19 | "skipLibCheck": true 20 | }, 21 | "include": ["packages/*/src", "packages/*/test", "vitest.config.ts"] 22 | } 23 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { babel } from '@rollup/plugin-babel' 2 | import { defineConfig } from 'vitest/config' 3 | import Jsx from './packages/babel-plugin-jsx/src' 4 | 5 | export default defineConfig({ 6 | esbuild: { 7 | jsx: 'preserve', 8 | }, 9 | plugins: [ 10 | babel({ 11 | babelHelpers: 'bundled', 12 | extensions: ['.tsx', '.jsx'], 13 | plugins: [ 14 | [ 15 | Jsx, 16 | { 17 | optimize: true, 18 | isCustomElement: (tag: string) => tag.startsWith('x-'), 19 | }, 20 | ], 21 | ], 22 | }), 23 | ], 24 | test: { 25 | globals: true, 26 | environment: 'jsdom', 27 | }, 28 | }) 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🐛 Report Bug 3 | about: Report Bug. 4 | title: '[BUG] Report bug' 5 | assignees: 6 | --- 7 | 8 | ### 🐛 Bug description 9 | 10 | 11 | 12 | ### 📝 Steps to reproduce 13 | 14 | 15 | 16 | Reproduction Link (required): 17 | 18 | ### 🏞 Desired result 19 | 20 | 21 | 22 | ### 🚑 Other information 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /packages/babel-plugin-jsx/test/resolve-type.test.tsx: -------------------------------------------------------------------------------- 1 | import { transformAsync } from '@babel/core' 2 | // @ts-expect-error missing types 3 | import typescript from '@babel/plugin-syntax-typescript' 4 | import VueJsx from '../src' 5 | 6 | describe('resolve type', () => { 7 | describe('runtime props', () => { 8 | test('basic', async () => { 9 | const result = await transformAsync( 10 | ` 11 | interface Props { foo?: string } 12 | const App = defineComponent((props: Props) =>
) 13 | `, 14 | { 15 | plugins: [ 16 | [typescript, { isTSX: true }], 17 | [VueJsx, { resolveType: true }], 18 | ], 19 | }, 20 | ) 21 | expect(result!.code).toMatchSnapshot() 22 | }) 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /packages/babel-plugin-jsx/src/slotFlags.ts: -------------------------------------------------------------------------------- 1 | // https://github.com/vuejs/core/blob/main/packages/shared/src/slotFlags.ts 2 | export enum SlotFlags { 3 | /** 4 | * Stable slots that only reference slot props or context state. The slot 5 | * can fully capture its own dependencies so when passed down the parent won't 6 | * need to force the child to update. 7 | */ 8 | STABLE = 1, 9 | /** 10 | * Slots that reference scope variables (v-for or an outer slot prop), or 11 | * has conditional structure (v-if, v-for). The parent will need to force 12 | * the child to update because the slot does not fully capture its dependencies. 13 | */ 14 | DYNAMIC = 2, 15 | /** 16 | * `` being forwarded into a child component. Whether the parent needs 17 | * to update the child is dependent on what kind of slots the parent itself 18 | * received. This has to be refined at runtime, when the child's vnode 19 | * is being created (in `normalizeChildren`) 20 | */ 21 | FORWARDED = 3, 22 | } 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020-present vuejs 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 | -------------------------------------------------------------------------------- /packages/babel-plugin-jsx/src/patchFlags.ts: -------------------------------------------------------------------------------- 1 | // https://github.com/vuejs/core/blob/main/packages/shared/src/patchFlags.ts 2 | 3 | export enum PatchFlags { 4 | TEXT = 1, 5 | CLASS = 1 << 1, 6 | STYLE = 1 << 2, 7 | PROPS = 1 << 3, 8 | FULL_PROPS = 1 << 4, 9 | HYDRATE_EVENTS = 1 << 5, 10 | STABLE_FRAGMENT = 1 << 6, 11 | KEYED_FRAGMENT = 1 << 7, 12 | UNKEYED_FRAGMENT = 1 << 8, 13 | NEED_PATCH = 1 << 9, 14 | DYNAMIC_SLOTS = 1 << 10, 15 | HOISTED = -1, 16 | BAIL = -2, 17 | } 18 | 19 | // dev only flag -> name mapping 20 | export const PatchFlagNames = { 21 | [PatchFlags.TEXT]: 'TEXT', 22 | [PatchFlags.CLASS]: 'CLASS', 23 | [PatchFlags.STYLE]: 'STYLE', 24 | [PatchFlags.PROPS]: 'PROPS', 25 | [PatchFlags.FULL_PROPS]: 'FULL_PROPS', 26 | [PatchFlags.HYDRATE_EVENTS]: 'HYDRATE_EVENTS', 27 | [PatchFlags.STABLE_FRAGMENT]: 'STABLE_FRAGMENT', 28 | [PatchFlags.KEYED_FRAGMENT]: 'KEYED_FRAGMENT', 29 | [PatchFlags.UNKEYED_FRAGMENT]: 'UNKEYED_FRAGMENT', 30 | [PatchFlags.DYNAMIC_SLOTS]: 'DYNAMIC_SLOTS', 31 | [PatchFlags.NEED_PATCH]: 'NEED_PATCH', 32 | [PatchFlags.HOISTED]: 'HOISTED', 33 | [PatchFlags.BAIL]: 'BAIL', 34 | } 35 | -------------------------------------------------------------------------------- /packages/babel-plugin-jsx/src/interface.ts: -------------------------------------------------------------------------------- 1 | import type * as BabelCore from '@babel/core' 2 | import type t from '@babel/types' 3 | import type { Options } from '@vue/babel-plugin-resolve-type' 4 | 5 | export type Slots = t.Identifier | t.ObjectExpression | null 6 | 7 | export type State = { 8 | get: (name: string) => any 9 | set: (name: string, value: any) => any 10 | opts: VueJSXPluginOptions 11 | file: BabelCore.BabelFile 12 | } 13 | 14 | export interface VueJSXPluginOptions { 15 | /** transform `on: { click: xx }` to `onClick: xxx` */ 16 | transformOn?: boolean 17 | /** enable optimization or not. */ 18 | optimize?: boolean 19 | /** merge static and dynamic class / style attributes / onXXX handlers */ 20 | mergeProps?: boolean 21 | /** configuring custom elements */ 22 | isCustomElement?: (tag: string) => boolean 23 | /** enable object slots syntax */ 24 | enableObjectSlots?: boolean 25 | /** Replace the function used when compiling JSX expressions */ 26 | pragma?: string 27 | /** 28 | * (**Experimental**) Infer component metadata from types (e.g. `props`, `emits`, `name`) 29 | * @default false 30 | */ 31 | resolveType?: Options | boolean 32 | } 33 | -------------------------------------------------------------------------------- /packages/babel-plugin-jsx/src/sugar-fragment.ts: -------------------------------------------------------------------------------- 1 | import t from '@babel/types' 2 | import { createIdentifier, FRAGMENT } from './utils' 3 | import type { State } from './interface' 4 | import type { NodePath, Visitor } from '@babel/traverse' 5 | 6 | const transformFragment = ( 7 | path: NodePath, 8 | Fragment: t.JSXIdentifier | t.JSXMemberExpression, 9 | ) => { 10 | const children = path.get('children') || [] 11 | return t.jsxElement( 12 | t.jsxOpeningElement(Fragment, []), 13 | t.jsxClosingElement(Fragment), 14 | children.map(({ node }) => node), 15 | false, 16 | ) 17 | } 18 | 19 | const visitor: Visitor = { 20 | JSXFragment: { 21 | enter(path, state) { 22 | const fragmentCallee = createIdentifier(state, FRAGMENT) 23 | path.replaceWith( 24 | transformFragment( 25 | path, 26 | t.isIdentifier(fragmentCallee) 27 | ? t.jsxIdentifier(fragmentCallee.name) 28 | : t.jsxMemberExpression( 29 | t.jsxIdentifier((fragmentCallee.object as t.Identifier).name), 30 | t.jsxIdentifier((fragmentCallee.property as t.Identifier).name), 31 | ), 32 | ), 33 | ) 34 | }, 35 | }, 36 | } 37 | 38 | export default visitor 39 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 4 | 5 | ### 🤔 What is the nature of this change? 6 | 7 | - [ ] New feature 8 | - [ ] Fix bug 9 | - [ ] Style optimization 10 | - [ ] Code style optimization 11 | - [ ] Performance optimization 12 | - [ ] Build optimization 13 | - [ ] Refactor code or style 14 | - [ ] Test related 15 | - [ ] Other 16 | 17 | ### 🔗 Related Issue 18 | 19 | 22 | 23 | ### 💡 Background or solution 24 | 25 | 28 | 29 | ### 📝 Changelog 30 | 31 | 34 | 35 | | Language | Changelog | 36 | | ---------- | --------- | 37 | | 🇺🇸 English | | 38 | | 🇨🇳 Chinese | | 39 | 40 | ### ☑️ Self Check before Merge 41 | 42 | ⚠️ Please check all items below before review. ⚠️ 43 | 44 | - [ ] Doc is updated/provided or not needed 45 | - [ ] Demo is updated/provided or not needed 46 | - [ ] TypeScript definition is updated/provided or not needed 47 | - [ ] Changelog is provided or not needed 48 | 49 | 50 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-jsx-monorepo", 3 | "type": "module", 4 | "version": "2.0.1", 5 | "private": true, 6 | "packageManager": "pnpm@10.24.0", 7 | "license": "MIT", 8 | "keywords": [ 9 | "vue", 10 | "jsx" 11 | ], 12 | "scripts": { 13 | "dev": "pnpm -C packages/jsx-explorer run dev", 14 | "build": "tsdown", 15 | "build:playground": "pnpm -C packages/jsx-explorer build", 16 | "test": "vitest", 17 | "lint": "eslint --cache .", 18 | "format": "prettier --write .", 19 | "typecheck": "tsgo", 20 | "release": "bumpp -r" 21 | }, 22 | "devDependencies": { 23 | "@babel/plugin-syntax-typescript": "^7.27.1", 24 | "@rollup/plugin-babel": "^6.1.0", 25 | "@sxzz/eslint-config": "^7.4.1", 26 | "@sxzz/prettier-config": "^2.2.6", 27 | "@types/babel__core": "^7.20.5", 28 | "@types/babel__helper-module-imports": "^7.18.3", 29 | "@types/babel__helper-plugin-utils": "^7.10.3", 30 | "@types/node": "^24.10.1", 31 | "@typescript/native-preview": "7.0.0-dev.20251202.1", 32 | "@vitest/coverage-v8": "^4.0.15", 33 | "@vue/babel-plugin-jsx": "workspace:*", 34 | "bumpp": "^10.3.2", 35 | "eslint": "^9.39.1", 36 | "jsdom": "^27.2.0", 37 | "prettier": "3.7.3", 38 | "tsdown": "^0.17.0-beta.5", 39 | "tslib": "^2.8.1", 40 | "typescript": "~5.9.3", 41 | "vite": "^7.2.6", 42 | "vitest": "^4.0.15" 43 | }, 44 | "prettier": "@sxzz/prettier-config" 45 | } 46 | -------------------------------------------------------------------------------- /.github/workflows/issue-reply.yml: -------------------------------------------------------------------------------- 1 | name: Issue Reply 2 | 3 | on: 4 | issues: 5 | types: [labeled] 6 | 7 | jobs: 8 | reply-helper: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: help wanted 12 | if: github.event.label.name == 'help wanted' 13 | uses: actions-cool/issues-helper@45d75b6cf72bf4f254be6230cb887ad002702491 # v3.6.3 14 | with: 15 | actions: create-comment 16 | issue-number: ${{ github.event.issue.number }} 17 | body: | 18 | Hello @${{ github.event.issue.user.login }}. We totally like your proposal/feedback, welcome to send us a Pull Request for it. Please be sure to fill in the default template in the Pull Request, provide changelog/documentation/test cases if needed and make sure CI passed, we will review it soon. We appreciate your effort in advance and looking forward to your contribution! 19 | 20 | - name: need reproduction 21 | if: github.event.label.name == 'need reproduction' 22 | uses: actions-cool/issues-helper@45d75b6cf72bf4f254be6230cb887ad002702491 # v3.6.3 23 | with: 24 | actions: create-comment 25 | issue-number: ${{ github.event.issue.number }} 26 | body: | 27 | Hello @${{ github.event.issue.user.login }}. In order to facilitate location and troubleshooting, we need you to provide a realistic example. Please forking these link [codesandbox](https://codesandbox.io/s/magical-vaughan-byzhk?file=/src/App.jsx) or provide your GitHub repository. 28 | -------------------------------------------------------------------------------- /packages/babel-plugin-resolve-type/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vue/babel-plugin-resolve-type", 3 | "type": "module", 4 | "version": "2.0.1", 5 | "description": "Babel plugin for resolving Vue types.", 6 | "author": "Kevin Deng ", 7 | "license": "MIT", 8 | "funding": "https://github.com/sponsors/sxzz", 9 | "homepage": "https://github.com/vuejs/babel-plugin-jsx/tree/dev/packages/babel-plugin-resolve-type#readme", 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/vuejs/babel-plugin-jsx.git", 13 | "directory": "packages/babel-plugin-resolve-type" 14 | }, 15 | "bugs": { 16 | "url": "https://github.com/vuejs/babel-plugin-jsx/issues" 17 | }, 18 | "exports": { 19 | ".": { 20 | "dev": "./src/index.ts", 21 | "default": "./dist/index.js" 22 | }, 23 | "./package.json": "./package.json" 24 | }, 25 | "main": "./dist/index.js", 26 | "module": "./dist/index.js", 27 | "types": "./dist/index.d.ts", 28 | "files": [ 29 | "dist" 30 | ], 31 | "publishConfig": { 32 | "exports": { 33 | ".": "./dist/index.js", 34 | "./package.json": "./package.json" 35 | } 36 | }, 37 | "peerDependencies": { 38 | "@babel/core": "^7.0.0-0" 39 | }, 40 | "dependencies": { 41 | "@babel/code-frame": "^7.27.1", 42 | "@babel/helper-module-imports": "^7.27.1", 43 | "@babel/helper-plugin-utils": "^7.27.1", 44 | "@babel/parser": "^7.28.5", 45 | "@vue/compiler-sfc": "^3.5.25" 46 | }, 47 | "devDependencies": { 48 | "@babel/core": "^7.28.5", 49 | "@types/babel__code-frame": "^7.0.6", 50 | "vue": "catalog:" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /packages/jsx-explorer/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: 4 | -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, 5 | Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; 6 | } 7 | 8 | #header { 9 | position: absolute; 10 | top: 0; 11 | left: 0; 12 | right: 0; 13 | height: 60px; 14 | box-sizing: border-box; 15 | background-color: #1e1e1e; 16 | border-bottom: 1px solid #333; 17 | padding: 0.3em 1.6em; 18 | color: #fff; 19 | z-index: 1; 20 | } 21 | 22 | h1 { 23 | font-size: 18px; 24 | display: inline-block; 25 | margin-right: 15px; 26 | } 27 | 28 | #options-wrapper { 29 | position: absolute; 30 | top: 20px; 31 | right: 10px; 32 | } 33 | 34 | #options-wrapper:hover #options { 35 | display: block; 36 | } 37 | 38 | #options-label { 39 | cursor: pointer; 40 | text-align: right; 41 | padding-right: 10px; 42 | font-weight: bold; 43 | } 44 | 45 | #options { 46 | display: none; 47 | margin-top: 15px; 48 | list-style-type: none; 49 | background-color: #1e1e1e; 50 | border: 1px solid #333; 51 | padding: 15px 30px; 52 | } 53 | 54 | #options li { 55 | margin: 8px 0; 56 | } 57 | 58 | #header a { 59 | font-weight: 600; 60 | color: rgb(101, 163, 221); 61 | } 62 | 63 | #header .label { 64 | font-weight: bold; 65 | } 66 | 67 | #header input { 68 | margin-right: 6px; 69 | } 70 | 71 | #header label { 72 | color: #999; 73 | } 74 | 75 | .editor { 76 | position: absolute; 77 | top: 60px; 78 | bottom: 0; 79 | box-sizing: border-box; 80 | } 81 | 82 | #source { 83 | left: 0; 84 | width: 45%; 85 | } 86 | 87 | #output { 88 | left: 45%; 89 | width: 55%; 90 | } 91 | 92 | .highlight { 93 | background-color: rgba(46, 120, 190, 0.5); 94 | } 95 | -------------------------------------------------------------------------------- /packages/babel-plugin-jsx/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vue/babel-plugin-jsx", 3 | "type": "module", 4 | "version": "2.0.1", 5 | "description": "Babel plugin for Vue 3 JSX", 6 | "author": "Amour1688 ", 7 | "license": "MIT", 8 | "homepage": "https://github.com/vuejs/babel-plugin-jsx/tree/dev/packages/babel-plugin-jsx#readme", 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/vuejs/babel-plugin-jsx.git", 12 | "directory": "packages/babel-plugin-jsx" 13 | }, 14 | "bugs": { 15 | "url": "https://github.com/vuejs/babel-plugin-jsx/issues" 16 | }, 17 | "exports": { 18 | ".": { 19 | "dev": "./src/index.ts", 20 | "default": "./dist/index.js" 21 | }, 22 | "./package.json": "./package.json" 23 | }, 24 | "main": "./dist/index.js", 25 | "module": "./dist/index.js", 26 | "types": "./dist/index.d.ts", 27 | "files": [ 28 | "dist" 29 | ], 30 | "publishConfig": { 31 | "exports": { 32 | ".": "./dist/index.js", 33 | "./package.json": "./package.json" 34 | } 35 | }, 36 | "peerDependencies": { 37 | "@babel/core": "^7.0.0-0" 38 | }, 39 | "peerDependenciesMeta": { 40 | "@babel/core": { 41 | "optional": true 42 | } 43 | }, 44 | "dependencies": { 45 | "@babel/helper-module-imports": "^7.27.1", 46 | "@babel/helper-plugin-utils": "^7.27.1", 47 | "@babel/plugin-syntax-jsx": "^7.27.1", 48 | "@babel/template": "^7.27.2", 49 | "@babel/traverse": "^7.28.5", 50 | "@babel/types": "^7.28.5", 51 | "@vue/babel-helper-vue-transform-on": "workspace:*", 52 | "@vue/babel-plugin-resolve-type": "workspace:*", 53 | "@vue/shared": "^3.5.25" 54 | }, 55 | "devDependencies": { 56 | "@babel/core": "^7.28.5", 57 | "@babel/preset-env": "^7.28.5", 58 | "@types/babel__template": "^7.4.4", 59 | "@types/babel__traverse": "^7.28.0", 60 | "@vue/test-utils": "^2.4.6", 61 | "regenerator-runtime": "^0.14.1", 62 | "vue": "catalog:" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | pnpm-debug.log* 5 | 6 | # Diagnostic reports (https://nodejs.org/api/report.html) 7 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 8 | 9 | # Runtime data 10 | pids 11 | *.pid 12 | *.seed 13 | *.pid.lock 14 | 15 | # Directory for instrumented libs generated by jscoverage/JSCover 16 | lib-cov 17 | 18 | # Coverage directory used by tools like istanbul 19 | coverage 20 | *.lcov 21 | 22 | # nyc test coverage 23 | .nyc_output 24 | 25 | # Grunt intermediate storage (https://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 | # TypeScript cache 45 | *.tsbuildinfo 46 | 47 | # Optional npm cache directory 48 | .npm 49 | 50 | # Optional eslint cache 51 | .eslintcache 52 | 53 | # Microbundle cache 54 | .rpt2_cache/ 55 | .rts2_cache_cjs/ 56 | .rts2_cache_es/ 57 | .rts2_cache_umd/ 58 | 59 | # Optional REPL history 60 | .node_repl_history 61 | 62 | # Output of 'npm pack' 63 | *.tgz 64 | 65 | # Yarn Integrity file 66 | .yarn-integrity 67 | 68 | # dotenv environment variables file 69 | .env 70 | .env.test 71 | 72 | # parcel-bundler cache (https://parceljs.org/) 73 | .cache 74 | 75 | # Next.js build output 76 | .next 77 | 78 | # Nuxt.js build / generate output 79 | .nuxt 80 | dist 81 | 82 | # Gatsby files 83 | .cache/ 84 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 85 | # https://nextjs.org/blog/next-9-1#public-directory-support 86 | # public 87 | 88 | # vuepress build output 89 | .vuepress/dist 90 | 91 | # Serverless directories 92 | .serverless/ 93 | 94 | # FuseBox cache 95 | .fusebox/ 96 | 97 | # DynamoDB Local files 98 | .dynamodb/ 99 | 100 | # TernJS port file 101 | .tern-port 102 | 103 | dist 104 | .DS_Store 105 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.1.1 2 | 3 | `2021-10-17` 4 | 5 | - 🐞 fix: wrong compilation result when _Fragment is imported (#518) 6 | 7 | ## 1.1.0 8 | 9 | `2021-09-30` 10 | 11 | - 🌟 feat: allow string arguments on directives [#496] 12 | 13 | ## 1.0.6 14 | 15 | `2021-05-02` 16 | 17 | - 🐞 fix wrong compilation result of custom directives 18 | 19 | ## 1.0.5 20 | 21 | `2021-04-18` 22 | 23 | - 🐞 using v-slots without children should not be spread 24 | 25 | ## 1.0.4 26 | 27 | `2021-03-29` 28 | 29 | - 🌟 add pragma option and support @jsx annotation [#322] 30 | 31 | ## 1.0.3 32 | 33 | `2021-02-06` 34 | 35 | - 🐞 the child nodes of `keepAlive` should not be transformed to slots 36 | 37 | ## 1.0.2 38 | 39 | `2021-01-17` 40 | 41 | - 🛠 merge generated imports [#274] 42 | 43 | ## 1.0.1 44 | 45 | `2021-01-09` 46 | 47 | - 🌟 support optional `enableObjectSlots` [#259] 48 | 49 | ## 1.0.0 50 | 51 | `2020-12-26` 52 | 53 | ## 1.0.0-rc.5 54 | 55 | `2020-12-12` 56 | 57 | - 🐞 wrong result in slots array map expression [#218](https://github.com/vuejs/babel-plugin-jsx/pull/218) 58 | 59 | ## 1.0.0-rc.4 60 | 61 | `2020-12-08` 62 | 63 | - 🌟 support multiple v-models 64 | - 🌟 support support passing object slots via JSX children 65 | 66 | ## 1.0.0-rc.3 67 | 68 | `2020-09-14` 69 | 70 | - 🐞 fix mergeProps order error ([bf59811](https://github.com/vuejs/babel-plugin-jsx/commit/bf59811f4334dbc30fd62ba33a33926031dd8835)) 71 | - 🌟 optional mergeProps ([e16695d](https://github.com/vuejs/babel-plugin-jsx/commit/e16695d87e269000055828f32492690c4cf796b2)) 72 | 73 | ## 1.0.0-rc.2 74 | 75 | `2020-08-28` 76 | 77 | - 🌟 rename package scope from ant-design-vue to vue ([09c220e](https://github.com/vuejs/babel-plugin-jsx/commit/09c220eeff98bbec757a83d41af1f0731652d00c)) 78 | - 🌟 replace namespace imports with named imports [#67](https://github.com/vuejs/babel-plugin-jsx/pull/67) 79 | 80 | ## 1.0.0-rc.1 81 | 82 | `2020-07-29` 83 | 84 | - 🌟 support `v-html` and `v-text` 85 | - 🌟 add `isCustomElement` 86 | - 🛠 do not optimize by default 87 | 88 | ### Breaking Change 89 | 90 | - remove `compatibleProps` 91 | - rename `usePatchFlag` as `optimize` 92 | 93 | ## 1.0.0-beta.4 94 | 95 | `2020-07-22` 96 | 97 | - 🐞 Properly force update on forwarded slots [#33](https://github.com/vueComponent/jsx/pull/33) 98 | 99 | ## 1.0.0-beta.3 100 | 101 | `2020-07-15` 102 | 103 | - 🐞 Fix directive with single param did not work 104 | 105 | ## 1.0.0-beta.2 106 | 107 | `2020-07-15` 108 | 109 | - 🐞 Fix walksScope throw error when path.parentPath is null [#25](https://github.com/vueComponent/jsx/pull/25) 110 | - 🐞 Fix fragment with condition fails with undefined vnode [#28](https://github.com/vueComponent/jsx/pull/28) 111 | - 🌟 New Directive API 112 | 113 | ## 1.0.0-beta.1 114 | 115 | `2020-07-12` 116 | 117 | - 🐞 Fix component doesn't render when variables outside slot 118 | - 🌟 support `vSlots` 119 | - 🌟 optional `usePatchFlag` 120 | -------------------------------------------------------------------------------- /packages/babel-plugin-jsx/test/v-models.test.tsx: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils' 2 | import { defineComponent } from 'vue' 3 | 4 | test('single value binding should work', async () => { 5 | const Child = defineComponent({ 6 | props: { 7 | foo: Number, 8 | }, 9 | emits: ['update:foo'], 10 | setup(props, { emit }) { 11 | const handleClick = () => { 12 | emit('update:foo', 3) 13 | } 14 | return () =>
{props.foo}
15 | }, 16 | }) 17 | 18 | const wrapper = mount( 19 | defineComponent({ 20 | data() { 21 | return { 22 | foo: 1, 23 | } 24 | }, 25 | render() { 26 | return 27 | }, 28 | }), 29 | ) 30 | 31 | expect(wrapper.html()).toBe('
1
') 32 | wrapper.vm.$data.foo += 1 33 | await wrapper.vm.$nextTick() 34 | expect(wrapper.html()).toBe('
2
') 35 | await wrapper.trigger('click') 36 | expect(wrapper.html()).toBe('
3
') 37 | }) 38 | 39 | test('multiple values binding should work', async () => { 40 | const Child = defineComponent({ 41 | props: { 42 | foo: Number, 43 | bar: Number, 44 | }, 45 | emits: ['update:foo', 'update:bar'], 46 | setup(props, { emit }) { 47 | const handleClick = () => { 48 | emit('update:foo', 3) 49 | emit('update:bar', 2) 50 | } 51 | return () => ( 52 |
53 | {props.foo},{props.bar} 54 |
55 | ) 56 | }, 57 | }) 58 | 59 | const wrapper = mount( 60 | defineComponent({ 61 | data() { 62 | return { 63 | foo: 1, 64 | bar: 0, 65 | } 66 | }, 67 | render() { 68 | return ( 69 | 75 | ) 76 | }, 77 | }), 78 | ) 79 | 80 | expect(wrapper.html()).toBe('
1,0
') 81 | wrapper.vm.$data.foo += 1 82 | wrapper.vm.$data.bar += 1 83 | await wrapper.vm.$nextTick() 84 | expect(wrapper.html()).toBe('
2,1
') 85 | await wrapper.trigger('click') 86 | expect(wrapper.html()).toBe('
3,2
') 87 | }) 88 | 89 | test('modifier should work', async () => { 90 | const Child = defineComponent({ 91 | props: { 92 | foo: { 93 | type: Number, 94 | default: 0, 95 | }, 96 | fooModifiers: { 97 | default: () => ({ double: false }), 98 | }, 99 | }, 100 | emits: ['update:foo'], 101 | setup(props, { emit }) { 102 | const handleClick = () => { 103 | emit('update:foo', 3) 104 | } 105 | return () => ( 106 |
107 | {props.fooModifiers.double ? props.foo * 2 : props.foo} 108 |
109 | ) 110 | }, 111 | }) 112 | 113 | const wrapper = mount( 114 | defineComponent({ 115 | data() { 116 | return { 117 | foo: 1, 118 | } 119 | }, 120 | render() { 121 | return 122 | }, 123 | }), 124 | ) 125 | 126 | expect(wrapper.html()).toBe('
2
') 127 | wrapper.vm.$data.foo += 1 128 | await wrapper.vm.$nextTick() 129 | expect(wrapper.html()).toBe('
4
') 130 | await wrapper.trigger('click') 131 | expect(wrapper.html()).toBe('
6
') 132 | }) 133 | -------------------------------------------------------------------------------- /packages/jsx-explorer/src/options.tsx: -------------------------------------------------------------------------------- 1 | import { createApp, defineComponent, reactive } from 'vue' 2 | import type { VueJSXPluginOptions } from '@vue/babel-plugin-jsx' 3 | 4 | export type { VueJSXPluginOptions } 5 | 6 | export const compilerOptions: VueJSXPluginOptions = reactive({ 7 | mergeProps: true, 8 | optimize: false, 9 | transformOn: false, 10 | enableObjectSlots: true, 11 | resolveType: false, 12 | }) 13 | 14 | const App = defineComponent({ 15 | setup() { 16 | return () => [ 17 | <> 18 |

Vue 3 JSX Explorer

19 | 23 | History 24 | 25 |
26 |
Options ↘
27 |
    28 |
  • 29 | { 35 | compilerOptions.mergeProps = ( 36 | e.target as HTMLInputElement 37 | ).checked 38 | }} 39 | /> 40 | 41 |
  • 42 | 43 |
  • 44 | { 50 | compilerOptions.optimize = ( 51 | e.target as HTMLInputElement 52 | ).checked 53 | }} 54 | /> 55 | 56 |
  • 57 | 58 |
  • 59 | { 65 | compilerOptions.transformOn = ( 66 | e.target as HTMLInputElement 67 | ).checked 68 | }} 69 | /> 70 | 71 |
  • 72 | 73 |
  • 74 | { 80 | compilerOptions.enableObjectSlots = ( 81 | e.target as HTMLInputElement 82 | ).checked 83 | }} 84 | /> 85 | 86 |
  • 87 | 88 |
  • 89 | { 95 | compilerOptions.resolveType = ( 96 | e.target as HTMLInputElement 97 | ).checked 98 | }} 99 | /> 100 | 101 |
  • 102 |
103 |
104 | , 105 | ] 106 | }, 107 | }) 108 | 109 | export function initOptions() { 110 | createApp(App).mount(document.querySelector('#header')!) 111 | } 112 | -------------------------------------------------------------------------------- /packages/jsx-explorer/src/index.ts: -------------------------------------------------------------------------------- 1 | // @ts-expect-error missing types 2 | import typescript from '@babel/plugin-syntax-typescript' 3 | import { transform } from '@babel/standalone' 4 | import babelPluginJsx from '@vue/babel-plugin-jsx' 5 | import * as monaco from 'monaco-editor' 6 | import { watchEffect } from 'vue' 7 | import { 8 | compilerOptions, 9 | initOptions, 10 | type VueJSXPluginOptions, 11 | } from './options' 12 | import './editor.worker' 13 | import './index.css' 14 | 15 | main() 16 | 17 | interface PersistedState { 18 | src: string 19 | options: VueJSXPluginOptions 20 | } 21 | 22 | function main() { 23 | const persistedState: PersistedState = JSON.parse( 24 | localStorage.getItem('state') || '{}', 25 | ) 26 | 27 | Object.assign(compilerOptions, persistedState.options) 28 | 29 | const sharedEditorOptions: monaco.editor.IStandaloneEditorConstructionOptions = 30 | { 31 | language: 'typescript', 32 | tabSize: 2, 33 | theme: 'vs-dark', 34 | fontSize: 14, 35 | wordWrap: 'on', 36 | scrollBeyondLastLine: false, 37 | renderWhitespace: 'selection', 38 | contextmenu: false, 39 | minimap: { 40 | enabled: false, 41 | }, 42 | } 43 | 44 | monaco.typescript.typescriptDefaults.setDiagnosticsOptions({ 45 | noSemanticValidation: true, 46 | }) 47 | monaco.typescript.typescriptDefaults.setCompilerOptions({ 48 | allowJs: true, 49 | allowNonTsExtensions: true, 50 | jsx: monaco.typescript.JsxEmit.Preserve, 51 | target: monaco.typescript.ScriptTarget.Latest, 52 | module: monaco.typescript.ModuleKind.ESNext, 53 | isolatedModules: true, 54 | }) 55 | 56 | const editor = monaco.editor.create(document.querySelector('#source')!, { 57 | ...sharedEditorOptions, 58 | model: monaco.editor.createModel( 59 | decodeURIComponent(globalThis.location.hash.slice(1)) || 60 | persistedState.src || 61 | `import { defineComponent } from 'vue' 62 | 63 | const App = defineComponent((props) =>
Hello World
)`, 64 | 'typescript', 65 | monaco.Uri.parse('file:///app.tsx'), 66 | ), 67 | }) 68 | 69 | const output = monaco.editor.create(document.querySelector('#output')!, { 70 | readOnly: true, 71 | ...sharedEditorOptions, 72 | model: monaco.editor.createModel( 73 | '', 74 | 'typescript', 75 | monaco.Uri.parse('file:///output.tsx'), 76 | ), 77 | }) 78 | 79 | const reCompile = () => { 80 | const src = editor.getValue() 81 | const state = JSON.stringify({ 82 | src, 83 | options: compilerOptions, 84 | }) 85 | localStorage.setItem('state', state) 86 | globalThis.location.hash = encodeURIComponent(src) 87 | console.clear() 88 | try { 89 | const res = transform(src, { 90 | babelrc: false, 91 | plugins: [ 92 | [babelPluginJsx, { ...compilerOptions }], 93 | [typescript, { isTSX: true }], 94 | ], 95 | ast: true, 96 | }) 97 | console.info('AST', res.ast!) 98 | output.setValue(res.code!) 99 | } catch (error: any) { 100 | console.error(error) 101 | output.setValue(error.message!) 102 | } 103 | } 104 | 105 | // handle resize 106 | window.addEventListener('resize', () => { 107 | editor.layout() 108 | output.layout() 109 | }) 110 | 111 | initOptions() 112 | watchEffect(reCompile) 113 | 114 | // update compile output when input changes 115 | editor.onDidChangeModelContent(debounce(reCompile)) 116 | } 117 | 118 | function debounce any>(fn: T, delay = 300): T { 119 | let prevTimer: ReturnType | null = null 120 | return ((...args: any[]) => { 121 | if (prevTimer) { 122 | clearTimeout(prevTimer) 123 | } 124 | prevTimer = globalThis.setTimeout(() => { 125 | fn(...args) 126 | prevTimer = null 127 | }, delay) 128 | }) as any 129 | } 130 | -------------------------------------------------------------------------------- /packages/babel-plugin-resolve-type/test/__snapshots__/resolve-type.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`resolve type > defineComponent scope > fake 1`] = ` 4 | "const defineComponent = () => {}; 5 | defineComponent((props: { 6 | msg?: string; 7 | }) => { 8 | return () =>
; 9 | });" 10 | `; 11 | 12 | exports[`resolve type > defineComponent scope > import sub-package 1`] = ` 13 | "import { defineComponent } from 'vue/dist/vue.esm-bundler'; 14 | defineComponent((props: { 15 | msg?: string; 16 | }) => { 17 | return () =>
; 18 | }, { 19 | props: { 20 | msg: { 21 | type: String, 22 | required: false 23 | } 24 | } 25 | });" 26 | `; 27 | 28 | exports[`resolve type > defineComponent scope > w/o import 1`] = ` 29 | "defineComponent((props: { 30 | msg?: string; 31 | }) => { 32 | return () =>
; 33 | }, { 34 | props: { 35 | msg: { 36 | type: String, 37 | required: false 38 | } 39 | } 40 | });" 41 | `; 42 | 43 | exports[`resolve type > infer component name > identifier options 1`] = ` 44 | "import { defineComponent } from 'vue'; 45 | const Foo = defineComponent(() => {}, { 46 | name: "Foo", 47 | ...opts 48 | });" 49 | `; 50 | 51 | exports[`resolve type > infer component name > no options 1`] = ` 52 | "import { defineComponent } from 'vue'; 53 | const Foo = defineComponent(() => {}, { 54 | name: "Foo" 55 | });" 56 | `; 57 | 58 | exports[`resolve type > infer component name > object options 1`] = ` 59 | "import { defineComponent } from 'vue'; 60 | const Foo = defineComponent(() => {}, { 61 | name: "Foo", 62 | foo: 'bar' 63 | });" 64 | `; 65 | 66 | exports[`resolve type > infer component name > rest param 1`] = ` 67 | "import { defineComponent } from 'vue'; 68 | const Foo = defineComponent(() => {}, ...args);" 69 | `; 70 | 71 | exports[`resolve type > runtime emits > basic 1`] = ` 72 | "import { type SetupContext, defineComponent } from 'vue'; 73 | defineComponent((props, { 74 | emit 75 | }: SetupContext<{ 76 | change(val: string): void; 77 | click(): void; 78 | }>) => { 79 | emit('change'); 80 | return () => {}; 81 | }, { 82 | emits: ["change", "click"] 83 | });" 84 | `; 85 | 86 | exports[`resolve type > runtime emits > with generic emit type 1`] = ` 87 | "import { type SetupContext, defineComponent } from 'vue'; 88 | type EmitEvents = { 89 | change(val: string): void; 90 | click(): void; 91 | }; 92 | defineComponent<{}, EmitEvents>((props, { 93 | emit 94 | }) => { 95 | emit('change'); 96 | return () => {}; 97 | }, { 98 | emits: ["change", "click"] 99 | });" 100 | `; 101 | 102 | exports[`resolve type > runtime props > basic 1`] = ` 103 | "import { defineComponent, h } from 'vue'; 104 | interface Props { 105 | msg: string; 106 | optional?: boolean; 107 | } 108 | interface Props2 { 109 | set: Set; 110 | } 111 | defineComponent((props: Props & Props2) => { 112 | return () => h('div', props.msg); 113 | }, { 114 | props: { 115 | msg: { 116 | type: String, 117 | required: true 118 | }, 119 | optional: { 120 | type: Boolean, 121 | required: false 122 | }, 123 | set: { 124 | type: Set, 125 | required: true 126 | } 127 | } 128 | });" 129 | `; 130 | 131 | exports[`resolve type > runtime props > with dynamic default value 1`] = ` 132 | "import { defineComponent, h, _mergeDefaults } from 'vue'; 133 | const defaults = {}; 134 | defineComponent((props: { 135 | msg?: string; 136 | } = defaults) => { 137 | return () => h('div', props.msg); 138 | }, { 139 | props: /*@__PURE__*/_mergeDefaults({ 140 | msg: { 141 | type: String, 142 | required: false 143 | } 144 | }, defaults) 145 | });" 146 | `; 147 | 148 | exports[`resolve type > runtime props > with generic 1`] = ` 149 | "import { defineComponent, h } from 'vue'; 150 | interface Props { 151 | msg: string; 152 | optional?: boolean; 153 | } 154 | defineComponent(props => { 155 | return () => h('div', props.msg); 156 | }, { 157 | props: { 158 | msg: { 159 | type: String, 160 | required: true 161 | }, 162 | optional: { 163 | type: Boolean, 164 | required: false 165 | } 166 | } 167 | });" 168 | `; 169 | 170 | exports[`resolve type > runtime props > with static default value 1`] = ` 171 | "import { defineComponent, h } from 'vue'; 172 | defineComponent((props: { 173 | msg?: string; 174 | } = { 175 | msg: 'hello' 176 | }) => { 177 | return () => h('div', props.msg); 178 | }, { 179 | props: { 180 | msg: { 181 | type: String, 182 | required: false, 183 | default: 'hello' 184 | } 185 | } 186 | });" 187 | `; 188 | 189 | exports[`resolve type > runtime props > with static default value and generic 1`] = ` 190 | "import { defineComponent, h } from 'vue'; 191 | type Props = { 192 | msg: string; 193 | optional?: boolean; 194 | }; 195 | defineComponent((props = { 196 | msg: 'hello' 197 | }) => { 198 | return () => h('div', props.msg); 199 | }, { 200 | props: { 201 | msg: { 202 | type: String, 203 | required: true, 204 | default: 'hello' 205 | }, 206 | optional: { 207 | type: Boolean, 208 | required: false 209 | } 210 | } 211 | });" 212 | `; 213 | 214 | exports[`resolve type > w/ tsx 1`] = ` 215 | "import { type SetupContext, defineComponent } from 'vue'; 216 | defineComponent(() => { 217 | return () =>
; 218 | }, {});" 219 | `; 220 | -------------------------------------------------------------------------------- /packages/babel-plugin-resolve-type/test/resolve-type.test.tsx: -------------------------------------------------------------------------------- 1 | import { transformAsync } from '@babel/core' 2 | // @ts-expect-error missing types 3 | import typescript from '@babel/plugin-syntax-typescript' 4 | import ResolveType from '../src' 5 | 6 | async function transform(code: string): Promise { 7 | const result = await transformAsync(code, { 8 | plugins: [[typescript, { isTSX: true }], ResolveType], 9 | }) 10 | return result!.code! 11 | } 12 | 13 | describe('resolve type', () => { 14 | describe('runtime props', () => { 15 | test('basic', async () => { 16 | const result = await transform( 17 | ` 18 | import { defineComponent, h } from 'vue'; 19 | interface Props { 20 | msg: string; 21 | optional?: boolean; 22 | } 23 | interface Props2 { 24 | set: Set; 25 | } 26 | defineComponent((props: Props & Props2) => { 27 | return () => h('div', props.msg); 28 | }) 29 | `, 30 | ) 31 | expect(result).toMatchSnapshot() 32 | }) 33 | 34 | test('with generic', async () => { 35 | const result = await transform( 36 | ` 37 | import { defineComponent, h } from 'vue'; 38 | interface Props { 39 | msg: string; 40 | optional?: boolean; 41 | } 42 | defineComponent((props) => { 43 | return () => h('div', props.msg); 44 | }) 45 | `, 46 | ) 47 | expect(result).toMatchSnapshot() 48 | }) 49 | 50 | test('with static default value and generic', async () => { 51 | const result = await transform( 52 | ` 53 | import { defineComponent, h } from 'vue'; 54 | type Props = { 55 | msg: string; 56 | optional?: boolean; 57 | }; 58 | defineComponent((props = { msg: 'hello' }) => { 59 | return () => h('div', props.msg); 60 | }) 61 | `, 62 | ) 63 | expect(result).toMatchSnapshot() 64 | }) 65 | 66 | test('with static default value', async () => { 67 | const result = await transform( 68 | ` 69 | import { defineComponent, h } from 'vue'; 70 | defineComponent((props: { msg?: string } = { msg: 'hello' }) => { 71 | return () => h('div', props.msg); 72 | }) 73 | `, 74 | ) 75 | expect(result).toMatchSnapshot() 76 | }) 77 | 78 | test('with dynamic default value', async () => { 79 | const result = await transform( 80 | ` 81 | import { defineComponent, h } from 'vue'; 82 | const defaults = {} 83 | defineComponent((props: { msg?: string } = defaults) => { 84 | return () => h('div', props.msg); 85 | }) 86 | `, 87 | ) 88 | expect(result).toMatchSnapshot() 89 | }) 90 | }) 91 | 92 | describe('runtime emits', () => { 93 | test('basic', async () => { 94 | const result = await transform( 95 | ` 96 | import { type SetupContext, defineComponent } from 'vue'; 97 | defineComponent( 98 | ( 99 | props, 100 | { emit }: SetupContext<{ change(val: string): void; click(): void }> 101 | ) => { 102 | emit('change'); 103 | return () => {}; 104 | } 105 | ); 106 | `, 107 | ) 108 | expect(result).toMatchSnapshot() 109 | }) 110 | 111 | test('with generic emit type', async () => { 112 | const result = await transform( 113 | ` 114 | import { type SetupContext, defineComponent } from 'vue'; 115 | type EmitEvents = { 116 | change(val: string): void; 117 | click(): void; 118 | }; 119 | defineComponent<{}, EmitEvents>( 120 | (props, { emit }) => { 121 | emit('change'); 122 | return () => {}; 123 | } 124 | ); 125 | `, 126 | ) 127 | expect(result).toMatchSnapshot() 128 | }) 129 | }) 130 | 131 | test('w/ tsx', async () => { 132 | const result = await transform( 133 | ` 134 | import { type SetupContext, defineComponent } from 'vue'; 135 | defineComponent(() => { 136 | return () =>
; 137 | }); 138 | `, 139 | ) 140 | expect(result).toMatchSnapshot() 141 | }) 142 | 143 | describe('defineComponent scope', () => { 144 | test('fake', async () => { 145 | const result = await transform( 146 | ` 147 | const defineComponent = () => {}; 148 | defineComponent((props: { msg?: string }) => { 149 | return () =>
; 150 | }); 151 | `, 152 | ) 153 | expect(result).toMatchSnapshot() 154 | }) 155 | 156 | test('w/o import', async () => { 157 | const result = await transform( 158 | ` 159 | defineComponent((props: { msg?: string }) => { 160 | return () =>
; 161 | }); 162 | `, 163 | ) 164 | expect(result).toMatchSnapshot() 165 | }) 166 | 167 | test('import sub-package', async () => { 168 | const result = await transform( 169 | ` 170 | import { defineComponent } from 'vue/dist/vue.esm-bundler'; 171 | defineComponent((props: { msg?: string }) => { 172 | return () =>
; 173 | }); 174 | `, 175 | ) 176 | expect(result).toMatchSnapshot() 177 | }) 178 | }) 179 | 180 | describe('infer component name', () => { 181 | test('no options', async () => { 182 | const result = await transform( 183 | ` 184 | import { defineComponent } from 'vue'; 185 | const Foo = defineComponent(() => {}) 186 | `, 187 | ) 188 | expect(result).toMatchSnapshot() 189 | }) 190 | 191 | test('object options', async () => { 192 | const result = await transform( 193 | ` 194 | import { defineComponent } from 'vue'; 195 | const Foo = defineComponent(() => {}, { foo: 'bar' }) 196 | `, 197 | ) 198 | expect(result).toMatchSnapshot() 199 | }) 200 | 201 | test('identifier options', async () => { 202 | const result = await transform( 203 | ` 204 | import { defineComponent } from 'vue'; 205 | const Foo = defineComponent(() => {}, opts) 206 | `, 207 | ) 208 | expect(result).toMatchSnapshot() 209 | }) 210 | 211 | test('rest param', async () => { 212 | const result = await transform( 213 | ` 214 | import { defineComponent } from 'vue'; 215 | const Foo = defineComponent(() => {}, ...args) 216 | `, 217 | ) 218 | expect(result).toMatchSnapshot() 219 | }) 220 | }) 221 | }) 222 | -------------------------------------------------------------------------------- /packages/babel-plugin-jsx/src/parseDirectives.ts: -------------------------------------------------------------------------------- 1 | import t from '@babel/types' 2 | import { createIdentifier } from './utils' 3 | import type { State } from './interface' 4 | import type { NodePath } from '@babel/traverse' 5 | 6 | export type Tag = 7 | | t.Identifier 8 | | t.MemberExpression 9 | | t.StringLiteral 10 | | t.CallExpression 11 | 12 | /** 13 | * Get JSX element type 14 | * 15 | * @param path Path 16 | */ 17 | const getType = (path: NodePath) => { 18 | const typePath = path.get('attributes').find((attribute) => { 19 | if (!attribute.isJSXAttribute()) { 20 | return false 21 | } 22 | return ( 23 | attribute.get('name').isJSXIdentifier() && 24 | (attribute.get('name') as NodePath).node.name === 'type' 25 | ) 26 | }) as NodePath | undefined 27 | 28 | return typePath ? typePath.get('value').node : null 29 | } 30 | 31 | const parseModifiers = (value: any): string[] => 32 | t.isArrayExpression(value) 33 | ? value.elements 34 | .map((el) => (t.isStringLiteral(el) ? el.value : '')) 35 | .filter(Boolean) 36 | : [] 37 | 38 | const parseDirectives = (params: { 39 | name: string 40 | path: NodePath 41 | value: t.Expression | null 42 | state: State 43 | tag: Tag 44 | isComponent: boolean 45 | }) => { 46 | const { path, value, state, tag, isComponent } = params 47 | const args: Array = [] 48 | const vals: t.Expression[] = [] 49 | const modifiersSet: Set[] = [] 50 | 51 | let directiveName 52 | let directiveArgument 53 | let directiveModifiers 54 | if ('namespace' in path.node.name) { 55 | ;[directiveName, directiveArgument] = params.name.split(':') 56 | directiveName = path.node.name.namespace.name 57 | directiveArgument = path.node.name.name.name 58 | directiveModifiers = directiveArgument.split('_').slice(1) 59 | } else { 60 | const underscoreModifiers = params.name.split('_') 61 | directiveName = underscoreModifiers.shift() || '' 62 | directiveModifiers = underscoreModifiers 63 | } 64 | directiveName = directiveName 65 | .replace(/^v/, '') 66 | .replace(/^-/, '') 67 | .replace(/^\S/, (s: string) => s.toLowerCase()) 68 | 69 | if (directiveArgument) { 70 | args.push(t.stringLiteral(directiveArgument.split('_')[0])) 71 | } 72 | 73 | const isVModels = directiveName === 'models' 74 | const isVModel = directiveName === 'model' 75 | if (isVModel && !path.get('value').isJSXExpressionContainer()) { 76 | throw new Error('You have to use JSX Expression inside your v-model') 77 | } 78 | 79 | if (isVModels && !isComponent) { 80 | throw new Error('v-models can only use in custom components') 81 | } 82 | 83 | const shouldResolve = 84 | !['html', 'text', 'model', 'slots', 'models'].includes(directiveName) || 85 | (isVModel && !isComponent) 86 | 87 | let modifiers = directiveModifiers 88 | 89 | if (t.isArrayExpression(value)) { 90 | const elementsList = isVModels ? value.elements! : [value] 91 | 92 | elementsList.forEach((element) => { 93 | if (isVModels && !t.isArrayExpression(element)) { 94 | throw new Error('You should pass a Two-dimensional Arrays to v-models') 95 | } 96 | 97 | const { elements } = element as t.ArrayExpression 98 | const [first, second, third] = elements 99 | 100 | if ( 101 | second && 102 | !t.isArrayExpression(second) && 103 | !t.isSpreadElement(second) 104 | ) { 105 | args.push(second) 106 | modifiers = parseModifiers(third as t.ArrayExpression) 107 | } else if (t.isArrayExpression(second)) { 108 | if (!shouldResolve) { 109 | args.push(t.nullLiteral()) 110 | } 111 | modifiers = parseModifiers(second) 112 | } else if (!shouldResolve) { 113 | // work as v-model={[value]} or v-models={[[value]]} 114 | args.push(t.nullLiteral()) 115 | } 116 | modifiersSet.push(new Set(modifiers)) 117 | vals.push(first as t.Expression) 118 | }) 119 | } else if (isVModel && !shouldResolve) { 120 | // work as v-model={value} 121 | args.push(t.nullLiteral()) 122 | modifiersSet.push(new Set(directiveModifiers)) 123 | } else { 124 | modifiersSet.push(new Set(directiveModifiers)) 125 | } 126 | 127 | return { 128 | directiveName, 129 | modifiers: modifiersSet, 130 | values: vals.length ? vals : [value], 131 | args, 132 | directive: shouldResolve 133 | ? ([ 134 | resolveDirective(path, state, tag, directiveName), 135 | vals[0] || value, 136 | modifiersSet[0]?.size 137 | ? args[0] || t.unaryExpression('void', t.numericLiteral(0), true) 138 | : args[0], 139 | !!modifiersSet[0]?.size && 140 | t.objectExpression( 141 | [...modifiersSet[0]].map((modifier) => 142 | t.objectProperty( 143 | t.identifier(modifier), 144 | t.booleanLiteral(true), 145 | ), 146 | ), 147 | ), 148 | ].filter(Boolean) as t.Expression[]) 149 | : undefined, 150 | } 151 | } 152 | 153 | const resolveDirective = ( 154 | path: NodePath, 155 | state: State, 156 | tag: Tag, 157 | directiveName: string, 158 | ) => { 159 | if (directiveName === 'show') { 160 | return createIdentifier(state, 'vShow') 161 | } 162 | if (directiveName === 'model') { 163 | let modelToUse 164 | const type = getType(path.parentPath as NodePath) 165 | switch ((tag as t.StringLiteral).value) { 166 | case 'select': 167 | modelToUse = createIdentifier(state, 'vModelSelect') 168 | break 169 | case 'textarea': 170 | modelToUse = createIdentifier(state, 'vModelText') 171 | break 172 | default: 173 | if (t.isStringLiteral(type) || !type) { 174 | switch ((type as t.StringLiteral)?.value) { 175 | case 'checkbox': 176 | modelToUse = createIdentifier(state, 'vModelCheckbox') 177 | break 178 | case 'radio': 179 | modelToUse = createIdentifier(state, 'vModelRadio') 180 | break 181 | default: 182 | modelToUse = createIdentifier(state, 'vModelText') 183 | } 184 | } else { 185 | modelToUse = createIdentifier(state, 'vModelDynamic') 186 | } 187 | } 188 | return modelToUse 189 | } 190 | const referenceName = `v${directiveName[0].toUpperCase()}${directiveName.slice(1)}` 191 | if (path.scope.references[referenceName]) { 192 | return t.identifier(referenceName) 193 | } 194 | return t.callExpression(createIdentifier(state, 'resolveDirective'), [ 195 | t.stringLiteral(directiveName), 196 | ]) 197 | } 198 | 199 | export default parseDirectives 200 | -------------------------------------------------------------------------------- /packages/babel-plugin-jsx/src/index.ts: -------------------------------------------------------------------------------- 1 | import { addNamed, addNamespace, isModule } from '@babel/helper-module-imports' 2 | import { declare } from '@babel/helper-plugin-utils' 3 | // @ts-expect-error 4 | import _syntaxJsx from '@babel/plugin-syntax-jsx' 5 | import _template from '@babel/template' 6 | import t from '@babel/types' 7 | import ResolveType from '@vue/babel-plugin-resolve-type' 8 | import sugarFragment from './sugar-fragment' 9 | import transformVueJSX from './transform-vue-jsx' 10 | import type { State, VueJSXPluginOptions } from './interface' 11 | import type * as BabelCore from '@babel/core' 12 | import type { NodePath, Visitor } from '@babel/traverse' 13 | 14 | export type { VueJSXPluginOptions } 15 | 16 | const hasJSX = (parentPath: NodePath) => { 17 | let fileHasJSX = false 18 | parentPath.traverse({ 19 | JSXElement(path) { 20 | // skip ts error 21 | fileHasJSX = true 22 | path.stop() 23 | }, 24 | JSXFragment(path) { 25 | fileHasJSX = true 26 | path.stop() 27 | }, 28 | }) 29 | 30 | return fileHasJSX 31 | } 32 | 33 | const JSX_ANNOTATION_REGEX = /\*?\s*@jsx\s+(\S+)/ 34 | 35 | /* #__NO_SIDE_EFFECTS__ */ 36 | function interopDefault(m: any) { 37 | return m.default || m 38 | } 39 | 40 | const syntaxJsx = /*#__PURE__*/ interopDefault(_syntaxJsx) 41 | const template = /*#__PURE__*/ interopDefault(_template) 42 | 43 | const plugin: ( 44 | api: object, 45 | options: VueJSXPluginOptions | null | undefined, 46 | dirname: string, 47 | ) => BabelCore.PluginObj = declare< 48 | VueJSXPluginOptions, 49 | BabelCore.PluginObj 50 | >((api, opt, dirname) => { 51 | const { types } = api 52 | let resolveType: BabelCore.PluginObj | undefined 53 | if (opt.resolveType) { 54 | if (typeof opt.resolveType === 'boolean') opt.resolveType = {} 55 | resolveType = ResolveType(api, opt.resolveType, dirname) 56 | } 57 | return { 58 | ...resolveType, 59 | name: 'babel-plugin-jsx', 60 | inherits: /*#__PURE__*/ interopDefault(syntaxJsx), 61 | visitor: { 62 | ...(resolveType?.visitor as Visitor), 63 | ...transformVueJSX, 64 | ...sugarFragment, 65 | Program: { 66 | enter(path, state) { 67 | if (hasJSX(path)) { 68 | const importNames = [ 69 | 'createVNode', 70 | 'Fragment', 71 | 'resolveComponent', 72 | 'withDirectives', 73 | 'vShow', 74 | 'vModelSelect', 75 | 'vModelText', 76 | 'vModelCheckbox', 77 | 'vModelRadio', 78 | 'vModelText', 79 | 'vModelDynamic', 80 | 'resolveDirective', 81 | 'mergeProps', 82 | 'createTextVNode', 83 | 'isVNode', 84 | ] 85 | if (isModule(path)) { 86 | // import { createVNode } from "vue"; 87 | const importMap: Record< 88 | string, 89 | t.MemberExpression | t.Identifier 90 | > = {} 91 | importNames.forEach((name) => { 92 | state.set(name, () => { 93 | if (importMap[name]) { 94 | return types.cloneNode(importMap[name]) 95 | } 96 | const identifier = addNamed(path, name, 'vue', { 97 | ensureLiveReference: true, 98 | }) 99 | importMap[name] = identifier 100 | return identifier 101 | }) 102 | }) 103 | const { enableObjectSlots = true } = state.opts 104 | if (enableObjectSlots) { 105 | state.set('@vue/babel-plugin-jsx/runtimeIsSlot', () => { 106 | if (importMap.runtimeIsSlot) { 107 | return importMap.runtimeIsSlot 108 | } 109 | const { name: isVNodeName } = state.get( 110 | 'isVNode', 111 | )() as t.Identifier 112 | const isSlot = path.scope.generateUidIdentifier('isSlot') 113 | const ast = template.ast` 114 | function ${isSlot.name}(s) { 115 | return typeof s === 'function' || (Object.prototype.toString.call(s) === '[object Object]' && !${isVNodeName}(s)); 116 | } 117 | ` 118 | const lastImport = (path.get('body') as NodePath[]).findLast( 119 | (p) => p.isImportDeclaration(), 120 | ) 121 | if (lastImport) { 122 | lastImport.insertAfter(ast) 123 | } 124 | importMap.runtimeIsSlot = isSlot 125 | return isSlot 126 | }) 127 | } 128 | } else { 129 | // var _vue = require('vue'); 130 | let sourceName: t.Identifier 131 | importNames.forEach((name) => { 132 | state.set(name, () => { 133 | if (!sourceName) { 134 | sourceName = addNamespace(path, 'vue', { 135 | ensureLiveReference: true, 136 | }) 137 | } 138 | return t.memberExpression(sourceName, t.identifier(name)) 139 | }) 140 | }) 141 | 142 | const helpers: Record = {} 143 | 144 | const { enableObjectSlots = true } = state.opts 145 | if (enableObjectSlots) { 146 | state.set('@vue/babel-plugin-jsx/runtimeIsSlot', () => { 147 | if (helpers.runtimeIsSlot) { 148 | return helpers.runtimeIsSlot 149 | } 150 | const isSlot = path.scope.generateUidIdentifier('isSlot') 151 | const { object: objectName } = state.get( 152 | 'isVNode', 153 | )() as t.MemberExpression 154 | const ast = template.ast` 155 | function ${isSlot.name}(s) { 156 | return typeof s === 'function' || (Object.prototype.toString.call(s) === '[object Object]' && !${ 157 | (objectName as t.Identifier).name 158 | }.isVNode(s)); 159 | } 160 | ` 161 | 162 | const nodePaths = path.get('body') as NodePath[] 163 | const lastImport = nodePaths.findLast( 164 | (p) => 165 | p.isVariableDeclaration() && 166 | p.node.declarations.some( 167 | (d) => (d.id as t.Identifier)?.name === sourceName.name, 168 | ), 169 | ) 170 | if (lastImport) { 171 | lastImport.insertAfter(ast) 172 | } 173 | return isSlot 174 | }) 175 | } 176 | } 177 | 178 | const { 179 | opts: { pragma = '' }, 180 | file, 181 | } = state 182 | 183 | if (pragma) { 184 | state.set('createVNode', () => t.identifier(pragma)) 185 | } 186 | 187 | if (file.ast.comments) { 188 | for (const comment of file.ast.comments) { 189 | const jsxMatches = JSX_ANNOTATION_REGEX.exec(comment.value) 190 | if (jsxMatches) { 191 | state.set('createVNode', () => t.identifier(jsxMatches[1])) 192 | } 193 | } 194 | } 195 | } 196 | }, 197 | }, 198 | }, 199 | } 200 | }) 201 | 202 | export default plugin 203 | export { plugin as 'module.exports' } 204 | -------------------------------------------------------------------------------- /packages/babel-plugin-jsx/README-zh_CN.md: -------------------------------------------------------------------------------- 1 | # Vue 3 Babel JSX 插件 2 | 3 | [![npm package](https://img.shields.io/npm/v/@vue/babel-plugin-jsx.svg?style=flat-square)](https://www.npmjs.com/package/@vue/babel-plugin-jsx) 4 | [![issues-helper](https://img.shields.io/badge/Issues%20Manage%20By-issues--helper-orange?style=flat-square)](https://github.com/actions-cool/issues-helper) 5 | 6 | 以 JSX 的方式来编写 Vue 代码 7 | 8 | [English](/packages/babel-plugin-jsx/README.md) | 简体中文 9 | 10 | ## 安装 11 | 12 | 安装插件 13 | 14 | ```bash 15 | npm install @vue/babel-plugin-jsx -D 16 | ``` 17 | 18 | 配置 Babel 19 | 20 | ```json 21 | { 22 | "plugins": ["@vue/babel-plugin-jsx"] 23 | } 24 | ``` 25 | 26 | ## 使用 27 | 28 | ### 参数 29 | 30 | #### transformOn 31 | 32 | Type: `boolean` 33 | 34 | Default: `false` 35 | 36 | 把 `on: { click: xx }` 转成 `onClick: xxx` 37 | 38 | #### optimize 39 | 40 | Type: `boolean` 41 | 42 | Default: `false` 43 | 44 | 开启此选项后,JSX 插件会尝试使用 [`PatchFlags`](https://cn.vuejs.org/guide/extras/rendering-mechanism#patch-flags) 和 [`SlotFlags`](https://github.com/vuejs/core/blob/v3.5.13/packages/runtime-core/src/componentSlots.ts#L69-L77) 来优化运行时代码,从而提升渲染性能。需要注意的是,JSX 的灵活性远高于模板语法,这使得编译优化的可能性相对有限,其优化效果会比 Vue 官方模板编译器更为有限。 45 | 46 | 优化后的代码会选择性地跳过一些重渲染操作以提高性能。因此,建议在开启此选项后对应用进行完整的测试,确保所有功能都能正常工作。 47 | 48 | #### isCustomElement 49 | 50 | Type: `(tag: string) => boolean` 51 | 52 | Default: `undefined` 53 | 54 | 自定义元素 55 | 56 | #### mergeProps 57 | 58 | Type: `boolean` 59 | 60 | Default: `true` 61 | 62 | 合并 class / style / onXXX handlers 63 | 64 | #### enableObjectSlots 65 | 66 | 使用 `enableObjectSlots` (文档下面会提到)。虽然在 JSX 中比较好使,但是会增加一些 `_isSlot` 的运行时条件判断,这会增加你的项目体积。即使你关闭了 `enableObjectSlots`,`v-slots` 还是可以使用 67 | 68 | #### pragma 69 | 70 | Type: `string` 71 | 72 | Default: `createVNode` 73 | 74 | 替换编译 JSX 表达式的时候使用的函数 75 | 76 | #### resolveType 77 | 78 | Type: `boolean` 79 | 80 | Default: `false` 81 | 82 | (**Experimental**) Infer component metadata from types (e.g. `props`, `emits`, `name`). This is an experimental feature and may not work in all cases. 83 | 84 | ## 表达式 85 | 86 | ### 内容 87 | 88 | 函数式组件 89 | 90 | ```jsx 91 | const App = () =>
92 | ``` 93 | 94 | 在 render 中使用 95 | 96 | ```jsx 97 | const App = { 98 | render() { 99 | return
Vue 3.0
100 | }, 101 | } 102 | ``` 103 | 104 | ```jsx 105 | import { defineComponent, withModifiers } from 'vue' 106 | 107 | const App = defineComponent({ 108 | setup() { 109 | const count = ref(0) 110 | 111 | const inc = () => { 112 | count.value++ 113 | } 114 | 115 | return () =>
{count.value}
116 | }, 117 | }) 118 | ``` 119 | 120 | Fragment 121 | 122 | ```jsx 123 | const App = () => ( 124 | <> 125 | I'm 126 | Fragment 127 | 128 | ) 129 | ``` 130 | 131 | ### Attributes / Props 132 | 133 | ```jsx 134 | const App = () => 135 | ``` 136 | 137 | 动态绑定: 138 | 139 | ```jsx 140 | const placeholderText = 'email' 141 | const App = () => 142 | ``` 143 | 144 | ### 指令 145 | 146 | #### v-show 147 | 148 | ```jsx 149 | const App = { 150 | data() { 151 | return { visible: true } 152 | }, 153 | render() { 154 | return 155 | }, 156 | } 157 | ``` 158 | 159 | #### v-model 160 | 161 | > 注意:如果想要使用 `arg`, 第二个参数需要为字符串 162 | 163 | ```jsx 164 | 165 | ``` 166 | 167 | ```jsx 168 | 169 | ``` 170 | 171 | ```jsx 172 | ; 173 | // 或者 174 | ; 175 | ``` 176 | 177 | ```jsx 178 | ; 179 | // 或者 180 | ; 181 | ``` 182 | 183 | 会编译成: 184 | 185 | ```js 186 | h(A, { 187 | argument: val, 188 | argumentModifiers: { 189 | modifier: true, 190 | }, 191 | 'onUpdate:argument': ($event) => (val = $event), 192 | }) 193 | ``` 194 | 195 | #### v-models (从 1.1.0 开始不推荐使用) 196 | 197 | > 注意: 你应该传递一个二维数组给 v-models。 198 | 199 | ```jsx 200 | 201 | ``` 202 | 203 | ```jsx 204 | 210 | ``` 211 | 212 | ```jsx 213 | 219 | ``` 220 | 221 | 会编译成: 222 | 223 | ```js 224 | h(A, { 225 | modelValue: foo, 226 | modelModifiers: { 227 | modifier: true, 228 | }, 229 | 'onUpdate:modelValue': ($event) => (foo = $event), 230 | bar, 231 | barModifiers: { 232 | modifier: true, 233 | }, 234 | 'onUpdate:bar': ($event) => (bar = $event), 235 | }) 236 | ``` 237 | 238 | #### 自定义指令 239 | 240 | 只有 argument 的时候推荐使用 241 | 242 | ```jsx 243 | const App = { 244 | directives: { custom: customDirective }, 245 | setup() { 246 | return () => 247 | }, 248 | } 249 | ``` 250 | 251 | ```jsx 252 | const App = { 253 | directives: { custom: customDirective }, 254 | setup() { 255 | return () => 256 | }, 257 | } 258 | ``` 259 | 260 | ### 插槽 261 | 262 | > 注意: 在 `jsx` 中,应该使用 **`v-slots`** 代替 _`v-slot`_ 263 | 264 | ```jsx 265 | const A = (props, { slots }) => ( 266 | <> 267 |

{slots.default ? slots.default() : 'foo'}

268 |

{slots.bar?.()}

269 | 270 | ) 271 | 272 | const App = { 273 | setup() { 274 | const slots = { 275 | bar: () => B, 276 | } 277 | return () => ( 278 |
279 |
A
280 |
281 | ) 282 | }, 283 | } 284 | 285 | // or 286 | 287 | const App2 = { 288 | setup() { 289 | const slots = { 290 | default: () =>
A
, 291 | bar: () => B, 292 | } 293 | return () => 294 | }, 295 | } 296 | 297 | // 或者,当 `enableObjectSlots` 不是 `false` 时,您可以使用对象插槽 298 | const App3 = { 299 | setup() { 300 | return () => ( 301 | <> 302 | 303 | {{ 304 | default: () =>
A
, 305 | bar: () => B, 306 | }} 307 |
308 | {() => 'foo'} 309 | 310 | ) 311 | }, 312 | } 313 | ``` 314 | 315 | ### 在 TypeScript 中使用 316 | 317 | `tsconfig.json`: 318 | 319 | ```json 320 | { 321 | "compilerOptions": { 322 | "jsx": "preserve" 323 | } 324 | } 325 | ``` 326 | 327 | ## 谁在使用 328 | 329 | 330 | 331 | 332 | 342 | 353 | 364 | 375 | 376 | 377 |
333 | 334 | 338 |
339 | Ant Design Vue 340 |
341 |
343 | 344 | 349 |
350 | Vant 351 |
352 |
354 | 355 | 360 |
361 | Element Plus 362 |
363 |
365 | 366 | 371 |
372 | Vue Json Pretty 373 |
374 |
378 | 379 | ## 兼容性 380 | 381 | 要求: 382 | 383 | - **Babel 7+** 384 | - **Vue 3+** 385 | -------------------------------------------------------------------------------- /packages/babel-plugin-jsx/README.md: -------------------------------------------------------------------------------- 1 | # Babel Plugin JSX for Vue 3 2 | 3 | [![npm package](https://img.shields.io/npm/v/@vue/babel-plugin-jsx.svg?style=flat-square)](https://www.npmjs.com/package/@vue/babel-plugin-jsx) 4 | [![issues-helper](https://img.shields.io/badge/Issues%20Manage%20By-issues--helper-blueviolet?style=flat-square)](https://github.com/actions-cool/issues-helper) 5 | 6 | To add Vue JSX support. 7 | 8 | English | [简体中文](/packages/babel-plugin-jsx/README-zh_CN.md) 9 | 10 | ## Installation 11 | 12 | Install the plugin with: 13 | 14 | ```bash 15 | npm install @vue/babel-plugin-jsx -D 16 | ``` 17 | 18 | Then add the plugin to your babel config: 19 | 20 | ```json 21 | { 22 | "plugins": ["@vue/babel-plugin-jsx"] 23 | } 24 | ``` 25 | 26 | ## Usage 27 | 28 | ### options 29 | 30 | #### transformOn 31 | 32 | Type: `boolean` 33 | 34 | Default: `false` 35 | 36 | transform `on: { click: xx }` to `onClick: xxx` 37 | 38 | #### optimize 39 | 40 | Type: `boolean` 41 | 42 | Default: `false` 43 | 44 | When enabled, this plugin generates optimized runtime code using [`PatchFlags`](https://vuejs.org/guide/extras/rendering-mechanism#patch-flags) and [`SlotFlags`](https://github.com/vuejs/core/blob/v3.5.13/packages/runtime-core/src/componentSlots.ts#L69-L77) to improve rendering performance. However, due to JSX's dynamic nature, the optimizations are not as comprehensive as those in Vue's official template compiler. 45 | 46 | Since the optimized code may skip certain re-renders to improve performance, we strongly recommend thorough testing of your application after enabling this option to ensure everything works as expected. 47 | 48 | #### isCustomElement 49 | 50 | Type: `(tag: string) => boolean` 51 | 52 | Default: `undefined` 53 | 54 | configuring custom elements 55 | 56 | #### mergeProps 57 | 58 | Type: `boolean` 59 | 60 | Default: `true` 61 | 62 | merge static and dynamic class / style attributes / onXXX handlers 63 | 64 | #### enableObjectSlots 65 | 66 | Type: `boolean` 67 | 68 | Default: `true` 69 | 70 | Whether to enable `object slots` (mentioned below the document) syntax". It might be useful in JSX, but it will add a lot of `_isSlot` condition expressions which increase your bundle size. And `v-slots` is still available even if `enableObjectSlots` is turned off. 71 | 72 | #### pragma 73 | 74 | Type: `string` 75 | 76 | Default: `createVNode` 77 | 78 | Replace the function used when compiling JSX expressions. 79 | 80 | #### resolveType 81 | 82 | Type: `boolean` 83 | 84 | Default: `false` 85 | 86 | (**Experimental**) Infer component metadata from types (e.g. `props`, `emits`, `name`). This is an experimental feature and may not work in all cases. 87 | 88 | ## Syntax 89 | 90 | ### Content 91 | 92 | functional component 93 | 94 | ```jsx 95 | const App = () =>
Vue 3.0
96 | ``` 97 | 98 | with render 99 | 100 | ```jsx 101 | const App = { 102 | render() { 103 | return
Vue 3.0
104 | }, 105 | } 106 | ``` 107 | 108 | ```jsx 109 | import { defineComponent, withModifiers } from 'vue' 110 | 111 | const App = defineComponent({ 112 | setup() { 113 | const count = ref(0) 114 | 115 | const inc = () => { 116 | count.value++ 117 | } 118 | 119 | return () =>
{count.value}
120 | }, 121 | }) 122 | ``` 123 | 124 | Fragment 125 | 126 | ```jsx 127 | const App = () => ( 128 | <> 129 | I'm 130 | Fragment 131 | 132 | ) 133 | ``` 134 | 135 | ### Attributes / Props 136 | 137 | ```jsx 138 | const App = () => 139 | ``` 140 | 141 | with a dynamic binding: 142 | 143 | ```jsx 144 | const placeholderText = 'email' 145 | const App = () => 146 | ``` 147 | 148 | ### Directives 149 | 150 | #### v-show 151 | 152 | ```jsx 153 | const App = { 154 | data() { 155 | return { visible: true } 156 | }, 157 | render() { 158 | return 159 | }, 160 | } 161 | ``` 162 | 163 | #### v-model 164 | 165 | > Note: You should pass the second param as string for using `arg`. 166 | 167 | ```jsx 168 | 169 | ``` 170 | 171 | ```jsx 172 | 173 | ``` 174 | 175 | ```jsx 176 | ; 177 | // Or 178 | ; 179 | ``` 180 | 181 | ```jsx 182 | ; 183 | // Or 184 | ; 185 | ``` 186 | 187 | Will compile to: 188 | 189 | ```js 190 | h(A, { 191 | argument: val, 192 | argumentModifiers: { 193 | modifier: true, 194 | }, 195 | 'onUpdate:argument': ($event) => (val = $event), 196 | }) 197 | ``` 198 | 199 | #### v-models (Not recommended since v1.1.0) 200 | 201 | > Note: You should pass a Two-dimensional Arrays to v-models. 202 | 203 | ```jsx 204 | 205 | ``` 206 | 207 | ```jsx 208 | 214 | ``` 215 | 216 | ```jsx 217 | 223 | ``` 224 | 225 | Will compile to: 226 | 227 | ```js 228 | h(A, { 229 | modelValue: foo, 230 | modelModifiers: { 231 | modifier: true, 232 | }, 233 | 'onUpdate:modelValue': ($event) => (foo = $event), 234 | bar, 235 | barModifiers: { 236 | modifier: true, 237 | }, 238 | 'onUpdate:bar': ($event) => (bar = $event), 239 | }) 240 | ``` 241 | 242 | #### custom directive 243 | 244 | Recommended when using string arguments 245 | 246 | ```jsx 247 | const App = { 248 | directives: { custom: customDirective }, 249 | setup() { 250 | return () => 251 | }, 252 | } 253 | ``` 254 | 255 | ```jsx 256 | const App = { 257 | directives: { custom: customDirective }, 258 | setup() { 259 | return () => 260 | }, 261 | } 262 | ``` 263 | 264 | ### Slot 265 | 266 | > Note: In `jsx`, _`v-slot`_ should be replaced with **`v-slots`** 267 | 268 | ```jsx 269 | const A = (props, { slots }) => ( 270 | <> 271 |

{slots.default ? slots.default() : 'foo'}

272 |

{slots.bar?.()}

273 | 274 | ) 275 | 276 | const App = { 277 | setup() { 278 | const slots = { 279 | bar: () => B, 280 | } 281 | return () => ( 282 |
283 |
A
284 |
285 | ) 286 | }, 287 | } 288 | 289 | // or 290 | 291 | const App2 = { 292 | setup() { 293 | const slots = { 294 | default: () =>
A
, 295 | bar: () => B, 296 | } 297 | return () => 298 | }, 299 | } 300 | 301 | // or you can use object slots when `enableObjectSlots` is not false. 302 | const App3 = { 303 | setup() { 304 | return () => ( 305 | <> 306 | 307 | {{ 308 | default: () =>
A
, 309 | bar: () => B, 310 | }} 311 |
312 | {() => 'foo'} 313 | 314 | ) 315 | }, 316 | } 317 | ``` 318 | 319 | ### In TypeScript 320 | 321 | `tsconfig.json`: 322 | 323 | ```json 324 | { 325 | "compilerOptions": { 326 | "jsx": "preserve" 327 | } 328 | } 329 | ``` 330 | 331 | ## Who is using 332 | 333 | 334 | 335 | 336 | 346 | 357 | 368 | 379 | 380 | 381 |
337 | 338 | 342 |
343 | Ant Design Vue 344 |
345 |
347 | 348 | 353 |
354 | Vant 355 |
356 |
358 | 359 | 364 |
365 | Element Plus 366 |
367 |
369 | 370 | 375 |
376 | Vue Json Pretty 377 |
378 |
382 | 383 | ## Compatibility 384 | 385 | This repo is only compatible with: 386 | 387 | - **Babel 7+** 388 | - **Vue 3+** 389 | -------------------------------------------------------------------------------- /packages/babel-plugin-jsx/test/v-model.test.tsx: -------------------------------------------------------------------------------- 1 | import { mount, shallowMount } from '@vue/test-utils' 2 | import { defineComponent, type VNode } from 'vue' 3 | 4 | test('input[type="checkbox"] should work', async () => { 5 | const wrapper = shallowMount( 6 | defineComponent({ 7 | data() { 8 | return { 9 | test: true, 10 | } 11 | }, 12 | render() { 13 | return 14 | }, 15 | }), 16 | { attachTo: document.body }, 17 | ) 18 | 19 | expect(wrapper.vm.$el.checked).toBe(true) 20 | wrapper.vm.test = false 21 | await wrapper.vm.$nextTick() 22 | expect(wrapper.vm.$el.checked).toBe(false) 23 | expect(wrapper.vm.test).toBe(false) 24 | await wrapper.trigger('click') 25 | expect(wrapper.vm.$el.checked).toBe(true) 26 | expect(wrapper.vm.test).toBe(true) 27 | }) 28 | 29 | test('input[type="radio"] should work', async () => { 30 | const wrapper = shallowMount( 31 | defineComponent({ 32 | data: () => ({ 33 | test: '1', 34 | }), 35 | render() { 36 | return ( 37 | <> 38 | 39 | 40 | 41 | ) 42 | }, 43 | }), 44 | { attachTo: document.body }, 45 | ) 46 | 47 | const [a, b] = wrapper.vm.$.subTree.children as VNode[] 48 | 49 | expect(a.el!.checked).toBe(true) 50 | wrapper.vm.test = '2' 51 | await wrapper.vm.$nextTick() 52 | expect(a.el!.checked).toBe(false) 53 | expect(b.el!.checked).toBe(true) 54 | await a.el!.click() 55 | expect(a.el!.checked).toBe(true) 56 | expect(b.el!.checked).toBe(false) 57 | expect(wrapper.vm.test).toBe('1') 58 | }) 59 | 60 | test('select should work with value bindings', async () => { 61 | const wrapper = shallowMount( 62 | defineComponent({ 63 | data: () => ({ 64 | test: 2, 65 | }), 66 | render() { 67 | return ( 68 | 73 | ) 74 | }, 75 | }), 76 | ) 77 | 78 | const el = wrapper.vm.$el 79 | 80 | expect(el.value).toBe('2') 81 | expect(el.children[1].selected).toBe(true) 82 | wrapper.vm.test = 3 83 | await wrapper.vm.$nextTick() 84 | expect(el.value).toBe('3') 85 | expect(el.children[2].selected).toBe(true) 86 | 87 | el.value = '1' 88 | await wrapper.trigger('change') 89 | expect(wrapper.vm.test).toBe('1') 90 | 91 | el.value = '2' 92 | await wrapper.trigger('change') 93 | expect(wrapper.vm.test).toBe(2) 94 | }) 95 | 96 | test('textarea should update value both ways', async () => { 97 | const wrapper = shallowMount( 98 | defineComponent({ 99 | data: () => ({ 100 | test: 'b', 101 | }), 102 | render() { 103 | return