diff options
author | 2025-04-22 13:05:13 +0200 | |
---|---|---|
committer | 2025-04-22 12:05:13 +0100 | |
commit | a19a185efd75334f2f417b433fcfaa0017fe41ee (patch) | |
tree | 853831aa7f68a8e0ee1aab0b231ba5d22b42c61c /packages/integrations/svelte/src | |
parent | 620d15d8483dfb1822cd47833bc1653e0b704ccb (diff) | |
download | astro-a19a185efd75334f2f417b433fcfaa0017fe41ee.tar.gz astro-a19a185efd75334f2f417b433fcfaa0017fe41ee.tar.zst astro-a19a185efd75334f2f417b433fcfaa0017fe41ee.zip |
feat: convert integrations to TS (#13663)
Diffstat (limited to 'packages/integrations/svelte/src')
-rw-r--r-- | packages/integrations/svelte/src/client.svelte.ts | 86 | ||||
-rw-r--r-- | packages/integrations/svelte/src/context.ts | 26 | ||||
-rw-r--r-- | packages/integrations/svelte/src/server.ts | 74 | ||||
-rw-r--r-- | packages/integrations/svelte/src/types.ts | 4 |
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 |