summaryrefslogtreecommitdiff
path: root/packages/integrations/svelte
diff options
context:
space:
mode:
authorGravatar Jacob Jenkins <jacob_jenkins@live.com> 2024-11-14 15:31:51 +0000
committerGravatar GitHub <noreply@github.com> 2024-11-14 23:31:51 +0800
commit9fc2ab8cc848739a21bfa3f754e9bec4926dc034 (patch)
treea184ada6711296569a064c01defd2fa6a74f63c5 /packages/integrations/svelte
parentbdc0890061533466da19660ff83a331a3136f6c4 (diff)
downloadastro-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.js60
-rw-r--r--packages/integrations/svelte/client.js125
-rw-r--r--packages/integrations/svelte/client.svelte.js79
-rw-r--r--packages/integrations/svelte/package.json18
-rw-r--r--packages/integrations/svelte/server-v5.d.ts2
-rw-r--r--packages/integrations/svelte/server-v5.js57
-rw-r--r--packages/integrations/svelte/server.js48
-rw-r--r--packages/integrations/svelte/src/index.ts99
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)],
+ },
});
},
},