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
@@ -1,687 +0,0 @@
1
- """Vite Plugin for Litestar.
2
-
3
- This module provides the VitePlugin class for integrating Vite with Litestar.
4
- The plugin handles:
5
-
6
- - Static file serving configuration
7
- - Jinja2 template callable registration
8
- - Vite dev server process management
9
- - Async asset loader initialization
10
- - Development proxies for Vite HTTP and HMR WebSockets (with hop-by-hop header filtering)
11
-
12
- Example::
13
-
14
- from litestar import Litestar
15
- from litestar_vite import VitePlugin, ViteConfig
16
-
17
- app = Litestar(
18
- plugins=[VitePlugin(config=ViteConfig(dev_mode=True))],
19
- )
20
- """
21
-
22
- import os
23
- from contextlib import asynccontextmanager, contextmanager
24
- from pathlib import Path
25
- from typing import TYPE_CHECKING, Any, cast
26
-
27
- import httpx
28
- from litestar.exceptions import NotFoundException
29
- from litestar.middleware import DefineMiddleware
30
- from litestar.plugins import CLIPlugin, InitPluginProtocol
31
- from litestar.static_files import create_static_files_router # pyright: ignore[reportUnknownVariableType]
32
-
33
- from litestar_vite.config import JINJA_INSTALLED, TRUE_VALUES, ExternalDevServer
34
- from litestar_vite.loader import ViteAssetLoader
35
- from litestar_vite.plugin._process import ViteProcess
36
- from litestar_vite.plugin._proxy import ViteProxyMiddleware, create_ssr_proxy_controller, create_vite_hmr_handler
37
- from litestar_vite.plugin._proxy_headers import ProxyHeadersMiddleware, TrustedHosts
38
- from litestar_vite.plugin._static import StaticFilesConfig
39
- from litestar_vite.plugin._utils import (
40
- create_proxy_client,
41
- get_litestar_route_prefixes,
42
- is_litestar_route,
43
- is_non_serving_assets_cli,
44
- log_fail,
45
- log_info,
46
- log_success,
47
- log_warn,
48
- pick_free_port,
49
- resolve_litestar_version,
50
- set_app_environment,
51
- set_environment,
52
- static_not_found_handler,
53
- vite_not_found_handler,
54
- )
55
-
56
- if TYPE_CHECKING:
57
- from collections.abc import AsyncIterator, Iterator
58
-
59
- from click import Group
60
- from litestar import Litestar
61
- from litestar.config.app import AppConfig
62
- from litestar.types import ExceptionHandlersMap
63
-
64
- from litestar_vite.config import ViteConfig
65
- from litestar_vite.handler import AppHandler
66
-
67
-
68
- class VitePlugin(InitPluginProtocol, CLIPlugin):
69
- """Vite plugin for Litestar.
70
-
71
- This plugin integrates Vite with Litestar, providing:
72
-
73
- - Static file serving configuration
74
- - Jinja2 template callables for asset tags
75
- - Vite dev server process management
76
- - Async asset loader initialization
77
-
78
- Example::
79
-
80
- from litestar import Litestar
81
- from litestar_vite import VitePlugin, ViteConfig
82
-
83
- app = Litestar(
84
- plugins=[
85
- VitePlugin(config=ViteConfig(dev_mode=True))
86
- ],
87
- )
88
- """
89
-
90
- __slots__ = (
91
- "_asset_loader",
92
- "_config",
93
- "_proxy_client",
94
- "_proxy_target",
95
- "_spa_handler",
96
- "_static_files_config",
97
- "_vite_process",
98
- )
99
-
100
- def __init__(
101
- self,
102
- config: "ViteConfig | None" = None,
103
- asset_loader: "ViteAssetLoader | None" = None,
104
- static_files_config: "StaticFilesConfig | None" = None,
105
- ) -> None:
106
- """Initialize the Vite plugin.
107
-
108
- Args:
109
- config: Vite configuration. Defaults to ViteConfig() if not provided.
110
- asset_loader: Optional pre-initialized asset loader.
111
- static_files_config: Optional configuration for static file serving.
112
- """
113
- from litestar_vite.config import ViteConfig
114
-
115
- if config is None:
116
- config = ViteConfig()
117
- self._config = config
118
- self._asset_loader = asset_loader
119
- self._vite_process = ViteProcess(executor=config.executor)
120
- self._static_files_config: dict[str, Any] = static_files_config.__dict__ if static_files_config else {}
121
- self._proxy_target: "str | None" = None
122
- self._proxy_client: "httpx.AsyncClient | None" = None
123
- self._spa_handler: "AppHandler | None" = None
124
-
125
- @property
126
- def config(self) -> "ViteConfig":
127
- """Get the Vite configuration.
128
-
129
- Returns:
130
- The ViteConfig instance.
131
- """
132
- return self._config
133
-
134
- @property
135
- def asset_loader(self) -> "ViteAssetLoader":
136
- """Get the asset loader instance.
137
-
138
- Lazily initializes the loader if not already set.
139
-
140
- Returns:
141
- The ViteAssetLoader instance.
142
- """
143
-
144
- if self._asset_loader is None:
145
- self._asset_loader = ViteAssetLoader.initialize_loader(config=self._config)
146
- return self._asset_loader
147
-
148
- @property
149
- def spa_handler(self) -> "AppHandler | None":
150
- """Return the configured SPA handler when SPA mode is enabled.
151
-
152
- Returns:
153
- The AppHandler instance, or None when SPA mode is disabled/not configured.
154
- """
155
- return self._spa_handler
156
-
157
- @property
158
- def proxy_client(self) -> "httpx.AsyncClient | None":
159
- """Return the shared httpx.AsyncClient for proxy requests.
160
-
161
- The client is initialized during app lifespan (dev mode only) and provides
162
- connection pooling, TLS session reuse, and HTTP/2 multiplexing benefits.
163
-
164
- Returns:
165
- The shared AsyncClient instance, or None if not initialized or not in dev mode.
166
- """
167
- return self._proxy_client
168
-
169
- def _resolve_bundle_dir(self) -> Path:
170
- """Resolve the bundle directory to an absolute path.
171
-
172
- Returns:
173
- The absolute path to the bundle directory.
174
- """
175
- bundle_dir = Path(self._config.bundle_dir)
176
- if not bundle_dir.is_absolute():
177
- return self._config.root_dir / bundle_dir
178
- return bundle_dir
179
-
180
- def _resolve_hotfile_path(self) -> Path:
181
- """Resolve the path to the hotfile.
182
-
183
- Returns:
184
- The absolute path to the hotfile.
185
- """
186
- return self._resolve_bundle_dir() / self._config.hot_file
187
-
188
- def _write_hotfile(self, content: str) -> None:
189
- """Write content to the hotfile.
190
-
191
- Args:
192
- content: The content to write (usually the dev server URL).
193
- """
194
- hotfile_path = self._resolve_hotfile_path()
195
- hotfile_path.parent.mkdir(parents=True, exist_ok=True)
196
- hotfile_path.write_text(content, encoding="utf-8")
197
-
198
- def _resolve_dev_command(self) -> "list[str]":
199
- """Resolve the command to run for the dev server.
200
-
201
- Returns:
202
- The list of command arguments.
203
- """
204
- ext = self._config.runtime.external_dev_server
205
- if isinstance(ext, ExternalDevServer) and ext.enabled:
206
- command = ext.command or self._config.executor.start_command
207
- log_info(f"Starting external dev server: {' '.join(command)}")
208
- return command
209
-
210
- if self._config.hot_reload:
211
- log_info("Starting Vite dev server (HMR enabled)")
212
- return self._config.run_command
213
-
214
- log_info("Starting Vite watch build process")
215
- return self._config.build_watch_command
216
-
217
- def _ensure_proxy_target(self) -> None:
218
- """Prepare proxy target URL and port for proxy modes (vite, proxy, ssr).
219
-
220
- For all proxy modes in dev mode:
221
- - Auto-selects a free port if VITE_PORT is not explicitly set
222
- - Sets the port in runtime config for JS integrations to read
223
-
224
- For 'vite' mode specifically:
225
- - Forces loopback host unless VITE_ALLOW_REMOTE is set
226
- - Sets _proxy_target directly (JS writes hotfile when server starts)
227
-
228
- For 'proxy'/'ssr' modes:
229
- - Port is written to .litestar.json for SSR framework to read
230
- - SSR framework writes hotfile with actual URL when ready
231
- - Proxy discovers target from hotfile at request time
232
- """
233
- if not self._config.is_dev_mode:
234
- return
235
-
236
- if self._config.proxy_mode is None:
237
- return
238
-
239
- if os.getenv("VITE_PORT") is None and self._config.runtime.port == 5173:
240
- self._config.runtime.port = pick_free_port()
241
-
242
- if self._config.proxy_mode == "vite":
243
- if self._proxy_target is not None:
244
- return
245
- if os.getenv("VITE_ALLOW_REMOTE", "False") not in TRUE_VALUES:
246
- self._config.runtime.host = "127.0.0.1"
247
- self._proxy_target = f"{self._config.protocol}://{self._config.host}:{self._config.port}"
248
-
249
- def _configure_inertia(self, app_config: "AppConfig") -> "AppConfig":
250
- """Configure Inertia.js by registering an InertiaPlugin instance.
251
-
252
- This is called automatically when `inertia` config is provided to ViteConfig.
253
- Users can still use InertiaPlugin manually for more control.
254
-
255
- Args:
256
- app_config: The Litestar application configuration.
257
-
258
- Returns:
259
- The modified application configuration.
260
- """
261
- from litestar_vite.inertia.plugin import InertiaPlugin
262
-
263
- inertia_plugin = InertiaPlugin(config=self._config.inertia) # type: ignore[arg-type]
264
- app_config.plugins.append(inertia_plugin)
265
-
266
- return app_config
267
-
268
- def on_cli_init(self, cli: "Group") -> None:
269
- """Register CLI commands.
270
-
271
- Args:
272
- cli: The Click command group to add commands to.
273
- """
274
- from litestar_vite.cli import vite_group
275
-
276
- cli.add_command(vite_group)
277
-
278
- def _configure_jinja_callables(self, app_config: "AppConfig") -> None:
279
- """Register Jinja2 template callables for Vite asset handling.
280
-
281
- Args:
282
- app_config: The Litestar application configuration.
283
- """
284
- from litestar.contrib.jinja import JinjaTemplateEngine
285
-
286
- from litestar_vite.loader import render_asset_tag, render_hmr_client, render_routes, render_static_asset
287
-
288
- template_config = app_config.template_config # pyright: ignore[reportUnknownMemberType,reportUnknownVariableType]
289
- if template_config and isinstance(
290
- template_config.engine_instance, # pyright: ignore[reportUnknownMemberType]
291
- JinjaTemplateEngine,
292
- ):
293
- engine = template_config.engine_instance # pyright: ignore[reportUnknownMemberType]
294
- engine.register_template_callable(key="vite_hmr", template_callable=render_hmr_client)
295
- engine.register_template_callable(key="vite", template_callable=render_asset_tag)
296
- engine.register_template_callable(key="vite_static", template_callable=render_static_asset)
297
- engine.register_template_callable(key="vite_routes", template_callable=render_routes)
298
-
299
- def _configure_static_files(self, app_config: "AppConfig") -> None:
300
- """Configure static file serving for Vite assets.
301
-
302
- The static files router serves real files (JS, CSS, images). SPA fallback (serving
303
- index.html for client-side routes) is handled by the AppHandler.
304
-
305
- Args:
306
- app_config: The Litestar application configuration.
307
- """
308
- bundle_dir = self._resolve_bundle_dir()
309
-
310
- resource_dir = Path(self._config.resource_dir)
311
- if not resource_dir.is_absolute():
312
- resource_dir = self._config.root_dir / resource_dir
313
-
314
- static_dir = Path(self._config.static_dir)
315
- if not static_dir.is_absolute():
316
- static_dir = self._config.root_dir / static_dir
317
-
318
- static_dirs = [bundle_dir, resource_dir]
319
- if static_dir.exists() and static_dir != bundle_dir:
320
- static_dirs.append(static_dir)
321
-
322
- opt: dict[str, Any] = {}
323
- if self._config.exclude_static_from_auth:
324
- opt["exclude_from_auth"] = True
325
- user_opt = self._static_files_config.get("opt", {})
326
- if user_opt:
327
- opt = {**opt, **user_opt}
328
-
329
- base_config: dict[str, Any] = {
330
- "directories": (static_dirs if self._config.is_dev_mode else [bundle_dir]),
331
- "path": self._config.asset_url,
332
- "name": "vite",
333
- "html_mode": False,
334
- "include_in_schema": False,
335
- "opt": opt,
336
- "exception_handlers": {NotFoundException: static_not_found_handler},
337
- }
338
- user_config = {k: v for k, v in self._static_files_config.items() if k != "opt"}
339
- static_files_config: dict[str, Any] = {**base_config, **user_config}
340
- app_config.route_handlers.append(create_static_files_router(**static_files_config))
341
-
342
- def _configure_dev_proxy(self, app_config: "AppConfig") -> None:
343
- """Configure dev proxy middleware and handlers based on proxy_mode.
344
-
345
- Args:
346
- app_config: The Litestar application configuration.
347
- """
348
- proxy_mode = self._config.proxy_mode
349
- hotfile_path = self._resolve_hotfile_path()
350
-
351
- if proxy_mode == "vite":
352
- self._configure_vite_proxy(app_config, hotfile_path)
353
- elif proxy_mode == "proxy":
354
- self._configure_ssr_proxy(app_config, hotfile_path)
355
-
356
- def _configure_vite_proxy(self, app_config: "AppConfig", hotfile_path: Path) -> None:
357
- """Configure Vite proxy mode (allow list).
358
-
359
- Args:
360
- app_config: The Litestar application configuration.
361
- hotfile_path: Path to the hotfile.
362
- """
363
- self._ensure_proxy_target()
364
- app_config.middleware.append(
365
- DefineMiddleware(
366
- ViteProxyMiddleware,
367
- hotfile_path=hotfile_path,
368
- asset_url=self._config.asset_url,
369
- resource_dir=self._config.resource_dir,
370
- bundle_dir=self._config.bundle_dir,
371
- root_dir=self._config.root_dir,
372
- http2=self._config.http2,
373
- plugin=self,
374
- )
375
- )
376
- hmr_path = f"{self._config.asset_url.rstrip('/')}/vite-hmr"
377
- app_config.route_handlers.append(
378
- create_vite_hmr_handler(hotfile_path=hotfile_path, hmr_path=hmr_path, asset_url=self._config.asset_url)
379
- )
380
-
381
- def _configure_ssr_proxy(self, app_config: "AppConfig", hotfile_path: Path) -> None:
382
- """Configure SSR proxy mode (deny list).
383
-
384
- Args:
385
- app_config: The Litestar application configuration.
386
- hotfile_path: Path to the hotfile.
387
- """
388
- self._ensure_proxy_target()
389
- external = self._config.external_dev_server
390
- static_target = external.target if external else None
391
-
392
- app_config.route_handlers.append(
393
- create_ssr_proxy_controller(
394
- target=static_target,
395
- hotfile_path=hotfile_path if static_target is None else None,
396
- http2=external.http2 if external else True,
397
- plugin=self,
398
- )
399
- )
400
- hmr_path = f"{self._config.asset_url.rstrip('/')}/vite-hmr"
401
- app_config.route_handlers.append(
402
- create_vite_hmr_handler(hotfile_path=hotfile_path, hmr_path=hmr_path, asset_url=self._config.asset_url)
403
- )
404
-
405
- def on_app_init(self, app_config: "AppConfig") -> "AppConfig":
406
- """Configure the Litestar application for Vite.
407
-
408
- This method wires up supporting configuration for dev/prod operation:
409
-
410
- - Adds types used by generated handlers to the signature namespace.
411
- - Ensures a consistent NotFound handler for asset/proxy lookups.
412
- - Registers optional Inertia and Jinja integrations.
413
- - Configures static file routing when enabled.
414
- - Configures dev proxy middleware based on proxy_mode.
415
- - Creates/initializes the SPA handler where applicable and registers lifespans.
416
-
417
- Args:
418
- app_config: The Litestar application configuration.
419
-
420
- Returns:
421
- The modified application configuration.
422
- """
423
- from litestar import Response
424
- from litestar.connection import Request as LitestarRequest
425
-
426
- app_config.signature_namespace["Response"] = Response
427
- app_config.signature_namespace["Request"] = LitestarRequest
428
-
429
- # Register proxy headers middleware FIRST if configured
430
- # This must run before other middleware to ensure correct scheme/client in scope
431
- if self._config.trusted_proxies is not None:
432
- from litestar_vite.plugin._proxy_headers import ProxyHeadersMiddleware
433
-
434
- app_config.middleware.insert(
435
- 0, # Insert at beginning for early processing
436
- DefineMiddleware(ProxyHeadersMiddleware, trusted_hosts=self._config.trusted_proxies),
437
- )
438
-
439
- handlers: ExceptionHandlersMap = cast("ExceptionHandlersMap", app_config.exception_handlers or {}) # pyright: ignore
440
- if NotFoundException not in handlers:
441
- handlers[NotFoundException] = vite_not_found_handler
442
- app_config.exception_handlers = handlers # pyright: ignore[reportUnknownMemberType]
443
-
444
- if self._config.inertia is not None:
445
- app_config = self._configure_inertia(app_config)
446
-
447
- if JINJA_INSTALLED and self._config.mode in {"template", "htmx"}:
448
- self._configure_jinja_callables(app_config)
449
-
450
- skip_static = self._config.mode == "external" and self._config.is_dev_mode
451
- if self._config.set_static_folders and not skip_static:
452
- self._configure_static_files(app_config)
453
-
454
- if self._config.is_dev_mode and self._config.proxy_mode is not None and not is_non_serving_assets_cli():
455
- self._configure_dev_proxy(app_config)
456
-
457
- use_spa_handler = self._config.spa_handler and self._config.mode in {"spa", "framework"}
458
- use_spa_handler = use_spa_handler or (self._config.mode == "external" and not self._config.is_dev_mode)
459
- if use_spa_handler:
460
- from litestar_vite.handler import AppHandler
461
-
462
- self._spa_handler = AppHandler(self._config)
463
- app_config.route_handlers.append(self._spa_handler.create_route_handler())
464
- elif self._config.mode == "hybrid":
465
- from litestar_vite.handler import AppHandler
466
-
467
- self._spa_handler = AppHandler(self._config)
468
-
469
- app_config.lifespan.append(self.lifespan) # pyright: ignore[reportUnknownMemberType]
470
-
471
- return app_config
472
-
473
- def _check_health(self) -> None:
474
- """Check if the Vite dev server is running and ready.
475
-
476
- Polls the dev server URL for up to 5 seconds.
477
- """
478
- import time
479
-
480
- url = f"{self._config.protocol}://{self._config.host}:{self._config.port}/__vite_ping"
481
- for _ in range(50):
482
- try:
483
- httpx.get(url, timeout=0.1)
484
- except httpx.HTTPError:
485
- time.sleep(0.1)
486
- else:
487
- log_success("Vite dev server responded to health check")
488
- return
489
- log_fail("Vite server health check failed")
490
-
491
- def _run_health_check(self) -> None:
492
- """Run the appropriate health check based on proxy mode."""
493
- match self._config.proxy_mode:
494
- case "proxy":
495
- self._check_ssr_health(self._resolve_hotfile_path())
496
- case _:
497
- self._check_health()
498
-
499
- def _check_ssr_health(self, hotfile_path: Path, timeout: float = 10.0) -> bool:
500
- """Wait for SSR framework to be ready via hotfile.
501
-
502
- Polls intelligently for the hotfile and validates HTTP connectivity.
503
- Exits early as soon as the server is confirmed ready.
504
-
505
- Args:
506
- hotfile_path: Path to the hotfile written by the SSR framework.
507
- timeout: Maximum time to wait in seconds (default 10s).
508
-
509
- Returns:
510
- True if SSR server is ready, False if timeout reached.
511
- """
512
- import time
513
-
514
- start = time.time()
515
- last_url = None
516
-
517
- while time.time() - start < timeout:
518
- if hotfile_path.exists():
519
- try:
520
- url = hotfile_path.read_text(encoding="utf-8").strip()
521
- if url:
522
- last_url = url
523
- resp = httpx.get(url, timeout=0.5, follow_redirects=True)
524
- if resp.status_code < 500:
525
- log_success(f"SSR server ready at {url}")
526
- return True
527
- except OSError:
528
- pass
529
- except httpx.HTTPError:
530
- pass
531
-
532
- time.sleep(0.1)
533
-
534
- if last_url:
535
- log_fail(f"SSR server at {last_url} did not respond within {timeout}s")
536
- else:
537
- log_fail(f"SSR hotfile not found at {hotfile_path} within {timeout}s")
538
- return False
539
-
540
- def _export_types_sync(self, app: "Litestar") -> None:
541
- """Export type metadata synchronously on startup.
542
-
543
- This exports OpenAPI schema, route metadata (JSON), typed routes (TypeScript),
544
- and Inertia pages metadata when type generation is enabled. The Vite plugin
545
- watches these files and triggers @hey-api/openapi-ts when they change.
546
-
547
- Uses the shared `export_integration_assets` function to guarantee
548
- byte-identical output between CLI and plugin.
549
-
550
- Args:
551
- app: The Litestar application instance.
552
- """
553
- from litestar_vite.codegen import export_integration_assets
554
-
555
- try:
556
- result = export_integration_assets(app, self._config)
557
-
558
- if result.exported_files:
559
- log_success(f"Types exported → {', '.join(result.exported_files)}")
560
- except (OSError, TypeError, ValueError, ImportError) as e: # pragma: no cover
561
- log_warn(f"Type export failed: {e}")
562
-
563
- @contextmanager
564
- def server_lifespan(self, app: "Litestar") -> "Iterator[None]":
565
- """Server-level lifespan context manager (runs ONCE per server, before workers).
566
-
567
- This is called by Litestar CLI before workers start. It handles:
568
- - Environment variable setup (with logging)
569
- - Vite dev server process start/stop (ONE instance for all workers)
570
- - Type export on startup
571
-
572
- Note: SPA handler and asset loader initialization happens in the per-worker
573
- `lifespan` method, which is auto-registered in `on_app_init`.
574
-
575
- Hotfile behavior: the hotfile is written before starting the dev server to ensure proxy
576
- middleware and SPA handlers can resolve a target URL immediately on first request.
577
-
578
- Args:
579
- app: The Litestar application instance.
580
-
581
- Yields:
582
- None
583
- """
584
- if self._config.is_dev_mode:
585
- self._ensure_proxy_target()
586
-
587
- if self._config.set_environment:
588
- set_environment(config=self._config)
589
- set_app_environment(app)
590
- log_info("Applied Vite environment variables")
591
-
592
- self._export_types_sync(app)
593
-
594
- if self._config.is_dev_mode and self._config.runtime.start_dev_server:
595
- ext = self._config.runtime.external_dev_server
596
- is_external = isinstance(ext, ExternalDevServer) and ext.enabled
597
-
598
- command_to_run = self._resolve_dev_command()
599
- if is_external and isinstance(ext, ExternalDevServer) and ext.target:
600
- self._write_hotfile(ext.target)
601
- elif not is_external:
602
- target_url = f"{self._config.protocol}://{self._config.host}:{self._config.port}"
603
- self._write_hotfile(target_url)
604
-
605
- try:
606
- self._vite_process.start(command_to_run, self._config.root_dir)
607
- log_success("Dev server process started")
608
- if self._config.health_check and not is_external:
609
- self._run_health_check()
610
- yield
611
- finally:
612
- self._vite_process.stop()
613
- log_info("Dev server process stopped.")
614
- else:
615
- yield
616
-
617
- @asynccontextmanager
618
- async def lifespan(self, app: "Litestar") -> "AsyncIterator[None]":
619
- """Worker-level lifespan context manager (runs per worker process).
620
-
621
- This is auto-registered in `on_app_init` and handles per-worker initialization:
622
- - Environment variable setup (silently - each worker needs process-local env vars)
623
- - Shared proxy client initialization (dev mode only, for ViteProxyMiddleware/SSRProxyController)
624
- - Asset loader initialization
625
- - SPA handler initialization
626
- - Route metadata injection
627
-
628
- Note: The Vite dev server process is started in `server_lifespan`, which
629
- runs ONCE per server before workers start.
630
-
631
- Args:
632
- app: The Litestar application instance.
633
-
634
- Yields:
635
- None
636
- """
637
- from litestar_vite.loader import ViteAssetLoader
638
-
639
- if self._config.set_environment:
640
- set_environment(config=self._config)
641
- set_app_environment(app)
642
-
643
- # Initialize shared proxy client for ViteProxyMiddleware/SSRProxyController
644
- # Uses connection pooling for better performance (HTTP/2 multiplexing, TLS reuse)
645
- if self._config.is_dev_mode and self._config.proxy_mode is not None:
646
- self._proxy_client = create_proxy_client(http2=self._config.http2)
647
-
648
- if self._asset_loader is None:
649
- self._asset_loader = ViteAssetLoader(config=self._config)
650
- await self._asset_loader.initialize()
651
-
652
- if self._spa_handler is not None and not self._spa_handler.is_initialized:
653
- self._spa_handler.initialize_sync(vite_url=self._proxy_target)
654
- log_success("SPA handler initialized")
655
-
656
- is_ssr_mode = self._config.mode == "framework" or self._config.proxy_mode == "proxy"
657
- if not self._config.is_dev_mode and not self._config.has_built_assets() and not is_ssr_mode:
658
- log_warn(
659
- "Vite dev server is disabled (dev_mode=False) but no index.html was found. "
660
- "Run your front-end build or set VITE_DEV_MODE=1 to enable HMR."
661
- )
662
-
663
- try:
664
- yield
665
- finally:
666
- if self._proxy_client is not None:
667
- await self._proxy_client.aclose()
668
- self._proxy_client = None
669
- if self._spa_handler is not None:
670
- await self._spa_handler.shutdown_async()
671
-
672
-
673
- __all__ = (
674
- "ProxyHeadersMiddleware",
675
- "StaticFilesConfig",
676
- "TrustedHosts",
677
- "VitePlugin",
678
- "ViteProcess",
679
- "ViteProxyMiddleware",
680
- "create_ssr_proxy_controller",
681
- "create_vite_hmr_handler",
682
- "get_litestar_route_prefixes",
683
- "is_litestar_route",
684
- "resolve_litestar_version",
685
- "set_app_environment",
686
- "set_environment",
687
- )