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
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Internal code generation implementation.
|
|
2
|
+
|
|
3
|
+
This package contains the implementation behind the public functions exposed via
|
|
4
|
+
``litestar_vite.codegen``.
|
|
5
|
+
|
|
6
|
+
We keep Litestar private OpenAPI integration and TypeScript conversion details
|
|
7
|
+
isolated here to make the public module easier to navigate and maintain.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from litestar_vite._codegen.inertia import InertiaPageMetadata, extract_inertia_pages, generate_inertia_pages_json
|
|
11
|
+
from litestar_vite._codegen.routes import (
|
|
12
|
+
RouteMetadata,
|
|
13
|
+
extract_route_metadata,
|
|
14
|
+
generate_routes_json,
|
|
15
|
+
generate_routes_ts,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
__all__ = (
|
|
19
|
+
"InertiaPageMetadata",
|
|
20
|
+
"RouteMetadata",
|
|
21
|
+
"extract_inertia_pages",
|
|
22
|
+
"extract_route_metadata",
|
|
23
|
+
"generate_inertia_pages_json",
|
|
24
|
+
"generate_routes_json",
|
|
25
|
+
"generate_routes_ts",
|
|
26
|
+
)
|
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
"""Inertia page-props metadata extraction and export."""
|
|
2
|
+
|
|
3
|
+
import datetime
|
|
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
|
+
|
|
33
|
+
def _str_list_factory() -> list[str]:
|
|
34
|
+
"""Return an empty ``list[str]`` (typed for pyright).
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
An empty list.
|
|
38
|
+
"""
|
|
39
|
+
return []
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class InertiaPageMetadata:
|
|
44
|
+
"""Metadata for a single Inertia page component."""
|
|
45
|
+
|
|
46
|
+
component: str
|
|
47
|
+
route_path: str
|
|
48
|
+
props_type: str | None = None
|
|
49
|
+
schema_ref: str | None = None
|
|
50
|
+
handler_name: str | None = None
|
|
51
|
+
ts_type: str | None = None
|
|
52
|
+
custom_types: list[str] = field(default_factory=_str_list_factory)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _get_return_type_name(handler: HTTPRouteHandler) -> "str | None":
|
|
56
|
+
field_definition = handler.parsed_fn_signature.return_type
|
|
57
|
+
excluded_types: tuple[type[Any], ...] = (NoneType, ASGIResponse)
|
|
58
|
+
if field_definition.is_subclass_of(excluded_types):
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
fn = handler.fn
|
|
62
|
+
with suppress(AttributeError):
|
|
63
|
+
return_annotation = fn.__annotations__.get("return")
|
|
64
|
+
if isinstance(return_annotation, str) and return_annotation:
|
|
65
|
+
return return_annotation
|
|
66
|
+
|
|
67
|
+
raw = field_definition.raw
|
|
68
|
+
if isinstance(raw, str):
|
|
69
|
+
return raw
|
|
70
|
+
if isinstance(raw, type):
|
|
71
|
+
return raw.__name__
|
|
72
|
+
origin: Any = None
|
|
73
|
+
with suppress(AttributeError):
|
|
74
|
+
origin = field_definition.origin
|
|
75
|
+
if isinstance(origin, type):
|
|
76
|
+
return origin.__name__
|
|
77
|
+
return str(raw)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _get_openapi_schema_ref(
|
|
81
|
+
handler: HTTPRouteHandler, openapi_schema: dict[str, Any] | None, route_path: str, method: str = "GET"
|
|
82
|
+
) -> "str | None":
|
|
83
|
+
if not openapi_schema:
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
paths = openapi_schema.get("paths", {})
|
|
87
|
+
path_item = paths.get(route_path, {})
|
|
88
|
+
operation = path_item.get(method.lower(), {})
|
|
89
|
+
|
|
90
|
+
responses = operation.get("responses", {})
|
|
91
|
+
success_response = responses.get("200", responses.get("2XX", {}))
|
|
92
|
+
content = success_response.get("content", {})
|
|
93
|
+
|
|
94
|
+
json_content = content.get("application/json", {})
|
|
95
|
+
schema = json_content.get("schema", {})
|
|
96
|
+
|
|
97
|
+
ref = schema.get("$ref")
|
|
98
|
+
return cast("str | None", ref) if ref else None
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _extract_inertia_component(handler: HTTPRouteHandler) -> str | None:
|
|
102
|
+
opt = handler.opt or {}
|
|
103
|
+
component = opt.get("component") or opt.get("page")
|
|
104
|
+
return component if isinstance(component, str) and component else None
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _infer_inertia_props_type(
|
|
108
|
+
component: str,
|
|
109
|
+
handler: HTTPRouteHandler,
|
|
110
|
+
schema_creator: Any,
|
|
111
|
+
page_schema_keys: dict[str, tuple[str, ...]],
|
|
112
|
+
page_schema_dicts: dict[str, dict[str, Any]],
|
|
113
|
+
*,
|
|
114
|
+
fallback_type: str,
|
|
115
|
+
) -> str | None:
|
|
116
|
+
if schema_creator is not None:
|
|
117
|
+
field_def, schema_result = resolve_page_props_field_definition(handler, schema_creator)
|
|
118
|
+
if field_def is not None and isinstance(schema_result, Reference):
|
|
119
|
+
page_schema_keys[component] = _get_normalized_schema_key(field_def)
|
|
120
|
+
return None
|
|
121
|
+
if isinstance(schema_result, Schema):
|
|
122
|
+
schema_dict = schema_result.to_schema()
|
|
123
|
+
page_schema_dicts[component] = schema_dict
|
|
124
|
+
return ts_type_from_openapi(schema_dict)
|
|
125
|
+
return None
|
|
126
|
+
|
|
127
|
+
raw_type = _get_return_type_name(handler)
|
|
128
|
+
if not raw_type:
|
|
129
|
+
return None
|
|
130
|
+
props_type, _ = python_type_to_typescript(raw_type, fallback=fallback_type)
|
|
131
|
+
return props_type
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _finalize_inertia_pages(
|
|
135
|
+
pages: list[InertiaPageMetadata],
|
|
136
|
+
*,
|
|
137
|
+
openapi_support: OpenAPISupport,
|
|
138
|
+
page_schema_keys: dict[str, tuple[str, ...]],
|
|
139
|
+
page_schema_dicts: dict[str, dict[str, Any]],
|
|
140
|
+
) -> None:
|
|
141
|
+
context = openapi_support.context
|
|
142
|
+
if context is None:
|
|
143
|
+
return
|
|
144
|
+
|
|
145
|
+
generated_components = context.schema_registry.generate_components_schemas()
|
|
146
|
+
name_map = build_schema_name_map(context.schema_registry)
|
|
147
|
+
openapi_components = openapi_components_schemas(openapi_support.openapi_schema)
|
|
148
|
+
|
|
149
|
+
if openapi_support.openapi_schema is not None:
|
|
150
|
+
merge_generated_components_into_openapi(openapi_support.openapi_schema, generated_components)
|
|
151
|
+
|
|
152
|
+
for page in pages:
|
|
153
|
+
schema_key = page_schema_keys.get(page.component)
|
|
154
|
+
|
|
155
|
+
schema_name: str | None = None
|
|
156
|
+
if page.schema_ref:
|
|
157
|
+
schema_name = schema_name_from_ref(page.schema_ref)
|
|
158
|
+
elif schema_key:
|
|
159
|
+
schema_name = name_map.get(schema_key)
|
|
160
|
+
|
|
161
|
+
if schema_name:
|
|
162
|
+
page.ts_type = schema_name
|
|
163
|
+
page.props_type = schema_name
|
|
164
|
+
|
|
165
|
+
custom_types: set[str] = set()
|
|
166
|
+
if page.ts_type:
|
|
167
|
+
custom_types.add(page.ts_type)
|
|
168
|
+
|
|
169
|
+
if page.schema_ref:
|
|
170
|
+
openapi_schema_dict = openapi_components.get(page.ts_type or "")
|
|
171
|
+
if isinstance(openapi_schema_dict, dict):
|
|
172
|
+
custom_types.update(collect_ref_names(openapi_schema_dict))
|
|
173
|
+
else:
|
|
174
|
+
page_schema_dict = page_schema_dicts.get(page.component)
|
|
175
|
+
if isinstance(page_schema_dict, dict):
|
|
176
|
+
custom_types.update(collect_ref_names(page_schema_dict))
|
|
177
|
+
elif schema_key:
|
|
178
|
+
registered = context.schema_registry._schema_key_map.get( # pyright: ignore[reportPrivateUsage]
|
|
179
|
+
schema_key
|
|
180
|
+
)
|
|
181
|
+
if registered:
|
|
182
|
+
custom_types.update(collect_ref_names(registered.schema.to_schema()))
|
|
183
|
+
|
|
184
|
+
page.custom_types = sorted(custom_types)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def extract_inertia_pages(
|
|
188
|
+
app: "Litestar", *, openapi_schema: dict[str, Any] | None = None, fallback_type: "str" = "unknown"
|
|
189
|
+
) -> list[InertiaPageMetadata]:
|
|
190
|
+
pages: list[InertiaPageMetadata] = []
|
|
191
|
+
|
|
192
|
+
openapi_support = OpenAPISupport.from_app(app, openapi_schema)
|
|
193
|
+
|
|
194
|
+
page_schema_keys: dict[str, tuple[str, ...]] = {}
|
|
195
|
+
page_schema_dicts: dict[str, dict[str, Any]] = {}
|
|
196
|
+
|
|
197
|
+
for http_route, route_handler in _iter_route_handlers(app):
|
|
198
|
+
component = _extract_inertia_component(route_handler)
|
|
199
|
+
if not component:
|
|
200
|
+
continue
|
|
201
|
+
|
|
202
|
+
normalized_path = normalize_path(str(http_route.path))
|
|
203
|
+
handler_name = route_handler.handler_name or route_handler.name
|
|
204
|
+
|
|
205
|
+
props_type = _infer_inertia_props_type(
|
|
206
|
+
component,
|
|
207
|
+
route_handler,
|
|
208
|
+
openapi_support.schema_creator,
|
|
209
|
+
page_schema_keys,
|
|
210
|
+
page_schema_dicts,
|
|
211
|
+
fallback_type=fallback_type,
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
method = next(iter(route_handler.http_methods), "GET") if route_handler.http_methods else "GET"
|
|
215
|
+
schema_ref = _get_openapi_schema_ref(route_handler, openapi_schema, normalized_path, method=str(method))
|
|
216
|
+
|
|
217
|
+
pages.append(
|
|
218
|
+
InertiaPageMetadata(
|
|
219
|
+
component=component,
|
|
220
|
+
route_path=normalized_path,
|
|
221
|
+
props_type=props_type,
|
|
222
|
+
schema_ref=schema_ref,
|
|
223
|
+
handler_name=handler_name,
|
|
224
|
+
)
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
if openapi_support.enabled:
|
|
228
|
+
_finalize_inertia_pages(
|
|
229
|
+
pages,
|
|
230
|
+
openapi_support=openapi_support,
|
|
231
|
+
page_schema_keys=page_schema_keys,
|
|
232
|
+
page_schema_dicts=page_schema_dicts,
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
return pages
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _iter_route_handlers(app: "Litestar") -> "list[tuple[HTTPRoute, HTTPRouteHandler]]":
|
|
239
|
+
"""Iterate over HTTP route handlers in an app.
|
|
240
|
+
|
|
241
|
+
Returns:
|
|
242
|
+
A list of (http_route, route_handler) tuples.
|
|
243
|
+
"""
|
|
244
|
+
handlers: list[tuple[HTTPRoute, HTTPRouteHandler]] = []
|
|
245
|
+
for route in app.routes:
|
|
246
|
+
if isinstance(route, HTTPRoute):
|
|
247
|
+
handlers.extend((route, route_handler) for route_handler in route.route_handlers)
|
|
248
|
+
return handlers
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _fallback_ts_type(types_config: "TypeGenConfig | None") -> str:
|
|
252
|
+
fallback_type = types_config.fallback_type if types_config is not None else "unknown"
|
|
253
|
+
return "any" if fallback_type == "any" else "unknown"
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _ts_type_from_value(value: Any, *, fallback_ts_type: str) -> str:
|
|
257
|
+
ts_type = fallback_ts_type
|
|
258
|
+
if value is None:
|
|
259
|
+
ts_type = "null"
|
|
260
|
+
elif isinstance(value, bool):
|
|
261
|
+
ts_type = "boolean"
|
|
262
|
+
elif isinstance(value, str):
|
|
263
|
+
ts_type = "string"
|
|
264
|
+
elif isinstance(value, (int, float)):
|
|
265
|
+
ts_type = "number"
|
|
266
|
+
elif isinstance(value, (bytes, bytearray, Path)):
|
|
267
|
+
ts_type = "string"
|
|
268
|
+
elif isinstance(value, (list, tuple, set, frozenset)):
|
|
269
|
+
ts_type = f"{fallback_ts_type}[]"
|
|
270
|
+
elif isinstance(value, dict):
|
|
271
|
+
ts_type = f"Record<string, {fallback_ts_type}>"
|
|
272
|
+
return ts_type
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _should_register_value_schema(value: Any) -> bool:
|
|
276
|
+
if value is None:
|
|
277
|
+
return False
|
|
278
|
+
return not isinstance(value, (bool, str, int, float, bytes, bytearray, Path, list, tuple, set, frozenset, dict))
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def _build_inertia_shared_props(
|
|
282
|
+
app: "Litestar",
|
|
283
|
+
*,
|
|
284
|
+
openapi_schema: dict[str, Any] | None,
|
|
285
|
+
include_default_auth: bool,
|
|
286
|
+
include_default_flash: bool,
|
|
287
|
+
inertia_config: "InertiaConfig | None",
|
|
288
|
+
types_config: "TypeGenConfig | None",
|
|
289
|
+
) -> dict[str, dict[str, Any]]:
|
|
290
|
+
"""Build shared props metadata (built-ins + configured props).
|
|
291
|
+
|
|
292
|
+
Returns:
|
|
293
|
+
Mapping of shared prop name to metadata payload.
|
|
294
|
+
"""
|
|
295
|
+
fallback_ts_type = _fallback_ts_type(types_config)
|
|
296
|
+
|
|
297
|
+
shared_props: dict[str, dict[str, Any]] = {
|
|
298
|
+
"errors": {"type": "Record<string, string[]>", "optional": True},
|
|
299
|
+
"csrf_token": {"type": "string", "optional": True},
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if include_default_auth or include_default_flash:
|
|
303
|
+
shared_props["auth"] = {"type": "AuthData", "optional": True}
|
|
304
|
+
shared_props["flash"] = {"type": "FlashMessages", "optional": True}
|
|
305
|
+
|
|
306
|
+
if inertia_config is None:
|
|
307
|
+
return shared_props
|
|
308
|
+
|
|
309
|
+
openapi_support = OpenAPISupport.from_app(app, openapi_schema)
|
|
310
|
+
shared_schema_keys: dict[str, tuple[str, ...]] = {}
|
|
311
|
+
|
|
312
|
+
for key, value in inertia_config.extra_static_page_props.items():
|
|
313
|
+
if not key:
|
|
314
|
+
continue
|
|
315
|
+
|
|
316
|
+
shared_props[key] = {"type": _ts_type_from_value(value, fallback_ts_type=fallback_ts_type), "optional": True}
|
|
317
|
+
|
|
318
|
+
if openapi_support.enabled and isinstance(openapi_schema, dict) and _should_register_value_schema(value):
|
|
319
|
+
try:
|
|
320
|
+
field_def = FieldDefinition.from_annotation(value.__class__)
|
|
321
|
+
schema_result = openapi_support.schema_creator.for_field_definition(field_def) # type: ignore[union-attr]
|
|
322
|
+
if isinstance(schema_result, Reference):
|
|
323
|
+
shared_schema_keys[key] = _get_normalized_schema_key(field_def)
|
|
324
|
+
except (AttributeError, TypeError, ValueError): # pragma: no cover - defensive
|
|
325
|
+
pass
|
|
326
|
+
|
|
327
|
+
for key in inertia_config.extra_session_page_props:
|
|
328
|
+
if not key:
|
|
329
|
+
continue
|
|
330
|
+
shared_props.setdefault(key, {"type": fallback_ts_type, "optional": True})
|
|
331
|
+
|
|
332
|
+
if not (
|
|
333
|
+
openapi_support.context
|
|
334
|
+
and openapi_support.schema_creator
|
|
335
|
+
and isinstance(openapi_schema, dict)
|
|
336
|
+
and shared_schema_keys
|
|
337
|
+
):
|
|
338
|
+
return shared_props
|
|
339
|
+
|
|
340
|
+
generated_components = openapi_support.context.schema_registry.generate_components_schemas()
|
|
341
|
+
name_map = build_schema_name_map(openapi_support.context.schema_registry)
|
|
342
|
+
merge_generated_components_into_openapi(openapi_schema, generated_components)
|
|
343
|
+
|
|
344
|
+
for prop_name, schema_key in shared_schema_keys.items():
|
|
345
|
+
type_name = name_map.get(schema_key)
|
|
346
|
+
if type_name:
|
|
347
|
+
shared_props[prop_name]["type"] = type_name
|
|
348
|
+
|
|
349
|
+
return shared_props
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def generate_inertia_pages_json(
|
|
353
|
+
app: "Litestar",
|
|
354
|
+
*,
|
|
355
|
+
openapi_schema: dict[str, Any] | None = None,
|
|
356
|
+
include_default_auth: bool = True,
|
|
357
|
+
include_default_flash: bool = True,
|
|
358
|
+
inertia_config: "InertiaConfig | None" = None,
|
|
359
|
+
types_config: "TypeGenConfig | None" = None,
|
|
360
|
+
) -> dict[str, Any]:
|
|
361
|
+
"""Generate Inertia pages metadata JSON.
|
|
362
|
+
|
|
363
|
+
Returns:
|
|
364
|
+
An Inertia pages metadata payload as a dictionary.
|
|
365
|
+
"""
|
|
366
|
+
pages_metadata = extract_inertia_pages(
|
|
367
|
+
app,
|
|
368
|
+
openapi_schema=openapi_schema,
|
|
369
|
+
fallback_type=types_config.fallback_type if types_config is not None else "unknown",
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
pages_dict: dict[str, dict[str, Any]] = {}
|
|
373
|
+
for page in pages_metadata:
|
|
374
|
+
page_data: dict[str, Any] = {"route": page.route_path}
|
|
375
|
+
if page.props_type:
|
|
376
|
+
page_data["propsType"] = page.props_type
|
|
377
|
+
if page.ts_type:
|
|
378
|
+
page_data["tsType"] = page.ts_type
|
|
379
|
+
if page.custom_types:
|
|
380
|
+
page_data["customTypes"] = page.custom_types
|
|
381
|
+
if page.schema_ref:
|
|
382
|
+
page_data["schemaRef"] = page.schema_ref
|
|
383
|
+
if page.handler_name:
|
|
384
|
+
page_data["handler"] = page.handler_name
|
|
385
|
+
pages_dict[page.component] = page_data
|
|
386
|
+
|
|
387
|
+
shared_props = _build_inertia_shared_props(
|
|
388
|
+
app,
|
|
389
|
+
openapi_schema=openapi_schema,
|
|
390
|
+
include_default_auth=include_default_auth,
|
|
391
|
+
include_default_flash=include_default_flash,
|
|
392
|
+
inertia_config=inertia_config,
|
|
393
|
+
types_config=types_config,
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
root: dict[str, Any] = {
|
|
397
|
+
"pages": pages_dict,
|
|
398
|
+
"sharedProps": shared_props,
|
|
399
|
+
"typeGenConfig": {"includeDefaultAuth": include_default_auth, "includeDefaultFlash": include_default_flash},
|
|
400
|
+
"generatedAt": datetime.datetime.now(tz=datetime.timezone.utc).isoformat(),
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if types_config is not None:
|
|
404
|
+
root["typeImportPaths"] = types_config.type_import_paths
|
|
405
|
+
root["fallbackType"] = types_config.fallback_type
|
|
406
|
+
|
|
407
|
+
return root
|
|
@@ -6,6 +6,7 @@ the codegen logic can remain stable and easier to reason about.
|
|
|
6
6
|
|
|
7
7
|
import contextlib
|
|
8
8
|
from dataclasses import dataclass
|
|
9
|
+
from pathlib import Path
|
|
9
10
|
from typing import TYPE_CHECKING, Any, Protocol, cast
|
|
10
11
|
|
|
11
12
|
from litestar._openapi.datastructures import OpenAPIContext # pyright: ignore[reportPrivateUsage]
|
|
@@ -181,64 +182,12 @@ def schema_name_from_ref(ref: str) -> str:
|
|
|
181
182
|
return ref.rsplit("/", maxsplit=1)[-1]
|
|
182
183
|
|
|
183
184
|
|
|
184
|
-
def _filter_response_types_from_union(field_definition: FieldDefinition) -> FieldDefinition | None:
|
|
185
|
-
"""Filter out ASGIResponse subtypes from a union type.
|
|
186
|
-
|
|
187
|
-
For union types like `InertiaRedirect | NoProps`, this filters out the response
|
|
188
|
-
types (InertiaRedirect) and returns only the props types (NoProps).
|
|
189
|
-
|
|
190
|
-
Args:
|
|
191
|
-
field_definition: The field definition to filter.
|
|
192
|
-
|
|
193
|
-
Returns:
|
|
194
|
-
Filtered FieldDefinition with response types removed, or None if all types are responses.
|
|
195
|
-
"""
|
|
196
|
-
# Not a union - return as-is (caller handles response type check)
|
|
197
|
-
if not field_definition.is_union:
|
|
198
|
-
return field_definition
|
|
199
|
-
|
|
200
|
-
# Filter inner types, keeping only non-response types
|
|
201
|
-
# IMPORTANT: Check order matters! LitestarResponse is a subclass of ASGIResponse,
|
|
202
|
-
# so we must check LitestarResponse FIRST to extract inner types before the
|
|
203
|
-
# general ASGIResponse check skips it entirely.
|
|
204
|
-
props_types: list[type] = []
|
|
205
|
-
for inner in field_definition.inner_types:
|
|
206
|
-
# Skip None types
|
|
207
|
-
if inner.is_subclass_of(NoneType):
|
|
208
|
-
continue
|
|
209
|
-
# For LitestarResponse[T], extract T as the props type
|
|
210
|
-
if inner.is_subclass_of(LitestarResponse):
|
|
211
|
-
if inner.inner_types:
|
|
212
|
-
props_types.append(inner.inner_types[0].annotation)
|
|
213
|
-
continue
|
|
214
|
-
# Skip other ASGIResponse subtypes (Redirect, etc.)
|
|
215
|
-
if inner.is_subclass_of(ASGIResponse):
|
|
216
|
-
continue
|
|
217
|
-
props_types.append(inner.annotation)
|
|
218
|
-
|
|
219
|
-
if not props_types:
|
|
220
|
-
return None
|
|
221
|
-
if len(props_types) == 1:
|
|
222
|
-
return FieldDefinition.from_annotation(props_types[0])
|
|
223
|
-
|
|
224
|
-
# Sort types by qualified name for deterministic union construction
|
|
225
|
-
# This prevents cache key inconsistencies from type ordering
|
|
226
|
-
props_types.sort(key=lambda t: getattr(t, "__qualname__", str(t)))
|
|
227
|
-
|
|
228
|
-
# Rebuild union type
|
|
229
|
-
from typing import Union
|
|
230
|
-
|
|
231
|
-
union_type = Union[tuple(props_types)] # type: ignore[valid-type] # noqa: UP007
|
|
232
|
-
return FieldDefinition.from_annotation(union_type)
|
|
233
|
-
|
|
234
|
-
|
|
235
185
|
def resolve_page_props_field_definition(
|
|
236
186
|
handler: HTTPRouteHandler, schema_creator: SchemaCreator
|
|
237
187
|
) -> tuple[FieldDefinition | None, Schema | Reference | None]:
|
|
238
188
|
"""Resolve FieldDefinition and schema result for a handler's response.
|
|
239
189
|
|
|
240
190
|
Mirrors Litestar's response schema generation to ensure consistent schema registration.
|
|
241
|
-
Filters out ASGIResponse subtypes from union types.
|
|
242
191
|
|
|
243
192
|
Args:
|
|
244
193
|
handler: HTTP route handler.
|
|
@@ -247,12 +196,7 @@ def resolve_page_props_field_definition(
|
|
|
247
196
|
Returns:
|
|
248
197
|
Tuple of (FieldDefinition or None, Schema/Reference or None).
|
|
249
198
|
"""
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
# Filter response types from unions (e.g., InertiaRedirect | NoProps -> NoProps)
|
|
253
|
-
field_definition = _filter_response_types_from_union(original_field)
|
|
254
|
-
if field_definition is None:
|
|
255
|
-
return None, None
|
|
199
|
+
field_definition = handler.parsed_fn_signature.return_type
|
|
256
200
|
|
|
257
201
|
if field_definition.is_subclass_of((NoneType, ASGIResponse)):
|
|
258
202
|
return None, None
|
|
@@ -278,3 +222,12 @@ def resolve_page_props_field_definition(
|
|
|
278
222
|
resolved_field = field_definition
|
|
279
223
|
|
|
280
224
|
return resolved_field, schema_creator.for_field_definition(resolved_field)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def to_root_path(root_dir: Path, path: Path) -> Path:
|
|
228
|
+
"""Resolve a path relative to ``root_dir`` when it is not absolute.
|
|
229
|
+
|
|
230
|
+
Returns:
|
|
231
|
+
Absolute path.
|
|
232
|
+
"""
|
|
233
|
+
return path if path.is_absolute() else (root_dir / path)
|