litestar-vite-plugin 0.15.0-rc.2 → 0.15.0-rc.4

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 CHANGED
@@ -5,7 +5,7 @@ Litestar Vite connects the Litestar backend to a Vite toolchain. It supports SPA
5
5
  ## Features
6
6
 
7
7
  - One-port dev: proxies Vite HTTP + WS/HMR through Litestar by default; switch to two-port with `VITE_PROXY_MODE=direct`.
8
- - SSR framework support: use `proxy_mode="ssr"` for Astro, Nuxt, SvelteKit - proxies everything except your API routes.
8
+ - SSR framework support: use `mode="ssr"` for Astro, Nuxt, SvelteKit - proxies everything except your API routes.
9
9
  - Production assets: reads Vite manifest from `public/manifest.json` (configurable) and serves under `asset_url`.
10
10
  - Type-safe frontends: optional OpenAPI/routes export + `@hey-api/openapi-ts` via the Vite plugin.
11
11
  - Inertia support: v2 protocol with session middleware and optional SPA mode.
@@ -103,13 +103,13 @@ app = Litestar(
103
103
 
104
104
  ## Meta-frameworks (Astro, Nuxt, SvelteKit)
105
105
 
106
- Use `proxy_mode="ssr"` to proxy non-API routes to the framework's dev server:
106
+ Use `mode="ssr"` (or `mode="framework"`) to proxy non-API routes to the framework's dev server:
107
107
 
108
108
  ```python
109
109
  import os
110
110
  from pathlib import Path
111
111
  from litestar import Litestar
112
- from litestar_vite import VitePlugin, ViteConfig, PathConfig, RuntimeConfig
112
+ from litestar_vite import VitePlugin, ViteConfig, PathConfig
113
113
 
114
114
  here = Path(__file__).parent
115
115
  DEV_MODE = os.getenv("VITE_DEV_MODE", "true").lower() in ("true", "1", "yes")
@@ -117,9 +117,9 @@ DEV_MODE = os.getenv("VITE_DEV_MODE", "true").lower() in ("true", "1", "yes")
117
117
  app = Litestar(
118
118
  plugins=[
119
119
  VitePlugin(config=ViteConfig(
120
+ mode="ssr",
120
121
  dev_mode=DEV_MODE,
121
122
  paths=PathConfig(root=here),
122
- runtime=RuntimeConfig(proxy_mode="ssr"),
123
123
  ))
124
124
  ],
125
125
  )
@@ -31,6 +31,7 @@
31
31
  export interface RouteDefinition {
32
32
  path: string;
33
33
  methods: readonly string[];
34
+ method: string;
34
35
  pathParams: readonly string[];
35
36
  queryParams: readonly string[];
36
37
  component?: string;
@@ -100,6 +100,13 @@ export interface PluginConfig {
100
100
  * @default '/static/'
101
101
  */
102
102
  assetUrl?: string;
103
+ /**
104
+ * Optional asset URL to use only during production builds.
105
+ *
106
+ * This is typically derived from Python DeployConfig.asset_url and written into `.litestar.json`
107
+ * as `deployAssetUrl`. It is only used when `command === "build"`.
108
+ */
109
+ deployAssetUrl?: string;
103
110
  /**
104
111
  * The public directory where all compiled/bundled assets should be written.
105
112
  *
@@ -160,11 +167,12 @@ export interface PluginConfig {
160
167
  */
161
168
  autoDetectIndex?: boolean;
162
169
  /**
163
- * Enable Inertia mode, which disables index.html auto-detection.
170
+ * Enable Inertia mode.
164
171
  *
165
172
  * In Inertia apps, the backend (Litestar) serves all HTML responses.
166
173
  * When enabled, direct access to the Vite dev server will show a placeholder
167
- * page directing users to access the app through the backend.
174
+ * page directing users to access the app through the backend (even if an
175
+ * index.html exists for the backend to render).
168
176
  *
169
177
  * Auto-detected from `.litestar.json` when mode is "inertia".
170
178
  *
package/dist/js/index.js CHANGED
@@ -28,9 +28,6 @@ function litestar(config) {
28
28
  return plugins;
29
29
  }
30
30
  async function findIndexHtmlPath(server, pluginConfig) {
31
- if (pluginConfig.inertiaMode) {
32
- return null;
33
- }
34
31
  if (!pluginConfig.autoDetectIndex) {
35
32
  return null;
36
33
  }
@@ -81,7 +78,8 @@ function resolveLitestarPlugin(pluginConfig) {
81
78
  userConfig = config;
82
79
  const ssr = !!userConfig.build?.ssr;
83
80
  const env = loadEnv(mode, userConfig.envDir || process.cwd(), "");
84
- const assetUrl = normalizeAssetUrl(env.ASSET_URL || pluginConfig.assetUrl);
81
+ const runtimeAssetUrl = normalizeAssetUrl(env.ASSET_URL || pluginConfig.assetUrl);
82
+ const buildAssetUrl = pluginConfig.deployAssetUrl ?? runtimeAssetUrl;
85
83
  const serverConfig = command === "serve" ? resolveDevelopmentEnvironmentServerConfig(pluginConfig.detectTls) ?? resolveEnvironmentServerConfig(env) : void 0;
86
84
  const withProxyErrorSilencer = (proxyConfig) => {
87
85
  if (!proxyConfig) return void 0;
@@ -114,7 +112,7 @@ function resolveLitestarPlugin(pluginConfig) {
114
112
  const devBase = pluginConfig.assetUrl.startsWith("/") ? pluginConfig.assetUrl : pluginConfig.assetUrl.replace(/\/+$/, "");
115
113
  ensureCommandShouldRunInEnvironment(command, env, mode);
116
114
  return {
117
- base: userConfig.base ?? (command === "build" ? resolveBase(pluginConfig, assetUrl) : devBase),
115
+ base: userConfig.base ?? (command === "build" ? resolveBase(pluginConfig, buildAssetUrl) : devBase),
118
116
  publicDir: userConfig.publicDir ?? pluginConfig.staticDir ?? false,
119
117
  clearScreen: false,
120
118
  build: {
@@ -237,15 +235,21 @@ function resolveLitestarPlugin(pluginConfig) {
237
235
  setTimeout(async () => {
238
236
  if (logger.config.level === "quiet") return;
239
237
  const litestarVersion = litestarMeta.litestarVersion ?? process.env.LITESTAR_VERSION ?? "unknown";
240
- const backendStatus = await checkBackendAvailability(appUrl);
238
+ let backendStatus = await checkBackendAvailability(appUrl);
239
+ if (!backendStatus.available) {
240
+ for (let i = 0; i < 3 && !backendStatus.available; i++) {
241
+ await new Promise((resolve) => setTimeout(resolve, 500));
242
+ backendStatus = await checkBackendAvailability(appUrl);
243
+ }
244
+ }
241
245
  resolvedConfig.logger.info(`
242
246
  ${colors.red(`${colors.bold("LITESTAR")} ${litestarVersion}`)}`);
243
247
  resolvedConfig.logger.info("");
244
- if (initialIndexPath) {
248
+ if (pluginConfig.inertiaMode) {
249
+ resolvedConfig.logger.info(` ${colors.green("\u279C")} ${colors.bold("Mode")}: Inertia`);
250
+ } else if (initialIndexPath) {
245
251
  const relIndexPath = logger.path(initialIndexPath, server.config.root);
246
252
  resolvedConfig.logger.info(` ${colors.green("\u279C")} ${colors.bold("Mode")}: SPA (${colors.cyan(relIndexPath)})`);
247
- } else if (pluginConfig.inertiaMode) {
248
- resolvedConfig.logger.info(` ${colors.green("\u279C")} ${colors.bold("Mode")}: Inertia`);
249
253
  } else {
250
254
  resolvedConfig.logger.info(` ${colors.green("\u279C")} ${colors.bold("Mode")}: Litestar`);
251
255
  }
@@ -314,24 +318,51 @@ function resolveLitestarPlugin(pluginConfig) {
314
318
  exitHandlersBound = true;
315
319
  }
316
320
  server.middlewares.use(async (req, res, next) => {
317
- const indexPath = await findIndexHtmlPath(server, pluginConfig);
318
- if (indexPath && (req.url === "/" || req.url === "/index.html")) {
319
- const currentUrl = req.url;
321
+ const requestUrl = req.originalUrl ?? req.url ?? "/";
322
+ const requestPath = requestUrl.split("?")[0];
323
+ const isRootRequest = requestPath === "/" || requestPath === "/index.html";
324
+ if (requestPath === "/__litestar__/transform-index") {
325
+ if (req.method !== "POST") {
326
+ res.statusCode = 405;
327
+ res.setHeader("Content-Type", "text/plain");
328
+ res.end("Method Not Allowed");
329
+ return;
330
+ }
331
+ const readBody = async () => new Promise((resolve, reject) => {
332
+ let data = "";
333
+ req.on("data", (chunk) => {
334
+ data += chunk;
335
+ });
336
+ req.on("end", () => resolve(data));
337
+ req.on("error", (err) => reject(err));
338
+ });
320
339
  try {
321
- const htmlContent = await fs.promises.readFile(indexPath, "utf-8");
322
- const transformedHtml = await server.transformIndexHtml(req.originalUrl ?? currentUrl, htmlContent, req.originalUrl);
340
+ const body = await readBody();
341
+ const payload = JSON.parse(body);
342
+ if (!payload.html || typeof payload.html !== "string") {
343
+ res.statusCode = 400;
344
+ res.setHeader("Content-Type", "text/plain");
345
+ res.end("Invalid payload");
346
+ return;
347
+ }
348
+ const url = typeof payload.url === "string" && payload.url ? payload.url : "/";
349
+ const transformedHtml = await server.transformIndexHtml(url, payload.html, url);
323
350
  res.statusCode = 200;
324
351
  res.setHeader("Content-Type", "text/html");
325
352
  res.end(transformedHtml);
326
- return;
327
353
  } catch (e) {
328
- const relIndexPath = path.relative(server.config.root, indexPath);
329
- resolvedConfig.logger.error(`Error serving index.html from ${relIndexPath}: ${e instanceof Error ? e.message : e}`);
330
- next(e);
331
- return;
354
+ resolvedConfig.logger.error(`Error transforming index.html: ${e instanceof Error ? e.message : e}`);
355
+ res.statusCode = 500;
356
+ res.setHeader("Content-Type", "text/plain");
357
+ res.end("Error transforming HTML");
332
358
  }
359
+ return;
333
360
  }
334
- if (!indexPath && (req.url === "/" || req.url === "/index.html")) {
361
+ if (!isRootRequest) {
362
+ next();
363
+ return;
364
+ }
365
+ if (pluginConfig.inertiaMode) {
335
366
  try {
336
367
  const placeholderPath = path.join(dirname(), "dev-server-index.html");
337
368
  const placeholderContent = await fs.promises.readFile(placeholderPath, "utf-8");
@@ -345,7 +376,33 @@ function resolveLitestarPlugin(pluginConfig) {
345
376
  }
346
377
  return;
347
378
  }
348
- next();
379
+ const indexPath = await findIndexHtmlPath(server, pluginConfig);
380
+ if (indexPath) {
381
+ try {
382
+ const htmlContent = await fs.promises.readFile(indexPath, "utf-8");
383
+ const transformedHtml = await server.transformIndexHtml(requestUrl, htmlContent, requestUrl);
384
+ res.statusCode = 200;
385
+ res.setHeader("Content-Type", "text/html");
386
+ res.end(transformedHtml);
387
+ return;
388
+ } catch (e) {
389
+ const relIndexPath = path.relative(server.config.root, indexPath);
390
+ resolvedConfig.logger.error(`Error serving index.html from ${relIndexPath}: ${e instanceof Error ? e.message : e}`);
391
+ next(e);
392
+ return;
393
+ }
394
+ }
395
+ try {
396
+ const placeholderPath = path.join(dirname(), "dev-server-index.html");
397
+ const placeholderContent = await fs.promises.readFile(placeholderPath, "utf-8");
398
+ res.statusCode = 200;
399
+ res.setHeader("Content-Type", "text/html");
400
+ res.end(placeholderContent.replace(/{{ APP_URL }}/g, appUrl));
401
+ } catch (e) {
402
+ resolvedConfig.logger.error(`Error serving placeholder index.html: ${e instanceof Error ? e.message : e}`);
403
+ res.statusCode = 404;
404
+ res.end("Not Found (Error loading placeholder)");
405
+ }
349
406
  });
350
407
  }
351
408
  };
@@ -512,9 +569,11 @@ function resolvePluginConfig(config) {
512
569
  }
513
570
  const inertiaMode = resolvedConfig.inertiaMode ?? (pythonDefaults?.mode === "hybrid" || pythonDefaults?.mode === "inertia");
514
571
  const effectiveResourceDir = resolvedConfig.resourceDir ?? pythonDefaults?.resourceDir ?? "src";
572
+ const deployAssetUrlRaw = resolvedConfig.deployAssetUrl ?? pythonDefaults?.deployAssetUrl ?? void 0;
515
573
  const result = {
516
574
  input: resolvedConfig.input,
517
575
  assetUrl: normalizeAssetUrl(resolvedConfig.assetUrl ?? pythonDefaults?.assetUrl ?? "/static/"),
576
+ deployAssetUrl: typeof deployAssetUrlRaw === "string" ? normalizeAssetUrl(deployAssetUrlRaw) : void 0,
518
577
  resourceDir: effectiveResourceDir,
519
578
  bundleDir: resolvedConfig.bundleDir ?? pythonDefaults?.bundleDir ?? "public",
520
579
  staticDir: resolvedConfig.staticDir ?? pythonDefaults?.staticDir ?? path.join(effectiveResourceDir, "public"),
@@ -549,7 +608,8 @@ function validateAgainstPythonDefaults(resolved, pythonDefaults, userConfig) {
549
608
  if (userConfig.staticDir !== void 0 && hasPythonValue(pythonDefaults.staticDir) && resolved.staticDir !== pythonDefaults.staticDir) {
550
609
  warnings.push(`staticDir: vite.config.ts="${resolved.staticDir}" differs from Python="${pythonDefaults.staticDir}"`);
551
610
  }
552
- if (pythonDefaults.ssrEnabled && userConfig.ssrOutDir !== void 0 && hasPythonValue(pythonDefaults.ssrOutDir) && resolved.ssrOutDir !== pythonDefaults.ssrOutDir) {
611
+ const frameworkMode = pythonDefaults.mode === "framework" || pythonDefaults.mode === "ssr" || pythonDefaults.mode === "ssg";
612
+ if (frameworkMode && userConfig.ssrOutDir !== void 0 && hasPythonValue(pythonDefaults.ssrOutDir) && resolved.ssrOutDir !== pythonDefaults.ssrOutDir) {
553
613
  warnings.push(`ssrOutDir: vite.config.ts="${resolved.ssrOutDir}" differs from Python="${pythonDefaults.ssrOutDir}"`);
554
614
  }
555
615
  if (warnings.length > 0) {
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Base types for Inertia.js type generation.
3
+ *
4
+ * These types are generated by `litestar assets generate-types` in your
5
+ * project's `generated/page-props.ts` file. This module provides fallback
6
+ * types for when the generated file hasn't been created yet.
7
+ *
8
+ * To customize types, edit your project's `generated/page-props.user.ts` file.
9
+ */
10
+ /** User interface - customize in page-props.user.ts */
11
+ export interface User {
12
+ }
13
+ /** Authentication data interface */
14
+ export interface AuthData {
15
+ }
16
+ /** Flash messages interface */
17
+ export interface FlashMessages {
18
+ }
19
+ /** User-defined shared props - customize in page-props.user.ts */
20
+ export interface SharedProps {
21
+ }
22
+ /** Generated shared props (populated by page-props.ts) */
23
+ export interface GeneratedSharedProps {
24
+ }
25
+ /** Full shared props = generated + user-defined */
26
+ export type FullSharedProps = GeneratedSharedProps & SharedProps & {
27
+ [key: string]: unknown;
28
+ };
29
+ /** Page props mapped by component name (populated by page-props.ts) */
30
+ export interface PageProps {
31
+ }
32
+ /** Component name union type */
33
+ export type ComponentName = keyof PageProps;
34
+ /** Type-safe props for a specific component */
35
+ export type InertiaPageProps<C extends ComponentName> = PageProps[C];
36
+ /** Get props type for a specific page component */
37
+ export type PagePropsFor<C extends ComponentName> = PageProps[C];
File without changes
@@ -1,7 +1,12 @@
1
+ /**
2
+ * Detect the executor from .litestar.json or environment.
3
+ * Priority: LITESTAR_VITE_RUNTIME env > .litestar.json executor > lockfile detection > 'node'
4
+ */
5
+ export declare function detectExecutor(): string;
1
6
  export declare function resolveInstallHint(pkg?: string): string;
2
7
  /**
3
8
  * Resolves the package executor command based on runtime.
4
- * Priority: explicit executor > LITESTAR_VITE_RUNTIME env > 'node' default
9
+ * Priority: explicit executor > .litestar.json > LITESTAR_VITE_RUNTIME env > lockfile detection > 'npx'
5
10
  *
6
11
  * @param pkg - The package command to execute (e.g., "@hey-api/openapi-ts -i schema.json -o src/types")
7
12
  * @param executor - Optional explicit executor override
@@ -1,5 +1,37 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ function detectExecutor() {
4
+ const envRuntime = (process.env.LITESTAR_VITE_RUNTIME ?? "").toLowerCase();
5
+ if (envRuntime) return envRuntime;
6
+ const configPath = process.env.LITESTAR_VITE_CONFIG_PATH ?? path.resolve(process.cwd(), ".litestar.json");
7
+ if (fs.existsSync(configPath)) {
8
+ try {
9
+ const raw = fs.readFileSync(configPath, "utf8");
10
+ const data = JSON.parse(raw);
11
+ const executor = data?.executor;
12
+ if (typeof executor === "string" && executor.trim()) {
13
+ return executor.trim().toLowerCase();
14
+ }
15
+ } catch {
16
+ }
17
+ }
18
+ const cwd = process.cwd();
19
+ if (fs.existsSync(path.join(cwd, "bun.lockb")) || fs.existsSync(path.join(cwd, "bun.lock"))) {
20
+ return "bun";
21
+ }
22
+ if (fs.existsSync(path.join(cwd, "pnpm-lock.yaml"))) {
23
+ return "pnpm";
24
+ }
25
+ if (fs.existsSync(path.join(cwd, "yarn.lock"))) {
26
+ return "yarn";
27
+ }
28
+ if (fs.existsSync(path.join(cwd, "deno.lock"))) {
29
+ return "deno";
30
+ }
31
+ return "node";
32
+ }
1
33
  function resolveInstallHint(pkg = "@hey-api/openapi-ts") {
2
- const runtime = (process.env.LITESTAR_VITE_RUNTIME ?? "").toLowerCase();
34
+ const runtime = detectExecutor();
3
35
  switch (runtime) {
4
36
  case "bun":
5
37
  return `bun add -d ${pkg}`;
@@ -17,7 +49,7 @@ function resolveInstallHint(pkg = "@hey-api/openapi-ts") {
17
49
  return `npm install -D ${pkg}`;
18
50
  }
19
51
  function resolvePackageExecutor(pkg, executor) {
20
- const runtime = (executor ?? process.env.LITESTAR_VITE_RUNTIME ?? "").toLowerCase();
52
+ const runtime = executor || detectExecutor();
21
53
  switch (runtime) {
22
54
  case "bun":
23
55
  return `bunx ${pkg}`;
@@ -32,6 +64,7 @@ function resolvePackageExecutor(pkg, executor) {
32
64
  }
33
65
  }
34
66
  export {
67
+ detectExecutor,
35
68
  resolveInstallHint,
36
69
  resolvePackageExecutor
37
70
  };
@@ -8,7 +8,7 @@
8
8
  *
9
9
  * @module
10
10
  */
11
- export type BridgeMode = "spa" | "template" | "htmx" | "hybrid" | "inertia" | "ssr" | "ssg" | "external";
11
+ export type BridgeMode = "spa" | "template" | "htmx" | "hybrid" | "inertia" | "framework" | "ssr" | "ssg" | "external";
12
12
  export type BridgeProxyMode = "vite" | "direct" | "proxy" | null;
13
13
  export type BridgeExecutor = "node" | "bun" | "deno" | "yarn" | "pnpm";
14
14
  export interface BridgeTypesConfig {
@@ -23,8 +23,13 @@ export interface BridgeTypesConfig {
23
23
  generatePageProps: boolean;
24
24
  globalRoute: boolean;
25
25
  }
26
+ export interface BridgeSpaConfig {
27
+ /** Use script element instead of data-page attribute for Inertia page data */
28
+ useScriptElement: boolean;
29
+ }
26
30
  export interface BridgeSchema {
27
31
  assetUrl: string;
32
+ deployAssetUrl: string | null;
28
33
  bundleDir: string;
29
34
  resourceDir: string;
30
35
  staticDir: string;
@@ -34,9 +39,9 @@ export interface BridgeSchema {
34
39
  proxyMode: BridgeProxyMode;
35
40
  host: string;
36
41
  port: number;
37
- ssrEnabled: boolean;
38
42
  ssrOutDir: string | null;
39
43
  types: BridgeTypesConfig | null;
44
+ spa: BridgeSpaConfig | null;
40
45
  executor: BridgeExecutor;
41
46
  logging: {
42
47
  level: "quiet" | "normal" | "verbose";
@@ -2,6 +2,7 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  const allowedTopLevelKeys = /* @__PURE__ */ new Set([
4
4
  "assetUrl",
5
+ "deployAssetUrl",
5
6
  "bundleDir",
6
7
  "resourceDir",
7
8
  "staticDir",
@@ -11,14 +12,14 @@ const allowedTopLevelKeys = /* @__PURE__ */ new Set([
11
12
  "proxyMode",
12
13
  "host",
13
14
  "port",
14
- "ssrEnabled",
15
15
  "ssrOutDir",
16
16
  "types",
17
+ "spa",
17
18
  "executor",
18
19
  "logging",
19
20
  "litestarVersion"
20
21
  ]);
21
- const allowedModes = /* @__PURE__ */ new Set(["spa", "template", "htmx", "hybrid", "inertia", "ssr", "ssg", "external"]);
22
+ const allowedModes = /* @__PURE__ */ new Set(["spa", "template", "htmx", "hybrid", "inertia", "framework", "ssr", "ssg", "external"]);
22
23
  const allowedProxyModes = /* @__PURE__ */ new Set(["vite", "direct", "proxy"]);
23
24
  const allowedExecutors = /* @__PURE__ */ new Set(["node", "bun", "deno", "yarn", "pnpm"]);
24
25
  const allowedLogLevels = /* @__PURE__ */ new Set(["quiet", "normal", "verbose"]);
@@ -106,6 +107,12 @@ function parseLogging(value) {
106
107
  const timestamps = assertBoolean(obj, "timestamps");
107
108
  return { level, showPathsAbsolute, suppressNpmOutput, suppressViteBanner, timestamps };
108
109
  }
110
+ function parseSpaConfig(value) {
111
+ if (value === null || value === void 0) return null;
112
+ const obj = assertObject(value, "spa");
113
+ const useScriptElement = assertBoolean(obj, "useScriptElement");
114
+ return { useScriptElement };
115
+ }
109
116
  function parseBridgeSchema(value) {
110
117
  const obj = assertObject(value, "root");
111
118
  for (const key of Object.keys(obj)) {
@@ -114,6 +121,7 @@ function parseBridgeSchema(value) {
114
121
  }
115
122
  }
116
123
  const assetUrl = assertString(obj, "assetUrl");
124
+ const deployAssetUrl = assertNullableString(obj, "deployAssetUrl");
117
125
  const bundleDir = assertString(obj, "bundleDir");
118
126
  const resourceDir = assertString(obj, "resourceDir");
119
127
  const staticDir = assertString(obj, "staticDir");
@@ -123,14 +131,15 @@ function parseBridgeSchema(value) {
123
131
  const proxyMode = assertProxyMode(obj.proxyMode);
124
132
  const host = assertString(obj, "host");
125
133
  const port = assertNumber(obj, "port");
126
- const ssrEnabled = assertBoolean(obj, "ssrEnabled");
127
134
  const ssrOutDir = assertNullableString(obj, "ssrOutDir");
128
135
  const types = parseTypesConfig(obj.types);
136
+ const spa = parseSpaConfig(obj.spa);
129
137
  const executor = assertEnum(obj.executor, "executor", allowedExecutors);
130
138
  const logging = parseLogging(obj.logging);
131
139
  const litestarVersion = assertString(obj, "litestarVersion");
132
140
  return {
133
141
  assetUrl,
142
+ deployAssetUrl,
134
143
  bundleDir,
135
144
  resourceDir,
136
145
  staticDir,
@@ -140,9 +149,9 @@ function parseBridgeSchema(value) {
140
149
  proxyMode,
141
150
  host,
142
151
  port,
143
- ssrEnabled,
144
152
  ssrOutDir,
145
153
  types,
154
+ spa,
146
155
  executor,
147
156
  logging,
148
157
  litestarVersion
@@ -1,4 +1,6 @@
1
1
  /**
2
2
  * Generate `page-props.ts` from `inertia-pages.json` metadata.
3
+ *
4
+ * @returns true if file was changed, false if unchanged
3
5
  */
4
- export declare function emitPagePropsTypes(pagesPath: string, outputDir: string): Promise<void>;
6
+ export declare function emitPagePropsTypes(pagesPath: string, outputDir: string): Promise<boolean>;
@@ -1,5 +1,6 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
+ import { writeIfChanged } from "./write-if-changed.js";
3
4
  async function emitPagePropsTypes(pagesPath, outputDir) {
4
5
  const contents = await fs.promises.readFile(pagesPath, "utf-8");
5
6
  const json = JSON.parse(contents);
@@ -16,18 +17,9 @@ async function emitPagePropsTypes(pagesPath, outputDir) {
16
17
  if (includeDefaultAuth) {
17
18
  userTypes = `/**
18
19
  * Default User interface - minimal baseline for common auth patterns.
19
- * Users extend this via module augmentation with their full user model.
20
- *
21
- * @example
22
- * declare module 'litestar-vite-plugin/inertia' {
23
- * interface User {
24
- * avatarUrl?: string | null
25
- * roles: Role[]
26
- * teams: Team[]
27
- * }
28
- * }
20
+ * Extend by adding properties to UserExtensions in page-props.user.ts
29
21
  */
30
- export interface User {
22
+ export interface User extends UserExtensions {
31
23
  id: string
32
24
  email: string
33
25
  name?: string | null
@@ -46,23 +38,13 @@ export interface AuthData {
46
38
  `;
47
39
  } else {
48
40
  userTypes = `/**
49
- * User interface - define via module augmentation.
50
- * Default auth types are disabled.
51
- *
52
- * @example
53
- * declare module 'litestar-vite-plugin/inertia' {
54
- * interface User {
55
- * uuid: string
56
- * username: string
57
- * }
58
- * }
41
+ * User interface - add properties to UserExtensions in page-props.user.ts
59
42
  */
60
- export interface User {}
43
+ export interface User extends UserExtensions {}
61
44
 
62
45
  `;
63
46
  authTypes = `/**
64
- * AuthData interface - define via module augmentation.
65
- * Default auth types are disabled.
47
+ * AuthData interface - define your auth structure here or in page-props.user.ts
66
48
  */
67
49
  export interface AuthData {}
68
50
 
@@ -163,7 +145,7 @@ export interface FlashMessages {}
163
145
  const availableApiTypes = /* @__PURE__ */ new Set();
164
146
  if (fs.existsSync(apiTypesPath)) {
165
147
  const content = await fs.promises.readFile(apiTypesPath, "utf-8");
166
- for (const match of content.matchAll(/export (?:type|interface|enum|class) (\\w+)/g)) {
148
+ for (const match of content.matchAll(/export (?:type|interface|enum|class) (\w+)/g)) {
167
149
  if (match[1]) {
168
150
  availableApiTypes.add(match[1]);
169
151
  }
@@ -184,7 +166,13 @@ export interface FlashMessages {}
184
166
  }
185
167
  }
186
168
  if (unresolvedTypes.length > 0) {
187
- console.warn(`litestar-vite: unresolved Inertia props types: ${unresolvedTypes.join(", ")}. Add them to TypeGenConfig.type_import_paths or include them in OpenAPI.`);
169
+ console.warn(
170
+ `litestar-vite: Unresolved Inertia props types: ${unresolvedTypes.join(", ")}.
171
+ To fix:
172
+ 1. Add to OpenAPI by including in route return types
173
+ 2. Or configure TypeGenConfig.type_import_paths:
174
+ types=TypeGenConfig(type_import_paths={"${unresolvedTypes[0]}": "@/types/custom"})`
175
+ );
188
176
  }
189
177
  let importStatement = "";
190
178
  if (apiImports.length > 0) {
@@ -211,6 +199,9 @@ export interface FlashMessages {}
211
199
  const body = `// AUTO-GENERATED by litestar-vite. Do not edit.
212
200
  /* eslint-disable */
213
201
 
202
+ // Import user-defined type extensions (edit page-props.user.ts to customize)
203
+ import type { UserExtensions, SharedPropsExtensions } from "./page-props.user"
204
+
214
205
  ${importStatement}${userTypes}${authTypes}${flashTypes}/**
215
206
  * Generated shared props (always present).
216
207
  * Includes built-in props + static config props.
@@ -221,26 +212,13 @@ ${generatedSharedPropLines.join("\n")}
221
212
 
222
213
  /**
223
214
  * User-defined shared props for dynamic share() calls in guards/middleware.
224
- * Extend this interface via module augmentation.
225
- *
226
- * @example
227
- * declare module 'litestar-vite-plugin/inertia' {
228
- * interface User {
229
- * avatarUrl?: string | null
230
- * roles: Role[]
231
- * teams: Team[]
232
- * }
233
- * interface SharedProps {
234
- * locale?: string
235
- * currentTeam?: CurrentTeam
236
- * }
237
- * }
215
+ * Extend by adding properties to SharedPropsExtensions in page-props.user.ts
238
216
  */
239
- export interface SharedProps {
240
- }
217
+ export type SharedProps = SharedPropsExtensions
241
218
 
242
- /** Full shared props = generated + user-defined */
243
- export type FullSharedProps = GeneratedSharedProps & SharedProps
219
+ /** Full shared props = generated + user-defined.
220
+ * Includes index signature for compatibility with Inertia's usePage<T>(). */
221
+ export type FullSharedProps = GeneratedSharedProps & SharedProps & { [key: string]: unknown }
244
222
 
245
223
  /** Page props mapped by component name */
246
224
  export interface PageProps {
@@ -255,13 +233,57 @@ export type InertiaPageProps<C extends ComponentName> = PageProps[C]
255
233
 
256
234
  /** Get props type for a specific page component */
257
235
  export type PagePropsFor<C extends ComponentName> = PageProps[C]
236
+ `;
237
+ const result = await writeIfChanged(outFile, body, { encoding: "utf-8" });
238
+ const userStubFile = path.join(outDir, "page-props.user.ts");
239
+ if (!fs.existsSync(userStubFile)) {
240
+ const userStub = `/**
241
+ * User-defined type extensions for Inertia page props.
242
+ * This file is generated ONCE and never overwritten - edit freely!
243
+ *
244
+ * Add properties to these interfaces to extend the generated types.
245
+ * The main page-props.ts file imports and uses these extensions.
246
+ */
247
+
248
+ /**
249
+ * Extend the User interface with additional properties.
250
+ *
251
+ * @example
252
+ * export interface UserExtensions {
253
+ * avatarUrl?: string | null
254
+ * roles: string[]
255
+ * teams: Team[]
256
+ * }
257
+ */
258
+ export interface UserExtensions {
259
+ // Add your custom User properties here
260
+ }
258
261
 
259
- // Re-export for module augmentation
260
- declare module "litestar-vite-plugin/inertia" {
261
- export { User, AuthData, FlashMessages, SharedProps, GeneratedSharedProps, FullSharedProps, PageProps, ComponentName, InertiaPageProps, PagePropsFor }
262
+ /**
263
+ * Extend SharedProps with session-based or dynamic properties.
264
+ *
265
+ * @example
266
+ * export interface SharedPropsExtensions {
267
+ * locale?: string
268
+ * currentTeam?: {
269
+ * teamId: string
270
+ * teamName: string
271
+ * }
272
+ * }
273
+ */
274
+ export interface SharedPropsExtensions {
275
+ // Add your custom shared props here
262
276
  }
277
+
278
+ // Export custom types that can be used in page props
279
+ // export interface CurrentTeam {
280
+ // teamId: string
281
+ // teamName: string
282
+ // }
263
283
  `;
264
- await fs.promises.writeFile(outFile, body, "utf-8");
284
+ await fs.promises.writeFile(userStubFile, userStub, "utf-8");
285
+ }
286
+ return result.changed;
265
287
  }
266
288
  export {
267
289
  emitPagePropsTypes