@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/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 { INERTIA_TOKENS } from "@stratal/inertia";
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 = { ModalService: Symbol.for("stratal:inertia-modal:service") };
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("inertiaModal", function(component, props, options) {
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 internal sub-request to fetch the background page fails
19
- * (e.g., non-2xx response, redirect, or empty body).
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 — the modal service acted as a proxy and the
22
- * upstream (background page) returned an unexpected response.
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, "Failed to load background page for modal");
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.133.0/helpers/esm/decorateParam.js
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.133.0/helpers/esm/decorate.js
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/services/modal.service.ts
46
- let ModalService = class ModalService {
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
- ssr;
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
- async render(ctx, component, props, options) {
56
- const isInertia = ctx.c.req.header("x-inertia") === "true";
57
- const partialComponent = ctx.c.req.header("x-inertia-partial-component");
58
- const partialData = ctx.c.req.header("x-inertia-partial-data");
59
- const redirectURL = this.resolveRedirectURL(ctx, options.baseURL);
60
- const key = ctx.c.req.header("x-inertia-modal-key") ?? crypto.randomUUID();
61
- const modalURL = new URL(ctx.c.req.url).pathname;
62
- if (isInertia && partialComponent && partialData) {
63
- if (partialData.split(",").map((s) => s.trim()).includes("modal")) {
64
- const page = {
65
- component: partialComponent,
66
- props: {
67
- modal: {
68
- component,
69
- props,
70
- baseURL: options.baseURL,
71
- redirectURL,
72
- key,
73
- nativeBack: true
74
- },
75
- errors: {}
76
- },
77
- url: modalURL,
78
- version: null,
79
- flash: {},
80
- rememberedState: {},
81
- rescuedProps: []
82
- };
83
- return new Response(JSON.stringify(page), {
84
- status: 200,
85
- headers: {
86
- "Content-Type": "application/json",
87
- "X-Inertia": "true",
88
- "Vary": "X-Inertia"
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
- const modalData = {
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
- baseURL: options.baseURL,
97
- redirectURL,
98
- key,
99
- nativeBack: false
304
+ props: resolution.resolvedProps,
305
+ url: `${requestURL.pathname}${requestURL.search}`,
306
+ base: options.base,
307
+ close
100
308
  };
101
- const bgResponse = await this.fetchBackground(ctx, redirectURL);
102
- const bgText = await bgResponse.text();
103
- if (!bgText || bgResponse.status >= 300) throw new ModalBackgroundFetchError();
104
- const bgPage = JSON.parse(bgText);
105
- const combinedPage = {
106
- ...bgPage,
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
- ...bgPage.props,
109
- modal: modalData
330
+ ...chain.page.props,
331
+ errors,
332
+ [MODAL_PROP]: modal,
333
+ [MODAL_BENEATH_PROP]: chain.levels
110
334
  },
111
- url: modalURL
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
- if (isInertia) return new Response(JSON.stringify(combinedPage), {
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(ROUTER_TOKENS.HonoApp)),
169
- __decorateParam(1, inject(INERTIA_TOKENS.SsrRenderer)),
170
- __decorateParam(2, inject(INERTIA_TOKENS.TemplateService))
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({ providers: [{
182
- provide: MODAL_TOKENS.ModalService,
183
- useClass: ModalService
184
- }] })], ModalModule);
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