diff --git a/.changeset/four-crews-know.md b/.changeset/four-crews-know.md
new file mode 100644
index 000000000..efd0ae37a
--- /dev/null
+++ b/.changeset/four-crews-know.md
@@ -0,0 +1,9 @@
+---
+"@vue-macros/jsx-macros": minor
+"unplugin-vue-macros": minor
+"@vue-macros/volar": minor
+"@vue-macros/config": patch
+---
+
+introduce jsx-macros
+
\ No newline at end of file
diff --git a/.changeset/friendly-rings-destroy.md b/.changeset/friendly-rings-destroy.md
new file mode 100644
index 000000000..9c3fb14a9
--- /dev/null
+++ b/.changeset/friendly-rings-destroy.md
@@ -0,0 +1,6 @@
+---
+"@vue-macros/common": patch
+---
+
+add `as` option for importHelperFn
+
\ No newline at end of file
diff --git a/.changeset/nasty-bees-tickle.md b/.changeset/nasty-bees-tickle.md
new file mode 100644
index 000000000..c8d638d9b
--- /dev/null
+++ b/.changeset/nasty-bees-tickle.md
@@ -0,0 +1,7 @@
+---
+"@vue-macros/volar": patch
+"@vue-macros/jsx": patch
+---
+
+introduce jsx
+
\ No newline at end of file
diff --git a/.github/workflows/release-pr.yml b/.github/workflows/release-pr.yml
index bbb732aa9..c03fb160a 100644
--- a/.github/workflows/release-pr.yml
+++ b/.github/workflows/release-pr.yml
@@ -31,4 +31,4 @@ jobs:
run: pnpm build
- name: Publish
- run: pnpm dlx pkg-pr-new@0.0 publish --compact --pnpm './packages/*'
+ run: pnpm dlx pkg-pr-new@0.0 publish --pnpm './packages/*'
diff --git a/cspell.json b/cspell.json
index 12d38110b..dd64ef8af 100644
--- a/cspell.json
+++ b/cspell.json
@@ -31,6 +31,7 @@
"esbuild",
"foobaz",
"interface",
+ "jsxs",
"monoman",
"mousemove",
"nolebase",
diff --git a/docs/.vitepress/config/theme.ts b/docs/.vitepress/config/theme.ts
index b0622f38f..5bbe0fb87 100644
--- a/docs/.vitepress/config/theme.ts
+++ b/docs/.vitepress/config/theme.ts
@@ -185,6 +185,10 @@ export function getLocaleConfig(lang: string) {
text: 'defineStyleX',
link: `/define-stylex`,
},
+ {
+ text: 'jsxMacros',
+ link: `/jsx-macros`,
+ },
],
},
],
diff --git a/docs/macros/index.md b/docs/macros/index.md
index 332c662a4..33ae61cbd 100644
--- a/docs/macros/index.md
+++ b/docs/macros/index.md
@@ -26,3 +26,4 @@ Please make sure `vue-macros` is set up correctly. If you haven't yet, read [Get
- [setupSFC](./setup-sfc.md)
- [chainCall](./chain-call.md)
- [defineStyleX](./define-stylex.md)
+- [jsxMacros](./jsx-macros.md)
diff --git a/docs/macros/jsx-macros.md b/docs/macros/jsx-macros.md
new file mode 100644
index 000000000..995af57c9
--- /dev/null
+++ b/docs/macros/jsx-macros.md
@@ -0,0 +1,379 @@
+# jsxMacros
+
+
+
+A collection of JSX macros.
+
+| Directive | vue3 / vapor | react / preact | Volar |
+| :---------------: | :----------------: | :----------------: | :----------------: |
+| `defineComponent` | :white_check_mark: | :x: | :white_check_mark: |
+| `defineModel` | :white_check_mark: | :x: | :white_check_mark: |
+| `defineSlots` | :white_check_mark: | :white_check_mark: | :white_check_mark: |
+| `defineExpose` | :white_check_mark: | :white_check_mark: | :white_check_mark: |
+| `defineStyle` | :white_check_mark: | :white_check_mark: | :white_check_mark: |
+
+## Options
+
+```ts
+interface Options {
+ /**
+ * @default 'vue'
+ */
+ lib?: 'vue' | 'vue/vapor' | 'react' | 'preact' | 'solid' | string
+}
+```
+
+## defineComponent
+
+- Support directly returns JSX.
+- Support using `getCurrentInstance()` after an `await` expression.
+- Automatically collects used props to the defineComponent's props option.
+
+```tsx
+import { defineComponent, getCurrentInstance, nextTick, Suspense } from 'vue'
+
+const Comp = defineComponent(
+ async (props: {
+ foo?: string
+ bar?: string
+ // ^ unused prop will be as a fallthrough attribute.
+ }) => {
+ await nextTick()
+ const i = getCurrentInstance()
+ return (
+
+ {props.foo}
+
+ )
+ },
+)
+
+export default () => (
+
+
+
+)
+```
+
+::: details Compiled Code
+
+```tsx
+import { defineComponent, getCurrentInstance, withAsyncContext } from 'vue'
+defineComponent(
+ async (props) => {
+ let __temp, __restore
+ ;([__temp, __restore] = withAsyncContext(() => nextTick())),
+ await __temp,
+ __restore()
+ const i = getCurrentInstance()
+ return () => (
+
+ {props.foo}
+
+ )
+ },
+ { props: { foo: null } },
+)
+```
+
+:::
+
+- The destructured props will be automatically restructured.
+- If the prop's default value ends with `!`, the prop will be inferred as required.
+- If a rest prop is defined, it will be converted to `useAttrs()`, and the `inheritAttrs` option will default to `false`.
+
+```tsx
+import { defineComponent } from 'vue'
+
+const Comp = defineComponent(
+ ({ foo = undefined as T, bar = ''!, ...attrs }) => {
+ return (
+
+ {foo}
+
+ )
+ },
+)
+
+export default () => foo={1} bar="bar" />
+```
+
+::: details Compiled Code
+
+```tsx
+import { defineComponent } from 'vue'
+import { createPropsDefaultProxy } from '/vue-macros/jsx-macros/with-defaults'
+defineComponent(
+ (_props) => {
+ const props = createPropsDefaultProxy(_props, { bar: '' })
+ const attrs = useAttrs()
+ return () => (
+
+ {_props.foo}
+
+ )
+ },
+ { props: { foo: null, bar: { required: true } }, inheritAttrs: false },
+)
+```
+
+:::
+
+## defineModel
+
+- Doesn't support hyphenated model names.
+- Will be inferred as a required prop when the expression ends with `!`.
+- The modified model's value can be read synchronously, without needing to `await nextTick()`. [Related issue](https://github.com/vuejs/core/issues/11080)
+
+```tsx
+import { ref } from 'vue'
+
+function Comp() {
+ const modelValue = defineModel()!
+ return {modelValue.value}
+}
+
+export default () => {
+ const foo = ref(1)
+ return
+}
+```
+
+::: details Compiled Code
+
+```tsx
+import { ref } from 'vue'
+import { useModel } from '/vue-macros/jsx-macros/use-model'
+
+function Comp(_props: {
+ modelValue: string
+ 'onUpdate:modelValue': (value: string) => any
+}) {
+ const modelValue = useModel(_props, 'modelValue', { required: true })
+ return {modelValue.value}
+}
+```
+
+:::
+
+## defineSlots
+
+- If using generics to define slots, all slots will be optional.
+
+```tsx
+const slots = defineSlots<{
+ default: () => any
+}>()
+
+slots.default?.()
+// ^ optional
+```
+
+- Support default slots (Recommended).
+
+```tsx
+function Comp() {
+ const slots = defineSlots({
+ title: (props: { bar?: T }) => title slot: {props.bar}
,
+ default: (props: { foo: number }) => default slot: {props.foo}
,
+ })
+
+ return (
+ <>
+
+
+ >
+ )
+}
+
+export default () => (
+ >
+ {bar}
+ {foo}
+
+)
+```
+
+## defineExpose
+
+Just like in Vue SFC.
+
+```tsx
+import { shallowRef as useRef } from 'vue'
+
+const Comp = ({ foo = undefined as T }) => {
+ defineExpose({
+ foo,
+ })
+ return
+}
+
+export default () => {
+ const compRef = useRef()
+ console.log(compRef.value!.foo === 1)
+ return
+}
+```
+
+::: details Compiled Code
+
+::: code-group
+
+```tsx [vue]
+import { getCurrentInstance, shallowRef as useRef } from 'vue'
+import { useExpose } from '/vue-macros/jsx-macros/use-expose'
+
+const Comp = ({ foo }) => {
+ useExpose(getCurrentInstance(), {
+ foo,
+ })
+ return
+}
+```
+
+```tsx [react]
+/**
+ * vite.config.ts
+ *
+ * jsxMacros({
+ * lib: 'react'
+ * })
+ */
+import { forwardRef, useImperativeHandle } from 'react'
+import { useExpose } from '/vue-macros/jsx-macros/use-expose'
+
+const Comp = forwardRef(({ foo }, _ref) => {
+ useImperativeHandle(
+ _ref,
+ () => ({
+ foo,
+ }),
+ [foo],
+ )
+ return
+})
+```
+
+```tsx [react19]
+/**
+ * vite.config.ts
+ *
+ * jsxMacros({
+ * lib: 'react',
+ * version: 19
+ * })
+ */
+import { forwardRef, useImperativeHandle } from 'react'
+import { useExpose } from '/vue-macros/jsx-macros/use-expose'
+
+const Comp = ({ foo, ..._props }) => {
+ useImperativeHandle(
+ _props.ref,
+ () => ({
+ foo,
+ }),
+ [foo],
+ )
+ return
+}
+```
+
+```tsx [preact]
+/**
+ * vite.config.ts
+ *
+ * jsxMacros({
+ * lib: 'preact'
+ * })
+ */
+import { forwardRef } from 'preact/compat'
+import { useImperativeHandle } from 'preact/hooks'
+import { useExpose } from '/vue-macros/jsx-macros/use-expose'
+
+const Comp = forwardRef(({ foo }, _ref) => {
+ useImperativeHandle(
+ _ref,
+ () => ({
+ foo,
+ }),
+ [foo],
+ )
+ return
+})
+```
+
+:::
+
+## defineStyle
+
+```ts
+declare function defineStyle(
+ style: string,
+ options?: { scoped?: boolean },
+): void
+```
+
+- Support CSS-variable and JS-variable binding.
+- Support defining multiple style macros in a file.
+- Support CSS pre-processors: `css`, `scss`, `sass`, `less`, `stylus`, `postcss`.
+
+```ts
+defineStyle.scss(`...`)
+defineStyle.stylus(`...`)
+// ...
+```
+
+- Support scoped mode.
+ - If defined at the top level of the file, the scoped option defaults to `false`.
+ - If defined within a function, the scoped option defaults to `true`.
+
+```tsx
+function Comp({ color = 'red' }) {
+ defineStyle.scss(`
+ .foo {
+ color: ${color};
+
+ :deep(.bar) {
+ color: blue;
+ }
+ }
+ `)
+ return
+}
+
+defineStyle(`
+ .bar {
+ background: black;
+ }
+`)
+```
+
+- Support `css modules`, if the macro is an assignment expression.
+
+```tsx
+export default () => {
+ const styles = defineStyle.scss(`
+ .foo {
+ color: blue;
+ .bar {
+ background: red;
+ }
+ }
+ `)
+ return
+}
+```
+
+## Volar Configuration
+
+```json {5} [tsconfig.json]
+{
+ "vueCompilerOptions": {
+ "plugins": ["unplugin-vue-macros/volar"],
+ "vueMacros": {
+ "jsxMacros": true,
+ "scriptSFC": true
+ }
+ }
+}
+```
diff --git a/docs/vue-macros.config.ts b/docs/vue-macros.config.ts
index 7ad446f61..a6670df69 100644
--- a/docs/vue-macros.config.ts
+++ b/docs/vue-macros.config.ts
@@ -6,6 +6,7 @@ export default defineConfig({
defineProp: true,
defineStyleX: true,
exportRender: true,
+ jsxMacros: true,
jsxRef: true,
scriptLang: true,
setupSFC: true,
diff --git a/docs/zh-CN/macros/index.md b/docs/zh-CN/macros/index.md
index c75fc1206..07493805d 100644
--- a/docs/zh-CN/macros/index.md
+++ b/docs/zh-CN/macros/index.md
@@ -26,3 +26,4 @@
- [setupSFC](./setup-sfc.md)
- [chainCall](./chain-call.md)
- [defineStyleX](./define-stylex.md)
+- [jsxMaros](./jsx-macros.md)
diff --git a/docs/zh-CN/macros/jsx-macros.md b/docs/zh-CN/macros/jsx-macros.md
new file mode 100644
index 000000000..cf7ce2131
--- /dev/null
+++ b/docs/zh-CN/macros/jsx-macros.md
@@ -0,0 +1,381 @@
+# jsxMacros
+
+
+
+JSX 的宏集合.
+
+| Directive | vue3 / vapor | react / preact | Volar |
+| :---------------: | :----------------: | :----------------: | :----------------: |
+| `defineComponent` | :white_check_mark: | :x: | :white_check_mark: |
+| `defineModel` | :white_check_mark: | :x: | :white_check_mark: |
+| `defineSlots` | :white_check_mark: | :white_check_mark: | :white_check_mark: |
+| `defineExpose` | :white_check_mark: | :white_check_mark: | :white_check_mark: |
+| `defineStyle` | :white_check_mark: | :white_check_mark: | :white_check_mark: |
+
+## 选项
+
+```ts
+interface Options {
+ /**
+ * @default 'vue'
+ */
+ lib?: 'vue' | 'vue/vapor' | 'react' | 'preact' | 'solid' | string
+}
+```
+
+## defineComponent
+
+- 支持直接返回 `JSX`.
+- 支持在 `await` 表达式后使用 `getCurrentInstance()`。
+- 自动收集使用过的 props 到 defineComponent 的 props 选项中。
+
+```tsx
+import { defineComponent, getCurrentInstance, nextTick, Suspense } from 'vue'
+
+const Comp = defineComponent(
+ async (props: {
+ foo?: string
+ bar?: string
+ // ^ 没有使用的 prop 将作为 attribute 自动透传
+ }) => {
+ await nextTick()
+ const i = getCurrentInstance()
+ return (
+
+ {props.foo}
+
+ )
+ },
+)
+
+export default () => (
+
+
+
+)
+```
+
+::: details 编译后代码
+
+```tsx
+import { getCurrentInstance, withAsyncContext } from 'vue'
+
+defineComponent(
+ async (props) => {
+ let __temp, __restore
+ ;([__temp, __restore] = withAsyncContext(() => nextTick())),
+ await __temp,
+ __restore()
+ const i = getCurrentInstance()
+ return () => (
+
+ {props.foo}
+
+ )
+ },
+ { props: { foo: null } },
+)
+```
+
+:::
+
+- 解构的 props 将自动重构。
+- 如果 prop 的默认值以 `!` 结尾,该 prop 将被推断为必传的。
+- 如果定义了 rest prop,它将被转换为 `useAttrs()`,并且 `inheritAttrs` 选项默认为 `false`。
+
+```tsx
+import { defineComponent } from 'vue'
+
+const Comp = defineComponent(
+ ({ foo = undefined as T, bar = ''!, ...attrs }) => {
+ return (
+
+ {foo}
+
+ )
+ },
+)
+
+export default () => foo={1} bar="bar" />
+```
+
+::: details 编译后代码
+
+```tsx
+import { defineComponent } from 'vue'
+import { createPropsDefaultProxy } from '/vue-macros/jsx-macros/with-defaults'
+defineComponent(
+ (_props) => {
+ const props = createPropsDefaultProxy(_props, { bar: '' })
+ const attrs = useAttrs()
+ return () => (
+
+ {_props.foo}
+
+ )
+ },
+ { props: { foo: null, bar: { required: true } }, inheritAttrs: false },
+)
+```
+
+:::
+
+## defineModel
+
+- 不支持使用连字符的 model 名称。
+- 当表达式以 `!` 结尾时,将被推断为必需的 model。
+- 修改后的 model 可以同步读取,无需 `await nextTick()`。[相关问题](https://github.com/vuejs/core/issues/11080)
+
+```tsx
+import { ref } from 'vue'
+
+function Comp() {
+ const modelValue = defineModel()!
+ return {modelValue.value}
+}
+
+export default () => {
+ const foo = ref(1)
+ return
+}
+```
+
+::: details 编译后代码
+
+```tsx
+import { ref } from 'vue'
+import { useModel } from '/vue-macros/jsx-macros/use-model'
+
+function Comp(_props: {
+ modelValue: string
+ 'onUpdate:modelValue': (value: string) => any
+}) {
+ const modelValue = useModel(_props, 'modelValue', { required: true })
+ return {modelValue.value}
+}
+```
+
+:::
+
+## defineSlots
+
+- 如果使用泛型定义插槽,所有插槽将是可选的。
+
+```tsx
+const slots = defineSlots<{
+ default: () => any
+}>()
+
+slots.default?.()
+// ^ optional
+```
+
+- 支持默认插槽(推荐)。
+
+```tsx
+function Comp() {
+ const slots = defineSlots({
+ title: (props: { bar?: T }) => title slot: {props.bar}
,
+ default: (props: { foo: number }) => default slot: {props.foo}
,
+ })
+
+ return (
+ <>
+
+
+ >
+ )
+}
+
+export default () => (
+ >
+ {bar}
+ {foo}
+
+)
+```
+
+## defineExpose
+
+就像在 Vue SFC 中一样。
+
+```tsx
+import { shallowRef as useRef } from 'vue'
+
+const Comp = ({ foo = undefined as T }) => {
+ defineExpose({
+ foo,
+ })
+ return
+}
+
+export default () => {
+ const compRef = useRef()
+ console.log(compRef.value!.foo === 1)
+ return
+}
+```
+
+::: details 编译后代码
+
+::: code-group
+
+```tsx [vue]
+import { getCurrentInstance, shallowRef as useRef } from 'vue'
+import { useExpose } from '/vue-macros/jsx-macros/use-expose'
+
+const Comp = ({ foo }) => {
+ useExpose(getCurrentInstance(), {
+ foo,
+ })
+ return
+}
+```
+
+```tsx [react]
+/**
+ * vite.config.ts
+ *
+ * jsxMacros({
+ * lib: 'react'
+ * })
+ */
+import { forwardRef, useImperativeHandle } from 'react'
+import { useExpose } from '/vue-macros/jsx-macros/use-expose'
+
+const Comp = forwardRef(({ foo }, _ref) => {
+ useImperativeHandle(
+ _ref,
+ () => ({
+ foo,
+ }),
+ [foo],
+ )
+ return
+})
+```
+
+```tsx [react19]
+/**
+ * vite.config.ts
+ *
+ * jsxMacros({
+ * lib: 'react',
+ * version: 19
+ * })
+ */
+import { forwardRef, useImperativeHandle } from 'react'
+import { useExpose } from '/vue-macros/jsx-macros/use-expose'
+
+const Comp = ({ foo, ..._props }) => {
+ useImperativeHandle(
+ _props.ref,
+ () => ({
+ foo,
+ }),
+ [foo],
+ )
+ return
+}
+```
+
+```tsx [preact]
+/**
+ * vite.config.ts
+ *
+ * jsxMacros({
+ * lib: 'preact'
+ * })
+ */
+import { forwardRef } from 'preact/compat'
+import { useImperativeHandle } from 'preact/hooks'
+import { useExpose } from '/vue-macros/jsx-macros/use-expose'
+
+const Comp = forwardRef(({ foo }, _ref) => {
+ useImperativeHandle(
+ _ref,
+ () => ({
+ foo,
+ }),
+ [foo],
+ )
+ return
+})
+```
+
+:::
+
+## defineStyle
+
+```ts
+declare function defineStyle(
+ style: string,
+ options?: { scoped?: boolean },
+): void
+```
+
+- 支持 CSS 变量和 JS 变量绑定。
+- 支持在一个文件中定义多个 style 宏。
+- 支持 CSS 预处理器:`css`、`scss`、`sass`、`less`、`stylus`、`postcss`。
+
+```ts
+defineStyle.scss(`...`)
+defineStyle.stylus(`...`)
+// ...
+```
+
+- 支持 scoped 模式。
+ - 如果在文件的顶层定义,scoped 选项默认为 `false`。
+ - 如果在函数内定义,scoped 选项默认为 `true`。
+
+```tsx
+function Comp({ color = 'red' }) {
+ defineStyle.scss(`
+ .foo {
+ color: ${color};
+
+ :deep(.bar) {
+ color: blue;
+ }
+ }
+ `)
+ return
+}
+
+defineStyle(`
+ .bar {
+ background: black;
+ }
+`)
+```
+
+- 支持 `css modules`, 如果宏是赋值表达式。
+
+```tsx
+export default () => {
+ const styles = defineStyle.scss(`
+ .foo {
+ color: blue;
+ .bar {
+ background: red;
+ }
+ }
+ `)
+
+ return
+}
+```
+
+## Volar 配置
+
+```json {5} [tsconfig.json]
+{
+ "vueCompilerOptions": {
+ "plugins": ["unplugin-vue-macros/volar"],
+ "vueMacros": {
+ "jsxMacros": true,
+ "scriptSFC": true
+ }
+ }
+}
+```
diff --git a/eslint.config.ts b/eslint.config.ts
index 4842e9924..6f8da68f7 100644
--- a/eslint.config.ts
+++ b/eslint.config.ts
@@ -36,6 +36,7 @@ export default sxzz({
'playground/vue3/**',
'packages/{volar,eslint-config}/**',
'**/raw.ts',
+ '**/volar.ts',
],
rules: {
'import/no-default-export': 'off',
diff --git a/monoman.config.ts b/monoman.config.ts
index 1184a7819..4508188a3 100644
--- a/monoman.config.ts
+++ b/monoman.config.ts
@@ -162,7 +162,7 @@ export default unplugin.${entry} as typeof unplugin.${entry}\n`,
const map: Record = {}
if (withDev) map.dev = `./src/${entry}.ts`
- if (entry === 'volar') {
+ if (entry === 'volar' && pkgName !== 'jsx') {
map.types = `./volar.d.ts`
map.default = `./volar.cjs`
} else {
diff --git a/packages/config/package.json b/packages/config/package.json
index 52761a660..04fb5e34d 100644
--- a/packages/config/package.json
+++ b/packages/config/package.json
@@ -96,6 +96,7 @@
"@vue-macros/export-render": "workspace:*",
"@vue-macros/hoist-static": "workspace:*",
"@vue-macros/jsx-directive": "workspace:*",
+ "@vue-macros/jsx-macros": "workspace:*",
"@vue-macros/named-template": "workspace:*",
"@vue-macros/reactivity-transform": "workspace:*",
"@vue-macros/script-lang": "workspace:*",
diff --git a/packages/config/src/options.ts b/packages/config/src/options.ts
index 40fbe751e..3f26ca87f 100644
--- a/packages/config/src/options.ts
+++ b/packages/config/src/options.ts
@@ -36,6 +36,7 @@ import type { Options as OptionsExportProps } from '@vue-macros/export-props'
import type { Options as OptionsExportRender } from '@vue-macros/export-render'
import type { Options as OptionsHoistStatic } from '@vue-macros/hoist-static'
import type { Options as OptionsJsxDirective } from '@vue-macros/jsx-directive'
+import type { Options as OptionsJsxMacros } from '@vue-macros/jsx-macros'
import type { Options as OptionsNamedTemplate } from '@vue-macros/named-template'
import type { Options as OptionsReactivityTransform } from '@vue-macros/reactivity-transform'
import type { Options as OptionsScriptLang } from '@vue-macros/script-lang'
@@ -151,6 +152,11 @@ export interface FeatureOptionsMap {
* @default true
*/
jsxDirective: OptionsJsxDirective
+ /**
+ * @see {@link https://vue-macros.dev/features/jsx-macros.html}
+ * @default false
+ */
+ jsxMacros: OptionsJsxMacros
/**
* @see {@link https://vue-macros.dev/features/jsx-ref.html}
* @default false
@@ -266,6 +272,7 @@ export const resolveOptions: QuansyncFn<
exportRender: resolveSubOptions('exportRender', false),
hoistStatic: resolveSubOptions('hoistStatic'),
jsxDirective: resolveSubOptions('jsxDirective'),
+ jsxMacros: resolveSubOptions('jsxMacros', false),
jsxRef: resolveSubOptions('jsxRef', false),
namedTemplate: resolveSubOptions('namedTemplate'),
reactivityTransform: resolveSubOptions('reactivityTransform'),
diff --git a/packages/jsx-macros/README.md b/packages/jsx-macros/README.md
new file mode 100644
index 000000000..ada30e804
--- /dev/null
+++ b/packages/jsx-macros/README.md
@@ -0,0 +1,3 @@
+# @vue-macros/jsx-macros [](https://npmjs.com/package/@vue-macros/jsx-macros)
+
+Please refer to [README.md](https://github.com/vue-macros/vue-macros#readme)
diff --git a/packages/jsx-macros/package.json b/packages/jsx-macros/package.json
new file mode 100644
index 000000000..b95042232
--- /dev/null
+++ b/packages/jsx-macros/package.json
@@ -0,0 +1,121 @@
+{
+ "name": "@vue-macros/jsx-macros",
+ "version": "3.0.0-beta.5",
+ "packageManager": "pnpm@10.4.1",
+ "description": "jsxMacros feature from Vue Macros.",
+ "type": "module",
+ "keywords": [
+ "vue-macros",
+ "macros",
+ "vue",
+ "sfc",
+ "setup",
+ "script-setup",
+ "jsx-macros",
+ "unplugin"
+ ],
+ "license": "MIT",
+ "homepage": "https://vue-macros.dev",
+ "bugs": {
+ "url": "https://github.com/vue-macros/vue-macros/issues"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/vue-macros/vue-macros.git",
+ "directory": "packages/jsx-macros"
+ },
+ "author": "zhiyuanzmj",
+ "contributors": [
+ "三咲智子 "
+ ],
+ "funding": "https://github.com/sponsors/vue-macros",
+ "files": [
+ "dist"
+ ],
+ "main": "dist/index.js",
+ "module": "dist/index.js",
+ "types": "dist/index.d.ts",
+ "exports": {
+ ".": {
+ "dev": "./src/index.ts",
+ "default": "./dist/index.js"
+ },
+ "./api": {
+ "dev": "./src/api.ts",
+ "default": "./dist/api.js"
+ },
+ "./esbuild": {
+ "dev": "./src/esbuild.ts",
+ "default": "./dist/esbuild.js"
+ },
+ "./raw": {
+ "dev": "./src/raw.ts",
+ "default": "./dist/raw.js"
+ },
+ "./rolldown": {
+ "dev": "./src/rolldown.ts",
+ "default": "./dist/rolldown.js"
+ },
+ "./rollup": {
+ "dev": "./src/rollup.ts",
+ "default": "./dist/rollup.js"
+ },
+ "./rspack": {
+ "dev": "./src/rspack.ts",
+ "default": "./dist/rspack.js"
+ },
+ "./vite": {
+ "dev": "./src/vite.ts",
+ "default": "./dist/vite.js"
+ },
+ "./webpack": {
+ "dev": "./src/webpack.ts",
+ "default": "./dist/webpack.js"
+ },
+ "./*": "./*"
+ },
+ "typesVersions": {
+ "*": {
+ "*": [
+ "./dist/*",
+ "./*"
+ ]
+ }
+ },
+ "publishConfig": {
+ "access": "public",
+ "exports": {
+ ".": "./dist/index.js",
+ "./api": "./dist/api.js",
+ "./esbuild": "./dist/esbuild.js",
+ "./raw": "./dist/raw.js",
+ "./rolldown": "./dist/rolldown.js",
+ "./rollup": "./dist/rollup.js",
+ "./rspack": "./dist/rspack.js",
+ "./vite": "./dist/vite.js",
+ "./webpack": "./dist/webpack.js",
+ "./*": "./*"
+ },
+ "tag": "next"
+ },
+ "scripts": {
+ "build": "tsup",
+ "dev": "DEV=true tsup"
+ },
+ "peerDependencies": {
+ "vue": "^2.7.0 || ^3.0.0"
+ },
+ "dependencies": {
+ "@vue-macros/common": "workspace:*",
+ "@vue/compiler-sfc": "catalog:",
+ "hash-sum": "catalog:",
+ "unplugin": "catalog:"
+ },
+ "devDependencies": {
+ "@types/hash-sum": "catalog:",
+ "vue": "catalog:"
+ },
+ "engines": {
+ "node": ">=20.18.0"
+ }
+}
diff --git a/packages/jsx-macros/src/api.ts b/packages/jsx-macros/src/api.ts
new file mode 100644
index 000000000..46d458ad7
--- /dev/null
+++ b/packages/jsx-macros/src/api.ts
@@ -0,0 +1 @@
+export * from './core'
diff --git a/packages/jsx-macros/src/core/define-component/await.ts b/packages/jsx-macros/src/core/define-component/await.ts
new file mode 100644
index 000000000..f63edcfad
--- /dev/null
+++ b/packages/jsx-macros/src/core/define-component/await.ts
@@ -0,0 +1,97 @@
+// Modified from: https://github.com/vuejs/core/blob/main/packages/compiler-sfc/src/script/topLevelAwait.ts
+
+import {
+ importHelperFn,
+ walkAST,
+ type MagicStringAST,
+} from '@vue-macros/common'
+import type { FunctionalNode } from '..'
+import type { AwaitExpression, Function, Node, Statement } from '@babel/types'
+
+// Copied from @vue/compiler-core
+export const isFunctionType = (node: Node): node is Function => {
+ return /Function(?:Expression|Declaration)$|Method$/.test(node.type)
+}
+
+export function transformAwait(root: FunctionalNode, s: MagicStringAST): void {
+ if (root.body.type !== 'BlockStatement') return
+ let hasAwait = false
+ for (const node of root.body.body) {
+ if (
+ (node.type === 'VariableDeclaration' && !node.declare) ||
+ node.type.endsWith('Statement')
+ ) {
+ const scope: Statement[][] = [root.body.body]
+ walkAST(node, {
+ enter(child, parent) {
+ if (isFunctionType(child)) {
+ this.skip()
+ }
+ if (child.type === 'BlockStatement') {
+ scope.push(child.body)
+ }
+ if (child.type === 'AwaitExpression') {
+ hasAwait = true
+ // if the await expression is an expression statement and
+ // - is in the root scope
+ // - or is not the first statement in a nested block scope
+ // then it needs a semicolon before the generated code.
+ const currentScope = scope.at(-1)
+ const needsSemi = !!currentScope?.some((n, i) => {
+ return (
+ (scope.length === 1 || i > 0) &&
+ n.type === 'ExpressionStatement' &&
+ n.start === child.start
+ )
+ })
+ processAwait(
+ s,
+ child,
+ needsSemi,
+ parent!.type === 'ExpressionStatement',
+ )
+ }
+ },
+ leave(node: Node) {
+ if (node.type === 'BlockStatement') scope.pop()
+ },
+ })
+ }
+ }
+
+ if (hasAwait) {
+ s.prependLeft(root.body.start! + 1, `\nlet __temp, __restore\n`)
+ }
+}
+
+function processAwait(
+ s: MagicStringAST,
+ node: AwaitExpression,
+ needSemi: boolean,
+ isStatement: boolean,
+): void {
+ const argumentStart =
+ node.argument.extra && node.argument.extra.parenthesized
+ ? (node.argument.extra.parenStart as number)
+ : node.argument.start!
+
+ const argumentStr = s.slice(argumentStart, node.argument.end!)
+
+ const containsNestedAwait = /\bawait\b/.test(argumentStr)
+
+ s.overwrite(
+ node.start!,
+ argumentStart,
+ `${needSemi ? `;` : ``}(\n ([__temp,__restore] = ${importHelperFn(
+ s,
+ 0,
+ `withAsyncContext`,
+ )}(${containsNestedAwait ? `async ` : ``}() => `,
+ )
+ s.appendLeft(
+ node.end!,
+ `)),\n ${isStatement ? `` : `__temp = `}await __temp,\n __restore()${
+ isStatement ? `` : `,\n __temp`
+ }\n)`,
+ )
+}
diff --git a/packages/jsx-macros/src/core/define-component/index.ts b/packages/jsx-macros/src/core/define-component/index.ts
new file mode 100644
index 000000000..f1542b6ac
--- /dev/null
+++ b/packages/jsx-macros/src/core/define-component/index.ts
@@ -0,0 +1,144 @@
+import { importHelperFn, type MagicStringAST } from '@vue-macros/common'
+import { walkIdentifiers } from '@vue/compiler-sfc'
+import { restructure } from '../restructure'
+import type { FunctionalNode, RootMapValue } from '..'
+import type { OptionsResolved } from '../..'
+import { transformAwait } from './await'
+import { transformReturn } from './return'
+import type { ObjectExpression, Program } from '@babel/types'
+
+export function transformDefineComponent(
+ root: FunctionalNode,
+ propsName: string,
+ map: RootMapValue,
+ s: MagicStringAST,
+ ast: Program,
+ options: OptionsResolved,
+): void {
+ if (!map.defineComponent) return
+
+ const defineComponentName = s.sliceNode(map.defineComponent.callee)
+ if (
+ defineComponentName &&
+ !['defineComponent', 'defineVaporComponent'].includes(defineComponentName)
+ ) {
+ importHelperFn(s, 0, 'defineComponent', defineComponentName)
+ }
+
+ let hasRestProp = false
+ const props: Record = {}
+ if (root.params[0]) {
+ if (root.params[0].type === 'Identifier') {
+ getWalkedIds(root, propsName).forEach((id) => (props[id] = null))
+ } else {
+ const restructuredProps = restructure(s, root, {
+ generateRestProps: (restPropsName, index, list) => {
+ if (index === list.length - 1) {
+ hasRestProp = true
+ const useAttrs = importHelperFn(s, 0, 'useAttrs')
+ return `const ${restPropsName} = ${useAttrs}()`
+ }
+ },
+ })
+ for (const prop of restructuredProps) {
+ if (prop.path.endsWith('props') && !prop.isRest) {
+ props[prop.name] = prop.isRequired ? '{ required: true }' : null
+ }
+ }
+ }
+ }
+
+ for (const { expression, isRequired } of map.defineModel || []) {
+ const modelOptions =
+ expression.arguments[0]?.type === 'ObjectExpression'
+ ? expression.arguments[0]
+ : expression.arguments[1]?.type === 'ObjectExpression'
+ ? expression.arguments[1]
+ : undefined
+ const options: any = {}
+ if (isRequired) options.required = true
+ for (const prop of modelOptions?.properties || []) {
+ if (
+ prop.type === 'ObjectProperty' &&
+ prop.key.type === 'Identifier' &&
+ ['validator', 'type', 'required'].includes(prop.key.name)
+ ) {
+ options[prop.key.name] = s.sliceNode(prop.value)
+ }
+ }
+ const propName =
+ expression.arguments[0]?.type === 'StringLiteral'
+ ? expression.arguments[0].value
+ : 'modelValue'
+ props[propName] = Object.keys(options).length
+ ? `{ ${Object.entries(options)
+ .map(([key, value]) => `${key}: ${value}`)
+ .join(', ')} }`
+ : null
+ props[`onUpdate:${propName}`] = null
+ props[`${propName === 'modelValue' ? 'model' : propName}Modifiers`] = null
+ }
+
+ const propsString = Object.entries(props)
+ .map(([key, value]) => `'${key}': ${value}`)
+ .join(', ')
+ if (propsString) {
+ const argument = map.defineComponent.arguments[1]
+ if (!argument) {
+ s.appendRight(
+ root.end!,
+ `, {${hasRestProp ? 'inheritAttrs: false,' : ''} props: { ${propsString} } }`,
+ )
+ } else if (argument.type === 'ObjectExpression') {
+ prependObjectExpression(argument, 'props', `{ ${propsString} }`, s)
+ if (hasRestProp) {
+ prependObjectExpression(argument, 'inheritAttrs', 'false', s)
+ }
+ }
+ }
+
+ transformAwait(root, s)
+ transformReturn(root, s, options.lib)
+}
+
+function prependObjectExpression(
+ argument: ObjectExpression,
+ name: string,
+ value: string,
+ s: MagicStringAST,
+) {
+ if (
+ !argument.properties?.find(
+ (prop) =>
+ prop.type === 'ObjectProperty' &&
+ prop.key.type === 'Identifier' &&
+ prop.key.name === name,
+ )
+ ) {
+ s.appendRight(argument.start! + 1, `${name}: ${value},`)
+ }
+}
+
+function getWalkedIds(root: FunctionalNode, propsName: string) {
+ const walkedIds = new Set()
+ walkIdentifiers(
+ root.body,
+ (id, parent) => {
+ if (
+ id.name === propsName &&
+ (parent?.type === 'MemberExpression' ||
+ parent?.type === 'OptionalMemberExpression')
+ ) {
+ const prop =
+ parent.property.type === 'Identifier'
+ ? parent.property.name
+ : parent.property.type === 'StringLiteral'
+ ? parent.property.value
+ : ''
+ if (prop) walkedIds.add(prop)
+ }
+ },
+ false,
+ )
+ return walkedIds
+}
diff --git a/packages/jsx-macros/src/core/define-component/return.ts b/packages/jsx-macros/src/core/define-component/return.ts
new file mode 100644
index 000000000..2364930c0
--- /dev/null
+++ b/packages/jsx-macros/src/core/define-component/return.ts
@@ -0,0 +1,21 @@
+import { isFunctionalNode, type FunctionalNode } from '..'
+import type { MagicStringAST } from '@vue-macros/common'
+
+export function transformReturn(
+ root: FunctionalNode,
+ s: MagicStringAST,
+ lib: string,
+): void {
+ if (lib !== 'vue') return
+
+ const node =
+ root.body.type === 'BlockStatement'
+ ? root.body.body.find((node) => node.type === 'ReturnStatement')?.argument
+ : root.body
+ if (!node || isFunctionalNode(node)) return
+
+ s.appendRight(
+ node.extra?.parenthesized ? (node.extra.parenStart as number) : node.start!,
+ '() => ',
+ )
+}
diff --git a/packages/jsx-macros/src/core/define-expose/index.ts b/packages/jsx-macros/src/core/define-expose/index.ts
new file mode 100644
index 000000000..812bb85fa
--- /dev/null
+++ b/packages/jsx-macros/src/core/define-expose/index.ts
@@ -0,0 +1,23 @@
+import type { FunctionalNode } from '..'
+import { transformReactDefineExpose } from './react'
+import { transformVueDefineExpose } from './vue'
+import type { CallExpression } from '@babel/types'
+import type { MagicStringAST } from '@vue-macros/common'
+
+export { transformVueDefineExpose } from './vue'
+export { transformReactDefineExpose } from './react'
+
+export function transformDefineExpose(
+ node: CallExpression,
+ propsName: string,
+ root: FunctionalNode,
+ s: MagicStringAST,
+ lib: string,
+ version: number,
+): void {
+ if (lib.includes('vue')) {
+ transformVueDefineExpose(node, s, lib)
+ } else if (lib.includes('react')) {
+ transformReactDefineExpose(node, propsName, root, s, lib, version)
+ }
+}
diff --git a/packages/jsx-macros/src/core/define-expose/react.ts b/packages/jsx-macros/src/core/define-expose/react.ts
new file mode 100644
index 000000000..109fbaa30
--- /dev/null
+++ b/packages/jsx-macros/src/core/define-expose/react.ts
@@ -0,0 +1,105 @@
+import {
+ HELPER_PREFIX,
+ importHelperFn,
+ type MagicStringAST,
+} from '@vue-macros/common'
+import { walkIdentifiers } from 'vue/compiler-sfc'
+import { getParamsStart, type FunctionalNode } from '..'
+import type { CallExpression, Node } from '@babel/types'
+
+export function transformReactDefineExpose(
+ node: CallExpression,
+ propsName: string,
+ root: FunctionalNode,
+ s: MagicStringAST,
+ lib: string,
+ version: number,
+): void {
+ const useImperativeHandle = importHelperFn(
+ s,
+ 0,
+ 'useImperativeHandle',
+ undefined,
+ lib === 'preact' ? 'preact/hooks' : lib,
+ )
+ const isReact19 = lib === 'react' && version >= 19
+ let refName = ''
+ if (isReact19) {
+ if (root.params[0]?.type === 'ObjectPattern') {
+ for (const prop of root.params[0].properties) {
+ if (
+ prop.type === 'ObjectProperty' &&
+ prop.key.type === 'Identifier' &&
+ prop.key.name === 'ref'
+ ) {
+ refName =
+ prop.value.type === 'Identifier' ? prop.value.name : prop.key.name
+ break
+ }
+ }
+ } else {
+ refName = `${propsName}.ref`
+ }
+ } else {
+ const forwardRef = importHelperFn(
+ s,
+ 0,
+ 'forwardRef',
+ undefined,
+ lib === 'preact' ? 'preact/compat' : lib,
+ )
+ if (root.type === 'FunctionDeclaration' && root.id) {
+ s.appendLeft(root.start!, `const ${root.id.name} = `)
+ }
+ s.appendLeft(root.start!, `${forwardRef}(`)
+
+ refName = root.params[1]
+ ? s.sliceNode(root.params[1])
+ : `${HELPER_PREFIX}ref`
+ if (!root.params[0]) {
+ s.appendRight(getParamsStart(root, s.original), `, ${refName}`)
+ } else if (!root.params[1]) {
+ s.appendLeft(root.params[0].end!, `, ${refName}`)
+ }
+ s.appendRight(root.end!, `)`)
+ }
+
+ s.overwrite(
+ node.start!,
+ node.arguments[0].start!,
+ `${useImperativeHandle}(${refName}, () =>(`,
+ )
+ const result = new Set()
+ walkIdentifiers(
+ node.arguments[0],
+ (id, _, parentStack, ___, isLocal) => {
+ if (!isLocal) {
+ let res: Node | null = id
+ for (let i = parentStack.length - 1; i >= 0; i--) {
+ if (
+ ['MemberExpression', 'OptionalMemberExpression'].includes(
+ parentStack[i].type,
+ )
+ ) {
+ res = parentStack[i]
+ } else {
+ if (
+ ['CallExpression', 'OptionalCallExpression'].includes(
+ parentStack[i].type,
+ )
+ ) {
+ res = null
+ }
+ break
+ }
+ }
+ if (res) result.add(s.sliceNode(res))
+ }
+ },
+ false,
+ )
+ s.appendRight(
+ node.arguments[0].end!,
+ `), ${`[${result.size ? [...result] : ''}]`}`,
+ )
+}
diff --git a/packages/jsx-macros/src/core/define-expose/vue.ts b/packages/jsx-macros/src/core/define-expose/vue.ts
new file mode 100644
index 000000000..85298f383
--- /dev/null
+++ b/packages/jsx-macros/src/core/define-expose/vue.ts
@@ -0,0 +1,20 @@
+import { importHelperFn, type MagicStringAST } from '@vue-macros/common'
+import { useExposeHelperId } from '../helper'
+import type { CallExpression } from '@babel/types'
+
+export function transformVueDefineExpose(
+ node: CallExpression,
+ s: MagicStringAST,
+ lib: string,
+): void {
+ s.overwriteNode(
+ node.callee,
+ importHelperFn(s, 0, 'useExpose', undefined, useExposeHelperId),
+ )
+ s.appendRight(
+ node.arguments[0]?.start || node.end! - 1,
+ lib.includes('vapor')
+ ? `${importHelperFn(s, 0, 'currentInstance')}, `
+ : `${importHelperFn(s, 0, 'getCurrentInstance')}(), `,
+ )
+}
diff --git a/packages/jsx-macros/src/core/define-model.ts b/packages/jsx-macros/src/core/define-model.ts
new file mode 100644
index 000000000..0423717cc
--- /dev/null
+++ b/packages/jsx-macros/src/core/define-model.ts
@@ -0,0 +1,20 @@
+import { importHelperFn, type MagicStringAST } from '@vue-macros/common'
+import { useModelHelperId } from './helper'
+import type { CallExpression } from '@babel/types'
+
+export function transformDefineModel(
+ node: CallExpression,
+ propsName: string,
+ s: MagicStringAST,
+): void {
+ s.overwriteNode(
+ node.callee,
+ importHelperFn(s, 0, 'useModel', undefined, useModelHelperId),
+ )
+ s.appendRight(
+ node.arguments[0]?.start || node.end! - 1,
+ `${propsName}, ${
+ node.arguments[0]?.type !== 'StringLiteral' ? `'modelValue',` : ''
+ }`,
+ )
+}
diff --git a/packages/jsx-macros/src/core/define-slots.ts b/packages/jsx-macros/src/core/define-slots.ts
new file mode 100644
index 000000000..de3e762c6
--- /dev/null
+++ b/packages/jsx-macros/src/core/define-slots.ts
@@ -0,0 +1,21 @@
+import { importHelperFn, type MagicStringAST } from '@vue-macros/common'
+import type { CallExpression } from '@babel/types'
+
+export function transformDefineSlots(
+ node: CallExpression,
+ propsName: string,
+ s: MagicStringAST,
+ lib: string,
+): void {
+ s.overwrite(
+ node.start!,
+ (node.arguments[0]?.start && node.arguments[0].start - 1) ||
+ node.typeArguments?.end ||
+ node.callee.end!,
+ `Object.assign`,
+ )
+ const slots = lib.includes('vue')
+ ? `${importHelperFn(s, 0, 'useSlots')}()`
+ : `${propsName}.vSlots`
+ s.appendLeft(node.end! - 1, `${node.arguments[0] ? ',' : '{}, '}${slots}`)
+}
diff --git a/packages/jsx-macros/src/core/define-style.ts b/packages/jsx-macros/src/core/define-style.ts
new file mode 100644
index 000000000..16c2488ca
--- /dev/null
+++ b/packages/jsx-macros/src/core/define-style.ts
@@ -0,0 +1,116 @@
+import { walkAST, type MagicStringAST } from '@vue-macros/common'
+import hash from 'hash-sum'
+import { helperPrefix } from './helper'
+import { isFunctionalNode, type DefineStyle, type FunctionalNode } from '.'
+import type { Node } from '@babel/types'
+
+export function transformDefineStyle(
+ defineStyle: DefineStyle,
+ index: number,
+ root: FunctionalNode | undefined,
+ s: MagicStringAST,
+ importMap: Map,
+): void {
+ const { expression, lang, isDeclaration } = defineStyle
+ if (expression.arguments[0]?.type !== 'TemplateLiteral') return
+
+ let css = s.sliceNode(expression.arguments[0]).slice(1, -1)
+ const scopeId = hash(css)
+ const vars = new Map()
+ expression.arguments[0].expressions.forEach((exp) => {
+ const cssVar = s.sliceNode(exp)
+ const cssVarId = toCssVarId(cssVar, `--${scopeId}-`)
+ s.overwrite(exp.start! - 2, exp.end! + 1, `var(${cssVarId})`)
+ vars.set(cssVarId, cssVar)
+ })
+
+ let returnExpression = root && getReturnStatement(root)
+ if (isFunctionalNode(returnExpression)) {
+ returnExpression = getReturnStatement(returnExpression)
+ }
+ if (vars.size && returnExpression) {
+ const children =
+ returnExpression.type === 'JSXElement'
+ ? [returnExpression]
+ : returnExpression.type === 'JSXFragment'
+ ? returnExpression.children
+ : []
+ const varString = Array.from(vars.entries())
+ .map(([key, value]) => `'${key}': ${value}`)
+ .join(', ')
+ for (const child of children) {
+ if (child.type === 'JSXElement') {
+ s.appendRight(
+ child.openingElement.name.end!,
+ ` {...{style:{${varString}}}}`,
+ )
+ }
+ }
+ }
+
+ let scoped = !!root
+ if (expression.arguments[1]?.type === 'ObjectExpression') {
+ for (const prop of expression.arguments[1].properties) {
+ if (
+ prop.type === 'ObjectProperty' &&
+ prop.key.type === 'Identifier' &&
+ prop.key.name === 'scoped' &&
+ prop.value.type === 'BooleanLiteral'
+ ) {
+ scoped = prop.value.value
+ }
+ }
+ }
+
+ if (scoped && returnExpression) {
+ walkAST(returnExpression, {
+ enter(node) {
+ if (
+ node.type === 'JSXElement' &&
+ s.sliceNode(node.openingElement.name) !== 'template'
+ ) {
+ s.appendRight(node.openingElement.name.end!, ` data-v-${scopeId}=""`)
+ }
+ },
+ })
+ }
+
+ css = s
+ .sliceNode(expression.arguments[0])
+ .slice(1, -1)
+ .replaceAll(/\/\/(.*)(?=\n)/g, '/*$1*/')
+ const module = isDeclaration ? 'module.' : ''
+ const importId = `${helperPrefix}/define-style/${index}?scopeId=${scopeId}&scoped=${scoped}&lang.${module}${lang}`
+ importMap.set(importId, css)
+ s.appendLeft(
+ 0,
+ isDeclaration
+ ? `import style${index} from "${importId}";`
+ : `import "${importId}";`,
+ )
+ s.overwriteNode(expression, isDeclaration ? `style${index}` : '')
+}
+
+function getReturnStatement(root: FunctionalNode) {
+ if (root.body.type === 'BlockStatement') {
+ const returnStatement = root.body.body.find(
+ (node) => node.type === 'ReturnStatement',
+ )
+ if (returnStatement) {
+ return returnStatement.argument
+ }
+ } else {
+ return root.body
+ }
+}
+
+function toCssVarId(name: string, prefix = '') {
+ return (
+ prefix +
+ name.replaceAll(/\W/g, (searchValue, replaceValue) => {
+ return searchValue === '.'
+ ? '-'
+ : name.charCodeAt(replaceValue).toString()
+ })
+ )
+}
diff --git a/packages/jsx-macros/src/core/helper/index.ts b/packages/jsx-macros/src/core/helper/index.ts
new file mode 100644
index 000000000..8e778b72f
--- /dev/null
+++ b/packages/jsx-macros/src/core/helper/index.ts
@@ -0,0 +1,12 @@
+import { VIRTUAL_ID_PREFIX } from '@vue-macros/common'
+
+export const helperPrefix: '/vue-macros/jsx-macros' = `${VIRTUAL_ID_PREFIX}/jsx-macros`
+
+export const useExposeHelperId: '/vue-macros/jsx-macros/use-expose' = `${helperPrefix}/use-expose`
+export { default as useExposeHelperCode } from './use-expose?raw'
+
+export const useModelHelperId: '/vue-macros/jsx-macros/use-model' = `${helperPrefix}/use-model`
+export { default as useModelHelperCode } from './use-model?raw'
+
+export const withDefaultsHelperId: '/vue-macros/jsx-macros/with-defaults' = `${helperPrefix}/with-defaults`
+export { default as withDefaultsHelperCode } from './with-defaults?raw'
diff --git a/packages/jsx-macros/src/core/helper/use-expose.ts b/packages/jsx-macros/src/core/helper/use-expose.ts
new file mode 100644
index 000000000..bd21d5a28
--- /dev/null
+++ b/packages/jsx-macros/src/core/helper/use-expose.ts
@@ -0,0 +1,10 @@
+export function useExpose>(
+ i: any,
+ exposed: T,
+): T {
+ if (i) {
+ i.exposed = exposed
+ if (i.vnode) i.vnode.shapeFlag = 4
+ }
+ return exposed
+}
diff --git a/packages/jsx-macros/src/core/helper/use-model.ts b/packages/jsx-macros/src/core/helper/use-model.ts
new file mode 100644
index 000000000..37402a9f4
--- /dev/null
+++ b/packages/jsx-macros/src/core/helper/use-model.ts
@@ -0,0 +1,66 @@
+import { customRef, watchSyncEffect, type ModelRef } from 'vue'
+
+type DefineModelOptions> = {
+ default?: any
+ get?: (v: T) => any
+ set?: (v: T) => any
+}
+export function useModel<
+ M extends PropertyKey,
+ T extends Record,
+ K extends keyof T,
+>(props: T, name: K, options?: DefineModelOptions): ModelRef
+export function useModel(
+ props: Record,
+ name: string,
+ options: DefineModelOptions = {},
+): any {
+ const res = customRef((track, trigger) => {
+ let localValue: any = options && options.default
+ let prevEmittedValue: any
+
+ watchSyncEffect(() => {
+ const propValue = props[name]
+ if (!Object.is(prevEmittedValue, propValue)) {
+ localValue = propValue
+ trigger()
+ }
+ })
+
+ return {
+ get() {
+ track()
+ return options.get ? options.get(localValue) : localValue
+ },
+
+ set(value) {
+ if (Object.is(value, localValue)) return
+ localValue = value
+ trigger()
+ const emittedValue = (prevEmittedValue = options.set
+ ? options.set(value)
+ : value)
+ for (const emit of [props[`onUpdate:${name}`]].flat()) {
+ if (typeof emit === 'function') emit(emittedValue)
+ }
+ },
+ }
+ })
+
+ const modifiers =
+ name === 'modelValue' ? props.modelModifiers : props[`${name}Modifiers`]
+ // @ts-expect-error
+ res[Symbol.iterator] = () => {
+ let i = 0
+ return {
+ next() {
+ if (i < 2) {
+ return { value: i++ ? modifiers || {} : res, done: false }
+ } else {
+ return { done: true }
+ }
+ },
+ }
+ }
+ return res
+}
diff --git a/packages/jsx-macros/src/core/helper/with-defaults.ts b/packages/jsx-macros/src/core/helper/with-defaults.ts
new file mode 100644
index 000000000..2db52b8de
--- /dev/null
+++ b/packages/jsx-macros/src/core/helper/with-defaults.ts
@@ -0,0 +1,41 @@
+function resolveDefaultProps(paths: Record): any {
+ const result: Record = {}
+
+ for (const path of Object.keys(paths)) {
+ const segments = path.split(/[?.[\]]/).filter(Boolean)
+ let current = result
+
+ for (let i = 0; i < segments.length; i++) {
+ const segment = segments[i]
+ if (i === segments.length - 1) {
+ current[segment] = paths[path]
+ } else {
+ if (!current[segment]) {
+ current[segment] = Number.isNaN(Number(segments[i + 1])) ? {} : []
+ }
+ current = current[segment]
+ }
+ }
+ }
+
+ return result
+}
+
+export function createPropsDefaultProxy(
+ props: Record,
+ defaults: Record,
+): Record {
+ const defaultProps = resolveDefaultProps(defaults)
+ const result: Record = {}
+
+ for (const key of [
+ ...new Set([...Object.keys(props), ...Object.keys(defaultProps)]),
+ ]) {
+ Object.defineProperty(result, key, {
+ enumerable: true,
+ get: () => (props[key] === undefined ? defaultProps[key] : props[key]),
+ })
+ }
+
+ return result
+}
diff --git a/packages/jsx-macros/src/core/index.ts b/packages/jsx-macros/src/core/index.ts
new file mode 100644
index 000000000..a001e0499
--- /dev/null
+++ b/packages/jsx-macros/src/core/index.ts
@@ -0,0 +1,237 @@
+import {
+ babelParse,
+ generateTransform,
+ getLang,
+ HELPER_PREFIX,
+ MagicStringAST,
+ walkAST,
+ type CodeTransform,
+} from '@vue-macros/common'
+import type { OptionsResolved } from '..'
+import { transformDefineComponent } from './define-component'
+import { transformDefineExpose } from './define-expose'
+import { transformDefineModel } from './define-model'
+import { transformDefineSlots } from './define-slots'
+import { transformDefineStyle } from './define-style'
+import type {
+ ArrowFunctionExpression,
+ CallExpression,
+ FunctionDeclaration,
+ FunctionExpression,
+ Node,
+ Program,
+} from '@babel/types'
+
+export { restructure } from './restructure'
+
+export type FunctionalNode =
+ | FunctionDeclaration
+ | FunctionExpression
+ | ArrowFunctionExpression
+
+export type DefineStyle = {
+ expression: CallExpression
+ isDeclaration: boolean
+ lang: string
+}
+
+export type RootMapValue = {
+ defineComponent?: CallExpression
+ defineModel?: {
+ expression: CallExpression
+ isRequired: boolean
+ }[]
+ defineSlots?: CallExpression
+ defineExpose?: CallExpression
+ defineStyle?: DefineStyle[]
+}
+
+export function transformJsxMacros(
+ code: string,
+ id: string,
+ importMap: Map,
+ options: OptionsResolved,
+): CodeTransform | undefined {
+ const s = new MagicStringAST(code)
+ const ast = babelParse(s.original, getLang(id))
+ const rootMap = getRootMap(ast, s, options)
+
+ let defineStyleIndex = 0
+ for (const [root, map] of rootMap) {
+ map.defineStyle?.forEach((defineStyle) => {
+ transformDefineStyle(defineStyle, defineStyleIndex++, root, s, importMap)
+ })
+
+ if (root === undefined) continue
+
+ let propsName = `${HELPER_PREFIX}props`
+ if (root.params[0]) {
+ if (root.params[0].type === 'Identifier') {
+ propsName = root.params[0].name
+ } else if (root.params[0].type === 'ObjectPattern') {
+ const lastProp = root.params[0].properties.at(-1)
+ if (
+ !map.defineComponent &&
+ lastProp?.type === 'RestElement' &&
+ lastProp.argument.type === 'Identifier'
+ ) {
+ propsName = lastProp.argument.name
+ } else {
+ s.appendRight(
+ root.params[0].extra?.trailingComma
+ ? (root.params[0].extra?.trailingComma as number) + 1
+ : lastProp?.end || root.params[0].end! - 1,
+ `${
+ !root.params[0].extra?.trailingComma &&
+ root.params[0].properties.length
+ ? ','
+ : ''
+ } ...${HELPER_PREFIX}props`,
+ )
+ }
+ }
+ } else {
+ s.appendRight(getParamsStart(root, s.original), propsName)
+ }
+
+ if (map.defineComponent) {
+ transformDefineComponent(root, propsName, map, s, ast, options)
+ }
+ if (map.defineModel?.length) {
+ map.defineModel.forEach(({ expression }) => {
+ transformDefineModel(expression, propsName, s)
+ })
+ }
+ if (map.defineSlots) {
+ transformDefineSlots(map.defineSlots, propsName, s, options.lib)
+ }
+ if (map.defineExpose) {
+ transformDefineExpose(
+ map.defineExpose,
+ propsName,
+ root,
+ s,
+ options.lib,
+ options.version,
+ )
+ }
+ }
+
+ return generateTransform(s, id)
+}
+
+function getRootMap(ast: Program, s: MagicStringAST, options: OptionsResolved) {
+ const parents: (Node | undefined | null)[] = []
+ const rootMap = new Map()
+ walkAST(ast, {
+ enter(node, parent) {
+ parents.unshift(parent)
+ const root = isFunctionalNode(parents[1]) ? parents[1] : undefined
+
+ if (
+ root &&
+ parents[2]?.type === 'CallExpression' &&
+ options.defineComponent.alias.includes(s.sliceNode(parents[2].callee))
+ ) {
+ if (!rootMap.has(root)) rootMap.set(root, {})
+ if (!rootMap.get(root)!.defineComponent) {
+ rootMap.get(root)!.defineComponent = parents[2]
+ }
+ }
+
+ const expression =
+ node.type === 'VariableDeclaration'
+ ? node.declarations[0].init?.type === 'CallExpression' &&
+ s.sliceNode(node.declarations[0].init.callee) === '$'
+ ? node.declarations[0].init.arguments[0]
+ : node.declarations[0].init
+ : node.type === 'ExpressionStatement'
+ ? node.expression
+ : undefined
+ if (!expression) return
+ const macroExpression = getMacroExpression(expression, options)
+ if (!macroExpression) return
+ if (!rootMap.has(root)) rootMap.set(root, {})
+ const macroName = s.sliceNode(
+ macroExpression.callee.type === 'MemberExpression'
+ ? macroExpression.callee.object
+ : macroExpression.callee,
+ )
+ if (macroName) {
+ if (options.defineModel.alias.includes(macroName)) {
+ ;(rootMap.get(root)!.defineModel ??= []).push({
+ expression: macroExpression,
+ isRequired: expression.type === 'TSNonNullExpression',
+ })
+ } else if (options.defineStyle.alias.includes(macroName)) {
+ const lang =
+ macroExpression.callee.type === 'MemberExpression' &&
+ macroExpression.callee.property.type === 'Identifier'
+ ? macroExpression.callee.property.name
+ : 'css'
+ ;(rootMap.get(root)!.defineStyle ??= []).push({
+ expression: macroExpression,
+ isDeclaration: node.type === 'VariableDeclaration',
+ lang,
+ })
+ } else if (options.defineSlots.alias.includes(macroName)) {
+ rootMap.get(root)!.defineSlots = macroExpression
+ } else if (options.defineExpose.alias.includes(macroName)) {
+ rootMap.get(root)!.defineExpose = macroExpression
+ }
+ }
+ },
+ leave() {
+ parents.shift()
+ },
+ })
+ return rootMap
+}
+
+export function isFunctionalNode(node?: Node | null): node is FunctionalNode {
+ return !!(
+ node &&
+ (node.type === 'ArrowFunctionExpression' ||
+ node.type === 'FunctionDeclaration' ||
+ node.type === 'FunctionExpression')
+ )
+}
+
+export function getMacroExpression(
+ node: Node,
+ options: OptionsResolved,
+): CallExpression | undefined {
+ if (node.type === 'TSNonNullExpression') {
+ node = node.expression
+ }
+
+ if (node.type === 'CallExpression') {
+ if (
+ node.callee.type === 'MemberExpression' &&
+ node.callee.object.type === 'Identifier' &&
+ node.callee.object.name === 'defineStyle'
+ ) {
+ return node
+ } else if (
+ node.callee.type === 'Identifier' &&
+ [
+ ...options.defineComponent.alias,
+ ...options.defineSlots.alias,
+ ...options.defineModel.alias,
+ ...options.defineExpose.alias,
+ ...options.defineStyle.alias,
+ ].includes(node.callee.name!)
+ ) {
+ return node
+ }
+ }
+}
+
+export function getParamsStart(node: FunctionalNode, code: string): number {
+ return node.params[0]
+ ? node.params[0].start!
+ : node.start! +
+ (code.slice(node.start!, node.body.start!).match(/\(\s*\)/)?.index ||
+ 0) +
+ 1
+}
diff --git a/packages/jsx-macros/src/core/plugin.ts b/packages/jsx-macros/src/core/plugin.ts
new file mode 100644
index 000000000..affaa9043
--- /dev/null
+++ b/packages/jsx-macros/src/core/plugin.ts
@@ -0,0 +1,112 @@
+import {
+ createFilter,
+ detectVueVersion,
+ FilterFileType,
+ getFilterPattern,
+ normalizePath,
+ REGEX_NODE_MODULES,
+ REGEX_SETUP_SFC,
+ type BaseOptions,
+ type MarkRequired,
+} from '@vue-macros/common'
+import { generatePluginName } from '#macros' with { type: 'macro' }
+import {
+ helperPrefix,
+ useExposeHelperCode,
+ useExposeHelperId,
+ useModelHelperCode,
+ useModelHelperId,
+ withDefaultsHelperCode,
+ withDefaultsHelperId,
+} from './helper'
+import { transformStyle } from './style'
+import { transformJsxMacros } from '.'
+import type { UnpluginContextMeta, UnpluginFactory } from 'unplugin'
+
+export type Options = BaseOptions & {
+ lib?: 'vue' | 'vue/vapor' | 'react' | 'preact' | 'solid' | (string & {})
+ defineComponent?: { alias: string[] }
+ defineModel?: { alias: string[] }
+ defineExpose?: { alias: string[] }
+ defineSlots?: { alias: string[] }
+ defineStyle?: { alias: string[] }
+}
+export type OptionsResolved = MarkRequired<
+ Options,
+ | 'include'
+ | 'version'
+ | 'lib'
+ | 'defineComponent'
+ | 'defineModel'
+ | 'defineExpose'
+ | 'defineSlots'
+ | 'defineStyle'
+>
+
+function resolveOptions(
+ options: Options,
+ framework: UnpluginContextMeta['framework'],
+): OptionsResolved {
+ const version = options.version || detectVueVersion()
+ const lib = options.lib || 'vue'
+ const include = getFilterPattern([FilterFileType.SRC_FILE], framework)
+ return {
+ include,
+ exclude: [REGEX_SETUP_SFC, REGEX_NODE_MODULES],
+ ...options,
+ version,
+ lib,
+ defineComponent: {
+ alias:
+ options?.defineComponent?.alias ??
+ [
+ 'defineComponent',
+ lib === 'vue/vapor' ? 'defineVaporComponent' : '',
+ ].filter(Boolean),
+ },
+ defineModel: { alias: options?.defineModel?.alias ?? ['defineModel'] },
+ defineSlots: { alias: options?.defineSlots?.alias ?? ['defineSlots'] },
+ defineExpose: { alias: options?.defineExpose?.alias ?? ['defineExpose'] },
+ defineStyle: { alias: options?.defineStyle?.alias ?? ['defineStyle'] },
+ }
+}
+
+const name = generatePluginName()
+
+export const plugin: UnpluginFactory = (
+ userOptions: Options = {},
+ meta = { framework: 'vite' },
+) => {
+ const { framework } = meta
+ const options = resolveOptions(userOptions, framework)
+ const filter = createFilter(options)
+ const importMap = new Map()
+
+ return {
+ name,
+ enforce: 'pre',
+
+ resolveId(id) {
+ if (normalizePath(id).startsWith(helperPrefix)) return id
+ },
+ loadInclude(id) {
+ return normalizePath(id).startsWith(helperPrefix)
+ },
+ load(_id) {
+ const id = normalizePath(_id)
+ if (id === useExposeHelperId) return useExposeHelperCode
+ else if (id === useModelHelperId) return useModelHelperCode
+ else if (id === withDefaultsHelperId) return withDefaultsHelperCode
+ else if (importMap.get(id)) return importMap.get(id)
+ },
+
+ transformInclude(id) {
+ if (importMap.get(id)) return true
+ return filter(id)
+ },
+ transform(code, id) {
+ if (importMap.get(id)) return transformStyle(code, id, options)
+ return transformJsxMacros(code, id, importMap, options)
+ },
+ }
+}
diff --git a/packages/jsx-macros/src/core/restructure.ts b/packages/jsx-macros/src/core/restructure.ts
new file mode 100644
index 000000000..134c91fd5
--- /dev/null
+++ b/packages/jsx-macros/src/core/restructure.ts
@@ -0,0 +1,222 @@
+import {
+ HELPER_PREFIX,
+ importHelperFn,
+ type MagicString,
+} from '@vue-macros/common'
+import { walkIdentifiers } from '@vue/compiler-sfc'
+import { withDefaultsHelperId } from './helper'
+import type { FunctionalNode } from '.'
+import type { Node } from '@babel/types'
+
+type Options = {
+ withDefaultsFrom?: string
+ generateRestProps?: (
+ restPropsName: string,
+ index: number,
+ list: Prop[],
+ ) => string | undefined
+}
+
+type Prop = {
+ path: string
+ name: string
+ value: string
+ defaultValue?: string
+ isRest?: boolean
+ isRequired?: boolean
+}
+
+export function restructure(
+ s: MagicString,
+ node: FunctionalNode,
+ options: Options = {},
+): Prop[] {
+ let index = 0
+ const propList: Prop[] = []
+ for (const param of node.params) {
+ const path = `${HELPER_PREFIX}props${index++ || ''}`
+ const props = getProps(param, path, s, [], options)
+ if (props) {
+ const hasDefaultValue = props.some((i) => i.defaultValue)
+ s.overwrite(param.start!, param.end!, path)
+ propList.push(
+ ...(hasDefaultValue
+ ? props.map((i) => ({
+ ...i,
+ path: i.path.replace(HELPER_PREFIX, `${HELPER_PREFIX}default_`),
+ }))
+ : props),
+ )
+ }
+ }
+
+ if (propList.length) {
+ const defaultValues: Record = {}
+ const rests = []
+ for (const prop of propList) {
+ if (prop.defaultValue) {
+ const basePath = prop.path.split(/\.|\[/)[0]
+ ;(defaultValues[basePath] ??= []).push(prop)
+ }
+ if (prop.isRest) {
+ rests.push(prop)
+ }
+ }
+ for (const [path, values] of Object.entries(defaultValues)) {
+ const createPropsDefaultProxy = importHelperFn(
+ s,
+ 0,
+ 'createPropsDefaultProxy',
+ undefined,
+ options.withDefaultsFrom ?? withDefaultsHelperId,
+ )
+ const resolvedPath = path.replace(
+ `${HELPER_PREFIX}default_`,
+ HELPER_PREFIX,
+ )
+ const resolvedValues = values
+ .map(
+ (i) => `'${i.path.replace(path, '')}${i.value}': ${i.defaultValue}`,
+ )
+ .join(', ')
+ prependFunctionalNode(
+ node,
+ s,
+ `\nconst ${path} = ${createPropsDefaultProxy}(${resolvedPath}, {${resolvedValues}})`,
+ )
+ }
+
+ for (const [index, rest] of rests.entries()) {
+ prependFunctionalNode(
+ node,
+ s,
+ options.generateRestProps?.(rest.name, index, rests) ??
+ `\nconst ${rest.name} = ${importHelperFn(
+ s,
+ 0,
+ 'createPropsRestProxy',
+ )}(${rest.path}, [${rest.value}])`,
+ )
+ }
+
+ walkIdentifiers(
+ node.body,
+ (id, parent) => {
+ const prop = propList.find((i) => i.name === id.name)
+ if (prop && !prop.isRest) {
+ s.overwrite(
+ id.start!,
+ id.end!,
+ `${
+ parent?.type === 'ObjectProperty' && parent.shorthand
+ ? `${id.name}: `
+ : ''
+ }${prop.path}${prop.value}`,
+ )
+ }
+ },
+ false,
+ )
+ }
+
+ return propList
+}
+
+function getProps(
+ node: Node,
+ path: string = '',
+ s: MagicString,
+ props: Prop[] = [],
+ options: Options,
+) {
+ const properties =
+ node.type === 'ObjectPattern'
+ ? node.properties
+ : node.type === 'ArrayPattern'
+ ? node.elements
+ : []
+ if (!properties.length) return
+
+ const propNames: string[] = []
+ properties.forEach((prop, index) => {
+ if (prop?.type === 'Identifier') {
+ // { foo }
+ props.push({ name: prop.name, path, value: `[${index}]` })
+ propNames.push(`'${prop.name}'`)
+ } else if (
+ prop?.type === 'AssignmentPattern' &&
+ prop.left.type === 'Identifier'
+ ) {
+ // [foo = 'foo']
+ props.push({
+ path,
+ name: prop.left.name,
+ value: `[${index}]`,
+ defaultValue: s.slice(prop.right.start!, prop.right.end!),
+ })
+ propNames.push(`'${prop.left.name}'`)
+ } else if (
+ prop?.type === 'ObjectProperty' &&
+ prop.key.type === 'Identifier'
+ ) {
+ if (
+ prop.value.type === 'AssignmentPattern' &&
+ prop.value.left.type === 'Identifier'
+ ) {
+ // { foo: bar = 'foo' }
+ props.push({
+ path,
+ name: prop.value.left.name,
+ value: `.${prop.key.name}`,
+ defaultValue: s.slice(prop.value.right.start!, prop.value.right.end!),
+ isRequired: prop.value.right.type === 'TSNonNullExpression',
+ })
+ } else if (
+ !getProps(prop.value, `${path}.${prop.key.name}`, s, props, options)
+ ) {
+ // { foo: bar }
+ props.push({
+ path,
+ name:
+ prop.value.type === 'Identifier' ? prop.value.name : prop.key.name,
+ value: `.${prop.key.name}`,
+ })
+ }
+ propNames.push(`'${prop.key.name}'`)
+ } else if (
+ prop?.type === 'RestElement' &&
+ prop.argument.type === 'Identifier' &&
+ !prop.argument.name.startsWith(`${HELPER_PREFIX}props`)
+ ) {
+ // { ...rest }
+ props.push({
+ path,
+ name: prop.argument.name,
+ value: propNames.join(', '),
+ isRest: true,
+ })
+ } else if (prop) {
+ getProps(prop, `${path}[${index}]`, s, props, options)
+ }
+ })
+ return props.length ? props : undefined
+}
+
+function prependFunctionalNode(
+ node: FunctionalNode,
+ s: MagicString,
+ result: string,
+): void {
+ const isBlockStatement = node.body.type === 'BlockStatement'
+ const start = node.body.extra?.parenthesized
+ ? (node.body.extra.parenStart as number)
+ : node.body.start!
+ s.appendRight(
+ start + (isBlockStatement ? 1 : 0),
+ `${result};${!isBlockStatement ? 'return ' : ''}`,
+ )
+ if (!isBlockStatement) {
+ s.appendLeft(start, '{')
+ s.appendRight(node.end!, '}')
+ }
+}
diff --git a/packages/jsx-macros/src/core/style.ts b/packages/jsx-macros/src/core/style.ts
new file mode 100644
index 000000000..e431b8636
--- /dev/null
+++ b/packages/jsx-macros/src/core/style.ts
@@ -0,0 +1,19 @@
+import { compileStyleAsync } from 'vue/compiler-sfc'
+import type { OptionsResolved } from '..'
+
+export async function transformStyle(
+ code: string,
+ id: string,
+ options: OptionsResolved,
+): Promise {
+ const query = new URLSearchParams(id.split('?')[1])
+ const result = await compileStyleAsync({
+ filename: id,
+ id: `data-v-${query.get('scopeId')}`,
+ isProd: options.isProduction,
+ source: code,
+ scoped: query.get('scoped') === 'true',
+ })
+
+ return result.code
+}
diff --git a/packages/jsx-macros/src/esbuild.ts b/packages/jsx-macros/src/esbuild.ts
new file mode 100644
index 000000000..e8c6460dd
--- /dev/null
+++ b/packages/jsx-macros/src/esbuild.ts
@@ -0,0 +1,3 @@
+import unplugin from '.'
+
+export default unplugin.esbuild as typeof unplugin.esbuild
diff --git a/packages/jsx-macros/src/index.ts b/packages/jsx-macros/src/index.ts
new file mode 100644
index 000000000..bee1f04dc
--- /dev/null
+++ b/packages/jsx-macros/src/index.ts
@@ -0,0 +1,7 @@
+import { createUnplugin, type UnpluginInstance } from 'unplugin'
+import { plugin, type Options, type OptionsResolved } from './core/plugin'
+
+export type { Options, OptionsResolved }
+
+const unplugin: UnpluginInstance = createUnplugin(plugin)
+export default unplugin
diff --git a/packages/jsx-macros/src/raw.ts b/packages/jsx-macros/src/raw.ts
new file mode 100644
index 000000000..f0df9423f
--- /dev/null
+++ b/packages/jsx-macros/src/raw.ts
@@ -0,0 +1,3 @@
+import { plugin } from './core/plugin'
+
+export default plugin
diff --git a/packages/jsx-macros/src/rolldown.ts b/packages/jsx-macros/src/rolldown.ts
new file mode 100644
index 000000000..082a55c18
--- /dev/null
+++ b/packages/jsx-macros/src/rolldown.ts
@@ -0,0 +1,3 @@
+import unplugin from '.'
+
+export default unplugin.rolldown as typeof unplugin.rolldown
diff --git a/packages/jsx-macros/src/rollup.ts b/packages/jsx-macros/src/rollup.ts
new file mode 100644
index 000000000..45545feb1
--- /dev/null
+++ b/packages/jsx-macros/src/rollup.ts
@@ -0,0 +1,3 @@
+import unplugin from '.'
+
+export default unplugin.rollup as typeof unplugin.rollup
diff --git a/packages/jsx-macros/src/rspack.ts b/packages/jsx-macros/src/rspack.ts
new file mode 100644
index 000000000..6df8a0299
--- /dev/null
+++ b/packages/jsx-macros/src/rspack.ts
@@ -0,0 +1,3 @@
+import unplugin from '.'
+
+export default unplugin.rspack as typeof unplugin.rspack
diff --git a/packages/jsx-macros/src/vite.ts b/packages/jsx-macros/src/vite.ts
new file mode 100644
index 000000000..a7c5db2c1
--- /dev/null
+++ b/packages/jsx-macros/src/vite.ts
@@ -0,0 +1,3 @@
+import unplugin from '.'
+
+export default unplugin.vite as typeof unplugin.vite
diff --git a/packages/jsx-macros/src/webpack.ts b/packages/jsx-macros/src/webpack.ts
new file mode 100644
index 000000000..74c1c9020
--- /dev/null
+++ b/packages/jsx-macros/src/webpack.ts
@@ -0,0 +1,3 @@
+import unplugin from '.'
+
+export default unplugin.webpack as typeof unplugin.webpack
diff --git a/packages/jsx-macros/tests/__snapshots__/fixtures.test.ts.snap b/packages/jsx-macros/tests/__snapshots__/fixtures.test.ts.snap
new file mode 100644
index 000000000..a1edd9d61
--- /dev/null
+++ b/packages/jsx-macros/tests/__snapshots__/fixtures.test.ts.snap
@@ -0,0 +1,330 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`fixtures > ./fixtures/define-component.tsx 1`] = `
+"
+import { createPropsDefaultProxy as __MACROS_createPropsDefaultProxy } from "/vue-macros/jsx-macros/with-defaults";
+import { useAttrs as __MACROS_useAttrs } from "vue";
+import { useModel as __MACROS_useModel } from "/vue-macros/jsx-macros/use-model";
+import { withAsyncContext as __MACROS_withAsyncContext } from "vue";import { defineComponent, nextTick } from 'vue'
+
+const Comp = defineComponent(
+ (__MACROS_props) => {
+const __MACROS_default_props = __MACROS_createPropsDefaultProxy(__MACROS_props, {'.bar': 'bar'!});const attrs = __MACROS_useAttrs();
+ const foo = $(
+ __MACROS_useModel(__MACROS_props, 'foo', {
+ validator: (value) => {
+ return value === 'foo'
+ },
+ type: String,
+ })!,
+ )
+ return () => {[foo, __MACROS_default_props.bar, attrs.baz]}
+ },
+ {props: { 'bar': { required: true }, 'foo': { required: true, validator: (value) => {
+ return value === 'foo'
+ }, type: String }, 'onUpdate:foo': null, 'fooModifiers': null },inheritAttrs: false, name: 'Comp' },
+)
+
+const Comp1 = defineComponent((props: { bar: 'bar'; 'onUpdate:bar': any }) => {
+ const foo = __MACROS_useModel(props, 'foo')
+ return () => {[foo.value, props['bar'], props['onUpdate:bar']]}
+}, { props: { 'bar': null, 'onUpdate:bar': null, 'foo': null, 'onUpdate:foo': null, 'fooModifiers': null } })
+
+const Comp2 = defineComponent(async (__MACROS_props) => {
+let __temp, __restore
+
+ ;(
+ ([__temp,__restore] = __MACROS_withAsyncContext(() => nextTick())),
+ await __temp,
+ __restore()
+)
+ let foo = (
+ ([__temp,__restore] = __MACROS_withAsyncContext(() => new Promise((resolve) => {
+ setTimeout(() => resolve('foo'), 1000)
+ }))),
+ __temp = await __temp,
+ __restore(),
+ __temp
+)
+ return () => {foo}
+})
+"
+`;
+
+exports[`fixtures > ./fixtures/define-expose.tsx 1`] = `
+"
+import { useExpose as __MACROS_useExpose } from "/vue-macros/jsx-macros/use-expose";
+import { getCurrentInstance as __MACROS_getCurrentInstance } from "vue";export function Comp(__MACROS_props) {
+ __MACROS_useExpose(__MACROS_getCurrentInstance(), {
+ foo: 1,
+ })
+ return
+}
+
+export const Comp1 = function (props: any) {
+ __MACROS_useExpose(__MACROS_getCurrentInstance(), {
+ foo: props.foo,
+ })
+ return
+}
+
+export const Comp2 = ({ foo, ...__MACROS_props }: any) => {
+ __MACROS_useExpose(__MACROS_getCurrentInstance(), {
+ foo,
+ })
+ return
+}
+"
+`;
+
+exports[`fixtures > ./fixtures/define-model.tsx 1`] = `
+"
+import { useModel as __MACROS_useModel } from "/vue-macros/jsx-macros/use-model";export const Comp = ({ bar, ...__MACROS_props }: { bar: string }) => {
+ const foo = __MACROS_useModel(__MACROS_props, 'foo', { default: bar })
+ return {foo.value}
+}
+
+export default function (__MACROS_props) {
+ const modelValue = $(__MACROS_useModel(__MACROS_props, 'modelValue',)!)
+ return (
+
+ {modelValue}
+
+ )
+}
+"
+`;
+
+exports[`fixtures > ./fixtures/define-slots.tsx 1`] = `
+"
+import { useSlots as __MACROS_useSlots } from "vue";export const Comp = (__MACROS_props) => {
+ const slots = Object.assign<{
+ default: () => any
+ }>({}, __MACROS_useSlots())
+ return {slots.default?.()}
+}
+
+export default function (__MACROS_props) {
+ const slots = Object.assign({
+ default: () => default
,
+ },__MACROS_useSlots())
+ return {slots.default?.()}
+}
+"
+`;
+
+exports[`fixtures > ./fixtures/define-style.tsx 1`] = `
+"import "/vue-macros/jsx-macros/define-style/0?scopeId=4e9d5cd0&scoped=true&lang.css";import style1 from "/vue-macros/jsx-macros/define-style/1?scopeId=426a859d&scoped=true&lang.module.scss";import "/vue-macros/jsx-macros/define-style/2?scopeId=63a7910c&scoped=false&lang.scss";import { defineComponent, ref } from 'vue'
+
+export const Comp = (__MACROS_props) => {
+ const color = ref('red')
+
+ return foo
+}
+
+export default defineComponent((__MACROS_props) => {
+ const color = ref('red')
+ const styles = style1
+ return () => (
+ <>
+ foo
+
+ bar
+
+ >
+ )
+})
+
+
+"
+`;
+
+exports[`react fixtures > ./fixtures/define-expose.tsx 1`] = `
+"
+import { useImperativeHandle as __MACROS_useImperativeHandle } from "react";
+import { forwardRef as __MACROS_forwardRef } from "react";export const Comp = __MACROS_forwardRef(function Comp(__MACROS_props, __MACROS_ref) {
+ __MACROS_useImperativeHandle(__MACROS_ref, () =>({
+ foo: 1,
+ }), [])
+ return
+})
+
+export const Comp1 = __MACROS_forwardRef(function (props: any, __MACROS_ref) {
+ __MACROS_useImperativeHandle(__MACROS_ref, () =>({
+ foo: props.foo,
+ }), [props.foo])
+ return
+})
+
+export const Comp2 = __MACROS_forwardRef(({ foo, ...__MACROS_props }: any, __MACROS_ref) => {
+ __MACROS_useImperativeHandle(__MACROS_ref, () =>({
+ foo,
+ }), [foo])
+ return
+})
+"
+`;
+
+exports[`react19 fixtures > ./fixtures/define-expose.tsx 1`] = `
+"
+import { useImperativeHandle as __MACROS_useImperativeHandle } from "react";export function Comp(__MACROS_props) {
+ __MACROS_useImperativeHandle(__MACROS_props.ref, () =>({
+ foo: 1,
+ }), [])
+ return
+}
+
+export const Comp1 = function (props: any) {
+ __MACROS_useImperativeHandle(props.ref, () =>({
+ foo: props.foo,
+ }), [props.foo])
+ return
+}
+
+export const Comp2 = ({ foo, ...__MACROS_props }: any) => {
+ __MACROS_useImperativeHandle(, () =>({
+ foo,
+ }), [foo])
+ return
+}
+"
+`;
+
+exports[`vue/vapor fixtures > ./fixtures/define-component.tsx 1`] = `
+"
+import { createPropsDefaultProxy as __MACROS_createPropsDefaultProxy } from "/vue-macros/jsx-macros/with-defaults";
+import { useAttrs as __MACROS_useAttrs } from "vue";
+import { useModel as __MACROS_useModel } from "/vue-macros/jsx-macros/use-model";
+import { withAsyncContext as __MACROS_withAsyncContext } from "vue";import { defineComponent, nextTick } from 'vue'
+
+const Comp = defineComponent(
+ (__MACROS_props) => {
+const __MACROS_default_props = __MACROS_createPropsDefaultProxy(__MACROS_props, {'.bar': 'bar'!});const attrs = __MACROS_useAttrs();
+ const foo = $(
+ __MACROS_useModel(__MACROS_props, 'foo', {
+ validator: (value) => {
+ return value === 'foo'
+ },
+ type: String,
+ })!,
+ )
+ return {[foo, __MACROS_default_props.bar, attrs.baz]}
+ },
+ {props: { 'bar': { required: true }, 'foo': { required: true, validator: (value) => {
+ return value === 'foo'
+ }, type: String }, 'onUpdate:foo': null, 'fooModifiers': null },inheritAttrs: false, name: 'Comp' },
+)
+
+const Comp1 = defineComponent((props: { bar: 'bar'; 'onUpdate:bar': any }) => {
+ const foo = __MACROS_useModel(props, 'foo')
+ return {[foo.value, props['bar'], props['onUpdate:bar']]}
+}, { props: { 'bar': null, 'onUpdate:bar': null, 'foo': null, 'onUpdate:foo': null, 'fooModifiers': null } })
+
+const Comp2 = defineComponent(async (__MACROS_props) => {
+let __temp, __restore
+
+ ;(
+ ([__temp,__restore] = __MACROS_withAsyncContext(() => nextTick())),
+ await __temp,
+ __restore()
+)
+ let foo = (
+ ([__temp,__restore] = __MACROS_withAsyncContext(() => new Promise((resolve) => {
+ setTimeout(() => resolve('foo'), 1000)
+ }))),
+ __temp = await __temp,
+ __restore(),
+ __temp
+)
+ return () => {foo}
+})
+"
+`;
+
+exports[`vue/vapor fixtures > ./fixtures/define-expose.tsx 1`] = `
+"
+import { useExpose as __MACROS_useExpose } from "/vue-macros/jsx-macros/use-expose";
+import { currentInstance as __MACROS_currentInstance } from "vue";export function Comp(__MACROS_props) {
+ __MACROS_useExpose(__MACROS_currentInstance, {
+ foo: 1,
+ })
+ return
+}
+
+export const Comp1 = function (props: any) {
+ __MACROS_useExpose(__MACROS_currentInstance, {
+ foo: props.foo,
+ })
+ return
+}
+
+export const Comp2 = ({ foo, ...__MACROS_props }: any) => {
+ __MACROS_useExpose(__MACROS_currentInstance, {
+ foo,
+ })
+ return
+}
+"
+`;
+
+exports[`vue/vapor fixtures > ./fixtures/define-model.tsx 1`] = `
+"
+import { useModel as __MACROS_useModel } from "/vue-macros/jsx-macros/use-model";export const Comp = ({ bar, ...__MACROS_props }: { bar: string }) => {
+ const foo = __MACROS_useModel(__MACROS_props, 'foo', { default: bar })
+ return {foo.value}
+}
+
+export default function (__MACROS_props) {
+ const modelValue = $(__MACROS_useModel(__MACROS_props, 'modelValue',)!)
+ return (
+
+ {modelValue}
+
+ )
+}
+"
+`;
+
+exports[`vue/vapor fixtures > ./fixtures/define-slots.tsx 1`] = `
+"
+import { useSlots as __MACROS_useSlots } from "vue";export const Comp = (__MACROS_props) => {
+ const slots = Object.assign<{
+ default: () => any
+ }>({}, __MACROS_useSlots())
+ return {slots.default?.()}
+}
+
+export default function (__MACROS_props) {
+ const slots = Object.assign({
+ default: () => default
,
+ },__MACROS_useSlots())
+ return {slots.default?.()}
+}
+"
+`;
+
+exports[`vue/vapor fixtures > ./fixtures/define-style.tsx 1`] = `
+"import "/vue-macros/jsx-macros/define-style/0?scopeId=4e9d5cd0&scoped=true&lang.css";import style1 from "/vue-macros/jsx-macros/define-style/1?scopeId=426a859d&scoped=true&lang.module.scss";import "/vue-macros/jsx-macros/define-style/2?scopeId=63a7910c&scoped=false&lang.scss";import { defineComponent, ref } from 'vue'
+
+export const Comp = (__MACROS_props) => {
+ const color = ref('red')
+
+ return foo
+}
+
+export default defineComponent((__MACROS_props) => {
+ const color = ref('red')
+ const styles = style1
+ return () => (
+ <>
+ foo
+
+ bar
+
+ >
+ )
+})
+
+
+"
+`;
diff --git a/packages/jsx-macros/tests/__snapshots__/restructure.test.ts.snap b/packages/jsx-macros/tests/__snapshots__/restructure.test.ts.snap
new file mode 100644
index 000000000..b0cf2b2f5
--- /dev/null
+++ b/packages/jsx-macros/tests/__snapshots__/restructure.test.ts.snap
@@ -0,0 +1,37 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`transform > reconstruct 1`] = `
+"const App = (__MACROS_props, __MACROS_props1) => {
+ function onClick(__MACROS_props){
+ return { foo: __MACROS_props.foo, baz: __MACROS_props1.baz.baz }
+ };
+ return [ __MACROS_props[0][0][1], __MACROS_props[1].id.foo[0], __MACROS_props1.baz ]
+ }"
+`;
+
+exports[`transform > reconstruct arrowFunctionExpression 1`] = `
+"const App = (__MACROS_props) => (
+ <>{[__MACROS_props[0].root.foo]}>
+ )"
+`;
+
+exports[`transform > reconstruct default-prop 1`] = `
+"
+import { createPropsDefaultProxy as __MACROS_createPropsDefaultProxy } from "/vue-macros/jsx-macros/with-defaults";
+import { createPropsRestProxy as __MACROS_createPropsRestProxy } from "vue";function App(__MACROS_props, __MACROS_props1){
+const __MACROS_default_props = __MACROS_createPropsDefaultProxy(__MACROS_props, {'.foo': 'bar'});
+const __MACROS_default_props1 = __MACROS_createPropsDefaultProxy(__MACROS_props1, {'[0]': 'foo'});
+const rest = __MACROS_createPropsRestProxy(__MACROS_default_props, ['foo', 'baz']);
+ return <>{[__MACROS_default_props.foo, __MACROS_default_props.baz, rest, __MACROS_default_props1[0]]}>
+ }"
+`;
+
+exports[`transform > reconstruct rest-prop 1`] = `
+"
+import { createPropsDefaultProxy as __MACROS_createPropsDefaultProxy } from "/vue-macros/jsx-macros/with-defaults";
+import { createPropsRestProxy as __MACROS_createPropsRestProxy } from "vue";function App(__MACROS_props){
+const __MACROS_default_props = __MACROS_createPropsDefaultProxy(__MACROS_props, {'.bar': 1});
+const rest = __MACROS_createPropsRestProxy(__MACROS_default_props, ['foo', 'bar']);
+ return <>{[__MACROS_default_props.foo, __MACROS_default_props.bar, rest]}>
+ }"
+`;
diff --git a/packages/jsx-macros/tests/fixtures.test.ts b/packages/jsx-macros/tests/fixtures.test.ts
new file mode 100644
index 000000000..3be1f155a
--- /dev/null
+++ b/packages/jsx-macros/tests/fixtures.test.ts
@@ -0,0 +1,75 @@
+import { testFixtures } from '@vue-macros/test-utils'
+import { describe } from 'vitest'
+import { transformJsxMacros } from '../src/core'
+
+const options = {
+ defineModel: { alias: ['defineModel'] },
+ defineSlots: { alias: ['defineSlots'] },
+ defineStyle: { alias: ['defineStyle'] },
+ defineExpose: { alias: ['defineExpose'] },
+ defineComponent: { alias: ['defineComponent'] },
+}
+
+describe('fixtures', async () => {
+ await testFixtures(
+ import.meta.glob('./fixtures/**/*.tsx', {
+ eager: true,
+ as: 'raw',
+ }),
+ (args, id, code) =>
+ transformJsxMacros(code, id, new Map(), {
+ lib: 'vue',
+ include: ['*.tsx'],
+ version: 3.5,
+ ...options,
+ })?.code,
+ )
+})
+
+describe('vue/vapor fixtures', async () => {
+ await testFixtures(
+ import.meta.glob('./fixtures/**/*.tsx', {
+ eager: true,
+ as: 'raw',
+ }),
+ (args, id, code) =>
+ transformJsxMacros(code, id, new Map(), {
+ lib: 'vue/vapor',
+ include: ['*.tsx'],
+ version: 3.5,
+ ...options,
+ })?.code,
+ )
+})
+
+describe('react fixtures', async () => {
+ await testFixtures(
+ import.meta.glob('./fixtures/**/define-expose.tsx', {
+ eager: true,
+ as: 'raw',
+ }),
+ (args, id, code) =>
+ transformJsxMacros(code, id, new Map(), {
+ lib: 'react',
+ include: ['*.tsx'],
+ version: 18,
+ ...options,
+ })?.code,
+ )
+})
+
+describe('react19 fixtures', async () => {
+ await testFixtures(
+ import.meta.glob('./fixtures/**/define-expose.tsx', {
+ eager: true,
+ as: 'raw',
+ }),
+ (args, id, code) =>
+ transformJsxMacros(code, id, new Map(), {
+ lib: 'react',
+ include: ['*.tsx'],
+ version: 19,
+ ...options,
+ })?.code,
+ )
+})
diff --git a/packages/jsx-macros/tests/fixtures/define-component.tsx b/packages/jsx-macros/tests/fixtures/define-component.tsx
new file mode 100644
index 000000000..f80899a39
--- /dev/null
+++ b/packages/jsx-macros/tests/fixtures/define-component.tsx
@@ -0,0 +1,29 @@
+import { defineComponent, nextTick } from 'vue'
+
+const Comp = defineComponent(
+ ({ bar = 'bar'!, ...attrs }: { bar: 'bar'; baz: 'baz' }) => {
+ const foo = $(
+ defineModel('foo', {
+ validator: (value) => {
+ return value === 'foo'
+ },
+ type: String,
+ })!,
+ )
+ return {[foo, bar, attrs.baz]}
+ },
+ { name: 'Comp' },
+)
+
+const Comp1 = defineComponent((props: { bar: 'bar'; 'onUpdate:bar': any }) => {
+ const foo = defineModel('foo')
+ return {[foo.value, props['bar'], props['onUpdate:bar']]}
+})
+
+const Comp2 = defineComponent(async () => {
+ await nextTick()
+ let foo = await new Promise((resolve) => {
+ setTimeout(() => resolve('foo'), 1000)
+ })
+ return () => {foo}
+})
diff --git a/packages/jsx-macros/tests/fixtures/define-expose.tsx b/packages/jsx-macros/tests/fixtures/define-expose.tsx
new file mode 100644
index 000000000..ad6a8d2b4
--- /dev/null
+++ b/packages/jsx-macros/tests/fixtures/define-expose.tsx
@@ -0,0 +1,20 @@
+export function Comp() {
+ defineExpose({
+ foo: 1,
+ })
+ return
+}
+
+export const Comp1 = function (props: any) {
+ defineExpose({
+ foo: props.foo,
+ })
+ return
+}
+
+export const Comp2 = ({ foo }: any) => {
+ defineExpose({
+ foo,
+ })
+ return
+}
diff --git a/packages/jsx-macros/tests/fixtures/define-model.tsx b/packages/jsx-macros/tests/fixtures/define-model.tsx
new file mode 100644
index 000000000..1767dccf8
--- /dev/null
+++ b/packages/jsx-macros/tests/fixtures/define-model.tsx
@@ -0,0 +1,13 @@
+export const Comp = ({ bar }: { bar: string }) => {
+ const foo = defineModel('foo', { default: bar })
+ return {foo.value}
+}
+
+export default function () {
+ const modelValue = $(defineModel()!)
+ return (
+
+ {modelValue}
+
+ )
+}
diff --git a/packages/jsx-macros/tests/fixtures/define-slots.tsx b/packages/jsx-macros/tests/fixtures/define-slots.tsx
new file mode 100644
index 000000000..0d3fcb541
--- /dev/null
+++ b/packages/jsx-macros/tests/fixtures/define-slots.tsx
@@ -0,0 +1,13 @@
+export const Comp = () => {
+ const slots = defineSlots<{
+ default: () => any
+ }>()
+ return {slots.default?.()}
+}
+
+export default function () {
+ const slots = defineSlots({
+ default: () => default
,
+ })
+ return {slots.default?.()}
+}
diff --git a/packages/jsx-macros/tests/fixtures/define-style.tsx b/packages/jsx-macros/tests/fixtures/define-style.tsx
new file mode 100644
index 000000000..a585a0756
--- /dev/null
+++ b/packages/jsx-macros/tests/fixtures/define-style.tsx
@@ -0,0 +1,37 @@
+import { defineComponent, ref } from 'vue'
+
+export const Comp = () => {
+ const color = ref('red')
+ defineStyle(`
+ .foo {
+ color: ${color.value};
+ }
+ `)
+ return foo
+}
+
+export default defineComponent(() => {
+ const color = ref('red')
+ const styles = defineStyle.scss(`
+ .bar {
+ color: ${color.value};
+ .barBaz {
+ background: red;
+ }
+ }
+ `)
+ return () => (
+ <>
+ foo
+
+ bar
+
+ >
+ )
+})
+
+defineStyle.scss(`
+ .bar {
+ color: red;
+ }
+`)
diff --git a/packages/jsx-macros/tests/restructure.test.ts b/packages/jsx-macros/tests/restructure.test.ts
new file mode 100644
index 000000000..9fc11879c
--- /dev/null
+++ b/packages/jsx-macros/tests/restructure.test.ts
@@ -0,0 +1,61 @@
+import { babelParse, MagicStringAST, walkAST } from '@vue-macros/common'
+import { describe, expect, test } from 'vitest'
+import { restructure } from '../src/api'
+
+const transformRestructure = (code: string): string => {
+ const s = new MagicStringAST(code)
+ const ast = babelParse(code, 'tsx')
+ walkAST(ast, {
+ enter(node) {
+ if (
+ node.type === 'FunctionExpression' ||
+ node.type === 'ArrowFunctionExpression' ||
+ node.type === 'FunctionDeclaration'
+ ) {
+ restructure(s, node)
+ }
+ },
+ })
+ return s.toString()
+}
+
+describe('transform', () => {
+ test('reconstruct', () => {
+ const code = transformRestructure(
+ `const App = ([[[,foo]], {id: {foo: [bar]}}], { baz }) => {
+ function onClick({ foo }){
+ return { foo, baz: baz.baz }
+ };
+ return [ foo, bar, baz ]
+ }`,
+ )!
+ expect(code).toMatchSnapshot()
+ })
+
+ test('reconstruct arrowFunctionExpression', () => {
+ const code = transformRestructure(
+ `const App = ([{root: {foo}}]) => (
+ <>{[foo]}>
+ )`,
+ )!
+ expect(code).toMatchSnapshot()
+ })
+
+ test('reconstruct default-prop', () => {
+ const code = transformRestructure(
+ `function App({foo: bar = 'bar', baz: qux, ...rest}, [foo = 'foo']){
+ return <>{[bar, qux, rest, foo]}>
+ }`,
+ )!
+ expect(code).toMatchSnapshot()
+ })
+
+ test('reconstruct rest-prop', () => {
+ const code = transformRestructure(
+ `function App({foo, bar = 1, ...rest}){
+ return <>{[foo, bar, rest]}>
+ }`,
+ )!
+ expect(code).toMatchSnapshot()
+ })
+})
diff --git a/packages/jsx-macros/tsup.config.ts b/packages/jsx-macros/tsup.config.ts
new file mode 100644
index 000000000..e5efb9683
--- /dev/null
+++ b/packages/jsx-macros/tsup.config.ts
@@ -0,0 +1,3 @@
+import { config } from '../../tsup.config.js'
+
+export default config()
diff --git a/packages/jsx/README.md b/packages/jsx/README.md
new file mode 100644
index 000000000..fbad5293a
--- /dev/null
+++ b/packages/jsx/README.md
@@ -0,0 +1,3 @@
+# @vue-macros/jsx [](https://npmjs.com/package/@vue-macros/jsx)
+
+Please refer to [README.md](https://github.com/vue-macros/vue-macros#readme)
diff --git a/packages/jsx/package.json b/packages/jsx/package.json
new file mode 100644
index 000000000..172a7b988
--- /dev/null
+++ b/packages/jsx/package.json
@@ -0,0 +1,137 @@
+{
+ "name": "@vue-macros/jsx",
+ "version": "3.0.0-beta.4",
+ "packageManager": "pnpm@10.4.1",
+ "description": "jsx feature from Vue Macros.",
+ "type": "module",
+ "keywords": [
+ "vue-macros",
+ "macros",
+ "vue",
+ "sfc",
+ "setup",
+ "script-setup",
+ "jsx"
+ ],
+ "license": "MIT",
+ "homepage": "https://vue-macros.dev",
+ "bugs": {
+ "url": "https://github.com/vue-macros/vue-macros/issues"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/vue-macros/vue-macros.git",
+ "directory": "packages/jsx"
+ },
+ "author": "zhiyuanzmj",
+ "contributors": [
+ "三咲智子 "
+ ],
+ "funding": "https://github.com/sponsors/vue-macros",
+ "files": [
+ "dist"
+ ],
+ "main": "dist/index.js",
+ "module": "dist/index.js",
+ "types": "dist/index.d.ts",
+ "exports": {
+ ".": {
+ "dev": "./src/index.ts",
+ "default": "./dist/index.js"
+ },
+ "./esbuild": {
+ "dev": "./src/esbuild.ts",
+ "default": "./dist/esbuild.js"
+ },
+ "./jsx-runtime": {
+ "dev": "./src/jsx-runtime.ts",
+ "default": "./dist/jsx-runtime.js"
+ },
+ "./options": {
+ "dev": "./src/options.ts",
+ "default": "./dist/options.js"
+ },
+ "./raw": {
+ "dev": "./src/raw.ts",
+ "default": "./dist/raw.js"
+ },
+ "./rolldown": {
+ "dev": "./src/rolldown.ts",
+ "default": "./dist/rolldown.js"
+ },
+ "./rollup": {
+ "dev": "./src/rollup.ts",
+ "default": "./dist/rollup.js"
+ },
+ "./rspack": {
+ "dev": "./src/rspack.ts",
+ "default": "./dist/rspack.js"
+ },
+ "./runtime": {
+ "dev": "./src/runtime.ts",
+ "default": "./dist/runtime.js"
+ },
+ "./vite": {
+ "dev": "./src/vite.ts",
+ "default": "./dist/vite.js"
+ },
+ "./volar": {
+ "dev": "./src/volar.ts",
+ "default": "./dist/volar.js"
+ },
+ "./webpack": {
+ "dev": "./src/webpack.ts",
+ "default": "./dist/webpack.js"
+ },
+ "./*": "./*"
+ },
+ "typesVersions": {
+ "*": {
+ "*": [
+ "./dist/*",
+ "./*"
+ ]
+ }
+ },
+ "publishConfig": {
+ "access": "public",
+ "exports": {
+ ".": "./dist/index.js",
+ "./esbuild": "./dist/esbuild.js",
+ "./jsx-runtime": "./dist/jsx-runtime.js",
+ "./options": "./dist/options.js",
+ "./raw": "./dist/raw.js",
+ "./rolldown": "./dist/rolldown.js",
+ "./rollup": "./dist/rollup.js",
+ "./rspack": "./dist/rspack.js",
+ "./runtime": "./dist/runtime.js",
+ "./vite": "./dist/vite.js",
+ "./volar": "./dist/volar.js",
+ "./webpack": "./dist/webpack.js",
+ "./*": "./*"
+ },
+ "tag": "next"
+ },
+ "scripts": {
+ "build": "tsup",
+ "dev": "DEV=true tsup"
+ },
+ "peerDependencies": {
+ "vue": "^2.7.0 || ^3.2.25"
+ },
+ "dependencies": {
+ "@vue-macros/common": "workspace:*",
+ "@vue-macros/jsx-directive": "workspace:*",
+ "@vue-macros/jsx-macros": "workspace:*",
+ "@vue-macros/volar": "workspace:*",
+ "ts-macro": "catalog:",
+ "unplugin-combine": "catalog:"
+ },
+ "devDependencies": {
+ "csstype": "catalog:",
+ "vue": "catalog:"
+ },
+ "engines": {
+ "node": ">=20.18.0"
+ }
+}
diff --git a/packages/jsx/src/esbuild.ts b/packages/jsx/src/esbuild.ts
new file mode 100644
index 000000000..e8c6460dd
--- /dev/null
+++ b/packages/jsx/src/esbuild.ts
@@ -0,0 +1,3 @@
+import unplugin from '.'
+
+export default unplugin.esbuild as typeof unplugin.esbuild
diff --git a/packages/jsx/src/index.ts b/packages/jsx/src/index.ts
new file mode 100644
index 000000000..ea963bfef
--- /dev/null
+++ b/packages/jsx/src/index.ts
@@ -0,0 +1,30 @@
+import jsxDirective from '@vue-macros/jsx-directive'
+import jsxMacros from '@vue-macros/jsx-macros'
+
+import { generatePluginName } from '#macros' with { type: 'macro' }
+import {
+ createCombinePlugin,
+ type UnpluginCombineInstance,
+} from 'unplugin-combine'
+import { resolveJSXOptions, type JSXOptions } from './options'
+
+const jsxPlugins = [
+ ['directive', jsxDirective],
+ ['macros', jsxMacros],
+] as const
+
+const name = generatePluginName()
+const plugin: UnpluginCombineInstance =
+ createCombinePlugin((userOptions = {}, meta) => {
+ const options = resolveJSXOptions(userOptions)
+ const plugins = jsxPlugins.flatMap(([name, plugin]) =>
+ options[name] ? plugin[meta.framework!](options[name]) : [],
+ )
+
+ return {
+ name,
+ plugins,
+ }
+ })
+
+export default plugin
diff --git a/packages/jsx/src/jsx-runtime.ts b/packages/jsx/src/jsx-runtime.ts
new file mode 100644
index 000000000..51a01c3a9
--- /dev/null
+++ b/packages/jsx/src/jsx-runtime.ts
@@ -0,0 +1,39 @@
+import { Fragment, h } from 'vue'
+import type { NativeElements } from './jsx-runtime/dom'
+
+function jsx(type: any, props: any, key: any): ReturnType {
+ const { children } = props
+ delete props.children
+ if (arguments.length > 2) {
+ props.key = key
+ }
+ return h(type, props, children)
+}
+
+export { Fragment, jsx, jsx as jsxDEV, jsx as jsxs }
+
+type VNode = import('vue').VNode
+type ReservedProps = import('vue').ReservedProps
+type AllowedComponentProps = import('vue').AllowedComponentProps
+type ComponentCustomProps = import('vue').ComponentCustomProps
+
+declare global {
+ // eslint-disable-next-line @typescript-eslint/no-namespace
+ namespace JSX {
+ interface Element extends VNode {}
+ interface ElementClass {
+ $props: {}
+ }
+ interface ElementAttributesProperty {
+ $props: {}
+ }
+ interface IntrinsicElements extends NativeElements {
+ [name: string]: any
+ }
+
+ interface IntrinsicAttributes
+ extends ReservedProps,
+ AllowedComponentProps,
+ ComponentCustomProps {}
+ }
+}
diff --git a/packages/jsx/src/jsx-runtime/dom.ts b/packages/jsx/src/jsx-runtime/dom.ts
new file mode 100644
index 000000000..21fa30c82
--- /dev/null
+++ b/packages/jsx/src/jsx-runtime/dom.ts
@@ -0,0 +1,1656 @@
+/* cSpell:disable */
+// Note: this file is auto concatenated to the end of the bundled d.ts during
+// build.
+
+// This code is based on react definition in DefinitelyTyped published under the MIT license.
+// Repository: https://github.com/DefinitelyTyped/DefinitelyTyped
+// Path in the repository: types/react/index.d.ts
+//
+// Copyrights of original definition are:
+// AssureSign
+// Microsoft
+// John Reilly
+// Benoit Benezech
+// Patricio Zavolinsky
+// Digiguru
+// Eric Anderson
+// Dovydas Navickas
+// Josh Rutherford
+// Guilherme Hübner
+// Ferdy Budhidharma
+// Johann Rakotoharisoa
+// Olivier Pascal
+// Martin Hochel
+// Frank Li
+// Jessica Franco
+// Saransh Kataria
+// Kanitkorn Sujautra
+// Sebastian Silbermann
+
+import type * as CSS from 'csstype'
+
+export interface CSSProperties
+ extends CSS.Properties,
+ CSS.PropertiesHyphen {
+ /**
+ * The index signature was removed to enable closed typing for style
+ * using CSSType. You're able to use type assertion or module augmentation
+ * to add properties or an index signature of your own.
+ *
+ * For examples and more information, visit:
+ * https://github.com/frenic/csstype#what-should-i-do-when-i-get-type-errors
+ */
+ [v: `--${string}`]: string | number | undefined
+}
+
+type Booleanish = boolean | 'true' | 'false'
+type Numberish = number | string
+
+// All the WAI-ARIA 1.1 attributes from https://www.w3.org/TR/wai-aria-1.1/
+export interface AriaAttributes {
+ /** Identifies the currently active element when DOM focus is on a composite widget, textbox, group, or application. */
+ 'aria-activedescendant'?: string
+ /** Indicates whether assistive technologies will present all, or only parts of, the changed region based on the change notifications defined by the aria-relevant attribute. */
+ 'aria-atomic'?: Booleanish
+ /**
+ * Indicates whether inputting text could trigger display of one or more predictions of the user's intended value for an input and specifies how predictions would be
+ * presented if they are made.
+ */
+ 'aria-autocomplete'?: 'none' | 'inline' | 'list' | 'both'
+ /** Indicates an element is being modified and that assistive technologies MAY want to wait until the modifications are complete before exposing them to the user. */
+ 'aria-busy'?: Booleanish
+ /**
+ * Indicates the current "checked" state of checkboxes, radio buttons, and other widgets.
+ * @see aria-pressed @see aria-selected.
+ */
+ 'aria-checked'?: Booleanish | 'mixed'
+ /**
+ * Defines the total number of columns in a table, grid, or treegrid.
+ * @see aria-colindex.
+ */
+ 'aria-colcount'?: Numberish
+ /**
+ * Defines an element's column index or position with respect to the total number of columns within a table, grid, or treegrid.
+ * @see aria-colcount @see aria-colspan.
+ */
+ 'aria-colindex'?: Numberish
+ /**
+ * Defines the number of columns spanned by a cell or gridcell within a table, grid, or treegrid.
+ * @see aria-colindex @see aria-rowspan.
+ */
+ 'aria-colspan'?: Numberish
+ /**
+ * Identifies the element (or elements) whose contents or presence are controlled by the current element.
+ * @see aria-owns.
+ */
+ 'aria-controls'?: string
+ /** Indicates the element that represents the current item within a container or set of related elements. */
+ 'aria-current'?: Booleanish | 'page' | 'step' | 'location' | 'date' | 'time'
+ /**
+ * Identifies the element (or elements) that describes the object.
+ * @see aria-labelledby
+ */
+ 'aria-describedby'?: string
+ /**
+ * Identifies the element that provides a detailed, extended description for the object.
+ * @see aria-describedby.
+ */
+ 'aria-details'?: string
+ /**
+ * Indicates that the element is perceivable but disabled, so it is not editable or otherwise operable.
+ * @see aria-hidden @see aria-readonly.
+ */
+ 'aria-disabled'?: Booleanish
+ /**
+ * Indicates what functions can be performed when a dragged object is released on the drop target.
+ * @deprecated in ARIA 1.1
+ */
+ 'aria-dropeffect'?: 'none' | 'copy' | 'execute' | 'link' | 'move' | 'popup'
+ /**
+ * Identifies the element that provides an error message for the object.
+ * @see aria-invalid @see aria-describedby.
+ */
+ 'aria-errormessage'?: string
+ /** Indicates whether the element, or another grouping element it controls, is currently expanded or collapsed. */
+ 'aria-expanded'?: Booleanish
+ /**
+ * Identifies the next element (or elements) in an alternate reading order of content which, at the user's discretion,
+ * allows assistive technology to override the general default of reading in document source order.
+ */
+ 'aria-flowto'?: string
+ /**
+ * Indicates an element's "grabbed" state in a drag-and-drop operation.
+ * @deprecated in ARIA 1.1
+ */
+ 'aria-grabbed'?: Booleanish
+ /** Indicates the availability and type of interactive popup element, such as menu or dialog, that can be triggered by an element. */
+ 'aria-haspopup'?: Booleanish | 'menu' | 'listbox' | 'tree' | 'grid' | 'dialog'
+ /**
+ * Indicates whether the element is exposed to an accessibility API.
+ * @see aria-disabled.
+ */
+ 'aria-hidden'?: Booleanish
+ /**
+ * Indicates the entered value does not conform to the format expected by the application.
+ * @see aria-errormessage.
+ */
+ 'aria-invalid'?: Booleanish | 'grammar' | 'spelling'
+ /** Indicates keyboard shortcuts that an author has implemented to activate or give focus to an element. */
+ 'aria-keyshortcuts'?: string
+ /**
+ * Defines a string value that labels the current element.
+ * @see aria-labelledby.
+ */
+ 'aria-label'?: string
+ /**
+ * Identifies the element (or elements) that labels the current element.
+ * @see aria-describedby.
+ */
+ 'aria-labelledby'?: string
+ /** Defines the hierarchical level of an element within a structure. */
+ 'aria-level'?: Numberish
+ /** Indicates that an element will be updated, and describes the types of updates the user agents, assistive technologies, and user can expect from the live region. */
+ 'aria-live'?: 'off' | 'assertive' | 'polite'
+ /** Indicates whether an element is modal when displayed. */
+ 'aria-modal'?: Booleanish
+ /** Indicates whether a text box accepts multiple lines of input or only a single line. */
+ 'aria-multiline'?: Booleanish
+ /** Indicates that the user may select more than one item from the current selectable descendants. */
+ 'aria-multiselectable'?: Booleanish
+ /** Indicates whether the element's orientation is horizontal, vertical, or unknown/ambiguous. */
+ 'aria-orientation'?: 'horizontal' | 'vertical'
+ /**
+ * Identifies an element (or elements) in order to define a visual, functional, or contextual parent/child relationship
+ * between DOM elements where the DOM hierarchy cannot be used to represent the relationship.
+ * @see aria-controls.
+ */
+ 'aria-owns'?: string
+ /**
+ * Defines a short hint (a word or short phrase) intended to aid the user with data entry when the control has no value.
+ * A hint could be a sample value or a brief description of the expected format.
+ */
+ 'aria-placeholder'?: string
+ /**
+ * Defines an element's number or position in the current set of listitems or treeitems. Not required if all elements in the set are present in the DOM.
+ * @see aria-setsize.
+ */
+ 'aria-posinset'?: Numberish
+ /**
+ * Indicates the current "pressed" state of toggle buttons.
+ * @see aria-checked @see aria-selected.
+ */
+ 'aria-pressed'?: Booleanish | 'mixed'
+ /**
+ * Indicates that the element is not editable, but is otherwise operable.
+ * @see aria-disabled.
+ */
+ 'aria-readonly'?: Booleanish
+ /**
+ * Indicates what notifications the user agent will trigger when the accessibility tree within a live region is modified.
+ * @see aria-atomic.
+ */
+ 'aria-relevant'?:
+ | 'additions'
+ | 'additions removals'
+ | 'additions text'
+ | 'all'
+ | 'removals'
+ | 'removals additions'
+ | 'removals text'
+ | 'text'
+ | 'text additions'
+ | 'text removals'
+ /** Indicates that user input is required on the element before a form may be submitted. */
+ 'aria-required'?: Booleanish
+ /** Defines a human-readable, author-localized description for the role of an element. */
+ 'aria-roledescription'?: string
+ /**
+ * Defines the total number of rows in a table, grid, or treegrid.
+ * @see aria-rowindex.
+ */
+ 'aria-rowcount'?: Numberish
+ /**
+ * Defines an element's row index or position with respect to the total number of rows within a table, grid, or treegrid.
+ * @see aria-rowcount @see aria-rowspan.
+ */
+ 'aria-rowindex'?: Numberish
+ /**
+ * Defines the number of rows spanned by a cell or gridcell within a table, grid, or treegrid.
+ * @see aria-rowindex @see aria-colspan.
+ */
+ 'aria-rowspan'?: Numberish
+ /**
+ * Indicates the current "selected" state of various widgets.
+ * @see aria-checked @see aria-pressed.
+ */
+ 'aria-selected'?: Booleanish
+ /**
+ * Defines the number of items in the current set of listitems or treeitems. Not required if all elements in the set are present in the DOM.
+ * @see aria-posinset.
+ */
+ 'aria-setsize'?: Numberish
+ /** Indicates if items in a table or grid are sorted in ascending or descending order. */
+ 'aria-sort'?: 'none' | 'ascending' | 'descending' | 'other'
+ /** Defines the maximum allowed value for a range widget. */
+ 'aria-valuemax'?: Numberish
+ /** Defines the minimum allowed value for a range widget. */
+ 'aria-valuemin'?: Numberish
+ /**
+ * Defines the current value for a range widget.
+ * @see aria-valuetext.
+ */
+ 'aria-valuenow'?: Numberish
+ /** Defines the human readable text alternative of aria-valuenow for a range widget. */
+ 'aria-valuetext'?: string
+}
+
+/**
+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/crossorigin MDN}
+ */
+type CrossOrigin = 'anonymous' | 'use-credentials' | ''
+
+// Vue's style normalization supports nested arrays
+export type StyleValue =
+ | false
+ | null
+ | undefined
+ | string
+ | CSSProperties
+ | Array
+
+export interface HTMLAttributes
+ extends AriaAttributes,
+ EventHandlers> {
+ innerHTML?: string
+
+ class?: any
+ style?: StyleValue
+
+ // Standard HTML Attributes
+ accesskey?: string
+ autocapitalize?:
+ | 'off'
+ | 'none'
+ | 'on'
+ | 'sentences'
+ | 'words'
+ | 'characters'
+ | undefined
+ | (string & {})
+ autofocus?: Booleanish
+ contenteditable?: Booleanish | 'inherit' | 'plaintext-only'
+ contextmenu?: string
+ dir?: string
+ draggable?: Booleanish
+ enterKeyHint?:
+ | 'enter'
+ | 'done'
+ | 'go'
+ | 'next'
+ | 'previous'
+ | 'search'
+ | 'send'
+ hidden?: Booleanish | '' | 'hidden' | 'until-found'
+ id?: string
+ inert?: Booleanish
+ lang?: string
+ nonce?: string
+ placeholder?: string
+ spellcheck?: Booleanish
+ tabindex?: Numberish
+ title?: string
+ translate?: 'yes' | 'no'
+
+ // Unknown
+ radiogroup?: string // ,