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 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.0.6",
18
- "litestar-vite-plugin": "^0.5.1",
19
- "@types/node": "^20.10.3",
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.6.2"}
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
- entry_point: list[str] = []
54
+ template_path = module_loader.module_to_os_path("litestar_vite.templates")
55
55
  vite_template_env = Environment(
56
- loader=FileSystemLoader([VITE_INIT_TEMPLATES_PATH]),
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.removesuffix(".j2")
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).absolute()}")
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=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=str(root_path.relative_to(Path.cwd().absolute())),
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(Path.cwd().absolute())),
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
- with Path(resource_path / resource_name).open(mode="w") as file:
96
- console.print(f" * Writing {resource_name} to {Path(resource_path / resource_name).absolute()}")
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, 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
- from litestar_vite.template_engine import ViteTemplateEngine
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=None)
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 is not None and isinstance(self.public_dir, str):
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(Generic[T]):
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
- engine: type[ViteTemplateEngine]
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
- config: ViteConfig
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[[T], None] | None = field(default=None)
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) -> T:
158
+ def to_engine(self) -> EngineType:
156
159
  """Instantiate the template engine."""
157
160
  template_engine = cast(
158
- "T",
159
- self.engine(directory=self.directory, config=self.config) if isclass(self.engine) else self.engine,
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) -> T:
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