Skip to content

Commit

Permalink
feat: add jsx-directive plugin (#430)
Browse files Browse the repository at this point in the history
Co-authored-by: 三咲智子 Kevin Deng <sxzz@sxzz.moe>
  • Loading branch information
zhiyuanzmj and sxzz committed Jul 24, 2023
1 parent d1c2f52 commit d0d31ec
Show file tree
Hide file tree
Showing 33 changed files with 763 additions and 0 deletions.
6 changes: 6 additions & 0 deletions .changeset/brave-badgers-cheer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@vue-macros/jsx-directive': minor
'unplugin-vue-macros': minor
---

add jsx-directive plugin
4 changes: 4 additions & 0 deletions docs/.vitepress/locales/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,10 @@ export const sidebar = (lang: string): DefaultTheme.SidebarItem[] => {
text: 'exportExpose',
link: `${urlPrefix}/features/export-expose`,
},
{
text: 'jsxDirective',
link: `${urlPrefix}/features/jsx-directive`,
},
],
},
],
Expand Down
37 changes: 37 additions & 0 deletions docs/features/jsx-directive.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# jsxDirective

<StabilityLevel level="experimental" />

`v-if` and `v-for` directive for jsx.

| Features | Supported |
| :----------: | :----------------: |
| Vue 3 | :white_check_mark: |
| Nuxt 3 | :white_check_mark: |
| Vue 2 | :white_check_mark: |
| Volar(v-for) | :x: |

## Usage

```vue
<script setup lang="tsx">
const { foo, list } = defineProps<{
foo: number
list: number[]
}>()
defineRender(() => (
<>
<div v-if={foo === 0}>
<div v-if={foo === 0}>0-0</div>
<div v-else-if={foo === 1}>0-1</div>
<div v-else>0-2</div>
</div>
<div v-for={(i, index) in list} key={index}>
{i}
</div>
</>
))
</script>
```
37 changes: 37 additions & 0 deletions docs/zh-CN/features/jsx-directive.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# jsxDirective

<StabilityLevel level="experimental" />

`v-if` and `v-for` directive for jsx.

| Features | Supported |
| :----------: | :----------------: |
| Vue 3 | :white_check_mark: |
| Nuxt 3 | :white_check_mark: |
| Vue 2 | :white_check_mark: |
| Volar(v-for) | :x: |

## Usage

```vue
<script setup lang="tsx">
const { foo, list } = defineProps<{
foo: number
list: number[]
}>()
defineRender(() => (
<>
<div v-if={foo === 0}>
<div v-if={foo === 0}>0-0</div>
<div v-else-if={foo === 1}>0-1</div>
<div v-else>0-2</div>
</div>
<div v-for={(i, index) in list} key={index}>
{i}
</div>
</>
))
</script>
```
3 changes: 3 additions & 0 deletions packages/jsx-directive/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# @vue-macros/jsx-directive [![npm](https://img.shields.io/npm/v/@vue-macros/jsx-directive.svg)](https://npmjs.com/package/@vue-macros/jsx-directive)

