diff --git a/.changeset/smart-cameras-kneel.md b/.changeset/smart-cameras-kneel.md new file mode 100644 index 000000000000..d5cb0264fbe8 --- /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 2c234952b693..c0fda26606c4 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 { +async function getViteConfiguration(options: ViteOptions): Promise { 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 b20e7be7e33c..308f10149fc1 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 000000000000..fa04f9c8b592 --- /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 000000000000..29ab3c34264c --- /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 000000000000..9e690ea06adc --- /dev/null +++ b/packages/integrations/vue/test/fixtures/app-entrypoint-no-export-default/src/components/Bar.vue @@ -0,0 +1,3 @@ + 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 000000000000..cf2bd92fc135 --- /dev/null +++ b/packages/integrations/vue/test/fixtures/app-entrypoint-no-export-default/src/components/Circle.svg @@ -0,0 +1 @@ + \ 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 000000000000..7f6808477f18 --- /dev/null +++ b/packages/integrations/vue/test/fixtures/app-entrypoint-no-export-default/src/components/Foo.vue @@ -0,0 +1,11 @@ + + + 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 000000000000..808620814b9b --- /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 000000000000..3240cbe0fd73 --- /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'; +--- + + + + Vue App Entrypoint + + + + + 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 000000000000..acafc8270a9d --- /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 000000000000..80483c7c6ce8 --- /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 000000000000..9e690ea06adc --- /dev/null +++ b/packages/integrations/vue/test/fixtures/app-entrypoint-relative/src/components/Bar.vue @@ -0,0 +1,3 @@ + 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 000000000000..cf2bd92fc135 --- /dev/null +++ b/packages/integrations/vue/test/fixtures/app-entrypoint-relative/src/components/Circle.svg @@ -0,0 +1 @@ + \ 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 000000000000..7f6808477f18 --- /dev/null +++ b/packages/integrations/vue/test/fixtures/app-entrypoint-relative/src/components/Foo.vue @@ -0,0 +1,11 @@ + + + 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 000000000000..3240cbe0fd73 --- /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'; +--- + + + + Vue App Entrypoint + + + + + 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 000000000000..ead516c976e9 --- /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 2e9dce56f88b..be1d4f9153ca 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: