From cc6293af685e6c1f844e2841b332a3644ea29e8a Mon Sep 17 00:00:00 2001 From: Bjorn Lu Date: Wed, 15 Nov 2023 23:40:23 +0800 Subject: [PATCH] Support Svelte 5 (experimental) (#9098) Co-authored-by: Nate Moore --- .changeset/twelve-mails-drive.md | 5 +++ packages/integrations/svelte/README.md | 2 +- packages/integrations/svelte/client-v5.js | 43 +++++++++++++++++++++++ packages/integrations/svelte/package.json | 10 ++++-- packages/integrations/svelte/server-v5.js | 42 ++++++++++++++++++++++ packages/integrations/svelte/src/index.ts | 19 +++++++--- 6 files changed, 112 insertions(+), 9 deletions(-) create mode 100644 .changeset/twelve-mails-drive.md create mode 100644 packages/integrations/svelte/client-v5.js create mode 100644 packages/integrations/svelte/server-v5.js diff --git a/.changeset/twelve-mails-drive.md b/.changeset/twelve-mails-drive.md new file mode 100644 index 0000000000000..b8ff5d6e58f41 --- /dev/null +++ b/.changeset/twelve-mails-drive.md @@ -0,0 +1,5 @@ +--- +'@astrojs/svelte': patch +--- + +Adds experimental support for Svelte 5 diff --git a/packages/integrations/svelte/README.md b/packages/integrations/svelte/README.md index 5df61fab09fc5..25513790bae77 100644 --- a/packages/integrations/svelte/README.md +++ b/packages/integrations/svelte/README.md @@ -1,6 +1,6 @@ # @astrojs/svelte 🧡 -This **[Astro integration][astro-integration]** enables server-side rendering and client-side hydration for your [Svelte](https://svelte.dev/) components. +This **[Astro integration][astro-integration]** enables server-side rendering and client-side hydration for your [Svelte](https://svelte.dev/) components. It supports Svelte 3, 4, and 5 (experimental). ## Installation diff --git a/packages/integrations/svelte/client-v5.js b/packages/integrations/svelte/client-v5.js new file mode 100644 index 0000000000000..b3d2e1964eb8e --- /dev/null +++ b/packages/integrations/svelte/client-v5.js @@ -0,0 +1,43 @@ +import { mount } from 'svelte'; + +export default (element) => { + return async (Component, props, slotted) => { + if (!element.hasAttribute('ssr')) return; + + let children = undefined; + let $$slots = undefined; + for (const [key, value] of Object.entries(slotted)) { + if (key === 'default') { + children = createSlotDefinition(key, value); + } else { + $$slots ??= {}; + $$slots[key] = createSlotDefinition(key, value); + } + } + + const [, destroy] = mount(Component, { + target: element, + props: { + ...props, + children, + $$slots, + }, + }); + + element.addEventListener('astro:unmount', () => destroy(), { once: true }); + }; +}; + +function createSlotDefinition(key, children) { + /** + * @param {Comment} $$anchor A comment node for slots in Svelte 5 + */ + return ($$anchor, _$$slotProps) => { + const parent = $$anchor.parentNode; + const el = document.createElement('div'); + el.innerHTML = `${children}`; + parent.insertBefore(el.children[0], $$anchor); + }; +} diff --git a/packages/integrations/svelte/package.json b/packages/integrations/svelte/package.json index 8cf1c9d872edc..65c8522f91823 100644 --- a/packages/integrations/svelte/package.json +++ b/packages/integrations/svelte/package.json @@ -24,13 +24,17 @@ "./editor": "./dist/editor.cjs", "./*": "./*", "./client.js": "./client.js", + "./client-v5.js": "./client-v5.js", "./server.js": "./server.js", + "./server-v5.js": "./server-v5.js", "./package.json": "./package.json" }, "files": [ "dist", "client.js", - "server.js" + "client-v5.js", + "server.js", + "server-v5.js" ], "scripts": { "build": "astro-scripts build \"src/index.ts\" && astro-scripts build \"src/editor.cts\" --force-cjs --no-clean-dist && tsc", @@ -38,7 +42,7 @@ "dev": "astro-scripts dev \"src/**/*.ts\"" }, "dependencies": { - "@sveltejs/vite-plugin-svelte": "^2.4.5", + "@sveltejs/vite-plugin-svelte": "^2.5.2", "svelte2tsx": "^0.6.20" }, "devDependencies": { @@ -49,7 +53,7 @@ }, "peerDependencies": { "astro": "^3.0.0", - "svelte": "^3.55.0 || ^4.0.0" + "svelte": "^3.55.0 || ^4.0.0 || ^5.0.0-next.1" }, "engines": { "node": ">=18.14.1" diff --git a/packages/integrations/svelte/server-v5.js b/packages/integrations/svelte/server-v5.js new file mode 100644 index 0000000000000..105b843fb1bf0 --- /dev/null +++ b/packages/integrations/svelte/server-v5.js @@ -0,0 +1,42 @@ +import { render } from 'svelte/server'; + +function check(Component) { + // Svelte 5 generated components always accept these two props + const str = Component.toString(); + return str.includes('$$payload') && str.includes('$$props'); +} + +function needsHydration(metadata) { + // Adjust how this is hydrated only when the version of Astro supports `astroStaticSlot` + return metadata.astroStaticSlot ? !!metadata.hydrate : true; +} + +async function renderToStaticMarkup(Component, props, slotted, metadata) { + const tagName = needsHydration(metadata) ? 'astro-slot' : 'astro-static-slot'; + + let children = undefined; + let $$slots = undefined; + for (const [key, value] of Object.entries(slotted)) { + if (key === 'default') { + children = () => `<${tagName}>${value}`; + } else { + $$slots ??= {}; + $$slots[key] = () => `<${tagName} name="${key}">${value}`; + } + } + + const { html } = render(Component, { + props: { + ...props, + children, + $$slots, + }, + }); + return { html }; +} + +export default { + check, + renderToStaticMarkup, + supportsAstroStaticSlot: true, +}; diff --git a/packages/integrations/svelte/src/index.ts b/packages/integrations/svelte/src/index.ts index a9d4f37c9ac68..5348f4c9340d9 100644 --- a/packages/integrations/svelte/src/index.ts +++ b/packages/integrations/svelte/src/index.ts @@ -1,14 +1,17 @@ import type { Options } from '@sveltejs/vite-plugin-svelte'; import { svelte, vitePreprocess } from '@sveltejs/vite-plugin-svelte'; +import { VERSION } from 'svelte/compiler'; import type { AstroIntegration, AstroRenderer } from 'astro'; import { fileURLToPath } from 'node:url'; import type { UserConfig } from 'vite'; +const isSvelte5 = Number.parseInt(VERSION.split('.').at(0)!) >= 5; + function getRenderer(): AstroRenderer { return { name: '@astrojs/svelte', - clientEntrypoint: '@astrojs/svelte/client.js', - serverEntrypoint: '@astrojs/svelte/server.js', + clientEntrypoint: isSvelte5 ? '@astrojs/svelte/client-v5.js' : '@astrojs/svelte/client.js', + serverEntrypoint: isSvelte5 ? '@astrojs/svelte/server-v5.js' : '@astrojs/svelte/server.js', }; } @@ -37,9 +40,15 @@ async function getViteConfiguration({ }: ViteConfigurationArgs): Promise { const defaultOptions: Partial = { emitCss: true, - compilerOptions: { dev: isDev, hydratable: true }, + compilerOptions: { dev: isDev }, }; + // `hydratable` does not need to be set in Svelte 5 as it's always hydratable by default + if (!isSvelte5) { + // @ts-ignore ignore Partial type above + defaultOptions.compilerOptions.hydratable = true; + } + // Disable hot mode during the build if (!isDev) { defaultOptions.hot = false; @@ -69,8 +78,8 @@ async function getViteConfiguration({ return { optimizeDeps: { - include: ['@astrojs/svelte/client.js'], - exclude: ['@astrojs/svelte/server.js'], + include: [isSvelte5 ? '@astrojs/svelte/client-v5.js' : '@astrojs/svelte/client.js'], + exclude: [isSvelte5 ? '@astrojs/svelte/server-v5.js' : '@astrojs/svelte/server.js'], }, plugins: [svelte(resolvedOptions)], };