litestar-vite 0.1.1__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 (154) hide show
  1. litestar_vite/__init__.py +54 -4
  2. litestar_vite/__metadata__.py +12 -7
  3. litestar_vite/_codegen/__init__.py +26 -0
  4. litestar_vite/_codegen/inertia.py +407 -0
  5. litestar_vite/_codegen/openapi.py +233 -0
  6. litestar_vite/_codegen/routes.py +653 -0
  7. litestar_vite/_codegen/ts.py +235 -0
  8. litestar_vite/_handler/__init__.py +8 -0
  9. litestar_vite/_handler/app.py +524 -0
  10. litestar_vite/_handler/routing.py +130 -0
  11. litestar_vite/cli.py +1147 -10
  12. litestar_vite/codegen.py +39 -0
  13. litestar_vite/commands.py +79 -0
  14. litestar_vite/config.py +1594 -70
  15. litestar_vite/deploy.py +355 -0
  16. litestar_vite/doctor.py +1179 -0
  17. litestar_vite/exceptions.py +78 -0
  18. litestar_vite/executor.py +316 -0
  19. litestar_vite/handler.py +9 -0
  20. litestar_vite/html_transform.py +426 -0
  21. litestar_vite/inertia/__init__.py +53 -0
  22. litestar_vite/inertia/_utils.py +114 -0
  23. litestar_vite/inertia/exception_handler.py +172 -0
  24. litestar_vite/inertia/helpers.py +1043 -0
  25. litestar_vite/inertia/middleware.py +54 -0
  26. litestar_vite/inertia/plugin.py +133 -0
  27. litestar_vite/inertia/request.py +286 -0
  28. litestar_vite/inertia/response.py +706 -0
  29. litestar_vite/inertia/types.py +316 -0
  30. litestar_vite/loader.py +462 -121
  31. litestar_vite/plugin.py +2160 -21
  32. litestar_vite/py.typed +0 -0
  33. litestar_vite/scaffolding/__init__.py +20 -0
  34. litestar_vite/scaffolding/generator.py +270 -0
  35. litestar_vite/scaffolding/templates.py +437 -0
  36. litestar_vite/templates/__init__.py +0 -0
  37. litestar_vite/templates/addons/tailwindcss/tailwind.css.j2 +1 -0
  38. litestar_vite/templates/angular/index.html.j2 +12 -0
  39. litestar_vite/templates/angular/openapi-ts.config.ts.j2 +18 -0
  40. litestar_vite/templates/angular/package.json.j2 +35 -0
  41. litestar_vite/templates/angular/src/app/app.component.css.j2 +3 -0
  42. litestar_vite/templates/angular/src/app/app.component.html.j2 +1 -0
  43. litestar_vite/templates/angular/src/app/app.component.ts.j2 +9 -0
  44. litestar_vite/templates/angular/src/app/app.config.ts.j2 +5 -0
  45. litestar_vite/templates/angular/src/main.ts.j2 +9 -0
  46. litestar_vite/templates/angular/src/styles.css.j2 +9 -0
  47. litestar_vite/templates/angular/tsconfig.app.json.j2 +34 -0
  48. litestar_vite/templates/angular/tsconfig.json.j2 +20 -0
  49. litestar_vite/templates/angular/vite.config.ts.j2 +21 -0
  50. litestar_vite/templates/angular-cli/.postcssrc.json.j2 +5 -0
  51. litestar_vite/templates/angular-cli/angular.json.j2 +36 -0
  52. litestar_vite/templates/angular-cli/openapi-ts.config.ts.j2 +18 -0
  53. litestar_vite/templates/angular-cli/package.json.j2 +27 -0
  54. litestar_vite/templates/angular-cli/proxy.conf.json.j2 +18 -0
  55. litestar_vite/templates/angular-cli/src/app/app.component.css.j2 +3 -0
  56. litestar_vite/templates/angular-cli/src/app/app.component.html.j2 +1 -0
  57. litestar_vite/templates/angular-cli/src/app/app.component.ts.j2 +9 -0
  58. litestar_vite/templates/angular-cli/src/app/app.config.ts.j2 +5 -0
  59. litestar_vite/templates/angular-cli/src/index.html.j2 +13 -0
  60. litestar_vite/templates/angular-cli/src/main.ts.j2 +6 -0
  61. litestar_vite/templates/angular-cli/src/styles.css.j2 +10 -0
  62. litestar_vite/templates/angular-cli/tailwind.config.js.j2 +4 -0
  63. litestar_vite/templates/angular-cli/tsconfig.app.json.j2 +16 -0
  64. litestar_vite/templates/angular-cli/tsconfig.json.j2 +26 -0
  65. litestar_vite/templates/angular-cli/tsconfig.spec.json.j2 +9 -0
  66. litestar_vite/templates/astro/astro.config.mjs.j2 +28 -0
  67. litestar_vite/templates/astro/openapi-ts.config.ts.j2 +15 -0
  68. litestar_vite/templates/astro/src/layouts/Layout.astro.j2 +63 -0
  69. litestar_vite/templates/astro/src/pages/index.astro.j2 +36 -0
  70. litestar_vite/templates/astro/src/styles/global.css.j2 +1 -0
  71. litestar_vite/templates/base/.gitignore.j2 +42 -0
  72. litestar_vite/templates/base/openapi-ts.config.ts.j2 +15 -0
  73. litestar_vite/templates/base/package.json.j2 +38 -0
  74. litestar_vite/templates/base/resources/vite-env.d.ts.j2 +1 -0
  75. litestar_vite/templates/base/tsconfig.json.j2 +37 -0
  76. litestar_vite/templates/htmx/src/main.js.j2 +8 -0
  77. litestar_vite/templates/htmx/templates/base.html.j2.j2 +56 -0
  78. litestar_vite/templates/htmx/templates/index.html.j2.j2 +13 -0
  79. litestar_vite/templates/htmx/vite.config.ts.j2 +40 -0
  80. litestar_vite/templates/nuxt/app.vue.j2 +29 -0
  81. litestar_vite/templates/nuxt/composables/useApi.ts.j2 +33 -0
  82. litestar_vite/templates/nuxt/nuxt.config.ts.j2 +31 -0
  83. litestar_vite/templates/nuxt/openapi-ts.config.ts.j2 +15 -0
  84. litestar_vite/templates/nuxt/pages/index.vue.j2 +54 -0
  85. litestar_vite/templates/react/index.html.j2 +13 -0
  86. litestar_vite/templates/react/src/App.css.j2 +56 -0
  87. litestar_vite/templates/react/src/App.tsx.j2 +19 -0
  88. litestar_vite/templates/react/src/main.tsx.j2 +10 -0
  89. litestar_vite/templates/react/vite.config.ts.j2 +39 -0
  90. litestar_vite/templates/react-inertia/index.html.j2 +14 -0
  91. litestar_vite/templates/react-inertia/package.json.j2 +46 -0
  92. litestar_vite/templates/react-inertia/resources/App.css.j2 +68 -0
  93. litestar_vite/templates/react-inertia/resources/main.tsx.j2 +17 -0
  94. litestar_vite/templates/react-inertia/resources/pages/Home.tsx.j2 +18 -0
  95. litestar_vite/templates/react-inertia/resources/ssr.tsx.j2 +19 -0
  96. litestar_vite/templates/react-inertia/vite.config.ts.j2 +59 -0
  97. litestar_vite/templates/react-router/index.html.j2 +12 -0
  98. litestar_vite/templates/react-router/src/App.css.j2 +17 -0
  99. litestar_vite/templates/react-router/src/App.tsx.j2 +7 -0
  100. litestar_vite/templates/react-router/src/main.tsx.j2 +10 -0
  101. litestar_vite/templates/react-router/vite.config.ts.j2 +39 -0
  102. litestar_vite/templates/react-tanstack/index.html.j2 +12 -0
  103. litestar_vite/templates/react-tanstack/openapi-ts.config.ts.j2 +18 -0
  104. litestar_vite/templates/react-tanstack/src/App.css.j2 +17 -0
  105. litestar_vite/templates/react-tanstack/src/main.tsx.j2 +21 -0
  106. litestar_vite/templates/react-tanstack/src/routeTree.gen.ts.j2 +7 -0
  107. litestar_vite/templates/react-tanstack/src/routes/__root.tsx.j2 +9 -0
  108. litestar_vite/templates/react-tanstack/src/routes/books.tsx.j2 +9 -0
  109. litestar_vite/templates/react-tanstack/src/routes/index.tsx.j2 +9 -0
  110. litestar_vite/templates/react-tanstack/vite.config.ts.j2 +39 -0
  111. litestar_vite/templates/svelte/index.html.j2 +13 -0
  112. litestar_vite/templates/svelte/src/App.svelte.j2 +30 -0
  113. litestar_vite/templates/svelte/src/app.css.j2 +45 -0
  114. litestar_vite/templates/svelte/src/main.ts.j2 +8 -0
  115. litestar_vite/templates/svelte/src/vite-env.d.ts.j2 +2 -0
  116. litestar_vite/templates/svelte/svelte.config.js.j2 +5 -0
  117. litestar_vite/templates/svelte/vite.config.ts.j2 +39 -0
  118. litestar_vite/templates/svelte-inertia/index.html.j2 +14 -0
  119. litestar_vite/templates/svelte-inertia/resources/app.css.j2 +21 -0
  120. litestar_vite/templates/svelte-inertia/resources/main.ts.j2 +11 -0
  121. litestar_vite/templates/svelte-inertia/resources/pages/Home.svelte.j2 +43 -0
  122. litestar_vite/templates/svelte-inertia/resources/vite-env.d.ts.j2 +2 -0
  123. litestar_vite/templates/svelte-inertia/svelte.config.js.j2 +5 -0
  124. litestar_vite/templates/svelte-inertia/vite.config.ts.j2 +37 -0
  125. litestar_vite/templates/sveltekit/openapi-ts.config.ts.j2 +15 -0
  126. litestar_vite/templates/sveltekit/src/app.css.j2 +40 -0
  127. litestar_vite/templates/sveltekit/src/app.html.j2 +12 -0
  128. litestar_vite/templates/sveltekit/src/hooks.server.ts.j2 +55 -0
  129. litestar_vite/templates/sveltekit/src/routes/+layout.svelte.j2 +12 -0
  130. litestar_vite/templates/sveltekit/src/routes/+page.svelte.j2 +34 -0
  131. litestar_vite/templates/sveltekit/svelte.config.js.j2 +12 -0
  132. litestar_vite/templates/sveltekit/tsconfig.json.j2 +14 -0
  133. litestar_vite/templates/sveltekit/vite.config.ts.j2 +31 -0
  134. litestar_vite/templates/vue/env.d.ts.j2 +7 -0
  135. litestar_vite/templates/vue/index.html.j2 +13 -0
  136. litestar_vite/templates/vue/src/App.vue.j2 +28 -0
  137. litestar_vite/templates/vue/src/main.ts.j2 +5 -0
  138. litestar_vite/templates/vue/src/style.css.j2 +45 -0
  139. litestar_vite/templates/vue/vite.config.ts.j2 +39 -0
  140. litestar_vite/templates/vue-inertia/env.d.ts.j2 +7 -0
  141. litestar_vite/templates/vue-inertia/index.html.j2 +14 -0
  142. litestar_vite/templates/vue-inertia/package.json.j2 +49 -0
  143. litestar_vite/templates/vue-inertia/resources/main.ts.j2 +18 -0
  144. litestar_vite/templates/vue-inertia/resources/pages/Home.vue.j2 +22 -0
  145. litestar_vite/templates/vue-inertia/resources/ssr.ts.j2 +21 -0
  146. litestar_vite/templates/vue-inertia/resources/style.css.j2 +21 -0
  147. litestar_vite/templates/vue-inertia/vite.config.ts.j2 +59 -0
  148. litestar_vite-0.15.0rc2.dist-info/METADATA +230 -0
  149. litestar_vite-0.15.0rc2.dist-info/RECORD +151 -0
  150. {litestar_vite-0.1.1.dist-info → litestar_vite-0.15.0rc2.dist-info}/WHEEL +1 -1
  151. litestar_vite/template_engine.py +0 -103
  152. litestar_vite-0.1.1.dist-info/METADATA +0 -68
  153. litestar_vite-0.1.1.dist-info/RECORD +0 -11
  154. {litestar_vite-0.1.1.dist-info → litestar_vite-0.15.0rc2.dist-info}/licenses/LICENSE +0 -0
