From fd80061b6efbfaedb1ddfdbbb5669c07fc57ba5e Mon Sep 17 00:00:00 2001 From: bluwy Date: Tue, 14 Nov 2023 23:47:03 +0800 Subject: [PATCH 1/6] Support Svelte 5 --- packages/integrations/svelte/client-v5.js | 43 +++++++++++++++++++++++ packages/integrations/svelte/package.json | 6 ++-- packages/integrations/svelte/server.js | 23 ++++++++++-- packages/integrations/svelte/src/index.ts | 14 ++++++-- pnpm-lock.yaml | 16 ++++----- 5 files changed, 87 insertions(+), 15 deletions(-) create mode 100644 packages/integrations/svelte/client-v5.js diff --git a/packages/integrations/svelte/client-v5.js b/packages/integrations/svelte/client-v5.js new file mode 100644 index 000000000000..2ef98fbb1873 --- /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 + */ + return ($$anchor, _$$props) => { + // $$anchor is a comment node for slots in Svelte 5 + const el = document.createElement('div'); + el.innerHTML = `${children}`; + $$anchor.replaceWith(el.children[0]); + }; +} diff --git a/packages/integrations/svelte/package.json b/packages/integrations/svelte/package.json index 8cf1c9d872ed..b9a3c96ae3a6 100644 --- a/packages/integrations/svelte/package.json +++ b/packages/integrations/svelte/package.json @@ -24,12 +24,14 @@ "./editor": "./dist/editor.cjs", "./*": "./*", "./client.js": "./client.js", + "./client-v5.js": "./client-v5.js", "./server.js": "./server.js", "./package.json": "./package.json" }, "files": [ "dist", "client.js", + "client-v5.js", "server.js" ], "scripts": { @@ -38,7 +40,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 +51,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.js b/packages/integrations/svelte/server.js index 9878d3b59dcf..4cce1aa0d487 100644 --- a/packages/integrations/svelte/server.js +++ b/packages/integrations/svelte/server.js @@ -1,5 +1,13 @@ +import { VERSION } from 'svelte/compiler'; + +const isSvelte5 = VERSION.startsWith('5'); + function check(Component) { - return Component['render'] && Component['$$render']; + if (isSvelte5) { + return Component.toString().includes('$$payload'); + } else { + return Component['render'] && Component['$$render']; + } } function needsHydration(metadata) { @@ -14,7 +22,18 @@ async function renderToStaticMarkup(Component, props, slotted, metadata) { slots[key] = () => `<${tagName}${key === 'default' ? '' : ` name="${key}"`}>${value}`; } - const { html } = Component.render(props, { $$slots: slots }); + let html; + if (isSvelte5) { + const { render } = await import('svelte/server'); + html = render(Component, { + props: { + ...props, + $$slots: slots, + }, + }).html; + } else { + html = Component.render(props, { $$slots: slots }).html; + } return { html }; } diff --git a/packages/integrations/svelte/src/index.ts b/packages/integrations/svelte/src/index.ts index a9d4f37c9ac6..addbb1c50a80 100644 --- a/packages/integrations/svelte/src/index.ts +++ b/packages/integrations/svelte/src/index.ts @@ -1,13 +1,16 @@ 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 = VERSION.startsWith('5'); + function getRenderer(): AstroRenderer { return { name: '@astrojs/svelte', - clientEntrypoint: '@astrojs/svelte/client.js', + clientEntrypoint: isSvelte5 ? '@astrojs/svelte/client-v5.js' : '@astrojs/svelte/client.js', serverEntrypoint: '@astrojs/svelte/server.js', }; } @@ -37,9 +40,14 @@ async function getViteConfiguration({ }: ViteConfigurationArgs): Promise { const defaultOptions: Partial = { emitCss: true, - compilerOptions: { dev: isDev, hydratable: true }, + compilerOptions: { dev: isDev }, }; + if (!isSvelte5) { + // @ts-ignore + defaultOptions.compilerOptions.hydratable = true; + } + // Disable hot mode during the build if (!isDev) { defaultOptions.hot = false; @@ -69,7 +77,7 @@ async function getViteConfiguration({ return { optimizeDeps: { - include: ['@astrojs/svelte/client.js'], + include: [isSvelte5 ? '@astrojs/svelte/client-v5.js' : '@astrojs/svelte/client.js'], exclude: ['@astrojs/svelte/server.js'], }, plugins: [svelte(resolvedOptions)], diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0147967d9d47..37616480c6b2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4578,8 +4578,8 @@ importers: packages/integrations/svelte: dependencies: '@sveltejs/vite-plugin-svelte': - specifier: ^2.4.5 - version: 2.4.6(svelte@4.2.2)(vite@4.5.0) + specifier: ^2.5.2 + version: 2.5.2(svelte@4.2.2)(vite@4.5.0) svelte2tsx: specifier: ^0.6.20 version: 0.6.23(svelte@4.2.2)(typescript@5.1.6) @@ -8237,7 +8237,7 @@ packages: string.prototype.matchall: 4.0.10 dev: false - /@sveltejs/vite-plugin-svelte-inspector@1.0.4(@sveltejs/vite-plugin-svelte@2.4.6)(svelte@4.2.2)(vite@4.5.0): + /@sveltejs/vite-plugin-svelte-inspector@1.0.4(@sveltejs/vite-plugin-svelte@2.5.2)(svelte@4.2.2)(vite@4.5.0): resolution: {integrity: sha512-zjiuZ3yydBtwpF3bj0kQNV0YXe+iKE545QGZVTaylW3eAzFr+pJ/cwK8lZEaRp4JtaJXhD5DyWAV4AxLh6DgaQ==} engines: {node: ^14.18.0 || >= 16} peerDependencies: @@ -8248,7 +8248,7 @@ packages: vite: optional: true dependencies: - '@sveltejs/vite-plugin-svelte': 2.4.6(svelte@4.2.2)(vite@4.5.0) + '@sveltejs/vite-plugin-svelte': 2.5.2(svelte@4.2.2)(vite@4.5.0) debug: 4.3.4(supports-color@8.1.1) svelte: 4.2.2 vite: 4.5.0(@types/node@18.18.6)(sass@1.69.4) @@ -8256,17 +8256,17 @@ packages: - supports-color dev: false - /@sveltejs/vite-plugin-svelte@2.4.6(svelte@4.2.2)(vite@4.5.0): - resolution: {integrity: sha512-zO79p0+DZnXPnF0ltIigWDx/ux7Ni+HRaFOw720Qeivc1azFUrJxTl0OryXVibYNx1hCboGia1NRV3x8RNv4cA==} + /@sveltejs/vite-plugin-svelte@2.5.2(svelte@4.2.2)(vite@4.5.0): + resolution: {integrity: sha512-Dfy0Rbl+IctOVfJvWGxrX/3m6vxPLH8o0x+8FA5QEyMUQMo4kGOVIojjryU7YomBAexOTAuYf1RT7809yDziaA==} engines: {node: ^14.18.0 || >= 16} peerDependencies: - svelte: ^3.54.0 || ^4.0.0 + svelte: ^3.54.0 || ^4.0.0 || ^5.0.0-next.0 vite: ^4.0.0 peerDependenciesMeta: vite: optional: true dependencies: - '@sveltejs/vite-plugin-svelte-inspector': 1.0.4(@sveltejs/vite-plugin-svelte@2.4.6)(svelte@4.2.2)(vite@4.5.0) + '@sveltejs/vite-plugin-svelte-inspector': 1.0.4(@sveltejs/vite-plugin-svelte@2.5.2)(svelte@4.2.2)(vite@4.5.0) debug: 4.3.4(supports-color@8.1.1) deepmerge: 4.3.1 kleur: 4.1.5 From 287f78436e5b761f032f5f36149d3f7c4edf6126 Mon Sep 17 00:00:00 2001 From: Bjorn Lu Date: Wed, 15 Nov 2023 21:27:44 +0800 Subject: [PATCH 2/6] Update packages/integrations/svelte/src/index.ts Co-authored-by: Nate Moore --- packages/integrations/svelte/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/integrations/svelte/src/index.ts b/packages/integrations/svelte/src/index.ts index addbb1c50a80..cd96a8b5b592 100644 --- a/packages/integrations/svelte/src/index.ts +++ b/packages/integrations/svelte/src/index.ts @@ -5,7 +5,7 @@ import type { AstroIntegration, AstroRenderer } from 'astro'; import { fileURLToPath } from 'node:url'; import type { UserConfig } from 'vite'; -const isSvelte5 = VERSION.startsWith('5'); +const isSvelte5 = Number.parseInt(VERSION.split('.').at(0)) >= 5; function getRenderer(): AstroRenderer { return { From cf38b83175f0c47aae0db258ea25a5db889e85e8 Mon Sep 17 00:00:00 2001 From: bluwy Date: Wed, 15 Nov 2023 21:50:59 +0800 Subject: [PATCH 3/6] Fix server build --- packages/integrations/svelte/README.md | 2 +- packages/integrations/svelte/package.json | 4 ++- packages/integrations/svelte/server-v5.js | 42 +++++++++++++++++++++++ packages/integrations/svelte/server.js | 23 ++----------- packages/integrations/svelte/src/index.ts | 9 ++--- 5 files changed, 53 insertions(+), 27 deletions(-) create mode 100644 packages/integrations/svelte/server-v5.js diff --git a/packages/integrations/svelte/README.md b/packages/integrations/svelte/README.md index 5df61fab09fc..25513790bae7 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/package.json b/packages/integrations/svelte/package.json index b9a3c96ae3a6..65c8522f9182 100644 --- a/packages/integrations/svelte/package.json +++ b/packages/integrations/svelte/package.json @@ -26,13 +26,15 @@ "./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", "client-v5.js", - "server.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", diff --git a/packages/integrations/svelte/server-v5.js b/packages/integrations/svelte/server-v5.js new file mode 100644 index 000000000000..5e13e3abbb20 --- /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, + }, + }).html; + return { html }; +} + +export default { + check, + renderToStaticMarkup, + supportsAstroStaticSlot: true, +}; diff --git a/packages/integrations/svelte/server.js b/packages/integrations/svelte/server.js index 4cce1aa0d487..9878d3b59dcf 100644 --- a/packages/integrations/svelte/server.js +++ b/packages/integrations/svelte/server.js @@ -1,13 +1,5 @@ -import { VERSION } from 'svelte/compiler'; - -const isSvelte5 = VERSION.startsWith('5'); - function check(Component) { - if (isSvelte5) { - return Component.toString().includes('$$payload'); - } else { - return Component['render'] && Component['$$render']; - } + return Component['render'] && Component['$$render']; } function needsHydration(metadata) { @@ -22,18 +14,7 @@ async function renderToStaticMarkup(Component, props, slotted, metadata) { slots[key] = () => `<${tagName}${key === 'default' ? '' : ` name="${key}"`}>${value}`; } - let html; - if (isSvelte5) { - const { render } = await import('svelte/server'); - html = render(Component, { - props: { - ...props, - $$slots: slots, - }, - }).html; - } else { - html = Component.render(props, { $$slots: slots }).html; - } + const { html } = Component.render(props, { $$slots: slots }); return { html }; } diff --git a/packages/integrations/svelte/src/index.ts b/packages/integrations/svelte/src/index.ts index cd96a8b5b592..5348f4c9340d 100644 --- a/packages/integrations/svelte/src/index.ts +++ b/packages/integrations/svelte/src/index.ts @@ -5,13 +5,13 @@ 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; +const isSvelte5 = Number.parseInt(VERSION.split('.').at(0)!) >= 5; function getRenderer(): AstroRenderer { return { name: '@astrojs/svelte', clientEntrypoint: isSvelte5 ? '@astrojs/svelte/client-v5.js' : '@astrojs/svelte/client.js', - serverEntrypoint: '@astrojs/svelte/server.js', + serverEntrypoint: isSvelte5 ? '@astrojs/svelte/server-v5.js' : '@astrojs/svelte/server.js', }; } @@ -43,8 +43,9 @@ async function getViteConfiguration({ compilerOptions: { dev: isDev }, }; + // `hydratable` does not need to be set in Svelte 5 as it's always hydratable by default if (!isSvelte5) { - // @ts-ignore + // @ts-ignore ignore Partial type above defaultOptions.compilerOptions.hydratable = true; } @@ -78,7 +79,7 @@ async function getViteConfiguration({ return { optimizeDeps: { include: [isSvelte5 ? '@astrojs/svelte/client-v5.js' : '@astrojs/svelte/client.js'], - exclude: ['@astrojs/svelte/server.js'], + exclude: [isSvelte5 ? '@astrojs/svelte/server-v5.js' : '@astrojs/svelte/server.js'], }, plugins: [svelte(resolvedOptions)], }; From 98cc25c13d185a610d9b53fdbb2f4c1758a166f5 Mon Sep 17 00:00:00 2001 From: bluwy Date: Wed, 15 Nov 2023 22:33:58 +0800 Subject: [PATCH 4/6] Fix slot insertion with sibling elements --- packages/integrations/svelte/client-v5.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/integrations/svelte/client-v5.js b/packages/integrations/svelte/client-v5.js index 2ef98fbb1873..b3d2e1964eb8 100644 --- a/packages/integrations/svelte/client-v5.js +++ b/packages/integrations/svelte/client-v5.js @@ -30,14 +30,14 @@ export default (element) => { function createSlotDefinition(key, children) { /** - * @param {Comment} $$anchor + * @param {Comment} $$anchor A comment node for slots in Svelte 5 */ - return ($$anchor, _$$props) => { - // $$anchor is a comment node for slots in Svelte 5 + return ($$anchor, _$$slotProps) => { + const parent = $$anchor.parentNode; const el = document.createElement('div'); el.innerHTML = `${children}`; - $$anchor.replaceWith(el.children[0]); + parent.insertBefore(el.children[0], $$anchor); }; } From 7ec96ce1dea3bd56901f36d04a04e990a83e7619 Mon Sep 17 00:00:00 2001 From: bluwy Date: Wed, 15 Nov 2023 22:35:19 +0800 Subject: [PATCH 5/6] Tweak --- packages/integrations/svelte/server-v5.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/integrations/svelte/server-v5.js b/packages/integrations/svelte/server-v5.js index 5e13e3abbb20..105b843fb1bf 100644 --- a/packages/integrations/svelte/server-v5.js +++ b/packages/integrations/svelte/server-v5.js @@ -25,13 +25,13 @@ async function renderToStaticMarkup(Component, props, slotted, metadata) { } } - const html = render(Component, { + const { html } = render(Component, { props: { ...props, children, $$slots, }, - }).html; + }); return { html }; } From 6e495744cf0482265c2255c19154d43f0f250c21 Mon Sep 17 00:00:00 2001 From: bluwy Date: Wed, 15 Nov 2023 22:35:58 +0800 Subject: [PATCH 6/6] Add changeset --- .changeset/twelve-mails-drive.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/twelve-mails-drive.md diff --git a/.changeset/twelve-mails-drive.md b/.changeset/twelve-mails-drive.md new file mode 100644 index 000000000000..b8ff5d6e58f4 --- /dev/null +++ b/.changeset/twelve-mails-drive.md @@ -0,0 +1,5 @@ +--- +'@astrojs/svelte': patch +--- + +Adds experimental support for Svelte 5