@stratal/inertia 0.0.22 → 0.0.24
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/README.md +33 -1
- package/dist/build-seo-tags-DBsHKxX9.mjs +123 -0
- package/dist/build-seo-tags-DBsHKxX9.mjs.map +1 -0
- package/dist/{decorate-CzXVx7ZH.mjs → decorate-B7nr7eBl.mjs} +1 -1
- package/dist/generator/type-generator.worker.mjs +1 -1
- package/dist/generator/type-generator.worker.mjs.map +1 -1
- package/dist/index.d.mts +209 -78
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +274 -60
- package/dist/index.mjs.map +1 -1
- package/dist/quarry.d.mts +9 -0
- package/dist/quarry.d.mts.map +1 -1
- package/dist/quarry.mjs +56 -9
- package/dist/quarry.mjs.map +1 -1
- package/dist/react.d.mts +15 -3
- package/dist/react.d.mts.map +1 -1
- package/dist/react.mjs +21 -8
- package/dist/react.mjs.map +1 -1
- package/dist/seo-runtime.d.mts +1 -0
- package/dist/seo-runtime.mjs +56 -0
- package/dist/seo-runtime.mjs.map +1 -0
- package/dist/ssr.d.mts +65 -0
- package/dist/ssr.d.mts.map +1 -0
- package/dist/ssr.mjs +56 -0
- package/dist/ssr.mjs.map +1 -0
- package/dist/testing.d.mts +1 -1
- package/dist/testing.mjs.map +1 -1
- package/dist/{type-generator-bfo14BJI.mjs → type-generator-DFpha_Fp.mjs} +178 -28
- package/dist/type-generator-DFpha_Fp.mjs.map +1 -0
- package/dist/types-BhgXhWx6.d.mts +82 -0
- package/dist/types-BhgXhWx6.d.mts.map +1 -0
- package/dist/types-DzE1pdZs.d.mts +76 -0
- package/dist/types-DzE1pdZs.d.mts.map +1 -0
- package/dist/vite.d.mts.map +1 -1
- package/dist/vite.mjs +22 -2
- package/dist/vite.mjs.map +1 -1
- package/package.json +27 -18
- package/dist/type-generator-bfo14BJI.mjs.map +0 -1
package/dist/index.mjs
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { t as __decorate } from "./decorate-
|
|
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";
|
|
2
3
|
import { ApplicationError } from "stratal/errors";
|
|
3
4
|
import { Module } from "stratal/module";
|
|
4
|
-
import { Delete, Get, Patch, Post, Put, ROUTER_TOKENS, Route, RouterContext, SchemaValidationError } from "stratal/router";
|
|
5
|
-
import { DI_TOKENS, Request, Singleton, Transient, inject } from "stratal/di";
|
|
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";
|
|
6
7
|
import { I18N_TOKENS } from "stratal/i18n";
|
|
7
|
-
import { LOGGER_TOKENS } from "stratal/logger";
|
|
8
8
|
import { deleteCookie, getSignedCookie, setSignedCookie } from "hono/cookie";
|
|
9
9
|
import { z } from "stratal/validation";
|
|
10
10
|
//#region src/augment/router-context.ts
|
|
@@ -39,6 +39,12 @@ function augmentRouterContext(resolveService) {
|
|
|
39
39
|
const flashOut = this.c.get("inertiaFlashOut");
|
|
40
40
|
if (flashOut) flashOut[key] = value;
|
|
41
41
|
});
|
|
42
|
+
RouterContext.macro("share", function(key, value) {
|
|
43
|
+
resolveService(this).share(key, value);
|
|
44
|
+
});
|
|
45
|
+
RouterContext.macro("seo", function(data) {
|
|
46
|
+
resolveService(this).seo(data);
|
|
47
|
+
});
|
|
42
48
|
RouterContext.macro("withoutSsr", function() {
|
|
43
49
|
this.c.set("withoutSsr", true);
|
|
44
50
|
});
|
|
@@ -50,10 +56,12 @@ const INERTIA_TOKENS = {
|
|
|
50
56
|
InertiaService: Symbol.for("stratal:inertia:service"),
|
|
51
57
|
TemplateService: Symbol.for("stratal:inertia:template"),
|
|
52
58
|
ManifestService: Symbol.for("stratal:inertia:manifest"),
|
|
53
|
-
SsrRenderer: Symbol.for("stratal:inertia:ssr-renderer")
|
|
59
|
+
SsrRenderer: Symbol.for("stratal:inertia:ssr-renderer"),
|
|
60
|
+
HreflangService: Symbol.for("stratal:inertia:hreflang"),
|
|
61
|
+
SeoService: Symbol.for("stratal:inertia:seo")
|
|
54
62
|
};
|
|
55
63
|
//#endregion
|
|
56
|
-
//#region \0@oxc-project+runtime@0.
|
|
64
|
+
//#region \0@oxc-project+runtime@0.133.0/helpers/esm/decorateParam.js
|
|
57
65
|
function __decorateParam(paramIndex, decorator) {
|
|
58
66
|
return function(target, key) {
|
|
59
67
|
decorator(target, key, paramIndex);
|
|
@@ -105,6 +113,60 @@ let InertiaMiddleware = class InertiaMiddleware {
|
|
|
105
113
|
};
|
|
106
114
|
InertiaMiddleware = __decorate([Transient(), __decorateParam(0, inject(INERTIA_TOKENS.Options))], InertiaMiddleware);
|
|
107
115
|
//#endregion
|
|
116
|
+
//#region src/services/hreflang.service.ts
|
|
117
|
+
let HreflangService = class HreflangService {
|
|
118
|
+
container;
|
|
119
|
+
constructor(container) {
|
|
120
|
+
this.container = container;
|
|
121
|
+
}
|
|
122
|
+
buildLinks(currentUrl) {
|
|
123
|
+
const i18n = this.container.tryResolve(I18N_TOKENS.Options);
|
|
124
|
+
if (!i18n) return [];
|
|
125
|
+
const locales = i18n.locales ?? ["en"];
|
|
126
|
+
if (locales.length < 2) return [];
|
|
127
|
+
const defaultLocale = i18n.defaultLocale ?? "en";
|
|
128
|
+
const trailingSlash = this.container.resolve(DI_TOKENS.Application).config.trailingSlash ?? "ignore";
|
|
129
|
+
const localeUrl = this.container.resolve(ROUTER_TOKENS.LocaleUrlService);
|
|
130
|
+
if (localeUrl.pathEnabled) return this.buildPathLinks(currentUrl, locales, defaultLocale, localeUrl, trailingSlash);
|
|
131
|
+
if ((i18n.detection && "strategy" in i18n.detection ? i18n.detection.strategy : void 0) === "querystring") return this.buildQuerystringLinks(currentUrl, locales, defaultLocale, trailingSlash);
|
|
132
|
+
return [];
|
|
133
|
+
}
|
|
134
|
+
buildPathLinks(url, locales, defaultLocale, localeUrl, trailingSlash) {
|
|
135
|
+
const basePath = localeUrl.stripPrefix(url.pathname);
|
|
136
|
+
const links = locales.map((locale) => this.linkTag(locale, this.compose(url, localeUrl.applyPrefix(basePath, locale), url.search, trailingSlash)));
|
|
137
|
+
links.push(this.linkTag("x-default", this.compose(url, localeUrl.applyPrefix(basePath, defaultLocale), url.search, trailingSlash)));
|
|
138
|
+
return links;
|
|
139
|
+
}
|
|
140
|
+
buildQuerystringLinks(url, locales, defaultLocale, trailingSlash) {
|
|
141
|
+
const params = new URLSearchParams(url.search);
|
|
142
|
+
params.delete("locale");
|
|
143
|
+
const baseQs = params.toString();
|
|
144
|
+
const links = locales.map((locale) => {
|
|
145
|
+
const qs = this.composeQuery(baseQs, locale === defaultLocale ? null : ["locale", locale]);
|
|
146
|
+
return this.linkTag(locale, this.compose(url, url.pathname, qs, trailingSlash));
|
|
147
|
+
});
|
|
148
|
+
const xDefaultQs = baseQs ? `?${baseQs}` : "";
|
|
149
|
+
links.push(this.linkTag("x-default", this.compose(url, url.pathname, xDefaultQs, trailingSlash)));
|
|
150
|
+
return links;
|
|
151
|
+
}
|
|
152
|
+
compose(url, pathname, search, mode) {
|
|
153
|
+
return applyTrailingSlash(url.origin + pathname + search, mode);
|
|
154
|
+
}
|
|
155
|
+
composeQuery(baseQs, extra) {
|
|
156
|
+
if (!extra) return baseQs ? `?${baseQs}` : "";
|
|
157
|
+
const tail = `${extra[0]}=${encodeURIComponent(extra[1])}`;
|
|
158
|
+
return baseQs ? `?${baseQs}&${tail}` : `?${tail}`;
|
|
159
|
+
}
|
|
160
|
+
linkTag(hreflang, href) {
|
|
161
|
+
return {
|
|
162
|
+
rel: "alternate",
|
|
163
|
+
hreflang,
|
|
164
|
+
href
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
HreflangService = __decorate([Singleton(), __decorateParam(0, inject(CONTAINER_TOKEN))], HreflangService);
|
|
169
|
+
//#endregion
|
|
108
170
|
//#region src/types.ts
|
|
109
171
|
const INERTIA_PROP_OPTIONAL = Symbol.for("stratal:inertia:prop:optional");
|
|
110
172
|
const INERTIA_PROP_DEFERRED = Symbol.for("stratal:inertia:prop:deferred");
|
|
@@ -117,15 +179,20 @@ let InertiaService = class InertiaService {
|
|
|
117
179
|
options;
|
|
118
180
|
template;
|
|
119
181
|
ssr;
|
|
182
|
+
seoService;
|
|
120
183
|
sharedData = {};
|
|
121
|
-
constructor(options, template, ssr) {
|
|
184
|
+
constructor(options, template, ssr, seoService) {
|
|
122
185
|
this.options = options;
|
|
123
186
|
this.template = template;
|
|
124
187
|
this.ssr = ssr;
|
|
188
|
+
this.seoService = seoService;
|
|
125
189
|
}
|
|
126
190
|
share(key, value) {
|
|
127
191
|
this.sharedData[key] = value;
|
|
128
192
|
}
|
|
193
|
+
seo(data) {
|
|
194
|
+
this.seoService.set(data);
|
|
195
|
+
}
|
|
129
196
|
location(url) {
|
|
130
197
|
return new Response("", {
|
|
131
198
|
status: 409,
|
|
@@ -170,14 +237,21 @@ let InertiaService = class InertiaService {
|
|
|
170
237
|
async render(ctx, component, props = {}, renderOptions = {}) {
|
|
171
238
|
const reqUrl = new URL(ctx.c.req.url);
|
|
172
239
|
const url = reqUrl.search ? `${reqUrl.pathname}${reqUrl.search}` : reqUrl.pathname;
|
|
240
|
+
const pathname = reqUrl.pathname;
|
|
173
241
|
const isInertia = ctx.c.get("inertia");
|
|
174
242
|
const { shared: resolvedShared, sharedKeys } = await this.resolveSharedData(ctx);
|
|
243
|
+
const resolvedSeo = await this.seoService.resolve(ctx);
|
|
175
244
|
const allProps = {
|
|
176
245
|
...resolvedShared,
|
|
177
246
|
...this.sharedData,
|
|
247
|
+
seo: this.always(() => resolvedSeo),
|
|
178
248
|
...props
|
|
179
249
|
};
|
|
180
|
-
const allSharedKeys = [
|
|
250
|
+
const allSharedKeys = [
|
|
251
|
+
...sharedKeys,
|
|
252
|
+
...Object.keys(this.sharedData),
|
|
253
|
+
"seo"
|
|
254
|
+
];
|
|
181
255
|
const result = await this.processProps(allProps, ctx, component, isInertia);
|
|
182
256
|
const { errors: flashErrors, ...flash } = ctx.c.get("inertiaFlash") ?? {};
|
|
183
257
|
const errors = flashErrors && typeof flashErrors === "object" && !Array.isArray(flashErrors) ? flashErrors : {};
|
|
@@ -213,12 +287,17 @@ let InertiaService = class InertiaService {
|
|
|
213
287
|
"Vary": "X-Inertia"
|
|
214
288
|
}
|
|
215
289
|
});
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
290
|
+
const seoTags = this.seoService.tagsFor(resolvedSeo);
|
|
291
|
+
if (ctx.c.get("withoutSsr") || this.isSsrDisabled(pathname) || !this.options.ssr) {
|
|
292
|
+
const html = this.template.renderClientOnly(page, seoTags);
|
|
293
|
+
return new Response(html, {
|
|
294
|
+
status,
|
|
295
|
+
headers: { "Content-Type": "text/html; charset=utf-8" }
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
const { head, stream } = await this.ssr.render(page);
|
|
299
|
+
const body = this.template.renderStream(page, [...head, ...seoTags], stream);
|
|
300
|
+
return new Response(body, {
|
|
222
301
|
status,
|
|
223
302
|
headers: { "Content-Type": "text/html; charset=utf-8" }
|
|
224
303
|
});
|
|
@@ -248,6 +327,7 @@ let InertiaService = class InertiaService {
|
|
|
248
327
|
const uri = container.resolve(ROUTER_TOKENS.Uri);
|
|
249
328
|
const name = registry.findNameByRoute(ctx.c.req.method, ctx.c.req.routePath) ?? null;
|
|
250
329
|
const params = { ...ctx.param() };
|
|
330
|
+
const localePathService = container.resolve(ROUTER_TOKENS.LocalePathService);
|
|
251
331
|
shared.routes = this.serializeRoutes(registry.named());
|
|
252
332
|
shared.trailingSlash = application.config.trailingSlash ?? "ignore";
|
|
253
333
|
shared.route = {
|
|
@@ -255,6 +335,10 @@ let InertiaService = class InertiaService {
|
|
|
255
335
|
params,
|
|
256
336
|
defaults: uri.getDefaults()
|
|
257
337
|
};
|
|
338
|
+
shared.localeConfig = {
|
|
339
|
+
defaultLocale: localePathService.localePathConfig?.defaultLocale ?? null,
|
|
340
|
+
prefixDefaultLocale: localePathService.prefixDefaultLocale
|
|
341
|
+
};
|
|
258
342
|
}
|
|
259
343
|
return {
|
|
260
344
|
shared,
|
|
@@ -393,7 +477,8 @@ InertiaService = __decorate([
|
|
|
393
477
|
Request(INERTIA_TOKENS.InertiaService),
|
|
394
478
|
__decorateParam(0, inject(INERTIA_TOKENS.Options)),
|
|
395
479
|
__decorateParam(1, inject(INERTIA_TOKENS.TemplateService)),
|
|
396
|
-
__decorateParam(2, inject(INERTIA_TOKENS.SsrRenderer))
|
|
480
|
+
__decorateParam(2, inject(INERTIA_TOKENS.SsrRenderer)),
|
|
481
|
+
__decorateParam(3, inject(INERTIA_TOKENS.SeoService))
|
|
397
482
|
], InertiaService);
|
|
398
483
|
//#endregion
|
|
399
484
|
//#region src/services/manifest.service.ts
|
|
@@ -402,12 +487,20 @@ let ManifestService = class ManifestService {
|
|
|
402
487
|
manifest;
|
|
403
488
|
entryClientPath;
|
|
404
489
|
isDev = Boolean(import.meta.env.DEV);
|
|
490
|
+
headTags = null;
|
|
491
|
+
scriptTags = null;
|
|
405
492
|
constructor(options) {
|
|
406
493
|
this.manifest = globalThis.__STRATAL_INERTIA_MANIFEST__ ?? null;
|
|
407
494
|
this.entryClientPath = (options.entryClientPath ?? DEFAULT_ENTRY_CLIENT_PATH).replace(/^\/+/, "");
|
|
408
495
|
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.");
|
|
409
496
|
}
|
|
410
497
|
getHeadTags() {
|
|
498
|
+
return this.headTags ??= this.buildHeadTags();
|
|
499
|
+
}
|
|
500
|
+
getScriptTags() {
|
|
501
|
+
return this.scriptTags ??= this.buildScriptTags();
|
|
502
|
+
}
|
|
503
|
+
buildHeadTags() {
|
|
411
504
|
if (this.isDev) return "<link rel=\"stylesheet\" href=\"/__inertia/ssr-css\" data-ssr-css />";
|
|
412
505
|
const tags = [];
|
|
413
506
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -418,7 +511,7 @@ let ManifestService = class ManifestService {
|
|
|
418
511
|
}
|
|
419
512
|
return tags.join("\n");
|
|
420
513
|
}
|
|
421
|
-
|
|
514
|
+
buildScriptTags() {
|
|
422
515
|
if (this.isDev) return [
|
|
423
516
|
"<script type=\"module\" src=\"/@vite/client\"><\/script>",
|
|
424
517
|
`<script type="module">
|
|
@@ -435,28 +528,90 @@ hot.on("vite:afterUpdate", () => {
|
|
|
435
528
|
return tags.join("\n");
|
|
436
529
|
}
|
|
437
530
|
};
|
|
438
|
-
ManifestService = __decorate([
|
|
531
|
+
ManifestService = __decorate([Singleton(), __decorateParam(0, inject(INERTIA_TOKENS.Options))], ManifestService);
|
|
532
|
+
//#endregion
|
|
533
|
+
//#region src/services/seo.service.ts
|
|
534
|
+
let SeoService = class SeoService {
|
|
535
|
+
options;
|
|
536
|
+
hreflang;
|
|
537
|
+
accumulated = {};
|
|
538
|
+
constructor(options, hreflang) {
|
|
539
|
+
this.options = options;
|
|
540
|
+
this.hreflang = hreflang;
|
|
541
|
+
}
|
|
542
|
+
/** Merges the given metadata into the request's accumulated SEO data. */
|
|
543
|
+
set(data) {
|
|
544
|
+
this.accumulated = mergeSeo(this.accumulated, data);
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* Resolves the final SEO data: module defaults (base) merged with the
|
|
548
|
+
* request's accumulated data, then the title template applied. Resolver
|
|
549
|
+
* functions for `defaults`/`titleTemplate` are awaited with the request `ctx`.
|
|
550
|
+
* Locale-aware `hreflang` alternates are appended last so they ride the same
|
|
551
|
+
* head injection and SPA reconciliation as the rest of the SEO tags.
|
|
552
|
+
*
|
|
553
|
+
* The resolved `title` is ALWAYS a string (falling back to `''`). This makes
|
|
554
|
+
* the `<title>` descriptor deterministic: every navigation — including to a
|
|
555
|
+
* page with no SEO — produces a title, so the client head-sync sets
|
|
556
|
+
* `document.title` rather than leaving the previous page's title stale.
|
|
557
|
+
*/
|
|
558
|
+
async resolve(ctx) {
|
|
559
|
+
const seo = this.options.seo;
|
|
560
|
+
const resolved = mergeSeo(typeof seo?.defaults === "function" ? await seo.defaults(ctx) : seo?.defaults ?? {}, this.accumulated);
|
|
561
|
+
const template = seo?.titleTemplate;
|
|
562
|
+
if (typeof template === "function") resolved.title = await template(resolved.title, ctx);
|
|
563
|
+
else if (typeof template === "string" && this.accumulated.title != null) resolved.title = template.split("%s").join(this.accumulated.title);
|
|
564
|
+
resolved.title ??= "";
|
|
565
|
+
const hreflang = this.hreflang.buildLinks(new URL(ctx.c.req.url));
|
|
566
|
+
if (hreflang.length > 0) resolved.link = [...resolved.link ?? [], ...hreflang];
|
|
567
|
+
return resolved;
|
|
568
|
+
}
|
|
569
|
+
/** Renders resolved SEO data into a list of head-tag HTML strings. */
|
|
570
|
+
tagsFor(resolved) {
|
|
571
|
+
return buildSeoTags(resolved).map(descriptorToHtml);
|
|
572
|
+
}
|
|
573
|
+
};
|
|
574
|
+
SeoService = __decorate([
|
|
575
|
+
Request(INERTIA_TOKENS.SeoService),
|
|
576
|
+
__decorateParam(0, inject(INERTIA_TOKENS.Options)),
|
|
577
|
+
__decorateParam(1, inject(INERTIA_TOKENS.HreflangService))
|
|
578
|
+
], SeoService);
|
|
579
|
+
/** Merges `b` over `a`: `openGraph`/`twitter` shallow-merge, `meta`/`link` concat, scalars overwrite. */
|
|
580
|
+
function mergeSeo(a, b) {
|
|
581
|
+
return {
|
|
582
|
+
...a,
|
|
583
|
+
...b,
|
|
584
|
+
...a.openGraph || b.openGraph ? { openGraph: {
|
|
585
|
+
...a.openGraph,
|
|
586
|
+
...b.openGraph
|
|
587
|
+
} } : {},
|
|
588
|
+
...a.twitter || b.twitter ? { twitter: {
|
|
589
|
+
...a.twitter,
|
|
590
|
+
...b.twitter
|
|
591
|
+
} } : {},
|
|
592
|
+
...a.meta || b.meta ? { meta: [...a.meta ?? [], ...b.meta ?? []] } : {},
|
|
593
|
+
...a.link || b.link ? { link: [...a.link ?? [], ...b.link ?? []] } : {}
|
|
594
|
+
};
|
|
595
|
+
}
|
|
439
596
|
//#endregion
|
|
440
597
|
//#region src/services/ssr-renderer.service.ts
|
|
441
598
|
let SsrRendererService = class SsrRendererService {
|
|
442
599
|
options;
|
|
443
|
-
logger;
|
|
444
600
|
bundle = null;
|
|
445
601
|
loadPromise = null;
|
|
446
|
-
constructor(options
|
|
602
|
+
constructor(options) {
|
|
447
603
|
this.options = options;
|
|
448
|
-
this.logger = logger;
|
|
449
604
|
}
|
|
605
|
+
/**
|
|
606
|
+
* Render a page to a streaming SSR result.
|
|
607
|
+
*
|
|
608
|
+
* The SSR bundle is imported once per worker (memoized). Bundle-load and render
|
|
609
|
+
* errors propagate — there is no silent client-side fallback. Callers must only
|
|
610
|
+
* invoke this when `options.ssr` is configured.
|
|
611
|
+
*/
|
|
450
612
|
async render(page) {
|
|
451
|
-
if (!this.options.ssr)
|
|
452
|
-
head: [],
|
|
453
|
-
body: ""
|
|
454
|
-
};
|
|
613
|
+
if (!this.options.ssr) throw new ApplicationError("[stratal:inertia] SSR bundle is not configured.");
|
|
455
614
|
await this.ensureBundle();
|
|
456
|
-
if (!this.bundle) return {
|
|
457
|
-
head: [],
|
|
458
|
-
body: ""
|
|
459
|
-
};
|
|
460
615
|
return this.bundle.render(page);
|
|
461
616
|
}
|
|
462
617
|
async ensureBundle() {
|
|
@@ -464,52 +619,84 @@ let SsrRendererService = class SsrRendererService {
|
|
|
464
619
|
this.loadPromise ??= this.loadBundle();
|
|
465
620
|
try {
|
|
466
621
|
await this.loadPromise;
|
|
467
|
-
} catch {}
|
|
468
|
-
}
|
|
469
|
-
async loadBundle() {
|
|
470
|
-
if (!this.options.ssr) return;
|
|
471
|
-
try {
|
|
472
|
-
const mod = await this.options.ssr.bundle();
|
|
473
|
-
const resolved = "default" in mod ? mod.default : mod;
|
|
474
|
-
this.bundle = resolved;
|
|
475
622
|
} catch (error) {
|
|
476
|
-
this.logger.warn("[stratal:inertia] Failed to load SSR bundle. Falling back to client-side rendering.", { error });
|
|
477
623
|
this.loadPromise = null;
|
|
624
|
+
throw error;
|
|
478
625
|
}
|
|
479
626
|
}
|
|
627
|
+
async loadBundle() {
|
|
628
|
+
const mod = await this.options.ssr.bundle();
|
|
629
|
+
this.bundle = "default" in mod ? mod.default : mod;
|
|
630
|
+
}
|
|
480
631
|
};
|
|
481
|
-
SsrRendererService = __decorate([
|
|
482
|
-
Singleton(),
|
|
483
|
-
__decorateParam(0, inject(INERTIA_TOKENS.Options)),
|
|
484
|
-
__decorateParam(1, inject(LOGGER_TOKENS.LoggerService))
|
|
485
|
-
], SsrRendererService);
|
|
632
|
+
SsrRendererService = __decorate([Singleton(), __decorateParam(0, inject(INERTIA_TOKENS.Options))], SsrRendererService);
|
|
486
633
|
//#endregion
|
|
487
634
|
//#region src/services/template.service.ts
|
|
635
|
+
const APP_ID = "app";
|
|
488
636
|
let TemplateService = class TemplateService {
|
|
489
|
-
options;
|
|
490
637
|
manifest;
|
|
638
|
+
pre;
|
|
639
|
+
post;
|
|
491
640
|
constructor(options, manifest) {
|
|
492
|
-
this.options = options;
|
|
493
641
|
this.manifest = manifest;
|
|
642
|
+
const match = /@inertia\b/.exec(options.rootView);
|
|
643
|
+
if (!match) throw new ApplicationError("[stratal:inertia] rootView template is missing the @inertia placeholder.");
|
|
644
|
+
this.pre = options.rootView.slice(0, match.index);
|
|
645
|
+
this.post = options.rootView.slice(match.index + 8);
|
|
646
|
+
}
|
|
647
|
+
/**
|
|
648
|
+
* Compose the streamed HTML response: the document shell (head + opening
|
|
649
|
+
* `#app` wrapper) is flushed first, the React stream is piped verbatim, then
|
|
650
|
+
* the wrapper is closed and the trailing scripts are appended.
|
|
651
|
+
*
|
|
652
|
+
* Reproduces Inertia's `buildSSRBody` markup: a `<script data-page>` JSON tag
|
|
653
|
+
* (parsed before hydration) followed by `<div data-server-rendered id="app">`.
|
|
654
|
+
*/
|
|
655
|
+
renderStream(page, head, reactStream) {
|
|
656
|
+
const encoder = new TextEncoder();
|
|
657
|
+
const shellPre = this.fillPlaceholders(this.pre, head) + `<script data-page="${APP_ID}" type="application/json">${this.serialize(page)}<\/script><div data-server-rendered="true" id="${APP_ID}">`;
|
|
658
|
+
const shellPost = `</div>${this.fillPlaceholders(this.post, head)}`;
|
|
659
|
+
let reader;
|
|
660
|
+
return new ReadableStream({
|
|
661
|
+
async start(controller) {
|
|
662
|
+
controller.enqueue(encoder.encode(shellPre));
|
|
663
|
+
reader = reactStream.getReader();
|
|
664
|
+
try {
|
|
665
|
+
for (;;) {
|
|
666
|
+
const { done, value } = await reader.read();
|
|
667
|
+
if (done) break;
|
|
668
|
+
controller.enqueue(value);
|
|
669
|
+
}
|
|
670
|
+
controller.enqueue(encoder.encode(shellPost));
|
|
671
|
+
controller.close();
|
|
672
|
+
} catch (error) {
|
|
673
|
+
controller.error(error);
|
|
674
|
+
} finally {
|
|
675
|
+
reader.releaseLock();
|
|
676
|
+
reader = void 0;
|
|
677
|
+
}
|
|
678
|
+
},
|
|
679
|
+
cancel(reason) {
|
|
680
|
+
return reader?.cancel(reason) ?? reactStream.cancel(reason);
|
|
681
|
+
}
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
/**
|
|
685
|
+
* Buffered, client-only document used when SSR is disabled for the request.
|
|
686
|
+
* Emits an empty `#app` div for the client bundle to hydrate.
|
|
687
|
+
*/
|
|
688
|
+
renderClientOnly(page, head) {
|
|
689
|
+
return this.fillPlaceholders(this.pre, head) + `<script data-page="${APP_ID}" type="application/json">${this.serialize(page)}<\/script><div id="${APP_ID}"></div>` + this.fillPlaceholders(this.post, head);
|
|
690
|
+
}
|
|
691
|
+
fillPlaceholders(segment, head) {
|
|
692
|
+
return segment.replace("@inertiaHead", () => head.join("\n")).replace("@viteHead", () => this.manifest.getHeadTags()).replace("@viteScripts", () => this.manifest.getScriptTags());
|
|
494
693
|
}
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
const headTags = ssrHead.length > 0 ? ssrHead.join("\n") : "";
|
|
498
|
-
const viteHead = this.manifest.getHeadTags();
|
|
499
|
-
const viteScripts = this.manifest.getScriptTags();
|
|
500
|
-
let html = this.options.rootView;
|
|
501
|
-
html = html.replace("@inertiaHead", headTags);
|
|
502
|
-
html = html.replace("@inertia", appHtml);
|
|
503
|
-
html = html.replace("@viteHead", viteHead);
|
|
504
|
-
html = html.replace("@viteScripts", viteScripts);
|
|
505
|
-
return html;
|
|
506
|
-
}
|
|
507
|
-
buildClientOnlyBody(page) {
|
|
508
|
-
return `<script data-page="app" type="application/json">${JSON.stringify(page).replace(/\//g, "\\/")}<\/script><div id="app"></div>`;
|
|
694
|
+
serialize(page) {
|
|
695
|
+
return JSON.stringify(page).replace(/\//g, "\\/");
|
|
509
696
|
}
|
|
510
697
|
};
|
|
511
698
|
TemplateService = __decorate([
|
|
512
|
-
|
|
699
|
+
Singleton(),
|
|
513
700
|
__decorateParam(0, inject(INERTIA_TOKENS.Options)),
|
|
514
701
|
__decorateParam(1, inject(INERTIA_TOKENS.ManifestService))
|
|
515
702
|
], TemplateService);
|
|
@@ -544,6 +731,7 @@ let InertiaModule = _InertiaModule = class InertiaModule {
|
|
|
544
731
|
if (context.type !== "http") return void 0;
|
|
545
732
|
if (this.isPrecognitionRequest(context)) return this.handlePrecognitionValidationError(error, context);
|
|
546
733
|
if (!this.isInertiaRequest(context)) return void 0;
|
|
734
|
+
if (this.isReadRequest(context)) return void 0;
|
|
547
735
|
const issues = error.issues ?? [];
|
|
548
736
|
const errors = {};
|
|
549
737
|
for (const issue of issues) errors[issue.path] = issue.message;
|
|
@@ -555,6 +743,7 @@ let InertiaModule = _InertiaModule = class InertiaModule {
|
|
|
555
743
|
const message = error.message;
|
|
556
744
|
if (this.isPrecognitionRequest(context)) return this.createPrecognitionErrorResponse({ _form: message });
|
|
557
745
|
if (!this.isInertiaRequest(context)) return void 0;
|
|
746
|
+
if (this.isReadRequest(context)) return void 0;
|
|
558
747
|
context.ctx.flash("errors", { _form: message });
|
|
559
748
|
return this.redirectBack(context);
|
|
560
749
|
});
|
|
@@ -577,6 +766,23 @@ let InertiaModule = _InertiaModule = class InertiaModule {
|
|
|
577
766
|
isInertiaRequest(context) {
|
|
578
767
|
return context.ctx.header("x-inertia") === "true";
|
|
579
768
|
}
|
|
769
|
+
/**
|
|
770
|
+
* GET/HEAD requests are idempotent navigations — including Inertia deferred
|
|
771
|
+
* partial reloads, which fetch deferred props over a follow-up XHR that still
|
|
772
|
+
* carries `X-Inertia: true`.
|
|
773
|
+
*
|
|
774
|
+
* Such requests must NOT use the flash-errors + redirect-back convention: the
|
|
775
|
+
* redirect points back at the very URL that just threw, so an error raised
|
|
776
|
+
* while resolving a deferred prop would redirect → re-request → throw again
|
|
777
|
+
* in an infinite loop (`ERR_TOO_MANY_REDIRECTS`). For these we fall through to
|
|
778
|
+
* the errorPage pipeline, which renders `Errors/${status}` in place as an
|
|
779
|
+
* Inertia response. Redirect-back stays for mutations (POST/PUT/PATCH/DELETE),
|
|
780
|
+
* where it drives the post-submit form-error flow.
|
|
781
|
+
*/
|
|
782
|
+
isReadRequest(context) {
|
|
783
|
+
const method = context.ctx.c.req.method.toUpperCase();
|
|
784
|
+
return method === "GET" || method === "HEAD";
|
|
785
|
+
}
|
|
580
786
|
isPrecognitionRequest(context) {
|
|
581
787
|
return context.ctx.header("precognition") === "true";
|
|
582
788
|
}
|
|
@@ -637,6 +843,14 @@ InertiaModule = _InertiaModule = __decorate([Module({ providers: [
|
|
|
637
843
|
{
|
|
638
844
|
provide: INERTIA_TOKENS.SsrRenderer,
|
|
639
845
|
useClass: SsrRendererService
|
|
846
|
+
},
|
|
847
|
+
{
|
|
848
|
+
provide: INERTIA_TOKENS.HreflangService,
|
|
849
|
+
useClass: HreflangService
|
|
850
|
+
},
|
|
851
|
+
{
|
|
852
|
+
provide: INERTIA_TOKENS.SeoService,
|
|
853
|
+
useClass: SeoService
|
|
640
854
|
}
|
|
641
855
|
] })], InertiaModule);
|
|
642
856
|
//#endregion
|
|
@@ -829,6 +1043,6 @@ let HandlePrecognitiveRequests = class HandlePrecognitiveRequests {
|
|
|
829
1043
|
};
|
|
830
1044
|
HandlePrecognitiveRequests = __decorate([Transient()], HandlePrecognitiveRequests);
|
|
831
1045
|
//#endregion
|
|
832
|
-
export { CookieFlashStore, HandlePrecognitiveRequests, INERTIA_TOKENS, InertiaDelete, InertiaGet, InertiaMiddleware, InertiaModule, InertiaPatch, InertiaPost, InertiaPut, InertiaRoute, InertiaService, ManifestService, SsrRendererService, TemplateService };
|
|
1046
|
+
export { CookieFlashStore, DATA_SEO_ATTR, HandlePrecognitiveRequests, INERTIA_TOKENS, InertiaDelete, InertiaGet, InertiaMiddleware, InertiaModule, InertiaPatch, InertiaPost, InertiaPut, InertiaRoute, InertiaService, ManifestService, SeoService, SsrRendererService, TemplateService, buildSeoTags, descriptorToHtml };
|
|
833
1047
|
|
|
834
1048
|
//# sourceMappingURL=index.mjs.map
|