diff --git a/packages/qwik/src/core/client/vnode-diff.ts b/packages/qwik/src/core/client/vnode-diff.ts index 56c9e177b47..641acfa3abe 100644 --- a/packages/qwik/src/core/client/vnode-diff.ts +++ b/packages/qwik/src/core/client/vnode-diff.ts @@ -32,7 +32,7 @@ import { dangerouslySetInnerHTML, } from '../shared/utils/markers'; import { isPromise } from '../shared/utils/promises'; -import { type ValueOrPromise } from '../shared/utils/types'; +import { isArray, type ValueOrPromise } from '../shared/utils/types'; import { getEventNameFromJsxEvent, getEventNameScopeFromJsxEvent, @@ -83,10 +83,10 @@ import { clearAllEffects } from '../reactive-primitives/cleanup'; import { serializeAttribute } from '../shared/utils/styles'; import { QError, qError } from '../shared/error/error'; import { getFileLocationFromJsx } from '../shared/utils/jsx-filename'; -import { EffectProperty } from '../reactive-primitives/types'; +import { EffectProperty, EffectSubscriptionProp } from '../reactive-primitives/types'; import { SubscriptionData } from '../reactive-primitives/subscription-data'; import { WrappedSignalImpl } from '../reactive-primitives/impl/wrapped-signal-impl'; -import { _CONST_PROPS, _VAR_PROPS } from '../internal'; +import { _CONST_PROPS, _EFFECT_BACK_REF, _VAR_PROPS } from '../internal'; import { isSyncQrl } from '../shared/qrl/qrl-utils'; import type { ElementVNode, TextVNode, VirtualVNode, VNode } from './vnode-impl'; @@ -123,12 +123,14 @@ export const vnode_diff = ( /// and is not connected to the tree. let vNewNode: VNode | null = null; - /// When elements have keys they can be consumed out of order and therefore we can't use nextSibling. - /// In such a case this array will contain the elements after the current location. - /// The array even indices will contains keys and odd indices the vNode. let vSiblings: Map | null = null; + /// The array even indices will contains keys and odd indices the non keyed siblings. let vSiblingsArray: Array | null = null; + /// Side buffer to store nodes that are moved out of order during key scanning. + /// This contains nodes that were found before the target key and need to be moved later. + let vSideBuffer: Map | null = null; + /// Current set of JSX children. let jsxChildren: JSXChildren[] = null!; // Current JSX child. @@ -181,19 +183,23 @@ export const vnode_diff = ( if (Array.isArray(jsxValue)) { descend(jsxValue, false); } else if (isSignal(jsxValue)) { - if (vCurrent) { - clearAllEffects(container, vCurrent); - } expectVirtual(VirtualType.WrappedSignal, null); - descend( - trackSignalAndAssignHost( - jsxValue as Signal, - (vNewNode || vCurrent)!, - EffectProperty.VNODE, - container - ), - true - ); + const unwrappedSignal = + jsxValue instanceof WrappedSignalImpl ? jsxValue.$unwrapIfSignal$() : jsxValue; + const currentSignal = vCurrent?.[_EFFECT_BACK_REF]?.get(EffectProperty.VNODE)?.[ + EffectSubscriptionProp.CONSUMER + ]; + if (currentSignal !== unwrappedSignal) { + descend( + trackSignalAndAssignHost( + unwrappedSignal, + (vNewNode || vCurrent)!, + EffectProperty.VNODE, + container + ), + true + ); + } } else if (isPromise(jsxValue)) { expectVirtual(VirtualType.Awaited, null); asyncQueue.push(jsxValue, vNewNode || vCurrent); @@ -216,7 +222,13 @@ export const vnode_diff = ( } } else if (type === Projection) { expectProjection(); - descend(jsxValue.children, true); + descend( + jsxValue.children, + true, + // special case for projection, we don't want to expect no children + // because the projection's children are not removed + false + ); } else if (type === SSRComment) { expectNoMore(); } else if (type === SSRRaw) { @@ -237,6 +249,7 @@ export const vnode_diff = ( advance(); } expectNoMore(); + cleanupSideBuffer(); ascend(); } } @@ -264,26 +277,14 @@ export const vnode_diff = ( } } - /** - * Advance the `vCurrent` to the next sibling. - * - * Normally this is just `vCurrent = vCurrent.nextSibling`. However, this gets complicated if - * `retrieveChildWithKey` was called, because then we are consuming nodes out of order and can't - * rely on `nextSibling` and instead we need to go by `vSiblings`. - */ + /** Advance the `vCurrent` to the next sibling. */ function peekNextSibling() { // If we don't have a `vNewNode`, than that means we just reconciled the current node. // So advance it. return vCurrent ? (vCurrent.nextSibling as VNode | null) : null; } - /** - * Advance the `vCurrent` to the next sibling. - * - * Normally this is just `vCurrent = vCurrent.nextSibling`. However, this gets complicated if - * `retrieveChildWithKey` was called, because then we are consuming nodes out of order and can't - * rely on `nextSibling` and instead we need to go by `vSiblings`. - */ + /** Advance the `vCurrent` to the next sibling. */ function advanceToNextSibling() { vCurrent = peekNextSibling(); } @@ -307,14 +308,22 @@ export const vnode_diff = ( * In the above example all nodes are on same level so we don't `descendVNode` even thought there * is an array produced by the `map` function. */ - function descend(children: JSXChildren, descendVNode: boolean) { - if (children == null) { + function descend( + children: JSXChildren, + descendVNode: boolean, + shouldExpectNoChildren: boolean = true + ) { + if ( + shouldExpectNoChildren && + (children == null || (descendVNode && isArray(children) && children.length === 0)) + ) { expectNoChildren(); return; } stackPush(children, descendVNode); if (descendVNode) { assertDefined(vCurrent || vNewNode, 'Expecting vCurrent to be defined.'); + vSideBuffer = null; vSiblings = null; vSiblingsArray = null; vParent = (vNewNode || vCurrent!) as ElementVNode | VirtualVNode; @@ -327,6 +336,7 @@ export const vnode_diff = ( function ascend() { const descendVNode = stack.pop(); // boolean: descendVNode if (descendVNode) { + vSideBuffer = stack.pop(); vSiblings = stack.pop(); vSiblingsArray = stack.pop(); vNewNode = stack.pop(); @@ -343,7 +353,7 @@ export const vnode_diff = ( function stackPush(children: JSXChildren, descendVNode: boolean) { stack.push(jsxChildren, jsxIdx, jsxCount, jsxValue); if (descendVNode) { - stack.push(vParent, vCurrent, vNewNode, vSiblingsArray, vSiblings); + stack.push(vParent, vCurrent, vNewNode, vSiblingsArray, vSiblings, vSideBuffer); } stack.push(descendVNode); if (Array.isArray(children)) { @@ -510,6 +520,22 @@ export const vnode_diff = ( return directGetPropsProxyProp(jsxNode, 'name') || QDefaultSlot; } + function cleanupSideBuffer() { + if (vSideBuffer) { + // Remove all nodes in the side buffer as they are no longer needed + for (const vNode of vSideBuffer.values()) { + if (vNode.flags & VNodeFlags.Deleted) { + continue; + } + cleanup(container, vNode); + vnode_remove(journal, vParent, vNode, true); + } + vSideBuffer.clear(); + vSideBuffer = null; + } + vCurrent = null; + } + function drainAsyncQueue(): ValueOrPromise { while (asyncQueue.length) { const jsxNode = asyncQueue.shift() as ValueOrPromise; @@ -702,23 +728,16 @@ export const vnode_diff = ( vCurrent && vnode_isElementVNode(vCurrent) && elementName === vnode_getElementName(vCurrent); const jsxKey: string | null = jsx.key; let needsQDispatchEventPatch = false; - if (!isSameElementName || jsxKey !== getKey(vCurrent)) { - // So we have a key and it does not match the current node. - // We need to do a forward search to find it. - // The complication is that once we start taking nodes out of order we can't use `nextSibling` - vNewNode = retrieveChildWithKey(elementName, jsxKey); - if (vNewNode === null) { - // No existing node with key exists, just create a new one. - needsQDispatchEventPatch = createNewElement(jsx, elementName); - } else { - // Existing keyed node - vnode_insertBefore(journal, vParent as ElementVNode, vNewNode, vCurrent); - // We are here, so jsx is different from the vCurrent, so now we want to point to the moved node. - vCurrent = vNewNode; - // We need to clean up the vNewNode, because we don't want to skip advance to next sibling (see `advance` function). - vNewNode = null; - } + const currentKey = getKey(vCurrent); + if (!isSameElementName || jsxKey !== currentKey) { + const sideBufferKey = getSideBufferKey(elementName, jsxKey); + const createNew = () => (needsQDispatchEventPatch = createNewElement(jsx, elementName)); + moveOrCreateKeyedNode(elementName, jsxKey, sideBufferKey, vParent as ElementVNode, createNew); + } else { + // delete the key from the side buffer if it is the same element + deleteFromSideBuffer(elementName, jsxKey); } + // reconcile attributes const jsxAttrs = [] as ClientAttrs; @@ -812,7 +831,20 @@ export const vnode_diff = ( } if (isSignal(value)) { - value = trackSignalAndAssignHost(value, vnode, key, container, NON_CONST_SUBSCRIPTION_DATA); + const unwrappedSignal = + value instanceof WrappedSignalImpl ? value.$unwrapIfSignal$() : value; + const currentSignal = + vnode?.[_EFFECT_BACK_REF]?.get(key)?.[EffectSubscriptionProp.CONSUMER]; + if (currentSignal === unwrappedSignal) { + return; + } + value = trackSignalAndAssignHost( + unwrappedSignal, + vnode, + key, + container, + NON_CONST_SUBSCRIPTION_DATA + ); } vnode.setAttr( @@ -925,18 +957,6 @@ export const vnode_diff = ( } } - /** - * This function is used to retrieve the child with the given key. If the child is not found, it - * will return null. - * - * After finding the first child with the given key we will create a map of all the keyed siblings - * and an array of non-keyed siblings. This is done to optimize the search for the next child with - * the specified key. - * - * @param nodeName - The name of the node. - * @param key - The key of the node. - * @returns The child with the given key or null if not found. - */ function retrieveChildWithKey( nodeName: string | null, key: string | null @@ -957,7 +977,7 @@ export const vnode_diff = ( vSiblingsArray.push(name, vNode); } else { // we only add the elements which we did not find yet. - vSiblings.set(name + ':' + vKey, vNode); + vSiblings.set(getSideBufferKey(name, vKey), vNode); } } vNode = vNode.nextSibling as VNode | null; @@ -972,49 +992,166 @@ export const vnode_diff = ( } } } else { - const vSibling = vSiblings.get(nodeName + ':' + key); - if (vSibling) { - vNodeWithKey = vSibling as ElementVNode | VirtualVNode; - vSiblings.delete(nodeName + ':' + key); + const siblingsKey = getSideBufferKey(nodeName, key); + if (vSiblings.has(siblingsKey)) { + vNodeWithKey = vSiblings.get(siblingsKey) as ElementVNode | VirtualVNode; + vSiblings.delete(siblingsKey); } } } + + collectSideBufferSiblings(vNodeWithKey); + return vNodeWithKey; } + function collectSideBufferSiblings(targetNode: VNode | null): void { + if (!targetNode) { + if (vCurrent) { + const name = vnode_isElementVNode(vCurrent) ? vnode_getElementName(vCurrent) : null; + const vKey = getKey(vCurrent) || getComponentHash(vCurrent, container.$getObjectById$); + if (vKey != null) { + const sideBufferKey = getSideBufferKey(name, vKey); + vSideBuffer ||= new Map(); + vSideBuffer.set(sideBufferKey, vCurrent); + vSiblings?.delete(sideBufferKey); + } + } + + return; + } + + // Walk from vCurrent up to the target node and collect all keyed siblings + let vNode = vCurrent; + while (vNode && vNode !== targetNode) { + const name = vnode_isElementVNode(vNode) ? vnode_getElementName(vNode) : null; + const vKey = getKey(vNode) || getComponentHash(vNode, container.$getObjectById$); + + if (vKey != null) { + const sideBufferKey = getSideBufferKey(name, vKey); + vSideBuffer ||= new Map(); + vSideBuffer.set(sideBufferKey, vNode); + vSiblings?.delete(sideBufferKey); + } + + vNode = vNode.nextSibling as VNode | null; + } + } + + function getSideBufferKey(nodeName: string | null, key: string): string; + function getSideBufferKey(nodeName: string | null, key: string | null): string | null; + function getSideBufferKey(nodeName: string | null, key: string | null): string | null { + if (key == null) { + return null; + } + return nodeName ? nodeName + ':' + key : key; + } + + function deleteFromSideBuffer(nodeName: string | null, key: string | null): boolean { + const sbKey = getSideBufferKey(nodeName, key); + if (sbKey && vSideBuffer?.has(sbKey)) { + vSideBuffer.delete(sbKey); + return true; + } + return false; + } + + /** + * Shared utility to resolve a keyed node by: + * + * 1. Scanning forward siblings via `retrieveChildWithKey` + * 2. Falling back to the side buffer using the provided `sideBufferKey` + * 3. Creating a new node via `createNew` when not found + * + * If a node is moved from the side buffer, it is inserted before `vCurrent` under + * `parentForInsert`. The function updates `vCurrent`/`vNewNode` accordingly and returns the value + * from `createNew` when a new node is created. + */ + function moveOrCreateKeyedNode( + nodeName: string | null, + lookupKey: string | null, + sideBufferKey: string | null, + parentForInsert: VNode, + createNew: () => any, + addCurrentToSideBufferOnSideInsert?: boolean + ): any { + // 1) Try to find the node among upcoming siblings + vNewNode = retrieveChildWithKey(nodeName, lookupKey); + + if (vNewNode) { + vCurrent = vNewNode; + vNewNode = null; + return; + } + + // 2) Try side buffer + if (sideBufferKey != null) { + const buffered = vSideBuffer?.get(sideBufferKey) || null; + if (buffered) { + vSideBuffer!.delete(sideBufferKey); + if (addCurrentToSideBufferOnSideInsert && vCurrent) { + const currentKey = + getKey(vCurrent) || getComponentHash(vCurrent, container.$getObjectById$); + if (currentKey != null) { + const currentName = vnode_isElementVNode(vCurrent) + ? vnode_getElementName(vCurrent) + : null; + const currentSideKey = getSideBufferKey(currentName, currentKey); + if (currentSideKey != null) { + vSideBuffer ||= new Map(); + vSideBuffer.set(currentSideKey, vCurrent); + } + } + } + vnode_insertBefore(journal, parentForInsert as any, buffered, vCurrent); + vCurrent = buffered; + vNewNode = null; + return; + } + } + + // 3) Create new + return createNew(); + } + function expectVirtual(type: VirtualType, jsxKey: string | null) { const checkKey = type === VirtualType.Fragment; - if ( + const currentKey = getKey(vCurrent); + const isSameNode = vCurrent && vnode_isVirtualVNode(vCurrent) && - getKey(vCurrent) === jsxKey && - (checkKey ? !!jsxKey : true) - ) { + currentKey === jsxKey && + (checkKey ? !!jsxKey : true); + + if (isSameNode) { // All is good. + deleteFromSideBuffer(null, currentKey); return; - } else if (jsxKey !== null) { - // We have a key find it - vNewNode = retrieveChildWithKey(null, jsxKey); - if (vNewNode != null) { - // We found it, move it up. - vnode_insertBefore( - journal, - vParent as VirtualVNode, - vNewNode, - vCurrent && getInsertBefore() - ); - return; - } } - // Did not find it, insert a new one. - vnode_insertBefore( - journal, + + const createNew = () => { + vnode_insertBefore( + journal, + vParent as VirtualVNode, + (vNewNode = vnode_newVirtual()), + vCurrent && getInsertBefore() + ); + (vNewNode as VirtualVNode).setProp(ELEMENT_KEY, jsxKey); + isDev && (vNewNode as VirtualVNode).setProp(DEBUG_TYPE, type); + }; + // For fragments without a key, always create a new virtual node (ensures rerender semantics) + if (checkKey && jsxKey === null) { + createNew(); + return; + } + moveOrCreateKeyedNode( + null, + jsxKey, + getSideBufferKey(null, jsxKey), vParent as VirtualVNode, - (vNewNode = vnode_newVirtual()), - vCurrent && getInsertBefore() + createNew, + true ); - (vNewNode as VirtualVNode).setProp(ELEMENT_KEY, jsxKey); - isDev && (vNewNode as VirtualVNode).setProp(DEBUG_TYPE, type); } function expectComponent(component: Function) { @@ -1037,21 +1174,19 @@ export const vnode_diff = ( const hashesAreEqual = componentHash === vNodeComponentHash; if (!lookupKeysAreEqual) { - // See if we already have this component later on. - vNewNode = retrieveChildWithKey(null, lookupKey); - if (vNewNode) { - // We found the component, move it up. - vnode_insertBefore(journal, vParent as VirtualVNode, vNewNode, vCurrent); - } else { - // We did not find the component, create it. + const createNew = () => { insertNewComponent(host, componentQRL, jsxProps); shouldRender = true; - } - host = vNewNode as VirtualVNode; + }; + moveOrCreateKeyedNode(null, lookupKey, lookupKey, vParent as VirtualVNode, createNew); + host = (vNewNode || vCurrent) as VirtualVNode; } else if (!hashesAreEqual || !jsxNode.key) { insertNewComponent(host, componentQRL, jsxProps); host = vNewNode as VirtualVNode; shouldRender = true; + } else { + // delete the key from the side buffer if it is the same component + deleteFromSideBuffer(null, lookupKey); } if (host) { @@ -1100,23 +1235,21 @@ export const vnode_diff = ( const vNodeLookupKey = getKey(host); const lookupKeysAreEqual = lookupKey === vNodeLookupKey; const vNodeComponentHash = getComponentHash(host, container.$getObjectById$); + const isInlineComponent = vNodeComponentHash == null; - if (!lookupKeysAreEqual) { - // See if we already have this inline component later on. - vNewNode = retrieveChildWithKey(null, lookupKey); - if (vNewNode) { - // We found the inline component, move it up. - vnode_insertBefore(journal, vParent as VirtualVNode, vNewNode, vCurrent); - } else { - // We did not find the inline component, create it. - insertNewInlineComponent(); - } - host = vNewNode as VirtualVNode; - } - // inline components don't have component hash - q:renderFn prop, so it should be null - else if (vNodeComponentHash != null) { + if ((host && !isInlineComponent) || lookupKey == null) { insertNewInlineComponent(); host = vNewNode as VirtualVNode; + } else if (!lookupKeysAreEqual) { + const createNew = () => { + // We did not find the inline component, create it. + insertNewInlineComponent(); + }; + moveOrCreateKeyedNode(null, lookupKey, lookupKey, vParent as VirtualVNode, createNew); + host = (vNewNode || vCurrent) as VirtualVNode; + } else { + // delete the key from the side buffer if it is the same component + deleteFromSideBuffer(null, lookupKey); } if (host) { diff --git a/packages/qwik/src/core/client/vnode-diff.unit.tsx b/packages/qwik/src/core/client/vnode-diff.unit.tsx index 6216dc05f53..f95b81b0cb4 100644 --- a/packages/qwik/src/core/client/vnode-diff.unit.tsx +++ b/packages/qwik/src/core/client/vnode-diff.unit.tsx @@ -431,6 +431,30 @@ describe('vNode-diff', () => { expect(b1).toBe(selectB1()); expect(b2).toBe(selectB2()); }); + + it('should remove or add keyed nodes', () => { + const { vNode, vParent, container } = vnode_fromJSX( + _jsxSorted( + 'test', + {}, + null, + [_jsxSorted('b', {}, null, '1', 0, '1'), _jsxSorted('b', {}, null, '2', 0, null)], + 0, + 'KA_6' + ) + ); + const test = _jsxSorted( + 'test', + {}, + null, + [_jsxSorted('b', {}, null, '2', 0, null), _jsxSorted('b', {}, null, '2', 0, '2')], + 0, + 'KA_6' + ); + vnode_diff(container, test, vParent, null); + vnode_applyJournal(container.$journal$); + expect(vNode).toMatchVDOM(test); + }); }); describe('fragments', () => { it('should not rerender signal wrapper fragment', async () => { diff --git a/packages/qwik/src/core/client/vnode-impl.ts b/packages/qwik/src/core/client/vnode-impl.ts index 8c20cd1858c..b9ae72bb7eb 100644 --- a/packages/qwik/src/core/client/vnode-impl.ts +++ b/packages/qwik/src/core/client/vnode-impl.ts @@ -9,6 +9,7 @@ import { import type { ChoreArray } from './chore-array'; import { _EFFECT_BACK_REF } from '../reactive-primitives/types'; import { BackRef } from '../reactive-primitives/cleanup'; +import { isDev } from '@qwik.dev/core/build'; /** @internal */ export abstract class VNode extends BackRef { @@ -91,8 +92,11 @@ export abstract class VNode extends BackRef { } } - toString() { - return vnode_toString.call(this); + toString(): string { + if (isDev) { + return vnode_toString.call(this); + } + return String(this); } } diff --git a/packages/qwik/src/core/client/vnode.ts b/packages/qwik/src/core/client/vnode.ts index 7e77c0ce9de..232aa4fd4f9 100644 --- a/packages/qwik/src/core/client/vnode.ts +++ b/packages/qwik/src/core/client/vnode.ts @@ -183,8 +183,9 @@ export const enum VNodeJournalOpCode { SetText = 1, // ------ [SetAttribute, target, text] SetAttribute = 2, // - [SetAttribute, target, ...(key, values)]] HoistStyles = 3, // -- [HoistStyles, document] - Remove = 4, // ------- [Insert, target(parent), ...nodes] - Insert = 5, // ------- [Insert, target(parent), reference, ...nodes] + Remove = 4, // ------- [Remove, target(parent), ...nodes] + RemoveAll = 5, // ------- [RemoveAll, target(parent)] + Insert = 6, // ------- [Insert, target(parent), reference, ...nodes] } export type VNodeJournal = Array< @@ -968,6 +969,15 @@ export const vnode_applyJournal = (journal: VNodeJournal) => { idx++; } break; + case VNodeJournalOpCode.RemoveAll: + const removeAllParent = journal[idx++] as Element; + if (removeAllParent.replaceChildren) { + removeAllParent.replaceChildren(); + } else { + // fallback if replaceChildren is not supported + removeAllParent.textContent = ''; + } + break; case VNodeJournalOpCode.Insert: const insertParent = journal[idx++] as Element; const insertBefore = journal[idx++] as Element | Text | null; @@ -1220,8 +1230,14 @@ export const vnode_truncate = ( ) => { assertDefined(vDelete, 'Missing vDelete.'); const parent = vnode_getDomParent(vParent); - const children = vnode_getDOMChildNodes(journal, vDelete); - parent && children.length && journal.push(VNodeJournalOpCode.Remove, parent, ...children); + if (parent) { + if (vnode_isElementVNode(vParent)) { + journal.push(VNodeJournalOpCode.RemoveAll, parent); + } else { + const children = vnode_getDOMChildNodes(journal, vParent); + children.length && journal.push(VNodeJournalOpCode.Remove, parent, ...children); + } + } const vPrevious = vDelete.previousSibling; if (vPrevious) { vPrevious.nextSibling = null; diff --git a/packages/qwik/src/core/debug.ts b/packages/qwik/src/core/debug.ts index aad50e09d15..0bbb745b04c 100644 --- a/packages/qwik/src/core/debug.ts +++ b/packages/qwik/src/core/debug.ts @@ -50,6 +50,8 @@ export function qwikDebugToString(value: any): any { return 'Store'; } else if (isJSXNode(value)) { return jsxToString(value); + } else if (vnode_isVNode(value)) { + return '(' + value.getProp(DEBUG_TYPE, null) + ')'; } } finally { stringifyPath.pop(); diff --git a/packages/qwik/src/core/reactive-primitives/impl/signal-impl.ts b/packages/qwik/src/core/reactive-primitives/impl/signal-impl.ts index d64c8fbd3cb..6845433c9d1 100644 --- a/packages/qwik/src/core/reactive-primitives/impl/signal-impl.ts +++ b/packages/qwik/src/core/reactive-primitives/impl/signal-impl.ts @@ -13,6 +13,7 @@ import { import type { Signal } from '../signal.public'; import { SignalFlags, type EffectSubscription } from '../types'; import { ChoreType } from '../../shared/util-chore-type'; +import type { WrappedSignalImpl } from './wrapped-signal-impl'; const DEBUG = false; // eslint-disable-next-line no-console @@ -23,8 +24,8 @@ export class SignalImpl implements Signal { /** Store a list of effects which are dependent on this signal. */ $effects$: null | Set = null; - $container$: Container | null = null; + $wrappedSignal$: WrappedSignalImpl | null = null; constructor(container: Container | null, value: T) { this.$container$ = container; diff --git a/packages/qwik/src/core/reactive-primitives/impl/wrapped-signal-impl.ts b/packages/qwik/src/core/reactive-primitives/impl/wrapped-signal-impl.ts index 4810bc624ab..fd744510585 100644 --- a/packages/qwik/src/core/reactive-primitives/impl/wrapped-signal-impl.ts +++ b/packages/qwik/src/core/reactive-primitives/impl/wrapped-signal-impl.ts @@ -4,6 +4,7 @@ import type { Container, HostElement } from '../../shared/types'; import { ChoreType } from '../../shared/util-chore-type'; import { trackSignal } from '../../use/use-core'; import type { BackRef } from '../cleanup'; +import { getValueProp } from '../internal-api'; import type { AllSignalFlags, EffectSubscription } from '../types'; import { _EFFECT_BACK_REF, @@ -12,7 +13,7 @@ import { SignalFlags, WrappedSignalFlags, } from '../types'; -import { scheduleEffects } from '../utils'; +import { isSignal, scheduleEffects } from '../utils'; import { SignalImpl } from './signal-impl'; export class WrappedSignalImpl extends SignalImpl implements BackRef { @@ -106,6 +107,13 @@ export class WrappedSignalImpl extends SignalImpl implements BackRef { this.$untrackedValue$ = untrackedValue; } } + + $unwrapIfSignal$(): SignalImpl | WrappedSignalImpl { + return this.$func$ === getValueProp && isSignal(this.$args$[0]) + ? (this.$args$[0] as SignalImpl) + : this; + } + // Make this signal read-only set value(_: any) { throw qError(QError.wrappedReadOnly); diff --git a/packages/qwik/src/core/reactive-primitives/internal-api.ts b/packages/qwik/src/core/reactive-primitives/internal-api.ts index 0e91dc679ed..e510e6fae74 100644 --- a/packages/qwik/src/core/reactive-primitives/internal-api.ts +++ b/packages/qwik/src/core/reactive-primitives/internal-api.ts @@ -2,18 +2,34 @@ import { _CONST_PROPS, _IMMUTABLE } from '../shared/utils/constants'; import { assertEqual } from '../shared/error/assert'; import { isObject } from '../shared/utils/types'; import { isSignal, type Signal } from './signal.public'; -import { getStoreTarget } from './impl/store'; +import { getStoreTarget, isStore } from './impl/store'; import { isPropsProxy } from '../shared/jsx/jsx-runtime'; import { WrappedSignalFlags } from './types'; import { WrappedSignalImpl } from './impl/wrapped-signal-impl'; import { AsyncComputedSignalImpl } from './impl/async-computed-signal-impl'; +import type { SignalImpl } from './impl/signal-impl'; // Keep these properties named like this so they're the same as from wrapSignal -const getValueProp = (p0: { value: T }) => p0.value; +export const getValueProp = (p0: { value: T }) => p0.value; const getProp = (p0: T, p1: P) => p0[p1]; -const getWrapped = (args: [T, (keyof T | undefined)?]) => - new WrappedSignalImpl(null, args.length === 1 ? getValueProp : getProp, args, null); +const getWrapped = (args: [T, (keyof T | undefined)?]) => { + if (args.length === 1) { + if (isSignal(args[0])) { + return ((args[0] as unknown as SignalImpl).$wrappedSignal$ ||= new WrappedSignalImpl( + null, + getValueProp, + args, + null + )); + } else if (isStore(args[0])) { + return new WrappedSignalImpl(null, getValueProp, args, null); + } + return (args[0] as { value: T }).value; + } else { + return new WrappedSignalImpl(null, getProp, args, null); + } +}; type PropType = P extends keyof T ? T[P] diff --git a/packages/qwik/src/core/shared/component-execution.ts b/packages/qwik/src/core/shared/component-execution.ts index ade736e308e..d803792976a 100644 --- a/packages/qwik/src/core/shared/component-execution.ts +++ b/packages/qwik/src/core/shared/component-execution.ts @@ -101,7 +101,7 @@ export const executeComponent = ( container.setHostProp(renderHost, USE_ON_LOCAL_SEQ_IDX, null); } - if (vnode_isVNode(renderHost)) { + if (retryCount > 0 && vnode_isVNode(renderHost)) { clearAllEffects(container, renderHost); } diff --git a/packages/qwik/src/core/ssr/ssr-render-jsx.ts b/packages/qwik/src/core/ssr/ssr-render-jsx.ts index bcc49bc3c28..5503c3448e3 100644 --- a/packages/qwik/src/core/ssr/ssr-render-jsx.ts +++ b/packages/qwik/src/core/ssr/ssr-render-jsx.ts @@ -129,8 +129,11 @@ function processJSXNode( } else if (isSignal(value)) { ssr.openFragment(isDev ? [DEBUG_TYPE, VirtualType.WrappedSignal] : EMPTY_ARRAY); const signalNode = ssr.getOrCreateLastNode(); + const unwrappedSignal = value instanceof WrappedSignalImpl ? value.$unwrapIfSignal$() : value; enqueue(ssr.closeFragment); - enqueue(() => trackSignalAndAssignHost(value, signalNode, EffectProperty.VNODE, ssr)); + enqueue(() => + trackSignalAndAssignHost(unwrappedSignal, signalNode, EffectProperty.VNODE, ssr) + ); enqueue(MaybeAsyncSignal); } else if (isPromise(value)) { ssr.openFragment(isDev ? [DEBUG_TYPE, VirtualType.Awaited] : EMPTY_ARRAY); diff --git a/packages/qwik/src/core/tests/component.spec.tsx b/packages/qwik/src/core/tests/component.spec.tsx index 88106f0533d..e37690503cb 100644 --- a/packages/qwik/src/core/tests/component.spec.tsx +++ b/packages/qwik/src/core/tests/component.spec.tsx @@ -86,6 +86,49 @@ describe.each([ ); }); + it('should render component with key', async () => { + (globalThis as any).componentExecuted = []; + const Cmp = component$(() => { + (globalThis as any).componentExecuted.push('Cmp'); + return
; + }); + + const Parent = component$(() => { + const counter = useSignal(0); + return ( + <> + + + + ); + }); + + const { vNode, document } = await render(, { debug }); + expect((globalThis as any).componentExecuted).toEqual(['Cmp']); + expect(vNode).toMatchVDOM( + + + +
+
+ +
+
+ ); + await trigger(document.body, 'button', 'click'); + expect((globalThis as any).componentExecuted).toEqual(['Cmp', 'Cmp']); + expect(vNode).toMatchVDOM( + + + +
+
+ +
+
+ ); + }); + it('should handle null as empty string', async () => { const MyComp = component$(() => { return ( @@ -2410,6 +2453,131 @@ describe.each([ await expect(document.querySelector('div')).toMatchDOM(
); }); + it('should correctly remove all children for empty array', async () => { + const Cmp = component$(() => { + const list = useSignal([1, 2, 3]); + return ( +
+ + {list.value.map((item) => ( +
{item}
+ ))} +
+ ); + }); + const { vNode, document } = await render(, { debug }); + expect(vNode).toMatchVDOM( + +
+ +
1
+
2
+
3
+
+
+ ); + await trigger(document.body, 'button', 'click'); + expect(vNode).toMatchVDOM( + +
+ +
+
+ ); + expect(document.querySelector('main')).toMatchDOM( +
+ +
+ ); + }); + + it('should correctly remove all children for empty array - case 2', async () => { + const Cmp = component$(() => { + const list = useSignal([1, 2, 3]); + return ( +
+ +
+ {list.value.map((item) => ( +
{item}
+ ))} +
+
+ ); + }); + const { vNode, document } = await render(, { debug }); + expect(vNode).toMatchVDOM( + +
+ +
+
1
+
2
+
3
+
+
+
+ ); + await trigger(document.body, 'button', 'click'); + expect(vNode).toMatchVDOM( + +
+ +
+
+
+ ); + expect(document.querySelector('main')).toMatchDOM( +
+ +
+
+ ); + }); + + it('should correctly remove all children for empty array within virtual node', async () => { + const Cmp = component$(() => { + const list = useSignal([1, 2, 3]); + return ( +
+ + <> + {list.value.map((item) => ( +
{item}
+ ))} + +
+ ); + }); + const { vNode, document } = await render(, { debug }); + expect(vNode).toMatchVDOM( + +
+ + +
1
+
2
+
3
+
+
+
+ ); + await trigger(document.body, 'button', 'click'); + expect(vNode).toMatchVDOM( + +
+ + +
+
+ ); + await expect(document.querySelector('main')).toMatchDOM( +
+ +
+ ); + }); + describe('regression', () => { it('#3643', async () => { const Issue3643 = component$(() => { diff --git a/packages/qwik/src/core/tests/render-api.spec.tsx b/packages/qwik/src/core/tests/render-api.spec.tsx index 6ddb6f6046d..c5e10a10a12 100644 --- a/packages/qwik/src/core/tests/render-api.spec.tsx +++ b/packages/qwik/src/core/tests/render-api.spec.tsx @@ -752,7 +752,7 @@ describe('render api', () => { }} > {v}} /> - {sig.value} + {sig.value + 'test'} ); }); @@ -964,7 +964,7 @@ describe('render api', () => { streaming, }); // This can change when the size of the output changes - expect(stream.write).toHaveBeenCalledTimes(7); + expect(stream.write).toHaveBeenCalledTimes(5); }); }); }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b9ed8a3a75d..9fa830610a0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -317,10 +317,10 @@ importers: version: 4.14.3 '@builder.io/qwik': specifier: npm:@qwik.dev/core@* - version: '@qwik.dev/core@2.0.0-beta.9(prettier@3.6.2)(vite@7.1.7(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1))' + version: '@qwik.dev/core@2.0.0-beta.11(prettier@3.6.2)(vite@7.1.7(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1))' '@builder.io/qwik-city': specifier: npm:@qwik.dev/router - version: '@qwik.dev/router@2.0.0-beta.9(acorn@8.15.0)(rollup@4.52.2(patch_hash=be6a3611c102e8170dc7eb51e7f6da5d05be4729edb9f87434bf2c1f9007986c))(typescript@5.9.2)' + version: '@qwik.dev/router@2.0.0-beta.11(@qwik.dev/core@packages+qwik)(acorn@8.15.0)(rollup@4.52.2(patch_hash=be6a3611c102e8170dc7eb51e7f6da5d05be4729edb9f87434bf2c1f9007986c))(vite@7.1.7(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1))' '@emotion/react': specifier: 11.14.0 version: 11.14.0(@types/react@19.1.13)(react@19.1.1) @@ -329,7 +329,7 @@ importers: version: 11.14.1(@emotion/react@11.14.0(@types/react@19.1.13)(react@19.1.1))(@types/react@19.1.13)(react@19.1.1) '@modular-forms/qwik': specifier: 0.29.1 - version: 0.29.1(@qwik.dev/core@2.0.0-beta.9(prettier@3.6.2)(vite@7.1.7(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1)))(@qwik.dev/router@2.0.0-beta.9(acorn@8.15.0)(rollup@4.52.2(patch_hash=be6a3611c102e8170dc7eb51e7f6da5d05be4729edb9f87434bf2c1f9007986c))(typescript@5.9.2))(typescript@5.9.2) + version: 0.29.1(@qwik.dev/core@2.0.0-beta.11(prettier@3.6.2)(vite@7.1.7(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1)))(@qwik.dev/router@2.0.0-beta.11(@qwik.dev/core@packages+qwik)(acorn@8.15.0)(rollup@4.52.2(patch_hash=be6a3611c102e8170dc7eb51e7f6da5d05be4729edb9f87434bf2c1f9007986c))(vite@7.1.7(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1)))(typescript@5.9.2) '@mui/material': specifier: 7.3.2 version: 7.3.2(@emotion/react@11.14.0(@types/react@19.1.13)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.13)(react@19.1.1))(@types/react@19.1.13)(react@19.1.1))(@types/react@19.1.13)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) @@ -341,7 +341,7 @@ importers: version: 8.11.3(@emotion/react@11.14.0(@types/react@19.1.13)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.13)(react@19.1.1))(@types/react@19.1.13)(react@19.1.1))(@mui/material@7.3.2(@emotion/react@11.14.0(@types/react@19.1.13)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.13)(react@19.1.1))(@types/react@19.1.13)(react@19.1.1))(@types/react@19.1.13)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(@mui/system@7.3.2(@emotion/react@11.14.0(@types/react@19.1.13)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.13)(react@19.1.1))(@types/react@19.1.13)(react@19.1.1))(@types/react@19.1.13)(react@19.1.1))(@types/react@19.1.13)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) '@qwik-ui/headless': specifier: 0.6.7 - version: 0.6.7(@qwik.dev/core@2.0.0-beta.9(prettier@3.6.2)(vite@7.1.7(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1))) + version: 0.6.7(@qwik.dev/core@2.0.0-beta.11(prettier@3.6.2)(vite@7.1.7(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1))) '@qwik.dev/core': specifier: workspace:* version: link:../qwik @@ -398,7 +398,7 @@ importers: version: 0.0.42 '@unpic/qwik': specifier: 0.0.38 - version: 0.0.38(@qwik.dev/core@2.0.0-beta.9(prettier@3.6.2)(vite@7.1.7(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1))) + version: 0.0.38(@qwik.dev/core@2.0.0-beta.11(prettier@3.6.2)(vite@7.1.7(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1))) algoliasearch: specifier: 4.16.0 version: 4.16.0 @@ -507,7 +507,7 @@ importers: version: 0.15.15 '@modular-forms/qwik': specifier: ^0.29.1 - version: 0.29.1(@qwik.dev/core@2.0.0-beta.9(prettier@3.6.2)(vite@7.1.7(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1)))(@qwik.dev/router@2.0.0-beta.9(acorn@8.15.0)(rollup@4.52.2(patch_hash=be6a3611c102e8170dc7eb51e7f6da5d05be4729edb9f87434bf2c1f9007986c))(typescript@5.9.2))(typescript@5.9.2) + version: 0.29.1(@qwik.dev/core@2.0.0-beta.11(prettier@3.6.2)(vite@7.1.7(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1)))(@qwik.dev/router@2.0.0-beta.11(@qwik.dev/core@packages+qwik)(acorn@8.15.0)(rollup@4.52.2(patch_hash=be6a3611c102e8170dc7eb51e7f6da5d05be4729edb9f87434bf2c1f9007986c))(vite@7.1.7(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1)))(typescript@5.9.2) '@typescript/analyze-trace': specifier: ^0.10.1 version: 0.10.1 @@ -526,10 +526,10 @@ importers: devDependencies: '@builder.io/qwik': specifier: npm:@qwik.dev/core@* - version: '@qwik.dev/core@2.0.0-beta.9(prettier@3.6.2)(vite@7.1.7(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1))' + version: '@qwik.dev/core@2.0.0-beta.11(prettier@3.6.2)(vite@7.1.7(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1))' '@builder.io/qwik-city': specifier: npm:@qwik.dev/router - version: '@qwik.dev/router@2.0.0-beta.9(acorn@8.15.0)(rollup@4.52.2(patch_hash=be6a3611c102e8170dc7eb51e7f6da5d05be4729edb9f87434bf2c1f9007986c))(typescript@5.9.2)' + version: '@qwik.dev/router@2.0.0-beta.11(@qwik.dev/core@packages+qwik)(acorn@8.15.0)(rollup@4.52.2(patch_hash=be6a3611c102e8170dc7eb51e7f6da5d05be4729edb9f87434bf2c1f9007986c))(vite@7.1.7(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1))' '@builder.io/vite-plugin-macro': specifier: 0.0.7 version: 0.0.7(@types/node@24.3.0)(lightningcss@1.30.1)(rollup@4.52.2(patch_hash=be6a3611c102e8170dc7eb51e7f6da5d05be4729edb9f87434bf2c1f9007986c))(terser@5.43.1) @@ -2757,8 +2757,8 @@ packages: peerDependencies: '@builder.io/qwik': '>=1.3.1' - '@qwik.dev/core@2.0.0-beta.9': - resolution: {integrity: sha512-MemsRTsLbqF17c/rfTWAojZTCpdKg6tWXs/xjpgeMcCzcmDwoqm3KrQ3GqgrBNu+7gMBPSWK0cb7hrd7q9uTQg==} + '@qwik.dev/core@2.0.0-beta.11': + resolution: {integrity: sha512-D2RT/LKln2/2QgkTpeDkXjpX7mCUGYFXHT0W04JlpNDBiJ1vj4jfcTZqcauFWnSS+TwOE6u22Mz4HbvxiFMyTQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} hasBin: true peerDependencies: @@ -2776,9 +2776,12 @@ packages: engines: {node: '>=18.0.0'} hasBin: true - '@qwik.dev/router@2.0.0-beta.9': - resolution: {integrity: sha512-lLNZov52LTGUZaC+ZWxdnFxd6FBf10gM1ZFhI0rvJwtWAuE0GlqG5ILa63jkvmDsg7cPTmbsCZnazCFxSqbAng==} + '@qwik.dev/router@2.0.0-beta.11': + resolution: {integrity: sha512-YwvE1laJpaV4T65ncL1dZOJCNH0P5WyajQFfx3b5/idvQ/anXuS5CichyCCAv0SRFYUjKGHCnBuQIChzWYESXQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + peerDependencies: + '@qwik.dev/core': ^2.0.0-beta.11 + vite: '>=5 <8' '@rolldown/browser@1.0.0-beta.36': resolution: {integrity: sha512-IMG0sSqiM1DT/fxRVeUTu02szosEUH0eywp5LMedsA4BuDYGQjBZi/ywnkeQGp31cPvQRAxDwljOXorq+gGg4w==} @@ -5816,10 +5819,6 @@ packages: engines: {node: '>=16.x'} hasBin: true - imagetools-core@7.1.0: - resolution: {integrity: sha512-8Aa4NecBBGmTkaAUjcuRYgTPKHCsBEWYmCnvKCL6/bxedehtVVFyZPdXe8DD0Nevd6UWBq85ifUaJ8498lgqNQ==} - engines: {node: '>=18.0.0'} - imagetools-core@8.0.0: resolution: {integrity: sha512-5i4Cx5vrBpVdvT3gvkSGAzzkUCrg/5Jm54UwWbDUSTMp4AjDI4IxiC6dI4+X1PRJYi6eKqWuE+684NJY2iOn3w==} engines: {node: '>=18.0.0'} @@ -7614,7 +7613,6 @@ packages: puppeteer@22.13.1: resolution: {integrity: sha512-PwXLDQK5u83Fm5A7TGMq+9BR7iHDJ8a3h21PSsh/E6VfhxiKYkU7+tvGZNSCap6k3pCNDd9oNteVBEctcBalmQ==} engines: {node: '>=18'} - deprecated: < 24.10.2 is no longer supported hasBin: true pure-rand@6.1.0: @@ -8675,10 +8673,6 @@ packages: resolution: {integrity: sha512-o/MQLTwRm9IVhOqhZ0NQ9oXax1ygPjw6Vs+Vq/4QRjbOAC3B1GCHy7TYxxbExKlb7bzDRzt9vBWU6BDz0RFfYg==} engines: {node: '>=18.17'} - undici@7.16.0: - resolution: {integrity: sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g==} - engines: {node: '>=20.18.1'} - unenv-nightly@1.10.0-1717606461.a117952: resolution: {integrity: sha512-u3TfBX02WzbHTpaEfWEKwDijDSFAHcgXkayUZ+MVDrjhLFvgAJzFGTSTmwlEhwWi2exyRQey23ah9wELMM6etg==} @@ -8906,10 +8900,6 @@ packages: peerDependencies: vite: ^2.6.0 || ^3.0.0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0 - vite-imagetools@7.1.1: - resolution: {integrity: sha512-6Dz0ZD2u1u59dw0ryid4beaDcCNNiTfLXUg6XLxx9n7Qm4BgAAm8TozYUnIH0aT4FDAADZqHsh9ZtISzAs4eKA==} - engines: {node: '>=18.0.0'} - vite-imagetools@8.0.0: resolution: {integrity: sha512-3bkkA0vQ57tMynsetY2j4QhCnZKrxFv0RScaZipzYgkjkkUBEmZL5UIVHOUHhVMfwCetAeM9e3DNwyPK1ff4xg==} engines: {node: '>=18.0.0'} @@ -10558,10 +10548,10 @@ snapshots: '@microsoft/tsdoc@0.15.1': {} - '@modular-forms/qwik@0.29.1(@qwik.dev/core@2.0.0-beta.9(prettier@3.6.2)(vite@7.1.7(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1)))(@qwik.dev/router@2.0.0-beta.9(acorn@8.15.0)(rollup@4.52.2(patch_hash=be6a3611c102e8170dc7eb51e7f6da5d05be4729edb9f87434bf2c1f9007986c))(typescript@5.9.2))(typescript@5.9.2)': + '@modular-forms/qwik@0.29.1(@qwik.dev/core@2.0.0-beta.11(prettier@3.6.2)(vite@7.1.7(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1)))(@qwik.dev/router@2.0.0-beta.11(@qwik.dev/core@packages+qwik)(acorn@8.15.0)(rollup@4.52.2(patch_hash=be6a3611c102e8170dc7eb51e7f6da5d05be4729edb9f87434bf2c1f9007986c))(vite@7.1.7(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1)))(typescript@5.9.2)': dependencies: - '@builder.io/qwik': '@qwik.dev/core@2.0.0-beta.9(prettier@3.6.2)(vite@7.1.7(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1))' - '@builder.io/qwik-city': '@qwik.dev/router@2.0.0-beta.9(acorn@8.15.0)(rollup@4.52.2(patch_hash=be6a3611c102e8170dc7eb51e7f6da5d05be4729edb9f87434bf2c1f9007986c))(typescript@5.9.2)' + '@builder.io/qwik': '@qwik.dev/core@2.0.0-beta.11(prettier@3.6.2)(vite@7.1.7(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1))' + '@builder.io/qwik-city': '@qwik.dev/router@2.0.0-beta.11(@qwik.dev/core@packages+qwik)(acorn@8.15.0)(rollup@4.52.2(patch_hash=be6a3611c102e8170dc7eb51e7f6da5d05be4729edb9f87434bf2c1f9007986c))(vite@7.1.7(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1))' decode-formdata: 0.8.0 valibot: 1.1.0(typescript@5.9.2) transitivePeerDependencies: @@ -11340,18 +11330,19 @@ snapshots: transitivePeerDependencies: - supports-color - '@qwik-ui/headless@0.6.7(@qwik.dev/core@2.0.0-beta.9(prettier@3.6.2)(vite@7.1.7(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1)))': + '@qwik-ui/headless@0.6.7(@qwik.dev/core@2.0.0-beta.11(prettier@3.6.2)(vite@7.1.7(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1)))': dependencies: - '@builder.io/qwik': '@qwik.dev/core@2.0.0-beta.9(prettier@3.6.2)(vite@7.1.7(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1))' + '@builder.io/qwik': '@qwik.dev/core@2.0.0-beta.11(prettier@3.6.2)(vite@7.1.7(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1))' '@floating-ui/core': 1.6.2 '@floating-ui/dom': 1.6.5 '@oddbird/popover-polyfill': 0.4.3 body-scroll-lock-upgrade: 1.1.0 focus-trap: 7.5.4 - '@qwik.dev/core@2.0.0-beta.9(prettier@3.6.2)(vite@7.1.7(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1))': + '@qwik.dev/core@2.0.0-beta.11(prettier@3.6.2)(vite@7.1.7(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1))': dependencies: csstype: 3.1.3 + launch-editor: 2.11.1 rollup: 4.52.2(patch_hash=be6a3611c102e8170dc7eb51e7f6da5d05be4729edb9f87434bf2c1f9007986c) vite: 7.1.7(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1) optionalDependencies: @@ -11362,22 +11353,40 @@ snapshots: dependencies: dotenv: 16.5.0 - '@qwik.dev/router@2.0.0-beta.9(acorn@8.15.0)(rollup@4.52.2(patch_hash=be6a3611c102e8170dc7eb51e7f6da5d05be4729edb9f87434bf2c1f9007986c))(typescript@5.9.2)': + '@qwik.dev/router@2.0.0-beta.11(@qwik.dev/core@packages+qwik)(acorn@8.15.0)(rollup@4.52.2(patch_hash=be6a3611c102e8170dc7eb51e7f6da5d05be4729edb9f87434bf2c1f9007986c))(vite@7.1.7(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1))': dependencies: + '@azure/functions': 3.5.1 '@mdx-js/mdx': 3.1.0(acorn@8.15.0) + '@netlify/edge-functions': 2.17.0 + '@qwik.dev/core': link:packages/qwik '@types/mdx': 2.0.13 + estree-util-value-to-estree: 3.4.0 + github-slugger: 2.0.0 + hast-util-heading-rank: 2.1.1 + hast-util-to-string: 2.0.0 + kleur: 4.1.5 + marked: 12.0.2 + mdast-util-mdx: 3.0.0 + refractor: 4.8.1 + rehype-autolink-headings: 7.1.0 + remark-frontmatter: 5.0.0 + remark-gfm: 4.0.1 + set-cookie-parser: 2.7.1 source-map: 0.7.6 svgo: 3.3.2 - undici: 7.16.0 + typescript: 5.9.2 + unified: 11.0.5 + unist-util-visit: 5.0.0 valibot: 1.1.0(typescript@5.9.2) vfile: 6.0.3 - vite-imagetools: 7.1.1(rollup@4.52.2(patch_hash=be6a3611c102e8170dc7eb51e7f6da5d05be4729edb9f87434bf2c1f9007986c)) + vite: 7.1.7(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1) + vite-imagetools: 8.0.0(rollup@4.52.2(patch_hash=be6a3611c102e8170dc7eb51e7f6da5d05be4729edb9f87434bf2c1f9007986c)) + yaml: 2.8.1 zod: 3.25.48 transitivePeerDependencies: - acorn - rollup - supports-color - - typescript '@rolldown/browser@1.0.0-beta.36': dependencies: @@ -12121,9 +12130,9 @@ snapshots: dependencies: unpic: 3.18.0 - '@unpic/qwik@0.0.38(@qwik.dev/core@2.0.0-beta.9(prettier@3.6.2)(vite@7.1.7(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1)))': + '@unpic/qwik@0.0.38(@qwik.dev/core@2.0.0-beta.11(prettier@3.6.2)(vite@7.1.7(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1)))': dependencies: - '@builder.io/qwik': '@qwik.dev/core@2.0.0-beta.9(prettier@3.6.2)(vite@7.1.7(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1))' + '@builder.io/qwik': '@qwik.dev/core@2.0.0-beta.11(prettier@3.6.2)(vite@7.1.7(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1))' '@vercel/nft@0.29.4(rollup@4.52.2(patch_hash=be6a3611c102e8170dc7eb51e7f6da5d05be4729edb9f87434bf2c1f9007986c))(supports-color@10.2.2)': dependencies: @@ -14832,8 +14841,6 @@ snapshots: image-size@2.0.2: {} - imagetools-core@7.1.0: {} - imagetools-core@8.0.0: {} import-fresh@3.3.1: @@ -18231,8 +18238,6 @@ snapshots: undici@6.18.2: {} - undici@7.16.0: {} - unenv-nightly@1.10.0-1717606461.a117952: dependencies: consola: 3.4.2 @@ -18418,14 +18423,6 @@ snapshots: dependencies: vite: 7.1.7(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1) - vite-imagetools@7.1.1(rollup@4.52.2(patch_hash=be6a3611c102e8170dc7eb51e7f6da5d05be4729edb9f87434bf2c1f9007986c)): - dependencies: - '@rollup/pluginutils': 5.3.0(rollup@4.52.2(patch_hash=be6a3611c102e8170dc7eb51e7f6da5d05be4729edb9f87434bf2c1f9007986c)) - imagetools-core: 7.1.0 - sharp: 0.34.4 - transitivePeerDependencies: - - rollup - vite-imagetools@8.0.0(rollup@4.52.2(patch_hash=be6a3611c102e8170dc7eb51e7f6da5d05be4729edb9f87434bf2c1f9007986c)): dependencies: '@rollup/pluginutils': 5.3.0(rollup@4.52.2(patch_hash=be6a3611c102e8170dc7eb51e7f6da5d05be4729edb9f87434bf2c1f9007986c))