litestar-vite 0.15.0__py3-none-any.whl → 0.15.0rc2__py3-none-any.whl

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.
Files changed (55) hide show
  1. litestar_vite/_codegen/__init__.py +26 -0
  2. litestar_vite/_codegen/inertia.py +407 -0
  3. litestar_vite/{codegen/_openapi.py → _codegen/openapi.py} +11 -58
  4. litestar_vite/{codegen/_routes.py → _codegen/routes.py} +43 -110
  5. litestar_vite/{codegen/_ts.py → _codegen/ts.py} +19 -19
  6. litestar_vite/_handler/__init__.py +8 -0
  7. litestar_vite/{handler/_app.py → _handler/app.py} +29 -117
  8. litestar_vite/cli.py +254 -155
  9. litestar_vite/codegen.py +39 -0
  10. litestar_vite/commands.py +6 -0
  11. litestar_vite/{config/__init__.py → config.py} +726 -99
  12. litestar_vite/deploy.py +3 -14
  13. litestar_vite/doctor.py +6 -8
  14. litestar_vite/executor.py +1 -45
  15. litestar_vite/handler.py +9 -0
  16. litestar_vite/html_transform.py +5 -148
  17. litestar_vite/inertia/__init__.py +0 -24
  18. litestar_vite/inertia/_utils.py +0 -5
  19. litestar_vite/inertia/exception_handler.py +16 -22
  20. litestar_vite/inertia/helpers.py +18 -546
  21. litestar_vite/inertia/plugin.py +11 -77
  22. litestar_vite/inertia/request.py +0 -48
  23. litestar_vite/inertia/response.py +17 -113
  24. litestar_vite/inertia/types.py +0 -19
  25. litestar_vite/loader.py +7 -7
  26. litestar_vite/plugin.py +2184 -0
  27. litestar_vite/templates/angular/package.json.j2 +1 -2
  28. litestar_vite/templates/angular-cli/package.json.j2 +1 -2
  29. litestar_vite/templates/base/package.json.j2 +1 -2
  30. litestar_vite/templates/react-inertia/package.json.j2 +1 -2
  31. litestar_vite/templates/vue-inertia/package.json.j2 +1 -2
  32. {litestar_vite-0.15.0.dist-info → litestar_vite-0.15.0rc2.dist-info}/METADATA +5 -5
  33. {litestar_vite-0.15.0.dist-info → litestar_vite-0.15.0rc2.dist-info}/RECORD +36 -49
  34. litestar_vite/codegen/__init__.py +0 -48
  35. litestar_vite/codegen/_export.py +0 -229
  36. litestar_vite/codegen/_inertia.py +0 -619
  37. litestar_vite/codegen/_utils.py +0 -141
  38. litestar_vite/config/_constants.py +0 -97
  39. litestar_vite/config/_deploy.py +0 -70
  40. litestar_vite/config/_inertia.py +0 -241
  41. litestar_vite/config/_paths.py +0 -63
  42. litestar_vite/config/_runtime.py +0 -235
  43. litestar_vite/config/_spa.py +0 -93
  44. litestar_vite/config/_types.py +0 -94
  45. litestar_vite/handler/__init__.py +0 -9
  46. litestar_vite/inertia/precognition.py +0 -274
  47. litestar_vite/plugin/__init__.py +0 -687
  48. litestar_vite/plugin/_process.py +0 -185
  49. litestar_vite/plugin/_proxy.py +0 -689
  50. litestar_vite/plugin/_proxy_headers.py +0 -244
  51. litestar_vite/plugin/_static.py +0 -37
  52. litestar_vite/plugin/_utils.py +0 -489
  53. /litestar_vite/{handler/_routing.py → _handler/routing.py} +0 -0
  54. {litestar_vite-0.15.0.dist-info → litestar_vite-0.15.0rc2.dist-info}/WHEEL +0 -0
  55. {litestar_vite-0.15.0.dist-info → litestar_vite-0.15.0rc2.dist-info}/licenses/LICENSE +0 -0
@@ -26,28 +26,13 @@ Example usage::
26
26
  import logging
27
27
  import os
28
28
  from dataclasses import dataclass, field, replace
29
+ from importlib.util import find_spec
29
30
  from pathlib import Path
30
31
  from typing import TYPE_CHECKING, Any, Literal, Protocol, cast, runtime_checkable
31
32
 
32
33
  from litestar.exceptions import SerializationException
33
34
  from litestar.serialization import decode_json
34
35
 
35
- from litestar_vite.config._constants import ( # pyright: ignore[reportPrivateUsage]
36
- FSSPEC_INSTALLED,
37
- JINJA_INSTALLED,
38
- TRUE_VALUES,
39
- )
40
- from litestar_vite.config._deploy import DeployConfig # pyright: ignore[reportPrivateUsage]
41
- from litestar_vite.config._inertia import ( # pyright: ignore[reportPrivateUsage]
42
- InertiaConfig,
43
- InertiaSSRConfig,
44
- InertiaTypeGenConfig,
45
- )
46
- from litestar_vite.config._paths import PathConfig # pyright: ignore[reportPrivateUsage]
47
- from litestar_vite.config._runtime import ExternalDevServer, RuntimeConfig # pyright: ignore[reportPrivateUsage]
48
- from litestar_vite.config._spa import LoggingConfig, SPAConfig # pyright: ignore[reportPrivateUsage]
49
- from litestar_vite.config._types import TypeGenConfig # pyright: ignore[reportPrivateUsage]
50
-
51
36
  logger = logging.getLogger("litestar_vite")
52
37
 
53
38
  if TYPE_CHECKING:
@@ -100,6 +85,657 @@ class PaginationContainer(Protocol):
100
85
  items: "Sequence[Any]"
101
86
 
102
87
 
88
+ TRUE_VALUES = {"True", "true", "1", "yes", "Y", "T"}
89
+ JINJA_INSTALLED = bool(find_spec("jinja2"))
90
+ FSSPEC_INSTALLED = bool(find_spec("fsspec"))
91
+
92
+
93
+ def _empty_dict_factory() -> dict[str, Any]:
94
+ """Return an empty ``dict[str, Any]``.
95
+
96
+ Returns:
97
+ An empty dictionary.
98
+ """
99
+ return {}
100
+
101
+
102
+ def _empty_set_factory() -> set[str]:
103
+ """Return an empty ``set[str]``.
104
+
105
+ Returns:
106
+ An empty set.
107
+ """
108
+ return set()
109
+
110
+
111
+ def _default_content_types() -> dict[str, str]:
112
+ """Default content-type mappings keyed by file extension.
113
+
114
+ Returns:
115
+ Dictionary mapping file extensions to MIME types.
116
+ """
117
+ return {
118
+ ".js": "application/javascript",
119
+ ".mjs": "application/javascript",
120
+ ".cjs": "application/javascript",
121
+ ".css": "text/css",
122
+ ".html": "text/html",
123
+ ".json": "application/json",
124
+ ".svg": "image/svg+xml",
125
+ ".png": "image/png",
126
+ ".jpg": "image/jpeg",
127
+ ".jpeg": "image/jpeg",
128
+ ".webp": "image/webp",
129
+ ".woff2": "font/woff2",
130
+ ".woff": "font/woff",
131
+ }
132
+
133
+
134
+ def _default_storage_options() -> dict[str, Any]:
135
+ """Return an empty storage options dictionary.
136
+
137
+ Returns:
138
+ An empty dictionary.
139
+ """
140
+ return {}
141
+
142
+
143
+ @dataclass
144
+ class DeployConfig:
145
+ """CDN deployment configuration.
146
+
147
+ Attributes:
148
+ enabled: Enable deployment features.
149
+ storage_backend: fsspec URL for the target location (e.g., ``gcs://bucket/path``).
150
+ storage_options: Provider options forwarded to ``fsspec`` (credentials, region, etc.).
151
+ delete_orphaned: Remove remote files not present in the local bundle.
152
+ include_manifest: Upload ``manifest.json`` alongside assets.
153
+ content_types: Optional content-type overrides keyed by file extension.
154
+ """
155
+
156
+ enabled: bool = False
157
+ storage_backend: "str | None" = field(default_factory=lambda: os.getenv("VITE_DEPLOY_STORAGE"))
158
+ storage_options: dict[str, Any] = field(default_factory=_default_storage_options)
159
+ delete_orphaned: bool = field(default_factory=lambda: os.getenv("VITE_DEPLOY_DELETE", "true") in TRUE_VALUES)
160
+ include_manifest: bool = True
161
+ content_types: dict[str, str] = field(default_factory=_default_content_types)
162
+
163
+ def __post_init__(self) -> None:
164
+ """Apply environment fallbacks."""
165
+ if self.storage_backend is None:
166
+ self.storage_backend = os.getenv("VITE_DEPLOY_STORAGE")
167
+
168
+ def with_overrides(
169
+ self,
170
+ storage_backend: "str | None" = None,
171
+ storage_options: "dict[str, Any] | None" = None,
172
+ delete_orphaned: "bool | None" = None,
173
+ ) -> "DeployConfig":
174
+ """Return a copy with overrides applied.
175
+
176
+ Args:
177
+ storage_backend: Override for the storage URL.
178
+ storage_options: Override for backend options.
179
+ delete_orphaned: Override deletion behaviour.
180
+
181
+ Returns:
182
+ DeployConfig copy with updated fields.
183
+ """
184
+ return replace(
185
+ self,
186
+ storage_backend=storage_backend or self.storage_backend,
187
+ storage_options=storage_options or self.storage_options,
188
+ delete_orphaned=self.delete_orphaned if delete_orphaned is None else delete_orphaned,
189
+ )
190
+
191
+
192
+ @dataclass
193
+ class InertiaSSRConfig:
194
+ """Server-side rendering settings for Inertia.js.
195
+
196
+ Inertia SSR runs a separate Node server that renders the initial HTML for an
197
+ Inertia page object. Litestar sends the page payload to the SSR server (by
198
+ default at ``http://127.0.0.1:13714/render``) and injects the returned head
199
+ tags and body markup into the HTML response.
200
+
201
+ Notes:
202
+ - This is *not* Litestar-Vite's ``mode="ssr"`` (Astro/Nuxt/SvelteKit proxy mode).
203
+ - When enabled, failures to contact the SSR server are treated as errors (no silent fallback).
204
+ """
205
+
206
+ enabled: bool = True
207
+ url: str = "http://127.0.0.1:13714/render"
208
+ timeout: float = 2.0
209
+
210
+
211
+ @dataclass
212
+ class InertiaConfig:
213
+ """Configuration for InertiaJS support.
214
+
215
+ This is the canonical configuration class for Inertia.js integration.
216
+ Presence of an InertiaConfig instance indicates Inertia is enabled.
217
+
218
+ Note:
219
+ SPA mode (HTML transformation vs Jinja2 templates) is controlled by
220
+ ViteConfig.mode='hybrid'. The app_selector for data-page injection
221
+ is configured via SPAConfig.app_selector.
222
+
223
+ Attributes:
224
+ root_template: Name of the root template to use.
225
+ component_opt_keys: Identifiers for getting inertia component from route opts.
226
+ redirect_unauthorized_to: Path for unauthorized request redirects.
227
+ redirect_404: Path for 404 request redirects.
228
+ extra_static_page_props: Static props added to every page response.
229
+ extra_session_page_props: Session keys to include in page props.
230
+ """
231
+
232
+ root_template: str = "index.html"
233
+ """Name of the root template to use.
234
+
235
+ This must be a path that is found by the Vite Plugin template config
236
+ """
237
+ component_opt_keys: "tuple[str, ...]" = ("component", "page")
238
+ """Identifiers to use on routes to get the inertia component to render.
239
+
240
+ The first key found in the route handler opts will be used. This allows
241
+ semantic flexibility - use "component" or "page" depending on preference.
242
+
243
+ Example:
244
+ # All equivalent:
245
+ @get("/", component="Home")
246
+ @get("/", page="Home")
247
+
248
+ # Custom keys:
249
+ InertiaConfig(component_opt_keys=("view", "component", "page"))
250
+ """
251
+ redirect_unauthorized_to: "str | None" = None
252
+ """Optionally supply a path where unauthorized requests should redirect."""
253
+ redirect_404: "str | None" = None
254
+ """Optionally supply a path where 404 requests should redirect."""
255
+ extra_static_page_props: "dict[str, Any]" = field(default_factory=_empty_dict_factory)
256
+ """A dictionary of values to automatically add in to page props on every response."""
257
+ extra_session_page_props: "set[str]" = field(default_factory=_empty_set_factory)
258
+ """A set of session keys for which the value automatically be added (if it exists) to the response."""
259
+ encrypt_history: bool = False
260
+ """Enable browser history encryption globally (v2 feature).
261
+
262
+ When True, all Inertia responses will include `encryptHistory: true`
263
+ in the page object. The Inertia client will encrypt history state
264
+ using browser's crypto API before pushing to history.
265
+
266
+ This prevents sensitive data from being visible in browser history
267
+ after a user logs out. Individual responses can override this setting.
268
+
269
+ Note: Encryption happens client-side; requires HTTPS in production.
270
+ See: https://inertiajs.com/history-encryption
271
+ """
272
+ type_gen: "InertiaTypeGenConfig | None" = None
273
+ """Type generation options for Inertia page props.
274
+
275
+ Controls default types in generated page-props.ts. Set to InertiaTypeGenConfig()
276
+ or leave as None for defaults. Use InertiaTypeGenConfig(include_default_auth=False)
277
+ to disable default User/AuthData interfaces for non-standard user models.
278
+ """
279
+
280
+ ssr: "InertiaSSRConfig | bool | None" = None
281
+ """Enable server-side rendering (SSR) for Inertia responses.
282
+
283
+ When enabled, full-page HTML responses will be pre-rendered by a Node SSR server
284
+ and injected into the SPA HTML before returning to the client.
285
+
286
+ Supports:
287
+ - True: enable with defaults -> ``InertiaSSRConfig()``
288
+ - False/None: disabled -> ``None``
289
+ - InertiaSSRConfig: use as-is
290
+ """
291
+
292
+ def __post_init__(self) -> None:
293
+ """Normalize optional sub-configs."""
294
+ if self.ssr is True:
295
+ self.ssr = InertiaSSRConfig()
296
+ elif self.ssr is False:
297
+ self.ssr = None
298
+
299
+ @property
300
+ def ssr_config(self) -> "InertiaSSRConfig | None":
301
+ """Return the SSR config when enabled, otherwise None.
302
+
303
+ Returns:
304
+ The resolved SSR config when enabled, otherwise None.
305
+ """
306
+ if isinstance(self.ssr, InertiaSSRConfig) and self.ssr.enabled:
307
+ return self.ssr
308
+ return None
309
+
310
+
311
+ @dataclass
312
+ class InertiaTypeGenConfig:
313
+ """Type generation options for Inertia page props.
314
+
315
+ Controls which default types are included in the generated page-props.ts file.
316
+ This follows Laravel Jetstream patterns - sensible defaults for common auth patterns.
317
+
318
+ Attributes:
319
+ include_default_auth: Include default User and AuthData interfaces.
320
+ Default User has: id, email, name. Users extend via module augmentation.
321
+ Set to False if your User model doesn't have these fields (uses uuid, username, etc.)
322
+ include_default_flash: Include default FlashMessages interface.
323
+ Uses { [category: string]: string[] } pattern for flash messages.
324
+
325
+ Example:
326
+ Standard auth (95% of users) - just extend defaults::
327
+
328
+ # Python: use defaults
329
+ ViteConfig(inertia=InertiaConfig())
330
+
331
+ # TypeScript: extend User interface
332
+ declare module 'litestar-vite-plugin/inertia' {
333
+ interface User {
334
+ avatarUrl?: string
335
+ roles: Role[]
336
+ }
337
+ }
338
+
339
+ Custom auth (5% of users) - define from scratch::
340
+
341
+ # Python: disable defaults
342
+ ViteConfig(inertia=InertiaConfig(
343
+ type_gen=InertiaTypeGenConfig(include_default_auth=False)
344
+ ))
345
+
346
+ # TypeScript: define your custom User
347
+ declare module 'litestar-vite-plugin/inertia' {
348
+ interface User {
349
+ uuid: string // No id!
350
+ username: string // No email!
351
+ }
352
+ }
353
+ """
354
+
355
+ include_default_auth: bool = True
356
+ """Include default User and AuthData interfaces.
357
+
358
+ When True, generates:
359
+ - User: { id: string, email: string, name?: string | null }
360
+ - AuthData: { isAuthenticated: boolean, user?: User }
361
+
362
+ Users extend via TypeScript module augmentation.
363
+ Set to False if your User model has different required fields.
364
+ """
365
+
366
+ include_default_flash: bool = True
367
+ """Include default FlashMessages interface.
368
+
369
+ When True, generates:
370
+ - FlashMessages: { [category: string]: string[] }
371
+
372
+ Standard flash message pattern used by most web frameworks.
373
+ """
374
+
375
+
376
+ def _resolve_proxy_mode() -> "Literal['vite', 'direct', 'proxy'] | None":
377
+ """Resolve proxy_mode from environment variable.
378
+
379
+ Reads VITE_PROXY_MODE env var. Valid values:
380
+ - "vite" (default): Proxy to internal Vite server (allow list - assets only)
381
+ - "direct": Expose Vite port directly (no proxy)
382
+ - "proxy": Proxy everything except Litestar routes (deny list)
383
+ - "none": Disable proxy (for production)
384
+
385
+ Raises:
386
+ ValueError: If an invalid value is provided.
387
+
388
+ Returns:
389
+ The resolved proxy mode, or None if disabled.
390
+ """
391
+ env_value = os.getenv("VITE_PROXY_MODE")
392
+ match env_value.strip().lower() if env_value is not None else None:
393
+ case None:
394
+ return "vite"
395
+ case "none":
396
+ return None
397
+ case "direct":
398
+ return "direct"
399
+ case "proxy":
400
+ return "proxy"
401
+ case "vite":
402
+ return "vite"
403
+ case _:
404
+ msg = f"Invalid VITE_PROXY_MODE: {env_value!r}. Expected one of: vite, direct, proxy, none"
405
+ raise ValueError(msg)
406
+
407
+
408
+ @dataclass
409
+ class PathConfig:
410
+ """File system paths configuration.
411
+
412
+ Attributes:
413
+ root: The root directory of the project. Defaults to current working directory.
414
+ bundle_dir: Location of compiled assets and manifest.json.
415
+ resource_dir: TypeScript/JavaScript source directory (equivalent to ./src in Vue/React).
416
+ static_dir: Static public assets directory (served as-is by Vite).
417
+ manifest_name: Name of the Vite manifest file.
418
+ hot_file: Name of the hot file indicating dev server URL.
419
+ asset_url: Base URL for static asset references (prepended to Vite output).
420
+ ssr_output_dir: SSR output directory (optional).
421
+ """
422
+
423
+ root: "str | Path" = field(default_factory=Path.cwd)
424
+ bundle_dir: "str | Path" = field(default_factory=lambda: Path("public"))
425
+ resource_dir: "str | Path" = field(default_factory=lambda: Path("src"))
426
+ static_dir: "str | Path" = field(default_factory=lambda: Path("public"))
427
+ manifest_name: str = "manifest.json"
428
+ hot_file: str = "hot"
429
+ asset_url: str = field(default_factory=lambda: os.getenv("ASSET_URL", "/static/"))
430
+ ssr_output_dir: "str | Path | None" = None
431
+
432
+ def __post_init__(self) -> None:
433
+ """Normalize path types to Path objects.
434
+
435
+ This also adjusts defaults to prevent Vite's ``publicDir`` (input) from
436
+ colliding with ``outDir`` (output). ``bundle_dir`` is treated as the build
437
+ output directory. When ``static_dir`` equals ``bundle_dir``, Vite may warn
438
+ and effectively disable public asset copying, so ``static_dir`` defaults to
439
+ ``<resource_dir>/public`` in that case.
440
+ """
441
+ if isinstance(self.root, str):
442
+ object.__setattr__(self, "root", Path(self.root))
443
+ if isinstance(self.bundle_dir, str):
444
+ object.__setattr__(self, "bundle_dir", Path(self.bundle_dir))
445
+ if isinstance(self.resource_dir, str):
446
+ object.__setattr__(self, "resource_dir", Path(self.resource_dir))
447
+ if isinstance(self.static_dir, str):
448
+ object.__setattr__(self, "static_dir", Path(self.static_dir))
449
+ if isinstance(self.ssr_output_dir, str):
450
+ object.__setattr__(self, "ssr_output_dir", Path(self.ssr_output_dir))
451
+
452
+ if (
453
+ isinstance(self.bundle_dir, Path)
454
+ and isinstance(self.static_dir, Path)
455
+ and self.static_dir == self.bundle_dir
456
+ ):
457
+ object.__setattr__(self, "static_dir", Path(self.resource_dir) / "public")
458
+
459
+
460
+ @dataclass
461
+ class ExternalDevServer:
462
+ """Configuration for external (non-Vite) dev servers.
463
+
464
+ Use this when your frontend uses a framework with its own dev server
465
+ (Angular CLI, Next.js, Create React App, etc.) instead of Vite.
466
+
467
+ For SSR frameworks (Astro, Nuxt, SvelteKit) using Vite internally, leave
468
+ target as None - the proxy will read the dynamic port from the hotfile.
469
+
470
+ Attributes:
471
+ target: The URL of the external dev server (e.g., "http://localhost:4200").
472
+ If None, the proxy reads the target URL from the Vite hotfile.
473
+ command: Custom command to start the dev server (e.g., ["ng", "serve"]).
474
+ If None and start_dev_server=True, uses executor's default start command.
475
+ build_command: Custom command to build for production (e.g., ["ng", "build"]).
476
+ If None, uses executor's default build command (e.g., "npm run build").
477
+ http2: Enable HTTP/2 for proxy connections.
478
+ enabled: Whether the external proxy is enabled.
479
+ """
480
+
481
+ target: "str | None" = None
482
+ command: "list[str] | None" = None
483
+ build_command: "list[str] | None" = None
484
+ http2: bool = False
485
+ enabled: bool = True
486
+
487
+
488
+ @dataclass
489
+ class RuntimeConfig:
490
+ """Runtime execution settings.
491
+
492
+ Attributes:
493
+ dev_mode: Enable development mode with HMR/watch.
494
+ proxy_mode: Proxy handling mode:
495
+ - "vite" (default): Proxy Vite assets only (allow list - SPA mode)
496
+ - "direct": Expose Vite port directly (no proxy)
497
+ - "proxy": Proxy everything except Litestar routes (deny list - SSR mode)
498
+ - None: No proxy (production mode)
499
+ external_dev_server: Configuration for external dev server (used with proxy_mode="proxy").
500
+ host: Vite dev server host.
501
+ port: Vite dev server port.
502
+ protocol: Protocol for dev server (http/https).
503
+ executor: JavaScript runtime executor (node, bun, deno).
504
+ run_command: Custom command to run Vite dev server (auto-detect if None).
505
+ build_command: Custom command to build with Vite (auto-detect if None).
506
+ build_watch_command: Custom command for watch mode build.
507
+ serve_command: Custom command to run production server (for SSR frameworks).
508
+ install_command: Custom command to install dependencies.
509
+ is_react: Enable React Fast Refresh support.
510
+ ssr_enabled: Enable Server-Side Rendering.
511
+ health_check: Enable health check for dev server startup.
512
+ detect_nodeenv: Detect and use nodeenv in virtualenv (opt-in).
513
+ set_environment: Set Vite environment variables from config.
514
+ set_static_folders: Automatically configure static file serving.
515
+ csp_nonce: Content Security Policy nonce for inline scripts.
516
+ spa_handler: Auto-register catch-all SPA route when mode="spa".
517
+ http2: Enable HTTP/2 for proxy HTTP requests (better multiplexing).
518
+ WebSocket traffic (HMR) uses a separate connection and is unaffected.
519
+ """
520
+
521
+ dev_mode: bool = field(default_factory=lambda: os.getenv("VITE_DEV_MODE", "False") in TRUE_VALUES)
522
+ proxy_mode: "Literal['vite', 'direct', 'proxy'] | None" = field(default_factory=_resolve_proxy_mode)
523
+ external_dev_server: "ExternalDevServer | str | None" = None
524
+ host: str = field(default_factory=lambda: os.getenv("VITE_HOST", "127.0.0.1"))
525
+ port: int = field(default_factory=lambda: int(os.getenv("VITE_PORT", "5173")))
526
+ protocol: Literal["http", "https"] = "http"
527
+ executor: "Literal['node', 'bun', 'deno', 'yarn', 'pnpm'] | None" = None
528
+ run_command: "list[str] | None" = None
529
+ build_command: "list[str] | None" = None
530
+ build_watch_command: "list[str] | None" = None
531
+ serve_command: "list[str] | None" = None
532
+ install_command: "list[str] | None" = None
533
+ is_react: bool = False
534
+ ssr_enabled: bool = False
535
+ health_check: bool = field(default_factory=lambda: os.getenv("VITE_HEALTH_CHECK", "False") in TRUE_VALUES)
536
+ detect_nodeenv: bool = False
537
+ set_environment: bool = True
538
+ set_static_folders: bool = True
539
+ csp_nonce: "str | None" = None
540
+ spa_handler: bool = True
541
+ http2: bool = True
542
+ start_dev_server: bool = True
543
+
544
+ def __post_init__(self) -> None:
545
+ """Normalize runtime settings and apply derived defaults."""
546
+ if isinstance(self.external_dev_server, str):
547
+ self.external_dev_server = ExternalDevServer(target=self.external_dev_server)
548
+
549
+ if self.external_dev_server is not None and self.proxy_mode in {None, "vite"}:
550
+ self.proxy_mode = "proxy"
551
+
552
+ if self.executor is None:
553
+ self.executor = "node"
554
+
555
+ executor_commands = {
556
+ "node": {
557
+ "run": ["npm", "run", "dev"],
558
+ "build": ["npm", "run", "build"],
559
+ "build_watch": ["npm", "run", "watch"],
560
+ "serve": ["npm", "run", "serve"],
561
+ "install": ["npm", "install"],
562
+ },
563
+ "bun": {
564
+ "run": ["bun", "run", "dev"],
565
+ "build": ["bun", "run", "build"],
566
+ "build_watch": ["bun", "run", "watch"],
567
+ "serve": ["bun", "run", "serve"],
568
+ "install": ["bun", "install"],
569
+ },
570
+ "deno": {
571
+ "run": ["deno", "task", "dev"],
572
+ "build": ["deno", "task", "build"],
573
+ "build_watch": ["deno", "task", "watch"],
574
+ "serve": ["deno", "task", "serve"],
575
+ "install": ["deno", "install"],
576
+ },
577
+ "yarn": {
578
+ "run": ["yarn", "dev"],
579
+ "build": ["yarn", "build"],
580
+ "build_watch": ["yarn", "watch"],
581
+ "serve": ["yarn", "serve"],
582
+ "install": ["yarn", "install"],
583
+ },
584
+ "pnpm": {
585
+ "run": ["pnpm", "dev"],
586
+ "build": ["pnpm", "build"],
587
+ "build_watch": ["pnpm", "watch"],
588
+ "serve": ["pnpm", "serve"],
589
+ "install": ["pnpm", "install"],
590
+ },
591
+ }
592
+
593
+ if self.executor in executor_commands:
594
+ cmds = executor_commands[self.executor]
595
+ if self.run_command is None:
596
+ self.run_command = cmds["run"]
597
+ if self.build_command is None:
598
+ self.build_command = cmds["build"]
599
+ if self.build_watch_command is None:
600
+ self.build_watch_command = cmds["build_watch"]
601
+ if self.serve_command is None:
602
+ self.serve_command = cmds["serve"]
603
+ if self.install_command is None:
604
+ self.install_command = cmds["install"]
605
+
606
+
607
+ @dataclass
608
+ class TypeGenConfig:
609
+ """Type generation settings.
610
+
611
+ Presence of this config enables type generation. Use ``types=None`` or
612
+ ``types=False`` in ViteConfig to disable.
613
+
614
+ Attributes:
615
+ output: Output directory for generated types.
616
+ openapi_path: Path to export OpenAPI schema.
617
+ routes_path: Path to export routes metadata (JSON format).
618
+ routes_ts_path: Path to export typed routes TypeScript file.
619
+ generate_zod: Generate Zod schemas from OpenAPI.
620
+ generate_sdk: Generate SDK client from OpenAPI.
621
+ generate_routes: Generate typed routes.ts file (Ziggy-style).
622
+ generate_page_props: Generate Inertia page props TypeScript file.
623
+ Auto-enabled when both types and inertia are configured.
624
+ page_props_path: Path to export page props metadata (JSON format).
625
+ watch_patterns: File patterns to watch for type regeneration.
626
+ global_route: Register route() function globally on window object.
627
+ When True, adds ``window.route = route`` to generated routes.ts,
628
+ providing Laravel/Ziggy-style global access without imports.
629
+ fallback_type: Fallback value type for untyped containers in generated Inertia props.
630
+ Controls whether untyped dict/list become `unknown` (default) or `any`.
631
+ type_import_paths: Map schema/type names to TypeScript import paths for props types
632
+ that are not present in OpenAPI (e.g., internal/excluded schemas).
633
+ """
634
+
635
+ output: Path = field(default_factory=lambda: Path("src/generated"))
636
+ openapi_path: "Path | None" = field(default=None)
637
+ routes_path: "Path | None" = field(default=None)
638
+ routes_ts_path: "Path | None" = field(default=None)
639
+ generate_zod: bool = False
640
+ generate_sdk: bool = True
641
+ generate_routes: bool = True
642
+ generate_page_props: bool = True
643
+ global_route: bool = False
644
+ """Register route() function globally on window object.
645
+
646
+ When True, the generated routes.ts will include code that registers
647
+ the type-safe route() function on ``window.route``, similar to Laravel's
648
+ Ziggy library. This allows using route() without imports:
649
+
650
+ .. code-block:: typescript
651
+
652
+ // With global_route=True, no import needed:
653
+ window.route('user-profile', { userId: 123 })
654
+
655
+ // TypeScript users should add to global.d.ts:
656
+ // declare const route: typeof import('@/generated/routes').route
657
+
658
+ Default is False to encourage explicit imports for better tree-shaking.
659
+ """
660
+ fallback_type: "Literal['unknown', 'any']" = "unknown"
661
+ type_import_paths: dict[str, str] = field(default_factory=lambda: cast("dict[str, str]", {}))
662
+ """Map schema/type names to TypeScript import paths for Inertia props.
663
+
664
+ Use this for prop types that are not present in OpenAPI (e.g., internal schemas).
665
+ """
666
+ page_props_path: "Path | None" = field(default=None)
667
+ """Path to export page props metadata JSON.
668
+
669
+ The Vite plugin reads this file to generate page-props.ts.
670
+ Defaults to output / "inertia-pages.json".
671
+ """
672
+ watch_patterns: list[str] = field(
673
+ default_factory=lambda: ["**/routes.py", "**/handlers.py", "**/controllers/**/*.py"]
674
+ )
675
+
676
+ def __post_init__(self) -> None:
677
+ """Normalize path types and compute defaults based on output directory."""
678
+ if isinstance(self.output, str):
679
+ self.output = Path(self.output)
680
+ if self.openapi_path is None:
681
+ self.openapi_path = self.output / "openapi.json"
682
+ elif isinstance(self.openapi_path, str):
683
+ self.openapi_path = Path(self.openapi_path)
684
+ if self.routes_path is None:
685
+ self.routes_path = self.output / "routes.json"
686
+ elif isinstance(self.routes_path, str):
687
+ self.routes_path = Path(self.routes_path)
688
+ if self.routes_ts_path is None:
689
+ self.routes_ts_path = self.output / "routes.ts"
690
+ elif isinstance(self.routes_ts_path, str):
691
+ self.routes_ts_path = Path(self.routes_ts_path)
692
+ if self.page_props_path is None:
693
+ self.page_props_path = self.output / "inertia-pages.json"
694
+ elif isinstance(self.page_props_path, str):
695
+ self.page_props_path = Path(self.page_props_path)
696
+
697
+
698
+ @dataclass
699
+ class SPAConfig:
700
+ """Configuration for SPA HTML transformations.
701
+
702
+ This configuration controls how the SPA HTML is transformed before serving,
703
+ including CSRF token injection and Inertia.js page data handling.
704
+
705
+ Note:
706
+ Route metadata is now generated as TypeScript (routes.ts) at build time
707
+ instead of runtime injection. Use TypeGenConfig.generate_routes to enable.
708
+
709
+ Attributes:
710
+ inject_csrf: Whether to inject CSRF token into HTML (as window.__LITESTAR_CSRF__).
711
+ csrf_var_name: Global variable name for CSRF token (e.g., window.__LITESTAR_CSRF__).
712
+ app_selector: CSS selector for the app root element (used for data attributes).
713
+ cache_transformed_html: Cache transformed HTML in production; disabled when inject_csrf=True because CSRF tokens are per-request.
714
+ """
715
+
716
+ inject_csrf: bool = True
717
+ csrf_var_name: str = "__LITESTAR_CSRF__"
718
+ app_selector: str = "#app"
719
+ cache_transformed_html: bool = True
720
+
721
+
722
+ def _get_default_log_level() -> "Literal['quiet', 'normal', 'verbose']":
723
+ """Get default log level from environment variable.
724
+
725
+ Checks LITESTAR_VITE_LOG_LEVEL environment variable.
726
+ Falls back to "normal" if not set or invalid.
727
+
728
+ Returns:
729
+ The log level from environment or "normal" default.
730
+ """
731
+ env_level = os.getenv("LITESTAR_VITE_LOG_LEVEL", "").lower()
732
+ match env_level:
733
+ case "quiet" | "normal" | "verbose":
734
+ return env_level
735
+ case _:
736
+ return "normal"
737
+
738
+
103
739
  def _to_root_path(root_dir: Path, path: Path) -> Path:
104
740
  """Resolve a path relative to the configured root directory.
105
741
 
@@ -113,6 +749,49 @@ def _to_root_path(root_dir: Path, path: Path) -> Path:
113
749
  return path if path.is_absolute() else (root_dir / path)
114
750
 
115
751
 
752
+ @dataclass
753
+ class LoggingConfig:
754
+ """Logging configuration for console output.
755
+
756
+ Controls the verbosity and style of console output from both Python
757
+ and TypeScript (via .litestar.json bridge).
758
+
759
+ Attributes:
760
+ level: Logging verbosity level.
761
+ - "quiet": Minimal output (errors only)
762
+ - "normal": Standard operational messages (default)
763
+ - "verbose": Detailed debugging information
764
+ Can also be set via LITESTAR_VITE_LOG_LEVEL environment variable.
765
+ Precedence: explicit config > env var > default ("normal")
766
+ show_paths_absolute: Show absolute paths instead of relative paths.
767
+ Default False shows cleaner relative paths in output.
768
+ suppress_npm_output: Suppress npm/yarn/pnpm script echo lines.
769
+ When True, hides lines like "> dev" / "> vite" from output.
770
+ suppress_vite_banner: Suppress the Vite startup banner.
771
+ When True, only the LITESTAR banner is shown.
772
+ timestamps: Include timestamps in log messages.
773
+
774
+ Example:
775
+ Quiet mode for CI/CD::
776
+
777
+ ViteConfig(logging=LoggingConfig(level="quiet"))
778
+
779
+ Verbose debugging::
780
+
781
+ ViteConfig(logging=LoggingConfig(level="verbose", show_paths_absolute=True))
782
+
783
+ Environment variable::
784
+
785
+ export LITESTAR_VITE_LOG_LEVEL=quiet
786
+ """
787
+
788
+ level: "Literal['quiet', 'normal', 'verbose']" = field(default_factory=_get_default_log_level)
789
+ show_paths_absolute: bool = False
790
+ suppress_npm_output: bool = False
791
+ suppress_vite_banner: bool = False
792
+ timestamps: bool = False
793
+
794
+
116
795
  @dataclass
117
796
  class ViteConfig:
118
797
  """Root Vite configuration.
@@ -142,7 +821,7 @@ class ViteConfig:
142
821
  - Explicit mode parameter overrides auto-detection
143
822
 
144
823
  Attributes:
145
- mode: Serving mode - "spa", "template", "htmx", "hybrid", "framework", "ssr", "ssg", or "external".
824
+ mode: Serving mode - "spa", "template", "htmx", "hybrid", "ssr", "ssg", or "external".
146
825
  Auto-detected if not set. Use "external" for non-Vite frameworks (Angular CLI, etc.)
147
826
  that have their own build system - auto-serves bundle_dir in production.
148
827
  paths: File system paths configuration.
@@ -152,11 +831,11 @@ class ViteConfig:
152
831
  spa: SPA transformation settings (True enables with defaults, False disables).
153
832
  logging: Logging configuration (True enables with defaults, None uses defaults).
154
833
  dev_mode: Convenience shortcut for runtime.dev_mode.
155
- base_url: Base URL for the app entry point.
834
+ base_url: Base URL for production assets (CDN support).
156
835
  deploy: Deployment configuration for CDN publishing.
157
836
  """
158
837
 
159
- mode: "Literal['spa', 'template', 'htmx', 'hybrid', 'inertia', 'framework', 'ssr', 'ssg', 'external'] | None" = None
838
+ mode: "Literal['spa', 'template', 'htmx', 'hybrid', 'inertia', 'ssr', 'ssg', 'external'] | None" = None
160
839
  paths: PathConfig = field(default_factory=PathConfig)
161
840
  runtime: RuntimeConfig = field(default_factory=RuntimeConfig)
162
841
  types: "TypeGenConfig | bool | None" = None
@@ -170,7 +849,7 @@ class ViteConfig:
170
849
  """Custom guards for the SPA catch-all route.
171
850
 
172
851
  When set, these guards are applied to the SPA handler route that serves the
173
- SPA index.html (mode="spa"/"framework" with spa_handler=True).
852
+ SPA index.html (mode="spa"/"ssr" with spa_handler=True).
174
853
  """
175
854
  exclude_static_from_auth: bool = True
176
855
  """Exclude static file routes from authentication.
@@ -214,7 +893,7 @@ class ViteConfig:
214
893
  self._auto_detect_mode()
215
894
  self._auto_configure_inertia()
216
895
  self._auto_detect_react()
217
- self._apply_framework_mode_defaults()
896
+ self._apply_ssr_mode_defaults()
218
897
  self._normalize_deploy()
219
898
  self._ensure_spa_default()
220
899
  self._auto_enable_dev_mode()
@@ -259,20 +938,17 @@ class ViteConfig:
259
938
  """Normalize mode aliases.
260
939
 
261
940
  Aliases:
262
- - 'framework': Canonical name for meta-framework integration mode (Astro/Nuxt/SvelteKit, etc.)
263
- that uses dev-time proxying to a frontend dev server and optional SSR/SSG output handling.
264
-
265
- - 'ssr' / 'ssg' → 'framework': Aliases for framework proxy mode.
266
- Static Site Generation (SSG) uses the same dev-time proxy behavior as SSR:
267
- forward non-API routes to the framework dev server. SSG pre-renders at build time,
268
- SSR renders per-request, but their dev-time proxy behavior is identical.
941
+ - 'ssg' → 'ssr': Static Site Generation uses the same proxy behavior as SSR.
942
+ Both use deny list proxy in dev mode (forward non-API routes to framework's
943
+ dev server). SSG pre-renders at build time, SSR renders per-request, but
944
+ their dev-time proxy behavior is identical.
269
945
 
270
946
  - 'inertia' → 'hybrid': Inertia.js apps without Jinja templates use hybrid mode.
271
947
  This is clearer terminology since "hybrid" refers to the SPA-with-server-routing
272
948
  pattern that Inertia implements.
273
949
  """
274
- if self.mode in {"ssr", "ssg"}:
275
- self.mode = "framework"
950
+ if self.mode == "ssg":
951
+ self.mode = "ssr"
276
952
  elif self.mode == "inertia":
277
953
  self.mode = "hybrid"
278
954
 
@@ -337,10 +1013,10 @@ class ViteConfig:
337
1013
  if isinstance(self.inertia, InertiaConfig) and isinstance(self.spa, SPAConfig) and not self.spa.inject_csrf:
338
1014
  self.spa = replace(self.spa, inject_csrf=True)
339
1015
 
340
- def _apply_framework_mode_defaults(self) -> None:
341
- """Apply intelligent defaults for framework proxy mode.
1016
+ def _apply_ssr_mode_defaults(self) -> None:
1017
+ """Apply intelligent defaults for mode='ssr'.
342
1018
 
343
- When mode='framework' is set, automatically configure proxy_mode and spa_handler
1019
+ When mode='ssr' is set, automatically configure proxy_mode and spa_handler
344
1020
  based on dev_mode and whether built assets exist:
345
1021
 
346
1022
  - Dev mode: proxy_mode='proxy', spa_handler=False
@@ -350,7 +1026,7 @@ class ViteConfig:
350
1026
  - Prod mode without built assets: proxy_mode=None, spa_handler=False
351
1027
  (True SSR - Node server handles HTML, Litestar only serves API)
352
1028
  """
353
- if self.mode != "framework":
1029
+ if self.mode != "ssr":
354
1030
  return
355
1031
 
356
1032
  if self.runtime.dev_mode:
@@ -461,14 +1137,14 @@ class ViteConfig:
461
1137
  if self.runtime.external_dev_server is not None:
462
1138
  return
463
1139
 
464
- candidates = self.candidate_manifest_paths()
465
- manifest_locations = " or ".join(str(path) for path in candidates)
1140
+ bundle_path = self._resolve_to_root(self.bundle_dir)
1141
+ manifest_path = bundle_path / ".vite" / self.manifest_name
466
1142
  logger.warning(
467
1143
  "Vite manifest not found at %s. "
468
1144
  "Run 'litestar assets build' (or 'npm run build') to build assets, "
469
1145
  "or set dev_mode=True for development. "
470
1146
  "Assets will not load correctly without built files or a running Vite dev server.",
471
- manifest_locations,
1147
+ manifest_path,
472
1148
  )
473
1149
 
474
1150
  def _detect_mode(self) -> Literal["spa", "template", "htmx", "hybrid"]:
@@ -692,49 +1368,11 @@ class ViteConfig:
692
1368
  unique.append(path)
693
1369
  return unique
694
1370
 
695
- def candidate_manifest_paths(self) -> list[Path]:
696
- """Return possible manifest.json locations in the bundle directory.
697
-
698
- Some meta-frameworks emit the manifest under a ``.vite/`` subdirectory
699
- (e.g. ``<bundle_dir>/.vite/manifest.json``), while plain Vite builds may
700
- write it directly to ``<bundle_dir>/manifest.json``.
701
-
702
- Returns:
703
- A de-duplicated list of candidate manifest paths, ordered by preference.
704
- """
705
- bundle_path = self._resolve_to_root(self.bundle_dir)
706
- manifest_rel = Path(self.manifest_name)
707
-
708
- candidates: list[Path] = [bundle_path / manifest_rel]
709
- if not manifest_rel.is_absolute() and (not manifest_rel.parts or manifest_rel.parts[0] != ".vite"):
710
- candidates.append(bundle_path / ".vite" / manifest_rel)
711
-
712
- unique: list[Path] = []
713
- seen: set[Path] = set()
714
- for path in candidates:
715
- if path in seen:
716
- continue
717
- seen.add(path)
718
- unique.append(path)
719
- return unique
720
-
721
- def resolve_manifest_path(self) -> Path:
722
- """Resolve the most likely manifest path.
723
-
724
- Returns:
725
- The first existing manifest path, or the highest-priority candidate when none exist.
726
- """
727
- candidates = self.candidate_manifest_paths()
728
- for candidate in candidates:
729
- if candidate.exists():
730
- return candidate
731
- return candidates[0]
732
-
733
1371
  def has_built_assets(self) -> bool:
734
1372
  """Check if production assets exist in the bundle directory.
