From 4d4e34d451e351f8a52e04124027850a575ba752 Mon Sep 17 00:00:00 2001 From: Amumu <yoyo837@hotmail.com> Date: Wed, 6 Dec 2023 04:07:28 +0800 Subject: [PATCH] Improve Vue `appEntrypoint` handling (#8794) Co-authored-by: Nate Moore <natemoo-re@users.noreply.github.com> Co-authored-by: Arsh <69170106+lilnasy@users.noreply.github.com> Co-authored-by: Florian LEFEBVRE <contact@florian-lefebvre.dev> Co-authored-by: Nate Moore <nate@astro.build> --- .changeset/smart-cameras-kneel.md | 5 ++ packages/integrations/vue/src/index.ts | 57 +++++++++++---- .../vue/test/app-entrypoint.test.js | 70 +++++++++++++++++++ .../astro.config.mjs | 14 ++++ .../package.json | 13 ++++ .../src/components/Bar.vue | 3 + .../src/components/Circle.svg | 1 + .../src/components/Foo.vue | 11 +++ .../src/pages/_app.ts | 3 + .../src/pages/index.astro | 12 ++++ .../app-entrypoint-relative/astro.config.mjs | 8 +++ .../app-entrypoint-relative/package.json | 12 ++++ .../src/components/Bar.vue | 3 + .../src/components/Circle.svg | 1 + .../src/components/Foo.vue | 11 +++ .../src/pages/index.astro | 12 ++++ .../app-entrypoint-relative/src/vue.ts | 1 + pnpm-lock.yaml | 33 +++++++++ 18 files changed, 257 insertions(+), 13 deletions(-) create mode 100644 .changeset/smart-cameras-kneel.md create mode 100644 packages/integrations/vue/test/fixtures/app-entrypoint-no-export-default/astro.config.mjs create mode 100644 packages/integrations/vue/test/fixtures/app-entrypoint-no-export-default/package.json create mode 100644 packages/integrations/vue/test/fixtures/app-entrypoint-no-export-default/src/components/Bar.vue create mode 100644 packages/integrations/vue/test/fixtures/app-entrypoint-no-export-default/src/components/Circle.svg create mode 100644 packages/integrations/vue/test/fixtures/app-entrypoint-no-export-default/src/components/Foo.vue create mode 100644 packages/integrations/vue/test/fixtures/app-entrypoint-no-export-default/src/pages/_app.ts create mode 100644 packages/integrations/vue/test/fixtures/app-entrypoint-no-export-default/src/pages/index.astro create mode 100644 packages/integrations/vue/test/fixtures/app-entrypoint-relative/astro.config.mjs create mode 100644 packages/integrations/vue/test/fixtures/app-entrypoint-relative/package.json create mode 100644 packages/integrations/vue/test/fixtures/app-entrypoint-relative/src/components/Bar.vue create mode 100644 packages/integrations/vue/test/fixtures/app-entrypoint-relative/src/components/Circle.svg create mode 100644 packages/integrations/vue/test/fixtures/app-entrypoint-relative/src/components/Foo.vue create mode 100644 packages/integrations/vue/test/fixtures/app-entrypoint-relative/src/pages/index.astro create mode 100644 packages/integrations/vue/test/fixtures/app-entrypoint-relative/src/vue.ts diff --git a/.changeset/smart-cameras-kneel.md b/.changeset/smart-cameras-kneel.md new file mode 100644 index 0000000000..d5cb0264fb --- /dev/null +++ b/.changeset/smart-cameras-kneel.md @@ -0,0 +1,5 @@ +--- +'@astrojs/vue': patch +--- + +Prevents Astro from crashing when no default function is exported from the `appEntrypoint`. Now, the entrypoint will be ignored with a warning instead. diff --git a/packages/integrations/vue/src/index.ts b/packages/integrations/vue/src/index.ts index 2c234952b6..c0fda26606 100644 --- a/packages/integrations/vue/src/index.ts +++ b/packages/integrations/vue/src/index.ts @@ -1,14 +1,21 @@ import type { Options as VueOptions } from '@vitejs/plugin-vue'; -import vue from '@vitejs/plugin-vue'; import type { Options as VueJsxOptions } from '@vitejs/plugin-vue-jsx'; -import type { AstroIntegration, AstroRenderer } from 'astro'; -import type { UserConfig } from 'vite'; +import type { AstroIntegration, AstroIntegrationLogger, AstroRenderer } from 'astro'; +import type { UserConfig, Rollup } from 'vite'; + +import { fileURLToPath } from 'node:url'; +import vue from '@vitejs/plugin-vue'; interface Options extends VueOptions { jsx?: boolean | VueJsxOptions; appEntrypoint?: string; } +interface ViteOptions extends Options { + root: URL; + logger: AstroIntegrationLogger; +} + function getRenderer(): AstroRenderer { return { name: '@astrojs/vue', @@ -32,7 +39,7 @@ function getJsxRenderer(): AstroRenderer { }; } -function virtualAppEntrypoint(options?: Options) { +function virtualAppEntrypoint(options: ViteOptions) { const virtualModuleId = 'virtual:@astrojs/vue/app'; const resolvedVirtualModuleId = '\0' + virtualModuleId; return { @@ -42,18 +49,40 @@ function virtualAppEntrypoint(options?: Options) { return resolvedVirtualModuleId; } }, - load(id: string) { + async load(id: string) { + const noop = `export const setup = () => {}`; if (id === resolvedVirtualModuleId) { - if (options?.appEntrypoint) { - return `export { default as setup } from "${options.appEntrypoint}";`; + if (options.appEntrypoint) { + try { + let resolved; + if (options.appEntrypoint.startsWith('.')) { + resolved = await this.resolve(fileURLToPath(new URL(options.appEntrypoint, options.root))); + } else { + resolved = await this.resolve(options.appEntrypoint, fileURLToPath(options.root)); + } + if (!resolved) { + // This error is handled below, the message isn't shown to the user + throw new Error('Unable to resolve appEntrypoint'); + } + const loaded = await this.load(resolved); + if (!loaded.hasDefaultExport) { + options.logger.warn( + `appEntrypoint \`${options.appEntrypoint}\` does not export a default function. Check out https://docs.astro.build/en/guides/integrations-guide/vue/#appentrypoint.` + ); + return noop; + } + return `export { default as setup } from "${resolved.id}";`; + } catch { + options.logger.warn(`Unable to resolve appEntrypoint \`${options.appEntrypoint}\`. Does the file exist?`); + } } - return `export const setup = () => {};`; + return noop; } - }, - }; + } + } satisfies Rollup.Plugin; } -async function getViteConfiguration(options?: Options): Promise<UserConfig> { +async function getViteConfiguration(options: ViteOptions): Promise<UserConfig> { const config: UserConfig = { optimizeDeps: { include: ['@astrojs/vue/client.js', 'vue'], @@ -79,12 +108,14 @@ export default function (options?: Options): AstroIntegration { return { name: '@astrojs/vue', hooks: { - 'astro:config:setup': async ({ addRenderer, updateConfig }) => { + 'astro:config:setup': async ({ addRenderer, updateConfig, config, logger }) => { addRenderer(getRenderer()); if (options?.jsx) { addRenderer(getJsxRenderer()); } - updateConfig({ vite: await getViteConfiguration(options) }); + updateConfig({ + vite: await getViteConfiguration({ ...options, root: config.root, logger }), + }); }, }, }; diff --git a/packages/integrations/vue/test/app-entrypoint.test.js b/packages/integrations/vue/test/app-entrypoint.test.js index b20e7be7e3..308f10149f 100644 --- a/packages/integrations/vue/test/app-entrypoint.test.js +++ b/packages/integrations/vue/test/app-entrypoint.test.js @@ -51,3 +51,73 @@ describe('App Entrypoint', () => { expect(client).not.to.be.undefined; }); }); + +describe('App Entrypoint no export default', () => { + /** @type {import('./test-utils').Fixture} */ + let fixture; + + before(async () => { + fixture = await loadFixture({ + root: './fixtures/app-entrypoint-no-export-default/', + }); + await fixture.build(); + }); + + it('loads during SSR', async () => { + const data = await fixture.readFile('/index.html'); + const { document } = parseHTML(data); + const bar = document.querySelector('#foo > #bar'); + expect(bar).not.to.be.undefined; + expect(bar.textContent).to.eq('works'); + }); + + it('component not included in renderer bundle', async () => { + const data = await fixture.readFile('/index.html'); + const { document } = parseHTML(data); + const island = document.querySelector('astro-island'); + const client = island.getAttribute('renderer-url'); + expect(client).not.to.be.undefined; + + const js = await fixture.readFile(client); + expect(js).not.to.match(/\w+\.component\(\"Bar\"/gm); + }); + + it('loads svg components without transforming them to assets', async () => { + const data = await fixture.readFile('/index.html'); + const { document } = parseHTML(data); + const client = document.querySelector('astro-island svg'); + + expect(client).not.to.be.undefined; + }); +}); + +describe('App Entrypoint relative', () => { + /** @type {import('./test-utils').Fixture} */ + let fixture; + + before(async () => { + fixture = await loadFixture({ + root: './fixtures/app-entrypoint-relative/', + }); + await fixture.build(); + }); + + it('loads during SSR', async () => { + const data = await fixture.readFile('/index.html'); + const { document } = parseHTML(data); + const bar = document.querySelector('#foo > #bar'); + expect(bar).not.to.be.undefined; + expect(bar.textContent).to.eq('works'); + }); + + it('component not included in renderer bundle', async () => { + const data = await fixture.readFile('/index.html'); + const { document } = parseHTML(data); + const island = document.querySelector('astro-island'); + const client = island.getAttribute('renderer-url'); + expect(client).not.to.be.undefined; + + const js = await fixture.readFile(client); + expect(js).not.to.match(/\w+\.component\(\"Bar\"/gm); + }); +}); diff --git a/packages/integrations/vue/test/fixtures/app-entrypoint-no-export-default/astro.config.mjs b/packages/integrations/vue/test/fixtures/app-entrypoint-no-export-default/astro.config.mjs new file mode 100644 index 0000000000..fa04f9c8b5 --- /dev/null +++ b/packages/integrations/vue/test/fixtures/app-entrypoint-no-export-default/astro.config.mjs @@ -0,0 +1,14 @@ +import { defineConfig } from 'astro/config'; +import vue from '@astrojs/vue'; +import ViteSvgLoader from 'vite-svg-loader' + +export default defineConfig({ + integrations: [vue({ + appEntrypoint: '/src/pages/_app' + })], + vite: { + plugins: [ + ViteSvgLoader(), + ], + }, +}) diff --git a/packages/integrations/vue/test/fixtures/app-entrypoint-no-export-default/package.json b/packages/integrations/vue/test/fixtures/app-entrypoint-no-export-default/package.json new file mode 100644 index 0000000000..29ab3c3426 --- /dev/null +++ b/packages/integrations/vue/test/fixtures/app-entrypoint-no-export-default/package.json @@ -0,0 +1,13 @@ +{ + "name": "@test/vue-app-entrypoint-no-export-default", + "version": "0.0.0", + "private": true, + "scripts": { + "astro": "astro" + }, + "dependencies": { + "@astrojs/vue": "workspace:*", + "astro": "workspace:*", + "vite-svg-loader": "4.0.0" + } +} diff --git a/packages/integrations/vue/test/fixtures/app-entrypoint-no-export-default/src/components/Bar.vue b/packages/integrations/vue/test/fixtures/app-entrypoint-no-export-default/src/components/Bar.vue new file mode 100644 index 0000000000..9e690ea06a --- /dev/null +++ b/packages/integrations/vue/test/fixtures/app-entrypoint-no-export-default/src/components/Bar.vue @@ -0,0 +1,3 @@ +<template> + <div id="bar">works</div> +</template> diff --git a/packages/integrations/vue/test/fixtures/app-entrypoint-no-export-default/src/components/Circle.svg b/packages/integrations/vue/test/fixtures/app-entrypoint-no-export-default/src/components/Circle.svg new file mode 100644 index 0000000000..cf2bd92fc1 --- /dev/null +++ b/packages/integrations/vue/test/fixtures/app-entrypoint-no-export-default/src/components/Circle.svg @@ -0,0 +1 @@ +<svg fill="none" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="50" fill="#ff0" r="40" stroke="#008000" stroke-width="4"/></svg> \ No newline at end of file diff --git a/packages/integrations/vue/test/fixtures/app-entrypoint-no-export-default/src/components/Foo.vue b/packages/integrations/vue/test/fixtures/app-entrypoint-no-export-default/src/components/Foo.vue new file mode 100644 index 0000000000..7f6808477f --- /dev/null +++ b/packages/integrations/vue/test/fixtures/app-entrypoint-no-export-default/src/components/Foo.vue @@ -0,0 +1,11 @@ +<script setup> +import Bar from './Bar.vue' +import Circle from './Circle.svg?component' +</script> + +<template> + <div id="foo"> + <Bar /> + <Circle/> + </div> +</template> diff --git a/packages/integrations/vue/test/fixtures/app-entrypoint-no-export-default/src/pages/_app.ts b/packages/integrations/vue/test/fixtures/app-entrypoint-no-export-default/src/pages/_app.ts new file mode 100644 index 0000000000..808620814b --- /dev/null +++ b/packages/integrations/vue/test/fixtures/app-entrypoint-no-export-default/src/pages/_app.ts @@ -0,0 +1,3 @@ +console.log(123); + +// no default export diff --git a/packages/integrations/vue/test/fixtures/app-entrypoint-no-export-default/src/pages/index.astro b/packages/integrations/vue/test/fixtures/app-entrypoint-no-export-default/src/pages/index.astro new file mode 100644 index 0000000000..3240cbe0fd --- /dev/null +++ b/packages/integrations/vue/test/fixtures/app-entrypoint-no-export-default/src/pages/index.astro @@ -0,0 +1,12 @@ +--- +import Foo from '../components/Foo.vue'; +--- + +<html> + <head> + <title>Vue App Entrypoint</title> + </head> + <body> + <Foo client:load /> + </body> +</html> diff --git a/packages/integrations/vue/test/fixtures/app-entrypoint-relative/astro.config.mjs b/packages/integrations/vue/test/fixtures/app-entrypoint-relative/astro.config.mjs new file mode 100644 index 0000000000..acafc8270a --- /dev/null +++ b/packages/integrations/vue/test/fixtures/app-entrypoint-relative/astro.config.mjs @@ -0,0 +1,8 @@ +import { defineConfig } from 'astro/config'; +import vue from '@astrojs/vue'; + +export default defineConfig({ + integrations: [vue({ + appEntrypoint: './src/vue.ts' + })] +}) diff --git a/packages/integrations/vue/test/fixtures/app-entrypoint-relative/package.json b/packages/integrations/vue/test/fixtures/app-entrypoint-relative/package.json new file mode 100644 index 0000000000..80483c7c6c --- /dev/null +++ b/packages/integrations/vue/test/fixtures/app-entrypoint-relative/package.json @@ -0,0 +1,12 @@ +{ + "name": "@test/vue-app-entrypoint-relative", + "version": "0.0.0", + "private": true, + "scripts": { + "astro": "astro" + }, + "dependencies": { + "@astrojs/vue": "workspace:*", + "astro": "workspace:*" + } +} diff --git a/packages/integrations/vue/test/fixtures/app-entrypoint-relative/src/components/Bar.vue b/packages/integrations/vue/test/fixtures/app-entrypoint-relative/src/components/Bar.vue new file mode 100644 index 0000000000..9e690ea06a --- /dev/null +++ b/packages/integrations/vue/test/fixtures/app-entrypoint-relative/src/components/Bar.vue @@ -0,0 +1,3 @@ +<template> + <div id="bar">works</div> +</template> diff --git a/packages/integrations/vue/test/fixtures/app-entrypoint-relative/src/components/Circle.svg b/packages/integrations/vue/test/fixtures/app-entrypoint-relative/src/components/Circle.svg new file mode 100644 index 0000000000..cf2bd92fc1 --- /dev/null +++ b/packages/integrations/vue/test/fixtures/app-entrypoint-relative/src/components/Circle.svg @@ -0,0 +1 @@ +<svg fill="none" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="50" fill="#ff0" r="40" stroke="#008000" stroke-width="4"/></svg> \ No newline at end of file diff --git a/packages/integrations/vue/test/fixtures/app-entrypoint-relative/src/components/Foo.vue b/packages/integrations/vue/test/fixtures/app-entrypoint-relative/src/components/Foo.vue new file mode 100644 index 0000000000..7f6808477f --- /dev/null +++ b/packages/integrations/vue/test/fixtures/app-entrypoint-relative/src/components/Foo.vue @@ -0,0 +1,11 @@ +<script setup> +import Bar from './Bar.vue' +import Circle from './Circle.svg?component' +</script> + +<template> + <div id="foo"> + <Bar /> + <Circle/> + </div> +</template> diff --git a/packages/integrations/vue/test/fixtures/app-entrypoint-relative/src/pages/index.astro b/packages/integrations/vue/test/fixtures/app-entrypoint-relative/src/pages/index.astro new file mode 100644 index 0000000000..3240cbe0fd --- /dev/null +++ b/packages/integrations/vue/test/fixtures/app-entrypoint-relative/src/pages/index.astro @@ -0,0 +1,12 @@ +--- +import Foo from '../components/Foo.vue'; +--- + +<html> + <head> + <title>Vue App Entrypoint</title> + </head> + <body> + <Foo client:load /> + </body> +</html> diff --git a/packages/integrations/vue/test/fixtures/app-entrypoint-relative/src/vue.ts b/packages/integrations/vue/test/fixtures/app-entrypoint-relative/src/vue.ts new file mode 100644 index 0000000000..ead516c976 --- /dev/null +++ b/packages/integrations/vue/test/fixtures/app-entrypoint-relative/src/vue.ts @@ -0,0 +1 @@ +export default () => {} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2e9dce56f8..be1d4f9153 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4839,6 +4839,27 @@ importers: specifier: 5.0.1 version: 5.0.1 + packages/integrations/vue/test/fixtures/app-entrypoint-no-export-default: + dependencies: + '@astrojs/vue': + specifier: workspace:* + version: link:../../.. + astro: + specifier: workspace:* + version: link:../../../../../astro + vite-svg-loader: + specifier: 4.0.0 + version: 4.0.0 + + packages/integrations/vue/test/fixtures/app-entrypoint-relative: + dependencies: + '@astrojs/vue': + specifier: workspace:* + version: link:../../.. + astro: + specifier: workspace:* + version: link:../../../../../astro + packages/integrations/vue/test/fixtures/basics: dependencies: '@astrojs/vue': @@ -15793,6 +15814,18 @@ packages: - supports-color dev: false + /vite-svg-loader@4.0.0: + resolution: {integrity: sha512-0MMf1yzzSYlV4MGePsLVAOqXsbF5IVxbn4EEzqRnWxTQl8BJg/cfwIzfQNmNQxZp5XXwd4kyRKF1LytuHZTnqA==} + peerDependencies: + vue: '*' + peerDependenciesMeta: + vue: + optional: true + dependencies: + '@vue/compiler-sfc': 3.3.8 + svgo: 3.0.4 + dev: false + /vite-svg-loader@5.0.1: resolution: {integrity: sha512-EUfcuqk1NomuacwiuL3mvCfinkm4XN0AHN8BXG737eDlhC0jnp5jxdCxakV+juP/YhhjV5tq/c/bLcm3waWv4Q==} peerDependencies: