@stratal/inertia 0.0.21 → 0.0.23

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,14 +1,10 @@
1
- import { n as runTypeGeneration, t as findPagesDir } from "./type-generator-o_PxETTs.mjs";
2
- import { DI_TOKENS, Scope, Transient, inject } from "stratal/di";
1
+ import { t as __decorate } from "./decorate-B7nr7eBl.mjs";
2
+ import { n as buildSeoTags, r as descriptorToHtml, t as DATA_SEO_ATTR } from "./build-seo-tags-DBsHKxX9.mjs";
3
3
  import { ApplicationError } from "stratal/errors";
4
- import { I18N_TOKENS } from "stratal/i18n";
5
4
  import { Module } from "stratal/module";
6
- import { Delete, Get, Patch, Post, Put, ROUTER_TOKENS, Route, RouterContext, SchemaValidationError } from "stratal/router";
7
- import { spawn } from "node:child_process";
8
- import { existsSync, mkdirSync, writeFileSync } from "node:fs";
9
- import { dirname, join, relative } from "node:path";
10
- import { Command } from "stratal/quarry";
11
- import { watch } from "node:fs/promises";
5
+ import { Delete, Get, Patch, Post, Put, ROUTER_TOKENS, Route, RouterContext, SchemaValidationError, applyTrailingSlash } from "stratal/router";
6
+ import { CONTAINER_TOKEN, DI_TOKENS, Request, Singleton, Transient, inject } from "stratal/di";
7
+ import { I18N_TOKENS } from "stratal/i18n";
12
8
  import { LOGGER_TOKENS } from "stratal/logger";
13
9
  import { deleteCookie, getSignedCookie, setSignedCookie } from "hono/cookie";
14
10
  import { z } from "stratal/validation";
@@ -44,359 +40,35 @@ function augmentRouterContext(resolveService) {
44
40
  const flashOut = this.c.get("inertiaFlashOut");
45
41
  if (flashOut) flashOut[key] = value;
46
42
  });
43
+ RouterContext.macro("share", function(key, value) {
44
+ resolveService(this).share(key, value);
45
+ });
46
+ RouterContext.macro("seo", function(data) {
47
+ resolveService(this).seo(data);
48
+ });
47
49
  RouterContext.macro("withoutSsr", function() {
48
50
  this.c.set("withoutSsr", true);
49
51
  });
50
52
  }
51
53
  //#endregion
