@stratal/inertia-modal 0.0.27 → 0.1.1
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 +298 -0
- package/README.md +223 -0
- package/dist/index.d.mts +85 -22
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +437 -123
- package/dist/index.mjs.map +1 -1
- package/dist/level-path-DCJD-aS3.mjs +33 -0
- package/dist/level-path-DCJD-aS3.mjs.map +1 -0
- package/dist/page-props-BCEWEw3V.d.mts +23 -0
- package/dist/page-props-BCEWEw3V.d.mts.map +1 -0
- package/dist/react.d.mts +144 -21
- package/dist/react.d.mts.map +1 -1
- package/dist/react.mjs +557 -46
- package/dist/react.mjs.map +1 -1
- package/dist/testing.d.mts +33 -0
- package/dist/testing.d.mts.map +1 -0
- package/dist/testing.mjs +99 -0
- package/dist/testing.mjs.map +1 -0
- package/dist/wire-BNVvmku4.mjs +93 -0
- package/dist/wire-BNVvmku4.mjs.map +1 -0
- package/dist/wire-CbwmWkPr.d.mts +54 -0
- package/dist/wire-CbwmWkPr.d.mts.map +1 -0
- package/package.json +41 -17
package/dist/index.mjs
CHANGED
|
@@ -1,40 +1,75 @@
|
|
|
1
|
+
import { a as MODAL_PROP, c as isModalData, i as MODAL_MARKER_HEADER, l as isModalPropPath, n as MODAL_DOCUMENT_HEADER, o as decodeHeldLevels, t as MODAL_BENEATH_PROP } from "./wire-BNVvmku4.mjs";
|
|
2
|
+
import { n as samePath, t as levelPath } from "./level-path-DCJD-aS3.mjs";
|
|
3
|
+
import { I18nModule, withI18n } from "stratal/i18n";
|
|
1
4
|
import { Module } from "stratal/module";
|
|
2
|
-
import { ROUTER_TOKENS, RouterContext } from "stratal/router";
|
|
3
|
-
import {
|
|
4
|
-
import { Request as Request$1, inject } from "stratal/di";
|
|
5
|
+
import { ROUTER_TOKENS, RouterContext, markNestedDispatch } from "stratal/router";
|
|
6
|
+
import { Request as Request$1, Transient, inject } from "stratal/di";
|
|
5
7
|
import { HttpException } from "stratal/errors";
|
|
8
|
+
import { INERTIA_TOKENS } from "@stratal/inertia";
|
|
6
9
|
//#region src/tokens.ts
|
|
7
|
-
const MODAL_TOKENS = {
|
|
10
|
+
const MODAL_TOKENS = {
|
|
11
|
+
ModalService: Symbol.for("stratal:inertia-modal:service"),
|
|
12
|
+
/**
|
|
13
|
+
* How the page beneath a modal is fetched on a document request. Override it to dispatch through
|
|
14
|
+
* something other than the app in process.
|
|
15
|
+
*/
|
|
16
|
+
BackgroundDispatcher: Symbol.for("stratal:inertia-modal:background-dispatcher")
|
|
17
|
+
};
|
|
8
18
|
//#endregion
|
|
9
19
|
//#region src/augment/router-context.ts
|
|
10
20
|
function augmentRouterContextWithModal(resolveService) {
|
|
11
|
-
RouterContext.macro("
|
|
21
|
+
RouterContext.macro("modal", function(component, props, options) {
|
|
12
22
|
return resolveService(this).render(this, component, props, options);
|
|
13
23
|
});
|
|
14
24
|
}
|
|
15
25
|
//#endregion
|
|
26
|
+
//#region src/i18n/en.ts
|
|
27
|
+
const modalMessages = { en: { errors: {
|
|
28
|
+
backgroundFetchFailed: "Failed to load background page for modal",
|
|
29
|
+
baseCycle: "The modal base chain leads back to {url}"
|
|
30
|
+
} } };
|
|
31
|
+
//#endregion
|
|
16
32
|
//#region src/errors/modal-background-fetch.error.ts
|
|
17
33
|
/**
|
|
18
|
-
* Thrown when the
|
|
19
|
-
*
|
|
34
|
+
* Thrown when the sub-request for the page beneath a modal answers with something the chain cannot
|
|
35
|
+
* be built from — a non-2xx, a redirect, an empty body, a body that is not a page, or a level of a
|
|
36
|
+
* shape this build cannot read.
|
|
37
|
+
*
|
|
38
|
+
* `cause` carries the parse failure where there was one. The status this reports is a property of
|
|
39
|
+
* the exchange, not of the reason, so every one of those answers the caller identically; a `base`
|
|
40
|
+
* pointing at a route that does not render a page is still a mistake someone has to find, and the
|
|
41
|
+
* reason is the only thing that says which mistake it was.
|
|
20
42
|
*
|
|
21
|
-
* HTTP Status: 502 Bad Gateway —
|
|
22
|
-
*
|
|
43
|
+
* HTTP Status: 502 Bad Gateway — this service acted as a proxy and the upstream answered
|
|
44
|
+
* unexpectedly.
|
|
23
45
|
*/
|
|
24
46
|
var ModalBackgroundFetchError = class extends HttpException {
|
|
25
|
-
constructor() {
|
|
26
|
-
super(502, "
|
|
47
|
+
constructor(cause) {
|
|
48
|
+
super(502, withI18n("modal.errors.backgroundFetchFailed"), cause);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
//#endregion
|
|
52
|
+
//#region src/errors/modal-base-cycle.error.ts
|
|
53
|
+
/**
|
|
54
|
+
* Raised when a route's `base` chain leads back to a route already in it.
|
|
55
|
+
*
|
|
56
|
+
* Assembling the chain costs one sub-request per level, so a cycle would otherwise run until the
|
|
57
|
+
* runtime's sub-request budget is exhausted and surface as an opaque failure.
|
|
58
|
+
*/
|
|
59
|
+
var ModalBaseCycleError = class extends HttpException {
|
|
60
|
+
constructor(url) {
|
|
61
|
+
super(500, withI18n("modal.errors.baseCycle", { url }));
|
|
27
62
|
}
|
|
28
63
|
};
|
|
29
64
|
//#endregion
|
|
30
|
-
//#region \0@oxc-project+runtime@0.
|
|
65
|
+
//#region \0@oxc-project+runtime@0.150.0/helpers/esm/decorateParam.js
|
|
31
66
|
function __decorateParam(paramIndex, decorator) {
|
|
32
67
|
return function(target, key) {
|
|
33
68
|
decorator(target, key, paramIndex);
|
|
34
69
|
};
|
|
35
70
|
}
|
|
36
71
|
//#endregion
|
|
37
|
-
//#region \0@oxc-project+runtime@0.
|
|
72
|
+
//#region \0@oxc-project+runtime@0.150.0/helpers/esm/decorate.js
|
|
38
73
|
function __decorate(decorators, target, key, desc) {
|
|
39
74
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
40
75
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
@@ -42,133 +77,406 @@ function __decorate(decorators, target, key, desc) {
|
|
|
42
77
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
43
78
|
}
|
|
44
79
|
//#endregion
|
|
45
|
-
//#region src/
|
|
46
|
-
|
|
80
|
+
//#region src/server/background.ts
|
|
81
|
+
/**
|
|
82
|
+
* What the marker header carries, minted once and never sent to a client.
|
|
83
|
+
*
|
|
84
|
+
* The answer has to survive every hop between the dispatch and the route — a caching entrypoint
|
|
85
|
+
* re-dispatches the request, and each hop rebuilds it — so it travels in a header, the only thing
|
|
86
|
+
* that does. A header is otherwise a value anyone can send, and this one decides whether a route
|
|
87
|
+
* that refuses clients answers instead; carrying a value from here closes that, because the value
|
|
88
|
+
* rides only on requests this package makes and is never part of a response.
|
|
89
|
+
*
|
|
90
|
+
* Minted on first use rather than at module scope, where a runtime may refuse to generate it.
|
|
91
|
+
*/
|
|
92
|
+
let token;
|
|
93
|
+
function backgroundToken() {
|
|
94
|
+
return token ??= crypto.randomUUID();
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Whether this request is the background render issued for the page beneath a modal.
|
|
98
|
+
*
|
|
99
|
+
* A route that answers a client with a redirect still has to render when it is the `base` of a
|
|
100
|
+
* modal that client may open — otherwise the redirect is followed back to the modal and the chain
|
|
101
|
+
* reports a cycle.
|
|
102
|
+
*
|
|
103
|
+
* A dispatcher that leaves the isolate answers `false` on the far side, which is the safe
|
|
104
|
+
* direction: the route gates as it would for a client rather than opening for one.
|
|
105
|
+
*
|
|
106
|
+
* @example
|
|
107
|
+
* ```typescript
|
|
108
|
+
* if (isModalBackground(ctx)) return next()
|
|
109
|
+
* ```
|
|
110
|
+
*/
|
|
111
|
+
function isModalBackground(ctx) {
|
|
112
|
+
return ctx.c.req.header(MODAL_DOCUMENT_HEADER) === backgroundToken();
|
|
113
|
+
}
|
|
114
|
+
let HonoBackgroundDispatcher = class HonoBackgroundDispatcher {
|
|
47
115
|
app;
|
|
48
|
-
|
|
49
|
-
template;
|
|
50
|
-
constructor(app, ssr, template) {
|
|
116
|
+
constructor(app) {
|
|
51
117
|
this.app = app;
|
|
52
|
-
this.ssr = ssr;
|
|
53
|
-
this.template = template;
|
|
54
118
|
}
|
|
55
|
-
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
119
|
+
fetch(request, ctx) {
|
|
120
|
+
const marked = markNestedDispatch(new Request(request));
|
|
121
|
+
marked.headers.set(MODAL_DOCUMENT_HEADER, backgroundToken());
|
|
122
|
+
return this.app.fetch(marked, ctx.c.env, ctx.c.executionCtx);
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
HonoBackgroundDispatcher = __decorate([Transient(), __decorateParam(0, inject(ROUTER_TOKENS.HonoApp))], HonoBackgroundDispatcher);
|
|
126
|
+
/**
|
|
127
|
+
* Headers a background render needs to answer as the caller would have been answered.
|
|
128
|
+
*
|
|
129
|
+
* The host and the forwarded set matter because middleware reconstructs the canonical request URL
|
|
130
|
+
* from them, and auth derives its cookie name from that URL's protocol — without them a background
|
|
131
|
+
* render is unauthenticated even though the cookie was forwarded.
|
|
132
|
+
*/
|
|
133
|
+
const FORWARDED_HEADERS = [
|
|
134
|
+
"cookie",
|
|
135
|
+
"host",
|
|
136
|
+
"x-forwarded-proto",
|
|
137
|
+
"x-forwarded-host",
|
|
138
|
+
"x-forwarded-for",
|
|
139
|
+
"x-forwarded-port",
|
|
140
|
+
"x-real-ip",
|
|
141
|
+
"accept-language",
|
|
142
|
+
"user-agent"
|
|
143
|
+
];
|
|
144
|
+
let ModalBackground = class ModalBackground {
|
|
145
|
+
dispatcher;
|
|
146
|
+
constructor(dispatcher) {
|
|
147
|
+
this.dispatcher = dispatcher;
|
|
148
|
+
}
|
|
149
|
+
async chainFor(ctx, base) {
|
|
150
|
+
const origin = new URL(ctx.c.req.url).origin;
|
|
151
|
+
const levels = [];
|
|
152
|
+
const followed = /* @__PURE__ */ new Set();
|
|
153
|
+
let next = base;
|
|
154
|
+
for (;;) {
|
|
155
|
+
const url = new URL(next, origin);
|
|
156
|
+
if (followed.has(url.pathname)) throw new ModalBaseCycleError(url.pathname);
|
|
157
|
+
followed.add(url.pathname);
|
|
158
|
+
const response = await this.dispatcher.fetch(this.requestFor(ctx, url), ctx);
|
|
159
|
+
const body = await response.text();
|
|
160
|
+
if (body === "" || response.status >= 300) throw new ModalBackgroundFetchError();
|
|
161
|
+
let page;
|
|
162
|
+
try {
|
|
163
|
+
page = JSON.parse(body);
|
|
164
|
+
} catch (error) {
|
|
165
|
+
throw new ModalBackgroundFetchError(error);
|
|
91
166
|
}
|
|
167
|
+
if (response.headers.get("stratal-modal") !== "true") return {
|
|
168
|
+
page,
|
|
169
|
+
levels
|
|
170
|
+
};
|
|
171
|
+
const level = page.props[MODAL_PROP];
|
|
172
|
+
if (!isModalData(level)) throw new ModalBackgroundFetchError();
|
|
173
|
+
levels.unshift(level);
|
|
174
|
+
next = level.base;
|
|
92
175
|
}
|
|
93
|
-
|
|
176
|
+
}
|
|
177
|
+
requestFor(ctx, url) {
|
|
178
|
+
const headers = new Headers({
|
|
179
|
+
"x-inertia": "true",
|
|
180
|
+
"accept": "application/json"
|
|
181
|
+
});
|
|
182
|
+
for (const name of FORWARDED_HEADERS) {
|
|
183
|
+
const value = ctx.c.req.header(name);
|
|
184
|
+
if (value !== void 0 && value !== "") headers.set(name, value);
|
|
185
|
+
}
|
|
186
|
+
return new Request(url.toString(), {
|
|
187
|
+
method: "GET",
|
|
188
|
+
headers
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
ModalBackground = __decorate([Transient(), __decorateParam(0, inject(MODAL_TOKENS.BackgroundDispatcher))], ModalBackground);
|
|
193
|
+
//#endregion
|
|
194
|
+
//#region src/core/close-target.ts
|
|
195
|
+
/**
|
|
196
|
+
* Where a level lands when it closes.
|
|
197
|
+
*
|
|
198
|
+
* Resolved ONCE, when the level opens, and echoed by the client on every later request for it. A
|
|
199
|
+
* target re-derived per answer drifts: on a refresh the browser's `Referer` is the sheet itself, so
|
|
200
|
+
* the referer branch stops matching and the answer silently falls back to `base` — losing whatever
|
|
201
|
+
* query the list had.
|
|
202
|
+
*/
|
|
203
|
+
/**
|
|
204
|
+
* The page this level closes onto.
|
|
205
|
+
*
|
|
206
|
+
* The referer is preferred because it is the page the user is actually looking at, query and all,
|
|
207
|
+
* where `base` is usually written query-free.
|
|
208
|
+
*/
|
|
209
|
+
function resolveCloseTarget({ referer, base, requestURL, origin, held }) {
|
|
210
|
+
if (referer === null) return base;
|
|
211
|
+
let refererURL;
|
|
212
|
+
try {
|
|
213
|
+
refererURL = new URL(referer);
|
|
214
|
+
} catch {
|
|
215
|
+
return base;
|
|
216
|
+
}
|
|
217
|
+
if (refererURL.origin !== origin) return base;
|
|
218
|
+
const refererPath = levelPath(refererURL.pathname);
|
|
219
|
+
if (!samePath(refererPath, base) && held.some((url) => samePath(url, refererPath))) return base;
|
|
220
|
+
if (samePath(refererURL.pathname, new URL(requestURL).pathname)) return base;
|
|
221
|
+
return `${refererURL.pathname}${refererURL.search}`;
|
|
222
|
+
}
|
|
223
|
+
//#endregion
|
|
224
|
+
//#region src/core/level-props.ts
|
|
225
|
+
const PREFIX = `${MODAL_PROP}.props.`;
|
|
226
|
+
const ARRAY_KEYS = [
|
|
227
|
+
"mergeProps",
|
|
228
|
+
"prependProps",
|
|
229
|
+
"deepMergeProps",
|
|
230
|
+
"matchPropsOn"
|
|
231
|
+
];
|
|
232
|
+
const RECORD_OF_ARRAY_KEYS = ["deferredProps"];
|
|
233
|
+
/** Rewrites every metadata entry on `page` to address `modal.props.*`. */
|
|
234
|
+
function anchorPropMetadata(page) {
|
|
235
|
+
const anchored = { ...page };
|
|
236
|
+
for (const key of ARRAY_KEYS) {
|
|
237
|
+
const value = anchored[key];
|
|
238
|
+
if (Array.isArray(value)) anchored[key] = value.map((entry) => `${PREFIX}${String(entry)}`);
|
|
239
|
+
}
|
|
240
|
+
for (const key of RECORD_OF_ARRAY_KEYS) {
|
|
241
|
+
const value = anchored[key];
|
|
242
|
+
if (isRecord(value)) anchored[key] = Object.fromEntries(Object.entries(value).map(([group, names]) => [group, names.map((name) => `${PREFIX}${name}`)]));
|
|
243
|
+
}
|
|
244
|
+
const scrollProps = anchored.scrollProps;
|
|
245
|
+
if (isRecord(scrollProps)) anchored.scrollProps = Object.fromEntries(Object.entries(scrollProps).map(([name, entry]) => [`${PREFIX}${name}`, entry]));
|
|
246
|
+
const onceProps = anchored.onceProps;
|
|
247
|
+
if (isRecord(onceProps)) anchored.onceProps = Object.fromEntries(Object.entries(onceProps).map(([name, entry]) => [`${PREFIX}${name}`, {
|
|
248
|
+
...entry,
|
|
249
|
+
prop: `${PREFIX}${entry.prop}`
|
|
250
|
+
}]));
|
|
251
|
+
return anchored;
|
|
252
|
+
}
|
|
253
|
+
function isRecord(value) {
|
|
254
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
255
|
+
}
|
|
256
|
+
function levelAskFor(partialData) {
|
|
257
|
+
if (partialData === null || partialData.trim() === "") return { kind: "whole" };
|
|
258
|
+
const modalNames = partialData.split(",").map((name) => name.trim()).filter((name) => isModalPropPath(name));
|
|
259
|
+
if (modalNames.length === 0) return { kind: "unchanged" };
|
|
260
|
+
if (modalNames.includes("modal")) return { kind: "whole" };
|
|
261
|
+
return {
|
|
262
|
+
kind: "props",
|
|
263
|
+
names: levelPropNames(modalNames)
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* The level-relative names among a page-anchored list, dropping any addressed elsewhere.
|
|
268
|
+
*
|
|
269
|
+
* Inertia's prop metadata travels page-anchored, because `modal.props.x` is where the client sees
|
|
270
|
+
* the prop. The level's own props are keyed by the bare name, so anything resolving them has to ask
|
|
271
|
+
* in those terms or it names nothing that exists.
|
|
272
|
+
*/
|
|
273
|
+
function levelPropNames(names) {
|
|
274
|
+
return names.filter((name) => name.startsWith(PREFIX)).map((name) => name.slice(PREFIX.length));
|
|
275
|
+
}
|
|
276
|
+
//#endregion
|
|
277
|
+
//#region src/server/modal.service.ts
|
|
278
|
+
let ModalService = class ModalService {
|
|
279
|
+
documentRenderer;
|
|
280
|
+
inertia;
|
|
281
|
+
seo;
|
|
282
|
+
background;
|
|
283
|
+
constructor(documentRenderer, inertia, seo, background) {
|
|
284
|
+
this.documentRenderer = documentRenderer;
|
|
285
|
+
this.inertia = inertia;
|
|
286
|
+
this.seo = seo;
|
|
287
|
+
this.background = background;
|
|
288
|
+
}
|
|
289
|
+
async render(ctx, component, props, options) {
|
|
290
|
+
const isInertia = ctx.header("x-inertia") === "true";
|
|
291
|
+
const requestURL = new URL(ctx.c.req.url);
|
|
292
|
+
const referer = ctx.header("referer") ?? null;
|
|
293
|
+
const close = resolveCloseTarget({
|
|
294
|
+
referer,
|
|
295
|
+
base: options.base,
|
|
296
|
+
requestURL: ctx.c.req.url,
|
|
297
|
+
origin: requestURL.origin,
|
|
298
|
+
held: decodeHeldLevels(ctx.header("stratal-modal-held") ?? null)
|
|
299
|
+
});
|
|
300
|
+
const ask = this.askedOf(ctx, referer, requestURL);
|
|
301
|
+
const resolution = ask.kind === "unchanged" ? EMPTY_RESOLUTION : await this.inertia.resolveProps(props, this.levelRequest(this.inertia.partialRequestFor(ctx, component, isInertia), ask));
|
|
302
|
+
const modal = {
|
|
94
303
|
component,
|
|
95
|
-
props,
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
nativeBack: false
|
|
304
|
+
props: resolution.resolvedProps,
|
|
305
|
+
url: `${requestURL.pathname}${requestURL.search}`,
|
|
306
|
+
base: options.base,
|
|
307
|
+
close
|
|
100
308
|
};
|
|
101
|
-
const
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
309
|
+
const anchored = anchorPropMetadata(metadataOf(resolution));
|
|
310
|
+
const seoProp = this.seo.contributed() ? { seo: await this.seo.resolve(ctx) } : {};
|
|
311
|
+
const { flash, errors } = this.flashFrom(ctx);
|
|
312
|
+
if (isInertia) return this.json({
|
|
313
|
+
component,
|
|
314
|
+
props: {
|
|
315
|
+
...ask.kind === "unchanged" ? {} : { [MODAL_PROP]: modal },
|
|
316
|
+
errors,
|
|
317
|
+
...seoProp
|
|
318
|
+
},
|
|
319
|
+
url: modal.url,
|
|
320
|
+
version: null,
|
|
321
|
+
flash,
|
|
322
|
+
rememberedState: {},
|
|
323
|
+
rescuedProps: [],
|
|
324
|
+
...anchored
|
|
325
|
+
});
|
|
326
|
+
const chain = await this.background.chainFor(ctx, options.base);
|
|
327
|
+
const page = {
|
|
328
|
+
...chain.page,
|
|
107
329
|
props: {
|
|
108
|
-
...
|
|
109
|
-
|
|
330
|
+
...chain.page.props,
|
|
331
|
+
errors,
|
|
332
|
+
[MODAL_PROP]: modal,
|
|
333
|
+
[MODAL_BENEATH_PROP]: chain.levels
|
|
110
334
|
},
|
|
111
|
-
url:
|
|
335
|
+
url: modal.url,
|
|
336
|
+
flash,
|
|
337
|
+
...mergeMetadata(chain.page, anchored)
|
|
338
|
+
};
|
|
339
|
+
const seoTags = this.seo.contributed() ? this.applyLevelSeo(page, await this.seo.resolve(ctx)) : [];
|
|
340
|
+
return this.documentRenderer.render(page, 200, seoTags);
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* The partial request as the level's own props see it.
|
|
344
|
+
*
|
|
345
|
+
* Inertia decides what to resolve by matching the request's names against the keys of the record
|
|
346
|
+
* it is given, and the two are in different namespaces here: the names arrive page-anchored
|
|
347
|
+
* (`modal.props.items`), while the record is the level's own props keyed bare (`items`). Passed
|
|
348
|
+
* through untranslated, a partial reload names nothing that exists — every prop is skipped, a
|
|
349
|
+
* deferred one is never resolved, and the level answers empty. `<Deferred>` reads that as the
|
|
350
|
+
* prop still being missing and asks again, which is a loop that does not end.
|
|
351
|
+
*
|
|
352
|
+
* `isPartial` is taken from what the request names rather than from `request.isPartial`, which
|
|
353
|
+
* is false for every level. Inertia decides that by comparing `X-Inertia-Partial-Component`
|
|
354
|
+
* against the component being rendered, and the client sends the component of the page it holds
|
|
355
|
+
* — the page beneath, since a level is grafted onto it as a prop, not swapped in for it. So the
|
|
356
|
+
* comparison is between a page and a level and can never match. `narrowedTo` has already
|
|
357
|
+
* established that this request is addressed to this level, by the referer, which is the signal
|
|
358
|
+
* that actually means what `isPartial` is being asked here.
|
|
359
|
+
*
|
|
360
|
+
* `null` resolves the level whole, covering both of its readings: a request addressed elsewhere,
|
|
361
|
+
* and one asking for the level itself.
|
|
362
|
+
*/
|
|
363
|
+
levelRequest(request, ask) {
|
|
364
|
+
return {
|
|
365
|
+
...request,
|
|
366
|
+
isPartial: ask.kind === "props",
|
|
367
|
+
requested: ask.kind === "props" ? ask.names : [],
|
|
368
|
+
except: levelPropNames(request.except),
|
|
369
|
+
reset: levelPropNames(request.reset)
|
|
112
370
|
};
|
|
113
|
-
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* What this request asks of the level.
|
|
374
|
+
*
|
|
375
|
+
* Answering with anything but the whole level is only safe when the client is already looking at
|
|
376
|
+
* it, because a narrowed answer is merged over the props it holds and an `unchanged` one carries
|
|
377
|
+
* none at all. `Referer` is what says so: an XHR from inside the sheet reports the sheet's own
|
|
378
|
+
* url, while a partial re-issued into a modal route by a redirect reports the page the request
|
|
379
|
+
* started from. Its partial headers survive that redirect unchanged, so without this check such
|
|
380
|
+
* a response would leave the level with holes nothing will ever fill.
|
|
381
|
+
*/
|
|
382
|
+
askedOf(ctx, referer, requestURL) {
|
|
383
|
+
if (referer === null) return { kind: "whole" };
|
|
384
|
+
let refererURL;
|
|
385
|
+
try {
|
|
386
|
+
refererURL = new URL(referer);
|
|
387
|
+
} catch {
|
|
388
|
+
return { kind: "whole" };
|
|
389
|
+
}
|
|
390
|
+
if (refererURL.origin !== requestURL.origin || refererURL.pathname !== requestURL.pathname) return { kind: "whole" };
|
|
391
|
+
return levelAskFor(ctx.header("x-inertia-partial-data") ?? null);
|
|
392
|
+
}
|
|
393
|
+
/** Writes a level's resolved SEO onto the page and returns its head tags. */
|
|
394
|
+
applyLevelSeo(page, resolved) {
|
|
395
|
+
page.props.seo = resolved;
|
|
396
|
+
return this.seo.tagsFor(resolved);
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* The validation errors this response carries.
|
|
400
|
+
*
|
|
401
|
+
* Load-bearing on this path rather than incidental: a failed submission redirects back into the
|
|
402
|
+
* modal route, and `preserveState: 'errors'` resolves against the response — an empty record here
|
|
403
|
+
* closes the sheet the user was filling in.
|
|
404
|
+
*/
|
|
405
|
+
flashFrom(ctx) {
|
|
406
|
+
const { errors: rawErrors, ...flash } = ctx.c.get("inertiaFlash") ?? {};
|
|
407
|
+
return {
|
|
408
|
+
flash,
|
|
409
|
+
errors: rawErrors !== void 0 && typeof rawErrors === "object" && !Array.isArray(rawErrors) && rawErrors !== null ? rawErrors : {}
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
json(page) {
|
|
413
|
+
return new Response(JSON.stringify(page), {
|
|
114
414
|
status: 200,
|
|
115
415
|
headers: {
|
|
116
416
|
"Content-Type": "application/json",
|
|
117
417
|
"X-Inertia": "true",
|
|
418
|
+
[MODAL_MARKER_HEADER]: "true",
|
|
118
419
|
"Vary": "X-Inertia"
|
|
119
420
|
}
|
|
120
421
|
});
|
|
121
|
-
const { head, stream } = await this.ssr.render(combinedPage);
|
|
122
|
-
const body = this.template.renderStream(combinedPage, head, stream);
|
|
123
|
-
return new Response(body, {
|
|
124
|
-
status: 200,
|
|
125
|
-
headers: { "Content-Type": "text/html; charset=utf-8" }
|
|
126
|
-
});
|
|
127
|
-
}
|
|
128
|
-
resolveRedirectURL(ctx, baseURL) {
|
|
129
|
-
const referer = ctx.c.req.header("referer");
|
|
130
|
-
if (ctx.c.req.header("x-inertia") === "true" && referer) try {
|
|
131
|
-
const refererURL = new URL(referer);
|
|
132
|
-
const currentURL = new URL(ctx.c.req.url);
|
|
133
|
-
if (refererURL.pathname !== currentURL.pathname) return refererURL.pathname + refererURL.search;
|
|
134
|
-
} catch {}
|
|
135
|
-
return baseURL;
|
|
136
|
-
}
|
|
137
|
-
async fetchBackground(ctx, url) {
|
|
138
|
-
const currentURL = new URL(ctx.c.req.url);
|
|
139
|
-
const bgURL = new URL(url, currentURL.origin);
|
|
140
|
-
const headers = {
|
|
141
|
-
"x-inertia": "true",
|
|
142
|
-
"x-inertia-resolve-deferred": "true",
|
|
143
|
-
"accept": "application/json",
|
|
144
|
-
"cookie": ctx.c.req.header("cookie") ?? "",
|
|
145
|
-
"host": ctx.c.req.header("host") ?? ""
|
|
146
|
-
};
|
|
147
|
-
for (const name of [
|
|
148
|
-
"x-forwarded-proto",
|
|
149
|
-
"x-forwarded-host",
|
|
150
|
-
"x-forwarded-for",
|
|
151
|
-
"x-forwarded-port",
|
|
152
|
-
"x-real-ip",
|
|
153
|
-
"accept-language",
|
|
154
|
-
"user-agent"
|
|
155
|
-
]) {
|
|
156
|
-
const value = ctx.c.req.header(name);
|
|
157
|
-
if (value) headers[name] = value;
|
|
158
|
-
}
|
|
159
|
-
const bgRequest = new Request(bgURL.toString(), {
|
|
160
|
-
method: "GET",
|
|
161
|
-
headers
|
|
162
|
-
});
|
|
163
|
-
return this.app.fetch(bgRequest, ctx.c.env, ctx.c.executionCtx);
|
|
164
422
|
}
|
|
165
423
|
};
|
|
166
424
|
ModalService = __decorate([
|
|
167
425
|
Request$1(),
|
|
168
|
-
__decorateParam(0, inject(
|
|
169
|
-
__decorateParam(1, inject(INERTIA_TOKENS.
|
|
170
|
-
__decorateParam(2, inject(INERTIA_TOKENS.
|
|
426
|
+
__decorateParam(0, inject(INERTIA_TOKENS.DocumentRenderer)),
|
|
427
|
+
__decorateParam(1, inject(INERTIA_TOKENS.InertiaService)),
|
|
428
|
+
__decorateParam(2, inject(INERTIA_TOKENS.SeoService)),
|
|
429
|
+
__decorateParam(3, inject(ModalBackground))
|
|
171
430
|
], ModalService);
|
|
431
|
+
/** What a level contributes when the request asked nothing of it. */
|
|
432
|
+
const EMPTY_RESOLUTION = {
|
|
433
|
+
resolvedProps: {},
|
|
434
|
+
mergeProps: [],
|
|
435
|
+
prependProps: [],
|
|
436
|
+
deepMergeProps: [],
|
|
437
|
+
matchPropsOn: [],
|
|
438
|
+
scrollProps: {},
|
|
439
|
+
deferredProps: {},
|
|
440
|
+
onceProps: {}
|
|
441
|
+
};
|
|
442
|
+
/** A resolution's metadata in the shape a page object carries it. */
|
|
443
|
+
function metadataOf(resolution) {
|
|
444
|
+
return {
|
|
445
|
+
...resolution.mergeProps.length > 0 ? { mergeProps: resolution.mergeProps } : {},
|
|
446
|
+
...resolution.prependProps.length > 0 ? { prependProps: resolution.prependProps } : {},
|
|
447
|
+
...resolution.deepMergeProps.length > 0 ? { deepMergeProps: resolution.deepMergeProps } : {},
|
|
448
|
+
...resolution.matchPropsOn.length > 0 ? { matchPropsOn: resolution.matchPropsOn } : {},
|
|
449
|
+
...Object.keys(resolution.scrollProps).length > 0 ? { scrollProps: resolution.scrollProps } : {},
|
|
450
|
+
...Object.keys(resolution.deferredProps).length > 0 ? { deferredProps: resolution.deferredProps } : {},
|
|
451
|
+
...Object.keys(resolution.onceProps).length > 0 ? { onceProps: resolution.onceProps } : {}
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
/** The page's own metadata with the level's anchored metadata added to it. */
|
|
455
|
+
function mergeMetadata(base, anchored) {
|
|
456
|
+
const deferredProps = { ...base.deferredProps };
|
|
457
|
+
for (const [group, names] of Object.entries(anchored.deferredProps ?? {})) deferredProps[group] = [...deferredProps[group] ?? [], ...names];
|
|
458
|
+
const mergeProps = [...base.mergeProps ?? [], ...anchored.mergeProps ?? []];
|
|
459
|
+
const prependProps = [...base.prependProps ?? [], ...anchored.prependProps ?? []];
|
|
460
|
+
const deepMergeProps = [...base.deepMergeProps ?? [], ...anchored.deepMergeProps ?? []];
|
|
461
|
+
const matchPropsOn = [...base.matchPropsOn ?? [], ...anchored.matchPropsOn ?? []];
|
|
462
|
+
const scrollProps = {
|
|
463
|
+
...base.scrollProps,
|
|
464
|
+
...anchored.scrollProps
|
|
465
|
+
};
|
|
466
|
+
const onceProps = {
|
|
467
|
+
...base.onceProps,
|
|
468
|
+
...anchored.onceProps
|
|
469
|
+
};
|
|
470
|
+
return {
|
|
471
|
+
...mergeProps.length > 0 ? { mergeProps } : {},
|
|
472
|
+
...prependProps.length > 0 ? { prependProps } : {},
|
|
473
|
+
...deepMergeProps.length > 0 ? { deepMergeProps } : {},
|
|
474
|
+
...matchPropsOn.length > 0 ? { matchPropsOn } : {},
|
|
475
|
+
...Object.keys(scrollProps).length > 0 ? { scrollProps } : {},
|
|
476
|
+
...Object.keys(deferredProps).length > 0 ? { deferredProps } : {},
|
|
477
|
+
...Object.keys(onceProps).length > 0 ? { onceProps } : {}
|
|
478
|
+
};
|
|
479
|
+
}
|
|
172
480
|
//#endregion
|
|
173
481
|
//#region src/modal.module.ts
|
|
174
482
|
let ModalModule = class ModalModule {
|
|
@@ -178,11 +486,17 @@ let ModalModule = class ModalModule {
|
|
|
178
486
|
});
|
|
179
487
|
}
|
|
180
488
|
};
|
|
181
|
-
ModalModule = __decorate([Module({
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
489
|
+
ModalModule = __decorate([Module({
|
|
490
|
+
imports: [I18nModule.registerMessages({ en: { modal: modalMessages.en } })],
|
|
491
|
+
providers: [{
|
|
492
|
+
provide: MODAL_TOKENS.ModalService,
|
|
493
|
+
useClass: ModalService
|
|
494
|
+
}, {
|
|
495
|
+
provide: MODAL_TOKENS.BackgroundDispatcher,
|
|
496
|
+
useClass: HonoBackgroundDispatcher
|
|
497
|
+
}]
|
|
498
|
+
})], ModalModule);
|
|
185
499
|
//#endregion
|
|
186
|
-
export { MODAL_TOKENS, ModalModule };
|
|
500
|
+
export { MODAL_BENEATH_PROP, MODAL_DOCUMENT_HEADER, MODAL_MARKER_HEADER, MODAL_PROP, MODAL_TOKENS, ModalBackgroundFetchError, ModalBaseCycleError, ModalModule, isModalBackground, modalMessages };
|
|
187
501
|
|
|
188
502
|
//# sourceMappingURL=index.mjs.map
|