From aa745da3ae59d5d42992f85575b32d7526f53531 Mon Sep 17 00:00:00 2001 From: Wout Mertens Date: Thu, 15 May 2025 18:07:39 +0200 Subject: [PATCH 1/2] feat: qidlevisible event / 'idle-visible' vistask strategy --- .changeset/slick-squids-draw.md | 6 ++++ packages/docs/src/routes/api/qwik/api.json | 18 ++++++++-- packages/docs/src/routes/api/qwik/index.mdx | 15 ++++++-- .../src/runtime/src/link-component.tsx | 17 +++++---- packages/qwik/src/core/index.ts | 3 +- packages/qwik/src/core/qwik.core.api.md | 5 ++- .../render/jsx/types/jsx-qwik-attributes.ts | 3 ++ .../core/render/jsx/types/jsx-qwik-events.ts | 5 +++ packages/qwik/src/core/use/use-task.ts | 8 ++++- packages/qwik/src/qwikloader.ts | 35 ++++++++++++------- packages/qwik/src/qwikloader.unit.ts | 6 ++-- .../effect-client/effect-client.tsx | 11 ++++++ starters/e2e/e2e.effect-client.spec.ts | 3 +- 13 files changed, 104 insertions(+), 31 deletions(-) create mode 100644 .changeset/slick-squids-draw.md diff --git a/.changeset/slick-squids-draw.md b/.changeset/slick-squids-draw.md new file mode 100644 index 00000000000..344511606aa --- /dev/null +++ b/.changeset/slick-squids-draw.md @@ -0,0 +1,6 @@ +--- +'@builder.io/qwik-city': minor +'@builder.io/qwik': minor +--- + +FEAT: useVisibleTask$ now accepts the strategy "idle-visible", which waits until document idle before running visible tasks. This improves the LCP Web Vitals metric. It typically delays visible tasks for less than a second at document load, and is our recommended setting. diff --git a/packages/docs/src/routes/api/qwik/api.json b/packages/docs/src/routes/api/qwik/api.json index ad623491861..eeb1702e398 100644 --- a/packages/docs/src/routes/api/qwik/api.json +++ b/packages/docs/src/routes/api/qwik/api.json @@ -1774,7 +1774,7 @@ } ], "kind": "Function", - "content": "> This API is provided as an alpha preview for developers and may change based on feedback that we receive. Do not use this API in a production environment.\n> \n\n> Warning: This API is now obsolete.\n> \n> This is no longer needed as the preloading happens automatically in qrl-class.ts. Leave this in your app for a while so it uninstalls existing service workers, but don't use it for new projects.\n> \n\n\n```typescript\nPrefetchServiceWorker: (opts: {\n base?: string;\n scope?: string;\n path?: string;\n verbose?: boolean;\n fetchBundleGraph?: boolean;\n nonce?: string;\n}) => JSXNode<'script'>\n```\n\n\n\n\n
\n\nParameter\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
\n\nopts\n\n\n\n\n{ base?: string; scope?: string; path?: string; verbose?: boolean; fetchBundleGraph?: boolean; nonce?: string; }\n\n\n\n\n\n
\n**Returns:**\n\n[JSXNode](#jsxnode)<'script'>", + "content": "> This API is provided as an alpha preview for developers and may change based on feedback that we receive. Do not use this API in a production environment.\n> \n\n> Warning: This API is now obsolete.\n> \n> This is no longer needed as the preloading happens automatically in qrl-class.ts. Leave this in your app for a while so it uninstalls existing service workers, but don't use it for new projects.\n> \n\n\n```typescript\nPrefetchServiceWorker: (opts: {\n base?: string;\n scope?: string;\n path?: string;\n verbose?: boolean;\n fetchBundleGraph?: boolean;\n nonce?: string;\n}) => JSXNode<'script'>\n```\n\n\n\n\n
\n\nParameter\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
\n\nopts\n\n\n\n\n{ base?: string; scope?: string; path?: string; verbose?: boolean; fetchBundleGraph?: boolean; nonce?: string; }\n\n\n\n\n\n
\n**Returns:**\n\nJSXNode<'script'>", "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/components/prefetch.ts", "mdFile": "qwik.prefetchserviceworker.md" }, @@ -2058,6 +2058,20 @@ "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/render/jsx/types/jsx-qwik-events.ts", "mdFile": "qwik.qwikidleevent.md" }, + { + "name": "QwikIdlevisibleEvent", + "id": "qwikidlevisibleevent", + "hierarchy": [ + { + "name": "QwikIdlevisibleEvent", + "id": "qwikidlevisibleevent" + } + ], + "kind": "TypeAlias", + "content": "Emitted by qwik-loader when an element becomes visible, after document idle. Used by `useVisibleTask$`\n\n\n```typescript\nexport type QwikIdlevisibleEvent = CustomEvent;\n```", + "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/render/jsx/types/jsx-qwik-events.ts", + "mdFile": "qwik.qwikidlevisibleevent.md" + }, { "name": "QwikInitEvent", "id": "qwikinitevent", @@ -3454,7 +3468,7 @@ } ], "kind": "TypeAlias", - "content": "```typescript\nexport type VisibleTaskStrategy = 'intersection-observer' | 'document-ready' | 'document-idle';\n```", + "content": "```typescript\nexport type VisibleTaskStrategy = 'intersection-observer' | 'document-ready' | 'document-idle' | 'idle-visible';\n```", "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-task.ts", "mdFile": "qwik.visibletaskstrategy.md" }, diff --git a/packages/docs/src/routes/api/qwik/index.mdx b/packages/docs/src/routes/api/qwik/index.mdx index 8414e2e4b8b..34157761a35 100644 --- a/packages/docs/src/routes/api/qwik/index.mdx +++ b/packages/docs/src/routes/api/qwik/index.mdx @@ -3651,7 +3651,7 @@ opts **Returns:** -[JSXNode](#jsxnode)<'script'> +JSXNode<'script'> [Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/components/prefetch.ts) @@ -4186,6 +4186,16 @@ export type QwikIdleEvent = CustomEvent<{}>; [Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/render/jsx/types/jsx-qwik-events.ts) +## QwikIdlevisibleEvent + +Emitted by qwik-loader when an element becomes visible, after document idle. Used by `useVisibleTask$` + +```typescript +export type QwikIdlevisibleEvent = CustomEvent; +``` + +[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/render/jsx/types/jsx-qwik-events.ts) + ## QwikInitEvent Emitted by qwik-loader on document when the document first becomes interactive @@ -11565,7 +11575,8 @@ export interface VideoHTMLAttributes extends Attrs<'video', T export type VisibleTaskStrategy = | "intersection-observer" | "document-ready" - | "document-idle"; + | "document-idle" + | "idle-visible"; ``` [Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-task.ts) diff --git a/packages/qwik-city/src/runtime/src/link-component.tsx b/packages/qwik-city/src/runtime/src/link-component.tsx index 765463fc3fb..818ae9d5f69 100644 --- a/packages/qwik-city/src/runtime/src/link-component.tsx +++ b/packages/qwik-city/src/runtime/src/link-component.tsx @@ -83,13 +83,16 @@ export const Link = component$((props) => { }) : undefined; - useVisibleTask$(({ track }) => { - track(() => loc.url.pathname); - // Don't prefetch on visible in dev mode - if (!isDev && anchorRef.value) { - handlePrefetch?.(undefined, anchorRef.value!); - } - }); + useVisibleTask$( + ({ track }) => { + track(() => loc.url.pathname); + // Don't prefetch on visible in dev mode + if (!isDev && anchorRef.value) { + handlePrefetch?.(undefined, anchorRef.value!); + } + }, + { strategy: 'idle-visible' } + ); return ( ; +// @public +export type QwikIdlevisibleEvent = CustomEvent; + // @public export type QwikInitEvent = CustomEvent<{}>; @@ -1756,7 +1759,7 @@ export interface VideoHTMLAttributes extends Attrs<'video', T } // @public (undocumented) -export type VisibleTaskStrategy = 'intersection-observer' | 'document-ready' | 'document-idle'; +export type VisibleTaskStrategy = 'intersection-observer' | 'document-ready' | 'document-idle' | 'idle-visible'; // @internal (undocumented) export const _waitUntilRendered: (elm: Element) => Promise; diff --git a/packages/qwik/src/core/render/jsx/types/jsx-qwik-attributes.ts b/packages/qwik/src/core/render/jsx/types/jsx-qwik-attributes.ts index b713873224c..a8906275151 100644 --- a/packages/qwik/src/core/render/jsx/types/jsx-qwik-attributes.ts +++ b/packages/qwik/src/core/render/jsx/types/jsx-qwik-attributes.ts @@ -3,6 +3,7 @@ import type { Signal } from '../../../state/signal'; import type { JSXNode } from './jsx-node'; import type { QwikIdleEvent, + QwikIdlevisibleEvent, QwikInitEvent, QwikSymbolEvent, QwikViewTransitionEvent, @@ -65,6 +66,7 @@ type PascalCaseNames = | 'PointerOver' | 'PointerUp' | 'QIdle' + | 'QIdlevisible' | 'QInit' | 'QSymbol' | 'QVisible' @@ -111,6 +113,7 @@ type AllEventMapRaw = HTMLElementEventMap & DocumentEventMap & WindowEventHandlersEventMap & { qidle: QwikIdleEvent; + qidlevisible: QwikIdlevisibleEvent; qinit: QwikInitEvent; qsymbol: QwikSymbolEvent; qvisible: QwikVisibleEvent; diff --git a/packages/qwik/src/core/render/jsx/types/jsx-qwik-events.ts b/packages/qwik/src/core/render/jsx/types/jsx-qwik-events.ts index a4e020ae00e..8a090a82519 100644 --- a/packages/qwik/src/core/render/jsx/types/jsx-qwik-events.ts +++ b/packages/qwik/src/core/render/jsx/types/jsx-qwik-events.ts @@ -2,6 +2,11 @@ import type { AllEventKeys } from './jsx-qwik-attributes'; /** Emitted by qwik-loader when an element becomes visible. Used by `useVisibleTask$` @public */ export type QwikVisibleEvent = CustomEvent; +/** + * Emitted by qwik-loader when an element becomes visible, after document idle. Used by + * `useVisibleTask$` @public + */ +export type QwikIdlevisibleEvent = CustomEvent; /** Emitted by qwik-loader when a module was lazily loaded @public */ export type QwikSymbolEvent = CustomEvent<{ symbol: string; element: Element; reqTime: number }>; /** Emitted by qwik-loader on document when the document first becomes interactive @public */ diff --git a/packages/qwik/src/core/use/use-task.ts b/packages/qwik/src/core/use/use-task.ts index 85eca6b64c9..9bd54958110 100644 --- a/packages/qwik/src/core/use/use-task.ts +++ b/packages/qwik/src/core/use/use-task.ts @@ -190,7 +190,11 @@ export interface DescriptorBase { export type EagernessOptions = 'visible' | 'load' | 'idle'; /** @public */ -export type VisibleTaskStrategy = 'intersection-observer' | 'document-ready' | 'document-idle'; +export type VisibleTaskStrategy = + | 'intersection-observer' + | 'document-ready' + | 'document-idle' + | 'idle-visible'; /** @public */ export interface OnVisibleTaskOptions { @@ -809,6 +813,8 @@ const useRunTask = ( ) => { if (eagerness === 'visible' || eagerness === 'intersection-observer') { useOn('qvisible', getTaskHandlerQrl(task)); + } else if (eagerness === 'idle-visible') { + useOn('qidlevisible', getTaskHandlerQrl(task)); } else if (eagerness === 'load' || eagerness === 'document-ready') { useOnDocument('qinit', getTaskHandlerQrl(task)); } else if (eagerness === 'idle' || eagerness === 'document-idle') { diff --git a/packages/qwik/src/qwikloader.ts b/packages/qwik/src/qwikloader.ts index c4886f6950c..f868691d651 100644 --- a/packages/qwik/src/qwikloader.ts +++ b/packages/qwik/src/qwikloader.ts @@ -203,6 +203,21 @@ type qWindow = Window & { broadcast('-window', ev, camelToKebab(ev.type)); }; + const observeVisible = (type: string) => { + const results = querySelectorAll(`[on\\:${type}]`); + if (results.length) { + const observer = new IntersectionObserver((entries) => { + for (const entry of entries) { + if (entry.isIntersecting) { + observer.unobserve(entry.target); + dispatch(entry.target, '', createEvent(type, entry)); + } + } + }); + results.forEach((el) => observer.observe(el)); + } + }; + const processReadyStateChange = () => { const readyState = doc.readyState; if (!hasInitialized && (readyState == 'interactive' || readyState == 'complete')) { @@ -211,21 +226,15 @@ type qWindow = Window & { hasInitialized = 1; emitEvent('qinit'); + const riC = win.requestIdleCallback ?? win.setTimeout; - riC.bind(win)(() => emitEvent('qidle')); + riC.bind(win)(() => { + emitEvent('qidle'); - if (events.has('qvisible')) { - const results = querySelectorAll('[on\\:qvisible]'); - const observer = new IntersectionObserver((entries) => { - for (const entry of entries) { - if (entry.isIntersecting) { - observer.unobserve(entry.target); - dispatch(entry.target, '', createEvent('qvisible', entry)); - } - } - }); - results.forEach((el) => observer.observe(el)); - } + observeVisible('qidlevisible'); + }); + + observeVisible('qvisible'); } }; diff --git a/packages/qwik/src/qwikloader.unit.ts b/packages/qwik/src/qwikloader.unit.ts index 99051350611..096fbf8aef7 100644 --- a/packages/qwik/src/qwikloader.unit.ts +++ b/packages/qwik/src/qwikloader.unit.ts @@ -21,9 +21,9 @@ test('qwikloader script', () => { * dereference objects etc, but that actually results in worse compression */ const compressed = compress(Buffer.from(qwikLoader), { mode: 1, quality: 11 }); - expect(compressed.length).toBe(1428); - expect(qwikLoader.length).toBe(3126); + expect(compressed.length).toBe(1444); + expect(qwikLoader.length).toBe(3148); expect(qwikLoader).toMatchInlineSnapshot( - `"(()=>{const t=document,e=window,n=new Set,o=new Set([t]);let r;const s=(t,e)=>Array.from(t.querySelectorAll(e)),a=t=>{const e=[];return o.forEach((n=>e.push(...s(n,t)))),e},i=t=>{w(t),s(t,"[q\\\\:shadowroot]").forEach((t=>{const e=t.shadowRoot;e&&i(e)}))},c=t=>t&&"function"==typeof t.then,l=(t,e,n=e.type)=>{a("[on"+t+"\\\\:"+n+"]").forEach((o=>b(o,t,e,n)))},f=e=>{if(void 0===e._qwikjson_){let n=(e===t.documentElement?t.body:e).lastElementChild;for(;n;){if("SCRIPT"===n.tagName&&"qwik/json"===n.getAttribute("type")){e._qwikjson_=JSON.parse(n.textContent.replace(/\\\\x3C(\\/?script)/gi,"<$1"));break}n=n.previousElementSibling}}},p=(t,e)=>new CustomEvent(t,{detail:e}),b=async(e,n,o,r=o.type)=>{const s="on"+n+":"+r;e.hasAttribute("preventdefault:"+r)&&o.preventDefault(),e.hasAttribute("stoppropagation:"+r)&&o.stopPropagation();const a=e._qc_,i=a&&a.li.filter((t=>t[0]===s));if(i&&i.length>0){for(const t of i){const n=t[1].getFn([e,o],(()=>e.isConnected))(o,e),r=o.cancelBubble;c(n)&&await n,r&&o.stopPropagation()}return}const l=e.getAttribute(s);if(l){const n=e.closest("[q\\\\:container]"),r=n.getAttribute("q:base"),s=n.getAttribute("q:version")||"unknown",a=n.getAttribute("q:manifest-hash")||"dev",i=new URL(r,t.baseURI);for(const p of l.split("\\n")){const l=new URL(p,i),b=l.href,h=l.hash.replace(/^#?([^?[|]*).*$/,"$1")||"default",q=performance.now();let _,d,y;const w=p.startsWith("#"),g={qBase:r,qManifest:a,qVersion:s,href:b,symbol:h,element:e,reqTime:q};if(w){const e=n.getAttribute("q:instance");_=(t["qFuncs_"+e]||[])[Number.parseInt(h)],_||(d="sync",y=Error("sym:"+h))}else{const t=l.href.split("#")[0];try{const e=import(t);f(n),_=(await e)[h],_||(d="no-symbol",y=Error(\`\${h} not in \${t}\`))}catch(t){d||(d="async"),y=t}}if(!_){u("qerror",{importError:d,error:y,...g}),console.error(y);break}const m=t.__q_context__;if(e.isConnected)try{t.__q_context__=[e,o,l],w||u("qsymbol",{...g});const n=_(o,e);c(n)&&await n}catch(t){u("qerror",{error:t,...g})}finally{t.__q_context__=m}}}},u=(e,n)=>{t.dispatchEvent(p(e,n))},h=t=>t.replace(/([A-Z])/g,(t=>"-"+t.toLowerCase())),q=async t=>{let e=h(t.type),n=t.target;for(l("-document",t,e);n&&n.getAttribute;){const o=b(n,"",t,e);let r=t.cancelBubble;c(o)&&await o,r=r||t.cancelBubble||n.hasAttribute("stoppropagation:"+t.type),n=t.bubbles&&!0!==r?n.parentElement:null}},_=t=>{l("-window",t,h(t.type))},d=()=>{var s;const c=t.readyState;if(!r&&("interactive"==c||"complete"==c)&&(o.forEach(i),r=1,u("qinit"),(null!=(s=e.requestIdleCallback)?s:e.setTimeout).bind(e)((()=>u("qidle"))),n.has("qvisible"))){const t=a("[on\\\\:qvisible]"),e=new IntersectionObserver((t=>{for(const n of t)n.isIntersecting&&(e.unobserve(n.target),b(n.target,"",p("qvisible",n)))}));t.forEach((t=>e.observe(t)))}},y=(t,e,n,o=!1)=>t.addEventListener(e,n,{capture:o,passive:!1}),w=(...t)=>{for(const r of t)"string"==typeof r?n.has(r)||(o.forEach((t=>y(t,r,q,!0))),y(e,r,_,!0),n.add(r)):o.has(r)||(n.forEach((t=>y(r,t,q,!0))),o.add(r))};if(!("__q_context__"in t)){t.__q_context__=0;const r=e.qwikevents;Array.isArray(r)&&w(...r),e.qwikevents={events:n,roots:o,push:w},y(t,"readystatechange",d),d()}})();"` + `"(()=>{const t=document,e=window,n=new Set,o=new Set([t]);let r;const s=(t,e)=>Array.from(t.querySelectorAll(e)),a=t=>{const e=[];return o.forEach((n=>e.push(...s(n,t)))),e},i=t=>{w(t),s(t,"[q\\\\:shadowroot]").forEach((t=>{const e=t.shadowRoot;e&&i(e)}))},c=t=>t&&"function"==typeof t.then,l=(t,e,n=e.type)=>{a("[on"+t+"\\\\:"+n+"]").forEach((o=>u(o,t,e,n)))},f=e=>{if(void 0===e._qwikjson_){let n=(e===t.documentElement?t.body:e).lastElementChild;for(;n;){if("SCRIPT"===n.tagName&&"qwik/json"===n.getAttribute("type")){e._qwikjson_=JSON.parse(n.textContent.replace(/\\\\x3C(\\/?script)/gi,"<$1"));break}n=n.previousElementSibling}}},p=(t,e)=>new CustomEvent(t,{detail:e}),u=async(e,n,o,r=o.type)=>{const s="on"+n+":"+r;e.hasAttribute("preventdefault:"+r)&&o.preventDefault(),e.hasAttribute("stoppropagation:"+r)&&o.stopPropagation();const a=e._qc_,i=a&&a.li.filter((t=>t[0]===s));if(i&&i.length>0){for(const t of i){const n=t[1].getFn([e,o],(()=>e.isConnected))(o,e),r=o.cancelBubble;c(n)&&await n,r&&o.stopPropagation()}return}const l=e.getAttribute(s);if(l){const n=e.closest("[q\\\\:container]"),r=n.getAttribute("q:base"),s=n.getAttribute("q:version")||"unknown",a=n.getAttribute("q:manifest-hash")||"dev",i=new URL(r,t.baseURI);for(const p of l.split("\\n")){const l=new URL(p,i),u=l.href,h=l.hash.replace(/^#?([^?[|]*).*$/,"$1")||"default",_=performance.now();let q,d,y;const g=p.startsWith("#"),w={qBase:r,qManifest:a,qVersion:s,href:u,symbol:h,element:e,reqTime:_};if(g){const e=n.getAttribute("q:instance");q=(t["qFuncs_"+e]||[])[Number.parseInt(h)],q||(d="sync",y=Error("sym:"+h))}else{const t=l.href.split("#")[0];try{const e=import(t);f(n),q=(await e)[h],q||(d="no-symbol",y=Error(\`\${h} not in \${t}\`))}catch(t){d||(d="async"),y=t}}if(!q){b("qerror",{importError:d,error:y,...w}),console.error(y);break}const m=t.__q_context__;if(e.isConnected)try{t.__q_context__=[e,o,l],g||b("qsymbol",{...w});const n=q(o,e);c(n)&&await n}catch(t){b("qerror",{error:t,...w})}finally{t.__q_context__=m}}}},b=(e,n)=>{t.dispatchEvent(p(e,n))},h=t=>t.replace(/([A-Z])/g,(t=>"-"+t.toLowerCase())),_=async t=>{let e=h(t.type),n=t.target;for(l("-document",t,e);n&&n.getAttribute;){const o=u(n,"",t,e);let r=t.cancelBubble;c(o)&&await o,r=r||t.cancelBubble||n.hasAttribute("stoppropagation:"+t.type),n=t.bubbles&&!0!==r?n.parentElement:null}},q=t=>{l("-window",t,h(t.type))},d=t=>{const e=a(\`[on\\\\:\${t}]\`);if(e.length){const n=new IntersectionObserver((e=>{for(const o of e)o.isIntersecting&&(n.unobserve(o.target),u(o.target,"",p(t,o)))}));e.forEach((t=>n.observe(t)))}},y=()=>{var n;const s=t.readyState;r||"interactive"!=s&&"complete"!=s||(o.forEach(i),r=1,b("qinit"),(null!=(n=e.requestIdleCallback)?n:e.setTimeout).bind(e)((()=>{b("qidle"),d("qidlevisible")})),d("qvisible"))},g=(t,e,n,o=!1)=>t.addEventListener(e,n,{capture:o,passive:!1}),w=(...t)=>{for(const r of t)"string"==typeof r?n.has(r)||(o.forEach((t=>g(t,r,_,!0))),g(e,r,q,!0),n.add(r)):o.has(r)||(n.forEach((t=>g(r,t,_,!0))),o.add(r))};if(!("__q_context__"in t)){t.__q_context__=0;const r=e.qwikevents;Array.isArray(r)&&w(...r),e.qwikevents={events:n,roots:o,push:w},g(t,"readystatechange",y),y()}})();"` ); }); diff --git a/starters/apps/e2e/src/components/effect-client/effect-client.tsx b/starters/apps/e2e/src/components/effect-client/effect-client.tsx index f5386352cd6..661a862de08 100644 --- a/starters/apps/e2e/src/components/effect-client/effect-client.tsx +++ b/starters/apps/e2e/src/components/effect-client/effect-client.tsx @@ -114,6 +114,7 @@ export const ClientSide = component$(() => { text1: "empty 1", text2: "empty 2", text3: "empty 3", + text4: "empty 4", }); useVisibleTask$( @@ -138,11 +139,21 @@ export const ClientSide = component$(() => { }, ); + useVisibleTask$( + () => { + state.text4 = "run"; + }, + { + strategy: "idle-visible", + }, + ); + return ( <>
{state.text1}
{state.text2}
{state.text3}
+
{state.text4}
); }); diff --git a/starters/e2e/e2e.effect-client.spec.ts b/starters/e2e/e2e.effect-client.spec.ts index 8addef302cd..20fe20734e1 100644 --- a/starters/e2e/e2e.effect-client.spec.ts +++ b/starters/e2e/e2e.effect-client.spec.ts @@ -19,6 +19,7 @@ test.describe("effect-client", () => { const msgClientSide1 = page.locator("#client-side-msg-1"); const msgClientSide2 = page.locator("#client-side-msg-2"); const msgClientSide3 = page.locator("#client-side-msg-3"); + const msgClientSide4 = page.locator("#client-side-msg-4"); await expect(container).not.hasAttribute("data-effect"); await expect(counter).toHaveText("0"); @@ -27,7 +28,7 @@ test.describe("effect-client", () => { await expect(msgClientSide1).toHaveText("run"); await expect(msgClientSide2).toHaveText("run"); await expect(msgClientSide3).toHaveText("run"); - + await expect(msgClientSide4).toHaveText("run"); await counter.scrollIntoViewIfNeeded(); await expect(container).toHaveAttribute("data-effect", "true"); From cdb40db09ee2f655d84439a45354ce7837145876 Mon Sep 17 00:00:00 2001 From: Wout Mertens Date: Thu, 15 May 2025 23:11:24 +0200 Subject: [PATCH 2/2] fix(prefetchSW): correct script path replace --- packages/qwik/src/core/components/prefetch.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/qwik/src/core/components/prefetch.ts b/packages/qwik/src/core/components/prefetch.ts index 39eed14c65a..679152242b5 100644 --- a/packages/qwik/src/core/components/prefetch.ts +++ b/packages/qwik/src/core/components/prefetch.ts @@ -42,7 +42,7 @@ export const PrefetchServiceWorker = (opts: { // the file 'qwik-prefetch-service-worker.js' is not located in /build/ resolvedOpts.path = baseUrl + resolvedOpts.path; } - let code = PREFETCH_CODE.replace('URL', resolvedOpts.path); + let code = PREFETCH_CODE.replace("'_URL_'", JSON.stringify(resolvedOpts.path)); if (!isDev) { // consecutive spaces are indentation code = code.replaceAll(/\s\s+/gm, ''); @@ -67,7 +67,7 @@ const PREFETCH_CODE = /*#__PURE__*/ (( c.getRegistrations().then((registrations) => { registrations.forEach((registration) => { if (registration.active) { - if (registration.active.scriptURL.endsWith('URL')) { + if (registration.active.scriptURL.endsWith('_URL_')) { registration.unregister().catch(console.error); } }