litestar-vite-plugin 0.15.0-rc.1 → 0.15.0-rc.3

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
  )
@@ -6,23 +6,33 @@
6
6
  *
7
7
  * @example
8
8
  * ```ts
9
+ * // CSRF utilities
9
10
  * import { getCsrfToken, csrfFetch } from 'litestar-vite-plugin/helpers'
10
11
  *
11
- * // Get CSRF token
12
12
  * const token = getCsrfToken()
13
+ * await csrfFetch('/api/submit', { method: 'POST', body: JSON.stringify(data) })
14
+ * ```
15
+ *
16
+ * @example
17
+ * ```ts
18
+ * // Route matching utilities
19
+ * import { createRouteHelpers } from 'litestar-vite-plugin/helpers'
20
+ * import { routeDefinitions } from '@/generated/routes'
13
21
  *
14
- * // Make a fetch request with CSRF token
15
- * await csrfFetch('/api/submit', {
16
- * method: 'POST',
17
- * body: JSON.stringify(data),
18
- * })
22
+ * const { isCurrentRoute, currentRoute, toRoute, isRoute } = createRouteHelpers(routeDefinitions)
23
+ *
24
+ * // Highlight active nav items
25
+ * if (isCurrentRoute('dashboard')) { ... }
26
+ *
27
+ * // Match with wildcards
28
+ * if (isCurrentRoute('book_*')) { ... }
19
29
  * ```
20
30
  *
21
- * For type-safe routing, import from your generated routes file:
31
+ * @example
22
32
  * ```ts
23
- * import { route, routeDefinitions, type RouteName } from '@/generated/routes'
33
+ * // Type-safe URL generation (from generated routes)
34
+ * import { route } from '@/generated/routes'
24
35
  *
25
- * // Type-safe URL generation
26
36
  * const url = route('user_detail', { user_id: 123 }) // Compile-time checked!
27
37
  * ```
28
38
  *
@@ -30,3 +40,4 @@
30
40
  */
31
41
  export { csrfFetch, csrfHeaders, getCsrfToken } from "./csrf.js";
32
42
  export { addDirective, registerHtmxExtension, setDebug as setHtmxDebug, swapJson } from "./htmx.js";
43
+ export { createRouteHelpers, currentRoute, isCurrentRoute, isRoute, type RouteDefinition, type RouteDefinitions, type RouteHelpers, toRoute, } from "./routes.js";
@@ -6,23 +6,33 @@
6
6
  *
7
7
  * @example
8
8
  * ```ts
9
+ * // CSRF utilities
9
10
  * import { getCsrfToken, csrfFetch } from 'litestar-vite-plugin/helpers'
10
11
  *
11
- * // Get CSRF token
12
12
  * const token = getCsrfToken()
13
+ * await csrfFetch('/api/submit', { method: 'POST', body: JSON.stringify(data) })
14
+ * ```
15
+ *
16
+ * @example
17
+ * ```ts
18
+ * // Route matching utilities
19
+ * import { createRouteHelpers } from 'litestar-vite-plugin/helpers'
20
+ * import { routeDefinitions } from '@/generated/routes'
13
21
  *
14
- * // Make a fetch request with CSRF token
15
- * await csrfFetch('/api/submit', {
16
- * method: 'POST',
17
- * body: JSON.stringify(data),
18
- * })
22
+ * const { isCurrentRoute, currentRoute, toRoute, isRoute } = createRouteHelpers(routeDefinitions)
23
+ *
24
+ * // Highlight active nav items
25
+ * if (isCurrentRoute('dashboard')) { ... }
26
+ *
27
+ * // Match with wildcards
28
+ * if (isCurrentRoute('book_*')) { ... }
19
29
  * ```
20
30
  *
21
- * For type-safe routing, import from your generated routes file:
31
+ * @example
22
32
  * ```ts
23
- * import { route, routeDefinitions, type RouteName } from '@/generated/routes'
33
+ * // Type-safe URL generation (from generated routes)
34
+ * import { route } from '@/generated/routes'
24
35
  *
25
- * // Type-safe URL generation
26
36
  * const url = route('user_detail', { user_id: 123 }) // Compile-time checked!
27
37
  * ```
28
38
  *
@@ -32,3 +42,5 @@
32
42
  export { csrfFetch, csrfHeaders, getCsrfToken } from "./csrf.js";
33
43
  // HTMX utilities
34
44
  export { addDirective, registerHtmxExtension, setDebug as setHtmxDebug, swapJson } from "./htmx.js";
45
+ // Route matching utilities
46
+ export { createRouteHelpers, currentRoute, isCurrentRoute, isRoute, toRoute, } from "./routes.js";
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Route matching utilities for Litestar applications.
3
+ *
4
+ * These helpers work with the generated `routeDefinitions` from your routes.ts file
5
+ * to provide runtime route matching capabilities.
6
+ *
7
+ * **IMPORTANT**: This file serves as both the runtime library AND the reference
8
+ * implementation for the Python code generator. The same logic is also generated
9
+ * inline in `routes.ts` by `src/py/litestar_vite/_codegen/routes.py`.
10
+ *
11
+ * **If you modify the logic here, you MUST also update the Python generator!**
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * import { routeDefinitions } from '@/generated/routes'
16
+ * import { createRouteHelpers } from 'litestar-vite-plugin/helpers'
17
+ *
18
+ * const { isCurrentRoute, currentRoute, toRoute, isRoute } = createRouteHelpers(routeDefinitions)
19
+ *
20
+ * // Check if current URL matches a route
21
+ * if (isCurrentRoute('dashboard')) {
22
+ * // highlight nav item
23
+ * }
24
+ * ```
25
+ *
26
+ * @module
27
+ */
28
+ /**
29
+ * Route definition structure from generated routes.
30
+ */
31
+ export interface RouteDefinition {
32
+ path: string;
33
+ methods: readonly string[];
34
+ method: string;
35
+ pathParams: readonly string[];
36
+ queryParams: readonly string[];
37
+ component?: string;
38
+ }
39
+ /**
40
+ * Map of route names to their definitions.
41
+ */
42
+ export type RouteDefinitions = Record<string, RouteDefinition>;
43
+ /**
44
+ * Convert a URL to its corresponding route name.
45
+ *
46
+ * @param url - URL or path to match (query strings and hashes are stripped)
47
+ * @param routes - The routeDefinitions object from generated routes
48
+ * @returns The matching route name, or null if no match found
49
+ *
50
+ * @example
51
+ * toRoute('/api/books', routeDefinitions) // 'books'
52
+ * toRoute('/api/books/123', routeDefinitions) // 'book_detail'
53
+ * toRoute('/unknown', routeDefinitions) // null
54
+ */
55
+ export declare function toRoute<T extends string>(url: string, routes: Record<T, RouteDefinition>): T | null;
56
+ /**
57
+ * Get the current route name based on the browser URL.
58
+ *
59
+ * Returns null in SSR/non-browser environments.
60
+ *
61
+ * @param routes - The routeDefinitions object from generated routes
62
+ * @returns Current route name, or null if no match or not in browser
63
+ *
64
+ * @example
65
+ * // On page /api/books/123
66
+ * currentRoute(routeDefinitions) // 'book_detail'
67
+ */
68
+ export declare function currentRoute<T extends string>(routes: Record<T, RouteDefinition>): T | null;
69
+ /**
70
+ * Check if a URL matches a route name or pattern.
71
+ *
72
+ * Supports wildcard patterns with `*` to match multiple routes.
73
+ *
74
+ * @param url - URL or path to check
75
+ * @param pattern - Route name or pattern (e.g., 'books', 'book_*', '*_detail')
76
+ * @param routes - The routeDefinitions object from generated routes
77
+ * @returns True if the URL matches the route pattern
78
+ *
79
+ * @example
80
+ * isRoute('/api/books', 'books', routeDefinitions) // true
81
+ * isRoute('/api/books/123', 'book_detail', routeDefinitions) // true
82
+ * isRoute('/api/books/123', 'book_*', routeDefinitions) // true (wildcard)
83
+ * isRoute('/api/users', 'book_*', routeDefinitions) // false
84
+ */
85
+ export declare function isRoute<T extends string>(url: string, pattern: string, routes: Record<T, RouteDefinition>): boolean;
86
+ /**
87
+ * Check if the current browser URL matches a route name or pattern.
88
+ *
89
+ * Supports wildcard patterns with `*` to match multiple routes.
90
+ * Returns false in SSR/non-browser environments.
91
+ *
92
+ * @param pattern - Route name or pattern (e.g., 'books', 'book_*', '*_page')
93
+ * @param routes - The routeDefinitions object from generated routes
94
+ * @returns True if current URL matches the route pattern
95
+ *
96
+ * @example
97
+ * // On page /books
98
+ * isCurrentRoute('books_page', routeDefinitions) // true
99
+ * isCurrentRoute('book_*', routeDefinitions) // false (no match)
100
+ * isCurrentRoute('*_page', routeDefinitions) // true (wildcard)
101
+ */
102
+ export declare function isCurrentRoute<T extends string>(pattern: string, routes: Record<T, RouteDefinition>): boolean;
103
+ /**
104
+ * Route helpers interface returned by createRouteHelpers.
105
+ */
106
+ export interface RouteHelpers<T extends string> {
107
+ /** Convert URL to route name */
108
+ toRoute: (url: string) => T | null;
109
+ /** Get current route name (SSR-safe) */
110
+ currentRoute: () => T | null;
111
+ /** Check if URL matches route pattern */
112
+ isRoute: (url: string, pattern: string) => boolean;
113
+ /** Check if current URL matches route pattern */
114
+ isCurrentRoute: (pattern: string) => boolean;
115
+ }
116
+ /**
117
+ * Create route helpers bound to a specific routeDefinitions object.
118
+ *
119
+ * This is the recommended way to use route helpers - it creates bound functions
120
+ * so you don't need to pass routeDefinitions to every call.
121
+ *
122
+ * @param routes - The routeDefinitions object from your generated routes
123
+ * @returns Object with bound route helper functions
124
+ *
125
+ * @example
126
+ * ```ts
127
+ * import { routeDefinitions } from '@/generated/routes'
128
+ * import { createRouteHelpers } from 'litestar-vite-plugin/helpers'
129
+ *
130
+ * export const { isCurrentRoute, currentRoute, toRoute, isRoute } = createRouteHelpers(routeDefinitions)
131
+ *
132
+ * // Now use without passing routes:
133
+ * if (isCurrentRoute('dashboard')) {
134
+ * // ...
135
+ * }
136
+ * ```
137
+ */
138
+ export declare function createRouteHelpers<T extends string>(routes: Record<T, RouteDefinition>): RouteHelpers<T>;
@@ -0,0 +1,181 @@
1
+ /**
2
+ * Route matching utilities for Litestar applications.
3
+ *
4
+ * These helpers work with the generated `routeDefinitions` from your routes.ts file
5
+ * to provide runtime route matching capabilities.
6
+ *
7
+ * **IMPORTANT**: This file serves as both the runtime library AND the reference
8
+ * implementation for the Python code generator. The same logic is also generated
9
+ * inline in `routes.ts` by `src/py/litestar_vite/_codegen/routes.py`.
10
+ *
11
+ * **If you modify the logic here, you MUST also update the Python generator!**
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * import { routeDefinitions } from '@/generated/routes'
16
+ * import { createRouteHelpers } from 'litestar-vite-plugin/helpers'
17
+ *
18
+ * const { isCurrentRoute, currentRoute, toRoute, isRoute } = createRouteHelpers(routeDefinitions)
19
+ *
20
+ * // Check if current URL matches a route
21
+ * if (isCurrentRoute('dashboard')) {
22
+ * // highlight nav item
23
+ * }
24
+ * ```
25
+ *
26
+ * @module
27
+ */
28
+ /** Cache for compiled route patterns */
29
+ const patternCache = new Map();
30
+ /**
31
+ * Compile a route path pattern to a regex for URL matching.
32
+ * Results are cached for performance.
33
+ *
34
+ * @param path - Route path with {param} placeholders
35
+ * @returns Compiled regex pattern
36
+ */
37
+ function compilePattern(path) {
38
+ const cached = patternCache.get(path);
39
+ if (cached)
40
+ return cached;
41
+ // Escape special regex characters except { }
42
+ let pattern = path.replace(/[.*+?^$|()[\]]/g, "\\$&");
43
+ // Replace {param} or {param:type} with matchers
44
+ pattern = pattern.replace(/\{([^}]+)\}/g, (_match, paramSpec) => {
45
+ const paramType = paramSpec.includes(":") ? paramSpec.split(":")[1] : "str";
46
+ switch (paramType) {
47
+ case "uuid":
48
+ return "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}";
49
+ case "path":
50
+ return ".*";
51
+ case "int":
52
+ return "\\d+";
53
+ default:
54
+ return "[^/]+";
55
+ }
56
+ });
57
+ const regex = new RegExp(`^${pattern}$`, "i");
58
+ patternCache.set(path, regex);
59
+ return regex;
60
+ }
61
+ /**
62
+ * Convert a URL to its corresponding route name.
63
+ *
64
+ * @param url - URL or path to match (query strings and hashes are stripped)
65
+ * @param routes - The routeDefinitions object from generated routes
66
+ * @returns The matching route name, or null if no match found
67
+ *
68
+ * @example
69
+ * toRoute('/api/books', routeDefinitions) // 'books'
70
+ * toRoute('/api/books/123', routeDefinitions) // 'book_detail'
71
+ * toRoute('/unknown', routeDefinitions) // null
72
+ */
73
+ export function toRoute(url, routes) {
74
+ // Strip query string and hash
75
+ const path = url.split("?")[0].split("#")[0];
76
+ // Normalize: remove trailing slash except for root
77
+ const normalized = path === "/" ? path : path.replace(/\/$/, "");
78
+ for (const [name, def] of Object.entries(routes)) {
79
+ if (compilePattern(def.path).test(normalized)) {
80
+ return name;
81
+ }
82
+ }
83
+ return null;
84
+ }
85
+ /**
86
+ * Get the current route name based on the browser URL.
87
+ *
88
+ * Returns null in SSR/non-browser environments.
89
+ *
90
+ * @param routes - The routeDefinitions object from generated routes
91
+ * @returns Current route name, or null if no match or not in browser
92
+ *
93
+ * @example
94
+ * // On page /api/books/123
95
+ * currentRoute(routeDefinitions) // 'book_detail'
96
+ */
97
+ export function currentRoute(routes) {
98
+ if (typeof window === "undefined")
99
+ return null;
100
+ return toRoute(window.location.pathname, routes);
101
+ }
102
+ /**
103
+ * Check if a URL matches a route name or pattern.
104
+ *
105
+ * Supports wildcard patterns with `*` to match multiple routes.
106
+ *
107
+ * @param url - URL or path to check
108
+ * @param pattern - Route name or pattern (e.g., 'books', 'book_*', '*_detail')
109
+ * @param routes - The routeDefinitions object from generated routes
110
+ * @returns True if the URL matches the route pattern
111
+ *
112
+ * @example
113
+ * isRoute('/api/books', 'books', routeDefinitions) // true
114
+ * isRoute('/api/books/123', 'book_detail', routeDefinitions) // true
115
+ * isRoute('/api/books/123', 'book_*', routeDefinitions) // true (wildcard)
116
+ * isRoute('/api/users', 'book_*', routeDefinitions) // false
117
+ */
118
+ export function isRoute(url, pattern, routes) {
119
+ const routeName = toRoute(url, routes);
120
+ if (!routeName)
121
+ return false;
122
+ // Escape special regex chars (except *), then convert * to .*
123
+ const escaped = pattern.replace(/[.+?^$|()[\]{}]/g, "\\$&");
124
+ const regex = new RegExp(`^${escaped.replace(/\*/g, ".*")}$`);
125
+ return regex.test(routeName);
126
+ }
127
+ /**
128
+ * Check if the current browser URL matches a route name or pattern.
129
+ *
130
+ * Supports wildcard patterns with `*` to match multiple routes.
131
+ * Returns false in SSR/non-browser environments.
132
+ *
133
+ * @param pattern - Route name or pattern (e.g., 'books', 'book_*', '*_page')
134
+ * @param routes - The routeDefinitions object from generated routes
135
+ * @returns True if current URL matches the route pattern
136
+ *
137
+ * @example
138
+ * // On page /books
139
+ * isCurrentRoute('books_page', routeDefinitions) // true
140
+ * isCurrentRoute('book_*', routeDefinitions) // false (no match)
141
+ * isCurrentRoute('*_page', routeDefinitions) // true (wildcard)
142
+ */
143
+ export function isCurrentRoute(pattern, routes) {
144
+ const current = currentRoute(routes);
145
+ if (!current)
146
+ return false;
147
+ // Escape special regex chars (except *), then convert * to .*
148
+ const escaped = pattern.replace(/[.+?^$|()[\]{}]/g, "\\$&");
149
+ const regex = new RegExp(`^${escaped.replace(/\*/g, ".*")}$`);
150
+ return regex.test(current);
151
+ }
152
+ /**
153
+ * Create route helpers bound to a specific routeDefinitions object.
154
+ *
155
+ * This is the recommended way to use route helpers - it creates bound functions
156
+ * so you don't need to pass routeDefinitions to every call.
157
+ *
158
+ * @param routes - The routeDefinitions object from your generated routes
159
+ * @returns Object with bound route helper functions
160
+ *
161
+ * @example
162
+ * ```ts
163
+ * import { routeDefinitions } from '@/generated/routes'
164
+ * import { createRouteHelpers } from 'litestar-vite-plugin/helpers'
165
+ *
166
+ * export const { isCurrentRoute, currentRoute, toRoute, isRoute } = createRouteHelpers(routeDefinitions)
167
+ *
168
+ * // Now use without passing routes:
169
+ * if (isCurrentRoute('dashboard')) {
170
+ * // ...
171
+ * }
172
+ * ```
173
+ */
174
+ export function createRouteHelpers(routes) {
175
+ return {
176
+ toRoute: (url) => toRoute(url, routes),
177
+ currentRoute: () => currentRoute(routes),
178
+ isRoute: (url, pattern) => isRoute(url, pattern, routes),
179
+ isCurrentRoute: (pattern) => isCurrentRoute(pattern, routes),
180
+ };
181
+ }
@@ -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
  };
@@ -510,11 +567,13 @@ function resolvePluginConfig(config) {
510
567
  typesConfig.pagePropsPath = path.join(typesConfig.output, "inertia-pages.json");
511
568
  }
512
569
  }
513
- const inertiaMode = resolvedConfig.inertiaMode ?? pythonDefaults?.mode === "inertia";
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) {