@stratal/inertia 0.0.22 → 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/README.md +33 -1
- package/dist/build-seo-tags-DBsHKxX9.mjs +123 -0
- package/dist/build-seo-tags-DBsHKxX9.mjs.map +1 -0
- package/dist/{decorate-CzXVx7ZH.mjs → decorate-B7nr7eBl.mjs} +1 -1
- package/dist/generator/type-generator.worker.mjs +1 -1
- package/dist/generator/type-generator.worker.mjs.map +1 -1
- package/dist/index.d.mts +209 -78
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +274 -60
- package/dist/index.mjs.map +1 -1
- package/dist/quarry.d.mts +9 -0
- package/dist/quarry.d.mts.map +1 -1
- package/dist/quarry.mjs +56 -9
- package/dist/quarry.mjs.map +1 -1
- package/dist/react.d.mts +15 -3
- package/dist/react.d.mts.map +1 -1
- package/dist/react.mjs +21 -8
- package/dist/react.mjs.map +1 -1
- package/dist/seo-runtime.d.mts +1 -0
- package/dist/seo-runtime.mjs +56 -0
- package/dist/seo-runtime.mjs.map +1 -0
- package/dist/ssr.d.mts +65 -0
- package/dist/ssr.d.mts.map +1 -0
- package/dist/ssr.mjs +56 -0
- package/dist/ssr.mjs.map +1 -0
- package/dist/testing.d.mts +1 -1
- package/dist/testing.mjs.map +1 -1
- package/dist/{type-generator-bfo14BJI.mjs → type-generator-DFpha_Fp.mjs} +178 -28
- package/dist/type-generator-DFpha_Fp.mjs.map +1 -0
- package/dist/types-BhgXhWx6.d.mts +82 -0
- package/dist/types-BhgXhWx6.d.mts.map +1 -0
- package/dist/types-DzE1pdZs.d.mts +76 -0
- package/dist/types-DzE1pdZs.d.mts.map +1 -0
- package/dist/vite.d.mts.map +1 -1
- package/dist/vite.mjs +22 -2
- package/dist/vite.mjs.map +1 -1
- package/package.json +27 -18
- package/dist/type-generator-bfo14BJI.mjs.map +0 -1
package/dist/quarry.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"quarry.mjs","names":[],"sources":["../src/vite/create-client-vite-config.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/quarry.ts"],"sourcesContent":["import { existsSync, mkdirSync, writeFileSync } from 'node:fs'\nimport { dirname, join } from 'node:path'\n\nexport interface TempClientViteConfigOptions {\n cwd: string\n entry?: string\n outDir?: string\n}\n\n/**\n * Emits a standalone Vite config for building the Inertia browser bundle.\n *\n * This runs as a separate `vite build` invocation BEFORE the worker build so\n * the worker's `stratal:inertia-inject-manifest` plugin has a finished\n * `<outDir>/.vite/manifest.json` to read. `@cloudflare/vite-plugin` builds its\n * environments in parallel, which made a single-config build racy — splitting\n * the two phases removes the race entirely and keeps each build minimal.\n */\nexport function writeTempClientViteConfig(options: TempClientViteConfigOptions): string {\n const configDir = join(options.cwd, 'node_modules', '.stratal')\n const configPath = join(configDir, 'vite.client.config.mjs')\n mkdirSync(dirname(configPath), { recursive: true })\n\n const entry = (options.entry ?? 'src/inertia/app.tsx').replace(/\\\\/g, '/')\n const outDir = (options.outDir ?? 'dist/client').replace(/\\\\/g, '/')\n const hasUserConfig = existsSync(join(options.cwd, 'vite.config.ts'))\n const publicDir = join(options.cwd, 'src', 'inertia', 'public').replace(/\\\\/g, '/')\n\n const content = `\nimport { mergeConfig } from 'vite'\n\nconst baseConfig = {\n publicDir: '${publicDir}',\n build: {\n outDir: '${outDir}',\n manifest: true,\n emptyOutDir: true,\n rollupOptions: {\n input: { app: '${entry}' },\n },\n },\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(userConfig, baseConfig)`\n : 'export default baseConfig'\n }\n`\n\n writeFileSync(configPath, content, 'utf-8')\n return configPath\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 * Path (relative to `cwd`) to the Vite client manifest the worker bundle\n * should inline. Defaults to `dist/client/.vite/manifest.json`, matching\n * what `quarry inertia:build` emits in phase 1.\n */\n clientManifestPath?: 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 stratalArgs = options.clientManifestPath\n ? `{ clientManifestPath: ${JSON.stringify(options.clientManifestPath)} }`\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(${stratalArgs}),\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 { writeTempClientViteConfig } from '../vite/create-client-vite-config'\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 // Phase 1: standalone browser-bundle build. Runs without the Cloudflare\n // vite-plugin so it isn't subject to its parallel env orchestration. The\n // resulting `<clientOutDir>/.vite/manifest.json` is what the worker build\n // (phase 2) inlines into the worker entry via `stratal:inertia-inject-manifest`.\n const clientOutDir = join(outDir, 'client').replace(/\\\\/g, '/')\n const clientConfigPath = writeTempClientViteConfig({\n cwd,\n entry: entryPath,\n outDir: clientOutDir,\n })\n\n this.info('Building Inertia.js browser bundle...')\n const browserCode = await this.spawnVite(cwd, clientConfigPath, ['build'])\n if (browserCode !== 0) {\n this.fail('Browser bundle build failed.')\n return browserCode\n }\n this.success(`Browser bundle written to ${clientOutDir}/`)\n\n // Phase 2: worker build (Cloudflare vite-plugin). The injector plugin\n // reads the manifest produced in phase 1 and inlines it onto the worker\n // entry chunk.\n const configPath = writeTempViteConfig({\n cwd,\n outDir,\n clientManifestPath: join(clientOutDir, '.vite', 'manifest.json').replace(/\\\\/g, '/'),\n })\n\n this.info('Building Cloudflare worker bundle...')\n const workerCode = await this.spawnVite(cwd, configPath, ['build'])\n if (workerCode !== 0) {\n this.fail('Worker build failed.')\n return workerCode\n }\n this.success('Worker 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","import { Module } from 'stratal/module'\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'\n\n@Module({\n providers: [\n InertiaInstallCommand,\n InertiaTypesCommand,\n InertiaDevCommand,\n InertiaBuildCommand,\n ],\n})\nexport class InertiaQuarryModule {}\n\nexport { InertiaBuildCommand } from './commands/inertia-build.command'\nexport { InertiaDevCommand } from './commands/inertia-dev.command'\nexport { InertiaInstallCommand } from './commands/inertia-install.command'\nexport { InertiaTypesCommand } from './commands/inertia-types.command'\nexport { runTypeGeneration } from './generator/type-generator'\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"quarry.mjs","names":[],"sources":["../src/vite/create-client-vite-config.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/quarry.ts"],"sourcesContent":["import { existsSync, mkdirSync, writeFileSync } from 'node:fs'\nimport { dirname, join } from 'node:path'\n\nexport interface TempClientViteConfigOptions {\n cwd: string\n entry?: string\n outDir?: string\n}\n\n/**\n * Emits a standalone Vite config for building the Inertia browser bundle.\n *\n * This runs as a separate `vite build` invocation BEFORE the worker build so\n * the worker's `stratal:inertia-inject-manifest` plugin has a finished\n * `<outDir>/.vite/manifest.json` to read. `@cloudflare/vite-plugin` builds its\n * environments in parallel, which made a single-config build racy — splitting\n * the two phases removes the race entirely and keeps each build minimal.\n */\nexport function writeTempClientViteConfig(options: TempClientViteConfigOptions): string {\n const configDir = join(options.cwd, 'node_modules', '.stratal')\n const configPath = join(configDir, 'vite.client.config.mjs')\n mkdirSync(dirname(configPath), { recursive: true })\n\n const entry = (options.entry ?? 'src/inertia/app.tsx').replace(/\\\\/g, '/')\n const outDir = (options.outDir ?? 'dist/client').replace(/\\\\/g, '/')\n const hasUserConfig = existsSync(join(options.cwd, 'vite.config.ts'))\n const publicDir = join(options.cwd, 'src', 'inertia', 'public').replace(/\\\\/g, '/')\n\n const content = `\nimport { mergeConfig } from 'vite'\n\nconst baseConfig = {\n publicDir: '${publicDir}',\n build: {\n outDir: '${outDir}',\n manifest: true,\n emptyOutDir: true,\n rollupOptions: {\n input: { app: '${entry}' },\n },\n },\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(userConfig, baseConfig)`\n : 'export default baseConfig'\n }\n`\n\n writeFileSync(configPath, content, 'utf-8')\n return configPath\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 * Path (relative to `cwd`) to the Vite client manifest the worker bundle\n * should inline. Defaults to `dist/client/.vite/manifest.json`, matching\n * what `quarry inertia:build` emits in phase 1.\n */\n clientManifestPath?: 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 stratalArgs = options.clientManifestPath\n ? `{ clientManifestPath: ${JSON.stringify(options.clientManifestPath)} }`\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 publicDir: 'src/inertia/public',\n plugins: [\n cloudflare(${cloudflareArgs}),\n ...(inertiaPlugin ? [inertiaPlugin] : []),\n ...stratalInertia(${stratalArgs}),\n ],\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 { writeTempClientViteConfig } from '../vite/create-client-vite-config'\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 // Phase 1: standalone browser-bundle build. Runs without the Cloudflare\n // vite-plugin so it isn't subject to its parallel env orchestration. The\n // resulting `<clientOutDir>/.vite/manifest.json` is what the worker build\n // (phase 2) inlines into the worker entry via `stratal:inertia-inject-manifest`.\n const clientOutDir = join(outDir, 'client').replace(/\\\\/g, '/')\n const clientConfigPath = writeTempClientViteConfig({\n cwd,\n entry: entryPath,\n outDir: clientOutDir,\n })\n\n this.info('Building Inertia.js browser bundle...')\n const browserCode = await this.spawnVite(cwd, clientConfigPath, ['build'])\n if (browserCode !== 0) {\n this.fail('Browser bundle build failed.')\n return browserCode\n }\n this.success(`Browser bundle written to ${clientOutDir}/`)\n\n // Phase 2: worker build (Cloudflare vite-plugin). The injector plugin\n // reads the manifest produced in phase 1 and inlines it onto the worker\n // entry chunk.\n const configPath = writeTempViteConfig({\n cwd,\n outDir,\n clientManifestPath: join(clientOutDir, '.vite', 'manifest.json').replace(/\\\\/g, '/'),\n })\n\n this.info('Building Cloudflare worker bundle...')\n const workerCode = await this.spawnVite(cwd, configPath, ['build'])\n if (workerCode !== 0) {\n this.fail('Worker build failed.')\n return workerCode\n }\n this.success('Worker 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 type { SourceFile, SyntaxKind } from 'ts-morph'\nimport { runTypeGeneration } from '../generator/type-generator'\n\n/** Outcome of reconciling `src/app.module.ts` with the SSR-enabled InertiaModule. */\ntype AppModuleUpdate = 'created' | 'ssr-added' | 'unchanged' | 'unwired'\n/**\n * The subset of ts-morph's runtime `SyntaxKind` enum that `ensureSsrWiring` reads.\n * Declared structurally (via the enum-member literal types) so ts-morph stays a\n * type-only import here and is loaded lazily where it's actually used.\n */\ninterface SyntaxKinds {\n CallExpression: SyntaxKind.CallExpression\n ObjectLiteralExpression: SyntaxKind.ObjectLiteralExpression\n}\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 SSR_TSX = `import { createInertiaSsrApp } from '@stratal/inertia/ssr'\n\nexport const { render } = createInertiaSsrApp({\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(inertiaDir, 'ssr.tsx'), content: SSR_TSX, name: 'ssr.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 result = await this.updateAppModule(appModulePath)\n if (result === 'created') {\n this.success('Updated src/app.module.ts with InertiaModule')\n } else if (result === 'ssr-added') {\n this.success('Enabled streaming SSR in src/app.module.ts')\n } else if (result === 'unchanged') {\n this.info('InertiaModule (with SSR) already configured in app.module.ts')\n } else {\n this.warn('InertiaModule is configured but SSR could not be auto-wired.')\n this.info(\"Add `ssr: { bundle: () => import('./inertia/ssr') }` to your InertiaModule options\")\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<AppModuleUpdate> {\n const { Project, SyntaxKind } = await import('ts-morph')\n\n const project = new Project({ useInMemoryFileSystem: false })\n const sourceFile = project.addSourceFileAtPath(modulePath)\n\n // Already importing the package — an older install that predates streaming\n // SSR. Wire the existing InertiaModule config to the SSR bundle rather than\n // bailing (which would leave SSR silently disabled).\n const existingImport = sourceFile.getImportDeclaration((decl) =>\n decl.getModuleSpecifierValue() === '@stratal/inertia',\n )\n if (existingImport) {\n const result = this.ensureSsrWiring(sourceFile, SyntaxKind)\n if (result === 'ssr-added') await sourceFile.save()\n return result\n }\n\n // Fresh install: add the imports and an InertiaModule.forRoot wired for SSR.\n sourceFile.addImportDeclaration({\n defaultImport: 'rootView',\n moduleSpecifier: './inertia/root.html?raw',\n })\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 ssr: { bundle: () => import('./inertia/ssr') },\\n })`)\n }\n } else {\n // Add imports property\n objLiteral.addPropertyAssignment({\n name: 'imports',\n initializer: `[\\n InertiaModule.forRoot({\\n rootView,\\n ssr: { bundle: () => import('./inertia/ssr') },\\n }),\\n ]`,\n })\n }\n\n break\n }\n\n await sourceFile.save()\n return 'created'\n }\n\n /**\n * Ensure an existing `InertiaModule.forRoot({...})` call opts into the streaming\n * SSR bundle. Returns `ssr-added` when the option is inserted, `unchanged` when\n * one is already present, or `unwired` when no plain `forRoot({...})` object\n * literal is found (e.g. `forRootAsync`, or a config passed by reference) — in\n * which case the caller surfaces a manual instruction.\n */\n private ensureSsrWiring(sourceFile: SourceFile, syntaxKind: SyntaxKinds): AppModuleUpdate {\n const calls = sourceFile.getDescendantsOfKind(syntaxKind.CallExpression)\n for (const call of calls) {\n if (call.getExpression().getText() !== 'InertiaModule.forRoot') continue\n\n const objLiteral = call.getArguments()[0]?.asKind(syntaxKind.ObjectLiteralExpression)\n if (!objLiteral) continue\n\n if (objLiteral.getProperty('ssr')) return 'unchanged'\n\n objLiteral.addPropertyAssignment({\n name: 'ssr',\n initializer: `{ bundle: () => import('./inertia/ssr') }`,\n })\n return 'ssr-added'\n }\n return 'unwired'\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","import { Module } from 'stratal/module'\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'\n\n@Module({\n providers: [\n InertiaInstallCommand,\n InertiaTypesCommand,\n InertiaDevCommand,\n InertiaBuildCommand,\n ],\n})\nexport class InertiaQuarryModule {}\n\nexport { InertiaBuildCommand } from './commands/inertia-build.command'\nexport { InertiaDevCommand } from './commands/inertia-dev.command'\nexport { InertiaInstallCommand } from './commands/inertia-install.command'\nexport { InertiaTypesCommand } from './commands/inertia-types.command'\nexport { runTypeGeneration } from './generator/type-generator'\n"],"mappings":";;;;;;;;;;;;;;;;;;AAkBA,SAAgB,0BAA0B,SAA8C;CAEtF,MAAM,aAAa,KADD,KAAK,QAAQ,KAAK,gBAAgB,UACpB,GAAG,wBAAwB;CAC3D,UAAU,QAAQ,UAAU,GAAG,EAAE,WAAW,KAAK,CAAC;CAElD,MAAM,SAAS,QAAQ,SAAS,uBAAuB,QAAQ,OAAO,GAAG;CACzE,MAAM,UAAU,QAAQ,UAAU,eAAe,QAAQ,OAAO,GAAG;CACnE,MAAM,gBAAgB,WAAW,KAAK,QAAQ,KAAK,gBAAgB,CAAC;CA0BpE,cAAc,YAAY;;;;gBAzBR,KAAK,QAAQ,KAAK,OAAO,WAAW,QAAQ,EAAE,QAAQ,OAAO,GAMzD,EAAE;;eAEX,OAAO;;;;uBAIC,MAAM;;;;;EAK3B,gBACM,oCAAoC,KAAK,QAAQ,KAAK,gBAAgB,EAAE,QAAQ,OAAO,GAAG,EAAE;;sDAG5F,4BACH;GAGgC,OAAO;CAC1C,OAAO;AACT;;;ACtCA,SAAgB,oBAAoB,SAAwC;CAE1E,MAAM,aAAa,KADD,KAAK,QAAQ,KAAK,gBAAgB,UACpB,GAAG,iBAAiB;CACpD,UAAU,QAAQ,UAAU,GAAG,EAAE,WAAW,KAAK,CAAC;CAElD,MAAM,gBAAgB,WAAW,KAAK,QAAQ,KAAK,gBAAgB,CAAC;CAEpE,MAAM,eAAe,QAAQ,SACzB,mBAAmB,QAAQ,OAAO,KAAK,UAAU,QAAQ,OAAO,OAAO,SAAS,YAAY,OAC5F;CAEJ,MAAM,eAAe,QAAQ,SACzB,YAAY,QAAQ,OAAO,MAC3B;CA2CJ,cAAc,YAAY;;;;;;;;;;;;;;;iBAzCH,QAAQ,YAC3B,2BAA2B,KAAK,UAAU,QAAQ,SAAS,EAAE,QAC7D,GAqB0B;;wBAnBV,QAAQ,qBACxB,yBAAyB,KAAK,UAAU,QAAQ,kBAAkB,EAAE,MACpE,GAmB8B;;;MAG9B,aAAa;;IAEf,aAAa;;;EAGf,gBACM,oCAAoC,KAAK,QAAQ,KAAK,gBAAgB,EAAE,QAAQ,OAAO,GAAG,EAAE;;sDAG5F,4BACH;GAGgC,OAAO;CAC1C,OAAO;AACT;;;AClEA,IAAa,sBAAb,cAAyC,QAAQ;CAC/C,OAAO,UAAU;CACjB,OAAO,cAAc;CAErB,MAAM,SAAsC;EAC1C,MAAM,SAAS,KAAK,OAAO,QAAQ,KAAK;EACxC,MAAM,iBAAiB,KAAK,QAAQ,KAAK;EACzC,MAAM,MAAM,QAAQ,IAAI;EAExB,MAAM,YAAY;EAClB,IAAI,CAAC,WAAW,KAAK,KAAK,SAAS,CAAC,GAAG;GACrC,KAAK,KAAK,oEAAoE;GAC9E,OAAO;EACT;EAMA,MAAM,eAAe,KAAK,QAAQ,QAAQ,EAAE,QAAQ,OAAO,GAAG;EAC9D,MAAM,mBAAmB,0BAA0B;GACjD;GACA,OAAO;GACP,QAAQ;EACV,CAAC;EAED,KAAK,KAAK,uCAAuC;EACjD,MAAM,cAAc,MAAM,KAAK,UAAU,KAAK,kBAAkB,CAAC,OAAO,CAAC;EACzE,IAAI,gBAAgB,GAAG;GACrB,KAAK,KAAK,8BAA8B;GACxC,OAAO;EACT;EACA,KAAK,QAAQ,6BAA6B,aAAa,EAAE;EAKzD,MAAM,aAAa,oBAAoB;GACrC;GACA;GACA,oBAAoB,KAAK,cAAc,SAAS,eAAe,EAAE,QAAQ,OAAO,GAAG;EACrF,CAAC;EAED,KAAK,KAAK,sCAAsC;EAChD,MAAM,aAAa,MAAM,KAAK,UAAU,KAAK,YAAY,CAAC,OAAO,CAAC;EAClE,IAAI,eAAe,GAAG;GACpB,KAAK,KAAK,sBAAsB;GAChC,OAAO;EACT;EACA,KAAK,QAAQ,wBAAwB;EAErC,IAAI,gBAAgB;GAClB,KAAK,KAAK,wBAAwB;GAClC,MAAM,UAAU,MAAM,KAAK,UAAU,KAAK,YAAY,CAAC,SAAS,OAAO,CAAC;GACxE,IAAI,YAAY,GAAG;IACjB,KAAK,KAAK,mBAAmB;IAC7B,OAAO;GACT;GACA,KAAK,QAAQ,qBAAqB;EACpC;EAEA,KAAK,QAAQ,aAAa,OAAO,EAAE;EACnC,KAAK,KAAK,kCAAkC;EAC5C,OAAO;CACT;CAEA,UAAkB,KAAa,YAAoB,MAAiC;EAClF,OAAO,IAAI,SAAS,YAAY;GAC9B,MAAM,QAAQ,MAAM,OAAO;IAAC;IAAQ;IAAY;IAAY,GAAG;GAAI,GAAG;IACpE;IACA,OAAO;IACP,OAAO;GACT,CAAC;GAED,MAAM,GAAG,UAAU,QAAQ;IACzB,KAAK,KAAK,uBAAuB,IAAI,SAAS;IAC9C,QAAQ,CAAC;GACX,CAAC;GAED,MAAM,GAAG,UAAU,SAAS;IAC1B,QAAQ,QAAQ,CAAC;GACnB,CAAC;EACH,CAAC;CACH;AACF;;;ACrFA,IAAa,oBAAb,cAAuC,QAAQ;CAC7C,OAAO,UAAU;CACjB,OAAO,cAAc;CAErB,MAAM,SAAsC;EAC1C,MAAM,OAAO,KAAK,OAAO,MAAM;EAC/B,MAAM,OAAO,KAAK,QAAQ,MAAM;EAChC,MAAM,YAAY,KAAK,OAAO,YAAY;EAC1C,MAAM,MAAM,QAAQ,IAAI;EAGxB,IAAI,CAAC,WAAW,KAAK,KAAK,qBAAS,CAAC,GAAG;GACrC,KAAK,KAAK,oEAAoE;GAC9E,OAAO;EACT;EAEA,MAAM,aAAa,oBAAoB;GACrC;GACA,QAAQ;IAAE;IAAM;GAAK;GACrB;EACF,CAAC;EAED,KAAK,KAAK,6BAA6B;EAEvC,MAAM,OAAO;GAAC;GAAQ;GAAO;GAAY;EAAU;EACnD,IAAI,MAAM,KAAK,KAAK,QAAQ;EAE5B,OAAO,IAAI,SAAiB,YAAY;GACtC,MAAM,QAAQ,MAAM,OAAO,MAAM;IAC/B;IACA,OAAO;IACP,OAAO;GACT,CAAC;GAED,MAAM,GAAG,UAAU,QAAQ;IACzB,KAAK,KAAK,+BAA+B,IAAI,SAAS;IACtD,QAAQ,CAAC;GACX,CAAC;GAED,MAAM,GAAG,UAAU,SAAS;IAC1B,QAAQ,QAAQ,CAAC;GACnB,CAAC;EACH,CAAC;CACH;AACF;;;AChCA,MAAM,YAAY;;;;;;;;;;;;;AAclB,MAAM,UAAU;;;;;;;;;;AAWhB,MAAM,UAAU;;;;;;;;;;AAWhB,MAAM,WAAW;;;;;;;;AASjB,IAAa,wBAAb,cAA2C,QAAQ;CACjD,OAAO,UAAU;CACjB,OAAO,cAAc;CAErB,MAAM,SAAsC;EAC1C,MAAM,WAAW,KAAK,QAAQ,WAAW;EACzC,MAAM,MAAM,QAAQ,IAAI;EACxB,MAAM,aAAa,KAAK,KAAK,OAAO,SAAS;EAC7C,MAAM,WAAW,KAAK,YAAY,OAAO;EAGzC,KAAK,KAAK,oCAAoC;EAC9C,UAAU,UAAU,EAAE,WAAW,KAAK,CAAC;EAEvC,MAAM,YAAY,KAAK,YAAY,QAAQ;EAC3C,UAAU,WAAW,EAAE,WAAW,KAAK,CAAC;EACxC,MAAM,cAAc,KAAK,WAAW,UAAU;EAC9C,IAAI,CAAC,WAAW,WAAW,GACzB,cAAc,aAAa,IAAI,OAAO;EAExC,KAAK,QAAQ,6BAA6B;EAG1C,MAAM,QAAQ;GACZ;IAAE,MAAM,KAAK,YAAY,WAAW;IAAG,SAAS;IAAW,MAAM;GAAY;GAC7E;IAAE,MAAM,KAAK,YAAY,SAAS;IAAG,SAAS;IAAS,MAAM;GAAU;GACvE;IAAE,MAAM,KAAK,YAAY,SAAS;IAAG,SAAS;IAAS,MAAM;GAAU;GACvE;IAAE,MAAM,KAAK,UAAU,UAAU;IAAG,SAAS;IAAU,MAAM;GAAiB;EAChF;EAEA,KAAK,MAAM,QAAQ,OACjB,IAAI,WAAW,KAAK,IAAI,GACtB,KAAK,KAAK,YAAY,KAAK,KAAK,kBAAkB;OAC7C;GACL,cAAc,KAAK,MAAM,KAAK,SAAS,OAAO;GAC9C,KAAK,QAAQ,uBAAuB,KAAK,MAAM;EACjD;EAIF,MAAM,gBAAgB,KAAK,KAAK,OAAO,eAAe;EACtD,IAAI,WAAW,aAAa,GAAG;GAC7B,KAAK,KAAK,+BAA+B;GACzC,IAAI;IACF,MAAM,SAAS,MAAM,KAAK,gBAAgB,aAAa;IACvD,IAAI,WAAW,WACb,KAAK,QAAQ,8CAA8C;SACtD,IAAI,WAAW,aACpB,KAAK,QAAQ,4CAA4C;SACpD,IAAI,WAAW,aACpB,KAAK,KAAK,8DAA8D;SACnE;KACL,KAAK,KAAK,8DAA8D;KACxE,KAAK,KAAK,oFAAoF;IAChG;GACF,SAAS,KAAK;IACZ,KAAK,KAAK,wCAAyC,IAAc,SAAS;IAC1E,KAAK,KAAK,oEAAoE;GAChF;EACF,OACE,KAAK,KAAK,sEAAsE;EAIlF,IAAI;GACF,MAAM,EAAE,YAAY,cAAc,MAAM,kBAAkB,GAAG;GAC7D,MAAM,UAAU,SAAS,KAAK,UAAU;GACxC,KAAK,QAAQ,aAAa,QAAQ,IAAI,UAAU,OAAO,cAAc,IAAI,MAAM,GAAG,EAAE;EACtF,QAAQ;GACN,KAAK,KAAK,mFAAmF;EAC/F;EAEA,IAAI,CAAC,UAAU;GACb,KAAK,QAAQ;GACb,KAAK,KAAK,qCAAqC;GAC/C,KAAK,KAAK,iFAAiF;GAC3F,KAAK,KAAK,6EAA6E;EACzF;EAEA,KAAK,QAAQ;EACb,KAAK,QAAQ,kCAAkC;EAC/C,KAAK,KAAK,kDAAkD;EAE5D,OAAO;CACT;CAEA,MAAc,gBAAgB,YAA8C;EAC1E,MAAM,EAAE,SAAS,eAAe,MAAM,OAAO;EAG7C,MAAM,aAAa,IADC,QAAQ,EAAE,uBAAuB,MAAM,CAClC,EAAE,oBAAoB,UAAU;EAQzD,IAHuB,WAAW,sBAAsB,SACtD,KAAK,wBAAwB,MAAM,kBAEpB,GAAG;GAClB,MAAM,SAAS,KAAK,gBAAgB,YAAY,UAAU;GAC1D,IAAI,WAAW,aAAa,MAAM,WAAW,KAAK;GAClD,OAAO;EACT;EAGA,WAAW,qBAAqB;GAC9B,eAAe;GACf,iBAAiB;EACnB,CAAC;EACD,WAAW,qBAAqB;GAC9B,cAAc,CAAC,eAAe;GAC9B,iBAAiB;EACnB,CAAC;EAGD,MAAM,UAAU,WAAW,WAAW;EACtC,KAAK,MAAM,OAAO,SAAS;GACzB,MAAM,kBAAkB,IAAI,aAAa,QAAQ;GACjD,IAAI,CAAC,iBAAiB;GAEtB,MAAM,OAAO,gBAAgB,aAAa;GAC1C,IAAI,KAAK,WAAW,GAAG;GAEvB,MAAM,aAAa,KAAK,GAAG,OAAO,WAAW,uBAAuB;GACpE,IAAI,CAAC,YAAY;GAEjB,MAAM,cAAc,WAAW,YAAY,SAAS;GACpD,IAAI,aAAa;IAGf,MAAM,gBADc,YAAY,OAAO,WAAW,kBAAkB,GAAG,eAAe,IACpD,OAAO,WAAW,sBAAsB;IAC1E,IAAI,cACF,aAAa,WAAW,mGAAmG;GAE/H,OAEE,WAAW,sBAAsB;IAC/B,MAAM;IACN,aAAa;GACf,CAAC;GAGH;EACF;EAEA,MAAM,WAAW,KAAK;EACtB,OAAO;CACT;;;;;;;;CASA,gBAAwB,YAAwB,YAA0C;EACxF,MAAM,QAAQ,WAAW,qBAAqB,WAAW,cAAc;EACvE,KAAK,MAAM,QAAQ,OAAO;GACxB,IAAI,KAAK,cAAc,EAAE,QAAQ,MAAM,yBAAyB;GAEhE,MAAM,aAAa,KAAK,aAAa,EAAE,IAAI,OAAO,WAAW,uBAAuB;GACpF,IAAI,CAAC,YAAY;GAEjB,IAAI,WAAW,YAAY,KAAK,GAAG,OAAO;GAE1C,WAAW,sBAAsB;IAC/B,MAAM;IACN,aAAa;GACf,CAAC;GACD,OAAO;EACT;EACA,OAAO;CACT;AACF;;;ACvOA,IAAa,sBAAb,cAAyC,QAAQ;CAC/C,OAAO,UAAU;CACjB,OAAO,cAAc;CAErB,MAAM,SAAsC;EAC1C,MAAM,MAAM,QAAQ,IAAI;EAGxB,IAAI,CAAC,WAFY,aAAa,GAEP,CAAC,GAAG;GACzB,KAAK,KAAK,mEAAmE;GAC7E,OAAO;EACT;EAGA,IAAI,CAAC,MADgB,KAAK,SAAS,GAAG,GACzB,OAAO;EAEpB,IAAI,KAAK,QAAQ,OAAO,GAAG;GACzB,KAAK,KAAK,yBAAyB;GACnC,MAAM,KAAK,gBAAgB,GAAG;EAChC;EAEA,OAAO;CACT;CAEA,MAAc,SAAS,KAA+B;EACpD,IAAI;GACF,MAAM,EAAE,YAAY,cAAc,MAAM,kBAAkB,GAAG;GAC7D,MAAM,UAAU,SAAS,KAAK,UAAU;GACxC,KAAK,QAAQ,aAAa,QAAQ,IAAI,UAAU,OAAO,cAAc,IAAI,MAAM,GAAG,EAAE;GACpF,OAAO;EACT,SAAS,KAAK;GACZ,KAAK,KAAK,2BAA4B,IAAc,SAAS;GAC7D,OAAO;EACT;CACF;CAEA,MAAc,gBAAgB,KAA4B;EACxD,MAAM,SAAS,KAAK,KAAK,KAAK;EAE9B,IAAI;GACF,MAAM,UAAU,MAAM,QAAQ,EAAE,WAAW,KAAK,CAAC;GACjD,WAAW,MAAM,SAAS,SACxB,IAAI,MAAM,YAAY,cAAc,KAAK,MAAM,QAAQ,GAAG;IACxD,KAAK,KAAK,oBAAoB,MAAM,UAAU;IAC9C,MAAM,KAAK,SAAS,GAAG;GACzB;EAEJ,SAAS,KAAK;GACZ,KAAK,KAAK,iBAAkB,IAAc,SAAS;EACrD;CACF;AACF;;;AC3CO,IAAA,sBAAA,MAAM,oBAAoB,CAAC;kCARjC,OAAO,EACN,WAAW;CACT;CACA;CACA;CACA;AACF,EACF,CAAC,CAAA,GAAA,mBAAA"}
|
package/dist/react.d.mts
CHANGED
|
@@ -1,10 +1,22 @@
|
|
|
1
1
|
/// <reference path="../global.d.ts" />
|
|
2
|
+
import { t as SeoData } from "./types-DzE1pdZs.mjs";
|
|
3
|
+
import { g as InertiaTranslationKeys } from "./types-BhgXhWx6.mjs";
|
|
2
4
|
import { CurrentRoute, RouteMatcher, RouteName, RouteParams } from "stratal/router";
|
|
3
|
-
import {
|
|
5
|
+
import { MessageParams } from "stratal/i18n";
|
|
4
6
|
|
|
7
|
+
//#region src/react/seo.d.ts
|
|
8
|
+
/**
|
|
9
|
+
* Returns the resolved SEO data shared by the backend for the current page.
|
|
10
|
+
*
|
|
11
|
+
* The document head is kept in sync automatically (server injection on the
|
|
12
|
+
* initial paint + the auto-injected client runtime on navigation); use this
|
|
13
|
+
* hook only when you want to read the metadata inside a component.
|
|
14
|
+
*/
|
|
15
|
+
declare function useSeo(): SeoData;
|
|
16
|
+
//#endregion
|
|
5
17
|
//#region src/react/use-i18n.d.ts
|
|
6
18
|
declare function useI18n(): {
|
|
7
|
-
t: (key:
|
|
19
|
+
t: (key: InertiaTranslationKeys, params?: MessageParams) => string;
|
|
8
20
|
locale: string;
|
|
9
21
|
};
|
|
10
22
|
//#endregion
|
|
@@ -56,5 +68,5 @@ declare function useRoute(): {
|
|
|
56
68
|
params: Record<string, string>;
|
|
57
69
|
};
|
|
58
70
|
//#endregion
|
|
59
|
-
export { useI18n, useRoute };
|
|
71
|
+
export { useI18n, useRoute, useSeo };
|
|
60
72
|
//# sourceMappingURL=react.d.mts.map
|
package/dist/react.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"react.d.mts","names":[],"sources":["../src/react/use-i18n.ts","../src/react/use-route.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"react.d.mts","names":[],"sources":["../src/react/seo.ts","../src/react/use-i18n.ts","../src/react/use-route.ts"],"mappings":";;;;;;;;;;;AAeA;;iBAAgB,MAAA,IAAU,OAAO;;;iBCHjB,OAAA;WAUC,sBAAA,EAAsB,MAAA,GAAW,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBCmN/C,QAAA;oBAKK,SAAA,EAAS,IAAA,EAAQ,CAAA,EAAC,MAAA,GAAW,WAAA,CAAY,CAAA;;QAOvC,SAAA;IAAA,OACG,YAAA;EAAA"}
|
package/dist/react.mjs
CHANGED
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
import { usePage } from "@inertiajs/react";
|
|
2
2
|
import IntlMessageFormat from "intl-messageformat";
|
|
3
3
|
import { useMemo } from "react";
|
|
4
|
+
//#region src/react/seo.ts
|
|
5
|
+
/**
|
|
6
|
+
* Returns the resolved SEO data shared by the backend for the current page.
|
|
7
|
+
*
|
|
8
|
+
* The document head is kept in sync automatically (server injection on the
|
|
9
|
+
* initial paint + the auto-injected client runtime on navigation); use this
|
|
10
|
+
* hook only when you want to read the metadata inside a component.
|
|
11
|
+
*/
|
|
12
|
+
function useSeo() {
|
|
13
|
+
return usePage().props.seo ?? {};
|
|
14
|
+
}
|
|
15
|
+
//#endregion
|
|
4
16
|
//#region src/react/use-i18n.ts
|
|
5
17
|
function useI18n() {
|
|
6
18
|
const { locale, translations } = usePage().props;
|
|
@@ -59,12 +71,12 @@ function encodePathParam(value) {
|
|
|
59
71
|
* Mirrors `buildRouteUrl()` from `stratal/router` (pure reimplementation to
|
|
60
72
|
* avoid pulling server-side dependencies into the browser bundle).
|
|
61
73
|
*/
|
|
62
|
-
function buildUrl(route, name, params) {
|
|
74
|
+
function buildUrl(route, name, params, localeConfig) {
|
|
63
75
|
const allParams = { ...params };
|
|
64
76
|
const consumedKeys = /* @__PURE__ */ new Set();
|
|
65
77
|
let url = route.path;
|
|
66
78
|
if (allParams.locale && route.localePaths?.length) {
|
|
67
|
-
url = `/${allParams.locale}${url === "/" ? "" : url}`;
|
|
79
|
+
if (!localeConfig || localeConfig.prefixDefaultLocale === true || allParams.locale !== localeConfig.defaultLocale) url = `/${allParams.locale}${url === "/" ? "" : url}`;
|
|
68
80
|
consumedKeys.add("locale");
|
|
69
81
|
}
|
|
70
82
|
for (const paramName of route.paramNames) {
|
|
@@ -111,14 +123,14 @@ function filterCarryover(carryover, route) {
|
|
|
111
123
|
* Merges params in order (last wins): sticky `defaults`, current-route
|
|
112
124
|
* carryover (filtered to the target's declared params), explicit params.
|
|
113
125
|
*/
|
|
114
|
-
function resolveUrl(name, explicitParams, routes, currentRoute, trailingSlash = "ignore") {
|
|
126
|
+
function resolveUrl(name, explicitParams, routes, currentRoute, trailingSlash = "ignore", localeConfig) {
|
|
115
127
|
const target = routes[name];
|
|
116
128
|
if (!target) throw new Error(`Route "${name}" not found.`);
|
|
117
129
|
return applyTrailingSlash(buildUrl(target, name, {
|
|
118
130
|
...currentRoute.defaults,
|
|
119
131
|
...filterCarryover(currentRoute.params, target),
|
|
120
132
|
...explicitParams
|
|
121
|
-
}), trailingSlash);
|
|
133
|
+
}, localeConfig), trailingSlash);
|
|
122
134
|
}
|
|
123
135
|
function matchCurrent(currentRoute, name) {
|
|
124
136
|
if (name === void 0) return currentRoute.name;
|
|
@@ -167,12 +179,13 @@ function matchCurrent(currentRoute, name) {
|
|
|
167
179
|
* ```
|
|
168
180
|
*/
|
|
169
181
|
function useRoute() {
|
|
170
|
-
const { routes, trailingSlash = "ignore", route: currentRoute } = usePage().props;
|
|
182
|
+
const { routes, trailingSlash = "ignore", route: currentRoute, localeConfig } = usePage().props;
|
|
171
183
|
return {
|
|
172
|
-
route: useMemo(() => (name, params) => resolveUrl(name, params, routes, currentRoute, trailingSlash), [
|
|
184
|
+
route: useMemo(() => (name, params) => resolveUrl(name, params, routes, currentRoute, trailingSlash, localeConfig), [
|
|
173
185
|
routes,
|
|
174
186
|
trailingSlash,
|
|
175
|
-
currentRoute
|
|
187
|
+
currentRoute,
|
|
188
|
+
localeConfig
|
|
176
189
|
]),
|
|
177
190
|
current: useMemo(() => {
|
|
178
191
|
function impl(name) {
|
|
@@ -185,6 +198,6 @@ function useRoute() {
|
|
|
185
198
|
};
|
|
186
199
|
}
|
|
187
200
|
//#endregion
|
|
188
|
-
export { useI18n, useRoute };
|
|
201
|
+
export { useI18n, useRoute, useSeo };
|
|
189
202
|
|
|
190
203
|
//# sourceMappingURL=react.mjs.map
|
package/dist/react.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"react.mjs","names":[],"sources":["../src/react/use-i18n.ts","../src/react/use-route.ts"],"sourcesContent":["import type { PageProps } from '@inertiajs/core'\nimport { usePage } from '@inertiajs/react'\nimport IntlMessageFormat from 'intl-messageformat'\nimport { useMemo } from 'react'\nimport type { MessageKeys, MessageParams } from 'stratal/i18n'\n\ninterface I18nPageProps extends PageProps {\n locale: string\n translations: Record<string, string>\n}\n\nexport function useI18n() {\n const { locale, translations } = usePage<I18nPageProps>().props\n\n const t = useMemo(() => {\n const compiled = new Map<string, IntlMessageFormat>()\n\n for (const [key, value] of Object.entries(translations)) {\n compiled.set(key, new IntlMessageFormat(value, locale))\n }\n\n return (key: MessageKeys, params?: MessageParams): string => {\n const msg = compiled.get(key)\n if (!msg) return key\n return String(msg.format(params as Record<string, string | number | boolean>))\n }\n }, [locale, translations])\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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"}
|
|
1
|
+
{"version":3,"file":"react.mjs","names":[],"sources":["../src/react/seo.ts","../src/react/use-i18n.ts","../src/react/use-route.ts"],"sourcesContent":["import type { PageProps } from '@inertiajs/core'\nimport { usePage } from '@inertiajs/react'\nimport type { SeoData } from '../seo/types'\n\ninterface SeoPageProps extends PageProps {\n seo?: SeoData\n}\n\n/**\n * Returns the resolved SEO data shared by the backend for the current page.\n *\n * The document head is kept in sync automatically (server injection on the\n * initial paint + the auto-injected client runtime on navigation); use this\n * hook only when you want to read the metadata inside a component.\n */\nexport function useSeo(): SeoData {\n return usePage<SeoPageProps>().props.seo ?? {}\n}\n","import type { PageProps } from '@inertiajs/core'\nimport { usePage } from '@inertiajs/react'\nimport IntlMessageFormat from 'intl-messageformat'\nimport { useMemo } from 'react'\nimport type { MessageParams } from 'stratal/i18n'\nimport type { InertiaTranslationKeys } from '../types'\n\ninterface I18nPageProps extends PageProps {\n locale: string\n translations: Record<string, string>\n}\n\nexport function useI18n() {\n const { locale, translations } = usePage<I18nPageProps>().props\n\n const t = useMemo(() => {\n const compiled = new Map<string, IntlMessageFormat>()\n\n for (const [key, value] of Object.entries(translations)) {\n compiled.set(key, new IntlMessageFormat(value, locale))\n }\n\n return (key: InertiaTranslationKeys, params?: MessageParams): string => {\n const msg = compiled.get(key)\n if (!msg) return key\n return String(msg.format(params as Record<string, string | number | boolean>))\n }\n }, [locale, translations])\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, LocaleUrlConfig, RouteMatcher, RouteName, RouteParams, SerializedRoute, SerializedRoutes, TrailingSlashMode } from 'stratal/router'\n\ninterface RoutesPageProps extends PageProps {\n routes: SerializedRoutes\n trailingSlash?: TrailingSlashMode\n route: CurrentRoute\n localeConfig?: LocaleUrlConfig\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>, localeConfig?: LocaleUrlConfig): 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 const shouldPrefix = !localeConfig\n || localeConfig.prefixDefaultLocale === true\n || allParams.locale !== localeConfig.defaultLocale\n if (shouldPrefix) {\n url = `/${allParams.locale}${url === '/' ? '' : url}`\n }\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 localeConfig?: LocaleUrlConfig,\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, localeConfig), 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, localeConfig } = page.props\n\n const route = useMemo(\n () => <N extends RouteName>(name: N, params?: RouteParams<N>): string =>\n resolveUrl(name, params, routes, currentRoute, trailingSlash, localeConfig),\n [routes, trailingSlash, currentRoute, localeConfig],\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 @@
|
|
|
1
|
+
export { };
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { n as buildSeoTags, t as DATA_SEO_ATTR } from "./build-seo-tags-DBsHKxX9.mjs";
|
|
2
|
+
import { router } from "@inertiajs/core";
|
|
3
|
+
//#region src/seo/apply-seo-to-head.ts
|
|
4
|
+
/**
|
|
5
|
+
* Reconciles `document.head` with the resolved SEO data (client-side).
|
|
6
|
+
*
|
|
7
|
+
* Removes only the previously managed `[data-seo]` tags and re-creates them
|
|
8
|
+
* from {@link buildSeoTags}; the title is applied via `doc.title` so the single
|
|
9
|
+
* `<title>` element is updated in place rather than duplicated, and the
|
|
10
|
+
* {@link DATA_SEO_ATTR} marker is re-stamped on it so the next reconcile finds
|
|
11
|
+
* and replaces it instead of leaving a stale title behind.
|
|
12
|
+
*
|
|
13
|
+
* Pure and DOM-only (no React) so it can be unit-tested under jsdom.
|
|
14
|
+
*/
|
|
15
|
+
function applySeoToHead(seo, doc = document) {
|
|
16
|
+
const head = doc.head;
|
|
17
|
+
head.querySelectorAll(`[${DATA_SEO_ATTR}]`).forEach((el) => el.remove());
|
|
18
|
+
for (const descriptor of buildSeoTags(seo)) {
|
|
19
|
+
if (descriptor.tag === "title") {
|
|
20
|
+
doc.title = descriptor.content ?? "";
|
|
21
|
+
doc.head.querySelector("title")?.setAttribute(DATA_SEO_ATTR, "");
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
const el = doc.createElement(descriptor.tag);
|
|
25
|
+
for (const [key, value] of Object.entries(descriptor.attrs)) try {
|
|
26
|
+
el.setAttribute(key, value);
|
|
27
|
+
} catch {}
|
|
28
|
+
head.appendChild(el);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
//#endregion
|
|
32
|
+
//#region src/seo-runtime.ts
|
|
33
|
+
/**
|
|
34
|
+
* Client-side SEO head sync. Side-effect module: importing it registers a
|
|
35
|
+
* single Inertia `navigate` listener that reconciles `document.head` from the
|
|
36
|
+
* shared `seo` prop on every SPA visit.
|
|
37
|
+
*
|
|
38
|
+
* Consumers never import this directly — the `stratalInertia()` Vite plugin
|
|
39
|
+
* injects it into the client entry, so backend `ctx.seo()` metadata stays in
|
|
40
|
+
* sync across navigations with zero app wiring. The server still injects the
|
|
41
|
+
* tags for the initial paint; this only runs on subsequent client visits.
|
|
42
|
+
*/
|
|
43
|
+
const INSTALLED_KEY = "__stratalInertiaSeoInstalled";
|
|
44
|
+
const globalScope = globalThis;
|
|
45
|
+
if (!globalScope[INSTALLED_KEY]) {
|
|
46
|
+
globalScope[INSTALLED_KEY] = true;
|
|
47
|
+
router.on("navigate", (event) => {
|
|
48
|
+
const props = event.detail.page.props;
|
|
49
|
+
if (!("seo" in props)) return;
|
|
50
|
+
applySeoToHead(props.seo ?? {});
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
//#endregion
|
|
54
|
+
export {};
|
|
55
|
+
|
|
56
|
+
//# sourceMappingURL=seo-runtime.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"seo-runtime.mjs","names":[],"sources":["../src/seo/apply-seo-to-head.ts","../src/seo-runtime.ts"],"sourcesContent":["/// <reference lib=\"dom\" />\nimport { DATA_SEO_ATTR, buildSeoTags } from './build-seo-tags'\nimport type { SeoData } from './types'\n\n/**\n * Reconciles `document.head` with the resolved SEO data (client-side).\n *\n * Removes only the previously managed `[data-seo]` tags and re-creates them\n * from {@link buildSeoTags}; the title is applied via `doc.title` so the single\n * `<title>` element is updated in place rather than duplicated, and the\n * {@link DATA_SEO_ATTR} marker is re-stamped on it so the next reconcile finds\n * and replaces it instead of leaving a stale title behind.\n *\n * Pure and DOM-only (no React) so it can be unit-tested under jsdom.\n */\nexport function applySeoToHead(seo: SeoData, doc: Document = document): void {\n const head = doc.head\n // Remove only previously SEO-managed tags; unmanaged head content is untouched.\n head.querySelectorAll(`[${DATA_SEO_ATTR}]`).forEach((el) => el.remove())\n\n for (const descriptor of buildSeoTags(seo)) {\n if (descriptor.tag === 'title') {\n // `doc.title` updates the single <title> in place. Re-stamp the marker so\n // the element is tracked as managed and replaced on the next navigation.\n doc.title = descriptor.content ?? ''\n doc.head.querySelector('title')?.setAttribute(DATA_SEO_ATTR, '')\n continue\n }\n const el = doc.createElement(descriptor.tag)\n for (const [key, value] of Object.entries(descriptor.attrs)) {\n // A single malformed attribute name must not abort the reconcile and\n // leave the head half-updated. `setAttribute` throws on invalid names,\n // so isolate each one and skip the offending attribute only.\n try {\n el.setAttribute(key, value)\n } catch {\n // Invalid attribute name — drop this attribute, keep building the tag.\n }\n }\n head.appendChild(el)\n }\n}\n","/**\n * Client-side SEO head sync. Side-effect module: importing it registers a\n * single Inertia `navigate` listener that reconciles `document.head` from the\n * shared `seo` prop on every SPA visit.\n *\n * Consumers never import this directly — the `stratalInertia()` Vite plugin\n * injects it into the client entry, so backend `ctx.seo()` metadata stays in\n * sync across navigations with zero app wiring. The server still injects the\n * tags for the initial paint; this only runs on subsequent client visits.\n */\nimport { router } from '@inertiajs/core'\nimport { applySeoToHead } from './seo/apply-seo-to-head'\nimport type { SeoData } from './seo/types'\n\n// Guard against duplicate registration when the module is re-evaluated (e.g.\n// dev-server HMR, or the runtime injected into more than one client entry).\nconst INSTALLED_KEY = '__stratalInertiaSeoInstalled'\nconst globalScope = globalThis as Record<string, unknown>\n\nif (!globalScope[INSTALLED_KEY]) {\n globalScope[INSTALLED_KEY] = true\n router.on('navigate', (event) => {\n const props = event.detail.page.props as { seo?: SeoData }\n // The backend shares `seo` as an always-evaluated prop, so it is present on\n // every response — including partial reloads. Only reconcile the head when\n // the key is actually present; never act on a guessed-empty value, which\n // would wipe managed tags a partial reload didn't intend to touch.\n if (!('seo' in props)) return\n applySeoToHead(props.seo ?? {})\n })\n}\n"],"mappings":";;;;;;;;;;;;;;AAeA,SAAgB,eAAe,KAAc,MAAgB,UAAgB;CAC3E,MAAM,OAAO,IAAI;CAEjB,KAAK,iBAAiB,IAAI,cAAc,EAAE,EAAE,SAAS,OAAO,GAAG,OAAO,CAAC;CAEvE,KAAK,MAAM,cAAc,aAAa,GAAG,GAAG;EAC1C,IAAI,WAAW,QAAQ,SAAS;GAG9B,IAAI,QAAQ,WAAW,WAAW;GAClC,IAAI,KAAK,cAAc,OAAO,GAAG,aAAa,eAAe,EAAE;GAC/D;EACF;EACA,MAAM,KAAK,IAAI,cAAc,WAAW,GAAG;EAC3C,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,WAAW,KAAK,GAIxD,IAAI;GACF,GAAG,aAAa,KAAK,KAAK;EAC5B,QAAQ,CAER;EAEF,KAAK,YAAY,EAAE;CACrB;AACF;;;;;;;;;;;;;ACzBA,MAAM,gBAAgB;AACtB,MAAM,cAAc;AAEpB,IAAI,CAAC,YAAY,gBAAgB;CAC/B,YAAY,iBAAiB;CAC7B,OAAO,GAAG,aAAa,UAAU;EAC/B,MAAM,QAAQ,MAAM,OAAO,KAAK;EAKhC,IAAI,EAAE,SAAS,QAAQ;EACvB,eAAe,MAAM,OAAO,CAAC,CAAC;CAChC,CAAC;AACH"}
|
package/dist/ssr.d.mts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { h as InertiaSsrResult } from "./types-BhgXhWx6.mjs";
|
|
2
|
+
import { ComponentType, ReactNode } from "react";
|
|
3
|
+
import { HeadManagerTitleCallback, Page } from "@inertiajs/core";
|
|
4
|
+
|
|
5
|
+
//#region src/ssr.d.ts
|
|
6
|
+
/**
|
|
7
|
+
* The props Inertia's `App` component receives, reconstructed locally from
|
|
8
|
+
* `@inertiajs/core` + React types. Mirrors `@inertiajs/react`'s `InertiaAppProps`
|
|
9
|
+
* without importing it — see the `App` import note above for why that matters.
|
|
10
|
+
*/
|
|
11
|
+
interface AppProps {
|
|
12
|
+
initialPage: Page;
|
|
13
|
+
initialComponent?: ComponentType<any>;
|
|
14
|
+
resolveComponent?: (name: string, page?: Page) => ComponentType<any> | Promise<ComponentType<any>>;
|
|
15
|
+
titleCallback?: HeadManagerTitleCallback;
|
|
16
|
+
onHeadUpdate?: (elements: string[]) => void;
|
|
17
|
+
}
|
|
18
|
+
/** A page component for `TProps`, or a module namespace whose `default` is one. */
|
|
19
|
+
type ResolvedPage<TProps> = ComponentType<TProps> | {
|
|
20
|
+
default: ComponentType<TProps>;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* The resolver's return type, keyed on whether a props type argument was supplied:
|
|
24
|
+
* with none (`TProps` defaults to `unknown`) it stays opaque — matching what
|
|
25
|
+
* `import.meta.glob` yields — and with one it is the typed component/module.
|
|
26
|
+
*/
|
|
27
|
+
type ResolverReturn<TProps> = [unknown] extends [TProps] ? unknown : ResolvedPage<TProps> | Promise<ResolvedPage<TProps>>;
|
|
28
|
+
interface CreateInertiaSsrAppOptions<TProps = unknown> {
|
|
29
|
+
/**
|
|
30
|
+
* Resolve a page by name. Typically backed by `import.meta.glob`, whose modules
|
|
31
|
+
* are opaque (`unknown`) — the returned value is unwrapped (a `default` export is
|
|
32
|
+
* taken when present) and narrowed to a component at runtime, so an invalid
|
|
33
|
+
* resolver result fails loudly rather than rendering nothing. Pass a props type
|
|
34
|
+
* argument to {@link createInertiaSsrApp} to type the resolver's return.
|
|
35
|
+
*/
|
|
36
|
+
resolve: (name: string) => ResolverReturn<NoInfer<TProps>>;
|
|
37
|
+
/**
|
|
38
|
+
* Optional wrapper for application-level providers (theme, store, i18n, …).
|
|
39
|
+
* Receives the Inertia `App` component and its props; return the React tree to
|
|
40
|
+
* render. When omitted, `App` is rendered directly.
|
|
41
|
+
*/
|
|
42
|
+
setup?: (args: {
|
|
43
|
+
App: ComponentType<AppProps>;
|
|
44
|
+
props: AppProps;
|
|
45
|
+
}) => ReactNode;
|
|
46
|
+
/**
|
|
47
|
+
* Optional document-title callback (Inertia `title`), applied to page titles.
|
|
48
|
+
*/
|
|
49
|
+
title?: HeadManagerTitleCallback;
|
|
50
|
+
}
|
|
51
|
+
interface InertiaSsrApp {
|
|
52
|
+
render(page: Page): Promise<InertiaSsrResult>;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Build a streaming Inertia SSR handler.
|
|
56
|
+
*
|
|
57
|
+
* The returned `render(page)` resolves once React's shell is ready — at which
|
|
58
|
+
* point Inertia's `<Head>` tags have been collected — and streams the body
|
|
59
|
+
* progressively. Head tags rendered inside a *suspended* boundary are not
|
|
60
|
+
* captured; use Stratal's server-side SEO (`ctx.seo()`) for `<head>` metadata.
|
|
61
|
+
*/
|
|
62
|
+
declare function createInertiaSsrApp<TProps = unknown>(options: CreateInertiaSsrAppOptions<TProps>): InertiaSsrApp;
|
|
63
|
+
//#endregion
|
|
64
|
+
export { CreateInertiaSsrAppOptions, InertiaSsrApp, createInertiaSsrApp };
|
|
65
|
+
//# sourceMappingURL=ssr.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ssr.d.mts","names":[],"sources":["../src/ssr.ts"],"mappings":";;;;;;;;;;UAqCU,QAAA;EACR,WAAA,EAAa,IAAA;EAKb,gBAAA,GAAmB,aAAA;EAEnB,gBAAA,IAAoB,IAAA,UAAc,IAAA,GAAO,IAAA,KAAS,aAAA,QAAqB,OAAA,CAAQ,aAAA;EAC/E,aAAA,GAAgB,wBAAA;EAChB,YAAA,IAAgB,QAAA;AAAA;;KAIb,YAAA,WAAuB,aAAA,CAAc,MAAA;EAAY,OAAA,EAAS,aAAA,CAAc,MAAA;AAAA;;;AAJzC;AAAA;;KAW/B,cAAA,8BAA4C,MAAA,cAE7C,YAAA,CAAa,MAAA,IAAU,OAAA,CAAQ,YAAA,CAAa,MAAA;AAAA,UAkB/B,0BAAA;EA3BW;;;;;;;EAqC1B,OAAA,GAAU,IAAA,aAAiB,cAAA,CAAe,OAAA,CAAQ,MAAA;EArCE;;;;AAA6B;EA2CjF,KAAA,IAAS,IAAA;IAAQ,GAAA,EAAK,aAAA,CAAc,QAAA;IAAW,KAAA,EAAO,QAAA;EAAA,MAAe,SAAA;EAlCtD;;;EAsCf,KAAA,GAAQ,wBAAA;AAAA;AAAA,UAGO,aAAA;EACf,MAAA,CAAO,IAAA,EAAM,IAAA,GAAO,OAAA,CAAQ,gBAAA;AAAA;;;;;;;;AA1CwB;iBAqDtC,mBAAA,mBACd,OAAA,EAAS,0BAAA,CAA2B,MAAA,IACnC,aAAA"}
|
package/dist/ssr.mjs
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { ApplicationError } from "stratal/errors";
|
|
2
|
+
import { App } from "@inertiajs/react";
|
|
3
|
+
import { createElement } from "react";
|
|
4
|
+
import { renderToReadableStream } from "react-dom/server";
|
|
5
|
+
//#region src/ssr.ts
|
|
6
|
+
/** Unwrap a module namespace's `default` export, leaving a bare component as-is. */
|
|
7
|
+
function unwrapDefault(module) {
|
|
8
|
+
return typeof module === "object" && module !== null && "default" in module ? module.default : module;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* A React component is either a function (function/class component) or an object
|
|
12
|
+
* (a `memo`/`forwardRef`/`lazy` exotic component). This narrows the opaque value a
|
|
13
|
+
* dynamic import yields without admitting `any`.
|
|
14
|
+
*/
|
|
15
|
+
function isPageComponent(value) {
|
|
16
|
+
return typeof value === "function" || typeof value === "object" && value !== null;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Build a streaming Inertia SSR handler.
|
|
20
|
+
*
|
|
21
|
+
* The returned `render(page)` resolves once React's shell is ready — at which
|
|
22
|
+
* point Inertia's `<Head>` tags have been collected — and streams the body
|
|
23
|
+
* progressively. Head tags rendered inside a *suspended* boundary are not
|
|
24
|
+
* captured; use Stratal's server-side SEO (`ctx.seo()`) for `<head>` metadata.
|
|
25
|
+
*/
|
|
26
|
+
function createInertiaSsrApp(options) {
|
|
27
|
+
const resolveComponent = (name) => Promise.resolve(options.resolve(name)).then((module) => {
|
|
28
|
+
const component = unwrapDefault(module);
|
|
29
|
+
if (!isPageComponent(component)) throw new ApplicationError(`[stratal:inertia] resolve("${name}") did not return a React component.`);
|
|
30
|
+
return component;
|
|
31
|
+
});
|
|
32
|
+
return { async render(page) {
|
|
33
|
+
let head = [];
|
|
34
|
+
const props = {
|
|
35
|
+
initialPage: page,
|
|
36
|
+
initialComponent: await resolveComponent(page.component),
|
|
37
|
+
resolveComponent,
|
|
38
|
+
titleCallback: options.title,
|
|
39
|
+
onHeadUpdate: (elements) => {
|
|
40
|
+
head = elements;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
const stream = await renderToReadableStream(options.setup ? options.setup({
|
|
44
|
+
App,
|
|
45
|
+
props
|
|
46
|
+
}) : createElement(App, props));
|
|
47
|
+
return {
|
|
48
|
+
head,
|
|
49
|
+
stream
|
|
50
|
+
};
|
|
51
|
+
} };
|
|
52
|
+
}
|
|
53
|
+
//#endregion
|
|
54
|
+
export { createInertiaSsrApp };
|
|
55
|
+
|
|
56
|
+
//# sourceMappingURL=ssr.mjs.map
|
package/dist/ssr.mjs.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ssr.mjs","names":[],"sources":["../src/ssr.ts"],"sourcesContent":["/**\n * Server-side rendering entry for Stratal Inertia.\n *\n * Provides {@link createInertiaSsrApp}, which encapsulates React 19 streaming SSR\n * (`renderToReadableStream`) and Inertia's head collection, returning the\n * `render(page)` function the `InertiaModule` SSR bundle option expects.\n *\n * This entry pulls React + `react-dom/server` into the worker SSR bundle and is\n * intentionally separate from the client (`./react`) and server (`.`) entries.\n *\n * @packageDocumentation\n */\n\nimport type { HeadManagerTitleCallback, Page } from '@inertiajs/core'\n// Import `App` as a runtime value only — never reference its *type*\n// (`typeof App`, `Parameters<typeof App>`, `ComponentProps<typeof App>`, …) in\n// this module's exported surface. Any such reference makes the emitted `.d.mts`\n// re-export `import { App } from '@inertiajs/react'`, which pulls Inertia's whole\n// type graph into resolution the moment a consumer imports this SSR entry. That\n// eagerly evaluates `@inertiajs/core`'s config-driven types (`FlashData`,\n// `SharedPageProps`, derived from its `InertiaConfig` interface) before a\n// consumer's own `declare module '@inertiajs/core'` augmentation has been\n// applied, caching the un-augmented defaults — so `usePage().flash` /\n// `usePage().props` degrade to `unknown` at call sites. Typing this entry's\n// surface structurally (below) keeps `@inertiajs/react` out of the generated\n// declarations and avoids the hazard.\nimport { App } from '@inertiajs/react'\nimport { type ComponentType, type ReactNode, createElement } from 'react'\nimport { renderToReadableStream } from 'react-dom/server'\nimport { ApplicationError } from 'stratal/errors'\nimport type { InertiaSsrResult } from './types'\n\n/**\n * The props Inertia's `App` component receives, reconstructed locally from\n * `@inertiajs/core` + React types. Mirrors `@inertiajs/react`'s `InertiaAppProps`\n * without importing it — see the `App` import note above for why that matters.\n */\ninterface AppProps {\n initialPage: Page\n // `ComponentType<any>` mirrors Inertia's own `ReactComponent` (page components\n // are resolved opaquely), keeping the resolver's `ComponentType<TProps>` output\n // assignable here without coupling to `@inertiajs/react`'s exported types.\n // oxlint-disable-next-line typescript/no-explicit-any\n initialComponent?: ComponentType<any>\n // oxlint-disable-next-line typescript/no-explicit-any\n resolveComponent?: (name: string, page?: Page) => ComponentType<any> | Promise<ComponentType<any>>\n titleCallback?: HeadManagerTitleCallback\n onHeadUpdate?: (elements: string[]) => void\n}\n\n/** A page component for `TProps`, or a module namespace whose `default` is one. */\ntype ResolvedPage<TProps> = ComponentType<TProps> | { default: ComponentType<TProps> }\n\n/**\n * The resolver's return type, keyed on whether a props type argument was supplied:\n * with none (`TProps` defaults to `unknown`) it stays opaque — matching what\n * `import.meta.glob` yields — and with one it is the typed component/module.\n */\ntype ResolverReturn<TProps> = [unknown] extends [TProps]\n ? unknown\n : ResolvedPage<TProps> | Promise<ResolvedPage<TProps>>\n\n/** Unwrap a module namespace's `default` export, leaving a bare component as-is. */\nfunction unwrapDefault(module: unknown): unknown {\n return typeof module === 'object' && module !== null && 'default' in module\n ? (module as { default: unknown }).default\n : module\n}\n\n/**\n * A React component is either a function (function/class component) or an object\n * (a `memo`/`forwardRef`/`lazy` exotic component). This narrows the opaque value a\n * dynamic import yields without admitting `any`.\n */\nfunction isPageComponent<TProps>(value: unknown): value is ComponentType<TProps> {\n return typeof value === 'function' || (typeof value === 'object' && value !== null)\n}\n\nexport interface CreateInertiaSsrAppOptions<TProps = unknown> {\n /**\n * Resolve a page by name. Typically backed by `import.meta.glob`, whose modules\n * are opaque (`unknown`) — the returned value is unwrapped (a `default` export is\n * taken when present) and narrowed to a component at runtime, so an invalid\n * resolver result fails loudly rather than rendering nothing. Pass a props type\n * argument to {@link createInertiaSsrApp} to type the resolver's return.\n */\n // `NoInfer` keeps `TProps` pinned to its explicit type argument (or the\n // `unknown` default) instead of being widened back out of the resolver return.\n resolve: (name: string) => ResolverReturn<NoInfer<TProps>>\n /**\n * Optional wrapper for application-level providers (theme, store, i18n, …).\n * Receives the Inertia `App` component and its props; return the React tree to\n * render. When omitted, `App` is rendered directly.\n */\n setup?: (args: { App: ComponentType<AppProps>; props: AppProps }) => ReactNode\n /**\n * Optional document-title callback (Inertia `title`), applied to page titles.\n */\n title?: HeadManagerTitleCallback\n}\n\nexport interface InertiaSsrApp {\n render(page: Page): Promise<InertiaSsrResult>\n}\n\n/**\n * Build a streaming Inertia SSR handler.\n *\n * The returned `render(page)` resolves once React's shell is ready — at which\n * point Inertia's `<Head>` tags have been collected — and streams the body\n * progressively. Head tags rendered inside a *suspended* boundary are not\n * captured; use Stratal's server-side SEO (`ctx.seo()`) for `<head>` metadata.\n */\nexport function createInertiaSsrApp<TProps = unknown>(\n options: CreateInertiaSsrAppOptions<TProps>,\n): InertiaSsrApp {\n const resolveComponent = (name: string): Promise<ComponentType<TProps>> =>\n Promise.resolve(options.resolve(name)).then((module) => {\n const component = unwrapDefault(module)\n if (!isPageComponent<TProps>(component)) {\n throw new ApplicationError(`[stratal:inertia] resolve(\"${name}\") did not return a React component.`)\n }\n return component\n })\n\n return {\n async render(page: Page): Promise<InertiaSsrResult> {\n let head: string[] = []\n const initialComponent = await resolveComponent(page.component)\n const props: AppProps = {\n initialPage: page,\n initialComponent,\n resolveComponent,\n titleCallback: options.title,\n onHeadUpdate: (elements: string[]) => { head = elements },\n }\n const app = options.setup\n ? options.setup({ App, props })\n : createElement(App, props)\n const stream = await renderToReadableStream(app)\n return { head, stream }\n },\n }\n}\n"],"mappings":";;;;;;AA+DA,SAAS,cAAc,QAA0B;CAC/C,OAAO,OAAO,WAAW,YAAY,WAAW,QAAQ,aAAa,SAChE,OAAgC,UACjC;AACN;;;;;;AAOA,SAAS,gBAAwB,OAAgD;CAC/E,OAAO,OAAO,UAAU,cAAe,OAAO,UAAU,YAAY,UAAU;AAChF;;;;;;;;;AAqCA,SAAgB,oBACd,SACe;CACf,MAAM,oBAAoB,SACxB,QAAQ,QAAQ,QAAQ,QAAQ,IAAI,CAAC,EAAE,MAAM,WAAW;EACtD,MAAM,YAAY,cAAc,MAAM;EACtC,IAAI,CAAC,gBAAwB,SAAS,GACpC,MAAM,IAAI,iBAAiB,8BAA8B,KAAK,qCAAqC;EAErG,OAAO;CACT,CAAC;CAEH,OAAO,EACL,MAAM,OAAO,MAAuC;EAClD,IAAI,OAAiB,CAAC;EAEtB,MAAM,QAAkB;GACtB,aAAa;GACb,kBAAA,MAH6B,iBAAiB,KAAK,SAAS;GAI5D;GACA,eAAe,QAAQ;GACvB,eAAe,aAAuB;IAAE,OAAO;GAAS;EAC1D;EAIA,MAAM,SAAS,MAAM,uBAHT,QAAQ,QAChB,QAAQ,MAAM;GAAE;GAAK;EAAM,CAAC,IAC5B,cAAc,KAAK,KAAK,CACmB;EAC/C,OAAO;GAAE;GAAM;EAAO;CACxB,EACF;AACF"}
|
package/dist/testing.d.mts
CHANGED
package/dist/testing.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"testing.mjs","names":[],"sources":["../src/augment/test-response.ts","../src/testing.ts"],"sourcesContent":["import type { Page } from '@inertiajs/core'\nimport { getValueAtPath, hasValueAtPath, TestResponse } from '@stratal/testing'\nimport { expect } from 'vitest'\n\ndeclare module '@stratal/testing' {\n interface TestResponse {\n /** Assert the response is an Inertia response. Optionally run a callback with the page object for custom assertions. */\n assertInertia(callback?: (page: Page) => void): Promise<this>\n /** Assert the Inertia page component matches the expected name. */\n assertInertiaComponent(component: string): Promise<this>\n /** Assert the Inertia page prop at the given dot-path equals the expected value. */\n assertInertiaProp(path: string, expected: unknown): Promise<this>\n /** Assert the Inertia page prop at the given dot-path exists. */\n assertInertiaPropExists(path: string): Promise<this>\n /** Assert the Inertia page prop at the given dot-path does not exist. */\n assertInertiaPropMissing(path: string): Promise<this>\n /** Assert the Inertia page URL matches the expected value. */\n assertInertiaUrl(url: string): Promise<this>\n /** Assert the Inertia page version matches the expected value. */\n assertInertiaVersion(version: string | null): Promise<this>\n /** Assert the Inertia page flash data contains the given key with the expected value. */\n assertInertiaFlash(key: string, value: unknown): Promise<this>\n /** Assert a prop is listed as deferred in the given group. */\n assertInertiaDeferredProp(prop: string, group: string): Promise<this>\n /** Assert a prop is listed as a merge prop. */\n assertInertiaMergeProp(prop: string): Promise<this>\n /** Assert a prop is listed as a shared prop. */\n assertInertiaSharedProp(prop: string): Promise<this>\n /** Assert the response is a successful precognition response (204 with precognition headers). */\n assertSuccessfulPrecognition(): this\n /** Assert the response is a precognition validation error (422 with precognition headers). Optionally assert specific errors. */\n assertPrecognitionValidationErrors(errors?: Record<string, string>): Promise<this>\n }\n}\n\nexport function augmentTestResponse(): void {\n TestResponse.macro('assertInertia', async function (this: TestResponse, callback?: (page: Page) => void) {\n this.assertHeader('x-inertia', 'true')\n this.assertOk()\n\n if (callback) {\n const page = await this.json<Page>()\n callback(page)\n }\n\n return this\n })\n\n TestResponse.macro('assertInertiaComponent', async function (this: TestResponse, component: string) {\n const page = await this.json<Page>()\n\n expect(\n page.component,\n `Expected Inertia component \"${component}\", got \"${page.component}\"`,\n ).toBe(component)\n\n return this\n })\n\n TestResponse.macro('assertInertiaProp', async function (this: TestResponse, path: string, expected: unknown) {\n const page = await this.json<Page>()\n const actual = getValueAtPath(page.props, path)\n\n expect(\n actual,\n `Expected Inertia prop \"${path}\" to be ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`,\n ).toStrictEqual(expected)\n\n return this\n })\n\n TestResponse.macro('assertInertiaPropExists', async function (this: TestResponse, path: string) {\n const page = await this.json<Page>()\n const exists = hasValueAtPath(page.props, path)\n\n expect(\n exists,\n `Expected Inertia prop \"${path}\" to exist`,\n ).toBe(true)\n\n return this\n })\n\n TestResponse.macro('assertInertiaPropMissing', async function (this: TestResponse, path: string) {\n const page = await this.json<Page>()\n const exists = hasValueAtPath(page.props, path)\n\n expect(\n exists,\n `Expected Inertia prop \"${path}\" to not exist`,\n ).toBe(false)\n\n return this\n })\n\n TestResponse.macro('assertInertiaUrl', async function (this: TestResponse, url: string) {\n const page = await this.json<Page>()\n\n expect(\n page.url,\n `Expected Inertia URL \"${url}\", got \"${page.url}\"`,\n ).toBe(url)\n\n return this\n })\n\n TestResponse.macro('assertInertiaVersion', async function (this: TestResponse, version: string | null) {\n const page = await this.json<Page>()\n\n expect(\n page.version,\n `Expected Inertia version \"${version}\", got \"${page.version}\"`,\n ).toBe(version)\n\n return this\n })\n\n TestResponse.macro('assertInertiaFlash', async function (this: TestResponse, key: string, value: unknown) {\n const page = await this.json<Page>()\n const actual = page.flash?.[key]\n\n expect(\n actual,\n `Expected Inertia flash \"${key}\" to be ${JSON.stringify(value)}, got ${JSON.stringify(actual)}`,\n ).toStrictEqual(value)\n\n return this\n })\n\n TestResponse.macro('assertInertiaDeferredProp', async function (this: TestResponse, prop: string, group: string) {\n const page = await this.json<Page>()\n\n expect(\n page.deferredProps?.[group],\n `Expected Inertia deferred group \"${group}\" to contain \"${prop}\"`,\n ).toContain(prop)\n\n return this\n })\n\n TestResponse.macro('assertInertiaMergeProp', async function (this: TestResponse, prop: string) {\n const page = await this.json<Page>()\n\n expect(\n page.mergeProps,\n `Expected Inertia mergeProps to contain \"${prop}\"`,\n ).toContain(prop)\n\n return this\n })\n\n TestResponse.macro('assertInertiaSharedProp', async function (this: TestResponse, prop: string) {\n const page = await this.json<Page>()\n\n expect(\n page.sharedProps,\n `Expected Inertia sharedProps to contain \"${prop}\"`,\n ).toContain(prop)\n\n return this\n })\n\n TestResponse.macro('assertSuccessfulPrecognition', function (this: TestResponse) {\n this.assertNoContent()\n this.assertHeader('Precognition', 'true')\n this.assertHeader('Precognition-Success', 'true')\n\n return this\n })\n\n TestResponse.macro('assertPrecognitionValidationErrors', async function (this: TestResponse, errors?: Record<string, string>) {\n this.assertUnprocessable()\n this.assertHeader('Precognition', 'true')\n\n if (errors) {\n const body = await this.json<{ errors: Record<string, string> }>()\n\n expect(\n body.errors,\n `Expected precognition errors to match ${JSON.stringify(errors)}, got ${JSON.stringify(body.errors)}`,\n ).toStrictEqual(errors)\n }\n\n return this\n })\n}\n","import { augmentTestResponse } from './augment/test-response'\n\n// Augmentation (side-effect import: augments TestResponse types)\nimport './augment/test-response'\n\n// Patch TestResponse.prototype with Inertia assertion methods\naugmentTestResponse()\n\n// Re-export useful types for test authors\nexport type { Page as InertiaPage } from '@inertiajs/core'\n"],"mappings":";;;AAmCA,SAAgB,sBAA4B;CAC1C,aAAa,MAAM,iBAAiB,eAAoC,UAAiC;EACvG,KAAK,aAAa,aAAa,
|
|
1
|
+
{"version":3,"file":"testing.mjs","names":[],"sources":["../src/augment/test-response.ts","../src/testing.ts"],"sourcesContent":["import type { Page } from '@inertiajs/core'\nimport { getValueAtPath, hasValueAtPath, TestResponse } from '@stratal/testing'\nimport { expect } from 'vitest'\n\ndeclare module '@stratal/testing' {\n interface TestResponse {\n /** Assert the response is an Inertia response. Optionally run a callback with the page object for custom assertions. */\n assertInertia(callback?: (page: Page) => void): Promise<this>\n /** Assert the Inertia page component matches the expected name. */\n assertInertiaComponent(component: string): Promise<this>\n /** Assert the Inertia page prop at the given dot-path equals the expected value. */\n assertInertiaProp(path: string, expected: unknown): Promise<this>\n /** Assert the Inertia page prop at the given dot-path exists. */\n assertInertiaPropExists(path: string): Promise<this>\n /** Assert the Inertia page prop at the given dot-path does not exist. */\n assertInertiaPropMissing(path: string): Promise<this>\n /** Assert the Inertia page URL matches the expected value. */\n assertInertiaUrl(url: string): Promise<this>\n /** Assert the Inertia page version matches the expected value. */\n assertInertiaVersion(version: string | null): Promise<this>\n /** Assert the Inertia page flash data contains the given key with the expected value. */\n assertInertiaFlash(key: string, value: unknown): Promise<this>\n /** Assert a prop is listed as deferred in the given group. */\n assertInertiaDeferredProp(prop: string, group: string): Promise<this>\n /** Assert a prop is listed as a merge prop. */\n assertInertiaMergeProp(prop: string): Promise<this>\n /** Assert a prop is listed as a shared prop. */\n assertInertiaSharedProp(prop: string): Promise<this>\n /** Assert the response is a successful precognition response (204 with precognition headers). */\n assertSuccessfulPrecognition(): this\n /** Assert the response is a precognition validation error (422 with precognition headers). Optionally assert specific errors. */\n assertPrecognitionValidationErrors(errors?: Record<string, string>): Promise<this>\n }\n}\n\nexport function augmentTestResponse(): void {\n TestResponse.macro('assertInertia', async function (this: TestResponse, callback?: (page: Page) => void) {\n this.assertHeader('x-inertia', 'true')\n this.assertOk()\n\n if (callback) {\n const page = await this.json<Page>()\n callback(page)\n }\n\n return this\n })\n\n TestResponse.macro('assertInertiaComponent', async function (this: TestResponse, component: string) {\n const page = await this.json<Page>()\n\n expect(\n page.component,\n `Expected Inertia component \"${component}\", got \"${page.component}\"`,\n ).toBe(component)\n\n return this\n })\n\n TestResponse.macro('assertInertiaProp', async function (this: TestResponse, path: string, expected: unknown) {\n const page = await this.json<Page>()\n const actual = getValueAtPath(page.props, path)\n\n expect(\n actual,\n `Expected Inertia prop \"${path}\" to be ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`,\n ).toStrictEqual(expected)\n\n return this\n })\n\n TestResponse.macro('assertInertiaPropExists', async function (this: TestResponse, path: string) {\n const page = await this.json<Page>()\n const exists = hasValueAtPath(page.props, path)\n\n expect(\n exists,\n `Expected Inertia prop \"${path}\" to exist`,\n ).toBe(true)\n\n return this\n })\n\n TestResponse.macro('assertInertiaPropMissing', async function (this: TestResponse, path: string) {\n const page = await this.json<Page>()\n const exists = hasValueAtPath(page.props, path)\n\n expect(\n exists,\n `Expected Inertia prop \"${path}\" to not exist`,\n ).toBe(false)\n\n return this\n })\n\n TestResponse.macro('assertInertiaUrl', async function (this: TestResponse, url: string) {\n const page = await this.json<Page>()\n\n expect(\n page.url,\n `Expected Inertia URL \"${url}\", got \"${page.url}\"`,\n ).toBe(url)\n\n return this\n })\n\n TestResponse.macro('assertInertiaVersion', async function (this: TestResponse, version: string | null) {\n const page = await this.json<Page>()\n\n expect(\n page.version,\n `Expected Inertia version \"${version}\", got \"${page.version}\"`,\n ).toBe(version)\n\n return this\n })\n\n TestResponse.macro('assertInertiaFlash', async function (this: TestResponse, key: string, value: unknown) {\n const page = await this.json<Page>()\n const actual = page.flash?.[key]\n\n expect(\n actual,\n `Expected Inertia flash \"${key}\" to be ${JSON.stringify(value)}, got ${JSON.stringify(actual)}`,\n ).toStrictEqual(value)\n\n return this\n })\n\n TestResponse.macro('assertInertiaDeferredProp', async function (this: TestResponse, prop: string, group: string) {\n const page = await this.json<Page>()\n\n expect(\n page.deferredProps?.[group],\n `Expected Inertia deferred group \"${group}\" to contain \"${prop}\"`,\n ).toContain(prop)\n\n return this\n })\n\n TestResponse.macro('assertInertiaMergeProp', async function (this: TestResponse, prop: string) {\n const page = await this.json<Page>()\n\n expect(\n page.mergeProps,\n `Expected Inertia mergeProps to contain \"${prop}\"`,\n ).toContain(prop)\n\n return this\n })\n\n TestResponse.macro('assertInertiaSharedProp', async function (this: TestResponse, prop: string) {\n const page = await this.json<Page>()\n\n expect(\n page.sharedProps,\n `Expected Inertia sharedProps to contain \"${prop}\"`,\n ).toContain(prop)\n\n return this\n })\n\n TestResponse.macro('assertSuccessfulPrecognition', function (this: TestResponse) {\n this.assertNoContent()\n this.assertHeader('Precognition', 'true')\n this.assertHeader('Precognition-Success', 'true')\n\n return this\n })\n\n TestResponse.macro('assertPrecognitionValidationErrors', async function (this: TestResponse, errors?: Record<string, string>) {\n this.assertUnprocessable()\n this.assertHeader('Precognition', 'true')\n\n if (errors) {\n const body = await this.json<{ errors: Record<string, string> }>()\n\n expect(\n body.errors,\n `Expected precognition errors to match ${JSON.stringify(errors)}, got ${JSON.stringify(body.errors)}`,\n ).toStrictEqual(errors)\n }\n\n return this\n })\n}\n","import { augmentTestResponse } from './augment/test-response'\n\n// Augmentation (side-effect import: augments TestResponse types)\nimport './augment/test-response'\n\n// Patch TestResponse.prototype with Inertia assertion methods\naugmentTestResponse()\n\n// Re-export useful types for test authors\nexport type { Page as InertiaPage } from '@inertiajs/core'\n"],"mappings":"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"}
|