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
@@ -0,0 +1,653 @@
1
+ """Route metadata extraction and Ziggy-compatible generation."""
2
+
3
+ import contextlib
4
+ import re
5
+ from collections.abc import Generator
6
+ from dataclasses import dataclass, field
7
+ from typing import Any, cast
8
+
9
+ from litestar import Litestar
10
+ from litestar._openapi.datastructures import OpenAPIContext # pyright: ignore[reportPrivateUsage]
11
+ from litestar._openapi.parameters import ( # pyright: ignore[reportPrivateUsage,reportPrivateImportUsage]
12
+ create_parameters_for_handler,
13
+ )
14
+ from litestar.handlers import HTTPRouteHandler
15
+ from litestar.routes import HTTPRoute
16
+
17
+ from litestar_vite._codegen.ts import normalize_path, ts_type_from_openapi
18
+
19
+ _PATH_PARAM_EXTRACT_PATTERN = re.compile(r"\{([^:}]+)(?::([^}]+))?\}")
20
+
21
+ _TS_SEMANTIC_ALIASES: dict[str, tuple[str, str]] = {
22
+ "UUID": ("UUID v4 string", "string"),
23
+ "DateTime": ("RFC 3339 date-time string", "string"),
24
+ "DateOnly": ("ISO 8601 date string (YYYY-MM-DD)", "string"),
25
+ "TimeOnly": ("ISO 8601 time string", "string"),
26
+ "Duration": ("ISO 8601 duration string", "string"),
27
+ "Email": ("Email address string", "string"),
28
+ "URI": ("URI/URL string", "string"),
29
+ "IPv4": ("IPv4 address string", "string"),
30
+ "IPv6": ("IPv6 address string", "string"),
31
+ }
32
+
33
+
34
+ def _str_dict_factory() -> dict[str, str]:
35
+ """Return an empty ``dict[str, str]`` (typed for pyright).
36
+
37
+ Returns:
38
+ An empty dictionary.
39
+ """
40
+ return {}
41
+
42
+
43
+ @dataclass
44
+ class RouteMetadata:
45
+ """Metadata for a single route."""
46
+
47
+ name: str
48
+ path: str
49
+ methods: list[str]
50
+ params: dict[str, str] = field(default_factory=_str_dict_factory)
51
+ query_params: dict[str, str] = field(default_factory=_str_dict_factory)
52
+ component: "str | None" = None
53
+
54
+
55
+ def _extract_path_params(path: str) -> dict[str, str]:
56
+ """Extract path parameters and their types from a route.
57
+
58
+ Args:
59
+ path: The route path template.
60
+
61
+ Returns:
62
+ Mapping of parameter name to TypeScript type.
63
+ """
64
+ return {match.group(1): "string" for match in _PATH_PARAM_EXTRACT_PATTERN.finditer(path)}
65
+
66
+
67
+ def _iter_route_handlers(app: Litestar) -> Generator[tuple["HTTPRoute", HTTPRouteHandler], None, None]:
68
+ """Iterate over HTTP route handlers in an app.
69
+
70
+ Args:
71
+ app: The Litestar application.
72
+
73
+ Yields:
74
+ Tuples of (HTTPRoute, HTTPRouteHandler).
75
+ """
76
+ for route in app.routes:
77
+ if isinstance(route, HTTPRoute):
78
+ for route_handler in route.route_handlers:
79
+ yield route, route_handler
80
+
81
+
82
+ def _extract_params_from_litestar(
83
+ handler: HTTPRouteHandler, http_route: "HTTPRoute", openapi_context: OpenAPIContext | None
84
+ ) -> tuple[dict[str, str], dict[str, str]]:
85
+ """Extract path and query parameters using Litestar's native OpenAPI generation.
86
+
87
+ Args:
88
+ handler: The route handler.
89
+ http_route: The HTTP route.
90
+ openapi_context: The OpenAPI context, if available.
91
+
92
+ Returns:
93
+ A tuple of (path_params, query_params) maps.
94
+ """
95
+ path_params: dict[str, str] = {}
96
+ query_params: dict[str, str] = {}
97
+
98
+ if openapi_context is None:
99
+ return path_params, query_params
100
+
101
+ try:
102
+ route_path_params = http_route.path_parameters
103
+ params = create_parameters_for_handler(openapi_context, handler, route_path_params)
104
+
105
+ for param in params:
106
+ schema_dict = param.schema.to_schema() if param.schema else None
107
+ ts_type = ts_type_from_openapi(schema_dict or {}) if schema_dict else "any"
108
+ # For URL generation, `null` is not a meaningful value (it would stringify to "null").
109
+ # Treat `null` as "missing" rather than emitting `| null` into route parameter types.
110
+ ts_type = ts_type.replace(" | null", "").replace("null | ", "")
111
+
112
+ if not param.required and ts_type != "any" and "undefined" not in ts_type:
113
+ ts_type = f"{ts_type} | undefined"
114
+
115
+ match param.param_in:
116
+ case "path":
117
+ path_params[param.name] = ts_type.replace(" | undefined", "")
118
+ case "query":
119
+ query_params[param.name] = ts_type
120
+ case _:
121
+ pass
122
+
123
+ except (AttributeError, TypeError, ValueError, KeyError):
124
+ pass
125
+
126
+ return path_params, query_params
127
+
128
+
129
+ def _make_unique_name(base_name: str, used_names: set[str], path: str, methods: list[str]) -> str:
130
+ """Generate a unique route name, avoiding collisions.
131
+
132
+ Returns:
133
+ A unique route name.
134
+ """
135
+ if base_name not in used_names:
136
+ return base_name
137
+
138
+ path_suffix = path.strip("/").replace("/", "_").replace("{", "").replace("}", "").replace("-", "_")
139
+ method_suffix = methods[0].lower() if methods else ""
140
+
141
+ candidate = f"{base_name}_{path_suffix}" if path_suffix else base_name
142
+ if candidate not in used_names:
143
+ return candidate
144
+
145
+ candidate = f"{base_name}_{path_suffix}_{method_suffix}" if path_suffix else f"{base_name}_{method_suffix}"
146
+ if candidate not in used_names:
147
+ return candidate
148
+
149
+ counter = 2
150
+ while f"{candidate}_{counter}" in used_names:
151
+ counter += 1
152
+ return f"{candidate}_{counter}"
153
+
154
+
155
+ def extract_route_metadata(
156
+ app: Litestar,
157
+ *,
158
+ only: "list[str] | None" = None,
159
+ exclude: "list[str] | None" = None,
160
+ openapi_schema: dict[str, Any] | None = None,
161
+ ) -> list[RouteMetadata]:
162
+ """Extract route metadata from a Litestar application.
163
+
164
+ Note:
165
+ ``openapi_schema`` is accepted for API compatibility and future enrichment,
166
+ but parameter typing is currently derived from Litestar's OpenAPI parameter
167
+ generation, not the exported schema document.
168
+
169
+ Returns:
170
+ A list of RouteMetadata objects.
171
+ """
172
+ routes_metadata: list[RouteMetadata] = []
173
+ used_names: set[str] = set()
174
+
175
+ openapi_context: OpenAPIContext | None = None
176
+ if app.openapi_config is not None:
177
+ with contextlib.suppress(AttributeError, TypeError, ValueError):
178
+ openapi_context = OpenAPIContext(openapi_config=app.openapi_config, plugins=app.plugins.openapi)
179
+
180
+ for http_route, route_handler in _iter_route_handlers(app):
181
+ base_name = route_handler.name or route_handler.handler_name or str(route_handler)
182
+ methods = [method.upper() for method in route_handler.http_methods]
183
+
184
+ if methods in (["OPTIONS"], ["HEAD"]):
185
+ continue
186
+
187
+ full_path = str(http_route.path)
188
+
189
+ if full_path.startswith("/schema"):
190
+ if "openapi.json" in full_path:
191
+ if "openapi.json" in used_names:
192
+ continue
193
+ base_name = "openapi.json"
194
+ elif "openapi.yaml" in full_path or "openapi.yml" in full_path:
195
+ if "openapi.yaml" in used_names:
196
+ continue
197
+ base_name = "openapi.yaml"
198
+ else:
199
+ continue
200
+
201
+ route_name = _make_unique_name(base_name, used_names, full_path, methods)
202
+ used_names.add(route_name)
203
+
204
+ if only and not any(pattern in route_name or pattern in full_path for pattern in only):
205
+ continue
206
+ if exclude and any(pattern in route_name or pattern in full_path for pattern in exclude):
207
+ continue
208
+
209
+ params, query_params = _extract_params_from_litestar(route_handler, http_route, openapi_context)
210
+
211
+ if not params:
212
+ params = _extract_path_params(full_path)
213
+
214
+ normalized_path = normalize_path(full_path)
215
+
216
+ opt: dict[str, Any] = route_handler.opt or {}
217
+ component = opt.get("component")
218
+
219
+ routes_metadata.append(
220
+ RouteMetadata(
221
+ name=route_name,
222
+ path=normalized_path,
223
+ methods=methods,
224
+ params=params,
225
+ query_params=query_params,
226
+ component=cast("str | None", component),
227
+ )
228
+ )
229
+
230
+ return routes_metadata
231
+
232
+
233
+ def generate_routes_json(
234
+ app: Litestar,
235
+ *,
236
+ only: "list[str] | None" = None,
237
+ exclude: "list[str] | None" = None,
238
+ include_components: bool = False,
239
+ openapi_schema: dict[str, Any] | None = None,
240
+ ) -> dict[str, Any]:
241
+ """Generate Ziggy-compatible routes JSON.
242
+
243
+ Returns:
244
+ A Ziggy-compatible routes payload as a dictionary.
245
+ """
246
+ routes_metadata = extract_route_metadata(app, only=only, exclude=exclude, openapi_schema=openapi_schema)
247
+
248
+ routes_dict: dict[str, Any] = {}
249
+
250
+ for route in routes_metadata:
251
+ route_data: dict[str, Any] = {"uri": route.path, "methods": route.methods}
252
+
253
+ if route.params:
254
+ route_data["parameters"] = list(route.params.keys())
255
+ route_data["parameterTypes"] = route.params
256
+
257
+ if route.query_params:
258
+ route_data["queryParameters"] = route.query_params
259
+
260
+ if include_components and route.component:
261
+ route_data["component"] = route.component
262
+
263
+ routes_dict[route.name] = route_data
264
+
265
+ return {"routes": routes_dict}
266
+
267
+
268
+ _TS_TYPE_MAP: dict[str, str] = {
269
+ "string": "string",
270
+ "integer": "number",
271
+ "number": "number",
272
+ "boolean": "boolean",
273
+ "array": "unknown[]",
274
+ "object": "Record<string, unknown>",
275
+ "uuid": "string",
276
+ "date": "string",
277
+ "date-time": "string",
278
+ "email": "string",
279
+ "uri": "string",
280
+ "url": "string",
281
+ "int": "number",
282
+ "float": "number",
283
+ "str": "string",
284
+ "bool": "boolean",
285
+ "path": "string",
286
+ "unknown": "unknown",
287
+ }
288
+
289
+
290
+ def _ts_type_for_param(param_type: str) -> str:
291
+ """Map a parameter type string to TypeScript type.
292
+
293
+ Returns:
294
+ The TypeScript type for the parameter.
295
+ """
296
+ is_optional = "undefined" in param_type or param_type.endswith("?")
297
+ clean_type = param_type.replace(" | undefined", "").replace("?", "").strip()
298
+
299
+ ts_type = _TS_TYPE_MAP.get(clean_type) or clean_type or "string"
300
+
301
+ if is_optional and "undefined" not in ts_type:
302
+ return f"{ts_type} | undefined"
303
+ return ts_type
304
+
305
+
306
+ def _is_type_required(param_type: str) -> bool:
307
+ """Check if a parameter type indicates a required field.
308
+
309
+ Returns:
310
+ True if the parameter is required, otherwise False.
311
+ """
312
+ return "undefined" not in param_type and not param_type.endswith("?")
313
+
314
+
315
+ def _escape_ts_string(s: str) -> str:
316
+ """Escape a string for use in TypeScript string literals.
317
+
318
+ Returns:
319
+ The escaped string.
320
+ """
321
+ return s.replace("\\", "\\\\").replace("'", "\\'").replace('"', '\\"')
322
+
323
+
324
+ def generate_routes_ts(
325
+ app: Litestar,
326
+ *,
327
+ only: "list[str] | None" = None,
328
+ exclude: "list[str] | None" = None,
329
+ openapi_schema: dict[str, Any] | None = None,
330
+ ) -> str:
331
+ """Generate typed routes TypeScript file (Ziggy-style).
332
+
333
+ Returns:
334
+ The generated TypeScript source.
335
+ """
336
+ routes_metadata = extract_route_metadata(app, only=only, exclude=exclude, openapi_schema=openapi_schema)
337
+
338
+ route_names: list[str] = []
339
+ path_params_entries: list[str] = []
340
+ query_params_entries: list[str] = []
341
+ routes_entries: list[str] = []
342
+ used_aliases: set[str] = set()
343
+
344
+ for route in routes_metadata:
345
+ route_name = route.name
346
+ route_names.append(route_name)
347
+
348
+ if route.params:
349
+ param_fields: list[str] = []
350
+ for param_name, param_type in route.params.items():
351
+ ts_type = _ts_type_for_param(param_type)
352
+ ts_type_clean = ts_type.replace(" | undefined", "")
353
+ used_aliases.update(_collect_semantic_aliases(ts_type_clean))
354
+ param_fields.append(f" {param_name}: {ts_type_clean};")
355
+ path_params_entries.append(f" '{route_name}': {{\n" + "\n".join(param_fields) + "\n };")
356
+ else:
357
+ path_params_entries.append(f" '{route_name}': Record<string, never>;")
358
+
359
+ if route.query_params:
360
+ query_param_fields: list[str] = []
361
+ for param_name, param_type in route.query_params.items():
362
+ ts_type = _ts_type_for_param(param_type)
363
+ is_required = _is_type_required(param_type)
364
+ ts_type_clean = ts_type.replace(" | undefined", "")
365
+ used_aliases.update(_collect_semantic_aliases(ts_type_clean))
366
+ if is_required:
367
+ query_param_fields.append(f" {param_name}: {ts_type_clean};")
368
+ else:
369
+ query_param_fields.append(f" {param_name}?: {ts_type_clean};")
370
+ query_params_entries.append(f" '{route_name}': {{\n" + "\n".join(query_param_fields) + "\n };")
371
+ else:
372
+ query_params_entries.append(f" '{route_name}': Record<string, never>;")
373
+
374
+ methods_str = ", ".join(f"'{m}'" for m in route.methods)
375
+ route_entry_lines = [
376
+ f" '{route_name}': {{",
377
+ f" path: '{_escape_ts_string(route.path)}',",
378
+ f" methods: [{methods_str}] as const,",
379
+ ]
380
+ param_names_str = ", ".join(f"'{p}'" for p in route.params) if route.params else ""
381
+ route_entry_lines.append(f" pathParams: [{param_names_str}] as const,")
382
+
383
+ query_names_str = ", ".join(f"'{p}'" for p in route.query_params) if route.query_params else ""
384
+ route_entry_lines.append(f" queryParams: [{query_names_str}] as const,")
385
+ if route.component:
386
+ route_entry_lines.append(f" component: '{_escape_ts_string(route.component)}',")
387
+ route_entry_lines.append(" },")
388
+ routes_entries.append("\n".join(route_entry_lines))
389
+
390
+ route_names_union = "\n | ".join(f"'{name}'" for name in route_names) if route_names else "never"
391
+
392
+ alias_block = _render_semantic_aliases(used_aliases)
393
+ alias_preamble = f"{alias_block}\n\n" if alias_block else ""
394
+
395
+ return f"""// AUTO-GENERATED by litestar-vite. Do not edit.
396
+ /* eslint-disable */
397
+
398
+ // API base URL - only needed for separate dev servers
399
+ // Set VITE_API_URL=http://localhost:8000 when running Vite separately
400
+ const API_URL = (typeof import.meta !== 'undefined' && (import.meta as any).env?.VITE_API_URL) ?? '';
401
+
402
+ {alias_preamble}
403
+ /** All available route names */
404
+ export type RouteName =
405
+ | {route_names_union};
406
+
407
+ /** Path parameter definitions per route */
408
+ export interface RoutePathParams {{
409
+ {chr(10).join(path_params_entries)}
410
+ }}
411
+
412
+ /** Query parameter definitions per route */
413
+ export interface RouteQueryParams {{
414
+ {chr(10).join(query_params_entries)}
415
+ }}
416
+
417
+ type EmptyParams = Record<string, never>
418
+ type MergeParams<A, B> =
419
+ A extends EmptyParams ? (B extends EmptyParams ? EmptyParams : B) : B extends EmptyParams ? A : A & B
420
+
421
+ /** Combined parameters (path + query) */
422
+ export type RouteParams<T extends RouteName> = MergeParams<RoutePathParams[T], RouteQueryParams[T]>
423
+
424
+ /** Route metadata */
425
+ export const routeDefinitions = {{
426
+ {chr(10).join(routes_entries)}
427
+ }} as const
428
+
429
+ /** Check if path params are required for a route */
430
+ type HasRequiredPathParams<T extends RouteName> =
431
+ RoutePathParams[T] extends Record<string, never> ? false : true;
432
+
433
+ /** Check if query params have any required fields */
434
+ type HasRequiredQueryParams<T extends RouteName> =
435
+ RouteQueryParams[T] extends Record<string, never>
436
+ ? false
437
+ : Partial<RouteQueryParams[T]> extends RouteQueryParams[T]
438
+ ? false
439
+ : true;
440
+
441
+ /** Routes that require parameters (path or query) */
442
+ type RoutesWithRequiredParams = {{
443
+ [K in RouteName]: HasRequiredPathParams<K> extends true
444
+ ? K
445
+ : HasRequiredQueryParams<K> extends true
446
+ ? K
447
+ : never;
448
+ }}[RouteName];
449
+
450
+ /** Routes without any required parameters */
451
+ type RoutesWithoutRequiredParams = Exclude<RouteName, RoutesWithRequiredParams>;
452
+
453
+ /**
454
+ * Generate a URL for a named route.
455
+ *
456
+ * @example
457
+ * route('books') // '/api/books'
458
+ * route('book_detail', {{ book_id: 123 }}) // '/api/books/123'
459
+ * route('search', {{ q: 'test', limit: 5 }}) // '/api/search?q=test&limit=5'
460
+ */
461
+ export function route<T extends RoutesWithoutRequiredParams>(name: T): string;
462
+ export function route<T extends RoutesWithoutRequiredParams>(
463
+ name: T,
464
+ params?: RouteParams<T>,
465
+ ): string;
466
+ export function route<T extends RoutesWithRequiredParams>(
467
+ name: T,
468
+ params: RouteParams<T>,
469
+ ): string;
470
+ export function route<T extends RouteName>(
471
+ name: T,
472
+ params?: RouteParams<T>,
473
+ ): string {{
474
+ const def = routeDefinitions[name];
475
+ let url: string = def.path;
476
+
477
+ // Replace path parameters (use replaceAll to handle multiple occurrences)
478
+ if (params) {{
479
+ for (const param of def.pathParams) {{
480
+ const value = (params as Record<string, unknown>)[param];
481
+ if (value !== undefined) {{
482
+ url = url.replaceAll("{{" + param + "}}", String(value));
483
+ }}
484
+ }}
485
+ }}
486
+
487
+ // Add query parameters
488
+ if (params) {{
489
+ const queryParts: string[] = [];
490
+ for (const param of def.queryParams) {{
491
+ const value = (params as Record<string, unknown>)[param];
492
+ if (value !== undefined) {{
493
+ queryParts.push(encodeURIComponent(param) + "=" + encodeURIComponent(String(value)));
494
+ }}
495
+ }}
496
+ if (queryParts.length > 0) {{
497
+ url += "?" + queryParts.join("&");
498
+ }}
499
+ }}
500
+
501
+ // Apply API URL if set (for separate dev servers)
502
+ return API_URL ? API_URL.replace(/\\/$/, '') + url : url;
503
+ }}
504
+
505
+ /** Check if a route exists */
506
+ export function hasRoute(name: string): name is RouteName {{
507
+ return name in routeDefinitions;
508
+ }}
509
+
510
+ /** Get all route names */
511
+ export function getRouteNames(): RouteName[] {{
512
+ return Object.keys(routeDefinitions) as RouteName[];
513
+ }}
514
+
515
+ /** Get route metadata */
516
+ export function getRoute<T extends RouteName>(name: T): (typeof routeDefinitions)[T] {{
517
+ return routeDefinitions[name];
518
+ }}
519
+
520
+ // ============================================================================
521
+ // Route Matching Helpers
522
+ // ============================================================================
523
+
524
+ /** Cache for compiled route patterns */
525
+ const patternCache = new Map<string, RegExp>();
526
+
527
+ /**
528
+ * Compile a route path pattern to a regex for URL matching.
529
+ * Results are cached for performance.
530
+ */
531
+ function compilePattern(path: string): RegExp {{
532
+ const cached = patternCache.get(path);
533
+ if (cached) return cached;
534
+
535
+ // Escape special regex characters except {{ }}
536
+ let pattern = path.replace(/[.*+?^$|()\\[\\]]/g, '\\\\$&');
537
+ // Replace {{param}} or {{param:type}} with matchers
538
+ pattern = pattern.replace(/\\{{([^}}]+)\\}}/g, (_match, paramSpec: string) => {{
539
+ const paramType = paramSpec.includes(':') ? paramSpec.split(':')[1] : 'str';
540
+ switch (paramType) {{
541
+ case 'uuid':
542
+ return '[0-9a-f]{{8}}-[0-9a-f]{{4}}-[0-9a-f]{{4}}-[0-9a-f]{{4}}-[0-9a-f]{{12}}';
543
+ case 'path':
544
+ return '.*';
545
+ case 'int':
546
+ return '\\\\d+';
547
+ default:
548
+ return '[^/]+';
549
+ }}
550
+ }});
551
+ const regex = new RegExp(`^${{pattern}}$`, 'i');
552
+ patternCache.set(path, regex);
553
+ return regex;
554
+ }}
555
+
556
+ /**
557
+ * Convert a URL to its corresponding route name.
558
+ *
559
+ * @param url - URL or path to match (query strings and hashes are stripped)
560
+ * @returns The matching route name, or null if no match found
561
+ *
562
+ * @example
563
+ * toRoute('/api/books') // 'books'
564
+ * toRoute('/api/books/123') // 'book_detail'
565
+ * toRoute('/unknown') // null
566
+ */
567
+ export function toRoute(url: string): RouteName | null {{
568
+ // Strip query string and hash
569
+ const path = url.split('?')[0].split('#')[0];
570
+ // Normalize: remove trailing slash except for root
571
+ const normalized = path === '/' ? path : path.replace(/\\/$/, '');
572
+
573
+ for (const [name, def] of Object.entries(routeDefinitions)) {{
574
+ if (compilePattern(def.path).test(normalized)) {{
575
+ return name as RouteName;
576
+ }}
577
+ }}
578
+ return null;
579
+ }}
580
+
581
+ /**
582
+ * Get the current route name based on the browser URL.
583
+ * Returns null in SSR/non-browser environments.
584
+ *
585
+ * @returns Current route name, or null if no match or not in browser
586
+ *
587
+ * @example
588
+ * // On page /api/books/123
589
+ * currentRoute() // 'book_detail'
590
+ */
591
+ export function currentRoute(): RouteName | null {{
592
+ if (typeof window === 'undefined') return null;
593
+ return toRoute(window.location.pathname);
594
+ }}
595
+
596
+ /**
597
+ * Check if a URL matches a route name or pattern.
598
+ * Supports wildcard patterns with `*` to match multiple routes.
599
+ *
600
+ * @param url - URL or path to check
601
+ * @param pattern - Route name or pattern (e.g., 'books', 'book_*', '*_detail')
602
+ * @returns True if the URL matches the route pattern
603
+ *
604
+ * @example
605
+ * isRoute('/api/books', 'books') // true
606
+ * isRoute('/api/books/123', 'book_*') // true (wildcard)
607
+ */
608
+ export function isRoute(url: string, pattern: string): boolean {{
609
+ const routeName = toRoute(url);
610
+ if (!routeName) return false;
611
+ // Escape special regex chars (except *), then convert * to .*
612
+ const escaped = pattern.replace(/[.+?^$|()\\[\\]{{}}]/g, '\\\\$&');
613
+ const regex = new RegExp(`^${{escaped.replace(/\\*/g, '.*')}}$`);
614
+ return regex.test(routeName);
615
+ }}
616
+
617
+ /**
618
+ * Check if the current browser URL matches a route name or pattern.
619
+ * Supports wildcard patterns with `*` to match multiple routes.
620
+ * Returns false in SSR/non-browser environments.
621
+ *
622
+ * @param pattern - Route name or pattern (e.g., 'books', 'book_*', '*_page')
623
+ * @returns True if current URL matches the route pattern
624
+ *
625
+ * @example
626
+ * // On page /books
627
+ * isCurrentRoute('books_page') // true
628
+ * isCurrentRoute('*_page') // true (wildcard)
629
+ */
630
+ export function isCurrentRoute(pattern: string): boolean {{
631
+ const current = currentRoute();
632
+ if (!current) return false;
633
+ // Escape special regex chars (except *), then convert * to .*
634
+ const escaped = pattern.replace(/[.+?^$|()\\[\\]{{}}]/g, '\\\\$&');
635
+ const regex = new RegExp(`^${{escaped.replace(/\\*/g, '.*')}}$`);
636
+ return regex.test(current);
637
+ }}
638
+ """
639
+
640
+
641
+ def _collect_semantic_aliases(type_expr: str) -> set[str]:
642
+ return {alias for alias in _TS_SEMANTIC_ALIASES if alias in type_expr}
643
+
644
+
645
+ def _render_semantic_aliases(aliases: set[str]) -> str:
646
+ if not aliases:
647
+ return ""
648
+
649
+ lines: list[str] = ["/** Semantic string aliases derived from OpenAPI `format`. */"]
650
+ for alias in sorted(aliases):
651
+ doc, base = _TS_SEMANTIC_ALIASES[alias]
652
+ lines.extend((f"/** {doc} */", f"export type {alias} = {base};", ""))
653
+ return "\n".join(lines).rstrip()