Skip to content
76 changes: 76 additions & 0 deletions packages/core/src/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { get, has, set } from 'lodash-es'
import { InertiaAppConfig } from './types'

// Generate all possible nested paths
type ConfigKeys<T> = T extends Function
? never
: string extends keyof T
? string
:
| Extract<keyof T, string>
| {
[Key in Extract<keyof T, string>]: T[Key] extends object ? `${Key}.${ConfigKeys<T[Key]> & string}` : never
}[Extract<keyof T, string>]

// Extract the value type at a given path
type ConfigValue<T, K extends ConfigKeys<T>> = K extends `${infer P}.${infer Rest}`
? P extends keyof T
? Rest extends ConfigKeys<T[P]>
? ConfigValue<T[P], Rest>
: never
: never
: K extends keyof T
? T[K]
: never

// Helper type for setting multiple config values with an object
type ConfigSetObject<T> = {
[K in ConfigKeys<T>]?: ConfigValue<T, K>
}

export class Config<TConfig extends {} = {}> {
protected config: Partial<TConfig> = {}
protected defaults: TConfig

public constructor(defaults: TConfig) {
this.defaults = defaults
}

public extend<TExtension extends {}>(defaults?: TExtension): Config<TConfig & TExtension> {
if (defaults) {
this.defaults = { ...this.defaults, ...defaults } as TConfig & TExtension
}

return this as unknown as Config<TConfig & TExtension>
}

public replace(newConfig: Partial<TConfig>): void {
this.config = newConfig
}

public get<K extends ConfigKeys<TConfig>>(key: K): ConfigValue<TConfig, K> {
return (has(this.config, key) ? get(this.config, key) : get(this.defaults, key)) as ConfigValue<TConfig, K>
}

public set<K extends ConfigKeys<TConfig>>(
keyOrValues: K | Partial<ConfigSetObject<TConfig>>,
value?: ConfigValue<TConfig, K>,
): void {
if (typeof keyOrValues === 'string') {
set(this.config, keyOrValues, value)
} else {
Object.entries(keyOrValues).forEach(([key, val]) => {
set(this.config, key, val)
})
}
}
}

export const config = new Config<InertiaAppConfig>({
form: {
recentlySuccessfulDuration: 2_000,
},
prefetch: {
cacheFor: 30_000,
},
})
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Router } from './router'

export { config } from './config'
export { getScrollableParent } from './domUtils'
export { objectToFormData } from './formData'
export { formDataToObject } from './formObject'
Expand Down
10 changes: 9 additions & 1 deletion packages/core/src/router.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { cloneDeep, get, set } from 'lodash-es'
import { progress } from '.'
import { config } from './config'
import { eventHandler } from './eventHandler'
import { fireBeforeEvent } from './events'
import { history } from './history'
Expand Down Expand Up @@ -295,7 +296,7 @@ export class Router {
this.asyncRequestStream.send(Request.create(params, currentPage.get()))
},
{
cacheFor: 30_000,
cacheFor: config.get('prefetch.cacheFor'),
cacheTags: [],
...prefetchOptions,
},
Expand Down Expand Up @@ -443,6 +444,12 @@ export class Router {
options.method = options.method ?? urlMethodPair.method
}

const defaultVisitOptionsCallback = config.get('visitOptions')

const configuredOptions = defaultVisitOptionsCallback
? defaultVisitOptionsCallback(href.toString(), cloneDeep(options)) || {}
: {}

