summaryrefslogtreecommitdiff
path: root/packages/integrations/vue/src
diff options
context:
space:
mode:
Diffstat (limited to 'packages/integrations/vue/src')
-rw-r--r--packages/integrations/vue/src/client.ts67
-rw-r--r--packages/integrations/vue/src/context.ts26
-rw-r--r--packages/integrations/vue/src/server.ts50
-rw-r--r--packages/integrations/vue/src/static-html.ts33
-rw-r--r--packages/integrations/vue/src/types.ts4
5 files changed, 180 insertions, 0 deletions
diff --git a/packages/integrations/vue/src/client.ts b/packages/integrations/vue/src/client.ts
new file mode 100644
index 000000000..8f02d534e
--- /dev/null
+++ b/packages/integrations/vue/src/client.ts
@@ -0,0 +1,67 @@
+import { setup } from 'virtual:@astrojs/vue/app';
+import { Suspense, createApp, createSSRApp, h } from 'vue';
+import StaticHtml from './static-html.js';
+
+// keep track of already initialized apps, so we don't hydrate again for view transitions
+let appMap = new WeakMap<
+ HTMLElement,
+ { props: Record<string, any>; slots: Record<string, any>; component?: any }
+>();
+
+export default (element: HTMLElement) =>
+ async (
+ Component: any,
+ props: Record<string, any>,
+ slotted: Record<string, any>,
+ { client }: Record<string, string>,
+ ) => {
+ if (!element.hasAttribute('ssr')) return;
+
+ // Expose name on host component for Vue devtools
+ const name = Component.name ? `${Component.name} Host` : undefined;
+ const slots: Record<string, any> = {};
+ for (const [key, value] of Object.entries(slotted)) {
+ slots[key] = () => h(StaticHtml, { value, name: key === 'default' ? undefined : key });
+ }
+
+ const isHydrate = client !== 'only';
+ const bootstrap = isHydrate ? createSSRApp : createApp;
+
+ // keep a reference to the app, props and slots so we can update a running instance later
+ let appInstance = appMap.get(element);
+
+ if (!appInstance) {
+ appInstance = {
+ props,
+ slots,
+ };
+ const app = bootstrap({
+ name,
+ render() {
+ // At this point, appInstance has been set so it's safe to use a non-null assertion
+ let content = h(Component, appInstance!.props, appInstance!.slots);
+ appInstance!.component = this;
+ // related to https://github.com/withastro/astro/issues/6549
+ // if the component is async, wrap it in a Suspense component
+ if (isAsync(Component.setup)) {
+ content = h(Suspense, null, content);
+ }
+ return content;
+ },
+ });
+ app.config.idPrefix = element.getAttribute('prefix') ?? undefined;
+ await setup(app);
+ app.mount(element, isHydrate);
+ appMap.set(element, appInstance);
+ element.addEventListener('astro:unmount', () => app.unmount(), { once: true });
+ } else {
+ appInstance.props = props;
+ appInstance.slots = slots;
+ appInstance.component.$forceUpdate();
+ }
+ };
+
+function isAsync(fn: () => any) {
+ const constructor = fn?.constructor;
+ return constructor && constructor.name === 'AsyncFunction';
+}
diff --git a/packages/integrations/vue/src/context.ts b/packages/integrations/vue/src/context.ts
new file mode 100644
index 000000000..833755044
--- /dev/null
+++ b/packages/integrations/vue/src/context.ts
@@ -0,0 +1,26 @@
+import type { SSRResult } from 'astro';
+
+const contexts = new WeakMap<SSRResult, { currentIndex: number; readonly id: string }>();
+
+const ID_PREFIX = 's';
+
+function getContext(rendererContextResult: SSRResult) {
+ if (contexts.has(rendererContextResult)) {
+ return contexts.get(rendererContextResult);
+ }
+ const ctx = {
+ currentIndex: 0,
+ get id() {
+ return ID_PREFIX + this.currentIndex.toString();
+ },
+ };
+ contexts.set(rendererContextResult, ctx);
+ return ctx;
+}
+
+export function incrementId(rendererContextResult: SSRResult) {
+ const ctx = getContext(rendererContextResult)!;
+ const id = ctx.id;
+ ctx.currentIndex++;
+ return id;
+}
diff --git a/packages/integrations/vue/src/server.ts b/packages/integrations/vue/src/server.ts
new file mode 100644
index 000000000..6b4c2a3f4
--- /dev/null
+++ b/packages/integrations/vue/src/server.ts
@@ -0,0 +1,50 @@
+import { setup } from 'virtual:@astrojs/vue/app';
+import type { AstroComponentMetadata } from 'astro';
+import { createSSRApp, h } from 'vue';
+import { renderToString } from 'vue/server-renderer';
+import { incrementId } from './context.js';
+import StaticHtml from './static-html.js';
+import type { RendererContext } from './types.js';
+
+function check(Component: any) {
+ return !!Component['ssrRender'] || !!Component['__ssrInlineRender'];
+}
+
+async function renderToStaticMarkup(
+ this: RendererContext,
+ Component: any,
+ inputProps: Record<string, any>,
+ slotted: Record<string, any>,
+ metadata: AstroComponentMetadata,
+) {
+ let prefix;
+ if (this && this.result) {
+ prefix = incrementId(this.result);
+ }
+ const attrs = { prefix };
+
+ const slots: Record<string, any> = {};
+ const props = { ...inputProps };
+ delete props.slot;
+ for (const [key, value] of Object.entries(slotted)) {
+ slots[key] = () =>
+ h(StaticHtml, {
+ value,
+ name: key === 'default' ? undefined : key,
+ // Adjust how this is hydrated only when the version of Astro supports `astroStaticSlot`
+ hydrate: metadata.astroStaticSlot ? !!metadata.hydrate : true,
+ });
+ }
+ const app = createSSRApp({ render: () => h(Component, props, slots) });
+ app.config.idPrefix = prefix;
+ await setup(app);
+ const html = await renderToString(app);
+ return { html, attrs };
+}
+
+export default {
+ name: '@astrojs/vue',
+ check,
+ renderToStaticMarkup,
+ supportsAstroStaticSlot: true,
+};
diff --git a/packages/integrations/vue/src/static-html.ts b/packages/integrations/vue/src/static-html.ts
new file mode 100644
index 000000000..689b56a70
--- /dev/null
+++ b/packages/integrations/vue/src/static-html.ts
@@ -0,0 +1,33 @@
+import { defineComponent, h } from 'vue';
+
+/**
+ * Astro passes `children` as a string of HTML, so we need
+ * a wrapper `div` to render that content as VNodes.
+ *
+ * This is the Vue + JSX equivalent of using `<div v-html="value" />`
+ */
+const StaticHtml = defineComponent({
+ props: {
+ value: String,
+ name: String,
+ hydrate: {
+ type: Boolean,
+ default: true,
+ },
+ },
+ setup({ name, value, hydrate }) {
+ if (!value) return () => null;
+ let tagName = hydrate ? 'astro-slot' : 'astro-static-slot';
+ return () => h(tagName, { name, innerHTML: value });
+ },
+});
+
+/**
+ * Other frameworks have `shouldComponentUpdate` in order to signal
+ * that this subtree is entirely static and will not be updated
+ *
+ * Fortunately, Vue is smart enough to figure that out without any
+ * help from us, so this just works out of the box!
+ */
+
+export default StaticHtml;
diff --git a/packages/integrations/vue/src/types.ts b/packages/integrations/vue/src/types.ts
new file mode 100644
index 000000000..5dff5b0b4
--- /dev/null
+++ b/packages/integrations/vue/src/types.ts
@@ -0,0 +1,4 @@
+import type { SSRResult } from 'astro';
+export type RendererContext = {
+ result: SSRResult;
+};