Skip to content

Commit da64170

Browse files
committed
Merge branch 'master' into once-prop
2 parents e3ca7a5 + 3516b03 commit da64170

40 files changed

+817
-83
lines changed

.github/workflows/playwright-chromium.yml

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,45 @@ jobs:
4242
with:
4343
name: playwright-failure-screenshots-${{ matrix.adapter }}-chromium
4444
path: test-results
45+
46+
test-chromium-ssr:
47+
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.repository
48+
name: Chromium SSR (${{ matrix.adapter }})
49+
timeout-minutes: 15
50+
runs-on: ubuntu-24.04
51+
strategy:
52+
matrix:
53+
adapter: ['vue', 'react', 'svelte']
54+
steps:
55+
- name: Checkout
56+
uses: actions/checkout@v4
57+
58+
- name: Install pnpm
59+
uses: pnpm/action-setup@v3
60+
with:
61+
version: 10
62+
63+
- name: Setup Node.js
64+
uses: actions/setup-node@v4
65+
with:
66+
node-version: 22.14
67+
cache: pnpm
68+
69+
- name: Install dependencies
70+
run: pnpm install
71+
72+
- name: Build Inertia
73+
run: pnpm -r --filter ./packages/core --filter ./packages/${{ matrix.adapter }}* build
74+
75+
- name: Install Playwright Browsers
76+
run: pnpm playwright install chromium
77+
78+
- name: Run Playwright SSR Tests
79+
run: pnpm test:ssr:${{ matrix.adapter }} --project=chromium
80+
81+
- name: Upload failure screenshots
82+
if: failure()
83+
uses: actions/upload-artifact@v4
84+
with:
85+
name: playwright-failure-screenshots-${{ matrix.adapter }}-chromium-ssr
86+
path: test-results

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@
2525
"test:react": "PACKAGE=react node playwright.js",
2626
"test:svelte": "PACKAGE=svelte node playwright.js",
2727
"test:vue": "PACKAGE=vue3 node playwright.js",
28+
"test:ssr:react": "PACKAGE=react SSR=true npx playwright test",
29+
"test:ssr:svelte": "PACKAGE=svelte SSR=true npx playwright test",
30+
"test:ssr:vue": "PACKAGE=vue3 SSR=true npx playwright test",
2831
"playground:react": "cd playgrounds/react && ./init.sh && composer run dev",
2932
"playground:svelte4": "cd playgrounds/svelte4 && ./init.sh && composer run dev",
3033
"playground:svelte5": "cd playgrounds/svelte5 && ./init.sh && composer run dev",