litestar_vite/cli.py CHANGED
@@ -1,24 +1,1161 @@
1
- from __future__ import annotations
1
+ import os
2
+ import subprocess
3
+ import sys
4
+ from pathlib import Path
5
+ from textwrap import dedent
6
+ from typing import TYPE_CHECKING, Any
2
7
 
3
- from typing import TYPE_CHECKING
8
+ import msgspec
9
+ from click import Choice, Context, group, option
10
+ from click import Path as ClickPath
11
+ from litestar.cli._utils import ( # pyright: ignore[reportPrivateImportUsage]
12
+ LitestarCLIException,
13
+ LitestarEnv,
14
+ LitestarGroup,
15
+ console,
16
+ )
17
+ from litestar.exceptions import SerializationException
18
+ from litestar.serialization import decode_json, encode_json, get_serializer
19
+ from rich.panel import Panel
20
+ from rich.prompt import Confirm, Prompt
4
21
 
5
- from click import group, option
6
- from litestar.cli._utils import LitestarGroup, console
22
+ from litestar_vite.codegen import generate_inertia_pages_json, generate_routes_json, generate_routes_ts
23
+ from litestar_vite.config import (
24
+ DeployConfig,
25
+ ExternalDevServer,
26
+ InertiaConfig,
27
+ InertiaTypeGenConfig,
28
+ LoggingConfig,
29
+ TypeGenConfig,
30
+ ViteConfig,
31
+ )
32
+ from litestar_vite.deploy import ViteDeployer, format_bytes
33
+ from litestar_vite.doctor import ViteDoctor
34
+ from litestar_vite.exceptions import ViteExecutionError
35
+ from litestar_vite.plugin import VitePlugin, resolve_litestar_version, set_environment
36
+ from litestar_vite.scaffolding import TemplateContext, generate_project, get_available_templates
37
+ from litestar_vite.scaffolding.templates import FrameworkType, get_template
7
38
 
8
39
  if TYPE_CHECKING:
9
40
  from litestar import Litestar
10
41
 
11
42
 
