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 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.0.6",
17
- "litestar-vite-plugin": "^0.5.1",
18
- "@types/node": "^20.10.3",
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.6.2"}
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.removesuffix(".j2")
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.removesuffix('.j2')}" for resource_name in enabled_resources
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.removesuffix('.j2')}"
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(f" * Writing {resource_name.removesuffix('.j2')} to {Path(target_file_name)!s}")
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, Generic, TypeVar, cast
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=None)
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 {"True", "1", "yes", "Y", "T"},
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 {"True", "1", "yes", "Y", "T"},
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 {"True", "1", "yes", "Y", "T"},
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 is not None and isinstance(self.public_dir, str):
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(Generic[EngineType]):
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) if isclass(self.engine) else self.engine,
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