@dudousxd/nestjs-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 ADDED
@@ -0,0 +1,139 @@
1
+ # Changelog — @dudousxd/nestjs-inertia
2
+
3
+ ## 1.0.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [`c5878e3`](https://github.com/DavideCarvalho/nestjs-inertia/commit/c5878e3f8827d9e89710df0154ea76996b6db62a) - First public release — Inertia.js v3 adapter for NestJS.
8
+
9
+ - Core: InertiaModule.forRoot/forRootAsync/forFeature, @Inertia decorator, Inertia.optional/defer/merge/always markers, CSRF with tokenContext, SSR support, Express + Fastify adapters
10
+ - Vite: setupInertiaVite + nestInertia plugin, @inertia/@vite/@inertiaHead shell directives
11
+ - Codegen: nestjs-inertia init (full scaffold + auto-patch), auto-watch in dev, static AST discovery, class-validator DTO support, Route/Path type helpers, @As hierarchical naming
12
+ - Client: defineContract + @ApplyContract, typed Link for React/Vue/Svelte with context providers, createFetcher, SSR hydration, rich error messages
13
+ - Testing: expectInertia matchers, assertInertia, InertiaTestingModule, fakes
14
+
15
+ For the full repository changelog see [`../../CHANGELOG.md`](../../CHANGELOG.md).
16
+
17
+ ## 0.9.0-alpha.0 — 2026-05-22
18
+
19
+ ### BREAKING CHANGE — Removed unimplemented CodegenOptions fields
20
+
21
+ `CodegenOptions.configFile` and `CodegenOptions.debounceMs` have been removed.
22
+ Both fields were declared in the public type but were never read by the implementation —
23
+ setting them had no effect. They were never functional, so this removal is strictly
24
+ a type cleanup. If you referenced these fields in your code, simply remove them.
25
+
26
+ ### BREAKING CHANGE — Inertia v3 protocol
27
+
28
+ Four changes to align with the Inertia.js v3 wire protocol:
29
+
30
+ 1. **Shell HTML format** — `@inertia` directive now emits `<div id="app"></div><script id="inertia-page" type="application/json">…</script>` instead of `<div id="app" data-page="…">`. Clients must be on Inertia v3+.
31
+ 2. **`clearHistory` / `encryptHistory` omitted when falsy** — these page-level properties are no longer sent on the wire when their value is `false`/`undefined`, matching the v3 spec.
32
+ 3. **`Inertia.lazy()` deprecated** — renamed to `Inertia.optional()`; the `lazy` alias is kept for backwards compatibility and logs a deprecation warning at runtime.
33
+ 4. **Nested partial-reload dot-notation** — `only`/`except` arrays now support dot-notation paths (e.g. `"user.profile"`) to target nested props.
34
+
35
+ ### Changed
36
+
37
+ - Version bump to `0.9.0-alpha.0`
38
+
39
+ ## 0.8.0-alpha.0 — 2026-05-22
40
+
41
+ ### Added
42
+
43
+ - **Auto-bootstrap codegen** — `InertiaModule` implements `OnApplicationBootstrap`; codegen is triggered automatically at app startup when `autoCodegen` is enabled in the module options
44
+ - **`RegistryRoutes` helper** — new export enabling module augmentation for typed route names and params; consumers import `RegistryRoutes` to get the full typed route map from the augmented `InertiaRegistry`
45
+ - **Probe loop fix** — `NESTJS_INERTIA_CODEGEN_PROBE` env guard prevents `OnApplicationBootstrap` from re-triggering codegen when the app is bootstrapped by the codegen probe itself
46
+
47
+ ### Changed
48
+
49
+ - Version bump to `0.8.0-alpha.0`
50
+
51
+ ## 0.7.0-alpha.0 — 2026-05-22
52
+
53
+ ### Changed
54
+
55
+ - Bundled with example app, CI workflows, Changesets, MIT LICENSE, and slim docs.
56
+
57
+ ## [0.6.0-alpha.0] - 2026-05-22
58
+
59
+ ### Changed
60
+
61
+ - Version bump to `0.6.0-alpha.0` (monorepo coordination with Plan D client release; no source changes)
62
+
63
+ ## [0.5.0-alpha.0] - 2026-05-22
64
+
65
+ ### Added
66
+
67
+ - `InertiaRegistry` interface — empty extensible interface for codegen-driven module augmentation. Augment it in your project to get typed page names and route params.
68
+
69
+ ### Changed
70
+
71
+ - Version bump to `0.5.0-alpha.0` (monorepo coordination with codegen release)
72
+
73
+ ## [0.4.0-alpha.0] - 2026-05-22
74
+
75
+ ### Added (companion packages)
76
+
77
+ - `@dudousxd/nestjs-inertia-vite@0.1.0-alpha.0` — Vite dev/build helpers + plugin (`nestInertia({ ssr, react|vue|svelte, ... })`)
78
+ - `@dudousxd/nestjs-inertia-testing@0.1.0-alpha.0` — `expectInertia(res)` fluent matchers + `assertInertia(payload)` + `createFakeInertiaRequest/Response` + `InertiaTestingModule.forTest()` + Jest/Vitest `expect.extend` integration
79
+
80
+ ### Changed
81
+
82
+ - `@dudousxd/nestjs-inertia` core bumped to `0.4.0-alpha.0` (monorepo coordination; no source changes)
83
+
84
+ ## [0.3.0-alpha.0] - 2026-05-22
85
+
86
+ ### Added
87
+
88
+ - `InertiaModule.forFeature({ scope })` and `forFeatureAsync` — multi-app support
89
+ - `@UseInertia('scope')` decorator (class + method level) for selecting scope
90
+ - `InertiaScopeSwitcherInterceptor` — auto-installed; replaces `req.inertia` with scoped service
91
+ - 4 template engine adapters: Handlebars, EJS, Pug, LiquidJS (peer deps, all optional)
92
+ - `MissingTemplateEngineDepException` with installation hint
93
+ - CSRF: `CsrfCookieInterceptor` (writes XSRF-TOKEN cookie) + `CsrfGuard` (validates X-XSRF-TOKEN header)
94
+ - `generateCsrfToken` / `verifyCsrfToken` HMAC-SHA256 helpers
95
+ - `MissingCookieDepException` + `InvalidCsrfTokenException` (extends `ForbiddenException` → 403)
96
+ - Fastify adapter — full parity with Express: `fastifyAdapter`, `registerFastifyInertia` (decorateRequest + onRequest), `registerFastifyMethodSpoof` (preHandler)
97
+ - Platform detection in `InertiaModule.onApplicationBootstrap` via `HttpAdapterHost.httpAdapter.getType()`
98
+ - `InertiaAuthGuard` is now platform-aware (Express `redirect(status, url)` vs Fastify `redirect(url, status)`)
99
+ - `RedirectInterceptor` patches `reply.code()` on Fastify for `@Res()` handlers that send manually
100
+
101
+ ### Pending for future plans
102
+
103
+ - Companion packages: `@dudousxd/nestjs-inertia-vite` (Plan B), `-testing` (Plan B), `-codegen` (Plan C), `-client` (Plan D — Tuyau-style)
104
+ - Examples + docs site + CI workflows (Plan E)
105
+
106
+ ## [0.2.0-alpha.0] - 2026-05-22
107
+
108
+ ### Added
109
+
110
+ - `InertiaModule.forRootAsync()` with `useFactory + inject`, `useClass`, `useExisting` paths
111
+ - `@Inertia('Page')` decorator + `InertiaRenderInterceptor` (coexists with imperative `req.inertia.render()`)
112
+ - `Inertia.once()` prop marker (resolves once per session, refreshed via `X-Inertia-Reset-Once`)
113
+ - `RedirectInterceptor` — auto 302→303 for PUT/PATCH/DELETE Inertia requests
114
+ - `MethodSpoofMiddleware` — `_method=PUT/PATCH/DELETE` override on POST + multipart
115
+ - Shell HTML directives in file-based `rootView`: `@inertia`, `@inertiaHead`, `@vite('entry')`, `@viteRefresh`, `@asset('path')`
116
+ - Real SSR loader (dynamic `import(pathToFileURL)`, cache, `throwOnError`)
117
+ - `InertiaAuthGuard` (409 vs 302 based on X-Inertia header, return_to preserved)
118
+ - `InertiaNotFoundFilter` (JSON for `/api/*`, Inertia 'NotFound' page elsewhere)
119
+ - `ErrorBagInterceptor` (`X-Inertia-Error-Bag` namespaces props.errors)
120
+ - `FlashStore` interface (pluggable session-errors source, default no-op)
121
+ - `X-Inertia-Partial-Except` header
122
+ - `X-Inertia-Reset` header
123
+ - Dot-notation top-level prop unpacking
124
+ - Plain (non-marker) function props auto-invoked and awaited
125
+
126
+ ### Changed
127
+
128
+ - `undefined` values in props converted to `null` on the JSON wire (Laravel parity)
129
+ - `Inertia` is now a callable function (decorator) AND retains namespace methods
130
+
131
+ ### Pending for 0.3.0 (Plan A.3)
132
+
133
+ - `forFeature` / multi-app, template engines, CSRF, Fastify adapter
134
+
135
+ ## [0.1.0-alpha.0] - 2026-05-22
136
+
137
+ ### Added
138
+
139
+ - Initial release with core Express Inertia v2 protocol, partial reloads, prop markers, SSR stub, manifest/asset version providers, suppressPostSendWrites helper
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Davide Carvalho
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,317 @@
1
+ # @dudousxd/nestjs-inertia
2
+
3
+ > Inertia.js v3 adapter for NestJS — Express + Fastify, multi-app via `forFeature`, 4 template engines, CSRF protection, full Inertia v3 protocol parity.
4
+
5
+ [![npm](https://img.shields.io/npm/v/@dudousxd/nestjs-inertia.svg)](https://npmjs.com/package/@dudousxd/nestjs-inertia)
6
+ [![license](https://img.shields.io/npm/l/@dudousxd/nestjs-inertia.svg)](https://github.com/DavideCarvalho/nestjs-inertia/blob/main/LICENSE)
7
+
8
+ > **Status: alpha.** API is stabilising but may change before `1.0`. Not recommended for production use yet.
9
+
10
+ ## Install
11
+
12
+ ```bash
13
+ pnpm add @dudousxd/nestjs-inertia
14
+
15
+ # Pick your HTTP platform
16
+ pnpm add express @types/express # Express adapter (default)
17
+ # OR
18
+ pnpm add fastify @nestjs/platform-fastify @fastify/cookie # Fastify adapter
19
+ ```
20
+
21
+ ## Quick start
22
+
23
+ ```ts
24
+ // app.module.ts
25
+ import { Module } from '@nestjs/common';
26
+ import { InertiaModule } from '@dudousxd/nestjs-inertia';
27
+
28
+ @Module({
29
+ imports: [
30
+ InertiaModule.forRoot({
31
+ version: () => process.env.ASSET_VERSION ?? 'dev',
32
+ rootView: 'inertia/root.html',
33
+ share: async (req) => ({ auth: req.user ?? null }),
34
+ }),
35
+ ],
36
+ })
37
+ export class AppModule {}
38
+ ```
39
+
40
+ `inertia/root.html`:
41
+
42
+ ```html
43
+ <!doctype html>
44
+ <html lang="en">
45
+ <head>
46
+ <meta charset="utf-8" />
47
+ <title>My App</title>
48
+ @inertiaHead
49
+ @vite('app/client.tsx')
50
+ @viteRefresh
51
+ </head>
52
+ <body>
53
+ @inertia
54
+ </body>
55
+ </html>
56
+ ```
57
+
58
+ ## Controllers
59
+
60
+ Two equivalent patterns coexist:
61
+
62
+ ```ts
63
+ import { Controller, Get, Req } from '@nestjs/common';
64
+ import { Inertia } from '@dudousxd/nestjs-inertia';
65
+ import type { Request } from 'express';
66
+
67
+ @Controller()
68
+ export class HomeController {
69
+ // Decorator pattern (idiomatic for new code)
70
+ @Get('/')
71
+ @Inertia('Home')
72
+ show() {
73
+ return { hello: 'world' };
74
+ }
75
+
76
+ // Imperative pattern (use when you need fine control)
77
+ @Get('/crew')
78
+ async list(@Req() req: Request) {
79
+ await req.inertia
80
+ .share({ flash: req.session?.flash ?? {} })
81
+ .render('Crew', { crew: await this.svc.list() });
82
+ }
83
+ }
84
+ ```
85
+
86
+ ## Async config
87
+
88
+ ```ts
89
+ InertiaModule.forRootAsync({
90
+ imports: [ConfigModule],
91
+ inject: [ConfigService],
92
+ useFactory: (cfg: ConfigService) => ({
93
+ version: cfg.get('ASSET_VERSION'),
94
+ rootView: 'inertia/root.html',
95
+ }),
96
+ });
97
+ ```
98
+
99
+ Three paths: `useFactory + inject` (most common), `useClass`, `useExisting`.
100
+
101
+ ## Multi-app with `forFeature`
102
+
103
+ Host two or more Inertia apps in the same NestJS process — each with its own Vite entry, shell, version, share, SSR bundle. Useful for admin panels, multi-tenant white-label, or migrations between frontend stacks.
104
+
105
+ ```ts
106
+ @Module({
107
+ imports: [
108
+ InertiaModule.forRoot({ // main app
109
+ vite: { entry: 'app/client.tsx' },
110
+ rootView: 'inertia/root.html',
111
+ share: req => ({ auth: req.user }),
112
+ }),
113
+ InertiaModule.forFeature({ // admin app
114
+ scope: 'admin',
115
+ vite: { entry: 'admin/client.tsx' },
116
+ rootView: 'inertia/admin-root.html',
117
+ share: req => ({ admin: req.adminContext }),
118
+ }),
119
+ ],
120
+ })
121
+ export class AppModule {}
122
+ ```
123
+
124
+ Select scope per controller / method with `@UseInertia('scope')`:
125
+
126
+ ```ts
127
+ @Controller('admin')
128
+ @UseInertia('admin')
129
+ export class AdminDashboardController {
130
+ @Get('/')
131
+ @Inertia('AdminDashboard')
132
+ show() { return { stats: ... }; }
133
+ }
134
+ ```
135
+
136
+ `forFeatureAsync` works the same as `forRootAsync` (useFactory/useClass/useExisting). Reserved scope: `'default'` is owned by `forRoot()`.
137
+
138
+ ## Template engines
139
+
140
+ `rootView` accepts `.html` (own parser) plus `.hbs` / `.ejs` / `.pug` / `.liquid` if the engine package is installed:
141
+
142
+ ```bash
143
+ pnpm add handlebars # or: ejs, pug, liquidjs
144
+ ```
145
+
146
+ ```ts
147
+ InertiaModule.forRoot({
148
+ rootView: 'inertia/root.hbs', // auto-detects Handlebars
149
+ });
150
+ ```
151
+
152
+ Each engine sees locals `{ page, inertia, inertiaHead, vite, viteRefresh, asset }`. Use the engine's own escape rules (e.g., `{{{inertia}}}` triple-stache in Handlebars, `<%- inertia %>` in EJS, `!= inertia` in Pug). The `@inertia`/`@vite`/`@asset` directives are also processed on the engine's output, so you can mix and match.
153
+
154
+ ## CSRF
155
+
156
+ ```ts
157
+ import { CsrfCookieInterceptor, CsrfGuard } from '@dudousxd/nestjs-inertia';
158
+
159
+ // Global cookie writer
160
+ app.useGlobalInterceptors(new CsrfCookieInterceptor({ secret: process.env.CSRF_SECRET }));
161
+
162
+ // Per-route validation
163
+ @UseGuards(new CsrfGuard({ secret: process.env.CSRF_SECRET }))
164
+ @Post('/profile')
165
+ async update() { ... }
166
+ ```
167
+
168
+ Cookie name `XSRF-TOKEN`, header name `X-XSRF-TOKEN` — both match the Inertia client convention. Signed via HMAC-SHA256. Requires `cookie-parser` (Express) or `@fastify/cookie` (Fastify) as peer dep.
169
+
170
+ ## Fastify
171
+
172
+ ```ts
173
+ import { FastifyAdapter } from '@nestjs/platform-fastify';
174
+
175
+ const app = await NestFactory.create<NestFastifyApplication>(AppModule, new FastifyAdapter());
176
+ ```
177
+
178
+ Full feature parity with Express: middleware, decorator, all interceptors, guards, filters, shell directives, SSR, FlashStore. `request.inertia` is wired via `decorateRequest` + `onRequest` hook automatically when the FastifyAdapter is detected.
179
+
180
+ ## Prop markers
181
+
182
+ ```ts
183
+ import { Inertia } from '@dudousxd/nestjs-inertia';
184
+
185
+ return {
186
+ user: Inertia.always(() => currentUser()),
187
+ stats: Inertia.optional(() => heavyStatsCalculation()),
188
+ activity: Inertia.defer(() => activityFeed(), 'secondary'),
189
+ rows: Inertia.merge(() => paginated(p), { matchOn: 'id', deep: false }),
190
+ csrfToken: Inertia.once(() => generateToken()),
191
+ };
192
+ ```
193
+
194
+ - `always` — resolves on every render (including partial reloads)
195
+ - `optional` — resolves only when listed in `X-Inertia-Partial-Data`
196
+ - `defer` — listed in `page.deferredProps`; client v2 dispatches a follow-up request
197
+ - `merge` — resolves + marks as merge target (client appends/replaces)
198
+ - `once` — resolves on first visit, cached until `X-Inertia-Reset-Once` lists the key
199
+ - `lazy` — alias for `optional` (v1 compat)
200
+
201
+ ## Auto-included infrastructure
202
+
203
+ `forRoot()` installs:
204
+ - `InertiaMiddleware` (Express) or `FastifyInertiaPlugin` (Fastify) — `req.inertia` available everywhere
205
+ - `MethodSpoofMiddleware` (POST + multipart + `_method=PUT/PATCH/DELETE`)
206
+ - `RedirectInterceptor` (302 → 303 upgrade on PUT/PATCH/DELETE Inertia requests)
207
+ - `InertiaRenderInterceptor` (handles `@Inertia('Page')` decorator)
208
+ - `InertiaScopeSwitcherInterceptor` (handles `@UseInertia('scope')`)
209
+
210
+ Disable via knobs:
211
+
212
+ ```ts
213
+ InertiaModule.forRoot({
214
+ methodSpoofing: false,
215
+ autoUpgrade303: false,
216
+ });
217
+ ```
218
+
219
+ ## Opt-in utilities
220
+
221
+ ```ts
222
+ import {
223
+ InertiaAuthGuard,
224
+ InertiaNotFoundFilter,
225
+ ErrorBagInterceptor,
226
+ } from '@dudousxd/nestjs-inertia';
227
+
228
+ // Auth guard — applies per controller / handler
229
+ @UseGuards(new InertiaAuthGuard({ signInUrl: '/signin', allowList: ['/signin/*'] }))
230
+
231
+ // Not-found filter — register globally
232
+ app.useGlobalFilters(new InertiaNotFoundFilter({ apiPrefix: '/api', component: 'NotFound' }));
233
+
234
+ // Error bag interceptor — opt-in per route
235
+ @UseInterceptors(ErrorBagInterceptor)
236
+ ```
237
+
238
+ ## FlashStore (errors)
239
+
240
+ NestJS has no session of its own. Plug an adapter:
241
+
242
+ ```ts
243
+ import type { FlashStore } from '@dudousxd/nestjs-inertia';
244
+
245
+ class ExpressSessionFlashStore implements FlashStore {
246
+ read(req) {
247
+ return (req as Request).session?.flash?.errors ?? {};
248
+ }
249
+ }
250
+
251
+ InertiaModule.forRoot({
252
+ flashStore: new ExpressSessionFlashStore(),
253
+ });
254
+ ```
255
+
256
+ ## SSR
257
+
258
+ ```ts
259
+ InertiaModule.forRoot({
260
+ ssr: {
261
+ enabled: process.env.NODE_ENV === 'production',
262
+ bundlePath: 'dist/inertia/ssr/ssr.mjs',
263
+ throwOnError: false,
264
+ },
265
+ });
266
+ ```
267
+
268
+ Bundle must export `default { render(page) }` or named `render(page)` returning `{ head: string[], body: string }`.
269
+
270
+ ## Codegen auto-watch (dev mode)
271
+
272
+ When `@dudousxd/nestjs-inertia-codegen` is installed and a `nestjs-inertia.config.ts` config file is present, `InertiaModule` automatically starts the codegen file watcher when your app bootstraps — so `nest start --watch` is the only command you need in dev mode. Generated files appear under `.nestjs-inertia/` and update on every controller or page save. **No extra command, no extra terminal.**
273
+
274
+ **Auto-watch starts when all of these are true:**
275
+ 1. `NODE_ENV !== 'production'`
276
+ 2. `@dudousxd/nestjs-inertia-codegen` is installed (peer-optional — silently skipped if absent)
277
+ 3. `nestjs-inertia.config.ts` is present at the project root
278
+ 4. `NESTJS_INERTIA_DISABLE_AUTO_CODEGEN` is not set to `'1'`
279
+
280
+ **Running the CLI watcher manually:** `pnpm nestjs-inertia codegen --watch` in a separate terminal gives you explicit control. When both the auto-watcher and the CLI watcher run at the same time, only one holds the lock and generates files; the other logs a warning and becomes a no-op. Stale locks from crashed processes are detected via PID-liveness check and overwritten automatically.
281
+
282
+ **Disable auto-watch** (CI or explicit control):
283
+
284
+ ```ts
285
+ InertiaModule.forRoot({
286
+ codegen: { enabled: false },
287
+ });
288
+ ```
289
+
290
+ **Optional `nest-cli.json` snippet** — only useful if your server bundle imports generated files:
291
+
292
+ ```json
293
+ {
294
+ "compilerOptions": {
295
+ "assets": [".nestjs-inertia/**/*"],
296
+ "watchAssets": true
297
+ }
298
+ }
299
+ ```
300
+
301
+ ## Protocol parity
302
+
303
+ Full Inertia protocol: X-Inertia headers, version mismatch (409 + X-Inertia-Location, GET only), partial reloads, deferred props, merge/deepMerge with matchOn, once, history encryption / clear, error bags, X-Inertia-Reset, X-Inertia-Partial-Except, X-Inertia-Reset-Once, dot-notation unpacking, undefined→null wire conversion.
304
+
305
+ ## Companion packages (planned)
306
+
307
+ - `@dudousxd/nestjs-inertia-vite` — Vite dev/build helpers (Plan B)
308
+ - `@dudousxd/nestjs-inertia-testing` — `expectInertia(res)` matchers (Plan B)
309
+ - `@dudousxd/nestjs-inertia-codegen` — typed pages (Plan C)
310
+ - `@dudousxd/nestjs-inertia-client` — Tuyau-style typed REST + TanStack Query (Plan D)
311
+ - Examples + docs site + CI workflows (Plan E)
312
+
313
+ See [`docs/design.md`](../../docs/design.md) for full design.
314
+
315
+ ## License
316
+
317
+ MIT © Davi Carvalho