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 () => ( + > + + +
+) +``` + +## 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 () => ( + > + + +
+) +``` + +## 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 [![npm](https://img.shields.io/npm/v/@vue-macros/jsx-macros.svg)](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 [![npm](https://img.shields.io/npm/v/@vue-macros/jsx.svg)](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 // , + + // WAI-ARIA + role?: string + + // RDFa Attributes + about?: string + content?: string + datatype?: string + inlist?: any + prefix?: string + property?: string + rel?: string + resource?: string + rev?: string + typeof?: string + vocab?: string + + // Non-standard Attributes + autocorrect?: string + autosave?: string + color?: string + itemprop?: string + itemscope?: Booleanish + itemtype?: string + itemid?: string + itemref?: string + results?: Numberish + security?: string + unselectable?: 'on' | 'off' + + // Living Standard + /** + * Hints at the type of data that might be entered by the user while editing the element or its contents + * @see https://html.spec.whatwg.org/multipage/interaction.html#input-modalities:-the-inputmode-attribute + */ + inputmode?: + | 'none' + | 'text' + | 'tel' + | 'url' + | 'email' + | 'numeric' + | 'decimal' + | 'search' + /** + * Specify that a standard HTML element should behave like a defined custom built-in element + * @see https://html.spec.whatwg.org/multipage/custom-elements.html#attr-is + */ + is?: string +} + +type HTMLAttributeReferrerPolicy = + | '' + | 'no-referrer' + | 'no-referrer-when-downgrade' + | 'origin' + | 'origin-when-cross-origin' + | 'same-origin' + | 'strict-origin' + | 'strict-origin-when-cross-origin' + | 'unsafe-url' + +export interface AnchorHTMLAttributes extends HTMLAttributes { + download?: any + href?: string + hreflang?: string + media?: string + ping?: string + rel?: string + target?: string + type?: string + referrerpolicy?: HTMLAttributeReferrerPolicy +} + +export interface AreaHTMLAttributes extends HTMLAttributes { + alt?: string + coords?: string + download?: any + href?: string + hreflang?: string + media?: string + referrerpolicy?: HTMLAttributeReferrerPolicy + shape?: string + target?: string +} + +export interface AudioHTMLAttributes extends MediaHTMLAttributes {} + +export interface BaseHTMLAttributes extends HTMLAttributes { + href?: string + target?: string +} + +export interface BlockquoteHTMLAttributes extends HTMLAttributes { + cite?: string +} + +export interface ButtonHTMLAttributes extends HTMLAttributes { + disabled?: Booleanish + form?: string + formaction?: string + formenctype?: string + formmethod?: string + formnovalidate?: Booleanish + formtarget?: string + name?: string + type?: 'submit' | 'reset' | 'button' + value?: string | ReadonlyArray | number +} + +export interface CanvasHTMLAttributes extends HTMLAttributes { + height?: Numberish + width?: Numberish +} + +export interface ColHTMLAttributes extends HTMLAttributes { + span?: Numberish + width?: Numberish +} + +export interface ColgroupHTMLAttributes extends HTMLAttributes { + span?: Numberish +} + +export interface DataHTMLAttributes extends HTMLAttributes { + value?: string | ReadonlyArray | number +} + +export interface DetailsHTMLAttributes extends HTMLAttributes { + name?: string + open?: Booleanish + onToggle?: (payload: ToggleEvent) => void +} + +export interface DelHTMLAttributes extends HTMLAttributes { + cite?: string + datetime?: string +} + +export interface DialogHTMLAttributes extends HTMLAttributes { + open?: Booleanish + onClose?: (payload: Event) => void +} + +export interface EmbedHTMLAttributes extends HTMLAttributes { + height?: Numberish + src?: string + type?: string + width?: Numberish +} + +export interface FieldsetHTMLAttributes extends HTMLAttributes { + disabled?: Booleanish + form?: string + name?: string +} + +export interface FormHTMLAttributes extends HTMLAttributes { + acceptcharset?: string + action?: string + autocomplete?: string + enctype?: string + method?: string + name?: string + novalidate?: Booleanish + target?: string +} + +export interface HtmlHTMLAttributes extends HTMLAttributes { + manifest?: string +} + +export interface IframeHTMLAttributes extends HTMLAttributes { + allow?: string + allowfullscreen?: Booleanish + allowtransparency?: Booleanish + /** @deprecated */ + frameborder?: Numberish + height?: Numberish + loading?: 'eager' | 'lazy' + /** @deprecated */ + marginheight?: Numberish + /** @deprecated */ + marginwidth?: Numberish + name?: string + referrerpolicy?: HTMLAttributeReferrerPolicy + sandbox?: string + /** @deprecated */ + scrolling?: string + seamless?: Booleanish + src?: string + srcdoc?: string + width?: Numberish +} + +export interface ImgHTMLAttributes extends HTMLAttributes { + alt?: string + crossorigin?: CrossOrigin + decoding?: 'async' | 'auto' | 'sync' + height?: Numberish + loading?: 'eager' | 'lazy' + referrerpolicy?: HTMLAttributeReferrerPolicy + sizes?: string + src?: string + srcset?: string + usemap?: string + width?: Numberish +} + +export interface InsHTMLAttributes extends HTMLAttributes { + cite?: string + datetime?: string +} + +export type InputTypeHTMLAttribute = + | 'button' + | 'checkbox' + | 'color' + | 'date' + | 'datetime-local' + | 'email' + | 'file' + | 'hidden' + | 'image' + | 'month' + | 'number' + | 'password' + | 'radio' + | 'range' + | 'reset' + | 'search' + | 'submit' + | 'tel' + | 'text' + | 'time' + | 'url' + | 'week' + | (string & {}) + +type AutoFillAddressKind = 'billing' | 'shipping' +type AutoFillBase = '' | 'off' | 'on' +type AutoFillContactField = + | 'email' + | 'tel' + | 'tel-area-code' + | 'tel-country-code' + | 'tel-extension' + | 'tel-local' + | 'tel-local-prefix' + | 'tel-local-suffix' + | 'tel-national' +type AutoFillContactKind = 'home' | 'mobile' | 'work' +type AutoFillCredentialField = 'webauthn' +type AutoFillNormalField = + | 'additional-name' + | 'address-level1' + | 'address-level2' + | 'address-level3' + | 'address-level4' + | 'address-line1' + | 'address-line2' + | 'address-line3' + | 'bday-day' + | 'bday-month' + | 'bday-year' + | 'cc-csc' + | 'cc-exp' + | 'cc-exp-month' + | 'cc-exp-year' + | 'cc-family-name' + | 'cc-given-name' + | 'cc-name' + | 'cc-number' + | 'cc-type' + | 'country' + | 'country-name' + | 'current-password' + | 'family-name' + | 'given-name' + | 'honorific-prefix' + | 'honorific-suffix' + | 'name' + | 'new-password' + | 'one-time-code' + | 'organization' + | 'postal-code' + | 'street-address' + | 'transaction-amount' + | 'transaction-currency' + | 'username' +type OptionalPrefixToken = `${T} ` | '' +type OptionalPostfixToken = ` ${T}` | '' +type AutoFillField = + | AutoFillNormalField + | `${OptionalPrefixToken}${AutoFillContactField}` +type AutoFillSection = `section-${string}` +type AutoFill = + | AutoFillBase + | `${OptionalPrefixToken}${OptionalPrefixToken}${AutoFillField}${OptionalPostfixToken}` +type HTMLInputAutoCompleteAttribute = AutoFill | (string & {}) + +export interface InputHTMLAttributes extends HTMLAttributes { + accept?: string + alt?: string + autocomplete?: HTMLInputAutoCompleteAttribute + capture?: boolean | 'user' | 'environment' // https://www.w3.org/tr/html-media-capture/#the-capture-attribute + checked?: Booleanish | any[] | Set // for IDE v-model multi-checkbox support + disabled?: Booleanish + form?: string + formaction?: string + formenctype?: string + formmethod?: string + formnovalidate?: Booleanish + formtarget?: string + height?: Numberish + indeterminate?: boolean + list?: string + max?: Numberish + maxlength?: Numberish + min?: Numberish + minlength?: Numberish + multiple?: Booleanish + name?: string + pattern?: string + placeholder?: string + readonly?: Booleanish + required?: Booleanish + size?: Numberish + src?: string + step?: Numberish + type?: InputTypeHTMLAttribute + value?: any // we support :value to be bound to anything w/ v-model + width?: Numberish +} + +export interface KeygenHTMLAttributes extends HTMLAttributes { + challenge?: string + disabled?: Booleanish + form?: string + keytype?: string + keyparams?: string + name?: string +} + +export interface LabelHTMLAttributes extends HTMLAttributes { + for?: string + form?: string +} + +export interface LiHTMLAttributes extends HTMLAttributes { + value?: string | ReadonlyArray | number +} + +export interface LinkHTMLAttributes extends HTMLAttributes { + as?: string + crossorigin?: CrossOrigin + fetchPriority?: 'high' | 'low' | 'auto' + href?: string + hreflang?: string + integrity?: string + media?: string + imageSrcSet?: string + imageSizes?: string + referrerpolicy?: HTMLAttributeReferrerPolicy + sizes?: string + type?: string + charset?: string +} + +export interface MapHTMLAttributes extends HTMLAttributes { + name?: string +} + +export interface MenuHTMLAttributes extends HTMLAttributes { + type?: string +} + +export interface MediaHTMLAttributes extends HTMLAttributes { + autoplay?: Booleanish + controls?: Booleanish + controlslist?: string + crossorigin?: CrossOrigin + loop?: Booleanish + mediagroup?: string + muted?: Booleanish + playsinline?: Booleanish + preload?: string + src?: string +} + +export interface MetaHTMLAttributes extends HTMLAttributes { + charset?: string + content?: string + httpequiv?: string + media?: string | undefined + name?: string +} + +export interface MeterHTMLAttributes extends HTMLAttributes { + form?: string + high?: Numberish + low?: Numberish + max?: Numberish + min?: Numberish + optimum?: Numberish + value?: string | ReadonlyArray | number +} + +export interface QuoteHTMLAttributes extends HTMLAttributes { + cite?: string +} + +export interface ObjectHTMLAttributes extends HTMLAttributes { + classid?: string + data?: string + form?: string + height?: Numberish + name?: string + type?: string + usemap?: string + width?: Numberish + wmode?: string +} + +export interface OlHTMLAttributes extends HTMLAttributes { + reversed?: Booleanish + start?: Numberish + type?: '1' | 'a' | 'A' | 'i' | 'I' +} + +export interface OptgroupHTMLAttributes extends HTMLAttributes { + disabled?: Booleanish + label?: string +} + +export interface OptionHTMLAttributes extends HTMLAttributes { + disabled?: Booleanish + label?: string + selected?: Booleanish + value?: any // we support :value to be bound to anything w/ v-model +} + +export interface OutputHTMLAttributes extends HTMLAttributes { + for?: string + form?: string + name?: string +} + +export interface ParamHTMLAttributes extends HTMLAttributes { + name?: string + value?: string | ReadonlyArray | number +} + +export interface ProgressHTMLAttributes extends HTMLAttributes { + max?: Numberish + value?: string | ReadonlyArray | number +} + +export interface ScriptHTMLAttributes extends HTMLAttributes { + async?: Booleanish + /** @deprecated */ + charset?: string + crossorigin?: CrossOrigin + defer?: Booleanish + integrity?: string + nomodule?: Booleanish + referrerpolicy?: HTMLAttributeReferrerPolicy + src?: string + type?: string +} + +export interface SelectHTMLAttributes extends HTMLAttributes { + autocomplete?: string + disabled?: Booleanish + form?: string + multiple?: Booleanish + name?: string + required?: Booleanish + size?: Numberish + value?: any // we support :value to be bound to anything w/ v-model +} + +export interface SourceHTMLAttributes extends HTMLAttributes { + height?: number + media?: string + sizes?: string + src?: string + srcset?: string + type?: string + width?: number +} + +export interface StyleHTMLAttributes extends HTMLAttributes { + media?: string + scoped?: Booleanish + type?: string +} + +export interface TableHTMLAttributes extends HTMLAttributes { + align?: 'left' | 'center' | 'right' + bgcolor?: string + border?: number + cellpadding?: Numberish + cellspacing?: Numberish + frame?: Booleanish + rules?: 'none' | 'groups' | 'rows' | 'columns' | 'all' + summary?: string + width?: Numberish +} + +export interface TextareaHTMLAttributes extends HTMLAttributes { + autocomplete?: string + cols?: Numberish + dirname?: string + disabled?: Booleanish + form?: string + maxlength?: Numberish + minlength?: Numberish + name?: string + placeholder?: string + readonly?: Booleanish + required?: Booleanish + rows?: Numberish + value?: string | ReadonlyArray | number | null + wrap?: string +} + +export interface TdHTMLAttributes extends HTMLAttributes { + align?: 'left' | 'center' | 'right' | 'justify' | 'char' + colspan?: Numberish + headers?: string + rowspan?: Numberish + scope?: string + abbr?: string + height?: Numberish + width?: Numberish + valign?: 'top' | 'middle' | 'bottom' | 'baseline' +} + +export interface ThHTMLAttributes extends HTMLAttributes { + align?: 'left' | 'center' | 'right' | 'justify' | 'char' + colspan?: Numberish + headers?: string + rowspan?: Numberish + scope?: string + abbr?: string +} + +export interface TimeHTMLAttributes extends HTMLAttributes { + datetime?: string +} + +export interface TrackHTMLAttributes extends HTMLAttributes { + default?: Booleanish + kind?: string + label?: string + src?: string + srclang?: string +} + +export interface VideoHTMLAttributes extends MediaHTMLAttributes { + height?: Numberish + playsinline?: Booleanish + poster?: string + width?: Numberish + disablePictureInPicture?: Booleanish + disableRemotePlayback?: Booleanish +} + +export interface WebViewHTMLAttributes extends HTMLAttributes { + allowfullscreen?: Booleanish + allowpopups?: Booleanish + autosize?: Booleanish + blinkfeatures?: string + disableblinkfeatures?: string + disableguestresize?: Booleanish + disablewebsecurity?: Booleanish + guestinstance?: string + httpreferrer?: string + nodeintegration?: Booleanish + partition?: string + plugins?: Booleanish + preload?: string + src?: string + useragent?: string + webpreferences?: string +} + +export interface SVGAttributes extends AriaAttributes, EventHandlers { + innerHTML?: string + + /** + * SVG Styling Attributes + * @see https://www.w3.org/TR/SVG/styling.html#ElementSpecificStyling + */ + class?: any + style?: StyleValue + + color?: string + height?: Numberish + id?: string + lang?: string + max?: Numberish + media?: string + method?: string + min?: Numberish + name?: string + target?: string + type?: string + width?: Numberish + + // Other HTML properties supported by SVG elements in browsers + role?: string + tabindex?: Numberish + crossOrigin?: CrossOrigin + + // SVG Specific attributes + 'accent-height'?: Numberish + accumulate?: 'none' | 'sum' + additive?: 'replace' | 'sum' + 'alignment-baseline'?: + | 'auto' + | 'baseline' + | 'before-edge' + | 'text-before-edge' + | 'middle' + | 'central' + | 'after-edge' + | 'text-after-edge' + | 'ideographic' + | 'alphabetic' + | 'hanging' + | 'mathematical' + | 'inherit' + allowReorder?: 'no' | 'yes' + alphabetic?: Numberish + amplitude?: Numberish + 'arabic-form'?: 'initial' | 'medial' | 'terminal' | 'isolated' + ascent?: Numberish + attributeName?: string + attributeType?: string + autoReverse?: Numberish + azimuth?: Numberish + baseFrequency?: Numberish + 'baseline-shift'?: Numberish + baseProfile?: Numberish + bbox?: Numberish + begin?: Numberish + bias?: Numberish + by?: Numberish + calcMode?: Numberish + 'cap-height'?: Numberish + clip?: Numberish + 'clip-path'?: string + clipPathUnits?: Numberish + 'clip-rule'?: Numberish + 'color-interpolation'?: Numberish + 'color-interpolation-filters'?: 'auto' | 'sRGB' | 'linearRGB' | 'inherit' + 'color-profile'?: Numberish + 'color-rendering'?: Numberish + contentScriptType?: Numberish + contentStyleType?: Numberish + cursor?: Numberish + cx?: Numberish + cy?: Numberish + d?: string + decelerate?: Numberish + descent?: Numberish + diffuseConstant?: Numberish + direction?: Numberish + display?: Numberish + divisor?: Numberish + 'dominant-baseline'?: Numberish + dur?: Numberish + dx?: Numberish + dy?: Numberish + edgeMode?: Numberish + elevation?: Numberish + 'enable-background'?: Numberish + end?: Numberish + exponent?: Numberish + externalResourcesRequired?: Numberish + fill?: string + 'fill-opacity'?: Numberish + 'fill-rule'?: 'nonzero' | 'evenodd' | 'inherit' + filter?: string + filterRes?: Numberish + filterUnits?: Numberish + 'flood-color'?: Numberish + 'flood-opacity'?: Numberish + focusable?: Numberish + 'font-family'?: string + 'font-size'?: Numberish + 'font-size-adjust'?: Numberish + 'font-stretch'?: Numberish + 'font-style'?: Numberish + 'font-variant'?: Numberish + 'font-weight'?: Numberish + format?: Numberish + from?: Numberish + fx?: Numberish + fy?: Numberish + g1?: Numberish + g2?: Numberish + 'glyph-name'?: Numberish + 'glyph-orientation-horizontal'?: Numberish + 'glyph-orientation-vertical'?: Numberish + glyphRef?: Numberish + gradientTransform?: string + gradientUnits?: string + hanging?: Numberish + 'horiz-adv-x'?: Numberish + 'horiz-origin-x'?: Numberish + href?: string + ideographic?: Numberish + 'image-rendering'?: Numberish + in2?: Numberish + in?: string + intercept?: Numberish + k1?: Numberish + k2?: Numberish + k3?: Numberish + k4?: Numberish + k?: Numberish + kernelMatrix?: Numberish + kernelUnitLength?: Numberish + kerning?: Numberish + keyPoints?: Numberish + keySplines?: Numberish + keyTimes?: Numberish + lengthAdjust?: Numberish + 'letter-spacing'?: Numberish + 'lighting-color'?: Numberish + limitingConeAngle?: Numberish + local?: Numberish + 'marker-end'?: string + markerHeight?: Numberish + 'marker-mid'?: string + 'marker-start'?: string + markerUnits?: Numberish + markerWidth?: Numberish + mask?: string + maskContentUnits?: Numberish + maskUnits?: Numberish + mathematical?: Numberish + mode?: Numberish + numOctaves?: Numberish + offset?: Numberish + opacity?: Numberish + operator?: Numberish + order?: Numberish + orient?: Numberish + orientation?: Numberish + origin?: Numberish + overflow?: Numberish + 'overline-position'?: Numberish + 'overline-thickness'?: Numberish + 'paint-order'?: Numberish + 'panose-1'?: Numberish + pathLength?: Numberish + patternContentUnits?: string + patternTransform?: Numberish + patternUnits?: string + 'pointer-events'?: Numberish + points?: string + pointsAtX?: Numberish + pointsAtY?: Numberish + pointsAtZ?: Numberish + preserveAlpha?: Numberish + preserveAspectRatio?: string + primitiveUnits?: Numberish + r?: Numberish + radius?: Numberish + refX?: Numberish + refY?: Numberish + renderingIntent?: Numberish + repeatCount?: Numberish + repeatDur?: Numberish + requiredExtensions?: Numberish + requiredFeatures?: Numberish + restart?: Numberish + result?: string + rotate?: Numberish + rx?: Numberish + ry?: Numberish + scale?: Numberish + seed?: Numberish + 'shape-rendering'?: Numberish + slope?: Numberish + spacing?: Numberish + specularConstant?: Numberish + specularExponent?: Numberish + speed?: Numberish + spreadMethod?: string + startOffset?: Numberish + stdDeviation?: Numberish + stemh?: Numberish + stemv?: Numberish + stitchTiles?: Numberish + 'stop-color'?: string + 'stop-opacity'?: Numberish + 'strikethrough-position'?: Numberish + 'strikethrough-thickness'?: Numberish + string?: Numberish + stroke?: string + 'stroke-dasharray'?: Numberish + 'stroke-dashoffset'?: Numberish + 'stroke-linecap'?: 'butt' | 'round' | 'square' | 'inherit' + 'stroke-linejoin'?: 'miter' | 'round' | 'bevel' | 'inherit' + 'stroke-miterlimit'?: Numberish + 'stroke-opacity'?: Numberish + 'stroke-width'?: Numberish + surfaceScale?: Numberish + systemLanguage?: Numberish + tableValues?: Numberish + targetX?: Numberish + targetY?: Numberish + 'text-anchor'?: string + 'text-decoration'?: Numberish + textLength?: Numberish + 'text-rendering'?: Numberish + to?: Numberish + transform?: string + u1?: Numberish + u2?: Numberish + 'underline-position'?: Numberish + 'underline-thickness'?: Numberish + unicode?: Numberish + 'unicode-bidi'?: Numberish + 'unicode-range'?: Numberish + 'unitsPer-em'?: Numberish + 'v-alphabetic'?: Numberish + values?: string + 'vector-effect'?: Numberish + version?: string + 'vert-adv-y'?: Numberish + 'vert-origin-x'?: Numberish + 'vert-origin-y'?: Numberish + 'v-hanging'?: Numberish + 'v-ideographic'?: Numberish + viewBox?: string + viewTarget?: Numberish + visibility?: Numberish + 'v-mathematical'?: Numberish + widths?: Numberish + 'word-spacing'?: Numberish + 'writing-mode'?: Numberish + x1?: Numberish + x2?: Numberish + x?: Numberish + xChannelSelector?: string + 'x-height'?: Numberish + xlinkActuate?: string + xlinkArcrole?: string + xlinkHref?: string + xlinkRole?: string + xlinkShow?: string + xlinkTitle?: string + xlinkType?: string + xmlns?: string + xmlnsXlink?: string + y1?: Numberish + y2?: Numberish + y?: Numberish + yChannelSelector?: string + z?: Numberish + zoomAndPan?: string +} + +export interface IntrinsicElementAttributes { + a: AnchorHTMLAttributes + abbr: HTMLAttributes + address: HTMLAttributes + area: AreaHTMLAttributes + article: HTMLAttributes + aside: HTMLAttributes + audio: AudioHTMLAttributes + b: HTMLAttributes + base: BaseHTMLAttributes + bdi: HTMLAttributes + bdo: HTMLAttributes + big: HTMLAttributes + blockquote: BlockquoteHTMLAttributes + body: HTMLAttributes + br: HTMLAttributes + button: ButtonHTMLAttributes + canvas: CanvasHTMLAttributes + caption: HTMLAttributes + cite: HTMLAttributes + code: HTMLAttributes + col: ColHTMLAttributes + colgroup: ColgroupHTMLAttributes + data: DataHTMLAttributes + datalist: HTMLAttributes + dd: HTMLAttributes + del: DelHTMLAttributes + details: DetailsHTMLAttributes + dfn: HTMLAttributes + dialog: DialogHTMLAttributes + div: HTMLAttributes + dl: HTMLAttributes + dt: HTMLAttributes + em: HTMLAttributes + embed: EmbedHTMLAttributes + fieldset: FieldsetHTMLAttributes + figcaption: HTMLAttributes + figure: HTMLAttributes + footer: HTMLAttributes + form: FormHTMLAttributes + h1: HTMLAttributes + h2: HTMLAttributes + h3: HTMLAttributes + h4: HTMLAttributes + h5: HTMLAttributes + h6: HTMLAttributes + head: HTMLAttributes + header: HTMLAttributes + hgroup: HTMLAttributes + hr: HTMLAttributes + html: HtmlHTMLAttributes + i: HTMLAttributes + iframe: IframeHTMLAttributes + img: ImgHTMLAttributes + input: InputHTMLAttributes + ins: InsHTMLAttributes + kbd: HTMLAttributes + keygen: KeygenHTMLAttributes + label: LabelHTMLAttributes + legend: HTMLAttributes + li: LiHTMLAttributes + link: LinkHTMLAttributes + main: HTMLAttributes + map: MapHTMLAttributes + mark: HTMLAttributes + menu: MenuHTMLAttributes + menuitem: HTMLAttributes + meta: MetaHTMLAttributes + meter: MeterHTMLAttributes + nav: HTMLAttributes + noindex: HTMLAttributes + noscript: HTMLAttributes + object: ObjectHTMLAttributes + ol: OlHTMLAttributes + optgroup: OptgroupHTMLAttributes + option: OptionHTMLAttributes + output: OutputHTMLAttributes + p: HTMLAttributes + param: ParamHTMLAttributes + picture: HTMLAttributes + pre: HTMLAttributes + progress: ProgressHTMLAttributes + q: QuoteHTMLAttributes + rp: HTMLAttributes + rt: HTMLAttributes + ruby: HTMLAttributes + s: HTMLAttributes + samp: HTMLAttributes + search: HTMLAttributes + script: ScriptHTMLAttributes + section: HTMLAttributes + select: SelectHTMLAttributes + small: HTMLAttributes + source: SourceHTMLAttributes + span: HTMLAttributes + strong: HTMLAttributes + style: StyleHTMLAttributes + sub: HTMLAttributes + summary: HTMLAttributes + sup: HTMLAttributes + table: TableHTMLAttributes + template: HTMLAttributes + tbody: HTMLAttributes + td: TdHTMLAttributes + textarea: TextareaHTMLAttributes + tfoot: HTMLAttributes + th: ThHTMLAttributes + thead: HTMLAttributes + time: TimeHTMLAttributes + title: HTMLAttributes + tr: HTMLAttributes + track: TrackHTMLAttributes + u: HTMLAttributes + ul: HTMLAttributes + var: HTMLAttributes + video: VideoHTMLAttributes + wbr: HTMLAttributes + webview: WebViewHTMLAttributes + + // SVG + svg: SVGAttributes + + animate: SVGAttributes + animateMotion: SVGAttributes + animateTransform: SVGAttributes + circle: SVGAttributes + clipPath: SVGAttributes + defs: SVGAttributes + desc: SVGAttributes + ellipse: SVGAttributes + feBlend: SVGAttributes + feColorMatrix: SVGAttributes + feComponentTransfer: SVGAttributes + feComposite: SVGAttributes + feConvolveMatrix: SVGAttributes + feDiffuseLighting: SVGAttributes + feDisplacementMap: SVGAttributes + feDistantLight: SVGAttributes + feDropShadow: SVGAttributes + feFlood: SVGAttributes + feFuncA: SVGAttributes + feFuncB: SVGAttributes + feFuncG: SVGAttributes + feFuncR: SVGAttributes + feGaussianBlur: SVGAttributes + feImage: SVGAttributes + feMerge: SVGAttributes + feMergeNode: SVGAttributes + feMorphology: SVGAttributes + feOffset: SVGAttributes + fePointLight: SVGAttributes + feSpecularLighting: SVGAttributes + feSpotLight: SVGAttributes + feTile: SVGAttributes + feTurbulence: SVGAttributes + filter: SVGAttributes + foreignObject: SVGAttributes + g: SVGAttributes + image: SVGAttributes + line: SVGAttributes + linearGradient: SVGAttributes + marker: SVGAttributes + mask: SVGAttributes + metadata: SVGAttributes + mpath: SVGAttributes + path: SVGAttributes + pattern: SVGAttributes + polygon: SVGAttributes + polyline: SVGAttributes + radialGradient: SVGAttributes + rect: SVGAttributes + stop: SVGAttributes + switch: SVGAttributes + symbol: SVGAttributes + text: SVGAttributes + textPath: SVGAttributes + tspan: SVGAttributes + use: SVGAttributes + view: SVGAttributes +} + +export interface Events { + // clipboard events + onCopy: ClipboardEventHandler + onCut: ClipboardEventHandler + onPaste: ClipboardEventHandler + + // composition events + onCompositionend: CompositionEventHandler + onCompositionstart: CompositionEventHandler + onCompositionupdate: CompositionEventHandler + + // drag drop events + onDrag: DragEventHandler + onDragend: DragEventHandler + onDragenter: DragEventHandler + onDragexit: DragEventHandler + onDragleave: DragEventHandler + onDragover: DragEventHandler + onDragstart: DragEventHandler + onDrop: DragEventHandler + + // focus events + onFocus: FocusEventHandler + onFocusin: FocusEventHandler + onFocusout: FocusEventHandler + onBlur: FocusEventHandler + + // form events + onChange: ChangeEventHandler + onBeforeinput: FormEventHandler + onInput: FormEventHandler + onReset: FormEventHandler + onSubmit: FormEventHandler + onInvalid: FormEventHandler + + // image events + onLoad: BaseEventHandler + onError: BaseEventHandler + + // keyboard events + onKeydown: KeyboardEventHandler + onKeypress: KeyboardEventHandler + onKeyup: KeyboardEventHandler + + // mouse events + onAuxclick: MouseEventHandler + onClick: MouseEventHandler + onContextmenu: MouseEventHandler + onDblclick: MouseEventHandler + onMousedown: MouseEventHandler + onMouseenter: MouseEventHandler + onMouseleave: MouseEventHandler + onMousemove: MouseEventHandler + onMouseout: MouseEventHandler + onMouseover: MouseEventHandler + onMouseup: MouseEventHandler + + // media events + onAbort: BaseEventHandler + onCanplay: BaseEventHandler + onCanplaythrough: BaseEventHandler + onDurationchange: BaseEventHandler + onEmptied: BaseEventHandler + onEncrypted: BaseEventHandler + onEnded: BaseEventHandler + onLoadeddata: BaseEventHandler + onLoadedmetadata: BaseEventHandler + onLoadstart: BaseEventHandler + onPause: BaseEventHandler + onPlay: BaseEventHandler + onPlaying: BaseEventHandler + onProgress: BaseEventHandler + onRatechange: BaseEventHandler + onSeeked: BaseEventHandler + onSeeking: BaseEventHandler + onStalled: BaseEventHandler + onSuspend: BaseEventHandler + onTimeupdate: BaseEventHandler + onVolumechange: BaseEventHandler + onWaiting: BaseEventHandler + + // selection events + onSelect: BaseEventHandler + + // scroll events + onScroll: UIEventHandler + onScrollend: UIEventHandler + + // touch events + onTouchcancel: TouchEvent + onTouchend: TouchEvent + onTouchmove: TouchEvent + onTouchstart: TouchEvent + + // pointer events + onPointerdown: PointerEvent + onPointermove: PointerEvent + onPointerup: PointerEvent + onPointercancel: PointerEvent + onPointerenter: PointerEvent + onPointerleave: PointerEvent + onPointerover: PointerEvent + onPointerout: PointerEvent + + // wheel events + onWheel: WheelEventHandler + + // animation events + onAnimationstart: AnimationEventHandler + onAnimationend: AnimationEventHandler + onAnimationiteration: AnimationEventHandler + + // transition events + onTransitionend: TransitionEventHandler + onTransitionstart: TransitionEventHandler +} + +type EventHandlers = { + [K in keyof E]?: E[K] extends (...args: any) => any + ? E[K] + : (payload: E[K]) => void +} + +export type ReservedProps = { + key?: PropertyKey + ref?: import('vue').VNodeRef + ref_for?: boolean + ref_key?: string +} + +export type NativeElements = { + [K in keyof IntrinsicElementAttributes]: IntrinsicElementAttributes[K] & + ReservedProps +} + +interface BaseSyntheticEvent { + nativeEvent: E + currentTarget: C + target: T + bubbles: boolean + cancelable: boolean + defaultPrevented: boolean + eventPhase: number + isTrusted: boolean + preventDefault: () => void + isDefaultPrevented: () => boolean + stopPropagation: () => void + isPropagationStopped: () => boolean + persist: () => void + timeStamp: number + type: string +} + +/** + * currentTarget - a reference to the element on which the event listener is registered. + * + * target - a reference to the element from which the event was originally dispatched. + * This might be a child element to the element on which the event listener is registered. + * If you thought this should be `EventTarget & T`, see https://github.com/DefinitelyTyped/DefinitelyTyped/issues/11508#issuecomment-256045682 + */ +interface SyntheticEvent + extends BaseSyntheticEvent {} + +type EventHandler> = { + bivarianceHack: (event: E) => void +}['bivarianceHack'] + +type BaseEventHandler = EventHandler> + +interface ClipboardEvent + extends SyntheticEvent { + clipboardData: DataTransfer +} +type ClipboardEventHandler = EventHandler> + +interface CompositionEvent + extends SyntheticEvent { + data: string +} +type CompositionEventHandler = EventHandler> + +interface DragEvent extends MouseEvent { + dataTransfer: DataTransfer +} +type DragEventHandler = EventHandler> + +interface FocusEvent + extends SyntheticEvent { + relatedTarget: (EventTarget & RelatedTarget) | null + target: EventTarget & Target +} +type FocusEventHandler = EventHandler> + +interface FormEvent extends SyntheticEvent {} +type FormEventHandler = EventHandler> + +interface ChangeEvent extends SyntheticEvent { + target: EventTarget & T +} +type ChangeEventHandler = EventHandler> + +interface KeyboardEvent + extends UIEvent { + altKey: boolean + /** @deprecated */ + charCode: number + ctrlKey: boolean + code: string + /** + * See [DOM Level 3 Events spec](https://www.w3.org/TR/uievents-key/#keys-modifier). for a list of valid (case-sensitive) arguments to this method. + */ + getModifierState: (key: ModifierKey) => boolean + /** + * See the [DOM Level 3 Events spec](https://www.w3.org/TR/uievents-key/#named-key-attribute-values). for possible values + */ + key: string + /** @deprecated */ + keyCode: number + locale: string + location: number + metaKey: boolean + repeat: boolean + shiftKey: boolean + /** @deprecated */ + which: number +} +type KeyboardEventHandler = EventHandler> + +export type ModifierKey = + | 'Alt' + | 'AltGraph' + | 'CapsLock' + | 'Control' + | 'Fn' + | 'FnLock' + | 'Hyper' + | 'Meta' + | 'NumLock' + | 'ScrollLock' + | 'Shift' + | 'Super' + | 'Symbol' + | 'SymbolLock' +interface MouseEvent + extends UIEvent { + altKey: boolean + button: number + buttons: number + clientX: number + clientY: number + ctrlKey: boolean + /** + * See [DOM Level 3 Events spec](https://www.w3.org/TR/uievents-key/#keys-modifier). for a list of valid (case-sensitive) arguments to this method. + */ + getModifierState: (key: ModifierKey) => boolean + metaKey: boolean + movementX: number + movementY: number + pageX: number + pageY: number + relatedTarget: EventTarget | null + screenX: number + screenY: number + shiftKey: boolean +} +type MouseEventHandler = EventHandler> + +interface AbstractView { + styleMedia: StyleMedia + document: Document +} +interface UIEvent + extends SyntheticEvent { + detail: number + view: AbstractView +} +type UIEventHandler = EventHandler> + +interface WheelEvent extends MouseEvent { + deltaMode: number + deltaX: number + deltaY: number + deltaZ: number +} +type WheelEventHandler = EventHandler> + +interface AnimationEvent + extends SyntheticEvent { + animationName: string + elapsedTime: number + pseudoElement: string +} +type AnimationEventHandler = EventHandler> + +interface TransitionEvent + extends SyntheticEvent { + elapsedTime: number + propertyName: string + pseudoElement: string +} +type TransitionEventHandler = EventHandler> diff --git a/packages/jsx/src/jsx-runtime/global.d.ts b/packages/jsx/src/jsx-runtime/global.d.ts new file mode 100644 index 000000000..9fa3ed42c --- /dev/null +++ b/packages/jsx/src/jsx-runtime/global.d.ts @@ -0,0 +1,153 @@ +// Copied from https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/react/global.d.ts + +interface Event {} +interface AnimationEvent extends Event {} +interface ClipboardEvent extends Event {} +interface CompositionEvent extends Event {} +interface DragEvent extends Event {} +interface FocusEvent extends Event {} +interface KeyboardEvent extends Event {} +interface MouseEvent extends Event {} +interface TouchEvent extends Event {} +interface PointerEvent extends Event {} +interface ToggleEvent extends Event {} +interface TransitionEvent extends Event {} +interface UIEvent extends Event {} +interface WheelEvent extends Event {} + +interface EventTarget {} +interface Document {} +interface DataTransfer {} +interface StyleMedia {} + +interface Element {} +interface DocumentFragment {} + +interface HTMLElement extends Element {} +interface HTMLAnchorElement extends HTMLElement {} +interface HTMLAreaElement extends HTMLElement {} +interface HTMLAudioElement extends HTMLElement {} +interface HTMLBaseElement extends HTMLElement {} +interface HTMLBodyElement extends HTMLElement {} +interface HTMLBRElement extends HTMLElement {} +interface HTMLButtonElement extends HTMLElement {} +interface HTMLCanvasElement extends HTMLElement {} +interface HTMLDataElement extends HTMLElement {} +interface HTMLDataListElement extends HTMLElement {} +interface HTMLDetailsElement extends HTMLElement {} +interface HTMLDialogElement extends HTMLElement {} +interface HTMLDivElement extends HTMLElement {} +interface HTMLDListElement extends HTMLElement {} +interface HTMLEmbedElement extends HTMLElement {} +interface HTMLFieldSetElement extends HTMLElement {} +interface HTMLFormElement extends HTMLElement {} +interface HTMLHeadingElement extends HTMLElement {} +interface HTMLHeadElement extends HTMLElement {} +interface HTMLHRElement extends HTMLElement {} +interface HTMLHtmlElement extends HTMLElement {} +interface HTMLIFrameElement extends HTMLElement {} +interface HTMLImageElement extends HTMLElement {} +interface HTMLInputElement extends HTMLElement {} +interface HTMLModElement extends HTMLElement {} +interface HTMLLabelElement extends HTMLElement {} +interface HTMLLegendElement extends HTMLElement {} +interface HTMLLIElement extends HTMLElement {} +interface HTMLLinkElement extends HTMLElement {} +interface HTMLMapElement extends HTMLElement {} +interface HTMLMetaElement extends HTMLElement {} +interface HTMLMeterElement extends HTMLElement {} +interface HTMLObjectElement extends HTMLElement {} +interface HTMLOListElement extends HTMLElement {} +interface HTMLOptGroupElement extends HTMLElement {} +interface HTMLOptionElement extends HTMLElement {} +interface HTMLOutputElement extends HTMLElement {} +interface HTMLParagraphElement extends HTMLElement {} +interface HTMLParamElement extends HTMLElement {} +interface HTMLPreElement extends HTMLElement {} +interface HTMLProgressElement extends HTMLElement {} +interface HTMLQuoteElement extends HTMLElement {} +interface HTMLSlotElement extends HTMLElement {} +interface HTMLScriptElement extends HTMLElement {} +interface HTMLSelectElement extends HTMLElement {} +interface HTMLSourceElement extends HTMLElement {} +interface HTMLSpanElement extends HTMLElement {} +interface HTMLStyleElement extends HTMLElement {} +interface HTMLTableElement extends HTMLElement {} +interface HTMLTableColElement extends HTMLElement {} +interface HTMLTableDataCellElement extends HTMLElement {} +interface HTMLTableHeaderCellElement extends HTMLElement {} +interface HTMLTableRowElement extends HTMLElement {} +interface HTMLTableSectionElement extends HTMLElement {} +interface HTMLTemplateElement extends HTMLElement {} +interface HTMLTextAreaElement extends HTMLElement {} +interface HTMLTimeElement extends HTMLElement {} +interface HTMLTitleElement extends HTMLElement {} +interface HTMLTrackElement extends HTMLElement {} +interface HTMLUListElement extends HTMLElement {} +interface HTMLVideoElement extends HTMLElement {} +interface HTMLWebViewElement extends HTMLElement {} + +interface SVGElement extends Element {} +interface SVGSVGElement extends SVGElement {} +interface SVGCircleElement extends SVGElement {} +interface SVGClipPathElement extends SVGElement {} +interface SVGDefsElement extends SVGElement {} +interface SVGDescElement extends SVGElement {} +interface SVGEllipseElement extends SVGElement {} +interface SVGFEBlendElement extends SVGElement {} +interface SVGFEColorMatrixElement extends SVGElement {} +interface SVGFEComponentTransferElement extends SVGElement {} +interface SVGFECompositeElement extends SVGElement {} +interface SVGFEConvolveMatrixElement extends SVGElement {} +interface SVGFEDiffuseLightingElement extends SVGElement {} +interface SVGFEDisplacementMapElement extends SVGElement {} +interface SVGFEDistantLightElement extends SVGElement {} +interface SVGFEDropShadowElement extends SVGElement {} +interface SVGFEFloodElement extends SVGElement {} +interface SVGFEFuncAElement extends SVGElement {} +interface SVGFEFuncBElement extends SVGElement {} +interface SVGFEFuncGElement extends SVGElement {} +interface SVGFEFuncRElement extends SVGElement {} +interface SVGFEGaussianBlurElement extends SVGElement {} +interface SVGFEImageElement extends SVGElement {} +interface SVGFEMergeElement extends SVGElement {} +interface SVGFEMergeNodeElement extends SVGElement {} +interface SVGFEMorphologyElement extends SVGElement {} +interface SVGFEOffsetElement extends SVGElement {} +interface SVGFEPointLightElement extends SVGElement {} +interface SVGFESpecularLightingElement extends SVGElement {} +interface SVGFESpotLightElement extends SVGElement {} +interface SVGFETileElement extends SVGElement {} +interface SVGFETurbulenceElement extends SVGElement {} +interface SVGFilterElement extends SVGElement {} +interface SVGForeignObjectElement extends SVGElement {} +interface SVGGElement extends SVGElement {} +interface SVGImageElement extends SVGElement {} +interface SVGLineElement extends SVGElement {} +interface SVGLinearGradientElement extends SVGElement {} +interface SVGMarkerElement extends SVGElement {} +interface SVGMaskElement extends SVGElement {} +interface SVGMetadataElement extends SVGElement {} +interface SVGPathElement extends SVGElement {} +interface SVGPatternElement extends SVGElement {} +interface SVGPolygonElement extends SVGElement {} +interface SVGPolylineElement extends SVGElement {} +interface SVGRadialGradientElement extends SVGElement {} +interface SVGRectElement extends SVGElement {} +interface SVGSetElement extends SVGElement {} +interface SVGStopElement extends SVGElement {} +interface SVGSwitchElement extends SVGElement {} +interface SVGSymbolElement extends SVGElement {} +interface SVGTextElement extends SVGElement {} +interface SVGTextPathElement extends SVGElement {} +interface SVGTSpanElement extends SVGElement {} +interface SVGUseElement extends SVGElement {} +interface SVGViewElement extends SVGElement {} + +interface FormData {} +interface Text {} +interface TouchList {} +interface WebGLRenderingContext {} +interface WebGL2RenderingContext {} + +interface TrustedHTML {} diff --git a/packages/jsx/src/options.ts b/packages/jsx/src/options.ts new file mode 100644 index 000000000..163055b89 --- /dev/null +++ b/packages/jsx/src/options.ts @@ -0,0 +1,36 @@ +import type { FilterOptions } from '@vue-macros/common' +import type { Options as OptionsJsxDirective } from '@vue-macros/jsx-directive' +import type { Options as OptionsJsxMacros } from '@vue-macros/jsx-macros' + +export type JSXOptions = { + lib?: 'vue' | 'vue/vapor' | 'react' | 'preact' | 'solid' | (string & {}) + ref?: ResolvedJSXOptions['ref'] | boolean + macros?: ResolvedJSXOptions['macros'] | boolean + directive?: ResolvedJSXOptions['directive'] | boolean +} + +export type ResolvedJSXOptions = { + ref?: FilterOptions & { alias?: string[] } + macros?: OptionsJsxMacros + directive?: OptionsJsxDirective +} + +export function resolveJSXOptions( + options: JSXOptions = {}, +): ResolvedJSXOptions { + const resolveOptions: ResolvedJSXOptions = {} + if (options.directive !== false) + resolveOptions.directive = + options.directive === true ? {} : (options.directive ?? {}) + if (options.macros !== false) + resolveOptions.macros = + options.macros === true ? {} : (options.macros ?? {}) + if (options.ref !== false) + resolveOptions.ref = options.ref === true ? {} : (options.ref ?? {}) + + if (options.lib) { + resolveOptions.directive && (resolveOptions.directive.lib ??= options.lib) + resolveOptions.macros && (resolveOptions.macros.lib ??= options.lib) + } + return resolveOptions +} diff --git a/packages/jsx/src/raw.ts b/packages/jsx/src/raw.ts new file mode 100644 index 000000000..751dd57ee --- /dev/null +++ b/packages/jsx/src/raw.ts @@ -0,0 +1,22 @@ +import jsxDirective from '@vue-macros/jsx-directive/raw' +import jsxMacros from '@vue-macros/jsx-macros/raw' + +import { resolveJSXOptions, type JSXOptions } from './options' +import type { UnpluginFactory } from 'unplugin' + +const jsxPlugins = [ + ['directive', jsxDirective], + ['macros', jsxMacros], +] as const + +const plugin: UnpluginFactory = ( + userOptions = {}, + meta, +) => { + const options = resolveJSXOptions(userOptions) + return jsxPlugins.flatMap(([name, plugin]) => + options[name] ? plugin(options[name], meta) : [], + ) +} + +export default plugin diff --git a/packages/jsx/src/rolldown.ts b/packages/jsx/src/rolldown.ts new file mode 100644 index 000000000..082a55c18 --- /dev/null +++ b/packages/jsx/src/rolldown.ts @@ -0,0 +1,3 @@ +import unplugin from '.' + +export default unplugin.rolldown as typeof unplugin.rolldown diff --git a/packages/jsx/src/rollup.ts b/packages/jsx/src/rollup.ts new file mode 100644 index 000000000..45545feb1 --- /dev/null +++ b/packages/jsx/src/rollup.ts @@ -0,0 +1,3 @@ +import unplugin from '.' + +export default unplugin.rollup as typeof unplugin.rollup diff --git a/packages/jsx/src/rspack.ts b/packages/jsx/src/rspack.ts new file mode 100644 index 000000000..6df8a0299 --- /dev/null +++ b/packages/jsx/src/rspack.ts @@ -0,0 +1,3 @@ +import unplugin from '.' + +export default unplugin.rspack as typeof unplugin.rspack diff --git a/packages/jsx/src/runtime.ts b/packages/jsx/src/runtime.ts new file mode 100644 index 000000000..76bffcd98 --- /dev/null +++ b/packages/jsx/src/runtime.ts @@ -0,0 +1 @@ +export { shallowRef as useRef } from 'vue' diff --git a/packages/jsx/src/vite.ts b/packages/jsx/src/vite.ts new file mode 100644 index 000000000..a7c5db2c1 --- /dev/null +++ b/packages/jsx/src/vite.ts @@ -0,0 +1,3 @@ +import unplugin from '.' + +export default unplugin.vite as typeof unplugin.vite diff --git a/packages/jsx/src/volar.ts b/packages/jsx/src/volar.ts new file mode 100644 index 000000000..4d8383904 --- /dev/null +++ b/packages/jsx/src/volar.ts @@ -0,0 +1,22 @@ +import jsxDirective from '@vue-macros/volar/jsx-directive' +import jsxMacros from '@vue-macros/volar/jsx-macros' +import jsxRef from '@vue-macros/volar/jsx-ref' +import { createPlugin, type PluginReturn } from 'ts-macro' +import { resolveJSXOptions, type JSXOptions } from './options' + +const jsxPlugins = [ + ['directive', jsxDirective], + ['macros', jsxMacros], + ['ref', jsxRef], +] as const + +const plugin: PluginReturn = createPlugin( + (ctx, userOptions) => { + const options = resolveJSXOptions(userOptions) + return jsxPlugins.flatMap(([name, plugin]) => + options[name] ? plugin(options[name])(ctx) : [], + ) + }, +) + +export default plugin diff --git a/packages/jsx/src/webpack.ts b/packages/jsx/src/webpack.ts new file mode 100644 index 000000000..74c1c9020 --- /dev/null +++ b/packages/jsx/src/webpack.ts @@ -0,0 +1,3 @@ +import unplugin from '.' + +export default unplugin.webpack as typeof unplugin.webpack diff --git a/packages/jsx/tests/__snapshots__/fixtures.test.ts.snap b/packages/jsx/tests/__snapshots__/fixtures.test.ts.snap new file mode 100644 index 000000000..a39a39aa3 --- /dev/null +++ b/packages/jsx/tests/__snapshots__/fixtures.test.ts.snap @@ -0,0 +1,14 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`fixtures > tests/fixtures/index.tsx 1`] = ` +"// index.js +import { createVNode, Fragment } from 'vue'; + +var index = () => { + let foo = 1; + return createVNode(Fragment, null, [foo === 1 ? createVNode("div", null, [foo]) : null]); +}; + +export { index as default }; +" +`; diff --git a/packages/jsx/tests/fixtures.test.ts b/packages/jsx/tests/fixtures.test.ts new file mode 100644 index 000000000..8f8f6cc2c --- /dev/null +++ b/packages/jsx/tests/fixtures.test.ts @@ -0,0 +1,17 @@ +import { resolve } from 'node:path' +import { rollupBuild, RollupVueJsx, testFixtures } from '@vue-macros/test-utils' +import { describe } from 'vitest' +import VueJsxMacros from '../src/rollup' + +describe('fixtures', async () => { + await testFixtures( + ['tests/fixtures/**/*'], + (_, id) => { + return rollupBuild(id, [VueJsxMacros(), RollupVueJsx()]) + }, + { + cwd: resolve(__dirname, '..'), + promise: true, + }, + ) +}) diff --git a/packages/jsx/tests/fixtures/index.tsx b/packages/jsx/tests/fixtures/index.tsx new file mode 100644 index 000000000..32481e904 --- /dev/null +++ b/packages/jsx/tests/fixtures/index.tsx @@ -0,0 +1,4 @@ +export default () => { + let foo = 1 + return
{foo}
+} diff --git a/packages/jsx/tsup.config.ts b/packages/jsx/tsup.config.ts new file mode 100644 index 000000000..e5efb9683 --- /dev/null +++ b/packages/jsx/tsup.config.ts @@ -0,0 +1,3 @@ +import { config } from '../../tsup.config.js' + +export default config() diff --git a/packages/macros/package.json b/packages/macros/package.json index 53ae36c37..5dc4efe62 100644 --- a/packages/macros/package.json +++ b/packages/macros/package.json @@ -133,6 +133,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/macros/src/index.ts b/packages/macros/src/index.ts index ee74a68ac..540befa19 100644 --- a/packages/macros/src/index.ts +++ b/packages/macros/src/index.ts @@ -36,6 +36,7 @@ import VueExportProps from '@vue-macros/export-props' import VueExportRender from '@vue-macros/export-render' import VueHoistStatic from '@vue-macros/hoist-static' import VueJsxDirective from '@vue-macros/jsx-directive' +import VueJsxMacros from '@vue-macros/jsx-macros' import VueNamedTemplate from '@vue-macros/named-template' import VueReactivityTransform from '@vue-macros/reactivity-transform' import VueScriptLang from '@vue-macros/script-lang' @@ -135,6 +136,7 @@ const plugin: UnpluginCombineInstance = : []), options.plugins.vue, + resolvePlugin(VueJsxMacros, framework, options.jsxMacros), options.plugins.vueJsx, resolvePlugin(VueDefineRender, framework, options.defineRender), setupComponentPlugins?.[1], diff --git a/packages/volar/package.json b/packages/volar/package.json index 5df282548..a8bedff5f 100644 --- a/packages/volar/package.json +++ b/packages/volar/package.json @@ -119,6 +119,12 @@ "import": "./dist/jsx-directive.js", "default": "./dist/loader/jsx-directive.cjs" }, + "./jsx-macros": { + "types": "./dist/jsx-macros.d.ts", + "module-sync": "./dist/jsx-macros.js", + "import": "./dist/jsx-macros.js", + "default": "./dist/loader/jsx-macros.cjs" + }, "./jsx-ref": { "types": "./dist/jsx-ref.d.ts", "module-sync": "./dist/jsx-ref.js", @@ -195,13 +201,13 @@ "@vue-macros/config": "workspace:*", "@vue-macros/short-bind": "workspace:*", "@vue-macros/short-vmodel": "workspace:*", + "@vue/compiler-dom": "catalog:", "@vue/language-core": "catalog:", "jiti": "catalog:", "muggle-string": "catalog:", "ts-macro": "catalog:" }, "devDependencies": { - "@vue/compiler-dom": "catalog:", "typescript": "catalog:", "vue-tsc": "catalog:" }, diff --git a/packages/volar/src/index.ts b/packages/volar/src/index.ts index 2ad5e677c..248c50158 100644 --- a/packages/volar/src/index.ts +++ b/packages/volar/src/index.ts @@ -12,6 +12,7 @@ import exportExpose from './export-expose' import exportProps from './export-props' import exportRender from './export-render' import jsxDirective from './jsx-directive' +import jsxMacros from './jsx-macros' import jsxRef from './jsx-ref' import scriptLang from './script-lang' import scriptSFC from './script-sfc' @@ -42,6 +43,7 @@ const plugins = { scriptSFC, scriptLang, jsxRef, + jsxMacros, } const plugin: VueLanguagePlugin = (ctx) => diff --git a/packages/volar/src/jsx-directive/ref.ts b/packages/volar/src/jsx-directive/ref.ts index a6bb7b133..41e012a3b 100644 --- a/packages/volar/src/jsx-directive/ref.ts +++ b/packages/volar/src/jsx-directive/ref.ts @@ -32,7 +32,7 @@ export function transformRef( getStart(attribute.initializer.expression, options), allCodeFeatures, ], - `} satisfies { ref: (e: Parameters[0]) => any }) as {}}`, + `} satisfies { ref: (e: Parameters[0]) => any }) as {}}`, ) } } diff --git a/packages/volar/src/jsx-macros.ts b/packages/volar/src/jsx-macros.ts new file mode 100644 index 000000000..bcf8ce472 --- /dev/null +++ b/packages/volar/src/jsx-macros.ts @@ -0,0 +1,67 @@ +import { createFilter } from '@vue-macros/common' +import { createPlugin, type PluginReturn } from 'ts-macro' +import { + getGlobalTypes, + getRootMap, + transformJsxMacros, +} from './jsx-macros/index' +import type { OptionsResolved } from '@vue-macros/config' + +const plugin: PluginReturn = + createPlugin( + ( + { ts, vueCompilerOptions }, + userOptions = vueCompilerOptions?.vueMacros?.jsxMacros === true + ? {} + : (vueCompilerOptions?.vueMacros?.jsxMacros ?? {}), + ) => { + if (!userOptions) return [] + + const filter = createFilter(userOptions) + const lib = userOptions.lib ?? 'vue' + const macros = { + defineComponent: { + alias: + userOptions.defineComponent?.alias ?? + [ + 'defineComponent', + lib === 'vue/vapor' ? 'defineVaporComponent' : '', + ].filter(Boolean), + }, + defineModel: { + alias: userOptions.defineModel?.alias ?? ['defineModel'], + }, + defineExpose: { + alias: userOptions.defineExpose?.alias ?? ['defineExpose'], + }, + defineSlots: { + alias: userOptions.defineSlots?.alias ?? ['defineSlots'], + }, + defineStyle: { + alias: userOptions.defineStyle?.alias ?? ['defineStyle'], + }, + } + + return { + name: 'vue-macros-jsx-macros', + resolveVirtualCode(virtualCode) { + const { filePath, languageId, codes } = virtualCode + if (!filter(filePath) || !['jsx', 'tsx'].includes(languageId)) return + + const options = { + ...virtualCode, + ts, + lib, + ...macros, + } + const rootMap = getRootMap(options) + if (rootMap.size) { + transformJsxMacros(rootMap, options) + codes.push(getGlobalTypes(options)) + } + }, + } + }, + ) +export default plugin +export { plugin as 'module.exports' } diff --git a/packages/volar/src/jsx-macros/define-component.ts b/packages/volar/src/jsx-macros/define-component.ts new file mode 100644 index 000000000..c8f2c2609 --- /dev/null +++ b/packages/volar/src/jsx-macros/define-component.ts @@ -0,0 +1,38 @@ +import { replaceSourceRange } from 'muggle-string' +import { allCodeFeatures } from 'ts-macro' +import { getStart, getText } from '../common' +import type { TransformOptions } from '.' + +export function transformDefineComponent( + node: import('typescript').CallExpression, + options: TransformOptions, +): void { + const { codes, source } = options + + replaceSourceRange(codes, source, node.arguments[0].end, node.end - 1) + + const componentOptions = node.arguments[1] + replaceSourceRange( + codes, + source, + getStart(node, options), + node.expression.end + 1, + '(', + [ + getText(node.expression, options), + source, + getStart(node, options), + allCodeFeatures, + ], + '(() => ({}) as any, ', + componentOptions + ? [ + getText(componentOptions, options), + source, + getStart(componentOptions, options), + allCodeFeatures, + ] + : '', + '), ', + ) +} diff --git a/packages/volar/src/jsx-macros/define-style.ts b/packages/volar/src/jsx-macros/define-style.ts new file mode 100644 index 000000000..e441d702f --- /dev/null +++ b/packages/volar/src/jsx-macros/define-style.ts @@ -0,0 +1,91 @@ +import { HELPER_PREFIX } from '@vue-macros/common' +import { generateClassProperty } from '@vue/language-core/lib/codegen/style/classProperty.js' +import { parseCssClassNames } from '@vue/language-core/lib/utils/parseCssClassNames.js' +import { replaceSourceRange } from 'muggle-string' +import { getStart, getText } from '../common' +import type { JsxMacros, TransformOptions } from '.' + +export function transformDefineStyle( + defineStyles: JsxMacros['defineStyle'], + options: TransformOptions, +): void { + if (!defineStyles?.length) return + const { ts, source, codes } = options + defineStyles.forEach(({ expression, isCssModules }, index) => { + if ( + isCssModules && + expression?.arguments[0] && + !expression.typeArguments && + ts.isTemplateLiteral(expression.arguments[0]) + ) { + replaceSourceRange( + codes, + source, + expression.arguments.pos - 1, + expression.arguments.pos - 1, + `<${HELPER_PREFIX}PrettifyLocal<{}`, + ...generateCssClassesType( + getText(expression.arguments[0], options).slice(1, -1), + getStart(expression.arguments[0], options) + 1, + index, + ), + '>>', + ) + } + + addEmbeddedCode(expression, index, options) + }) +} + +function* generateCssClassesType(css: string, offset: number, index: number) { + for (const className of [...parseCssClassNames(css)]) { + yield* generateClassProperty( + index, + className.text, + className.offset + offset, + 'string', + ) + } +} + +function addEmbeddedCode( + expression: import('typescript').CallExpression, + index: number, + options: TransformOptions, +) { + const { ts } = options + const languageId = + ts.isPropertyAccessExpression(expression.expression) && + ts.isIdentifier(expression.expression.name) + ? expression.expression.name.text + : 'css' + const style = expression.arguments[0] + const styleText = getText(style, options) + .slice(1, -1) + .replaceAll(/\$\{.*\}/g, (str) => '_'.repeat(str.length)) + options.embeddedCodes.push({ + id: `style_${index}`, + languageId, + snapshot: { + getText: (start, end) => styleText.slice(start, end), + getLength: () => styleText.length, + getChangeRange: () => undefined, + }, + mappings: [ + { + sourceOffsets: [getStart(style, options)! + 1], + generatedOffsets: [0], + lengths: [styleText.length], + data: { + completion: true, + format: true, + navigation: true, + semantic: true, + structure: true, + verification: true, + }, + }, + ], + embeddedCodes: [], + }) +} diff --git a/packages/volar/src/jsx-macros/global-types.ts b/packages/volar/src/jsx-macros/global-types.ts new file mode 100644 index 000000000..b02db64b8 --- /dev/null +++ b/packages/volar/src/jsx-macros/global-types.ts @@ -0,0 +1,48 @@ +import { HELPER_PREFIX } from '@vue-macros/common' +import type { TransformOptions } from '.' + +export function getGlobalTypes(options: TransformOptions): string { + const defineSlots = options.defineSlots.alias + .flatMap((alias) => [ + `declare function ${alias}>(): Partial;`, + `declare function ${alias}>(slots: T): T;\n`, + ]) + .join('') + const defineExpose = options.defineExpose.alias + .map( + (alias) => + `declare function ${alias} = Record>(exposed?: Exposed): Exposed;`, + ) + .join('') + const defineStyle = options.defineStyle.alias + .map( + (alias) => + `declare const ${alias}: { (...args: ${HELPER_PREFIX}StyleArgs): T; scss: (...args: ${HELPER_PREFIX}StyleArgs)=> T; sass: (...args: ${HELPER_PREFIX}StyleArgs)=> T; stylus: (...args: ${HELPER_PREFIX}StyleArgs)=> T; less: (...args: ${HELPER_PREFIX}StyleArgs)=> T; postcss: (...args: ${HELPER_PREFIX}StyleArgs)=> T };\n`, + ) + .join('') + const defineModel = options.defineModel.alias + .map((alias) => + alias === 'defineModel' ? 'defineModel' : `defineModel: ${alias}`, + ) + .join(', ') + const defineComponent = options.defineComponent.alias + .map((alias) => + ['defineComponent', 'defineVaporComponent'].includes(alias) + ? '' + : `defineComponent: ${alias}`, + ) + .filter(Boolean) + .join(', ') + return ` +declare const { ${defineModel}, ${defineComponent} }: typeof import('vue') +${defineSlots} +${defineExpose} +${defineStyle} +type ${HELPER_PREFIX}StyleArgs = [style: string, options?: { scoped?: boolean }]; +type ${HELPER_PREFIX}PrettifyLocal = { [K in keyof T]: T[K]; } & {}; +// @ts-ignore +type __VLS_IsAny = 0 extends 1 & T ? true : false; +// @ts-ignore +type __VLS_PickNotAny = __VLS_IsAny extends true ? B : A; +` +} diff --git a/packages/volar/src/jsx-macros/index.ts b/packages/volar/src/jsx-macros/index.ts new file mode 100644 index 000000000..379e60bdf --- /dev/null +++ b/packages/volar/src/jsx-macros/index.ts @@ -0,0 +1,257 @@ +import { + HELPER_PREFIX, + type MarkRequired, + type Overwrite, +} from '@vue-macros/common' +import { toValidAssetId } from '@vue/compiler-dom' +import { replaceSourceRange } from 'muggle-string' +import { getStart, getText } from '../common' +import { transformDefineComponent } from './define-component' +import type { OptionsResolved } from '@vue-macros/config' +import type { TsmVirtualCode } from 'ts-macro' + +export { transformJsxMacros } from './transform' +export { getGlobalTypes } from './global-types' + +type UserOptions = MarkRequired< + Exclude, + | 'lib' + | 'defineModel' + | 'defineSlots' + | 'defineStyle' + | 'defineExpose' + | 'defineComponent' +> +export type TransformOptions = Overwrite< + TsmVirtualCode, + { + ts: typeof import('typescript') + lib: UserOptions['lib'] + defineModel: UserOptions['defineModel'] + defineSlots: UserOptions['defineSlots'] + defineStyle: UserOptions['defineStyle'] + defineExpose: UserOptions['defineExpose'] + defineComponent: UserOptions['defineComponent'] + } +> + +export type JsxMacros = { + defineModel?: string[] + defineSlots?: string + defineExpose?: string + defineStyle?: { + expression: import('typescript').CallExpression + isCssModules: boolean + }[] + defineComponent?: true +} +export type RootMap = Map< + | import('typescript').ArrowFunction + | import('typescript').FunctionExpression + | import('typescript').FunctionDeclaration + | undefined, + JsxMacros +> + +function getMacro( + node: import('typescript').Node | undefined, + ts: typeof import('typescript'), + options: TransformOptions, +): + | { + expression: import('typescript').CallExpression + initializer: import('typescript').Node + isRequired: boolean + } + | undefined { + if (!node) return + + if (ts.isVariableStatement(node)) { + return ts.forEachChild(node.declarationList, (decl) => getExpression(decl)) + } else { + return getExpression(node) + } + + function getExpression(decl: import('typescript').Node) { + if (ts.isVariableDeclaration(decl) && decl.initializer) { + const initializer = + ts.isCallExpression(decl.initializer) && + ts.isIdentifier(decl.initializer.expression) && + decl.initializer.expression.escapedText === '$' && + decl.initializer.arguments[0] + ? decl.initializer.arguments[0] + : decl.initializer + const expression = getMacroExpression(initializer) + if (expression) { + return { + expression, + initializer: decl.initializer, + isRequired: ts.isNonNullExpression(initializer), + } + } + } else if (ts.isExpressionStatement(decl)) { + const expression = getMacroExpression(decl.expression) + if (expression) + return { + expression, + initializer: decl.expression, + isRequired: ts.isNonNullExpression(decl.expression), + } + } + } + + function getMacroExpression(node: import('typescript').Node) { + if (ts.isNonNullExpression(node)) { + node = node.expression + } + if (!ts.isCallExpression(node)) return + const expression = ts.isPropertyAccessExpression(node.expression) + ? node.expression + : node + return ( + ts.isIdentifier(expression.expression) && + [ + ...options.defineModel.alias, + ...options.defineSlots.alias, + ...options.defineStyle.alias, + ...options.defineExpose.alias, + ...options.defineComponent.alias, + ].includes(expression.expression.escapedText!) && + node + ) + } +} + +export function getRootMap(options: TransformOptions): RootMap { + const { ts, ast, source, codes } = options + const rootMap: RootMap = new Map() + + function walk( + node: import('typescript').Node, + parents: import('typescript').Node[], + ) { + ts.forEachChild(node, (child) => { + parents.unshift(node) + walk(child, parents) + parents.shift() + }) + + const root = + parents[1] && + (ts.isArrowFunction(parents[1]) || + ts.isFunctionExpression(parents[1]) || + ts.isFunctionDeclaration(parents[1])) + ? parents[1] + : undefined + + if ( + root && + parents[2] && + ts.isCallExpression(parents[2]) && + !parents[2].typeArguments && + options.defineComponent.alias.includes( + getText(parents[2].expression, options), + ) + ) { + if (!rootMap.has(root)) rootMap.set(root, {}) + if (!rootMap.get(root)!.defineComponent) { + rootMap.get(root)!.defineComponent = true + transformDefineComponent(parents[2], options) + } + } + + const macro = getMacro(node, ts, options) + if (!macro) return + + const { expression, initializer } = macro + let isRequired = macro.isRequired + if (!rootMap.has(root)) rootMap.set(root, {}) + const macroName = getText(expression.expression, options) + if (macroName.startsWith('defineStyle')) { + ;(rootMap.get(root)!.defineStyle ??= [])!.push({ + expression, + isCssModules: !!ts.isVariableStatement(node), + }) + } + + if (!root) return + + if (options.defineModel.alias.includes(macroName)) { + const modelName = + expression.arguments[0] && + ts.isStringLiteralLike(expression.arguments[0]) + ? expression.arguments[0].text + : 'modelValue' + const modelOptions = + expression.arguments[0] && + ts.isStringLiteralLike(expression.arguments[0]) + ? expression.arguments[1] + : expression.arguments[0] + if (modelOptions && ts.isObjectLiteralExpression(modelOptions)) { + let hasRequired = false + for (const prop of modelOptions.properties) { + if ( + ts.isPropertyAssignment(prop) && + getText(prop.name, options) === 'required' + ) { + hasRequired = true + isRequired = prop.initializer.kind === ts.SyntaxKind.TrueKeyword + } + } + + if (!hasRequired && isRequired) { + replaceSourceRange( + codes, + source, + modelOptions.end - 1, + modelOptions.end - 1, + `${!modelOptions.properties.hasTrailingComma && modelOptions.properties.length ? ',' : ''} required: true`, + ) + } + } else if (isRequired) { + replaceSourceRange( + codes, + source, + expression.arguments.end, + expression.arguments.end, + `${!expression.arguments.hasTrailingComma && expression.arguments.length ? ',' : ''} { required: true }`, + ) + } + + const id = toValidAssetId(modelName, `${HELPER_PREFIX}model` as any) + const typeString = `import('vue').UnwrapRef` + ;(rootMap.get(root)!.defineModel ??= [])!.push( + `${modelName.includes('-') ? `'${modelName}'` : modelName}${isRequired ? ':' : '?:'} ${typeString}`, + `'onUpdate:${modelName}'?: ($event: ${typeString}) => any`, + ) + replaceSourceRange( + codes, + source, + getStart(initializer, options), + getStart(initializer, options), + `// @ts-ignore\n${id};\nlet ${id} =`, + ) + } else if (options.defineSlots.alias.includes(macroName)) { + replaceSourceRange( + codes, + source, + getStart(expression, options), + getStart(expression, options), + `// @ts-ignore\n${HELPER_PREFIX}slots;\nconst ${HELPER_PREFIX}slots = `, + ) + rootMap.get(root)!.defineSlots = `Partial` + } else if (options.defineExpose.alias.includes(macroName)) { + replaceSourceRange( + codes, + source, + getStart(expression, options), + getStart(expression, options), + `// @ts-ignore\n${HELPER_PREFIX}exposed;\nconst ${HELPER_PREFIX}exposed = `, + ) + rootMap.get(root)!.defineExpose = `typeof ${HELPER_PREFIX}exposed` + } + } + + ts.forEachChild(ast, (node) => walk(node, [])) + return rootMap +} diff --git a/packages/volar/src/jsx-macros/transform.ts b/packages/volar/src/jsx-macros/transform.ts new file mode 100644 index 000000000..e40eb5021 --- /dev/null +++ b/packages/volar/src/jsx-macros/transform.ts @@ -0,0 +1,121 @@ +import { HELPER_PREFIX } from '@vue-macros/common' +import { replaceSourceRange } from 'muggle-string' +import { getStart, getText } from '../common' +import { transformDefineStyle } from './define-style' +import type { RootMap, TransformOptions } from '.' + +export function transformJsxMacros( + rootMap: RootMap, + options: TransformOptions, +): void { + const { ts, source, codes } = options + + for (const [root, map] of rootMap) { + transformDefineStyle(map.defineStyle, options) + + if (!root?.body) continue + + const asyncModifier = root.modifiers?.find( + (modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword, + ) + if (asyncModifier && map.defineComponent) + replaceSourceRange(codes, source, asyncModifier.pos, asyncModifier.end) + const result = `({}) as __VLS_PickNotAny>['render'], {}> & { __ctx: Awaited> }` + + const propsType = root.parameters[0]?.type + ? String(getText(root.parameters[0].type, options)) + : '{}' + replaceSourceRange( + codes, + source, + getStart(root.parameters, options), + getStart(root.parameters, options), + ts.isArrowFunction(root) && root.parameters.pos === root.pos ? '(' : '', + `${HELPER_PREFIX}props: Awaited>['props'] & ${propsType},`, + `${HELPER_PREFIX}placeholder?: {},`, + `${HELPER_PREFIX}setup = (${asyncModifier ? 'async' : ''}(`, + ) + if (ts.isArrowFunction(root)) { + replaceSourceRange( + codes, + source, + root.end, + root.end, + `))${root.pos === root.parameters.pos ? ')' : ''} => `, + result, + ) + } else { + replaceSourceRange( + codes, + source, + getStart(root.body, options), + getStart(root.body, options), + '=>', + ) + replaceSourceRange( + codes, + source, + root.end, + root.end, + `)){ return `, + result, + '}', + ) + } + + ts.forEachChild(root.body, (node) => { + if (ts.isReturnStatement(node) && node.expression) { + const props = [...(map.defineModel ?? [])] + const elements = + root.parameters[0] && + !root.parameters[0].type && + ts.isObjectBindingPattern(root.parameters[0].name) + ? root.parameters[0].name.elements + : [] + for (const element of elements) { + if (ts.isIdentifier(element.name)) + props.push( + `${element.name.escapedText}${ + element.initializer && + ts.isNonNullExpression(element.initializer) + ? ':' + : '?:' + } typeof ${element.name.escapedText}`, + ) + } + + const shouldWrapByCall = + (ts.isArrowFunction(node.expression) || + ts.isFunctionExpression(node.expression)) && + map.defineComponent + replaceSourceRange( + codes, + source, + getStart(node, options), + getStart(node.expression, options), + `return {\nprops: {} as { ${props.join(', ')} }`, + `,\nslots: {} as ${map.defineSlots ?? '{}'}`, + `,\nexpose: (exposed: ${ + options.lib === 'vue' + ? `import('vue').ShallowUnwrapRef` + : 'NonNullable' + }<${map.defineExpose ?? '{}'}>) => {}`, + `,\nrender: `, + shouldWrapByCall ? '(' : '', + ) + replaceSourceRange( + codes, + source, + node.expression.end, + node.expression.end, + shouldWrapByCall ? ')()' : '', + `\n}`, + ) + } + }) + } +} diff --git a/playground/vue3/src/App.vue b/playground/vue3/src/App.vue index b4a699be6..61d9379d9 100644 --- a/playground/vue3/src/App.vue +++ b/playground/vue3/src/App.vue @@ -19,6 +19,7 @@ import Full from './examples/full.setup' import HoistStatic from './examples/hoist-static/index.vue' import JsxDirective from './examples/jsx-directive/index.vue' +import JsxMacros from './examples/jsx-macros/index' import ReactivityTransform from './examples/reactivity-transform/index.vue' import ScriptLang from './examples/script-lang/index.vue' @@ -145,6 +146,11 @@ import ShortVmodel from './examples/short-vmodel/parent.vue' +
+ jsxMacros + +
+
booleanProp diff --git a/playground/vue3/src/examples/jsx-directive/v-on/child.vue b/playground/vue3/src/examples/jsx-directive/v-on/child.vue index 41d9c13b9..1ae8082c4 100644 --- a/playground/vue3/src/examples/jsx-directive/v-on/child.vue +++ b/playground/vue3/src/examples/jsx-directive/v-on/child.vue @@ -11,7 +11,7 @@ const emit = defineEmits<{ defineRender(() => (
emit('log', 1)}> - + )) diff --git a/playground/vue3/src/examples/jsx-macros/comp.tsx b/playground/vue3/src/examples/jsx-macros/comp.tsx new file mode 100644 index 000000000..4ca8abe40 --- /dev/null +++ b/playground/vue3/src/examples/jsx-macros/comp.tsx @@ -0,0 +1,46 @@ +import { defineComponent, ref, watch } from 'vue' + +export const Comp = defineComponent(function ({ foo }: { foo: T }) { + const slots = defineSlots({ + default: (props: { bar: string }) =>
{props.bar}
, + }) + + const [modelValue, modifiers] = defineModel({ + required: true, + get(value) { + if (modifiers.trim) return value.trim() + return value + }, + set(value) { + if (modifiers.trim) return value.trim() + return value + }, + }) + watch( + modelValue, + () => { + console.log(modelValue.value) + }, + { immediate: true }, + ) + + defineExpose({ + foo, + }) + + const color = ref('green') + defineStyle(` + .foo { + color: ${color.value}; + } + `) + + return () => ( +
+ color: +
+ + +
+ ) +}) diff --git a/playground/vue3/src/examples/jsx-macros/index.tsx b/playground/vue3/src/examples/jsx-macros/index.tsx new file mode 100644 index 000000000..c00e98a59 --- /dev/null +++ b/playground/vue3/src/examples/jsx-macros/index.tsx @@ -0,0 +1,26 @@ +import { expectTypeOf } from 'expect-type' +import { defineComponent, ref } from 'vue' +import { useRef } from 'vue-macros/runtime' +import { Comp } from './comp' + +export default defineComponent(() => { + const foo = ref('1') + const compRef = useRef() + expectTypeOf(compRef.value?.foo).toEqualTypeOf<1 | undefined>() + + return () => ( +
+ (compRef.value = e)} + v-model_trim={foo.value} + foo={1} + v-slot={{ bar }} + > + {bar} + + + + {foo.value} +
+ ) +}) diff --git a/playground/vue3/vue-macros.config.ts b/playground/vue3/vue-macros.config.ts index e1c913b1b..f168f66e2 100644 --- a/playground/vue3/vue-macros.config.ts +++ b/playground/vue3/vue-macros.config.ts @@ -18,12 +18,13 @@ export default defineConfig({ include: [/export-render.*\.vue$/, /\.setup\.tsx?$/], }, hoistStatic: true, + jsxMacros: true, jsxRef: true, namedTemplate: false, reactivityTransform: true, scriptLang: true, scriptSFC: { - include: [/script-sfc.*\.tsx$/], + include: [/vue3\/.*\.tsx$/], }, setupBlock: true, setupSFC: true, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 945f4df13..c795611c6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -66,6 +66,9 @@ catalogs: '@sxzz/test-utils': specifier: ^0.5.2 version: 0.5.2 + '@types/hash-sum': + specifier: ^1.0.2 + version: 1.0.2 '@types/node': specifier: ^22.13.10 version: 22.13.10 @@ -123,6 +126,9 @@ catalogs: cspell: specifier: ^8.17.5 version: 8.17.5 + csstype: + specifier: ^3.1.3 + version: 3.1.3 eslint: specifier: ^9.22.0 version: 9.22.0 @@ -135,6 +141,9 @@ catalogs: get-port: specifier: ^7.1.0 version: 7.1.0 + hash-sum: + specifier: ^2.0.0 + version: 2.0.0 jiti: specifier: ^2.4.2 version: 2.4.2 @@ -625,6 +634,9 @@ importers: '@vue-macros/jsx-directive': specifier: workspace:* version: link:../jsx-directive + '@vue-macros/jsx-macros': + specifier: workspace:* + version: link:../jsx-macros '@vue-macros/named-template': specifier: workspace:* version: link:../named-template @@ -876,6 +888,34 @@ importers: specifier: 'catalog:' version: 2.2.0 + packages/jsx: + dependencies: + '@vue-macros/common': + specifier: workspace:* + version: link:../common + '@vue-macros/jsx-directive': + specifier: workspace:* + version: link:../jsx-directive + '@vue-macros/jsx-macros': + specifier: workspace:* + version: link:../jsx-macros + '@vue-macros/volar': + specifier: workspace:* + version: link:../volar + ts-macro: + specifier: 'catalog:' + version: 0.1.23(typescript@5.8.2) + unplugin-combine: + specifier: 'catalog:' + version: 1.2.1(@rspack/core@1.2.8(@swc/helpers@0.5.15))(esbuild@0.25.1)(rolldown@1.0.0-beta.4(typescript@5.8.2))(rollup@4.35.0)(unplugin@2.2.0)(vite@6.2.1(@types/node@22.13.10)(jiti@2.4.2)(less@4.2.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0))(webpack@5.98.0(esbuild@0.25.1)) + devDependencies: + csstype: + specifier: 'catalog:' + version: 3.1.3 + vue: + specifier: 'catalog:' + version: 3.5.13(typescript@5.8.2) + packages/jsx-directive: dependencies: '@vue-macros/common': @@ -888,6 +928,28 @@ importers: specifier: 'catalog:' version: 3.5.13(typescript@5.8.2) + packages/jsx-macros: + dependencies: + '@vue-macros/common': + specifier: workspace:* + version: link:../common + '@vue/compiler-sfc': + specifier: 'catalog:' + version: 3.5.13 + hash-sum: + specifier: 'catalog:' + version: 2.0.0 + unplugin: + specifier: 'catalog:' + version: 2.2.0 + devDependencies: + '@types/hash-sum': + specifier: 'catalog:' + version: 1.0.2 + vue: + specifier: 'catalog:' + version: 3.5.13(typescript@5.8.2) + packages/macros: dependencies: '@vue-macros/better-define': @@ -947,6 +1009,9 @@ importers: '@vue-macros/jsx-directive': specifier: workspace:* version: link:../jsx-directive + '@vue-macros/jsx-macros': + specifier: workspace:* + version: link:../jsx-macros '@vue-macros/named-template': specifier: workspace:* version: link:../named-template @@ -1184,6 +1249,9 @@ importers: '@vue-macros/short-vmodel': specifier: workspace:* version: link:../short-vmodel + '@vue/compiler-dom': + specifier: 'catalog:' + version: 3.5.13 '@vue/language-core': specifier: 2.2.8 version: 2.2.8(typescript@5.8.2) @@ -1197,9 +1265,6 @@ importers: specifier: 'catalog:' version: 0.1.23(typescript@5.8.2) devDependencies: - '@vue/compiler-dom': - specifier: 'catalog:' - version: 3.5.13 typescript: specifier: 'catalog:' version: 5.8.2 @@ -3172,6 +3237,9 @@ packages: '@types/estree@1.0.6': resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} + '@types/hash-sum@1.0.2': + resolution: {integrity: sha512-UP28RddqY8xcU0SCEp9YKutQICXpaAq9N8U2klqF5hegGha7KzTOL8EdhIIV3bOSGBzjEpN9bU/d+nNZBdJYVw==} + '@types/hast@3.0.4': resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} @@ -5018,6 +5086,9 @@ packages: resolution: {integrity: sha512-Pq0h+hvsVm6dDEa8x82GnLSYHOzNDt7f0ddFa3FqcQlgzEiptPqL+XrOJNavjOzSYiYWIrgeVYYgGlLmnxwilQ==} engines: {node: '>=8'} + hash-sum@2.0.0: + resolution: {integrity: sha512-WdZTbAByD+pHfl/g9QSsBIIwy8IT+EsPiKDs0KNX+zSHhdDLFKdZu0BQHljvO+0QI/BasbMSUa8wYNCZTvhslg==} + hast-util-from-html@2.0.3: resolution: {integrity: sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==} @@ -9768,6 +9839,8 @@ snapshots: '@types/estree@1.0.6': {} + '@types/hash-sum@1.0.2': {} + '@types/hast@3.0.4': dependencies: '@types/unist': 3.0.3 @@ -12067,6 +12140,8 @@ snapshots: has-own-prop@2.0.0: {} + hash-sum@2.0.0: {} + hast-util-from-html@2.0.3: dependencies: '@types/hast': 3.0.4 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 4de0bb018..b7d44b7c7 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -15,8 +15,11 @@ defines: catalog: '@babel/parser': *babel '@babel/types': *babel + '@types/hash-sum': ^1.0.2 '@vue/language-core': *volar + csstype: ^3.1.3 + hash-sum: ^2.0.0 vue-tsc: *volar '@vitest/coverage-v8': *vitest diff --git a/tsconfig.fixture.json b/tsconfig.fixture.json index fa936647c..3ac377c96 100644 --- a/tsconfig.fixture.json +++ b/tsconfig.fixture.json @@ -24,6 +24,12 @@ }, "scriptLang": { "include": ["**/script-lang/**"] + }, + "jsxMacros": { + "include": ["**/jsx-macros/**"] + }, + "scriptSFC": { + "include": ["**/jsx-macros/**"] } } }