43
+ FRAMEWORK_CHOICES = [
44
+ "react",
45
+ "react-router",
46
+ "react-tanstack",
47
+ "react-inertia",
48
+ "vue",
49
+ "vue-inertia",
50
+ "svelte",
51
+ "svelte-inertia",
52
+ "sveltekit",
53
+ "nuxt",
54
+ "astro",
55
+ "htmx",
56
+ "angular",
57
+ "angular-cli",
58
+ ]
59
+
60
+
61
+ def _format_command(command: "list[str] | None") -> str:
62
+ """Join a command list for display.
63
+
64
+ Returns:
65
+ Space-joined command string or empty string.
66
+ """
67
+ return " ".join(command or [])
68
+
69
+
70
+ def _apply_cli_log_level(config: ViteConfig, *, verbose: bool = False, quiet: bool = False) -> None:
71
+ """Apply CLI log level overrides to the config.
72
+
73
+ Precedence: --quiet > --verbose > config/env
74
+
75
+ Args:
76
+ config: The ViteConfig to modify.
77
+ verbose: If True, set log level to "verbose".
78
+ quiet: If True, set log level to "quiet" (takes precedence over verbose).
79
+ """
80
+ if quiet:
81
+ config.logging = LoggingConfig(
82
+ level="quiet",
83
+ show_paths_absolute=config.logging_config.show_paths_absolute,
84
+ suppress_npm_output=config.logging_config.suppress_npm_output,
85
+ suppress_vite_banner=config.logging_config.suppress_vite_banner,
86
+ timestamps=config.logging_config.timestamps,
87
+ )
88
+ config.reset_executor()
89
+ elif verbose:
90
+ config.logging = LoggingConfig(
91
+ level="verbose",
92
+ show_paths_absolute=config.logging_config.show_paths_absolute,
93
+ suppress_npm_output=config.logging_config.suppress_npm_output,
94
+ suppress_vite_banner=config.logging_config.suppress_vite_banner,
95
+ timestamps=config.logging_config.timestamps,
96
+ )
97
+ config.reset_executor()
98
+
99
+
100
+ def _relative_path(path: Path) -> str:
101
+ """Return path relative to CWD when possible.
102
+
103
+ Returns:
104
+ The relative path string when possible, otherwise the absolute path string.
105
+ """
106
+
107
+ try:
108
+ return str(path.relative_to(Path.cwd()))
109
+ except ValueError:
110
+ return str(path)
111
+
112
+
113
+ def _print_recommended_config(template_name: str, resource_dir: str, bundle_dir: str) -> None:
114
+ """Print recommended ViteConfig for the scaffolded template.
115
+
116
+ Args:
117
+ template_name: The name of the template that was scaffolded.
118
+ resource_dir: The resource directory used.
119
+ bundle_dir: The bundle directory used.
120
+ """
121
+ spa_templates = {"react-router", "react-tanstack"}
122
+ mode = "spa" if template_name in spa_templates else "template"
123
+
124
+ config_snippet = dedent(
125
+ f"""\
126
+ from pathlib import Path
127
+ from litestar_vite import ViteConfig, PathConfig
128
+
129
+ vite_config = ViteConfig(
130
+ mode="{mode}",
131
+ dev_mode=True,
132
+ types=True,
133
+ paths=PathConfig(
134
+ root=Path(__file__).parent,
135
+ resource_dir="{resource_dir}",
136
+ bundle_dir="{bundle_dir}",
137
+ ),
138
+ )
139
+ """
140
+ )
141
+
142
+ console.print("\n[bold cyan]Recommended ViteConfig:[/]")
143
+ console.print(Panel(config_snippet, title="app.py", border_style="dim"))
144
+ console.print("[dim]Note: set dev_mode=False in production; set types=False to disable TypeScript generation.[/]")
145
+
146
+
147
+ def _coerce_option_value(value: str) -> object:
148
+ """Convert CLI key/value strings into basic Python types.
149
+
150
+ Returns:
151
+ Converted value (bool, int, float, or original string).
152
+ """
153
+ lowered = value.lower()
154
+ if lowered in {"true", "false"}:
155
+ return lowered == "true"
156
+ if value.isdigit():
157
+ return int(value)
158
+ try:
159
+ return float(value)
160
+ except ValueError:
161
+ return value
162
+
163
+
164
+ def _parse_storage_options(values: tuple[str, ...]) -> dict[str, object]:
165
+ """Parse repeated --storage-option entries into a dictionary.
166
+
167
+ Returns:
168
+ Dictionary of parsed storage options.
169
+
170
+ Raises:
171
+ ValueError: If an option is not in key=value format.
172
+ """
173
+ options: dict[str, object] = {}
174
+ for item in values:
175
+ if "=" not in item:
176
+ msg = f"Invalid storage option '{item}'. Expected key=value."
177
+ raise ValueError(msg)
178
+ key, raw = item.split("=", 1)
179
+ options[key] = _coerce_option_value(raw)
180
+ return options
181
+
182
+
183
+ def _build_deploy_config(
184
+ base_config: ViteConfig, storage: str | None, storage_options: dict[str, object], no_delete: bool
185
+ ) -> DeployConfig:
186
+ """Resolve deploy configuration from CLI overrides.
187
+
188
+ Returns:
189
+ Resolved DeployConfig with CLI overrides applied.
190
+
191
+ Raises:
192
+ SystemExit: If deployment is not configured or storage backend is missing.
193
+ """
194
+ deploy_config = base_config.deploy_config
195
+ if deploy_config is None:
196
+ msg = "Deployment is not configured. Set ViteConfig.deploy to enable."
197
+ raise SystemExit(msg)
198
+
199
+ merged_options = {**deploy_config.storage_options, **storage_options}
200
+ deploy_config = deploy_config.with_overrides(
201
+ storage_backend=storage, storage_options=merged_options, delete_orphaned=False if no_delete else None
202
+ )
203
+
204
+ if not deploy_config.storage_backend:
205
+ msg = "Storage backend is required (e.g., gcs://bucket/assets)."
206
+ raise SystemExit(msg)
207
+
208
+ return deploy_config
209
+
210
+
211
+ def _run_vite_build(config: ViteConfig, root_dir: Path, console: Any, no_build: bool) -> None:
212
+ """Run Vite build unless skipped.
213
+
214
+ Raises:
215
+ SystemExit: If the build fails.
216
+ """
217
+ if no_build:
218
+ console.print("[dim]Skipping Vite build (--no-build).[/]")
219
+ return
220
+
221
+ console.rule("[yellow]Starting Vite build process[/]", align="left")
222
+ if config.set_environment:
223
+ set_environment(config=config, asset_url_override=config.base_url or config.asset_url)
224
+ os.environ["VITE_BASE_URL"] = config.base_url or config.asset_url or "/"
225
+ try:
226
+ config.executor.execute(config.build_command, cwd=root_dir)
227
+ console.print("[bold green]✓ Build complete[/]")
228
+ except ViteExecutionError as exc:
229
+ msg = f"Build failed: {exc!s}"
230
+ raise SystemExit(msg) from exc
231
+
232
+
233
+ def _generate_schema_and_routes(app: "Litestar", config: ViteConfig, console: Any) -> None:
234
+ """Export OpenAPI schema, routes, and Inertia page props prior to running a build.
235
+
236
+ Skips generation when type generation is disabled.
237
+
238
+ When Inertia page-props export is enabled, the generator may add additional component schemas
239
+ for routes that are excluded from OpenAPI. In that case, this function persists the augmented
240
+ OpenAPI document so the TypeScript generator can emit those types.
241
+
242
+ Raises:
243
+ LitestarCLIException: If export fails.
244
+ """
245
+ types_config = config.types
246
+ if not isinstance(types_config, TypeGenConfig):
247
+ return
248
+
249
+ console.print("[dim]Preparing OpenAPI schema and routes...[/]")
250
+ _export_openapi_schema(app, types_config)
251
+ _export_routes_metadata(app, types_config)
252
+
253
+ if types_config.generate_routes:
254
+ console.print("[dim]3. Exporting typed routes...[/]")
255
+ try:
256
+ routes_ts_content = generate_routes_ts(app)
257
+ routes_ts_path = types_config.routes_ts_path or (types_config.output / "routes.ts")
258
+ routes_ts_path.parent.mkdir(parents=True, exist_ok=True)
259
+ routes_ts_path.write_text(routes_ts_content, encoding="utf-8")
260
+ console.print(f"[green]✓ Typed routes exported to {_relative_path(routes_ts_path)}[/]")
261
+ except OSError as exc: # pragma: no cover
262
+ msg = f"Failed to export typed routes: {exc}"
263
+ raise LitestarCLIException(msg) from exc
264
+
265
+ if isinstance(config.inertia, InertiaConfig) and types_config.generate_page_props and types_config.page_props_path:
266
+ openapi_schema: dict[str, Any] | None = None
267
+ try:
268
+ if types_config.openapi_path and types_config.openapi_path.exists():
269
+ openapi_schema = decode_json(types_config.openapi_path.read_bytes())
270
+ except (OSError, SerializationException):
271
+ pass
272
+
273
+ _export_inertia_pages_metadata(app, types_config, config.inertia, openapi_schema)
274
+ if openapi_schema is not None and types_config.openapi_path is not None:
275
+ try:
276
+ schema_content = msgspec.json.format(encode_json(openapi_schema), indent=2)
277
+ types_config.openapi_path.parent.mkdir(parents=True, exist_ok=True)
278
+ types_config.openapi_path.write_bytes(schema_content)
279
+ except OSError as exc: # pragma: no cover
280
+ msg = f"Failed to update OpenAPI schema: {exc}"
281
+ raise LitestarCLIException(msg) from exc
282
+
283
+
12
284
  @group(cls=LitestarGroup, name="assets")
