Skip to content

Commit 35e43d6

Browse files
committed
feat(v2): support immutable props from optimizer
1 parent 62eaec4 commit 35e43d6

11 files changed

Lines changed: 231 additions & 64 deletions

File tree

packages/docs/src/routes/api/qwik/api.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1718,7 +1718,7 @@
17181718
}
17191719
],
17201720
"kind": "Function",
1721-
"content": "> This API is provided as an alpha preview for developers and may change based on feedback that we receive. Do not use this API in a production environment.\n> \n\nLoad the prefetch graph for the container.\n\nEach Qwik container needs to include its own prefetch graph.\n\n\n```typescript\nPrefetchGraph: (opts?: {\n base?: string;\n manifestHash?: string;\n manifestURL?: string;\n}) => import(\"@builder.io/qwik/jsx-runtime\").JSXNode<\"script\">\n```\n\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| opts | { base?: string; manifestHash?: string; manifestURL?: string; } | <p>_(Optional)_ Options for the loading prefetch graph.</p><p>- <code>base</code> - Base of the graph. For a default installation this will default to <code>/build/</code>. But if more than one MFE is installed on the page, then each MFE needs to have its own base. - <code>manifestHash</code> - Hash of the manifest file to load. If not provided the hash will be extracted from the container attribute <code>q:manifest-hash</code> and assume the default build file <code>${base}/q-bundle-graph-${manifestHash}.json</code>. - <code>manifestURL</code> - URL of the manifest file to load if non-standard bundle graph location name.</p> |\n\n**Returns:**\n\nimport(\"@builder.io/qwik/jsx-runtime\").[JSXNode](#jsxnode)<!-- -->&lt;\"script\"&gt;",
1721+
"content": "> This API is provided as an alpha preview for developers and may change based on feedback that we receive. Do not use this API in a production environment.\n> \n\nLoad the prefetch graph for the container.\n\nEach Qwik container needs to include its own prefetch graph.\n\n\n```typescript\nPrefetchGraph: (opts?: {\n base?: string;\n manifestHash?: string;\n manifestURL?: string;\n}) => import(\"@builder.io/qwik/jsx-runtime\").JSXNode<\"script\">\n```\n\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| opts | { base?: string; manifestHash?: string; manifestURL?: string; } | <p>_(Optional)_ Options for the loading prefetch graph.</p><p>- <code>base</code> - Base of the graph. For a default installation this will default to <code>/build/</code>. But if more than one MFE is installed on the page, then each MFE needs to have its own base. - <code>manifestHash</code> - Hash of the manifest file to load. If not provided the hash will be extracted from the container attribute <code>q:manifest-hash</code> and assume the default build file <code>${base}/q-bundle-graph-${manifestHash}.json</code>. - <code>manifestURL</code> - URL of the manifest file to load if non-standard bundle graph location name.</p> |\n\n**Returns:**\n\nimport(\"@builder.io/qwik/jsx-runtime\").JSXNode&lt;\"script\"&gt;",
17221722
"editUrl": "https://github.com/BuilderIO/qwik/tree/main/packages/qwik/src/core/components/prefetch.ts",
17231723
"mdFile": "qwik.prefetchgraph.md"
17241724
},
@@ -1732,7 +1732,7 @@
17321732
}
17331733
],
17341734
"kind": "Function",
1735-
"content": "> This API is provided as an alpha preview for developers and may change based on feedback that we receive. Do not use this API in a production environment.\n> \n\nInstall a service worker which will prefetch the bundles.\n\nThere can only be one service worker per page. Because there can be many separate Qwik Containers on the page each container needs to load its prefetch graph using `PrefetchGraph` component.\n\n\n```typescript\nPrefetchServiceWorker: (opts: {\n base?: string;\n path?: string;\n verbose?: boolean;\n fetchBundleGraph?: boolean;\n}) => import(\"@builder.io/qwik/jsx-runtime\").JSXNode<\"script\">\n```\n\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| opts | { base?: string; path?: string; verbose?: boolean; fetchBundleGraph?: boolean; } | <p>Options for the prefetch service worker.</p><p>- <code>base</code> - Base URL for the service worker. - <code>path</code> - Path to the service worker.</p> |\n\n**Returns:**\n\nimport(\"@builder.io/qwik/jsx-runtime\").[JSXNode](#jsxnode)<!-- -->&lt;\"script\"&gt;",
1735+
"content": "> This API is provided as an alpha preview for developers and may change based on feedback that we receive. Do not use this API in a production environment.\n> \n\nInstall a service worker which will prefetch the bundles.\n\nThere can only be one service worker per page. Because there can be many separate Qwik Containers on the page each container needs to load its prefetch graph using `PrefetchGraph` component.\n\n\n```typescript\nPrefetchServiceWorker: (opts: {\n base?: string;\n path?: string;\n verbose?: boolean;\n fetchBundleGraph?: boolean;\n}) => import(\"@builder.io/qwik/jsx-runtime\").JSXNode<\"script\">\n```\n\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| opts | { base?: string; path?: string; verbose?: boolean; fetchBundleGraph?: boolean; } | <p>Options for the prefetch service worker.</p><p>- <code>base</code> - Base URL for the service worker. - <code>path</code> - Path to the service worker.</p> |\n\n**Returns:**\n\nimport(\"@builder.io/qwik/jsx-runtime\").JSXNode&lt;\"script\"&gt;",
17361736
"editUrl": "https://github.com/BuilderIO/qwik/tree/main/packages/qwik/src/core/components/prefetch.ts",
17371737
"mdFile": "qwik.prefetchserviceworker.md"
17381738
},

