litestar-vite 0.15.0__py3-none-any.whl → 0.15.0rc2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- litestar_vite/_codegen/__init__.py +26 -0
- litestar_vite/_codegen/inertia.py +407 -0
- litestar_vite/{codegen/_openapi.py → _codegen/openapi.py} +11 -58
- litestar_vite/{codegen/_routes.py → _codegen/routes.py} +43 -110
- litestar_vite/{codegen/_ts.py → _codegen/ts.py} +19 -19
- litestar_vite/_handler/__init__.py +8 -0
- litestar_vite/{handler/_app.py → _handler/app.py} +29 -117
- litestar_vite/cli.py +254 -155
- litestar_vite/codegen.py +39 -0
- litestar_vite/commands.py +6 -0
- litestar_vite/{config/__init__.py → config.py} +726 -99
- litestar_vite/deploy.py +3 -14
- litestar_vite/doctor.py +6 -8
- litestar_vite/executor.py +1 -45
- litestar_vite/handler.py +9 -0
- litestar_vite/html_transform.py +5 -148
- litestar_vite/inertia/__init__.py +0 -24
- litestar_vite/inertia/_utils.py +0 -5
- litestar_vite/inertia/exception_handler.py +16 -22
- litestar_vite/inertia/helpers.py +18 -546
- litestar_vite/inertia/plugin.py +11 -77
- litestar_vite/inertia/request.py +0 -48
- litestar_vite/inertia/response.py +17 -113
- litestar_vite/inertia/types.py +0 -19
- litestar_vite/loader.py +7 -7
- litestar_vite/plugin.py +2184 -0
- litestar_vite/templates/angular/package.json.j2 +1 -2
- litestar_vite/templates/angular-cli/package.json.j2 +1 -2
- litestar_vite/templates/base/package.json.j2 +1 -2
- litestar_vite/templates/react-inertia/package.json.j2 +1 -2
- litestar_vite/templates/vue-inertia/package.json.j2 +1 -2
- {litestar_vite-0.15.0.dist-info → litestar_vite-0.15.0rc2.dist-info}/METADATA +5 -5
- {litestar_vite-0.15.0.dist-info → litestar_vite-0.15.0rc2.dist-info}/RECORD +36 -49
- litestar_vite/codegen/__init__.py +0 -48
- litestar_vite/codegen/_export.py +0 -229
- litestar_vite/codegen/_inertia.py +0 -619
- litestar_vite/codegen/_utils.py +0 -141
- litestar_vite/config/_constants.py +0 -97
- litestar_vite/config/_deploy.py +0 -70
- litestar_vite/config/_inertia.py +0 -241
- litestar_vite/config/_paths.py +0 -63
- litestar_vite/config/_runtime.py +0 -235
- litestar_vite/config/_spa.py +0 -93
- litestar_vite/config/_types.py +0 -94
- litestar_vite/handler/__init__.py +0 -9
- litestar_vite/inertia/precognition.py +0 -274
- litestar_vite/plugin/__init__.py +0 -687
- litestar_vite/plugin/_process.py +0 -185
- litestar_vite/plugin/_proxy.py +0 -689
- litestar_vite/plugin/_proxy_headers.py +0 -244
- litestar_vite/plugin/_static.py +0 -37
- litestar_vite/plugin/_utils.py +0 -489
- /litestar_vite/{handler/_routing.py → _handler/routing.py} +0 -0
- {litestar_vite-0.15.0.dist-info → litestar_vite-0.15.0rc2.dist-info}/WHEEL +0 -0
- {litestar_vite-0.15.0.dist-info → litestar_vite-0.15.0rc2.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,619 +0,0 @@
|
|
|
1
|
-
"""Inertia page-props metadata extraction and export."""
|
|
2
|
-
|
|
3
|
-
import re
|
|
4
|
-
from contextlib import suppress
|
|
5
|
-
from dataclasses import dataclass, field
|
|
6
|
-
from pathlib import Path
|
|
7
|
-
from typing import TYPE_CHECKING, Any, cast
|
|
8
|
-
|
|
9
|
-
from litestar._openapi.datastructures import _get_normalized_schema_key # pyright: ignore[reportPrivateUsage]
|
|
10
|
-
from litestar.handlers import HTTPRouteHandler
|
|
11
|
-
from litestar.openapi.spec import Reference, Schema
|
|
12
|
-
from litestar.response.base import ASGIResponse
|
|
13
|
-
from litestar.routes import HTTPRoute
|
|
14
|
-
from litestar.types.builtin_types import NoneType
|
|
15
|
-
from litestar.typing import FieldDefinition
|
|
16
|
-
|
|
17
|
-
from litestar_vite.codegen._openapi import (
|
|
18
|
-
OpenAPISupport,
|
|
19
|
-
build_schema_name_map,
|
|
20
|
-
merge_generated_components_into_openapi,
|
|
21
|
-
openapi_components_schemas,
|
|
22
|
-
resolve_page_props_field_definition,
|
|
23
|
-
schema_name_from_ref,
|
|
24
|
-
)
|
|
25
|
-
from litestar_vite.codegen._ts import collect_ref_names, normalize_path, python_type_to_typescript, ts_type_from_openapi
|
|
26
|
-
|
|
27
|
-
if TYPE_CHECKING:
|
|
28
|
-
from litestar import Litestar
|
|
29
|
-
|
|
30
|
-
from litestar_vite.config import InertiaConfig, TypeGenConfig
|
|
31
|
-
|
|
32
|
-
# Compiled regex for splitting TypeScript type strings on union/intersection operators
|
|
33
|
-
_TYPE_OPERATOR_RE = re.compile(r"(\s*[|&]\s*)")
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
def str_list_factory() -> list[str]:
|
|
37
|
-
"""Return an empty ``list[str]`` (typed for pyright).
|
|
38
|
-
|
|
39
|
-
Returns:
|
|
40
|
-
An empty list.
|
|
41
|
-
"""
|
|
42
|
-
return []
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
def _pick_inertia_method(http_methods: "set[Any] | frozenset[Any] | None") -> str:
|
|
46
|
-
"""Pick a deterministic HTTP method for Inertia page props inference.
|
|
47
|
-
|
|
48
|
-
Inertia pages are typically loaded via GET requests, so we prefer GET.
|
|
49
|
-
For determinism, we sort remaining methods alphabetically.
|
|
50
|
-
|
|
51
|
-
Args:
|
|
52
|
-
http_methods: Set of HTTP methods from the route handler.
|
|
53
|
-
|
|
54
|
-
Returns:
|
|
55
|
-
The selected HTTP method string.
|
|
56
|
-
"""
|
|
57
|
-
if not http_methods:
|
|
58
|
-
return "GET"
|
|
59
|
-
# Prefer GET for Inertia page loads
|
|
60
|
-
if "GET" in http_methods:
|
|
61
|
-
return "GET"
|
|
62
|
-
# Fallback to alphabetically first method for determinism
|
|
63
|
-
return sorted(http_methods)[0]
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
def normalize_type_name(type_name: str, openapi_schemas: set[str]) -> str:
|
|
67
|
-
"""Strip module prefix from mangled type names.
|
|
68
|
-
|
|
69
|
-
Always converts 'app_lib_schema_NoProps' -> 'NoProps' because:
|
|
70
|
-
1. If 'NoProps' exists in OpenAPI, it will be imported correctly
|
|
71
|
-
2. If 'NoProps' doesn't exist, the error message is clearer for users
|
|
72
|
-
(they can add it to OpenAPI or configure type_import_paths)
|
|
73
|
-
|
|
74
|
-
The mangled name 'app_lib_schema_NoProps' will NEVER work - it doesn't
|
|
75
|
-
exist anywhere. The short name is always preferable.
|
|
76
|
-
|
|
77
|
-
Args:
|
|
78
|
-
type_name: The potentially mangled type name.
|
|
79
|
-
openapi_schemas: Set of available OpenAPI schema names.
|
|
80
|
-
|
|
81
|
-
Returns:
|
|
82
|
-
The normalized (unmangled) type name.
|
|
83
|
-
"""
|
|
84
|
-
if type_name in openapi_schemas:
|
|
85
|
-
return type_name
|
|
86
|
-
|
|
87
|
-
# Check if this looks like a mangled module path (contains underscores)
|
|
88
|
-
if "_" not in type_name:
|
|
89
|
-
return type_name
|
|
90
|
-
|
|
91
|
-
# Try progressively shorter suffixes to find the class name
|
|
92
|
-
parts = type_name.split("_")
|
|
93
|
-
for i in range(len(parts)):
|
|
94
|
-
short_name = "_".join(parts[i:])
|
|
95
|
-
# Prefer OpenAPI match, but if we get to the last part, use it anyway
|
|
96
|
-
if short_name in openapi_schemas:
|
|
97
|
-
return short_name
|
|
98
|
-
|
|
99
|
-
# Use the last part as the class name (e.g., 'NoProps' from 'app_lib_schema_NoProps')
|
|
100
|
-
# This is always better than the mangled name for error messages
|
|
101
|
-
return parts[-1] if parts else type_name
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
def normalize_type_string(type_string: str, openapi_schemas: set[str]) -> str:
|
|
105
|
-
"""Normalize all type names within a TypeScript type string.
|
|
106
|
-
|
|
107
|
-
Handles union types like 'any | app_lib_schema_NoProps' by parsing the
|
|
108
|
-
string and normalizing each type name individually.
|
|
109
|
-
|
|
110
|
-
Args:
|
|
111
|
-
type_string: A TypeScript type string (may contain unions, intersections).
|
|
112
|
-
openapi_schemas: Set of available OpenAPI schema names.
|
|
113
|
-
|
|
114
|
-
Returns:
|
|
115
|
-
The type string with all type names normalized.
|
|
116
|
-
"""
|
|
117
|
-
# Primitives and special types that should not be normalized
|
|
118
|
-
skip_types = {"any", "unknown", "null", "undefined", "void", "never", "string", "number", "boolean", "object"}
|
|
119
|
-
|
|
120
|
-
# Split on | and & while preserving whitespace
|
|
121
|
-
tokens = _TYPE_OPERATOR_RE.split(type_string)
|
|
122
|
-
result_parts: list[str] = []
|
|
123
|
-
|
|
124
|
-
for token in tokens:
|
|
125
|
-
stripped = token.strip()
|
|
126
|
-
# Keep operators and whitespace as-is
|
|
127
|
-
if stripped in {"|", "&", ""} or stripped in skip_types or stripped == "{}":
|
|
128
|
-
result_parts.append(token)
|
|
129
|
-
# Normalize type names
|
|
130
|
-
else:
|
|
131
|
-
normalized = normalize_type_name(stripped, openapi_schemas)
|
|
132
|
-
# Preserve original whitespace around the type
|
|
133
|
-
prefix = token[: len(token) - len(token.lstrip())]
|
|
134
|
-
suffix = token[len(token.rstrip()) :]
|
|
135
|
-
result_parts.append(prefix + normalized + suffix)
|
|
136
|
-
|
|
137
|
-
return "".join(result_parts)
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
@dataclass
|
|
141
|
-
class InertiaPageMetadata:
|
|
142
|
-
"""Metadata for a single Inertia page component."""
|
|
143
|
-
|
|
144
|
-
component: str
|
|
145
|
-
route_path: str
|
|
146
|
-
props_type: str | None = None
|
|
147
|
-
schema_ref: str | None = None
|
|
148
|
-
handler_name: str | None = None
|
|
149
|
-
ts_type: str | None = None
|
|
150
|
-
custom_types: list[str] = field(default_factory=str_list_factory)
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
def get_return_type_name(handler: HTTPRouteHandler) -> "str | None":
|
|
154
|
-
field_definition = handler.parsed_fn_signature.return_type
|
|
155
|
-
excluded_types: tuple[type[Any], ...] = (NoneType, ASGIResponse)
|
|
156
|
-
if field_definition.is_subclass_of(excluded_types):
|
|
157
|
-
return None
|
|
158
|
-
|
|
159
|
-
fn = handler.fn
|
|
160
|
-
with suppress(AttributeError):
|
|
161
|
-
return_annotation = fn.__annotations__.get("return")
|
|
162
|
-
if isinstance(return_annotation, str) and return_annotation:
|
|
163
|
-
return return_annotation
|
|
164
|
-
|
|
165
|
-
raw = field_definition.raw
|
|
166
|
-
if isinstance(raw, str):
|
|
167
|
-
return raw
|
|
168
|
-
if isinstance(raw, type):
|
|
169
|
-
return raw.__name__
|
|
170
|
-
origin: Any = None
|
|
171
|
-
with suppress(AttributeError):
|
|
172
|
-
origin = field_definition.origin
|
|
173
|
-
if isinstance(origin, type):
|
|
174
|
-
return origin.__name__
|
|
175
|
-
return str(raw)
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
def get_openapi_schema_ref(
|
|
179
|
-
handler: HTTPRouteHandler, openapi_schema: dict[str, Any] | None, route_path: str, method: str = "GET"
|
|
180
|
-
) -> "str | None":
|
|
181
|
-
if not openapi_schema:
|
|
182
|
-
return None
|
|
183
|
-
|
|
184
|
-
paths = openapi_schema.get("paths", {})
|
|
185
|
-
path_item = paths.get(route_path, {})
|
|
186
|
-
operation = path_item.get(method.lower(), {})
|
|
187
|
-
|
|
188
|
-
responses = operation.get("responses", {})
|
|
189
|
-
success_response = responses.get("200", responses.get("2XX", {}))
|
|
190
|
-
content = success_response.get("content", {})
|
|
191
|
-
|
|
192
|
-
json_content = content.get("application/json", {})
|
|
193
|
-
schema = json_content.get("schema", {})
|
|
194
|
-
|
|
195
|
-
ref = schema.get("$ref")
|
|
196
|
-
return cast("str | None", ref) if ref else None
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
def extract_inertia_component(handler: HTTPRouteHandler) -> str | None:
|
|
200
|
-
opt = handler.opt or {}
|
|
201
|
-
component = opt.get("component") or opt.get("page")
|
|
202
|
-
return component if isinstance(component, str) and component else None
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
def infer_inertia_props_type(
|
|
206
|
-
component: str,
|
|
207
|
-
handler: HTTPRouteHandler,
|
|
208
|
-
schema_creator: Any,
|
|
209
|
-
page_schema_keys: dict[str, tuple[str, ...]],
|
|
210
|
-
page_schema_dicts: dict[str, dict[str, Any]],
|
|
211
|
-
*,
|
|
212
|
-
fallback_type: str,
|
|
213
|
-
) -> str | None:
|
|
214
|
-
if schema_creator is not None:
|
|
215
|
-
field_def, schema_result = resolve_page_props_field_definition(handler, schema_creator)
|
|
216
|
-
if field_def is not None and isinstance(schema_result, Reference):
|
|
217
|
-
page_schema_keys[component] = _get_normalized_schema_key(field_def)
|
|
218
|
-
return None
|
|
219
|
-
if isinstance(schema_result, Schema):
|
|
220
|
-
schema_dict = schema_result.to_schema()
|
|
221
|
-
page_schema_dicts[component] = schema_dict
|
|
222
|
-
return ts_type_from_openapi(schema_dict)
|
|
223
|
-
return None
|
|
224
|
-
|
|
225
|
-
raw_type = get_return_type_name(handler)
|
|
226
|
-
if not raw_type:
|
|
227
|
-
return None
|
|
228
|
-
props_type, _ = python_type_to_typescript(raw_type, fallback=fallback_type)
|
|
229
|
-
return props_type
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
def finalize_inertia_pages(
|
|
233
|
-
pages: list[InertiaPageMetadata],
|
|
234
|
-
*,
|
|
235
|
-
openapi_support: OpenAPISupport,
|
|
236
|
-
page_schema_keys: dict[str, tuple[str, ...]],
|
|
237
|
-
page_schema_dicts: dict[str, dict[str, Any]],
|
|
238
|
-
) -> None:
|
|
239
|
-
context = openapi_support.context
|
|
240
|
-
if context is None:
|
|
241
|
-
return
|
|
242
|
-
|
|
243
|
-
generated_components = context.schema_registry.generate_components_schemas()
|
|
244
|
-
name_map = build_schema_name_map(context.schema_registry)
|
|
245
|
-
openapi_components = openapi_components_schemas(openapi_support.openapi_schema)
|
|
246
|
-
|
|
247
|
-
# Build set of available OpenAPI schema names for type normalization
|
|
248
|
-
openapi_schema_names: set[str] = set(openapi_components.keys())
|
|
249
|
-
openapi_schema_names.update(generated_components.keys())
|
|
250
|
-
|
|
251
|
-
if openapi_support.openapi_schema is not None:
|
|
252
|
-
merge_generated_components_into_openapi(openapi_support.openapi_schema, generated_components)
|
|
253
|
-
|
|
254
|
-
for page in pages:
|
|
255
|
-
schema_key = page_schema_keys.get(page.component)
|
|
256
|
-
|
|
257
|
-
schema_name: str | None = None
|
|
258
|
-
if page.schema_ref:
|
|
259
|
-
schema_name = schema_name_from_ref(page.schema_ref)
|
|
260
|
-
elif schema_key:
|
|
261
|
-
schema_name = name_map.get(schema_key)
|
|
262
|
-
|
|
263
|
-
if schema_name:
|
|
264
|
-
# Normalize mangled type names (e.g., 'app_lib_schema_NoProps' -> 'NoProps')
|
|
265
|
-
normalized_name = normalize_type_name(schema_name, openapi_schema_names)
|
|
266
|
-
page.ts_type = normalized_name
|
|
267
|
-
page.props_type = normalized_name
|
|
268
|
-
elif page.props_type:
|
|
269
|
-
# Normalize type names in union/intersection type strings
|
|
270
|
-
# (e.g., 'any | app_lib_schema_NoProps' -> 'any | NoProps')
|
|
271
|
-
page.props_type = normalize_type_string(page.props_type, openapi_schema_names)
|
|
272
|
-
|
|
273
|
-
custom_types: set[str] = set()
|
|
274
|
-
if page.ts_type:
|
|
275
|
-
custom_types.add(page.ts_type)
|
|
276
|
-
|
|
277
|
-
if page.schema_ref:
|
|
278
|
-
openapi_schema_dict = openapi_components.get(page.ts_type or "")
|
|
279
|
-
if isinstance(openapi_schema_dict, dict):
|
|
280
|
-
custom_types.update(collect_ref_names(openapi_schema_dict))
|
|
281
|
-
else:
|
|
282
|
-
page_schema_dict = page_schema_dicts.get(page.component)
|
|
283
|
-
if isinstance(page_schema_dict, dict):
|
|
284
|
-
custom_types.update(collect_ref_names(page_schema_dict))
|
|
285
|
-
elif schema_key:
|
|
286
|
-
registered = context.schema_registry._schema_key_map.get( # pyright: ignore[reportPrivateUsage]
|
|
287
|
-
schema_key
|
|
288
|
-
)
|
|
289
|
-
if registered:
|
|
290
|
-
custom_types.update(collect_ref_names(registered.schema.to_schema()))
|
|
291
|
-
|
|
292
|
-
# Normalize all custom type names
|
|
293
|
-
page.custom_types = sorted(normalize_type_name(t, openapi_schema_names) for t in custom_types)
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
def extract_inertia_pages(
|
|
297
|
-
app: "Litestar",
|
|
298
|
-
*,
|
|
299
|
-
openapi_schema: dict[str, Any] | None = None,
|
|
300
|
-
fallback_type: "str" = "unknown",
|
|
301
|
-
openapi_support: OpenAPISupport | None = None,
|
|
302
|
-
) -> list[InertiaPageMetadata]:
|
|
303
|
-
"""Extract Inertia page metadata from an application.
|
|
304
|
-
|
|
305
|
-
When multiple handlers map to the same component, GET handlers are preferred
|
|
306
|
-
since Inertia pages are typically loaded via GET requests.
|
|
307
|
-
|
|
308
|
-
Args:
|
|
309
|
-
app: Litestar application instance.
|
|
310
|
-
openapi_schema: Optional OpenAPI schema dict.
|
|
311
|
-
fallback_type: TypeScript fallback type for unknown types.
|
|
312
|
-
openapi_support: Optional shared OpenAPISupport instance. If not provided,
|
|
313
|
-
a new one will be created. Sharing improves determinism and performance.
|
|
314
|
-
|
|
315
|
-
Returns:
|
|
316
|
-
List of InertiaPageMetadata for each discovered page.
|
|
317
|
-
"""
|
|
318
|
-
# Track seen components: component -> (metadata, is_get_handler)
|
|
319
|
-
# When multiple handlers map to the same component, prefer GET handlers
|
|
320
|
-
seen_components: dict[str, tuple[InertiaPageMetadata, bool]] = {}
|
|
321
|
-
|
|
322
|
-
if openapi_support is None:
|
|
323
|
-
openapi_support = OpenAPISupport.from_app(app, openapi_schema)
|
|
324
|
-
|
|
325
|
-
page_schema_keys: dict[str, tuple[str, ...]] = {}
|
|
326
|
-
page_schema_dicts: dict[str, dict[str, Any]] = {}
|
|
327
|
-
|
|
328
|
-
for http_route, route_handler in iter_route_handlers(app):
|
|
329
|
-
component = extract_inertia_component(route_handler)
|
|
330
|
-
if not component:
|
|
331
|
-
continue
|
|
332
|
-
|
|
333
|
-
normalized_path = normalize_path(str(http_route.path))
|
|
334
|
-
handler_name = route_handler.handler_name or route_handler.name
|
|
335
|
-
is_get_handler = "GET" in (route_handler.http_methods or set())
|
|
336
|
-
|
|
337
|
-
props_type = infer_inertia_props_type(
|
|
338
|
-
component,
|
|
339
|
-
route_handler,
|
|
340
|
-
openapi_support.schema_creator,
|
|
341
|
-
page_schema_keys,
|
|
342
|
-
page_schema_dicts,
|
|
343
|
-
fallback_type=fallback_type,
|
|
344
|
-
)
|
|
345
|
-
|
|
346
|
-
method = _pick_inertia_method(route_handler.http_methods)
|
|
347
|
-
schema_ref = get_openapi_schema_ref(route_handler, openapi_schema, normalized_path, method=str(method))
|
|
348
|
-
|
|
349
|
-
page_metadata = InertiaPageMetadata(
|
|
350
|
-
component=component,
|
|
351
|
-
route_path=normalized_path,
|
|
352
|
-
props_type=props_type,
|
|
353
|
-
schema_ref=schema_ref,
|
|
354
|
-
handler_name=handler_name,
|
|
355
|
-
)
|
|
356
|
-
|
|
357
|
-
# Prefer GET handlers when multiple handlers map to the same component
|
|
358
|
-
existing = seen_components.get(component)
|
|
359
|
-
if existing is None:
|
|
360
|
-
seen_components[component] = (page_metadata, is_get_handler)
|
|
361
|
-
elif is_get_handler and not existing[1]:
|
|
362
|
-
# New handler is GET, existing is not - prefer the GET handler
|
|
363
|
-
seen_components[component] = (page_metadata, is_get_handler)
|
|
364
|
-
# Otherwise keep existing (it's either GET or we prefer first-seen for determinism)
|
|
365
|
-
|
|
366
|
-
pages = [entry[0] for entry in seen_components.values()]
|
|
367
|
-
|
|
368
|
-
if openapi_support.enabled:
|
|
369
|
-
finalize_inertia_pages(
|
|
370
|
-
pages,
|
|
371
|
-
openapi_support=openapi_support,
|
|
372
|
-
page_schema_keys=page_schema_keys,
|
|
373
|
-
page_schema_dicts=page_schema_dicts,
|
|
374
|
-
)
|
|
375
|
-
|
|
376
|
-
return pages
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
def iter_route_handlers(app: "Litestar") -> "list[tuple[HTTPRoute, HTTPRouteHandler]]":
|
|
380
|
-
"""Iterate over HTTP route handlers in an app.
|
|
381
|
-
|
|
382
|
-
Returns a deterministically sorted list to ensure consistent output
|
|
383
|
-
across multiple runs. Handlers are sorted by (route_path, handler_name).
|
|
384
|
-
|
|
385
|
-
Returns:
|
|
386
|
-
A list of (http_route, route_handler) tuples, sorted for determinism.
|
|
387
|
-
"""
|
|
388
|
-
handlers: list[tuple[HTTPRoute, HTTPRouteHandler]] = []
|
|
389
|
-
for route in app.routes:
|
|
390
|
-
if isinstance(route, HTTPRoute):
|
|
391
|
-
handlers.extend((route, route_handler) for route_handler in route.route_handlers)
|
|
392
|
-
# Sort by route path, then handler name for deterministic ordering
|
|
393
|
-
return sorted(handlers, key=lambda x: (str(x[0].path), x[1].handler_name or x[1].name or ""))
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
def get_fallback_ts_type(types_config: "TypeGenConfig | None") -> str:
|
|
397
|
-
fallback_type = types_config.fallback_type if types_config is not None else "unknown"
|
|
398
|
-
return "any" if fallback_type == "any" else "unknown"
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
def ts_type_from_value(value: Any, *, fallback_ts_type: str) -> str:
|
|
402
|
-
ts_type = fallback_ts_type
|
|
403
|
-
if value is None:
|
|
404
|
-
ts_type = "null"
|
|
405
|
-
elif isinstance(value, bool):
|
|
406
|
-
ts_type = "boolean"
|
|
407
|
-
elif isinstance(value, str):
|
|
408
|
-
ts_type = "string"
|
|
409
|
-
elif isinstance(value, (int, float)):
|
|
410
|
-
ts_type = "number"
|
|
411
|
-
elif isinstance(value, (bytes, bytearray, Path)):
|
|
412
|
-
ts_type = "string"
|
|
413
|
-
elif isinstance(value, (list, tuple, set, frozenset)):
|
|
414
|
-
ts_type = f"{fallback_ts_type}[]"
|
|
415
|
-
elif isinstance(value, dict):
|
|
416
|
-
ts_type = f"Record<string, {fallback_ts_type}>"
|
|
417
|
-
return ts_type
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
def should_register_value_schema(value: Any) -> bool:
|
|
421
|
-
if value is None:
|
|
422
|
-
return False
|
|
423
|
-
return not isinstance(value, (bool, str, int, float, bytes, bytearray, Path, list, tuple, set, frozenset, dict))
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
def process_session_props(
|
|
427
|
-
session_props: "set[str] | dict[str, type]",
|
|
428
|
-
shared_props: dict[str, dict[str, Any]],
|
|
429
|
-
shared_schema_keys: dict[str, tuple[str, ...]],
|
|
430
|
-
openapi_support: OpenAPISupport,
|
|
431
|
-
fallback_ts_type: str,
|
|
432
|
-
) -> None:
|
|
433
|
-
"""Process session props and add them to shared_props.
|
|
434
|
-
|
|
435
|
-
Handles both set[str] (legacy) and dict[str, type] (new typed) formats.
|
|
436
|
-
"""
|
|
437
|
-
if isinstance(session_props, dict):
|
|
438
|
-
# New behavior: dict maps prop names to Python types
|
|
439
|
-
for key, prop_type_class in session_props.items():
|
|
440
|
-
if not key:
|
|
441
|
-
continue
|
|
442
|
-
# Register the type with OpenAPI if possible
|
|
443
|
-
if openapi_support.enabled and openapi_support.schema_creator:
|
|
444
|
-
try:
|
|
445
|
-
field_def = FieldDefinition.from_annotation(prop_type_class)
|
|
446
|
-
schema_result = openapi_support.schema_creator.for_field_definition(field_def)
|
|
447
|
-
if isinstance(schema_result, Reference):
|
|
448
|
-
shared_schema_keys[key] = _get_normalized_schema_key(field_def)
|
|
449
|
-
type_name = prop_type_class.__name__ if hasattr(prop_type_class, "__name__") else fallback_ts_type
|
|
450
|
-
shared_props.setdefault(key, {"type": type_name, "optional": True})
|
|
451
|
-
except (AttributeError, TypeError, ValueError): # pragma: no cover - defensive
|
|
452
|
-
shared_props.setdefault(key, {"type": fallback_ts_type, "optional": True})
|
|
453
|
-
else:
|
|
454
|
-
type_name = prop_type_class.__name__ if hasattr(prop_type_class, "__name__") else fallback_ts_type
|
|
455
|
-
shared_props.setdefault(key, {"type": type_name, "optional": True})
|
|
456
|
-
else:
|
|
457
|
-
# Legacy behavior: set of prop names (types are unknown)
|
|
458
|
-
for key in session_props:
|
|
459
|
-
if not key:
|
|
460
|
-
continue
|
|
461
|
-
shared_props.setdefault(key, {"type": fallback_ts_type, "optional": True})
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
def build_inertia_shared_props(
|
|
465
|
-
app: "Litestar",
|
|
466
|
-
*,
|
|
467
|
-
openapi_schema: dict[str, Any] | None,
|
|
468
|
-
include_default_auth: bool,
|
|
469
|
-
include_default_flash: bool,
|
|
470
|
-
inertia_config: "InertiaConfig | None",
|
|
471
|
-
types_config: "TypeGenConfig | None",
|
|
472
|
-
openapi_support: OpenAPISupport | None = None,
|
|
473
|
-
) -> dict[str, dict[str, Any]]:
|
|
474
|
-
"""Build shared props metadata (built-ins + configured props).
|
|
475
|
-
|
|
476
|
-
Args:
|
|
477
|
-
app: Litestar application instance.
|
|
478
|
-
openapi_schema: Optional OpenAPI schema dict.
|
|
479
|
-
include_default_auth: Include default auth shared prop.
|
|
480
|
-
include_default_flash: Include default flash shared prop.
|
|
481
|
-
inertia_config: Optional Inertia configuration.
|
|
482
|
-
types_config: Optional type generation configuration.
|
|
483
|
-
openapi_support: Optional shared OpenAPISupport instance. If not provided,
|
|
484
|
-
a new one will be created. Sharing improves determinism and performance.
|
|
485
|
-
|
|
486
|
-
Returns:
|
|
487
|
-
Mapping of shared prop name to metadata payload.
|
|
488
|
-
"""
|
|
489
|
-
fallback_ts_type = get_fallback_ts_type(types_config)
|
|
490
|
-
|
|
491
|
-
shared_props: dict[str, dict[str, Any]] = {
|
|
492
|
-
"errors": {"type": "Record<string, string[]>", "optional": True},
|
|
493
|
-
"csrf_token": {"type": "string", "optional": True},
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
if include_default_auth or include_default_flash:
|
|
497
|
-
shared_props["auth"] = {"type": "AuthData", "optional": True}
|
|
498
|
-
shared_props["flash"] = {"type": "FlashMessages", "optional": True}
|
|
499
|
-
|
|
500
|
-
if inertia_config is None:
|
|
501
|
-
return shared_props
|
|
502
|
-
|
|
503
|
-
if openapi_support is None:
|
|
504
|
-
openapi_support = OpenAPISupport.from_app(app, openapi_schema)
|
|
505
|
-
shared_schema_keys: dict[str, tuple[str, ...]] = {}
|
|
506
|
-
|
|
507
|
-
for key, value in inertia_config.extra_static_page_props.items():
|
|
508
|
-
if not key:
|
|
509
|
-
continue
|
|
510
|
-
|
|
511
|
-
shared_props[key] = {"type": ts_type_from_value(value, fallback_ts_type=fallback_ts_type), "optional": True}
|
|
512
|
-
|
|
513
|
-
if openapi_support.enabled and isinstance(openapi_schema, dict) and should_register_value_schema(value):
|
|
514
|
-
try:
|
|
515
|
-
field_def = FieldDefinition.from_annotation(value.__class__)
|
|
516
|
-
schema_result = openapi_support.schema_creator.for_field_definition(field_def) # type: ignore[union-attr]
|
|
517
|
-
if isinstance(schema_result, Reference):
|
|
518
|
-
shared_schema_keys[key] = _get_normalized_schema_key(field_def)
|
|
519
|
-
except (AttributeError, TypeError, ValueError): # pragma: no cover - defensive
|
|
520
|
-
pass
|
|
521
|
-
|
|
522
|
-
# Handle session props - can be set[str] or dict[str, type]
|
|
523
|
-
process_session_props(
|
|
524
|
-
inertia_config.extra_session_page_props, shared_props, shared_schema_keys, openapi_support, fallback_ts_type
|
|
525
|
-
)
|
|
526
|
-
|
|
527
|
-
if not (
|
|
528
|
-
openapi_support.context
|
|
529
|
-
and openapi_support.schema_creator
|
|
530
|
-
and isinstance(openapi_schema, dict)
|
|
531
|
-
and shared_schema_keys
|
|
532
|
-
):
|
|
533
|
-
return shared_props
|
|
534
|
-
|
|
535
|
-
generated_components = openapi_support.context.schema_registry.generate_components_schemas()
|
|
536
|
-
name_map = build_schema_name_map(openapi_support.context.schema_registry)
|
|
537
|
-
merge_generated_components_into_openapi(openapi_schema, generated_components)
|
|
538
|
-
|
|
539
|
-
for prop_name, schema_key in shared_schema_keys.items():
|
|
540
|
-
type_name = name_map.get(schema_key)
|
|
541
|
-
if type_name:
|
|
542
|
-
shared_props[prop_name]["type"] = type_name
|
|
543
|
-
|
|
544
|
-
return shared_props
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
def generate_inertia_pages_json(
|
|
548
|
-
app: "Litestar",
|
|
549
|
-
*,
|
|
550
|
-
openapi_schema: dict[str, Any] | None = None,
|
|
551
|
-
include_default_auth: bool = True,
|
|
552
|
-
include_default_flash: bool = True,
|
|
553
|
-
inertia_config: "InertiaConfig | None" = None,
|
|
554
|
-
types_config: "TypeGenConfig | None" = None,
|
|
555
|
-
) -> dict[str, Any]:
|
|
556
|
-
"""Generate Inertia pages metadata JSON.
|
|
557
|
-
|
|
558
|
-
The output is deterministic: all dict keys are sorted alphabetically
|
|
559
|
-
to produce byte-identical output for the same input data.
|
|
560
|
-
|
|
561
|
-
A single OpenAPISupport instance is shared across both page extraction and
|
|
562
|
-
shared props building to ensure consistent schema registration and naming.
|
|
563
|
-
This eliminates non-determinism from split schema registries.
|
|
564
|
-
|
|
565
|
-
Returns:
|
|
566
|
-
An Inertia pages metadata payload as a dictionary with sorted keys.
|
|
567
|
-
"""
|
|
568
|
-
# Create a single OpenAPISupport instance to share across the entire pipeline.
|
|
569
|
-
# This ensures consistent schema registration and prevents "split-brain" issues
|
|
570
|
-
# where separate registries could produce different schema names.
|
|
571
|
-
openapi_support = OpenAPISupport.from_app(app, openapi_schema)
|
|
572
|
-
|
|
573
|
-
pages_metadata = extract_inertia_pages(
|
|
574
|
-
app,
|
|
575
|
-
openapi_schema=openapi_schema,
|
|
576
|
-
fallback_type=types_config.fallback_type if types_config is not None else "unknown",
|
|
577
|
-
openapi_support=openapi_support,
|
|
578
|
-
)
|
|
579
|
-
|
|
580
|
-
pages_dict: dict[str, dict[str, Any]] = {}
|
|
581
|
-
for page in pages_metadata:
|
|
582
|
-
page_data: dict[str, Any] = {"route": page.route_path}
|
|
583
|
-
if page.props_type:
|
|
584
|
-
page_data["propsType"] = page.props_type
|
|
585
|
-
if page.ts_type:
|
|
586
|
-
page_data["tsType"] = page.ts_type
|
|
587
|
-
if page.custom_types:
|
|
588
|
-
page_data["customTypes"] = page.custom_types
|
|
589
|
-
if page.schema_ref:
|
|
590
|
-
page_data["schemaRef"] = page.schema_ref
|
|
591
|
-
if page.handler_name:
|
|
592
|
-
page_data["handler"] = page.handler_name
|
|
593
|
-
pages_dict[page.component] = page_data
|
|
594
|
-
|
|
595
|
-
shared_props = build_inertia_shared_props(
|
|
596
|
-
app,
|
|
597
|
-
openapi_schema=openapi_schema,
|
|
598
|
-
include_default_auth=include_default_auth,
|
|
599
|
-
include_default_flash=include_default_flash,
|
|
600
|
-
inertia_config=inertia_config,
|
|
601
|
-
types_config=types_config,
|
|
602
|
-
openapi_support=openapi_support,
|
|
603
|
-
)
|
|
604
|
-
|
|
605
|
-
# Sort all dict keys for deterministic output
|
|
606
|
-
# Pages sorted by component name, shared props sorted by prop name
|
|
607
|
-
sorted_pages = dict(sorted(pages_dict.items()))
|
|
608
|
-
sorted_shared_props = dict(sorted(shared_props.items()))
|
|
609
|
-
|
|
610
|
-
root: dict[str, Any] = {
|
|
611
|
-
"fallbackType": types_config.fallback_type if types_config is not None else None,
|
|
612
|
-
"pages": sorted_pages,
|
|
613
|
-
"sharedProps": sorted_shared_props,
|
|
614
|
-
"typeGenConfig": {"includeDefaultAuth": include_default_auth, "includeDefaultFlash": include_default_flash},
|
|
615
|
-
"typeImportPaths": types_config.type_import_paths if types_config is not None else None,
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
# Remove None values for cleaner output
|
|
619
|
-
return {k: v for k, v in root.items() if v is not None}
|