13
285
  def vite_group() -> None:
14
286
  """Manage Vite Tasks."""
15
287
 
16
288
 
17
- @vite_group.command( # type: ignore # noqa: PGH003
18
- name="build",
19
- help="Building frontend assets with Vite.",
289
+ def _select_framework_template(template: "str | None", no_prompt: bool) -> "tuple[str, Any]":
290
+ """Select and validate the framework template.
291
+
292
+ Args:
293
+ template: User-provided template name or None.
294
+ no_prompt: Whether to skip interactive prompts.
295
+
296
+ Returns:
297
+ Tuple of (template_name, framework_template).
298
+
299
+ Notes:
300
+ When ``no_prompt=True`` and no template is provided, defaults to the ``react`` template.
301
+ """
302
+ if template is None and not no_prompt:
303
+ available = get_available_templates()
304
+ console.print("\n[bold]Available framework templates:[/]")
305
+ for i, tmpl in enumerate(available, 1):
306
+ console.print(f" {i}. [cyan]{tmpl.type.value}[/] - {tmpl.description}")
307
+
308
+ template = Prompt.ask(
309
+ "\nSelect a framework template", choices=[t.type.value for t in available], default="react"
310
+ )
311
+ elif template is None:
312
+ template = "react"
313
+
314
+ framework = get_template(template)
315
+ if framework is None:
316
+ console.print(f"[red]Unknown template: {template}[/]")
317
+ sys.exit(1)
318
+
319
+ return template, framework
320
+
321
+
322
+ def _prompt_for_options(
323
+ framework: "Any",
324
+ enable_ssr: "bool | None",
325
+ tailwind: bool,
326
+ enable_types: bool,
327
+ generate_zod: bool,
328
+ generate_client: bool,
329
+ no_prompt: bool,
330
+ ) -> "tuple[bool, bool, bool, bool, bool]":
331
+ """Prompt user for optional features if not specified.
332
+
333
+ Args:
334
+ framework: The framework template.
335
+ enable_ssr: SSR flag or None.
336
+ tailwind: TailwindCSS flag.
337
+ enable_types: Type generation flag.
338
+ generate_zod: Zod schema generation flag.
339
+ generate_client: API client generation flag.
340
+ no_prompt: Whether to skip prompts.
341
+
342
+ Returns:
343
+ Tuple of (enable_ssr, tailwind, enable_types, generate_zod, generate_client).
344
+ """
345
+ if enable_ssr is None:
346
+ enable_ssr = (
347
+ framework.has_ssr if no_prompt else Confirm.ask("Enable server-side rendering?", default=framework.has_ssr)
348
+ )
349
+
350
+ if not tailwind and not no_prompt:
351
+ tailwind = Confirm.ask("Add TailwindCSS?", default=False)
352
+
353
+ if not enable_types and not no_prompt:
354
+ enable_types = Confirm.ask("Enable TypeScript type generation?", default=True)
355
+
356
+ if enable_types:
357
+ if not generate_zod and not no_prompt:
358
+ generate_zod = Confirm.ask("Generate Zod schemas for validation?", default=False)
359
+
360
+ if not generate_client and not no_prompt:
361
+ generate_client = Confirm.ask("Generate API client?", default=True)
362
+ else:
363
+ generate_zod = False
364
+ generate_client = False
365
+
366
+ return enable_ssr or False, tailwind, enable_types, generate_zod, generate_client
367
+
368
+
369
+ @vite_group.command(name="doctor", help="Diagnose and fix Vite configuration issues.")
370
+ @option("--check", is_flag=True, help="Exit with non-zero status if errors are found (for CI).")
371
+ @option("--fix", is_flag=True, help="Auto-fix detected issues (with confirmation).")
372
+ @option("--no-prompt", is_flag=True, help="Apply fixes without confirmation.")
373
+ @option("--verbose", is_flag=True, help="Enable verbose output.")
374
+ @option("--show-config", is_flag=True, help="Print .litestar.json and extracted litestar({ ... }) config block.")
375
+ @option("--runtime-checks", is_flag=True, help="Run runtime-state checks (Vite reachable / hotfile presence).")
376
+ def vite_doctor(
377
+ app: "Litestar", check: bool, fix: bool, no_prompt: bool, verbose: bool, show_config: bool, runtime_checks: bool
378
+ ) -> None:
379
+ """Diagnose and fix Vite configuration issues."""
380
+ if verbose:
381
+ app.debug = True
382
+
383
+ plugin = app.plugins.get(VitePlugin)
384
+ doctor = ViteDoctor(plugin.config, verbose=verbose)
385
+
386
+ success = doctor.run(fix=fix, no_prompt=no_prompt, show_config=show_config, runtime_checks=runtime_checks)
387
+
388
+ if check and not success:
389
+ sys.exit(1)
390
+
391
+
392
+ @vite_group.command(name="init", help="Initialize vite for your project.")
393
+ @option(
394
+ "--template",
395
+ type=Choice(FRAMEWORK_CHOICES, case_sensitive=False),
396
+ help="Frontend framework template to use. Inertia variants available: react-inertia, vue-inertia, svelte-inertia.",
397
+ default=None,
398
+ required=False,
399
+ )
400
+ @option(
401
+ "--root-path",
402
+ type=ClickPath(dir_okay=True, file_okay=False, path_type=Path),
403
+ help="The path for the initial the Vite application. This is the equivalent to the top-level folder in a normal Vue or React application..",
404
+ default=None,
405
+ required=False,
406
+ )
407
+ @option(
408
+ "--frontend-dir",
409
+ type=str,
410
+ help="Optional subdirectory under root to place the generated frontend (e.g., 'web').",
411
+ default=".",
412
+ required=False,
413
+ )
414
+ @option(
415
+ "--bundle-path",
416
+ type=ClickPath(dir_okay=True, file_okay=False, path_type=Path),
417
+ help="The path for the built Vite assets. This is the where the output of `npm run build` will write files.",
418
+ default=None,
419
+ required=False,
420
+ )
421
+ @option(
422
+ "--resource-path",
423
+ type=ClickPath(dir_okay=True, file_okay=False, path_type=Path),
424
+ help="The path to your Javascript/Typescript source and associated assets. If this were a standalone Vue or React app, this would point to your `src/` folder.",
425
+ default=None,
426
+ required=False,
20
427
  )
428
+ @option(
429
+ "--static-path",
430
+ type=ClickPath(dir_okay=True, file_okay=False, path_type=Path),
431
+ help="The optional path to your static (unprocessed) frontend assets. If this were a standalone Vite app, this would point to your `public/` folder.",
432
+ default=None,
433
+ required=False,
434
+ )
435
+ @option("--asset-url", type=str, help="Base url to serve assets from.", default=None, required=False)
436
+ @option("--vite-port", type=int, help="The port to run the vite server against.", default=None, required=False)
437
+ @option(
438
+ "--enable-ssr",
439
+ "enable_ssr",
440
+ flag_value=True,
441
+ default=None,
442
+ required=False,
443
+ show_default=False,
444
+ help="Enable SSR support.",
445
+ )
446
+ @option(
447
+ "--disable-ssr",
448
+ "enable_ssr",
449
+ flag_value=False,
450
+ default=None,
451
+ required=False,
452
+ show_default=False,
453
+ help="Disable SSR support.",
454
+ )
455
+ @option(
456
+ "--tailwind", type=bool, help="Add TailwindCSS to the project.", required=False, show_default=False, is_flag=True
457
+ )
458
+ @option(
459
+ "--enable-types",
460
+ type=bool,
461
+ help="Enable TypeScript type generation from routes.",
462
+ required=False,
463
+ show_default=False,
464
+ is_flag=True,
465
+ )
466
+ @option(
467
+ "--generate-zod",
468
+ type=bool,
469
+ help="Generate Zod schemas for runtime validation.",
470
+ required=False,
471
+ show_default=False,
472
+ is_flag=True,
473
+ )
474
+ @option(
475
+ "--generate-client",
476
+ type=bool,
477
+ help="Generate API client from OpenAPI schema.",
478
+ required=False,
479
+ show_default=False,
480
+ is_flag=True,
481
+ )
482
+ @option("--overwrite", type=bool, help="Overwrite any files in place.", default=False, is_flag=True)
21
483
  @option("--verbose", type=bool, help="Enable verbose output.", default=False, is_flag=True)
22
- def vite_build(app: Litestar, verbose: bool) -> None: # noqa: ARG001
23
- """Run vite build."""
24
- console.rule("[yellow]Starting Vite build process[/]", align="left")
484
+ @option(
485
+ "--no-prompt",
486
+ help="Do not prompt for confirmation and use all defaults for initializing the project.",
487
+ type=bool,
488
+ default=False,
489
+ required=False,
490
+ show_default=True,
491
+ is_flag=True,
492
+ )
493
+ @option(
494
+ "--no-install",
495
+ help="Do not execute the installation commands after generating templates.",
496
+ type=bool,
497
+ default=False,
498
+ required=False,
499
+ show_default=True,
500
+ is_flag=True,
501
+ )
502
+ def vite_init(
503
+ ctx: "Context",
504
+ template: "str | None",
505
+ vite_port: "int | None",
506
+ enable_ssr: "bool | None",
507
+ asset_url: "str | None",
508
+ root_path: "Path | None",
509
+ frontend_dir: str,
510
+ bundle_path: "Path | None",
511
+ resource_path: "Path | None",
512
+ static_path: "Path | None",
513
+ tailwind: "bool",
514
+ enable_types: "bool",
515
+ generate_zod: "bool",
516
+ generate_client: "bool",
517
+ overwrite: "bool",
518
+ verbose: "bool",
519
+ no_prompt: "bool",
520
+ no_install: "bool",
521
+ ) -> None:
522
+ """Initialize a new Vite project with framework templates."""
523
+ if callable(ctx.obj):
524
+ ctx.obj = ctx.obj()
525
+ elif verbose:
526
+ ctx.obj.app.debug = True
527
+ env: LitestarEnv = ctx.obj
528
+ plugin = env.app.plugins.get(VitePlugin)
529
+ config = plugin._config # pyright: ignore[reportPrivateUsage]
530
+
531
+ console.rule("[yellow]Initializing Vite[/]", align="left")
532
+
533
+ root_path = Path(root_path or config.root_dir or Path.cwd())
534
+ frontend_dir = frontend_dir or "."
535
+ asset_url = asset_url or config.asset_url
536
+ vite_port = vite_port or config.port
537
+ litestar_port = env.port or 8000
538
+ template, framework = _select_framework_template(template, no_prompt)
539
+ console.print(f"\n[green]Using {framework.name} template[/]")
540
+ resource_path_str = str(resource_path or framework.resource_dir or config.resource_dir)
541
+ bundle_path_str = str(bundle_path or config.bundle_dir)
542
+ static_path_str = str(static_path or config.static_dir)
543
+
544
+ if (
545
+ any((root_path / p).exists() for p in [resource_path_str, bundle_path_str, static_path_str])
546
+ and not any([overwrite, no_prompt])
547
+ and not Confirm.ask("Files were found in the paths specified. Are you sure you wish to overwrite the contents?")
548
+ ):
549
+ console.print("Skipping Vite initialization")
550
+ sys.exit(2)
551
+
552
+ enable_ssr, tailwind, enable_types, generate_zod, generate_client = _prompt_for_options(
553
+ framework, enable_ssr, tailwind, enable_types, generate_zod, generate_client, no_prompt
554
+ )
555
+
556
+ project_name = root_path.name or "my-project"
557
+ is_inertia = framework.type in {
558
+ FrameworkType.REACT_INERTIA,
559
+ FrameworkType.VUE_INERTIA,
560
+ FrameworkType.SVELTE_INERTIA,
561
+ }
562
+ context = TemplateContext(
563
+ project_name=project_name,
564
+ framework=framework,
565
+ use_typescript=framework.uses_typescript,
566
+ use_tailwind=tailwind,
567
+ vite_port=vite_port,
568
+ litestar_port=litestar_port,
569
+ asset_url=asset_url,
570
+ resource_dir=resource_path_str,
571
+ bundle_dir=bundle_path_str,
572
+ static_dir=static_path_str,
573
+ base_dir=frontend_dir,
574
+ enable_ssr=enable_ssr,
575
+ enable_inertia=is_inertia,
576
+ enable_types=enable_types,
577
+ generate_zod=generate_zod,
578
+ generate_client=generate_client,
579
+ )
580
+
581
+ console.print("\n[yellow]Generating project files...[/]")
582
+ generated = generate_project(root_path, context, overwrite=overwrite)
583
+ console.print(f"\n[green]Generated {len(generated)} files[/]")
584
+
585
+ if not no_install:
586
+ console.rule("[yellow]Starting package installation process[/]", align="left")
587
+ config.executor.install(root_path)
588
+
589
+ console.print("\n[bold green]Vite initialization complete![/]")
590
+
591
+ _print_recommended_config(template, context.resource_dir, context.bundle_dir)
592
+
593
+ next_steps_cmd = _format_command(config.run_command)
594
+ console.print(f"\n[dim]Next steps:\n cd {root_path}\n {next_steps_cmd}[/]")
595
+
596
+
597
+ @vite_group.command(name="install", help="Install frontend packages.")
598
+ @option("--verbose", type=bool, help="Enable verbose output.", default=False, is_flag=True)
599
+ @option("--quiet", type=bool, help="Suppress non-essential output.", default=False, is_flag=True)
600
+ def vite_install(app: "Litestar", verbose: "bool", quiet: "bool") -> None:
601
+ """Install frontend packages."""
602
+ if verbose:
603
+ app.debug = True
604
+
605
+ plugin = app.plugins.get(VitePlugin)
606
+
607
+ _apply_cli_log_level(plugin.config, verbose=verbose, quiet=quiet)
608
+
609
+ if not quiet:
610
+ console.rule("[yellow]Starting package installation process[/]", align="left")
611
+
612
+ if plugin.config.executor:
613
+ root_dir = Path(plugin.config.root_dir or Path.cwd())
614
+ plugin.config.executor.install(root_dir)
615
+ else:
616
+ console.print("[red]Executor not configured.[/]")
617
+
618
+
619
+ @vite_group.command(name="build", help="Building frontend assets with Vite.")
620
+ @option("--verbose", type=bool, help="Enable verbose output.", default=False, is_flag=True)
621
+ @option("--quiet", type=bool, help="Suppress non-essential output.", default=False, is_flag=True)
622
+ def vite_build(app: "Litestar", verbose: "bool", quiet: "bool") -> None:
623
+ """Run vite build.
624
+
625
+ Raises:
626
+ SystemExit: If the build fails.
627
+ """
628
+ if verbose:
629
+ app.debug = True
630
+
631
+ plugin = app.plugins.get(VitePlugin)
632
+
633
+ _apply_cli_log_level(plugin.config, verbose=verbose, quiet=quiet)
634
+
635
+ if not quiet:
636
+ console.rule("[yellow]Starting Vite build process[/]", align="left")
637
+ _generate_schema_and_routes(app, plugin.config, console)
638
+ if plugin.config.set_environment:
639
+ set_environment(config=plugin.config)
640
+
641
+ executor = plugin.config.executor
642
+ try:
643
+ root_dir = plugin.config.root_dir or Path.cwd()
644
+ if not (Path(root_dir) / "node_modules").exists():
645
+ console.print("[dim]Installing frontend dependencies (node_modules missing)...[/]")
646
+ executor.install(Path(root_dir))
647
+ ext = plugin.config.runtime.external_dev_server
648
+ if isinstance(ext, ExternalDevServer) and ext.enabled:
649
+ build_cmd = ext.build_command or executor.build_command
650
+ console.print(f"[dim]Running external build: {' '.join(build_cmd)}[/]")
651
+ executor.execute(build_cmd, cwd=root_dir)
652
+ else:
653
+ executor.execute(plugin.config.build_command, cwd=root_dir)
654
+ console.print("[bold green]✓ Assets built[/]")
655
+ except ViteExecutionError as e:
656
+ console.print(f"[bold red]x Asset build failed: {e!s}[/]")
657
+ raise SystemExit(1) from None
658
+
659
+
660
+ @vite_group.command(name="deploy", help="Build and deploy Vite assets to remote storage via fsspec.")
661
+ @option("--storage", type=str, help="Override storage backend URL (e.g., gcs://bucket/assets).")
662
+ @option(
663
+ "--storage-option",
664
+ type=str,
665
+ multiple=True,
666
+ help="Storage option key=value forwarded to fsspec (repeat for multiple).",
667
+ )
668
+ @option("--no-build", is_flag=True, help="Deploy existing build without running Vite build.")
669
+ @option("--dry-run", is_flag=True, help="Preview upload/delete plan without making changes.")
670
+ @option("--no-delete", is_flag=True, help="Do not delete orphaned remote files.")
671
+ @option(
672
+ "--verbose",
673
+ is_flag=True,
674
+ help="Enable verbose output. Install providers separately: gcsfs for gcs://, s3fs for s3://, adlfs for abfs://.",
675
+ )
676
+ def vite_deploy(
677
+ app: "Litestar",
678
+ storage: "str | None",
679
+ storage_option: "tuple[str, ...]",
680
+ no_build: bool,
681
+ dry_run: bool,
682
+ no_delete: bool,
683
+ verbose: bool,
684
+ ) -> None:
685
+ """Build and deploy assets to CDN-backed storage."""
686
+ if verbose:
687
+ app.debug = True
688
+
689
+ plugin = app.plugins.get(VitePlugin)
690
+ config = plugin.config
691
+
692
+ try:
693
+ storage_options = _parse_storage_options(storage_option)
694
+ deploy_config = _build_deploy_config(config, storage, storage_options, no_delete)
695
+ except ValueError as exc: # pragma: no cover - CLI validation path
696
+ console.print(f"[red]{exc}[/]")
697
+ sys.exit(1)
698
+ except SystemExit as exc:
699
+ console.print(f"[red]{exc}[/]")
700
+ sys.exit(1)
701
+
702
+ root_dir = Path(config.root_dir or Path.cwd())
703
+ bundle_dir = config.bundle_dir
704
+ try:
705
+ _run_vite_build(config, root_dir, console, no_build)
706
+ except SystemExit as exc:
707
+ console.print(f"[red]{exc}[/]")
708
+ sys.exit(1)
709
+
710
+ console.rule("[yellow]Deploying assets[/]", align="left")
711
+ console.print(f"Storage: {deploy_config.storage_backend}")
712
+ console.print(f"Delete orphaned: {deploy_config.delete_orphaned}")
713
+ if dry_run:
714
+ console.print("[dim]Dry-run enabled. No changes will be made.[/]")
715
+
716
+ try:
717
+ deployer = ViteDeployer(bundle_dir=bundle_dir, manifest_name=config.manifest_name, deploy_config=deploy_config)
718
+ except ImportError as exc: # pragma: no cover - backend import errors
719
+ console.print(f"[red]Missing backend dependency: {exc}[/]")
720
+ console.print("Install provider package, e.g., `pip install gcsfs` for gcs:// URLs.")
721
+ sys.exit(1)
722
+ except ValueError as exc:
723
+ console.print(f"[red]{exc}[/]")
724
+ sys.exit(1)
725
+
726
+ def _on_progress(action: str, path: str) -> None:
727
+ symbol = "+" if action == "upload" else "-"
728
+ console.print(f" {symbol} {path}")
729
+
730
+ result = deployer.sync(dry_run=dry_run, on_progress=_on_progress)
731
+
732
+ console.rule("[yellow]Deploy summary[/]", align="left")
733
+ console.print(f"Uploaded: {len(result.uploaded)} files ({format_bytes(result.uploaded_bytes)})")
734
+ console.print(f"Deleted: {len(result.deleted)} files ({format_bytes(result.deleted_bytes)})")
735
+ console.print(f"Remote: {deployer.remote_path}")
736
+ if result.dry_run:
737
+ console.print("[dim]No changes applied (dry-run).[/]")
738
+ else:
739
+ console.print("[bold green]✓ Deploy complete[/]")
740
+
741
+
742
+ @vite_group.command(
743
+ name="serve",
744
+ help="Serve frontend assets. For SSR frameworks (mode='ssr'), runs production Node server. Otherwise runs Vite dev server.",
745
+ )
746
+ @option("--verbose", type=bool, help="Enable verbose output.", default=False, is_flag=True)
747
+ @option("--quiet", type=bool, help="Suppress non-essential output.", default=False, is_flag=True)
748
+ @option("--production", type=bool, help="Force production mode (run serve_command).", default=False, is_flag=True) # pyright: ignore
749
+ def vite_serve(app: "Litestar", verbose: "bool", quiet: "bool", production: "bool") -> None:
750
+ """Run frontend server.
751
+
752
+ In dev mode (default): Runs the dev server (npm run dev) for all frameworks.
753
+ In production mode (--production or dev_mode=False): Runs the production
754
+ server (npm run serve) for SSR frameworks.
755
+
756
+ Use --production to force running the production server (serve_command).
757
+ """
758
+ if verbose:
759
+ app.debug = True
760
+
761
+ plugin = app.plugins.get(VitePlugin)
762
+
763
+ _apply_cli_log_level(plugin.config, verbose=verbose, quiet=quiet)
764
+ if plugin.config.set_environment:
765
+ set_environment(config=plugin.config)
766
+
767
+ use_production_server = production or not plugin.config.dev_mode
768
+
769
+ if use_production_server:
770
+ console.rule("[yellow]Starting production server[/]", align="left")
771
+ command_to_run = plugin.config.serve_command
772
+ if command_to_run is None:
773
+ console.print("[red]serve_command not configured. Add 'serve' script to package.json.[/]")
774
+ return
775
+ elif plugin.config.hot_reload:
776
+ console.rule("[yellow]Starting Vite process with HMR Enabled[/]", align="left")
777
+ command_to_run = plugin.config.run_command
778
+ else:
779
+ console.rule("[yellow]Starting Vite watch and build process[/]", align="left")
780
+ command_to_run = plugin.config.build_watch_command
781
+
782
+ if plugin.config.executor:
783
+ try:
784
+ root_dir = plugin.config.root_dir or Path.cwd()
785
+ plugin.config.executor.execute(command_to_run, cwd=root_dir)
786
+ console.print("[yellow]Server process stopped.[/]")
787
+ except ViteExecutionError as e:
788
+ console.print(f"[bold red]Server process failed: {e!s}[/]")
789
+ else:
790
+ console.print("[red]Executor not configured.[/]")
791
+
792
+
793
+ @vite_group.command(name="export-routes", help="Export route metadata for type-safe routing.")
794
+ @option(
795
+ "--output",
796
+ help="Output file path",
797
+ type=ClickPath(dir_okay=False, path_type=Path),
798
+ default=None,
799
+ show_default=False,
800
+ )
801
+ @option("--only", help="Only include routes matching these patterns (comma-separated)", type=str, default=None)
802
+ @option("--except", "exclude", help="Exclude routes matching these patterns (comma-separated)", type=str, default=None)
803
+ @option("--include-components", help="Include Inertia component names", type=bool, default=True, is_flag=True)
804
+ @option(
805
+ "--typescript",
806
+ "--ts",
807
+ "typescript",
808
+ help="Generate typed routes.ts file (Ziggy-style) instead of JSON",
809
+ type=bool,
810
+ default=False,
811
+ is_flag=True,
812
+ )
813
+ @option("--verbose", type=bool, help="Enable verbose output.", default=False, is_flag=True)
814
+ def export_routes(
815
+ app: "Litestar",
816
+ output: "Path | None",
817
+ only: "str | None",
818
+ exclude: "str | None",
819
+ include_components: "bool",
820
+ typescript: "bool",
821
+ verbose: "bool",
822
+ ) -> None:
823
+ """Export route metadata for type-safe routing.
824
+
825
+ Args:
826
+ app: The Litestar application instance.
827
+ output: The path to the output file. Uses TypeGenConfig if not provided.
828
+ only: Comma-separated list of route patterns to include.
829
+ exclude: Comma-separated list of route patterns to exclude.
830
+ include_components: Include Inertia component names in output.
831
+ typescript: Generate typed routes.ts file instead of JSON.
832
+ verbose: Whether to enable verbose output.
833
+
834
+ Raises:
835
+ LitestarCLIException: If the output file cannot be written.
836
+ """
837
+ if verbose:
838
+ app.debug = True
839
+
840
+ plugin = app.plugins.get(VitePlugin)
841
+ config = plugin.config
842
+
843
+ only_list = [p.strip() for p in only.split(",")] if only else None
844
+ exclude_list = [p.strip() for p in exclude.split(",")] if exclude else None
845
+
846
+ if typescript:
847
+ if output is None:
848
+ if isinstance(config.types, TypeGenConfig) and config.types.routes_ts_path:
849
+ output = config.types.routes_ts_path
850
+ else:
851
+ output = Path("routes.ts")
852
+
853
+ console.rule(f"[yellow]Exporting typed routes to {output}[/]", align="left")
854
+
855
+ routes_ts_content = generate_routes_ts(app, only=only_list, exclude=exclude_list)
856
+
857
+ try:
858
+ output.parent.mkdir(parents=True, exist_ok=True)
859
+ output.write_text(routes_ts_content, encoding="utf-8")
860
+ console.print(f"[green]✓ Typed routes exported to {output}[/]")
861
+ except OSError as e: # pragma: no cover
862
+ msg = f"Failed to write routes to path {output}"
863
+ raise LitestarCLIException(msg) from e
864
+ else:
865
+ if output is None:
866
+ if isinstance(config.types, TypeGenConfig) and config.types.routes_path is not None:
867
+ output = config.types.routes_path
868
+ elif isinstance(config.types, TypeGenConfig):
869
+ output = config.types.output / "routes.json"
870
+ else:
871
+ output = Path("routes.json")
872
+
873
+ console.rule(f"[yellow]Exporting routes to {output}[/]", align="left")
874
+
875
+ routes_data = generate_routes_json(
876
+ app, only=only_list, exclude=exclude_list, include_components=include_components
877
+ )
878
+
879
+ try:
880
+ content = msgspec.json.format(encode_json(routes_data), indent=2)
881
+ output.parent.mkdir(parents=True, exist_ok=True)
882
+ output.write_bytes(content)
883
+ console.print(f"[green]✓ Routes exported to {output}[/]")
884
+ console.print(f"[dim] {len(routes_data.get('routes', {}))} routes exported[/]")
885
+ except OSError as e: # pragma: no cover
886
+ msg = f"Failed to write routes to path {output}"
887
+ raise LitestarCLIException(msg) from e
888
+
889
+
890
+ def _export_openapi_schema(app: "Litestar", types_config: Any) -> None:
891
+ """Export OpenAPI schema to file.
892
+
893
+ Args:
894
+ app: The Litestar application instance.
895
+ types_config: The TypeGenConfig instance.
896
+
897
+ Raises:
898
+ LitestarCLIException: If export fails.
899
+ """
900
+ console.print("[dim]1. Exporting OpenAPI schema...[/]")
901
+ try:
902
+ serializer = get_serializer(app.type_encoders)
903
+ schema_dict = app.openapi_schema.to_schema()
904
+ schema_content = msgspec.json.format(encode_json(schema_dict, serializer=serializer), indent=2)
905
+ types_config.openapi_path.parent.mkdir(parents=True, exist_ok=True)
906
+ types_config.openapi_path.write_bytes(schema_content)
907
+ console.print(f"[green]✓ Schema exported to {_relative_path(types_config.openapi_path)}[/]")
908
+ except OSError as e:
909
+ msg = f"Failed to export OpenAPI schema: {e}"
910
+ raise LitestarCLIException(msg) from e
911
+
912
+
913
+ def _export_routes_metadata(app: "Litestar", types_config: Any) -> None:
914
+ """Export routes metadata to file.
915
+
916
+ Args:
917
+ app: The Litestar application instance.
918
+ types_config: The TypeGenConfig instance.
919
+
920
+ Raises:
921
+ LitestarCLIException: If export fails.
922
+ """
923
+ console.print("[dim]2. Exporting route metadata...[/]")
924
+ try:
925
+ routes_data = generate_routes_json(app, include_components=True)
926
+ routes_data["litestar_version"] = resolve_litestar_version()
927
+ routes_content = msgspec.json.format(encode_json(routes_data), indent=2)
928
+ types_config.routes_path.parent.mkdir(parents=True, exist_ok=True)
929
+ types_config.routes_path.write_bytes(routes_content)
930
+ console.print(f"[green]✓ Routes exported to {_relative_path(types_config.routes_path)}[/]")
931
+ except OSError as e:
932
+ msg = f"Failed to export routes: {e}"
933
+ raise LitestarCLIException(msg) from e
934
+
935
+
936
+ def _export_routes_typescript(app: "Litestar", types_config: Any) -> None:
937
+ """Export typed routes TypeScript file (Ziggy-style).
938
+
939
+ Args:
940
+ app: The Litestar application instance.
941
+ types_config: The TypeGenConfig instance.
942
+
943
+ Raises:
944
+ LitestarCLIException: If export fails.
945
+ """
946
+ if not types_config.generate_routes:
947
+ return
948
+
949
+ console.print("[dim] Generating typed routes.ts...[/]")
950
+ try:
951
+ ts_content = generate_routes_ts(app)
952
+ types_config.routes_ts_path.parent.mkdir(parents=True, exist_ok=True)
953
+ types_config.routes_ts_path.write_text(ts_content)
954
+ console.print(f"[green]✓ Typed routes generated at {_relative_path(types_config.routes_ts_path)}[/]")
955
+ except OSError as e:
956
+ msg = f"Failed to generate typed routes: {e}"
957
+ raise LitestarCLIException(msg) from e
958
+
959
+
960
+ def _export_inertia_pages_metadata(
961
+ app: "Litestar", types_config: Any, inertia_config: Any, openapi_schema: "dict[str, Any] | None" = None
962
+ ) -> None:
963
+ """Export Inertia page props metadata to file.
964
+
965
+ Args:
966
+ app: The Litestar application instance.
967
+ types_config: The TypeGenConfig instance.
968
+ inertia_config: The InertiaConfig instance.
969
+ openapi_schema: Optional pre-loaded OpenAPI schema.
970
+
971
+ Raises:
972
+ LitestarCLIException: If export fails.
973
+ """
974
+ console.print("[dim]4. Exporting Inertia page props metadata...[/]")
975
+ inertia_type_gen = inertia_config.type_gen
976
+ if inertia_type_gen is None:
977
+ inertia_type_gen = InertiaTypeGenConfig()
978
+
979
+ try:
980
+ pages_data = generate_inertia_pages_json(
981
+ app,
982
+ openapi_schema=openapi_schema,
983
+ include_default_auth=inertia_type_gen.include_default_auth,
984
+ include_default_flash=inertia_type_gen.include_default_flash,
985
+ inertia_config=inertia_config,
986
+ types_config=types_config,
987
+ )
988
+ pages_content = msgspec.json.format(encode_json(pages_data), indent=2)
989
+ types_config.page_props_path.parent.mkdir(parents=True, exist_ok=True)
990
+ types_config.page_props_path.write_bytes(pages_content)
991
+ num_pages = len(pages_data.get("pages", {}))
992
+ console.print(f"[green]✓ Page props exported to {_relative_path(types_config.page_props_path)}[/]")
993
+ console.print(f"[dim] {num_pages} Inertia page(s) found[/]")
994
+ except OSError as e:
995
+ msg = f"Failed to export Inertia page props: {e}"
996
+ raise LitestarCLIException(msg) from e
997
+
998
+
999
+ def _get_package_executor_cmd(executor: "str | None", package: str) -> "list[str]":
1000
+ """Build package executor command list.
1001
+
1002
+ Maps executor to its "npx equivalent" and returns a command list
1003
+ suitable for subprocess.run.
1004
+
1005
+ Args:
1006
+ executor: The JS runtime executor (node, bun, deno, yarn, pnpm).
1007
+ package: The package to run.
1008
+
1009
+ Returns:
1010
+ Command list for subprocess.run.
1011
+ """
1012
+ match executor:
1013
+ case "bun":
1014
+ return ["bunx", package]
1015
+ case "deno":
1016
+ return ["deno", "run", "-A", f"npm:{package}"]
1017
+ case "yarn":
1018
+ return ["yarn", "dlx", package]
1019
+ case "pnpm":
1020
+ return ["pnpm", "dlx", package]
1021
+ case _:
1022
+ return ["npx", package]
1023
+
1024
+
1025
+ def _run_openapi_ts(
1026
+ config: Any, verbose: bool, install_command: "list[str] | None" = None, executor: "str | None" = None
1027
+ ) -> None:
1028
+ """Run @hey-api/openapi-ts to generate TypeScript types.
1029
+
1030
+ Args:
1031
+ config: The ViteConfig instance (with .types resolved).
1032
+ verbose: Whether to show verbose output.
1033
+ install_command: Command used to install JS dependencies.
1034
+ executor: The JS runtime executor (node, bun, deno, yarn, pnpm).
1035
+ """
1036
+ types_config = config.types
1037
+ root_dir = config.root_dir or Path.cwd()
1038
+ resource_dir = Path(config.resource_dir)
1039
+ if not resource_dir.is_absolute():
1040
+ resource_dir = root_dir / resource_dir
1041
+
1042
+ console.print("[dim]3. Running @hey-api/openapi-ts...[/]")
1043
+
1044
+ install_cmd = install_command or ["npm", "install"]
1045
+ pkg_cmd = _get_package_executor_cmd(executor, "@hey-api/openapi-ts")
1046
+
1047
+ try:
1048
+ check_cmd = [*pkg_cmd, "--version"]
1049
+ subprocess.run(check_cmd, check=True, capture_output=True, cwd=root_dir)
1050
+
1051
+ candidate_configs = [
1052
+ resource_dir / "openapi-ts.config.ts",
1053
+ resource_dir / "hey-api.config.ts",
1054
+ root_dir / "openapi-ts.config.ts",
1055
+ root_dir / "hey-api.config.ts",
1056
+ ]
1057
+ config_path = next((p for p in candidate_configs if p.exists()), None)
1058
+
1059
+ if config_path is not None:
1060
+ openapi_cmd = [*pkg_cmd, "--file", str(config_path)]
1061
+ else:
1062
+ openapi_cmd = [*pkg_cmd, "-i", str(types_config.openapi_path), "-o", str(types_config.output)]
1063
+
1064
+ plugins: list[str] = ["@hey-api/typescript", "@hey-api/schemas"]
1065
+ if types_config.generate_sdk:
1066
+ plugins.extend(["@hey-api/sdk"])
1067
+ if types_config.generate_zod:
1068
+ plugins.append("zod")
1069
+
1070
+ if plugins:
1071
+ openapi_cmd.extend(["--plugins", *plugins])
1072
+
1073
+ subprocess.run(openapi_cmd, check=True, cwd=root_dir)
1074
+ console.print(f"[green]✓ Types generated in {types_config.output}[/]")
1075
+ except subprocess.CalledProcessError as e:
1076
+ console.print("[yellow]! @hey-api/openapi-ts failed - install it with:[/]")
1077
+ extra = ["@hey-api/openapi-ts"]
1078
+ if types_config.generate_zod:
1079
+ extra.append("zod")
1080
+ console.print(f"[dim] {' '.join([*install_cmd, '-D', *extra])}[/]")
1081
+ if verbose:
1082
+ console.print(f"[dim]Error: {e!s}[/]")
1083
+ except FileNotFoundError:
1084
+ runtime_name = executor or "Node.js"
1085
+ console.print(f"[yellow]! Package executor not found - ensure {runtime_name} is installed[/]")
1086
+
1087
+
1088
+ @vite_group.command(name="generate-types", help="Generate TypeScript types from OpenAPI schema and routes.")
1089
+ @option("--verbose", type=bool, help="Enable verbose output.", default=False, is_flag=True)
1090
+ def generate_types(app: "Litestar", verbose: "bool") -> None:
1091
+ """Generate TypeScript types from OpenAPI schema and routes.
1092
+
1093
+ This command:
1094
+ 1. Exports the OpenAPI schema (uses litestar's built-in schema generation)
1095
+ 2. Exports route metadata
1096
+ 3. Runs @hey-api/openapi-ts to generate TypeScript types
1097
+ 4. If Inertia is enabled: exports page props metadata
1098
+
1099
+ Args:
1100
+ app: The Litestar application instance.
1101
+ verbose: Whether to enable verbose output.
1102
+ """
1103
+ if verbose:
1104
+ app.debug = True
1105
+
1106
+ plugin = app.plugins.get(VitePlugin)
1107
+ config = plugin.config
1108
+
1109
+ if not isinstance(config.types, TypeGenConfig):
1110
+ console.print("[yellow]Type generation is not enabled in ViteConfig[/]")
1111
+ console.print("[dim]Set types=True or types=TypeGenConfig() in ViteConfig[/]")
1112
+ return
1113
+
1114
+ console.rule("[yellow]Generating TypeScript types[/]", align="left")
1115
+
1116
+ _export_openapi_schema(app, config.types)
1117
+ _export_routes_metadata(app, config.types)
1118
+ _export_routes_typescript(app, config.types)
1119
+ _run_openapi_ts(config, verbose, config.install_command, config.runtime.executor)
1120
+ if isinstance(config.inertia, InertiaConfig) and config.types.generate_page_props and config.types.page_props_path:
1121
+ openapi_schema: dict[str, Any] | None = None
1122
+ try:
1123
+ if config.types.openapi_path and config.types.openapi_path.exists():
1124
+ openapi_schema = decode_json(config.types.openapi_path.read_bytes())
1125
+ except (OSError, SerializationException):
1126
+ if verbose:
1127
+ console.print("[dim]! Could not load OpenAPI schema for type references[/]")
1128
+
1129
+ _export_inertia_pages_metadata(app, config.types, config.inertia, openapi_schema)
1130
+
1131
+
1132
+ @vite_group.command(name="status", help="Check the status of the Vite integration.")
1133
+ def vite_status(app: "Litestar") -> None:
1134
+ """Check the status of the Vite integration."""
1135
+ import httpx
1136
+
1137
+ plugin = app.plugins.get(VitePlugin)
1138
+ config = plugin.config
1139
+
1140
+ console.rule("[yellow]Vite Integration Status[/]", align="left")
1141
+ console.print(f"Dev Mode: {config.dev_mode}")
1142
+ console.print(f"Hot Reload: {config.hot_reload}")
1143
+ console.print(f"Assets URL: {config.asset_url}")
1144
+ console.print(f"Base URL: {config.base_url}")
1145
+
1146
+ manifest_path = Path(f"{config.bundle_dir}/{config.manifest_name}")
1147
+ if manifest_path.exists():
1148
+ console.print(f"[green]✓ Manifest found at {manifest_path}[/]")
1149
+ else:
1150
+ console.print(f"[red]✗ Manifest not found at {manifest_path}[/]")
1151
+
1152
+ if config.dev_mode:
1153
+ url = f"{config.protocol}://{config.host}:{config.port}"
1154
+ try:
1155
+ response = httpx.get(url, timeout=0.5)
1156
+ if response.status_code == 200:
1157
+ console.print(f"[green]✓ Vite server running at {url}[/]")
1158
+ else:
1159
+ console.print(f"[yellow]! Vite server reachable at {url} but returned {response.status_code}[/]")
1160
+ except httpx.HTTPError as e:
1161
+ console.print(f"[red]✗ Vite server not reachable at {url}: {e!s}[/]")