Please refer to [README.md](https://github.com/sxzz/vue-macros#readme)
118 changes: 118 additions & 0 deletions packages/jsx-directive/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
{
"name": "@vue-macros/jsx-directive",
"version": "0.0.0",
"packageManager": "pnpm@8.6.7",
"description": "jsx-directive feature from Vue Macros.",
"keywords": [
"vue-macros",
"macros",
"vue",
"sfc",
"setup",
"script-setup",
"jsx-directive",
"unplugin"
],
"license": "MIT",
"homepage": "https://github.com/sxzz/vue-macros#readme",
"bugs": {
"url": "https://github.com/sxzz/vue-macros/issues"
},
"repository": {
"type": "git",
"url": "git+https://github.com/sxzz/vue-macros.git",
"directory": "packages/jsx-directive"
},
"author": "zhiyuanzmj",
"contributors": [
"三咲智子 <sxzz@sxzz.moe>"
],
"files": [
"dist"
],
"main": "dist/index.js",
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
"exports": {
".": {
"dev": "./src/index.ts",
"types": {
"require": "./dist/index.d.ts",
"import": "./dist/index.d.mts"
},
"require": "./dist/index.js",
"import": "./dist/index.mjs"
},
"./api": {
"dev": "./src/api.ts",
"types": {
"require": "./dist/api.d.ts",
"import": "./dist/api.d.mts"
},
"require": "./dist/api.js",
"import": "./dist/api.mjs"
},
"./esbuild": {
"dev": "./src/esbuild.ts",
"types": {
"require": "./dist/esbuild.d.ts",
"import": "./dist/esbuild.d.mts"
},
"require": "./dist/esbuild.js",
"import": "./dist/esbuild.mjs"
},
"./rollup": {
"dev": "./src/rollup.ts",
"types": {
"require": "./dist/rollup.d.ts",
"import": "./dist/rollup.d.mts"
},
"require": "./dist/rollup.js",
"import": "./dist/rollup.mjs"
},
"./vite": {
"dev": "./src/vite.ts",
"types": {
"require": "./dist/vite.d.ts",
"import": "./dist/vite.d.mts"
},
"require": "./dist/vite.js",
"import": "./dist/vite.mjs"
},
"./webpack": {
"dev": "./src/webpack.ts",
"types": {
"require": "./dist/webpack.d.ts",
"import": "./dist/webpack.d.mts"
},
"require": "./dist/webpack.js",
"import": "./dist/webpack.mjs"
},
"./*": [
"./*",
"./*.d.ts"
]
},
"typesVersions": {
"*": {
"*": [
"./dist/*",
"./*"
]
}
},
"scripts": {
"build": "tsup && tsx ../../scripts/postbuild.ts",
"dev": "DEV=true tsup"
},
"dependencies": {
"@vue-macros/common": "workspace:~",
"unplugin": "^1.3.1"
},
"devDependencies": {
"vue": "^3.3.4"
},
"engines": {
"node": ">=16.14.0"
}
}
1 change: 1 addition & 0 deletions packages/jsx-directive/src/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './core'
42 changes: 42 additions & 0 deletions packages/jsx-directive/src/core/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { type Program } from '@babel/types'
import {
MagicString,
babelParse,
getLang,
getTransformResult,
parseSFC,
} from '@vue-macros/common'
import { vIfTransform } from './v-if'
import { vForTransform } from './v-for'

export function transformJsxVueDirective(code: string, id: string) {
const lang = getLang(id)
let asts: {
ast: Program
offset: number
}[] = []
if (lang === 'vue') {
const { scriptSetup, getSetupAst, script, getScriptAst } = parseSFC(
code,
id
)
if (script) {
asts.push({ ast: getScriptAst()!, offset: script.loc.start.offset })
}
if (scriptSetup) {
asts.push({ ast: getSetupAst()!, offset: scriptSetup.loc.start.offset })
}
} else if (['jsx', 'tsx'].includes(lang)) {
asts = [{ ast: babelParse(code, lang), offset: 0 }]
} else {
return
}

const s = new MagicString(code)
for (const { ast, offset } of asts) {
vIfTransform(ast, s, offset)
vForTransform(ast, s, offset)
}

return getTransformResult(s, id)
}
50 changes: 50 additions & 0 deletions packages/jsx-directive/src/core/v-for.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import {
type JSXAttribute,
type JSXElement,
type Node,
type Program,
} from '@babel/types'
import { type MagicString, walkAST } from '@vue-macros/common'

export function vForTransform(ast: Program, s: MagicString, offset = 0) {
if (!s.sliceNode(ast, { offset }).includes('v-for')) return

const nodes: {
node: JSXElement
attribute: JSXAttribute
}[] = []

walkAST<Node>(ast, {
enter(node) {
if (node.type !== 'JSXElement') return

const attribute = node.openingElement.attributes.find(
(i): i is JSXAttribute =>
i.type === 'JSXAttribute' && ['v-for'].includes(`${i.name.name}`)
)
if (attribute) {
nodes.push({
node,
attribute,
})
}
},
})

nodes.forEach(({ node, attribute }) => {
if (`${attribute.name.name}` === 'v-for') {
if (!attribute.value) return
const [i, list] = s
.slice(
attribute.value.start! + offset + 1,
attribute.value.end! + offset - 1
)
.split(/\s+in\s+/)

s.appendLeft(node.start! + offset, ` { ${list}.map(${i}=> `)

s.appendRight(node.end! + offset, ') }')
s.remove(attribute.start! + offset - 1, attribute.end! + offset)
}
})
}
63 changes: 63 additions & 0 deletions packages/jsx-directive/src/core/v-if.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import {
type JSXAttribute,
type JSXElement,
type Node,
type Program,
} from '@babel/types'
import { type MagicString, walkAST } from '@vue-macros/common'

export function vIfTransform(ast: Program, s: MagicString, offset = 0) {
if (!s.sliceNode(ast, { offset }).includes('v-if')) return

const nodeMap = new Map<
Node,
{
node: JSXElement
attribute: JSXAttribute
}[]
>()

walkAST<Node>(ast, {
enter(node, parent) {
if (node.type !== 'JSXElement') return

const attribute = node.openingElement.attributes.find(
(i) =>
i.type === 'JSXAttribute' &&
['v-if', 'v-else-if', 'v-else'].includes(`${i.name.name}`)
) as JSXAttribute
if (attribute) {
if (!nodeMap.has(parent!)) nodeMap.set(parent!, [])

nodeMap.get(parent!)?.push({
node,
attribute,
})
}
},
})

const nodes = [...nodeMap.values()].flat()
nodes.forEach(({ node, attribute }, index) => {
if (['v-if', 'v-else-if'].includes(`${attribute.name.name}`)) {
if (attribute.value)
s.appendLeft(
node.start! + offset,
`${attribute.name.name === 'v-if' ? '{ ' : ''}${s.slice(
attribute.value.start! + offset + 1,
attribute.value.end! + offset - 1
)} ? `
)

s.appendRight(
node.end! + offset,
`${nodes[index + 1]?.attribute.name.name}`.startsWith('v-else')
? ' :'
: " : '' }"
)
s.remove(attribute.start! + offset - 1, attribute.end! + offset)
} else {
s.appendRight(node.end! + offset, ' }')
}
})
}
3 changes: 3 additions & 0 deletions packages/jsx-directive/src/esbuild.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import unplugin from '.'

export default unplugin.esbuild

0 comments on commit d0d31ec

Please sign in to comment.