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