diff --git a/packages/qwik/src/core/api.md b/packages/qwik/src/core/api.md index b903e8d9f4b..136a9987ba2 100644 --- a/packages/qwik/src/core/api.md +++ b/packages/qwik/src/core/api.md @@ -182,7 +182,7 @@ class DomContainer extends _SharedContainer implements ClientContainer { // (undocumented) ensureProjectionResolved(vNode: _VirtualVNode): void; // (undocumented) - getHostProp(host: HostElement, name: string): T | null; + getHostProp(host: HostElement, name: NumericPropKey): T | null; // (undocumented) getParentHost(host: HostElement): HostElement | null; // (undocumented) @@ -209,8 +209,10 @@ class DomContainer extends _SharedContainer implements ClientContainer { scheduleRender(): Promise; // (undocumented) setContext(host: HostElement, context: ContextId, value: T): void; + // Warning: (ae-forgotten-export) The symbol "NumericPropKey" needs to be exported by the entry point index.d.ts + // // (undocumented) - setHostProp(host: HostElement, name: string, value: T): void; + setHostProp(host: HostElement, name: NumericPropKey, value: T): void; // (undocumented) vNodeLocate: (id: string | Element) => _VNode; } @@ -247,7 +249,7 @@ _VNode | null | undefined, Element, //////////////////// 6 - Element string | undefined, -(string | null)[] +(NumericPropKey | string | null)[] ] & { __brand__: 'ElementVNode'; }; @@ -360,7 +362,7 @@ export interface ISsrComponentFrame { // (undocumented) distributeChildrenIntoSlots(children: JSXChildren, parentScopedStyle: string | null, parentComponentFrame: ISsrComponentFrame | null): void; // (undocumented) - hasSlot(slotName: string): boolean; + hasSlot(slotNameKey: NumericPropKey): boolean; // (undocumented) projectionComponentFrame: ISsrComponentFrame | null; // (undocumented) @@ -416,11 +418,11 @@ export interface JSXNode extends JSXNode { // (undocumented) - constProps: Record | null; + constProps: Record | null; // (undocumented) flags: number; // (undocumented) - varProps: Record; + varProps: Record; } // @public @@ -844,7 +846,7 @@ export abstract class _SharedContainer implements Container { // (undocumented) abstract ensureProjectionResolved(host: HostElement): void; // (undocumented) - abstract getHostProp(host: HostElement, name: string): T | null; + abstract getHostProp(host: HostElement, name: NumericPropKey): T | null; // (undocumented) abstract getParentHost(host: HostElement): HostElement | null; // (undocumented) @@ -868,7 +870,7 @@ export abstract class _SharedContainer implements Container { // (undocumented) abstract setContext(host: HostElement, context: ContextId, value: T): void; // (undocumented) - abstract setHostProp(host: HostElement, name: string, value: T): void; + abstract setHostProp(host: HostElement, name: NumericPropKey, value: T): void; // (undocumented) trackSignalValue(signal: Signal, subscriber: HostElement, property: string, data: _EffectData): T; } @@ -1712,7 +1714,7 @@ _VNode | null, _VNode | null, /////////////// 4 - First child _VNode | null, -(string | null | boolean)[] +(NumericPropKey | string | null | boolean)[] ] & { __brand__: 'FragmentNode' & 'HostElement'; }; diff --git a/packages/qwik/src/core/client/dom-container.ts b/packages/qwik/src/core/client/dom-container.ts index 04e32b4435b..da083efb769 100644 --- a/packages/qwik/src/core/client/dom-container.ts +++ b/packages/qwik/src/core/client/dom-container.ts @@ -12,27 +12,19 @@ import { inflateQRL, parseQRL, wrapDeserializerProxy } from '../shared/shared-se import { QContainerValue, type HostElement, type ObjToProxyMap } from '../shared/types'; import { EMPTY_ARRAY } from '../shared/utils/flyweight'; import { - ELEMENT_PROPS, - ELEMENT_SEQ, - ELEMENT_SEQ_IDX, - OnRenderProp, QBaseAttr, QContainerAttr, QContainerSelector, - QCtxAttr, QInstanceAttr, - QScopedStyle, - QSlotParent, QStyle, QStyleSelector, - QBackRefs, Q_PROPS_SEPARATOR, - USE_ON_LOCAL_SEQ_IDX, getQFuncs, QLocaleAttr, } from '../shared/utils/markers'; import { isPromise } from '../shared/utils/promises'; -import { isSlotProp } from '../shared/utils/prop'; +import { isSlotProp } from '../shared/utils/numeric-prop-key-flags'; +import { StaticPropId, getPropId, type NumericPropKey } from '../shared/utils/numeric-prop-key'; import { qDev } from '../shared/utils/qdev'; import { convertScopedStyleIdsToArray, @@ -224,18 +216,18 @@ export class DomContainer extends _SharedContainer implements IClientContainer { } setContext(host: HostElement, context: ContextId, value: T): void { - let ctx = this.getHostProp>(host, QCtxAttr); + let ctx = this.getHostProp>(host, StaticPropId.CTX); if (!ctx) { - this.setHostProp(host, QCtxAttr, (ctx = [])); + this.setHostProp(host, StaticPropId.CTX, (ctx = [])); } - mapArray_set(ctx, context.id, value, 0); + mapArray_set(ctx, getPropId(context.id), value, 0); } resolveContext(host: HostElement, contextId: ContextId): T | undefined { while (host) { - const ctx = this.getHostProp>(host, QCtxAttr); + const ctx = this.getHostProp>(host, StaticPropId.CTX); if (ctx) { - const value = mapArray_get(ctx, contextId.id, 0) as T; + const value = mapArray_get(ctx, getPropId(contextId.id), 0) as T; if (value) { return value as T; } @@ -249,13 +241,13 @@ export class DomContainer extends _SharedContainer implements IClientContainer { let vNode = vnode_getParent(host as any); while (vNode) { if (vnode_isVirtualVNode(vNode)) { - if (vnode_getProp(vNode, OnRenderProp, null) !== null) { + if (vnode_getProp(vNode, StaticPropId.ON_RENDER, null) !== null) { return vNode as any as HostElement; } vNode = vnode_getParent(vNode) || // If virtual node, than it could be a slot so we need to read its parent. - vnode_getProp(vNode, QSlotParent, this.vNodeLocate); + vnode_getProp(vNode, StaticPropId.SLOT_PARENT, this.vNodeLocate); } else { vNode = vnode_getParent(vNode); } @@ -263,24 +255,24 @@ export class DomContainer extends _SharedContainer implements IClientContainer { return null; } - setHostProp(host: HostElement, name: string, value: T): void { + setHostProp(host: HostElement, name: NumericPropKey, value: T): void { const vNode: VirtualVNode = host as any; vnode_setProp(vNode, name, value); } - getHostProp(host: HostElement, name: string): T | null { + getHostProp(host: HostElement, name: NumericPropKey): T | null { const vNode: VirtualVNode = host as any; let getObjectById: ((id: string) => any) | null = null; switch (name) { - case ELEMENT_SEQ: - case ELEMENT_PROPS: - case OnRenderProp: - case QCtxAttr: - case QBackRefs: + case StaticPropId.ELEMENT_SEQ: + case StaticPropId.ELEMENT_PROPS: + case StaticPropId.ON_RENDER: + case StaticPropId.CTX: + case StaticPropId.BACK_REFS: getObjectById = this.$getObjectById$; break; - case ELEMENT_SEQ_IDX: - case USE_ON_LOCAL_SEQ_IDX: + case StaticPropId.ELEMENT_SEQ_IDX: + case StaticPropId.USE_ON_LOCAL_SEQ_IDX: getObjectById = parseInt; break; } @@ -319,7 +311,7 @@ export class DomContainer extends _SharedContainer implements IClientContainer { vNode[VNodeProps.flags] |= VNodeFlags.Resolved; const props = vnode_getProps(vNode); for (let i = 0; i < props.length; i = i + 2) { - const prop = props[i] as string; + const prop = props[i] as NumericPropKey; if (isSlotProp(prop)) { const value = props[i + 1]; if (typeof value == 'string') { @@ -349,10 +341,10 @@ export class DomContainer extends _SharedContainer implements IClientContainer { $appendStyle$(content: string, styleId: string, host: VirtualVNode, scoped: boolean): void { if (scoped) { - const scopedStyleIdsString = this.getHostProp(host, QScopedStyle); + const scopedStyleIdsString = this.getHostProp(host, StaticPropId.SCOPED_STYLE); const scopedStyleIds = new Set(convertScopedStyleIdsToArray(scopedStyleIdsString)); scopedStyleIds.add(styleId); - this.setHostProp(host, QScopedStyle, convertStyleIdsToString(scopedStyleIds)); + this.setHostProp(host, StaticPropId.SCOPED_STYLE, convertStyleIdsToString(scopedStyleIds)); } if (this.$styleIds$ == null) { diff --git a/packages/qwik/src/core/client/types.ts b/packages/qwik/src/core/client/types.ts index 8d7cb8c6ea0..93008394044 100644 --- a/packages/qwik/src/core/client/types.ts +++ b/packages/qwik/src/core/client/types.ts @@ -2,11 +2,11 @@ import type { QRL } from '../shared/qrl/qrl.public'; import type { Container } from '../shared/types'; +import type { NumericPropKey } from '../shared/utils/numeric-prop-key'; import type { VNodeJournal } from './vnode'; -export type ClientAttrKey = string; -export type ClientAttrValue = string | null; -export type ClientAttrs = Array; +export type ClientAttrValue = unknown | null; +export type ClientAttrs = Array; /** @internal */ export interface ClientContainer extends Container { @@ -128,7 +128,7 @@ export type ElementVNode = [ Element, //////////////////// 6 - Element string | undefined, ///////// 7 - tag /// Props - (string | null)[], /////// 8 - attrs + (NumericPropKey | string | null)[], /////// 8 - attrs ] & { __brand__: 'ElementVNode' }; export const enum TextVNodeProps { @@ -165,7 +165,7 @@ export type VirtualVNode = [ VNode | null, /////////////// 4 - First child VNode | null, /////////////// 5 - Last child /// Props - (string | null | boolean)[], /////// 6 - attrs + (NumericPropKey | string | null | boolean)[], /////// 6 - attrs ] & { __brand__: 'FragmentNode' & 'HostElement' }; /** @internal */ diff --git a/packages/qwik/src/core/client/util-mapArray.ts b/packages/qwik/src/core/client/util-mapArray.ts index 1928c560369..4da4a585c72 100644 --- a/packages/qwik/src/core/client/util-mapArray.ts +++ b/packages/qwik/src/core/client/util-mapArray.ts @@ -1,12 +1,17 @@ import { assertTrue } from '../shared/error/assert'; +import type { NumericPropKey } from '../shared/utils/numeric-prop-key'; -export const mapApp_findIndx = (array: (T | null)[], key: string, start: number): number => { +export const mapApp_findIndx = ( + array: (T | null)[], + key: NumericPropKey, + start: number = 0 +): number => { assertTrue(start % 2 === 0, 'Expecting even number.'); let bottom = (start as number) >> 1; let top = (array.length - 2) >> 1; while (bottom <= top) { const mid = bottom + ((top - bottom) >> 1); - const midKey = array[mid << 1] as string; + const midKey = array[mid << 1] as NumericPropKey; if (midKey === key) { return mid << 1; } @@ -21,23 +26,27 @@ export const mapApp_findIndx = (array: (T | null)[], key: string, start: numb export const mapArray_set = ( array: (T | null)[], - key: string, - value: T | null, - start: number + key: NumericPropKey, + value: unknown | null, + start: number = 0 ) => { const indx = mapApp_findIndx(array, key, start); if (indx >= 0) { if (value == null) { array.splice(indx, 2); } else { - array[indx + 1] = value; + array[indx + 1] = value as T; } } else if (value != null) { - array.splice(indx ^ -1, 0, key as any, value); + array.splice(indx ^ -1, 0, key as any, value as T); } }; -export const mapApp_remove = (array: (T | null)[], key: string, start: number): T | null => { +export const mapApp_remove = ( + array: (T | null)[], + key: NumericPropKey, + start: number = 0 +): T | null => { const indx = mapApp_findIndx(array, key, start); let value: T | null = null; if (indx >= 0) { @@ -48,7 +57,11 @@ export const mapApp_remove = (array: (T | null)[], key: string, start: number return value; }; -export const mapArray_get = (array: (T | null)[], key: string, start: number): T | null => { +export const mapArray_get = ( + array: (T | null)[], + key: NumericPropKey, + start: number = 0 +): T | null => { const indx = mapApp_findIndx(array, key, start); if (indx >= 0) { return array[indx + 1] as T | null; @@ -57,6 +70,10 @@ export const mapArray_get = (array: (T | null)[], key: string, start: number) } }; -export const mapArray_has = (array: (T | null)[], key: string, start: number): boolean => { +export const mapArray_has = ( + array: (T | null)[], + key: NumericPropKey, + start: number = 0 +): boolean => { return mapApp_findIndx(array, key, start) >= 0; }; diff --git a/packages/qwik/src/core/client/vnode-diff.ts b/packages/qwik/src/core/client/vnode-diff.ts index 5ad790332d8..50be9192b30 100644 --- a/packages/qwik/src/core/client/vnode-diff.ts +++ b/packages/qwik/src/core/client/vnode-diff.ts @@ -19,17 +19,10 @@ import { TaskFlags, cleanupTask, isTask } from '../use/use-task'; import { EMPTY_OBJ } from '../shared/utils/flyweight'; import { ELEMENT_KEY, - ELEMENT_PROPS, - ELEMENT_SEQ, - OnRenderProp, QContainerAttr, QDefaultSlot, - QSlot, - QSlotParent, - QBackRefs, QTemplate, - Q_PREFIX, - dangerouslySetInnerHTML, + HANDLER_PREFIX, } from '../shared/utils/markers'; import { isPromise } from '../shared/utils/promises'; import { type ValueOrPromise } from '../shared/utils/types'; @@ -37,8 +30,6 @@ import { convertEventNameFromJsxPropToHtmlAttr, getEventNameFromJsxProp, getEventNameScopeFromJsxProp, - isHtmlAttributeAnEventName, - isJsxPropertyAnEventName, } from '../shared/utils/event-names'; import { ChoreType } from '../shared/util-chore-type'; import { hasClassAttr } from '../shared/utils/scoped-styles'; @@ -48,8 +39,6 @@ import type { DomContainer } from './dom-container'; import { VNodeFlags, VNodeProps, - type ClientAttrKey, - type ClientAttrs, type ClientContainer, type ElementVNode, type TextVNode, @@ -90,10 +79,22 @@ import { import { mapApp_findIndx } from './util-mapArray'; import { mapArray_set } from './util-mapArray'; import { getNewElementNamespaceData } from './vnode-namespace'; -import { WrappedSignal, EffectProperty, isSignal, SubscriptionData } from '../signal/signal'; +import { EffectProperty, isSignal, SubscriptionData } from '../signal/signal'; import type { Signal } from '../signal/signal.public'; import { executeComponent } from '../shared/component-execution'; -import { isSlotProp } from '../shared/utils/prop'; +import { getSlotName } from '../shared/utils/prop'; +import { + isEventProp, + startsWithColon, + isQProp, + isSlotProp, +} from '../shared/utils/numeric-prop-key-flags'; +import { + StaticPropId, + getPropId, + getPropName, + type NumericPropKey, +} from '../shared/utils/numeric-prop-key'; import { escapeHTML } from '../shared/utils/character-escaping'; import { clearAllEffects } from '../signal/signal-cleanup'; import { serializeAttribute } from '../shared/utils/styles'; @@ -402,15 +403,15 @@ export const vnode_diff = ( return new JSXNodeImpl(Projection, EMPTY_OBJ, null, [], 0, slotName); }; - const projections: Array = []; + const projections: Array = []; if (host) { const props = vnode_getProps(host); // we need to create empty projections for all the slots to remove unused slots content for (let i = 0; i < props.length; i = i + 2) { - const prop = props[i] as string; + const prop = props[i] as NumericPropKey; if (isSlotProp(prop)) { - const slotName = prop; - projections.push(slotName); + const slotName = getPropName(prop); + projections.push(prop); projections.push(createProjectionJSXNode(slotName)); } } @@ -425,14 +426,20 @@ export const vnode_diff = ( for (let i = 0; i < projectionChildren.length; i++) { const child = projectionChildren[i]; const slotName = String( - (isJSXNode(child) && directGetPropsProxyProp(child, QSlot)) || QDefaultSlot + (isJSXNode(child) && directGetPropsProxyProp(child, StaticPropId.SLOT)) || QDefaultSlot ); - const idx = mapApp_findIndx(projections, slotName, 0); + const slotNameNumeric = getPropId(slotName); + const idx = mapApp_findIndx(projections, slotNameNumeric, 0); let jsxBucket: JSXNodeImpl; if (idx >= 0) { jsxBucket = projections[idx + 1] as any; } else { - projections.splice(~idx, 0, slotName, (jsxBucket = createProjectionJSXNode(slotName))); + projections.splice( + ~idx, + 0, + slotNameNumeric, + (jsxBucket = createProjectionJSXNode(slotName)) + ); } const removeProjection = child === false; if (!removeProjection) { @@ -452,7 +459,7 @@ export const vnode_diff = ( // console.log('expectProjection', JSON.stringify(slotName)); vCurrent = vnode_getProp( vParent, // The parent is the component and it should have our portal. - slotName, + getPropId(slotName), (id) => vnode_locate(container.rootVNode, id) ); // if projection is marked as deleted then we need to create a new one @@ -463,24 +470,24 @@ export const vnode_diff = ( // that is wrong. We don't yet know if the projection will be projected, so // we should leave it unattached. // vNewNode[VNodeProps.parent] = vParent; - isDev && vnode_setProp(vNewNode, DEBUG_TYPE, VirtualType.Projection); - isDev && vnode_setProp(vNewNode, 'q:code', 'expectProjection'); - vnode_setProp(vNewNode as VirtualVNode, QSlot, slotName); - vnode_setProp(vNewNode as VirtualVNode, QSlotParent, vParent); - vnode_setProp(vParent as VirtualVNode, slotName, vNewNode); + isDev && vnode_setProp(vNewNode, getPropId(DEBUG_TYPE), VirtualType.Projection); + isDev && vnode_setProp(vNewNode, getPropId('q:code'), 'expectProjection'); + vnode_setProp(vNewNode as VirtualVNode, StaticPropId.SLOT, slotName); + vnode_setProp(vNewNode as VirtualVNode, StaticPropId.SLOT_PARENT, vParent); + vnode_setProp(vParent as VirtualVNode, getPropId(slotName), vNewNode); } } function expectSlot() { const vHost = vnode_getProjectionParentComponent(vParent, container.rootVNode); - const slotNameKey = getSlotNameKey(vHost); + const slotNameKey = getSlotName(vHost, jsxValue as JSXNodeInternal, container); // console.log('expectSlot', JSON.stringify(slotNameKey)); const vProjectedNode = vHost ? vnode_getProp( vHost, - slotNameKey, + getPropId(slotNameKey), // for slots this id is vnode ref id null // Projections should have been resolved through container.ensureProjectionResolved //(id) => vnode_locate(container.rootVNode, id) @@ -495,10 +502,10 @@ export const vnode_diff = ( (vNewNode = vnode_newVirtual()), vCurrent && getInsertBefore() ); - vnode_setProp(vNewNode, QSlot, slotNameKey); - vHost && vnode_setProp(vHost, slotNameKey, vNewNode); - isDev && vnode_setProp(vNewNode, DEBUG_TYPE, VirtualType.Projection); - isDev && vnode_setProp(vNewNode, 'q:code', 'expectSlot' + count++); + vnode_setProp(vNewNode, StaticPropId.SLOT, slotNameKey); + vHost && vnode_setProp(vHost, getPropId(slotNameKey), vNewNode); + isDev && vnode_setProp(vNewNode, getPropId(DEBUG_TYPE), VirtualType.Projection); + isDev && vnode_setProp(vNewNode, getPropId('q:code'), 'expectSlot' + count++); return false; } else if (vProjectedNode === vCurrent) { // All is good. @@ -526,26 +533,14 @@ export const vnode_diff = ( (vNewNode = vProjectedNode), vCurrent && getInsertBefore() ); - vnode_setProp(vNewNode, QSlot, slotNameKey); - vHost && vnode_setProp(vHost, slotNameKey, vNewNode); - isDev && vnode_setProp(vNewNode, DEBUG_TYPE, VirtualType.Projection); - isDev && vnode_setProp(vNewNode, 'q:code', 'expectSlot' + count++); + vnode_setProp(vNewNode, StaticPropId.SLOT, slotNameKey); + vHost && vnode_setProp(vHost, getPropId(slotNameKey), vNewNode); + isDev && vnode_setProp(vNewNode, getPropId(DEBUG_TYPE), VirtualType.Projection); + isDev && vnode_setProp(vNewNode, getPropId('q:code'), 'expectSlot' + count++); } return true; } - function getSlotNameKey(vHost: VNode | null) { - const jsxNode = jsxValue as JSXNodeInternal; - const constProps = jsxNode.constProps; - if (constProps && typeof constProps == 'object' && 'name' in constProps) { - const constValue = constProps.name; - if (vHost && constValue instanceof WrappedSignal) { - return trackSignalAndAssignHost(constValue, vHost, EffectProperty.COMPONENT, container); - } - } - return directGetPropsProxyProp(jsxNode, 'name') || QDefaultSlot; - } - function drainAsyncQueue(): ValueOrPromise { while (asyncQueue.length) { const jsxNode = asyncQueue.shift() as ValueOrPromise; @@ -619,16 +614,18 @@ export const vnode_diff = ( // For this reason we can cheat and write them directly into the DOM. // We never tell the vNode about them saving us time and memory. for (const key in constProps) { - let value = constProps[key]; - if (isJsxPropertyAnEventName(key)) { + let value = constProps[key as unknown as NumericPropKey]; + const numericKey = Number(key) as NumericPropKey; + const nameKey = getPropName(numericKey); + if (isEventProp(numericKey)) { // So for event handlers we must add them to the vNode so that qwikloader can look them up // But we need to mark them so that they don't get pulled into the diff. - const eventName = getEventNameFromJsxProp(key); - const scope = getEventNameScopeFromJsxProp(key); + const eventName = getEventNameFromJsxProp(nameKey); + const scope = getEventNameScopeFromJsxProp(nameKey); if (eventName) { vnode_setProp( vNewNode as ElementVNode, - HANDLER_PREFIX + ':' + scope + ':' + eventName, + getPropId(HANDLER_PREFIX + ':' + scope + ':' + eventName), value ); registerQwikLoaderEvent(eventName); @@ -638,9 +635,9 @@ export const vnode_diff = ( // add an event attr with empty value for qwikloader element selector. // We don't need value here. For ssr this value is a QRL, // but for CSR value should be just empty - const htmlEvent = convertEventNameFromJsxPropToHtmlAttr(key); + const htmlEvent = convertEventNameFromJsxPropToHtmlAttr(nameKey); if (htmlEvent) { - vnode_setAttr(journal, vNewNode as ElementVNode, htmlEvent, ''); + vnode_setAttr(journal, vNewNode as ElementVNode, getPropId(htmlEvent), ''); } } @@ -648,7 +645,7 @@ export const vnode_diff = ( continue; } - if (key === 'ref') { + if (numericKey === StaticPropId.REF) { if (isSignal(value)) { value.value = element; continue; @@ -670,19 +667,19 @@ export const vnode_diff = ( value = trackSignalAndAssignHost( value as Signal, vNewNode as ElementVNode, - key, + nameKey, container, signalData ); } - if (key === dangerouslySetInnerHTML) { + if (numericKey === StaticPropId.INNER_HTML) { element.innerHTML = value as string; element.setAttribute(QContainerAttr, QContainerValue.HTML); continue; } - if (elementName === 'textarea' && key === 'value') { + if (elementName === 'textarea' && numericKey === StaticPropId.VALUE) { if (value && typeof value !== 'string') { if (isDev) { throw qError(QError.wrongTextareaValue, [currentFile, value]); @@ -693,16 +690,16 @@ export const vnode_diff = ( continue; } - value = serializeAttribute(key, value, scopedStyleIdPrefix); + value = serializeAttribute(nameKey, value, scopedStyleIdPrefix); if (value != null) { - element.setAttribute(key, String(value)); + element.setAttribute(nameKey, String(value)); } } } const key = jsx.key; if (key) { element.setAttribute(ELEMENT_KEY, key); - vnode_setProp(vNewNode as ElementVNode, ELEMENT_KEY, key); + vnode_setProp(vNewNode as ElementVNode, StaticPropId.ELEMENT_KEY, key); } // append class attribute if styleScopedId exists and there is no class attribute @@ -759,16 +756,16 @@ export const vnode_diff = ( } // reconcile attributes - const jsxAttrs = [] as ClientAttrs; + const jsxAttrs: NumericPropKey[] = []; const props = jsx.varProps; for (const key in props) { - const value = props[key]; + const value = props[key as unknown as NumericPropKey]; if (value != null) { - mapArray_set(jsxAttrs, key, value, 0); + mapArray_set(jsxAttrs, Number(key) as NumericPropKey, value, 0); } } if (jsxKey !== null) { - mapArray_set(jsxAttrs, ELEMENT_KEY, jsxKey, 0); + mapArray_set(jsxAttrs, StaticPropId.ELEMENT_KEY as NumericPropKey, jsxKey, 0); } const vNode = (vNewNode || vCurrent) as ElementVNode; needsQDispatchEventPatch = @@ -781,8 +778,8 @@ export const vnode_diff = ( const eventName = event.type; const eventProp = ':' + scope.substring(1) + ':' + eventName; const qrls = [ - vnode_getProp(vNode, eventProp, null), - vnode_getProp(vNode, HANDLER_PREFIX + eventProp, null), + vnode_getProp(vNode, getPropId(eventProp), null), + vnode_getProp(vNode, getPropId(HANDLER_PREFIX + eventProp), null), ]; let returnValue = false; qrls.flat(2).forEach((qrl) => { @@ -805,26 +802,26 @@ export const vnode_diff = ( /** @returns True if `qDispatchEvent` needs patching */ function setBulkProps( vnode: ElementVNode, - srcAttrs: ClientAttrs, + srcAttrs: NumericPropKey[], currentFile?: string | null ): boolean { vnode_ensureElementInflated(vnode); - const dstAttrs = vnode_getProps(vnode) as ClientAttrs; + const dstAttrs = vnode_getProps(vnode) as NumericPropKey[]; let srcIdx = 0; const srcLength = srcAttrs.length; let dstIdx = 0; let dstLength = dstAttrs.length; - let srcKey: ClientAttrKey | null = srcIdx < srcLength ? srcAttrs[srcIdx++] : null; - let dstKey: ClientAttrKey | null = dstIdx < dstLength ? dstAttrs[dstIdx++] : null; + let srcKey: NumericPropKey | null = srcIdx < srcLength ? srcAttrs[srcIdx++] : null; + let dstKey: NumericPropKey | null = dstIdx < dstLength ? dstAttrs[dstIdx++] : null; let patchEventDispatch = false; - const record = (key: string, value: any) => { - if (key.startsWith(':')) { + const record = (key: NumericPropKey, value: any) => { + if (startsWithColon(key)) { vnode_setProp(vnode, key, value); return; } - if (key === 'ref') { + if (key === StaticPropId.REF) { const element = vnode_getNode(vnode) as Element; if (isSignal(value)) { value.value = element; @@ -839,26 +836,28 @@ export const vnode_diff = ( } } + const keyName = getPropName(key); if (isSignal(value)) { const signalData = new SubscriptionData({ $scopedStyleIdPrefix$: scopedStyleIdPrefix, $isConst$: false, }); - value = trackSignalAndAssignHost(value, vnode, key, container, signalData); + value = trackSignalAndAssignHost(value, vnode, keyName, container, signalData); } - vnode_setAttr(journal, vnode, key, serializeAttribute(key, value, scopedStyleIdPrefix)); + vnode_setAttr(journal, vnode, key, serializeAttribute(keyName, value, scopedStyleIdPrefix)); if (value === null) { // if we set `null` than attribute was removed and we need to shorten the dstLength dstLength = dstAttrs.length; } }; - const recordJsxEvent = (key: string, value: any) => { - const eventName = getEventNameFromJsxProp(key); - const scope = getEventNameScopeFromJsxProp(key); + const recordJsxEvent = (key: NumericPropKey, value: any) => { + const keyName = getPropName(key); + const eventName = getEventNameFromJsxProp(keyName); + const scope = getEventNameScopeFromJsxProp(keyName); if (eventName) { - record(':' + scope + ':' + eventName, value); + record(getPropId(':' + scope + ':' + eventName), value); // register an event for qwik loader registerQwikLoaderEvent(eventName); } @@ -867,22 +866,23 @@ export const vnode_diff = ( // add an event attr with empty value for qwikloader element selector. // We don't need value here. For ssr this value is a QRL, // but for CSR value should be just empty - const htmlEvent = convertEventNameFromJsxPropToHtmlAttr(key); + const htmlEvent = convertEventNameFromJsxPropToHtmlAttr(keyName); if (htmlEvent) { - record(htmlEvent, ''); + record(getPropId(htmlEvent), ''); } } }; while (srcKey !== null || dstKey !== null) { - if (dstKey?.startsWith(HANDLER_PREFIX) || dstKey?.startsWith(Q_PREFIX)) { + // if starts with colon it means that it is const event handler + if ((dstKey && startsWithColon(dstKey)) || (dstKey && isQProp(dstKey))) { // These are a special keys which we use to mark the event handlers as immutable or // element key we need to ignore them. dstIdx++; // skip the destination value, we don't care about it. dstKey = dstIdx < dstLength ? dstAttrs[dstIdx++] : null; } else if (srcKey == null) { // Source has more keys, so we need to remove them from destination - if (dstKey && isHtmlAttributeAnEventName(dstKey)) { + if (dstKey && isEventProp(dstKey)) { patchEventDispatch = true; dstIdx++; } else { @@ -892,7 +892,7 @@ export const vnode_diff = ( dstKey = dstIdx < dstLength ? dstAttrs[dstIdx++] : null; } else if (dstKey == null) { // Destination has more keys, so we need to insert them from source. - const isEvent = isJsxPropertyAnEventName(srcKey); + const isEvent = isEventProp(srcKey); if (isEvent) { // Special handling for events patchEventDispatch = true; @@ -916,7 +916,7 @@ export const vnode_diff = ( dstKey = dstIdx < dstLength ? dstAttrs[dstIdx++] : null; } else if (srcKey < dstKey) { // Destination is missing the key, so we need to insert it. - if (isJsxPropertyAnEventName(srcKey)) { + if (isEventProp(srcKey)) { // Special handling for events patchEventDispatch = true; recordJsxEvent(srcKey, srcAttrs[srcIdx]); @@ -933,7 +933,7 @@ export const vnode_diff = ( dstKey = dstIdx < dstLength ? dstAttrs[dstIdx++] : null; } else { // Source is missing the key, so we need to remove it from destination. - if (isHtmlAttributeAnEventName(dstKey)) { + if (isEventProp(dstKey)) { patchEventDispatch = true; dstIdx++; } else { @@ -1039,8 +1039,8 @@ export const vnode_diff = ( (vNewNode = vnode_newVirtual()), vCurrent && getInsertBefore() ); - vnode_setProp(vNewNode as VirtualVNode, ELEMENT_KEY, jsxKey); - isDev && vnode_setProp((vNewNode || vCurrent) as VirtualVNode, DEBUG_TYPE, type); + vnode_setProp(vNewNode as VirtualVNode, StaticPropId.ELEMENT_KEY, jsxKey); + isDev && vnode_setProp((vNewNode || vCurrent) as VirtualVNode, getPropId(DEBUG_TYPE), type); } function expectComponent(component: Function) { @@ -1081,7 +1081,11 @@ export const vnode_diff = ( } if (host) { - const vNodeProps = vnode_getProp(host, ELEMENT_PROPS, container.$getObjectById$); + const vNodeProps = vnode_getProp( + host, + StaticPropId.ELEMENT_PROPS, + container.$getObjectById$ + ); shouldRender = shouldRender || propsDiffer(jsxProps, vNodeProps); if (shouldRender) { /** @@ -1124,7 +1128,7 @@ export const vnode_diff = ( while ( componentHost && (vnode_isVirtualVNode(componentHost) - ? vnode_getProp(componentHost, OnRenderProp, null) === null + ? vnode_getProp(componentHost, StaticPropId.ON_RENDER, null) === null : true) ) { componentHost = vnode_getParent(componentHost); @@ -1158,10 +1162,10 @@ export const vnode_diff = ( vCurrent && getInsertBefore() ); const jsxNode = jsxValue as JSXNodeInternal; - isDev && vnode_setProp(vNewNode, DEBUG_TYPE, VirtualType.Component); - container.setHostProp(vNewNode, OnRenderProp, componentQRL); - container.setHostProp(vNewNode, ELEMENT_PROPS, jsxProps); - container.setHostProp(vNewNode, ELEMENT_KEY, jsxNode.key); + isDev && vnode_setProp(vNewNode, getPropId(DEBUG_TYPE), VirtualType.Component); + container.setHostProp(vNewNode, StaticPropId.ON_RENDER, componentQRL); + container.setHostProp(vNewNode, StaticPropId.ELEMENT_PROPS, jsxProps); + container.setHostProp(vNewNode, StaticPropId.ELEMENT_KEY, jsxNode.key); } function insertNewInlineComponent() { @@ -1172,10 +1176,10 @@ export const vnode_diff = ( vCurrent && getInsertBefore() ); const jsxNode = jsxValue as JSXNodeInternal; - isDev && vnode_setProp(vNewNode, DEBUG_TYPE, VirtualType.InlineComponent); - vnode_setProp(vNewNode, ELEMENT_PROPS, jsxNode.props); + isDev && vnode_setProp(vNewNode, getPropId(DEBUG_TYPE), VirtualType.InlineComponent); + vnode_setProp(vNewNode, StaticPropId.ELEMENT_PROPS, jsxNode.props); if (jsxNode.key) { - vnode_setProp(vNewNode, ELEMENT_KEY, jsxNode.key); + vnode_setProp(vNewNode, StaticPropId.ELEMENT_KEY, jsxNode.key); } } @@ -1209,7 +1213,15 @@ function getKey(vNode: VNode | null): string | null { if (vNode == null) { return null; } - return vnode_getProp(vNode, ELEMENT_KEY, null); + const type = vNode[VNodeProps.flags]; + if ((type & VNodeFlags.ELEMENT_OR_VIRTUAL_MASK) !== 0) { + const props = vnode_getProps(vNode); + // this works, because q:key is always at 0 position or it is not present + if (props[0] === StaticPropId.ELEMENT_KEY) { + return props[1] as string | null; + } + } + return null; } /** @@ -1223,7 +1235,7 @@ function getComponentHash(vNode: VNode | null, getObject: (id: string) => any): if (vNode == null) { return null; } - const qrl = vnode_getProp(vNode, OnRenderProp, getObject); + const qrl = vnode_getProp(vNode, StaticPropId.ON_RENDER, getObject); return qrl ? qrl.$hash$ : null; } @@ -1258,8 +1270,14 @@ function propsDiffer(src: Record, dst: Record): boolea if (!src || !dst) { return true; } - let srcKeys = removePropsKeys(Object.keys(src), ['children', QBackRefs]); - let dstKeys = removePropsKeys(Object.keys(dst), ['children', QBackRefs]); + let srcKeys = removePropsKeys(Object.keys(src).map(Number) as NumericPropKey[], [ + StaticPropId.CHILDREN, + StaticPropId.BACK_REFS, + ]); + let dstKeys = removePropsKeys(Object.keys(dst).map(Number) as NumericPropKey[], [ + StaticPropId.CHILDREN, + StaticPropId.BACK_REFS, + ]); if (srcKeys.length !== dstKeys.length) { return true; } @@ -1275,7 +1293,7 @@ function propsDiffer(src: Record, dst: Record): boolea return false; } -function removePropsKeys(keys: string[], propKeys: string[]): string[] { +function removePropsKeys(keys: NumericPropKey[], propKeys: NumericPropKey[]): NumericPropKey[] { for (let i = propKeys.length - 1; i >= 0; i--) { const propKey = propKeys[i]; const propIdx = keys.indexOf(propKey); @@ -1283,7 +1301,6 @@ function removePropsKeys(keys: string[], propKeys: string[]): string[] { keys.splice(propIdx, 1); } } - return keys; } @@ -1313,7 +1330,10 @@ export function cleanup(container: ClientContainer, vNode: VNode) { markVNodeAsDeleted(vCursor); // Only elements and virtual nodes need to be traversed for children if (type & VNodeFlags.Virtual) { - const seq = container.getHostProp>(vCursor as VirtualVNode, ELEMENT_SEQ); + const seq = container.getHostProp>( + vCursor as VirtualVNode, + StaticPropId.ELEMENT_SEQ + ); if (seq) { for (let i = 0; i < seq.length; i++) { const obj = seq[i]; @@ -1332,12 +1352,12 @@ export function cleanup(container: ClientContainer, vNode: VNode) { const isComponent = type & VNodeFlags.Virtual && - vnode_getProp(vCursor as VirtualVNode, OnRenderProp, null) !== null; + vnode_getProp(vCursor as VirtualVNode, StaticPropId.ON_RENDER, null) !== null; if (isComponent) { // SPECIAL CASE: If we are a component, we need to descend into the projected content and release the content. const attrs = vnode_getProps(vCursor); for (let i = 0; i < attrs.length; i = i + 2) { - const key = attrs[i] as string; + const key = attrs[i] as NumericPropKey; if (isSlotProp(key)) { const value = attrs[i + 1]; if (value) { @@ -1440,11 +1460,6 @@ function markVNodeAsDeleted(vCursor: VNode) { vCursor[VNodeProps.flags] |= VNodeFlags.Deleted; } -/** - * This marks the property as immutable. It is needed for the QRLs so that QwikLoader can get a hold - * of them. This character must be `:` so that the `vnode_getAttr` can ignore them. - */ -const HANDLER_PREFIX = ':'; let count = 0; const enum SiblingsArray { Name = 0, diff --git a/packages/qwik/src/core/client/vnode.ts b/packages/qwik/src/core/client/vnode.ts index cad430999b2..2ce39891289 100644 --- a/packages/qwik/src/core/client/vnode.ts +++ b/packages/qwik/src/core/client/vnode.ts @@ -125,23 +125,14 @@ import { DEBUG_TYPE, QContainerValue, VirtualType, VirtualTypeName } from '../sh import { isText } from '../shared/utils/element'; import { dangerouslySetInnerHTML, - ELEMENT_ID, - ELEMENT_KEY, - ELEMENT_PROPS, - ELEMENT_SEQ, - ELEMENT_SEQ_IDX, - OnRenderProp, Q_PROPS_SEPARATOR, QContainerAttr, QContainerAttrEnd, QContainerIsland, QContainerIslandEnd, - QCtxAttr, QIgnore, QIgnoreEnd, QScopedStyle, - QSlot, - QSlotParent, QStyle, QStylesAllSelector, } from '../shared/utils/markers'; @@ -170,6 +161,13 @@ import { } from './vnode-namespace'; import { mergeMaps } from '../shared/utils/maps'; import { _EFFECT_BACK_REF } from '../signal/flags'; +import { + StaticPropId, + getPropId, + getPropName, + type NumericPropKey, +} from '../shared/utils/numeric-prop-key'; +import { startsWithColon } from '../shared/utils/numeric-prop-key-flags'; ////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -336,7 +334,7 @@ export const vnode_isProjection = (vNode: VNode): vNode is VirtualVNode => { const flag = (vNode as VNode)[VNodeProps.flags]; return ( (flag & VNodeFlags.Virtual) === VNodeFlags.Virtual && - vnode_getProp(vNode as VirtualVNode, QSlot, null) !== null + vnode_getProp(vNode as VirtualVNode, StaticPropId.SLOT, null) !== null ); }; @@ -393,13 +391,13 @@ export const vnode_ensureElementInflated = (vnode: VNode) => { break; } else if (key.startsWith(QContainerAttr)) { if (attr.value === QContainerValue.HTML) { - mapArray_set(props, dangerouslySetInnerHTML, element.innerHTML, 0); + mapArray_set(props, StaticPropId.INNER_HTML, element.innerHTML, 0); } else if (attr.value === QContainerValue.TEXT && 'value' in element) { - mapArray_set(props, 'value', element.value, 0); + mapArray_set(props, StaticPropId.VALUE, element.value, 0); } } else if (!key.startsWith('on:')) { const value = attr.value; - mapArray_set(props, key, value, 0); + mapArray_set(props, getPropId(key), value, 0); } } } @@ -1082,7 +1080,7 @@ export const vnode_remove = ( vToRemove[VNodeProps.nextSibling] = null; if (removeDOM) { const domParent = vnode_getDomParent(vParent); - const isInnerHTMLParent = vnode_getAttr(vParent, dangerouslySetInnerHTML); + const isInnerHTMLParent = vnode_getAttr(vParent, StaticPropId.INNER_HTML); if (isInnerHTMLParent) { // ignore children, as they are inserted via innerHTML return; @@ -1438,7 +1436,7 @@ const materializeFromDOM = (vParent: ElementVNode, firstChild: Node | null, vDat } const id = consumeValue(); container.$setRawState$(parseInt(id), vParent); - isDev && vnode_setAttr(null, vParent, ELEMENT_ID, id); + isDev && vnode_setAttr(null, vParent, StaticPropId.ELEMENT_ID, id); } else if (peek() === VNodeDataChar.BACK_REFS) { if (!container) { container = getDomContainer(vParent[ElementVNodeProps.element]); @@ -1523,15 +1521,15 @@ export const vnode_getPreviousSibling = (vnode: VNode): VNode | null => { return vnode[VNodeProps.previousSibling]; }; -export const vnode_getAttrKeys = (vnode: ElementVNode | VirtualVNode): string[] => { +export const vnode_getAttrKeys = (vnode: ElementVNode | VirtualVNode): NumericPropKey[] => { const type = vnode[VNodeProps.flags]; if ((type & VNodeFlags.ELEMENT_OR_VIRTUAL_MASK) !== 0) { vnode_ensureElementInflated(vnode); - const keys: string[] = []; + const keys: NumericPropKey[] = []; const props = vnode_getProps(vnode); for (let i = 0; i < props.length; i = i + 2) { - const key = props[i] as string; - if (!key.startsWith(Q_PROPS_SEPARATOR)) { + const key = props[i] as NumericPropKey; + if (!startsWithColon(key)) { keys.push(key); } } @@ -1543,7 +1541,7 @@ export const vnode_getAttrKeys = (vnode: ElementVNode | VirtualVNode): string[] export const vnode_setAttr = ( journal: VNodeJournal | null, vnode: VNode, - key: string, + key: NumericPropKey, value: string | null | boolean ): void => { const type = vnode[VNodeProps.flags]; @@ -1556,7 +1554,7 @@ export const vnode_setAttr = ( if (props[idx + 1] != value && (type & VNodeFlags.Element) !== 0) { // Values are different, update DOM const element = vnode[ElementVNodeProps.element] as Element; - journal && journal.push(VNodeJournalOpCode.SetAttribute, element, key, value); + journal && journal.push(VNodeJournalOpCode.SetAttribute, element, getPropName(key), value); } if (value == null) { props.splice(idx, 2); @@ -1568,13 +1566,13 @@ export const vnode_setAttr = ( if ((type & VNodeFlags.Element) !== 0) { // New value, update DOM const element = vnode[ElementVNodeProps.element] as Element; - journal && journal.push(VNodeJournalOpCode.SetAttribute, element, key, value); + journal && journal.push(VNodeJournalOpCode.SetAttribute, element, getPropName(key), value); } } } }; -export const vnode_getAttr = (vnode: VNode, key: string): string | null => { +export const vnode_getAttr = (vnode: VNode, key: NumericPropKey): string | null => { const type = vnode[VNodeProps.flags]; if ((type & VNodeFlags.ELEMENT_OR_VIRTUAL_MASK) !== 0) { vnode_ensureElementInflated(vnode); @@ -1586,26 +1584,30 @@ export const vnode_getAttr = (vnode: VNode, key: string): string | null => { export const vnode_getProp = ( vnode: VNode, - key: string, + key: NumericPropKey, getObject: ((id: string) => any) | null ): T | null => { const type = vnode[VNodeProps.flags]; if ((type & VNodeFlags.ELEMENT_OR_VIRTUAL_MASK) !== 0) { type & VNodeFlags.Element && vnode_ensureElementInflated(vnode); const props = vnode_getProps(vnode); - const idx = mapApp_findIndx(props as any, key, 0); + const idx = mapApp_findIndx(props, key, 0); if (idx >= 0) { let value = props[idx + 1] as any; if (typeof value === 'string' && getObject) { props[idx + 1] = value = getObject(value); } - return value; + return value as T; } } return null; }; -export const vnode_setProp = (vnode: VirtualVNode | ElementVNode, key: string, value: unknown) => { +export const vnode_setProp = ( + vnode: VirtualVNode | ElementVNode, + key: NumericPropKey, + value: unknown +) => { ensureElementOrVirtualVNode(vnode); const props = vnode_getProps(vnode); const idx = mapApp_findIndx(props, key, 0); @@ -1663,6 +1665,7 @@ export function vnode_toString( return 'undefined'; } const strings: string[] = []; + const debugTypeId = getPropId(DEBUG_TYPE); do { if (vnode_isTextVNode(vnode)) { strings.push(qwikDebugToString(vnode_getText(vnode))); @@ -1670,13 +1673,13 @@ export function vnode_toString( const idx = vnode[VNodeProps.flags] >>> VNodeFlagsIndex.shift; const attrs: string[] = ['[' + String(idx) + ']']; vnode_getAttrKeys(vnode).forEach((key) => { - if (key !== DEBUG_TYPE) { + if (key !== debugTypeId) { const value = vnode_getAttr(vnode!, key); - attrs.push(' ' + key + '=' + qwikDebugToString(value)); + attrs.push(' ' + getPropName(key) + '=' + qwikDebugToString(value)); } }); const name = - VirtualTypeName[vnode_getAttr(vnode, DEBUG_TYPE) || VirtualType.Virtual] || + VirtualTypeName[vnode_getAttr(vnode, debugTypeId) || VirtualType.Virtual] || VirtualTypeName[VirtualType.Virtual]; strings.push('<' + name + attrs.join('') + '>'); const child = vnode_getFirstChild(vnode); @@ -1689,7 +1692,7 @@ export function vnode_toString( const keys = vnode_getAttrKeys(vnode); keys.forEach((key) => { const value = vnode_getAttr(vnode!, key); - attrs.push(' ' + key + '=' + qwikDebugToString(value)); + attrs.push(' ' + getPropName(key) + '=' + qwikDebugToString(value)); }); const node = vnode_getNode(vnode) as HTMLElement; if (node) { @@ -1701,7 +1704,7 @@ export function vnode_toString( const domAttrs = node.attributes; for (let i = 0; i < domAttrs.length; i++) { const attr = domAttrs[i]; - if (keys.indexOf(attr.name) === -1) { + if (keys.indexOf(getPropId(attr.name)) === -1) { attrs.push(' ' + attr.name + (attr.value ? '=' + qwikDebugToString(attr.value) : '')); } } @@ -1780,33 +1783,33 @@ function materializeFromVNodeData( } // collect the elements; } else if (peek() === VNodeDataChar.SCOPED_STYLE) { - vnode_setAttr(null, vParent, QScopedStyle, consumeValue()); + vnode_setAttr(null, vParent, StaticPropId.SCOPED_STYLE, consumeValue()); } else if (peek() === VNodeDataChar.RENDER_FN) { - vnode_setAttr(null, vParent, OnRenderProp, consumeValue()); + vnode_setAttr(null, vParent, StaticPropId.ON_RENDER, consumeValue()); } else if (peek() === VNodeDataChar.ID) { if (!container) { container = getDomContainer(element); } const id = consumeValue(); container.$setRawState$(parseInt(id), vParent); - isDev && vnode_setAttr(null, vParent, ELEMENT_ID, id); + isDev && vnode_setAttr(null, vParent, StaticPropId.ELEMENT_ID, id); } else if (peek() === VNodeDataChar.PROPS) { - vnode_setAttr(null, vParent, ELEMENT_PROPS, consumeValue()); + vnode_setAttr(null, vParent, StaticPropId.ELEMENT_PROPS, consumeValue()); } else if (peek() === VNodeDataChar.KEY) { - vnode_setAttr(null, vParent, ELEMENT_KEY, consumeValue()); + vnode_setAttr(null, vParent, StaticPropId.ELEMENT_KEY, consumeValue()); } else if (peek() === VNodeDataChar.SEQ) { - vnode_setAttr(null, vParent, ELEMENT_SEQ, consumeValue()); + vnode_setAttr(null, vParent, StaticPropId.ELEMENT_SEQ, consumeValue()); } else if (peek() === VNodeDataChar.SEQ_IDX) { - vnode_setAttr(null, vParent, ELEMENT_SEQ_IDX, consumeValue()); + vnode_setAttr(null, vParent, StaticPropId.ELEMENT_SEQ_IDX, consumeValue()); } else if (peek() === VNodeDataChar.BACK_REFS) { if (!container) { container = getDomContainer(element); } setEffectBackRefFromVNodeData(vParent, consumeValue(), container); } else if (peek() === VNodeDataChar.SLOT_PARENT) { - vnode_setProp(vParent, QSlotParent, consumeValue()); + vnode_setProp(vParent, StaticPropId.SLOT_PARENT, consumeValue()); } else if (peek() === VNodeDataChar.CONTEXT) { - vnode_setAttr(null, vParent, QCtxAttr, consumeValue()); + vnode_setAttr(null, vParent, StaticPropId.CTX, consumeValue()); } else if (peek() === VNodeDataChar.OPEN) { consume(); addVNode(vnode_newVirtual()); @@ -1817,7 +1820,7 @@ function materializeFromVNodeData( } else if (peek() === VNodeDataChar.SEPARATOR) { const key = consumeValue(); const value = consumeValue(); - vnode_setAttr(null, vParent as VirtualVNode, key, value); + vnode_setAttr(null, vParent as VirtualVNode, getPropId(key), value); } else if (peek() === VNodeDataChar.CLOSE) { consume(); vParent[ElementVNodeProps.lastChild] = vLast; @@ -1827,7 +1830,7 @@ function materializeFromVNodeData( vFirst = stack.pop(); vParent = stack.pop(); } else if (peek() === VNodeDataChar.SLOT) { - vnode_setAttr(null, vParent, QSlot, consumeValue()); + vnode_setAttr(null, vParent, StaticPropId.SLOT, consumeValue()); } else { const textNode = child && fastNodeType(child) === /* Node.TEXT_NODE */ 3 ? (child as Text) : null; @@ -1899,9 +1902,11 @@ export const vnode_getProjectionParentComponent = ( while (projectionDepth--) { while ( vHost && - (vnode_isVirtualVNode(vHost) ? vnode_getProp(vHost, OnRenderProp, null) === null : true) + (vnode_isVirtualVNode(vHost) + ? vnode_getProp(vHost, StaticPropId.ON_RENDER, null) === null + : true) ) { - const qSlotParent = vnode_getProp(vHost, QSlotParent, (id) => + const qSlotParent = vnode_getProp(vHost, StaticPropId.SLOT_PARENT, (id) => vnode_locate(rootVNode, id) ); const vProjectionParent = vnode_isVirtualVNode(vHost) && qSlotParent; diff --git a/packages/qwik/src/core/client/vnode.unit.tsx b/packages/qwik/src/core/client/vnode.unit.tsx index 70dc72b2ca3..a2e3a582697 100644 --- a/packages/qwik/src/core/client/vnode.unit.tsx +++ b/packages/qwik/src/core/client/vnode.unit.tsx @@ -28,6 +28,7 @@ import { vnode_setText, type VNodeJournal, } from './vnode'; +import { StaticPropId, getPropId } from '../../server/qwik-copy'; describe('vnode', () => { let parent: ContainerElement; @@ -285,9 +286,9 @@ describe('vnode', () => { const fragment1 = vnode_newVirtual(); const fragment2 = vnode_newVirtual(); const fragment3 = vnode_newVirtual(); - vnode_setAttr(null, fragment1, 'q:id', '1'); - vnode_setAttr(null, fragment2, 'q:id', '2'); - vnode_setAttr(null, fragment3, 'q:id', '3'); + vnode_setAttr(null, fragment1, StaticPropId.ELEMENT_ID, '1'); + vnode_setAttr(null, fragment2, StaticPropId.ELEMENT_ID, '2'); + vnode_setAttr(null, fragment3, StaticPropId.ELEMENT_ID, '3'); const textA = vnode_newText(document.createTextNode('1A'), '1A'); const textB = vnode_newText(document.createTextNode('2B'), '2B'); const textC = vnode_newText(document.createTextNode('3C'), '3C'); @@ -482,8 +483,8 @@ describe('vnode', () => { const v2 = vnode_getNextSibling(v1) as VirtualVNode; expect(v1).toMatchVDOM(<>A); expect(v2).toMatchVDOM(<>B); - expect(vnode_getProp(v1, '', getVNode)).toBe(v2); - expect(vnode_getProp(v2, ':', getVNode)).toBe(v1); + expect(vnode_getProp(v1, getPropId(''), getVNode)).toBe(v2); + expect(vnode_getProp(v2, getPropId(':'), getVNode)).toBe(v1); }); }); describe('attributes', () => { @@ -500,7 +501,7 @@ describe('vnode', () => { it('should update innerHTML', () => { parent.innerHTML = '
content
'; const div = vnode_getFirstChild(vParent) as ElementVNode; - vnode_setAttr(journal, div, 'dangerouslySetInnerHTML', 'new content'); + vnode_setAttr(journal, div, StaticPropId.INNER_HTML, 'new content'); vnode_applyJournal(journal); expect(parent.innerHTML).toBe('
new content
'); expect(vParent).toMatchVDOM( @@ -509,7 +510,7 @@ describe('vnode', () => {
); - expect(vnode_getAttr(div, 'dangerouslySetInnerHTML')).toBe('new content'); + expect(vnode_getAttr(div, StaticPropId.INNER_HTML)).toBe('new content'); }); it('should have empty child for dangerouslySetInnerHTML', () => { parent.innerHTML = '
content
'; @@ -536,7 +537,7 @@ describe('vnode', () => { it('should update textContent', () => { parent.innerHTML = ''; const textarea = vnode_getFirstChild(vParent) as ElementVNode; - vnode_setAttr(journal, textarea, 'value', 'new content'); + vnode_setAttr(journal, textarea, StaticPropId.VALUE, 'new content'); vnode_applyJournal(journal); expect(parent.innerHTML).toBe(''); expect(vParent).toMatchVDOM( @@ -545,7 +546,7 @@ describe('vnode', () => { '; @@ -650,10 +651,10 @@ describe('vnode', () => { it('should set attribute', () => { parent.innerHTML = '
'; const div = vnode_getFirstChild(vParent) as ElementVNode; - vnode_setAttr(journal, div, 'key', '123'); + vnode_setAttr(journal, div, getPropId('key'), '123'); vnode_applyJournal(journal); expect(parent.innerHTML).toBe('
'); - vnode_setAttr(journal, div, 'foo', null); + vnode_setAttr(journal, div, getPropId('foo'), null); vnode_applyJournal(journal); expect(parent.innerHTML).toBe('
'); }); diff --git a/packages/qwik/src/core/debug.ts b/packages/qwik/src/core/debug.ts index 4abc3b46532..657c8318a3e 100644 --- a/packages/qwik/src/core/debug.ts +++ b/packages/qwik/src/core/debug.ts @@ -1,10 +1,11 @@ import { isQrl } from '../server/prefetch-strategy'; -import { isJSXNode } from './shared/jsx/jsx-runtime'; +import { isJSXNode, isPropsProxy } from './shared/jsx/jsx-runtime'; import { isTask } from './use/use-task'; 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'; +import { getPropId } from '../server/qwik-copy'; const stringifyPath: any[] = []; export function qwikDebugToString(value: any): any { @@ -31,7 +32,7 @@ export function qwikDebugToString(value: any): any { stringifyPath.push(value); if (Array.isArray(value)) { if (vnode_isVNode(value)) { - return '(' + vnode_getProp(value, DEBUG_TYPE, null) + ')'; + return '(' + vnode_getProp(value, getPropId(DEBUG_TYPE), null) + ')'; } else { return value.map(qwikDebugToString); } @@ -47,6 +48,8 @@ export function qwikDebugToString(value: any): any { return 'Store'; } else if (isJSXNode(value)) { return jsxToString(value); + } else if (isPropsProxy(value)) { + return '{*}'; } } finally { stringifyPath.pop(); diff --git a/packages/qwik/src/core/shared/component-execution.ts b/packages/qwik/src/core/shared/component-execution.ts index 34551f5a379..4098d94016d 100644 --- a/packages/qwik/src/core/shared/component-execution.ts +++ b/packages/qwik/src/core/shared/component-execution.ts @@ -15,17 +15,11 @@ import { isQrl } from './qrl/qrl-utils'; import type { Container, HostElement } from './types'; import { EMPTY_OBJ } from './utils/flyweight'; import { logWarn } from './utils/log'; -import { - ELEMENT_PROPS, - ELEMENT_SEQ_IDX, - OnRenderProp, - RenderEvent, - USE_ON_LOCAL, - USE_ON_LOCAL_SEQ_IDX, -} from './utils/markers'; +import { RenderEvent } from './utils/markers'; import { MAX_RETRY_ON_PROMISE_COUNT, isPromise, maybeThen, safeCall } from './utils/promises'; import type { ValueOrPromise } from './utils/types'; import { getSubscriber } from '../signal/subscriber'; +import { StaticPropId } from './utils/numeric-prop-key'; /** * Use `executeComponent` to execute a component. @@ -69,11 +63,11 @@ export const executeComponent = ( container.ensureProjectionResolved(renderHost); let isInlineComponent = false; if (componentQRL === null) { - componentQRL = container.getHostProp(renderHost, OnRenderProp)!; + componentQRL = container.getHostProp(renderHost, StaticPropId.ON_RENDER)!; assertDefined(componentQRL, 'No Component found at this location'); } if (isQrl(componentQRL)) { - props = props || container.getHostProp(renderHost, ELEMENT_PROPS) || EMPTY_OBJ; + props = props || container.getHostProp(renderHost, StaticPropId.ELEMENT_PROPS) || EMPTY_OBJ; if (props.children) { delete props.children; } @@ -95,9 +89,9 @@ export const executeComponent = ( safeCall( () => { if (!isInlineComponent) { - container.setHostProp(renderHost, ELEMENT_SEQ_IDX, null); - container.setHostProp(renderHost, USE_ON_LOCAL_SEQ_IDX, null); - container.setHostProp(renderHost, ELEMENT_PROPS, props); + container.setHostProp(renderHost, StaticPropId.ELEMENT_SEQ_IDX, null); + container.setHostProp(renderHost, StaticPropId.USE_ON_LOCAL_SEQ_IDX, null); + container.setHostProp(renderHost, StaticPropId.ELEMENT_PROPS, props); } if (vnode_isVNode(renderHost)) { @@ -107,7 +101,7 @@ export const executeComponent = ( return componentFn(props); }, (jsx) => { - const useOnEvents = container.getHostProp(renderHost, USE_ON_LOCAL); + const useOnEvents = container.getHostProp(renderHost, StaticPropId.USE_ON_LOCAL); if (useOnEvents) { return addUseOnEvents(jsx, useOnEvents); } diff --git a/packages/qwik/src/core/shared/jsx/jsx-runtime.ts b/packages/qwik/src/core/shared/jsx/jsx-runtime.ts index be2ae811455..fa2c336e976 100644 --- a/packages/qwik/src/core/shared/jsx/jsx-runtime.ts +++ b/packages/qwik/src/core/shared/jsx/jsx-runtime.ts @@ -12,6 +12,7 @@ import { WrappedSignal, WrappedSignalFlags } from '../../signal/signal'; import type { DevJSX, FunctionComponent, JSXNode, JSXNodeInternal } from './types/jsx-node'; import type { QwikJSX } from './types/jsx-qwik'; import type { JSXChildren } from './types/jsx-qwik-attributes'; +import { getPropId, getPropName, type NumericPropKey } from '../utils/numeric-prop-key'; export type Props = Record; export type PropsProxy = { [_VAR_PROPS]: Props; [_CONST_PROPS]: Props | null }; @@ -203,6 +204,7 @@ export const isPropsProxy = (obj: any): obj is PropsProxy => { export class JSXNodeImpl implements JSXNodeInternal { dev?: DevJSX; + constructor( public type: T, public varProps: Props, @@ -211,6 +213,17 @@ export class JSXNodeImpl implements JSXNodeInternal { public flags: number, public key: string | null = null ) { + const numericVarProps: Props = {}; + convertToNumericProps(varProps, numericVarProps); + this.varProps = numericVarProps; + + let numericConstProps: Props | null = null; + if (constProps) { + numericConstProps = {}; + convertToNumericProps(constProps, numericConstProps); + } + this.constProps = numericConstProps; + if (qDev) { if (typeof varProps !== 'object') { throw new Error(`JSXNodeImpl: varProps must be objects: ` + JSON.stringify(varProps)); @@ -231,6 +244,12 @@ export class JSXNodeImpl implements JSXNodeInternal { } } +export function convertToNumericProps(originalProps: Props, propsObject: Props): void { + for (const key in originalProps) { + propsObject[getPropId(key)] = originalProps[key]; + } +} + /** @private */ export const Virtual: FunctionComponent<{ children?: JSXChildren; @@ -337,13 +356,14 @@ class PropsProxyHandler implements ProxyHandler { if (prop === _VAR_PROPS) { return this.$varProps$; } - if (this.$children$ != null && prop === 'children') { + if (prop === 'children') { return this.$children$; } + const numericProp = getPropId(prop); const value = - this.$constProps$ && prop in this.$constProps$ - ? this.$constProps$[prop as string] - : this.$varProps$[prop as string]; + this.$constProps$ && numericProp in this.$constProps$ + ? this.$constProps$[numericProp] + : this.$varProps$[numericProp]; // a proxied value that the optimizer made return value instanceof WrappedSignal && value.$flags$ & WrappedSignalFlags.UNWRAP ? value.value @@ -358,10 +378,11 @@ class PropsProxyHandler implements ProxyHandler { this.$varProps$ = value; return true; } - if (this.$constProps$ && prop in this.$constProps$) { - this.$constProps$[prop as string] = value; + const numericProp = getPropId(prop); + if (this.$constProps$ && numericProp in this.$constProps$) { + this.$constProps$[numericProp] = value; } else { - this.$varProps$[prop as string] = value; + this.$varProps$[numericProp] = value; } return true; } @@ -369,9 +390,10 @@ class PropsProxyHandler implements ProxyHandler { if (typeof prop !== 'string') { return false; } - let didDelete = delete this.$varProps$[prop]; + const numericProp = getPropId(prop); + let didDelete = delete this.$varProps$[numericProp]; if (this.$constProps$) { - didDelete = delete this.$constProps$[prop as string] || didDelete; + didDelete = delete this.$constProps$[numericProp] || didDelete; } if (this.$children$ != null && prop === 'children') { this.$children$ = null; @@ -379,21 +401,23 @@ class PropsProxyHandler implements ProxyHandler { return didDelete; } has(_: any, prop: string | symbol) { + const numericProp = getPropId(prop); const hasProp = (prop === 'children' && this.$children$ != null) || prop === _CONST_PROPS || prop === _VAR_PROPS || - prop in this.$varProps$ || - (this.$constProps$ ? prop in this.$constProps$ : false); + numericProp in this.$varProps$ || + (this.$constProps$ ? numericProp in this.$constProps$ : false); return hasProp; } - getOwnPropertyDescriptor(_: any, p: string | symbol): PropertyDescriptor | undefined { + getOwnPropertyDescriptor(_: any, prop: string | symbol): PropertyDescriptor | undefined { + const numericProp = getPropId(prop); const value = - p === 'children' && this.$children$ != null + prop === 'children' && this.$children$ != null ? this.$children$ - : this.$constProps$ && p in this.$constProps$ - ? this.$constProps$[p as string] - : this.$varProps$[p as string]; + : this.$constProps$ && numericProp in this.$constProps$ + ? this.$constProps$[numericProp] + : this.$varProps$[numericProp]; return { configurable: true, enumerable: true, @@ -401,14 +425,17 @@ class PropsProxyHandler implements ProxyHandler { }; } ownKeys() { - const out = Object.keys(this.$varProps$); + const out = []; + for (const key in this.$varProps$) { + out.push(getPropName(key as unknown as NumericPropKey)); + } if (this.$children$ != null && out.indexOf('children') === -1) { out.push('children'); } if (this.$constProps$) { for (const key in this.$constProps$) { if (out.indexOf(key) === -1) { - out.push(key); + out.push(getPropName(key as unknown as NumericPropKey)); } } } @@ -420,7 +447,10 @@ class PropsProxyHandler implements ProxyHandler { * Instead of using PropsProxyHandler getter (which could create a component-level subscription). * Use this function to get the props directly from a const or var props. */ -export const directGetPropsProxyProp = (jsx: JSXNodeInternal, prop: string): T => { +export const directGetPropsProxyProp = ( + jsx: JSXNodeInternal, + prop: NumericPropKey +): T => { return ( jsx.constProps && prop in jsx.constProps ? jsx.constProps[prop] : jsx.varProps[prop] ) as T; diff --git a/packages/qwik/src/core/shared/jsx/types/jsx-node.ts b/packages/qwik/src/core/shared/jsx/types/jsx-node.ts index e566d3627aa..912c8abcfca 100644 --- a/packages/qwik/src/core/shared/jsx/types/jsx-node.ts +++ b/packages/qwik/src/core/shared/jsx/types/jsx-node.ts @@ -1,3 +1,4 @@ +import type { NumericPropKey } from '../../utils/numeric-prop-key'; import type { JSXChildren } from './jsx-qwik-attributes'; /** @@ -46,7 +47,7 @@ export interface JSXNode extends JSXNode { - varProps: Record; - constProps: Record | null; + varProps: Record; + constProps: Record | null; flags: number; } diff --git a/packages/qwik/src/core/shared/scheduler-document-position.ts b/packages/qwik/src/core/shared/scheduler-document-position.ts index 7f73c4e2c7b..0d17365ef04 100644 --- a/packages/qwik/src/core/shared/scheduler-document-position.ts +++ b/packages/qwik/src/core/shared/scheduler-document-position.ts @@ -6,7 +6,7 @@ import { vnode_locate, } from '../client/vnode'; import type { ISsrNode } from '../ssr/ssr-types'; -import { QSlotParent } from './utils/markers'; +import { StaticPropId } from './utils/numeric-prop-key'; /// These global variables are used to avoid creating new arrays for each call to `vnode_documentPosition`. const aVNodePath: VNode[] = []; @@ -33,12 +33,14 @@ export const vnode_documentPosition = ( while (a) { const vNode = (aVNodePath[++aDepth] = a); a = (vNode[VNodeProps.parent] || - (rootVNode && vnode_getProp(a, QSlotParent, (id) => vnode_locate(rootVNode, id))))!; + (rootVNode && + vnode_getProp(a, StaticPropId.SLOT_PARENT, (id) => vnode_locate(rootVNode, id))))!; } while (b) { const vNode = (bVNodePath[++bDepth] = b); b = (vNode[VNodeProps.parent] || - (rootVNode && vnode_getProp(b, QSlotParent, (id) => vnode_locate(rootVNode, id))))!; + (rootVNode && + vnode_getProp(b, StaticPropId.SLOT_PARENT, (id) => vnode_locate(rootVNode, id))))!; } while (aDepth >= 0 && bDepth >= 0) { @@ -64,7 +66,10 @@ export const vnode_documentPosition = ( return -1; } } while (cursor); - if (rootVNode && vnode_getProp(b, QSlotParent, (id) => vnode_locate(rootVNode, id))) { + if ( + rootVNode && + vnode_getProp(b, StaticPropId.SLOT_PARENT, (id) => vnode_locate(rootVNode, id)) + ) { // The "b" node is a projection, so we need to set it after "a" node, // because the "a" node could be a context provider. return -1; diff --git a/packages/qwik/src/core/shared/scheduler.ts b/packages/qwik/src/core/shared/scheduler.ts index fdb0a54c6ee..1910c33d05e 100644 --- a/packages/qwik/src/core/shared/scheduler.ts +++ b/packages/qwik/src/core/shared/scheduler.ts @@ -114,11 +114,11 @@ import { type QRLInternal } from './qrl/qrl-class'; import { ssrNodeDocumentPosition, vnode_documentPosition } from './scheduler-document-position'; import type { Container, HostElement } from './types'; import { logWarn } from './utils/log'; -import { QScopedStyle } from './utils/markers'; import { isPromise, retryOnPromise, safeCall } from './utils/promises'; import { addComponentStylePrefix } from './utils/scoped-styles'; import { serializeAttribute } from './utils/styles'; import type { ValueOrPromise } from './utils/types'; +import { StaticPropId, getPropId } from './utils/numeric-prop-key'; // Turn this on to get debug output of what the scheduler is doing. const DEBUG: boolean = false; @@ -360,7 +360,10 @@ export const createScheduler = ( if (isServer) { return jsx; } else { - const styleScopedId = container.getHostProp(host, QScopedStyle); + const styleScopedId = container.getHostProp( + host, + StaticPropId.SCOPED_STYLE + ); return retryOnPromise(() => vnode_diff( container as ClientContainer, @@ -458,7 +461,7 @@ export const createScheduler = ( const element = virtualNode[ElementVNodeProps.element] as Element; journal.push(VNodeJournalOpCode.SetAttribute, element, property, serializedValue); } else { - vnode_setAttr(journal, virtualNode, property, serializedValue); + vnode_setAttr(journal, virtualNode, getPropId(property), serializedValue); } } break; diff --git a/packages/qwik/src/core/shared/scheduler.unit.tsx b/packages/qwik/src/core/shared/scheduler.unit.tsx index cd629b9ed63..ab591b7af7f 100644 --- a/packages/qwik/src/core/shared/scheduler.unit.tsx +++ b/packages/qwik/src/core/shared/scheduler.unit.tsx @@ -19,6 +19,7 @@ import { ChoreType } from './util-chore-type'; import type { HostElement } from './types'; import { QContainerAttr } from './utils/markers'; import { _EFFECT_BACK_REF } from '../signal/flags'; +import { StaticPropId } from './utils/numeric-prop-key'; declare global { let testLog: string[]; @@ -47,13 +48,13 @@ describe('scheduler', () => { vBody = vnode_newUnMaterializedElement(document.body); vA = vnode_locate(vBody, document.querySelector('a') as Element) as ElementVNode; vAHost = vnode_newVirtual(); - vnode_setProp(vAHost, 'q:id', 'A'); + vnode_setProp(vAHost, StaticPropId.ELEMENT_ID, 'A'); vnode_insertBefore([], vA, vAHost, null); vB = vnode_locate(vBody, document.querySelector('b') as Element) as ElementVNode; vBHost1 = vnode_newVirtual(); - vnode_setProp(vBHost1, 'q:id', 'b1'); + vnode_setProp(vBHost1, StaticPropId.ELEMENT_ID, 'b1'); vBHost2 = vnode_newVirtual(); - vnode_setProp(vBHost2, 'q:id', 'b2'); + vnode_setProp(vBHost2, StaticPropId.ELEMENT_ID, 'b2'); vnode_insertBefore([], vB, vBHost1, null); vnode_insertBefore([], vB, vBHost2, null); }); diff --git a/packages/qwik/src/core/shared/shared-container.ts b/packages/qwik/src/core/shared/shared-container.ts index ef5c507b492..121e93c0bc8 100644 --- a/packages/qwik/src/core/shared/shared-container.ts +++ b/packages/qwik/src/core/shared/shared-container.ts @@ -8,6 +8,7 @@ import type { Scheduler } from './scheduler'; import { createScheduler } from './scheduler'; import { createSerializationContext, type SerializationContext } from './shared-serialization'; import type { Container, HostElement, ObjToProxyMap } from './types'; +import type { NumericPropKey } from './utils/numeric-prop-key'; /** @internal */ export abstract class _SharedContainer implements Container { @@ -76,8 +77,8 @@ export abstract class _SharedContainer implements Container { abstract getParentHost(host: HostElement): HostElement | null; abstract setContext(host: HostElement, context: ContextId, value: T): void; abstract resolveContext(host: HostElement, contextId: ContextId): T | undefined; - abstract setHostProp(host: HostElement, name: string, value: T): void; - abstract getHostProp(host: HostElement, name: string): T | null; + abstract setHostProp(host: HostElement, name: NumericPropKey, value: T): void; + abstract getHostProp(host: HostElement, name: NumericPropKey): T | null; abstract $appendStyle$( content: string, styleId: string, diff --git a/packages/qwik/src/core/shared/shared-serialization.ts b/packages/qwik/src/core/shared/shared-serialization.ts index 48444dc9b63..8849a7d4618 100644 --- a/packages/qwik/src/core/shared/shared-serialization.ts +++ b/packages/qwik/src/core/shared/shared-serialization.ts @@ -51,10 +51,10 @@ 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 { isPromise } from './utils/promises'; import { fastSkipSerialize } from './utils/serialize-utils'; import { type ValueOrPromise } from './utils/types'; +import { StaticPropId, type NumericPropKey } from './utils/numeric-prop-key'; const deserializedProxyMap = new WeakMap(); @@ -654,8 +654,8 @@ export interface SerializationContext { $renderSymbols$: Set; $storeProxyMap$: ObjToProxyMap; - $getProp$: (obj: any, prop: string) => any; - $setProp$: (obj: any, prop: string, value: any) => void; + $getProp$: (obj: any, prop: NumericPropKey) => any; + $setProp$: (obj: any, prop: NumericPropKey, value: any) => void; $prepVNodeData$?: (vNodeData: VNodeData) => void; } @@ -674,8 +674,8 @@ export const createSerializationContext = ( new (...rest: any[]): { $ssrNode$: ISsrNode }; } | null, symbolToChunkResolver: SymbolToChunkResolver, - getProp: (obj: any, prop: string) => any, - setProp: (obj: any, prop: string, value: any) => void, + getProp: (obj: any, prop: NumericPropKey) => any, + setProp: (obj: any, prop: NumericPropKey, value: any) => void, storeProxyMap: ObjToProxyMap, writer?: StreamWriter, // temporary until we serdes the vnode data here @@ -1223,7 +1223,7 @@ function serialize(serializationContext: SerializationContext): void { } else if ($isSsrNode$(value)) { if (isRootObject) { // Tell the SsrNode which root id it is - $setProp$(value, ELEMENT_ID, String(idx)); + $setProp$(value, StaticPropId.ELEMENT_ID, String(idx)); // we need to output before the vnode overwrites its values output(TypeIds.VNode, value.id); const vNodeData = value.vnodeData; diff --git a/packages/qwik/src/core/shared/types.ts b/packages/qwik/src/core/shared/types.ts index ddde8a0e082..1ea26f95ef3 100644 --- a/packages/qwik/src/core/shared/types.ts +++ b/packages/qwik/src/core/shared/types.ts @@ -3,6 +3,7 @@ import type { VNode } from '../client/types'; import type { ISsrNode, StreamWriter, SymbolToChunkResolver } from '../ssr/ssr-types'; import type { Scheduler } from './scheduler'; import type { SerializationContext } from './shared-serialization'; +import type { NumericPropKey } from './utils/numeric-prop-key'; export interface DeserializeContainer { $getObjectById$: (id: number | string) => unknown; @@ -27,8 +28,8 @@ export interface Container { getParentHost(host: HostElement): HostElement | null; setContext(host: HostElement, context: ContextId, value: T): void; resolveContext(host: HostElement, contextId: ContextId): T | undefined; - setHostProp(host: HostElement, name: string, value: T): void; - getHostProp(host: HostElement, name: string): T | null; + setHostProp(host: HostElement, name: NumericPropKey, value: T): void; + getHostProp(host: HostElement, name: NumericPropKey): T | null; $appendStyle$(content: string, styleId: string, host: HostElement, scoped: boolean): void; /** * When component is about to be executed, it may add/remove children. This can cause problems diff --git a/packages/qwik/src/core/shared/utils/event-names.ts b/packages/qwik/src/core/shared/utils/event-names.ts index 9697c8e3375..e081be44c5e 100644 --- a/packages/qwik/src/core/shared/utils/event-names.ts +++ b/packages/qwik/src/core/shared/utils/event-names.ts @@ -9,6 +9,9 @@ * - A `-` (not at the beginning) makes next character uppercase: `dbl-click` => `dblClick` */ +import type { KnownEventNames } from '../jsx/types/jsx-qwik-events'; +import { DOMContentLoadedEvent } from './markers'; + export const isJsxPropertyAnEventName = (name: string): boolean => { return ( (name.startsWith('on') || name.startsWith('window:on') || name.startsWith('document:on')) && @@ -31,36 +34,41 @@ export const getEventNameFromJsxProp = (name: string): string | null => { idx = 11; } if (idx != -1) { - const isCaseSensitive = isDashAt(name, idx) && !isDashAt(name, idx + 1); - if (isCaseSensitive) { - idx++; - } - let lastIdx = idx; - let eventName = ''; - while (true as boolean) { - idx = name.indexOf('-', lastIdx); - const chunk = name.substring( - lastIdx, - idx === -1 ? name.length - 1 /* don't include `$` */ : idx - ); - eventName += isCaseSensitive ? chunk : chunk.toLowerCase(); - if (idx == -1) { - return eventName; - } - if (isDashAt(name, idx + 1)) { - eventName += '-'; - idx++; - } else { - eventName += name.charAt(idx + 1).toUpperCase(); - idx++; - } - lastIdx = idx + 1; - } + return parseEventNameFromIndex(name, idx); } } return null; }; +export const parseEventNameFromIndex = (name: string, idx: number): string => { + const isCaseSensitive = isDashAt(name, idx) && !isDashAt(name, idx + 1); + if (isCaseSensitive) { + idx++; + } + let lastIdx = idx; + let eventName = ''; + while (true as boolean) { + idx = name.indexOf('-', lastIdx); + const chunk = name.substring( + lastIdx, + idx === -1 ? name.length - 1 /* don't include `$` */ : idx + ); + eventName += isCaseSensitive ? chunk : chunk.toLowerCase(); + if (idx == -1) { + return eventName; + } + if (isDashAt(name, idx + 1)) { + eventName += '-'; + idx++; + } else { + eventName += name.charAt(idx + 1).toUpperCase(); + idx++; + } + lastIdx = idx + 1; + } + return eventName; +}; + export const getEventNameScopeFromJsxProp = (name: string): string => { const index = name.indexOf(':'); return index !== -1 ? name.substring(0, index) : ''; @@ -117,6 +125,24 @@ export const convertEventNameFromHtmlAttrToJsxProp = (name: string): string | nu return null; }; +const DOMContentLoadedEventLowercase = DOMContentLoadedEvent.toLowerCase(); +export const createEventName = ( + event: KnownEventNames | KnownEventNames[], + eventType?: 'window' | 'document' | undefined +) => { + const prefix = eventType !== undefined ? eventType + ':' : ''; + const map = (name: string) => { + // DOMContentLoaded is a special case, where the event name is case sensitive + // https://html.spec.whatwg.org/multipage/indices.html#event-domcontentloaded + const isDOMContentLoadedEvent = name.toLowerCase() === DOMContentLoadedEventLowercase; + const eventName = isDOMContentLoadedEvent + ? DOMContentLoadedEvent + : name.charAt(0).toUpperCase() + name.substring(1).toLowerCase(); + return prefix + 'on' + eventName + '$'; + }; + return Array.isArray(event) ? event.map(map) : map(event); +}; + export const convertEventNameFromJsxPropToHtmlAttr = (name: string): string | null => { if (name.endsWith('$')) { let prefix: string | null = null; diff --git a/packages/qwik/src/core/shared/utils/event-names.unit.tsx b/packages/qwik/src/core/shared/utils/event-names.unit.tsx index 1aff2172dea..963ac67fb97 100644 --- a/packages/qwik/src/core/shared/utils/event-names.unit.tsx +++ b/packages/qwik/src/core/shared/utils/event-names.unit.tsx @@ -2,6 +2,7 @@ import { describe, it, expect } from 'vitest'; import { convertEventNameFromHtmlAttrToJsxProp, convertEventNameFromJsxPropToHtmlAttr, + createEventName, getEventNameFromHtmlAttr, getEventNameFromJsxProp, } from './event-names'; @@ -25,9 +26,15 @@ describe('event-names', () => { it('1', () => expect(convertEventNameFromHtmlAttrToJsxProp('on:-dblclick')).toEqual('on-Dblclick$')); }); - // it.only('test', () => { - // expectMatch('on--click$', 'on:--click', '-click'); - // }); + + describe('createEventName', () => { + it('1', () => expect(createEventName('click')).toEqual('onClick$')); + it('1', () => expect(createEventName('click', 'window')).toEqual('window:onClick$')); + it('2', () => expect(createEventName('click', 'document')).toEqual('document:onClick$')); + it('3', () => expect(createEventName('dblclick', 'document')).toEqual('document:onDblclick$')); + it('4', () => expect(createEventName('dblClick', 'document')).toEqual('document:onDblclick$')); + it('4', () => expect(createEventName('DOMContentLoaded')).toEqual('onDOMContentLoaded$')); + }); it('should convert prefix', () => { expectMatch('onClick$', 'on:click', 'click'); diff --git a/packages/qwik/src/core/shared/utils/markers.ts b/packages/qwik/src/core/shared/utils/markers.ts index 98daa3f0a81..24abc0c221a 100644 --- a/packages/qwik/src/core/shared/utils/markers.ts +++ b/packages/qwik/src/core/shared/utils/markers.ts @@ -78,6 +78,12 @@ export const ELEMENT_SEQ = 'q:seq'; export const ELEMENT_SEQ_IDX = 'q:seqIdx'; export const Q_PREFIX = 'q:'; +/** + * This marks the property as immutable. It is needed for the QRLs so that QwikLoader can get a hold + * of them. This character must be `:` so that the `vnode_getAttr` can ignore them. + */ +export const HANDLER_PREFIX = ':'; + /** Non serializable markers - always begins with `:` character */ export const NON_SERIALIZABLE_MARKER_PREFIX = ':'; export const USE_ON_LOCAL = NON_SERIALIZABLE_MARKER_PREFIX + 'on'; @@ -92,4 +98,6 @@ export const STREAM_BLOCK_END_COMMENT = 'qkssr-po'; export const Q_PROPS_SEPARATOR = ':'; export const dangerouslySetInnerHTML = 'dangerouslySetInnerHTML'; +export const refAttr = 'ref'; export const qwikInspectorAttr = 'data-qwik-inspector'; +export const DOMContentLoadedEvent = 'DOMContentLoaded'; diff --git a/packages/qwik/src/core/shared/utils/numeric-prop-key-flags.ts b/packages/qwik/src/core/shared/utils/numeric-prop-key-flags.ts new file mode 100644 index 00000000000..b15dd4669d9 --- /dev/null +++ b/packages/qwik/src/core/shared/utils/numeric-prop-key-flags.ts @@ -0,0 +1,30 @@ +import { type NumericPropKey } from './numeric-prop-key'; + +export const enum NumericPropKeyFlags { + EVENT = 1, + Q_PREFIX = 2, + START_WITH_COLON = 4, + SLOT = 8, +} + +export const NumericFlagsShift = 4; + +function getFlags(id: number) { + return ((1 << NumericFlagsShift) - 1) & (id >> 0); +} + +export function isEventProp(numericProp: NumericPropKey): boolean { + return (getFlags(numericProp) & NumericPropKeyFlags.EVENT) !== 0; +} + +export function isQProp(numericProp: NumericPropKey): boolean { + return (getFlags(numericProp) & NumericPropKeyFlags.Q_PREFIX) !== 0; +} + +export function startsWithColon(numericProp: NumericPropKey): boolean { + return (getFlags(numericProp) & NumericPropKeyFlags.START_WITH_COLON) !== 0; +} + +export function isSlotProp(numericProp: NumericPropKey): boolean { + return (getFlags(numericProp) & NumericPropKeyFlags.SLOT) !== 0; +} diff --git a/packages/qwik/src/core/shared/utils/numeric-prop-key.ts b/packages/qwik/src/core/shared/utils/numeric-prop-key.ts new file mode 100644 index 00000000000..708d9a72f54 --- /dev/null +++ b/packages/qwik/src/core/shared/utils/numeric-prop-key.ts @@ -0,0 +1,98 @@ +import type { KnownEventNames } from '../jsx/types/jsx-qwik-events'; +import { + createEventName, + isHtmlAttributeAnEventName, + isJsxPropertyAnEventName, + parseEventNameFromIndex, +} from './event-names'; +import { + ELEMENT_ID, + ELEMENT_KEY, + ELEMENT_PROPS, + HANDLER_PREFIX, + NON_SERIALIZABLE_MARKER_PREFIX, + Q_PREFIX, + dangerouslySetInnerHTML, + refAttr, + OnRenderProp, + QSlotParent, + QCtxAttr, + QSlot, + QScopedStyle, + ELEMENT_SEQ, + ELEMENT_SEQ_IDX, + QBackRefs, + USE_ON_LOCAL_SEQ_IDX, + USE_ON_LOCAL, + USE_ON_LOCAL_FLAGS, +} from './markers'; +import { NumericFlagsShift, NumericPropKeyFlags } from './numeric-prop-key-flags'; + +const propNameToId = new Map(); +const idToPropName: (string | symbol)[] = []; +export type NumericPropKey = number & { __brand__: 'NumericPropKey' }; + +const colonOnLength = ':on'.length; + +export const getPropId = (name: string | symbol): NumericPropKey => { + let id = propNameToId.get(name); + if (id != null) { + return id; + } + id = (idToPropName.length << NumericFlagsShift) as NumericPropKey; + if (typeof name === 'string') { + if (isJsxPropertyAnEventName(name)) { + name = normalizeEvent(name); + (id as number) |= NumericPropKeyFlags.EVENT; + } else if (isHtmlAttributeAnEventName(name)) { + (id as number) |= NumericPropKeyFlags.EVENT; + } else if (name.startsWith(Q_PREFIX)) { + (id as number) |= NumericPropKeyFlags.Q_PREFIX; + } else if (name.startsWith(HANDLER_PREFIX)) { + (id as number) |= NumericPropKeyFlags.START_WITH_COLON; + } + + if (!name.startsWith(Q_PREFIX) && !name.startsWith(NON_SERIALIZABLE_MARKER_PREFIX)) { + (id as number) |= NumericPropKeyFlags.SLOT; + } + } + idToPropName.push(name); + propNameToId.set(name, id); + return id; +}; + +export const StaticPropId = { + // ELEMENT_KEY should be always first, because of `getKey` in vnode_diff.ts + ELEMENT_KEY: getPropId(ELEMENT_KEY), + ELEMENT_ID: getPropId(ELEMENT_ID), + ELEMENT_PROPS: getPropId(ELEMENT_PROPS), + REF: getPropId(refAttr), + INNER_HTML: getPropId(dangerouslySetInnerHTML), + VALUE: getPropId('value'), + ON_RENDER: getPropId(OnRenderProp), + CLASS: getPropId('class'), + CLASS_NAME: getPropId('classname'), + SLOT: getPropId(QSlot), + SLOT_PARENT: getPropId(QSlotParent), + CTX: getPropId(QCtxAttr), + SCOPED_STYLE: getPropId(QScopedStyle), + ELEMENT_SEQ: getPropId(ELEMENT_SEQ), + ELEMENT_SEQ_IDX: getPropId(ELEMENT_SEQ_IDX), + BACK_REFS: getPropId(QBackRefs), + USE_ON_LOCAL_SEQ_IDX: getPropId(USE_ON_LOCAL_SEQ_IDX), + USE_ON_LOCAL: getPropId(USE_ON_LOCAL), + USE_ON_LOCAL_FLAGS: getPropId(USE_ON_LOCAL_FLAGS), + CHILDREN: getPropId('children'), +}; + +export const getPropName = (id: NumericPropKey): T => { + return idToPropName[id >> NumericFlagsShift] as T; +}; + +function normalizeEvent(name: string): string { + const index = name.indexOf(':on'); + const scope = (name.substring(0, index) || undefined) as 'window' | 'document' | undefined; + const eventName = parseEventNameFromIndex(name, index + colonOnLength); + name = createEventName(eventName, scope) as KnownEventNames; + return name; +} diff --git a/packages/qwik/src/core/shared/utils/prop.ts b/packages/qwik/src/core/shared/utils/prop.ts index 95e2049aa07..9da72b5e49c 100644 --- a/packages/qwik/src/core/shared/utils/prop.ts +++ b/packages/qwik/src/core/shared/utils/prop.ts @@ -1,9 +1,31 @@ -import { createPropsProxy, type Props, type PropsProxy } from '../jsx/jsx-runtime'; +import { EffectProperty, WrappedSignal } from '../../signal/signal'; +import { trackSignalAndAssignHost } from '../../use/use-core'; +import { + createPropsProxy, + directGetPropsProxyProp, + type Props, + type PropsProxy, +} from '../jsx/jsx-runtime'; +import type { JSXNodeInternal } from '../jsx/types/jsx-node'; +import type { Container, HostElement } from '../types'; import { _CONST_PROPS, _VAR_PROPS } from './constants'; -import { NON_SERIALIZABLE_MARKER_PREFIX } from './markers'; +import { QDefaultSlot } from './markers'; +import { getPropId, getPropName, type NumericPropKey } from './numeric-prop-key'; -export function isSlotProp(prop: string): boolean { - return !prop.startsWith('q:') && !prop.startsWith(NON_SERIALIZABLE_MARKER_PREFIX); +export function getSlotName( + host: HostElement | null, + jsx: JSXNodeInternal, + container: Container +): string { + const constProps = jsx.constProps; + const nameId = getPropId('name'); + if (host && constProps && typeof constProps === 'object' && nameId in constProps) { + const constValue = constProps[nameId]; + if (constValue instanceof WrappedSignal) { + return trackSignalAndAssignHost(constValue, host, EffectProperty.COMPONENT, container); + } + } + return directGetPropsProxyProp(jsx, nameId) || QDefaultSlot; } /** @internal */ @@ -12,7 +34,7 @@ export const _restProps = (props: PropsProxy, omit: string[], target: Props = {} const constProps = props[_CONST_PROPS]; if (constProps) { for (const key in constProps) { - if (!omit.includes(key)) { + if (!omit.includes(getPropName(key as unknown as NumericPropKey))) { constPropsTarget ||= {}; constPropsTarget[key] = constProps[key]; } @@ -21,7 +43,7 @@ export const _restProps = (props: PropsProxy, omit: string[], target: Props = {} const varPropsTarget: Props = target; const varProps = props[_VAR_PROPS]; for (const key in varProps) { - if (!omit.includes(key)) { + if (!omit.includes(getPropName(key as unknown as NumericPropKey))) { varPropsTarget[key] = varProps[key]; } } diff --git a/packages/qwik/src/core/shared/utils/scoped-styles.ts b/packages/qwik/src/core/shared/utils/scoped-styles.ts index 2aede98ba44..0f705ffe5a6 100644 --- a/packages/qwik/src/core/shared/utils/scoped-styles.ts +++ b/packages/qwik/src/core/shared/utils/scoped-styles.ts @@ -1,17 +1,13 @@ import type { Props } from '../jsx/jsx-runtime'; import { ComponentStylesPrefixContent } from './markers'; +import { StaticPropId } from './numeric-prop-key'; export const styleContent = (styleId: string): string => { return ComponentStylesPrefixContent + styleId; }; export function hasClassAttr(props: Props): boolean { - for (const key in props) { - if (Object.prototype.hasOwnProperty.call(props, key) && isClassAttr(key)) { - return true; - } - } - return false; + return StaticPropId.CLASS in props || StaticPropId.CLASS_NAME in props; } export function isClassAttr(key: string): boolean { diff --git a/packages/qwik/src/core/signal/signal-utils.ts b/packages/qwik/src/core/signal/signal-utils.ts index d80e6bc65af..ec8a5771710 100644 --- a/packages/qwik/src/core/signal/signal-utils.ts +++ b/packages/qwik/src/core/signal/signal-utils.ts @@ -5,6 +5,7 @@ import { SignalFlags, WrappedSignal, WrappedSignalFlags } from './signal'; import { isSignal, type Signal } from './signal.public'; import { getStoreTarget } from './store'; import { isPropsProxy } from '../shared/jsx/jsx-runtime'; +import { getPropId } from '../shared/utils/numeric-prop-key'; // Keep these properties named like this so they're the same as from wrapSignal const getValueProp = (p0: any) => p0.value; @@ -40,9 +41,10 @@ export const _wrapProp = , P extends keyof T>(...args } if (isPropsProxy(obj)) { const constProps = obj[_CONST_PROPS] as any; - if (constProps && prop in constProps) { + const numericProp = getPropId(prop as string); + if (constProps && numericProp in constProps) { // Const props don't need wrapping - return constProps[prop]; + return constProps[numericProp]; } } else { const target = getStoreTarget(obj); diff --git a/packages/qwik/src/core/signal/signal.ts b/packages/qwik/src/core/signal/signal.ts index d90836b2e7b..2ccf82818c7 100644 --- a/packages/qwik/src/core/signal/signal.ts +++ b/packages/qwik/src/core/signal/signal.ts @@ -17,7 +17,6 @@ import { type QRLInternal } from '../shared/qrl/qrl-class'; import type { QRL } from '../shared/qrl/qrl.public'; import { trackSignal, tryGetInvokeContext } from '../use/use-core'; import { isTask, Task, TaskFlags } from '../use/use-task'; -import { ELEMENT_PROPS, OnRenderProp } from '../shared/utils/markers'; import { isPromise } from '../shared/utils/promises'; import { qDev } from '../shared/utils/qdev'; import type { VNode } from '../client/types'; @@ -34,6 +33,7 @@ import { QError, qError } from '../shared/error/error'; import { isDomContainer } from '../client/dom-container'; import { type BackRef } from './signal-cleanup'; import { getSubscriber } from './subscriber'; +import { StaticPropId } from '../../server/qwik-copy'; const DEBUG = false; @@ -263,7 +263,7 @@ export const addQrlToSerializationCtx = ( } else if (effect instanceof ComputedSignal) { qrl = effect.$computeQrl$; } else if (property === EffectProperty.COMPONENT) { - qrl = container.getHostProp(effect as ISsrNode, OnRenderProp); + qrl = container.getHostProp(effect as ISsrNode, StaticPropId.ON_RENDER); } if (qrl) { (container as SSRContainer).serializationCtx.$eventQrls$.add(qrl); @@ -304,9 +304,12 @@ export const triggerEffects = ( (consumer as ComputedSignal | WrappedSignal).$invalidate$(); } else if (property === EffectProperty.COMPONENT) { const host: HostElement = consumer as any; - const qrl = container.getHostProp>>(host, OnRenderProp); + const qrl = container.getHostProp>>( + host, + StaticPropId.ON_RENDER + ); assertDefined(qrl, 'Component must have QRL'); - const props = container.getHostProp(host, ELEMENT_PROPS); + const props = container.getHostProp(host, StaticPropId.ELEMENT_PROPS); container.$scheduler$(ChoreType.COMPONENT, host, qrl, props); } else if (isBrowser) { if (property === EffectProperty.VNODE) { diff --git a/packages/qwik/src/core/signal/store.ts b/packages/qwik/src/core/signal/store.ts index 645f5214f24..299e20030b8 100644 --- a/packages/qwik/src/core/signal/store.ts +++ b/packages/qwik/src/core/signal/store.ts @@ -86,7 +86,7 @@ export class StoreHandler implements ProxyHandler { return '[Store]'; } - get(target: TargetType, prop: string | symbol) { + get(target: TargetType, prop: number | string | symbol) { if (typeof prop === 'symbol') { if (prop === STORE_TARGET) { return target; @@ -219,7 +219,7 @@ export class StoreHandler implements ProxyHandler { export function addStoreEffect( target: TargetType, - prop: string | symbol, + prop: number | string | symbol, store: StoreHandler, effectSubscription: EffectSubscription ) { diff --git a/packages/qwik/src/core/signal/subscriber.ts b/packages/qwik/src/core/signal/subscriber.ts index 4a3944b5b7b..dd66c83d878 100644 --- a/packages/qwik/src/core/signal/subscriber.ts +++ b/packages/qwik/src/core/signal/subscriber.ts @@ -1,4 +1,3 @@ -import { QBackRefs } from '../shared/utils/markers'; import { type Consumer, EffectProperty, @@ -9,6 +8,7 @@ import type { ISsrNode } from '../ssr/ssr-types'; import { _EFFECT_BACK_REF } from '../signal/flags'; import { isServer } from '@qwik.dev/core/build'; import { BackRef } from './signal-cleanup'; +import { StaticPropId } from '../../server/qwik-copy'; export function getSubscriber( effect: Consumer, @@ -17,7 +17,7 @@ export function getSubscriber( ): EffectSubscription { if (!(effect as BackRef)[_EFFECT_BACK_REF]) { if (isServer && isSsrNode(effect)) { - effect.setProp(QBackRefs, new Map()); + effect.setProp(StaticPropId.BACK_REFS, new Map()); } else { (effect as BackRef)[_EFFECT_BACK_REF] = new Map(); } diff --git a/packages/qwik/src/core/ssr/ssr-render-component.ts b/packages/qwik/src/core/ssr/ssr-render-component.ts index 9858d059bf3..1cce2ea6961 100644 --- a/packages/qwik/src/core/ssr/ssr-render-component.ts +++ b/packages/qwik/src/core/ssr/ssr-render-component.ts @@ -1,12 +1,12 @@ import type { JSXNode } from '@qwik.dev/core'; import { SERIALIZABLE_STATE, type Component, type OnRenderFn } from '../shared/component.public'; import type { QRLInternal } from '../shared/qrl/qrl-class'; -import { ELEMENT_KEY, ELEMENT_PROPS, OnRenderProp } from '../shared/utils/markers'; import { type ISsrNode, type SSRContainer } from './ssr-types'; import { executeComponent } from '../shared/component-execution'; import { ChoreType } from '../shared/util-chore-type'; import type { ValueOrPromise } from '../shared/utils/types'; import type { JSXOutput } from '../shared/jsx/types/jsx-node'; +import { StaticPropId } from '../../server/qwik-copy'; export const applyInlineComponent = ( ssr: SSRContainer, @@ -30,10 +30,10 @@ export const applyQwikComponentBody = ( delete srcProps.children; } const scheduler = ssr.$scheduler$; - host.setProp(OnRenderProp, componentQrl); - host.setProp(ELEMENT_PROPS, srcProps); + host.setProp(StaticPropId.ON_RENDER, componentQrl); + host.setProp(StaticPropId.ELEMENT_PROPS, srcProps); if (jsx.key !== null) { - host.setProp(ELEMENT_KEY, jsx.key); + host.setProp(StaticPropId.ELEMENT_KEY, jsx.key); } return scheduler(ChoreType.COMPONENT, host, componentQrl, srcProps); }; diff --git a/packages/qwik/src/core/ssr/ssr-render-jsx.ts b/packages/qwik/src/core/ssr/ssr-render-jsx.ts index 3c1edddaeb2..4a5821323b8 100644 --- a/packages/qwik/src/core/ssr/ssr-render-jsx.ts +++ b/packages/qwik/src/core/ssr/ssr-render-jsx.ts @@ -14,7 +14,6 @@ import { isAsyncGenerator } from '../shared/utils/async-generator'; import { convertEventNameFromJsxPropToHtmlAttr, getEventNameFromJsxProp, - isJsxPropertyAnEventName, isPreventDefault, } from '../shared/utils/event-names'; import { EMPTY_ARRAY } from '../shared/utils/flyweight'; @@ -23,7 +22,6 @@ import { ELEMENT_KEY, FLUSH_COMMENT, QDefaultSlot, - QScopedStyle, QSlot, QSlotParent, qwikInspectorAttr, @@ -33,11 +31,19 @@ import { qInspector } from '../shared/utils/qdev'; import { addComponentStylePrefix, isClassAttr } from '../shared/utils/scoped-styles'; import { serializeAttribute } from '../shared/utils/styles'; import { isFunction, type ValueOrPromise } from '../shared/utils/types'; -import { EffectProperty, WrappedSignal, isSignal } from '../signal/signal'; +import { EffectProperty, isSignal } from '../signal/signal'; import { trackSignalAndAssignHost } from '../use/use-core'; import { applyInlineComponent, applyQwikComponentBody } from './ssr-render-component'; -import type { ISsrComponentFrame, ISsrNode, SSRContainer, SsrAttrs } from './ssr-types'; +import type { ISsrComponentFrame, SSRContainer, SsrAttrs } from './ssr-types'; import { isQrl } from '../shared/qrl/qrl-utils'; +import { getSlotName } from '../shared/utils/prop'; +import { + StaticPropId, + getPropId, + getPropName, + type NumericPropKey, +} from '../shared/utils/numeric-prop-key'; +import { isEventProp } from '../shared/utils/numeric-prop-key-flags'; class ParentComponentData { constructor( @@ -109,12 +115,12 @@ function processJSXNode( enqueue(value[i]); } } else if (isSignal(value)) { - ssr.openFragment(isDev ? [DEBUG_TYPE, VirtualType.WrappedSignal] : EMPTY_ARRAY); + ssr.openFragment(isDev ? [getPropId(DEBUG_TYPE), VirtualType.WrappedSignal] : EMPTY_ARRAY); const signalNode = ssr.getLastNode(); enqueue(ssr.closeFragment); enqueue(trackSignalAndAssignHost(value, signalNode, EffectProperty.VNODE, ssr)); } else if (isPromise(value)) { - ssr.openFragment(isDev ? [DEBUG_TYPE, VirtualType.Awaited] : EMPTY_ARRAY); + ssr.openFragment(isDev ? [getPropId(DEBUG_TYPE), VirtualType.Awaited] : EMPTY_ARRAY); enqueue(ssr.closeFragment); enqueue(value); enqueue(Promise); @@ -177,9 +183,9 @@ function processJSXNode( children != null && enqueue(children); } else if (isFunction(type)) { if (type === Fragment) { - let attrs = jsx.key != null ? [ELEMENT_KEY, jsx.key] : EMPTY_ARRAY; + let attrs = jsx.key != null ? [StaticPropId.ELEMENT_KEY, jsx.key] : EMPTY_ARRAY; if (isDev) { - attrs = [DEBUG_TYPE, VirtualType.Fragment, ...attrs]; // Add debug info. + attrs = [getPropId(DEBUG_TYPE), VirtualType.Fragment, ...attrs]; // Add debug info. } ssr.openFragment(attrs); ssr.addCurrentElementFrameAsComponentChild(); @@ -192,13 +198,15 @@ function processJSXNode( options.parentComponentFrame || ssr.unclaimedProjectionComponentFrameQueue.shift(); if (componentFrame) { const compId = componentFrame.componentNode.id || ''; - const projectionAttrs = isDev ? [DEBUG_TYPE, VirtualType.Projection] : []; - projectionAttrs.push(QSlotParent, compId); + const projectionAttrs: SsrAttrs = isDev + ? [getPropId(DEBUG_TYPE), VirtualType.Projection] + : []; + projectionAttrs.push(getPropId(QSlotParent), compId); ssr.openProjection(projectionAttrs); const host = componentFrame.componentNode; const node = ssr.getLastNode(); const slotName = getSlotName(host, jsx, ssr); - projectionAttrs.push(QSlot, slotName); + projectionAttrs.push(getPropId(QSlot), slotName); enqueue(new ParentComponentData(options.styleScoped, options.parentComponentFrame)); enqueue(ssr.closeProjection); @@ -217,11 +225,11 @@ function processJSXNode( ); } 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); + ssr.openFragment(isDev ? [getPropId(DEBUG_TYPE), VirtualType.Projection] : EMPTY_ARRAY); ssr.closeFragment(); } } else if (type === SSRComment) { - ssr.commentNode(directGetPropsProxyProp(jsx, 'data') || ''); + ssr.commentNode(directGetPropsProxyProp(jsx, getPropId('data')) || ''); } else if (type === SSRStream) { ssr.commentNode(FLUSH_COMMENT); const generator = jsx.children as SSRStreamChildren; @@ -243,10 +251,10 @@ function processJSXNode( enqueue(value as StackValue); isPromise(value) && enqueue(Promise); } else if (type === SSRRaw) { - ssr.htmlNode(directGetPropsProxyProp(jsx, 'data')); + ssr.htmlNode(directGetPropsProxyProp(jsx, getPropId('data'))); } 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] : []); + ssr.openComponent(isDev ? [getPropId(DEBUG_TYPE), VirtualType.Component] : []); const host = ssr.getLastNode(); const componentFrame = ssr.getParentComponentFrame()!; componentFrame!.distributeChildrenIntoSlots( @@ -256,17 +264,19 @@ function processJSXNode( ); const jsxOutput = applyQwikComponentBody(ssr, jsx, type); - const compStyleComponentId = addComponentStylePrefix(host.getProp(QScopedStyle)); + const compStyleComponentId = addComponentStylePrefix( + host.getProp(StaticPropId.SCOPED_STYLE) + ); enqueue(new ParentComponentData(options.styleScoped, options.parentComponentFrame)); enqueue(ssr.closeComponent); enqueue(jsxOutput); isPromise(jsxOutput) && enqueue(Promise); enqueue(new ParentComponentData(compStyleComponentId, componentFrame)); } else { - const inlineComponentProps = [ELEMENT_KEY, jsx.key]; + const inlineComponentProps = [StaticPropId.ELEMENT_KEY, jsx.key]; ssr.openFragment( isDev - ? [DEBUG_TYPE, VirtualType.InlineComponent, ...inlineComponentProps] + ? [getPropId(DEBUG_TYPE), VirtualType.InlineComponent, ...inlineComponentProps] : inlineComponentProps ); enqueue(ssr.closeFragment); @@ -333,8 +343,10 @@ export function toSsrAttrs( } const ssrAttrs: SsrAttrs = []; for (const key in record) { - let value = record[key]; - if (isJsxPropertyAnEventName(key)) { + const numericKey = key as unknown as NumericPropKey; + let value = record[numericKey]; + const nameKey = getPropName(numericKey); + if (isEventProp(numericKey)) { if (anotherRecord) { /** * If we have two sources of the same event like this: @@ -356,7 +368,7 @@ export function toSsrAttrs( * - For the var props we need to merge them into the one value (array) * - For the const props we need to just skip, because we will handle this in the var props */ - const anotherValue = getEventProp(anotherRecord, key); + const anotherValue = getEventProp(anotherRecord, numericKey); if (anotherValue) { if (pushMergedEventProps) { // merge values from the const props with the var props @@ -366,31 +378,31 @@ export function toSsrAttrs( } } } - const eventValue = setEvent(serializationCtx, key, value); + const eventValue = setEvent(serializationCtx, nameKey, value); if (eventValue) { - ssrAttrs.push(convertEventNameFromJsxPropToHtmlAttr(key), eventValue); + ssrAttrs.push(convertEventNameFromJsxPropToHtmlAttr(nameKey), eventValue); } continue; } if (isSignal(value)) { // write signal as is. We will track this signal inside `writeAttrs` - if (isClassAttr(key)) { + if (isClassAttr(nameKey)) { // additionally append styleScopedId for class attr - ssrAttrs.push(key, [value, styleScopedId]); + ssrAttrs.push(nameKey, [value, styleScopedId]); } else { - ssrAttrs.push(key, value); + ssrAttrs.push(nameKey, value); } continue; } - if (isPreventDefault(key)) { - addPreventDefaultEventToSerializationContext(serializationCtx, key); + if (isPreventDefault(nameKey)) { + addPreventDefaultEventToSerializationContext(serializationCtx, nameKey); } - value = serializeAttribute(key, value, styleScopedId); + value = serializeAttribute(nameKey, value, styleScopedId); - ssrAttrs.push(key, value as string); + ssrAttrs.push(nameKey, value as string); } if (key != null) { ssrAttrs.push(ELEMENT_KEY, key); @@ -418,10 +430,9 @@ function getMergedEventPropValues(value: unknown, anotherValue: unknown) { return mergedValue; } -function getEventProp(record: Record, propKey: string): unknown | null { - const eventProp = propKey.toLowerCase(); +function getEventProp(record: Record, numericKey: NumericPropKey): unknown | null { for (const prop in record) { - if (prop.toLowerCase() === eventProp) { + if ((prop as unknown as NumericPropKey) === numericKey) { return record[prop]; } } @@ -497,30 +508,20 @@ function addPreventDefaultEventToSerializationContext( } } -function getSlotName(host: ISsrNode, jsx: JSXNodeInternal, ssr: SSRContainer): string { - const constProps = jsx.constProps; - if (constProps && typeof constProps == 'object' && 'name' in constProps) { - const constValue = constProps.name; - if (constValue instanceof WrappedSignal) { - return trackSignalAndAssignHost(constValue, host, EffectProperty.COMPONENT, ssr); - } - } - return directGetPropsProxyProp(jsx, 'name') || QDefaultSlot; -} - function appendQwikInspectorAttribute(jsx: JSXNodeInternal, qwikInspectorAttrValue: string | null) { - if (qwikInspectorAttrValue && (!jsx.constProps || !(qwikInspectorAttr in jsx.constProps))) { - (jsx.constProps ||= {})[qwikInspectorAttr] = qwikInspectorAttrValue; + const qwikInspectorAttrId = getPropId(qwikInspectorAttr); + if (qwikInspectorAttrValue && (!jsx.constProps || !(qwikInspectorAttrId in jsx.constProps))) { + (jsx.constProps ||= {})[qwikInspectorAttrId] = qwikInspectorAttrValue; } } // append class attribute if styleScopedId exists and there is no class attribute function appendClassIfScopedStyleExists(jsx: JSXNodeInternal, styleScoped: string | null) { - const classAttributeExists = directGetPropsProxyProp(jsx, 'class') != null; + const classAttributeExists = directGetPropsProxyProp(jsx, StaticPropId.CLASS) != null; if (!classAttributeExists && styleScoped) { if (!jsx.constProps) { jsx.constProps = {}; } - jsx.constProps['class'] = ''; + jsx.constProps[StaticPropId.CLASS] = ''; } } diff --git a/packages/qwik/src/core/ssr/ssr-types.ts b/packages/qwik/src/core/ssr/ssr-types.ts index 8bc7d51bbc9..0f532e974a1 100644 --- a/packages/qwik/src/core/ssr/ssr-types.ts +++ b/packages/qwik/src/core/ssr/ssr-types.ts @@ -13,6 +13,7 @@ import type { JSXNodeInternal } from '../shared/jsx/types/jsx-node'; import type { ResourceReturnInternal } from '../use/use-resource'; import type { Signal } from '../signal/signal.public'; import type { VNodeData } from '../../server/vnode-data'; +import type { NumericPropKey } from '../shared/utils/numeric-prop-key'; export type SsrAttrKey = string; export type SsrAttrValue = string | Signal | boolean | object | null; @@ -27,9 +28,9 @@ export interface ISsrNode { id: string; currentComponentNode: ISsrNode | null; vnodeData?: VNodeData; - setProp(name: string, value: any): void; - getProp(name: string): any; - removeProp(name: string): void; + setProp(key: NumericPropKey, value: any): void; + getProp(key: NumericPropKey): any; + removeProp(key: NumericPropKey): void; addChildVNodeData(child: VNodeData): void; } @@ -49,7 +50,7 @@ export interface ISsrComponentFrame { parentScopedStyle: string | null, parentComponentFrame: ISsrComponentFrame | null ): void; - hasSlot(slotName: string): boolean; + hasSlot(slotNameKey: NumericPropKey): boolean; } export type SymbolToChunkResolver = (symbol: string) => string; diff --git a/packages/qwik/src/core/tests/container.spec.tsx b/packages/qwik/src/core/tests/container.spec.tsx index 15ae5244e49..10bd312c339 100644 --- a/packages/qwik/src/core/tests/container.spec.tsx +++ b/packages/qwik/src/core/tests/container.spec.tsx @@ -8,7 +8,12 @@ import { getDomContainer } from '../client/dom-container'; import type { ClientContainer, VNode } from '../client/types'; import { vnode_getAttr, vnode_getFirstChild, vnode_getText } from '../client/vnode'; import { SERIALIZABLE_STATE, component$ } from '../shared/component.public'; -import { Fragment, JSXNodeImpl, createPropsProxy } from '../shared/jsx/jsx-runtime'; +import { + Fragment, + JSXNodeImpl, + convertToNumericProps, + createPropsProxy, +} from '../shared/jsx/jsx-runtime'; import { Slot } from '../shared/jsx/slot.public'; import type { JSXOutput } from '../shared/jsx/types/jsx-node'; import { inlinedQrl, qrl } from '../shared/qrl/qrl'; @@ -20,6 +25,7 @@ import { constPropsToSsrAttrs, varPropsToSsrAttrs } from '../ssr/ssr-render-jsx' import { type SSRContainer } from '../ssr/ssr-types'; import { _qrlSync } from '../shared/qrl/qrl.public'; import { SignalFlags } from '../signal/signal'; +import { getPropId } from '../shared/utils/numeric-prop-key'; describe('serializer v2', () => { describe('rendering', () => { @@ -123,7 +129,7 @@ describe('serializer v2', () => { ssr.closeElement(); }); const vnodeSpan = await clientContainer.$getObjectById$(0).someProp; - expect(vnode_getAttr(vnodeSpan, 'id')).toBe('myId'); + expect(vnode_getAttr(vnodeSpan, getPropId('id'))).toBe('myId'); }); it('should retrieve text node', async () => { const clientContainer = await withContainer((ssr) => { @@ -495,11 +501,17 @@ describe('serializer v2', () => { describe('PropsProxySerializer, //// ' + TypeIds.PropsProxy, () => { it('should serialize and deserialize', async () => { - const obj = createPropsProxy({ number: 1, text: 'abc' }, { n: 2, t: 'test' }); + const varProps = {}; + convertToNumericProps({ number: 1, text: 'abc' }, varProps); + const constProps = {}; + convertToNumericProps({ n: 2, t: 'test' }, constProps); + const obj = createPropsProxy(varProps, constProps); expect((await withContainer((ssr) => ssr.addRoot(obj))).$getObjectById$(0)).toEqual(obj); }); it('should serialize and deserialize with null const props', async () => { - const obj = createPropsProxy({ number: 1, text: 'abc' }, null); + const varProps = {}; + convertToNumericProps({ number: 1, text: 'abc' }, varProps); + const obj = createPropsProxy(varProps, null); expect((await withContainer((ssr) => ssr.addRoot(obj))).$getObjectById$(0)).toEqual(obj); }); }); @@ -599,7 +611,7 @@ async function toHTML(jsx: JSXOutput): Promise { if (!jsx.constProps) { jsx.constProps = {}; } - jsx.constProps['class'] = ''; + jsx.constProps[getPropId('class')] = ''; } ssrContainer.openElement( jsx.type, diff --git a/packages/qwik/src/core/tests/projection.spec.tsx b/packages/qwik/src/core/tests/projection.spec.tsx index d85cbc1da97..b2b0fa875b5 100644 --- a/packages/qwik/src/core/tests/projection.spec.tsx +++ b/packages/qwik/src/core/tests/projection.spec.tsx @@ -23,6 +23,7 @@ import { cleanupAttrs } from 'packages/qwik/src/testing/element-fixture'; import { beforeEach, describe, expect, it } from 'vitest'; import { vnode_getNextSibling, vnode_getProp, vnode_locate } from '../client/vnode'; import { HTML_NS, SVG_NS } from '../shared/utils/markers'; +import { StaticPropId } from '../../server/qwik-copy'; const DEBUG = false; @@ -764,6 +765,7 @@ describe.each([ ); + await trigger(document.body, 'button', 'click'); expect(vNode).toMatchVDOM( @@ -2190,7 +2192,7 @@ describe.each([ if (ssrRenderToDom === render) { const CmpVNode = vnode_locate(container.rootVNode, '4A'); - const renderProp = vnode_getProp(CmpVNode, 'q:renderFn', null); + const renderProp = vnode_getProp(CmpVNode, StaticPropId.ON_RENDER, null); expect(renderProp).not.toBeNull(); } }); diff --git a/packages/qwik/src/core/use/use-id.ts b/packages/qwik/src/core/use/use-id.ts index bef810d9e01..b55c4ad948e 100644 --- a/packages/qwik/src/core/use/use-id.ts +++ b/packages/qwik/src/core/use/use-id.ts @@ -1,10 +1,10 @@ import type { QRL } from '..'; import { hashCode } from '../shared/utils/hash_code'; -import { OnRenderProp } from '../shared/utils/markers'; import { isDomContainer } from '../client/dom-container'; import type { SSRContainer } from '../ssr/ssr-types'; import { useSequentialScope } from './use-sequential-scope'; import { getNextUniqueIndex } from '../shared/utils/unique-index-generator'; +import { StaticPropId } from '../../server/qwik-copy'; /** @public */ export const useId = (): string => { @@ -16,7 +16,10 @@ export const useId = (): string => { ? '' : (iCtx.$container$ as SSRContainer).buildBase || ''; const base = containerBase ? hashCode(containerBase) : ''; - const componentQrl = iCtx.$container$.getHostProp(iCtx.$hostElement$, OnRenderProp); + const componentQrl = iCtx.$container$.getHostProp( + iCtx.$hostElement$, + StaticPropId.ON_RENDER + ); const hash = componentQrl?.getHash() || ''; const counter = getNextUniqueIndex(iCtx.$container$) || ''; const id = `${base}-${hash}-${counter}`; // If no base and no hash, then "--#" diff --git a/packages/qwik/src/core/use/use-on.ts b/packages/qwik/src/core/use/use-on.ts index dce4e152946..f23b2ac3891 100644 --- a/packages/qwik/src/core/use/use-on.ts +++ b/packages/qwik/src/core/use/use-on.ts @@ -7,7 +7,8 @@ import type { AllEventKeys, } from '../shared/jsx/types/jsx-qwik-attributes'; import type { HostElement } from '../shared/types'; -import { USE_ON_LOCAL, USE_ON_LOCAL_FLAGS, USE_ON_LOCAL_SEQ_IDX } from '../shared/utils/markers'; +import { createEventName } from '../shared/utils/event-names'; +import { StaticPropId } from '../shared/utils/numeric-prop-key'; export type EventQRL = | QRL, Element>> @@ -97,17 +98,6 @@ export const useOnWindow = (event: T | T[], eventQrl: _useOn(createEventName(event, 'window'), eventQrl); }; -const createEventName = ( - event: KnownEventNames | KnownEventNames[], - eventType: 'window' | 'document' | undefined -) => { - const prefix = eventType !== undefined ? eventType + ':' : ''; - const map = (name: string) => - prefix + 'on' + name.charAt(0).toUpperCase() + name.substring(1) + '$'; - const res = Array.isArray(event) ? event.map(map) : map(event); - return res; -}; - const _useOn = (eventName: string | string[], eventQrl: EventQRL) => { const { isAdded, addEvent } = useOnEventsSequentialScope(); if (isAdded) { @@ -135,20 +125,20 @@ const useOnEventsSequentialScope = () => { const iCtx = useInvokeContext(); const hostElement = iCtx.$hostElement$; const host: HostElement = hostElement as any; - let onMap = iCtx.$container$.getHostProp(host, USE_ON_LOCAL); + let onMap = iCtx.$container$.getHostProp(host, StaticPropId.USE_ON_LOCAL); if (onMap === null) { onMap = {}; - iCtx.$container$.setHostProp(host, USE_ON_LOCAL, onMap); + iCtx.$container$.setHostProp(host, StaticPropId.USE_ON_LOCAL, onMap); } - let seqIdx = iCtx.$container$.getHostProp(host, USE_ON_LOCAL_SEQ_IDX); + let seqIdx = iCtx.$container$.getHostProp(host, StaticPropId.USE_ON_LOCAL_SEQ_IDX); if (seqIdx === null) { seqIdx = 0; } - iCtx.$container$.setHostProp(host, USE_ON_LOCAL_SEQ_IDX, seqIdx + 1); - let addedFlags = iCtx.$container$.getHostProp(host, USE_ON_LOCAL_FLAGS); + iCtx.$container$.setHostProp(host, StaticPropId.USE_ON_LOCAL_SEQ_IDX, seqIdx + 1); + let addedFlags = iCtx.$container$.getHostProp(host, StaticPropId.USE_ON_LOCAL_FLAGS); if (addedFlags === null) { addedFlags = []; - iCtx.$container$.setHostProp(host, USE_ON_LOCAL_FLAGS, addedFlags); + iCtx.$container$.setHostProp(host, StaticPropId.USE_ON_LOCAL_FLAGS, addedFlags); } while (addedFlags.length <= seqIdx) { addedFlags.push(false); diff --git a/packages/qwik/src/core/use/use-sequential-scope.ts b/packages/qwik/src/core/use/use-sequential-scope.ts index 3594753ef82..9aebccde8a9 100644 --- a/packages/qwik/src/core/use/use-sequential-scope.ts +++ b/packages/qwik/src/core/use/use-sequential-scope.ts @@ -1,8 +1,8 @@ import { verifySerializable } from '../shared/utils/serialize-utils'; -import { ELEMENT_SEQ, ELEMENT_SEQ_IDX } from '../shared/utils/markers'; import { qDev, qSerialize } from '../shared/utils/qdev'; import type { HostElement } from '../shared/types'; import { useInvokeContext, type RenderInvokeContext } from './use-core'; +import { StaticPropId } from '../shared/utils/numeric-prop-key'; export interface SequentialScope { /** The currently stored data for the hook that calls this */ @@ -22,16 +22,16 @@ export const useSequentialScope = (): SequentialScope => { const iCtx = useInvokeContext(); const hostElement = iCtx.$hostElement$; const host: HostElement = hostElement as any; - let seq = iCtx.$container$.getHostProp(host, ELEMENT_SEQ); + let seq = iCtx.$container$.getHostProp(host, StaticPropId.ELEMENT_SEQ); if (seq === null) { seq = []; - iCtx.$container$.setHostProp(host, ELEMENT_SEQ, seq); + iCtx.$container$.setHostProp(host, StaticPropId.ELEMENT_SEQ, seq); } - let seqIdx = iCtx.$container$.getHostProp(host, ELEMENT_SEQ_IDX); + let seqIdx = iCtx.$container$.getHostProp(host, StaticPropId.ELEMENT_SEQ_IDX); if (seqIdx === null) { seqIdx = 0; } - iCtx.$container$.setHostProp(host, ELEMENT_SEQ_IDX, seqIdx + 1); + iCtx.$container$.setHostProp(host, StaticPropId.ELEMENT_SEQ_IDX, seqIdx + 1); while (seq.length <= seqIdx) { seq.push(undefined); } diff --git a/packages/qwik/src/server/qwik-copy.ts b/packages/qwik/src/server/qwik-copy.ts index 5e4fc0a103d..62262f41b62 100644 --- a/packages/qwik/src/server/qwik-copy.ts +++ b/packages/qwik/src/server/qwik-copy.ts @@ -53,6 +53,7 @@ export { STREAM_BLOCK_END_COMMENT, STREAM_BLOCK_START_COMMENT, dangerouslySetInnerHTML, + refAttr, } from '../core/shared/utils/markers'; export { maybeThen } from '../core/shared/utils/promises'; export { @@ -63,3 +64,5 @@ export { export { serializeAttribute } from '../core/shared/utils/styles'; export { VNodeDataChar, VNodeDataSeparator } from '../core/shared/vnode-data-types'; export { getValidManifest } from '../optimizer/src/manifest'; +export { getPropId, getPropName, StaticPropId } from '../core/shared/utils/numeric-prop-key'; +export { startsWithColon } from '../core/shared/utils/numeric-prop-key-flags'; diff --git a/packages/qwik/src/server/qwik-types.ts b/packages/qwik/src/server/qwik-types.ts index 37c16f083a1..2345cb70f04 100644 --- a/packages/qwik/src/server/qwik-types.ts +++ b/packages/qwik/src/server/qwik-types.ts @@ -32,3 +32,4 @@ export type { export type { ResolvedManifest, SymbolMapper } from '../optimizer/src/types'; export type { SymbolToChunkResolver } from '../core/ssr/ssr-types'; export type { NodePropData } from '../core/shared/scheduler'; +export type { NumericPropKey } from '../core/shared/utils/numeric-prop-key'; diff --git a/packages/qwik/src/server/ssr-container.ts b/packages/qwik/src/server/ssr-container.ts index ca450d7e940..9c80a8e0982 100644 --- a/packages/qwik/src/server/ssr-container.ts +++ b/packages/qwik/src/server/ssr-container.ts @@ -50,6 +50,10 @@ import { QError, qError, ChoreType, + getPropId, + getPropName, + refAttr, + StaticPropId, } from './qwik-copy'; import { type ContextId, @@ -60,6 +64,7 @@ import { type JSXChildren, type JSXNodeInternal, type JSXOutput, + type NumericPropKey, type SerializationContext, type SsrAttrKey, type SsrAttrValue, @@ -271,11 +276,11 @@ class SSRContainer extends _SharedContainer implements ISSRContainer { setContext(host: HostElement, context: ContextId, value: T): void { const ssrNode: ISsrNode = host as any; - let ctx: Array = ssrNode.getProp(QCtxAttr); + let ctx: Array = ssrNode.getProp(StaticPropId.CTX); if (!ctx) { - ssrNode.setProp(QCtxAttr, (ctx = [])); + ssrNode.setProp(StaticPropId.CTX, (ctx = [])); } - mapArray_set(ctx, context.id, value, 0); + mapArray_set(ctx, getPropId(context.id), value, 0); // Store the node which will store the context this.addRoot(ssrNode); } @@ -283,9 +288,9 @@ class SSRContainer extends _SharedContainer implements ISSRContainer { resolveContext(host: HostElement, contextId: ContextId): T | undefined { let ssrNode: ISsrNode | null = host as any; while (ssrNode) { - const ctx: Array = ssrNode.getProp(QCtxAttr); + const ctx: Array = ssrNode.getProp(StaticPropId.CTX); if (ctx) { - const value = mapArray_get(ctx, contextId.id, 0) as T; + const value = mapArray_get(ctx, getPropId(contextId.id), 0) as T; if (value) { return value; } @@ -300,12 +305,12 @@ class SSRContainer extends _SharedContainer implements ISSRContainer { return ssrNode.currentComponentNode as ISsrNode | null; } - setHostProp(host: ISsrNode, name: string, value: T): void { + setHostProp(host: ISsrNode, name: NumericPropKey, value: T): void { const ssrNode: ISsrNode = host as any; return ssrNode.setProp(name, value); } - getHostProp(host: ISsrNode, name: string): T | null { + getHostProp(host: ISsrNode, name: NumericPropKey): T | null { const ssrNode: ISsrNode = host as any; return ssrNode.getProp(name); } @@ -549,7 +554,7 @@ class SSRContainer extends _SharedContainer implements ISSRContainer { addUnclaimedProjection(frame: ISsrComponentFrame, name: string, children: JSXChildren): void { // componentFrame, scopedStyleIds, slotName, children - this.unclaimedProjections.push(frame, null, name, children); + this.unclaimedProjections.push(frame, null, getPropId(name), children); } private $processInjectionsFromManifest$(): void { @@ -575,7 +580,7 @@ class SSRContainer extends _SharedContainer implements ISSRContainer { const componentFrame = this.getComponentFrame(0)!; componentFrame.scopedStyleIds.add(styleId); const scopedStyleIds = convertStyleIdsToString(componentFrame.scopedStyleIds); - this.setHostProp(host, QScopedStyle, scopedStyleIds); + this.setHostProp(host, StaticPropId.SCOPED_STYLE, scopedStyleIds); } if (!this.styleIds.has(styleId)) { @@ -714,7 +719,7 @@ class SSRContainer extends _SharedContainer implements ISSRContainer { fragmentAttrs: SsrAttrs ): void { for (let i = 0; i < fragmentAttrs.length; ) { - const key = fragmentAttrs[i++] as string; + const key = getPropName(fragmentAttrs[i++] as unknown as NumericPropKey) as string; let value = fragmentAttrs[i++] as string; // if (key !== DEBUG_TYPE) continue; if (typeof value !== 'string') { @@ -804,7 +809,7 @@ class SSRContainer extends _SharedContainer implements ISSRContainer { while (depth-- > 0) { if (fragmentAttrs) { - for (let i = 0; i < fragmentAttrs.length; i++) { + 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)); @@ -969,15 +974,16 @@ class SSRContainer extends _SharedContainer implements ISSRContainer { ssrComponentFrame = value; // scopedStyleId is always after ssrComponentNode scopedStyleId = unclaimedProjections[idx++] as string; - } else if (typeof value === 'string') { + } else if (typeof value === 'number') { + const slotNameKey = value as NumericPropKey; const children = unclaimedProjections[idx++] as JSXOutput; - if (!ssrComponentFrame?.hasSlot(value)) { + if (!ssrComponentFrame?.hasSlot(slotNameKey)) { /** * Skip the slot if it is already claimed by previous unclaimed projections. We need * to remove the slot from the component frame so that it does not incorrectly resolve * non-existing node later */ - ssrComponentFrame && ssrComponentFrame.componentNode.removeProp(value); + ssrComponentFrame && ssrComponentFrame.componentNode.removeProp(slotNameKey); continue; } this.unclaimedProjectionComponentFrameQueue.shift(); @@ -990,7 +996,7 @@ class SSRContainer extends _SharedContainer implements ISSRContainer { if (lastNode.vnodeData) { lastNode.vnodeData[0] |= VNodeDataFlag.SERIALIZE; } - ssrComponentNode?.setProp(value, lastNode.id); + ssrComponentNode?.setProp(slotNameKey, lastNode.id); await _walkJSX(this, children, { currentStyleScoped: scopedStyleId, parentComponentFrame: null, @@ -1144,7 +1150,7 @@ class SSRContainer extends _SharedContainer implements ISSRContainer { styleScopedId = styleId; } - if (key === 'ref') { + if (key === refAttr) { const lastNode = this.getLastNode(); if (isSignal(value)) { value.value = new DomRef(lastNode); diff --git a/packages/qwik/src/server/ssr-node.ts b/packages/qwik/src/server/ssr-node.ts index 230c7d14b49..a31646cc5bb 100644 --- a/packages/qwik/src/server/ssr-node.ts +++ b/packages/qwik/src/server/ssr-node.ts @@ -6,18 +6,23 @@ import { } from '@qwik.dev/core'; import { isDev } from '@qwik.dev/core/build'; import { - QSlotParent, mapApp_remove, mapArray_get, mapArray_set, mapArray_has, - ELEMENT_SEQ, QSlot, QDefaultSlot, - NON_SERIALIZABLE_MARKER_PREFIX, - QBackRefs, + getPropId, + startsWithColon, + StaticPropId, } from './qwik-copy'; -import type { SsrAttrs, ISsrNode, ISsrComponentFrame, JSXChildren } from './qwik-types'; +import type { + SsrAttrs, + ISsrNode, + ISsrComponentFrame, + JSXChildren, + NumericPropKey, +} from './qwik-types'; import type { CleanupQueue } from './ssr-container'; import type { VNodeData } from './vnode-data'; @@ -51,7 +56,7 @@ export class SsrNode implements ISsrNode { public childrenVNodeData: VNodeData[] | null = null; get [_EFFECT_BACK_REF]() { - return this.getProp(QBackRefs); + return this.getProp(StaticPropId.BACK_REFS); } constructor( @@ -71,37 +76,37 @@ export class SsrNode implements ISsrNode { } } - setProp(name: string, value: any): void { + setProp(key: NumericPropKey, value: any): void { if (this.attrs === _EMPTY_ARRAY) { this.attrs = []; } - if (name.startsWith(NON_SERIALIZABLE_MARKER_PREFIX)) { - mapArray_set(this.locals || (this.locals = []), name, value, 0); + if (startsWithColon(key)) { + mapArray_set(this.locals || (this.locals = []), key, value, 0); } else { - mapArray_set(this.attrs, name, value, 0); + mapArray_set(this.attrs, key, value, 0); } - if (name == ELEMENT_SEQ && value) { + if (key == StaticPropId.ELEMENT_SEQ && value) { // Sequential Arrays contain Tasks. And Tasks contain cleanup functions. // We need to collect these cleanup functions and run them when the rendering is done. this.cleanupQueue.push(value); } } - getProp(name: string): any { - if (name.startsWith(NON_SERIALIZABLE_MARKER_PREFIX)) { - return this.locals ? mapArray_get(this.locals, name, 0) : null; + getProp(key: NumericPropKey): any { + if (startsWithColon(key)) { + return this.locals ? mapArray_get(this.locals, key, 0) : null; } else { - return mapArray_get(this.attrs, name, 0); + return mapArray_get(this.attrs, key, 0); } } - removeProp(name: string): void { - if (name.startsWith(NON_SERIALIZABLE_MARKER_PREFIX)) { + removeProp(key: NumericPropKey): void { + if (startsWithColon(key)) { if (this.locals) { - mapApp_remove(this.locals, name, 0); + mapApp_remove(this.locals, key, 0); } } else { - mapApp_remove(this.attrs, name, 0); + mapApp_remove(this.attrs, key, 0); } } @@ -151,7 +156,7 @@ export class SsrComponentFrame implements ISsrComponentFrame { this.projectionComponentFrame = projectionComponentFrame; if (isJSXNode(children)) { const slotName = this.getSlotName(children); - mapArray_set(this.slots, slotName, children, 0); + mapArray_set(this.slots, getPropId(slotName), children, 0); } else if (Array.isArray(children) && children.length > 0) { const defaultSlot = []; for (let i = 0; i < children.length; i++) { @@ -167,15 +172,15 @@ export class SsrComponentFrame implements ISsrComponentFrame { defaultSlot.push(child); } } - defaultSlot.length > 0 && mapArray_set(this.slots, QDefaultSlot, defaultSlot, 0); + defaultSlot.length > 0 && mapArray_set(this.slots, getPropId(QDefaultSlot), defaultSlot, 0); } else { - mapArray_set(this.slots, QDefaultSlot, children, 0); + mapArray_set(this.slots, getPropId(QDefaultSlot), children, 0); } } private updateSlot(slotName: string, child: JSXChildren) { // we need to check if the slot already has a value - let existingSlots = mapArray_get(this.slots, slotName, 0); + let existingSlots = mapArray_get(this.slots, getPropId(slotName), 0); if (existingSlots === null) { existingSlots = child; } else if (Array.isArray(existingSlots)) { @@ -186,7 +191,7 @@ export class SsrComponentFrame implements ISsrComponentFrame { existingSlots = [existingSlots, child]; } // set the new value - mapArray_set(this.slots, slotName, existingSlots, 0); + mapArray_set(this.slots, getPropId(slotName), existingSlots, 0); } private getSlotName(jsx: JSXNode): string { @@ -196,14 +201,15 @@ export class SsrComponentFrame implements ISsrComponentFrame { return QDefaultSlot; } - hasSlot(slotName: string): boolean { - return mapArray_has(this.slots, slotName, 0); + hasSlot(slotNameKey: NumericPropKey): boolean { + return mapArray_has(this.slots, slotNameKey, 0); } consumeChildrenForSlot(projectionNode: ISsrNode, slotName: string): JSXChildren | null { - const children = mapApp_remove(this.slots, slotName, 0); - this.componentNode.setProp(slotName, projectionNode.id); - projectionNode.setProp(QSlotParent, this.componentNode.id); + const slotNamePropId = getPropId(slotName); + const children = mapApp_remove(this.slots, slotNamePropId, 0); + this.componentNode.setProp(slotNamePropId, projectionNode.id); + projectionNode.setProp(StaticPropId.SLOT_PARENT, this.componentNode.id); return children; } diff --git a/packages/qwik/src/server/vnode-data.unit.tsx b/packages/qwik/src/server/vnode-data.unit.tsx index 2b4af925328..9a7ada5263f 100644 --- a/packages/qwik/src/server/vnode-data.unit.tsx +++ b/packages/qwik/src/server/vnode-data.unit.tsx @@ -5,10 +5,10 @@ import { useSignal } from '../core/use/use-signal'; import { ssrRenderToDom } from '../testing/rendering.unit-util'; import { encodeAsAlphanumeric } from './vnode-data'; import { vnode_getProp, vnode_locate } from '../core/client/vnode'; -import { ELEMENT_PROPS, OnRenderProp } from '../core/shared/utils/markers'; import { type QRLInternal } from '../core/shared/qrl/qrl-class'; import type { DomContainer } from '../core/client/dom-container'; import { createContextId, useContext, useContextProvider } from '@qwik.dev/core'; +import { StaticPropId } from '../core/shared/utils/numeric-prop-key'; const debug = false; @@ -225,13 +225,17 @@ function expectVNodeSymbol(container: DomContainer, vNodeId: string, cmpSymbol: const vnode = vnode_locate(container.rootVNode, vNodeId); expect( - vnode_getProp(vnode, OnRenderProp, container.$getObjectById$)?.$hash$ + vnode_getProp(vnode, StaticPropId.ON_RENDER, container.$getObjectById$)?.$hash$ ).toEqual(cmpSymbol); } function expectVNodeProps(container: DomContainer, vNodeId: string, props: any) { const vnode = vnode_locate(container.rootVNode, vNodeId); - const elementProps = vnode_getProp(vnode, ELEMENT_PROPS, container.$getObjectById$) as any; + const elementProps = vnode_getProp( + vnode, + StaticPropId.ELEMENT_PROPS, + container.$getObjectById$ + ) as any; // TODO(hack): elementProps object does not contain fields because it is a PropsProxy, // so we need to manually read the property value and create a new object diff --git a/packages/qwik/src/testing/rendering.unit-util.tsx b/packages/qwik/src/testing/rendering.unit-util.tsx index 520af41e3dc..a7587b39fc9 100644 --- a/packages/qwik/src/testing/rendering.unit-util.tsx +++ b/packages/qwik/src/testing/rendering.unit-util.tsx @@ -38,8 +38,6 @@ import { inlinedQrl } from '../core/shared/qrl/qrl'; import { ChoreType } from '../core/shared/util-chore-type'; import { dumpState } from '../core/shared/shared-serialization'; import { - ELEMENT_PROPS, - OnRenderProp, QContainerSelector, QFuncsPrefix, QInstanceAttr, @@ -53,7 +51,7 @@ import { createDocument } from './document'; import { getTestPlatform } from './platform'; import './vdom-diff.unit-util'; import { VNodeProps, VirtualVNodeProps, type VNode, type VirtualVNode } from '../core/client/types'; -import { DEBUG_TYPE, VirtualType } from '../server/qwik-copy'; +import { DEBUG_TYPE, StaticPropId, VirtualType, getPropId } from '../server/qwik-copy'; /** @public */ export async function domRender( @@ -175,7 +173,7 @@ export async function ssrRenderToDom( // Create a fragment const fragment = vnode_newVirtual(); - vnode_setProp(fragment, DEBUG_TYPE, VirtualType.Fragment); + vnode_setProp(fragment, getPropId(DEBUG_TYPE), VirtualType.Fragment); const childrenToMove = []; @@ -187,8 +185,8 @@ export async function ssrRenderToDom( if ( vnode_isElementVNode(child) && ((vnode_getElementName(child) === 'script' && - (vnode_getAttr(child, 'type') === 'qwik/state' || - vnode_getAttr(child, 'id') === 'qwikloader')) || + (vnode_getAttr(child, getPropId('type')) === 'qwik/state' || + vnode_getAttr(child, getPropId('id')) === 'qwikloader')) || vnode_getElementName(child) === 'q:template') ) { insertBefore = child; @@ -276,8 +274,11 @@ export async function rerenderComponent(element: HTMLElement, flush?: boolean) { const container = _getDomContainer(element); const vElement = vnode_locate(container.rootVNode, element); const host = getHostVNode(vElement) as HostElement; - const qrl = container.getHostProp>>(host, OnRenderProp)!; - const props = container.getHostProp(host, ELEMENT_PROPS); + const qrl = container.getHostProp>>( + host, + StaticPropId.ON_RENDER + )!; + const props = container.getHostProp(host, StaticPropId.ELEMENT_PROPS); container.$scheduler$(ChoreType.COMPONENT, host, qrl, props); if (flush) { // Note that this can deadlock @@ -287,7 +288,7 @@ export async function rerenderComponent(element: HTMLElement, flush?: boolean) { function getHostVNode(vElement: _VNode | null) { while (vElement != null) { - if (vnode_getAttr(vElement, OnRenderProp) != null) { + if (vnode_getAttr(vElement, StaticPropId.ON_RENDER) != null) { return vElement as _VirtualVNode; } vElement = vnode_getParent(vElement); diff --git a/packages/qwik/src/testing/vdom-diff.unit-util.ts b/packages/qwik/src/testing/vdom-diff.unit-util.ts index 442a30e54a0..bd1d9801c36 100644 --- a/packages/qwik/src/testing/vdom-diff.unit-util.ts +++ b/packages/qwik/src/testing/vdom-diff.unit-util.ts @@ -49,6 +49,12 @@ import { QBackRefs, Q_PROPS_SEPARATOR, } from '../core/shared/utils/markers'; +import { + StaticPropId, + getPropId, + getPropName, + type NumericPropKey, +} from '../core/shared/utils/numeric-prop-key'; expect.extend({ toMatchVDOM( @@ -101,7 +107,7 @@ function isSsrRenderer(container: _ContainerElement) { } function isSkippableNode(node: JSXNodeInternal): boolean { - return node.type === Fragment && !node.constProps?.['ssr-required']; + return node.type === Fragment && !node.constProps?.[getPropId('ssr-required')]; } function diffJsxVNode( @@ -153,8 +159,10 @@ function diffJsxVNode( ); } const allProps: string[] = []; - expected.varProps && propsAdd(allProps, Object.keys(expected.varProps)); - expected.constProps && propsAdd(allProps, Object.keys(expected.constProps)); + expected.varProps && + propsAdd(allProps, Object.keys(expected.varProps).map(Number) as NumericPropKey[]); + expected.constProps && + propsAdd(allProps, Object.keys(expected.constProps).map(Number) as NumericPropKey[]); const receivedElement = vnode_isElementVNode(received) ? (vnode_getNode(received) as Element) : null; @@ -162,7 +170,8 @@ function diffJsxVNode( allProps, vnode_isElementVNode(received) ? vnode_getAttrKeys(received) - .filter((key) => !ignoredAttributes.includes(key)) + .filter((key) => !ignoredAttributes.includes(getPropName(key))) + .map((key) => key) .sort() : [] ); @@ -178,8 +187,8 @@ function diffJsxVNode( // we need this, because Domino lowercases all attributes for `element.attributes` const propLowerCased = prop.toLowerCase(); let receivedValue = - vnode_getAttr(received, prop) || - vnode_getAttr(received, propLowerCased) || + vnode_getAttr(received, getPropId(prop)) || + vnode_getAttr(received, getPropId(propLowerCased)) || receivedElement?.getAttribute(prop) || receivedElement?.getAttribute(propLowerCased); let expectedValue = @@ -377,11 +386,12 @@ function tagToString(tag: any): string { function shouldSkip(vNode: _VNode | null) { if (vNode && vnode_isElementVNode(vNode)) { const tag = vnode_getElementName(vNode); + const typeId = getPropId('type'); if ( tag === 'script' && - (vnode_getAttr(vNode, 'type') === 'qwik/vnode' || - vnode_getAttr(vNode, 'type') === 'x-qwik/vnode' || - vnode_getAttr(vNode, 'type') === 'qwik/state') + (vnode_getAttr(vNode, typeId) === 'qwik/vnode' || + vnode_getAttr(vNode, typeId) === 'x-qwik/vnode' || + vnode_getAttr(vNode, typeId) === 'qwik/state') ) { return true; } @@ -444,11 +454,11 @@ export function vnode_fromJSX(jsx: JSXOutput) { const props = jsx.varProps; for (const key in props) { if (Object.prototype.hasOwnProperty.call(props, key)) { - vnode_setAttr(journal, child, key, String(props[key])); + vnode_setAttr(journal, child, Number(key) as NumericPropKey, String(props[key as any])); } } if (jsx.key != null) { - vnode_setAttr(journal, child, 'q:key', String(jsx.key)); + vnode_setAttr(journal, child, StaticPropId.ELEMENT_KEY, String(jsx.key)); } vParent = child; } else { @@ -471,29 +481,30 @@ export function vnode_fromJSX(jsx: JSXOutput) { return { vParent, vNode: vnode_getFirstChild(vParent), document: doc }; } function constPropsFromElement(element: Element) { - const props: string[] = []; + const props: NumericPropKey[] = []; for (let i = 0; i < element.attributes.length; i++) { const attr = element.attributes[i]; if (!ignoredAttributes.includes(attr.name)) { - props.push(attr.name); + props.push(getPropId(attr.name)); } } props.sort(); return props; } -function propsAdd(existing: string[], incoming: string[]) { +function propsAdd(existing: string[], incoming: NumericPropKey[]) { for (const prop of incoming) { - if (prop !== 'children') { + const propName = getPropName(prop as NumericPropKey); + if (propName !== 'children') { let found = false; for (let i = 0; i < existing.length; i++) { - if (existing[i].toLowerCase() === prop.toLowerCase()) { + if (existing[i].toLowerCase() === propName.toLowerCase()) { found = true; break; } } if (!found) { - existing.push(prop); + existing.push(propName); } } } @@ -532,7 +543,8 @@ async function diffNode(received: HTMLElement, expected: JSXOutput): Promise { + entries.forEach(([numericExpectedKey, expectedValue]) => { + const expectedKey = getPropName(numericExpectedKey as unknown as NumericPropKey); // we need this, because Domino lowercases all attributes for `element.attributes` const expectedKeyLowerCased = expectedKey.toLowerCase(); let receivedValue = diff --git a/scripts/submodule-optimizer.ts b/scripts/submodule-optimizer.ts index 74c3df85859..46b8e4a4175 100644 --- a/scripts/submodule-optimizer.ts +++ b/scripts/submodule-optimizer.ts @@ -51,7 +51,7 @@ export async function submoduleOptimizer(config: BuildConfig) { // throws an error if files from src/core are loaded, except for some allowed imports name: 'forbid-core', setup(build) { - build.onLoad({ filter: /src\/core\// }, (args) => { + build.onLoad({ filter: /src(\/|\\)core(\/|\\)/ }, (args) => { if (args.path.includes('util') || args.path.includes('shared')) { return null; } diff --git a/scripts/submodule-server.ts b/scripts/submodule-server.ts index f7503660ada..19bce71406a 100644 --- a/scripts/submodule-server.ts +++ b/scripts/submodule-server.ts @@ -48,7 +48,7 @@ export async function submoduleServer(config: BuildConfig) { // throws an error if files from src/core are loaded, except for some allowed imports name: 'forbid-core', setup(build) { - build.onLoad({ filter: /src\/core\// }, (args) => { + build.onLoad({ filter: /src(\/|\\)core(\/|\\)/ }, (args) => { if (args.path.includes('util') || args.path.includes('shared')) { return null; } diff --git a/steps.md b/steps.md new file mode 100644 index 00000000000..9f303fae20f --- /dev/null +++ b/steps.md @@ -0,0 +1,4 @@ +[ ] - convert to numeric object props +[ ] - implement flags +[ ] - convert to numeric array props +[ ] - check normalizing event names to all lovercased instead of scope:onEvent$