Skip to content

Commit 2ff2025

Browse files
authored
Merge branch 'build/v2' into router-dev-improvements
2 parents 4355c6a + b8b568e commit 2ff2025

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+1721
-266
lines changed

.changeset/honest-berries-knock.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@qwik.dev/core': patch
3+
---
4+
5+
feat: add SSR backpatching (attributes-only) to ensure SSR/CSR parity for signal-driven attributes; limited to attribute updates (not OoO streaming)

.changeset/lemon-dingos-dance.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
'@builder.io/qwik': minor
2+
'@qwik.dev/core': minor
33
---
44

5-
BREAKING: (slightly) Qwik will no longer scan all modules at build start to detect Qwik modules. Instead, a runtime check is done to prevent duplicate core imports. If you get a runtime error, you need to fix your build settings so they don't externalize qwik-related libraries.
5+
BREAKING: (slightly) Qwik will no longer scan all modules at build start to detect Qwik modules (which should be bundled into your server code). Instead, a much faster build-time check is done, and Qwik will tell you if you need to update your `ssr.noExternal` settings in your Vite config.

.changeset/plain-eggs-clean.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@qwik.dev/core': patch
3+
---
4+
5+
fix: handling spread props on element node

.changeset/ten-emus-jog.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@qwik.dev/core': patch
3+
---
4+
5+
fix: resuming nested container in shadow root

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,20 @@
22
"id": "qwik-server",
33
"package": "@qwik.dev/qwik/server",
44
"members": [
5+
{
6+
"name": "getQwikBackpatchExecutorScript",
7+
"id": "getqwikbackpatchexecutorscript",
8+
"hierarchy": [
9+
{
10+
"name": "getQwikBackpatchExecutorScript",
11+
"id": "getqwikbackpatchexecutorscript"
12+
}
13+
],
14+
"kind": "Function",
15+
"content": "Provides the `backpatch-executor.js` executor script as a string.\n\n\n```typescript\nexport declare function getQwikBackpatchExecutorScript(opts?: {\n debug?: boolean;\n}): string;\n```\n\n\n<table><thead><tr><th>\n\nParameter\n\n\n</th><th>\n\nType\n\n\n</th><th>\n\nDescription\n\n\n</th></tr></thead>\n<tbody><tr><td>\n\nopts\n\n\n</td><td>\n\n{ debug?: boolean; }\n\n\n</td><td>\n\n_(Optional)_\n\n\n</td></tr>\n</tbody></table>\n\n**Returns:**\n\nstring",
16+
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/server/scripts.ts",
17+
"mdFile": "core.getqwikbackpatchexecutorscript.md"
18+
},
519
{
620
"name": "getQwikLoaderScript",
721
"id": "getqwikloaderscript",

packages/docs/src/routes/api/qwik-server/index.mdx

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,50 @@ title: \@qwik.dev/qwik/server API Reference
44

55
# [API](/api) &rsaquo; @qwik.dev/qwik/server
66

7+
## getQwikBackpatchExecutorScript
8+
9+
Provides the `backpatch-executor.js` executor script as a string.
10+
11+
```typescript
12+
export declare function getQwikBackpatchExecutorScript(opts?: {
13+
debug?: boolean;
14+
}): string;
15+
```
16+
17+
<table><thead><tr><th>
18+
19+
Parameter
20+
21+
</th><th>
22+
23+
Type
24+
25+
</th><th>
26+
27+
Description
28+
29+
</th></tr></thead>
30+
<tbody><tr><td>
31+
32+
opts
33+
34+
</td><td>
35+
36+
\{ debug?: boolean; }
37+
38+
</td><td>
39+
40+
_(Optional)_
41+
42+
</td></tr>
43+
</tbody></table>
44+
45+
**Returns:**
46+
47+
string
48+
49+
[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/server/scripts.ts)
50+
751
## getQwikLoaderScript
852

953
Provides the `qwikloader.js` file as a string. Useful for tooling to inline the qwikloader script into HTML.

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

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2105,7 +2105,7 @@
21052105
}
21062106
],
21072107
"kind": "TypeAlias",
2108-
"content": "```typescript\nexport type SSRStreamChildren = AsyncGenerator<JSXChildren, void, any> | ((stream: StreamWriter) => Promise<void>) | (() => AsyncGenerator<JSXChildren, void, any>);\n```\n**References:** [JSXChildren](#jsxchildren)",
2108+
"content": "```typescript\nexport type SSRStreamChildren = AsyncGenerator<JSXChildren, void, any> | ((stream: SSRStreamWriter) => Promise<void>) | (() => AsyncGenerator<JSXChildren, void, any>);\n```\n**References:** [JSXChildren](#jsxchildren)<!-- -->, [SSRStreamWriter](#ssrstreamwriter)",
21092109
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/shared/jsx/utils.public.ts",
21102110
"mdFile": "core.ssrstreamchildren.md"
21112111
},
@@ -2123,6 +2123,20 @@
21232123
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/shared/jsx/utils.public.ts",
21242124
"mdFile": "core.ssrstreamprops.md"
21252125
},
2126+
{
2127+
"name": "SSRStreamWriter",
2128+
"id": "ssrstreamwriter",
2129+
"hierarchy": [
2130+
{
2131+
"name": "SSRStreamWriter",
2132+
"id": "ssrstreamwriter"
2133+
}
2134+
],
2135+
"kind": "Interface",
2136+
"content": "```typescript\nexport interface SSRStreamWriter \n```\n\n\n<table><thead><tr><th>\n\nMethod\n\n\n</th><th>\n\nDescription\n\n\n</th></tr></thead>\n<tbody><tr><td>\n\n[write(chunk)](#ssrstreamwriter-write)\n\n\n</td><td>\n\n\n</td></tr>\n</tbody></table>",
2137+
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/shared/jsx/utils.public.ts",
2138+
"mdFile": "core.ssrstreamwriter.md"
2139+
},
21262140
{
21272141
"name": "SVGAttributes",
21282142
"id": "svgattributes",
@@ -2612,6 +2626,23 @@
26122626
"content": "Override the `getLocale` with `lang` within the `fn` execution.\n\n\n```typescript\nexport declare function withLocale<T>(locale: string, fn: () => T): T;\n```\n\n\n<table><thead><tr><th>\n\nParameter\n\n\n</th><th>\n\nType\n\n\n</th><th>\n\nDescription\n\n\n</th></tr></thead>\n<tbody><tr><td>\n\nlocale\n\n\n</td><td>\n\nstring\n\n\n</td><td>\n\n\n</td></tr>\n<tr><td>\n\nfn\n\n\n</td><td>\n\n() =&gt; T\n\n\n</td><td>\n\n\n</td></tr>\n</tbody></table>\n\n**Returns:**\n\nT",
26132627
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-locale.ts",
26142628
"mdFile": "core.withlocale.md"
2629+
},
2630+
{
2631+
"name": "write",
2632+
"id": "ssrstreamwriter-write",
2633+
"hierarchy": [
2634+
{
2635+
"name": "SSRStreamWriter",
2636+
"id": "ssrstreamwriter-write"
2637+
},
2638+
{
2639+
"name": "write",
2640+
"id": "ssrstreamwriter-write"
2641+
}
2642+
],
2643+
"kind": "MethodSignature",
2644+
"content": "```typescript\nwrite(chunk: JSXOutput): void;\n```\n\n\n<table><thead><tr><th>\n\nParameter\n\n\n</th><th>\n\nType\n\n\n</th><th>\n\nDescription\n\n\n</th></tr></thead>\n<tbody><tr><td>\n\nchunk\n\n\n</td><td>\n\n[JSXOutput](#jsxoutput)\n\n\n</td><td>\n\n\n</td></tr>\n</tbody></table>\n\n**Returns:**\n\nvoid",
2645+
"mdFile": "core.ssrstreamwriter.write.md"
26152646
}
26162647
]
26172648
}

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

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4500,11 +4500,11 @@ SSRStreamBlock: FunctionComponent<{
45004500
```typescript
45014501
export type SSRStreamChildren =
45024502
| AsyncGenerator<JSXChildren, void, any>
4503-
| ((stream: StreamWriter) => Promise<void>)
4503+
| ((stream: SSRStreamWriter) => Promise<void>)
45044504
| (() => AsyncGenerator<JSXChildren, void, any>);
45054505
```
45064506
4507-
**References:** [JSXChildren](#jsxchildren)
4507+
**References:** [JSXChildren](#jsxchildren), [SSRStreamWriter](#ssrstreamwriter)
45084508
45094509
[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/shared/jsx/utils.public.ts)
45104510
@@ -4520,6 +4520,32 @@ export type SSRStreamProps = {
45204520
45214521
[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/shared/jsx/utils.public.ts)
45224522
4523+
## SSRStreamWriter
4524+
4525+
```typescript
4526+
export interface SSRStreamWriter
4527+
```
4528+
4529+
<table><thead><tr><th>
4530+
4531+
Method
4532+
4533+
</th><th>
4534+
4535+
Description
4536+
4537+
</th></tr></thead>
4538+
<tbody><tr><td>
4539+
4540+
[write(chunk)](#ssrstreamwriter-write)
4541+
4542+
</td><td>
4543+
4544+
</td></tr>
4545+
</tbody></table>
4546+
4547+
[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/shared/jsx/utils.public.ts)
4548+
45234549
## SVGAttributes
45244550
45254551
The TS types don't include the SVG attributes so we have to define them ourselves
@@ -9919,3 +9945,39 @@ fn
99199945
T
99209946
99219947
[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-locale.ts)
9948+
9949+
## write
9950+
9951+
```typescript
9952+
write(chunk: JSXOutput): void;
9953+
```
9954+
9955+
<table><thead><tr><th>
9956+
9957+
Parameter
9958+
9959+
</th><th>
9960+
9961+
Type
9962+
9963+
</th><th>
9964+
9965+
Description
9966+
9967+
</th></tr></thead>
9968+
<tbody><tr><td>
9969+
9970+
chunk
9971+
9972+
</td><td>
9973+
9974+
[JSXOutput](#jsxoutput)
9975+
9976+
</td><td>
9977+
9978+
</td></tr>
9979+
</tbody></table>
9980+
9981+
**Returns:**
9982+
9983+
void
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
---
2+
title: Backpatching | Advanced
3+
contributors:
4+
- thejackshelton
5+
updated_at: '2025-08-31T10:17:00Z'
6+
created_at: '2025-08-31T10:17:00Z'
7+
---
8+
9+
# Backpatching
10+
11+
Similar to the [Qwikloader](../qwikloader/index.mdx) that executes a small script, backpatching updates nodes already streamed on the server without waking up the Qwik runtime.
12+
13+
> Most useful when building component libraries or apps with interdependent elements that render in varying orders.
14+
15+
### What it is
16+
17+
Backpatching solves a fundamental difference between client and server rendering:
18+
19+
**Client rendering**: Components can render in any order, then establish relationships between each other afterward.
20+
21+
**SSR streaming**: Once HTML is sent to the browser, it's immutable—you can only stream more content forward.
22+
23+
This creates problems for component libraries where elements need to reference each other (like form inputs linking to their labels via `aria-labelledby`). If the input streams before its label, it can't know the label's ID to set the relationship.
24+
25+
Backpatching automatically fixes these relationships by updating attributes after the entire page has streamed, giving you the same flexibility as client-side rendering.
26+
27+
> Note: This is not Out-of-Order Streaming. It only corrects already-sent attributes without delaying the stream.
28+
29+
### Example
30+
31+
```tsx
32+
const fieldContextId = createContextId<{ isDescription: Signal<boolean> }>('field-context');
33+
34+
export const Field = component$(() => {
35+
const isDescription = useSignal(false);
36+
37+
const context = {
38+
isDescription,
39+
}
40+
41+
useContextProvider(fieldContextId, context);
42+
43+
return (
44+
<>
45+
<Label />
46+
<Input />
47+
{/* If the description component is not passed, it is a broken aria reference without backpatching, as the input would try to describe an element that does not exist */}
48+
<Description />
49+
</>
50+
)
51+
})
52+
53+
export const Label = component$(() => {
54+
return <label>Label</label>;
55+
});
56+
57+
export const Input = component$(() => {
58+
const context = useContext(fieldContextId);
59+
60+
return <input aria-describedby={context.isDescription.value ? "description" : undefined} />;
61+
});
62+
63+
export const Description = component$(() => {
64+
const context = useContext(fieldContextId);
65+
66+
useTask$(() => {
67+
context.isDescription = true;
68+
})
69+
70+
return <div id="description">Description</div>;
71+
});
72+
```
73+
74+
- Without backpatching, `<Input />` would never know about `<Description />`, leading to incorrect accessibility relationships.
75+
76+
- With backpatching, the aria-describedby attribute on `<Input>` will be automatically corrected even if `<Description>` runs after the input was streamed.
77+
78+
### Limitations
79+
80+
- **Attributes only**: Backpatching is currently limited to updating attributes. It does not change element children/text/structure.
81+
82+
### How it works (high level)
83+
84+
Here's how backpatching works under the hood:
85+
86+
1. **During Server-side streaming**: When a component tries to update an attribute on an element that's already been sent to the browser, Qwik detects this and remembers the intended change.
87+
88+
2. **Element Tracking**: Qwik assigns each element a unique index based on its position in the DOM tree, so it can reliably find the same element in the browser.
89+
90+
3. **Script Generation**: Instead of blocking the stream, Qwik generates a tiny JavaScript snippet that will run later to apply the fix.
91+
92+
4. **Browser Execution**: On page load, this script uses efficient DOM traversal to find and update the target elements with their correct attribute values.
93+
94+
5. **Zero Runtime Impact**: This all happens without waking up the Qwik framework, keeping your app fast and lightweight.
95+

packages/docs/src/routes/docs/menu.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@
144144
- [ESLint-Rules](</docs/(qwik)/advanced/eslint/index.mdx>)
145145
- [Content Security Policy](</docs/(qwikrouter)/advanced/content-security-policy/index.mdx>)
146146
- [Complex Forms](</docs/(qwikrouter)/advanced/complex-forms/index.mdx>)
147+
- [Backpatching](</docs/(qwik)/advanced/backpatching/index.mdx>)
147148

148149
## Reference
149150

0 commit comments

Comments
 (0)