Skip to content

Commit

Permalink
Merge pull request #6973 from QwikDev/v2-nested-slots
Browse files Browse the repository at this point in the history
fix(slots): get the right component frame while processing slots
  • Loading branch information
wmertens authored Oct 14, 2024
2 parents 0191736 + 17e6e9f commit 48b5156
Show file tree
Hide file tree
Showing 8 changed files with 268 additions and 99 deletions.
38 changes: 36 additions & 2 deletions packages/qwik/src/core/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -543,6 +543,32 @@ export const _isJSXNode: <T>(n: unknown) => n is JSXNode<T>;
// @public (undocumented)
export const isSignal: (value: any) => value is Signal<unknown>;

// Warning: (ae-internal-missing-underscore) The name "ISsrComponentFrame" should be prefixed with an underscore because the declaration is marked as @internal
//
// @internal (undocumented)
export interface ISsrComponentFrame {
// Warning: (ae-forgotten-export) The symbol "ISsrNode" needs to be exported by the entry point index.d.ts
//
// (undocumented)
componentNode: ISsrNode;
// (undocumented)
consumeChildrenForSlot(projectionNode: ISsrNode, slotName: string): JSXChildren | null;
// (undocumented)
distributeChildrenIntoSlots(children: JSXChildren, parentScopedStyle: string | null, parentComponentFrame: ISsrComponentFrame | null): void;
// (undocumented)
hasSlot(slotName: string): boolean;
// (undocumented)
projectionComponentFrame: ISsrComponentFrame | null;
// (undocumented)
projectionDepth: number;
// (undocumented)
projectionScopedStyle: string | null;
// (undocumented)
releaseUnclaimedProjections(unclaimedProjections: (ISsrComponentFrame | JSXChildren | string)[]): void;
// (undocumented)
scopedStyleIds: Set<string>;
}

// @internal (undocumented)
export function _isStringifiable(value: unknown): value is _Stringifiable;

Expand Down Expand Up @@ -2049,10 +2075,18 @@ export const _waitUntilRendered: (elm: Element) => Promise<void>;
// Warning: (ae-forgotten-export) The symbol "SSRContainer" needs to be exported by the entry point index.d.ts
//
// @internal (undocumented)
export function _walkJSX(ssr: SSRContainer, value: JSXOutput, allowPromises: true, currentStyleScoped: string | null): ValueOrPromise<void>;
export function _walkJSX(ssr: SSRContainer, value: JSXOutput, options: {
allowPromises: true;
currentStyleScoped: string | null;
parentComponentFrame: ISsrComponentFrame | null;
}): ValueOrPromise<void>;

// @internal (undocumented)
export function _walkJSX(ssr: SSRContainer, value: JSXOutput, allowPromises: false, currentStyleScoped: string | null): false;
export function _walkJSX(ssr: SSRContainer, value: JSXOutput, options: {
allowPromises: false;
currentStyleScoped: string | null;
parentComponentFrame: ISsrComponentFrame | null;
}): false;

// @internal (undocumented)
export const _weakSerialize: <T extends object>(input: T) => Partial<T>;
Expand Down
5 changes: 3 additions & 2 deletions packages/qwik/src/core/debug.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { isQrl } from '../server/prefetch-strategy';
import { isJSXNode } from './shared/jsx/jsx-runtime';
import { isTask } from './use/use-task';
import { vnode_isVNode, vnode_toString } from './client/vnode';
import { vnode_getProp, vnode_isVNode } from './client/vnode';
import { ComputedSignal, WrappedSignal, isSignal } from './signal/signal';
import { isStore } from './signal/store';
import { DEBUG_TYPE } from './shared/types';

