diff --git a/.changeset/honest-melons-walk.md b/.changeset/honest-melons-walk.md
new file mode 100644
index 000000000000..36226b4aa432
--- /dev/null
+++ b/.changeset/honest-melons-walk.md
@@ -0,0 +1,6 @@
+---
+'@astrojs/vue': minor
+'astro': patch
+---
+
+Support Vue JSX
diff --git a/packages/astro/src/runtime/server/render/component.ts b/packages/astro/src/runtime/server/render/component.ts
index e6aad1698dfb..7a82119aebc6 100644
--- a/packages/astro/src/runtime/server/render/component.ts
+++ b/packages/astro/src/runtime/server/render/component.ts
@@ -27,7 +27,7 @@ function guessRenderers(componentUrl?: string): string[] {
return ['@astrojs/vue'];
case 'jsx':
case 'tsx':
- return ['@astrojs/react', '@astrojs/preact'];
+ return ['@astrojs/react', '@astrojs/preact', '@astrojs/vue (jsx)'];
default:
return ['@astrojs/react', '@astrojs/preact', '@astrojs/vue', '@astrojs/svelte'];
}
diff --git a/packages/astro/test/fixtures/vue-jsx/astro.config.mjs b/packages/astro/test/fixtures/vue-jsx/astro.config.mjs
new file mode 100644
index 000000000000..ffd9016c29c3
--- /dev/null
+++ b/packages/astro/test/fixtures/vue-jsx/astro.config.mjs
@@ -0,0 +1,7 @@
+import { defineConfig } from 'astro/config';
+import vue from '@astrojs/vue';
+
+// https://astro.build/config
+export default defineConfig({
+ integrations: [vue({ jsx: true })],
+});
\ No newline at end of file
diff --git a/packages/astro/test/fixtures/vue-jsx/package.json b/packages/astro/test/fixtures/vue-jsx/package.json
new file mode 100644
index 000000000000..8aaa1991b590
--- /dev/null
+++ b/packages/astro/test/fixtures/vue-jsx/package.json
@@ -0,0 +1,10 @@
+{
+ "name": "@test/vue-jsx",
+ "version": "0.0.0",
+ "private": true,
+ "dependencies": {
+ "@astrojs/vue": "workspace:*",
+ "astro": "workspace:*",
+ "vue": "^3.2.39"
+ }
+}
diff --git a/packages/astro/test/fixtures/vue-jsx/src/components/Counter.jsx b/packages/astro/test/fixtures/vue-jsx/src/components/Counter.jsx
new file mode 100644
index 000000000000..da193a7b9c94
--- /dev/null
+++ b/packages/astro/test/fixtures/vue-jsx/src/components/Counter.jsx
@@ -0,0 +1,28 @@
+import { defineComponent, ref } from 'vue';
+
+export default defineComponent({
+ props: {
+ start: {
+ type: String,
+ required: true
+ },
+ stepSize: {
+ type: String,
+ default: "1"
+ }
+ },
+ setup(props) {
+ const count = ref(parseInt(props.start))
+ const stepSize = ref(parseInt(props.stepSize))
+ const add = () => (count.value = count.value + stepSize.value);
+ const subtract = () => (count.value = count.value - stepSize.value);
+ return () => (
+
+
+
+
{count.value}
+
+
+ )
+ },
+})
diff --git a/packages/astro/test/fixtures/vue-jsx/src/components/Result.vue b/packages/astro/test/fixtures/vue-jsx/src/components/Result.vue
new file mode 100644
index 000000000000..7795d5ae0d01
--- /dev/null
+++ b/packages/astro/test/fixtures/vue-jsx/src/components/Result.vue
@@ -0,0 +1,15 @@
+
+ {{ value }}
+
+
+
diff --git a/packages/astro/test/fixtures/vue-jsx/src/pages/index.astro b/packages/astro/test/fixtures/vue-jsx/src/pages/index.astro
new file mode 100644
index 000000000000..836d81f7b0dd
--- /dev/null
+++ b/packages/astro/test/fixtures/vue-jsx/src/pages/index.astro
@@ -0,0 +1,35 @@
+---
+import Counter from '../components/Counter.jsx'
+import Result from '../components/Result.vue'
+---
+
+
+
+
+ Vue component
+
+
+
+
+
+ SSR Rendered, No Client
+ SSR Rendered, client:load
+
+ SSR Rendered, client:load
+
+ SSR Rendered, client:load
+ SSR Rendered, client:idle
+
+ SSR Rendered, client:visible
+ SSR Rendered, client:visible
+
+
+
diff --git a/packages/astro/test/vue-jsx.test.js b/packages/astro/test/vue-jsx.test.js
new file mode 100644
index 000000000000..9307410fe8f5
--- /dev/null
+++ b/packages/astro/test/vue-jsx.test.js
@@ -0,0 +1,30 @@
+import { expect } from 'chai';
+import * as cheerio from 'cheerio';
+import { loadFixture } from './test-utils.js';
+
+describe('Vue JSX', () => {
+ let fixture;
+
+ before(async () => {
+ fixture = await loadFixture({
+ root: './fixtures/vue-jsx/',
+ });
+ });
+
+ describe('build', () => {
+ before(async () => {
+ await fixture.build();
+ });
+
+ it('Can load Vue JSX', async () => {
+ const html = await fixture.readFile('/index.html');
+ const $ = cheerio.load(html);
+
+ const allPreValues = $('pre')
+ .toArray()
+ .map((el) => $(el).text());
+
+ expect(allPreValues).to.deep.equal(['2345', '0', '1', '1', '1', '10', '100', '1000']);
+ });
+ });
+});
diff --git a/packages/integrations/vue/README.md b/packages/integrations/vue/README.md
index 2a1f94c6f509..75c861ce3a15 100644
--- a/packages/integrations/vue/README.md
+++ b/packages/integrations/vue/README.md
@@ -94,3 +94,40 @@ export default {
})],
}
```
+
+### jsx
+
+You can use Vue JSX by setting `jsx: true`.
+
+__`astro.config.mjs`__
+
+```js
+import { defineConfig } from 'astro/config';
+import vue from '@astrojs/vue';
+
+export default defineConfig({
+ integrations: [
+ vue({ jsx: true })
+ ],
+});
+```
+
+This will enable rendering for both Vue and Vue JSX components. To customize the Vue JSX compiler, pass an options object instead of a boolean. See the `@vitejs/plugin-vue-jsx` [docs](https://github.com/vitejs/vite/tree/main/packages/plugin-vue-jsx) for more details.
+
+__`astro.config.mjs`__
+
+```js
+import { defineConfig } from 'astro/config';
+import vue from '@astrojs/vue';
+
+export default defineConfig({
+ integrations: [
+ vue({
+ jsx: {
+ // treat any tag that starts with ion- as custom elements
+ isCustomElement: tag => tag.startsWith('ion-')
+ }
+ })
+ ],
+});
+```
diff --git a/packages/integrations/vue/package.json b/packages/integrations/vue/package.json
index efb18ca12300..707747f75306 100644
--- a/packages/integrations/vue/package.json
+++ b/packages/integrations/vue/package.json
@@ -34,6 +34,8 @@
},
"dependencies": {
"@vitejs/plugin-vue": "^3.0.0",
+ "@vitejs/plugin-vue-jsx": "^2.0.1",
+ "@vue/babel-plugin-jsx": "^1.1.1",
"@vue/compiler-sfc": "^3.2.39"
},
"devDependencies": {
diff --git a/packages/integrations/vue/src/index.ts b/packages/integrations/vue/src/index.ts
index 24df127d01a3..6ab63562ebd9 100644
--- a/packages/integrations/vue/src/index.ts
+++ b/packages/integrations/vue/src/index.ts
@@ -1,8 +1,13 @@
-import type { Options } from '@vitejs/plugin-vue';
+import type { Options as VueOptions } from '@vitejs/plugin-vue';
+import type { Options as VueJsxOptions } from '@vitejs/plugin-vue-jsx';
import vue from '@vitejs/plugin-vue';
import type { AstroIntegration, AstroRenderer } from 'astro';
import type { UserConfig } from 'vite';
+interface Options extends VueOptions {
+ jsx?: boolean | VueJsxOptions;
+}
+
function getRenderer(): AstroRenderer {
return {
name: '@astrojs/vue',
@@ -11,8 +16,23 @@ function getRenderer(): AstroRenderer {
};
}
-function getViteConfiguration(options?: Options): UserConfig {
+function getJsxRenderer(): AstroRenderer {
return {
+ name: '@astrojs/vue (jsx)',
+ clientEntrypoint: '@astrojs/vue/client.js',
+ serverEntrypoint: '@astrojs/vue/server.js',
+ jsxImportSource: 'vue',
+ jsxTransformOptions: async () => {
+ const jsxPlugin = (await import('@vue/babel-plugin-jsx')).default;
+ return {
+ plugins: [jsxPlugin],
+ };
+ },
+ };
+}
+
+async function getViteConfiguration(options?: Options): Promise {
+ const config: UserConfig = {
optimizeDeps: {
include: ['@astrojs/vue/client.js', 'vue'],
exclude: ['@astrojs/vue/server.js'],
@@ -23,15 +43,26 @@ function getViteConfiguration(options?: Options): UserConfig {
noExternal: ['vueperslides'],
},
};
+
+ if (options?.jsx) {
+ const vueJsx = (await import('@vitejs/plugin-vue-jsx')).default;
+ const jsxOptions = typeof options.jsx === 'object' ? options.jsx : undefined;
+ config.plugins?.push(vueJsx(jsxOptions));
+ }
+
+ return config;
}
export default function (options?: Options): AstroIntegration {
return {
name: '@astrojs/vue',
hooks: {
- 'astro:config:setup': ({ addRenderer, updateConfig }) => {
+ 'astro:config:setup': async ({ addRenderer, updateConfig }) => {
addRenderer(getRenderer());
- updateConfig({ vite: getViteConfiguration(options) });
+ if (options?.jsx) {
+ addRenderer(getJsxRenderer());
+ }
+ updateConfig({ vite: await getViteConfiguration(options) });
},
},
};
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 67b08ecc1fd3..a217091d8e69 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -2319,6 +2319,16 @@ importers:
astro: link:../../..
vue: 3.2.39
+ packages/astro/test/fixtures/vue-jsx:
+ specifiers:
+ '@astrojs/vue': workspace:*
+ astro: workspace:*
+ vue: ^3.2.39
+ dependencies:
+ '@astrojs/vue': link:../../../../integrations/vue
+ astro: link:../../..
+ vue: 3.2.39
+
packages/astro/test/fixtures/vue-with-multi-renderer:
specifiers:
'@astrojs/svelte': workspace:*
@@ -3044,6 +3054,8 @@ importers:
packages/integrations/vue:
specifiers:
'@vitejs/plugin-vue': ^3.0.0
+ '@vitejs/plugin-vue-jsx': ^2.0.1
+ '@vue/babel-plugin-jsx': ^1.1.1
'@vue/compiler-sfc': ^3.2.39
astro: workspace:*
astro-scripts: workspace:*
@@ -3051,6 +3063,8 @@ importers:
vue: ^3.2.37
dependencies:
'@vitejs/plugin-vue': 3.1.0_vite@3.1.3+vue@3.2.39
+ '@vitejs/plugin-vue-jsx': 2.0.1_vite@3.1.3+vue@3.2.39
+ '@vue/babel-plugin-jsx': 1.1.1
'@vue/compiler-sfc': 3.2.39
devDependencies:
astro: link:../../astro
@@ -4319,6 +4333,18 @@ packages:
'@babel/helper-plugin-utils': 7.19.0
dev: false
+ /@babel/plugin-syntax-import-meta/7.10.4_@babel+core@7.19.1:
+ resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ peerDependenciesMeta:
+ '@babel/core':
+ optional: true
+ dependencies:
+ '@babel/core': 7.19.1
+ '@babel/helper-plugin-utils': 7.19.0
+ dev: false
+
/@babel/plugin-syntax-json-strings/7.8.3_@babel+core@7.19.1:
resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==}
peerDependencies:
@@ -4454,6 +4480,19 @@ packages:
'@babel/helper-plugin-utils': 7.19.0
dev: false
+ /@babel/plugin-syntax-typescript/7.18.6_@babel+core@7.19.1:
+ resolution: {integrity: sha512-mAWAuq4rvOepWCBid55JuRNvpTNf2UGVgoz4JV0fXEKolsVZDzsa4NqCef758WZJj/GDu0gVGItjKFiClTAmZA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ peerDependenciesMeta:
+ '@babel/core':
+ optional: true
+ dependencies:
+ '@babel/core': 7.19.1
+ '@babel/helper-plugin-utils': 7.19.0
+ dev: false
+
/@babel/plugin-transform-arrow-functions/7.18.6_@babel+core@7.19.1:
resolution: {integrity: sha512-9S9X9RUefzrsHZmKMbDXxweEH+YlE8JJEuat9FdvW9Qh1cw7W64jELCtWNkPBPX5En45uy28KGvA/AySqUh8CQ==}
engines: {node: '>=6.9.0'}
@@ -4903,6 +4942,23 @@ packages:
'@babel/helper-plugin-utils': 7.19.0
dev: false
+ /@babel/plugin-transform-typescript/7.19.1_@babel+core@7.19.1:
+ resolution: {integrity: sha512-+ILcOU+6mWLlvCwnL920m2Ow3wWx3Wo8n2t5aROQmV55GZt+hOiLvBaa3DNzRjSEHa1aauRs4/YLmkCfFkhhRQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ peerDependenciesMeta:
+ '@babel/core':
+ optional: true
+ dependencies:
+ '@babel/core': 7.19.1
+ '@babel/helper-create-class-features-plugin': 7.19.0_@babel+core@7.19.1
+ '@babel/helper-plugin-utils': 7.19.0
+ '@babel/plugin-syntax-typescript': 7.18.6_@babel+core@7.19.1
+ transitivePeerDependencies:
+ - supports-color
+ dev: false
+
/@babel/plugin-transform-unicode-escapes/7.18.10_@babel+core@7.19.1:
resolution: {integrity: sha512-kKAdAI+YzPgGY/ftStBFXTI1LZFju38rYThnfMykS+IXy8BVx+res7s2fxf1l8I35DV2T97ezo6+SGrXz6B3iQ==}
engines: {node: '>=6.9.0'}
@@ -9868,6 +9924,26 @@ packages:
- supports-color
dev: false
+ /@vitejs/plugin-vue-jsx/2.0.1_vite@3.1.3+vue@3.2.39:
+ resolution: {integrity: sha512-lmiR1k9+lrF7LMczO0pxtQ8mOn6XeppJDHxnpxkJQpT5SiKz4SKhKdeNstXaTNuR8qZhUo5X0pJlcocn72Y4Jg==}
+ engines: {node: ^14.18.0 || >=16.0.0}
+ peerDependencies:
+ vite: ^3.0.0
+ vue: ^3.0.0
+ peerDependenciesMeta:
+ vite:
+ optional: true
+ dependencies:
+ '@babel/core': 7.19.1
+ '@babel/plugin-syntax-import-meta': 7.10.4_@babel+core@7.19.1
+ '@babel/plugin-transform-typescript': 7.19.1_@babel+core@7.19.1
+ '@vue/babel-plugin-jsx': 1.1.1_@babel+core@7.19.1
+ vite: 3.1.3
+ vue: 3.2.39
+ transitivePeerDependencies:
+ - supports-color
+ dev: false
+
/@vitejs/plugin-vue/3.1.0_vite@3.1.3+vue@3.2.39:
resolution: {integrity: sha512-fmxtHPjSOEIRg6vHYDaem+97iwCUg/uSIaTzp98lhELt2ISOQuDo2hbkBdXod0g15IhfPMQmAxh4heUks2zvDA==}
engines: {node: ^14.18.0 || >=16.0.0}
@@ -9893,6 +9969,44 @@ packages:
vscode-uri: 2.1.2
dev: false
+ /@vue/babel-helper-vue-transform-on/1.0.2:
+ resolution: {integrity: sha512-hz4R8tS5jMn8lDq6iD+yWL6XNB699pGIVLk7WSJnn1dbpjaazsjZQkieJoRX6gW5zpYSCFqQ7jUquPNY65tQYA==}
+ dev: false
+
+ /@vue/babel-plugin-jsx/1.1.1:
+ resolution: {integrity: sha512-j2uVfZjnB5+zkcbc/zsOc0fSNGCMMjaEXP52wdwdIfn0qjFfEYpYZBFKFg+HHnQeJCVrjOeO0YxgaL7DMrym9w==}
+ dependencies:
+ '@babel/helper-module-imports': 7.18.6
+ '@babel/plugin-syntax-jsx': 7.18.6
+ '@babel/template': 7.18.10
+ '@babel/traverse': 7.19.1
+ '@babel/types': 7.19.0
+ '@vue/babel-helper-vue-transform-on': 1.0.2
+ camelcase: 6.3.0
+ html-tags: 3.2.0
+ svg-tags: 1.0.0
+ transitivePeerDependencies:
+ - '@babel/core'
+ - supports-color
+ dev: false
+
+ /@vue/babel-plugin-jsx/1.1.1_@babel+core@7.19.1:
+ resolution: {integrity: sha512-j2uVfZjnB5+zkcbc/zsOc0fSNGCMMjaEXP52wdwdIfn0qjFfEYpYZBFKFg+HHnQeJCVrjOeO0YxgaL7DMrym9w==}
+ dependencies:
+ '@babel/helper-module-imports': 7.18.6
+ '@babel/plugin-syntax-jsx': 7.18.6_@babel+core@7.19.1
+ '@babel/template': 7.18.10
+ '@babel/traverse': 7.19.1
+ '@babel/types': 7.19.0
+ '@vue/babel-helper-vue-transform-on': 1.0.2
+ camelcase: 6.3.0
+ html-tags: 3.2.0
+ svg-tags: 1.0.0
+ transitivePeerDependencies:
+ - '@babel/core'
+ - supports-color
+ dev: false
+
/@vue/compiler-core/3.2.39:
resolution: {integrity: sha512-mf/36OWXqWn0wsC40nwRRGheR/qoID+lZXbIuLnr4/AngM0ov8Xvv8GHunC0rKRIkh60bTqydlqTeBo49rlbqw==}
dependencies:
@@ -12988,6 +13102,11 @@ packages:
resolution: {integrity: sha512-lNovG8CMCCmcVB1Q7xggMSf7tqPCijZXaH4gL6iE8BFghdQCbaY5Met9i1x2Ex8m/cZHDUtXK9H6/znKamRP8Q==}
dev: true
+ /html-tags/3.2.0:
+ resolution: {integrity: sha512-vy7ClnArOZwCnqZgvv+ddgHgJiAFXe3Ge9ML5/mBctVJoUoYPCdxVucOywjDARn6CVoh3dRSFdPHy2sX80L0Wg==}
+ engines: {node: '>=8'}
+ dev: false
+
/html-void-elements/2.0.1:
resolution: {integrity: sha512-0quDb7s97CfemeJAnW9wC0hw78MtW7NU3hqtCD75g2vFlDLt36llsYD7uB7SUzojLMP24N5IatXf7ylGXiGG9A==}
dev: false
@@ -16974,6 +17093,10 @@ packages:
svelte: 3.50.1
dev: false
+ /svg-tags/1.0.0:
+ resolution: {integrity: sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==}
+ dev: false
+
/synckit/0.7.3:
resolution: {integrity: sha512-jNroMv7Juy+mJ/CHW5H6TzsLWpa1qck6sCHbkv8YTur+irSq2PjbvmGnm2gy14BUQ6jF33vyR4DPssHqmqsDQw==}
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}