diff options
Diffstat (limited to 'packages/integrations/svelte/src')
-rw-r--r-- | packages/integrations/svelte/src/client.svelte.ts | 85 | ||||
-rw-r--r-- | packages/integrations/svelte/src/context.ts | 26 | ||||
-rw-r--r-- | packages/integrations/svelte/src/editor.cts | 21 | ||||
-rw-r--r-- | packages/integrations/svelte/src/index.ts | 40 | ||||
-rw-r--r-- | packages/integrations/svelte/src/server.ts | 76 | ||||
-rw-r--r-- | packages/integrations/svelte/src/types.ts | 4 |
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; +}; |