Skip to content

Commit

Permalink
[Vue] add support for appEntrypoint (#5075)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
natemoo-re and natemoo-re authored Oct 13, 2022
1 parent 6f9a88b commit d25f54c
Show file tree
Hide file tree
Showing 15 changed files with 205 additions and 4 deletions.
29 changes: 29 additions & 0 deletions .changeset/dry-moose-join.md
Original file line number Diff line number Diff line change
@@ -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 */ })
}
```
30 changes: 30 additions & 0 deletions packages/integrations/vue/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

You can use Vue JSX by setting `jsx: true`.
Expand Down
5 changes: 4 additions & 1 deletion packages/integrations/vue/client.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { h, createSSRApp, createApp } from 'vue';
import { setup } from 'virtual:@astrojs/vue/app'
import StaticHtml from './static-html.js';

export default (element) =>
(Component, props, slotted, { client }) => {
async (Component, props, slotted, { client }) => {
delete props['class'];
if (!element.hasAttribute('ssr')) return;

Expand All @@ -14,9 +15,11 @@ export default (element) =>
}
if (client === 'only') {
const app = createApp({ name, render: () => h(Component, props, slots) });
await setup(app);
app.mount(element, false);
} else {
const app = createSSRApp({ name, render: () => h(Component, props, slots) });
await setup(app);
app.mount(element, true);
}
};
7 changes: 6 additions & 1 deletion packages/integrations/vue/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@
"scripts": {
"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",
"dev": "astro-scripts dev \"src/**/*.ts\""
"dev": "astro-scripts dev \"src/**/*.ts\"",
"test": "mocha --timeout 20000"
},
"dependencies": {
"@vitejs/plugin-vue": "^3.0.0",
Expand All @@ -39,8 +40,12 @@
"@vue/compiler-sfc": "^3.2.39"
},
"devDependencies": {
"@types/chai": "^4.3.3",
"astro": "workspace:*",
"astro-scripts": "workspace:*",
"chai": "^4.3.6",
"linkedom": "^0.14.17",
"mocha": "^9.2.2",
"vite": "^3.0.0",
"vue": "^3.2.37"
},
Expand Down
2 changes: 2 additions & 0 deletions packages/integrations/vue/server.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { h, createSSRApp } from 'vue';
import { renderToString } from 'vue/server-renderer';
import { setup } from 'virtual:@astrojs/vue/app'
import StaticHtml from './static-html.js';

function check(Component) {
Expand All @@ -12,6 +13,7 @@ async function renderToStaticMarkup(Component, props, slotted) {
slots[key] = () => h(StaticHtml, { value, name: key === 'default' ? undefined : key });
}
const app = createSSRApp({ render: () => h(Component, props, slots) });
await setup(app);
const html = await renderToString(app);
return { html };
}
Expand Down
26 changes: 24 additions & 2 deletions packages/integrations/vue/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type { UserConfig } from 'vite';

interface Options extends VueOptions {
jsx?: boolean | VueJsxOptions;
appEntrypoint?: string;
}

function getRenderer(): AstroRenderer {
Expand All @@ -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> {
const config: UserConfig = {
optimizeDeps: {
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: {
external: ['@vue/server-renderer'],
noExternal: ['vueperslides'],
Expand Down
34 changes: 34 additions & 0 deletions packages/integrations/vue/test/app-entrypoint.test.js
Original file line number Diff line number Diff line change
@@ -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)
});
});
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/pages/_app'
})]
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"name": "@test/vue-app-entrypoint",
"version": "0.0.0",
"private": true,
"dependencies": {
"astro": "workspace:*",
"@astrojs/vue": "workspace:*"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<template>
<div id="bar">works</div>
</template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<template>
<div id="foo">
<Bar />
</div>
</template>
Original file line number Diff line number Diff line change
@@ -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);
}
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>
17 changes: 17 additions & 0 deletions packages/integrations/vue/test/test-utils.js
Original file line number Diff line number Diff line change
@@ -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(),
});
}
16 changes: 16 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 d25f54c

Please sign in to comment.