[Vue] add support for appEntrypoint
(#5075)
* feat(vue): add support for appEntrypoint * chore: add changeset * test(vue): add tests for app entrypoint * docs(vue): update README to include app entrypoint * fix(vue): prefer resolvedVirtualModuleId Co-authored-by: Nate Moore <nate@astro.build>
This commit is contained in:
parent
6f9a88b31b
commit
d25f54cb93
15 changed files with 205 additions and 4 deletions
29
.changeset/dry-moose-join.md
Normal file
29
.changeset/dry-moose-join.md
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
---
|
||||||
|
'@astrojs/vue': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Add support for the `appEntrypoint` option, which accepts a root-relative path to an app entrypoint. The default export of this file should be a function that accepts a Vue `App` instance prior to rendering. This opens up the ability to extend the `App` instance with [custom Vue plugins](https://vuejs.org/guide/reusability/plugins.html).
|
||||||
|
|
||||||
|
```js
|
||||||
|
// astro.config.mjs
|
||||||
|
import { defineConfig } from 'astro/config';
|
||||||
|
import vue from '@astrojs/vue';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
integrations: [
|
||||||
|
vue({
|
||||||
|
appEntrypoint: '/src/pages/_app'
|
||||||
|
})
|
||||||
|
]
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
```js
|
||||||
|
// src/pages/_app.ts
|
||||||
|
import type { App } from 'vue';
|
||||||
|
import i18nPlugin from '../plugins/i18n'
|
||||||
|
|
||||||
|
export default function setup(app: App) {
|
||||||
|
app.use(i18nPlugin, { /* options */ })
|
||||||
|
}
|
||||||
|
```
|
|
@ -95,6 +95,36 @@ export default {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### appEntrypoint
|
||||||
|
|
||||||
|
You can extend the Vue `app` instance setting the `appEntrypoint` option to a root-relative import specifier (for example, `appEntrypoint: "/src/pages/_app"`).
|
||||||
|
|
||||||
|
The default export of this file should be a function that accepts a Vue `App` instance prior to rendering, allowing the use of [custom Vue plugins](https://vuejs.org/guide/reusability/plugins.html), `app.use`, and other customizations for advanced use cases.
|
||||||
|
|
||||||
|
__`astro.config.mjs`__
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { defineConfig } from 'astro/config';
|
||||||
|
import vue from '@astrojs/vue';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
integrations: [
|
||||||
|
vue({ appEntrypoint: '/src/pages/_app' })
|
||||||
|
],
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
__`src/pages/_app.ts`__
|
||||||
|
|
||||||
|
```js
|
||||||
|
import type { App } from 'vue';
|
||||||
|
import i18nPlugin from 'my-vue-i18n-plugin';
|
||||||
|
|
||||||
|
export default (app: App) => {
|
||||||
|
app.use(i18nPlugin);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### jsx
|
### jsx
|
||||||
|
|
||||||
You can use Vue JSX by setting `jsx: true`.
|
You can use Vue JSX by setting `jsx: true`.
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import { h, createSSRApp, createApp } from 'vue';
|
import { h, createSSRApp, createApp } from 'vue';
|
||||||
|
import { setup } from 'virtual:@astrojs/vue/app'
|
||||||
import StaticHtml from './static-html.js';
|
import StaticHtml from './static-html.js';
|
||||||
|
|
||||||
export default (element) =>
|
export default (element) =>
|
||||||
(Component, props, slotted, { client }) => {
|
async (Component, props, slotted, { client }) => {
|
||||||
delete props['class'];
|
delete props['class'];
|
||||||
if (!element.hasAttribute('ssr')) return;
|
if (!element.hasAttribute('ssr')) return;
|
||||||
|
|
||||||
|
@ -14,9 +15,11 @@ export default (element) =>
|
||||||
}
|
}
|
||||||
if (client === 'only') {
|
if (client === 'only') {
|
||||||
const app = createApp({ name, render: () => h(Component, props, slots) });
|
const app = createApp({ name, render: () => h(Component, props, slots) });
|
||||||
|
await setup(app);
|
||||||
app.mount(element, false);
|
app.mount(element, false);
|
||||||
} else {
|
} else {
|
||||||
const app = createSSRApp({ name, render: () => h(Component, props, slots) });
|
const app = createSSRApp({ name, render: () => h(Component, props, slots) });
|
||||||
|
await setup(app);
|
||||||
app.mount(element, true);
|
app.mount(element, true);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -30,7 +30,8 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "astro-scripts build \"src/index.ts\" && astro-scripts build \"src/editor.cts\" --force-cjs --no-clean-dist && tsc",
|
"build": "astro-scripts build \"src/index.ts\" && astro-scripts build \"src/editor.cts\" --force-cjs --no-clean-dist && tsc",
|
||||||
"build:ci": "astro-scripts build \"src/**/*.ts\" && astro-scripts build \"src/editor.cts\" --force-cjs --no-clean-dist",
|
"build:ci": "astro-scripts build \"src/**/*.ts\" && astro-scripts build \"src/editor.cts\" --force-cjs --no-clean-dist",
|
||||||
"dev": "astro-scripts dev \"src/**/*.ts\""
|
"dev": "astro-scripts dev \"src/**/*.ts\"",
|
||||||
|
"test": "mocha --timeout 20000"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vitejs/plugin-vue": "^3.0.0",
|
"@vitejs/plugin-vue": "^3.0.0",
|
||||||
|
@ -39,8 +40,12 @@
|
||||||
"@vue/compiler-sfc": "^3.2.39"
|
"@vue/compiler-sfc": "^3.2.39"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/chai": "^4.3.3",
|
||||||
"astro": "workspace:*",
|
"astro": "workspace:*",
|
||||||
"astro-scripts": "workspace:*",
|
"astro-scripts": "workspace:*",
|
||||||
|
"chai": "^4.3.6",
|
||||||
|
"linkedom": "^0.14.17",
|
||||||
|
"mocha": "^9.2.2",
|
||||||
"vite": "^3.0.0",
|
"vite": "^3.0.0",
|
||||||
"vue": "^3.2.37"
|
"vue": "^3.2.37"
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { h, createSSRApp } from 'vue';
|
import { h, createSSRApp } from 'vue';
|
||||||
import { renderToString } from 'vue/server-renderer';
|
import { renderToString } from 'vue/server-renderer';
|
||||||
|
import { setup } from 'virtual:@astrojs/vue/app'
|
||||||
import StaticHtml from './static-html.js';
|
import StaticHtml from './static-html.js';
|
||||||
|
|
||||||
function check(Component) {
|
function check(Component) {
|
||||||
|
@ -12,6 +13,7 @@ async function renderToStaticMarkup(Component, props, slotted) {
|
||||||
slots[key] = () => h(StaticHtml, { value, name: key === 'default' ? undefined : key });
|
slots[key] = () => h(StaticHtml, { value, name: key === 'default' ? undefined : key });
|
||||||
}
|
}
|
||||||
const app = createSSRApp({ render: () => h(Component, props, slots) });
|
const app = createSSRApp({ render: () => h(Component, props, slots) });
|
||||||
|
await setup(app);
|
||||||
const html = await renderToString(app);
|
const html = await renderToString(app);
|
||||||
return { html };
|
return { html };
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import type { UserConfig } from 'vite';
|
||||||
|
|
||||||
interface Options extends VueOptions {
|
interface Options extends VueOptions {
|
||||||
jsx?: boolean | VueJsxOptions;
|
jsx?: boolean | VueJsxOptions;
|
||||||
|
appEntrypoint?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRenderer(): AstroRenderer {
|
function getRenderer(): AstroRenderer {
|
||||||
|
@ -31,13 +32,34 @@ function getJsxRenderer(): AstroRenderer {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function virtualAppEntrypoint(options?: Options) {
|
||||||
|
const virtualModuleId = 'virtual:@astrojs/vue/app';
|
||||||
|
const resolvedVirtualModuleId = '\0' + virtualModuleId;
|
||||||
|
return {
|
||||||
|
name: '@astrojs/vue/virtual-app',
|
||||||
|
resolveId(id: string) {
|
||||||
|
if (id == virtualModuleId) {
|
||||||
|
return resolvedVirtualModuleId;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
load(id: string) {
|
||||||
|
if (id === resolvedVirtualModuleId) {
|
||||||
|
if (options?.appEntrypoint) {
|
||||||
|
return `export { default as setup } from "${options.appEntrypoint}";`;
|
||||||
|
}
|
||||||
|
return `export const setup = () => {};`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async function getViteConfiguration(options?: Options): Promise<UserConfig> {
|
async function getViteConfiguration(options?: Options): Promise<UserConfig> {
|
||||||
const config: UserConfig = {
|
const config: UserConfig = {
|
||||||
optimizeDeps: {
|
optimizeDeps: {
|
||||||
include: ['@astrojs/vue/client.js', 'vue'],
|
include: ['@astrojs/vue/client.js', 'vue'],
|
||||||
exclude: ['@astrojs/vue/server.js'],
|
exclude: ['@astrojs/vue/server.js', 'virtual:@astrojs/vue/app']
|
||||||
},
|
},
|
||||||
plugins: [vue(options)],
|
plugins: [vue(options), virtualAppEntrypoint(options)],
|
||||||
ssr: {
|
ssr: {
|
||||||
external: ['@vue/server-renderer'],
|
external: ['@vue/server-renderer'],
|
||||||
noExternal: ['vueperslides'],
|
noExternal: ['vueperslides'],
|
||||||
|
|
34
packages/integrations/vue/test/app-entrypoint.test.js
Normal file
34
packages/integrations/vue/test/app-entrypoint.test.js
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import { loadFixture } from './test-utils.js';
|
||||||
|
import { expect } from 'chai';
|
||||||
|
import { parseHTML } from 'linkedom';
|
||||||
|
|
||||||
|
describe('App Entrypoint', () => {
|
||||||
|
/** @type {import('./test-utils').Fixture} */
|
||||||
|
let fixture;
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
fixture = await loadFixture({
|
||||||
|
root: './fixtures/app-entrypoint/'
|
||||||
|
});
|
||||||
|
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('setup 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).to.match(/\w+\.component\(\"Bar\"/gm)
|
||||||
|
});
|
||||||
|
});
|
8
packages/integrations/vue/test/fixtures/app-entrypoint/astro.config.mjs
vendored
Normal file
8
packages/integrations/vue/test/fixtures/app-entrypoint/astro.config.mjs
vendored
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import { defineConfig } from 'astro/config';
|
||||||
|
import vue from '@astrojs/vue';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
integrations: [vue({
|
||||||
|
appEntrypoint: '/src/pages/_app'
|
||||||
|
})]
|
||||||
|
})
|
9
packages/integrations/vue/test/fixtures/app-entrypoint/package.json
vendored
Normal file
9
packages/integrations/vue/test/fixtures/app-entrypoint/package.json
vendored
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"name": "@test/vue-app-entrypoint",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"astro": "workspace:*",
|
||||||
|
"@astrojs/vue": "workspace:*"
|
||||||
|
}
|
||||||
|
}
|
3
packages/integrations/vue/test/fixtures/app-entrypoint/src/components/Bar.vue
vendored
Normal file
3
packages/integrations/vue/test/fixtures/app-entrypoint/src/components/Bar.vue
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<template>
|
||||||
|
<div id="bar">works</div>
|
||||||
|
</template>
|
5
packages/integrations/vue/test/fixtures/app-entrypoint/src/components/Foo.vue
vendored
Normal file
5
packages/integrations/vue/test/fixtures/app-entrypoint/src/components/Foo.vue
vendored
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<template>
|
||||||
|
<div id="foo">
|
||||||
|
<Bar />
|
||||||
|
</div>
|
||||||
|
</template>
|
6
packages/integrations/vue/test/fixtures/app-entrypoint/src/pages/_app.ts
vendored
Normal file
6
packages/integrations/vue/test/fixtures/app-entrypoint/src/pages/_app.ts
vendored
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
import type { App } from 'vue'
|
||||||
|
import Bar from '../components/Bar.vue'
|
||||||
|
|
||||||
|
export default function setup(app: App) {
|
||||||
|
app.component('Bar', Bar);
|
||||||
|
}
|
12
packages/integrations/vue/test/fixtures/app-entrypoint/src/pages/index.astro
vendored
Normal file
12
packages/integrations/vue/test/fixtures/app-entrypoint/src/pages/index.astro
vendored
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
---
|
||||||
|
import Foo from '../components/Foo.vue';
|
||||||
|
---
|
||||||
|
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Vue App Entrypoint</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<Foo client:load />
|
||||||
|
</body>
|
||||||
|
</html>
|
17
packages/integrations/vue/test/test-utils.js
Normal file
17
packages/integrations/vue/test/test-utils.js
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import { loadFixture as baseLoadFixture } from '../../../astro/test/test-utils.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {import('../../../astro/test/test-utils').Fixture} Fixture
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function loadFixture(inlineConfig) {
|
||||||
|
if (!inlineConfig || !inlineConfig.root)
|
||||||
|
throw new Error("Must provide { root: './fixtures/...' }");
|
||||||
|
|
||||||
|
// resolve the relative root (i.e. "./fixtures/tailwindcss") to a full filepath
|
||||||
|
// without this, the main `loadFixture` helper will resolve relative to `packages/astro/test`
|
||||||
|
return baseLoadFixture({
|
||||||
|
...inlineConfig,
|
||||||
|
root: new URL(inlineConfig.root, import.meta.url).toString(),
|
||||||
|
});
|
||||||
|
}
|
|
@ -3142,12 +3142,16 @@ importers:
|
||||||
|
|
||||||
packages/integrations/vue:
|
packages/integrations/vue:
|
||||||
specifiers:
|
specifiers:
|
||||||
|
'@types/chai': ^4.3.3
|
||||||
'@vitejs/plugin-vue': ^3.0.0
|
'@vitejs/plugin-vue': ^3.0.0
|
||||||
'@vitejs/plugin-vue-jsx': ^2.0.1
|
'@vitejs/plugin-vue-jsx': ^2.0.1
|
||||||
'@vue/babel-plugin-jsx': ^1.1.1
|
'@vue/babel-plugin-jsx': ^1.1.1
|
||||||
'@vue/compiler-sfc': ^3.2.39
|
'@vue/compiler-sfc': ^3.2.39
|
||||||
astro: workspace:*
|
astro: workspace:*
|
||||||
astro-scripts: workspace:*
|
astro-scripts: workspace:*
|
||||||
|
chai: ^4.3.6
|
||||||
|
linkedom: ^0.14.17
|
||||||
|
mocha: ^9.2.2
|
||||||
vite: ^3.0.0
|
vite: ^3.0.0
|
||||||
vue: ^3.2.37
|
vue: ^3.2.37
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -3156,11 +3160,23 @@ importers:
|
||||||
'@vue/babel-plugin-jsx': 1.1.1
|
'@vue/babel-plugin-jsx': 1.1.1
|
||||||
'@vue/compiler-sfc': 3.2.40
|
'@vue/compiler-sfc': 3.2.40
|
||||||
devDependencies:
|
devDependencies:
|
||||||
|
'@types/chai': 4.3.3
|
||||||
astro: link:../../astro
|
astro: link:../../astro
|
||||||
astro-scripts: link:../../../scripts
|
astro-scripts: link:../../../scripts
|
||||||
|
chai: 4.3.6
|
||||||
|
linkedom: 0.14.17
|
||||||
|
mocha: 9.2.2
|
||||||
vite: 3.1.8
|
vite: 3.1.8
|
||||||
vue: 3.2.40
|
vue: 3.2.40
|
||||||
|
|
||||||
|
packages/integrations/vue/test/fixtures/app-entrypoint:
|
||||||
|
specifiers:
|
||||||
|
'@astrojs/vue': workspace:*
|
||||||
|
astro: workspace:*
|
||||||
|
dependencies:
|
||||||
|
'@astrojs/vue': link:../../..
|
||||||
|
astro: link:../../../../../astro
|
||||||
|
|
||||||
packages/markdown/component:
|
packages/markdown/component:
|
||||||
specifiers:
|
specifiers:
|
||||||
'@types/mocha': ^9.1.1
|
'@types/mocha': ^9.1.1
|
||||||
|
|
Loading…
Reference in a new issue