Skip to content

Commit

Permalink
Improve Vue appEntrypoint handling (#8794)
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
5 people authored Dec 5, 2023
1 parent 874f68c commit 4d4e34d
Show file tree
Hide file tree
Showing 18 changed files with 257 additions and 13 deletions.
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.

0 comments on commit 4d4e34d

Please sign in to comment.