Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/smart-cameras-kneel.md
Original file line number Diff line number Diff line change
@@ -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.
57 changes: 44 additions & 13 deletions packages/integrations/vue/src/index.ts
Original file line number Diff line number Diff line change
@@ -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',
Expand All @@ -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 {
Expand All @@ -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'],
Expand All @@ -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 }),
});
},
},
};
Expand Down
70 changes: 70 additions & 0 deletions packages/integrations/vue/test/app-entrypoint.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
Original file line number Diff line number Diff line change
@@ -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(),
],
},
})
Original file line number Diff line number Diff line change
@@ -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"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<template>
<div id="bar">works</div>
</template>
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -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>
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
console.log(123);

// no default export
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
import Foo from '../components/Foo.vue';
---

<html>
<head>
<title>Vue App Entrypoint</title>
</head>
<body>
<Foo client:load />
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { defineConfig } from 'astro/config';
import vue from '@astrojs/vue';

export default defineConfig({
integrations: [vue({
appEntrypoint: './src/vue.ts'
})]
})
Original file line number Diff line number Diff line change
@@ -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:*"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<template>
<div id="bar">works</div>
</template>
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -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>
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
import Foo from '../components/Foo.vue';
---

<html>
<head>
<title>Vue App Entrypoint</title>
</head>
<body>
<Foo client:load />
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default () => {}
33 changes: 33 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.