735
1373
 
736
1374
  Returns:
737
- True if a manifest or built index.html exists in bundle_dir.
1375
+ True if manifest.json or built index.html exists in bundle_dir.
738
1376
 
739
1377
  Note:
740
1378
  This method checks the bundle_dir (output directory) for built artifacts,
@@ -742,9 +1380,10 @@ class ViteConfig:
742
1380
  does not indicate built assets exist.
743
1381
  """
744
1382
  bundle_path = self._resolve_to_root(self.bundle_dir)
1383
+ manifest_path = bundle_path / self.manifest_name
745
1384
  index_path = bundle_path / "index.html"
746
1385
 
747
- return any(path.exists() for path in self.candidate_manifest_paths()) or index_path.exists()
1386
+ return manifest_path.exists() or index_path.exists()
748
1387
 
749
1388
  @property
750
1389
  def host(self) -> str:
@@ -777,7 +1416,7 @@ class ViteConfig:
777
1416
  def hot_reload(self) -> bool:
778
1417
  """Check if hot reload is enabled (derived from dev_mode and proxy_mode).
779
1418
 
780
- HMR requires dev_mode=True AND a Vite-based proxy mode (vite, direct, or proxy).
1419
+ HMR requires dev_mode=True AND a Vite-based mode (vite, direct, or proxy/ssr).
781
1420
  All modes support HMR since even SSR frameworks use Vite internally.
782
1421
 
783
1422
  Returns:
@@ -803,6 +1442,15 @@ class ViteConfig:
803
1442
  """
804
1443
  return self.runtime.is_react
805
1444
 
1445
+ @property
1446
+ def ssr_enabled(self) -> bool:
1447
+ """Check if SSR is enabled.
1448
+
1449
+ Returns:
1450
+ True if SSR is enabled, otherwise False.
1451
+ """
1452
+ return self.runtime.ssr_enabled
1453
+
806
1454
  @property
807
1455
  def run_command(self) -> list[str]:
808
1456
  """Get the run command.
@@ -927,27 +1575,6 @@ class ViteConfig:
927
1575
  """
928
1576
  return self.runtime.http2
929
1577
 
930
- @property
931
- def csp_nonce(self) -> "str | None":
932
- """Return the CSP nonce used for injected inline scripts.
933
-
934
- Returns:
935
- CSP nonce string when configured, otherwise None.
936
- """
937
- return self.runtime.csp_nonce
938
-
939
- @property
940
- def trusted_proxies(self) -> "list[str] | str | None":
941
- """Get trusted proxies configuration.
942
-
943
- When set, enables ProxyHeadersMiddleware to handle X-Forwarded-* headers
944
- from reverse proxies (Railway, Heroku, AWS ALB, nginx, etc.).
945
-
946
- Returns:
947
- The trusted proxies configuration, or None if disabled.
948
- """
949
- return self.runtime.trusted_proxies
950
-
951
1578
  @property
952
1579
  def ssr_output_dir(self) -> "Path | None":
953
1580
  """Get SSR output directory.