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