From 16954d4e7a96818dc77158935309a174c0ba6f14 Mon Sep 17 00:00:00 2001 From: Damian Tarnawski Date: Wed, 6 Aug 2025 12:37:01 +0200 Subject: [PATCH 1/8] Add custom solid universal renderer runtime with no array diffing --- src/render.ts | 74 +++++------- src/solidOpts.ts | 115 ------------------- src/universal.ts | 265 +++++++++++++++++++++++++++++++++++++++++++ tests/basic.test.tsx | 31 +++++ 4 files changed, 324 insertions(+), 161 deletions(-) delete mode 100644 src/solidOpts.ts create mode 100644 src/universal.ts diff --git a/src/render.ts b/src/render.ts index 718c8d4..8b4c934 100644 --- a/src/render.ts +++ b/src/render.ts @@ -1,44 +1,26 @@ -import { createRenderer as solidCreateRenderer } from 'solid-js/universal'; -import { - Config, - type NodeProps, - type TextProps, - startLightningRenderer, - type RendererMainSettings, -} from '@lightningtv/core'; -import nodeOpts from './solidOpts.js'; -import { - splitProps, - createMemo, - createRenderEffect, - untrack, - type JSXElement, - createRoot, - type Component, -} from 'solid-js'; -import type { SolidNode } from './types.js'; +import * as s from 'solid-js'; +import * as lng from '@lightningtv/core'; +import * as universal from './universal.js'; import { activeElement, setActiveElement } from './activeElement.js'; -const solidRenderer = solidCreateRenderer(nodeOpts); +let renderer: lng.IRendererMain; +export const rootNode = universal.createElement('App'); -let renderer; -export const rootNode = nodeOpts.createElement('App'); - -const render = function (code: () => JSXElement) { - // @ts-expect-error - code is jsx element and not SolidElement yet - return solidRenderer.render(code, rootNode); +const render = (code: () => s.JSX.Element): (() => void) => { + return universal.render(code as any, rootNode); }; export function createRenderer( - rendererOptions?: RendererMainSettings, + rendererOptions?: lng.RendererMainSettings, node?: HTMLElement | string, ) { - const options = - rendererOptions || (Config.rendererOptions as RendererMainSettings); + rendererOptions ??= lng.Config.rendererOptions as lng.RendererMainSettings; + lng.assertTruthy(rendererOptions, 'Renderer options must be provided'); + + renderer = lng.startLightningRenderer(rendererOptions, node); - renderer = startLightningRenderer(options, node || 'app'); //Prevent this from happening automatically - Config.setActiveElement = setActiveElement; + lng.Config.setActiveElement = setActiveElement; rootNode.lng = renderer.root!; rootNode.rendered = true; renderer.on('idle', () => { @@ -65,14 +47,14 @@ export const { setProp, mergeProps, use, -} = solidRenderer; +} = universal; type Task = () => void; const taskQueue: Task[] = []; let tasksEnabled = false; -createRoot(() => { - createRenderEffect(() => { +s.createRoot(() => { + s.createRenderEffect(() => { // should change whenever a keypress occurs, so we disable the task queue // until the renderer is idle again. activeElement(); @@ -108,7 +90,7 @@ function processTasks(): void { task(); processTasks(); } - }, Config.taskDelay || 50); + }, lng.Config.taskDelay || 50); } } @@ -120,17 +102,17 @@ function processTasks(): void { * @description https://www.solidjs.com/docs/latest/api#dynamic */ export function Dynamic>( - props: T & { component?: Component | undefined | null }, -): JSXElement { - const [p, others] = splitProps(props, ['component']); + props: T & { component?: s.Component | undefined | null }, +): s.JSXElement { + const [p, others] = s.splitProps(props, ['component']); - const cached = createMemo(() => p.component); + const cached = s.createMemo(() => p.component); - return createMemo(() => { + return s.createMemo(() => { const component = cached(); switch (typeof component) { case 'function': - return untrack(() => component(others)); + return s.untrack(() => component(others)); case 'string': { const el = createElement(component); @@ -141,18 +123,18 @@ export function Dynamic>( default: break; } - }) as unknown as JSXElement; + }) as unknown as s.JSXElement; } // Dont use JSX as it creates circular dependencies and causes trouble with the playground. -export const View = (props: NodeProps) => { +export const View = (props: lng.NodeProps) => { const el = createElement('node'); spread(el, props, false); - return el as unknown as JSXElement; + return el as unknown as s.JSXElement; }; -export const Text = (props: TextProps) => { +export const Text = (props: lng.TextProps) => { const el = createElement('text'); spread(el, props, false); - return el as unknown as JSXElement; + return el as unknown as s.JSXElement; }; diff --git a/src/solidOpts.ts b/src/solidOpts.ts deleted file mode 100644 index 3d7cc7c..0000000 --- a/src/solidOpts.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { - assertTruthy, - isElementText, - ElementNode, - NodeType, - log, - type ElementText, - type TextNode, -} from '@lightningtv/core'; -import type { SolidNode, SolidRendererOptions } from './types.js'; - -declare module '@lightningtv/core' { - interface ElementNode { - /** @internal for managing series of insertions and deletions */ - _queueDelete?: number; - preserve?: boolean; - } -} - -Object.defineProperty(ElementNode.prototype, 'preserve', { - get(): boolean | undefined { - return this._queueDelete === 0; - }, - set(v: boolean) { - this._queueDelete = v ? 0 : undefined; - }, -}); - -let elementDeleteQueue: ElementNode[] = []; - -function flushDeleteQueue(): void { - for (let el of elementDeleteQueue) { - if (Number(el._queueDelete) < 0) { - el.destroy(); - } - el._queueDelete = undefined; - } - elementDeleteQueue.length = 0; -} - -function pushDeleteQueue(node: ElementNode, n: number): void { - if (node._queueDelete === undefined) { - node._queueDelete = n; - if (elementDeleteQueue.push(node) === 1) { - queueMicrotask(flushDeleteQueue); - } - } else { - node._queueDelete += n; - } -} - -export default { - createElement(name: string): ElementNode { - return new ElementNode(name); - }, - createTextNode(text: string): TextNode { - // A text node is just a string - not the node - return { _type: NodeType.Text, text }; - }, - replaceText(node: TextNode, value: string): void { - log('Replace Text: ', node, value); - node.text = value; - const parent = node.parent; - assertTruthy(parent); - parent.text = parent.getText(); - }, - setProperty(node: ElementNode, name: string, value: any = true): void { - node[name] = value; - }, - insertNode(parent: ElementNode, node: SolidNode, anchor: SolidNode): void { - log('INSERT: ', parent, node, anchor); - - let prevParent = node.parent; - parent.insertChild(node, anchor); - - if (node instanceof ElementNode) { - node.parent!.rendered && node.render(true); - if (prevParent !== undefined) { - pushDeleteQueue(node, 1); - } - } else if (isElementText(parent)) { - // TextNodes can be placed outside of nodes when is used as placeholder - parent.text = parent.getText(); - } - }, - isTextNode(node: SolidNode): boolean { - return isElementText(node); - }, - removeNode(parent: ElementNode, node: SolidNode): void { - log('REMOVE: ', parent, node); - - parent.removeChild(node); - - if (node instanceof ElementNode) { - pushDeleteQueue(node, -1); - } else if (isElementText(parent)) { - // TextNodes can be placed outside of nodes when is used as placeholder - parent.text = parent.getText(); - } - }, - getParentNode(node: SolidNode): ElementNode | ElementText | undefined { - return node.parent; - }, - getFirstChild(node: ElementNode): SolidNode | undefined { - return node.children[0] as SolidNode; - }, - getNextSibling(node: SolidNode): SolidNode | undefined { - const children = node.parent!.children || []; - const index = children.indexOf(node as any) + 1; - if (index < children.length) { - return children[index] as SolidNode; - } - return undefined; - }, -} satisfies SolidRendererOptions; diff --git a/src/universal.ts b/src/universal.ts new file mode 100644 index 0000000..75e0965 --- /dev/null +++ b/src/universal.ts @@ -0,0 +1,265 @@ +import * as s from 'solid-js'; +import type * as su from 'solid-js/universal/types/universal.js'; +import * as lng from '@lightningtv/core'; +import { SolidNode } from './types.js'; + +declare module '@lightningtv/core' { + interface ElementNode { + /** @internal for managing series of insertions and deletions */ + _queueDelete?: number; + preserve?: boolean; + _childrenChunks?: NodeChunk[] | TextChunk[]; + } +} + +Object.defineProperty(lng.ElementNode.prototype, 'preserve', { + get(): boolean | undefined { + return this._queueDelete === 0; + }, + set(v: boolean) { + this._queueDelete = v ? 0 : undefined; + }, +}); + +let elementDeleteQueue: lng.ElementNode[] = []; + +function flushDeleteQueue(): void { + for (let el of elementDeleteQueue) { + if (Number(el._queueDelete) < 0) { + el.destroy(); + } + el._queueDelete = undefined; + } + elementDeleteQueue.length = 0; +} + +function pushDeleteQueue(node: lng.ElementNode, n: number): void { + if (node._queueDelete === undefined) { + node._queueDelete = n; + if (elementDeleteQueue.push(node) === 1) { + queueMicrotask(flushDeleteQueue); + } + } else { + node._queueDelete += n; + } +} + +type JSXChildren = s.JSX.Element | (() => JSXChildren); +type NodeChildren = (lng.ElementNode | lng.ElementText)[]; +type NodeChunk = NodeChildren; +type TextChunk = string[]; + +function resolveNodeJSXChildren( + jsx: JSXChildren, + out: NodeChildren = [], +): NodeChildren { + while (typeof jsx === 'function') jsx = jsx(); + if (Array.isArray(jsx)) { + for (let i = 0; i < jsx.length; i++) { + resolveNodeJSXChildren(jsx[i], out); + } + } else if (jsx instanceof lng.ElementNode) { + out.push(jsx); + } + return out; +} +function resolveTextJSXChildren( + jsx: JSXChildren, + out: string[] = [], +): string[] { + while (typeof jsx === 'function') jsx = jsx(); + if (Array.isArray(jsx)) { + for (let i = 0; i < jsx.length; i++) { + resolveTextJSXChildren(jsx[i], out); + } + } else if (typeof jsx === 'string' || typeof jsx === 'number') { + out.push(jsx.toString()); + } + return out; +} + +function commitChunks(el: lng.ElementNode | lng.ElementText): void { + if (lng.isElementText(el)) { + let text = ''; + for (let chunk of el._childrenChunks as TextChunk[]) { + text += chunk.join(''); + } + el.text = text; + } else { + el.children = (el._childrenChunks as NodeChunk[]).flat(); + } +} +function commitChild( + parent: lng.ElementNode, + child: lng.ElementNode | lng.ElementText, +): void { + let prevParent = child.parent; + child.parent = parent; + if (parent.rendered) child.render(true); + if (prevParent) pushDeleteQueue(child as lng.ElementNode, 1); +} + +function insertChunk(el: lng.ElementNode, before: any): NodeChunk; +function insertChunk(el: lng.ElementText, before: any): TextChunk; +function insertChunk( + el: lng.ElementNode | lng.ElementText, + before: any, +): TextChunk | NodeChunk { + let chunks = (el._childrenChunks ??= []); + let chunk: any[] = []; + if (before != null) { + for (let i = 0; i < chunks.length; i++) { + if (chunks[i]!.length > 0 && before === chunks[i]![0]) { + chunks.splice(i, 0, chunk); + return chunk; + } + } + } + chunks.push(chunk); + return chunk; +} + +// Core renderer functions - adapted from solid-js/universal +export const insert: su.Renderer['insert'] = ( + parent, + accessor: any, + before, +) => { + if (lng.isElementText(parent)) { + let chunk = insertChunk(parent, before); + if (typeof accessor === 'function') { + s.createRenderEffect(() => { + chunk.length = 0; + resolveTextJSXChildren(accessor(), chunk); + commitChunks(parent); + }); + } else { + resolveTextJSXChildren(accessor, chunk); + commitChunks(parent); + } + } else { + let el = parent as lng.ElementNode; + let chunk = insertChunk(el, before); + if (typeof accessor === 'function') { + s.createRenderEffect(() => { + let prev = chunk.slice(); + for (let c of prev) c.parent = undefined; + chunk.length = 0; + resolveNodeJSXChildren(accessor(), chunk); + commitChunks(el); + for (let c of chunk) commitChild(el, c); + for (let c of prev) { + if (c.parent !== el) { + (c as lng.ElementNode).onRemove?.(c as lng.ElementNode); + if (el.requiresLayout()) { + lng.addToLayoutQueue(el); + } + pushDeleteQueue(c as lng.ElementNode, -1); + } + } + }); + } else { + resolveNodeJSXChildren(accessor, chunk); + commitChunks(el); + for (let c of chunk) commitChild(el, c); + } + } +}; + +export const insertNode: su.Renderer['insertNode'] = ( + parent, + node, + before, +) => { + if (lng.isElementText(parent)) { + let chunk = insertChunk(parent, before); + resolveTextJSXChildren(node as any, chunk); + commitChunks(parent); + } else { + let el = parent as lng.ElementNode; + let chunk = insertChunk(el, before); + chunk.push(node as lng.ElementNode); + commitChunks(el); + commitChild(el, node as lng.ElementNode); + } +}; + +function spreadExpression( + node: lng.ElementNode, + props: any, + prevProps: any = {}, + skipChildren?: boolean, +): any { + props || (props = {}); + if (!skipChildren) { + insert(node, () => props.children); + } + s.createRenderEffect(() => props.ref && props.ref(node)); + s.createRenderEffect(() => { + for (let prop in props) { + if (prop === 'children' || prop === 'ref') continue; + let value = props[prop]; + if (value === prevProps[prop]) continue; + node[prop] = value; + prevProps[prop] = value; + } + }); + return prevProps; +} + +// Lightning TV specific renderer functions +export const createElement: su.Renderer['createElement'] = ( + name, +) => { + return new lng.ElementNode(name); +}; + +export const createTextNode: su.Renderer['createTextNode'] = ( + text, +) => { + // A text node is just a string - not the node + // return { _type: lng.NodeType.Text, text }; + return text; +}; + +export const render: su.Renderer['render'] = (code, element) => { + let disposer: (() => void) | undefined; + s.createRoot((dispose) => { + disposer = dispose; + insert(element, code()); + }); + return disposer!; +}; + +export const spread: su.Renderer['spread'] = ( + node, + accessor, + skipChildren, +) => { + if (typeof accessor === 'function') { + s.createRenderEffect((current) => + spreadExpression(node, accessor(), current, skipChildren), + ); + } else { + spreadExpression(node, accessor, undefined, skipChildren); + } +}; + +export const setProp: su.Renderer['setProp'] = ( + node, + name, + value, + prev, +) => { + return (node[name] = value); +}; + +export const mergeProps: su.Renderer['mergeProps'] = s.mergeProps; +export const effect: su.Renderer['effect'] = s.createRenderEffect; +export const memo: su.Renderer['memo'] = s.createMemo; +export const createComponent: su.Renderer['createComponent'] = + s.createComponent as any; + +export const use: su.Renderer['use'] = (fn, element, arg) => { + return s.untrack(() => fn(element, arg)); +}; diff --git a/tests/basic.test.tsx b/tests/basic.test.tsx index a696734..fb1dd3a 100644 --- a/tests/basic.test.tsx +++ b/tests/basic.test.tsx @@ -30,3 +30,34 @@ v.test('Update text', () => { dispose() }) + +v.test('reconcile children', () => { + + const [children, setChildren] = s.createSignal('') + + let view!: lng.ElementNode + const dispose = renderer.render(() => <> + + {children()} + + ) + + v.assert.equal(view.children.length, 0) + + setChildren([Child 1, undefined]) + v.assert.equal(view.children.length, 1) + v.assert.equal(view.children[0]!.text, 'Child 1') + + setChildren('') + v.assert.equal(view.children.length, 0) + + setChildren(Child 2) + v.assert.equal(view.children.length, 1) + v.assert.equal(view.children[0]!.text, 'Child 2') + + setChildren([Child 3, undefined]) + v.assert.equal(view.children.length, 1) + v.assert.equal(view.children[0]!.text, 'Child 3') + + dispose() +}) From f2a448e3b3e8c0933bedd1dbc30f2b84eeb3a87b Mon Sep 17 00:00:00 2001 From: Damian Tarnawski Date: Wed, 6 Aug 2025 16:21:47 +0200 Subject: [PATCH 2/8] Simplify spread --- src/universal.ts | 80 ++++++++++++++++++++++++------------------------ 1 file changed, 40 insertions(+), 40 deletions(-) diff --git a/src/universal.ts b/src/universal.ts index 75e0965..5c6d924 100644 --- a/src/universal.ts +++ b/src/universal.ts @@ -12,6 +12,11 @@ declare module '@lightningtv/core' { } } +type JSXChildren = s.JSX.Element | (() => JSXChildren); +type NodeChildren = (lng.ElementNode | lng.ElementText)[]; +type NodeChunk = NodeChildren; +type TextChunk = string[]; + Object.defineProperty(lng.ElementNode.prototype, 'preserve', { get(): boolean | undefined { return this._queueDelete === 0; @@ -44,11 +49,6 @@ function pushDeleteQueue(node: lng.ElementNode, n: number): void { } } -type JSXChildren = s.JSX.Element | (() => JSXChildren); -type NodeChildren = (lng.ElementNode | lng.ElementText)[]; -type NodeChunk = NodeChildren; -type TextChunk = string[]; - function resolveNodeJSXChildren( jsx: JSXChildren, out: NodeChildren = [], @@ -109,7 +109,7 @@ function insertChunk( let chunk: any[] = []; if (before != null) { for (let i = 0; i < chunks.length; i++) { - if (chunks[i]!.length > 0 && before === chunks[i]![0]) { + if (before === chunks[i]![0]) { chunks.splice(i, 0, chunk); return chunk; } @@ -184,18 +184,34 @@ export const insertNode: su.Renderer['insertNode'] = ( } }; -function spreadExpression( - node: lng.ElementNode, - props: any, - prevProps: any = {}, - skipChildren?: boolean, -): any { - props || (props = {}); +export const spread: su.Renderer['spread'] = ( + node, + accessor: any, + skipChildren, +) => { + /* Ref */ + if (typeof accessor === 'function') { + s.createRenderEffect(() => { + let { ref } = accessor(); + if (ref) ref(node); + }); + } else if ('ref' in accessor && typeof accessor.ref === 'function') { + accessor.ref(node); + } + + /* Children */ if (!skipChildren) { - insert(node, () => props.children); + if (typeof accessor === 'function') { + insert(node, () => accessor().children); + } else if ('children' in accessor) { + insert(node, () => accessor.children); + } } - s.createRenderEffect(() => props.ref && props.ref(node)); + + /* Rest */ + let prevProps: any = {}; s.createRenderEffect(() => { + let props = typeof accessor === 'function' ? accessor() : accessor; for (let prop in props) { if (prop === 'children' || prop === 'ref') continue; let value = props[prop]; @@ -204,8 +220,15 @@ function spreadExpression( prevProps[prop] = value; } }); - return prevProps; -} +}; + +export const setProp: su.Renderer['setProp'] = ( + node, + name, + value, +) => { + return (node[name] = value); +}; // Lightning TV specific renderer functions export const createElement: su.Renderer['createElement'] = ( @@ -231,29 +254,6 @@ export const render: su.Renderer['render'] = (code, element) => { return disposer!; }; -export const spread: su.Renderer['spread'] = ( - node, - accessor, - skipChildren, -) => { - if (typeof accessor === 'function') { - s.createRenderEffect((current) => - spreadExpression(node, accessor(), current, skipChildren), - ); - } else { - spreadExpression(node, accessor, undefined, skipChildren); - } -}; - -export const setProp: su.Renderer['setProp'] = ( - node, - name, - value, - prev, -) => { - return (node[name] = value); -}; - export const mergeProps: su.Renderer['mergeProps'] = s.mergeProps; export const effect: su.Renderer['effect'] = s.createRenderEffect; export const memo: su.Renderer['memo'] = s.createMemo; From f3afa55a008494b8d59684121e9aca7745d824af Mon Sep 17 00:00:00 2001 From: Damian Tarnawski Date: Wed, 6 Aug 2025 16:32:57 +0200 Subject: [PATCH 3/8] c-style loops and comments --- src/universal.ts | 44 +++++++++++++++++++++++++++++++------------- 1 file changed, 31 insertions(+), 13 deletions(-) diff --git a/src/universal.ts b/src/universal.ts index 5c6d924..3ec1db2 100644 --- a/src/universal.ts +++ b/src/universal.ts @@ -8,14 +8,12 @@ declare module '@lightningtv/core' { /** @internal for managing series of insertions and deletions */ _queueDelete?: number; preserve?: boolean; - _childrenChunks?: NodeChunk[] | TextChunk[]; + _childrenChunks?: NodeChildren[] | string[][]; } } type JSXChildren = s.JSX.Element | (() => JSXChildren); type NodeChildren = (lng.ElementNode | lng.ElementText)[]; -type NodeChunk = NodeChildren; -type TextChunk = string[]; Object.defineProperty(lng.ElementNode.prototype, 'preserve', { get(): boolean | undefined { @@ -81,12 +79,12 @@ function resolveTextJSXChildren( function commitChunks(el: lng.ElementNode | lng.ElementText): void { if (lng.isElementText(el)) { let text = ''; - for (let chunk of el._childrenChunks as TextChunk[]) { + for (let chunk of el._childrenChunks as string[][]) { text += chunk.join(''); } el.text = text; } else { - el.children = (el._childrenChunks as NodeChunk[]).flat(); + el.children = (el._childrenChunks as NodeChildren[]).flat(); } } function commitChild( @@ -99,12 +97,12 @@ function commitChild( if (prevParent) pushDeleteQueue(child as lng.ElementNode, 1); } -function insertChunk(el: lng.ElementNode, before: any): NodeChunk; -function insertChunk(el: lng.ElementText, before: any): TextChunk; +function insertChunk(el: lng.ElementNode, before: any): NodeChildren; +function insertChunk(el: lng.ElementText, before: any): string[]; function insertChunk( el: lng.ElementNode | lng.ElementText, before: any, -): TextChunk | NodeChunk { +): string[] | NodeChildren { let chunks = (el._childrenChunks ??= []); let chunk: any[] = []; if (before != null) { @@ -125,8 +123,11 @@ export const insert: su.Renderer['insert'] = ( accessor: any, before, ) => { + /* */ if (lng.isElementText(parent)) { let chunk = insertChunk(parent, before); + + /* dynamic chunk */ if (typeof accessor === 'function') { s.createRenderEffect(() => { chunk.length = 0; @@ -134,21 +135,33 @@ export const insert: su.Renderer['insert'] = ( commitChunks(parent); }); } else { + /* constant chunk */ resolveTextJSXChildren(accessor, chunk); commitChunks(parent); } + + return chunk[0] || ''; } else { + /* */ let el = parent as lng.ElementNode; let chunk = insertChunk(el, before); + + /* dynamic chunk */ if (typeof accessor === 'function') { s.createRenderEffect(() => { let prev = chunk.slice(); - for (let c of prev) c.parent = undefined; + for (let i = 0; i < prev.length; i++) { + prev[i]!.parent = undefined; // commitChild will set the parent again + } chunk.length = 0; resolveNodeJSXChildren(accessor(), chunk); commitChunks(el); - for (let c of chunk) commitChild(el, c); - for (let c of prev) { + for (let i = 0; i < chunk.length; i++) { + commitChild(el, chunk[i]!); + } + /* Handle removed children */ + for (let i = 0; i < prev.length; i++) { + let c = prev[i]!; if (c.parent !== el) { (c as lng.ElementNode).onRemove?.(c as lng.ElementNode); if (el.requiresLayout()) { @@ -159,10 +172,15 @@ export const insert: su.Renderer['insert'] = ( } }); } else { + /* constant chunk */ resolveNodeJSXChildren(accessor, chunk); commitChunks(el); - for (let c of chunk) commitChild(el, c); + for (let i = 0; i < chunk.length; i++) { + commitChild(el, chunk[i]!); + } } + + return chunk[0] as any; } }; @@ -195,7 +213,7 @@ export const spread: su.Renderer['spread'] = ( let { ref } = accessor(); if (ref) ref(node); }); - } else if ('ref' in accessor && typeof accessor.ref === 'function') { + } else if (typeof accessor.ref === 'function') { accessor.ref(node); } From dffec6150d7d41c54a206ce25f5be26047cb826b Mon Sep 17 00:00:00 2001 From: Damian Tarnawski Date: Wed, 6 Aug 2025 16:33:27 +0200 Subject: [PATCH 4/8] comments --- src/universal.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/universal.ts b/src/universal.ts index 3ec1db2..1235394 100644 --- a/src/universal.ts +++ b/src/universal.ts @@ -135,14 +135,14 @@ export const insert: su.Renderer['insert'] = ( commitChunks(parent); }); } else { - /* constant chunk */ + /* constant chunk */ resolveTextJSXChildren(accessor, chunk); commitChunks(parent); } return chunk[0] || ''; } else { - /* */ + /* */ let el = parent as lng.ElementNode; let chunk = insertChunk(el, before); @@ -172,7 +172,7 @@ export const insert: su.Renderer['insert'] = ( } }); } else { - /* constant chunk */ + /* constant chunk */ resolveNodeJSXChildren(accessor, chunk); commitChunks(el); for (let i = 0; i < chunk.length; i++) { @@ -189,11 +189,13 @@ export const insertNode: su.Renderer['insertNode'] = ( node, before, ) => { + /* */ if (lng.isElementText(parent)) { let chunk = insertChunk(parent, before); resolveTextJSXChildren(node as any, chunk); commitChunks(parent); } else { + /* */ let el = parent as lng.ElementNode; let chunk = insertChunk(el, before); chunk.push(node as lng.ElementNode); From d199d9006ef4f3cd238033e239891f2db15618aa Mon Sep 17 00:00:00 2001 From: Damian Tarnawski Date: Wed, 6 Aug 2025 16:34:24 +0200 Subject: [PATCH 5/8] comments --- src/universal.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/universal.ts b/src/universal.ts index 1235394..7373017 100644 --- a/src/universal.ts +++ b/src/universal.ts @@ -141,8 +141,9 @@ export const insert: su.Renderer['insert'] = ( } return chunk[0] || ''; - } else { - /* */ + } + // + else { let el = parent as lng.ElementNode; let chunk = insertChunk(el, before); @@ -189,13 +190,14 @@ export const insertNode: su.Renderer['insertNode'] = ( node, before, ) => { - /* */ + // if (lng.isElementText(parent)) { let chunk = insertChunk(parent, before); resolveTextJSXChildren(node as any, chunk); commitChunks(parent); - } else { - /* */ + } + // + else { let el = parent as lng.ElementNode; let chunk = insertChunk(el, before); chunk.push(node as lng.ElementNode); From 305f2b40addc340e9db79766784b528d0f08ea59 Mon Sep 17 00:00:00 2001 From: Damian Tarnawski Date: Wed, 6 Aug 2025 16:51:35 +0200 Subject: [PATCH 6/8] Cleanup rest prop loop --- src/universal.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/universal.ts b/src/universal.ts index 7373017..e6d4b0c 100644 --- a/src/universal.ts +++ b/src/universal.ts @@ -235,11 +235,16 @@ export const spread: su.Renderer['spread'] = ( s.createRenderEffect(() => { let props = typeof accessor === 'function' ? accessor() : accessor; for (let prop in props) { - if (prop === 'children' || prop === 'ref') continue; - let value = props[prop]; - if (value === prevProps[prop]) continue; - node[prop] = value; - prevProps[prop] = value; + switch (prop) { + case 'children': + case 'ref': + break; + default: + let value = props[prop]; + if (value !== prevProps[prop]) { + node[prop] = prevProps[prop] = value; + } + } } }); }; From f66cdd18667a48f9d8fadf261f31d09c9a4e0061 Mon Sep 17 00:00:00 2001 From: Damian Tarnawski Date: Wed, 6 Aug 2025 22:44:12 +0200 Subject: [PATCH 7/8] Cleanup --- src/universal.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/universal.ts b/src/universal.ts index e6d4b0c..be228de 100644 --- a/src/universal.ts +++ b/src/universal.ts @@ -217,7 +217,7 @@ export const spread: su.Renderer['spread'] = ( let { ref } = accessor(); if (ref) ref(node); }); - } else if (typeof accessor.ref === 'function') { + } else if (accessor.ref) { accessor.ref(node); } @@ -238,7 +238,7 @@ export const spread: su.Renderer['spread'] = ( switch (prop) { case 'children': case 'ref': - break; + continue; default: let value = props[prop]; if (value !== prevProps[prop]) { From 53c608bd9393c08d192aa7c5d856504333bafe8e Mon Sep 17 00:00:00 2001 From: Damian Tarnawski Date: Thu, 7 Aug 2025 10:07:49 +0200 Subject: [PATCH 8/8] onremove comment --- src/universal.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/universal.ts b/src/universal.ts index be228de..db978e1 100644 --- a/src/universal.ts +++ b/src/universal.ts @@ -164,7 +164,7 @@ export const insert: su.Renderer['insert'] = ( for (let i = 0; i < prev.length; i++) { let c = prev[i]!; if (c.parent !== el) { - (c as lng.ElementNode).onRemove?.(c as lng.ElementNode); + (c as lng.ElementNode).onRemove?.(c as lng.ElementNode); // ? Should this be called in effect? if (el.requiresLayout()) { lng.addToLayoutQueue(el); }