52
- //#region src/vite/create-vite-config.ts
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;
89
- }
90
- //#endregion
91
- //#region src/commands/inertia-build.command.ts
92
- var InertiaBuildCommand = class extends Command {
93
- static command = "inertia:build {--outDir=dist : Output directory} {--ssr : Also build SSR bundle}";
94
- static description = "Build Inertia.js frontend for production";
95
- async handle() {
96
- const outDir = this.string("outDir") || "dist";
97
- const shouldBuildSsr = this.boolean("ssr");
98
- const cwd = process.cwd();
99
- if (!existsSync(join(cwd, "src/inertia/app.tsx"))) {
100
- this.fail("src/inertia/app.tsx not found. Run `quarry inertia:install` first.");
101
- return 1;
102
- }
103
- const configPath = writeTempViteConfig({
104
- cwd,
105
- outDir
106
- });
107
- this.info("Building Inertia.js frontend for production...");
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;
120
- }
121
- this.success("SSR build complete!");
122
- }
123
- this.success(`Output in ${outDir}/`);
124
- this.info("Deploy with: npx wrangler deploy");
125
- return 0;
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
- }
148
- };
149
- //#endregion
150
- //#region src/commands/inertia-dev.command.ts
151
- var InertiaDevCommand = class extends Command {
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.}";
153
- static description = "Start Inertia.js Vite development server";
154
- async handle() {
155
- const port = this.number("port");
156
- const host = this.boolean("host");
157
- const persistTo = this.string("persist-to");
158
- const cwd = process.cwd();
159
- if (!existsSync(join(cwd, "src/inertia/app.tsx"))) {
160
- this.fail("src/inertia/app.tsx not found. Run `quarry inertia:install` first.");
161
- return 1;
162
- }
163
- const configPath = writeTempViteConfig({
164
- cwd,
165
- server: {
166
- port,
167
- host
168
- },
169
- persistTo
170
- });
171
- this.info("Starting Vite dev server...");
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, {
181
- cwd,
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
- });
193
- }
194
- };
195
- //#endregion
196
- //#region src/commands/inertia-install.command.ts
197
- const ROOT_HTML = `<!DOCTYPE html>
198
- <html lang="en">
199
- <head>
200
- <meta charset="utf-8" />
201
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
202
- @viteHead
203
- @inertiaHead
204
- </head>
205
- <body>
206
- @inertia
207
- @viteScripts
208
- </body>
209
- </html>`;
210
- const APP_TSX = `import { createInertiaApp } from '@inertiajs/react'
211
-
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
- })`;
220
- const HOME_TSX = `export default function Home({ message }: { message: string }) {
221
- return (
222
- <div>
223
- <h1>{message}</h1>
224
- <p>This page is rendered with Inertia.js and Stratal.</p>
225
- </div>
226
- )
227
- }`;
228
- var InertiaInstallCommand = class extends Command {
229
- static command = "inertia:install {--skip-deps : Skip installing npm dependencies}";
230
- static description = "Scaffold Inertia.js files for a Stratal project";
231
- async handle() {
232
- const skipDeps = this.boolean("skip-deps");
233
- const cwd = process.cwd();
234
- const inertiaDir = join(cwd, "src", "inertia");
235
- const pagesDir = join(inertiaDir, "pages");
236
- this.info("Creating src/inertia/ directory...");
237
- mkdirSync(pagesDir, { recursive: true });
238
- const publicDir = join(inertiaDir, "public");
239
- mkdirSync(publicDir, { recursive: true });
240
- const gitkeepPath = join(publicDir, ".gitkeep");
241
- if (!existsSync(gitkeepPath)) writeFileSync(gitkeepPath, "", "utf-8");
242
- this.success("Created src/inertia/public/");
243
- const files = [
244
- {
245
- path: join(inertiaDir, "root.html"),
246
- content: ROOT_HTML,
247
- name: "root.html"
248
- },
249
- {
250
- path: join(inertiaDir, "app.tsx"),
251
- content: APP_TSX,
252
- name: "app.tsx"
253
- },
254
- {
255
- path: join(pagesDir, "Home.tsx"),
256
- content: HOME_TSX,
257
- name: "pages/Home.tsx"
258
- }
259
- ];
260
- for (const file of files) if (existsSync(file.path)) this.warn(`Skipping ${file.name} (already exists)`);
261
- else {
262
- writeFileSync(file.path, file.content, "utf-8");
263
- this.success(`Created src/inertia/${file.name}`);
264
- }
265
- const appModulePath = join(cwd, "src", "app.module.ts");
266
- if (existsSync(appModulePath)) {
267
- this.info("Updating src/app.module.ts...");
268
- try {
269
- if (await this.updateAppModule(appModulePath)) this.success("Updated src/app.module.ts with InertiaModule");
270
- else this.info("InertiaModule already configured in app.module.ts");
271
- } catch (err) {
272
- this.warn(`Could not auto-update app.module.ts: ${err.message}`);
273
- this.info("Please manually add InertiaModule.forRoot() to your module imports");
274
- }
275
- } else this.info("No src/app.module.ts found — please manually configure InertiaModule");
276
- try {
277
- const { outputPath, pageCount } = await runTypeGeneration(cwd);
278
- const relPath = relative(cwd, outputPath);
279
- this.success(`Generated ${relPath} (${pageCount} page${pageCount !== 1 ? "s" : ""})`);
280
- } catch {
281
- this.warn("Could not generate initial type definitions. Run `quarry inertia:types` manually.");
282
- }
283
- if (!skipDeps) {
284
- this.newLine();
285
- this.info("Install the following dependencies:");
286
- this.line(" npm install @stratal/inertia @inertiajs/react @inertiajs/vite react react-dom");
287
- this.line(" npm install -D @types/react @types/react-dom vite @cloudflare/vite-plugin");
288
- }
289
- this.newLine();
290
- this.success("Inertia.js scaffolding complete!");
291
- this.info("Run `quarry inertia:dev` to start the dev server");
292
- return 0;
293
- }
294
- async updateAppModule(modulePath) {
295
- const { Project, SyntaxKind } = await import("ts-morph");
296
- const sourceFile = new Project({ useInMemoryFileSystem: false }).addSourceFileAtPath(modulePath);
297
- if (sourceFile.getImportDeclaration((decl) => decl.getModuleSpecifierValue() === "@stratal/inertia")) return false;
298
- sourceFile.addImportDeclaration({
299
- defaultImport: "rootView",
300
- moduleSpecifier: "./inertia/root.html?raw"
301
- });
302
- sourceFile.addImportDeclaration({
303
- namedImports: ["InertiaModule"],
304
- moduleSpecifier: "@stratal/inertia"
305
- });
306
- const classes = sourceFile.getClasses();
307
- for (const cls of classes) {
308
- const moduleDecorator = cls.getDecorator("Module");
309
- if (!moduleDecorator) continue;
310
- const args = moduleDecorator.getArguments();
311
- if (args.length === 0) continue;
312
- const objLiteral = args[0].asKind(SyntaxKind.ObjectLiteralExpression);
313
- if (!objLiteral) continue;
314
- const importsProp = objLiteral.getProperty("imports");
315
- if (importsProp) {
316
- const arrayLiteral = (importsProp.asKind(SyntaxKind.PropertyAssignment)?.getInitializer())?.asKind(SyntaxKind.ArrayLiteralExpression);
317
- if (arrayLiteral) arrayLiteral.addElement(`InertiaModule.forRoot({\n rootView,\n })`);
318
- } else objLiteral.addPropertyAssignment({
319
- name: "imports",
320
- initializer: `[\n InertiaModule.forRoot({\n rootView,\n }),\n ]`
321
- });
322
- break;
323
- }
324
- await sourceFile.save();
325
- return true;
326
- }
327
- };
328
- //#endregion
329
- //#region src/commands/inertia-types.command.ts
330
- var InertiaTypesCommand = class extends Command {
331
- static command = "inertia:types {--watch : Watch for changes and regenerate}";
332
- static description = "Generate Inertia.js page type definitions";
333
- async handle() {
334
- const cwd = process.cwd();
335
- if (!existsSync(findPagesDir(cwd))) {
336
- this.fail("src/inertia/pages/ not found. Run `quarry inertia:install` first.");
337
- return 1;
338
- }
339
- if (!await this.generate(cwd)) return 1;
340
- if (this.boolean("watch")) {
341
- this.info("Watching for changes...");
342
- await this.watchForChanges(cwd);
343
- }
344
- return 0;
345
- }
346
- async generate(cwd) {
347
- try {
348
- const { outputPath, pageCount } = await runTypeGeneration(cwd);
349
- const relPath = relative(cwd, outputPath);
350
- this.success(`Generated ${relPath} (${pageCount} page${pageCount !== 1 ? "s" : ""})`);
351
- return true;
352
- } catch (err) {
353
- this.fail(`Type generation failed: ${err.message}`);
354
- return false;
355
- }
356
- }
357
- async watchForChanges(cwd) {
358
- const srcDir = join(cwd, "src");
359
- try {
360
- const watcher = watch(srcDir, { recursive: true });
361
- for await (const event of watcher) if (event.filename && /\.(tsx|ts)$/.test(event.filename)) {
362
- this.info(`Change detected: ${event.filename}`);
363
- await this.generate(cwd);
364
- }
365
- } catch (err) {
366
- this.fail(`Watch failed: ${err.message}`);
367
- }
368
- }
369
- };
370
- //#endregion
371
54
  //#region src/inertia.tokens.ts
