@zerotal/inertia 1.0.0
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/CHANGELOG.md +17 -0
- package/LICENSE +21 -0
- package/README.md +140 -0
- package/package.json +80 -0
- package/src/PageRegistry.ts +101 -0
- package/src/SharedProps.ts +83 -0
- package/src/SsrHandler.ts +145 -0
- package/src/augment.ts +22 -0
- package/src/commands/InertiaBuildCommand.ts +82 -0
- package/src/commands/MakePageCommand.ts +207 -0
- package/src/commands/index.ts +3 -0
- package/src/config.ts +82 -0
- package/src/css.ts +7 -0
- package/src/errors.ts +40 -0
- package/src/facades/Inertia.ts +55 -0
- package/src/global.d.ts +70 -0
- package/src/historyState.ts +82 -0
- package/src/index.ts +102 -0
- package/src/inertia.ts +347 -0
- package/src/location.ts +35 -0
- package/src/middleware/InertiaMiddleware.ts +95 -0
- package/src/middleware/PrecognitionMiddleware.ts +33 -0
- package/src/props/PropTypes.ts +362 -0
- package/src/props/resolveProps.ts +189 -0
- package/src/provider/InertiaProvider.ts +154 -0
- package/src/route.ts +47 -0
- package/src/share.ts +65 -0
- package/src/ssr/renderPage.ts +114 -0
- package/src/types.ts +53 -0
- package/src/version.ts +22 -0
- package/src/vuePlugin.ts +144 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { Command } from "@zerotal/core";
|
|
2
|
+
import { pruneBuildOutput } from "@zerotal/core/dev";
|
|
3
|
+
import { generatePageRegistry } from "../PageRegistry.ts";
|
|
4
|
+
import { detectCssPlugins } from "../css.ts";
|
|
5
|
+
import { detectVuePlugin } from "../vuePlugin.ts";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* `inertia:build` — regenerate the page registry and bundle the frontend with
|
|
9
|
+
* `Bun.build` (code-split into per-page chunks) into `public/assets/`. Pass
|
|
10
|
+
* `--production` (`-p`) for a minified, source-map-free build.
|
|
11
|
+
*
|
|
12
|
+
* @category Build
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts
|
|
15
|
+
* // Dev build:
|
|
16
|
+
* // bun zt inertia:build
|
|
17
|
+
* // Production build:
|
|
18
|
+
* // bun zt inertia:build --production
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export class InertiaBuildCommand extends Command {
|
|
22
|
+
static override commandName = "inertia:build";
|
|
23
|
+
static override description =
|
|
24
|
+
"Build the Inertia frontend bundle and regenerate the page registry";
|
|
25
|
+
static override needsApp = false;
|
|
26
|
+
|
|
27
|
+
static override flags = [
|
|
28
|
+
{
|
|
29
|
+
name: "production",
|
|
30
|
+
short: "p",
|
|
31
|
+
type: "boolean" as const,
|
|
32
|
+
description: "Build for production (minified, no source maps)",
|
|
33
|
+
default: false,
|
|
34
|
+
},
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
override async run(): Promise<void> {
|
|
38
|
+
const isProd = this.flags["production"] as boolean;
|
|
39
|
+
const cwd = process.cwd();
|
|
40
|
+
|
|
41
|
+
// Step 1: Regenerate the page registry
|
|
42
|
+
this.section("Regenerating page registry...");
|
|
43
|
+
await generatePageRegistry(cwd);
|
|
44
|
+
this.info("Page registry updated.");
|
|
45
|
+
|
|
46
|
+
// Step 2: Build the frontend bundle with Bun.build
|
|
47
|
+
this.section("Building frontend bundle...");
|
|
48
|
+
|
|
49
|
+
const plugins = [...(await detectCssPlugins(cwd)), ...(await detectVuePlugin(cwd))];
|
|
50
|
+
const outdir = `${cwd}/public/assets`;
|
|
51
|
+
const result = await Bun.build({
|
|
52
|
+
entrypoints: [`${cwd}/resources/js/app.tsx`],
|
|
53
|
+
outdir,
|
|
54
|
+
target: "browser",
|
|
55
|
+
splitting: true, // REQUIRED: creates per-page chunks from dynamic imports
|
|
56
|
+
minify: isProd,
|
|
57
|
+
sourcemap: isProd ? "none" : "external",
|
|
58
|
+
...(plugins.length > 0 ? { plugins } : {}),
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
if (!result.success) {
|
|
62
|
+
for (const log of result.logs) {
|
|
63
|
+
this.error(String(log));
|
|
64
|
+
}
|
|
65
|
+
throw new Error("Frontend build failed.");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Chunks are named after their content, so the ones this build replaced
|
|
69
|
+
// would otherwise stay behind — and ship.
|
|
70
|
+
const removed = await pruneBuildOutput(outdir, result.outputs);
|
|
71
|
+
|
|
72
|
+
this.info(`Build complete: ${result.outputs.length} files → public/assets/`);
|
|
73
|
+
if (removed.length > 0) this.dim(` Removed ${removed.length} stale file(s).`);
|
|
74
|
+
|
|
75
|
+
this.table(
|
|
76
|
+
result.outputs.map((o) => [
|
|
77
|
+
o.path.replace(cwd, "").replace(/\\/g, "/"),
|
|
78
|
+
`${(o.size / 1024).toFixed(1)} KB`,
|
|
79
|
+
]) as [string, string][],
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { Command, config } from "@zerotal/core";
|
|
2
|
+
import { generatePageRegistry } from "../PageRegistry.ts";
|
|
3
|
+
import { DEFAULT_PAGES_DIR } from "../config.ts";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* `make:page` — scaffold a new Inertia page component under the configured pages dir
|
|
7
|
+
* and regenerate the page registry. The framework (React `.tsx` / Vue `.vue`) is
|
|
8
|
+
* auto-detected from the installed adapter unless forced with `--framework`; pass
|
|
9
|
+
* `--layout` to wrap the page in a persistent layout.
|
|
10
|
+
*
|
|
11
|
+
* @category Scaffolding
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* // bun zt make:page Users/Index
|
|
15
|
+
* // bun zt make:page Dashboard --layout MainLayout
|
|
16
|
+
* // bun zt make:page Settings --framework vue
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export class MakePageCommand extends Command {
|
|
20
|
+
static override commandName = "make:page";
|
|
21
|
+
static override description = "Create a new Inertia page component";
|
|
22
|
+
static override needsApp = false;
|
|
23
|
+
|
|
24
|
+
static override args = [
|
|
25
|
+
{
|
|
26
|
+
name: "name",
|
|
27
|
+
required: true,
|
|
28
|
+
description: "Page name, e.g. Dashboard or Users/Index",
|
|
29
|
+
},
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
static override flags = [
|
|
33
|
+
{
|
|
34
|
+
name: "layout",
|
|
35
|
+
type: "string" as const,
|
|
36
|
+
description: "Wrap the page in a persistent layout, e.g. --layout MainLayout",
|
|
37
|
+
default: "",
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: "framework",
|
|
41
|
+
type: "string" as const,
|
|
42
|
+
description: "Force the frontend framework: 'vue' or 'react' (auto-detected by default)",
|
|
43
|
+
default: "",
|
|
44
|
+
},
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
override async run(): Promise<void> {
|
|
48
|
+
const name = this.args["name"]!; // e.g. 'Users/Index'
|
|
49
|
+
const layout = (this.flags["layout"] as string | undefined) ?? "";
|
|
50
|
+
const pagesDir = config
|
|
51
|
+
.safe("inertia.pagesDir", DEFAULT_PAGES_DIR)
|
|
52
|
+
.replace(/\\/g, "/")
|
|
53
|
+
.replace(/\/+$/, "");
|
|
54
|
+
|
|
55
|
+
// Pick the framework: explicit --framework flag, else auto-detect from the
|
|
56
|
+
// installed Inertia adapter (@inertiajs/vue3 → vue, otherwise react).
|
|
57
|
+
const forced = (this.flags["framework"] as string | undefined)?.toLowerCase() ?? "";
|
|
58
|
+
const framework: Framework =
|
|
59
|
+
forced === "vue" || forced === "react"
|
|
60
|
+
? (forced as Framework)
|
|
61
|
+
: _detectFramework(process.cwd());
|
|
62
|
+
const ext = framework === "vue" ? "vue" : "tsx";
|
|
63
|
+
|
|
64
|
+
const path = `${pagesDir}/${name}.${ext}`;
|
|
65
|
+
|
|
66
|
+
if (await Bun.file(path).exists()) {
|
|
67
|
+
this.error(`File already exists: ${path}`);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const componentName = name.includes("/") ? name.split("/").pop()! : name;
|
|
72
|
+
|
|
73
|
+
// Compute how many levels deep the page is so the layout import is correct
|
|
74
|
+
const depth = name.split("/").length;
|
|
75
|
+
const prefix = "../".repeat(depth);
|
|
76
|
+
const stub =
|
|
77
|
+
framework === "vue"
|
|
78
|
+
? layout
|
|
79
|
+
? _vueLayoutPageStub(componentName, layout, prefix)
|
|
80
|
+
: _vuePageStub(componentName)
|
|
81
|
+
: layout
|
|
82
|
+
? _reactLayoutPageStub(componentName, layout, prefix)
|
|
83
|
+
: _reactPageStub(componentName);
|
|
84
|
+
|
|
85
|
+
await Bun.write(path, stub);
|
|
86
|
+
this.info(`Created: ${path}`);
|
|
87
|
+
|
|
88
|
+
if (layout) {
|
|
89
|
+
this.dim(` Layout: ${prefix}layouts/${layout}.${ext}`);
|
|
90
|
+
this.dim(" Adjust the import path if your layouts live elsewhere.");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
this.dim(" Regenerating page registry...");
|
|
94
|
+
await generatePageRegistry(process.cwd());
|
|
95
|
+
this.info("Page registry updated.");
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
type Framework = "vue" | "react";
|
|
100
|
+
|
|
101
|
+
/** Detect the frontend framework from the Inertia adapter installed in `cwd`. */
|
|
102
|
+
function _detectFramework(cwd: string): Framework {
|
|
103
|
+
try {
|
|
104
|
+
Bun.resolveSync("@inertiajs/vue3", cwd);
|
|
105
|
+
return "vue";
|
|
106
|
+
} catch {
|
|
107
|
+
return "react";
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function _reactPageStub(name: string): string {
|
|
112
|
+
return `import { Link, usePage } from '@inertiajs/react';
|
|
113
|
+
|
|
114
|
+
// Define the props this page receives from the controller
|
|
115
|
+
interface Props {
|
|
116
|
+
// TODO: add your props here
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export default function ${name}({}: Props) {
|
|
120
|
+
const { auth } = usePage<{ auth: { user: { name: string } | null } }>().props;
|
|
121
|
+
|
|
122
|
+
return (
|
|
123
|
+
<main>
|
|
124
|
+
<h1>${name}</h1>
|
|
125
|
+
{auth.user && <p>Hello, {auth.user.name}</p>}
|
|
126
|
+
<Link href="/">Home</Link>
|
|
127
|
+
</main>
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
`;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function _reactLayoutPageStub(name: string, layout: string, prefix: string): string {
|
|
134
|
+
return `import type { ReactNode } from 'react';
|
|
135
|
+
import { Link } from '@inertiajs/react';
|
|
136
|
+
// Adjust the path if your layouts directory is elsewhere
|
|
137
|
+
import ${layout} from '${prefix}layouts/${layout}.tsx';
|
|
138
|
+
|
|
139
|
+
// Define the props this page receives from the controller
|
|
140
|
+
interface Props {
|
|
141
|
+
// TODO: add your props here
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function ${name}({}: Props) {
|
|
145
|
+
return (
|
|
146
|
+
<main>
|
|
147
|
+
<h1>${name}</h1>
|
|
148
|
+
<Link href="/">Home</Link>
|
|
149
|
+
</main>
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Inertia persistent layout — preserved across client-side navigations.
|
|
154
|
+
// The layout renders once; only the page content re-renders on navigation.
|
|
155
|
+
(${name} as { layout?: (page: ReactNode) => ReactNode }).layout =
|
|
156
|
+
(page) => <${layout}>{page}</${layout}>;
|
|
157
|
+
|
|
158
|
+
export default ${name};
|
|
159
|
+
`;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function _vuePageStub(name: string): string {
|
|
163
|
+
return `<script setup lang="ts">
|
|
164
|
+
import { Link, usePage } from '@inertiajs/vue3';
|
|
165
|
+
|
|
166
|
+
const page = usePage<{ auth: { user: { name: string } | null } }>();
|
|
167
|
+
|
|
168
|
+
// Define the props this page receives from the controller
|
|
169
|
+
defineProps<{
|
|
170
|
+
// TODO: add your props here
|
|
171
|
+
}>();
|
|
172
|
+
</script>
|
|
173
|
+
|
|
174
|
+
<template>
|
|
175
|
+
<main>
|
|
176
|
+
<h1>${name}</h1>
|
|
177
|
+
<p v-if="page.props.auth.user">Hello, {{ page.props.auth.user.name }}</p>
|
|
178
|
+
<Link href="/">Home</Link>
|
|
179
|
+
</main>
|
|
180
|
+
</template>
|
|
181
|
+
`;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function _vueLayoutPageStub(name: string, layout: string, prefix: string): string {
|
|
185
|
+
return `<script setup lang="ts">
|
|
186
|
+
import { Link } from '@inertiajs/vue3';
|
|
187
|
+
// Adjust the path if your layouts directory is elsewhere
|
|
188
|
+
import ${layout} from '${prefix}layouts/${layout}.vue';
|
|
189
|
+
|
|
190
|
+
// Inertia persistent layout — preserved across client-side navigations.
|
|
191
|
+
// The layout renders once; only the page content re-renders on navigation.
|
|
192
|
+
defineOptions({ layout: ${layout} });
|
|
193
|
+
|
|
194
|
+
// Define the props this page receives from the controller
|
|
195
|
+
defineProps<{
|
|
196
|
+
// TODO: add your props here
|
|
197
|
+
}>();
|
|
198
|
+
</script>
|
|
199
|
+
|
|
200
|
+
<template>
|
|
201
|
+
<main>
|
|
202
|
+
<h1>${name}</h1>
|
|
203
|
+
<Link href="/">Home</Link>
|
|
204
|
+
</main>
|
|
205
|
+
</template>
|
|
206
|
+
`;
|
|
207
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { deepMerge } from "@zerotal/core";
|
|
2
|
+
|
|
3
|
+
export interface InertiaConfigShape {
|
|
4
|
+
/** Path to the HTML template file. Default: './resources/app.html' */
|
|
5
|
+
htmlTemplate: string;
|
|
6
|
+
/** Asset version string for cache-busting. Default: '1' */
|
|
7
|
+
version: string;
|
|
8
|
+
/** Public URL prefix for built assets. Default: '/' */
|
|
9
|
+
assetsUrl: string;
|
|
10
|
+
/**
|
|
11
|
+
* Directory (relative to the project root) where Inertia page components live.
|
|
12
|
+
* Used by the page-registry generator, the SSR handler, and `inertiaStream()`.
|
|
13
|
+
* Default: 'resources/js/pages'
|
|
14
|
+
*/
|
|
15
|
+
pagesDir: string;
|
|
16
|
+
/**
|
|
17
|
+
* Enable server-side rendering.
|
|
18
|
+
*
|
|
19
|
+
* When true, InertiaProvider registers POST /__ssr which accepts
|
|
20
|
+
* { component, props, url } and returns { body, head } - the same
|
|
21
|
+
* contract as the Inertia Node SSR server. The Inertia client calls
|
|
22
|
+
* this endpoint when rendering the first page load on the server.
|
|
23
|
+
*
|
|
24
|
+
* Pages are rendered with the framework they're authored in: React `.tsx`
|
|
25
|
+
* via `react-dom/server`, or Vue `.vue` via `@inertiajs/vue3` + `vue/server-renderer`.
|
|
26
|
+
* Install the server renderer for the framework(s) the app uses.
|
|
27
|
+
*
|
|
28
|
+
* Default: false
|
|
29
|
+
*/
|
|
30
|
+
ssr: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Shared secret required to reach `POST /__ssr` from off-box.
|
|
33
|
+
*
|
|
34
|
+
* The endpoint is loopback-only by default, because it takes an arbitrary component name
|
|
35
|
+
* and a props bag and does real rendering work with them — upstream Inertia runs SSR as a
|
|
36
|
+
* separate process on a private port for exactly this reason. Set this (and send it as
|
|
37
|
+
* `X-Inertia-SSR-Secret`) only when the renderer runs on another host.
|
|
38
|
+
*
|
|
39
|
+
* Default: `""` — loopback only.
|
|
40
|
+
*/
|
|
41
|
+
ssrSecret: string;
|
|
42
|
+
/**
|
|
43
|
+
* Encrypt browser history state by default for every page. Individual pages can still opt in/out
|
|
44
|
+
* per request via `Inertia.encryptHistory()` / `clearHistory()`. Default: false.
|
|
45
|
+
*/
|
|
46
|
+
encryptHistory: boolean;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Default directory (relative to the project root) for Inertia page components. */
|
|
50
|
+
export const DEFAULT_PAGES_DIR = "resources/js/pages";
|
|
51
|
+
|
|
52
|
+
const defaults: InertiaConfigShape = {
|
|
53
|
+
htmlTemplate: "./resources/app.html",
|
|
54
|
+
version: "1",
|
|
55
|
+
assetsUrl: "/",
|
|
56
|
+
pagesDir: DEFAULT_PAGES_DIR,
|
|
57
|
+
ssr: false,
|
|
58
|
+
ssrSecret: "",
|
|
59
|
+
encryptHistory: false,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Create a typed Inertia configuration object with defaults.
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* import { InertiaConfig } from '@zerotal/inertia';
|
|
67
|
+
* export default InertiaConfig({
|
|
68
|
+
* htmlTemplate: './resources/app.html',
|
|
69
|
+
* version: Bun.env['ASSET_VERSION'] ?? '1',
|
|
70
|
+
* ssr: true, // enable SSR endpoint at POST /__ssr
|
|
71
|
+
* });
|
|
72
|
+
*/
|
|
73
|
+
export function InertiaConfig(options: Partial<InertiaConfigShape> = {}): InertiaConfigShape {
|
|
74
|
+
return deepMerge(defaults, options);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Register this package's config namespace for typed config() dot-paths.
|
|
78
|
+
declare module "@zerotal/core" {
|
|
79
|
+
interface ConfigRegistry {
|
|
80
|
+
inertia: InertiaConfigShape;
|
|
81
|
+
}
|
|
82
|
+
}
|
package/src/css.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CSS plugin detection for Inertia's Bun.build() pipeline.
|
|
3
|
+
*
|
|
4
|
+
* Re-exports from @zerotal/core so Pulse and other packages can share the same
|
|
5
|
+
* detection logic without creating cross-package dependencies.
|
|
6
|
+
*/
|
|
7
|
+
export { detectCssPlugins } from "@zerotal/core/dev";
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { ZerotalError } from "@zerotal/core";
|
|
2
|
+
|
|
3
|
+
/** Base class for all @zerotal/inertia errors. */
|
|
4
|
+
export class InertiaError extends ZerotalError {
|
|
5
|
+
constructor(
|
|
6
|
+
message: string,
|
|
7
|
+
code = "E_INERTIA",
|
|
8
|
+
status = 500,
|
|
9
|
+
context?: Record<string, unknown>,
|
|
10
|
+
) {
|
|
11
|
+
super(message, code, status, context);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Thrown when `inertia()` / `inertiaStream()` runs before the HTML template has been
|
|
17
|
+
* loaded — almost always because `InertiaProvider` is not registered.
|
|
18
|
+
*/
|
|
19
|
+
export class InertiaTemplateNotLoadedError extends InertiaError {
|
|
20
|
+
constructor() {
|
|
21
|
+
super(
|
|
22
|
+
"[Zerotal Inertia] HTML template not loaded. Did you register InertiaProvider in bootstrap/app.ts?",
|
|
23
|
+
"E_INERTIA_TEMPLATE_NOT_LOADED",
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Thrown when a component name is unsafe (path traversal) or has no resolvable default export. */
|
|
29
|
+
export class InvalidComponentError extends InertiaError {
|
|
30
|
+
constructor(component: string, reason = "is not a valid component") {
|
|
31
|
+
super(
|
|
32
|
+
`[Zerotal Inertia] Invalid component name '${component}': ${reason}.`,
|
|
33
|
+
"E_INERTIA_INVALID_COMPONENT",
|
|
34
|
+
500,
|
|
35
|
+
{
|
|
36
|
+
component,
|
|
37
|
+
},
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { inertia, inertiaStream } from "../inertia.ts";
|
|
2
|
+
import { optional, lazy, always, defer, merge, deepMerge, scroll } from "../props/PropTypes.ts";
|
|
3
|
+
import { share } from "../share.ts";
|
|
4
|
+
import { encryptHistory, clearHistory } from "../historyState.ts";
|
|
5
|
+
import { location } from "../location.ts";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Unified Inertia facade.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* import { Inertia } from "@zerotal/inertia";
|
|
12
|
+
*
|
|
13
|
+
* async index(http: HttpContext) {
|
|
14
|
+
* return Inertia.render("Users/Index", {
|
|
15
|
+
* users: Inertia.optional(() => User.all()), // only on partial reload
|
|
16
|
+
* stats: Inertia.defer(() => computeStats()), // loaded after first paint
|
|
17
|
+
* feed: Inertia.merge(() => Post.paginate()), // append on "load more"
|
|
18
|
+
* });
|
|
19
|
+
* }
|
|
20
|
+
*
|
|
21
|
+
* Standalone helpers (`optional`, `always`, `defer`, `merge`, `deepMerge`, `lazy`) are also exported
|
|
22
|
+
* for direct import if you prefer not to go through the facade.
|
|
23
|
+
*/
|
|
24
|
+
export const Inertia = {
|
|
25
|
+
/** Render an Inertia page (= the `inertia()` helper). */
|
|
26
|
+
render: inertia,
|
|
27
|
+
/** Render an Inertia page using React streaming SSR. */
|
|
28
|
+
stream: inertiaStream,
|
|
29
|
+
|
|
30
|
+
/** Prop only included when explicitly requested via a partial reload's `only`. */
|
|
31
|
+
optional,
|
|
32
|
+
/** Alias of {@link optional}. */
|
|
33
|
+
lazy,
|
|
34
|
+
/** Prop always included, even on partial reloads that would otherwise exclude it. */
|
|
35
|
+
always,
|
|
36
|
+
/** Prop deferred to a follow-up request after the initial render. */
|
|
37
|
+
defer,
|
|
38
|
+
/** Prop the client appends (merges) into existing data on partial reloads. */
|
|
39
|
+
merge,
|
|
40
|
+
/** Prop the client deep-merges into existing data on partial reloads. */
|
|
41
|
+
deepMerge,
|
|
42
|
+
/** Infinite-scroll prop: merges paginated data and emits scroll metadata. */
|
|
43
|
+
scroll,
|
|
44
|
+
|
|
45
|
+
/** Register shared props included on every page. */
|
|
46
|
+
share,
|
|
47
|
+
|
|
48
|
+
/** Encrypt the current page's browser history state. */
|
|
49
|
+
encryptHistory,
|
|
50
|
+
/** Clear any encrypted history state (e.g. on logout). */
|
|
51
|
+
clearHistory,
|
|
52
|
+
|
|
53
|
+
/** External redirect (full-page visit). */
|
|
54
|
+
location,
|
|
55
|
+
} as const;
|
package/src/global.d.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// Ambient declarations specific to this package.
|
|
2
|
+
// Bun, Node (node:*), and bun:test types come from @types/bun (→ bun-types).
|
|
3
|
+
// Only declarations bun-types does NOT provide are kept here.
|
|
4
|
+
|
|
5
|
+
// ── Bun globals ───────────────────────────────────────────────────────────
|
|
6
|
+
interface Request {
|
|
7
|
+
readonly params?: Record<string, string>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// ── SQLInstance ───────────────────────────────────────────────────────────
|
|
11
|
+
interface SQLInstance {
|
|
12
|
+
<T = Record<string, unknown>>(
|
|
13
|
+
strings: TemplateStringsArray,
|
|
14
|
+
...values: unknown[]
|
|
15
|
+
): Promise<T[]>;
|
|
16
|
+
begin<T>(fn: (tx: SQLInstance) => Promise<T>): Promise<T>;
|
|
17
|
+
end(): Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// ── React type stubs ──────────────────────────────────────────────────────
|
|
21
|
+
declare module 'react' {
|
|
22
|
+
export function createElement(type: unknown, props?: unknown, ...children: unknown[]): unknown;
|
|
23
|
+
export const version: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
declare module 'react-dom/client' {
|
|
27
|
+
export function createRoot(container: Element | null): { render(el: unknown): void };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
declare module 'react-dom/server' {
|
|
31
|
+
/** Render a React element to an HTML string (SSR). */
|
|
32
|
+
export function renderToString(element: unknown): string;
|
|
33
|
+
/** Render a React element to a static HTML string (no data-react attributes). */
|
|
34
|
+
export function renderToStaticMarkup(element: unknown): string;
|
|
35
|
+
/** Render a React element to a streaming ReadableStream (React 18+). */
|
|
36
|
+
export function renderToReadableStream(
|
|
37
|
+
element: unknown,
|
|
38
|
+
options?: {
|
|
39
|
+
signal?: AbortSignal;
|
|
40
|
+
onError?: (error: unknown) => void;
|
|
41
|
+
},
|
|
42
|
+
): Promise<ReadableStream<Uint8Array>>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ── @inertiajs/react type stub ────────────────────────────────────────────
|
|
46
|
+
declare module '@inertiajs/react' {
|
|
47
|
+
export function createInertiaApp(options: {
|
|
48
|
+
resolve: (name: string) => unknown | Promise<unknown>;
|
|
49
|
+
setup: (args: { el: Element; App: unknown; props: unknown }) => void;
|
|
50
|
+
}): Promise<void>;
|
|
51
|
+
|
|
52
|
+
export function usePage<T = Record<string, unknown>>(): { props: T };
|
|
53
|
+
export function Link(props: { href: string; children?: unknown }): unknown;
|
|
54
|
+
export const router: {
|
|
55
|
+
visit(url: string): void;
|
|
56
|
+
post(url: string, data?: unknown): void;
|
|
57
|
+
put(url: string, data?: unknown): void;
|
|
58
|
+
delete(url: string): void;
|
|
59
|
+
};
|
|
60
|
+
export function useForm<T extends Record<string, unknown>>(initial: T): {
|
|
61
|
+
data: T;
|
|
62
|
+
errors: Partial<Record<keyof T, string>>;
|
|
63
|
+
processing: boolean;
|
|
64
|
+
setData(key: keyof T, value: unknown): void;
|
|
65
|
+
post(url: string): void;
|
|
66
|
+
put(url: string): void;
|
|
67
|
+
delete(url: string): void;
|
|
68
|
+
reset(): void;
|
|
69
|
+
};
|
|
70
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { RequestContext } from "@zerotal/core";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Per-request Inertia history-encryption flags.
|
|
5
|
+
*
|
|
6
|
+
* The server does not encrypt anything itself — encryption happens client-side. The server's job is
|
|
7
|
+
* to set the `encryptHistory` / `clearHistory` flags on the page object; the Inertia client reacts
|
|
8
|
+
* to them. Flags are stored per-request (keyed by the active HttpContext) so concurrent requests
|
|
9
|
+
* don't interfere.
|
|
10
|
+
*/
|
|
11
|
+
interface HistoryFlags {
|
|
12
|
+
encrypt?: boolean;
|
|
13
|
+
clear?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const _store = new WeakMap<object, HistoryFlags>();
|
|
17
|
+
let _defaultEncrypt = false;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Set the global default for history encryption (from `inertia.encryptHistory` config).
|
|
21
|
+
*
|
|
22
|
+
* @param on - When `true`, every page encrypts its history state unless overridden per request.
|
|
23
|
+
* @internal
|
|
24
|
+
*/
|
|
25
|
+
export function setHistoryEncryptionDefault(on: boolean): void {
|
|
26
|
+
_defaultEncrypt = on;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function flagsForCurrent(): HistoryFlags {
|
|
30
|
+
const ctx = RequestContext.get() as unknown as object;
|
|
31
|
+
let flags = _store.get(ctx);
|
|
32
|
+
if (!flags) {
|
|
33
|
+
flags = {};
|
|
34
|
+
_store.set(ctx, flags);
|
|
35
|
+
}
|
|
36
|
+
return flags;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Encrypt the current page's history state in the browser (protects props cached in
|
|
41
|
+
* `history.state` for a page holding sensitive data). Effective for the current request.
|
|
42
|
+
*
|
|
43
|
+
* @param on - Pass `false` to opt this page out when encryption is the global default. Default `true`.
|
|
44
|
+
* @example
|
|
45
|
+
* ```ts
|
|
46
|
+
* async show(http: HttpContext): Promise<void> {
|
|
47
|
+
* Inertia.encryptHistory();
|
|
48
|
+
* return inertia('Account/Settings', { account });
|
|
49
|
+
* }
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
export function encryptHistory(on = true): void {
|
|
53
|
+
flagsForCurrent().encrypt = on;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Clear any previously encrypted history state — call on logout so cached page data
|
|
58
|
+
* can't be recovered via the browser Back button.
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* ```ts
|
|
62
|
+
* async destroy(http: HttpContext): Promise<void> {
|
|
63
|
+
* await Auth.logout();
|
|
64
|
+
* Inertia.clearHistory();
|
|
65
|
+
* return redirect('/login');
|
|
66
|
+
* }
|
|
67
|
+
* ```
|
|
68
|
+
*/
|
|
69
|
+
export function clearHistory(): void {
|
|
70
|
+
flagsForCurrent().clear = true;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** @internal Resolve the effective flags for the current request, applying the global default. */
|
|
74
|
+
export function readHistoryFlags(): { encryptHistory?: boolean; clearHistory?: boolean } {
|
|
75
|
+
const ctx = RequestContext.tryGet() as unknown as object | undefined;
|
|
76
|
+
const flags = ctx ? _store.get(ctx) : undefined;
|
|
77
|
+
const encrypt = flags?.encrypt ?? _defaultEncrypt;
|
|
78
|
+
const out: { encryptHistory?: boolean; clearHistory?: boolean } = {};
|
|
79
|
+
if (encrypt) out.encryptHistory = true;
|
|
80
|
+
if (flags?.clear) out.clearHistory = true;
|
|
81
|
+
return out;
|
|
82
|
+
}
|