diff --git a/.changeset/honest-pears-sniff.md b/.changeset/honest-pears-sniff.md new file mode 100644 index 00000000000..2d9d57a57e2 --- /dev/null +++ b/.changeset/honest-pears-sniff.md @@ -0,0 +1,5 @@ +--- +'@qwik.dev/core': patch +--- + +fix: serialize less vnode data diff --git a/packages/docs/src/components/on-this-page/on-this-page-more.tsx b/packages/docs/src/components/on-this-page/on-this-page-more.tsx new file mode 100644 index 00000000000..9bee4ade3bc --- /dev/null +++ b/packages/docs/src/components/on-this-page/on-this-page-more.tsx @@ -0,0 +1,65 @@ +import { component$ } from '@qwik.dev/core'; +import { EditIcon } from '../svgs/edit-icon'; +import { AlertIcon } from '../svgs/alert-icon'; +import { ChatIcon } from '../svgs/chat-icon'; +import { GithubLogo } from '../svgs/github-logo'; +import { TwitterLogo } from '../svgs/twitter-logo'; + +type OnThisPageMoreProps = { + theme: 'light' | 'dark' | 'auto'; + editUrl: string; +}; + +export const OnThisPageMore = component$(({ theme, editUrl }) => { + const OnThisPageMore = [ + { + href: editUrl, + text: 'Edit this Page', + icon: EditIcon, + }, + { + href: 'https://github.com/QwikDev/qwik/issues/new/choose', + text: 'Create an issue', + icon: AlertIcon, + }, + { + href: 'https://qwik.dev/chat', + text: 'Join our community', + icon: ChatIcon, + }, + { + href: 'https://github.com/QwikDev/qwik', + text: 'GitHub', + icon: GithubLogo, + }, + { + href: 'https://twitter.com/QwikDev', + text: '@QwikDev', + icon: TwitterLogo, + }, + ]; + return ( + <> +
More
+ + + ); +}); diff --git a/packages/docs/src/components/on-this-page/on-this-page.tsx b/packages/docs/src/components/on-this-page/on-this-page.tsx index 305cef47f27..dec9022ed66 100644 --- a/packages/docs/src/components/on-this-page/on-this-page.tsx +++ b/packages/docs/src/components/on-this-page/on-this-page.tsx @@ -1,12 +1,8 @@ import { $, component$, useContext, useOnDocument, useSignal, useStyles$ } from '@qwik.dev/core'; import { useContent, useLocation } from '@qwik.dev/router'; import { GlobalStore } from '../../context'; -import { AlertIcon } from '../svgs/alert-icon'; -import { ChatIcon } from '../svgs/chat-icon'; -import { EditIcon } from '../svgs/edit-icon'; -import { GithubLogo } from '../svgs/github-logo'; -import { TwitterLogo } from '../svgs/twitter-logo'; import styles from './on-this-page.css?inline'; +import { OnThisPageMore } from './on-this-page-more'; const QWIK_GROUP = [ 'components', @@ -117,39 +113,9 @@ export const OnThisPage = component$(() => { const contentHeadings = headings?.filter((h) => h.level <= 3) || []; const { url } = useLocation(); - const githubEditRoute = makeEditPageUrl(url.pathname); - const editUrl = `https://github.com/QwikDev/qwik/edit/main/packages/docs/src/routes/${githubEditRoute}/index.mdx`; - const OnThisPageMore = [ - { - href: editUrl, - text: 'Edit this Page', - icon: EditIcon, - }, - { - href: 'https://github.com/QwikDev/qwik/issues/new/choose', - text: 'Create an issue', - icon: AlertIcon, - }, - { - href: 'https://qwik.dev/chat', - text: 'Join our community', - icon: ChatIcon, - }, - { - href: 'https://github.com/QwikDev/qwik', - text: 'GitHub', - icon: GithubLogo, - }, - { - href: 'https://twitter.com/QwikDev', - text: '@QwikDev', - icon: TwitterLogo, - }, - ]; - const useActiveItem = (itemIds: string[]) => { const activeId = useSignal(null); useOnDocument( @@ -214,29 +180,9 @@ export const OnThisPage = component$(() => { ))} + ) : null} - -
More
-
    - {OnThisPageMore.map((el, index) => { - return ( -
  • - - - {el.text} - -
  • - ); - })} -
); }); diff --git a/packages/qwik/src/core/qwik.core.api.md b/packages/qwik/src/core/qwik.core.api.md index 2df8dbce463..ab5e72356e1 100644 --- a/packages/qwik/src/core/qwik.core.api.md +++ b/packages/qwik/src/core/qwik.core.api.md @@ -892,14 +892,13 @@ export abstract class _SharedContainer implements Container { // (undocumented) serializationCtxFactory(NodeConstructor: { new (...rest: any[]): { - nodeType: number; - id: string; + __brand__: 'SsrNode'; }; } | null, DomRefConstructor: { new (...rest: any[]): { - $ssrNode$: ISsrNode; + __brand__: 'DomRef'; }; - } | null, symbolToChunkResolver: SymbolToChunkResolver, writer?: StreamWriter, prepVNodeData?: (vNode: any) => void): SerializationContext; + } | null, symbolToChunkResolver: SymbolToChunkResolver, writer?: StreamWriter): SerializationContext; // (undocumented) abstract setContext(host: HostElement, context: ContextId, value: T): void; // (undocumented) diff --git a/packages/qwik/src/core/reactive-primitives/subscriber.ts b/packages/qwik/src/core/reactive-primitives/subscriber.ts index dafe4077f85..32432ed8882 100644 --- a/packages/qwik/src/core/reactive-primitives/subscriber.ts +++ b/packages/qwik/src/core/reactive-primitives/subscriber.ts @@ -30,5 +30,5 @@ export function getSubscriber( } function isSsrNode(value: any): value is ISsrNode { - return '__brand__' in value && 'currentComponentNode' in value; + return '__brand__' in value && value.__brand__ === 'SsrNode'; } diff --git a/packages/qwik/src/core/shared/scheduler-document-position.ts b/packages/qwik/src/core/shared/scheduler-document-position.ts index 7f73c4e2c7b..b763489a3df 100644 --- a/packages/qwik/src/core/shared/scheduler-document-position.ts +++ b/packages/qwik/src/core/shared/scheduler-document-position.ts @@ -96,11 +96,11 @@ export const ssrNodeDocumentPosition = (a: ISsrNode, b: ISsrNode): -1 | 0 | 1 => let bDepth = -1; while (a) { const ssrNode = (aSsrNodePath[++aDepth] = a); - a = ssrNode.currentComponentNode!; + a = ssrNode.parentSsrNode!; } while (b) { const ssrNode = (bSsrNodePath[++bDepth] = b); - b = ssrNode.currentComponentNode!; + b = ssrNode.parentSsrNode!; } while (aDepth >= 0 && bDepth >= 0) { diff --git a/packages/qwik/src/core/shared/shared-container.ts b/packages/qwik/src/core/shared/shared-container.ts index 04cbaf8feb4..14cc690d1b1 100644 --- a/packages/qwik/src/core/shared/shared-container.ts +++ b/packages/qwik/src/core/shared/shared-container.ts @@ -3,7 +3,7 @@ import { trackSignalAndAssignHost } from '../use/use-core'; import { version } from '../version'; import type { SubscriptionData } from '../reactive-primitives/subscription-data'; import type { Signal } from '../reactive-primitives/signal.public'; -import type { ISsrNode, StreamWriter, SymbolToChunkResolver } from '../ssr/ssr-types'; +import type { StreamWriter, SymbolToChunkResolver } from '../ssr/ssr-types'; import type { Scheduler } from './scheduler'; import { createScheduler } from './scheduler'; import { createSerializationContext, type SerializationContext } from './shared-serialization'; @@ -51,14 +51,13 @@ export abstract class _SharedContainer implements Container { serializationCtxFactory( NodeConstructor: { - new (...rest: any[]): { nodeType: number; id: string }; + new (...rest: any[]): { __brand__: 'SsrNode' }; } | null, DomRefConstructor: { - new (...rest: any[]): { $ssrNode$: ISsrNode }; + new (...rest: any[]): { __brand__: 'DomRef' }; } | null, symbolToChunkResolver: SymbolToChunkResolver, - writer?: StreamWriter, - prepVNodeData?: (vNode: any) => void + writer?: StreamWriter ): SerializationContext { return createSerializationContext( NodeConstructor, @@ -67,8 +66,7 @@ export abstract class _SharedContainer implements Container { this.getHostProp.bind(this), this.setHostProp.bind(this), this.$storeProxyMap$, - writer, - prepVNodeData + writer ); } diff --git a/packages/qwik/src/core/shared/shared-serialization.ts b/packages/qwik/src/core/shared/shared-serialization.ts index 37eb46db9e0..b807871c2d9 100644 --- a/packages/qwik/src/core/shared/shared-serialization.ts +++ b/packages/qwik/src/core/shared/shared-serialization.ts @@ -38,7 +38,7 @@ import type { DeserializeContainer, HostElement, ObjToProxyMap } from './types'; import { _CONST_PROPS, _VAR_PROPS } from './utils/constants'; import { isElement, isNode } from './utils/element'; import { EMPTY_ARRAY, EMPTY_OBJ } from './utils/flyweight'; -import { ELEMENT_ID } from './utils/markers'; +import { ELEMENT_ID, ELEMENT_PROPS, QBackRefs } from './utils/markers'; import { isPromise } from './utils/promises'; import { SerializerSymbol, fastSkipSerialize } from './utils/serialize-utils'; import { @@ -601,9 +601,8 @@ export function inflateQRL(container: DeserializeContainer, qrl: QRLInternal | null; }; @@ -679,7 +678,6 @@ export interface SerializationContext { $getProp$: (obj: any, prop: string) => any; $setProp$: (obj: any, prop: string, value: any) => void; - $prepVNodeData$?: (vNodeData: VNodeData) => void; } export const createSerializationContext = ( @@ -690,19 +688,17 @@ export const createSerializationContext = ( * server will not know what to do with them. */ NodeConstructor: { - new (...rest: any[]): { nodeType: number; id: string }; + new (...rest: any[]): { __brand__: 'SsrNode' }; } | null, /** DomRef constructor, for instanceof checks. */ DomRefConstructor: { - new (...rest: any[]): { $ssrNode$: ISsrNode }; + new (...rest: any[]): { __brand__: 'DomRef' }; } | null, symbolToChunkResolver: SymbolToChunkResolver, getProp: (obj: any, prop: string) => any, setProp: (obj: any, prop: string, value: any) => void, storeProxyMap: ObjToProxyMap, - writer?: StreamWriter, - // temporary until we serdes the vnode data here - prepVNodeData?: (vNodeData: VNodeData) => void + writer?: StreamWriter ): SerializationContext => { if (!writer) { const buffer: string[] = []; @@ -763,9 +759,10 @@ export const createSerializationContext = ( return seen.$rootIndex$; }; - const isSsrNode = (NodeConstructor ? (obj) => obj instanceof NodeConstructor : () => false) as ( - obj: unknown - ) => obj is SsrNode; + const isSsrNode = ( + NodeConstructor ? (obj) => obj instanceof NodeConstructor : ((() => false) as any) + ) as (obj: unknown) => obj is SsrNode; + isDomRef = ( DomRefConstructor ? (obj) => obj instanceof DomRefConstructor : ((() => false) as any) ) as (obj: unknown) => obj is DomRef; @@ -816,7 +813,6 @@ export const createSerializationContext = ( $storeProxyMap$: storeProxyMap, $getProp$: getProp, $setProp$: setProp, - $prepVNodeData$: prepVNodeData, $pathMap$: rootsPathMap, }; }; @@ -847,8 +843,14 @@ const discoverValuesForVNodeData = (vnodeData: VNodeData, callback: (value: unkn for (const value of vnodeData) { if (isSsrAttrs(value)) { for (let i = 1; i < value.length; i += 2) { + const keyValue = value[i - 1]; const attrValue = value[i]; - if (typeof attrValue === 'string') { + if ( + typeof attrValue === 'string' || + // skip empty props + (keyValue === ELEMENT_PROPS && + Object.keys(attrValue as Record).length === 0) + ) { continue; } callback(attrValue); @@ -1192,14 +1194,25 @@ async function serialize(serializationContext: SerializationContext): Promise $addRoot$(vNodeDataValue)); vNodeData[0] |= VNodeDataFlag.SERIALIZE; } - if (value.childrenVNodeData) { - for (const vNodeData of value.childrenVNodeData) { - discoverValuesForVNodeData(vNodeData, (vNodeDataValue) => $addRoot$(vNodeDataValue)); - vNodeData[0] |= VNodeDataFlag.SERIALIZE; + if (value.children) { + // can be static, but we need to save vnode data structure + discover the back refs + for (const child of value.children) { + const childVNodeData = child.vnodeData; + if (childVNodeData) { + // add all back refs to the roots + for (const value of childVNodeData) { + if (isSsrAttrs(value)) { + const backRefKeyIndex = value.findIndex((v) => v === QBackRefs); + if (backRefKeyIndex !== -1) { + $addRoot$(value[backRefKeyIndex + 1]); + } + } + } + childVNodeData[0] |= VNodeDataFlag.SERIALIZE; + } } } } else if (typeof FormData !== 'undefined' && value instanceof FormData) { diff --git a/packages/qwik/src/core/shared/types.ts b/packages/qwik/src/core/shared/types.ts index 18b9f67b85d..dc44a3ca5e5 100644 --- a/packages/qwik/src/core/shared/types.ts +++ b/packages/qwik/src/core/shared/types.ts @@ -45,10 +45,10 @@ export interface Container { ensureProjectionResolved(host: HostElement): void; serializationCtxFactory( NodeConstructor: { - new (...rest: any[]): { nodeType: number; id: string }; + new (...rest: any[]): { __brand__: 'SsrNode' }; } | null, DomRefConstructor: { - new (...rest: any[]): { $ssrNode$: ISsrNode }; + new (...rest: any[]): { __brand__: 'DomRef' }; } | null, symbolToChunkResolver: SymbolToChunkResolver, writer?: StreamWriter diff --git a/packages/qwik/src/core/ssr/ssr-render-component.ts b/packages/qwik/src/core/ssr/ssr-render-component.ts index 9858d059bf3..51fc8a0f92a 100644 --- a/packages/qwik/src/core/ssr/ssr-render-component.ts +++ b/packages/qwik/src/core/ssr/ssr-render-component.ts @@ -14,7 +14,7 @@ export const applyInlineComponent = ( inlineComponentFunction: OnRenderFn, jsx: JSXNode ) => { - const host = ssr.getLastNode(); + const host = ssr.getOrCreateLastNode(); return executeComponent(ssr, host, componentHost, inlineComponentFunction, jsx.props); }; @@ -23,7 +23,7 @@ export const applyQwikComponentBody = ( jsx: JSXNode, component: Component ): ValueOrPromise => { - const host = ssr.getLastNode(); + const host = ssr.getOrCreateLastNode(); const [componentQrl] = (component as any)[SERIALIZABLE_STATE] as [QRLInternal>]; const srcProps = jsx.props; if (srcProps && srcProps.children) { diff --git a/packages/qwik/src/core/ssr/ssr-render-jsx.ts b/packages/qwik/src/core/ssr/ssr-render-jsx.ts index 51829a41093..4b3b7e6a37c 100644 --- a/packages/qwik/src/core/ssr/ssr-render-jsx.ts +++ b/packages/qwik/src/core/ssr/ssr-render-jsx.ts @@ -112,7 +112,7 @@ function processJSXNode( } } else if (isSignal(value)) { ssr.openFragment(isDev ? [DEBUG_TYPE, VirtualType.WrappedSignal] : EMPTY_ARRAY); - const signalNode = ssr.getLastNode(); + const signalNode = ssr.getOrCreateLastNode(); enqueue(ssr.closeFragment); enqueue(trackSignalAndAssignHost(value, signalNode, EffectProperty.VNODE, ssr)); } else if (isPromise(value)) { @@ -184,7 +184,6 @@ function processJSXNode( attrs = [DEBUG_TYPE, VirtualType.Fragment, ...attrs]; // Add debug info. } ssr.openFragment(attrs); - ssr.addCurrentElementFrameAsComponentChild(); enqueue(ssr.closeFragment); // In theory we could get functions or regexes, but we assume all is well const children = jsx.children as JSXOutput; @@ -198,7 +197,7 @@ function processJSXNode( projectionAttrs.push(QSlotParent, compId); ssr.openProjection(projectionAttrs); const host = componentFrame.componentNode; - const node = ssr.getLastNode(); + const node = ssr.getOrCreateLastNode(); const slotName = getSlotName(host, jsx, ssr); projectionAttrs.push(QSlot, slotName); @@ -249,7 +248,7 @@ function processJSXNode( } else if (isQwikComponent(type)) { // prod: use new instance of an array for props, we always modify props for a component ssr.openComponent(isDev ? [DEBUG_TYPE, VirtualType.Component] : []); - const host = ssr.getLastNode(); + const host = ssr.getOrCreateLastNode(); const componentFrame = ssr.getParentComponentFrame()!; componentFrame!.distributeChildrenIntoSlots( jsx.children, diff --git a/packages/qwik/src/core/ssr/ssr-types.ts b/packages/qwik/src/core/ssr/ssr-types.ts index 28697044f8b..4768e300e91 100644 --- a/packages/qwik/src/core/ssr/ssr-types.ts +++ b/packages/qwik/src/core/ssr/ssr-types.ts @@ -25,12 +25,12 @@ export interface StreamWriter { export interface ISsrNode { id: string; - currentComponentNode: ISsrNode | null; + parentSsrNode: ISsrNode | null; vnodeData?: VNodeData; setProp(name: string, value: any): void; getProp(name: string): any; removeProp(name: string): void; - addChildVNodeData(child: VNodeData): void; + addChild(child: ISsrNode): void; } /** @internal */ @@ -77,7 +77,6 @@ export interface SSRContainer extends Container { openFragment(attrs: SsrAttrs): void; closeFragment(): void; - addCurrentElementFrameAsComponentChild(): void; openProjection(attrs: SsrAttrs): void; closeProjection(): void; @@ -91,7 +90,7 @@ export interface SSRContainer extends Container { htmlNode(rawHtml: string): void; commentNode(text: string): void; addRoot(obj: any): number | undefined; - getLastNode(): ISsrNode; + getOrCreateLastNode(): ISsrNode; addUnclaimedProjection(frame: ISsrComponentFrame, name: string, children: JSXChildren): void; isStatic(): boolean; render(jsx: JSXOutput): Promise; diff --git a/packages/qwik/src/core/tests/container.spec.tsx b/packages/qwik/src/core/tests/container.spec.tsx index 67952a6b3b9..a2200ffdc61 100644 --- a/packages/qwik/src/core/tests/container.spec.tsx +++ b/packages/qwik/src/core/tests/container.spec.tsx @@ -114,7 +114,7 @@ describe('serializer v2', () => { ssr.openElement('div', ['id', 'parent']); ssr.textNode('Hello'); ssr.openElement('span', ['id', 'myId']); - const node = ssr.getLastNode(); + const node = ssr.getOrCreateLastNode(); ssr.addRoot({ someProp: node }); ssr.textNode('Hello'); ssr.openElement('b', ['id', 'child']); @@ -133,7 +133,7 @@ describe('serializer v2', () => { ssr.textNode('Greetings'); ssr.textNode(' '); ssr.textNode('World'); - const node = ssr.getLastNode(); + const node = ssr.getOrCreateLastNode(); expect(node.id).toBe('2C'); ssr.textNode('!'); ssr.addRoot({ someProp: node }); @@ -154,7 +154,7 @@ describe('serializer v2', () => { ssr.textNode(' '); // 2B ssr.openFragment([]); // 2C ssr.textNode('World'); // 2CA - const node = ssr.getLastNode(); + const node = ssr.getOrCreateLastNode(); expect(node.id).toBe('2CA'); ssr.textNode('!'); ssr.addRoot({ someProp: node }); @@ -512,7 +512,7 @@ describe('serializer v2', () => { describe('DocumentSerializer, //////', () => { it('should serialize and deserialize', async () => { - const obj = new SsrNode(null, SsrNode.DOCUMENT_NODE, '', [], [], [] as any); + const obj = new SsrNode(null, '', [], [], [] as any); const container = await withContainer((ssr) => ssr.addRoot(obj)); expect(container.$getObjectById$(0)).toEqual(container.element.ownerDocument); }); diff --git a/packages/qwik/src/core/tests/use-context.spec.tsx b/packages/qwik/src/core/tests/use-context.spec.tsx index 5ce5aeaae75..0570997ef9c 100644 --- a/packages/qwik/src/core/tests/use-context.spec.tsx +++ b/packages/qwik/src/core/tests/use-context.spec.tsx @@ -249,7 +249,7 @@ describe.each([

1

- 0 + 0

0 diff --git a/packages/qwik/src/server/ssr-container.ts b/packages/qwik/src/server/ssr-container.ts index edbaabe6f95..091cbc22f60 100644 --- a/packages/qwik/src/server/ssr-container.ts +++ b/packages/qwik/src/server/ssr-container.ts @@ -236,8 +236,7 @@ class SSRContainer extends _SharedContainer implements ISSRContainer { SsrNode, DomRef, this.symbolToChunkResolver, - opts.writer, - (vNodeData: VNodeData) => this.addVNodeToSerializationRoots(vNodeData) + opts.writer ); this.renderTimer = createTimer(); this.tag = opts.tagName; @@ -297,14 +296,14 @@ class SSRContainer extends _SharedContainer implements ISSRContainer { return value; } } - ssrNode = ssrNode.currentComponentNode; + ssrNode = ssrNode.parentSsrNode; } return undefined; } getParentHost(host: HostElement): HostElement | null { const ssrNode: ISsrNode = host as any; - return ssrNode.currentComponentNode as ISsrNode | null; + return ssrNode.parentSsrNode as ISsrNode | null; } setHostProp(host: ISsrNode, name: string, value: T): void { @@ -453,6 +452,8 @@ class SSRContainer extends _SharedContainer implements ISSRContainer { openFragment(attrs: SsrAttrs) { this.lastNode = null; vNodeData_openFragment(this.currentElementFrame!.vNodeData, attrs); + // create SSRNode and add it as component child to serialize its vnode data + this.getOrCreateLastNode(); } /** Writes closing data to vNodeData for fragment boundaries */ @@ -461,13 +462,6 @@ class SSRContainer extends _SharedContainer implements ISSRContainer { this.lastNode = null; } - addCurrentElementFrameAsComponentChild() { - const vNode = this.currentElementFrame?.vNodeData; - if (vNode) { - this.currentComponentNode?.addChildVNodeData(vNode); - } - } - openProjection(attrs: SsrAttrs) { this.openFragment(attrs); const componentFrame = this.getComponentFrame(); @@ -489,7 +483,7 @@ class SSRContainer extends _SharedContainer implements ISSRContainer { /** Writes opening data to vNodeData for component boundaries */ openComponent(attrs: SsrAttrs) { this.openFragment(attrs); - this.currentComponentNode = this.getLastNode(); + this.currentComponentNode = this.getOrCreateLastNode(); this.componentStack.push(new SsrComponentFrame(this.currentComponentNode)); } @@ -516,7 +510,7 @@ class SSRContainer extends _SharedContainer implements ISSRContainer { const componentFrame = this.componentStack.pop()!; componentFrame.releaseUnclaimedProjections(this.unclaimedProjections); this.closeFragment(); - this.currentComponentNode = this.currentComponentNode?.currentComponentNode || null; + this.currentComponentNode = this.currentComponentNode?.parentSsrNode || null; } /** Write a text node with correct escaping. Save the length of the text node in the vNodeData. */ @@ -541,7 +535,7 @@ class SSRContainer extends _SharedContainer implements ISSRContainer { return this.serializationCtx.$addRoot$(obj); } - getLastNode(): ISsrNode { + getOrCreateLastNode(): ISsrNode { if (!this.lastNode) { this.lastNode = vNodeData_createSsrNodeReference( this.currentComponentNode, @@ -779,51 +773,6 @@ class SSRContainer extends _SharedContainer implements ISSRContainer { this.closeElement(); } - /** This adds the vnode's data to the serialization roots */ - addVNodeToSerializationRoots(vNodeData: VNodeData) { - const vNodeAttrsStack: SsrAttrs[] = []; - const flag = vNodeData[0]; - if (flag !== VNodeDataFlag.NONE) { - if (flag & (VNodeDataFlag.TEXT_DATA | VNodeDataFlag.VIRTUAL_NODE)) { - let fragmentAttrs: SsrAttrs | null = null; - let depth = 0; - for (let i = 1; i < vNodeData.length; i++) { - const value = vNodeData[i]; - if (Array.isArray(value)) { - vNodeAttrsStack.push(fragmentAttrs!); - fragmentAttrs = value; - } else if (value === OPEN_FRAGMENT) { - depth++; - } else if (value === CLOSE_FRAGMENT) { - // write out fragment attributes - if (fragmentAttrs) { - for (let i = 1; i < fragmentAttrs.length; i += 2) { - const value = fragmentAttrs[i] as string; - if (typeof value !== 'string') { - fragmentAttrs[i] = String(this.addRoot(value)); - } - } - fragmentAttrs = vNodeAttrsStack.pop()!; - } - depth--; - } - } - - while (depth-- > 0) { - if (fragmentAttrs) { - for (let i = 0; i < fragmentAttrs.length; i++) { - const value = fragmentAttrs[i] as string; - if (typeof value !== 'string') { - fragmentAttrs[i] = String(this.addRoot(value)); - } - } - fragmentAttrs = vNodeAttrsStack.pop()!; - } - } - } - } - } - private emitStateData(): ValueOrPromise { if (!this.serializationCtx.$roots$.length) { return; @@ -990,7 +939,7 @@ class SSRContainer extends _SharedContainer implements ISSRContainer { ? [DEBUG_TYPE, VirtualType.Projection, QSlotParent, ssrComponentNode!.id] : [QSlotParent, ssrComponentNode!.id] ); - const lastNode = this.getLastNode(); + const lastNode = this.getOrCreateLastNode(); if (lastNode.vnodeData) { lastNode.vnodeData[0] |= VNodeDataFlag.SERIALIZE; } @@ -1149,7 +1098,7 @@ class SSRContainer extends _SharedContainer implements ISSRContainer { } if (key === 'ref') { - const lastNode = this.getLastNode(); + const lastNode = this.getOrCreateLastNode(); if (isSignal(value)) { (value as SignalImpl).$untrackedValue$ = new DomRef(lastNode); continue; @@ -1164,7 +1113,7 @@ class SSRContainer extends _SharedContainer implements ISSRContainer { } if (isSignal(value)) { - const lastNode = this.getLastNode(); + const lastNode = this.getOrCreateLastNode(); const signalData = new SubscriptionData({ $scopedStyleIdPrefix$: styleScopedId, $isConst$: isConst, diff --git a/packages/qwik/src/server/ssr-node.ts b/packages/qwik/src/server/ssr-node.ts index 230c7d14b49..540a27dbaff 100644 --- a/packages/qwik/src/server/ssr-node.ts +++ b/packages/qwik/src/server/ssr-node.ts @@ -28,43 +28,34 @@ import type { VNodeData } from './vnode-data'; * Once deserialized the client, they will be turned to ElementVNodes. */ export class SsrNode implements ISsrNode { - __brand__!: 'SsrNode'; - - static ELEMENT_NODE = 1 as const; - static TEXT_NODE = 3 as const; - static DOCUMENT_NODE = 9 as const; - static DOCUMENT_FRAGMENT_NODE = 11 as const; - - /** @param nodeType - Node type: ELEMENT_NODE, TEXT_NODE, DOCUMENT_NODE */ - public nodeType: SsrNodeType; + __brand__ = 'SsrNode' as const; /** * ID which the deserialize will use to retrieve the node. * - * @param refId - Unique id for the node. + * @param id - Unique id for the node. */ public id: string; + public parentSsrNode: ISsrNode | null; + public children: ISsrNode[] | null = null; + /** Local props which don't serialize; */ - private locals: SsrAttrs | null = null; - public currentComponentNode: ISsrNode | null; - public childrenVNodeData: VNodeData[] | null = null; + private localProps: SsrAttrs | null = null; get [_EFFECT_BACK_REF]() { return this.getProp(QBackRefs); } constructor( - currentComponentNode: ISsrNode | null, - nodeType: SsrNodeType, + parentSsrNode: ISsrNode | null, id: string, private attrs: SsrAttrs, private cleanupQueue: CleanupQueue, public vnodeData: VNodeData ) { - this.currentComponentNode = currentComponentNode; - this.currentComponentNode?.addChildVNodeData(this.vnodeData); - this.nodeType = nodeType; + this.parentSsrNode = parentSsrNode; + this.parentSsrNode?.addChild(this); this.id = id; if (isDev && id.indexOf('undefined') != -1) { throw new Error(`Invalid SSR node id: ${id}`); @@ -76,7 +67,7 @@ export class SsrNode implements ISsrNode { this.attrs = []; } if (name.startsWith(NON_SERIALIZABLE_MARKER_PREFIX)) { - mapArray_set(this.locals || (this.locals = []), name, value, 0); + mapArray_set(this.localProps || (this.localProps = []), name, value, 0); } else { mapArray_set(this.attrs, name, value, 0); } @@ -89,7 +80,7 @@ export class SsrNode implements ISsrNode { getProp(name: string): any { if (name.startsWith(NON_SERIALIZABLE_MARKER_PREFIX)) { - return this.locals ? mapArray_get(this.locals, name, 0) : null; + return this.localProps ? mapArray_get(this.localProps, name, 0) : null; } else { return mapArray_get(this.attrs, name, 0); } @@ -97,19 +88,19 @@ export class SsrNode implements ISsrNode { removeProp(name: string): void { if (name.startsWith(NON_SERIALIZABLE_MARKER_PREFIX)) { - if (this.locals) { - mapApp_remove(this.locals, name, 0); + if (this.localProps) { + mapApp_remove(this.localProps, name, 0); } } else { mapApp_remove(this.attrs, name, 0); } } - addChildVNodeData(child: VNodeData): void { - if (!this.childrenVNodeData) { - this.childrenVNodeData = []; + addChild(child: ISsrNode): void { + if (!this.children) { + this.children = []; } - this.childrenVNodeData.push(child); + this.children.push(child); } toString(): string { @@ -129,11 +120,10 @@ export class SsrNode implements ISsrNode { /** A ref to a DOM element */ export class DomRef { + __brand__ = 'DomRef' as const; constructor(public $ssrNode$: ISsrNode) {} } -export type SsrNodeType = 1 | 3 | 9 | 11; - export class SsrComponentFrame implements ISsrComponentFrame { public slots = []; public projectionDepth = 0; diff --git a/packages/qwik/src/server/vnode-data.ts b/packages/qwik/src/server/vnode-data.ts index a514827eab5..ca12f18598a 100644 --- a/packages/qwik/src/server/vnode-data.ts +++ b/packages/qwik/src/server/vnode-data.ts @@ -1,5 +1,5 @@ import type { ISsrNode, SsrAttrs } from './qwik-types'; -import { SsrNode, type SsrNodeType } from './ssr-node'; +import { SsrNode } from './ssr-node'; import type { CleanupQueue } from './ssr-container'; import { VNodeDataFlag } from './types'; import { _EMPTY_ARRAY } from '@qwik.dev/core'; @@ -88,7 +88,7 @@ export function vNodeData_createSsrNodeReference( ): ISsrNode { vNodeData[0] |= VNodeDataFlag.REFERENCE; let fragmentAttrs: SsrAttrs = _EMPTY_ARRAY; - const stack: (SsrNodeType | number)[] = [SsrNode.ELEMENT_NODE, -1]; + const stack: number[] = [-1]; // We are referring to a virtual node. We need to descend into the tree to find the path to the node. for (let i = 1; i < vNodeData.length; i++) { const value = vNodeData[i]; @@ -98,11 +98,10 @@ export function vNodeData_createSsrNodeReference( if (vNodeData[i] !== WRITE_ELEMENT_ATTRS) { // ignore pushing to the stack for WRITE_ELEMENT_ATTRS, because we don't want to create more depth. It is the same element stack[stack.length - 1]++; - stack.push(SsrNode.DOCUMENT_FRAGMENT_NODE, -1); + stack.push(-1); } } else if (value === CLOSE_FRAGMENT) { stack.pop(); // pop count - stack.pop(); // pop nodeType fragmentAttrs = _EMPTY_ARRAY; } else if (value < 0) { // Negative numbers are element counts. @@ -118,15 +117,14 @@ export function vNodeData_createSsrNodeReference( let refId = String(depthFirstElementIdx); if (vNodeData[0] & (VNodeDataFlag.VIRTUAL_NODE | VNodeDataFlag.TEXT_DATA)) { // encode as alphanumeric only for virtual and text nodes - for (let i = 1; i < stack.length; i += 2) { + for (let i = 0; i < stack.length; i++) { const childCount = stack[i] as number; if (childCount >= 0) { refId += encodeAsAlphanumeric(childCount); } } } - const type = stack[stack.length - 2] as SsrNodeType; - return new SsrNode(currentComponentNode, type, refId, fragmentAttrs, cleanupQueue, vNodeData); + return new SsrNode(currentComponentNode, refId, fragmentAttrs, cleanupQueue, vNodeData); } /**