372
55
  const INERTIA_TOKENS = {
373
56
  Options: Symbol.for("stratal:inertia:options"),
374
57
  InertiaService: Symbol.for("stratal:inertia:service"),
375
58
  TemplateService: Symbol.for("stratal:inertia:template"),
376
59
  ManifestService: Symbol.for("stratal:inertia:manifest"),
377
- SsrRenderer: Symbol.for("stratal:inertia:ssr-renderer")
60
+ SsrRenderer: Symbol.for("stratal:inertia:ssr-renderer"),
61
+ HreflangService: Symbol.for("stratal:inertia:hreflang"),
62
+ SeoService: Symbol.for("stratal:inertia:seo")
378
63
  };
379
64
  //#endregion
380
- //#region \0@oxc-project+runtime@0.129.0/helpers/decorateMetadata.js
381
- function __decorateMetadata(k, v) {
382
- if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
383
- }
384
- //#endregion
385
- //#region \0@oxc-project+runtime@0.129.0/helpers/decorateParam.js
65
+ //#region \0@oxc-project+runtime@0.133.0/helpers/esm/decorateParam.js
386
66
  function __decorateParam(paramIndex, decorator) {
387
67
  return function(target, key) {
388
68
  decorator(target, key, paramIndex);
389
69
  };
390
70
  }
391
71
  //#endregion
392
- //#region \0@oxc-project+runtime@0.129.0/helpers/decorate.js
393
- function __decorate(decorators, target, key, desc) {
394
- var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
395
- if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
396
- else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
397
- return c > 3 && r && Object.defineProperty(target, key, r), r;
398
- }
399
- //#endregion
400
72
  //#region src/middleware/inertia.middleware.ts
401
73
  let InertiaMiddleware = class InertiaMiddleware {
402
74
  options;
@@ -440,11 +112,61 @@ let InertiaMiddleware = class InertiaMiddleware {
440
112
  }
441
113
  }
442
114
  };
