diff --git a/packages/svelte/src/internal/client/custom-renderer/index.js b/packages/svelte/src/internal/client/custom-renderer/index.js index 8fc51d1f77..9d6fe9e840 100644 --- a/packages/svelte/src/internal/client/custom-renderer/index.js +++ b/packages/svelte/src/internal/client/custom-renderer/index.js @@ -1,5 +1,5 @@ /** @import { ComponentContext } from '#client' */ -/** @import { Renderer } from "./types.js" */ +/** @import { Renderer, RendererNodes, DefaultNodes } from "./types.js" */ /** @import { Component, ComponentType, SvelteComponent } from '../../../index.js' */ import { boundary } from '../dom/blocks/boundary.js'; import { branch, effect_root } from '../reactivity/effects.js'; @@ -8,10 +8,11 @@ import { push_renderer } from './state.js'; import { get_parent_node, remove_child } from '../dom/operations.js'; /** - * @template {object} [TFragment=object] - * @template {object} [TElement=object] - * @template {object} [TTextNode=object] - * @template {object} [TComment=object] + * @template {RendererNodes} [T=DefaultNodes] + * @template {object} [TFragment=T extends DefaultNodes ? object : T['fragment']] + * @template {object} [TElement=T extends DefaultNodes ? object : T['element']] + * @template {object} [TTextNode=T extends DefaultNodes ? object : T['text']] + * @template {object} [TComment=T extends DefaultNodes ? object : T['comment']] * @param {Renderer} renderer * @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 } }} */ diff --git a/packages/svelte/src/internal/client/custom-renderer/types.d.ts b/packages/svelte/src/internal/client/custom-renderer/types.d.ts index 81570b6b78..2d0887619f 100644 --- a/packages/svelte/src/internal/client/custom-renderer/types.d.ts +++ b/packages/svelte/src/internal/client/custom-renderer/types.d.ts @@ -1,5 +1,3 @@ -export type NodeType = 'fragment' | 'element' | 'text' | 'comment'; - export type Renderer< TFragment extends object = object, TElement extends object = object, @@ -86,3 +84,25 @@ export type Renderer< /** Remove an event listener of the given type and handler from the target node. */ removeEventListener(target: TElement, type: string, handler: any, options?: any): void; }; + +export type RendererNodes< + Fragment extends object, + Element extends object, + TextNode extends object, + Comment extends object +> = { + fragment: Fragment; + element: Element; + text: TextNode; + comment: Comment; +}; + +export type NodeType = keyof RendererNodes; + +// to detect if the user is passing a type or not we create this type utils that adds a unique symbol +// that the user will never be able to pass in. We then create a a DefaultNodes type that is used as the default +// type for the T generic of `createRenderer`. This means we can "detect" if the user is passing a type manually by +// checking if the type extends DefaultNodes and using different default values +// for the other arguments (TFragment, TElement, TTextNode, TComment) +export type UnsetObject = object & { readonly __unset: unique symbol }; +export type DefaultNodes = RendererNodes; diff --git a/packages/svelte/tests/custom-renderers/renderer.js b/packages/svelte/tests/custom-renderers/renderer.ts similarity index 76% rename from packages/svelte/tests/custom-renderers/renderer.js rename to packages/svelte/tests/custom-renderers/renderer.ts index a92161d4f6..674ac510e6 100644 --- a/packages/svelte/tests/custom-renderers/renderer.js +++ b/packages/svelte/tests/custom-renderers/renderer.ts @@ -8,20 +8,29 @@ import { createRenderer } from '../../src/renderer/index.js'; -/** - * @typedef {{ type: 'element', name: string, attributes: Record, children: ObjNode[], listeners: Record>, parent: ObjNode | null }} ObjElement - * @typedef {{ type: 'text', value: string, parent: ObjNode | null }} ObjText - * @typedef {{ type: 'comment', value: string, parent: ObjNode | null, before: (node: any)=> void }} ObjComment - * @typedef {{ type: 'fragment', children: ObjNode[], parent: ObjNode | null }} ObjFragment - * @typedef {ObjElement | ObjText | ObjComment | ObjFragment} ObjNode - */ - -/** - * @param {ObjNode & { children?: ObjNode[] }} parent - * @param {ObjNode} node - * @param {ObjNode | null} anchor - */ -function insert_node(parent, node, anchor) { +type ObjElement = { + type: 'element'; + name: string; + attributes: Record; + children: ObjNode[]; + listeners: Record>; + parent: ObjNode | null; +}; +type ObjText = { type: 'text'; value: string; parent: ObjNode | null }; +type ObjComment = { + type: 'comment'; + value: string; + parent: ObjNode | null; + before: (node: any) => void; +}; +type ObjFragment = { type: 'fragment'; children: ObjNode[]; parent: ObjNode | null }; +type ObjNode = ObjElement | ObjText | ObjComment | ObjFragment; + +function insert_node( + parent: ObjNode & { children?: ObjNode[] }, + node: ObjNode, + anchor: ObjNode | null +) { if (node.type === 'fragment') { const children = [...(node.children ?? [])]; for (const child of children) { @@ -35,7 +44,7 @@ function insert_node(parent, node, anchor) { remove_from_parent(node); } - const children = /** @type {ObjNode[]} */ (parent.children); + const children: ObjNode[] = parent.children ?? []; node.parent = parent; if (anchor === null) { @@ -47,8 +56,7 @@ function insert_node(parent, node, anchor) { } } -/** @param {ObjNode} node */ -function remove_from_parent(node) { +function remove_from_parent(node: ObjNode) { const parent = node.parent; if (!parent || !('children' in parent)) return; @@ -61,41 +69,43 @@ function remove_from_parent(node) { children.splice(idx, 1); } -/** - * @type {Array} - */ -export const dom_elements = []; +export const dom_elements: Array = []; -const renderer = createRenderer({ +const renderer = createRenderer<{ + fragment: ObjFragment; + element: ObjElement; + text: ObjText; + comment: ObjComment; +}>({ createFragment() { - return /** @type {ObjFragment} */ ({ + return { type: 'fragment', children: [], parent: null - }); + }; }, createElement(name) { - return /** @type {ObjElement} */ ({ + return { type: 'element', name, attributes: {}, children: [], listeners: {}, parent: null - }); + }; }, createTextNode(data) { - return /** @type {ObjText} */ ({ + return { type: 'text', value: data, parent: null - }); + }; }, createComment(data) { - return /** @type {ObjComment} */ ({ + return { type: 'comment', value: data, parent: null, @@ -104,7 +114,7 @@ const renderer = createRenderer({ before(node) { dom_elements.push(node); } - }); + }; }, nodeType(node) { @@ -186,7 +196,6 @@ const renderer = createRenderer({ /** * Create a root object for mounting components into. - * @returns {ObjFragment} */ export function create_root() { return renderer.createFragment(); @@ -194,11 +203,8 @@ export function create_root() { /** * Dispatch a synthetic event on an object node. - * @param {any} node - * @param {string} type - * @param {any} [detail] */ -export function dispatch_event(node, type, detail) { +export function dispatch_event(node: any, type: string, detail?: any) { const listeners = node.listeners?.[type]; if (!listeners) return; const event = { type, detail, target: node }; @@ -209,10 +215,8 @@ export function dispatch_event(node, type, detail) { /** * Serialize an object tree to an HTML-like string for easy assertion. - * @param {ObjNode} node - * @returns {string} */ -export function serialize(node) { +export function serialize(node: ObjNode): string { if (!node) return ''; switch (node.type) { diff --git a/packages/svelte/tests/custom-renderers/shared.ts b/packages/svelte/tests/custom-renderers/shared.ts index 8a269eb9ba..1ad0f49ecf 100644 --- a/packages/svelte/tests/custom-renderers/shared.ts +++ b/packages/svelte/tests/custom-renderers/shared.ts @@ -38,7 +38,7 @@ const console_log = console.log; // eslint-disable-next-line no-console const console_warn = console.warn; -const renderer_path = path.resolve(import.meta.dirname, 'renderer.js'); +const renderer_path = path.resolve(import.meta.dirname, 'renderer.ts'); export function custom_renderer_suite() { return suite_with_variants( diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 9c2e2ba2b9..dd264d3239 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -2568,7 +2568,7 @@ declare module 'svelte/reactivity/window' { } declare module 'svelte/renderer' { - export function createRenderer(renderer: Renderer): Renderer & { + export function createRenderer = DefaultNodes, TFragment extends object = T extends DefaultNodes ? object : T["fragment"], TElement extends object = T extends DefaultNodes ? object : T["element"], TTextNode extends object = T extends DefaultNodes ? object : T["text"], TComment extends object = T extends DefaultNodes ? object : T["comment"]>(renderer: Renderer): Renderer & { render: , Exports extends Record>(component: ComponentType> | Component, options: {} extends Props ? { target: TFragment | TElement | TTextNode | TComment; props?: Props; @@ -2582,8 +2582,6 @@ declare module 'svelte/renderer' { unmount: () => void; }; }; - type NodeType = 'fragment' | 'element' | 'text' | 'comment'; - type Renderer< TFragment extends object = object, TElement extends object = object, @@ -2670,6 +2668,28 @@ declare module 'svelte/renderer' { /** Remove an event listener of the given type and handler from the target node. */ removeEventListener(target: TElement, type: string, handler: any, options?: any): void; }; + + type RendererNodes< + Fragment extends object, + Element extends object, + TextNode extends object, + Comment extends object + > = { + fragment: Fragment; + element: Element; + text: TextNode; + comment: Comment; + }; + + type NodeType = keyof RendererNodes; + + // to detect if the user is passing a type or not we create this type utils that adds a unique symbol + // that the user will never be able to pass in. We then create a a DefaultNodes type that is used as the default + // type for the T generic of `createRenderer`. This means we can "detect" if the user is passing a type manually by + // checking if the type extends DefaultNodes and using different default values + // for the other arguments (TFragment, TElement, TTextNode, TComment) + type UnsetObject = object & { readonly __unset: unique symbol }; + type DefaultNodes = RendererNodes; /** * @deprecated In Svelte 4, components are classes. In Svelte 5, they are functions. * Use `mount` instead to instantiate components.