diff --git a/packages/core/src/page.ts b/packages/core/src/page.ts index b017ce3d1..3424b5ada 100644 --- a/packages/core/src/page.ts +++ b/packages/core/src/page.ts @@ -7,7 +7,7 @@ import { hrefToUrl, isSameUrlWithoutHash } from './url' class CurrentPage { protected page!: Page - protected swapComponent!: PageHandler + protected swapComponent!: PageHandler protected resolveComponent!: PageResolver protected componentId = {} protected listeners: { @@ -18,7 +18,11 @@ class CurrentPage { protected cleared = false protected pendingDeferredProps: Pick | null = null - public init({ initialPage, swapComponent, resolveComponent }: RouterInitParams) { + public init({ + initialPage, + swapComponent, + resolveComponent, + }: RouterInitParams) { this.page = initialPage this.swapComponent = swapComponent this.resolveComponent = resolveComponent diff --git a/packages/core/src/router.ts b/packages/core/src/router.ts index 9ede2173f..86751701b 100644 --- a/packages/core/src/router.ts +++ b/packages/core/src/router.ts @@ -48,7 +48,11 @@ export class Router { interruptible: false, }) - public init({ initialPage, resolveComponent, swapComponent }: RouterInitParams): void { + public init({ + initialPage, + resolveComponent, + swapComponent, + }: RouterInitParams): void { currentPage.init({ initialPage, resolveComponent, diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 156527921..ac03978f3 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -162,12 +162,12 @@ export interface ClientSideVisitOptions { export type PageResolver = (name: string) => Component -export type PageHandler = ({ +export type PageHandler = ({ component, page, preserveState, }: { - component: Component + component: ComponentType page: Page preserveState: boolean }) => Promise @@ -363,10 +363,10 @@ export type PollOptions = { export type VisitHelperOptions = Omit, 'method' | 'data'> -export type RouterInitParams = { +export type RouterInitParams = { initialPage: Page resolveComponent: PageResolver - swapComponent: PageHandler + swapComponent: PageHandler } export type PendingVisitOptions = { @@ -388,7 +388,45 @@ export type InternalActiveVisit = ActiveVisit & { export type VisitId = unknown export type Component = unknown -export type InertiaAppResponse = Promise<{ head: string[]; body: string } | void> +interface CreateInertiaAppOptions { + resolve: TComponentResolver + setup: (options: TSetupOptions) => TSetupReturn + title?: HeadManagerTitleCallback +} + +export interface CreateInertiaAppOptionsForCSR< + SharedProps extends PageProps, + TComponentResolver, + TSetupOptions, + TSetupReturn, +> extends CreateInertiaAppOptions { + id?: string + page?: Page + progress?: + | false + | { + delay?: number + color?: string + includeCSS?: boolean + showSpinner?: boolean + } + render?: undefined +} + +export interface CreateInertiaAppOptionsForSSR< + SharedProps extends PageProps, + TComponentResolver, + TSetupOptions, + TSetupReturn, +> extends CreateInertiaAppOptions { + id?: undefined + page: Page + progress?: undefined + render: unknown +} + +export type InertiaAppSSRResponse = { head: string[]; body: string } +export type InertiaAppResponse = Promise export type HeadManagerTitleCallback = (title: string) => string export type HeadManagerOnUpdateCallback = (elements: string[]) => void diff --git a/packages/react/src/App.ts b/packages/react/src/App.ts index fcf14ab80..2c165503f 100755 --- a/packages/react/src/App.ts +++ b/packages/react/src/App.ts @@ -1,26 +1,52 @@ -import { createHeadManager, PageHandler, router } from '@inertiajs/core' -import { createElement, useEffect, useMemo, useState } from 'react' +import { + createHeadManager, + HeadManagerOnUpdateCallback, + HeadManagerTitleCallback, + Page, + PageHandler, + PageProps, + router, +} from '@inertiajs/core' +import { createElement, FunctionComponent, ReactNode, useEffect, useMemo, useState } from 'react' import HeadContext from './HeadContext' import PageContext from './PageContext' +import { LayoutFunction, ReactComponent, ReactPageHandlerArgs } from './types' let currentIsInitialPage = true let routerIsInitialized = false -let swapComponent: PageHandler = async () => { +let swapComponent: PageHandler = async () => { // Dummy function so we can init the router outside of the useEffect hook. This is // needed so `router.reload()` works right away (on mount) in any of the user's // components. We swap in the real function in the useEffect hook below. currentIsInitialPage = false } -export default function App({ +type CurrentPage = { + component: ReactComponent | null + page: Page + key: number | null +} + +export interface InertiaAppProps { + children?: (options: { component: ReactComponent; props: PageProps; key: number | null }) => ReactNode + initialPage: Page + initialComponent?: ReactComponent + resolveComponent?: (name: string) => ReactComponent | Promise + titleCallback?: HeadManagerTitleCallback + onHeadUpdate?: HeadManagerOnUpdateCallback +} + +export type InertiaApp = FunctionComponent + +export default function App({ children, initialPage, initialComponent, resolveComponent, titleCallback, onHeadUpdate, -}) { - const [current, setCurrent] = useState({ +}: InertiaAppProps) { + const [current, setCurrent] = useState({ component: initialComponent || null, page: initialPage, key: null, @@ -35,9 +61,9 @@ export default function App({ }, []) if (!routerIsInitialized) { - router.init({ + router.init({ initialPage, - resolveComponent, + resolveComponent: resolveComponent!, swapComponent: async (args) => swapComponent(args), }) @@ -45,7 +71,7 @@ export default function App({ } useEffect(() => { - swapComponent = async ({ component, page, preserveState }) => { + swapComponent = async ({ component, page, preserveState }: ReactPageHandlerArgs) => { if (currentIsInitialPage) { // We block setting the current page on the initial page to // prevent the initial page from being re-rendered again. @@ -73,18 +99,18 @@ export default function App({ const renderChildren = children || - (({ Component, props, key }) => { - const child = createElement(Component, { key, ...props }) + (({ component, props, key }) => { + const child = createElement(component, { key, ...props }) - if (typeof Component.layout === 'function') { - return Component.layout(child) + if (typeof component.layout === 'function') { + return (component.layout as LayoutFunction)(child) } - if (Array.isArray(Component.layout)) { - return Component.layout + if (Array.isArray(component.layout)) { + return (component.layout as any) .concat(child) .reverse() - .reduce((children, Layout) => createElement(Layout, { children, ...props })) + .reduce((children: any, Layout: any) => createElement(Layout, { children, ...props })) } return child @@ -97,7 +123,7 @@ export default function App({ PageContext.Provider, { value: current.page }, renderChildren({ - Component: current.component, + component: current.component, key: current.key, props: current.page.props, }), diff --git a/packages/react/src/createInertiaApp.ts b/packages/react/src/createInertiaApp.ts index 7bfaedd6c..d74f7b925 100644 --- a/packages/react/src/createInertiaApp.ts +++ b/packages/react/src/createInertiaApp.ts @@ -1,73 +1,50 @@ import { - HeadManagerOnUpdateCallback, - HeadManagerTitleCallback, - Page, + CreateInertiaAppOptionsForCSR, + CreateInertiaAppOptionsForSSR, + InertiaAppResponse, + InertiaAppSSRResponse, PageProps, - PageResolver, router, setupProgress, } from '@inertiajs/core' -import { ComponentType, FunctionComponent, Key, ReactElement, ReactNode, createElement } from 'react' +import { ReactElement, createElement } from 'react' import { renderToString } from 'react-dom/server' -import App from './App' - -type ReactInstance = ReactElement -type ReactComponent = ReactNode - -type AppType = FunctionComponent< - { - children?: (props: { Component: ComponentType; key: Key; props: Page['props'] }) => ReactNode - } & SetupOptions['props'] -> +import App, { InertiaAppProps, type InertiaApp } from './App' +import { ReactComponent } from './types' export type SetupOptions = { el: ElementType - App: AppType - props: { - initialPage: Page - initialComponent: ReactComponent - resolveComponent: PageResolver - titleCallback?: HeadManagerTitleCallback - onHeadUpdate?: HeadManagerOnUpdateCallback - } + App: InertiaApp + props: InertiaAppProps } -type BaseInertiaAppOptions = { - title?: HeadManagerTitleCallback - resolve: PageResolver -} +// The 'unknown' type is necessary for backwards compatibility... +type ComponentResolver = ( + name: string, +) => ReactComponent | Promise | { default: ReactComponent } | unknown -type CreateInertiaAppSetupReturnType = ReactInstance | void -type InertiaAppOptionsForCSR = BaseInertiaAppOptions & { - id?: string - page?: Page | string - render?: undefined - progress?: - | false - | { - delay?: number - color?: string - includeCSS?: boolean - showSpinner?: boolean - } - setup(options: SetupOptions): CreateInertiaAppSetupReturnType -} +type InertiaAppOptionsForCSR = CreateInertiaAppOptionsForCSR< + SharedProps, + ComponentResolver, + SetupOptions, + void +> -type CreateInertiaAppSSRContent = { head: string[]; body: string } -type InertiaAppOptionsForSSR = BaseInertiaAppOptions & { - id?: undefined - page: Page | string +type InertiaAppOptionsForSSR = CreateInertiaAppOptionsForSSR< + SharedProps, + ComponentResolver, + SetupOptions, + ReactElement +> & { render: typeof renderToString - progress?: undefined - setup(options: SetupOptions): ReactInstance } export default async function createInertiaApp( options: InertiaAppOptionsForCSR, -): Promise +): Promise export default async function createInertiaApp( options: InertiaAppOptionsForSSR, -): Promise +): Promise export default async function createInertiaApp({ id = 'app', resolve, @@ -76,32 +53,42 @@ export default async function createInertiaApp | InertiaAppOptionsForSSR): Promise< - CreateInertiaAppSetupReturnType | CreateInertiaAppSSRContent -> { +}: InertiaAppOptionsForCSR | InertiaAppOptionsForSSR): InertiaAppResponse { const isServer = typeof window === 'undefined' const el = isServer ? null : document.getElementById(id) - const initialPage = page || JSON.parse(el.dataset.page) - // @ts-expect-error + const initialPage = page || JSON.parse(el?.dataset.page || '{}') + // @ts-expect-error - This can be improved once we remove the 'unknown' type from the resolver... const resolveComponent = (name) => Promise.resolve(resolve(name)).then((module) => module.default || module) - let head = [] + let head: string[] = [] const reactApp = await Promise.all([ resolveComponent(initialPage.component), router.decryptHistory().catch(() => {}), ]).then(([initialComponent]) => { - return setup({ - // @ts-expect-error - el, + const props = { + initialPage, + initialComponent, + resolveComponent, + titleCallback: title, + } + + if (isServer) { + const ssrSetup = setup as (options: SetupOptions) => ReactElement + + return ssrSetup({ + el: null, + App, + props: { ...props, onHeadUpdate: (elements: string[]) => (head = elements) }, + }) + } + + const csrSetup = setup as (options: SetupOptions) => void + + return csrSetup({ + el: el as HTMLElement, App, - props: { - initialPage, - initialComponent, - resolveComponent, - titleCallback: title, - onHeadUpdate: isServer ? (elements) => (head = elements) : null, - }, + props, }) }) @@ -109,7 +96,7 @@ export default async function createInertiaApp ReactNode +export type LayoutComponent = ComponentType<{ children: ReactNode }> + +export type ReactComponent = ComponentType & { + layout?: LayoutComponent | LayoutComponent[] | LayoutFunction +} + +export type ReactPageHandlerArgs = Parameters>[0] diff --git a/packages/react/test-app/app.tsx b/packages/react/test-app/app.tsx index c174b809e..bed4cf79b 100644 --- a/packages/react/test-app/app.tsx +++ b/packages/react/test-app/app.tsx @@ -5,6 +5,7 @@ createInertiaApp({ page: window.initialPage, resolve: async (name) => { const pages = import.meta.glob('./Pages/**/*.tsx', { eager: true }) + // const typedPages = import.meta.glob('./Pages/**/*.tsx', { eager: true }) if (name === 'DeferredProps/InstantReload') { // Add small delay to ensure the component is loaded after the initial page load diff --git a/packages/react/tsconfig.json b/packages/react/tsconfig.json index 934e86187..7f8afa41a 100644 --- a/packages/react/tsconfig.json +++ b/packages/react/tsconfig.json @@ -22,7 +22,7 @@ "noUnusedParameters": true, "preserveConstEnums": true, "removeComments": true, - "typeRoots": ["./node_modules/@types"] - // "strict": true + "typeRoots": ["./node_modules/@types"], + "strict": true } } diff --git a/packages/svelte/src/components/App.svelte b/packages/svelte/src/components/App.svelte index df454db00..24ad23044 100644 --- a/packages/svelte/src/components/App.svelte +++ b/packages/svelte/src/components/App.svelte @@ -1,17 +1,17 @@