packages/core/src/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ export const config = new Config<InertiaAppConfig>({
7979
preserveEqualProps: false,
8080
useDataInertiaHeadAttribute: false,
8181
useDialogForErrorModal: false,
82+
useScriptElementForInitialPage: false,
8283
},
8384
prefetch: {
8485
cacheFor: 30_000,

packages/core/src/requestParams.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,10 @@ export class RequestParams {
5555
return this.params.only.length > 0 || this.params.except.length > 0 || this.params.reset.length > 0
5656
}
5757

58+
public isDeferredPropsRequest() {
59+
return this.params.deferredProps === true
60+
}
61+
5862
public onCancelToken(cb: VoidFunction) {
5963
this.params.onCancelToken({
6064
cancel: cb,

packages/core/src/response.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,15 @@ export class Response {
333333

334334
pageResponse.props = { ...currentPage.get().props, ...pageResponse.props }
335335

336+
if (this.requestParams.isDeferredPropsRequest()) {
337+
const currentErrors = currentPage.get().props.errors
338+
339+
if (currentErrors && Object.keys(currentErrors).length > 0) {
340+
// Preserve existing errors during deferred props requests
341+
pageResponse.props.errors = currentErrors
342+
}
343+
}
344+
336345
// Preserve the existing scrollProps
337346
if (currentPage.get().scrollProps) {
338347
pageResponse.scrollProps = {

packages/core/src/router.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,14 @@ export class Router {
115115
}
116116

117117
public reload<T extends RequestPayload = RequestPayload>(options: ReloadOptions<T> = {}): void {
118+
return this.doReload(options)
119+
}
120+
121+
protected doReload<T extends RequestPayload = RequestPayload>(
122+
options: ReloadOptions<T> & {
123+
deferredProps?: boolean
124+
} = {},
125+
): void {
118126
if (typeof window === 'undefined') {
119127
return
120128
}
@@ -521,7 +529,7 @@ export class Router {
521529
protected loadDeferredProps(deferred: Page['deferredProps']): void {
522530
if (deferred) {
523531
Object.entries(deferred).forEach(([_, group]) => {
524-
this.reload({ only: group })
532+
this.doReload({ only: group, deferredProps: true })
525533
})
526534
}
527535
}

packages/core/src/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -431,6 +431,7 @@ export type ActiveVisit<T extends RequestPayload = RequestPayload> = PendingVisi
431431
export type InternalActiveVisit = ActiveVisit & {
432432
onPrefetchResponse?: (response: Response) => void
433433
onPrefetchError?: (error: Error) => void
434+
deferredProps?: boolean
434435
}
435436

436437
export type VisitId = unknown
@@ -518,6 +519,7 @@ export type InertiaAppConfig = {
518519
preserveEqualProps: boolean
519520
useDataInertiaHeadAttribute: boolean
520521
useDialogForErrorModal: boolean
522+
useScriptElementForInitialPage: boolean
521523
}
522524
prefetch: {
523525
cacheFor: CacheForOption | CacheForOption[]

packages/react/src/createInertiaApp.ts

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
router,
88
setupProgress,
99
} from '@inertiajs/core'
10-
import { ReactElement, createElement } from 'react'
10+
import { Fragment, ReactElement, createElement } from 'react'
1111
import { renderToString } from 'react-dom/server'
1212
import App, { InertiaAppProps, type InertiaApp } from './App'
1313
import { config } from './index'
@@ -61,8 +61,13 @@ export default async function createInertiaApp<SharedProps extends PageProps = P
6161
config.replace(defaults)
6262

6363
const isServer = typeof window === 'undefined'
64+
const useScriptElementForInitialPage = config.get('future.useScriptElementForInitialPage')
6465
const el = isServer ? null : document.getElementById(id)
65-
const initialPage = page || JSON.parse(el?.dataset.page || '{}')
66+
const elPage =
67+
isServer || !useScriptElementForInitialPage
68+
? null
69+
: document.querySelector(`script[data-page="${id}"][type="application/json"]`)
70+
const initialPage = page || JSON.parse(elPage?.textContent || el?.dataset.page || '{}')
6671

6772
// @ts-expect-error - This can be improved once we remove the 'unknown' type from the resolver...
6873
const resolveComponent = (name) => Promise.resolve(resolve(name)).then((module) => module.default || module)
@@ -104,16 +109,31 @@ export default async function createInertiaApp<SharedProps extends PageProps = P
104109
}
105110

106111
if (isServer && render) {
107-
const body = await render(
108-
createElement(
109-
'div',
110-
{
111-
id,
112-
'data-page': JSON.stringify(initialPage),
113-
},
114-
reactApp as ReactElement,
115-
),
116-
)
112+
const element = () => {
113+
if (!useScriptElementForInitialPage) {
114+
return createElement(
115+
'div',
116+
{
117+
id,
118+
'data-page': JSON.stringify(initialPage),
119+
},
120+
reactApp as ReactElement,
121+
)
122+
}
123+
124+
return createElement(
125+
Fragment,
126+
null,
127+
createElement('script', {
128+
'data-page': id,
129+
type: 'application/json',
130+
dangerouslySetInnerHTML: { __html: JSON.stringify(initialPage) },
131+
}),
132+
createElement('div', { id }, reactApp as ReactElement),
133+
)
134+
}
135+
136+
const body = await render(element())
117137

118138
return { head, body }
119139
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { Deferred, useForm, usePage } from '@inertiajs/react'
2+
3+
const Foo = () => {
4+
const { foo } = usePage<{ foo?: { text: string } }>().props
5+
6+
return <div id="foo">{foo?.text}</div>
7+
}
8+
9+
export default () => {
10+
const { errors } = usePage<{ errors: { name?: string } }>().props
11+
const form = useForm({
12+
name: '',
13+
})
14+
15+
const submit = () => {
16+
form.post('/deferred-props/with-errors')
17+
}
18+
19+
return (
20+
<>
21+
<Deferred data="foo" fallback={<div>Loading foo...</div>}>
22+
<Foo />
23+
</Deferred>
24+
25+
{errors?.name && <p id="page-error">{errors.name}</p>}
26+
{form.errors.name && <p id="form-error">{form.errors.name}</p>}
27+
28+
<button type="button" onClick={submit}>
29+
Submit
30+
</button>
31+
</>
32+
)
33+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { Link } from '@inertiajs/react'
2+
3+
export default ({ user, items, count }: { user: { name: string; email: string }; items: string[]; count: number }) => (
4+
<div>
5+
<h1 data-testid="ssr-title">SSR Page 1</h1>
6+
7+
<div data-testid="user-info">
8+
<p data-testid="user-name">Name: {user.name}</p>
9+
<p data-testid="user-email">Email: {user.email}</p>
10+
</div>
11+
12+
<ul data-testid="items-list">
13+
{items.map((item) => (
14+
<li key={item} data-testid="item">
15+
{item}
16+
</li>
17+
))}
18+
</ul>
19+
20+
<p data-testid="count">Count: {count}</p>
21+
22+
<Link href="/ssr/page2" data-testid="navigate-link">
23+
Navigate to another page
24+
</Link>
25+
</div>
26+
)

0 commit comments

Comments
 (0)