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.
Files changed (55) hide show
  1. litestar_vite/_codegen/__init__.py +26 -0
  2. litestar_vite/_codegen/inertia.py +407 -0
  3. litestar_vite/{codegen/_openapi.py → _codegen/openapi.py} +11 -58
  4. litestar_vite/{codegen/_routes.py → _codegen/routes.py} +43 -110
  5. litestar_vite/{codegen/_ts.py → _codegen/ts.py} +19 -19
  6. litestar_vite/_handler/__init__.py +8 -0
  7. litestar_vite/{handler/_app.py → _handler/app.py} +29 -117
  8. litestar_vite/cli.py +254 -155
  9. litestar_vite/codegen.py +39 -0
  10. litestar_vite/commands.py +6 -0
  11. litestar_vite/{config/__init__.py → config.py} +726 -99
  12. litestar_vite/deploy.py +3 -14
  13. litestar_vite/doctor.py +6 -8
  14. litestar_vite/executor.py +1 -45
  15. litestar_vite/handler.py +9 -0
  16. litestar_vite/html_transform.py +5 -148
  17. litestar_vite/inertia/__init__.py +0 -24
  18. litestar_vite/inertia/_utils.py +0 -5
  19. litestar_vite/inertia/exception_handler.py +16 -22
  20. litestar_vite/inertia/helpers.py +18 -546
  21. litestar_vite/inertia/plugin.py +11 -77
  22. litestar_vite/inertia/request.py +0 -48
  23. litestar_vite/inertia/response.py +17 -113
  24. litestar_vite/inertia/types.py +0 -19
  25. litestar_vite/loader.py +7 -7
  26. litestar_vite/plugin.py +2184 -0
  27. litestar_vite/templates/angular/package.json.j2 +1 -2
  28. litestar_vite/templates/angular-cli/package.json.j2 +1 -2
  29. litestar_vite/templates/base/package.json.j2 +1 -2
  30. litestar_vite/templates/react-inertia/package.json.j2 +1 -2
  31. litestar_vite/templates/vue-inertia/package.json.j2 +1 -2
  32. {litestar_vite-0.15.0.dist-info → litestar_vite-0.15.0rc2.dist-info}/METADATA +5 -5
  33. {litestar_vite-0.15.0.dist-info → litestar_vite-0.15.0rc2.dist-info}/RECORD +36 -49
  34. litestar_vite/codegen/__init__.py +0 -48
  35. litestar_vite/codegen/_export.py +0 -229
  36. litestar_vite/codegen/_inertia.py +0 -619
  37. litestar_vite/codegen/_utils.py +0 -141
  38. litestar_vite/config/_constants.py +0 -97
  39. litestar_vite/config/_deploy.py +0 -70
  40. litestar_vite/config/_inertia.py +0 -241
  41. litestar_vite/config/_paths.py +0 -63
  42. litestar_vite/config/_runtime.py +0 -235
  43. litestar_vite/config/_spa.py +0 -93
  44. litestar_vite/config/_types.py +0 -94
  45. litestar_vite/handler/__init__.py +0 -9
  46. litestar_vite/inertia/precognition.py +0 -274
  47. litestar_vite/plugin/__init__.py +0 -687
  48. litestar_vite/plugin/_process.py +0 -185
  49. litestar_vite/plugin/_proxy.py +0 -689
  50. litestar_vite/plugin/_proxy_headers.py +0 -244
  51. litestar_vite/plugin/_static.py +0 -37
  52. litestar_vite/plugin/_utils.py +0 -489
  53. /litestar_vite/{handler/_routing.py → _handler/routing.py} +0 -0
  54. {litestar_vite-0.15.0.dist-info → litestar_vite-0.15.0rc2.dist-info}/WHEEL +0 -0
  55. {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
- original_field = handler.parsed_fn_signature.return_type
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)