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 @@ + + + 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}