const stringifyPath: any[] = [];
export function qwikDebugToString(value: any): any {
Expand All @@ -30,7 +31,7 @@ export function qwikDebugToString(value: any): any {
stringifyPath.push(value);
if (Array.isArray(value)) {
if (vnode_isVNode(value)) {
return vnode_toString.apply(value);
return '(' + vnode_getProp(value, DEBUG_TYPE, null) + ')';
} else {
return value.map(qwikDebugToString);
}
Expand Down
1 change: 1 addition & 0 deletions packages/qwik/src/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export type {
SnapshotMeta,
SnapshotMetaValue,
SnapshotListener,
ISsrComponentFrame,
} from './ssr/ssr-types';

//////////////////////////////////////////////////////////////////////////////////////////
Expand Down
102 changes: 72 additions & 30 deletions packages/qwik/src/core/ssr/ssr-render-jsx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,44 +31,56 @@ import { qrlToString, type SerializationContext } from '../shared/shared-seriali
import { DEBUG_TYPE, VirtualType } from '../shared/types';
import { WrappedSignal, EffectProperty, isSignal } from '../signal/signal';
import { applyInlineComponent, applyQwikComponentBody } from './ssr-render-component';
import type { ISsrNode, SSRContainer, SsrAttrs } from './ssr-types';
import type { ISsrComponentFrame, ISsrNode, SSRContainer, SsrAttrs } from './ssr-types';
import { qInspector } from '../shared/utils/qdev';
import { serializeAttribute } from '../shared/utils/styles';

class SetScopedStyle {
constructor(public $scopedStyle$: string | null) {}
class ParentComponentData {
constructor(
public $scopedStyle$: string | null,
public $componentFrame$: ISsrComponentFrame | null
) {}
}
type StackFn = () => ValueOrPromise<void>;
type StackValue = ValueOrPromise<
JSXOutput | StackFn | Promise<JSXOutput> | typeof Promise | SetScopedStyle | AsyncGenerator
JSXOutput | StackFn | Promise<JSXOutput> | typeof Promise | ParentComponentData | AsyncGenerator
>;

/** @internal */
export function _walkJSX(
ssr: SSRContainer,
value: JSXOutput,
allowPromises: true,
currentStyleScoped: string | null
options: {
allowPromises: true;
currentStyleScoped: string | null;
parentComponentFrame: ISsrComponentFrame | null;
}
): ValueOrPromise<void>;
/** @internal */
export function _walkJSX(
ssr: SSRContainer,
value: JSXOutput,
allowPromises: false,
currentStyleScoped: string | null
options: {
allowPromises: false;
currentStyleScoped: string | null;
parentComponentFrame: ISsrComponentFrame | null;
}
): false;
/** @internal */
export function _walkJSX(
ssr: SSRContainer,
value: JSXOutput,
allowPromises: boolean,
currentStyleScoped: string | null
options: {
allowPromises: boolean;
currentStyleScoped: string | null;
parentComponentFrame: ISsrComponentFrame | null;
}
): ValueOrPromise<void> | false {
const stack: StackValue[] = [value];
let resolveDrain: () => void;
let rejectDrain: (reason: any) => void;
const drained =
allowPromises &&
options.allowPromises &&
new Promise<void>((res, rej) => {
resolveDrain = res;
rejectDrain = rej;
Expand All @@ -81,30 +93,34 @@ export function _walkJSX(
const drain = (): void => {
while (stack.length) {
const value = stack.pop();
if (value instanceof SetScopedStyle) {
currentStyleScoped = value.$scopedStyle$;
if (value instanceof ParentComponentData) {
options.currentStyleScoped = value.$scopedStyle$;
options.parentComponentFrame = value.$componentFrame$;
continue;
} else if (typeof value === 'function') {
if (value === Promise) {
if (!allowPromises) {
if (!options.allowPromises) {
return throwErrorAndStop('Promises not expected here.');
}
(stack.pop() as Promise<JSXOutput>).then(resolveValue, rejectDrain);
return;
}
const waitOn = (value as StackFn).apply(ssr);
if (waitOn) {
if (!allowPromises) {
if (!options.allowPromises) {
return throwErrorAndStop('Promises not expected here.');
}
waitOn.then(drain, rejectDrain);
return;
}
continue;
}
processJSXNode(ssr, enqueue, value as JSXOutput, currentStyleScoped);
processJSXNode(ssr, enqueue, value as JSXOutput, {
styleScoped: options.currentStyleScoped,
parentComponentFrame: options.parentComponentFrame,
});
}
if (stack.length === 0 && allowPromises) {
if (stack.length === 0 && options.allowPromises) {
resolveDrain();
}
};
Expand All @@ -116,7 +132,10 @@ function processJSXNode(
ssr: SSRContainer,
enqueue: (value: StackValue) => void,
value: JSXOutput,
styleScoped: string | null
options: {
styleScoped: string | null;
parentComponentFrame: ISsrComponentFrame | null;
}
) {
// console.log('processJSXNode', value);
if (value === null || value === undefined) {
Expand Down Expand Up @@ -146,7 +165,11 @@ function processJSXNode(
} else if (isAsyncGenerator(value)) {
enqueue(async () => {
for await (const chunk of value) {
await _walkJSX(ssr, chunk as JSXOutput, true, styleScoped);
await _walkJSX(ssr, chunk as JSXOutput, {
allowPromises: true,
currentStyleScoped: options.styleScoped,
parentComponentFrame: options.parentComponentFrame,
});
ssr.commentNode(FLUSH_COMMENT);
}
});
Expand All @@ -155,7 +178,7 @@ function processJSXNode(
const type = jsx.type;
// Below, JSXChildren allows functions and regexes, but we assume the dev only uses those as appropriate.
if (typeof type === 'string') {
appendClassIfScopedStyleExists(jsx, styleScoped);
appendClassIfScopedStyleExists(jsx, options.styleScoped);
appendQwikInspectorAttribute(jsx);

const innerHTML = ssr.openElement(
Expand All @@ -164,10 +187,15 @@ function processJSXNode(
jsx.varProps,
jsx.constProps,
ssr.serializationCtx,
styleScoped,
options.styleScoped,
jsx.key
),
constPropsToSsrAttrs(jsx.constProps, jsx.varProps, ssr.serializationCtx, styleScoped)
constPropsToSsrAttrs(
jsx.constProps,
jsx.varProps,
ssr.serializationCtx,
options.styleScoped
)
);
if (innerHTML) {
ssr.htmlNode(innerHTML);
Expand Down Expand Up @@ -197,17 +225,17 @@ function processJSXNode(
children != null && enqueue(children);
} else if (type === Slot) {
const componentFrame =
ssr.getNearestComponentFrame() || ssr.unclaimedProjectionComponentFrameQueue.shift();
const projectionAttrs = isDev ? [DEBUG_TYPE, VirtualType.Projection] : [];
options.parentComponentFrame || ssr.unclaimedProjectionComponentFrameQueue.shift();
if (componentFrame) {
const compId = componentFrame.componentNode.id || '';
const projectionAttrs = isDev ? [DEBUG_TYPE, VirtualType.Projection] : [];
projectionAttrs.push(':', compId);
ssr.openProjection(projectionAttrs);
const host = componentFrame.componentNode;
const node = ssr.getLastNode();
const slotName = getSlotName(host, jsx, ssr);
projectionAttrs.push(QSlot, slotName);
enqueue(new SetScopedStyle(styleScoped));
enqueue(new ParentComponentData(options.styleScoped, options.parentComponentFrame));
enqueue(ssr.closeProjection);
const slotDefaultChildren: JSXChildren | null = jsx.children || null;
const slotChildren =
Expand All @@ -216,7 +244,12 @@ function processJSXNode(
ssr.addUnclaimedProjection(componentFrame, QDefaultSlot, slotDefaultChildren);
}
enqueue(slotChildren as JSXOutput);
enqueue(new SetScopedStyle(componentFrame.childrenScopedStyle));
enqueue(
new ParentComponentData(
componentFrame.projectionScopedStyle,
componentFrame.projectionComponentFrame
)
);
} else {
// Even thought we are not projecting we still need to leave a marker for the slot.
ssr.openFragment(isDev ? [DEBUG_TYPE, VirtualType.Projection] : EMPTY_ARRAY);
Expand All @@ -231,7 +264,11 @@ function processJSXNode(
if (isFunction(generator)) {
value = generator({
async write(chunk) {
await _walkJSX(ssr, chunk as JSXOutput, true, styleScoped);
await _walkJSX(ssr, chunk as JSXOutput, {
allowPromises: true,
currentStyleScoped: options.styleScoped,
parentComponentFrame: options.parentComponentFrame,
});
ssr.commentNode(FLUSH_COMMENT);
},
});
Expand All @@ -247,14 +284,19 @@ function processJSXNode(
// 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();
ssr.getComponentFrame(0)!.distributeChildrenIntoSlots(jsx.children, styleScoped);
const componentFrame = ssr.getParentComponentFrame()!;
componentFrame!.distributeChildrenIntoSlots(
jsx.children,
options.styleScoped,
options.parentComponentFrame
);
const jsxOutput = applyQwikComponentBody(ssr, jsx, type);
const compStyleComponentId = addComponentStylePrefix(host.getProp(QScopedStyle));
enqueue(new SetScopedStyle(styleScoped));
enqueue(new ParentComponentData(options.styleScoped, options.parentComponentFrame));
enqueue(ssr.closeComponent);
enqueue(jsxOutput);
isPromise(jsxOutput) && enqueue(Promise);
enqueue(new SetScopedStyle(compStyleComponentId));
enqueue(new ParentComponentData(compStyleComponentId, componentFrame));
} else {
ssr.openFragment(isDev ? [DEBUG_TYPE, VirtualType.InlineComponent] : EMPTY_ARRAY);
enqueue(ssr.closeFragment);
Expand Down
12 changes: 9 additions & 3 deletions packages/qwik/src/core/ssr/ssr-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,22 @@ export interface ISsrNode {
removeProp(name: string): void;
}

/** @internal */
export interface ISsrComponentFrame {
componentNode: ISsrNode;
scopedStyleIds: Set<string>;
childrenScopedStyle: string | null;
projectionScopedStyle: string | null;
projectionComponentFrame: ISsrComponentFrame | null;
projectionDepth: number;
releaseUnclaimedProjections(
unclaimedProjections: (ISsrComponentFrame | JSXChildren | string)[]
): void;
consumeChildrenForSlot(projectionNode: ISsrNode, slotName: string): JSXChildren | null;
distributeChildrenIntoSlots(children: JSXChildren, scopedStyle: string | null): void;
distributeChildrenIntoSlots(
children: JSXChildren,
parentScopedStyle: string | null,
parentComponentFrame: ISsrComponentFrame | null
): void;
hasSlot(slotName: string): boolean;
}

Expand Down Expand Up @@ -74,7 +80,7 @@ export interface SSRContainer extends Container {

openComponent(attrs: SsrAttrs): void;
getComponentFrame(projectionDepth: number): ISsrComponentFrame | null;
getNearestComponentFrame(): ISsrComponentFrame | null;
getParentComponentFrame(): ISsrComponentFrame | null;
closeComponent(): void;

textNode(text: string): void;
Expand Down
Loading

0 comments on commit 48b5156

Please sign in to comment.