443
- InertiaMiddleware = __decorate([
444
- Transient(),
445
- __decorateParam(0, inject(INERTIA_TOKENS.Options)),
446
- __decorateMetadata("design:paramtypes", [Object])
447
- ], InertiaMiddleware);
115
+ InertiaMiddleware = __decorate([Transient(), __decorateParam(0, inject(INERTIA_TOKENS.Options))], InertiaMiddleware);
116
+ //#endregion
117
+ //#region src/services/hreflang.service.ts
118
+ let HreflangService = class HreflangService {
119
+ container;
120
+ constructor(container) {
121
+ this.container = container;
122
+ }
123
+ buildLinks(currentUrl) {
124
+ const i18n = this.container.tryResolve(I18N_TOKENS.Options);
125
+ if (!i18n) return [];
126
+ const locales = i18n.locales ?? ["en"];
127
+ if (locales.length < 2) return [];
128
+ const defaultLocale = i18n.defaultLocale ?? "en";
129
+ const trailingSlash = this.container.resolve(DI_TOKENS.Application).config.trailingSlash ?? "ignore";
130
+ const localeUrl = this.container.resolve(ROUTER_TOKENS.LocaleUrlService);
131
+ if (localeUrl.pathEnabled) return this.buildPathLinks(currentUrl, locales, defaultLocale, localeUrl, trailingSlash);
132
+ if ((i18n.detection && "strategy" in i18n.detection ? i18n.detection.strategy : void 0) === "querystring") return this.buildQuerystringLinks(currentUrl, locales, defaultLocale, trailingSlash);
133
+ return [];
134
+ }
135
+ buildPathLinks(url, locales, defaultLocale, localeUrl, trailingSlash) {
136
+ const basePath = localeUrl.stripPrefix(url.pathname);
137
+ const links = locales.map((locale) => this.linkTag(locale, this.compose(url, localeUrl.applyPrefix(basePath, locale), url.search, trailingSlash)));
138
+ links.push(this.linkTag("x-default", this.compose(url, localeUrl.applyPrefix(basePath, defaultLocale), url.search, trailingSlash)));
139
+ return links;
140
+ }
141
+ buildQuerystringLinks(url, locales, defaultLocale, trailingSlash) {
142
+ const params = new URLSearchParams(url.search);
143
+ params.delete("locale");
144
+ const baseQs = params.toString();
145
+ const links = locales.map((locale) => {
146
+ const qs = this.composeQuery(baseQs, locale === defaultLocale ? null : ["locale", locale]);
147
+ return this.linkTag(locale, this.compose(url, url.pathname, qs, trailingSlash));
148
+ });
149
+ const xDefaultQs = baseQs ? `?${baseQs}` : "";
150
+ links.push(this.linkTag("x-default", this.compose(url, url.pathname, xDefaultQs, trailingSlash)));
151
+ return links;
152
+ }
153
+ compose(url, pathname, search, mode) {
154
+ return applyTrailingSlash(url.origin + pathname + search, mode);
155
+ }
156
+ composeQuery(baseQs, extra) {
157
+ if (!extra) return baseQs ? `?${baseQs}` : "";
158
+ const tail = `${extra[0]}=${encodeURIComponent(extra[1])}`;
159
+ return baseQs ? `?${baseQs}&${tail}` : `?${tail}`;
160
+ }
161
+ linkTag(hreflang, href) {
162
+ return {
163
+ rel: "alternate",
164
+ hreflang,
165
+ href
166
+ };
167
+ }
168
+ };
169
+ HreflangService = __decorate([Singleton(), __decorateParam(0, inject(CONTAINER_TOKEN))], HreflangService);
448
170
  //#endregion
449
171
  //#region src/types.ts
450
172
  const INERTIA_PROP_OPTIONAL = Symbol.for("stratal:inertia:prop:optional");
