chore: replace custom renderer type arguments with a single one (#18244)

Alternative to #18216

Allows for inference to still works but also allow for explicit
declaration in the cases where it's needed.

I made some changes to the code proposed by @mrkishi (I used `unique
symbol` for the `DefaultType`, inferred the `NodeType`) but overall is
mostly his work.

I've also replaced the renderer for the test with a `TS` version of it
so we can better test the types there.

I think this is the best middle ground solution...inference works (when
it works...we can document that you need to put the create methods first
and the gotchas of automatic inference) but people can also quickly
specify their types in an object.
pull/18317/head
Paolo Ricciuti 4 weeks ago committed by GitHub
parent 4fb6b2fc9a
commit 516973c320
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -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<object, object, object, object>} [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<TFragment, TElement, TTextNode, TComment>} renderer
* @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 } }}
*/

@ -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<any, any, any, any>;
// 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<UnsetObject, UnsetObject, UnsetObject, UnsetObject>;

@ -8,20 +8,29 @@
import { createRenderer } from '../../src/renderer/index.js';
/**
* @typedef {{ type: 'element', name: string, attributes: Record<string, string>, children: ObjNode[], listeners: Record<string, Array<{handler: any, options?: any}>>, 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<string, string>;
children: ObjNode[];
listeners: Record<string, Array<{ handler: any; options?: any }>>;
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<DocumentFragment | Node>}
*/
export const dom_elements = [];
export const dom_elements: Array<DocumentFragment | Node> = [];
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) {

@ -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<CustomRendererTest, 'custom-renderer', CompileOptions | null>(

@ -2568,7 +2568,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> & {
export function createRenderer<T extends RendererNodes<object, object, object, object> = 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<TFragment, TElement, TTextNode, TComment>): 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;
@ -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<any, any, any, any>;
// 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<UnsetObject, UnsetObject, UnsetObject, UnsetObject>;
/**
* @deprecated In Svelte 4, components are classes. In Svelte 5, they are functions.
* Use `mount` instead to instantiate components.

Loading…
Cancel
Save