packages/docs/src/routes/api/qwik/index.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1833,7 +1833,7 @@ PrefetchGraph: (opts?: {
18331833
18341834
**Returns:**
18351835
1836-
import("@builder.io/qwik/jsx-runtime").[JSXNode](#jsxnode)&lt;"script"&gt;
1836+
import("@builder.io/qwik/jsx-runtime").JSXNode&lt;"script"&gt;
18371837
18381838
[Edit this section](https://github.com/BuilderIO/qwik/tree/main/packages/qwik/src/core/components/prefetch.ts)
18391839
@@ -1860,7 +1860,7 @@ PrefetchServiceWorker: (opts: {
18601860
18611861
**Returns:**
18621862
1863-
import("@builder.io/qwik/jsx-runtime").[JSXNode](#jsxnode)&lt;"script"&gt;
1863+
import("@builder.io/qwik/jsx-runtime").JSXNode&lt;"script"&gt;
18641864
18651865
[Edit this section](https://github.com/BuilderIO/qwik/tree/main/packages/qwik/src/core/components/prefetch.ts)
18661866

packages/qwik/src/core/api.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -932,7 +932,7 @@ export abstract class _SharedContainer implements Container2 {
932932
// (undocumented)
933933
readonly $scheduler$: Scheduler;
934934
// (undocumented)
935-
readonly $serverData$: Record<string, any>;
935+
$serverData$: Record<string, any>;
936936
// Warning: (ae-forgotten-export) The symbol "SubscriptionManager" needs to be exported by the entry point index.d.ts
937937
//
938938
// (undocumented)

packages/qwik/src/core/v2/client/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ export const enum VNodeFlags {
7171
ELEMENT_OR_VIRTUAL_MASK /* ** */ = 0b0011,
7272
ELEMENT_OR_TEXT_MASK /* ***** */ = 0b0101,
7373
TYPE_MASK /* **************** */ = 0b0111,
74+
INFLATED_TYPE_MASK /* ******* */ = 0b1111,
7475
Text /* ********************* */ = 0b0100,
7576
/// Extra flag which marks if a node needs to be inflated.
7677
Inflated /* ***************** */ = 0b1000,

packages/qwik/src/core/v2/client/vnode-diff.ts

Lines changed: 102 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ import { isDev } from '@builder.io/qwik/build';
22
import { type OnRenderFn } from '../../component/component.public';
33
import { SERIALIZABLE_STATE } from '../../container/serializers';
44
import { assertDefined, assertFalse } from '../../error/assert';
5+
import { _IMMUTABLE } from '../../internal';
56
import type { QRLInternal } from '../../qrl/qrl-class';
7+
import type { QRL } from '../../qrl/qrl.public';
68
import { serializeClass } from '../../render/execute-component';
79
import { Fragment, JSXNodeImpl, isJSXNode } from '../../render/jsx/jsx-runtime';
810
import { Slot } from '../../render/jsx/slot.public';
@@ -464,10 +466,47 @@ export const vnode_diff = (container: ClientContainer, jsxNode: JSXOutput, vStar
464466
}
465467
}
466468

469+
/** @param tag Returns true if `qDispatchEvent` needs patching */
470+
function createNewElement(jsx: JSXNode, tag: string): boolean {
471+
const element = container.document.createElement(tag);
472+
vnode_insertBefore(
473+
journal,
474+
vParent as ElementVNode,
475+
(vNewNode = vnode_newElement(vParent, element, tag)),
476+
vCurrent
477+
);
478+
const immutableProps = jsx.immutableProps;
479+
let needsQDispatchEventPatch = false;
480+
if (immutableProps) {
481+
// Immutable props are well immutable, they will never change!
482+
// For this reason we can cheat and write them directly into the DOM.
483+
// We never tell the vNode about them saving us time and memory.
484+
for (const key in immutableProps) {
485+
let value = immutableProps[key];
486+
if (value !== _IMMUTABLE) {
487+
if (isJsxPropertyAnEventName(key)) {
488+
// So for event handlers we must add them to the vNode so that qwikloader can look them up
489+
// But we need to mark them so that they don't get pulled into the diff.
490+
const eventName = getEventNameFromJsxProp(key);
491+
const scope = getEventNameScopeFromJsxProp(key);
492+
vnode_setProp(vNewNode, IMMUTABLE_PREFIX + ':' + scope + ':' + eventName, value);
493+
needsQDispatchEventPatch = true;
494+
continue;
495+
} else if (isClassAttr(key)) {
496+
value = serializeClassWithScopedStyle(value);
497+
}
498+
element.setAttribute(key, String(value));
499+
}
500+
}
501+
}
502+
return needsQDispatchEventPatch;
503+
}
504+
467505
function expectElement(jsx: JSXNode, tag: string) {
468506
const isSameTagName =
469507
vCurrent && vnode_isElementVNode(vCurrent) && tag === vnode_getElementName(vCurrent);
470508
let jsxKey: string | null = null;
509+
let needsQDispatchEventPatch = false;
471510
if (
472511
isSameTagName &&
473512
(jsxKey = jsx.key) == vnode_getProp(vCurrent as ElementVNode, ELEMENT_KEY, null)
@@ -480,24 +519,14 @@ export const vnode_diff = (container: ClientContainer, jsxNode: JSXOutput, vStar
480519
vNewNode = retrieveChildWithKey(jsxKey);
481520
if (vNewNode === null) {
482521
// No existing node with key exists, just create a new one.
483-
vnode_insertBefore(
484-
journal,
485-
vParent as ElementVNode,
486-
(vNewNode = vnode_newElement(vParent, container.document.createElement(tag), tag)),
487-
vCurrent
488-
);
522+
needsQDispatchEventPatch = createNewElement(jsx, tag);
489523
} else {
490524
// Existing keyed node
491525
vnode_remove(journal, vParent, vNewNode, false);
492526
vnode_insertBefore(journal, vParent as ElementVNode, vNewNode, vCurrent);
493527
}
494528
} else {
495-
vnode_insertBefore(
496-
journal,
497-
vParent as ElementVNode,
498-
(vNewNode = vnode_newElement(vParent, container.document.createElement(tag), tag)),
499-
vCurrent
500-
);
529+
needsQDispatchEventPatch = createNewElement(jsx, tag);
501530
}
502531
// reconcile attributes
503532
let jsxAttrs = (jsx as any as { attrs: SsrAttrs }).attrs;
@@ -518,16 +547,48 @@ export const vnode_diff = (container: ClientContainer, jsxNode: JSXOutput, vStar
518547
}
519548
}
520549
}
521-
setBulkProps((vNewNode || vCurrent) as ElementVNode, jsxAttrs);
550+
const vNode = (vNewNode || vCurrent) as ElementVNode;
551+
needsQDispatchEventPatch = setBulkProps(vNode, jsxAttrs) || needsQDispatchEventPatch;
552+
if (needsQDispatchEventPatch) {
553+
// Event handler needs to be patched onto the element.
554+
const element = vnode_getNode(vNode) as QElement2;
555+
if (!element.qDispatchEvent) {
556+
element.qDispatchEvent = (event: Event) => {
557+
let eventName = event.type;
558+
let scope = '';
559+
if (eventName.startsWith(':')) {
560+
// :document:event or :window:event
561+
const colonIndex = eventName.substring(1).indexOf(':');
562+
scope = eventName.substring(1, colonIndex + 1);
563+
eventName = eventName.substring(colonIndex + 2);
564+
}
565+
566+
const eventProp = ':' + scope + ':' + eventName;
567+
const qrls = [
568+
vnode_getProp<QRL>(vNode, eventProp, null),
569+
vnode_getProp<QRL>(vNode, IMMUTABLE_PREFIX + eventProp, null),
570+
];
571+
let returnValue = false;
572+
qrls.flat(2).forEach((qrl) => {
573+
if (qrl) {
574+
const value = qrl(event) as any;
575+
returnValue = returnValue || value === true;
576+
}
577+
});
578+
return returnValue;
579+
};
580+
}
581+
}
522582
}
523583

524-
function setBulkProps(vnode: ElementVNode, srcAttrs: SsrAttrs) {
584+
/** @param tag Returns true if `qDispatchEvent` needs patching */
585+
function setBulkProps(vnode: ElementVNode, srcAttrs: SsrAttrs): boolean {
525586
vnode_ensureElementInflated(vnode);
526587
const dstAttrs = vnode as SsrAttrs;
527588
let srcIdx = 0;
528589
const srcLength = srcAttrs.length;
529590
let dstIdx = ElementVNodeProps.PROPS_OFFSET;
530-
const dstLength = dstAttrs.length;
591+
let dstLength = dstAttrs.length;
531592
let srcKey: string | null = srcIdx < srcLength ? srcAttrs[srcIdx++] : null;
532593
let dstKey: string | null = dstIdx < dstLength ? dstAttrs[dstIdx++] : null;
533594
let patchEventDispatch = false;
@@ -537,18 +598,23 @@ export const vnode_diff = (container: ClientContainer, jsxNode: JSXOutput, vStar
537598
vnode_setProp(vnode, key, value);
538599
} else {
539600
if (isClassAttr(key)) {
540-
const serializedClass = serializeClass(value);
541-
value =
542-
scopedStyleIdPrefix && serializedClass
543-
? `${scopedStyleIdPrefix} ${serializedClass}`
544-
: serializedClass;
601+
value = serializeClassWithScopedStyle(value);
545602
}
546603
vnode_setAttr(journal, vnode, key, value);
604+
if (value === null) {
605+
// if we set `null` than attribute was removed and we need to shorten the dstLength
606+
dstLength = dstAttrs.length;
607+
}
547608
}
548609
};
549610

550611
while (srcKey !== null || dstKey !== null) {
551-
if (srcKey == null) {
612+
if (dstKey?.startsWith(IMMUTABLE_PREFIX)) {
613+
// This is a special key which we use to mark the event handlers as immutable.
614+
// we need to ignore them.
615+
dstIdx++; // skip the destination value, we don't care about it.
616+
dstKey = dstIdx < dstLength ? dstAttrs[dstIdx++] : null;
617+
} else if (srcKey == null) {
552618
// Source has more keys, so we need to remove them from destination
553619
if (dstKey && isHtmlAttributeAnEventName(dstKey)) {
554620
patchEventDispatch = true;
@@ -604,31 +670,16 @@ export const vnode_diff = (container: ClientContainer, jsxNode: JSXOutput, vStar
604670
dstKey = dstIdx < dstLength ? dstAttrs[dstIdx++] : null;
605671
}
606672
}
607-
if (patchEventDispatch) {
608-
const element = vnode_getNode(vnode) as QElement2;
609-
if (!element.qDispatchEvent) {
610-
element.qDispatchEvent = (event: Event) => {
611-
let eventName = event.type;
612-
let scope = '';
613-
if (eventName.startsWith(':')) {
614-
// :document:event or :window:event
615-
const colonIndex = eventName.substring(1).indexOf(':');
616-
scope = eventName.substring(1, colonIndex + 1);
617-
eventName = eventName.substring(colonIndex + 2);
618-
}
673+
return patchEventDispatch;
674+
}
619675

620-
const eventProp = ':' + scope + ':' + eventName;
621-
const qrls = vnode_getProp(vnode, eventProp, null);
622-
let returnValue = false;
623-
qrls &&
624-
(Array.isArray(qrls) ? qrls : [qrls]).forEach((qrl) => {
625-
const value = qrl(event);
626-
returnValue = returnValue || value === true;
627-
});
628-
return returnValue;
629-
};
630-
}
631-
}
676+
function serializeClassWithScopedStyle(value: any) {
677+
const serializedClass = serializeClass(value);
678+
value =
679+
scopedStyleIdPrefix && serializedClass
680+
? `${scopedStyleIdPrefix} ${serializedClass}`
681+
: serializedClass;
682+
return value;
632683
}
633684

634685
function retrieveScopedStyleIdPrefix() {
@@ -947,3 +998,9 @@ export function releaseSubscriptions(container: ClientContainer, vNode: VNode) {
947998
}
948999
} while (true as boolean);
9491000
}
1001+
1002+
/**
1003+
* This marks the property as immutable. It is needed for the QRLs so that QwikLoader can get a hold
1004+
* of them. This character must be `:` so that the `vnode_getAttr` can ignore them.
1005+
*/
1006+
const IMMUTABLE_PREFIX = ':';

packages/qwik/src/core/v2/client/vnode.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ export const vnode_newElement = (
179179
tag: string
180180
): ElementVNode => {
181181
const vnode: ElementVNode = VNodeArray.createElement(
182-
VNodeFlags.Element | (-1 << VNodeFlagsIndex.shift), // Flag
182+
VNodeFlags.Element | VNodeFlags.Inflated | (-1 << VNodeFlagsIndex.shift), // Flag
183183
parentNode as VNode | null,
184184
null,
185185
null,
@@ -359,15 +359,18 @@ export const vnode_getNodeTypeName = (vNode: VNode): string => {
359359

360360
export const vnode_ensureElementInflated = (vnode: VNode) => {
361361
const flags = vnode[VNodeProps.flags];
362-
if ((flags & VNodeFlags.TYPE_MASK) === VNodeFlags.Element) {
363-
const elementVNode = ensureElementVNode(vnode);
362+
if ((flags & VNodeFlags.INFLATED_TYPE_MASK) === VNodeFlags.Element) {
363+
const elementVNode = vnode as ElementVNode;
364364
elementVNode[VNodeProps.flags] ^= VNodeFlags.Inflated;
365365
const element = elementVNode[ElementVNodeProps.element];
366366
const attributes = element.attributes;
367367
for (let idx = 0; idx < attributes.length; idx++) {
368368
const attr = attributes[idx];
369369
const key = attr.name;
370-
if (!key.startsWith('on:')) {
370+
if (key == ':') {
371+
// all attributes after the ':' are considered immutable, and so we ignore them.
372+
break;
373+
} else if (!key.startsWith('on:')) {
371374
const value = attr.value;
372375
mapArray_set(elementVNode as string[], key, value, vnode_getPropStartIndex(vnode));
373376
}

packages/qwik/src/core/v2/container.unit.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -498,7 +498,8 @@ function toHTML(jsx: JSXOutput): string {
498498
if (typeof jsx.type === 'string') {
499499
ssrContainer.openElement(
500500
jsx.type,
501-
toSsrAttrs(jsx.props as any, ssrContainer.serializationCtx)
501+
toSsrAttrs(jsx.props as any, ssrContainer.serializationCtx),
502+
toSsrAttrs(jsx.immutableProps as any, ssrContainer.serializationCtx)
502503
);
503504
} else {
504505
ssrContainer.openFragment([]);

0 commit comments

Comments
 (0)