@@ -458,15 +180,20 @@ let InertiaService = class InertiaService {
458
180
  options;
459
181
  template;
460
182
  ssr;
183
+ seoService;
461
184
  sharedData = {};
462
- constructor(options, template, ssr) {
185
+ constructor(options, template, ssr, seoService) {
463
186
  this.options = options;
464
187
  this.template = template;
465
188
  this.ssr = ssr;
189
+ this.seoService = seoService;
466
190
  }
467
191
  share(key, value) {
468
192
  this.sharedData[key] = value;
469
193
  }
194
+ seo(data) {
195
+ this.seoService.set(data);
196
+ }
470
197
  location(url) {
471
198
  return new Response("", {
472
199
  status: 409,
@@ -513,12 +240,18 @@ let InertiaService = class InertiaService {
513
240
  const url = reqUrl.search ? `${reqUrl.pathname}${reqUrl.search}` : reqUrl.pathname;
514
241
  const isInertia = ctx.c.get("inertia");
515
242
  const { shared: resolvedShared, sharedKeys } = await this.resolveSharedData(ctx);
243
+ const resolvedSeo = await this.seoService.resolve(ctx);
516
244
  const allProps = {
517
245
  ...resolvedShared,
518
246
  ...this.sharedData,
247
+ seo: this.always(() => resolvedSeo),
519
248
  ...props
520
249
  };
521
- const allSharedKeys = [...sharedKeys, ...Object.keys(this.sharedData)];
250
+ const allSharedKeys = [
251
+ ...sharedKeys,
252
+ ...Object.keys(this.sharedData),
253
+ "seo"
254
+ ];
522
255
  const result = await this.processProps(allProps, ctx, component, isInertia);
523
256
  const { errors: flashErrors, ...flash } = ctx.c.get("inertiaFlash") ?? {};
524
257
  const errors = flashErrors && typeof flashErrors === "object" && !Array.isArray(flashErrors) ? flashErrors : {};
@@ -545,8 +278,9 @@ let InertiaService = class InertiaService {
545
278
  ...renderOptions.clearHistory ? { clearHistory: true } : {},
546
279
  ...renderOptions.preserveFragment ? { preserveFragment: true } : {}
547
280
  };
281
+ const status = renderOptions.status ?? 200;
548
282
  if (isInertia) return new Response(JSON.stringify(page), {
549
- status: 200,
283
+ status,
550
284
  headers: {
551
285
  "Content-Type": "application/json",
552
286
  "X-Inertia": "true",
@@ -557,9 +291,10 @@ let InertiaService = class InertiaService {
557
291
  head: [],
558
292
  body: ""
559
293
  } : await this.ssr.render(page);
560
- const html = this.template.render(page, ssrResult.head, ssrResult.body);
294
+ const seoTags = this.seoService.tagsFor(resolvedSeo);
295
+ const html = this.template.render(page, [...ssrResult.head, ...seoTags], ssrResult.body);
561
296
  return new Response(html, {
562
- status: 200,
297
+ status,
563
298
  headers: { "Content-Type": "text/html; charset=utf-8" }
564
299
  });
565
300
  }
@@ -588,6 +323,7 @@ let InertiaService = class InertiaService {
588
323
  const uri = container.resolve(ROUTER_TOKENS.Uri);
589
324
  const name = registry.findNameByRoute(ctx.c.req.method, ctx.c.req.routePath) ?? null;
590
325
  const params = { ...ctx.param() };
326
+ const localePathService = container.resolve(ROUTER_TOKENS.LocalePathService);
591
327
  shared.routes = this.serializeRoutes(registry.named());
592
328
  shared.trailingSlash = application.config.trailingSlash ?? "ignore";
593
329
  shared.route = {
@@ -595,6 +331,10 @@ let InertiaService = class InertiaService {
595
331
  params,
596
332
  defaults: uri.getDefaults()
597
333
  };
334
+ shared.localeConfig = {
335
+ defaultLocale: localePathService.localePathConfig?.defaultLocale ?? null,
336
+ prefixDefaultLocale: localePathService.prefixDefaultLocale
337
+ };
598
338
  }
599
339
  return {
600
340
  shared,
@@ -619,6 +359,7 @@ let InertiaService = class InertiaService {
619
359
  const partialDataHeader = ctx.header("x-inertia-partial-data");
620
360
  const partialExceptHeader = ctx.header("x-inertia-partial-except");
621
361
  const resetHeader = ctx.header("x-inertia-reset");
362
+ const shouldResolveDeferred = ctx.header("x-inertia-resolve-deferred") === "true";
622
363
  const isPartialReload = isInertia && partialComponent === component && partialDataHeader;
623
364
  const requestedProps = partialDataHeader?.split(",").map((s) => s.trim()) ?? [];
624
365
  const exceptProps = partialExceptHeader?.split(",").map((s) => s.trim()) ?? [];
@@ -641,7 +382,8 @@ let InertiaService = class InertiaService {
641
382
  }
642
383
  if (this.isDeferredProp(value)) {
643
384
  if (isPartialReload && this.isRequested(key, requestedProps)) resolvedProps[key] = await value.callback();
644
- else if (!isPartialReload) {
385
+ else if (!isPartialReload) if (shouldResolveDeferred) resolvedProps[key] = await value.callback();
386
+ else {
645
387
  deferredProps[value.group] ??= [];
646
388
  deferredProps[value.group].push(key);
647
389
  }
@@ -728,15 +470,11 @@ let InertiaService = class InertiaService {
728
470
  }
729
471
  };
730
472
  InertiaService = __decorate([
731
- Transient(INERTIA_TOKENS.InertiaService),
473
+ Request(INERTIA_TOKENS.InertiaService),
732
474
  __decorateParam(0, inject(INERTIA_TOKENS.Options)),
733
475
  __decorateParam(1, inject(INERTIA_TOKENS.TemplateService)),
734
476
  __decorateParam(2, inject(INERTIA_TOKENS.SsrRenderer)),
735
- __decorateMetadata("design:paramtypes", [
736
- Object,
737
- Object,
738
- Object
739
- ])
477
+ __decorateParam(3, inject(INERTIA_TOKENS.SeoService))
740
478
  ], InertiaService);
741
479
  //#endregion
742
480
  //#region src/services/manifest.service.ts
@@ -744,12 +482,11 @@ const DEFAULT_ENTRY_CLIENT_PATH = "src/inertia/app.tsx";
744
482
  let ManifestService = class ManifestService {
745
483
  manifest;
746
484
  entryClientPath;
485
+ isDev = Boolean(import.meta.env.DEV);
747
486
  constructor(options) {
748
- this.manifest = options.manifest ?? null;
487
+ this.manifest = globalThis.__STRATAL_INERTIA_MANIFEST__ ?? null;
749
488
  this.entryClientPath = (options.entryClientPath ?? DEFAULT_ENTRY_CLIENT_PATH).replace(/^\/+/, "");
750
- }
751
- get isDev() {
752
- return this.manifest === null;
489
+ if (!this.isDev && !this.manifest) throw new Error("@stratal/inertia: production build is missing the Vite client manifest. This is wired by stratalInertia() in vite.config.ts — confirm it is in your plugin list and that the client environment built successfully before the worker environment.");
753
490
  }
754
491
  getHeadTags() {
755
492
  if (this.isDev) return "<link rel=\"stylesheet\" href=\"/__inertia/ssr-css\" data-ssr-css />";
@@ -779,11 +516,71 @@ hot.on("vite:afterUpdate", () => {
779
516
  return tags.join("\n");
780
517
  }
781
518
  };
782
- ManifestService = __decorate([
783
- Transient(),
519
+ ManifestService = __decorate([Transient(), __decorateParam(0, inject(INERTIA_TOKENS.Options))], ManifestService);
520
+ //#endregion
521
+ //#region src/services/seo.service.ts
522
+ let SeoService = class SeoService {
523
+ options;
524
+ hreflang;
525
+ accumulated = {};
526
+ constructor(options, hreflang) {
527
+ this.options = options;
528
+ this.hreflang = hreflang;
529
+ }
530
+ /** Merges the given metadata into the request's accumulated SEO data. */
531
+ set(data) {
532
+ this.accumulated = mergeSeo(this.accumulated, data);
533
+ }
534
+ /**
535
+ * Resolves the final SEO data: module defaults (base) merged with the
536
+ * request's accumulated data, then the title template applied. Resolver
537
+ * functions for `defaults`/`titleTemplate` are awaited with the request `ctx`.
538
+ * Locale-aware `hreflang` alternates are appended last so they ride the same
539
+ * head injection and SPA reconciliation as the rest of the SEO tags.
540
+ *
541
+ * The resolved `title` is ALWAYS a string (falling back to `''`). This makes
542
+ * the `<title>` descriptor deterministic: every navigation — including to a
543
+ * page with no SEO — produces a title, so the client head-sync sets
544
+ * `document.title` rather than leaving the previous page's title stale.
545
+ */
546
+ async resolve(ctx) {
547
+ const seo = this.options.seo;
548
+ const resolved = mergeSeo(typeof seo?.defaults === "function" ? await seo.defaults(ctx) : seo?.defaults ?? {}, this.accumulated);
549
+ const template = seo?.titleTemplate;
550
+ if (typeof template === "function") resolved.title = await template(resolved.title, ctx);
551
+ else if (typeof template === "string" && this.accumulated.title != null) resolved.title = template.split("%s").join(this.accumulated.title);
552
+ resolved.title ??= "";
553
+ const hreflang = this.hreflang.buildLinks(new URL(ctx.c.req.url));
554
+ if (hreflang.length > 0) resolved.link = [...resolved.link ?? [], ...hreflang];
555
+ return resolved;
556
+ }
557
+ /** Renders resolved SEO data into a list of head-tag HTML strings. */
558
+ tagsFor(resolved) {
559
+ return buildSeoTags(resolved).map(descriptorToHtml);
560
+ }
561
+ };
562
+ SeoService = __decorate([
563
+ Request(INERTIA_TOKENS.SeoService),
784
564
  __decorateParam(0, inject(INERTIA_TOKENS.Options)),
785
- __decorateMetadata("design:paramtypes", [Object])
786
- ], ManifestService);
565
+ __decorateParam(1, inject(INERTIA_TOKENS.HreflangService))
566
+ ], SeoService);
567
+ /** Merges `b` over `a`: `openGraph`/`twitter` shallow-merge, `meta`/`link` concat, scalars overwrite. */
568
+ function mergeSeo(a, b) {
569
+ return {
570
+ ...a,
571
+ ...b,
572
+ ...a.openGraph || b.openGraph ? { openGraph: {
573
+ ...a.openGraph,
574
+ ...b.openGraph
575
+ } } : {},
576
+ ...a.twitter || b.twitter ? { twitter: {
577
+ ...a.twitter,
578
+ ...b.twitter
579
+ } } : {},
580
+ ...a.meta || b.meta ? { meta: [...a.meta ?? [], ...b.meta ?? []] } : {},
581
+ ...a.link || b.link ? { link: [...a.link ?? [], ...b.link ?? []] } : {}
582
+ };
583
+ }
787
584
  //#endregion
788
585
  //#region src/services/ssr-renderer.service.ts
789
586
  let SsrRendererService = class SsrRendererService {
@@ -827,10 +624,9 @@ let SsrRendererService = class SsrRendererService {
827
624
  }
828
625
  };
829
626
  SsrRendererService = __decorate([
830
- Transient(),
627
+ Singleton(),
831
628
  __decorateParam(0, inject(INERTIA_TOKENS.Options)),
832
- __decorateParam(1, inject(LOGGER_TOKENS.LoggerService)),
833
- __decorateMetadata("design:paramtypes", [Object, Object])
629
+ __decorateParam(1, inject(LOGGER_TOKENS.LoggerService))
834
630
  ], SsrRendererService);
835
631
  //#endregion
836
632
  //#region src/services/template.service.ts
@@ -847,10 +643,10 @@ let TemplateService = class TemplateService {
847
643
  const viteHead = this.manifest.getHeadTags();
848
644
  const viteScripts = this.manifest.getScriptTags();
849
645
  let html = this.options.rootView;
850
- html = html.replace("@inertiaHead", headTags);
851
- html = html.replace("@inertia", appHtml);
852
- html = html.replace("@viteHead", viteHead);
853
- html = html.replace("@viteScripts", viteScripts);
646
+ html = html.replace("@inertiaHead", () => headTags);
647
+ html = html.replace("@inertia", () => appHtml);
648
+ html = html.replace("@viteHead", () => viteHead);
649
+ html = html.replace("@viteScripts", () => viteScripts);
854
650
  return html;
855
651
  }
856
652
  buildClientOnlyBody(page) {
@@ -860,8 +656,7 @@ let TemplateService = class TemplateService {
860
656
  TemplateService = __decorate([
861
657
  Transient(),
862
658
  __decorateParam(0, inject(INERTIA_TOKENS.Options)),
863
- __decorateParam(1, inject(INERTIA_TOKENS.ManifestService)),
864
- __decorateMetadata("design:paramtypes", [Object, Object])
659
+ __decorateParam(1, inject(INERTIA_TOKENS.ManifestService))
865
660
  ], TemplateService);
866
661
  //#endregion
867
662
  //#region src/inertia.module.ts
@@ -894,7 +689,8 @@ let InertiaModule = _InertiaModule = class InertiaModule {
894
689
  if (context.type !== "http") return void 0;
895
690
  if (this.isPrecognitionRequest(context)) return this.handlePrecognitionValidationError(error, context);
896
691
  if (!this.isInertiaRequest(context)) return void 0;
897
- const issues = error.metadata?.issues ?? [];
692
+ if (this.isReadRequest(context)) return void 0;
693
+ const issues = error.issues ?? [];
898
694
  const errors = {};
899
695
  for (const issue of issues) errors[issue.path] = issue.message;
900
696
  context.ctx.flash("errors", errors);
@@ -902,12 +698,23 @@ let InertiaModule = _InertiaModule = class InertiaModule {
902
698
  });
903
699
  handler.renderable(ApplicationError, (error, context) => {
904
700
  if (context.type !== "http") return void 0;
905
- const message = context.ctx.getContainer().resolve(I18N_TOKENS.I18nService).t(error.message, error.metadata);
701
+ const message = error.message;
906
702
  if (this.isPrecognitionRequest(context)) return this.createPrecognitionErrorResponse({ _form: message });
907
703
  if (!this.isInertiaRequest(context)) return void 0;
704
+ if (this.isReadRequest(context)) return void 0;
908
705
  context.ctx.flash("errors", { _form: message });
909
706
  return this.redirectBack(context);
910
707
  });
708
+ handler.errorPage(async (errorResponse, status, context) => {
709
+ try {
710
+ return await context.ctx.getContainer().resolve(INERTIA_TOKENS.InertiaService).render(context.ctx, `Errors/${status}`, {
711
+ status,
712
+ message: errorResponse.message
713
+ }, { status });
714
+ } catch {
715
+ return;
716
+ }
717
+ });
911
718
  }
912
719
  onInitialize() {
913
720
  augmentRouterContext((ctx) => {
@@ -917,11 +724,28 @@ let InertiaModule = _InertiaModule = class InertiaModule {
917
724
  isInertiaRequest(context) {
918
725
  return context.ctx.header("x-inertia") === "true";
919
726
  }
727
+ /**
728
+ * GET/HEAD requests are idempotent navigations — including Inertia deferred
729
+ * partial reloads, which fetch deferred props over a follow-up XHR that still
730
+ * carries `X-Inertia: true`.
731
+ *
732
+ * Such requests must NOT use the flash-errors + redirect-back convention: the
733
+ * redirect points back at the very URL that just threw, so an error raised
734
+ * while resolving a deferred prop would redirect → re-request → throw again
735
+ * in an infinite loop (`ERR_TOO_MANY_REDIRECTS`). For these we fall through to
736
+ * the errorPage pipeline, which renders `Errors/${status}` in place as an
737
+ * Inertia response. Redirect-back stays for mutations (POST/PUT/PATCH/DELETE),
738
+ * where it drives the post-submit form-error flow.
739
+ */
740
+ isReadRequest(context) {
741
+ const method = context.ctx.c.req.method.toUpperCase();
742
+ return method === "GET" || method === "HEAD";
743
+ }
920
744
  isPrecognitionRequest(context) {
921
745
  return context.ctx.header("precognition") === "true";
922
746
  }
923
747
  handlePrecognitionValidationError(error, context) {
924
- const issues = error.metadata?.issues ?? [];
748
+ const issues = error.issues ?? [];
925
749
  let errors = {};
926
750
  for (const issue of issues) errors[issue.path] = issue.message;
927
751
  const validateOnly = context.ctx.header("precognition-validate-only");
@@ -964,8 +788,7 @@ let InertiaModule = _InertiaModule = class InertiaModule {
964
788
  InertiaModule = _InertiaModule = __decorate([Module({ providers: [
965
789
  {
966
790
  provide: INERTIA_TOKENS.InertiaService,
967
- useClass: InertiaService,
968
- scope: Scope.Request
791
+ useClass: InertiaService
969
792
  },
970
793
  {
971
794
  provide: INERTIA_TOKENS.TemplateService,
@@ -977,13 +800,16 @@ InertiaModule = _InertiaModule = __decorate([Module({ providers: [
977
800
  },
978
801
  {
979
802
  provide: INERTIA_TOKENS.SsrRenderer,
980
- useClass: SsrRendererService,
981
- scope: Scope.Singleton
803
+ useClass: SsrRendererService
804
+ },
805
+ {
806
+ provide: INERTIA_TOKENS.HreflangService,
807
+ useClass: HreflangService
982
808
  },
983
- InertiaInstallCommand,
984
- InertiaTypesCommand,
985
- InertiaDevCommand,
986
- InertiaBuildCommand
809
+ {
810
+ provide: INERTIA_TOKENS.SeoService,
811
+ useClass: SeoService
812
+ }
987
813
  ] })], InertiaModule);
988
814
  //#endregion
989
815
  //#region src/flash/cookie-flash-store.ts
@@ -1175,6 +1001,6 @@ let HandlePrecognitiveRequests = class HandlePrecognitiveRequests {
1175
1001
  };
1176
1002
  HandlePrecognitiveRequests = __decorate([Transient()], HandlePrecognitiveRequests);
1177
1003
  //#endregion
1178
- export { CookieFlashStore, HandlePrecognitiveRequests, INERTIA_TOKENS, InertiaBuildCommand, InertiaDelete, InertiaDevCommand, InertiaGet, InertiaInstallCommand, InertiaMiddleware, InertiaModule, InertiaPatch, InertiaPost, InertiaPut, InertiaRoute, InertiaService, InertiaTypesCommand, ManifestService, SsrRendererService, TemplateService, runTypeGeneration };
1004
+ export { CookieFlashStore, DATA_SEO_ATTR, HandlePrecognitiveRequests, INERTIA_TOKENS, InertiaDelete, InertiaGet, InertiaMiddleware, InertiaModule, InertiaPatch, InertiaPost, InertiaPut, InertiaRoute, InertiaService, ManifestService, SeoService, SsrRendererService, TemplateService, buildSeoTags, descriptorToHtml };
1179
1005
 
1180
1006
  //# sourceMappingURL=index.mjs.map