diff options
author | 2024-11-14 15:31:51 +0000 | |
---|---|---|
committer | 2024-11-14 23:31:51 +0800 | |
commit | 9fc2ab8cc848739a21bfa3f754e9bec4926dc034 (patch) | |
tree | a184ada6711296569a064c01defd2fa6a74f63c5 /packages/integrations/svelte | |
parent | bdc0890061533466da19660ff83a331a3136f6c4 (diff) | |
download | astro-9fc2ab8cc848739a21bfa3f754e9bec4926dc034.tar.gz astro-9fc2ab8cc848739a21bfa3f754e9bec4926dc034.tar.zst astro-9fc2ab8cc848739a21bfa3f754e9bec4926dc034.zip |
Update to svelte 5 (#12364)
Co-authored-by: bluwy <bjornlu.dev@gmail.com>
Diffstat (limited to 'packages/integrations/svelte')
-rw-r--r-- | packages/integrations/svelte/client-v5.js | 60 | ||||
-rw-r--r-- | packages/integrations/svelte/client.js | 125 | ||||
-rw-r--r-- | packages/integrations/svelte/client.svelte.js | 79 | ||||
-rw-r--r-- | packages/integrations/svelte/package.json | 18 | ||||
-rw-r--r-- | packages/integrations/svelte/server-v5.d.ts | 2 | ||||
-rw-r--r-- | packages/integrations/svelte/server-v5.js | 57 | ||||
-rw-r--r-- | packages/integrations/svelte/server.js | 48 | ||||
-rw-r--r-- | packages/integrations/svelte/src/index.ts | 99 |
8 files changed, 138 insertions, 350 deletions
diff --git a/packages/integrations/svelte/client-v5.js b/packages/integrations/svelte/client-v5.js deleted file mode 100644 index 123e544f6..000000000 --- a/packages/integrations/svelte/client-v5.js +++ /dev/null @@ -1,60 +0,0 @@ -import { createRawSnippet, hydrate, mount, unmount } from 'svelte'; - -const existingApplications = new WeakMap(); - -export default (element) => { - return async (Component, props, slotted, { client }) => { - if (!element.hasAttribute('ssr')) return; - - let children = undefined; - let $$slots = undefined; - let renderFns = {}; - - 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 bootstrap = client !== 'only' ? hydrate : mount; - if (existingApplications.has(element)) { - existingApplications.get(element).$set({ - ...props, - children, - $$slots, - ...renderFns, - }); - } else { - const component = bootstrap(Component, { - target: element, - props: { - ...props, - children, - $$slots, - ...renderFns, - }, - }); - existingApplications.set(element, component); - element.addEventListener('astro:unmount', () => unmount(component), { once: true }); - } - }; -}; diff --git a/packages/integrations/svelte/client.js b/packages/integrations/svelte/client.js deleted file mode 100644 index 288c7a661..000000000 --- a/packages/integrations/svelte/client.js +++ /dev/null @@ -1,125 +0,0 @@ -const noop = () => {}; - -let originalConsoleWarning; -let consoleFilterRefs = 0; - -const existingApplications = new WeakMap(); - -export default (element) => { - return (Component, props, slotted, { client }) => { - if (!element.hasAttribute('ssr')) return; - const slots = {}; - for (const [key, value] of Object.entries(slotted)) { - slots[key] = createSlotDefinition(key, value); - } - - try { - if (import.meta.env.DEV) useConsoleFilter(); - - if (existingApplications.has(element)) { - existingApplications.get(element).$set({ ...props, $$slots: slots, $$scope: { ctx: [] } }); - } else { - const component = new Component({ - target: element, - props: { - ...props, - $$slots: slots, - $$scope: { ctx: [] }, - }, - hydrate: client !== 'only', - $$inline: true, - }); - existingApplications.set(element, component); - - element.addEventListener('astro:unmount', () => component.$destroy(), { once: true }); - } - } finally { - if (import.meta.env.DEV) finishUsingConsoleFilter(); - } - }; -}; - -function createSlotDefinition(key, children) { - let parent; - return [ - () => ({ - // mount - m(target) { - parent = target; - target.insertAdjacentHTML( - 'beforeend', - `<astro-slot${key === 'default' ? '' : ` name="${key}"`}>${children}</astro-slot>`, - ); - }, - // create - c: noop, - // hydrate - l: noop, - // destroy - d() { - if (!parent) return; - const slot = parent.querySelector( - `astro-slot${key === 'default' ? ':not([name])' : `[name="${key}"]`}`, - ); - if (slot) slot.remove(); - }, - }), - noop, - noop, - ]; -} - -/** - * Reduces console noise by filtering known non-problematic warnings. - * - * Performs reference counting to allow parallel usage from async code. - * - * To stop filtering, please ensure that there always is a matching call - * to `finishUsingConsoleFilter` afterwards. - */ -function useConsoleFilter() { - consoleFilterRefs++; - - if (!originalConsoleWarning) { - originalConsoleWarning = console.warn; - try { - console.warn = filteredConsoleWarning; - } catch { - // If we're unable to hook `console.warn`, just accept it - } - } -} - -/** - * Indicates that the filter installed by `useConsoleFilter` - * is no longer needed by the calling code. - */ -function finishUsingConsoleFilter() { - consoleFilterRefs--; - - // Note: Instead of reverting `console.warning` back to the original - // when the reference counter reaches 0, we leave our hook installed - // to prevent potential race conditions once `check` is made async -} - -/** - * Hook/wrapper function for the global `console.warning` function. - * - * Ignores known non-problematic errors while any code is using the console filter. - * Otherwise, simply forwards all arguments to the original function. - */ -function filteredConsoleWarning(msg, ...rest) { - if (consoleFilterRefs > 0 && typeof msg === 'string') { - // Astro passes `class` and `data-astro-cid` props to the Svelte component, which - // outputs the following warning, which we can safely filter out. - - // NOTE: In practice data-astro-cid props have a hash suffix. Hence the use of a - // quoted prop name string without a closing quote. - - const isKnownSvelteError = - msg.endsWith("was created with unknown prop 'class'") || - msg.includes("was created with unknown prop 'data-astro-cid"); - if (isKnownSvelteError) return; - } - originalConsoleWarning(msg, ...rest); -} diff --git a/packages/integrations/svelte/client.svelte.js b/packages/integrations/svelte/client.svelte.js new file mode 100644 index 000000000..1bff1bf24 --- /dev/null +++ b/packages/integrations/svelte/client.svelte.js @@ -0,0 +1,79 @@ +import { createRawSnippet, hydrate, mount, unmount } from 'svelte'; + +/** @type {WrakMap<any, ReturnType<typeof createComponent>} */ +const existingApplications = new WeakMap(); + +export default (element) => { + return async (Component, props, slotted, { client }) => { + if (!element.hasAttribute('ssr')) return; + + let children = undefined; + let _$$slots = undefined; + let renderFns = {}; + + 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 }); + } + }; +}; + +/** + * @param {any} Component + * @param {HTMLElement} target + * @param {Record<string, any>} props + * @param {boolean} shouldHydrate + */ +function createComponent(Component, target, props, shouldHydrate) { + let propsState = $state(props); + const bootstrap = shouldHydrate ? hydrate : mount; + const component = bootstrap(Component, { target, props: propsState }); + return { + setProps(newProps) { + 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/package.json b/packages/integrations/svelte/package.json index 08a87c366..7c38648d1 100644 --- a/packages/integrations/svelte/package.json +++ b/packages/integrations/svelte/package.json @@ -23,26 +23,18 @@ ".": "./dist/index.js", "./editor": "./dist/editor.cjs", "./*": "./*", - "./client.js": "./client.js", - "./client-v5.js": "./client-v5.js", + "./client.js": "./client.svelte.js", "./server.js": { "default": "./server.js", "types": "./server.d.ts" }, - "./server-v5.js": { - "default": "./server-v5.js", - "types": "./server-v5.d.ts" - }, "./package.json": "./package.json" }, "files": [ "dist", "client.js", - "client-v5.js", "server.js", - "server.d.ts", - "server-v5.js", - "server-v5.d.ts" + "server.d.ts" ], "scripts": { "build": "astro-scripts build \"src/index.ts\" && astro-scripts build \"src/editor.cts\" --force-cjs --no-clean-dist && tsc", @@ -50,18 +42,18 @@ "dev": "astro-scripts dev \"src/**/*.ts\"" }, "dependencies": { - "@sveltejs/vite-plugin-svelte": "^3.1.2", + "@sveltejs/vite-plugin-svelte": "^4.0.0", "svelte2tsx": "^0.7.22" }, "devDependencies": { "astro": "workspace:*", "astro-scripts": "workspace:*", - "svelte": "^4.2.19", + "svelte": "^5.1.16", "vite": "^5.4.10" }, "peerDependencies": { "astro": "^4.0.0", - "svelte": "^4.0.0 || ^5.0.0-next.190", + "svelte": "^5.1.16", "typescript": "^5.3.3" }, "engines": { diff --git a/packages/integrations/svelte/server-v5.d.ts b/packages/integrations/svelte/server-v5.d.ts deleted file mode 100644 index bb2f29556..000000000 --- a/packages/integrations/svelte/server-v5.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -import type { NamedSSRLoadedRendererValue } from 'astro'; -export default NamedSSRLoadedRendererValue; diff --git a/packages/integrations/svelte/server-v5.js b/packages/integrations/svelte/server-v5.js deleted file mode 100644 index a38d38260..000000000 --- a/packages/integrations/svelte/server-v5.js +++ /dev/null @@ -1,57 +0,0 @@ -import { createRawSnippet } from 'svelte'; -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; - const renderProps = {}; - - 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, - }, - }); - return { html: result.body }; -} - -export default { - check, - renderToStaticMarkup, - supportsAstroStaticSlot: true, -}; diff --git a/packages/integrations/svelte/server.js b/packages/integrations/svelte/server.js index 9878d3b59..ac133dced 100644 --- a/packages/integrations/svelte/server.js +++ b/packages/integrations/svelte/server.js @@ -1,5 +1,13 @@ +import { createRawSnippet } from 'svelte'; +import { render } from 'svelte/server'; + function check(Component) { - return Component['render'] && Component['$$render']; + 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) { @@ -9,16 +17,44 @@ function needsHydration(metadata) { async function renderToStaticMarkup(Component, props, slotted, metadata) { const tagName = needsHydration(metadata) ? 'astro-slot' : 'astro-static-slot'; - const slots = {}; + + let children = undefined; + let $$slots = undefined; + const renderProps = {}; + for (const [key, value] of Object.entries(slotted)) { - slots[key] = () => - `<${tagName}${key === 'default' ? '' : ` name="${key}"`}>${value}</${tagName}>`; + // 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 { html } = Component.render(props, { $$slots: slots }); - return { html }; + + const result = render(Component, { + props: { + ...props, + children, + $$slots, + ...renderProps, + }, + }); + return { html: result.body }; } export default { + name: '@astrojs/svelte', check, renderToStaticMarkup, supportsAstroStaticSlot: true, diff --git a/packages/integrations/svelte/src/index.ts b/packages/integrations/svelte/src/index.ts index b0db3505c..0db02aff3 100644 --- a/packages/integrations/svelte/src/index.ts +++ b/packages/integrations/svelte/src/index.ts @@ -1,111 +1,36 @@ -import { fileURLToPath } from 'node:url'; import type { Options } from '@sveltejs/vite-plugin-svelte'; import { svelte, vitePreprocess } from '@sveltejs/vite-plugin-svelte'; import type { AstroIntegration, AstroRenderer, ContainerRenderer } from 'astro'; -import { VERSION } from 'svelte/compiler'; -import type { UserConfig } from 'vite'; - -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: isSvelte5 ? '@astrojs/svelte/server-v5.js' : '@astrojs/svelte/server.js', + clientEntrypoint: '@astrojs/svelte/client.js', + serverEntrypoint: '@astrojs/svelte/server.js', }; } export function getContainerRenderer(): ContainerRenderer { return { name: '@astrojs/svelte', - serverEntrypoint: isSvelte5 ? '@astrojs/svelte/server-v5.js' : '@astrojs/svelte/server.js', - }; -} - -async function svelteConfigHasPreprocess(root: URL) { - const svelteConfigFiles = ['./svelte.config.js', './svelte.config.cjs', './svelte.config.mjs']; - for (const file of svelteConfigFiles) { - const filePath = fileURLToPath(new URL(file, root)); - try { - // Suppress warnings by vite: "The above dynamic import cannot be analyzed by Vite." - const config = (await import(/* @vite-ignore */ filePath)).default; - return !!config.preprocess; - } catch {} - } -} - -type ViteConfigurationArgs = { - isDev: boolean; - options?: Options | OptionsCallback; - root: URL; -}; - -async function getViteConfiguration({ - options, - isDev, - root, -}: ViteConfigurationArgs): Promise<UserConfig> { - const defaultOptions: Partial<Options> = { - emitCss: 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; - } - - let resolvedOptions: Partial<Options>; - - if (!options) { - resolvedOptions = defaultOptions; - } else if (typeof options === 'function') { - resolvedOptions = options(defaultOptions); - } else { - resolvedOptions = { - ...options, - ...defaultOptions, - compilerOptions: { - ...options.compilerOptions, - // Always use dev and hydratable from defaults - ...defaultOptions.compilerOptions, - }, - }; - } - - if (!resolvedOptions.preprocess && !(await svelteConfigHasPreprocess(root))) { - resolvedOptions.preprocess = vitePreprocess(); - } - - return { - optimizeDeps: { - 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)], + serverEntrypoint: '@astrojs/svelte/server.js', }; } -type OptionsCallback = (defaultOptions: Options) => Options; -export default function (options?: Options | OptionsCallback): AstroIntegration { +export default function svelteIntegration(options?: Options): AstroIntegration { return { name: '@astrojs/svelte', hooks: { - // Anything that gets returned here is merged into Astro Config - 'astro:config:setup': async ({ command, updateConfig, addRenderer, config }) => { + 'astro:config:setup': async ({ updateConfig, addRenderer }) => { addRenderer(getRenderer()); updateConfig({ - vite: await getViteConfiguration({ - options, - isDev: command === 'dev', - root: config.root, - }), + vite: { + optimizeDeps: { + include: ['@astrojs/svelte/client.js'], + exclude: ['@astrojs/svelte/server.js'], + }, + plugins: [svelte(options)], + }, }); }, }, |