fix: return component exports from `render`

pull/18058/head
paoloricciuti 4 months ago
parent 8a82750657
commit ac62333832

@ -40,19 +40,24 @@ import { get_parent_node, remove_child } from '../dom/operations.js';
* @template {object} [TTextNode=object]
* @template {object} [TComment=object]
* @param {Renderer<TFragment, TElement, TTextNode, TComment>} renderer
* @returns {Renderer<TFragment, TElement, TTextNode, TComment> & { render: <Props extends Record<string, any>>(component: ComponentType<SvelteComponent<Props>> | Component<Props, any, any>, options: {} extends Props ? { target: TFragment | TElement | TTextNode | TComment, props?: Props, context?: Map<any, any> } : { target: TFragment | TElement | TTextNode | TComment, props: Props, context?: Map<any, any> }) => () => void }}
* @returns {Renderer<TFragment, TElement, TTextNode, TComment> & { render: <Props extends Record<string, any>, Exports extends Record<string, any>>(component: ComponentType<SvelteComponent<Props>> | Component<Props, Exports, any>, options: {} extends Props ? { target: TFragment | TElement | TTextNode | TComment, props?: Props, context?: Map<any, any> } : { target: TFragment | TElement | TTextNode | TComment, props: Props, context?: Map<any, any> }) => { component: Exports, unmount: () => void } }}
*/
export function createRenderer(renderer) {
return {
...renderer,
/**
* @template {Record<string, any>} Props
* @param {ComponentType<SvelteComponent<Props>> | Component<Props, any, any>} Component
* @template {Record<string, any>} Exports
* @param {ComponentType<SvelteComponent<Props>> | Component<Props, Exports, any>} Component
* @param {{} extends Props ? { target: TFragment | TElement | TTextNode | TComment, props?: Props, context?: Map<any, any> } : { target: TFragment | TElement | TTextNode | TComment, props: Props, context?: Map<any, any> }} 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();
}

@ -0,0 +1,29 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
html: '<p>0</p>',
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), '<p>1</p>');
// Call it a few more times
component.increment();
component.increment();
flushSync();
assert.equal(component.get_count(), 3);
assert.equal(serialize(target), '<p>3</p>');
}
});

@ -0,0 +1,13 @@
<script>
let count = $state(0);
export function increment() {
count += 1;
}
export function get_count() {
return count;
}
</script>
<p>{count}</p>

@ -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<string, any> | 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,

@ -2569,7 +2569,7 @@ declare module 'svelte/reactivity/window' {
declare module 'svelte/renderer' {
export function createRenderer<TFragment extends object = object, TElement extends object = object, TTextNode extends object = object, TComment extends object = object>(renderer: Renderer<TFragment, TElement, TTextNode, TComment>): Renderer<TFragment, TElement, TTextNode, TComment> & {
render: <Props extends Record<string, any>>(component: ComponentType<SvelteComponent<Props>> | Component<Props, any, any>, options: {} extends Props ? {
render: <Props extends Record<string, any>, Exports extends Record<string, any>>(component: ComponentType<SvelteComponent<Props>> | Component<Props, Exports, any>, options: {} extends Props ? {
target: TFragment | TElement | TTextNode | TComment;
props?: Props;
context?: Map<any, any>;
@ -2577,7 +2577,10 @@ declare module 'svelte/renderer' {
target: TFragment | TElement | TTextNode | TComment;
props: Props;
context?: Map<any, any>;
}) => () => void;
}) => {
component: Exports;
unmount: () => void;
};
};
export type Renderer<TFragment extends object = object, TElement extends object = object, TTextNode extends object = object, TComment extends object = object, TNode = TFragment | TElement | TTextNode | TComment> = {
/**

Loading…
Cancel
Save