const mergedOptions: Visit = {
method: 'get',
data: {},
Expand All @@ -463,6 +470,7 @@ export class Router {
prefetch: false,
invalidateCacheTags: [],
...options,
...configuredOptions,
}

const [url, _data] = transformUrlAndData(
Expand Down
6 changes: 4 additions & 2 deletions packages/core/src/time.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
const conversionMap = {
import { CacheForOption, TimeUnit } from './types'

const conversionMap: Record<TimeUnit, number> = {
ms: 1,
s: 1000,
m: 1000 * 60,
h: 1000 * 60 * 60,
d: 1000 * 60 * 60 * 24,
}

export const timeToMs = (time: string | number): number => {
export const timeToMs = (time: CacheForOption): number => {
if (typeof time === 'number') {
return time
}
Expand Down
22 changes: 18 additions & 4 deletions packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -388,18 +388,20 @@ export type InternalActiveVisit = ActiveVisit & {
export type VisitId = unknown
export type Component = unknown

interface CreateInertiaAppOptions<TComponentResolver, TSetupOptions, TSetupReturn> {
interface CreateInertiaAppOptions<TComponentResolver, TSetupOptions, TSetupReturn, TAdditionalInertiaAppConfig> {
resolve: TComponentResolver
setup: (options: TSetupOptions) => TSetupReturn
title?: HeadManagerTitleCallback
defaults?: Partial<InertiaAppConfig & TAdditionalInertiaAppConfig>
}

export interface CreateInertiaAppOptionsForCSR<
SharedProps extends PageProps,
TComponentResolver,
TSetupOptions,
TSetupReturn,
> extends CreateInertiaAppOptions<TComponentResolver, TSetupOptions, TSetupReturn> {
TAdditionalInertiaAppConfig,
> extends CreateInertiaAppOptions<TComponentResolver, TSetupOptions, TSetupReturn, TAdditionalInertiaAppConfig> {
id?: string
page?: Page<SharedProps>
progress?:
Expand All @@ -418,7 +420,8 @@ export interface CreateInertiaAppOptionsForSSR<
TComponentResolver,
TSetupOptions,
TSetupReturn,
> extends CreateInertiaAppOptions<TComponentResolver, TSetupOptions, TSetupReturn> {
TAdditionalInertiaAppConfig,
> extends CreateInertiaAppOptions<TComponentResolver, TSetupOptions, TSetupReturn, TAdditionalInertiaAppConfig> {
id?: undefined
page: Page<SharedProps>
progress?: undefined
Expand All @@ -441,13 +444,24 @@ export type HeadManager = {

export type LinkPrefetchOption = 'mount' | 'hover' | 'click'

export type CacheForOption = number | string
export type TimeUnit = 'ms' | 's' | 'm' | 'h' | 'd'
export type CacheForOption = number | `${number}${TimeUnit}` | string

export type PrefetchOptions = {
cacheFor: CacheForOption | CacheForOption[]
cacheTags: string | string[]
}

export type InertiaAppConfig = {
form: {
recentlySuccessfulDuration: number
}
prefetch: {
cacheFor: CacheForOption | CacheForOption[]
}
visitOptions?: (href: string, options: VisitOptions) => VisitOptions
}

export interface LinkComponentBaseProps
extends Partial<
Pick<
Expand Down
3 changes: 2 additions & 1 deletion packages/react/src/Link.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
VisitOptions,
} from '@inertiajs/core'
import { createElement, ElementType, forwardRef, useEffect, useMemo, useRef, useState } from 'react'
import { config } from '.'

const noop = () => undefined

Expand Down Expand Up @@ -152,7 +153,7 @@ const Link = forwardRef<unknown, InertiaLinkProps>(
}

// Otherwise, default to 30 seconds
return 30_000
return config.get('prefetch.cacheFor')
}, [cacheFor, prefetchModes])

const doPrefetch = useMemo(() => {
Expand Down
13 changes: 10 additions & 3 deletions packages/react/src/createInertiaApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import {
import { ReactElement, createElement } from 'react'
import { renderToString } from 'react-dom/server'
import App, { InertiaAppProps, type InertiaApp } from './App'
import { ReactComponent } from './types'
import { config } from './index'
import { ReactComponent, ReactInertiaAppConfig } from './types'

export type SetupOptions<ElementType, SharedProps extends PageProps> = {
el: ElementType
Expand All @@ -27,14 +28,16 @@ type InertiaAppOptionsForCSR<SharedProps extends PageProps> = CreateInertiaAppOp
SharedProps,
ComponentResolver,
SetupOptions<HTMLElement, SharedProps>,
void
void,
ReactInertiaAppConfig
>

type InertiaAppOptionsForSSR<SharedProps extends PageProps> = CreateInertiaAppOptionsForSSR<
SharedProps,
ComponentResolver,
SetupOptions<null, SharedProps>,
ReactElement
ReactElement,
ReactInertiaAppConfig
> & {
render: typeof renderToString
}
Expand All @@ -53,10 +56,14 @@ export default async function createInertiaApp<SharedProps extends PageProps = P
progress = {},
page,
render,
defaults = {},
}: InertiaAppOptionsForCSR<SharedProps> | InertiaAppOptionsForSSR<SharedProps>): InertiaAppResponse {
config.replace(defaults)

const isServer = typeof window === 'undefined'
const el = isServer ? null : document.getElementById(id)
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)

Expand Down
5 changes: 4 additions & 1 deletion packages/react/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { progress as Progress, router as Router } from '@inertiajs/core'
import { config as coreConfig, progress as Progress, router as Router } from '@inertiajs/core'
import { ReactInertiaAppConfig } from './types'

export const progress = Progress
export const router = Router
Expand All @@ -21,3 +22,5 @@ export { default as usePoll } from './usePoll'
export { default as usePrefetch } from './usePrefetch'
export { default as useRemember } from './useRemember'
export { default as WhenVisible } from './WhenVisible'

export const config = coreConfig.extend<ReactInertiaAppConfig>()
1 change: 1 addition & 0 deletions packages/react/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ export type ReactComponent = ComponentType<any> & {
}

export type ReactPageHandlerArgs = Parameters<PageHandler<ComponentType>>[0]
export type ReactInertiaAppConfig = {}
3 changes: 2 additions & 1 deletion packages/react/src/useForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
} from '@inertiajs/core'
import { cloneDeep, get, has, isEqual, set } from 'lodash-es'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { config } from '.'
import { useIsomorphicLayoutEffect } from './react'
import useRemember from './useRemember'

Expand Down Expand Up @@ -154,7 +155,7 @@ export default function useForm<TForm extends FormDataType<TForm>>(
if (isMounted.current) {
setRecentlySuccessful(false)
}
}, 2000)
}, config.get('form.recentlySuccessfulDuration'))
}

const onSuccess = options.onSuccess ? await options.onSuccess(page) : null
Expand Down
37 changes: 37 additions & 0 deletions packages/react/test-app/Pages/CustomConfig.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import type { VisitOptions } from '@inertiajs/core'
import { config, Link, useForm, usePage } from '@inertiajs/react'

export default () => {
const page = usePage()
const form = useForm({})

const submit = () => {
form.post(page.url)
}

config.set({
'form.recentlySuccessfulDuration': 1000,
'prefetch.cacheFor': '2s',
})

config.set('visitOptions', (href: string, options: VisitOptions) => {
if (href !== '/dump/post') {
return {}
}

return { headers: { ...options.headers, 'X-From-Callback': 'bar' } }
})

return (
<div>
<Link prefetch href="/dump/get">
Prefetch Link
</Link>
<Link method="post" headers={{ 'X-From-Link': 'foo' }} href="/dump/post">
Post Dump
</Link>
<button onClick={submit}>Submit Form</button>
{form.recentlySuccessful && <p>Form was recently successful!</p>}
</div>
)
}
14 changes: 13 additions & 1 deletion packages/react/test-app/app.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { createInertiaApp } from '@inertiajs/react'
import type { VisitOptions } from '@inertiajs/core'
import { createInertiaApp, router } from '@inertiajs/react'
import { createRoot } from 'react-dom/client'

window.testing = { Inertia: router }

const withAppDefaults = new URLSearchParams(window.location.search).get('withAppDefaults')

createInertiaApp({
page: window.initialPage,
resolve: async (name) => {
Expand All @@ -22,4 +27,11 @@ createInertiaApp({
delay: 0,
color: 'red',
},
...(withAppDefaults && {
defaults: {
visitOptions: (href: string, options: VisitOptions) => {
return { headers: { ...options.headers, 'X-From-App-Defaults': 'test' } }
},
},
}),
})
10 changes: 8 additions & 2 deletions packages/svelte/src/createInertiaApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import {
} from '@inertiajs/core'
import { escape } from 'lodash-es'
import App, { type InertiaAppProps } from './components/App.svelte'
import type { ComponentResolver } from './types'
import { config } from './index'
import type { ComponentResolver, SvelteInertiaAppConfig } from './types'

type SvelteRenderResult = { html: string; head: string; css?: { code: string } }

Expand All @@ -23,7 +24,8 @@ type InertiaAppOptions<SharedProps extends PageProps> = CreateInertiaAppOptionsF
SharedProps,
ComponentResolver,
SetupOptions<SharedProps>,
SvelteRenderResult | void
SvelteRenderResult | void,
SvelteInertiaAppConfig
>

export default async function createInertiaApp<SharedProps extends PageProps = PageProps>({
Expand All @@ -32,10 +34,14 @@ export default async function createInertiaApp<SharedProps extends PageProps = P
setup,
progress = {},
page,
defaults = {},
}: InertiaAppOptions<SharedProps>): InertiaAppResponse {
config.replace(defaults)

const isServer = typeof window === 'undefined'
const el = isServer ? null : document.getElementById(id)
const initialPage = page || JSON.parse(el?.dataset.page || '{}')

const resolveComponent = (name: string) => Promise.resolve(resolve(name))

const svelteApp = await Promise.all([
Expand Down
Loading