diff --git a/packages/qwik/src/core/client/vnode-diff.ts b/packages/qwik/src/core/client/vnode-diff.ts index 38bdbd76224..8d720289625 100644 --- a/packages/qwik/src/core/client/vnode-diff.ts +++ b/packages/qwik/src/core/client/vnode-diff.ts @@ -99,10 +99,11 @@ import { escapeHTML } from '../shared/utils/character-escaping'; import { clearAllEffects } from '../reactive-primitives/cleanup'; import { serializeAttribute } from '../shared/utils/styles'; import { QError, qError } from '../shared/error/error'; -import { getFileLocationFromJsx } from '../shared/utils/jsx-filename'; +import { appendQwikInspectorAttribute, getFileLocationFromJsx } from '../shared/utils/jsx-filename'; import { EffectProperty } from '../reactive-primitives/types'; import { SubscriptionData } from '../reactive-primitives/subscription-data'; import { WrappedSignalImpl } from '../reactive-primitives/impl/wrapped-signal-impl'; +import { qInspector } from '../shared/utils/qdev'; export const vnode_diff = ( container: ClientContainer, @@ -712,6 +713,9 @@ export const vnode_diff = ( const jsxKey: string | null = jsx.key; let needsQDispatchEventPatch = false; const currentFile = getFileLocationFromJsx(jsx.dev); + if (isDev && currentFile && qInspector) { + appendQwikInspectorAttribute(jsx, currentFile); + } if (!isSameElementName || jsxKey !== getKey(vCurrent)) { // So we have a key and it does not match the current node. // We need to do a forward search to find it. diff --git a/packages/qwik/src/core/client/vnode.ts b/packages/qwik/src/core/client/vnode.ts index e9bca1ac00a..49d838ac33b 100644 --- a/packages/qwik/src/core/client/vnode.ts +++ b/packages/qwik/src/core/client/vnode.ts @@ -150,6 +150,7 @@ import { QSlotParent, QStyle, QStylesAllSelector, + qwikInspectorAttr, } from '../shared/utils/markers'; import { isHtmlElement } from '../shared/utils/types'; import { VNodeDataChar } from '../shared/vnode-data-types'; @@ -1829,9 +1830,49 @@ function materializeFromVNodeData( if (isNumber(peek())) { // Element counts get encoded as numbers. while (!isElement(child)) { + const previousChild = child; child = fastNextSibling(child); if (!child) { - throw qError(QError.materializeVNodeDataError, [vData, peek(), nextToConsumeIdx]); + let childDescription: string | null = null; + let childDescriptionFile: string | null = null; + if (isDev) { + const getChildDescription = () => { + if (previousChild && isElement(previousChild)) { + return previousChild.outerHTML; + } else if (previousChild && isText(previousChild)) { + return previousChild.nodeValue; + } else { + return previousChild?.nodeName || null; + } + }; + + const getChildDescriptionFile = () => { + let previousChildWithFileLocation = previousChild; + while (!childDescriptionFile && previousChildWithFileLocation) { + if ( + isElement(previousChildWithFileLocation) && + previousChildWithFileLocation.hasAttribute(qwikInspectorAttr) + ) { + return previousChildWithFileLocation.getAttribute(qwikInspectorAttr); + } + previousChildWithFileLocation = + previousChildWithFileLocation.parentNode as Node | null; + } + return null; + }; + + childDescription = getChildDescription(); + childDescriptionFile = getChildDescriptionFile(); + } else { + childDescription = previousChild?.nodeName || null; + } + throw qError(QError.materializeVNodeDataError, [ + childDescription, + childDescriptionFile, + vData, + peek(), + nextToConsumeIdx, + ]); } } // We pretend that style element's don't exist as they can get moved out. diff --git a/packages/qwik/src/core/shared/error/error.ts b/packages/qwik/src/core/shared/error/error.ts index 90dac9d21ff..60b4a6e166f 100644 --- a/packages/qwik/src/core/shared/error/error.ts +++ b/packages/qwik/src/core/shared/error/error.ts @@ -49,7 +49,7 @@ export const codeToText = (code: number, ...parts: any[]): string => { 'Unable to find q:container', // 41 "Element must have 'q:container' attribute.", // 42 'Unknown vnode type {{0}}.', // 43 - 'Materialize error: missing element: {{0}} {{1}} {{2}}', // 44 + 'Resuming error: Expected text or element node following:\n{{0}}\nat location {{1}}\n metadata: {{2}},\n value: {{3}},\n next id: {{4}}', // 44 'Cannot coerce a Signal, use `.value` instead', // 45 'useComputedSignal$ QRL {{0}} {{1}} returned a Promise', // 46 'ComputedSignal is read-only', // 47 diff --git a/packages/qwik/src/core/shared/utils/jsx-filename.ts b/packages/qwik/src/core/shared/utils/jsx-filename.ts index 6f4e8a7763b..4f2dba5dca0 100644 --- a/packages/qwik/src/core/shared/utils/jsx-filename.ts +++ b/packages/qwik/src/core/shared/utils/jsx-filename.ts @@ -1,4 +1,14 @@ -import type { DevJSX } from '../jsx/types/jsx-node'; +import type { DevJSX, JSXNodeInternal } from '../jsx/types/jsx-node'; +import { qwikInspectorAttr } from './markers'; + +export function appendQwikInspectorAttribute( + jsx: JSXNodeInternal, + qwikInspectorAttrValue: string | null +) { + if (qwikInspectorAttrValue && (!jsx.constProps || !(qwikInspectorAttr in jsx.constProps))) { + (jsx.constProps ||= {})[qwikInspectorAttr] = qwikInspectorAttrValue; + } +} export function getFileLocationFromJsx(jsxDev?: DevJSX): string | null { if (!jsxDev) { diff --git a/packages/qwik/src/core/ssr/ssr-render-jsx.ts b/packages/qwik/src/core/ssr/ssr-render-jsx.ts index 51829a41093..29548886406 100644 --- a/packages/qwik/src/core/ssr/ssr-render-jsx.ts +++ b/packages/qwik/src/core/ssr/ssr-render-jsx.ts @@ -18,7 +18,7 @@ import { jsxEventToHtmlAttribute, } from '../shared/utils/event-names'; import { EMPTY_ARRAY } from '../shared/utils/flyweight'; -import { getFileLocationFromJsx } from '../shared/utils/jsx-filename'; +import { appendQwikInspectorAttribute, getFileLocationFromJsx } from '../shared/utils/jsx-filename'; import { ELEMENT_KEY, FLUSH_COMMENT, @@ -26,7 +26,6 @@ import { QScopedStyle, QSlot, QSlotParent, - qwikInspectorAttr, } from '../shared/utils/markers'; import { isPromise } from '../shared/utils/promises'; import { qInspector } from '../shared/utils/qdev'; @@ -510,12 +509,6 @@ function getSlotName(host: ISsrNode, jsx: JSXNodeInternal, ssr: SSRContainer): s return directGetPropsProxyProp(jsx, 'name') || QDefaultSlot; } -function appendQwikInspectorAttribute(jsx: JSXNodeInternal, qwikInspectorAttrValue: string | null) { - if (qwikInspectorAttrValue && (!jsx.constProps || !(qwikInspectorAttr in jsx.constProps))) { - (jsx.constProps ||= {})[qwikInspectorAttr] = 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;