@octanejs/inertia 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Dominic Gannaway
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,33 @@
1
+ # `@octanejs/inertia`
2
+
3
+ Inertia.js 3 bindings for Octane.
4
+
5
+ The package reuses `@inertiajs/core` unchanged and ports the renderer-specific
6
+ `@inertiajs/react` adapter to Octane. It does not install or emulate React.
7
+
8
+ The initial port is pinned to `@inertiajs/react@3.6.1` and the Inertia 3.x
9
+ source tree at commit `68b13b662d7a6ecdd504026ee18733192b0c7d73`.
10
+ The exact React adapter source is retained in `upstream/` so each ported module
11
+ and future version update can be reviewed against its immutable baseline.
12
+
13
+ ## Upstream ledger
14
+
15
+ | Contract | Upstream source | Octane entry |
16
+ | --- | --- | --- |
17
+ | Router singleton | `packages/react/src/index.ts` | `src/index.ts` |
18
+ | HTTP singleton | `packages/react/src/index.ts` | `src/index.ts` |
19
+ | Progress singleton | `packages/react/src/index.ts` | `src/index.ts` |
20
+ | Server helper | `packages/react/src/server.ts` | `src/server.ts` |
21
+
22
+ These exports preserve object identity with `@inertiajs/core@3.6.1`.
23
+ Renderer-owned exports from `packages/react/src/index.ts` are intentionally
24
+ absent from this foundation unit and are added with their corresponding
25
+ behavioral tests. React, ReactDOM, StrictMode, `forwardRef`, and synthetic event
26
+ types are explicit exclusions from the Octane package.
27
+
28
+ ## Status
29
+
30
+ The framework-neutral exports, package boundary, contexts, layout-property
31
+ store, and renderer-independent hook/form layer are present. Components, root
32
+ lifecycle, full SSR/hydration, and the protocol playground are implemented by
33
+ the dependent units in the binding plan.
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "@octanejs/inertia",
3
+ "octane": {
4
+ "hookSlots": {
5
+ "manual": [
6
+ "src"
7
+ ]
8
+ }
9
+ },
10
+ "version": "0.0.1",
11
+ "license": "MIT",
12
+ "type": "module",
13
+ "sideEffects": false,
14
+ "engines": {
15
+ "node": ">=22.22.2"
16
+ },
17
+ "description": "Inertia.js bindings for the octane renderer — reuses @inertiajs/core and ports the React adapter onto octane.",
18
+ "author": {
19
+ "name": "Dominic Gannaway",
20
+ "email": "dg@domgan.com"
21
+ },
22
+ "publishConfig": {
23
+ "access": "public"
24
+ },
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "git+https://github.com/octanejs/octane.git",
28
+ "directory": "packages/inertia"
29
+ },
30
+ "main": "src/index.ts",
31
+ "module": "src/index.ts",
32
+ "types": "src/index.ts",
33
+ "files": [
34
+ "src",
35
+ "README.md"
36
+ ],
37
+ "exports": {
38
+ ".": "./src/index.ts",
39
+ "./server": "./src/server.ts",
40
+ "./package.json": "./package.json"
41
+ },
42
+ "dependencies": {
43
+ "@inertiajs/core": "3.6.1",
44
+ "es-toolkit": "^1.49.0",
45
+ "laravel-precognition": "^2.0.0"
46
+ },
47
+ "peerDependencies": {
48
+ "octane": "0.1.32"
49
+ },
50
+ "devDependencies": {
51
+ "@inertiajs/react": "3.6.1",
52
+ "@tsrx/react": "^0.2.56",
53
+ "@types/react": "^19.2.17",
54
+ "@types/react-dom": "^19.2.3",
55
+ "react": "^19.2.7",
56
+ "react-dom": "^19.2.7",
57
+ "vitest": "^4.1.10",
58
+ "@octanejs/testing-library": "0.1.29",
59
+ "octane": "0.1.32"
60
+ },
61
+ "scripts": {
62
+ "test": "vitest run --root ../.. --project inertia"
63
+ }
64
+ }
@@ -0,0 +1,5 @@
1
+ import { HeadManager } from '@inertiajs/core';
2
+ import { createContext } from 'octane';
3
+
4
+ const headContext = createContext<HeadManager | null>(null);
5
+ export default headContext;
@@ -0,0 +1,5 @@
1
+ import { Page } from '@inertiajs/core';
2
+ import { createContext } from 'octane';
3
+
4
+ const pageContext = createContext<Page | null>(null);
5
+ export default pageContext;
package/src/index.ts ADDED
@@ -0,0 +1,26 @@
1
+ // @octanejs/inertia
2
+ //
3
+ // The Inertia router, HTTP client, and progress APIs are framework-neutral.
4
+ // Keep their upstream identities intact while the renderer-specific adapter is
5
+ // implemented in this package.
6
+ import { config as coreConfig } from '@inertiajs/core';
7
+
8
+ export { http, progress, router } from '@inertiajs/core';
9
+ export { resetLayoutProps, setLayoutProps } from './layoutProps';
10
+ export { default as usePage } from './usePage';
11
+ export { default as usePoll } from './usePoll';
12
+ export { default as usePrefetch } from './usePrefetch';
13
+ export { default as useRemember } from './useRemember';
14
+ export {
15
+ type InertiaForm,
16
+ type InertiaFormProps,
17
+ type InertiaPrecognitiveFormProps,
18
+ type SetDataAction,
19
+ type SetDataByKeyValuePair,
20
+ type SetDataByMethod,
21
+ type SetDataByObject,
22
+ default as useForm,
23
+ } from './useForm';
24
+ export { default as useHttp } from './useHttp';
25
+
26
+ export const config = coreConfig;
@@ -0,0 +1,25 @@
1
+ import { createLayoutPropsStore, type LayoutProps, type NamedLayoutProps } from '@inertiajs/core';
2
+
3
+ export const store = createLayoutPropsStore();
4
+
5
+ export function setLayoutProps(props: Partial<LayoutProps>): void;
6
+ export function setLayoutProps<K extends keyof NamedLayoutProps>(
7
+ name: K,
8
+ props: Partial<NamedLayoutProps[K]>,
9
+ ): void;
10
+ export function setLayoutProps<T = never>(props: Partial<NoInfer<T>>): void;
11
+ export function setLayoutProps<T = never>(name: string, props: Partial<NoInfer<T>>): void;
12
+ export function setLayoutProps(
13
+ nameOrProps: string | Record<string, unknown>,
14
+ props?: Record<string, unknown>,
15
+ ): void {
16
+ if (typeof nameOrProps === 'string') {
17
+ store.setFor(nameOrProps, props!);
18
+ } else {
19
+ store.set(nameOrProps);
20
+ }
21
+ }
22
+
23
+ export function resetLayoutProps(): void {
24
+ store.reset();
25
+ }
package/src/server.ts ADDED
@@ -0,0 +1 @@
1
+ export { default } from '@inertiajs/core/server';
package/src/useForm.ts ADDED
@@ -0,0 +1,318 @@
1
+ import {
2
+ CancelToken,
3
+ ErrorValue,
4
+ FormDataErrors,
5
+ FormDataKeys,
6
+ FormDataType,
7
+ FormDataValues,
8
+ Method,
9
+ OptimisticCallback,
10
+ Progress,
11
+ RequestPayload,
12
+ router,
13
+ UrlMethodPair,
14
+ UseFormArguments,
15
+ UseFormSubmitArguments,
16
+ UseFormSubmitOptions,
17
+ UseFormTransformCallback,
18
+ UseFormUtils,
19
+ UseFormWithPrecognitionArguments,
20
+ VisitOptions,
21
+ } from '@inertiajs/core';
22
+ import { cloneDeep } from 'es-toolkit';
23
+ import type {
24
+ NamedInputEvent,
25
+ PrecognitionPath,
26
+ ValidationConfig,
27
+ Validator,
28
+ } from 'laravel-precognition';
29
+ import { useCallback, useMemo, useRef } from 'octane';
30
+ import useFormState, {
31
+ SetDataAction,
32
+ SetDataByKeyValuePair,
33
+ SetDataByMethod,
34
+ SetDataByObject,
35
+ } from './useFormState';
36
+ import useRemember from './useRemember';
37
+
38
+ const slots = Object.fromEntries(
39
+ ['cancelToken', 'excludeKeys', 'pendingOptimistic', 'submit', 'cancel', 'submitMethods'].map(
40
+ (name) => [name, Symbol(`Inertia.useForm.${name}`)],
41
+ ),
42
+ ) as Record<string, symbol>;
43
+
44
+ // Re-export types that were moved to useFormState
45
+ export { SetDataAction, SetDataByKeyValuePair, SetDataByMethod, SetDataByObject };
46
+
47
+ type PrecognitionValidationConfig<TKeys> = ValidationConfig & {
48
+ only?: TKeys[] | Iterable<TKeys> | ArrayLike<TKeys>;
49
+ };
50
+
51
+ export interface InertiaFormProps<TForm extends Record<string, any>> {
52
+ data: TForm;
53
+ isDirty: boolean;
54
+ errors: FormDataErrors<TForm>;
55
+ hasErrors: boolean;
56
+ processing: boolean;
57
+ progress: Progress | null;
58
+ wasSuccessful: boolean;
59
+ recentlySuccessful: boolean;
60
+ setData: SetDataAction<TForm>;
61
+ transform: (callback: UseFormTransformCallback<TForm>) => void;
62
+ setDefaults: {
63
+ (): void;
64
+ <T extends FormDataKeys<TForm>>(field: T, value: FormDataValues<TForm, T>): void;
65
+ (fields: Partial<TForm>): void;
66
+ };
67
+ reset: <K extends FormDataKeys<TForm>>(...fields: K[]) => void;
68
+ clearErrors: <K extends FormDataKeys<TForm>>(...fields: K[]) => void;
69
+ resetAndClearErrors: <K extends FormDataKeys<TForm>>(...fields: K[]) => void;
70
+ setError: {
71
+ <K extends FormDataKeys<TForm>>(field: K, value: ErrorValue): void;
72
+ (errors: FormDataErrors<TForm>): void;
73
+ };
74
+ submit: (...args: UseFormSubmitArguments) => void;
75
+ get: (url: string, options?: UseFormSubmitOptions) => void;
76
+ post: (url: string, options?: UseFormSubmitOptions) => void;
77
+ put: (url: string, options?: UseFormSubmitOptions) => void;
78
+ patch: (url: string, options?: UseFormSubmitOptions) => void;
79
+ delete: (url: string, options?: UseFormSubmitOptions) => void;
80
+ cancel: () => void;
81
+ dontRemember: <K extends FormDataKeys<TForm>>(...fields: K[]) => InertiaFormProps<TForm>;
82
+ optimistic: <TProps>(callback: OptimisticCallback<TProps>) => InertiaFormProps<TForm>;
83
+ withPrecognition: (
84
+ ...args: UseFormWithPrecognitionArguments
85
+ ) => InertiaPrecognitiveFormProps<TForm>;
86
+ }
87
+
88
+ export interface InertiaFormValidationProps<TForm extends Record<string, any>> {
89
+ invalid: <K extends FormDataKeys<TForm>>(field: K) => boolean;
90
+ setValidationTimeout: (duration: number) => InertiaPrecognitiveFormProps<TForm>;
91
+ touch: <K extends FormDataKeys<TForm>>(
92
+ field: K | NamedInputEvent | Array<K>,
93
+ ...fields: K[]
94
+ ) => InertiaPrecognitiveFormProps<TForm>;
95
+ touched: <K extends FormDataKeys<TForm>>(field?: K) => boolean;
96
+ valid: <K extends FormDataKeys<TForm>>(field: K) => boolean;
97
+ validate: <K extends FormDataKeys<TForm> | PrecognitionPath<TForm>>(
98
+ field?: K | NamedInputEvent | PrecognitionValidationConfig<K>,
99
+ config?: PrecognitionValidationConfig<K>,
100
+ ) => InertiaPrecognitiveFormProps<TForm>;
101
+ validateFiles: () => InertiaPrecognitiveFormProps<TForm>;
102
+ validating: boolean;
103
+ validator: () => Validator;
104
+ withAllErrors: () => InertiaPrecognitiveFormProps<TForm>;
105
+ withoutFileValidation: () => InertiaPrecognitiveFormProps<TForm>;
106
+ // Backward compatibility for easy migration from the original Precognition libraries
107
+ setErrors: (errors: FormDataErrors<TForm>) => InertiaPrecognitiveFormProps<TForm>;
108
+ forgetError: <K extends FormDataKeys<TForm> | NamedInputEvent>(
109
+ field: K,
110
+ ) => InertiaPrecognitiveFormProps<TForm>;
111
+ }
112
+
113
+ export type InertiaForm<TForm extends Record<string, any>> = InertiaFormProps<TForm>;
114
+ export type InertiaPrecognitiveFormProps<TForm extends Record<string, any>> =
115
+ InertiaFormProps<TForm> & InertiaFormValidationProps<TForm>;
116
+
117
+ export default function useForm<TForm extends FormDataType<TForm>>(
118
+ method: Method | (() => Method),
119
+ url: string | (() => string),
120
+ data: TForm | (() => TForm),
121
+ ): InertiaPrecognitiveFormProps<TForm>;
122
+ export default function useForm<TForm extends FormDataType<TForm>>(
123
+ urlMethodPair: UrlMethodPair | (() => UrlMethodPair),
124
+ data: TForm | (() => TForm),
125
+ ): InertiaPrecognitiveFormProps<TForm>;
126
+ export default function useForm<TForm extends FormDataType<TForm>>(
127
+ rememberKey: string,
128
+ data: TForm | (() => TForm),
129
+ ): InertiaFormProps<TForm>;
130
+ export default function useForm<TForm extends FormDataType<TForm>>(
131
+ data: TForm | (() => TForm),
132
+ ): InertiaFormProps<TForm>;
133
+ export default function useForm<TForm extends FormDataType<TForm>>(): InertiaFormProps<TForm>;
134
+ export default function useForm<TForm extends FormDataType<TForm>>(
135
+ ...args: UseFormArguments<TForm>
136
+ ): InertiaFormProps<TForm> | InertiaPrecognitiveFormProps<TForm> {
137
+ const publicArgs =
138
+ typeof args[args.length - 1] === 'symbol'
139
+ ? (args.slice(0, -1) as UseFormArguments<TForm>)
140
+ : args;
141
+ const { rememberKey, data, precognitionEndpoint } = UseFormUtils.parseUseFormArguments<TForm>(
142
+ ...publicArgs,
143
+ );
144
+
145
+ // Resolve initial data for remember functionality hooks
146
+ const initialDefaults = typeof data === 'function' ? cloneDeep(data()) : cloneDeep(data);
147
+
148
+ const cancelToken = useRef<CancelToken | null>(null, slots.cancelToken);
149
+ const excludeKeysRef = useRef<FormDataKeys<TForm>[]>([], slots.excludeKeys);
150
+ const pendingOptimisticRef = useRef<OptimisticCallback | null>(null, slots.pendingOptimistic);
151
+
152
+ // For remember functionality, we need custom state hooks
153
+ const useDataState = rememberKey
154
+ ? () => useRemember<TForm>(initialDefaults, `${rememberKey}:data`, excludeKeysRef)
155
+ : undefined;
156
+
157
+ const useErrorsState = rememberKey
158
+ ? () => useRemember<FormDataErrors<TForm>>({} as FormDataErrors<TForm>, `${rememberKey}:errors`)
159
+ : undefined;
160
+
161
+ const {
162
+ form: baseForm,
163
+ setDefaultsState,
164
+ transformRef,
165
+ precognitionEndpointRef,
166
+ dataRef,
167
+ isMounted,
168
+ setProcessing,
169
+ setProgress,
170
+ markAsSuccessful,
171
+ clearErrors,
172
+ setError,
173
+ defaultsCalledInOnSuccessRef,
174
+ resetBeforeSubmit,
175
+ finishProcessing,
176
+ } = useFormState<TForm>({
177
+ data,
178
+ precognitionEndpoint,
179
+ useDataState,
180
+ useErrorsState,
181
+ });
182
+
183
+ const submit = useCallback(
184
+ (...args: UseFormSubmitArguments) => {
185
+ const { method, url, options } = UseFormUtils.parseSubmitArguments(
186
+ args,
187
+ precognitionEndpointRef.current,
188
+ );
189
+
190
+ defaultsCalledInOnSuccessRef.current = false;
191
+
192
+ const _options: VisitOptions = {
193
+ ...options,
194
+ onCancelToken: (token) => {
195
+ cancelToken.current = token;
196
+
197
+ return options.onCancelToken?.(token);
198
+ },
199
+ onBefore: (visit) => {
200
+ resetBeforeSubmit();
201
+
202
+ return options.onBefore?.(visit);
203
+ },
204
+ onStart: (visit) => {
205
+ setProcessing(true);
206
+
207
+ return options.onStart?.(visit);
208
+ },
209
+ onProgress: (event) => {
210
+ setProgress(event || null);
211
+
212
+ return options.onProgress?.(event);
213
+ },
214
+ onSuccess: async (page) => {
215
+ if (isMounted.current) {
216
+ markAsSuccessful();
217
+ }
218
+
219
+ const onSuccess = options.onSuccess ? await options.onSuccess(page) : null;
220
+
221
+ if (isMounted.current && !defaultsCalledInOnSuccessRef.current) {
222
+ baseForm.setData((data: TForm) => {
223
+ setDefaultsState(cloneDeep(data));
224
+ return data;
225
+ });
226
+ }
227
+
228
+ return onSuccess;
229
+ },
230
+ onError: (errors) => {
231
+ if (isMounted.current) {
232
+ clearErrors();
233
+ setError(errors as FormDataErrors<TForm>);
234
+ }
235
+
236
+ return options.onError?.(errors);
237
+ },
238
+ onCancel: () => {
239
+ return options.onCancel?.();
240
+ },
241
+ onFinish: (visit) => {
242
+ if (isMounted.current) {
243
+ finishProcessing();
244
+ }
245
+
246
+ cancelToken.current = null;
247
+
248
+ return options.onFinish?.(visit);
249
+ },
250
+ };
251
+
252
+ _options.optimistic = _options.optimistic ?? pendingOptimisticRef.current ?? undefined;
253
+ pendingOptimisticRef.current = null;
254
+
255
+ const transformedData = transformRef.current(dataRef.current) as RequestPayload;
256
+
257
+ if (method === 'delete') {
258
+ router.delete(url, { ..._options, data: transformedData });
259
+ } else {
260
+ router[method](url, transformedData, _options);
261
+ }
262
+ },
263
+ [clearErrors, setError, transformRef],
264
+ slots.submit,
265
+ );
266
+
267
+ const cancel = useCallback(
268
+ () => {
269
+ if (cancelToken.current) {
270
+ cancelToken.current.cancel();
271
+ }
272
+ },
273
+ [],
274
+ slots.cancel,
275
+ );
276
+
277
+ const submitMethods = useMemo(
278
+ () => ({
279
+ get: (url: string, options: VisitOptions = {}) => submit('get', url, options),
280
+ post: (url: string, options: VisitOptions = {}) => submit('post', url, options),
281
+ put: (url: string, options: VisitOptions = {}) => submit('put', url, options),
282
+ patch: (url: string, options: VisitOptions = {}) => submit('patch', url, options),
283
+ delete: (url: string, options: VisitOptions = {}) => submit('delete', url, options),
284
+ }),
285
+ [submit],
286
+ slots.submitMethods,
287
+ );
288
+
289
+ // Add useForm-specific methods to the form object
290
+ Object.assign(baseForm, {
291
+ submit,
292
+ ...submitMethods,
293
+ cancel,
294
+ dontRemember: <K extends FormDataKeys<TForm>>(...keys: K[]) => {
295
+ excludeKeysRef.current = keys;
296
+ return form;
297
+ },
298
+
299
+ optimistic: <TProps>(callback: OptimisticCallback<TProps>) => {
300
+ pendingOptimisticRef.current = callback as OptimisticCallback;
301
+ return form;
302
+ },
303
+ });
304
+
305
+ // Cast to the full form type (baseForm now has submit methods)
306
+ const form = baseForm as unknown as InertiaFormProps<TForm>;
307
+
308
+ // Wrap withPrecognition to return the correct type with submit methods
309
+ const originalWithPrecognition = baseForm.withPrecognition;
310
+ form.withPrecognition = (
311
+ ...args: UseFormWithPrecognitionArguments
312
+ ): InertiaPrecognitiveFormProps<TForm> => {
313
+ originalWithPrecognition(...args);
314
+ return form as InertiaPrecognitiveFormProps<TForm>;
315
+ };
316
+
317
+ return precognitionEndpointRef.current ? (form as InertiaPrecognitiveFormProps<TForm>) : form;
318
+ }