litestar-vite 0.1.21__py3-none-any.whl → 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +46 -1
- litestar_vite/commands.py +23 -19
- litestar_vite/config.py +27 -22
- 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 +91 -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 +345 -0
- litestar_vite/inertia/routes.py +54 -0
- litestar_vite/inertia/types.py +39 -0
- litestar_vite/loader.py +57 -46
- litestar_vite/plugin.py +31 -14
- litestar_vite/template_engine.py +24 -5
- litestar_vite/templates/index.html.j2 +12 -14
- litestar_vite/templates/main.ts.j2 +1 -0
- {litestar_vite-0.1.21.dist-info → litestar_vite-0.2.0.dist-info}/METADATA +2 -12
- litestar_vite-0.2.0.dist-info/RECORD +30 -0
- {litestar_vite-0.1.21.dist-info → litestar_vite-0.2.0.dist-info}/WHEEL +1 -1
- litestar_vite-0.1.21.dist-info/RECORD +0 -19
- /litestar_vite/templates/{main.css.j2 → styles.css.j2} +0 -0
- {litestar_vite-0.1.21.dist-info → litestar_vite-0.2.0.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)
|
|
@@ -144,6 +144,8 @@ def vite_init(
|
|
|
144
144
|
console.print("Skipping Vite initialization")
|
|
145
145
|
sys.exit(2)
|
|
146
146
|
for output_path in (bundle_path, resource_path, root_path):
|
|
147
|
+
if output_path.exists():
|
|
148
|
+
console.print(f" * Creating {output_path!s}")
|
|
147
149
|
output_path.mkdir(parents=True, exist_ok=True)
|
|
148
150
|
|
|
149
151
|
enable_ssr = (
|
|
@@ -277,3 +279,46 @@ def vite_serve(app: Litestar, verbose: bool) -> None:
|
|
|
277
279
|
command_to_run = plugin.config.run_command if plugin.config.hot_reload else plugin.config.build_watch_command
|
|
278
280
|
execute_command(command_to_run=command_to_run, cwd=plugin.config.root_dir)
|
|
279
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
|
@@ -9,16 +9,15 @@ if TYPE_CHECKING:
|
|
|
9
9
|
from jinja2 import Environment, Template
|
|
10
10
|
from litestar import Litestar
|
|
11
11
|
|
|
12
|
-
VITE_INIT_TEMPLATES_PATH = f"{Path(__file__).parent}/templates"
|
|
13
12
|
VITE_INIT_TEMPLATES: set[str] = {"package.json.j2", "tsconfig.json.j2", "vite.config.ts.j2"}
|
|
14
|
-
DEFAULT_RESOURCES: set[str] = {"styles.css", "main.ts"}
|
|
13
|
+
DEFAULT_RESOURCES: set[str] = {"styles.css.j2", "main.ts.j2"}
|
|
15
14
|
DEFAULT_DEV_DEPENDENCIES: dict[str, str] = {
|
|
16
15
|
"typescript": "^5.3.3",
|
|
17
|
-
"vite": "^5.
|
|
18
|
-
"litestar-vite-plugin": "^0.
|
|
19
|
-
"@types/node": "^20.10
|
|
16
|
+
"vite": "^5.3.3",
|
|
17
|
+
"litestar-vite-plugin": "^0.6.2",
|
|
18
|
+
"@types/node": "^20.14.10",
|
|
20
19
|
}
|
|
21
|
-
DEFAULT_DEPENDENCIES: dict[str, str] = {"axios": "^1.
|
|
20
|
+
DEFAULT_DEPENDENCIES: dict[str, str] = {"axios": "^1.7.2"}
|
|
22
21
|
|
|
23
22
|
|
|
24
23
|
def to_json(value: Any) -> str:
|
|
@@ -50,10 +49,11 @@ def init_vite(
|
|
|
50
49
|
"""Initialize a new vite project."""
|
|
51
50
|
from jinja2 import Environment, FileSystemLoader, select_autoescape
|
|
52
51
|
from litestar.cli._utils import console
|
|
52
|
+
from litestar.utils import module_loader
|
|
53
53
|
|
|
54
|
-
|
|
54
|
+
template_path = module_loader.module_to_os_path("litestar_vite.templates")
|
|
55
55
|
vite_template_env = Environment(
|
|
56
|
-
loader=FileSystemLoader([
|
|
56
|
+
loader=FileSystemLoader([template_path]),
|
|
57
57
|
autoescape=select_autoescape(),
|
|
58
58
|
)
|
|
59
59
|
|
|
@@ -65,25 +65,24 @@ def init_vite(
|
|
|
65
65
|
template_name: get_template(environment=vite_template_env, name=template_name)
|
|
66
66
|
for template_name in enabled_templates
|
|
67
67
|
}
|
|
68
|
-
entry_point = [
|
|
69
|
-
str(Path(resource_path / resource_name).relative_to(Path.cwd().absolute()))
|
|
70
|
-
for resource_name in enabled_resources
|
|
71
|
-
]
|
|
72
68
|
for template_name, template in templates.items():
|
|
73
|
-
target_file_name = template_name.
|
|
69
|
+
target_file_name = template_name[:-3] if template_name.endswith(".j2") else template_name
|
|
74
70
|
with Path(target_file_name).open(mode="w") as file:
|
|
75
|
-
console.print(f" * Writing {target_file_name} to {Path(target_file_name)
|
|
71
|
+
console.print(f" * Writing {target_file_name} to {Path(target_file_name)!s}")
|
|
76
72
|
|
|
77
73
|
file.write(
|
|
78
74
|
template.render(
|
|
79
|
-
entry_point=
|
|
75
|
+
entry_point=[
|
|
76
|
+
f"{resource_path!s}/{resource_name[:-3] if resource_name.endswith('.j2') else resource_name}"
|
|
77
|
+
for resource_name in enabled_resources
|
|
78
|
+
],
|
|
80
79
|
enable_ssr=enable_ssr,
|
|
81
80
|
asset_url=asset_url,
|
|
82
|
-
root_path=
|
|
81
|
+
root_path=root_path,
|
|
83
82
|
resource_path=str(resource_path.relative_to(root_path)),
|
|
84
83
|
public_path=str(public_path.relative_to(root_path)),
|
|
85
84
|
bundle_path=str(bundle_path.relative_to(root_path)),
|
|
86
|
-
hot_file=str(hot_file.relative_to(
|
|
85
|
+
hot_file=str(hot_file.relative_to(root_path)),
|
|
87
86
|
vite_port=str(vite_port),
|
|
88
87
|
litestar_port=litestar_port,
|
|
89
88
|
dependencies=to_json(dependencies),
|
|
@@ -92,8 +91,13 @@ def init_vite(
|
|
|
92
91
|
)
|
|
93
92
|
|
|
94
93
|
for resource_name in enabled_resources:
|
|
95
|
-
|
|
96
|
-
|
|
94
|
+
template = get_template(environment=vite_template_env, name=resource_name)
|
|
95
|
+
target_file_name = f"{resource_path}/{resource_name[:-3] if resource_name.endswith('.j2') else resource_name}"
|
|
96
|
+
with Path(target_file_name).open(mode="w") as file:
|
|
97
|
+
console.print(
|
|
98
|
+
f" * Writing {resource_name[:-3] if resource_name.endswith('.j2') else resource_name} to {Path(target_file_name)!s}",
|
|
99
|
+
)
|
|
100
|
+
file.write(template.render())
|
|
97
101
|
console.print("[yellow]Vite initialization completed.[/]")
|
|
98
102
|
|
|
99
103
|
|
litestar_vite/config.py
CHANGED
|
@@ -5,22 +5,18 @@ 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
|
-
|
|
23
|
-
T = TypeVar("T", bound=TemplateEngineProtocol)
|
|
18
|
+
__all__ = ("ViteConfig", "ViteTemplateConfig")
|
|
19
|
+
EngineType = TypeVar("EngineType", bound=TemplateEngineProtocol[Any, Any])
|
|
24
20
|
|
|
25
21
|
|
|
26
22
|
@dataclass
|
|
@@ -42,7 +38,7 @@ class ViteConfig:
|
|
|
42
38
|
|
|
43
39
|
In a standalone Vue or React application, this would be equivalent to the ``./src`` directory.
|
|
44
40
|
"""
|
|
45
|
-
template_dir: Path | str | None = field(default=
|
|
41
|
+
template_dir: Path | str | None = field(default="templates")
|
|
46
42
|
"""Location of the Jinja2 template file."""
|
|
47
43
|
public_dir: Path | str = field(default="public")
|
|
48
44
|
"""The optional public directory Vite serves assets from.
|
|
@@ -116,7 +112,7 @@ class ViteConfig:
|
|
|
116
112
|
self.root_dir = Path(self.root_dir)
|
|
117
113
|
if self.template_dir is not None and isinstance(self.template_dir, str):
|
|
118
114
|
self.template_dir = Path(self.template_dir)
|
|
119
|
-
if self.public_dir
|
|
115
|
+
if self.public_dir and isinstance(self.public_dir, str):
|
|
120
116
|
self.public_dir = Path(self.public_dir)
|
|
121
117
|
if isinstance(self.resource_dir, str):
|
|
122
118
|
self.resource_dir = Path(self.resource_dir)
|
|
@@ -127,7 +123,7 @@ class ViteConfig:
|
|
|
127
123
|
|
|
128
124
|
|
|
129
125
|
@dataclass
|
|
130
|
-
class ViteTemplateConfig(
|
|
126
|
+
class ViteTemplateConfig(TemplateConfig[EngineType]):
|
|
131
127
|
"""Configuration for Templating.
|
|
132
128
|
|
|
133
129
|
To enable templating, pass an instance of this class to the
|
|
@@ -135,34 +131,43 @@ class ViteTemplateConfig(Generic[T]):
|
|
|
135
131
|
'template_config' key.
|
|
136
132
|
"""
|
|
137
133
|
|
|
138
|
-
|
|
134
|
+
config: ViteConfig = field(default_factory=lambda: ViteConfig())
|
|
135
|
+
"""A a config for the vite engine`."""
|
|
136
|
+
engine: type[EngineType] | EngineType | None = field(default=None)
|
|
139
137
|
"""A template engine adhering to the :class:`TemplateEngineProtocol
|
|
140
138
|
<litestar.template.base.TemplateEngineProtocol>`."""
|
|
141
|
-
|
|
142
|
-
"""A a config for the vite engine`."""
|
|
143
|
-
directory: PathType | None = field(default=None)
|
|
139
|
+
directory: PathType | list[PathType] | None = field(default=None)
|
|
144
140
|
"""A directory or list of directories from which to serve templates."""
|
|
145
|
-
engine_callback: Callable[[
|
|
141
|
+
engine_callback: Callable[[EngineType], None] | None = field(default=None)
|
|
146
142
|
"""A callback function that allows modifying the instantiated templating
|
|
147
143
|
protocol."""
|
|
148
144
|
|
|
145
|
+
instance: EngineType | None = field(default=None)
|
|
146
|
+
"""An instance of the templating protocol."""
|
|
147
|
+
|
|
149
148
|
def __post_init__(self) -> None:
|
|
150
149
|
"""Ensure that directory is set if engine is a class."""
|
|
151
|
-
if isclass(self.engine) and not self.directory:
|
|
150
|
+
if isclass(self.engine) and not self.directory: # pyright: ignore[reportUnknownMemberType]
|
|
152
151
|
msg = "directory is a required kwarg when passing a template engine class"
|
|
153
152
|
raise ImproperlyConfiguredException(msg)
|
|
153
|
+
"""Ensure that directory is not set if instance is."""
|
|
154
|
+
if self.instance is not None and self.directory is not None: # pyright: ignore[reportUnknownMemberType]
|
|
155
|
+
msg = "directory cannot be set if instance is"
|
|
156
|
+
raise ImproperlyConfiguredException(msg)
|
|
154
157
|
|
|
155
|
-
def to_engine(self) ->
|
|
158
|
+
def to_engine(self) -> EngineType:
|
|
156
159
|
"""Instantiate the template engine."""
|
|
157
160
|
template_engine = cast(
|
|
158
|
-
"
|
|
159
|
-
self.engine(directory=self.directory, config=self.config
|
|
161
|
+
"EngineType",
|
|
162
|
+
self.engine(directory=self.directory, config=self.config, engine_instance=None) # pyright: ignore[reportUnknownMemberType,reportCallIssue]
|
|
163
|
+
if isclass(self.engine)
|
|
164
|
+
else self.engine,
|
|
160
165
|
)
|
|
161
166
|
if callable(self.engine_callback):
|
|
162
167
|
self.engine_callback(template_engine)
|
|
163
168
|
return template_engine
|
|
164
169
|
|
|
165
170
|
@cached_property
|
|
166
|
-
def engine_instance(self) ->
|
|
171
|
+
def engine_instance(self) -> EngineType:
|
|
167
172
|
"""Return the template engine instance."""
|
|
168
|
-
return self.to_engine()
|
|
173
|
+
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,91 @@
|
|
|
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
|
+
NotFoundException,
|
|
11
|
+
PermissionDeniedException,
|
|
12
|
+
)
|
|
13
|
+
from litestar.exceptions.responses import (
|
|
14
|
+
create_debug_response, # pyright: ignore[reportUnknownVariableType]
|
|
15
|
+
create_exception_response, # pyright: ignore[reportUnknownVariableType]
|
|
16
|
+
)
|
|
17
|
+
from litestar.plugins.flash import flash
|
|
18
|
+
from litestar.repository.exceptions import (
|
|
19
|
+
NotFoundError, # pyright: ignore[reportUnknownVariableType,reportAttributeAccessIssue]
|
|
20
|
+
RepositoryError, # pyright: ignore[reportUnknownVariableType,reportAttributeAccessIssue]
|
|
21
|
+
)
|
|
22
|
+
from litestar.response import Response
|
|
23
|
+
from litestar.status_codes import (
|
|
24
|
+
HTTP_400_BAD_REQUEST,
|
|
25
|
+
HTTP_401_UNAUTHORIZED,
|
|
26
|
+
HTTP_409_CONFLICT,
|
|
27
|
+
HTTP_422_UNPROCESSABLE_ENTITY,
|
|
28
|
+
HTTP_500_INTERNAL_SERVER_ERROR,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
from litestar_vite.inertia.response import InertiaBack, InertiaRedirect, InertiaResponse, error
|
|
32
|
+
|
|
33
|
+
if TYPE_CHECKING:
|
|
34
|
+
from litestar_vite.inertia.plugin import InertiaPlugin
|
|
35
|
+
|
|
36
|
+
FIELD_ERR_RE = re.compile(r"field `(.+)`$")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class _HTTPConflictException(HTTPException):
|
|
40
|
+
"""Request conflict with the current state of the target resource."""
|
|
41
|
+
|
|
42
|
+
status_code = HTTP_409_CONFLICT
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def exception_to_http_response(request: Request[UserT, AuthT, StateT], exc: Exception) -> Response[Any]:
|
|
46
|
+
"""Handler for all exceptions subclassed from HTTPException."""
|
|
47
|
+
inertia_enabled = getattr(request, "inertia_enabled", False) or getattr(request, "is_inertia", False)
|
|
48
|
+
if isinstance(exc, NotFoundError):
|
|
49
|
+
http_exc = NotFoundException
|
|
50
|
+
elif isinstance(exc, RepositoryError):
|
|
51
|
+
http_exc = _HTTPConflictException # type: ignore[assignment]
|
|
52
|
+
else:
|
|
53
|
+
http_exc = InternalServerException # type: ignore[assignment]
|
|
54
|
+
if not inertia_enabled:
|
|
55
|
+
if request.app.debug and http_exc not in (PermissionDeniedException, NotFoundError):
|
|
56
|
+
return cast("Response[Any]", create_debug_response(request, exc))
|
|
57
|
+
return cast("Response[Any]", create_exception_response(request, http_exc(detail=str(exc.__cause__))))
|
|
58
|
+
is_inertia = getattr(request, "is_inertia", False)
|
|
59
|
+
status_code = getattr(exc, "status_code", HTTP_500_INTERNAL_SERVER_ERROR)
|
|
60
|
+
preferred_type = MediaType.HTML if inertia_enabled and not is_inertia else MediaType.JSON
|
|
61
|
+
detail = getattr(exc, "detail", "") # litestar exceptions
|
|
62
|
+
extras = getattr(exc, "extra", "") # msgspec exceptions
|
|
63
|
+
content = {"status_code": status_code, "message": getattr(exc, "detail", "")}
|
|
64
|
+
inertia_plugin = cast("InertiaPlugin", request.app.plugins.get("InertiaPlugin"))
|
|
65
|
+
if extras:
|
|
66
|
+
content.update({"extra": extras})
|
|
67
|
+
flash(request, detail, category="error")
|
|
68
|
+
if extras and len(extras) >= 1:
|
|
69
|
+
message = extras[0]
|
|
70
|
+
default_field = f"root.{message.get('key')}" if message.get("key", None) is not None else "root" # type: ignore
|
|
71
|
+
error_detail = cast("str", message.get("message", detail)) # type: ignore[union-attr] # pyright: ignore[reportUnknownMemberType,reportUnknownArgumentType]
|
|
72
|
+
match = FIELD_ERR_RE.search(error_detail)
|
|
73
|
+
field = match.group(1) if match else default_field
|
|
74
|
+
if isinstance(message, dict):
|
|
75
|
+
error(request, field, error_detail)
|
|
76
|
+
if status_code in {HTTP_422_UNPROCESSABLE_ENTITY, HTTP_400_BAD_REQUEST}:
|
|
77
|
+
return InertiaBack(request)
|
|
78
|
+
if (
|
|
79
|
+
status_code == HTTP_401_UNAUTHORIZED
|
|
80
|
+
and inertia_plugin.config.redirect_unauthorized_to is not None
|
|
81
|
+
and not request.url.path.startswith(inertia_plugin.config.redirect_unauthorized_to)
|
|
82
|
+
):
|
|
83
|
+
return InertiaRedirect(
|
|
84
|
+
request,
|
|
85
|
+
redirect_to=inertia_plugin.config.redirect_unauthorized_to,
|
|
86
|
+
)
|
|
87
|
+
return InertiaResponse[Any](
|
|
88
|
+
media_type=preferred_type,
|
|
89
|
+
content=content,
|
|
90
|
+
status_code=status_code,
|
|
91
|
+
)
|
|
@@ -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
|