@stratal/inertia-modal 0.0.23 → 0.0.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -118,9 +118,9 @@ let ModalService = class ModalService {
118
118
  "Vary": "X-Inertia"
119
119
  }
120
120
  });
121
- const ssrResult = await this.ssr.render(combinedPage);
122
- const html = this.template.render(combinedPage, ssrResult.head, ssrResult.body);
123
- return new Response(html, {
121
+ const { head, stream } = await this.ssr.render(combinedPage);
122
+ const body = this.template.renderStream(combinedPage, head, stream);
123
+ return new Response(body, {
124
124
  status: 200,
125
125
  headers: { "Content-Type": "text/html; charset=utf-8" }
126
126
  });
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":["RequestScoped"],"sources":["../src/tokens.ts","../src/augment/router-context.ts","../src/errors/modal-background-fetch.error.ts","../src/services/modal.service.ts","../src/modal.module.ts"],"sourcesContent":["export const MODAL_TOKENS = {\n ModalService: Symbol.for('stratal:inertia-modal:service'),\n} as const\n","import { RouterContext } from 'stratal/router'\nimport type { ModalRenderOptions, ModalService } from '../services/modal.service'\nimport { MODAL_TOKENS } from '../tokens'\n\ndeclare module 'stratal/router' {\n interface RouterContext {\n /**\n * Renders a modal page component over a background page.\n *\n * The background page at `options.baseURL` is always rendered as the main\n * Inertia page. The given `component` and `props` are embedded in the\n * background page's `modal` prop and rendered as an overlay by the\n * client-side `<Modal>` component.\n *\n * Handles direct URL visits by fetching the background page in-process.\n * Handles partial reloads (e.g., cascading selects) when `only: ['modal']`\n * is requested.\n */\n inertiaModal(\n component: string,\n props: Record<string, unknown>,\n options: ModalRenderOptions,\n ): Promise<Response>\n }\n}\n\nexport function augmentRouterContextWithModal(\n resolveService: (ctx: RouterContext) => ModalService,\n): void {\n RouterContext.macro('inertiaModal', function (\n this: RouterContext,\n component: string,\n props: Record<string, unknown>,\n options: ModalRenderOptions,\n ) {\n const service = resolveService(this)\n return service.render(this, component, props, options)\n })\n}\n\nexport { MODAL_TOKENS }\n","import { HttpException } from 'stratal/errors'\n\n/**\n * Thrown when the internal sub-request to fetch the background page fails\n * (e.g., non-2xx response, redirect, or empty body).\n *\n * HTTP Status: 502 Bad Gateway — the modal service acted as a proxy and the\n * upstream (background page) returned an unexpected response.\n */\nexport class ModalBackgroundFetchError extends HttpException {\n constructor() {\n super(502, 'Failed to load background page for modal')\n }\n}\n","import type { Page } from '@inertiajs/core'\nimport { INERTIA_TOKENS, type SsrRendererService, type TemplateService } from '@stratal/inertia'\nimport { Request as RequestScoped, inject } from 'stratal/di'\nimport type { RouterContext } from 'stratal/router'\nimport { ROUTER_TOKENS } from 'stratal/router'\nimport { ModalBackgroundFetchError } from '../errors/modal-background-fetch.error'\n\nexport interface ModalData {\n component: string\n props: Record<string, unknown>\n baseURL: string\n redirectURL: string\n key: string\n nativeBack: boolean\n}\n\nexport interface ModalRenderOptions {\n baseURL: string\n}\n\n// Page from @inertiajs/core doesn't have a 'modal' prop — we extend it here\ntype PageWithModal = Page\n\n// HonoApp extends OpenAPIHono which extends Hono — it has a standard fetch() method\ninterface FetchableApp {\n fetch(request: Request, env: unknown, ctx: unknown): Promise<Response>\n}\n\n@RequestScoped()\nexport class ModalService {\n constructor(\n @inject(ROUTER_TOKENS.HonoApp) private readonly app: FetchableApp,\n @inject(INERTIA_TOKENS.SsrRenderer) private readonly ssr: SsrRendererService,\n @inject(INERTIA_TOKENS.TemplateService) private readonly template: TemplateService,\n ) { }\n\n async render(\n ctx: RouterContext,\n component: string,\n props: Record<string, unknown>,\n options: ModalRenderOptions,\n ): Promise<Response> {\n const isInertia = ctx.c.req.header('x-inertia') === 'true'\n const partialComponent = ctx.c.req.header('x-inertia-partial-component')\n const partialData = ctx.c.req.header('x-inertia-partial-data')\n\n const redirectURL = this.resolveRedirectURL(ctx, options.baseURL)\n const key = ctx.c.req.header('x-inertia-modal-key') ?? crypto.randomUUID()\n const modalURL = new URL(ctx.c.req.url).pathname\n\n // Partial reload requesting 'modal' — skip background sub-request,\n // return just the modal prop with fresh data. The client already has\n // the background page loaded, so nativeBack: true tells useModal()\n // to use history.back() on close instead of a server round-trip.\n if (isInertia && partialComponent && partialData) {\n const requestedProps = partialData.split(',').map((s) => s.trim())\n if (requestedProps.includes('modal')) {\n const partialModalData: ModalData = {\n component,\n props,\n baseURL: options.baseURL,\n redirectURL,\n key,\n nativeBack: true,\n }\n const page: PageWithModal = {\n component: partialComponent,\n props: { modal: partialModalData, errors: {} },\n url: modalURL,\n version: null,\n flash: {},\n rememberedState: {},\n rescuedProps: [],\n }\n return new Response(JSON.stringify(page), {\n status: 200,\n headers: {\n 'Content-Type': 'application/json',\n 'X-Inertia': 'true',\n 'Vary': 'X-Inertia',\n },\n })\n }\n }\n\n const modalData: ModalData = {\n component,\n props,\n baseURL: options.baseURL,\n redirectURL,\n key,\n nativeBack: false,\n }\n\n // Fetch background page as an Inertia JSON request to get its component and\n // props without triggering SSR. We will run SSR ourselves below with the\n // combined page object so that page.url equals the modal URL in both the\n // SSR output and on the client — preventing React hydration mismatches.\n const bgResponse = await this.fetchBackground(ctx, redirectURL)\n const bgText = await bgResponse.text()\n if (!bgText || bgResponse.status >= 300) {\n throw new ModalBackgroundFetchError()\n }\n const bgPage = JSON.parse(bgText) as PageWithModal\n\n // Build the combined page: background props + modal data, URL = modal URL.\n // Setting url to the modal URL ensures Inertia's InitialVisit.handleDefault\n // calls history.replaceState with the modal URL (matching window.location),\n // so the address bar stays at the modal URL on direct visits.\n const combinedPage: PageWithModal = {\n ...bgPage,\n props: { ...bgPage.props, modal: modalData },\n url: modalURL,\n }\n\n if (isInertia) {\n // Inertia AJAX navigation: return JSON\n return new Response(JSON.stringify(combinedPage), {\n status: 200,\n headers: {\n 'Content-Type': 'application/json',\n 'X-Inertia': 'true',\n 'Vary': 'X-Inertia',\n },\n })\n }\n\n // Full-page (direct visit): run SSR with the combined page so that\n // page.url = modalURL in both the server-rendered HTML and the client\n // hydration pass. The Modal component renders null during SSR (effects\n // don't run server-side), so there is no hydration mismatch.\n const ssrResult = await this.ssr.render(combinedPage)\n const html = this.template.render(combinedPage, ssrResult.head, ssrResult.body)\n return new Response(html, {\n status: 200,\n headers: { 'Content-Type': 'text/html; charset=utf-8' },\n })\n }\n\n private resolveRedirectURL(ctx: RouterContext, baseURL: string): string {\n const referer = ctx.c.req.header('referer')\n const isInertia = ctx.c.req.header('x-inertia') === 'true'\n\n if (isInertia && referer) {\n try {\n const refererURL = new URL(referer)\n const currentURL = new URL(ctx.c.req.url)\n if (refererURL.pathname !== currentURL.pathname) {\n // Preserve the query string so the background page (and the\n // post-close redirect) keeps the filter/pagination state the\n // user had on the list view — without this, opening a modal\n // resets the parent page to defaults.\n return refererURL.pathname + refererURL.search\n }\n }\n catch {\n // malformed referer — fall through to baseURL\n }\n }\n\n return baseURL\n }\n\n private async fetchBackground(ctx: RouterContext, url: string): Promise<Response> {\n const currentURL = new URL(ctx.c.req.url)\n const bgURL = new URL(url, currentURL.origin)\n\n const headers: Record<string, string> = {\n // Always request JSON — we run SSR ourselves with the combined page object\n 'x-inertia': 'true',\n // Eagerly resolve deferred props so the background page renders with data\n 'x-inertia-resolve-deferred': 'true',\n // Deliberately omit x-inertia-version: the InertiaMiddleware version check\n // returns a 409 with no body when versions don't match, which would make\n // JSON.parse fail. Internal sub-requests don't need cache-bust checks.\n 'accept': 'application/json',\n // Forward auth/session cookies so the background request is authenticated\n 'cookie': ctx.c.req.header('cookie') ?? '',\n // Forward the host header so domain-pattern middleware can match the\n // request against the configured domain pattern. Without this, the host\n // resolves to the URL's origin (e.g., localhost:1234) which won't match\n // patterns like '{tenant}.admsn.test', causing a DomainMismatchError.\n 'host': ctx.c.req.header('host') ?? '',\n }\n\n // Forward proxy/forwarded-for headers when present so middleware that\n // reconstructs the canonical request URL (e.g. setting `appUrl` to\n // `https://...`) sees the same protocol/host the original request had.\n // Without this, downstream auth (better-auth's secure-cookie prefix is\n // derived from `baseURL`'s protocol) would look up the wrong cookie name\n // and the bg fetch would be unauthenticated — even though the cookie is\n // forwarded above.\n const passthrough = [\n 'x-forwarded-proto',\n 'x-forwarded-host',\n 'x-forwarded-for',\n 'x-forwarded-port',\n 'x-real-ip',\n 'accept-language',\n 'user-agent',\n ] as const\n for (const name of passthrough) {\n const value = ctx.c.req.header(name)\n if (value) headers[name] = value\n }\n\n const bgRequest = new Request(bgURL.toString(), { method: 'GET', headers })\n\n return this.app.fetch(bgRequest, ctx.c.env, ctx.c.executionCtx)\n }\n}\n","import type { OnInitialize } from 'stratal/module'\nimport { Module } from 'stratal/module'\nimport { augmentRouterContextWithModal } from './augment/router-context'\nimport { ModalService } from './services/modal.service'\nimport { MODAL_TOKENS } from './tokens'\n\n@Module({\n providers: [\n { provide: MODAL_TOKENS.ModalService, useClass: ModalService },\n ],\n})\nexport class ModalModule implements OnInitialize {\n onInitialize(): void {\n augmentRouterContextWithModal((ctx) => {\n return ctx.getContainer().resolve<ModalService>(MODAL_TOKENS.ModalService)\n })\n }\n}\n"],"mappings":";;;;;;AAAA,MAAa,eAAe,EAC1B,cAAc,OAAO,IAAI,+BAA+B,EAC1D;;;ACwBA,SAAgB,8BACd,gBACM;CACN,cAAc,MAAM,gBAAgB,SAElC,WACA,OACA,SACA;EAEA,OADgB,eAAe,IAClB,EAAE,OAAO,MAAM,WAAW,OAAO,OAAO;CACvD,CAAC;AACH;;;;;;;;;;AC7BA,IAAa,4BAAb,cAA+C,cAAc;CAC3D,cAAc;EACZ,MAAM,KAAK,0CAA0C;CACvD;AACF;;;;;;;;;;;;;;;;;;ACgBO,IAAA,eAAA,MAAM,aAAa;CAE0B;CACK;CACI;CAH3D,YACE,KACA,KACA,UACA;EAHgD,KAAA,MAAA;EACK,KAAA,MAAA;EACI,KAAA,WAAA;CACvD;CAEJ,MAAM,OACJ,KACA,WACA,OACA,SACmB;EACnB,MAAM,YAAY,IAAI,EAAE,IAAI,OAAO,WAAW,MAAM;EACpD,MAAM,mBAAmB,IAAI,EAAE,IAAI,OAAO,6BAA6B;EACvE,MAAM,cAAc,IAAI,EAAE,IAAI,OAAO,wBAAwB;EAE7D,MAAM,cAAc,KAAK,mBAAmB,KAAK,QAAQ,OAAO;EAChE,MAAM,MAAM,IAAI,EAAE,IAAI,OAAO,qBAAqB,KAAK,OAAO,WAAW;EACzE,MAAM,WAAW,IAAI,IAAI,IAAI,EAAE,IAAI,GAAG,EAAE;EAMxC,IAAI,aAAa,oBAAoB;OACZ,YAAY,MAAM,GAAG,EAAE,KAAK,MAAM,EAAE,KAAK,CAC/C,EAAE,SAAS,OAAO,GAAG;IASpC,MAAM,OAAsB;KAC1B,WAAW;KACX,OAAO;MAAE,OAAO;OAThB;OACA;OACA,SAAS,QAAQ;OACjB;OACA;OACA,YAAY;MAImB;MAAG,QAAQ,CAAC;KAAE;KAC7C,KAAK;KACL,SAAS;KACT,OAAO,CAAC;KACR,iBAAiB,CAAC;KAClB,cAAc,CAAC;IACjB;IACA,OAAO,IAAI,SAAS,KAAK,UAAU,IAAI,GAAG;KACxC,QAAQ;KACR,SAAS;MACP,gBAAgB;MAChB,aAAa;MACb,QAAQ;KACV;IACF,CAAC;GACH;;EAGF,MAAM,YAAuB;GAC3B;GACA;GACA,SAAS,QAAQ;GACjB;GACA;GACA,YAAY;EACd;EAMA,MAAM,aAAa,MAAM,KAAK,gBAAgB,KAAK,WAAW;EAC9D,MAAM,SAAS,MAAM,WAAW,KAAK;EACrC,IAAI,CAAC,UAAU,WAAW,UAAU,KAClC,MAAM,IAAI,0BAA0B;EAEtC,MAAM,SAAS,KAAK,MAAM,MAAM;EAMhC,MAAM,eAA8B;GAClC,GAAG;GACH,OAAO;IAAE,GAAG,OAAO;IAAO,OAAO;GAAU;GAC3C,KAAK;EACP;EAEA,IAAI,WAEF,OAAO,IAAI,SAAS,KAAK,UAAU,YAAY,GAAG;GAChD,QAAQ;GACR,SAAS;IACP,gBAAgB;IAChB,aAAa;IACb,QAAQ;GACV;EACF,CAAC;EAOH,MAAM,YAAY,MAAM,KAAK,IAAI,OAAO,YAAY;EACpD,MAAM,OAAO,KAAK,SAAS,OAAO,cAAc,UAAU,MAAM,UAAU,IAAI;EAC9E,OAAO,IAAI,SAAS,MAAM;GACxB,QAAQ;GACR,SAAS,EAAE,gBAAgB,2BAA2B;EACxD,CAAC;CACH;CAEA,mBAA2B,KAAoB,SAAyB;EACtE,MAAM,UAAU,IAAI,EAAE,IAAI,OAAO,SAAS;EAG1C,IAFkB,IAAI,EAAE,IAAI,OAAO,WAAW,MAAM,UAEnC,SACf,IAAI;GACF,MAAM,aAAa,IAAI,IAAI,OAAO;GAClC,MAAM,aAAa,IAAI,IAAI,IAAI,EAAE,IAAI,GAAG;GACxC,IAAI,WAAW,aAAa,WAAW,UAKrC,OAAO,WAAW,WAAW,WAAW;EAE5C,QACM,CAEN;EAGF,OAAO;CACT;CAEA,MAAc,gBAAgB,KAAoB,KAAgC;EAChF,MAAM,aAAa,IAAI,IAAI,IAAI,EAAE,IAAI,GAAG;EACxC,MAAM,QAAQ,IAAI,IAAI,KAAK,WAAW,MAAM;EAE5C,MAAM,UAAkC;GAEtC,aAAa;GAEb,8BAA8B;GAI9B,UAAU;GAEV,UAAU,IAAI,EAAE,IAAI,OAAO,QAAQ,KAAK;GAKxC,QAAQ,IAAI,EAAE,IAAI,OAAO,MAAM,KAAK;EACtC;EAkBA,KAAK,MAAM,QAAQ;GARjB;GACA;GACA;GACA;GACA;GACA;GACA;EAE2B,GAAG;GAC9B,MAAM,QAAQ,IAAI,EAAE,IAAI,OAAO,IAAI;GACnC,IAAI,OAAO,QAAQ,QAAQ;EAC7B;EAEA,MAAM,YAAY,IAAI,QAAQ,MAAM,SAAS,GAAG;GAAE,QAAQ;GAAO;EAAQ,CAAC;EAE1E,OAAO,KAAK,IAAI,MAAM,WAAW,IAAI,EAAE,KAAK,IAAI,EAAE,YAAY;CAChE;AACF;;CAtLCA,UAAc;oBAGV,OAAO,cAAc,OAAO,CAAA;oBAC5B,OAAO,eAAe,WAAW,CAAA;oBACjC,OAAO,eAAe,eAAe,CAAA;;;;ACtBnC,IAAA,cAAA,MAAM,YAAoC;CAC/C,eAAqB;EACnB,+BAA+B,QAAQ;GACrC,OAAO,IAAI,aAAa,EAAE,QAAsB,aAAa,YAAY;EAC3E,CAAC;CACH;AACF;0BAXC,OAAO,EACN,WAAW,CACT;CAAE,SAAS,aAAa;CAAc,UAAU;AAAa,CAC/D,EACF,CAAC,CAAA,GAAA,WAAA"}
1
+ {"version":3,"file":"index.mjs","names":["RequestScoped"],"sources":["../src/tokens.ts","../src/augment/router-context.ts","../src/errors/modal-background-fetch.error.ts","../src/services/modal.service.ts","../src/modal.module.ts"],"sourcesContent":["export const MODAL_TOKENS = {\n ModalService: Symbol.for('stratal:inertia-modal:service'),\n} as const\n","import { RouterContext } from 'stratal/router'\nimport type { ModalRenderOptions, ModalService } from '../services/modal.service'\nimport { MODAL_TOKENS } from '../tokens'\n\ndeclare module 'stratal/router' {\n interface RouterContext {\n /**\n * Renders a modal page component over a background page.\n *\n * The background page at `options.baseURL` is always rendered as the main\n * Inertia page. The given `component` and `props` are embedded in the\n * background page's `modal` prop and rendered as an overlay by the\n * client-side `<Modal>` component.\n *\n * Handles direct URL visits by fetching the background page in-process.\n * Handles partial reloads (e.g., cascading selects) when `only: ['modal']`\n * is requested.\n */\n inertiaModal(\n component: string,\n props: Record<string, unknown>,\n options: ModalRenderOptions,\n ): Promise<Response>\n }\n}\n\nexport function augmentRouterContextWithModal(\n resolveService: (ctx: RouterContext) => ModalService,\n): void {\n RouterContext.macro('inertiaModal', function (\n this: RouterContext,\n component: string,\n props: Record<string, unknown>,\n options: ModalRenderOptions,\n ) {\n const service = resolveService(this)\n return service.render(this, component, props, options)\n })\n}\n\nexport { MODAL_TOKENS }\n","import { HttpException } from 'stratal/errors'\n\n/**\n * Thrown when the internal sub-request to fetch the background page fails\n * (e.g., non-2xx response, redirect, or empty body).\n *\n * HTTP Status: 502 Bad Gateway — the modal service acted as a proxy and the\n * upstream (background page) returned an unexpected response.\n */\nexport class ModalBackgroundFetchError extends HttpException {\n constructor() {\n super(502, 'Failed to load background page for modal')\n }\n}\n","import type { Page } from '@inertiajs/core'\nimport { INERTIA_TOKENS, type SsrRendererService, type TemplateService } from '@stratal/inertia'\nimport { Request as RequestScoped, inject } from 'stratal/di'\nimport type { RouterContext } from 'stratal/router'\nimport { ROUTER_TOKENS } from 'stratal/router'\nimport { ModalBackgroundFetchError } from '../errors/modal-background-fetch.error'\n\nexport interface ModalData {\n component: string\n props: Record<string, unknown>\n baseURL: string\n redirectURL: string\n key: string\n nativeBack: boolean\n}\n\nexport interface ModalRenderOptions {\n baseURL: string\n}\n\n// Page from @inertiajs/core doesn't have a 'modal' prop — we extend it here\ntype PageWithModal = Page\n\n// HonoApp extends OpenAPIHono which extends Hono — it has a standard fetch() method\ninterface FetchableApp {\n fetch(request: Request, env: unknown, ctx: unknown): Promise<Response>\n}\n\n@RequestScoped()\nexport class ModalService {\n constructor(\n @inject(ROUTER_TOKENS.HonoApp) private readonly app: FetchableApp,\n @inject(INERTIA_TOKENS.SsrRenderer) private readonly ssr: SsrRendererService,\n @inject(INERTIA_TOKENS.TemplateService) private readonly template: TemplateService,\n ) { }\n\n async render(\n ctx: RouterContext,\n component: string,\n props: Record<string, unknown>,\n options: ModalRenderOptions,\n ): Promise<Response> {\n const isInertia = ctx.c.req.header('x-inertia') === 'true'\n const partialComponent = ctx.c.req.header('x-inertia-partial-component')\n const partialData = ctx.c.req.header('x-inertia-partial-data')\n\n const redirectURL = this.resolveRedirectURL(ctx, options.baseURL)\n const key = ctx.c.req.header('x-inertia-modal-key') ?? crypto.randomUUID()\n const modalURL = new URL(ctx.c.req.url).pathname\n\n // Partial reload requesting 'modal' — skip background sub-request,\n // return just the modal prop with fresh data. The client already has\n // the background page loaded, so nativeBack: true tells useModal()\n // to use history.back() on close instead of a server round-trip.\n if (isInertia && partialComponent && partialData) {\n const requestedProps = partialData.split(',').map((s) => s.trim())\n if (requestedProps.includes('modal')) {\n const partialModalData: ModalData = {\n component,\n props,\n baseURL: options.baseURL,\n redirectURL,\n key,\n nativeBack: true,\n }\n const page: PageWithModal = {\n component: partialComponent,\n props: { modal: partialModalData, errors: {} },\n url: modalURL,\n version: null,\n flash: {},\n rememberedState: {},\n rescuedProps: [],\n }\n return new Response(JSON.stringify(page), {\n status: 200,\n headers: {\n 'Content-Type': 'application/json',\n 'X-Inertia': 'true',\n 'Vary': 'X-Inertia',\n },\n })\n }\n }\n\n const modalData: ModalData = {\n component,\n props,\n baseURL: options.baseURL,\n redirectURL,\n key,\n nativeBack: false,\n }\n\n // Fetch background page as an Inertia JSON request to get its component and\n // props without triggering SSR. We will run SSR ourselves below with the\n // combined page object so that page.url equals the modal URL in both the\n // SSR output and on the client — preventing React hydration mismatches.\n const bgResponse = await this.fetchBackground(ctx, redirectURL)\n const bgText = await bgResponse.text()\n if (!bgText || bgResponse.status >= 300) {\n throw new ModalBackgroundFetchError()\n }\n const bgPage = JSON.parse(bgText) as PageWithModal\n\n // Build the combined page: background props + modal data, URL = modal URL.\n // Setting url to the modal URL ensures Inertia's InitialVisit.handleDefault\n // calls history.replaceState with the modal URL (matching window.location),\n // so the address bar stays at the modal URL on direct visits.\n const combinedPage: PageWithModal = {\n ...bgPage,\n props: { ...bgPage.props, modal: modalData },\n url: modalURL,\n }\n\n if (isInertia) {\n // Inertia AJAX navigation: return JSON\n return new Response(JSON.stringify(combinedPage), {\n status: 200,\n headers: {\n 'Content-Type': 'application/json',\n 'X-Inertia': 'true',\n 'Vary': 'X-Inertia',\n },\n })\n }\n\n // Full-page (direct visit): run SSR with the combined page so that\n // page.url = modalURL in both the server-rendered HTML and the client\n // hydration pass. The Modal component renders null during SSR (effects\n // don't run server-side), so there is no hydration mismatch.\n const { head, stream } = await this.ssr.render(combinedPage)\n const body = this.template.renderStream(combinedPage, head, stream)\n return new Response(body, {\n status: 200,\n headers: { 'Content-Type': 'text/html; charset=utf-8' },\n })\n }\n\n private resolveRedirectURL(ctx: RouterContext, baseURL: string): string {\n const referer = ctx.c.req.header('referer')\n const isInertia = ctx.c.req.header('x-inertia') === 'true'\n\n if (isInertia && referer) {\n try {\n const refererURL = new URL(referer)\n const currentURL = new URL(ctx.c.req.url)\n if (refererURL.pathname !== currentURL.pathname) {\n // Preserve the query string so the background page (and the\n // post-close redirect) keeps the filter/pagination state the\n // user had on the list view — without this, opening a modal\n // resets the parent page to defaults.\n return refererURL.pathname + refererURL.search\n }\n }\n catch {\n // malformed referer — fall through to baseURL\n }\n }\n\n return baseURL\n }\n\n private async fetchBackground(ctx: RouterContext, url: string): Promise<Response> {\n const currentURL = new URL(ctx.c.req.url)\n const bgURL = new URL(url, currentURL.origin)\n\n const headers: Record<string, string> = {\n // Always request JSON — we run SSR ourselves with the combined page object\n 'x-inertia': 'true',\n // Eagerly resolve deferred props so the background page renders with data\n 'x-inertia-resolve-deferred': 'true',\n // Deliberately omit x-inertia-version: the InertiaMiddleware version check\n // returns a 409 with no body when versions don't match, which would make\n // JSON.parse fail. Internal sub-requests don't need cache-bust checks.\n 'accept': 'application/json',\n // Forward auth/session cookies so the background request is authenticated\n 'cookie': ctx.c.req.header('cookie') ?? '',\n // Forward the host header so domain-pattern middleware can match the\n // request against the configured domain pattern. Without this, the host\n // resolves to the URL's origin (e.g., localhost:1234) which won't match\n // patterns like '{tenant}.admsn.test', causing a DomainMismatchError.\n 'host': ctx.c.req.header('host') ?? '',\n }\n\n // Forward proxy/forwarded-for headers when present so middleware that\n // reconstructs the canonical request URL (e.g. setting `appUrl` to\n // `https://...`) sees the same protocol/host the original request had.\n // Without this, downstream auth (better-auth's secure-cookie prefix is\n // derived from `baseURL`'s protocol) would look up the wrong cookie name\n // and the bg fetch would be unauthenticated — even though the cookie is\n // forwarded above.\n const passthrough = [\n 'x-forwarded-proto',\n 'x-forwarded-host',\n 'x-forwarded-for',\n 'x-forwarded-port',\n 'x-real-ip',\n 'accept-language',\n 'user-agent',\n ] as const\n for (const name of passthrough) {\n const value = ctx.c.req.header(name)\n if (value) headers[name] = value\n }\n\n const bgRequest = new Request(bgURL.toString(), { method: 'GET', headers })\n\n return this.app.fetch(bgRequest, ctx.c.env, ctx.c.executionCtx)\n }\n}\n","import type { OnInitialize } from 'stratal/module'\nimport { Module } from 'stratal/module'\nimport { augmentRouterContextWithModal } from './augment/router-context'\nimport { ModalService } from './services/modal.service'\nimport { MODAL_TOKENS } from './tokens'\n\n@Module({\n providers: [\n { provide: MODAL_TOKENS.ModalService, useClass: ModalService },\n ],\n})\nexport class ModalModule implements OnInitialize {\n onInitialize(): void {\n augmentRouterContextWithModal((ctx) => {\n return ctx.getContainer().resolve<ModalService>(MODAL_TOKENS.ModalService)\n })\n }\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stratal/inertia-modal",
3
- "version": "0.0.23",
3
+ "version": "0.0.24",
4
4
  "description": "Modal page primitive for Stratal Inertia — backend-driven modal dialogs",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -42,10 +42,10 @@
42
42
  "peerDependencies": {
43
43
  "@inertiajs/core": ">=3",
44
44
  "@inertiajs/react": ">=3",
45
- "@stratal/inertia": ">=0.0.23",
45
+ "@stratal/inertia": ">=0.0.24",
46
46
  "hono": ">=4",
47
47
  "react": ">=19",
48
- "stratal": ">=0.0.23"
48
+ "stratal": ">=0.0.24"
49
49
  },
50
50
  "peerDependenciesMeta": {
51
51
  "@inertiajs/core": {