@stratal/inertia 0.0.27 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +359 -0
- package/README.md +158 -14
- package/dist/build-seo-tags-DBsHKxX9.mjs.map +1 -1
- package/dist/{decorate-B7nr7eBl.mjs → decorate-RQD1h28J.mjs} +1 -1
- package/dist/generator/type-generator.worker.d.mts +1 -1
- package/dist/generator/type-generator.worker.mjs +1 -1
- package/dist/index.d.mts +214 -92
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +390 -130
- package/dist/index.mjs.map +1 -1
- package/dist/quarry.d.mts +6 -8
- package/dist/quarry.d.mts.map +1 -1
- package/dist/quarry.mjs +127 -12
- package/dist/quarry.mjs.map +1 -1
- package/dist/react/access.d.mts +95 -0
- package/dist/react/access.d.mts.map +1 -0
- package/dist/react/access.mjs +133 -0
- package/dist/react/access.mjs.map +1 -0
- package/dist/react-dom-server-legacy-stub.d.mts +20 -0
- package/dist/react-dom-server-legacy-stub.d.mts.map +1 -0
- package/dist/react-dom-server-legacy-stub.mjs +25 -0
- package/dist/react-dom-server-legacy-stub.mjs.map +1 -0
- package/dist/react.d.mts +4 -6
- package/dist/react.d.mts.map +1 -1
- package/dist/react.mjs +1 -1
- package/dist/react.mjs.map +1 -1
- package/dist/seo-runtime.d.mts +1 -1
- package/dist/seo-runtime.mjs +8 -6
- package/dist/seo-runtime.mjs.map +1 -1
- package/dist/services/ssr-exclusion.d.mts +38 -0
- package/dist/services/ssr-exclusion.d.mts.map +1 -0
- package/dist/services/ssr-exclusion.mjs +0 -0
- package/dist/services/ssr-exclusion.mjs.map +1 -0
- package/dist/ssr.d.mts +37 -8
- package/dist/ssr.d.mts.map +1 -1
- package/dist/ssr.mjs +7 -4
- package/dist/ssr.mjs.map +1 -1
- package/dist/testing.d.mts +3 -2
- package/dist/testing.d.mts.map +1 -1
- package/dist/testing.mjs +20 -6
- package/dist/testing.mjs.map +1 -1
- package/dist/{type-generator-DFpha_Fp.mjs → type-generator-BVw8mj1y.mjs} +373 -64
- package/dist/type-generator-BVw8mj1y.mjs.map +1 -0
- package/dist/types-BltKoOR7.d.mts +193 -0
- package/dist/types-BltKoOR7.d.mts.map +1 -0
- package/dist/types-D-j_Ee_h.d.mts +52 -0
- package/dist/types-D-j_Ee_h.d.mts.map +1 -0
- package/dist/types-DzE1pdZs.d.mts.map +1 -1
- package/dist/vite.d.mts +19 -6
- package/dist/vite.d.mts.map +1 -1
- package/dist/vite.mjs +67 -5
- package/dist/vite.mjs.map +1 -1
- package/package.json +38 -27
- package/dist/type-generator-DFpha_Fp.mjs.map +0 -1
- package/dist/types-BhgXhWx6.d.mts +0 -82
- package/dist/types-BhgXhWx6.d.mts.map +0 -1
package/dist/index.mjs
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import { t as __decorate } from "./decorate-
|
|
1
|
+
import { t as __decorate } from "./decorate-RQD1h28J.mjs";
|
|
2
|
+
import { getSsrExcludeMatchers, isSsrExcluded } from "./services/ssr-exclusion.mjs";
|
|
2
3
|
import { n as buildSeoTags, r as descriptorToHtml, t as DATA_SEO_ATTR } from "./build-seo-tags-DBsHKxX9.mjs";
|
|
3
4
|
import { ApplicationError } from "stratal/errors";
|
|
4
5
|
import { Module } from "stratal/module";
|
|
5
|
-
import { Delete, Get, Patch, Post, Put, ROUTER_TOKENS, Route, RouterContext, SchemaValidationError, applyTrailingSlash, resolveTrailingSlash } from "stratal/router";
|
|
6
|
+
import { Delete, Get, Patch, Post, Put, ROUTER_CONTEXT_KEYS, ROUTER_TOKENS, Route, RouterContext, SchemaValidationError, applyTrailingSlash, resolveTrailingSlash } from "stratal/router";
|
|
6
7
|
import { CONTAINER_TOKEN, DI_TOKENS, Request, Singleton, Transient, inject } from "stratal/di";
|
|
7
|
-
import { I18N_TOKENS } from "stratal/i18n";
|
|
8
|
+
import { I18N_TOKENS, resolveDetectionForPath } from "stratal/i18n";
|
|
8
9
|
import { deleteCookie, getSignedCookie, setSignedCookie } from "hono/cookie";
|
|
9
|
-
import {
|
|
10
|
+
import { array, boolean, nullable, number, object, optional, record, string, unknown } from "zod/mini";
|
|
10
11
|
//#region src/augment/router-context.ts
|
|
11
12
|
function augmentRouterContext(resolveService) {
|
|
12
13
|
const originalRedirect = RouterContext.prototype.redirect;
|
|
@@ -29,6 +30,9 @@ function augmentRouterContext(resolveService) {
|
|
|
29
30
|
RouterContext.macro("merge", function(callback, options) {
|
|
30
31
|
return resolveService(this).merge(callback, options);
|
|
31
32
|
});
|
|
33
|
+
RouterContext.macro("scroll", function(callback, options) {
|
|
34
|
+
return resolveService(this).scroll(callback, options);
|
|
35
|
+
});
|
|
32
36
|
RouterContext.macro("once", function(callback, options) {
|
|
33
37
|
return resolveService(this).once(callback, options);
|
|
34
38
|
});
|
|
@@ -45,9 +49,6 @@ function augmentRouterContext(resolveService) {
|
|
|
45
49
|
RouterContext.macro("seo", function(data) {
|
|
46
50
|
resolveService(this).seo(data);
|
|
47
51
|
});
|
|
48
|
-
RouterContext.macro("withoutSsr", function() {
|
|
49
|
-
this.c.set("withoutSsr", true);
|
|
50
|
-
});
|
|
51
52
|
}
|
|
52
53
|
//#endregion
|
|
53
54
|
//#region src/inertia.tokens.ts
|
|
@@ -57,11 +58,53 @@ const INERTIA_TOKENS = {
|
|
|
57
58
|
TemplateService: Symbol.for("stratal:inertia:template"),
|
|
58
59
|
ManifestService: Symbol.for("stratal:inertia:manifest"),
|
|
59
60
|
SsrRenderer: Symbol.for("stratal:inertia:ssr-renderer"),
|
|
61
|
+
DocumentRenderer: Symbol.for("stratal:inertia:document-renderer"),
|
|
60
62
|
HreflangService: Symbol.for("stratal:inertia:hreflang"),
|
|
61
63
|
SeoService: Symbol.for("stratal:inertia:seo")
|
|
62
64
|
};
|
|
63
65
|
//#endregion
|
|
64
|
-
//#region
|
|
66
|
+
//#region src/types.ts
|
|
67
|
+
const INERTIA_PROP_OPTIONAL = Symbol.for("stratal:inertia:prop:optional");
|
|
68
|
+
const INERTIA_PROP_DEFERRED = Symbol.for("stratal:inertia:prop:deferred");
|
|
69
|
+
const INERTIA_PROP_MERGE = Symbol.for("stratal:inertia:prop:merge");
|
|
70
|
+
const INERTIA_PROP_SCROLL = Symbol.for("stratal:inertia:prop:scroll");
|
|
71
|
+
const INERTIA_PROP_ONCE = Symbol.for("stratal:inertia:prop:once");
|
|
72
|
+
const INERTIA_PROP_ALWAYS = Symbol.for("stratal:inertia:prop:always");
|
|
73
|
+
/**
|
|
74
|
+
* Header the client sends on every infinite-scroll fetch to say which end of the
|
|
75
|
+
* accumulated list the response should be joined to. Absent on the first render,
|
|
76
|
+
* which is not a scroll fetch.
|
|
77
|
+
*/
|
|
78
|
+
const INERTIA_SCROLL_MERGE_INTENT_HEADER = "x-inertia-infinite-scroll-merge-intent";
|
|
79
|
+
/**
|
|
80
|
+
* Every request header an Inertia response body is a function of, beyond
|
|
81
|
+
* `X-Inertia` itself. This is the set `partialRequestFor` reads to decide which
|
|
82
|
+
* props to resolve, so two requests to one URL differing in any of them receive
|
|
83
|
+
* different bodies.
|
|
84
|
+
*
|
|
85
|
+
* They are declared in `Vary` on EVERY Inertia response, not only on partial
|
|
86
|
+
* reloads, and that is the load-bearing part: a cache matches a stored response
|
|
87
|
+
* against the `Vary` IT was stored with. A full page response stored under
|
|
88
|
+
* `Vary: X-Inertia` alone would be served to a partial reload, which sends the
|
|
89
|
+
* same `X-Inertia: true` and would receive the whole page in place of the props
|
|
90
|
+
* it asked for.
|
|
91
|
+
*
|
|
92
|
+
* `X-Inertia-Version` is deliberately absent. A mismatch answers 409, which is
|
|
93
|
+
* never cached, and the asset version only moves with a deploy — which moves the
|
|
94
|
+
* Worker version that Workers Caching already keys on, so a stale client cannot
|
|
95
|
+
* reach an entry stored before it.
|
|
96
|
+
*/
|
|
97
|
+
const INERTIA_VARY_HEADERS = [
|
|
98
|
+
"X-Inertia",
|
|
99
|
+
"X-Inertia-Partial-Component",
|
|
100
|
+
"X-Inertia-Partial-Data",
|
|
101
|
+
"X-Inertia-Partial-Except",
|
|
102
|
+
"X-Inertia-Reset",
|
|
103
|
+
"X-Inertia-Resolve-Deferred",
|
|
104
|
+
INERTIA_SCROLL_MERGE_INTENT_HEADER
|
|
105
|
+
];
|
|
106
|
+
//#endregion
|
|
107
|
+
//#region \0@oxc-project+runtime@0.150.0/helpers/esm/decorateParam.js
|
|
65
108
|
function __decorateParam(paramIndex, decorator) {
|
|
66
109
|
return function(target, key) {
|
|
67
110
|
decorator(target, key, paramIndex);
|
|
@@ -79,7 +122,6 @@ let InertiaMiddleware = class InertiaMiddleware {
|
|
|
79
122
|
const isPrefetch = ctx.header("purpose") === "prefetch";
|
|
80
123
|
ctx.c.set("inertia", isInertia);
|
|
81
124
|
ctx.c.set("inertiaPrefetch", isPrefetch);
|
|
82
|
-
ctx.c.set("withoutSsr", false);
|
|
83
125
|
ctx.c.set("inertiaFlashOut", {});
|
|
84
126
|
let hadFlash = false;
|
|
85
127
|
if (this.options.flash) {
|
|
@@ -91,8 +133,13 @@ let InertiaMiddleware = class InertiaMiddleware {
|
|
|
91
133
|
const clientVersion = ctx.header("x-inertia-version");
|
|
92
134
|
const serverVersion = this.options.version ?? "";
|
|
93
135
|
if (clientVersion && serverVersion && clientVersion !== serverVersion) {
|
|
94
|
-
ctx.c.
|
|
95
|
-
|
|
136
|
+
ctx.c.res = new Response(null, {
|
|
137
|
+
status: 409,
|
|
138
|
+
headers: {
|
|
139
|
+
"X-Inertia-Location": ctx.c.req.url,
|
|
140
|
+
"X-Inertia-Version": serverVersion
|
|
141
|
+
}
|
|
142
|
+
});
|
|
96
143
|
return;
|
|
97
144
|
}
|
|
98
145
|
}
|
|
@@ -104,7 +151,10 @@ let InertiaMiddleware = class InertiaMiddleware {
|
|
|
104
151
|
}
|
|
105
152
|
const status = ctx.c.res?.status;
|
|
106
153
|
if (typeof status !== "number" || status < 200 || status > 599) return;
|
|
107
|
-
ctx.c.
|
|
154
|
+
const declaredVary = (ctx.c.res.headers.get("Vary") ?? "").split(",").map((name) => name.trim()).filter(Boolean);
|
|
155
|
+
const seen = new Set(declaredVary.map((name) => name.toLowerCase()));
|
|
156
|
+
const missing = INERTIA_VARY_HEADERS.filter((name) => !seen.has(name.toLowerCase()));
|
|
157
|
+
ctx.c.header("Vary", [...missing, ...declaredVary].join(", "));
|
|
108
158
|
if (isInertia && status === 302) {
|
|
109
159
|
const method = ctx.c.req.method;
|
|
110
160
|
if (method !== "GET" && method !== "HEAD") ctx.c.status(303);
|
|
@@ -113,6 +163,45 @@ let InertiaMiddleware = class InertiaMiddleware {
|
|
|
113
163
|
};
|
|
114
164
|
InertiaMiddleware = __decorate([Transient(), __decorateParam(0, inject(INERTIA_TOKENS.Options))], InertiaMiddleware);
|
|
115
165
|
//#endregion
|
|
166
|
+
//#region src/services/document-renderer.service.ts
|
|
167
|
+
let DocumentRendererService = class DocumentRendererService {
|
|
168
|
+
options;
|
|
169
|
+
ssr;
|
|
170
|
+
template;
|
|
171
|
+
ssrExcludeMatchers = getSsrExcludeMatchers();
|
|
172
|
+
constructor(options, ssr, template) {
|
|
173
|
+
this.options = options;
|
|
174
|
+
this.ssr = ssr;
|
|
175
|
+
this.template = template;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Render `page` to an HTML document Response. `extraHead` tags are appended to
|
|
179
|
+
* `<head>` — after the SSR-rendered Inertia `<Head>` when streaming, or as the
|
|
180
|
+
* only head tags in the client-only shell.
|
|
181
|
+
*/
|
|
182
|
+
async render(page, status = 200, extraHead = []) {
|
|
183
|
+
if (!this.options.ssr || isSsrExcluded(page.component, this.ssrExcludeMatchers)) {
|
|
184
|
+
const html = this.template.renderClientOnly(page, extraHead);
|
|
185
|
+
return new Response(html, {
|
|
186
|
+
status,
|
|
187
|
+
headers: { "Content-Type": "text/html; charset=utf-8" }
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
const { head, stream } = await this.ssr.render(page);
|
|
191
|
+
const body = this.template.renderStream(page, [...head, ...extraHead], stream);
|
|
192
|
+
return new Response(body, {
|
|
193
|
+
status,
|
|
194
|
+
headers: { "Content-Type": "text/html; charset=utf-8" }
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
DocumentRendererService = __decorate([
|
|
199
|
+
Singleton(),
|
|
200
|
+
__decorateParam(0, inject(INERTIA_TOKENS.Options)),
|
|
201
|
+
__decorateParam(1, inject(INERTIA_TOKENS.SsrRenderer)),
|
|
202
|
+
__decorateParam(2, inject(INERTIA_TOKENS.TemplateService))
|
|
203
|
+
], DocumentRendererService);
|
|
204
|
+
//#endregion
|
|
116
205
|
//#region src/services/hreflang.service.ts
|
|
117
206
|
let HreflangService = class HreflangService {
|
|
118
207
|
container;
|
|
@@ -127,8 +216,9 @@ let HreflangService = class HreflangService {
|
|
|
127
216
|
const defaultLocale = i18n.defaultLocale ?? "en";
|
|
128
217
|
const trailingSlash = this.container.resolve(DI_TOKENS.Application).config.trailingSlash ?? "ignore";
|
|
129
218
|
const localeUrl = this.container.resolve(ROUTER_TOKENS.LocaleUrlService);
|
|
130
|
-
if (localeUrl.
|
|
131
|
-
|
|
219
|
+
if (localeUrl.isPathLocalized(currentUrl.pathname)) return this.buildPathLinks(currentUrl, locales, defaultLocale, localeUrl, trailingSlash);
|
|
220
|
+
const detection = resolveDetectionForPath(i18n.detection, currentUrl.pathname);
|
|
221
|
+
if (detection.enabled && detection.strategy === "querystring") return this.buildQuerystringLinks(currentUrl, locales, defaultLocale, trailingSlash);
|
|
132
222
|
return [];
|
|
133
223
|
}
|
|
134
224
|
buildPathLinks(url, locales, defaultLocale, localeUrl, trailingSlash) {
|
|
@@ -167,24 +257,95 @@ let HreflangService = class HreflangService {
|
|
|
167
257
|
};
|
|
168
258
|
HreflangService = __decorate([Singleton(), __decorateParam(0, inject(CONTAINER_TOKEN))], HreflangService);
|
|
169
259
|
//#endregion
|
|
170
|
-
//#region src/
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
260
|
+
//#region src/services/inertia-cache-signals.ts
|
|
261
|
+
/**
|
|
262
|
+
* Report the conditions that make an Inertia page unsafe to cache, and the
|
|
263
|
+
* request headers a safe one must be keyed on.
|
|
264
|
+
*
|
|
265
|
+
* A `once()` prop is the subtle one: it is contractually sent a single time,
|
|
266
|
+
* so replaying it from cache to every client breaks the guarantee the API makes.
|
|
267
|
+
*
|
|
268
|
+
* A partial reload is NOT among them. It is a deterministic function of the URL
|
|
269
|
+
* and of `INERTIA_VARY_HEADERS`, so it caches correctly once keyed on those —
|
|
270
|
+
* and refusing it outright made every deferred prop permanently uncacheable,
|
|
271
|
+
* which on a page that defers its expensive work is the whole of the cost.
|
|
272
|
+
*/
|
|
273
|
+
function buildInertiaCacheSignals(input) {
|
|
274
|
+
return {
|
|
275
|
+
hasFlash: Object.keys(input.flash).length > 0,
|
|
276
|
+
isPartial: input.isPartial,
|
|
277
|
+
hasOnceProps: Object.keys(input.onceProps).length > 0,
|
|
278
|
+
varyHeaders: INERTIA_VARY_HEADERS
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
//#endregion
|
|
282
|
+
//#region src/errors/unrecognized-scroll-shape.error.ts
|
|
283
|
+
/**
|
|
284
|
+
* Raised when `ctx.scroll()` is handed a value whose paging identifiers it
|
|
285
|
+
* cannot derive: neither the offset shape `{ data, pagination: { page,
|
|
286
|
+
* totalPages } }` nor the cursor shape `{ data, cursorName, cursor, nextCursor,
|
|
287
|
+
* prevCursor }`.
|
|
288
|
+
*
|
|
289
|
+
* A **programming error**, and deliberately loud: guessing an identifier is
|
|
290
|
+
* worse than failing, because a wrong `nextPage` reads to the client as "no
|
|
291
|
+
* more pages" and silently truncates the list. No request recovers from it and
|
|
292
|
+
* it carries no HTTP status. The fix is in the route — return one of the two
|
|
293
|
+
* recognized shapes, or pass `metadata` to `ctx.scroll()` to extract
|
|
294
|
+
* `pageName`, `currentPage`, `previousPage` and `nextPage` from this one.
|
|
295
|
+
*/
|
|
296
|
+
var UnrecognizedScrollShapeError = class extends ApplicationError {};
|
|
297
|
+
//#endregion
|
|
298
|
+
//#region src/services/scroll-metadata.ts
|
|
299
|
+
function isOffsetPaginatedResult(value) {
|
|
300
|
+
if (typeof value !== "object" || value === null || !("pagination" in value)) return false;
|
|
301
|
+
const { pagination } = value;
|
|
302
|
+
if (typeof pagination !== "object" || pagination === null) return false;
|
|
303
|
+
const { page, totalPages } = pagination;
|
|
304
|
+
return Number.isInteger(page) && Number.isInteger(totalPages);
|
|
305
|
+
}
|
|
306
|
+
function isCursorPageShape(value) {
|
|
307
|
+
if (typeof value !== "object" || value === null) return false;
|
|
308
|
+
const { cursorName, cursor, nextCursor, prevCursor } = value;
|
|
309
|
+
return typeof cursorName === "string" && (typeof cursor === "string" || cursor === null) && (typeof nextCursor === "string" || nextCursor === null) && (typeof prevCursor === "string" || prevCursor === null);
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Derives the infinite-scroll identifiers from a paginated result.
|
|
313
|
+
*
|
|
314
|
+
* Recognises both of the framework's own paginated shapes — the offset one
|
|
315
|
+
* `paginatedResponseSchema` describes, and the cursor one `db.$cursor`
|
|
316
|
+
* returns. Anything else throws rather than guessing: a wrong `nextPage` reads
|
|
317
|
+
* to the client as "no more pages" and silently truncates a list, so an
|
|
318
|
+
* unrecognised shape has to be named at the call site with
|
|
319
|
+
* `ctx.scroll(..., { metadata })`.
|
|
320
|
+
*/
|
|
321
|
+
function deriveScrollMetadata(value) {
|
|
322
|
+
if (isOffsetPaginatedResult(value)) {
|
|
323
|
+
const { page, totalPages } = value.pagination;
|
|
324
|
+
return {
|
|
325
|
+
pageName: "page",
|
|
326
|
+
currentPage: page,
|
|
327
|
+
previousPage: page > 1 ? page - 1 : null,
|
|
328
|
+
nextPage: page < totalPages ? page + 1 : null
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
if (isCursorPageShape(value)) return {
|
|
332
|
+
pageName: value.cursorName,
|
|
333
|
+
currentPage: value.cursor ?? 1,
|
|
334
|
+
previousPage: value.prevCursor,
|
|
335
|
+
nextPage: value.nextCursor
|
|
336
|
+
};
|
|
337
|
+
throw new UnrecognizedScrollShapeError("[stratal:inertia] Cannot derive infinite-scroll metadata: the value is neither an offset-paginated result (`{ data, pagination: { page, totalPages } }`) nor a cursor-paginated one (`{ data, cursorName, cursor, nextCursor, prevCursor }`). Pass `metadata` to `ctx.scroll()` to extract `pageName`, `currentPage`, `previousPage` and `nextPage` from this shape yourself.");
|
|
338
|
+
}
|
|
176
339
|
//#endregion
|
|
177
340
|
//#region src/services/inertia.service.ts
|
|
178
341
|
let InertiaService = class InertiaService {
|
|
179
342
|
options;
|
|
180
|
-
|
|
181
|
-
ssr;
|
|
343
|
+
documentRenderer;
|
|
182
344
|
seoService;
|
|
183
345
|
sharedData = {};
|
|
184
|
-
constructor(options,
|
|
346
|
+
constructor(options, documentRenderer, seoService) {
|
|
185
347
|
this.options = options;
|
|
186
|
-
this.
|
|
187
|
-
this.ssr = ssr;
|
|
348
|
+
this.documentRenderer = documentRenderer;
|
|
188
349
|
this.seoService = seoService;
|
|
189
350
|
}
|
|
190
351
|
share(key, value) {
|
|
@@ -220,6 +381,16 @@ let InertiaService = class InertiaService {
|
|
|
220
381
|
matchOn: options?.matchOn
|
|
221
382
|
};
|
|
222
383
|
}
|
|
384
|
+
scroll(callback, options) {
|
|
385
|
+
return {
|
|
386
|
+
[INERTIA_PROP_SCROLL]: true,
|
|
387
|
+
callback,
|
|
388
|
+
wrapper: options?.wrapper ?? "data",
|
|
389
|
+
matchOn: options?.matchOn,
|
|
390
|
+
pageName: options?.pageName,
|
|
391
|
+
metadata: options?.metadata
|
|
392
|
+
};
|
|
393
|
+
}
|
|
223
394
|
once(callback, options) {
|
|
224
395
|
return {
|
|
225
396
|
[INERTIA_PROP_ONCE]: true,
|
|
@@ -237,7 +408,6 @@ let InertiaService = class InertiaService {
|
|
|
237
408
|
async render(ctx, component, props = {}, renderOptions = {}) {
|
|
238
409
|
const reqUrl = new URL(ctx.c.req.url);
|
|
239
410
|
const url = reqUrl.search ? `${reqUrl.pathname}${reqUrl.search}` : reqUrl.pathname;
|
|
240
|
-
const pathname = reqUrl.pathname;
|
|
241
411
|
const isInertia = ctx.c.get("inertia");
|
|
242
412
|
const { shared: resolvedShared, sharedKeys } = await this.resolveSharedData(ctx);
|
|
243
413
|
const resolvedSeo = await this.seoService.resolve(ctx);
|
|
@@ -252,9 +422,19 @@ let InertiaService = class InertiaService {
|
|
|
252
422
|
...Object.keys(this.sharedData),
|
|
253
423
|
"seo"
|
|
254
424
|
];
|
|
255
|
-
const result = await this.processProps(allProps, ctx, component, isInertia);
|
|
256
|
-
const
|
|
425
|
+
const result = await this.processProps(allProps, this.partialRequestFor(ctx, component, isInertia));
|
|
426
|
+
const rawFlash = ctx.c.get("inertiaFlash") ?? {};
|
|
427
|
+
const { errors: flashErrors, ...flash } = rawFlash;
|
|
257
428
|
const errors = flashErrors && typeof flashErrors === "object" && !Array.isArray(flashErrors) ? flashErrors : {};
|
|
429
|
+
ctx.c.set(ROUTER_CONTEXT_KEYS.RESPONSE_PAYLOAD, {
|
|
430
|
+
...result.resolvedProps,
|
|
431
|
+
errors
|
|
432
|
+
});
|
|
433
|
+
ctx.c.set("inertiaCacheSignals", buildInertiaCacheSignals({
|
|
434
|
+
flash: rawFlash,
|
|
435
|
+
isPartial: this.isPartialReload(ctx, component),
|
|
436
|
+
onceProps: result.onceProps
|
|
437
|
+
}));
|
|
258
438
|
const page = {
|
|
259
439
|
component,
|
|
260
440
|
props: {
|
|
@@ -270,6 +450,7 @@ let InertiaService = class InertiaService {
|
|
|
270
450
|
...result.prependProps.length > 0 ? { prependProps: result.prependProps } : {},
|
|
271
451
|
...result.deepMergeProps.length > 0 ? { deepMergeProps: result.deepMergeProps } : {},
|
|
272
452
|
...result.matchPropsOn.length > 0 ? { matchPropsOn: result.matchPropsOn } : {},
|
|
453
|
+
...Object.keys(result.scrollProps).length > 0 ? { scrollProps: result.scrollProps } : {},
|
|
273
454
|
...Object.keys(result.deferredProps).length > 0 ? { deferredProps: result.deferredProps } : {},
|
|
274
455
|
...Object.keys(result.deferredProps).length > 0 && !this.isPartialReload(ctx, component) ? { initialDeferredProps: result.deferredProps } : {},
|
|
275
456
|
...Object.keys(result.onceProps).length > 0 ? { onceProps: result.onceProps } : {},
|
|
@@ -288,19 +469,7 @@ let InertiaService = class InertiaService {
|
|
|
288
469
|
}
|
|
289
470
|
});
|
|
290
471
|
const seoTags = this.seoService.tagsFor(resolvedSeo);
|
|
291
|
-
|
|
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, {
|
|
301
|
-
status,
|
|
302
|
-
headers: { "Content-Type": "text/html; charset=utf-8" }
|
|
303
|
-
});
|
|
472
|
+
return this.documentRenderer.render(page, status, seoTags);
|
|
304
473
|
}
|
|
305
474
|
/**
|
|
306
475
|
* Resolve shared data from module options and i18n configuration.
|
|
@@ -351,89 +520,172 @@ let InertiaService = class InertiaService {
|
|
|
351
520
|
const partialDataHeader = ctx.header("x-inertia-partial-data");
|
|
352
521
|
return !!(isInertia && partialComponent === component && partialDataHeader);
|
|
353
522
|
}
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
523
|
+
/**
|
|
524
|
+
* Reads everything about the incoming request that changes how props resolve.
|
|
525
|
+
*
|
|
526
|
+
* Split out so a caller assembling its own page — a modal level, whose props
|
|
527
|
+
* sit under a path rather than at the page root — can resolve props with the
|
|
528
|
+
* same semantics while supplying its own notion of which ones were asked for.
|
|
529
|
+
*/
|
|
530
|
+
partialRequestFor(ctx, component, isInertia) {
|
|
362
531
|
const partialComponent = ctx.header("x-inertia-partial-component");
|
|
363
532
|
const partialDataHeader = ctx.header("x-inertia-partial-data");
|
|
364
533
|
const partialExceptHeader = ctx.header("x-inertia-partial-except");
|
|
365
534
|
const resetHeader = ctx.header("x-inertia-reset");
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
535
|
+
return {
|
|
536
|
+
isPartial: !!(isInertia && partialComponent === component && partialDataHeader),
|
|
537
|
+
requested: partialDataHeader?.split(",").map((s) => s.trim()) ?? [],
|
|
538
|
+
except: partialExceptHeader?.split(",").map((s) => s.trim()) ?? [],
|
|
539
|
+
reset: resetHeader?.split(",").map((s) => s.trim()) ?? [],
|
|
540
|
+
prependIntent: ctx.header(INERTIA_SCROLL_MERGE_INTENT_HEADER) === "prepend",
|
|
541
|
+
resolveDeferred: ctx.header("x-inertia-resolve-deferred") === "true"
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* Resolves a prop record with the helper semantics `render()` applies, for a
|
|
546
|
+
* caller that owns its own page assembly.
|
|
547
|
+
*
|
|
548
|
+
* The returned paths are relative to the record passed in; a caller whose
|
|
549
|
+
* props live under a page path re-anchors them before they reach the page
|
|
550
|
+
* object.
|
|
551
|
+
*/
|
|
552
|
+
async resolveProps(props, request) {
|
|
553
|
+
return this.processProps(props, request);
|
|
554
|
+
}
|
|
555
|
+
async processProps(allProps, request) {
|
|
556
|
+
const resolution = {
|
|
557
|
+
resolvedProps: {},
|
|
558
|
+
mergeProps: [],
|
|
559
|
+
prependProps: [],
|
|
560
|
+
deepMergeProps: [],
|
|
561
|
+
matchPropsOn: [],
|
|
562
|
+
scrollProps: {},
|
|
563
|
+
deferredProps: {},
|
|
564
|
+
onceProps: {}
|
|
565
|
+
};
|
|
566
|
+
await this.collectProps(allProps, request, resolution, resolution.resolvedProps, "", false);
|
|
567
|
+
return resolution;
|
|
568
|
+
}
|
|
569
|
+
/**
|
|
570
|
+
* Resolve one record of props into `into`, naming each by its full path.
|
|
571
|
+
*
|
|
572
|
+
* Recursive because the protocol addresses a nested prop by its dot path: `only: ['auth.user']`
|
|
573
|
+
* asks for one field of `auth`, not for the whole of it. Answering with all of `auth` is the
|
|
574
|
+
* payload the partial reload was sent to avoid.
|
|
575
|
+
*
|
|
576
|
+
* Three things make a prop survive a partial reload, mirroring Laravel's resolver: the request
|
|
577
|
+
* is not partial, its own path was asked for, or an ancestor of it resolved. The last is why a
|
|
578
|
+
* `defer()`/`optional()` prop comes back whole when a path inside it is asked for — nothing
|
|
579
|
+
* inside a callback's result can be narrowed without running the callback, so once it runs, all
|
|
580
|
+
* of it is included.
|
|
581
|
+
*
|
|
582
|
+
* Only plain objects are descended into. An array is a value: the client reads `items.0.name`
|
|
583
|
+
* as a path, but a partial reload asking for one element of a list it does not hold yet has
|
|
584
|
+
* nothing to merge that element into.
|
|
585
|
+
*/
|
|
586
|
+
async collectProps(props, request, resolution, into, parentPath, parentWasResolved) {
|
|
587
|
+
const { isPartial: isPartialReload, requested: requestedProps, except: exceptProps, reset: resetProps, prependIntent, resolveDeferred: shouldResolveDeferred } = request;
|
|
588
|
+
for (const [key, value] of Object.entries(props)) {
|
|
589
|
+
const path = parentPath === "" ? key : `${parentPath}.${key}`;
|
|
590
|
+
const named = !isPartialReload || parentWasResolved || this.isNamed(path, requestedProps);
|
|
591
|
+
const encloses = isPartialReload && this.enclosesNamed(path, requestedProps);
|
|
372
592
|
if (this.isAlwaysProp(value)) {
|
|
373
|
-
|
|
593
|
+
into[key] = await value.callback();
|
|
374
594
|
continue;
|
|
375
595
|
}
|
|
376
596
|
if (this.isOnceProp(value)) {
|
|
377
|
-
if (isPartialReload &&
|
|
597
|
+
if (isPartialReload && (named || encloses)) into[key] = await value.callback();
|
|
378
598
|
else if (!isPartialReload) {
|
|
379
|
-
|
|
380
|
-
onceProps[
|
|
381
|
-
prop: value.key ??
|
|
599
|
+
into[key] = await value.callback();
|
|
600
|
+
resolution.onceProps[path] = {
|
|
601
|
+
prop: value.key ?? path,
|
|
382
602
|
...value.expiresAt != null ? { expiresAt: value.expiresAt } : {}
|
|
383
603
|
};
|
|
384
604
|
}
|
|
385
605
|
continue;
|
|
386
606
|
}
|
|
387
607
|
if (this.isDeferredProp(value)) {
|
|
388
|
-
if (isPartialReload &&
|
|
389
|
-
else if (!isPartialReload)
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
608
|
+
if (isPartialReload && (named || encloses)) into[key] = await value.callback();
|
|
609
|
+
else if (!isPartialReload) {
|
|
610
|
+
if (shouldResolveDeferred) into[key] = await value.callback();
|
|
611
|
+
else {
|
|
612
|
+
resolution.deferredProps[value.group] ??= [];
|
|
613
|
+
resolution.deferredProps[value.group].push(path);
|
|
614
|
+
}
|
|
393
615
|
}
|
|
394
616
|
continue;
|
|
395
617
|
}
|
|
396
618
|
if (this.isMergeProp(value)) {
|
|
397
|
-
if (isPartialReload && !
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
mergeProps.push(
|
|
407
|
-
|
|
619
|
+
if (isPartialReload && !named && !encloses) continue;
|
|
620
|
+
if (!resetProps.includes(path)) {
|
|
621
|
+
switch (value.strategy) {
|
|
622
|
+
case "prepend":
|
|
623
|
+
resolution.prependProps.push(path);
|
|
624
|
+
break;
|
|
625
|
+
case "deep":
|
|
626
|
+
resolution.deepMergeProps.push(path);
|
|
627
|
+
break;
|
|
628
|
+
default: resolution.mergeProps.push(path);
|
|
629
|
+
}
|
|
630
|
+
if (value.matchOn) resolution.matchPropsOn.push(`${path}.${value.matchOn}`);
|
|
631
|
+
}
|
|
632
|
+
into[key] = await value.callback();
|
|
633
|
+
continue;
|
|
634
|
+
}
|
|
635
|
+
if (this.isScrollProp(value)) {
|
|
636
|
+
if (isPartialReload && !named && !encloses) continue;
|
|
637
|
+
const resolved = await value.callback();
|
|
638
|
+
const isReset = resetProps.includes(path);
|
|
639
|
+
if (!isReset) {
|
|
640
|
+
const mergePath = `${path}.${value.wrapper}`;
|
|
641
|
+
if (prependIntent) resolution.prependProps.push(mergePath);
|
|
642
|
+
else resolution.mergeProps.push(mergePath);
|
|
643
|
+
if (value.matchOn) resolution.matchPropsOn.push(`${mergePath}.${value.matchOn}`);
|
|
408
644
|
}
|
|
409
|
-
|
|
410
|
-
|
|
645
|
+
const metadata = value.metadata ? value.metadata(resolved) : deriveScrollMetadata(resolved);
|
|
646
|
+
resolution.scrollProps[path] = {
|
|
647
|
+
pageName: value.pageName ?? metadata.pageName,
|
|
648
|
+
currentPage: metadata.currentPage,
|
|
649
|
+
previousPage: metadata.previousPage,
|
|
650
|
+
nextPage: metadata.nextPage,
|
|
651
|
+
reset: isReset
|
|
652
|
+
};
|
|
653
|
+
into[key] = resolved;
|
|
411
654
|
continue;
|
|
412
655
|
}
|
|
413
656
|
if (this.isOptionalProp(value)) {
|
|
414
|
-
if (isPartialReload &&
|
|
657
|
+
if (isPartialReload && (named || encloses)) into[key] = await value.callback();
|
|
658
|
+
continue;
|
|
659
|
+
}
|
|
660
|
+
if (!named && encloses && this.isPlainRecord(value)) {
|
|
661
|
+
const child = {};
|
|
662
|
+
await this.collectProps(value, request, resolution, child, path, false);
|
|
663
|
+
if (Object.keys(child).length > 0) into[key] = child;
|
|
664
|
+
continue;
|
|
665
|
+
}
|
|
666
|
+
if (!named || this.isExcepted(path, exceptProps)) continue;
|
|
667
|
+
if (this.isPlainRecord(value)) {
|
|
668
|
+
const child = {};
|
|
669
|
+
await this.collectProps(value, request, resolution, child, path, true);
|
|
670
|
+
into[key] = child;
|
|
415
671
|
continue;
|
|
416
672
|
}
|
|
417
|
-
|
|
418
|
-
if (this.isRequested(key, requestedProps) && !this.isExcepted(key, exceptProps)) resolvedProps[key] = value;
|
|
419
|
-
} else resolvedProps[key] = value;
|
|
673
|
+
into[key] = value;
|
|
420
674
|
}
|
|
421
|
-
return {
|
|
422
|
-
resolvedProps,
|
|
423
|
-
mergeProps,
|
|
424
|
-
prependProps,
|
|
425
|
-
deepMergeProps,
|
|
426
|
-
matchPropsOn,
|
|
427
|
-
deferredProps,
|
|
428
|
-
onceProps
|
|
429
|
-
};
|
|
430
675
|
}
|
|
431
|
-
/**
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
676
|
+
/** Whether a plain `{}` record, as opposed to an array, a class instance or a prop marker. */
|
|
677
|
+
isPlainRecord(value) {
|
|
678
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
679
|
+
const prototype = Object.getPrototypeOf(value);
|
|
680
|
+
return prototype === Object.prototype || prototype === null;
|
|
681
|
+
}
|
|
682
|
+
/** Whether this path was asked for, or sits inside a path that was. */
|
|
683
|
+
isNamed(path, requestedProps) {
|
|
684
|
+
return requestedProps.some((prop) => prop === path || path.startsWith(`${prop}.`));
|
|
685
|
+
}
|
|
686
|
+
/** Whether something asked for sits inside this path. */
|
|
687
|
+
enclosesNamed(path, requestedProps) {
|
|
688
|
+
return requestedProps.some((prop) => prop.startsWith(`${path}.`));
|
|
437
689
|
}
|
|
438
690
|
isExcepted(key, exceptProps) {
|
|
439
691
|
return exceptProps.some((prop) => prop === key || prop.startsWith(`${key}.`));
|
|
@@ -447,6 +699,9 @@ let InertiaService = class InertiaService {
|
|
|
447
699
|
isMergeProp(value) {
|
|
448
700
|
return typeof value === "object" && value !== null && INERTIA_PROP_MERGE in value;
|
|
449
701
|
}
|
|
702
|
+
isScrollProp(value) {
|
|
703
|
+
return typeof value === "object" && value !== null && INERTIA_PROP_SCROLL in value;
|
|
704
|
+
}
|
|
450
705
|
isOnceProp(value) {
|
|
451
706
|
return typeof value === "object" && value !== null && INERTIA_PROP_ONCE in value;
|
|
452
707
|
}
|
|
@@ -464,21 +719,12 @@ let InertiaService = class InertiaService {
|
|
|
464
719
|
};
|
|
465
720
|
return serialized;
|
|
466
721
|
}
|
|
467
|
-
isSsrDisabled(pathname) {
|
|
468
|
-
const patterns = this.options.ssr?.disabled;
|
|
469
|
-
if (!patterns || patterns.length === 0) return false;
|
|
470
|
-
return patterns.some((pattern) => {
|
|
471
|
-
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
|
|
472
|
-
return new RegExp(`^/${escaped.replace(/\*/g, "[^/]*")}$`).test(pathname);
|
|
473
|
-
});
|
|
474
|
-
}
|
|
475
722
|
};
|
|
476
723
|
InertiaService = __decorate([
|
|
477
724
|
Request(INERTIA_TOKENS.InertiaService),
|
|
478
725
|
__decorateParam(0, inject(INERTIA_TOKENS.Options)),
|
|
479
|
-
__decorateParam(1, inject(INERTIA_TOKENS.
|
|
480
|
-
__decorateParam(2, inject(INERTIA_TOKENS.
|
|
481
|
-
__decorateParam(3, inject(INERTIA_TOKENS.SeoService))
|
|
726
|
+
__decorateParam(1, inject(INERTIA_TOKENS.DocumentRenderer)),
|
|
727
|
+
__decorateParam(2, inject(INERTIA_TOKENS.SeoService))
|
|
482
728
|
], InertiaService);
|
|
483
729
|
//#endregion
|
|
484
730
|
//#region src/services/manifest.service.ts
|
|
@@ -544,6 +790,16 @@ let SeoService = class SeoService {
|
|
|
544
790
|
this.accumulated = mergeSeo(this.accumulated, data);
|
|
545
791
|
}
|
|
546
792
|
/**
|
|
793
|
+
* Whether anything has contributed SEO to this request.
|
|
794
|
+
*
|
|
795
|
+
* Told apart from "resolves to the defaults" so a caller rendering one page over another can
|
|
796
|
+
* keep the underlying page's metadata when the page on top named none of its own. Resolving
|
|
797
|
+
* regardless would answer with the module defaults and overwrite it.
|
|
798
|
+
*/
|
|
799
|
+
contributed() {
|
|
800
|
+
return Object.keys(this.accumulated).length > 0;
|
|
801
|
+
}
|
|
802
|
+
/**
|
|
547
803
|
* Resolves the final SEO data: module defaults (base) merged with the
|
|
548
804
|
* request's accumulated data, then the title template applied. Resolver
|
|
549
805
|
* functions for `defaults`/`titleTemplate` are awaited with the request `ctx`.
|
|
@@ -844,6 +1100,10 @@ InertiaModule = _InertiaModule = __decorate([Module({ providers: [
|
|
|
844
1100
|
provide: INERTIA_TOKENS.SsrRenderer,
|
|
845
1101
|
useClass: SsrRendererService
|
|
846
1102
|
},
|
|
1103
|
+
{
|
|
1104
|
+
provide: INERTIA_TOKENS.DocumentRenderer,
|
|
1105
|
+
useClass: DocumentRendererService
|
|
1106
|
+
},
|
|
847
1107
|
{
|
|
848
1108
|
provide: INERTIA_TOKENS.HreflangService,
|
|
849
1109
|
useClass: HreflangService
|
|
@@ -892,27 +1152,27 @@ var CookieFlashStore = class {
|
|
|
892
1152
|
}
|
|
893
1153
|
};
|
|
894
1154
|
const inertiaResponse = {
|
|
895
|
-
schema:
|
|
896
|
-
component:
|
|
897
|
-
props:
|
|
898
|
-
url:
|
|
899
|
-
version:
|
|
900
|
-
flash:
|
|
901
|
-
rememberedState:
|
|
902
|
-
mergeProps:
|
|
903
|
-
prependProps:
|
|
904
|
-
deepMergeProps:
|
|
905
|
-
matchPropsOn:
|
|
906
|
-
deferredProps:
|
|
907
|
-
initialDeferredProps:
|
|
908
|
-
onceProps:
|
|
909
|
-
prop:
|
|
910
|
-
expiresAt:
|
|
911
|
-
}))
|
|
912
|
-
sharedProps:
|
|
913
|
-
encryptHistory:
|
|
914
|
-
clearHistory:
|
|
915
|
-
preserveFragment:
|
|
1155
|
+
schema: object({
|
|
1156
|
+
component: string(),
|
|
1157
|
+
props: record(string(), unknown()),
|
|
1158
|
+
url: string(),
|
|
1159
|
+
version: nullable(string()),
|
|
1160
|
+
flash: record(string(), unknown()),
|
|
1161
|
+
rememberedState: record(string(), unknown()),
|
|
1162
|
+
mergeProps: optional(array(string())),
|
|
1163
|
+
prependProps: optional(array(string())),
|
|
1164
|
+
deepMergeProps: optional(array(string())),
|
|
1165
|
+
matchPropsOn: optional(array(string())),
|
|
1166
|
+
deferredProps: optional(record(string(), array(string()))),
|
|
1167
|
+
initialDeferredProps: optional(record(string(), array(string()))),
|
|
1168
|
+
onceProps: optional(record(string(), object({
|
|
1169
|
+
prop: string(),
|
|
1170
|
+
expiresAt: optional(nullable(number()))
|
|
1171
|
+
}))),
|
|
1172
|
+
sharedProps: optional(array(string())),
|
|
1173
|
+
encryptHistory: optional(boolean()),
|
|
1174
|
+
clearHistory: optional(boolean()),
|
|
1175
|
+
preserveFragment: optional(boolean())
|
|
916
1176
|
}),
|
|
917
1177
|
description: "Inertia page response",
|
|
918
1178
|
contentType: "text/html"
|
|
@@ -1047,6 +1307,6 @@ let HandlePrecognitiveRequests = class HandlePrecognitiveRequests {
|
|
|
1047
1307
|
};
|
|
1048
1308
|
HandlePrecognitiveRequests = __decorate([Transient()], HandlePrecognitiveRequests);
|
|
1049
1309
|
//#endregion
|
|
1050
|
-
export { CookieFlashStore, DATA_SEO_ATTR, HandlePrecognitiveRequests, INERTIA_TOKENS, InertiaDelete, InertiaGet, InertiaMiddleware, InertiaModule, InertiaPatch, InertiaPost, InertiaPut, InertiaRoute, InertiaService, ManifestService, SeoService, SsrRendererService, TemplateService, buildSeoTags, descriptorToHtml };
|
|
1310
|
+
export { CookieFlashStore, DATA_SEO_ATTR, DocumentRendererService, HandlePrecognitiveRequests, INERTIA_SCROLL_MERGE_INTENT_HEADER, INERTIA_TOKENS, INERTIA_VARY_HEADERS, InertiaDelete, InertiaGet, InertiaMiddleware, InertiaModule, InertiaPatch, InertiaPost, InertiaPut, InertiaRoute, InertiaService, ManifestService, SeoService, SsrRendererService, TemplateService, UnrecognizedScrollShapeError, buildSeoTags, deriveScrollMetadata, descriptorToHtml };
|
|
1051
1311
|
|
|
1052
1312
|
//# sourceMappingURL=index.mjs.map
|