@stratal/inertia 0.0.1 → 0.0.19

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/dist/index.mjs CHANGED
@@ -1,96 +1,91 @@
1
- import { n as runTypeGeneration, t as findPagesDir } from "./type-generator-DlXIc6e2.mjs";
1
+ import { n as runTypeGeneration, t as findPagesDir } from "./type-generator-C5JljyzK.mjs";
2
+ import { Scope, Transient, inject } from "stratal/di";
3
+ import { ApplicationError } from "stratal/errors";
4
+ import { I18N_TOKENS } from "stratal/i18n";
2
5
  import { Module } from "stratal/module";
3
- import { Route, RouterContext } from "stratal/router";
6
+ import { Delete, Get, Patch, Post, Put, ROUTER_TOKENS, Route, RouterContext, SchemaValidationError } from "stratal/router";
7
+ import { spawn } from "node:child_process";
4
8
  import { existsSync, mkdirSync, writeFileSync } from "node:fs";
5
- import { join, relative } from "node:path";
9
+ import { dirname, join, relative } from "node:path";
6
10
  import { Command } from "stratal/quarry";
7
11
  import { watch } from "node:fs/promises";
8
- import { Transient, inject } from "stratal/di";
12
+ import { LOGGER_TOKENS } from "stratal/logger";
13
+ import { deleteCookie, getSignedCookie, setSignedCookie } from "hono/cookie";
9
14
  import { z } from "stratal/validation";
10
15
  //#region src/augment/router-context.ts
11
16
  function augmentRouterContext(resolveService) {
12
- const proto = RouterContext.prototype;
13
- const originalRedirect = proto.redirect;
14
- proto.redirect = function(url, status) {
17
+ const originalRedirect = RouterContext.prototype.redirect;
18
+ RouterContext.macro("redirect", function(url, status) {
15
19
  if (!status || status === 302) {
16
20
  const method = this.c.req.method;
17
21
  if (method !== "GET" && method !== "HEAD") return originalRedirect.call(this, url, 303);
18
22
  }
19
23
  return originalRedirect.call(this, url, status);
20
- };
21
- proto.inertia = function(component, props, options) {
24
+ });
25
+ RouterContext.macro("inertia", function(component, props, options) {
22
26
  return resolveService(this).render(this, component, props, options);
23
- };
24
- proto.defer = function(callback, group) {
27
+ });
28
+ RouterContext.macro("defer", function(callback, group) {
25
29
  return resolveService(this).defer(callback, group);
26
- };
27
- proto.optional = function(callback) {
30
+ });
31
+ RouterContext.macro("optional", function(callback) {
28
32
  return resolveService(this).optional(callback);
29
- };
30
- proto.merge = function(callback) {
31
- return resolveService(this).merge(callback);
32
- };
33
- proto.withoutSsr = function() {
33
+ });
34
+ RouterContext.macro("merge", function(callback, options) {
35
+ return resolveService(this).merge(callback, options);
36
+ });
37
+ RouterContext.macro("once", function(callback, options) {
38
+ return resolveService(this).once(callback, options);
39
+ });
40
+ RouterContext.macro("always", function(callback) {
41
+ return resolveService(this).always(callback);
42
+ });
43
+ RouterContext.macro("flash", function(key, value) {
44
+ const flashOut = this.c.get("inertiaFlashOut");
45
+ if (flashOut) flashOut[key] = value;
46
+ });
47
+ RouterContext.macro("withoutSsr", function() {
34
48
  this.c.set("withoutSsr", true);
35
- };
49
+ });
36
50
  }
37
51
  //#endregion
38
52
  //#region src/vite/create-vite-config.ts
