aboutsummaryrefslogtreecommitdiff
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.ts85
-rw-r--r--packages/integrations/svelte/src/context.ts26
-rw-r--r--packages/integrations/svelte/src/editor.cts21
-rw-r--r--packages/integrations/svelte/src/index.ts40
-rw-r--r--packages/integrations/svelte/src/server.ts76
-rw-r--r--packages/integrations/svelte/src/types.ts4
6 files changed, 252 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..273c9753a
--- /dev/null
+++ b/packages/integrations/svelte/src/client.svelte.ts
@@ -0,0 +1,85 @@
+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/editor.cts b/packages/integrations/svelte/src/editor.cts
new file mode 100644
index 000000000..42a72913d
--- /dev/null
+++ b/packages/integrations/svelte/src/editor.cts
@@ -0,0 +1,21 @@
+import { svelte2tsx } from 'svelte2tsx';
+
+export function toTSX(code: string, className: string): string {
+ let result = `
+ let ${className}__AstroComponent_: Error
+ export default ${className}__AstroComponent_
+ `;
+
+ try {
+ let tsx = svelte2tsx(code, { mode: 'ts' }).code;
+ tsx = '/// <reference types="svelte2tsx/svelte-shims" />\n' + tsx;
+ result = tsx.replace(
+ 'export default class extends __sveltets_2_createSvelte2TsxComponent(',
+ `export default function ${className}__AstroComponent_(_props: typeof Component.props): any {}\nlet Component = `,
+ );
+ } catch {
+ return result;
+ }
+
+ return result;
+}
diff --git a/packages/integrations/svelte/src/index.ts b/packages/integrations/svelte/src/index.ts
new file mode 100644
index 000000000..0db02aff3
--- /dev/null
+++ b/packages/integrations/svelte/src/index.ts
@@ -0,0 +1,40 @@
+import type { Options } from '@sveltejs/vite-plugin-svelte';
+import { svelte, vitePreprocess } from '@sveltejs/vite-plugin-svelte';
+import type { AstroIntegration, AstroRenderer, ContainerRenderer } from 'astro';
+
+function getRenderer(): AstroRenderer {
+ return {
+ name: '@astrojs/svelte',
+ clientEntrypoint: '@astrojs/svelte/client.js',
+ serverEntrypoint: '@astrojs/svelte/server.js',
+ };
+}
+
+export function getContainerRenderer(): ContainerRenderer {
+ return {
+ name: '@astrojs/svelte',
+ serverEntrypoint: '@astrojs/svelte/server.js',
+ };
+}
+
+export default function svelteIntegration(options?: Options): AstroIntegration {
+ return {
+ name: '@astrojs/svelte',
+ hooks: {
+ 'astro:config:setup': async ({ updateConfig, addRenderer }) => {
+ addRenderer(getRenderer());
+ updateConfig({
+ vite: {
+ optimizeDeps: {
+ include: ['@astrojs/svelte/client.js'],
+ exclude: ['@astrojs/svelte/server.js'],
+ },
+ plugins: [svelte(options)],
+ },
+ });
+ },
+ },
+ };
+}
+
+export { vitePreprocess };
diff --git a/packages/integrations/svelte/src/server.ts b/packages/integrations/svelte/src/server.ts
new file mode 100644
index 000000000..14f869228
--- /dev/null
+++ b/packages/integrations/svelte/src/server.ts
@@ -0,0 +1,76 @@
+import type { AstroComponentMetadata, NamedSSRLoadedRendererValue } 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 };
+}
+
+const renderer: NamedSSRLoadedRendererValue = {
+ name: '@astrojs/svelte',
+ check,
+ renderToStaticMarkup,
+ supportsAstroStaticSlot: true,
+};
+
+export default renderer;
diff --git a/packages/integrations/svelte/src/types.ts b/packages/integrations/svelte/src/types.ts
new file mode 100644
index 000000000..5dff5b0b4
--- /dev/null
+++ b/packages/integrations/svelte/src/types.ts
@@ -0,0 +1,4 @@
+import type { SSRResult } from 'astro';
+export type RendererContext = {
+ result: SSRResult;
+};