From ac62333832cfe11d6ca21a476f2ccf83b3a60377 Mon Sep 17 00:00:00 2001 From: paoloricciuti Date: Thu, 2 Apr 2026 11:01:24 +0200 Subject: [PATCH] fix: return component exports from `render` --- .../internal/client/custom-renderer/index.js | 13 ++++++--- .../samples/component-exports/_config.js | 29 +++++++++++++++++++ .../samples/component-exports/main.svelte | 13 +++++++++ .../svelte/tests/custom-renderers/shared.ts | 7 +++-- packages/svelte/types/index.d.ts | 7 +++-- 5 files changed, 61 insertions(+), 8 deletions(-) create mode 100644 packages/svelte/tests/custom-renderers/samples/component-exports/_config.js create mode 100644 packages/svelte/tests/custom-renderers/samples/component-exports/main.svelte diff --git a/packages/svelte/src/internal/client/custom-renderer/index.js b/packages/svelte/src/internal/client/custom-renderer/index.js index ad6a83aa5d..8d68a5f060 100644 --- a/packages/svelte/src/internal/client/custom-renderer/index.js +++ b/packages/svelte/src/internal/client/custom-renderer/index.js @@ -40,19 +40,24 @@ import { get_parent_node, remove_child } from '../dom/operations.js'; * @template {object} [TTextNode=object] * @template {object} [TComment=object] * @param {Renderer} renderer - * @returns {Renderer & { render: >(component: ComponentType> | Component, options: {} extends Props ? { target: TFragment | TElement | TTextNode | TComment, props?: Props, context?: Map } : { target: TFragment | TElement | TTextNode | TComment, props: Props, context?: Map }) => () => void }} + * @returns {Renderer & { render: , Exports extends Record>(component: ComponentType> | Component, options: {} extends Props ? { target: TFragment | TElement | TTextNode | TComment, props?: Props, context?: Map } : { target: TFragment | TElement | TTextNode | TComment, props: Props, context?: Map }) => { component: Exports, unmount: () => void } }} */ export function createRenderer(renderer) { return { ...renderer, /** * @template {Record} Props - * @param {ComponentType> | Component} Component + * @template {Record} Exports + * @param {ComponentType> | Component} Component * @param {{} extends Props ? { target: TFragment | TElement | TTextNode | TComment, props?: Props, context?: Map } : { target: TFragment | TElement | TTextNode | TComment, props: Props, context?: Map }} options */ render(Component, { target, props, context }) { var cleanup = push_renderer(renderer); try { + /** @type {Exports} */ + // @ts-expect-error will be defined because the render effect runs synchronously + var component = undefined; + const unmount = effect_root(() => { var anchor = renderer.createComment(''); renderer.insert(/** @type {*} */ (target), anchor, null); @@ -61,7 +66,7 @@ export function createRenderer(renderer) { var ctx = /** @type {ComponentContext} */ (component_context); if (context) ctx.c = context; branch(() => { - /** @type {Function} */ (Component)(anchor, props ?? {}); + component = /** @type {Function} */ (Component)(anchor, props ?? {}) || {}; }); pop(); }); @@ -71,7 +76,7 @@ export function createRenderer(renderer) { if (parent) remove_child(parent, /** @type {*} */ (anchor)); }; }); - return unmount; + return { component, unmount }; } finally { cleanup(); } diff --git a/packages/svelte/tests/custom-renderers/samples/component-exports/_config.js b/packages/svelte/tests/custom-renderers/samples/component-exports/_config.js new file mode 100644 index 0000000000..07c63df3f4 --- /dev/null +++ b/packages/svelte/tests/custom-renderers/samples/component-exports/_config.js @@ -0,0 +1,29 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: '

0

', + test({ assert, target, component, serialize }) { + // Component exports should be accessible + assert.ok(typeof component.increment === 'function', 'increment should be exported'); + assert.ok(typeof component.get_count === 'function', 'get_count should be exported'); + + // Verify initial state + assert.equal(component.get_count(), 0); + + // Call exported function and verify it updates state + component.increment(); + flushSync(); + + assert.equal(component.get_count(), 1); + assert.equal(serialize(target), '

1

'); + + // Call it a few more times + component.increment(); + component.increment(); + flushSync(); + + assert.equal(component.get_count(), 3); + assert.equal(serialize(target), '

3

'); + } +}); diff --git a/packages/svelte/tests/custom-renderers/samples/component-exports/main.svelte b/packages/svelte/tests/custom-renderers/samples/component-exports/main.svelte new file mode 100644 index 0000000000..43f4023601 --- /dev/null +++ b/packages/svelte/tests/custom-renderers/samples/component-exports/main.svelte @@ -0,0 +1,13 @@ + + +

{count}

diff --git a/packages/svelte/tests/custom-renderers/shared.ts b/packages/svelte/tests/custom-renderers/shared.ts index 80461521cf..c8d735c9aa 100644 --- a/packages/svelte/tests/custom-renderers/shared.ts +++ b/packages/svelte/tests/custom-renderers/shared.ts @@ -133,13 +133,16 @@ async function run_test(cwd: string, config: CustomRendererTest, compile_options const target = create_root(); let unmount: (() => void) | undefined; + let component: Record | undefined; try { - unmount = renderer.render(mod.default, { + const result = renderer.render(mod.default, { target, props: config.props ?? {}, context: config.context }); + unmount = result.unmount; + component = result.component; } catch (err) { if (config.error) { assert.include((err as Error).message, config.error); @@ -175,7 +178,7 @@ async function run_test(cwd: string, config: CustomRendererTest, compile_options await config.test({ assert, target, - component: config.props ?? {}, + component: component ?? {}, mod, logs, warnings, diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index b455b92893..dd542392c7 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -2569,7 +2569,7 @@ declare module 'svelte/reactivity/window' { declare module 'svelte/renderer' { export function createRenderer(renderer: Renderer): Renderer & { - render: >(component: ComponentType> | Component, options: {} extends Props ? { + render: , Exports extends Record>(component: ComponentType> | Component, options: {} extends Props ? { target: TFragment | TElement | TTextNode | TComment; props?: Props; context?: Map; @@ -2577,7 +2577,10 @@ declare module 'svelte/renderer' { target: TFragment | TElement | TTextNode | TComment; props: Props; context?: Map; - }) => () => void; + }) => { + component: Exports; + unmount: () => void; + }; }; export type Renderer = { /**