summaryrefslogtreecommitdiff
path: root/packages/integrations/svelte/src
diff options
context:
space:
mode:
Diffstat (limited to 'packages/integrations/svelte/src')
-rw-r--r--packages/integrations/svelte/src/client.svelte.ts86
-rw-r--r--packages/integrations/svelte/src/context.ts26
-rw-r--r--packages/integrations/svelte/src/server.ts74
-rw-r--r--packages/integrations/svelte/src/types.ts4
4 files changed, 190 insertions, 0 deletions
diff --git a/packages/integrations/svelte/src/client.svelte.ts b/packages/integrations/svelte/src/client.svelte.ts
new file mode 100644
index 000000000..102f99490
--- /dev/null
+++ b/packages/integrations/svelte/src/client.svelte.ts
@@ -0,0 +1,86 @@
+import { createRawSnippet, hydrate, mount, unmount } from 'svelte';
+
+const existingApplications = new WeakMap<HTMLElement, ReturnType<typeof createComponent>>();
+
+export default (element: HTMLElement) => {
+ return async (
+ Component: any,
+ props: Record<string, any>,
+ slotted: Record<string, any>,
+ { client }: Record<string, string>,
+ ) => {
+ if (!element.hasAttribute('ssr')) return;
+
+ let children = undefined;
+ let _$$slots: Record<string, any> | undefined = undefined;
+ let renderFns: Record<string, any> = {};
+
+ for (const [key, value] of Object.entries(slotted)) {
+ // Legacy slot support
+ _$$slots ??= {};
+ if (key === 'default') {
+ _$$slots.default = true;
+ children = createRawSnippet(() => ({
+ render: () => `<astro-slot>${value}</astro-slot>`,
+ }));
+ } else {
+ _$$slots[key] = createRawSnippet(() => ({
+ render: () => `<astro-slot name="${key}">${value}</astro-slot>`,
+ }));
+ }
+ // @render support for Svelte ^5.0
+ if (key === 'default') {
+ renderFns.children = createRawSnippet(() => ({
+ render: () => `<astro-slot>${value}</astro-slot>`,
+ }));
+ } else {
+ renderFns[key] = createRawSnippet(() => ({
+ render: () => `<astro-slot name="${key}">${value}</astro-slot>`,
+ }));
+ }
+ }
+
+ const resolvedProps = {
+ ...props,
+ children,
+ $$slots: _$$slots,
+ ...renderFns,
+ };
+ if (existingApplications.has(element)) {
+ existingApplications.get(element)!.setProps(resolvedProps);
+ } else {
+ const component = createComponent(Component, element, resolvedProps, client !== 'only');
+ existingApplications.set(element, component);
+ element.addEventListener('astro:unmount', () => component.destroy(), { once: true });
+ }
+ };
+};
+
+
+function createComponent(
+ Component: any,
+ target: HTMLElement,
+ props: Record<string, any>,
+ shouldHydrate: boolean,
+) {
+ let propsState = $state(props);
+ const bootstrap = shouldHydrate ? hydrate : mount;
+ if (!shouldHydrate) {
+ target.innerHTML = '';
+ }
+ const component = bootstrap(Component, { target, props: propsState });
+ return {
+ setProps(newProps: Record<string, any>) {
+ Object.assign(propsState, newProps);
+ // Remove props in `propsState` but not in `newProps`
+ for (const key in propsState) {
+ if (!(key in newProps)) {
+ delete propsState[key];
+ }
+ }
+ },
+ destroy() {
+ unmount(component);
+ },
+ };
+}
diff --git a/packages/integrations/svelte/src/context.ts b/packages/integrations/svelte/src/context.ts
new file mode 100644
index 000000000..833755044
--- /dev/null
+++ b/packages/integrations/svelte/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/svelte/src/server.ts b/packages/integrations/svelte/src/server.ts
new file mode 100644
index 000000000..4b0fccb3d
--- /dev/null
+++ b/packages/integrations/svelte/src/server.ts
@@ -0,0 +1,74 @@
+import type { AstroComponentMetadata } from 'astro';
+import { createRawSnippet } from 'svelte';
+import { render } from 'svelte/server';
+import { incrementId } from './context.js';
+import type { RendererContext } from './types.js';
+
+function check(Component: any) {
+ if (typeof Component !== 'function') return false;
+ // Svelte 5 generated components always accept a `$$payload` prop.
+ // This assumes that the SSR build does not minify it (which Astro enforces by default).
+ // This isn't the best check, but the only other option otherwise is to try to render the
+ // component, which is taxing. We'll leave it as a last resort for the future for now.
+ return Component.toString().includes('$$payload');
+}
+
+function needsHydration(metadata: AstroComponentMetadata) {
+ // Adjust how this is hydrated only when the version of Astro supports `astroStaticSlot`
+ return metadata.astroStaticSlot ? !!metadata.hydrate : true;
+}
+
+async function renderToStaticMarkup(
+ this: RendererContext,
+ Component: any,
+ props: Record<string, any>,
+ slotted: Record<string, any>,
+ metadata: AstroComponentMetadata,
+) {
+ const tagName = needsHydration(metadata) ? 'astro-slot' : 'astro-static-slot';
+
+ let children = undefined;
+ let $$slots: Record<string, any> | undefined = undefined;
+ let idPrefix;
+ if (this && this.result) {
+ idPrefix = incrementId(this.result);
+ }
+ const renderProps: Record<string, any> = {};
+ for (const [key, value] of Object.entries(slotted)) {
+ // Legacy slot support
+ $$slots ??= {};
+ if (key === 'default') {
+ $$slots.default = true;
+ children = createRawSnippet(() => ({
+ render: () => `<${tagName}>${value}</${tagName}>`,
+ }));
+ } else {
+ $$slots[key] = createRawSnippet(() => ({
+ render: () => `<${tagName} name="${key}">${value}</${tagName}>`,
+ }));
+ }
+ // @render support for Svelte ^5.0
+ const slotName = key === 'default' ? 'children' : key;
+ renderProps[slotName] = createRawSnippet(() => ({
+ render: () => `<${tagName}${key !== 'default' ? ` name="${key}"` : ''}>${value}</${tagName}>`,
+ }));
+ }
+
+ const result = render(Component, {
+ props: {
+ ...props,
+ children,
+ $$slots,
+ ...renderProps,
+ },
+ idPrefix,
+ });
+ return { html: result.body };
+}
+
+export default {
+ name: '@astrojs/svelte',
+ check,
+ renderToStaticMarkup,
+ supportsAstroStaticSlot: true,
+};
diff --git a/packages/integrations/svelte/src/types.ts b/packages/integrations/svelte/src/types.ts
new file mode 100644
index 000000000..86834336e
--- /dev/null
+++ b/packages/integrations/svelte/src/types.ts
@@ -0,0 +1,4 @@
+import type { SSRResult } from 'astro';
+export type RendererContext = {
+ result: SSRResult;
+}; \ No newline at end of file