litestar-vite 0.1.22__py3-none-any.whl → 0.2.1__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.
Potentially problematic release.
This version of litestar-vite might be problematic. Click here for more details.
- litestar_vite/__init__.py +2 -1
- litestar_vite/cli.py +44 -1
- litestar_vite/commands.py +11 -8
- litestar_vite/config.py +21 -17
- litestar_vite/inertia/__init__.py +31 -0
- litestar_vite/inertia/_utils.py +62 -0
- litestar_vite/inertia/config.py +25 -0
- litestar_vite/inertia/exception_handler.py +99 -0
- litestar_vite/inertia/middleware.py +56 -0
- litestar_vite/inertia/plugin.py +64 -0
- litestar_vite/inertia/request.py +111 -0
- litestar_vite/inertia/response.py +347 -0
- litestar_vite/inertia/routes.py +54 -0
- litestar_vite/inertia/types.py +39 -0
- litestar_vite/loader.py +47 -35
- litestar_vite/plugin.py +20 -11
- litestar_vite/template_engine.py +24 -5
- litestar_vite/templates/index.html.j2 +12 -14
- {litestar_vite-0.1.22.dist-info → litestar_vite-0.2.1.dist-info}/METADATA +3 -13
- litestar_vite-0.2.1.dist-info/RECORD +30 -0
- {litestar_vite-0.1.22.dist-info → litestar_vite-0.2.1.dist-info}/WHEEL +1 -1
- litestar_vite-0.1.22.dist-info/RECORD +0 -20
- {litestar_vite-0.1.22.dist-info → litestar_vite-0.2.1.dist-info}/licenses/LICENSE +0 -0
litestar_vite/__init__.py
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
from litestar_vite import inertia
|
|
3
4
|
from litestar_vite.config import ViteConfig, ViteTemplateConfig
|
|
4
5
|
from litestar_vite.loader import ViteAssetLoader
|
|
5
6
|
from litestar_vite.plugin import VitePlugin
|
|
6
7
|
from litestar_vite.template_engine import ViteTemplateEngine
|
|
7
8
|
|
|
8
|
-
__all__ = ("ViteConfig", "ViteTemplateConfig", "VitePlugin", "ViteAssetLoader", "ViteTemplateEngine")
|
|
9
|
+
__all__ = ("ViteConfig", "ViteTemplateConfig", "VitePlugin", "ViteAssetLoader", "ViteTemplateEngine", "inertia")
|
litestar_vite/cli.py
CHANGED
|
@@ -122,7 +122,7 @@ def vite_init(
|
|
|
122
122
|
ctx.obj.app.debug = True
|
|
123
123
|
env: LitestarEnv = ctx.obj
|
|
124
124
|
plugin = env.app.plugins.get(VitePlugin)
|
|
125
|
-
config = plugin._config
|
|
125
|
+
config = plugin._config # pyright: ignore[reportPrivateUsage]
|
|
126
126
|
|
|
127
127
|
console.rule("[yellow]Initializing Vite[/]", align="left")
|
|
128
128
|
resource_path = Path(resource_path or config.resource_dir)
|
|
@@ -279,3 +279,46 @@ def vite_serve(app: Litestar, verbose: bool) -> None:
|
|
|
279
279
|
command_to_run = plugin.config.run_command if plugin.config.hot_reload else plugin.config.build_watch_command
|
|
280
280
|
execute_command(command_to_run=command_to_run, cwd=plugin.config.root_dir)
|
|
281
281
|
console.print("[yellow]Vite process stopped.[/]")
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
@vite_group.command(
|
|
285
|
+
name="generate-routes",
|
|
286
|
+
help="Generate a JSON file with the route configuration",
|
|
287
|
+
)
|
|
288
|
+
@option(
|
|
289
|
+
"--output",
|
|
290
|
+
help="output file path",
|
|
291
|
+
type=ClickPath(dir_okay=False, path_type=Path),
|
|
292
|
+
default=Path("routes.json"),
|
|
293
|
+
show_default=True,
|
|
294
|
+
)
|
|
295
|
+
@option("--verbose", type=bool, help="Enable verbose output.", default=False, is_flag=True)
|
|
296
|
+
def generate_js_routes(app: Litestar, output: Path, verbose: bool) -> None:
|
|
297
|
+
"""Run vite serve."""
|
|
298
|
+
import msgspec
|
|
299
|
+
from litestar.cli._utils import (
|
|
300
|
+
LitestarCLIException,
|
|
301
|
+
console,
|
|
302
|
+
)
|
|
303
|
+
from litestar.serialization import encode_json, get_serializer
|
|
304
|
+
|
|
305
|
+
from litestar_vite.plugin import VitePlugin, set_environment
|
|
306
|
+
|
|
307
|
+
if verbose:
|
|
308
|
+
app.debug = True
|
|
309
|
+
serializer = get_serializer(app.type_encoders)
|
|
310
|
+
plugin = app.plugins.get(VitePlugin)
|
|
311
|
+
if plugin.config.set_environment:
|
|
312
|
+
set_environment(config=plugin.config)
|
|
313
|
+
content = msgspec.json.format(
|
|
314
|
+
encode_json(app.openapi_schema.to_schema(), serializer=serializer),
|
|
315
|
+
indent=4,
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
try:
|
|
319
|
+
output.write_bytes(content)
|
|
320
|
+
except OSError as e: # pragma: no cover
|
|
321
|
+
msg = f"failed to write schema to path {output}"
|
|
322
|
+
raise LitestarCLIException(msg) from e
|
|
323
|
+
|
|
324
|
+
console.print("[yellow]Vite process stopped.[/]")
|
litestar_vite/commands.py
CHANGED
|
@@ -13,11 +13,11 @@ VITE_INIT_TEMPLATES: set[str] = {"package.json.j2", "tsconfig.json.j2", "vite.co
|
|
|
13
13
|
DEFAULT_RESOURCES: set[str] = {"styles.css.j2", "main.ts.j2"}
|
|
14
14
|
DEFAULT_DEV_DEPENDENCIES: dict[str, str] = {
|
|
15
15
|
"typescript": "^5.3.3",
|
|
16
|
-
"vite": "^5.
|
|
17
|
-
"litestar-vite-plugin": "^0.
|
|
18
|
-
"@types/node": "^20.10
|
|
16
|
+
"vite": "^5.3.3",
|
|
17
|
+
"litestar-vite-plugin": "^0.6.2",
|
|
18
|
+
"@types/node": "^20.14.10",
|
|
19
19
|
}
|
|
20
|
-
DEFAULT_DEPENDENCIES: dict[str, str] = {"axios": "^1.
|
|
20
|
+
DEFAULT_DEPENDENCIES: dict[str, str] = {"axios": "^1.7.2"}
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
def to_json(value: Any) -> str:
|
|
@@ -66,14 +66,15 @@ def init_vite(
|
|
|
66
66
|
for template_name in enabled_templates
|
|
67
67
|
}
|
|
68
68
|
for template_name, template in templates.items():
|
|
69
|
-
target_file_name = template_name.
|
|
69
|
+
target_file_name = template_name[:-3] if template_name.endswith(".j2") else template_name
|
|
70
70
|
with Path(target_file_name).open(mode="w") as file:
|
|
71
71
|
console.print(f" * Writing {target_file_name} to {Path(target_file_name)!s}")
|
|
72
72
|
|
|
73
73
|
file.write(
|
|
74
74
|
template.render(
|
|
75
75
|
entry_point=[
|
|
76
|
-
f"{resource_path!s}/{resource_name.
|
|
76
|
+
f"{resource_path!s}/{resource_name[:-3] if resource_name.endswith('.j2') else resource_name}"
|
|
77
|
+
for resource_name in enabled_resources
|
|
77
78
|
],
|
|
78
79
|
enable_ssr=enable_ssr,
|
|
79
80
|
asset_url=asset_url,
|
|
@@ -91,9 +92,11 @@ def init_vite(
|
|
|
91
92
|
|
|
92
93
|
for resource_name in enabled_resources:
|
|
93
94
|
template = get_template(environment=vite_template_env, name=resource_name)
|
|
94
|
-
target_file_name = f"{resource_path}/{resource_name.
|
|
95
|
+
target_file_name = f"{resource_path}/{resource_name[:-3] if resource_name.endswith('.j2') else resource_name}"
|
|
95
96
|
with Path(target_file_name).open(mode="w") as file:
|
|
96
|
-
console.print(
|
|
97
|
+
console.print(
|
|
98
|
+
f" * Writing {resource_name[:-3] if resource_name.endswith('.j2') else resource_name} to {Path(target_file_name)!s}",
|
|
99
|
+
)
|
|
97
100
|
file.write(template.render())
|
|
98
101
|
console.print("[yellow]Vite initialization completed.[/]")
|
|
99
102
|
|
litestar_vite/config.py
CHANGED
|
@@ -5,21 +5,19 @@ from dataclasses import dataclass, field
|
|
|
5
5
|
from functools import cached_property
|
|
6
6
|
from inspect import isclass
|
|
7
7
|
from pathlib import Path
|
|
8
|
-
from typing import TYPE_CHECKING,
|
|
8
|
+
from typing import TYPE_CHECKING, Any, TypeVar, cast
|
|
9
9
|
|
|
10
10
|
from litestar.exceptions import ImproperlyConfiguredException
|
|
11
|
-
from litestar.template import TemplateEngineProtocol
|
|
12
|
-
|
|
13
|
-
__all__ = ["ViteConfig", "ViteTemplateConfig"]
|
|
14
|
-
|
|
11
|
+
from litestar.template import TemplateConfig, TemplateEngineProtocol
|
|
15
12
|
|
|
16
13
|
if TYPE_CHECKING:
|
|
17
14
|
from collections.abc import Callable
|
|
18
15
|
|
|
19
16
|
from litestar.types import PathType
|
|
20
17
|
|
|
21
|
-
|
|
22
|
-
EngineType = TypeVar("EngineType", bound=TemplateEngineProtocol)
|
|
18
|
+
__all__ = ("ViteConfig", "ViteTemplateConfig")
|
|
19
|
+
EngineType = TypeVar("EngineType", bound=TemplateEngineProtocol[Any, Any])
|
|
20
|
+
TRUE_VALUES = {"True", "true", "1", "yes", "Y", "T"}
|
|
23
21
|
|
|
24
22
|
|
|
25
23
|
@dataclass
|
|
@@ -41,7 +39,7 @@ class ViteConfig:
|
|
|
41
39
|
|
|
42
40
|
In a standalone Vue or React application, this would be equivalent to the ``./src`` directory.
|
|
43
41
|
"""
|
|
44
|
-
template_dir: Path | str | None = field(default=
|
|
42
|
+
template_dir: Path | str | None = field(default="templates")
|
|
45
43
|
"""Location of the Jinja2 template file."""
|
|
46
44
|
public_dir: Path | str = field(default="public")
|
|
47
45
|
"""The optional public directory Vite serves assets from.
|
|
@@ -56,7 +54,7 @@ class ViteConfig:
|
|
|
56
54
|
This file contains a single line containing the host, protocol, and port the Vite server is running.
|
|
57
55
|
"""
|
|
58
56
|
hot_reload: bool = field(
|
|
59
|
-
default_factory=lambda: os.getenv("VITE_HOT_RELOAD", "True") in
|
|
57
|
+
default_factory=lambda: os.getenv("VITE_HOT_RELOAD", "True") in TRUE_VALUES,
|
|
60
58
|
)
|
|
61
59
|
"""Enable HMR for Vite development server."""
|
|
62
60
|
ssr_enabled: bool = False
|
|
@@ -91,11 +89,11 @@ class ViteConfig:
|
|
|
91
89
|
install_command: list[str] = field(default_factory=lambda: ["npm", "install"])
|
|
92
90
|
"""Default command to use for installing Vite."""
|
|
93
91
|
use_server_lifespan: bool = field(
|
|
94
|
-
default_factory=lambda: os.getenv("VITE_USE_SERVER_LIFESPAN", "False") in
|
|
92
|
+
default_factory=lambda: os.getenv("VITE_USE_SERVER_LIFESPAN", "False") in TRUE_VALUES,
|
|
95
93
|
)
|
|
96
94
|
"""Utilize the server lifespan hook to run Vite."""
|
|
97
95
|
dev_mode: bool = field(
|
|
98
|
-
default_factory=lambda: os.getenv("VITE_DEV_MODE", "False") in
|
|
96
|
+
default_factory=lambda: os.getenv("VITE_DEV_MODE", "False") in TRUE_VALUES,
|
|
99
97
|
)
|
|
100
98
|
"""When True, Vite will run with HMR or watch build"""
|
|
101
99
|
detect_nodeenv: bool = True
|
|
@@ -115,7 +113,7 @@ class ViteConfig:
|
|
|
115
113
|
self.root_dir = Path(self.root_dir)
|
|
116
114
|
if self.template_dir is not None and isinstance(self.template_dir, str):
|
|
117
115
|
self.template_dir = Path(self.template_dir)
|
|
118
|
-
if self.public_dir
|
|
116
|
+
if self.public_dir and isinstance(self.public_dir, str):
|
|
119
117
|
self.public_dir = Path(self.public_dir)
|
|
120
118
|
if isinstance(self.resource_dir, str):
|
|
121
119
|
self.resource_dir = Path(self.resource_dir)
|
|
@@ -126,7 +124,7 @@ class ViteConfig:
|
|
|
126
124
|
|
|
127
125
|
|
|
128
126
|
@dataclass
|
|
129
|
-
class ViteTemplateConfig(
|
|
127
|
+
class ViteTemplateConfig(TemplateConfig[EngineType]):
|
|
130
128
|
"""Configuration for Templating.
|
|
131
129
|
|
|
132
130
|
To enable templating, pass an instance of this class to the
|
|
@@ -134,7 +132,7 @@ class ViteTemplateConfig(Generic[EngineType]):
|
|
|
134
132
|
'template_config' key.
|
|
135
133
|
"""
|
|
136
134
|
|
|
137
|
-
config: ViteConfig
|
|
135
|
+
config: ViteConfig = field(default_factory=lambda: ViteConfig())
|
|
138
136
|
"""A a config for the vite engine`."""
|
|
139
137
|
engine: type[EngineType] | EngineType | None = field(default=None)
|
|
140
138
|
"""A template engine adhering to the :class:`TemplateEngineProtocol
|
|
@@ -150,15 +148,21 @@ class ViteTemplateConfig(Generic[EngineType]):
|
|
|
150
148
|
|
|
151
149
|
def __post_init__(self) -> None:
|
|
152
150
|
"""Ensure that directory is set if engine is a class."""
|
|
153
|
-
if isclass(self.engine) and not self.directory:
|
|
151
|
+
if isclass(self.engine) and not self.directory: # pyright: ignore[reportUnknownMemberType]
|
|
154
152
|
msg = "directory is a required kwarg when passing a template engine class"
|
|
155
153
|
raise ImproperlyConfiguredException(msg)
|
|
154
|
+
"""Ensure that directory is not set if instance is."""
|
|
155
|
+
if self.instance is not None and self.directory is not None: # pyright: ignore[reportUnknownMemberType]
|
|
156
|
+
msg = "directory cannot be set if instance is"
|
|
157
|
+
raise ImproperlyConfiguredException(msg)
|
|
156
158
|
|
|
157
159
|
def to_engine(self) -> EngineType:
|
|
158
160
|
"""Instantiate the template engine."""
|
|
159
161
|
template_engine = cast(
|
|
160
162
|
"EngineType",
|
|
161
|
-
self.engine(directory=self.directory, config=self.config
|
|
163
|
+
self.engine(directory=self.directory, config=self.config, engine_instance=None) # pyright: ignore[reportUnknownMemberType,reportCallIssue]
|
|
164
|
+
if isclass(self.engine)
|
|
165
|
+
else self.engine,
|
|
162
166
|
)
|
|
163
167
|
if callable(self.engine_callback):
|
|
164
168
|
self.engine_callback(template_engine)
|
|
@@ -167,4 +171,4 @@ class ViteTemplateConfig(Generic[EngineType]):
|
|
|
167
171
|
@cached_property
|
|
168
172
|
def engine_instance(self) -> EngineType:
|
|
169
173
|
"""Return the template engine instance."""
|
|
170
|
-
return self.to_engine()
|
|
174
|
+
return self.to_engine() if self.instance is None else self.instance
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from .config import InertiaConfig
|
|
2
|
+
from .middleware import InertiaMiddleware
|
|
3
|
+
from .plugin import InertiaPlugin
|
|
4
|
+
from .request import InertiaDetails, InertiaHeaders, InertiaRequest
|
|
5
|
+
from .response import (
|
|
6
|
+
InertiaBack,
|
|
7
|
+
InertiaExternalRedirect,
|
|
8
|
+
InertiaRedirect,
|
|
9
|
+
InertiaResponse,
|
|
10
|
+
error,
|
|
11
|
+
get_shared_props,
|
|
12
|
+
share,
|
|
13
|
+
)
|
|
14
|
+
from .routes import generate_js_routes
|
|
15
|
+
|
|
16
|
+
__all__ = (
|
|
17
|
+
"InertiaConfig",
|
|
18
|
+
"InertiaDetails",
|
|
19
|
+
"InertiaHeaders",
|
|
20
|
+
"InertiaRequest",
|
|
21
|
+
"InertiaResponse",
|
|
22
|
+
"InertiaExternalRedirect",
|
|
23
|
+
"InertiaPlugin",
|
|
24
|
+
"InertiaBack",
|
|
25
|
+
"share",
|
|
26
|
+
"error",
|
|
27
|
+
"get_shared_props",
|
|
28
|
+
"InertiaRedirect",
|
|
29
|
+
"generate_js_routes",
|
|
30
|
+
"InertiaMiddleware",
|
|
31
|
+
)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from typing import TYPE_CHECKING, Any, Callable
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from litestar_vite.inertia.types import InertiaHeaderType
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class InertiaHeaders(str, Enum):
|
|
11
|
+
"""Enum for Inertia Headers"""
|
|
12
|
+
|
|
13
|
+
ENABLED = "X-Inertia"
|
|
14
|
+
VERSION = "X-Inertia-Version"
|
|
15
|
+
PARTIAL_DATA = "X-Inertia-Partial-Data"
|
|
16
|
+
PARTIAL_COMPONENT = "X-Inertia-Partial-Component"
|
|
17
|
+
LOCATION = "X-Inertia-Location"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_enabled_header(enabled: bool = True) -> dict[str, Any]:
|
|
21
|
+
"""True if inertia is enabled."""
|
|
22
|
+
|
|
23
|
+
return {InertiaHeaders.ENABLED.value: "true" if enabled else "false"}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def get_version_header(version: str) -> dict[str, Any]:
|
|
27
|
+
"""Return headers for change swap method response."""
|
|
28
|
+
return {InertiaHeaders.VERSION.value: version}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def get_partial_data_header(partial: str) -> dict[str, Any]:
|
|
32
|
+
"""Return headers for a partial data response."""
|
|
33
|
+
return {InertiaHeaders.PARTIAL_DATA.value: partial}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def get_partial_component_header(partial: str) -> dict[str, Any]:
|
|
37
|
+
"""Return headers for a partial data response."""
|
|
38
|
+
return {InertiaHeaders.PARTIAL_COMPONENT.value: partial}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def get_headers(inertia_headers: InertiaHeaderType) -> dict[str, Any]:
|
|
42
|
+
"""Return headers for Inertia responses."""
|
|
43
|
+
if not inertia_headers:
|
|
44
|
+
msg = "Value for inertia_headers cannot be None."
|
|
45
|
+
raise ValueError(msg)
|
|
46
|
+
inertia_headers_dict: dict[str, Callable[..., dict[str, Any]]] = {
|
|
47
|
+
"enabled": get_enabled_header,
|
|
48
|
+
"partial_data": get_partial_data_header,
|
|
49
|
+
"partial_component": get_partial_component_header,
|
|
50
|
+
"version": get_version_header,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
header: dict[str, Any] = {}
|
|
54
|
+
response: dict[str, Any]
|
|
55
|
+
key: str
|
|
56
|
+
value: Any
|
|
57
|
+
|
|
58
|
+
for key, value in inertia_headers.items():
|
|
59
|
+
if value is not None:
|
|
60
|
+
response = inertia_headers_dict[key](value)
|
|
61
|
+
header.update(response)
|
|
62
|
+
return header
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
__all__ = ("InertiaConfig",)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class InertiaConfig:
|
|
11
|
+
"""Configuration for InertiaJS support."""
|
|
12
|
+
|
|
13
|
+
root_template: str = "index.html"
|
|
14
|
+
"""Name of the root template to use.
|
|
15
|
+
|
|
16
|
+
This must be a path that is found by the Vite Plugin template config
|
|
17
|
+
"""
|
|
18
|
+
component_opt_key: str = "component"
|
|
19
|
+
"""An identifier to use on routes to get the inertia component to render."""
|
|
20
|
+
exclude_from_js_routes_key: str = "exclude_from_routes"
|
|
21
|
+
"""An identifier to use on routes to exclude a route from the generated routes typescript file."""
|
|
22
|
+
redirect_unauthorized_to: str | None = None
|
|
23
|
+
"""Optionally supply a path where unauthorized requests should redirect."""
|
|
24
|
+
extra_page_props: dict[str, Any] = field(default_factory=dict)
|
|
25
|
+
"""A dictionary of values to automatically add in to page props on every request."""
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from typing import TYPE_CHECKING, Any, cast
|
|
3
|
+
|
|
4
|
+
from litestar import MediaType
|
|
5
|
+
from litestar.connection import Request
|
|
6
|
+
from litestar.connection.base import AuthT, StateT, UserT
|
|
7
|
+
from litestar.exceptions import (
|
|
8
|
+
HTTPException,
|
|
9
|
+
InternalServerException,
|
|
10
|
+
NotAuthorizedException,
|
|
11
|
+
NotFoundException,
|
|
12
|
+
PermissionDeniedException,
|
|
13
|
+
)
|
|
14
|
+
from litestar.exceptions.responses import (
|
|
15
|
+
create_debug_response, # pyright: ignore[reportUnknownVariableType]
|
|
16
|
+
create_exception_response, # pyright: ignore[reportUnknownVariableType]
|
|
17
|
+
)
|
|
18
|
+
from litestar.plugins.flash import flash
|
|
19
|
+
from litestar.repository.exceptions import (
|
|
20
|
+
NotFoundError, # pyright: ignore[reportUnknownVariableType,reportAttributeAccessIssue]
|
|
21
|
+
RepositoryError, # pyright: ignore[reportUnknownVariableType,reportAttributeAccessIssue]
|
|
22
|
+
)
|
|
23
|
+
from litestar.response import Response
|
|
24
|
+
from litestar.status_codes import (
|
|
25
|
+
HTTP_400_BAD_REQUEST,
|
|
26
|
+
HTTP_401_UNAUTHORIZED,
|
|
27
|
+
HTTP_409_CONFLICT,
|
|
28
|
+
HTTP_422_UNPROCESSABLE_ENTITY,
|
|
29
|
+
HTTP_500_INTERNAL_SERVER_ERROR,
|
|
30
|
+
)
|
|
31
|
+
from litestar.types import Empty
|
|
32
|
+
|
|
33
|
+
from litestar_vite.inertia.response import InertiaBack, InertiaRedirect, InertiaResponse, error
|
|
34
|
+
|
|
35
|
+
if TYPE_CHECKING:
|
|
36
|
+
from litestar_vite.inertia.plugin import InertiaPlugin
|
|
37
|
+
|
|
38
|
+
FIELD_ERR_RE = re.compile(r"field `(.+)`$")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class _HTTPConflictException(HTTPException):
|
|
42
|
+
"""Request conflict with the current state of the target resource."""
|
|
43
|
+
|
|
44
|
+
status_code = HTTP_409_CONFLICT
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def exception_to_http_response(request: Request[UserT, AuthT, StateT], exc: Exception) -> Response[Any]:
|
|
48
|
+
"""Handler for all exceptions subclassed from HTTPException."""
|
|
49
|
+
inertia_enabled = getattr(request, "inertia_enabled", False) or getattr(request, "is_inertia", False)
|
|
50
|
+
if isinstance(exc, NotFoundError):
|
|
51
|
+
http_exc = NotFoundException
|
|
52
|
+
elif isinstance(exc, RepositoryError):
|
|
53
|
+
http_exc = _HTTPConflictException # type: ignore[assignment]
|
|
54
|
+
else:
|
|
55
|
+
http_exc = InternalServerException # type: ignore[assignment]
|
|
56
|
+
if not inertia_enabled:
|
|
57
|
+
if request.app.debug and http_exc not in (PermissionDeniedException, NotFoundError):
|
|
58
|
+
return cast("Response[Any]", create_debug_response(request, exc))
|
|
59
|
+
return cast("Response[Any]", create_exception_response(request, http_exc(detail=str(exc.__cause__))))
|
|
60
|
+
has_active_session = not (not request.session or request.scope["session"] is Empty)
|
|
61
|
+
is_inertia = getattr(request, "is_inertia", False)
|
|
62
|
+
status_code = getattr(exc, "status_code", HTTP_500_INTERNAL_SERVER_ERROR)
|
|
63
|
+
preferred_type = MediaType.HTML if inertia_enabled and not is_inertia else MediaType.JSON
|
|
64
|
+
detail = getattr(exc, "detail", "") # litestar exceptions
|
|
65
|
+
extras = getattr(exc, "extra", "") # msgspec exceptions
|
|
66
|
+
content = {"status_code": status_code, "message": getattr(exc, "detail", "")}
|
|
67
|
+
inertia_plugin = cast("InertiaPlugin", request.app.plugins.get("InertiaPlugin"))
|
|
68
|
+
if extras:
|
|
69
|
+
content.update({"extra": extras})
|
|
70
|
+
if has_active_session:
|
|
71
|
+
flash(request, detail, category="error")
|
|
72
|
+
if extras and len(extras) >= 1:
|
|
73
|
+
message = extras[0]
|
|
74
|
+
default_field = f"root.{message.get('key')}" if message.get("key", None) is not None else "root" # type: ignore
|
|
75
|
+
error_detail = cast("str", message.get("message", detail)) # type: ignore[union-attr] # pyright: ignore[reportUnknownMemberType,reportUnknownArgumentType]
|
|
76
|
+
match = FIELD_ERR_RE.search(error_detail)
|
|
77
|
+
field = match.group(1) if match else default_field
|
|
78
|
+
if isinstance(message, dict) and has_active_session:
|
|
79
|
+
error(request, field, error_detail)
|
|
80
|
+
if status_code in {HTTP_422_UNPROCESSABLE_ENTITY, HTTP_400_BAD_REQUEST} or isinstance(
|
|
81
|
+
exc,
|
|
82
|
+
PermissionDeniedException,
|
|
83
|
+
):
|
|
84
|
+
return InertiaBack(request)
|
|
85
|
+
if isinstance(exc, PermissionDeniedException):
|
|
86
|
+
return InertiaBack(request)
|
|
87
|
+
if status_code == HTTP_401_UNAUTHORIZED or isinstance(exc, NotAuthorizedException):
|
|
88
|
+
redirect_to = (
|
|
89
|
+
inertia_plugin.config.redirect_unauthorized_to is not None
|
|
90
|
+
and str(request.url) != inertia_plugin.config.redirect_unauthorized_to
|
|
91
|
+
)
|
|
92
|
+
if redirect_to:
|
|
93
|
+
return InertiaRedirect(request, redirect_to=cast("str", inertia_plugin.config.redirect_unauthorized_to))
|
|
94
|
+
return InertiaBack(request)
|
|
95
|
+
return InertiaResponse[Any](
|
|
96
|
+
media_type=preferred_type,
|
|
97
|
+
content=content,
|
|
98
|
+
status_code=status_code,
|
|
99
|
+
)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any
|
|
4
|
+
|
|
5
|
+
from litestar import Request
|
|
6
|
+
from litestar.middleware import AbstractMiddleware
|
|
7
|
+
from litestar.types import Receive, Scope, Send
|
|
8
|
+
|
|
9
|
+
from litestar_vite.inertia.response import InertiaRedirect
|
|
10
|
+
from litestar_vite.plugin import VitePlugin
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from litestar.connection.base import (
|
|
14
|
+
AuthT,
|
|
15
|
+
StateT,
|
|
16
|
+
UserT,
|
|
17
|
+
)
|
|
18
|
+
from litestar.types import ASGIApp
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
async def redirect_on_asset_version_mismatch(request: Request[UserT, AuthT, StateT]) -> InertiaRedirect | None:
|
|
22
|
+
if getattr(request, "is_inertia", None) is None:
|
|
23
|
+
return None
|
|
24
|
+
inertia_version = request.headers.get("X-Inertia-Version")
|
|
25
|
+
if inertia_version is None:
|
|
26
|
+
return None
|
|
27
|
+
|
|
28
|
+
vite_plugin = request.app.plugins.get(VitePlugin)
|
|
29
|
+
template_engine = vite_plugin.template_config.to_engine()
|
|
30
|
+
if inertia_version == template_engine.asset_loader.version_id:
|
|
31
|
+
return None
|
|
32
|
+
return InertiaRedirect(request, redirect_to=str(request.url))
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
if TYPE_CHECKING:
|
|
36
|
+
from litestar.types import Receive, Scope, Send
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class InertiaMiddleware(AbstractMiddleware):
|
|
40
|
+
def __init__(self, app: ASGIApp) -> None:
|
|
41
|
+
super().__init__(app)
|
|
42
|
+
self.app = app
|
|
43
|
+
|
|
44
|
+
async def __call__(
|
|
45
|
+
self,
|
|
46
|
+
scope: "Scope",
|
|
47
|
+
receive: "Receive",
|
|
48
|
+
send: "Send",
|
|
49
|
+
) -> None:
|
|
50
|
+
request = Request[Any, Any, Any](scope=scope)
|
|
51
|
+
redirect = await redirect_on_asset_version_mismatch(request)
|
|
52
|
+
if redirect is not None:
|
|
53
|
+
response = redirect.to_asgi_response(app=None, request=request) # pyright: ignore[reportUnknownMemberType]
|
|
54
|
+
await response(scope, receive, send)
|
|
55
|
+
else:
|
|
56
|
+
await self.app(scope, receive, send)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from litestar.exceptions import ImproperlyConfiguredException
|
|
6
|
+
from litestar.middleware import DefineMiddleware
|
|
7
|
+
from litestar.middleware.session import SessionMiddleware
|
|
8
|
+
from litestar.plugins import InitPluginProtocol
|
|
9
|
+
from litestar.security.session_auth.middleware import MiddlewareWrapper
|
|
10
|
+
from litestar.utils.predicates import is_class_and_subclass
|
|
11
|
+
|
|
12
|
+
from litestar_vite.inertia.exception_handler import exception_to_http_response
|
|
13
|
+
from litestar_vite.inertia.middleware import InertiaMiddleware
|
|
14
|
+
from litestar_vite.inertia.request import InertiaRequest
|
|
15
|
+
from litestar_vite.inertia.response import InertiaResponse
|
|
16
|
+
from litestar_vite.inertia.routes import generate_js_routes
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from litestar import Litestar
|
|
20
|
+
from litestar.config.app import AppConfig
|
|
21
|
+
|
|
22
|
+
from litestar_vite.inertia.config import InertiaConfig
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def set_js_routes(app: Litestar) -> None:
|
|
26
|
+
"""Generate the route structure of the application on startup."""
|
|
27
|
+
js_routes = generate_js_routes(app)
|
|
28
|
+
app.state.js_routes = js_routes
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class InertiaPlugin(InitPluginProtocol):
|
|
32
|
+
"""Inertia plugin."""
|
|
33
|
+
|
|
34
|
+
__slots__ = ("config",)
|
|
35
|
+
|
|
36
|
+
def __init__(self, config: InertiaConfig) -> None:
|
|
37
|
+
"""Initialize ``Inertia``.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
config: configure and start Vite.
|
|
41
|
+
"""
|
|
42
|
+
self.config = config
|
|
43
|
+
|
|
44
|
+
def on_app_init(self, app_config: AppConfig) -> AppConfig:
|
|
45
|
+
"""Configure application for use with Vite.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
app_config: The :class:`AppConfig <.config.app.AppConfig>` instance.
|
|
49
|
+
"""
|
|
50
|
+
for mw in app_config.middleware:
|
|
51
|
+
if isinstance(mw, DefineMiddleware) and is_class_and_subclass(
|
|
52
|
+
mw.middleware,
|
|
53
|
+
(MiddlewareWrapper, SessionMiddleware),
|
|
54
|
+
):
|
|
55
|
+
break
|
|
56
|
+
else:
|
|
57
|
+
msg = "The Inertia plugin require a session middleware."
|
|
58
|
+
raise ImproperlyConfiguredException(msg)
|
|
59
|
+
app_config.exception_handlers.update({Exception: exception_to_http_response}) # pyright: ignore[reportUnknownMemberType]
|
|
60
|
+
app_config.request_class = InertiaRequest
|
|
61
|
+
app_config.response_class = InertiaResponse
|
|
62
|
+
app_config.middleware.append(InertiaMiddleware)
|
|
63
|
+
app_config.on_startup.append(set_js_routes)
|
|
64
|
+
return app_config
|