@stratal/inertia 0.0.18 → 0.0.20

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.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/augment/router-context.ts","../src/vite/create-vite-config.ts","../src/commands/inertia-build.command.ts","../src/commands/inertia-dev.command.ts","../src/commands/inertia-install.command.ts","../src/commands/inertia-types.command.ts","../src/inertia.tokens.ts","../src/middleware/inertia.middleware.ts","../src/types.ts","../src/services/inertia.service.ts","../src/services/manifest.service.ts","../src/services/ssr-renderer.service.ts","../src/services/template.service.ts","../src/inertia.module.ts","../src/flash/cookie-flash-store.ts","../src/decorators/inertia.decorators.ts","../src/middleware/handle-precognitive-requests.middleware.ts"],"sourcesContent":["import type { RedirectStatusCode } from 'hono/utils/http-status'\nimport { RouterContext } from 'stratal/router'\nimport type { InertiaService } from '../services/inertia.service'\nimport type {\n InertiaAlwaysProp,\n InertiaDeferredProp,\n InertiaMergeProp,\n InertiaMergeStrategy,\n InertiaOnceProp,\n InertiaOptionalProp,\n InertiaPageComponent,\n InertiaPageRegistry,\n InertiaRenderOptions,\n ResolvedInertiaPageProps,\n} from '../types'\n\nexport interface InertiaMergeOptions {\n strategy?: InertiaMergeStrategy\n matchOn?: string\n}\n\nexport interface InertiaOnceOptions {\n expiresAt?: number | null\n key?: string\n}\n\ndeclare module 'stratal/router' {\n interface RouterContext {\n /** Renders an Inertia page component with the given props and returns an HTTP response. */\n inertia<C extends InertiaPageComponent>(\n component: C,\n ...args: keyof InertiaPageRegistry extends never\n ? [props?: Record<string, unknown>, options?: InertiaRenderOptions]\n : Record<string, never> extends ResolvedInertiaPageProps<C>\n ? [props?: ResolvedInertiaPageProps<C>, options?: InertiaRenderOptions]\n : [props: ResolvedInertiaPageProps<C>, options?: InertiaRenderOptions]\n ): Promise<Response>\n /** Creates a deferred prop that is resolved after the initial page render, optionally grouped for batch loading. */\n defer<T>(callback: () => T, group?: string): InertiaDeferredProp<T>\n /** Creates an optional prop that is only included in the response when explicitly requested by the client. */\n optional<T>(callback: () => T): InertiaOptionalProp<T>\n /** Creates a mergeable prop that merges with existing client-side page data instead of replacing it. */\n merge<T>(callback: () => T, options?: InertiaMergeOptions): InertiaMergeProp<T>\n /** Creates a prop that is only sent on the first visit and cached for subsequent requests. */\n once<T>(callback: () => T, options?: InertiaOnceOptions): InertiaOnceProp<T>\n /** Creates a prop that is always evaluated and included, even on partial reload requests. */\n always<T>(callback: () => T): InertiaAlwaysProp<T>\n /** Sets a flash data entry that will be available on the next page visit. */\n flash(key: string, value: unknown): void\n /** Disables server-side rendering for the current request. */\n withoutSsr(): void\n }\n}\n\nexport function augmentRouterContext(resolveService: (ctx: RouterContext) => InertiaService): void {\n // Override redirect to auto-convert 302 → 303 for non-GET/HEAD requests\n // so the browser follows with GET instead of preserving the original method\n // eslint-disable-next-line @typescript-eslint/unbound-method -- intentionally saving reference, called with .call(this)\n const originalRedirect = RouterContext.prototype.redirect\n RouterContext.macro('redirect', function (this: RouterContext, url: string, status?: RedirectStatusCode) {\n if (!status || status === 302) {\n const method = this.c.req.method\n if (method !== 'GET' && method !== 'HEAD') {\n return originalRedirect.call(this, url, 303)\n }\n }\n return originalRedirect.call(this, url, status)\n })\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n RouterContext.macro('inertia', function (this: RouterContext, component: string, props?: any, options?: InertiaRenderOptions) {\n const service = resolveService(this)\n return service.render(this, component, props as Record<string, unknown>, options)\n })\n\n RouterContext.macro('defer', function <T>(this: RouterContext, callback: () => T, group?: string) {\n const service = resolveService(this)\n return service.defer(callback, group)\n })\n\n RouterContext.macro('optional', function <T>(this: RouterContext, callback: () => T) {\n const service = resolveService(this)\n return service.optional(callback)\n })\n\n RouterContext.macro('merge', function <T>(this: RouterContext, callback: () => T, options?: InertiaMergeOptions) {\n const service = resolveService(this)\n return service.merge(callback, options)\n })\n\n RouterContext.macro('once', function <T>(this: RouterContext, callback: () => T, options?: InertiaOnceOptions) {\n const service = resolveService(this)\n return service.once(callback, options)\n })\n\n RouterContext.macro('always', function <T>(this: RouterContext, callback: () => T) {\n const service = resolveService(this)\n return service.always(callback)\n })\n\n RouterContext.macro('flash', function (this: RouterContext, key: string, value: unknown) {\n const flashOut = this.c.get('inertiaFlashOut') as Record<string, unknown> | undefined\n if (flashOut) {\n flashOut[key] = value\n }\n })\n\n RouterContext.macro('withoutSsr', function (this: RouterContext) {\n this.c.set('withoutSsr', true)\n })\n}\n","import { existsSync, mkdirSync, writeFileSync } from 'node:fs';\nimport { dirname, join } from 'node:path';\nexport interface TempViteConfigOptions {\n cwd: string\n server?: { port?: number; host?: boolean }\n outDir?: string\n persistTo?: string\n}\n\nexport function writeTempViteConfig(options: TempViteConfigOptions): string {\n const configDir = join(options.cwd, 'node_modules', '.stratal')\n const configPath = join(configDir, 'vite.config.mjs')\n mkdirSync(dirname(configPath), { recursive: true })\n\n const hasUserConfig = existsSync(join(options.cwd, 'vite.config.ts'))\n\n const serverConfig = options.server\n ? `server: { port: ${options.server.port}, host: ${options.server.host ? 'true' : 'undefined'} },`\n : ''\n\n const outDirConfig = options.outDir\n ? `outDir: '${options.outDir}',`\n : ''\n\n const cloudflareArgs = options.persistTo\n ? `{ persistState: { path: ${JSON.stringify(options.persistTo)} } }`\n : ''\n\n const content = `\nimport { mergeConfig } from 'vite'\nimport { cloudflare } from '@cloudflare/vite-plugin'\nimport { stratalInertia } from '@stratal/inertia/vite'\n\nlet inertiaPlugin = null\ntry {\n const mod = await import('@inertiajs/vite')\n const inertia = mod.default ?? mod\n inertiaPlugin = inertia()\n} catch {}\n\nconst baseConfig = {\n plugins: [\n cloudflare(${cloudflareArgs}),\n ...(inertiaPlugin ? [inertiaPlugin] : []),\n ...stratalInertia(),\n ],\n publicDir: '${join(options.cwd, 'src', 'inertia', 'public').replace(/\\\\/g, '/')}',\n build: {\n ${outDirConfig}\n },\n ${serverConfig}\n}\n\n${hasUserConfig\n ? `const userModule = await import('${join(options.cwd, 'vite.config.ts').replace(/\\\\/g, '/')}')\nconst userConfig = userModule.default ?? userModule\nexport default mergeConfig(baseConfig, userConfig)`\n : 'export default baseConfig'\n }\n`\n\n writeFileSync(configPath, content, 'utf-8')\n return configPath\n}\n","import { spawn } from 'node:child_process'\nimport { existsSync } from 'node:fs'\nimport { join } from 'node:path'\nimport { Command } from 'stratal/quarry'\nimport { writeTempViteConfig } from '../vite/create-vite-config'\n\nexport class InertiaBuildCommand extends Command {\n static command = 'inertia:build {--outDir=dist : Output directory} {--ssr : Also build SSR bundle}'\n static description = 'Build Inertia.js frontend for production'\n\n async handle(): Promise<number | undefined> {\n const outDir = this.string('outDir') || 'dist'\n const shouldBuildSsr = this.boolean('ssr')\n const cwd = process.cwd()\n\n const entryPath = 'src/inertia/app.tsx'\n if (!existsSync(join(cwd, entryPath))) {\n this.fail('src/inertia/app.tsx not found. Run `quarry inertia:install` first.')\n return 1\n }\n\n const configPath = writeTempViteConfig({ cwd, outDir })\n\n this.info('Building Inertia.js frontend for production...')\n\n const clientCode = await this.spawnVite(cwd, configPath, ['build'])\n if (clientCode !== 0) {\n this.fail('Client build failed.')\n return clientCode\n }\n this.success('Client build complete!')\n\n if (shouldBuildSsr) {\n this.info('Building SSR bundle...')\n const ssrCode = await this.spawnVite(cwd, configPath, ['build', '--ssr'])\n if (ssrCode !== 0) {\n this.fail('SSR build failed.')\n return ssrCode\n }\n this.success('SSR build complete!')\n }\n\n this.success(`Output in ${outDir}/`)\n this.info('Deploy with: npx wrangler deploy')\n return 0\n }\n\n private spawnVite(cwd: string, configPath: string, args: string[]): Promise<number> {\n return new Promise((resolve) => {\n const child = spawn('npx', ['vite', '--config', configPath, ...args], {\n cwd,\n stdio: 'inherit',\n shell: true,\n })\n\n child.on('error', (err) => {\n this.fail(`Vite process error: ${err.message}`)\n resolve(1)\n })\n\n child.on('close', (code) => {\n resolve(code ?? 0)\n })\n })\n }\n}\n","import { spawn } from 'node:child_process'\nimport { existsSync } from 'node:fs'\nimport { join } from 'node:path'\nimport { Command } from 'stratal/quarry'\nimport { writeTempViteConfig } from '../vite/create-vite-config'\n\nexport class InertiaDevCommand extends Command {\n static command = 'inertia:dev {--port= : Dev server port} {--host : Expose to network} {--persist-to= : Shared persist directory for @cloudflare/vite-plugin (relative to cwd; the plugin appends /v3). Use to share R2/KV/cache emulator state across multiple workers in dev.}'\n static description = 'Start Inertia.js Vite development server'\n\n async handle(): Promise<number | undefined> {\n const port = this.number('port')\n const host = this.boolean('host')\n const persistTo = this.string('persist-to')\n const cwd = process.cwd()\n\n const entryPath = 'src/inertia/app.tsx'\n if (!existsSync(join(cwd, entryPath))) {\n this.fail('src/inertia/app.tsx not found. Run `quarry inertia:install` first.')\n return 1\n }\n\n const configPath = writeTempViteConfig({\n cwd,\n server: { port, host },\n persistTo,\n })\n\n this.info('Starting Vite dev server...')\n\n const args = ['vite', 'dev', '--config', configPath]\n if (host) args.push('--host')\n\n return new Promise<number>((resolve) => {\n const child = spawn('npx', args, {\n cwd,\n stdio: 'inherit',\n shell: true,\n })\n\n child.on('error', (err) => {\n this.fail(`Failed to start dev server: ${err.message}`)\n resolve(1)\n })\n\n child.on('close', (code) => {\n resolve(code ?? 0)\n })\n })\n }\n}\n","import { existsSync, mkdirSync, writeFileSync } from 'node:fs'\nimport { join, relative } from 'node:path'\nimport { Command } from 'stratal/quarry'\nimport { runTypeGeneration } from '../generator/type-generator'\n\nconst ROOT_HTML = `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n @viteHead\n @inertiaHead\n</head>\n<body>\n @inertia\n @viteScripts\n</body>\n</html>`\n\nconst APP_TSX = `import { createInertiaApp } from '@inertiajs/react'\n\ncreateInertiaApp({\n resolve: async (name) => {\n const pages = import.meta.glob('./pages/**/*.tsx')\n const page = await pages[\\`./pages/\\${name}.tsx\\`]?.()\n if (!page) throw new Error(\\`Page not found: \\${name}\\`)\n return page\n },\n})`\n\nconst HOME_TSX = `export default function Home({ message }: { message: string }) {\n return (\n <div>\n <h1>{message}</h1>\n <p>This page is rendered with Inertia.js and Stratal.</p>\n </div>\n )\n}`\n\nexport class InertiaInstallCommand extends Command {\n static command = 'inertia:install {--skip-deps : Skip installing npm dependencies}'\n static description = 'Scaffold Inertia.js files for a Stratal project'\n\n async handle(): Promise<number | undefined> {\n const skipDeps = this.boolean('skip-deps')\n const cwd = process.cwd()\n const inertiaDir = join(cwd, 'src', 'inertia')\n const pagesDir = join(inertiaDir, 'pages')\n\n // Create directories\n this.info('Creating src/inertia/ directory...')\n mkdirSync(pagesDir, { recursive: true })\n\n const publicDir = join(inertiaDir, 'public')\n mkdirSync(publicDir, { recursive: true })\n const gitkeepPath = join(publicDir, '.gitkeep')\n if (!existsSync(gitkeepPath)) {\n writeFileSync(gitkeepPath, '', 'utf-8')\n }\n this.success('Created src/inertia/public/')\n\n // Write template files\n const files = [\n { path: join(inertiaDir, 'root.html'), content: ROOT_HTML, name: 'root.html' },\n { path: join(inertiaDir, 'app.tsx'), content: APP_TSX, name: 'app.tsx' },\n { path: join(pagesDir, 'Home.tsx'), content: HOME_TSX, name: 'pages/Home.tsx' },\n ]\n\n for (const file of files) {\n if (existsSync(file.path)) {\n this.warn(`Skipping ${file.name} (already exists)`)\n } else {\n writeFileSync(file.path, file.content, 'utf-8')\n this.success(`Created src/inertia/${file.name}`)\n }\n }\n\n // Modify app.module.ts\n const appModulePath = join(cwd, 'src', 'app.module.ts')\n if (existsSync(appModulePath)) {\n this.info('Updating src/app.module.ts...')\n try {\n const updated = await this.updateAppModule(appModulePath)\n if (updated) {\n this.success('Updated src/app.module.ts with InertiaModule')\n } else {\n this.info('InertiaModule already configured in app.module.ts')\n }\n } catch (err) {\n this.warn(`Could not auto-update app.module.ts: ${(err as Error).message}`)\n this.info('Please manually add InertiaModule.forRoot() to your module imports')\n }\n } else {\n this.info('No src/app.module.ts found — please manually configure InertiaModule')\n }\n\n // Generate initial type definitions\n try {\n const { outputPath, pageCount } = await runTypeGeneration(cwd)\n const relPath = relative(cwd, outputPath)\n this.success(`Generated ${relPath} (${pageCount} page${pageCount !== 1 ? 's' : ''})`)\n } catch {\n this.warn('Could not generate initial type definitions. Run `quarry inertia:types` manually.')\n }\n\n if (!skipDeps) {\n this.newLine()\n this.info('Install the following dependencies:')\n this.line(' npm install @stratal/inertia @inertiajs/react @inertiajs/vite react react-dom')\n this.line(' npm install -D @types/react @types/react-dom vite @cloudflare/vite-plugin')\n }\n\n this.newLine()\n this.success('Inertia.js scaffolding complete!')\n this.info('Run `quarry inertia:dev` to start the dev server')\n\n return 0\n }\n\n private async updateAppModule(modulePath: string): Promise<boolean> {\n const { Project, SyntaxKind } = await import('ts-morph')\n\n const project = new Project({ useInMemoryFileSystem: false })\n const sourceFile = project.addSourceFileAtPath(modulePath)\n\n // Check if InertiaModule is already imported\n const existingImport = sourceFile.getImportDeclaration((decl) =>\n decl.getModuleSpecifierValue() === '@stratal/inertia',\n )\n if (existingImport) {\n return false\n }\n\n // Add rootView import\n sourceFile.addImportDeclaration({\n defaultImport: 'rootView',\n moduleSpecifier: './inertia/root.html?raw',\n })\n\n // Add InertiaModule import\n sourceFile.addImportDeclaration({\n namedImports: ['InertiaModule'],\n moduleSpecifier: '@stratal/inertia',\n })\n\n // Find the @Module decorator and add InertiaModule to imports\n const classes = sourceFile.getClasses()\n for (const cls of classes) {\n const moduleDecorator = cls.getDecorator('Module')\n if (!moduleDecorator) continue\n\n const args = moduleDecorator.getArguments()\n if (args.length === 0) continue\n\n const objLiteral = args[0].asKind(SyntaxKind.ObjectLiteralExpression)\n if (!objLiteral) continue\n\n const importsProp = objLiteral.getProperty('imports')\n if (importsProp) {\n // Add to existing imports array\n const initializer = importsProp.asKind(SyntaxKind.PropertyAssignment)?.getInitializer()\n const arrayLiteral = initializer?.asKind(SyntaxKind.ArrayLiteralExpression)\n if (arrayLiteral) {\n arrayLiteral.addElement(`InertiaModule.forRoot({\\n rootView,\\n })`)\n }\n } else {\n // Add imports property\n objLiteral.addPropertyAssignment({\n name: 'imports',\n initializer: `[\\n InertiaModule.forRoot({\\n rootView,\\n }),\\n ]`,\n })\n }\n\n break\n }\n\n await sourceFile.save()\n return true\n }\n}\n","import { existsSync } from 'node:fs'\nimport { watch } from 'node:fs/promises'\nimport { join, relative } from 'node:path'\nimport { Command } from 'stratal/quarry'\nimport { findPagesDir, runTypeGeneration } from '../generator/type-generator'\n\nexport class InertiaTypesCommand extends Command {\n static command = 'inertia:types {--watch : Watch for changes and regenerate}'\n static description = 'Generate Inertia.js page type definitions'\n\n async handle(): Promise<number | undefined> {\n const cwd = process.cwd()\n const pagesDir = findPagesDir(cwd)\n\n if (!existsSync(pagesDir)) {\n this.fail('src/inertia/pages/ not found. Run `quarry inertia:install` first.')\n return 1\n }\n\n const result = await this.generate(cwd)\n if (!result) return 1\n\n if (this.boolean('watch')) {\n this.info('Watching for changes...')\n await this.watchForChanges(cwd)\n }\n\n return 0\n }\n\n private async generate(cwd: string): Promise<boolean> {\n try {\n const { outputPath, pageCount } = await runTypeGeneration(cwd)\n const relPath = relative(cwd, outputPath)\n this.success(`Generated ${relPath} (${pageCount} page${pageCount !== 1 ? 's' : ''})`)\n return true\n } catch (err) {\n this.fail(`Type generation failed: ${(err as Error).message}`)\n return false\n }\n }\n\n private async watchForChanges(cwd: string): Promise<void> {\n const srcDir = join(cwd, 'src')\n\n try {\n const watcher = watch(srcDir, { recursive: true })\n for await (const event of watcher) {\n if (event.filename && /\\.(tsx|ts)$/.test(event.filename)) {\n this.info(`Change detected: ${event.filename}`)\n await this.generate(cwd)\n }\n }\n } catch (err) {\n this.fail(`Watch failed: ${(err as Error).message}`)\n }\n }\n}\n","export const INERTIA_TOKENS = {\n Options: Symbol.for('stratal:inertia:options'),\n InertiaService: Symbol.for('stratal:inertia:service'),\n TemplateService: Symbol.for('stratal:inertia:template'),\n ManifestService: Symbol.for('stratal:inertia:manifest'),\n SsrRenderer: Symbol.for('stratal:inertia:ssr-renderer'),\n} as const\n","import { Transient, inject } from 'stratal/di'\nimport type { Middleware, Next, RouterContext } from 'stratal/router'\nimport type { InertiaModuleOptions } from '../inertia.options'\nimport { INERTIA_TOKENS } from '../inertia.tokens'\n\n@Transient()\nexport class InertiaMiddleware implements Middleware {\n constructor(\n @inject(INERTIA_TOKENS.Options) private readonly options: InertiaModuleOptions,\n ) { }\n\n async handle(ctx: RouterContext, next: Next): Promise<void> {\n const isInertia = ctx.header('x-inertia') === 'true'\n const isPrefetch = ctx.header('purpose') === 'prefetch'\n\n // Store Inertia state on context for services to access\n ctx.c.set('inertia', isInertia)\n ctx.c.set('inertiaPrefetch', isPrefetch)\n ctx.c.set('withoutSsr', false)\n\n // Initialize flash buckets\n ctx.c.set('inertiaFlashOut', {})\n\n // Read incoming flash data from store (read-only — no response headers touched)\n let hadFlash = false\n if (this.options.flash) {\n const flashData = await this.options.flash.store.read(ctx)\n hadFlash = Object.keys(flashData).length > 0\n ctx.c.set('inertiaFlash', flashData)\n } else {\n ctx.c.set('inertiaFlash', {})\n }\n\n // Version mismatch check on GET requests\n if (isInertia && ctx.c.req.method === 'GET') {\n const clientVersion = ctx.header('x-inertia-version')\n const serverVersion = this.options.version ?? ''\n\n if (clientVersion && serverVersion && clientVersion !== serverVersion) {\n ctx.c.header('X-Inertia-Location', ctx.c.req.url)\n ctx.c.status(409)\n return\n }\n }\n\n await next()\n\n // Flash cookie operations AFTER next() — ctx.c.res is now the actual Response,\n // so setSignedCookie/deleteCookie will modify the real response headers.\n if (this.options.flash) {\n const flashOut = ctx.c.get('inertiaFlashOut')\n if (Object.keys(flashOut).length > 0) {\n // New flash data was set during this request — write cookie for next request\n await this.options.flash.store.write(ctx, flashOut)\n } else if (hadFlash) {\n // Flash was consumed but no new flash set — clear the cookie\n await this.options.flash.store.clear(ctx)\n }\n }\n\n // Skip response mutation for statuses Hono can't clone (e.g. 101 WebSocket\n // upgrades, Response.error()'s status 0). `c.header()` would otherwise call\n // `new Response(c.res.body, c.res)` and the Response constructor throws a\n // RangeError for any status outside 200-599.\n const status = ctx.c.res?.status\n if (typeof status !== 'number' || status < 200 || status > 599) return\n\n // Add Vary header to all responses\n ctx.c.header('Vary', 'X-Inertia')\n\n // Convert 302 to 303 for non-GET/HEAD Inertia requests\n if (isInertia && status === 302) {\n const method = ctx.c.req.method\n if (method !== 'GET' && method !== 'HEAD') {\n ctx.c.status(303)\n }\n }\n }\n}\n","import type { InertiaAppSSRResponse, Page, SharedPageProps } from '@inertiajs/core'\nimport type { RouterContext } from 'stratal/router'\n\n\nexport interface InertiaPageRegistry {}\n\n// Derive shared props from @inertiajs/core's InertiaConfig.sharedPageProps.\n// Users augment InertiaConfig in their global.d.ts — this type stays in sync automatically.\nexport type InertiaSharedProps = SharedPageProps\n\nexport type InertiaPageComponent = keyof InertiaPageRegistry extends never\n ? string\n : Extract<keyof InertiaPageRegistry, string>\n\n// Allows each prop value to be wrapped with defer/merge/optional/once/always\ntype AllowInertiaWrappers<T> = {\n [K in keyof T]: T[K] | InertiaDeferredProp | InertiaMergeProp | InertiaOptionalProp | InertiaOnceProp | InertiaAlwaysProp\n}\n\n// Props the controller passes to ctx.inertia() — page-specific only, shared props are auto-injected\n// Each prop can be the raw value OR a deferred/merge/optional/once/always wrapper\nexport type ResolvedInertiaPageProps<C extends InertiaPageComponent> =\n C extends keyof InertiaPageRegistry ? AllowInertiaWrappers<InertiaPageRegistry[C]> : Record<string, unknown>\n\n// Full props the React page component receives — page-specific + shared (auto-injected), no wrappers\nexport type InertiaFullPageProps<C extends InertiaPageComponent> =\n (C extends keyof InertiaPageRegistry ? InertiaPageRegistry[C] : Record<string, unknown>) & InertiaSharedProps\n\n// Re-export Page from @inertiajs/core as InertiaPage for convenience\nexport type { Page as InertiaPage } from '@inertiajs/core'\n\nexport interface InertiaRenderOptions {\n encryptHistory?: boolean\n clearHistory?: boolean\n preserveFragment?: boolean\n}\n\n// Use InertiaAppSSRResponse from @inertiajs/core — { head: string[]; body: string }\nexport type InertiaSsrResult = InertiaAppSSRResponse\n\nexport interface InertiaSsrBundle {\n render(page: Page): Promise<InertiaSsrResult>\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport type SharedDataResolver = (ctx: RouterContext) => any\n\nexport interface ViteManifestEntry {\n file: string\n css?: string[]\n isEntry?: boolean\n imports?: string[]\n dynamicImports?: string[]\n src?: string\n}\n\nexport type ViteManifest = Record<string, ViteManifestEntry>\n\nexport const INERTIA_PROP_OPTIONAL = Symbol.for('stratal:inertia:prop:optional')\nexport const INERTIA_PROP_DEFERRED = Symbol.for('stratal:inertia:prop:deferred')\nexport const INERTIA_PROP_MERGE = Symbol.for('stratal:inertia:prop:merge')\nexport const INERTIA_PROP_ONCE = Symbol.for('stratal:inertia:prop:once')\nexport const INERTIA_PROP_ALWAYS = Symbol.for('stratal:inertia:prop:always')\n\nexport interface InertiaOptionalProp<T = unknown> {\n [INERTIA_PROP_OPTIONAL]: true\n callback: () => T\n}\n\nexport interface InertiaDeferredProp<T = unknown> {\n [INERTIA_PROP_DEFERRED]: true\n callback: () => T\n group: string\n}\n\nexport type InertiaMergeStrategy = 'append' | 'prepend' | 'deep'\n\nexport interface InertiaMergeProp<T = unknown> {\n [INERTIA_PROP_MERGE]: true\n callback: () => T\n strategy: InertiaMergeStrategy\n matchOn?: string\n}\n\nexport interface InertiaOnceProp<T = unknown> {\n [INERTIA_PROP_ONCE]: true\n callback: () => T\n expiresAt?: number | null\n key?: string\n}\n\nexport interface InertiaAlwaysProp<T = unknown> {\n [INERTIA_PROP_ALWAYS]: true\n callback: () => T\n}\n","import type { Page } from '@inertiajs/core'\nimport type { Application } from 'stratal'\nimport { DI_TOKENS, Transient, inject } from 'stratal/di'\nimport { I18N_TOKENS, type MessageLoaderService } from 'stratal/i18n'\nimport { ROUTER_TOKENS, type CurrentRoute, type RegisteredRoute, type RouteRegistry, type RouterContext, type SerializedRoutes, type Uri } from 'stratal/router'\nimport type { InertiaMergeOptions, InertiaOnceOptions } from '../augment/router-context'\nimport type { InertiaModuleOptions } from '../inertia.options'\nimport { INERTIA_TOKENS } from '../inertia.tokens'\nimport type {\n InertiaAlwaysProp,\n InertiaDeferredProp,\n InertiaMergeProp,\n InertiaOnceProp,\n InertiaOptionalProp,\n InertiaRenderOptions,\n SharedDataResolver,\n} from '../types'\nimport {\n INERTIA_PROP_ALWAYS,\n INERTIA_PROP_DEFERRED,\n INERTIA_PROP_MERGE,\n INERTIA_PROP_ONCE,\n INERTIA_PROP_OPTIONAL,\n} from '../types'\nimport type { SsrRendererService } from './ssr-renderer.service'\nimport type { TemplateService } from './template.service'\n\n@Transient(INERTIA_TOKENS.InertiaService)\nexport class InertiaService {\n private sharedData: Record<string, unknown> = {}\n\n constructor(\n @inject(INERTIA_TOKENS.Options) private readonly options: InertiaModuleOptions,\n @inject(INERTIA_TOKENS.TemplateService) private readonly template: TemplateService,\n @inject(INERTIA_TOKENS.SsrRenderer) private readonly ssr: SsrRendererService,\n ) { }\n\n share(key: string, value: unknown): void {\n this.sharedData[key] = value\n }\n\n location(url: string): Response {\n return new Response('', {\n status: 409,\n headers: { 'X-Inertia-Location': url },\n })\n }\n\n optional<T>(callback: () => T): InertiaOptionalProp<T> {\n return { [INERTIA_PROP_OPTIONAL]: true, callback }\n }\n\n defer<T>(callback: () => T, group = 'default'): InertiaDeferredProp<T> {\n return { [INERTIA_PROP_DEFERRED]: true, callback, group }\n }\n\n merge<T>(callback: () => T, options?: InertiaMergeOptions): InertiaMergeProp<T> {\n return {\n [INERTIA_PROP_MERGE]: true,\n callback,\n strategy: options?.strategy ?? 'append',\n matchOn: options?.matchOn,\n }\n }\n\n once<T>(callback: () => T, options?: InertiaOnceOptions): InertiaOnceProp<T> {\n return {\n [INERTIA_PROP_ONCE]: true,\n callback,\n expiresAt: options?.expiresAt ?? null,\n key: options?.key,\n }\n }\n\n always<T>(callback: () => T): InertiaAlwaysProp<T> {\n return { [INERTIA_PROP_ALWAYS]: true, callback }\n }\n\n async render(\n ctx: RouterContext,\n component: string,\n props: Record<string, unknown> = {},\n renderOptions: InertiaRenderOptions = {},\n ): Promise<Response> {\n const reqUrl = new URL(ctx.c.req.url)\n const url = reqUrl.search ? `${reqUrl.pathname}${reqUrl.search}` : reqUrl.pathname\n const isInertia = ctx.c.get('inertia')\n\n // Resolve shared data from module options\n const { shared: resolvedShared, sharedKeys } = await this.resolveSharedData(ctx)\n\n // Merge shared data with route props\n const allProps = { ...resolvedShared, ...this.sharedData, ...props }\n\n // Track all shared prop keys (module config + per-request .share())\n const allSharedKeys = [...sharedKeys, ...Object.keys(this.sharedData)]\n\n // Process props: handle optional, deferred, merge, once, always\n const result = await this.processProps(allProps, ctx, component, isInertia)\n\n // Read flash data from context (set by middleware)\n const rawFlash = (ctx.c.get('inertiaFlash') as Record<string, unknown> | undefined) ?? {}\n const { errors: flashErrors, ...flash } = rawFlash\n const errors = (flashErrors && typeof flashErrors === 'object' && !Array.isArray(flashErrors))\n ? flashErrors as Page['props']['errors']\n : {} as Page['props']['errors']\n\n const page: Page = {\n component,\n props: { ...result.resolvedProps, errors },\n url,\n version: this.options.version ?? null,\n flash,\n rememberedState: {},\n ...(result.mergeProps.length > 0 ? { mergeProps: result.mergeProps } : {}),\n ...(result.prependProps.length > 0 ? { prependProps: result.prependProps } : {}),\n ...(result.deepMergeProps.length > 0 ? { deepMergeProps: result.deepMergeProps } : {}),\n ...(result.matchPropsOn.length > 0 ? { matchPropsOn: result.matchPropsOn } : {}),\n ...(Object.keys(result.deferredProps).length > 0 ? { deferredProps: result.deferredProps } : {}),\n ...(Object.keys(result.deferredProps).length > 0 && !this.isPartialReload(ctx, component) ? { initialDeferredProps: result.deferredProps } : {}),\n ...(Object.keys(result.onceProps).length > 0 ? { onceProps: result.onceProps } : {}),\n ...(allSharedKeys.length > 0 ? { sharedProps: allSharedKeys } : {}),\n ...(renderOptions.encryptHistory ? { encryptHistory: true } : {}),\n ...(renderOptions.clearHistory ? { clearHistory: true } : {}),\n ...(renderOptions.preserveFragment ? { preserveFragment: true } : {}),\n }\n\n if (isInertia) {\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 // Full page render — skip SSR if disabled for this route\n const ssrDisabled = ctx.c.get('withoutSsr') || this.isSsrDisabled(url)\n const ssrResult = ssrDisabled\n ? { head: [] as string[], body: '' }\n : await this.ssr.render(page)\n const html = this.template.render(page, ssrResult.head, ssrResult.body)\n\n return new Response(html, {\n status: 200,\n headers: {\n 'Content-Type': 'text/html; charset=utf-8',\n },\n })\n }\n\n /**\n * Resolve shared data from module options and i18n configuration.\n *\n * Processes static values and resolver functions from `sharedData` config.\n * When `i18n` option is set, auto-injects `locale` and `translations` props\n * using the core {@link MessageLoaderService} resolved from the request container.\n */\n private async resolveSharedData(ctx: RouterContext): Promise<{ shared: Record<string, unknown>; sharedKeys: string[] }> {\n const shared: Record<string, unknown> = {}\n const configShared = this.options.sharedData\n\n if (configShared) {\n for (const [key, value] of Object.entries(configShared)) {\n if (typeof value === 'function') {\n shared[key] = await (value as SharedDataResolver)(ctx)\n } else {\n shared[key] = value\n }\n }\n }\n\n if (this.options.i18n) {\n const loader = ctx.getContainer().resolve<MessageLoaderService>(I18N_TOKENS.MessageLoader)\n const locale = ctx.getLocale()\n shared.locale = locale\n shared.translations = loader.getFilteredMessages(locale, { only: this.options.i18n.only })\n }\n\n if (this.options.routes) {\n const container = ctx.getContainer()\n const registry = container.resolve<RouteRegistry>(ROUTER_TOKENS.RouteRegistry)\n const application = container.resolve<Application>(DI_TOKENS.Application)\n const uri = container.resolve<Uri>(ROUTER_TOKENS.Uri)\n\n const name = registry.findNameByRoute(ctx.c.req.method, ctx.c.req.routePath) ?? null\n const params = { ...ctx.param() }\n\n shared.routes = this.serializeRoutes(registry.named())\n shared.trailingSlash = application.config.trailingSlash ?? 'ignore'\n shared.route = { name, params, defaults: uri.getDefaults() } satisfies CurrentRoute\n }\n\n return { shared, sharedKeys: Object.keys(shared) }\n }\n\n private isPartialReload(ctx: RouterContext, component: string): boolean {\n const isInertia = ctx.c.get('inertia')\n const partialComponent = ctx.header('x-inertia-partial-component')\n const partialDataHeader = ctx.header('x-inertia-partial-data')\n return !!(isInertia && partialComponent === component && partialDataHeader)\n }\n\n private async processProps(\n allProps: Record<string, unknown>,\n ctx: RouterContext,\n component: string,\n isInertia: boolean,\n ): Promise<{\n resolvedProps: Record<string, unknown>\n mergeProps: string[]\n prependProps: string[]\n deepMergeProps: string[]\n matchPropsOn: string[]\n deferredProps: Record<string, string[]>\n onceProps: Record<string, { prop: string; expiresAt?: number | null }>\n }> {\n const resolvedProps: Record<string, unknown> = {}\n const mergeProps: string[] = []\n const prependProps: string[] = []\n const deepMergeProps: string[] = []\n const matchPropsOn: string[] = []\n const deferredProps: Record<string, string[]> = {}\n const onceProps: Record<string, { prop: string; expiresAt?: number | null }> = {}\n\n const partialComponent = ctx.header('x-inertia-partial-component')\n const partialDataHeader = ctx.header('x-inertia-partial-data')\n const partialExceptHeader = ctx.header('x-inertia-partial-except')\n const resetHeader = ctx.header('x-inertia-reset')\n const isPartialReload = isInertia && partialComponent === component && partialDataHeader\n\n const requestedProps = partialDataHeader?.split(',').map((s) => s.trim()) ?? []\n const exceptProps = partialExceptHeader?.split(',').map((s) => s.trim()) ?? []\n const _resetProps = resetHeader?.split(',').map((s) => s.trim()) ?? []\n\n for (const [key, value] of Object.entries(allProps)) {\n // Handle always props — always resolve regardless of partial reload\n if (this.isAlwaysProp(value)) {\n resolvedProps[key] = await value.callback()\n continue\n }\n\n // Handle once props\n if (this.isOnceProp(value)) {\n if (isPartialReload && this.isRequested(key, requestedProps)) {\n resolvedProps[key] = await value.callback()\n } else if (!isPartialReload) {\n resolvedProps[key] = await value.callback()\n onceProps[key] = {\n prop: value.key ?? key,\n ...(value.expiresAt != null ? { expiresAt: value.expiresAt } : {}),\n }\n }\n continue\n }\n\n // Handle deferred props\n if (this.isDeferredProp(value)) {\n if (isPartialReload && this.isRequested(key, requestedProps)) {\n resolvedProps[key] = await value.callback()\n } else if (!isPartialReload) {\n deferredProps[value.group] ??= []\n deferredProps[value.group].push(key)\n }\n continue\n }\n\n // Handle merge props (append/prepend/deep)\n if (this.isMergeProp(value)) {\n if (isPartialReload && !this.isRequested(key, requestedProps)) {\n continue\n }\n\n switch (value.strategy) {\n case 'prepend':\n prependProps.push(key)\n break\n case 'deep':\n deepMergeProps.push(key)\n break\n default:\n mergeProps.push(key)\n break\n }\n\n if (value.matchOn) {\n matchPropsOn.push(`${key}:${value.matchOn}`)\n }\n\n resolvedProps[key] = await value.callback()\n continue\n }\n\n // Handle optional props\n if (this.isOptionalProp(value)) {\n if (isPartialReload && this.isRequested(key, requestedProps)) {\n resolvedProps[key] = await value.callback()\n }\n continue\n }\n\n // Regular props\n if (isPartialReload) {\n if (this.isRequested(key, requestedProps) && !this.isExcepted(key, exceptProps)) {\n resolvedProps[key] = value\n }\n } else {\n resolvedProps[key] = value\n }\n }\n\n return { resolvedProps, mergeProps, prependProps, deepMergeProps, matchPropsOn, deferredProps, onceProps }\n }\n\n /**\n * Check if a prop key is requested — supports dot-notation (e.g., `user.permissions`\n * matches the top-level `user` key).\n */\n private isRequested(key: string, requestedProps: string[]): boolean {\n return requestedProps.some((prop) => prop === key || prop.startsWith(`${key}.`))\n }\n\n private isExcepted(key: string, exceptProps: string[]): boolean {\n return exceptProps.some((prop) => prop === key || prop.startsWith(`${key}.`))\n }\n\n private isOptionalProp(value: unknown): value is InertiaOptionalProp {\n return typeof value === 'object' && value !== null && INERTIA_PROP_OPTIONAL in value\n }\n\n private isDeferredProp(value: unknown): value is InertiaDeferredProp {\n return typeof value === 'object' && value !== null && INERTIA_PROP_DEFERRED in value\n }\n\n private isMergeProp(value: unknown): value is InertiaMergeProp {\n return typeof value === 'object' && value !== null && INERTIA_PROP_MERGE in value\n }\n\n private isOnceProp(value: unknown): value is InertiaOnceProp {\n return typeof value === 'object' && value !== null && INERTIA_PROP_ONCE in value\n }\n\n private isAlwaysProp(value: unknown): value is InertiaAlwaysProp {\n return typeof value === 'object' && value !== null && INERTIA_PROP_ALWAYS in value\n }\n\n private serializeRoutes(routes: RegisteredRoute[]): SerializedRoutes {\n const serialized: SerializedRoutes = {}\n for (const route of routes) {\n if (route.name) {\n serialized[route.name] = {\n path: route.path,\n paramNames: route.paramNames,\n domainParamNames: route.domainParamNames,\n ...(route.domain ? { domain: route.domain } : {}),\n ...(route.localePaths?.length ? { localePaths: route.localePaths } : {}),\n }\n }\n }\n return serialized\n }\n\n private isSsrDisabled(pathname: string): boolean {\n const patterns = this.options.ssr?.disabled\n if (!patterns || patterns.length === 0) return false\n\n return patterns.some((pattern) => {\n const escaped = pattern.replace(/[.+?^${}()|[\\]\\\\]/g, '\\\\$&')\n const regex = new RegExp(`^/${escaped.replace(/\\*/g, '[^/]*')}$`)\n return regex.test(pathname)\n })\n }\n}\n","import { Transient, inject } from 'stratal/di'\nimport type { InertiaModuleOptions } from '../inertia.options'\nimport { INERTIA_TOKENS } from '../inertia.tokens'\nimport type { ViteManifest } from '../types'\n\nconst DEFAULT_ENTRY_CLIENT_PATH = 'src/inertia/app.tsx'\n\n@Transient()\nexport class ManifestService {\n private readonly manifest: ViteManifest | null\n private readonly entryClientPath: string\n\n constructor(\n @inject(INERTIA_TOKENS.Options) options: InertiaModuleOptions,\n ) {\n this.manifest = options.manifest ?? null\n this.entryClientPath = (options.entryClientPath ?? DEFAULT_ENTRY_CLIENT_PATH).replace(/^\\/+/, '')\n }\n\n private get isDev(): boolean {\n return this.manifest === null\n }\n\n getHeadTags(): string {\n if (this.isDev) {\n return '<link rel=\"stylesheet\" href=\"/__inertia/ssr-css\" data-ssr-css />'\n }\n\n const tags: string[] = []\n const seen = new Set<string>()\n for (const entry of Object.values(this.manifest!)) {\n if (entry.css) {\n for (const cssFile of entry.css) {\n if (seen.has(cssFile)) continue\n seen.add(cssFile)\n tags.push(`<link rel=\"stylesheet\" href=\"/${cssFile}\" />`)\n }\n }\n }\n\n return tags.join('\\n')\n }\n\n getScriptTags(): string {\n if (this.isDev) {\n return [\n '<script type=\"module\" src=\"/@vite/client\"></script>',\n `<script type=\"module\">\nimport { createHotContext } from \"/@vite/client\";\nconst hot = createHotContext(\"/__ssr_css\");\nhot.on(\"vite:afterUpdate\", () => {\n document.querySelectorAll(\"[data-ssr-css]\").forEach(el => el.remove());\n});\n</script>`,\n `<script type=\"module\" src=\"/${this.entryClientPath}\"></script>`,\n ].join('\\n')\n }\n\n const tags: string[] = []\n for (const entry of Object.values(this.manifest!)) {\n if (entry.isEntry) {\n tags.push(`<script type=\"module\" src=\"/${entry.file}\"></script>`)\n }\n }\n\n return tags.join('\\n')\n }\n}\n","import type { InertiaAppSSRResponse, Page } from '@inertiajs/core'\nimport { Transient, inject } from 'stratal/di'\nimport { LOGGER_TOKENS, type LoggerService } from 'stratal/logger'\nimport type { InertiaModuleOptions } from '../inertia.options'\nimport { INERTIA_TOKENS } from '../inertia.tokens'\n\ninterface LoadedSsrBundle {\n render(page: Page): Promise<InertiaAppSSRResponse>\n}\n\n@Transient()\nexport class SsrRendererService {\n private bundle: LoadedSsrBundle | null = null\n private loadPromise: Promise<void> | null = null\n\n constructor(\n @inject(INERTIA_TOKENS.Options) private readonly options: InertiaModuleOptions,\n @inject(LOGGER_TOKENS.LoggerService) private readonly logger: LoggerService\n ) { }\n\n async render(page: Page): Promise<InertiaAppSSRResponse> {\n if (!this.options.ssr) {\n return { head: [], body: '' }\n }\n\n await this.ensureBundle()\n\n if (!this.bundle) {\n return { head: [], body: '' }\n }\n\n return this.bundle.render(page)\n }\n\n private async ensureBundle(): Promise<void> {\n if (this.bundle) return\n\n this.loadPromise ??= this.loadBundle()\n\n try {\n await this.loadPromise\n } catch {\n // loadBundle already clears loadPromise on failure\n }\n }\n\n private async loadBundle(): Promise<void> {\n if (!this.options.ssr) return\n\n try {\n const mod = await this.options.ssr.bundle()\n const resolved = ('default' in mod ? mod.default : mod) as LoadedSsrBundle\n this.bundle = resolved\n } catch (error: unknown) {\n this.logger.warn('[stratal:inertia] Failed to load SSR bundle. Falling back to client-side rendering.', { error })\n this.loadPromise = null\n }\n }\n}\n","import type { Page } from '@inertiajs/core'\nimport { Transient, inject } from 'stratal/di'\nimport type { InertiaModuleOptions } from '../inertia.options'\nimport { INERTIA_TOKENS } from '../inertia.tokens'\nimport type { ManifestService } from './manifest.service'\n\n@Transient()\nexport class TemplateService {\n constructor(\n @inject(INERTIA_TOKENS.Options) private readonly options: InertiaModuleOptions,\n @inject(INERTIA_TOKENS.ManifestService) private readonly manifest: ManifestService,\n ) { }\n\n render(page: Page, ssrHead: string[], ssrBody: string): string {\n // When SSR body is present, Inertia's buildSSRBody already returns the\n // <script data-page=\"app\"> tag + <div id=\"app\" data-server-rendered=\"true\">.\n // Without SSR, we generate both elements ourselves for client-side hydration.\n const appHtml = ssrBody || this.buildClientOnlyBody(page)\n\n const headTags = ssrHead.length > 0 ? ssrHead.join('\\n') : ''\n const viteHead = this.manifest.getHeadTags()\n const viteScripts = this.manifest.getScriptTags()\n\n let html = this.options.rootView\n html = html.replace('@inertiaHead', headTags)\n html = html.replace('@inertia', appHtml)\n html = html.replace('@viteHead', viteHead)\n html = html.replace('@viteScripts', viteScripts)\n\n return html\n }\n\n private buildClientOnlyBody(page: Page): string {\n const json = JSON.stringify(page).replace(/\\//g, '\\\\/')\n return `<script data-page=\"app\" type=\"application/json\">${json}</script><div id=\"app\"></div>`\n }\n}\n","import { Scope } from 'stratal/di'\nimport { ApplicationError, type ApplicationErrorConstructor, type ExceptionHandler, type HttpExceptionContext } from 'stratal/errors'\nimport { I18N_TOKENS, type II18nService } from 'stratal/i18n'\nimport type { AsyncModuleOptions, DynamicModule, OnException, OnInitialize } from 'stratal/module'\nimport { Module } from 'stratal/module'\nimport { SchemaValidationError, type RouteConfigurable, type Router } from 'stratal/router'\nimport { augmentRouterContext } from './augment/router-context'\nimport { InertiaBuildCommand } from './commands/inertia-build.command'\nimport { InertiaDevCommand } from './commands/inertia-dev.command'\nimport { InertiaInstallCommand } from './commands/inertia-install.command'\nimport { InertiaTypesCommand } from './commands/inertia-types.command'\nimport type { InertiaModuleOptions } from './inertia.options'\nimport { INERTIA_TOKENS } from './inertia.tokens'\nimport { InertiaMiddleware } from './middleware/inertia.middleware'\nimport { InertiaService } from './services/inertia.service'\nimport { ManifestService } from './services/manifest.service'\nimport { SsrRendererService } from './services/ssr-renderer.service'\nimport { TemplateService } from './services/template.service'\n\n@Module({\n providers: [\n { provide: INERTIA_TOKENS.InertiaService, useClass: InertiaService, scope: Scope.Request },\n { provide: INERTIA_TOKENS.TemplateService, useClass: TemplateService },\n { provide: INERTIA_TOKENS.ManifestService, useClass: ManifestService },\n { provide: INERTIA_TOKENS.SsrRenderer, useClass: SsrRendererService, scope: Scope.Singleton },\n InertiaInstallCommand,\n InertiaTypesCommand,\n InertiaDevCommand,\n InertiaBuildCommand,\n ],\n})\nexport class InertiaModule implements RouteConfigurable, OnInitialize, OnException {\n static forRoot(options: InertiaModuleOptions): DynamicModule {\n return {\n module: InertiaModule,\n providers: [\n { provide: INERTIA_TOKENS.Options, useValue: options },\n ],\n }\n }\n\n static forRootAsync(options: AsyncModuleOptions<InertiaModuleOptions>): DynamicModule {\n return {\n module: InertiaModule,\n providers: [\n {\n provide: INERTIA_TOKENS.Options,\n useFactory: options.useFactory,\n inject: options.inject,\n },\n ],\n }\n }\n\n configureRoutes(router: Router): void {\n router.use(InertiaMiddleware)\n }\n\n onException(handler: ExceptionHandler): void {\n // Convert Zod validation errors to Inertia form errors\n handler.renderable(SchemaValidationError, (error, context) => {\n if (context.type !== 'http') return undefined\n\n if (this.isPrecognitionRequest(context)) {\n return this.handlePrecognitionValidationError(error, context)\n }\n\n if (!this.isInertiaRequest(context)) return undefined\n\n const issues = (error.metadata?.issues as { path: string; message: string }[]) ?? []\n const errors: Record<string, string> = {}\n for (const issue of issues) {\n errors[issue.path] = issue.message\n }\n\n context.ctx.flash('errors', errors)\n return this.redirectBack(context)\n })\n\n // Convert business ApplicationErrors to Inertia form-level errors\n handler.renderable(ApplicationError as unknown as ApplicationErrorConstructor, (error, context) => {\n if (context.type !== 'http') return undefined\n\n const i18n = context.ctx.getContainer().resolve<II18nService>(I18N_TOKENS.I18nService)\n const message = i18n.t(error.message as Parameters<II18nService['t']>[0], error.metadata as Record<string, string | number>)\n\n if (this.isPrecognitionRequest(context)) {\n return this.createPrecognitionErrorResponse({ _form: message })\n }\n\n if (!this.isInertiaRequest(context)) return undefined\n\n context.ctx.flash('errors', { _form: message } as const)\n return this.redirectBack(context)\n })\n }\n\n onInitialize(): void {\n augmentRouterContext((ctx) => {\n const requestContainer = ctx.getContainer()\n return requestContainer.resolve<InertiaService>(INERTIA_TOKENS.InertiaService)\n })\n }\n\n private isInertiaRequest(context: HttpExceptionContext): boolean {\n return context.ctx.header('x-inertia') === 'true'\n }\n\n private isPrecognitionRequest(context: HttpExceptionContext): boolean {\n return context.ctx.header('precognition') === 'true'\n }\n\n private handlePrecognitionValidationError(error: SchemaValidationError, context: HttpExceptionContext): Response {\n const issues = (error.metadata?.issues as { path: string; message: string }[]) ?? []\n let errors: Record<string, string> = {}\n for (const issue of issues) {\n errors[issue.path] = issue.message\n }\n\n // Filter to only requested fields if Precognition-Validate-Only is present\n const validateOnly = context.ctx.header('precognition-validate-only')\n if (validateOnly) {\n const fields = validateOnly.split(',').map(f => f.trim())\n const filtered: Record<string, string> = {}\n for (const field of fields) {\n if (errors[field]) {\n filtered[field] = errors[field]\n }\n }\n errors = filtered\n }\n\n // If after filtering there are no errors for the requested fields, treat as success\n if (Object.keys(errors).length === 0) {\n return new Response(null, {\n status: 204,\n headers: {\n 'Precognition': 'true',\n 'Precognition-Success': 'true',\n 'Vary': 'Precognition',\n },\n })\n }\n\n return this.createPrecognitionErrorResponse(errors)\n }\n\n private createPrecognitionErrorResponse(errors: Record<string, string>): Response {\n return new Response(JSON.stringify({ errors }), {\n status: 422,\n headers: {\n 'Content-Type': 'application/json',\n 'Precognition': 'true',\n 'Vary': 'Precognition',\n },\n })\n }\n\n private redirectBack(context: HttpExceptionContext): Response {\n const referer = context.ctx.header('referer')\n if (referer) {\n const parsed = new URL(referer)\n const url = parsed.search ? `${parsed.pathname}${parsed.search}` : parsed.pathname\n return context.ctx.redirect(url, 303)\n }\n return context.ctx.redirect('/', 303)\n }\n}\n","import { deleteCookie, getSignedCookie, setSignedCookie } from 'hono/cookie'\nimport type { CookieOptions } from 'hono/utils/cookie'\nimport type { RouterContext } from 'stratal/router'\nimport type { FlashStore } from './flash-store'\n\nexport interface CookieFlashStoreOptions {\n secret: string | BufferSource\n cookie?: string\n cookieOptions?: CookieOptions\n}\n\nexport class CookieFlashStore implements FlashStore {\n private readonly cookieName: string\n private readonly secret: string | BufferSource\n private readonly cookieOptions: CookieOptions\n\n constructor(options: CookieFlashStoreOptions) {\n this.secret = options.secret\n this.cookieName = options.cookie ?? 'stratal_flash'\n this.cookieOptions = {\n path: '/',\n httpOnly: true,\n sameSite: 'Lax',\n ...options.cookieOptions,\n }\n }\n\n async read(ctx: RouterContext): Promise<Record<string, unknown>> {\n const value = await getSignedCookie(ctx.c, this.secret, this.cookieName)\n if (!value) return {}\n\n try {\n return JSON.parse(atob(value)) as Record<string, unknown>\n } catch {\n return {}\n }\n }\n\n async write(ctx: RouterContext, data: Record<string, unknown>): Promise<void> {\n const encoded = btoa(JSON.stringify(data))\n await setSignedCookie(ctx.c, this.cookieName, encoded, this.secret, this.cookieOptions)\n }\n\n clear(ctx: RouterContext): Promise<void> {\n deleteCookie(ctx.c, this.cookieName, { path: this.cookieOptions.path })\n return Promise.resolve()\n }\n}\n","import type { RouteConfig } from 'stratal/router'\nimport { Delete, Get, Patch, Post, Put, Route } from 'stratal/router'\nimport { z } from 'stratal/validation'\n\n/**\n * Zod schema for the Inertia page JSON response (returned for X-Inertia XHR requests)\n */\nexport const inertiaPageSchema = z.object({\n component: z.string(),\n props: z.record(z.string(), z.unknown()),\n url: z.string(),\n version: z.string().nullable(),\n flash: z.record(z.string(), z.unknown()),\n rememberedState: z.record(z.string(), z.unknown()),\n mergeProps: z.array(z.string()).optional(),\n prependProps: z.array(z.string()).optional(),\n deepMergeProps: z.array(z.string()).optional(),\n matchPropsOn: z.array(z.string()).optional(),\n deferredProps: z.record(z.string(), z.array(z.string())).optional(),\n initialDeferredProps: z.record(z.string(), z.array(z.string())).optional(),\n onceProps: z.record(z.string(), z.object({ prop: z.string(), expiresAt: z.number().nullable().optional() })).optional(),\n sharedProps: z.array(z.string()).optional(),\n encryptHistory: z.boolean().optional(),\n clearHistory: z.boolean().optional(),\n preserveFragment: z.boolean().optional(),\n})\n\nexport type InertiaRouteConfig = Omit<RouteConfig, 'response' | 'statusCode' | 'hideFromDocs'> & {\n hideFromDocs?: boolean\n}\n\nconst inertiaResponse = {\n schema: inertiaPageSchema,\n description: 'Inertia page response',\n contentType: 'text/html',\n} as const\n\n/**\n * Builds a full RouteConfig from InertiaRouteConfig by applying inertia defaults.\n */\nfunction buildInertiaConfig(config: InertiaRouteConfig): Omit<RouteConfig, 'statusCode'> {\n const { hideFromDocs = true, ...rest } = config\n return { ...rest, response: inertiaResponse, hideFromDocs }\n}\n\n/**\n * Decorator for Inertia page routes using convention-based routing.\n *\n * Wraps `@Route()` with:\n * - Auto-applied Inertia page response schema\n * - `hideFromDocs: true` by default (overridable)\n *\n * **Cannot be mixed with HTTP method decorators** (`@Get`, `@Post`, `@InertiaGet`, etc.)\n * in the same controller.\n *\n * @example\n * ```typescript\n * @Controller('/notes')\n * export class NotesController implements IController {\n * @InertiaRoute({ query: z.object({ page: z.string().optional() }) })\n * async index(ctx: RouterContext) {\n * return ctx.inertia('notes/Index', { notes: [] })\n * }\n * }\n * ```\n */\nexport function InertiaRoute(config: InertiaRouteConfig = {}) {\n return Route(buildInertiaConfig(config))\n}\n\n/**\n * Registers a GET route for an Inertia page.\n *\n * Wraps `@Get()` with auto-applied Inertia page response schema\n * and `hideFromDocs: true` by default.\n *\n * @param path - Route path relative to the controller base path\n * @param config - Optional route configuration (query, params, tags, etc.)\n *\n * @example\n * ```typescript\n * @Controller('/notes')\n * export class NotesController {\n * @InertiaGet('/')\n * async index(ctx: RouterContext) {\n * return ctx.inertia('notes/Index', { notes: [] })\n * }\n *\n * @InertiaGet('/:id', { params: z.object({ id: z.string() }) })\n * async show(ctx: RouterContext) {\n * return ctx.inertia('notes/Show', { note })\n * }\n * }\n * ```\n */\nexport function InertiaGet(path: string, config: InertiaRouteConfig = {}) {\n return Get(path, buildInertiaConfig(config))\n}\n\n/**\n * Registers a POST route for an Inertia form submission.\n *\n * Wraps `@Post()` with auto-applied Inertia page response schema\n * and `hideFromDocs: true` by default.\n *\n * @param path - Route path relative to the controller base path\n * @param config - Optional route configuration (body, params, tags, etc.)\n */\nexport function InertiaPost(path: string, config: InertiaRouteConfig = {}) {\n return Post(path, buildInertiaConfig(config))\n}\n\n/**\n * Registers a PUT route for an Inertia form submission.\n *\n * Wraps `@Put()` with auto-applied Inertia page response schema\n * and `hideFromDocs: true` by default.\n *\n * @param path - Route path relative to the controller base path\n * @param config - Optional route configuration (body, params, tags, etc.)\n */\nexport function InertiaPut(path: string, config: InertiaRouteConfig = {}) {\n return Put(path, buildInertiaConfig(config))\n}\n\n/**\n * Registers a PATCH route for an Inertia form submission.\n *\n * Wraps `@Patch()` with auto-applied Inertia page response schema\n * and `hideFromDocs: true` by default.\n *\n * @param path - Route path relative to the controller base path\n * @param config - Optional route configuration (body, params, tags, etc.)\n */\nexport function InertiaPatch(path: string, config: InertiaRouteConfig = {}) {\n return Patch(path, buildInertiaConfig(config))\n}\n\n/**\n * Registers a DELETE route for an Inertia form submission.\n *\n * Wraps `@Delete()` with auto-applied Inertia page response schema\n * and `hideFromDocs: true` by default.\n *\n * @param path - Route path relative to the controller base path\n * @param config - Optional route configuration (params, tags, etc.)\n */\nexport function InertiaDelete(path: string, config: InertiaRouteConfig = {}) {\n return Delete(path, buildInertiaConfig(config))\n}\n","import { Transient } from 'stratal/di'\nimport type { Middleware, Next, RouterContext } from 'stratal/router'\n\n@Transient()\nexport class HandlePrecognitiveRequests implements Middleware {\n async handle(ctx: RouterContext, next: Next): Promise<void> {\n const isPrecognition = ctx.header('precognition') === 'true'\n ctx.c.set('precognition', isPrecognition)\n\n if (isPrecognition) {\n ctx.c.set('validationSuccessResponse', new Response(null, {\n status: 204,\n headers: {\n 'Precognition': 'true',\n 'Precognition-Success': 'true',\n 'Vary': 'Precognition',\n },\n }))\n }\n\n await next()\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,90 @@
1
+ /// <reference path="../global.d.ts" />
2
+ import { MessageKeys, MessageParams } from "stratal/i18n";
3
+ import { CurrentRoute, RouteMatcher, RouteName, RouteParams } from "stratal/router";
4
+
5
+ //#region src/react/use-i18n.d.ts
6
+ /**
7
+ * Hook that provides i18n translation capabilities in React components.
8
+ *
9
+ * Consumes `locale` and `translations` from Inertia shared props and returns
10
+ * a `t()` function that translates message keys with optional interpolation.
11
+ *
12
+ * Requires the `i18n` option to be set on `InertiaModule.forRoot()` to inject
13
+ * the shared props.
14
+ *
15
+ * @returns An object with:
16
+ * - `t` — Translation function accepting a message key and optional params
17
+ * - `locale` — The current locale string
18
+ *
19
+ * @example
20
+ * ```tsx
21
+ * import { useI18n } from '@stratal/inertia/react'
22
+ *
23
+ * export default function Header() {
24
+ * const { t, locale } = useI18n()
25
+ *
26
+ * return (
27
+ * <header>
28
+ * <h1>{t('common.title')}</h1>
29
+ * <p>{t('common.greeting', { name: 'World' })}</p>
30
+ * <span>Locale: {locale}</span>
31
+ * </header>
32
+ * )
33
+ * }
34
+ * ```
35
+ */
36
+ declare function useI18n(): {
37
+ t: (key: MessageKeys, params?: MessageParams) => string;
38
+ locale: string;
39
+ };
40
+ //#endregion
41
+ //#region src/react/use-route.d.ts
42
+ /**
43
+ * Hook that provides Ziggy-like route URL generation in React components.
44
+ *
45
+ * Consumes `routes` and the current-request snapshot (`route`) from Inertia
46
+ * shared props. Route names and params are strictly typed from
47
+ * `StratalRouteMap` (generated by `quarry route:types`).
48
+ *
49
+ * Requires the `routes` option to be set on `InertiaModule.forRoot()`.
50
+ *
51
+ * Sticky params — anything in `defaults` (set server-side via `Uri.defaults()`)
52
+ * and anything in the current route's extracted `params` (filtered to the
53
+ * target route's declared params) — are merged into every `route()` call.
54
+ * Explicit params always win.
55
+ *
56
+ * @returns
57
+ * - `route(name, params?)` — URL builder
58
+ * - `current()` / `current(name)` — matched route name (or wildcard match)
59
+ * - `params` — extracted params for the current request URL
60
+ *
61
+ * @example
62
+ * ```tsx
63
+ * import { useRoute } from '@stratal/inertia/react'
64
+ *
65
+ * export default function UserProfile({ user }) {
66
+ * const { route, current, currentRoute } = useRoute()
67
+ *
68
+ * return (
69
+ * <nav>
70
+ * <a href={route('users.index')}>All Users</a>
71
+ * <a href={route('users.show', { id: user.id })}>{user.name}</a>
72
+ * {current('users.*') && <span>On a users page</span>}
73
+ * {currentRoute.name === 'users.show' && <span>#{currentRoute.params.id}</span>}
74
+ * </nav>
75
+ * )
76
+ * }
77
+ * ```
78
+ */
79
+ declare function useRoute(): {
80
+ route: <N extends RouteName>(name: N, params?: RouteParams<N>) => string;
81
+ current: {
82
+ (): RouteName | null;
83
+ (name: RouteMatcher): boolean;
84
+ };
85
+ currentRoute: CurrentRoute;
86
+ params: Record<string, string>;
87
+ };
88
+ //#endregion
89
+ export { useI18n, useRoute };
90
+ //# sourceMappingURL=react.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"react.d.mts","names":[],"sources":["../src/react/use-i18n.ts","../src/react/use-route.ts"],"mappings":";;;;;;;;;;;;;ACkOA;;;;;;;;;;;;;;;;;;;;;iBDzKgB,OAAA,CAAA;WAcA,WAAA,EAAW,MAAA,GAAW,aAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBC2JtB,QAAA,CAAA;oBAKK,SAAA,EAAS,IAAA,EAAQ,CAAA,EAAC,MAAA,GAAW,WAAA,CAAY,CAAA;;QAOvC,SAAA;IAAA,OACG,YAAA;EAAA"}
package/dist/react.mjs ADDED
@@ -0,0 +1,222 @@
1
+ import { usePage } from "@inertiajs/react";
2
+ import { compile, createCoreContext, registerMessageCompiler, translate } from "@intlify/core-base";
3
+ import { useMemo } from "react";
4
+ //#region src/react/use-i18n.ts
5
+ registerMessageCompiler(compile);
6
+ /**
7
+ * Hook that provides i18n translation capabilities in React components.
8
+ *
9
+ * Consumes `locale` and `translations` from Inertia shared props and returns
10
+ * a `t()` function that translates message keys with optional interpolation.
11
+ *
12
+ * Requires the `i18n` option to be set on `InertiaModule.forRoot()` to inject
13
+ * the shared props.
14
+ *
15
+ * @returns An object with:
16
+ * - `t` — Translation function accepting a message key and optional params
17
+ * - `locale` — The current locale string
18
+ *
19
+ * @example
20
+ * ```tsx
21
+ * import { useI18n } from '@stratal/inertia/react'
22
+ *
23
+ * export default function Header() {
24
+ * const { t, locale } = useI18n()
25
+ *
26
+ * return (
27
+ * <header>
28
+ * <h1>{t('common.title')}</h1>
29
+ * <p>{t('common.greeting', { name: 'World' })}</p>
30
+ * <span>Locale: {locale}</span>
31
+ * </header>
32
+ * )
33
+ * }
34
+ * ```
35
+ */
36
+ function useI18n() {
37
+ const { locale, translations } = usePage().props;
38
+ const context = useMemo(() => createCoreContext({
39
+ locale,
40
+ messages: { [locale]: translations },
41
+ missingWarn: !import.meta.env.PROD,
42
+ fallbackWarn: !import.meta.env.PROD
43
+ }), [locale, translations]);
44
+ return {
45
+ t: useMemo(() => (key, params) => {
46
+ const result = params !== void 0 ? translate(context, key, params) : translate(context, key);
47
+ return typeof result === "string" ? result : key;
48
+ }, [context]),
49
+ locale
50
+ };
51
+ }
52
+ //#endregion
53
+ //#region src/react/use-route.ts
54
+ /**
55
+ * Apply a trailing-slash mode to a URL or path.
56
+ *
57
+ * Pure reimplementation of `applyTrailingSlash()` from `stratal/router` —
58
+ * mirrored here to keep the React bundle decoupled from server-only deps.
59
+ *
60
+ * - `'ignore'` — return as-is.
61
+ * - `'always'` — append `/` unless path is root or last segment is file-like (`.json`, etc.).
62
+ * - `'never'` — strip a single trailing `/` from the pathname (skip root).
63
+ *
64
+ * Preserves query string and hash. Handles relative paths and absolute URLs.
65
+ */
66
+ function applyTrailingSlash(url, mode) {
67
+ if (mode === "ignore") return url;
68
+ const isAbsolute = /^https?:\/\//i.test(url);
69
+ const parsed = isAbsolute ? new URL(url) : new URL(url, "http://placeholder.local");
70
+ const path = parsed.pathname;
71
+ if (path === "/") return url;
72
+ const hasTrailing = path.endsWith("/");
73
+ if (mode === "always" && !hasTrailing) {
74
+ if (path.slice(path.lastIndexOf("/") + 1).includes(".")) return url;
75
+ parsed.pathname = `${path}/`;
76
+ } else if (mode === "never" && hasTrailing) parsed.pathname = path.slice(0, -1);
77
+ else return url;
78
+ return isAbsolute ? parsed.toString() : `${parsed.pathname}${parsed.search}${parsed.hash}`;
79
+ }
80
+ /**
81
+ * Encode a path-param value while preserving forward slashes so catch-all
82
+ * params (`:slug{.+}`) round-trip cleanly. Mirrors the server-side
83
+ * `encodePathParam()` in `stratal/router`.
84
+ */
85
+ function encodePathParam(value) {
86
+ return value.split("/").map(encodeURIComponent).join("/");
87
+ }
88
+ /**
89
+ * Build a URL from a serialized route definition.
90
+ *
91
+ * Mirrors `buildRouteUrl()` from `stratal/router` (pure reimplementation to
92
+ * avoid pulling server-side dependencies into the browser bundle).
93
+ */
94
+ function buildUrl(route, name, params) {
95
+ const allParams = { ...params };
96
+ const consumedKeys = /* @__PURE__ */ new Set();
97
+ let url = route.path;
98
+ if (allParams.locale && route.localePaths?.length) {
99
+ url = `/${allParams.locale}${url === "/" ? "" : url}`;
100
+ consumedKeys.add("locale");
101
+ }
102
+ for (const paramName of route.paramNames) {
103
+ const value = allParams[paramName];
104
+ if (value === void 0) throw new Error(`Missing required parameter "${paramName}" for route "${name}" (path: ${route.path})`);
105
+ url = url.replace(new RegExp(`:${paramName}(\\{[^}]*\\})?`), encodePathParam(value));
106
+ consumedKeys.add(paramName);
107
+ }
108
+ let domain;
109
+ if (route.domain) {
110
+ domain = route.domain;
111
+ for (const domainParam of route.domainParamNames) {
112
+ const value = allParams[domainParam];
113
+ if (value === void 0) throw new Error(`Missing required parameter "${domainParam}" for route "${name}" (domain: ${route.domain})`);
114
+ domain = domain.replace(`{${domainParam}}`, encodeURIComponent(value));
115
+ consumedKeys.add(domainParam);
116
+ }
117
+ }
118
+ const queryEntries = Object.entries(allParams).filter(([key]) => !consumedKeys.has(key));
119
+ if (queryEntries.length > 0) {
120
+ const queryString = queryEntries.filter(([, v]) => Boolean(v)).map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join("&");
121
+ url = `${url}${queryString.length ? `?${queryString}` : ""}`;
122
+ }
123
+ if (domain) url = `https://${domain}${url}`;
124
+ return url;
125
+ }
126
+ /**
127
+ * Filter a param bag down to the keys the target route actually declares —
128
+ * so a `companyId` carried over from the current URL never leaks into the
129
+ * query string of an unrelated route.
130
+ */
131
+ function filterCarryover(carryover, route) {
132
+ const allowed = new Set([...route.paramNames, ...route.domainParamNames]);
133
+ if (route.localePaths?.length) allowed.add("locale");
134
+ if (allowed.size === 0) return {};
135
+ const filtered = {};
136
+ for (const [key, value] of Object.entries(carryover)) if (allowed.has(key)) filtered[key] = value;
137
+ return filtered;
138
+ }
139
+ /**
140
+ * Pure URL resolver. Mirrors what {@link useRoute}'s `route()` does, but
141
+ * without React — exposed for testing and for non-hook callers.
142
+ *
143
+ * Merges params in order (last wins): sticky `defaults`, current-route
144
+ * carryover (filtered to the target's declared params), explicit params.
145
+ */
146
+ function resolveUrl(name, explicitParams, routes, currentRoute, trailingSlash = "ignore") {
147
+ const target = routes[name];
148
+ if (!target) throw new Error(`Route "${name}" not found.`);
149
+ return applyTrailingSlash(buildUrl(target, name, {
150
+ ...currentRoute.defaults,
151
+ ...filterCarryover(currentRoute.params, target),
152
+ ...explicitParams
153
+ }), trailingSlash);
154
+ }
155
+ function matchCurrent(currentRoute, name) {
156
+ if (name === void 0) return currentRoute.name;
157
+ if (currentRoute.name === null) return false;
158
+ if (typeof name === "string" && name.endsWith(".*")) {
159
+ const prefix = name.slice(0, -1);
160
+ return currentRoute.name.startsWith(prefix);
161
+ }
162
+ return currentRoute.name === name;
163
+ }
164
+ /**
165
+ * Hook that provides Ziggy-like route URL generation in React components.
166
+ *
167
+ * Consumes `routes` and the current-request snapshot (`route`) from Inertia
168
+ * shared props. Route names and params are strictly typed from
169
+ * `StratalRouteMap` (generated by `quarry route:types`).
170
+ *
171
+ * Requires the `routes` option to be set on `InertiaModule.forRoot()`.
172
+ *
173
+ * Sticky params — anything in `defaults` (set server-side via `Uri.defaults()`)
174
+ * and anything in the current route's extracted `params` (filtered to the
175
+ * target route's declared params) — are merged into every `route()` call.
176
+ * Explicit params always win.
177
+ *
178
+ * @returns
179
+ * - `route(name, params?)` — URL builder
180
+ * - `current()` / `current(name)` — matched route name (or wildcard match)
181
+ * - `params` — extracted params for the current request URL
182
+ *
183
+ * @example
184
+ * ```tsx
185
+ * import { useRoute } from '@stratal/inertia/react'
186
+ *
187
+ * export default function UserProfile({ user }) {
188
+ * const { route, current, currentRoute } = useRoute()
189
+ *
190
+ * return (
191
+ * <nav>
192
+ * <a href={route('users.index')}>All Users</a>
193
+ * <a href={route('users.show', { id: user.id })}>{user.name}</a>
194
+ * {current('users.*') && <span>On a users page</span>}
195
+ * {currentRoute.name === 'users.show' && <span>#{currentRoute.params.id}</span>}
196
+ * </nav>
197
+ * )
198
+ * }
199
+ * ```
200
+ */
201
+ function useRoute() {
202
+ const { routes, trailingSlash = "ignore", route: currentRoute } = usePage().props;
203
+ return {
204
+ route: useMemo(() => (name, params) => resolveUrl(name, params, routes, currentRoute, trailingSlash), [
205
+ routes,
206
+ trailingSlash,
207
+ currentRoute
208
+ ]),
209
+ current: useMemo(() => {
210
+ function impl(name) {
211
+ return name === void 0 ? matchCurrent(currentRoute) : matchCurrent(currentRoute, name);
212
+ }
213
+ return impl;
214
+ }, [currentRoute]),
215
+ currentRoute,
216
+ params: currentRoute.params
217
+ };
218
+ }
219
+ //#endregion
220
+ export { useI18n, useRoute };
221
+
222
+ //# sourceMappingURL=react.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"react.mjs","names":[],"sources":["../src/react/use-i18n.ts","../src/react/use-route.ts"],"sourcesContent":["/**\n * React hook for using Stratal's i18n translations on the frontend.\n *\n * Reads `locale` and `translations` from Inertia shared props (injected by\n * the `i18n` option on {@link InertiaModuleOptions}) and provides a type-safe\n * `t()` function powered by `@intlify/core-base`.\n *\n * @module\n */\n\nimport type { PageProps } from '@inertiajs/core'\nimport { usePage } from '@inertiajs/react'\nimport { compile, createCoreContext, registerMessageCompiler, translate } from '@intlify/core-base'\nimport { useMemo } from 'react'\nimport type { MessageKeys, MessageParams } from 'stratal/i18n'\n\n// Register JIT message compiler from the SAME @intlify/core-base instance that provides\n// createCoreContext/translate. Importing setupI18nCompiler from stratal/i18n/utils can\n// resolve a different @intlify/core-base copy (duplicate modules in node_modules),\n// causing the compiler registration to be invisible to this module's createCoreContext.\nregisterMessageCompiler(compile)\n\ninterface I18nPageProps extends PageProps {\n locale: string\n translations: Record<string, string>\n}\n\n/**\n * Hook that provides i18n translation capabilities in React components.\n *\n * Consumes `locale` and `translations` from Inertia shared props and returns\n * a `t()` function that translates message keys with optional interpolation.\n *\n * Requires the `i18n` option to be set on `InertiaModule.forRoot()` to inject\n * the shared props.\n *\n * @returns An object with:\n * - `t` — Translation function accepting a message key and optional params\n * - `locale` — The current locale string\n *\n * @example\n * ```tsx\n * import { useI18n } from '@stratal/inertia/react'\n *\n * export default function Header() {\n * const { t, locale } = useI18n()\n *\n * return (\n * <header>\n * <h1>{t('common.title')}</h1>\n * <p>{t('common.greeting', { name: 'World' })}</p>\n * <span>Locale: {locale}</span>\n * </header>\n * )\n * }\n * ```\n */\nexport function useI18n() {\n const { locale, translations } = usePage<I18nPageProps>().props\n\n const context = useMemo(\n () => createCoreContext({\n locale,\n messages: { [locale]: translations },\n missingWarn: !import.meta.env.PROD,\n fallbackWarn: !import.meta.env.PROD,\n }),\n [locale, translations],\n )\n\n const t = useMemo(\n () => (key: MessageKeys, params?: MessageParams): string => {\n const result = params !== undefined\n ? translate(context, key, params)\n : translate(context, key)\n return typeof result === 'string' ? result : key\n },\n [context],\n )\n\n return { t, locale }\n}\n","/**\n * React hook for Ziggy-like client-side URL generation.\n *\n * Reads serialized routes and the current request's matched-route snapshot\n * (injected by the `routes` option on {@link InertiaModuleOptions}) and\n * provides a type-safe `route()` function that mirrors the server-side\n * `buildRouteUrl()`, plus `current()` and `params` for current-route\n * introspection.\n *\n * @module\n */\n\nimport type { PageProps } from '@inertiajs/core'\nimport { usePage } from '@inertiajs/react'\nimport { useMemo } from 'react'\nimport type { CurrentRoute, RouteMatcher, RouteName, RouteParams, SerializedRoute, SerializedRoutes, TrailingSlashMode } from 'stratal/router'\n\ninterface RoutesPageProps extends PageProps {\n routes: SerializedRoutes\n trailingSlash?: TrailingSlashMode\n route: CurrentRoute\n}\n\n/**\n * Apply a trailing-slash mode to a URL or path.\n *\n * Pure reimplementation of `applyTrailingSlash()` from `stratal/router` —\n * mirrored here to keep the React bundle decoupled from server-only deps.\n *\n * - `'ignore'` — return as-is.\n * - `'always'` — append `/` unless path is root or last segment is file-like (`.json`, etc.).\n * - `'never'` — strip a single trailing `/` from the pathname (skip root).\n *\n * Preserves query string and hash. Handles relative paths and absolute URLs.\n */\nexport function applyTrailingSlash(url: string, mode: TrailingSlashMode): string {\n if (mode === 'ignore') return url\n\n const isAbsolute = /^https?:\\/\\//i.test(url)\n const parsed = isAbsolute ? new URL(url) : new URL(url, 'http://placeholder.local')\n const path = parsed.pathname\n if (path === '/') return url\n const hasTrailing = path.endsWith('/')\n\n if (mode === 'always' && !hasTrailing) {\n const lastSegment = path.slice(path.lastIndexOf('/') + 1)\n if (lastSegment.includes('.')) return url\n parsed.pathname = `${path}/`\n } else if (mode === 'never' && hasTrailing) {\n parsed.pathname = path.slice(0, -1)\n } else {\n return url\n }\n\n return isAbsolute\n ? parsed.toString()\n : `${parsed.pathname}${parsed.search}${parsed.hash}`\n}\n\n/**\n * Encode a path-param value while preserving forward slashes so catch-all\n * params (`:slug{.+}`) round-trip cleanly. Mirrors the server-side\n * `encodePathParam()` in `stratal/router`.\n */\nfunction encodePathParam(value: string): string {\n return value.split('/').map(encodeURIComponent).join('/')\n}\n\n/**\n * Build a URL from a serialized route definition.\n *\n * Mirrors `buildRouteUrl()` from `stratal/router` (pure reimplementation to\n * avoid pulling server-side dependencies into the browser bundle).\n */\nfunction buildUrl(route: SerializedRoute, name: string, params?: Record<string, string>): string {\n const allParams = { ...params }\n const consumedKeys = new Set<string>()\n let url = route.path\n\n if (allParams.locale && route.localePaths?.length) {\n url = `/${allParams.locale}${url === '/' ? '' : url}`\n consumedKeys.add('locale')\n }\n\n for (const paramName of route.paramNames) {\n const value = allParams[paramName]\n if (value === undefined) {\n throw new Error(`Missing required parameter \"${paramName}\" for route \"${name}\" (path: ${route.path})`)\n }\n url = url.replace(\n new RegExp(`:${paramName}(\\\\{[^}]*\\\\})?`),\n encodePathParam(value),\n )\n consumedKeys.add(paramName)\n }\n\n let domain: string | undefined\n if (route.domain) {\n domain = route.domain\n for (const domainParam of route.domainParamNames) {\n const value = allParams[domainParam]\n if (value === undefined) {\n throw new Error(`Missing required parameter \"${domainParam}\" for route \"${name}\" (domain: ${route.domain})`)\n }\n domain = domain.replace(`{${domainParam}}`, encodeURIComponent(value))\n consumedKeys.add(domainParam)\n }\n }\n\n const queryEntries = Object.entries(allParams).filter(([key]) => !consumedKeys.has(key))\n if (queryEntries.length > 0) {\n const queryString = queryEntries\n .filter(([, v]) => Boolean(v))\n .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)\n .join('&')\n url = `${url}${queryString.length ? `?${queryString}` : ''}`\n }\n\n if (domain) {\n url = `https://${domain}${url}`\n }\n\n return url\n}\n\n/**\n * Filter a param bag down to the keys the target route actually declares —\n * so a `companyId` carried over from the current URL never leaks into the\n * query string of an unrelated route.\n */\nfunction filterCarryover(carryover: Record<string, string>, route: SerializedRoute): Record<string, string> {\n const allowed = new Set<string>([...route.paramNames, ...route.domainParamNames])\n if (route.localePaths?.length) allowed.add('locale')\n if (allowed.size === 0) return {}\n\n const filtered: Record<string, string> = {}\n for (const [key, value] of Object.entries(carryover)) {\n if (allowed.has(key)) filtered[key] = value\n }\n return filtered\n}\n\n/**\n * Pure URL resolver. Mirrors what {@link useRoute}'s `route()` does, but\n * without React — exposed for testing and for non-hook callers.\n *\n * Merges params in order (last wins): sticky `defaults`, current-route\n * carryover (filtered to the target's declared params), explicit params.\n */\nexport function resolveUrl<N extends RouteName>(\n name: N,\n explicitParams: RouteParams<N> | undefined,\n routes: SerializedRoutes,\n currentRoute: CurrentRoute,\n trailingSlash: TrailingSlashMode = 'ignore',\n): string {\n const target = routes[name]\n if (!target) {\n throw new Error(`Route \"${name}\" not found.`)\n }\n\n const merged = {\n ...currentRoute.defaults,\n ...filterCarryover(currentRoute.params, target),\n ...explicitParams,\n } as Record<string, string>\n\n return applyTrailingSlash(buildUrl(target, name, merged), trailingSlash)\n}\n\n/**\n * Pure overload signatures for {@link matchCurrent} / `useRoute().current()`.\n *\n * - No arg → matched route name (or `null`).\n * - With a name → `true`/`false`. Strict-typed: only real route names and\n * dotted wildcard prefixes (`'users.*'`) are accepted.\n */\nexport function matchCurrent(currentRoute: CurrentRoute): RouteName | null\nexport function matchCurrent(currentRoute: CurrentRoute, name: RouteMatcher): boolean\nexport function matchCurrent(currentRoute: CurrentRoute, name?: RouteMatcher): RouteName | null | boolean {\n if (name === undefined) return currentRoute.name\n if (currentRoute.name === null) return false\n if (typeof name === 'string' && name.endsWith('.*')) {\n const prefix = name.slice(0, -1)\n return currentRoute.name.startsWith(prefix)\n }\n return currentRoute.name === name\n}\n\n/**\n * Hook that provides Ziggy-like route URL generation in React components.\n *\n * Consumes `routes` and the current-request snapshot (`route`) from Inertia\n * shared props. Route names and params are strictly typed from\n * `StratalRouteMap` (generated by `quarry route:types`).\n *\n * Requires the `routes` option to be set on `InertiaModule.forRoot()`.\n *\n * Sticky params — anything in `defaults` (set server-side via `Uri.defaults()`)\n * and anything in the current route's extracted `params` (filtered to the\n * target route's declared params) — are merged into every `route()` call.\n * Explicit params always win.\n *\n * @returns\n * - `route(name, params?)` — URL builder\n * - `current()` / `current(name)` — matched route name (or wildcard match)\n * - `params` — extracted params for the current request URL\n *\n * @example\n * ```tsx\n * import { useRoute } from '@stratal/inertia/react'\n *\n * export default function UserProfile({ user }) {\n * const { route, current, currentRoute } = useRoute()\n *\n * return (\n * <nav>\n * <a href={route('users.index')}>All Users</a>\n * <a href={route('users.show', { id: user.id })}>{user.name}</a>\n * {current('users.*') && <span>On a users page</span>}\n * {currentRoute.name === 'users.show' && <span>#{currentRoute.params.id}</span>}\n * </nav>\n * )\n * }\n * ```\n */\nexport function useRoute() {\n const page = usePage<RoutesPageProps>()\n const { routes, trailingSlash = 'ignore', route: currentRoute } = page.props\n\n const route = useMemo(\n () => <N extends RouteName>(name: N, params?: RouteParams<N>): string =>\n resolveUrl(name, params, routes, currentRoute, trailingSlash),\n [routes, trailingSlash, currentRoute],\n )\n\n const current = useMemo(\n () => {\n function impl(): RouteName | null\n function impl(name: RouteMatcher): boolean\n function impl(name?: RouteMatcher): RouteName | null | boolean {\n return name === undefined ? matchCurrent(currentRoute) : matchCurrent(currentRoute, name)\n }\n return impl\n },\n [currentRoute],\n )\n\n return { route, current, currentRoute, params: currentRoute.params }\n}\n"],"mappings":"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"}
@@ -0,0 +1,36 @@
1
+ import { Page, Page as InertiaPage } from "@inertiajs/core";
2
+
3
+ //#region src/augment/test-response.d.ts
4
+ declare module '@stratal/testing' {
5
+ interface TestResponse {
6
+ /** Assert the response is an Inertia response. Optionally run a callback with the page object for custom assertions. */
7
+ assertInertia(callback?: (page: Page) => void): Promise<this>;
8
+ /** Assert the Inertia page component matches the expected name. */
9
+ assertInertiaComponent(component: string): Promise<this>;
10
+ /** Assert the Inertia page prop at the given dot-path equals the expected value. */
11
+ assertInertiaProp(path: string, expected: unknown): Promise<this>;
12
+ /** Assert the Inertia page prop at the given dot-path exists. */
13
+ assertInertiaPropExists(path: string): Promise<this>;
14
+ /** Assert the Inertia page prop at the given dot-path does not exist. */
15
+ assertInertiaPropMissing(path: string): Promise<this>;
16
+ /** Assert the Inertia page URL matches the expected value. */
17
+ assertInertiaUrl(url: string): Promise<this>;
18
+ /** Assert the Inertia page version matches the expected value. */
19
+ assertInertiaVersion(version: string | null): Promise<this>;
20
+ /** Assert the Inertia page flash data contains the given key with the expected value. */
21
+ assertInertiaFlash(key: string, value: unknown): Promise<this>;
22
+ /** Assert a prop is listed as deferred in the given group. */
23
+ assertInertiaDeferredProp(prop: string, group: string): Promise<this>;
24
+ /** Assert a prop is listed as a merge prop. */
25
+ assertInertiaMergeProp(prop: string): Promise<this>;
26
+ /** Assert a prop is listed as a shared prop. */
27
+ assertInertiaSharedProp(prop: string): Promise<this>;
28
+ /** Assert the response is a successful precognition response (204 with precognition headers). */
29
+ assertSuccessfulPrecognition(): this;
30
+ /** Assert the response is a precognition validation error (422 with precognition headers). Optionally assert specific errors. */
31
+ assertPrecognitionValidationErrors(errors?: Record<string, string>): Promise<this>;
32
+ }
33
+ }
34
+ //#endregion
35
+ export { type InertiaPage };
36
+ //# sourceMappingURL=testing.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"testing.d.mts","names":[],"sources":["../src/augment/test-response.ts"],"mappings":";;;;YAKY,YAAA;IAL+B;IAOvC,aAAA,CAAc,QAAA,IAAY,IAAA,EAAM,IAAA,YAAgB,OAAA;IAPT;IASvC,sBAAA,CAAuB,SAAA,WAAoB,OAAA;IAFK;IAIhD,iBAAA,CAAkB,IAAA,UAAc,QAAA,YAAoB,OAAA;IAAA;IAEpD,uBAAA,CAAwB,IAAA,WAAe,OAAA;IAEC;IAAxC,wBAAA,CAAyB,IAAA,WAAe,OAAA;IAIM;IAF9C,gBAAA,CAAiB,GAAA,WAAc,OAAA;IAMyB;IAJxD,oBAAA,CAAqB,OAAA,kBAAyB,OAAA;IAQP;IANvC,kBAAA,CAAmB,GAAA,UAAa,KAAA,YAAiB,OAAA;IAUoB;IARrE,yBAAA,CAA0B,IAAA,UAAc,KAAA,WAAgB,OAAA;IAQoB;IAN5E,sBAAA,CAAuB,IAAA,WAAe,OAAA;IAlBtC;IAoBA,uBAAA,CAAwB,IAAA,WAAe,OAAA;IApBb;IAsB1B,4BAAA;IAtBgD;IAwBhD,kCAAA,CAAmC,MAAA,GAAS,MAAA,mBAAyB,OAAA;EAAA;AAAA"}