diff --git a/packages/svelte/src/components/App.svelte b/packages/svelte/src/components/App.svelte index 8fb50d5c5..df454db00 100644 --- a/packages/svelte/src/components/App.svelte +++ b/packages/svelte/src/components/App.svelte @@ -1,16 +1,52 @@ + + -{#if props} - -{/if} + diff --git a/packages/svelte/src/components/Render.svelte b/packages/svelte/src/components/Render.svelte index 23433e184..895dec332 100644 --- a/packages/svelte/src/components/Render.svelte +++ b/packages/svelte/src/components/Render.svelte @@ -7,7 +7,7 @@ props?: PageProps children?: RenderProps[] key?: number | null - } | null + } export type RenderFunction = { (component: ComponentType, props?: PageProps, children?: RenderProps[], key?: number | null): RenderProps diff --git a/packages/svelte/src/createInertiaApp.ts b/packages/svelte/src/createInertiaApp.ts index 54066b73a..87b646b55 100644 --- a/packages/svelte/src/createInertiaApp.ts +++ b/packages/svelte/src/createInertiaApp.ts @@ -1,25 +1,20 @@ import { router, setupProgress, type InertiaAppResponse, type Page } from '@inertiajs/core' import escape from 'html-escape' import type { ComponentType } from 'svelte' -import { version as SVELTE_VERSION } from 'svelte/package.json' -import App from './components/App.svelte' -import store from './store' -import type { ComponentResolver, ResolvedComponent } from './types' +import App, { type InertiaAppProps } from './components/App.svelte' +import type { ComponentResolver } from './types' type SvelteRenderResult = { html: string; head: string; css?: { code: string } } -type AppComponent = ComponentType & { render: () => SvelteRenderResult } +type AppComponent = ComponentType & { render: (props: InertiaAppProps) => SvelteRenderResult } interface CreateInertiaAppProps { id?: string resolve: ComponentResolver - setup?: (props: { - el: HTMLElement - App: ComponentType - props: { - initialPage: Page - resolveComponent: ComponentResolver - } - }) => void | App + setup: (props: { + el: HTMLElement | null + App: AppComponent + props: InertiaAppProps + }) => void | App | SvelteRenderResult progress?: | false | { @@ -40,31 +35,24 @@ export default async function createInertiaApp({ }: CreateInertiaAppProps): InertiaAppResponse { const isServer = typeof window === 'undefined' const el = isServer ? null : document.getElementById(id) - const initialPage: Page = page || JSON.parse(el?.dataset?.page || '{}') + const initialPage: Page = page || JSON.parse(el?.dataset.page || '{}') const resolveComponent = (name: string) => Promise.resolve(resolve(name)) - await Promise.all([resolveComponent(initialPage.component), router.decryptHistory().catch(() => {})]).then( - ([initialComponent]) => { - store.set({ - component: initialComponent, - page: initialPage, - key: null, - }) - }, - ) + const [initialComponent] = await Promise.all([ + resolveComponent(initialPage.component), + router.decryptHistory().catch(() => {}), + ]) - if (isServer) { - const isSvelte5 = SVELTE_VERSION.startsWith('5') - const { html, head, css } = await (async () => { - if (isSvelte5) { - const { render } = await dynamicImport('svelte/server') - if (typeof render === 'function') { - return render(App) as SvelteRenderResult - } - } + const props: InertiaAppProps = { initialPage, initialComponent, resolveComponent } - return (App as AppComponent).render() - })() + const svelteApp = setup({ + el, + App: App as unknown as AppComponent, + props + }) + + if (isServer) { + const { html, head, css } = svelteApp as SvelteRenderResult return { body: `
${html}
`, @@ -72,43 +60,7 @@ export default async function createInertiaApp({ } } - if (!el) { - throw new Error(`Element with ID "${id}" not found.`) - } - - router.init({ - initialPage, - resolveComponent, - swapComponent: async ({ component, page, preserveState }) => { - store.update((current) => ({ - component: component as ResolvedComponent, - page, - key: preserveState ? current.key : Date.now(), - })) - }, - }) - if (progress) { setupProgress(progress) } - - setup({ - el, - App, - props: { - initialPage, - resolveComponent, - }, - }) -} - -// Loads the module dynamically during execution instead of at -// build time. The `@vite-ignore` flag prevents Vite from -// analyzing or pre-bundling this import. -async function dynamicImport(module: string) { - try { - return await import(/* @vite-ignore */ module) - } catch { - return null - } } diff --git a/packages/svelte/src/page.ts b/packages/svelte/src/page.ts index d6daccf09..40a736ccc 100644 --- a/packages/svelte/src/page.ts +++ b/packages/svelte/src/page.ts @@ -1,6 +1,8 @@ -import { derived } from 'svelte/store' -import store from './store' +import { type Page } from '@inertiajs/core' +import { writable } from 'svelte/store' -const page = derived(store, ($store) => $store.page) +const { set, subscribe } = writable() -export default page +export const setPage = set + +export default { subscribe } diff --git a/packages/svelte/src/store.ts b/packages/svelte/src/store.ts deleted file mode 100644 index 0efc438d0..000000000 --- a/packages/svelte/src/store.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { Page } from '@inertiajs/core' -import { writable } from 'svelte/store' -import type { ResolvedComponent } from './types' - -export interface InertiaStore { - component: ResolvedComponent - page: Page - key: number | null -} - -const store = writable({ - component: null as unknown as ResolvedComponent, - page: null as unknown as Page, - key: null, -}) - -export default store diff --git a/packages/svelte/test-app/Pages/Svelte/PropsAndPageStore.svelte b/packages/svelte/test-app/Pages/Svelte/PropsAndPageStore.svelte new file mode 100644 index 000000000..17eb623bb --- /dev/null +++ b/packages/svelte/test-app/Pages/Svelte/PropsAndPageStore.svelte @@ -0,0 +1,41 @@ + + +
+ +

foo prop is {foo}

+

$page.props.foo is {$page.props.foo}

+ + + Bar + + + Baz + + + Home + +
diff --git a/packages/svelte/test-app/app.ts b/packages/svelte/test-app/app.ts index 67ed02424..37bbb7fba 100644 --- a/packages/svelte/test-app/app.ts +++ b/packages/svelte/test-app/app.ts @@ -8,7 +8,7 @@ createInertiaApp({ const pages = import.meta.glob('./Pages/**/*.svelte', { eager: true }) return pages[`./Pages/${name}.svelte`] }, - setup({ el, App }) { - new App({ target: el }) + setup({ el, App, props }) { + new App({ target: el, props }) }, }) diff --git a/packages/svelte/tsconfig.json b/packages/svelte/tsconfig.json index 47b7a5fc2..c9499c0fa 100644 --- a/packages/svelte/tsconfig.json +++ b/packages/svelte/tsconfig.json @@ -20,6 +20,6 @@ "noUnusedLocals": true, "noUnusedParameters": true, "preserveConstEnums": true, - "removeComments": false + "removeComments": true } } diff --git a/playgrounds/svelte4/resources/js/app.ts b/playgrounds/svelte4/resources/js/app.ts index 0a5c7e9aa..8853a5dd5 100644 --- a/playgrounds/svelte4/resources/js/app.ts +++ b/playgrounds/svelte4/resources/js/app.ts @@ -5,7 +5,7 @@ createInertiaApp({ const pages = import.meta.glob('./Pages/**/*.svelte', { eager: true }) return pages[`./Pages/${name}.svelte`] }, - setup({ el, App }) { - new App({ target: el, hydrate: true }) + setup({ el, App, props }) { + new App({ target: el, props, hydrate: true }) }, }) diff --git a/playgrounds/svelte4/resources/js/ssr.ts b/playgrounds/svelte4/resources/js/ssr.ts index 97b41c924..d4f3d4a51 100644 --- a/playgrounds/svelte4/resources/js/ssr.ts +++ b/playgrounds/svelte4/resources/js/ssr.ts @@ -8,5 +8,8 @@ createServer((page) => const pages = import.meta.glob('./Pages/**/*.svelte', { eager: true }) return pages[`./Pages/${name}.svelte`] }, + setup({ App, props }) { + return App.render(props) + } }), ) diff --git a/playgrounds/svelte5/resources/js/app.ts b/playgrounds/svelte5/resources/js/app.ts index e595b3a28..5f20e97d2 100644 --- a/playgrounds/svelte5/resources/js/app.ts +++ b/playgrounds/svelte5/resources/js/app.ts @@ -6,11 +6,11 @@ createInertiaApp({ const pages = import.meta.glob('./Pages/**/*.svelte', { eager: true }) return pages[`./Pages/${name}.svelte`] }, - setup({ el, App }) { + setup({ el, App, props }) { if (el.dataset.serverRendered === 'true') { - hydrate(App, { target: el }) + hydrate(App, { target: el, props }) } else { - mount(App, { target: el }) + mount(App, { target: el, props }) } }, }) diff --git a/playgrounds/svelte5/resources/js/ssr.ts b/playgrounds/svelte5/resources/js/ssr.ts index 97b41c924..037d9eecd 100644 --- a/playgrounds/svelte5/resources/js/ssr.ts +++ b/playgrounds/svelte5/resources/js/ssr.ts @@ -1,5 +1,6 @@ import { createInertiaApp, type ResolvedComponent } from '@inertiajs/svelte' import createServer from '@inertiajs/svelte/server' +import { render } from 'svelte/server' createServer((page) => createInertiaApp({ @@ -8,5 +9,8 @@ createServer((page) => const pages = import.meta.glob('./Pages/**/*.svelte', { eager: true }) return pages[`./Pages/${name}.svelte`] }, + setup({ App, props }) { + return render(App, { props }) + } }), ) diff --git a/tests/app/server.js b/tests/app/server.js index 84e51e752..9366d58c8 100644 --- a/tests/app/server.js +++ b/tests/app/server.js @@ -310,6 +310,10 @@ app.get('/deferred-props/page-2', (req, res) => { } }) +app.get('/svelte/props-and-page-store', (req, res) => + inertia.render(req, res, { component: 'Svelte/PropsAndPageStore', props: { foo: req.query.foo || 'default' }}), +) + app.all('/sleep', (req, res) => setTimeout(() => res.send(''), 2000)) app.post('/redirect', (req, res) => res.redirect(303, '/dump/get')) app.get('/location', ({ res }) => inertia.location(res, '/dump/get')) diff --git a/tests/svelte.spec.ts b/tests/svelte.spec.ts new file mode 100644 index 000000000..7f06df053 --- /dev/null +++ b/tests/svelte.spec.ts @@ -0,0 +1,68 @@ +import { expect, test } from '@playwright/test' +import { consoleMessages } from './support' + +test.beforeEach(async ({ page }) => { + test.skip(process.env.PACKAGE !== 'svelte', 'Svelte-only test') +}) + +test('props and page store are in sync', async ({ page }) => { + consoleMessages.listen(page) + + await page.goto('/svelte/props-and-page-store') + + await expect(page.getByText('foo prop is default')).toBeVisible() + await expect(page.getByText('$page.props.foo is default')).toBeVisible() + await expect(consoleMessages.messages).toHaveLength(7) + await expect(consoleMessages.messages[0]).toBe('[script] foo prop is default') + await expect(consoleMessages.messages[1]).toBe('[script] $page.props.foo is default') + await expect(consoleMessages.messages[2]).toBe('[reactive expression] foo prop is default') + await expect(consoleMessages.messages[3]).toBe('[reactive expression] $page.props.foo is default') + await expect(consoleMessages.messages[4]).toBe('[onMount] foo prop is default') + await expect(consoleMessages.messages[5]).toBe('[onMount] $page.props.foo is default') + await expect(consoleMessages.messages[6]).toBe('[reactive expression] $page.props.foo is default') + await expect(await page.locator('#input').inputValue()).toEqual('default') + + consoleMessages.messages = [] + await page.getByRole('link', { name: 'Bar' }).click() + + await expect(page.getByText('foo prop is bar')).toBeVisible() + await expect(page.getByText('$page.props.foo is bar')).toBeVisible() + await expect(consoleMessages.messages).toHaveLength(7) + await expect(consoleMessages.messages[0]).toBe('[reactive expression] $page.props.foo is bar') + await expect(consoleMessages.messages[1]).toBe('[script] foo prop is bar') + await expect(consoleMessages.messages[2]).toBe('[script] $page.props.foo is bar') + await expect(consoleMessages.messages[3]).toBe('[reactive expression] foo prop is bar') + await expect(consoleMessages.messages[4]).toBe('[reactive expression] $page.props.foo is bar') + await expect(consoleMessages.messages[5]).toBe('[onMount] foo prop is bar') + await expect(consoleMessages.messages[6]).toBe('[onMount] $page.props.foo is bar') + await expect(await page.locator('#input').inputValue()).toEqual('bar') + + consoleMessages.messages = [] + await page.getByRole('link', { name: 'Baz' }).click() + + await expect(page.getByText('foo prop is baz')).toBeVisible() + await expect(page.getByText('$page.props.foo is baz')).toBeVisible() + await expect(consoleMessages.messages).toHaveLength(7) + await expect(consoleMessages.messages[0]).toBe('[reactive expression] $page.props.foo is baz') + await expect(consoleMessages.messages[1]).toBe('[script] foo prop is baz') + await expect(consoleMessages.messages[2]).toBe('[script] $page.props.foo is baz') + await expect(consoleMessages.messages[3]).toBe('[reactive expression] foo prop is baz') + await expect(consoleMessages.messages[4]).toBe('[reactive expression] $page.props.foo is baz') + await expect(consoleMessages.messages[5]).toBe('[onMount] foo prop is baz') + await expect(consoleMessages.messages[6]).toBe('[onMount] $page.props.foo is baz') + await expect(await page.locator('#input').inputValue()).toEqual('baz') + + await page.getByRole('link', { name: 'Home' }).click() + consoleMessages.messages = [] + await page.goBack() + + await expect(page.getByText('foo prop is baz')).toBeVisible() + await expect(page.getByText('$page.props.foo is baz')).toBeVisible() + await expect(consoleMessages.messages).toHaveLength(6) + await expect(consoleMessages.messages[0]).toBe('[script] foo prop is baz') + await expect(consoleMessages.messages[1]).toBe('[script] $page.props.foo is baz') + await expect(consoleMessages.messages[2]).toBe('[reactive expression] foo prop is baz') + await expect(consoleMessages.messages[3]).toBe('[reactive expression] $page.props.foo is baz') + await expect(consoleMessages.messages[4]).toBe('[onMount] foo prop is baz') + await expect(consoleMessages.messages[5]).toBe('[onMount] $page.props.foo is baz') +}) \ No newline at end of file