39
- async function createInertiaViteConfig(options) {
40
- const { mergeConfig } = await import("vite");
41
- let userConfig = {};
42
- const viteConfigPath = join(options.cwd, "vite.config.ts");
43
- if (existsSync(viteConfigPath)) {
44
- const loaded = await import(
45
- /* @vite-ignore */
46
- viteConfigPath
47
- );
48
- userConfig = loaded.default ?? loaded;
49
- }
50
- const { cloudflare } = await import("@cloudflare/vite-plugin");
51
- const { stratalInertiaDevCss } = await import("./inertia-dev-css-plugin-BYromyO_.mjs").then((n) => n.t);
52
- const { stratalInertiaTypes } = await import("./inertia-types-plugin-NO_uxhxQ.mjs").then((n) => n.t);
53
- const hasInertiaPlugin = (Array.isArray(userConfig.plugins) ? userConfig.plugins.flat() : []).some((p) => p && typeof p === "object" && "name" in p && p.name === "inertia");
54
- const inertiaPlugins = [];
55
- if (!hasInertiaPlugin) try {
56
- const { default: inertia } = await import("@inertiajs/vite");
57
- inertiaPlugins.push(inertia({ pages: {
58
- path: "./src/inertia/pages",
59
- extension: ".tsx"
60
- } }));
61
- } catch {}
62
- const optimizeDepsExclude = [
63
- "@cloudflare/vite-plugin",
64
- "wrangler",
65
- "blake3-wasm"
66
- ];
67
- return mergeConfig({
68
- plugins: [
69
- cloudflare(),
70
- ...inertiaPlugins,
71
- stratalInertiaDevCss({ entries: ["/" + options.entryPath] }),
72
- stratalInertiaTypes(),
73
- {
74
- name: "stratal:optimize-deps-fix",
75
- configEnvironment(_name, env) {
76
- const existing = env.optimizeDeps?.exclude ?? [];
77
- env.optimizeDeps = {
78
- ...env.optimizeDeps,
79
- exclude: [...existing, ...optimizeDepsExclude]
80
- };
81
- }
82
- }
83
- ],
84
- publicDir: join(options.cwd, "src", "inertia", "public"),
85
- build: {
86
- ...options.outDir ? { outDir: options.outDir } : {},
87
- rolldownOptions: { input: options.entryPath }
88
- },
89
- ...options.server ? { server: {
90
- port: options.server.port,
91
- host: options.server.host ?? void 0
92
- } } : {}
93
- }, userConfig);
53
+ function writeTempViteConfig(options) {
54
+ const configPath = join(join(options.cwd, "node_modules", ".stratal"), "vite.config.mjs");
55
+ mkdirSync(dirname(configPath), { recursive: true });
56
+ const hasUserConfig = existsSync(join(options.cwd, "vite.config.ts"));
57
+ const serverConfig = options.server ? `server: { port: ${options.server.port}, host: ${options.server.host ? "true" : "undefined"} },` : "";
58
+ const outDirConfig = options.outDir ? `outDir: '${options.outDir}',` : "";
59
+ writeFileSync(configPath, `
60
+ import { mergeConfig } from 'vite'
61
+ import { cloudflare } from '@cloudflare/vite-plugin'
62
+ import { stratalInertia } from '@stratal/inertia/vite'
63
+
64
+ let inertiaPlugin = null
65
+ try {
66
+ const mod = await import('@inertiajs/vite')
67
+ const inertia = mod.default ?? mod
68
+ inertiaPlugin = inertia()
69
+ } catch {}
70
+
71
+ const baseConfig = {
72
+ plugins: [
73
+ cloudflare(${options.persistTo ? `{ persistState: { path: ${JSON.stringify(options.persistTo)} } }` : ""}),
74
+ ...(inertiaPlugin ? [inertiaPlugin] : []),
75
+ ...stratalInertia(),
76
+ ],
77
+ publicDir: '${join(options.cwd, "src", "inertia", "public").replace(/\\/g, "/")}',
78
+ build: {
79
+ ${outDirConfig}
80
+ },
81
+ ${serverConfig}
82
+ }
83
+
84
+ ${hasUserConfig ? `const userModule = await import('${join(options.cwd, "vite.config.ts").replace(/\\/g, "/")}')
85
+ const userConfig = userModule.default ?? userModule
86
+ export default mergeConfig(baseConfig, userConfig)` : "export default baseConfig"}
87
+ `, "utf-8");
88
+ return configPath;
94
89
  }
95
90
  //#endregion
96
91
  //#region src/commands/inertia-build.command.ts
@@ -101,72 +96,100 @@ var InertiaBuildCommand = class extends Command {
101
96
  const outDir = this.string("outDir") || "dist";
102
97
  const shouldBuildSsr = this.boolean("ssr");
103
98
  const cwd = process.cwd();
104
- const entryPath = "src/inertia/app.tsx";
105
- if (!existsSync(join(cwd, entryPath))) {
99
+ if (!existsSync(join(cwd, "src/inertia/app.tsx"))) {
106
100
  this.fail("src/inertia/app.tsx not found. Run `quarry inertia:install` first.");
107
101
  return 1;
108
102
  }
103
+ const configPath = writeTempViteConfig({
104
+ cwd,
105
+ outDir
106
+ });
109
107
  this.info("Building Inertia.js frontend for production...");
110
- try {
111
- const { build } = await import("vite");
112
- const config = await createInertiaViteConfig({
113
- cwd,
114
- entryPath,
115
- outDir
116
- });
117
- await build(config);
118
- this.success("Client build complete!");
119
- if (shouldBuildSsr) {
120
- this.info("Building SSR bundle...");
121
- await build({
122
- ...config,
123
- build: {
124
- ...config.build,
125
- ssr: true
126
- }
127
- });
128
- this.success("SSR build complete!");
108
+ const clientCode = await this.spawnVite(cwd, configPath, ["build"]);
109
+ if (clientCode !== 0) {
110
+ this.fail("Client build failed.");
111
+ return clientCode;
112
+ }
113
+ this.success("Client build complete!");
114
+ if (shouldBuildSsr) {
115
+ this.info("Building SSR bundle...");
116
+ const ssrCode = await this.spawnVite(cwd, configPath, ["build", "--ssr"]);
117
+ if (ssrCode !== 0) {
118
+ this.fail("SSR build failed.");
119
+ return ssrCode;
129
120
  }
130
- this.success(`Output in ${outDir}/`);
131
- } catch (err) {
132
- this.fail(`Build failed: ${err.message}`);
133
- return 1;
121
+ this.success("SSR build complete!");
134
122
  }
123
+ this.success(`Output in ${outDir}/`);
124
+ this.info("Deploy with: npx wrangler deploy");
135
125
  return 0;
136
126
  }
127
+ spawnVite(cwd, configPath, args) {
128
+ return new Promise((resolve) => {
129
+ const child = spawn("npx", [
130
+ "vite",
131
+ "--config",
132
+ configPath,
133
+ ...args
134
+ ], {
135
+ cwd,
136
+ stdio: "inherit",
137
+ shell: true
138
+ });
139
+ child.on("error", (err) => {
140
+ this.fail(`Vite process error: ${err.message}`);
141
+ resolve(1);
142
+ });
143
+ child.on("close", (code) => {
144
+ resolve(code ?? 0);
145
+ });
146
+ });
147
+ }
137
148
  };
138
149
  //#endregion
139
150
  //#region src/commands/inertia-dev.command.ts
140
151
  var InertiaDevCommand = class extends Command {
141
- static command = "inertia:dev {--port=5173 : Dev server port} {--host : Expose to network}";
152
+ static command = "inertia:dev {--port= : Dev server port} {--host : Expose to network} {--persist-to= : Shared persist directory for @cloudflare/vite-plugin (relative to cwd; the plugin appends /v3). Use to share R2/KV/cache emulator state across multiple workers in dev.}";
142
153
  static description = "Start Inertia.js Vite development server";
143
154
  async handle() {
144
- const port = this.number("port") || 5173;
155
+ const port = this.number("port");
145
156
  const host = this.boolean("host");
157
+ const persistTo = this.string("persist-to");
146
158
  const cwd = process.cwd();
147
- const entryPath = "src/inertia/app.tsx";
148
- if (!existsSync(join(cwd, entryPath))) {
159
+ if (!existsSync(join(cwd, "src/inertia/app.tsx"))) {
149
160
  this.fail("src/inertia/app.tsx not found. Run `quarry inertia:install` first.");
150
161
  return 1;
151
162
  }
163
+ const configPath = writeTempViteConfig({
164
+ cwd,
165
+ server: {
166
+ port,
167
+ host
168
+ },
169
+ persistTo
170
+ });
152
171
  this.info("Starting Vite dev server...");
153
- try {
154
- const { createServer } = await import("vite");
155
- const server = await createServer(await createInertiaViteConfig({
172
+ const args = [
173
+ "vite",
174
+ "dev",
175
+ "--config",
176
+ configPath
177
+ ];
178
+ if (host) args.push("--host");
179
+ return new Promise((resolve) => {
180
+ const child = spawn("npx", args, {
156
181
  cwd,
157
- entryPath,
158
- server: {
159
- port,
160
- host
161
- }
162
- }));
163
- await server.listen();
164
- server.printUrls();
165
- } catch (err) {
166
- this.fail(`Failed to start dev server: ${err.message}`);
167
- return 1;
168
- }
169
- return 0;
182
+ stdio: "inherit",
183
+ shell: true
184
+ });
185
+ child.on("error", (err) => {
186
+ this.fail(`Failed to start dev server: ${err.message}`);
187
+ resolve(1);
188
+ });
189
+ child.on("close", (code) => {
190
+ resolve(code ?? 0);
191
+ });
192
+ });
170
193
  }
171
194
  };
172
195
  //#endregion
@@ -186,7 +209,14 @@ const ROOT_HTML = `<!DOCTYPE html>
186
209
  </html>`;
187
210
  const APP_TSX = `import { createInertiaApp } from '@inertiajs/react'
188
211
 
189
- createInertiaApp()`;
212
+ createInertiaApp({
213
+ resolve: async (name) => {
214
+ const pages = import.meta.glob('./pages/**/*.tsx')
215
+ const page = await pages[\`./pages/\${name}.tsx\`]?.()
216
+ if (!page) throw new Error(\`Page not found: \${name}\`)
217
+ return page
218
+ },
219
+ })`;
190
220
  const HOME_TSX = `export default function Home({ message }: { message: string }) {
191
221
  return (
192
222
  <div>
@@ -302,15 +332,14 @@ var InertiaTypesCommand = class extends Command {
302
332
  static description = "Generate Inertia.js page type definitions";
303
333
  async handle() {
304
334
  const cwd = process.cwd();
305
- const pagesDir = findPagesDir(cwd);
306
- if (!existsSync(pagesDir)) {
335
+ if (!existsSync(findPagesDir(cwd))) {
307
336
  this.fail("src/inertia/pages/ not found. Run `quarry inertia:install` first.");
308
337
  return 1;
309
338
  }
310
339
  if (!await this.generate(cwd)) return 1;
311
340
  if (this.boolean("watch")) {
312
341
  this.info("Watching for changes...");
313
- await this.watchForChanges(cwd, pagesDir);
342
+ await this.watchForChanges(cwd);
314
343
  }
315
344
  return 0;
316
345
  }
@@ -325,9 +354,10 @@ var InertiaTypesCommand = class extends Command {
325
354
  return false;
326
355
  }
327
356
  }
328
- async watchForChanges(cwd, pagesDir) {
357
+ async watchForChanges(cwd) {
358
+ const srcDir = join(cwd, "src");
329
359
  try {
330
- const watcher = watch(pagesDir, { recursive: true });
360
+ const watcher = watch(srcDir, { recursive: true });
331
361
  for await (const event of watcher) if (event.filename && /\.(tsx|ts)$/.test(event.filename)) {
332
362
  this.info(`Change detected: ${event.filename}`);
333
363
  await this.generate(cwd);
@@ -347,19 +377,19 @@ const INERTIA_TOKENS = {
347
377
  SsrRenderer: Symbol.for("stratal:inertia:ssr-renderer")
348
378
  };
349
379
  //#endregion
350
- //#region \0@oxc-project+runtime@0.115.0/helpers/decorateMetadata.js
380
+ //#region \0@oxc-project+runtime@0.127.0/helpers/decorateMetadata.js
351
381
  function __decorateMetadata(k, v) {
352
382
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
353
383
  }
354
384
  //#endregion
355
- //#region \0@oxc-project+runtime@0.115.0/helpers/decorateParam.js
385
+ //#region \0@oxc-project+runtime@0.127.0/helpers/decorateParam.js
356
386
  function __decorateParam(paramIndex, decorator) {
357
387
  return function(target, key) {
358
388
  decorator(target, key, paramIndex);
359
389
  };
360
390
  }
361
391
  //#endregion
362
- //#region \0@oxc-project+runtime@0.115.0/helpers/decorate.js
392
+ //#region \0@oxc-project+runtime@0.127.0/helpers/decorate.js
363
393
  function __decorate(decorators, target, key, desc) {
364
394
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
365
395
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -378,6 +408,13 @@ let InertiaMiddleware = class InertiaMiddleware {
378
408
  ctx.c.set("inertia", isInertia);
379
409
  ctx.c.set("inertiaPrefetch", isPrefetch);
380
410
  ctx.c.set("withoutSsr", false);
411
+ ctx.c.set("inertiaFlashOut", {});
412
+ let hadFlash = false;
413
+ if (this.options.flash) {
414
+ const flashData = await this.options.flash.store.read(ctx);
415
+ hadFlash = Object.keys(flashData).length > 0;
416
+ ctx.c.set("inertiaFlash", flashData);
417
+ } else ctx.c.set("inertiaFlash", {});
381
418
  if (isInertia && ctx.c.req.method === "GET") {
382
419
  const clientVersion = ctx.header("x-inertia-version");
383
420
  const serverVersion = this.options.version ?? "";
@@ -388,6 +425,11 @@ let InertiaMiddleware = class InertiaMiddleware {
388
425
  }
389
426
  }
390
427
  await next();
428
+ if (this.options.flash) {
429
+ const flashOut = ctx.c.get("inertiaFlashOut");
430
+ if (Object.keys(flashOut).length > 0) await this.options.flash.store.write(ctx, flashOut);
431
+ else if (hadFlash) await this.options.flash.store.clear(ctx);
432
+ }
391
433
  ctx.c.header("Vary", "X-Inertia");
392
434
  if (isInertia) {
393
435
  const method = ctx.c.req.method;
@@ -405,6 +447,8 @@ InertiaMiddleware = __decorate([
405
447
  const INERTIA_PROP_OPTIONAL = Symbol.for("stratal:inertia:prop:optional");
406
448
  const INERTIA_PROP_DEFERRED = Symbol.for("stratal:inertia:prop:deferred");
407
449
  const INERTIA_PROP_MERGE = Symbol.for("stratal:inertia:prop:merge");
450
+ const INERTIA_PROP_ONCE = Symbol.for("stratal:inertia:prop:once");
451
+ const INERTIA_PROP_ALWAYS = Symbol.for("stratal:inertia:prop:always");
408
452
  //#endregion
409
453
  //#region src/services/inertia.service.ts
410
454
  let InertiaService = class InertiaService {
@@ -436,30 +480,63 @@ let InertiaService = class InertiaService {
436
480
  group
437
481
  };
438
482
  }
439
- merge(callback) {
483
+ merge(callback, options) {
440
484
  return {
441
485
  [INERTIA_PROP_MERGE]: true,
486
+ callback,
487
+ strategy: options?.strategy ?? "append",
488
+ matchOn: options?.matchOn
489
+ };
490
+ }
491
+ once(callback, options) {
492
+ return {
493
+ [INERTIA_PROP_ONCE]: true,
494
+ callback,
495
+ expiresAt: options?.expiresAt ?? null,
496
+ key: options?.key
497
+ };
498
+ }
499
+ always(callback) {
500
+ return {
501
+ [INERTIA_PROP_ALWAYS]: true,
442
502
  callback
443
503
  };
444
504
  }
445
505
  async render(ctx, component, props = {}, renderOptions = {}) {
446
- const url = new URL(ctx.c.req.url).pathname;
506
+ const reqUrl = new URL(ctx.c.req.url);
507
+ const url = reqUrl.search ? `${reqUrl.pathname}${reqUrl.search}` : reqUrl.pathname;
447
508
  const isInertia = ctx.c.get("inertia");
509
+ const { shared: resolvedShared, sharedKeys } = await this.resolveSharedData(ctx);
448
510
  const allProps = {
449
- ...await this.resolveSharedData(ctx),
511
+ ...resolvedShared,
450
512
  ...this.sharedData,
451
513
  ...props
452
514
  };
453
- const { resolvedProps, mergeProps, deferredProps } = await this.processProps(allProps, ctx, component, isInertia);
515
+ const allSharedKeys = [...sharedKeys, ...Object.keys(this.sharedData)];
516
+ const result = await this.processProps(allProps, ctx, component, isInertia);
517
+ const { errors: flashErrors, ...flash } = ctx.c.get("inertiaFlash") ?? {};
518
+ const errors = flashErrors && typeof flashErrors === "object" && !Array.isArray(flashErrors) ? flashErrors : {};
454
519
  const page = {
455
520
  component,
456
- props: resolvedProps,
521
+ props: {
522
+ ...result.resolvedProps,
523
+ errors
524
+ },
457
525
  url,
458
- version: this.options.version ?? "",
459
- mergeProps,
460
- deferredProps,
526
+ version: this.options.version ?? null,
527
+ flash,
528
+ rememberedState: {},
529
+ ...result.mergeProps.length > 0 ? { mergeProps: result.mergeProps } : {},
530
+ ...result.prependProps.length > 0 ? { prependProps: result.prependProps } : {},
531
+ ...result.deepMergeProps.length > 0 ? { deepMergeProps: result.deepMergeProps } : {},
532
+ ...result.matchPropsOn.length > 0 ? { matchPropsOn: result.matchPropsOn } : {},
533
+ ...Object.keys(result.deferredProps).length > 0 ? { deferredProps: result.deferredProps } : {},
534
+ ...Object.keys(result.deferredProps).length > 0 && !this.isPartialReload(ctx, component) ? { initialDeferredProps: result.deferredProps } : {},
535
+ ...Object.keys(result.onceProps).length > 0 ? { onceProps: result.onceProps } : {},
536
+ ...allSharedKeys.length > 0 ? { sharedProps: allSharedKeys } : {},
461
537
  ...renderOptions.encryptHistory ? { encryptHistory: true } : {},
462
- ...renderOptions.clearHistory ? { clearHistory: true } : {}
538
+ ...renderOptions.clearHistory ? { clearHistory: true } : {},
539
+ ...renderOptions.preserveFragment ? { preserveFragment: true } : {}
463
540
  };
464
541
  if (isInertia) return new Response(JSON.stringify(page), {
465
542
  status: 200,
@@ -479,23 +556,71 @@ let InertiaService = class InertiaService {
479
556
  headers: { "Content-Type": "text/html; charset=utf-8" }
480
557
  });
481
558
  }
559
+ /**
560
+ * Resolve shared data from module options and i18n configuration.
561
+ *
562
+ * Processes static values and resolver functions from `sharedData` config.
563
+ * When `i18n` option is set, auto-injects `locale` and `translations` props
564
+ * using the core {@link MessageLoaderService} resolved from the request container.
565
+ */
482
566
  async resolveSharedData(ctx) {
483
567
  const shared = {};
484
568
  const configShared = this.options.sharedData;
485
- if (!configShared) return shared;
486
- for (const [key, value] of Object.entries(configShared)) if (typeof value === "function") shared[key] = await value(ctx);
569
+ if (configShared) for (const [key, value] of Object.entries(configShared)) if (typeof value === "function") shared[key] = await value(ctx);
487
570
  else shared[key] = value;
488
- return shared;
571
+ if (this.options.i18n) {
572
+ const loader = ctx.getContainer().resolve(I18N_TOKENS.MessageLoader);
573
+ const locale = ctx.getLocale();
574
+ shared.locale = locale;
575
+ shared.translations = loader.getFilteredMessages(locale, { only: this.options.i18n.only });
576
+ }
577
+ if (this.options.routes) {
578
+ const registry = ctx.getContainer().resolve(ROUTER_TOKENS.RouteRegistry);
579
+ shared.routes = this.serializeRoutes(registry.named());
580
+ }
581
+ return {
582
+ shared,
583
+ sharedKeys: Object.keys(shared)
584
+ };
585
+ }
586
+ isPartialReload(ctx, component) {
587
+ const isInertia = ctx.c.get("inertia");
588
+ const partialComponent = ctx.header("x-inertia-partial-component");
589
+ const partialDataHeader = ctx.header("x-inertia-partial-data");
590
+ return !!(isInertia && partialComponent === component && partialDataHeader);
489
591
  }
490
592
  async processProps(allProps, ctx, component, isInertia) {
491
593
  const resolvedProps = {};
492
594
  const mergeProps = [];
595
+ const prependProps = [];
596
+ const deepMergeProps = [];
597
+ const matchPropsOn = [];
493
598
  const deferredProps = {};
599
+ const onceProps = {};
494
600
  const partialComponent = ctx.header("x-inertia-partial-component");
495
601
  const partialDataHeader = ctx.header("x-inertia-partial-data");
602
+ const partialExceptHeader = ctx.header("x-inertia-partial-except");
603
+ const resetHeader = ctx.header("x-inertia-reset");
496
604
  const isPartialReload = isInertia && partialComponent === component && partialDataHeader;
497
605
  const requestedProps = partialDataHeader?.split(",").map((s) => s.trim()) ?? [];
606
+ const exceptProps = partialExceptHeader?.split(",").map((s) => s.trim()) ?? [];
607
+ resetHeader?.split(",").map((s) => s.trim());
498
608
  for (const [key, value] of Object.entries(allProps)) {
609
+ if (this.isAlwaysProp(value)) {
610
+ resolvedProps[key] = await value.callback();
611
+ continue;
612
+ }
613
+ if (this.isOnceProp(value)) {
614
+ if (isPartialReload && this.isRequested(key, requestedProps)) resolvedProps[key] = await value.callback();
615
+ else if (!isPartialReload) {
616
+ resolvedProps[key] = await value.callback();
617
+ onceProps[key] = {
618
+ prop: value.key ?? key,
619
+ ...value.expiresAt != null ? { expiresAt: value.expiresAt } : {}
620
+ };
621
+ }
622
+ continue;
623
+ }
499
624
  if (this.isDeferredProp(value)) {
500
625
  if (isPartialReload && this.isRequested(key, requestedProps)) resolvedProps[key] = await value.callback();
501
626
  else if (!isPartialReload) {
@@ -506,7 +631,18 @@ let InertiaService = class InertiaService {
506
631
  }
507
632
  if (this.isMergeProp(value)) {
508
633
  if (isPartialReload && !this.isRequested(key, requestedProps)) continue;
509
- mergeProps.push(key);
634
+ switch (value.strategy) {
635
+ case "prepend":
636
+ prependProps.push(key);
637
+ break;
638
+ case "deep":
639
+ deepMergeProps.push(key);
640
+ break;
641
+ default:
642
+ mergeProps.push(key);
643
+ break;
644
+ }
645
+ if (value.matchOn) matchPropsOn.push(`${key}:${value.matchOn}`);
510
646
  resolvedProps[key] = await value.callback();
511
647
  continue;
512
648
  }
@@ -515,13 +651,17 @@ let InertiaService = class InertiaService {
515
651
  continue;
516
652
  }
517
653
  if (isPartialReload) {
518
- if (this.isRequested(key, requestedProps)) resolvedProps[key] = value;
654
+ if (this.isRequested(key, requestedProps) && !this.isExcepted(key, exceptProps)) resolvedProps[key] = value;
519
655
  } else resolvedProps[key] = value;
520
656
  }
521
657
  return {
522
658
  resolvedProps,
523
659
  mergeProps,
524
- deferredProps
660
+ prependProps,
661
+ deepMergeProps,
662
+ matchPropsOn,
663
+ deferredProps,
664
+ onceProps
525
665
  };
526
666
  }
527
667
  /**
@@ -531,6 +671,9 @@ let InertiaService = class InertiaService {
531
671
  isRequested(key, requestedProps) {
532
672
  return requestedProps.some((prop) => prop === key || prop.startsWith(`${key}.`));
533
673
  }
674
+ isExcepted(key, exceptProps) {
675
+ return exceptProps.some((prop) => prop === key || prop.startsWith(`${key}.`));
676
+ }
534
677
  isOptionalProp(value) {
535
678
  return typeof value === "object" && value !== null && INERTIA_PROP_OPTIONAL in value;
536
679
  }
@@ -540,6 +683,23 @@ let InertiaService = class InertiaService {
540
683
  isMergeProp(value) {
541
684
  return typeof value === "object" && value !== null && INERTIA_PROP_MERGE in value;
542
685
  }
686
+ isOnceProp(value) {
687
+ return typeof value === "object" && value !== null && INERTIA_PROP_ONCE in value;
688
+ }
689
+ isAlwaysProp(value) {
690
+ return typeof value === "object" && value !== null && INERTIA_PROP_ALWAYS in value;
691
+ }
692
+ serializeRoutes(routes) {
693
+ const serialized = {};
694
+ for (const route of routes) if (route.name) serialized[route.name] = {
695
+ path: route.path,
696
+ paramNames: route.paramNames,
697
+ domainParamNames: route.domainParamNames,
698
+ ...route.domain ? { domain: route.domain } : {},
699
+ ...route.localePaths?.length ? { localePaths: route.localePaths } : {}
700
+ };
701
+ return serialized;
702
+ }
543
703
  isSsrDisabled(pathname) {
544
704
  const patterns = this.options.ssr?.disabled;
545
705
  if (!patterns || patterns.length === 0) return false;
@@ -550,7 +710,7 @@ let InertiaService = class InertiaService {
550
710
  }
551
711
  };
552
712
  InertiaService = __decorate([
553
- Transient(),
713
+ Transient(INERTIA_TOKENS.InertiaService),
554
714
  __decorateParam(0, inject(INERTIA_TOKENS.Options)),
555
715
  __decorateParam(1, inject(INERTIA_TOKENS.TemplateService)),
556
716
  __decorateParam(2, inject(INERTIA_TOKENS.SsrRenderer)),
@@ -611,8 +771,9 @@ ManifestService = __decorate([
611
771
  let SsrRendererService = class SsrRendererService {
612
772
  bundle = null;
613
773
  loadPromise = null;
614
- constructor(options) {
774
+ constructor(options, logger) {
615
775
  this.options = options;
776
+ this.logger = logger;
616
777
  }
617
778
  async render(page) {
618
779
  if (!this.options.ssr) return {
@@ -637,8 +798,10 @@ let SsrRendererService = class SsrRendererService {
637
798
  if (!this.options.ssr) return;
638
799
  try {
639
800
  const mod = await this.options.ssr.bundle();
640
- this.bundle = "default" in mod ? mod.default : mod;
641
- } catch {
801
+ const resolved = "default" in mod ? mod.default : mod;
802
+ this.bundle = resolved;
803
+ } catch (error) {
804
+ this.logger.warn("[stratal:inertia] Failed to load SSR bundle. Falling back to client-side rendering.", { error });
642
805
  this.loadPromise = null;
643
806
  }
644
807
  }
@@ -646,7 +809,8 @@ let SsrRendererService = class SsrRendererService {
646
809
  SsrRendererService = __decorate([
647
810
  Transient(),
648
811
  __decorateParam(0, inject(INERTIA_TOKENS.Options)),
649
- __decorateMetadata("design:paramtypes", [Object])
812
+ __decorateParam(1, inject(LOGGER_TOKENS.LoggerService)),
813
+ __decorateMetadata("design:paramtypes", [Object, Object])
650
814
  ], SsrRendererService);
651
815
  //#endregion
652
816
  //#region src/services/template.service.ts
@@ -700,19 +864,86 @@ let InertiaModule = _InertiaModule = class InertiaModule {
700
864
  }]
701
865
  };
702
866
  }
703
- configure(consumer) {
704
- consumer.apply(InertiaMiddleware).forRoutes("*");
867
+ configureRoutes(router) {
868
+ router.use(InertiaMiddleware);
869
+ }
870
+ onException(handler) {
871
+ handler.renderable(SchemaValidationError, (error, context) => {
872
+ if (context.type !== "http") return void 0;
873
+ if (this.isPrecognitionRequest(context)) return this.handlePrecognitionValidationError(error, context);
874
+ if (!this.isInertiaRequest(context)) return void 0;
875
+ const issues = error.metadata?.issues ?? [];
876
+ const errors = {};
877
+ for (const issue of issues) errors[issue.path] = issue.message;
878
+ context.ctx.flash("errors", errors);
879
+ return this.redirectBack(context);
880
+ });
881
+ handler.renderable(ApplicationError, (error, context) => {
882
+ if (context.type !== "http") return void 0;
883
+ const message = context.ctx.getContainer().resolve(I18N_TOKENS.I18nService).t(error.message, error.metadata);
884
+ if (this.isPrecognitionRequest(context)) return this.createPrecognitionErrorResponse({ _form: message });
885
+ if (!this.isInertiaRequest(context)) return void 0;
886
+ context.ctx.flash("errors", { _form: message });
887
+ return this.redirectBack(context);
888
+ });
705
889
  }
706
890
  onInitialize() {
707
891
  augmentRouterContext((ctx) => {
708
892
  return ctx.getContainer().resolve(INERTIA_TOKENS.InertiaService);
709
893
  });
710
894
  }
895
+ isInertiaRequest(context) {
896
+ return context.ctx.header("x-inertia") === "true";
897
+ }
898
+ isPrecognitionRequest(context) {
899
+ return context.ctx.header("precognition") === "true";
900
+ }
901
+ handlePrecognitionValidationError(error, context) {
902
+ const issues = error.metadata?.issues ?? [];
903
+ let errors = {};
904
+ for (const issue of issues) errors[issue.path] = issue.message;
905
+ const validateOnly = context.ctx.header("precognition-validate-only");
906
+ if (validateOnly) {
907
+ const fields = validateOnly.split(",").map((f) => f.trim());
908
+ const filtered = {};
909
+ for (const field of fields) if (errors[field]) filtered[field] = errors[field];
910
+ errors = filtered;
911
+ }
912
+ if (Object.keys(errors).length === 0) return new Response(null, {
913
+ status: 204,
914
+ headers: {
915
+ "Precognition": "true",
916
+ "Precognition-Success": "true",
917
+ "Vary": "Precognition"
918
+ }
919
+ });
920
+ return this.createPrecognitionErrorResponse(errors);
921
+ }
922
+ createPrecognitionErrorResponse(errors) {
923
+ return new Response(JSON.stringify({ errors }), {
924
+ status: 422,
925
+ headers: {
926
+ "Content-Type": "application/json",
927
+ "Precognition": "true",
928
+ "Vary": "Precognition"
929
+ }
930
+ });
931
+ }
932
+ redirectBack(context) {
933
+ const referer = context.ctx.header("referer");
934
+ if (referer) {
935
+ const parsed = new URL(referer);
936
+ const url = parsed.search ? `${parsed.pathname}${parsed.search}` : parsed.pathname;
937
+ return context.ctx.redirect(url, 303);
938
+ }
939
+ return context.ctx.redirect("/", 303);
940
+ }
711
941
  };
712
942
  InertiaModule = _InertiaModule = __decorate([Module({ providers: [
713
943
  {
714
944
  provide: INERTIA_TOKENS.InertiaService,
715
- useClass: InertiaService
945
+ useClass: InertiaService,
946
+ scope: Scope.Request
716
947
  },
717
948
  {
718
949
  provide: INERTIA_TOKENS.TemplateService,
@@ -724,7 +955,8 @@ InertiaModule = _InertiaModule = __decorate([Module({ providers: [
724
955
  },
725
956
  {
726
957
  provide: INERTIA_TOKENS.SsrRenderer,
727
- useClass: SsrRendererService
958
+ useClass: SsrRendererService,
959
+ scope: Scope.Singleton
728
960
  },
729
961
  InertiaInstallCommand,
730
962
  InertiaTypesCommand,
@@ -732,28 +964,85 @@ InertiaModule = _InertiaModule = __decorate([Module({ providers: [
732
964
  InertiaBuildCommand
733
965
  ] })], InertiaModule);
734
966
  //#endregion
735
- //#region src/decorators/inertia-route.decorator.ts
967
+ //#region src/flash/cookie-flash-store.ts
968
+ var CookieFlashStore = class {
969
+ cookieName;
970
+ secret;
971
+ cookieOptions;
972
+ constructor(options) {
973
+ this.secret = options.secret;
974
+ this.cookieName = options.cookie ?? "stratal_flash";
975
+ this.cookieOptions = {
976
+ path: "/",
977
+ httpOnly: true,
978
+ sameSite: "Lax",
979
+ ...options.cookieOptions
980
+ };
981
+ }
982
+ async read(ctx) {
983
+ const value = await getSignedCookie(ctx.c, this.secret, this.cookieName);
984
+ if (!value) return {};
985
+ try {
986
+ return JSON.parse(atob(value));
987
+ } catch {
988
+ return {};
989
+ }
990
+ }
991
+ async write(ctx, data) {
992
+ const encoded = btoa(JSON.stringify(data));
993
+ await setSignedCookie(ctx.c, this.cookieName, encoded, this.secret, this.cookieOptions);
994
+ }
995
+ clear(ctx) {
996
+ deleteCookie(ctx.c, this.cookieName, { path: this.cookieOptions.path });
997
+ return Promise.resolve();
998
+ }
999
+ };
1000
+ const inertiaResponse = {
1001
+ schema: z.object({
1002
+ component: z.string(),
1003
+ props: z.record(z.string(), z.unknown()),
1004
+ url: z.string(),
1005
+ version: z.string().nullable(),
1006
+ flash: z.record(z.string(), z.unknown()),
1007
+ rememberedState: z.record(z.string(), z.unknown()),
1008
+ mergeProps: z.array(z.string()).optional(),
1009
+ prependProps: z.array(z.string()).optional(),
1010
+ deepMergeProps: z.array(z.string()).optional(),
1011
+ matchPropsOn: z.array(z.string()).optional(),
1012
+ deferredProps: z.record(z.string(), z.array(z.string())).optional(),
1013
+ initialDeferredProps: z.record(z.string(), z.array(z.string())).optional(),
1014
+ onceProps: z.record(z.string(), z.object({
1015
+ prop: z.string(),
1016
+ expiresAt: z.number().nullable().optional()
1017
+ })).optional(),
1018
+ sharedProps: z.array(z.string()).optional(),
1019
+ encryptHistory: z.boolean().optional(),
1020
+ clearHistory: z.boolean().optional(),
1021
+ preserveFragment: z.boolean().optional()
1022
+ }),
1023
+ description: "Inertia page response",
1024
+ contentType: "text/html"
1025
+ };
736
1026
  /**
737
- * Zod schema for the Inertia page JSON response (returned for X-Inertia XHR requests)
1027
+ * Builds a full RouteConfig from InertiaRouteConfig by applying inertia defaults.
738
1028
  */
739
- const inertiaPageSchema = z.object({
740
- component: z.string(),
741
- props: z.record(z.string(), z.unknown()),
742
- url: z.string(),
743
- version: z.string(),
744
- mergeProps: z.array(z.string()),
745
- deferredProps: z.record(z.string(), z.array(z.string())),
746
- encryptHistory: z.boolean(),
747
- clearHistory: z.boolean()
748
- });
1029
+ function buildInertiaConfig(config) {
1030
+ const { hideFromDocs = true, ...rest } = config;
1031
+ return {
1032
+ ...rest,
1033
+ response: inertiaResponse,
1034
+ hideFromDocs
1035
+ };
1036
+ }
749
1037
  /**
750
- * Decorator for Inertia page routes.
1038
+ * Decorator for Inertia page routes using convention-based routing.
751
1039
  *
752
1040
  * Wraps `@Route()` with:
753
1041
  * - Auto-applied Inertia page response schema
754
1042
  * - `hideFromDocs: true` by default (overridable)
755
1043
  *
756
- * Accepts `query`, `params`, `body`, `tags`, `summary`, `description`, `security`.
1044
+ * **Cannot be mixed with HTTP method decorators** (`@Get`, `@Post`, `@InertiaGet`, etc.)
1045
+ * in the same controller.
757
1046
  *
758
1047
  * @example
759
1048
  * ```typescript
@@ -767,18 +1056,103 @@ const inertiaPageSchema = z.object({
767
1056
  * ```
768
1057
  */
769
1058
  function InertiaRoute(config = {}) {
770
- const { hideFromDocs = true, ...rest } = config;
771
- return Route({
772
- ...rest,
773
- response: {
774
- schema: inertiaPageSchema,
775
- description: "Inertia page response",
776
- contentType: "text/html"
777
- },
778
- hideFromDocs
779
- });
1059
+ return Route(buildInertiaConfig(config));
780
1060
  }
1061
+ /**
1062
+ * Registers a GET route for an Inertia page.
1063
+ *
1064
+ * Wraps `@Get()` with auto-applied Inertia page response schema
1065
+ * and `hideFromDocs: true` by default.
1066
+ *
1067
+ * @param path - Route path relative to the controller base path
1068
+ * @param config - Optional route configuration (query, params, tags, etc.)
1069
+ *
1070
+ * @example
1071
+ * ```typescript
1072
+ * @Controller('/notes')
1073
+ * export class NotesController {
1074
+ * @InertiaGet('/')
1075
+ * async index(ctx: RouterContext) {
1076
+ * return ctx.inertia('notes/Index', { notes: [] })
1077
+ * }
1078
+ *
1079
+ * @InertiaGet('/:id', { params: z.object({ id: z.string() }) })
1080
+ * async show(ctx: RouterContext) {
1081
+ * return ctx.inertia('notes/Show', { note })
1082
+ * }
1083
+ * }
1084
+ * ```
1085
+ */
1086
+ function InertiaGet(path, config = {}) {
1087
+ return Get(path, buildInertiaConfig(config));
1088
+ }
1089
+ /**
1090
+ * Registers a POST route for an Inertia form submission.
1091
+ *
1092
+ * Wraps `@Post()` with auto-applied Inertia page response schema
1093
+ * and `hideFromDocs: true` by default.
1094
+ *
1095
+ * @param path - Route path relative to the controller base path
1096
+ * @param config - Optional route configuration (body, params, tags, etc.)
1097
+ */
1098
+ function InertiaPost(path, config = {}) {
1099
+ return Post(path, buildInertiaConfig(config));
1100
+ }
1101
+ /**
1102
+ * Registers a PUT route for an Inertia form submission.
1103
+ *
1104
+ * Wraps `@Put()` with auto-applied Inertia page response schema
1105
+ * and `hideFromDocs: true` by default.
1106
+ *
1107
+ * @param path - Route path relative to the controller base path
1108
+ * @param config - Optional route configuration (body, params, tags, etc.)
1109
+ */
1110
+ function InertiaPut(path, config = {}) {
1111
+ return Put(path, buildInertiaConfig(config));
1112
+ }
1113
+ /**
1114
+ * Registers a PATCH route for an Inertia form submission.
1115
+ *
1116
+ * Wraps `@Patch()` with auto-applied Inertia page response schema
1117
+ * and `hideFromDocs: true` by default.
1118
+ *
1119
+ * @param path - Route path relative to the controller base path
1120
+ * @param config - Optional route configuration (body, params, tags, etc.)
1121
+ */
1122
+ function InertiaPatch(path, config = {}) {
1123
+ return Patch(path, buildInertiaConfig(config));
1124
+ }
1125
+ /**
1126
+ * Registers a DELETE route for an Inertia form submission.
1127
+ *
1128
+ * Wraps `@Delete()` with auto-applied Inertia page response schema
1129
+ * and `hideFromDocs: true` by default.
1130
+ *
1131
+ * @param path - Route path relative to the controller base path
1132
+ * @param config - Optional route configuration (params, tags, etc.)
1133
+ */
1134
+ function InertiaDelete(path, config = {}) {
1135
+ return Delete(path, buildInertiaConfig(config));
1136
+ }
1137
+ //#endregion
1138
+ //#region src/middleware/handle-precognitive-requests.middleware.ts
1139
+ let HandlePrecognitiveRequests = class HandlePrecognitiveRequests {
1140
+ async handle(ctx, next) {
1141
+ const isPrecognition = ctx.header("precognition") === "true";
1142
+ ctx.c.set("precognition", isPrecognition);
1143
+ if (isPrecognition) ctx.c.set("validationSuccessResponse", new Response(null, {
1144
+ status: 204,
1145
+ headers: {
1146
+ "Precognition": "true",
1147
+ "Precognition-Success": "true",
1148
+ "Vary": "Precognition"
1149
+ }
1150
+ }));
1151
+ await next();
1152
+ }
1153
+ };
1154
+ HandlePrecognitiveRequests = __decorate([Transient()], HandlePrecognitiveRequests);
781
1155
  //#endregion
782
- export { INERTIA_TOKENS, InertiaBuildCommand, InertiaDevCommand, InertiaInstallCommand, InertiaMiddleware, InertiaModule, InertiaRoute, InertiaService, InertiaTypesCommand, ManifestService, SsrRendererService, TemplateService, runTypeGeneration };
1156
+ export { CookieFlashStore, HandlePrecognitiveRequests, INERTIA_TOKENS, InertiaBuildCommand, InertiaDelete, InertiaDevCommand, InertiaGet, InertiaInstallCommand, InertiaMiddleware, InertiaModule, InertiaPatch, InertiaPost, InertiaPut, InertiaRoute, InertiaService, InertiaTypesCommand, ManifestService, SsrRendererService, TemplateService, runTypeGeneration };
783
1157
 
784
1158
  //# sourceMappingURL=index.mjs.map