litestar-vite 0.1.1__py3-none-any.whl → 0.15.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.
Files changed (169) hide show
  1. litestar_vite/__init__.py +54 -4
  2. litestar_vite/__metadata__.py +12 -7
  3. litestar_vite/cli.py +1048 -10
  4. litestar_vite/codegen/__init__.py +48 -0
  5. litestar_vite/codegen/_export.py +229 -0
  6. litestar_vite/codegen/_inertia.py +619 -0
  7. litestar_vite/codegen/_openapi.py +280 -0
  8. litestar_vite/codegen/_routes.py +720 -0
  9. litestar_vite/codegen/_ts.py +235 -0
  10. litestar_vite/codegen/_utils.py +141 -0
  11. litestar_vite/commands.py +73 -0
  12. litestar_vite/config/__init__.py +997 -0
  13. litestar_vite/config/_constants.py +97 -0
  14. litestar_vite/config/_deploy.py +70 -0
  15. litestar_vite/config/_inertia.py +241 -0
  16. litestar_vite/config/_paths.py +63 -0
  17. litestar_vite/config/_runtime.py +235 -0
  18. litestar_vite/config/_spa.py +93 -0
  19. litestar_vite/config/_types.py +94 -0
  20. litestar_vite/deploy.py +366 -0
  21. litestar_vite/doctor.py +1181 -0
  22. litestar_vite/exceptions.py +78 -0
  23. litestar_vite/executor.py +360 -0
  24. litestar_vite/handler/__init__.py +9 -0
  25. litestar_vite/handler/_app.py +612 -0
  26. litestar_vite/handler/_routing.py +130 -0
  27. litestar_vite/html_transform.py +569 -0
  28. litestar_vite/inertia/__init__.py +77 -0
  29. litestar_vite/inertia/_utils.py +119 -0
  30. litestar_vite/inertia/exception_handler.py +178 -0
  31. litestar_vite/inertia/helpers.py +1571 -0
  32. litestar_vite/inertia/middleware.py +54 -0
  33. litestar_vite/inertia/plugin.py +199 -0
  34. litestar_vite/inertia/precognition.py +274 -0
  35. litestar_vite/inertia/request.py +334 -0
  36. litestar_vite/inertia/response.py +802 -0
  37. litestar_vite/inertia/types.py +335 -0
  38. litestar_vite/loader.py +464 -123
  39. litestar_vite/plugin/__init__.py +687 -0
  40. litestar_vite/plugin/_process.py +185 -0
  41. litestar_vite/plugin/_proxy.py +689 -0
  42. litestar_vite/plugin/_proxy_headers.py +244 -0
  43. litestar_vite/plugin/_static.py +37 -0
  44. litestar_vite/plugin/_utils.py +489 -0
  45. litestar_vite/py.typed +0 -0
  46. litestar_vite/scaffolding/__init__.py +20 -0
  47. litestar_vite/scaffolding/generator.py +270 -0
  48. litestar_vite/scaffolding/templates.py +437 -0
  49. litestar_vite/templates/__init__.py +0 -0
  50. litestar_vite/templates/addons/tailwindcss/tailwind.css.j2 +1 -0
  51. litestar_vite/templates/angular/index.html.j2 +12 -0
  52. litestar_vite/templates/angular/openapi-ts.config.ts.j2 +18 -0
  53. litestar_vite/templates/angular/package.json.j2 +36 -0
  54. litestar_vite/templates/angular/src/app/app.component.css.j2 +3 -0
  55. litestar_vite/templates/angular/src/app/app.component.html.j2 +1 -0
  56. litestar_vite/templates/angular/src/app/app.component.ts.j2 +9 -0
  57. litestar_vite/templates/angular/src/app/app.config.ts.j2 +5 -0
  58. litestar_vite/templates/angular/src/main.ts.j2 +9 -0
  59. litestar_vite/templates/angular/src/styles.css.j2 +9 -0
  60. litestar_vite/templates/angular/tsconfig.app.json.j2 +34 -0
  61. litestar_vite/templates/angular/tsconfig.json.j2 +20 -0
  62. litestar_vite/templates/angular/vite.config.ts.j2 +21 -0
  63. litestar_vite/templates/angular-cli/.postcssrc.json.j2 +5 -0
  64. litestar_vite/templates/angular-cli/angular.json.j2 +36 -0
  65. litestar_vite/templates/angular-cli/openapi-ts.config.ts.j2 +18 -0
  66. litestar_vite/templates/angular-cli/package.json.j2 +28 -0
  67. litestar_vite/templates/angular-cli/proxy.conf.json.j2 +18 -0
  68. litestar_vite/templates/angular-cli/src/app/app.component.css.j2 +3 -0
  69. litestar_vite/templates/angular-cli/src/app/app.component.html.j2 +1 -0
  70. litestar_vite/templates/angular-cli/src/app/app.component.ts.j2 +9 -0
  71. litestar_vite/templates/angular-cli/src/app/app.config.ts.j2 +5 -0
  72. litestar_vite/templates/angular-cli/src/index.html.j2 +13 -0
  73. litestar_vite/templates/angular-cli/src/main.ts.j2 +6 -0
  74. litestar_vite/templates/angular-cli/src/styles.css.j2 +10 -0
  75. litestar_vite/templates/angular-cli/tailwind.config.js.j2 +4 -0
  76. litestar_vite/templates/angular-cli/tsconfig.app.json.j2 +16 -0
  77. litestar_vite/templates/angular-cli/tsconfig.json.j2 +26 -0
  78. litestar_vite/templates/angular-cli/tsconfig.spec.json.j2 +9 -0
  79. litestar_vite/templates/astro/astro.config.mjs.j2 +28 -0
  80. litestar_vite/templates/astro/openapi-ts.config.ts.j2 +15 -0
  81. litestar_vite/templates/astro/src/layouts/Layout.astro.j2 +63 -0
  82. litestar_vite/templates/astro/src/pages/index.astro.j2 +36 -0
  83. litestar_vite/templates/astro/src/styles/global.css.j2 +1 -0
  84. litestar_vite/templates/base/.gitignore.j2 +42 -0
  85. litestar_vite/templates/base/openapi-ts.config.ts.j2 +15 -0
  86. litestar_vite/templates/base/package.json.j2 +39 -0
  87. litestar_vite/templates/base/resources/vite-env.d.ts.j2 +1 -0
  88. litestar_vite/templates/base/tsconfig.json.j2 +37 -0
  89. litestar_vite/templates/htmx/src/main.js.j2 +8 -0
  90. litestar_vite/templates/htmx/templates/base.html.j2.j2 +56 -0
  91. litestar_vite/templates/htmx/templates/index.html.j2.j2 +13 -0
  92. litestar_vite/templates/htmx/vite.config.ts.j2 +40 -0
  93. litestar_vite/templates/nuxt/app.vue.j2 +29 -0
  94. litestar_vite/templates/nuxt/composables/useApi.ts.j2 +33 -0
  95. litestar_vite/templates/nuxt/nuxt.config.ts.j2 +31 -0
  96. litestar_vite/templates/nuxt/openapi-ts.config.ts.j2 +15 -0
  97. litestar_vite/templates/nuxt/pages/index.vue.j2 +54 -0
  98. litestar_vite/templates/react/index.html.j2 +13 -0
  99. litestar_vite/templates/react/src/App.css.j2 +56 -0
  100. litestar_vite/templates/react/src/App.tsx.j2 +19 -0
  101. litestar_vite/templates/react/src/main.tsx.j2 +10 -0
  102. litestar_vite/templates/react/vite.config.ts.j2 +39 -0
  103. litestar_vite/templates/react-inertia/index.html.j2 +14 -0
  104. litestar_vite/templates/react-inertia/package.json.j2 +47 -0
  105. litestar_vite/templates/react-inertia/resources/App.css.j2 +68 -0
  106. litestar_vite/templates/react-inertia/resources/main.tsx.j2 +17 -0
  107. litestar_vite/templates/react-inertia/resources/pages/Home.tsx.j2 +18 -0
  108. litestar_vite/templates/react-inertia/resources/ssr.tsx.j2 +19 -0
  109. litestar_vite/templates/react-inertia/vite.config.ts.j2 +59 -0
  110. litestar_vite/templates/react-router/index.html.j2 +12 -0
  111. litestar_vite/templates/react-router/src/App.css.j2 +17 -0
  112. litestar_vite/templates/react-router/src/App.tsx.j2 +7 -0
  113. litestar_vite/templates/react-router/src/main.tsx.j2 +10 -0
  114. litestar_vite/templates/react-router/vite.config.ts.j2 +39 -0
  115. litestar_vite/templates/react-tanstack/index.html.j2 +12 -0
  116. litestar_vite/templates/react-tanstack/openapi-ts.config.ts.j2 +18 -0
  117. litestar_vite/templates/react-tanstack/src/App.css.j2 +17 -0
  118. litestar_vite/templates/react-tanstack/src/main.tsx.j2 +21 -0
  119. litestar_vite/templates/react-tanstack/src/routeTree.gen.ts.j2 +7 -0
  120. litestar_vite/templates/react-tanstack/src/routes/__root.tsx.j2 +9 -0
  121. litestar_vite/templates/react-tanstack/src/routes/books.tsx.j2 +9 -0
  122. litestar_vite/templates/react-tanstack/src/routes/index.tsx.j2 +9 -0
  123. litestar_vite/templates/react-tanstack/vite.config.ts.j2 +39 -0
  124. litestar_vite/templates/svelte/index.html.j2 +13 -0
  125. litestar_vite/templates/svelte/src/App.svelte.j2 +30 -0
  126. litestar_vite/templates/svelte/src/app.css.j2 +45 -0
  127. litestar_vite/templates/svelte/src/main.ts.j2 +8 -0
  128. litestar_vite/templates/svelte/src/vite-env.d.ts.j2 +2 -0
  129. litestar_vite/templates/svelte/svelte.config.js.j2 +5 -0
  130. litestar_vite/templates/svelte/vite.config.ts.j2 +39 -0
  131. litestar_vite/templates/svelte-inertia/index.html.j2 +14 -0
  132. litestar_vite/templates/svelte-inertia/resources/app.css.j2 +21 -0
  133. litestar_vite/templates/svelte-inertia/resources/main.ts.j2 +11 -0
  134. litestar_vite/templates/svelte-inertia/resources/pages/Home.svelte.j2 +43 -0
  135. litestar_vite/templates/svelte-inertia/resources/vite-env.d.ts.j2 +2 -0
  136. litestar_vite/templates/svelte-inertia/svelte.config.js.j2 +5 -0
  137. litestar_vite/templates/svelte-inertia/vite.config.ts.j2 +37 -0
  138. litestar_vite/templates/sveltekit/openapi-ts.config.ts.j2 +15 -0
  139. litestar_vite/templates/sveltekit/src/app.css.j2 +40 -0
  140. litestar_vite/templates/sveltekit/src/app.html.j2 +12 -0
  141. litestar_vite/templates/sveltekit/src/hooks.server.ts.j2 +55 -0
  142. litestar_vite/templates/sveltekit/src/routes/+layout.svelte.j2 +12 -0
  143. litestar_vite/templates/sveltekit/src/routes/+page.svelte.j2 +34 -0
  144. litestar_vite/templates/sveltekit/svelte.config.js.j2 +12 -0
  145. litestar_vite/templates/sveltekit/tsconfig.json.j2 +14 -0
  146. litestar_vite/templates/sveltekit/vite.config.ts.j2 +31 -0
  147. litestar_vite/templates/vue/env.d.ts.j2 +7 -0
  148. litestar_vite/templates/vue/index.html.j2 +13 -0
  149. litestar_vite/templates/vue/src/App.vue.j2 +28 -0
  150. litestar_vite/templates/vue/src/main.ts.j2 +5 -0
  151. litestar_vite/templates/vue/src/style.css.j2 +45 -0
  152. litestar_vite/templates/vue/vite.config.ts.j2 +39 -0
  153. litestar_vite/templates/vue-inertia/env.d.ts.j2 +7 -0
  154. litestar_vite/templates/vue-inertia/index.html.j2 +14 -0
  155. litestar_vite/templates/vue-inertia/package.json.j2 +50 -0
  156. litestar_vite/templates/vue-inertia/resources/main.ts.j2 +18 -0
  157. litestar_vite/templates/vue-inertia/resources/pages/Home.vue.j2 +22 -0
  158. litestar_vite/templates/vue-inertia/resources/ssr.ts.j2 +21 -0
  159. litestar_vite/templates/vue-inertia/resources/style.css.j2 +21 -0
  160. litestar_vite/templates/vue-inertia/vite.config.ts.j2 +59 -0
  161. litestar_vite-0.15.0.dist-info/METADATA +230 -0
  162. litestar_vite-0.15.0.dist-info/RECORD +164 -0
  163. {litestar_vite-0.1.1.dist-info → litestar_vite-0.15.0.dist-info}/WHEEL +1 -1
  164. litestar_vite/config.py +0 -100
  165. litestar_vite/plugin.py +0 -45
  166. litestar_vite/template_engine.py +0 -103
  167. litestar_vite-0.1.1.dist-info/METADATA +0 -68
  168. litestar_vite-0.1.1.dist-info/RECORD +0 -11
  169. {litestar_vite-0.1.1.dist-info → litestar_vite-0.15.0.dist-info}/licenses/LICENSE +0 -0
litestar_vite/loader.py CHANGED
@@ -1,193 +1,534 @@
1
- from __future__ import annotations
2
-
3
- import json
1
+ """Vite Asset Loader.
2
+
3
+ This module provides the ViteAssetLoader class for loading and rendering
4
+ Vite-managed assets. The loader handles both development mode (with HMR)
5
+ and production mode (with manifest-based asset resolution).
6
+
7
+ Key features:
8
+ - Async initialization for non-blocking I/O during app startup
9
+ - Manifest parsing for production asset resolution
10
+ - HMR client script generation for development
11
+ - React Fast Refresh support
12
+ """
13
+
14
+ import hashlib
15
+ import html
16
+ from functools import cached_property
4
17
  from pathlib import Path
5
- from typing import TYPE_CHECKING, Any, ClassVar, TypeVar
18
+ from textwrap import dedent
19
+ from typing import TYPE_CHECKING, Any
6
20
  from urllib.parse import urljoin
7
21
 
8
- from litestar.template import TemplateEngineProtocol
22
+ import anyio
23
+ import markupsafe
24
+ from litestar.exceptions import SerializationException
25
+ from litestar.serialization import decode_json
26
+
27
+ from litestar_vite.exceptions import AssetNotFoundError, ManifestNotFoundError
9
28
 
10
29
  if TYPE_CHECKING:
30
+ from collections.abc import Mapping
31
+
32
+ from litestar.connection import Request
33
+
11
34
  from litestar_vite.config import ViteConfig
35
+ from litestar_vite.plugin import VitePlugin
36
+
37
+
38
+ def _get_request_from_context(context: "Mapping[str, Any]") -> "Request[Any, Any, Any]":
39
+ """Get the request from the template context.
40
+
41
+ Args:
42
+ context: The template context.
43
+
44
+ Returns:
45
+ The request object from the template context.
46
+
47
+ Raises:
48
+ ValueError: If 'request' is not found in the template context.
49
+ TypeError: If 'request' is not a Litestar Request object.
50
+ """
51
+ from litestar.connection import Request
52
+
53
+ request = context.get("request")
54
+ if request is None:
55
+ msg = "Request not found in template context. Ensure 'request' is passed to the template."
56
+ raise ValueError(msg)
57
+ if not isinstance(request, Request): # pyright: ignore[reportUnknownVariableType]
58
+ msg = f"Expected Request object, got {type(request)}"
59
+ raise TypeError(msg)
60
+ return request # pyright: ignore[reportReturnType,reportUnknownVariableType]
61
+
62
+
63
+ def _get_vite_plugin(context: "Mapping[str, Any]") -> "VitePlugin | None":
64
+ """Return the VitePlugin from the template context, if registered.
65
+
66
+ Returns:
67
+ The VitePlugin instance, or None if not registered.
68
+ """
69
+ request = _get_request_from_context(context)
70
+ return request.app.plugins.get("VitePlugin")
71
+
72
+
73
+ def render_hmr_client(context: "Mapping[str, Any]", /) -> "markupsafe.Markup":
74
+ """Render the HMR client script tag.
75
+
76
+ This is a Jinja2 template callable that renders the Vite HMR client
77
+ script tag for development mode.
78
+
79
+ Args:
80
+ context: The template context containing the request.
81
+
82
+ Returns:
83
+ HTML markup for the HMR client script, or empty markup if
84
+ VitePlugin is not registered.
85
+ """
86
+ vite_plugin = _get_vite_plugin(context)
87
+ if vite_plugin is None:
88
+ return markupsafe.Markup("")
89
+ return vite_plugin.asset_loader.render_hmr_client()
90
+
91
+
92
+ def render_asset_tag(
93
+ context: "Mapping[str, Any]", /, path: "str | list[str]", scripts_attrs: "dict[str, str] | None" = None
94
+ ) -> "markupsafe.Markup":
95
+ """Render asset tags for the specified path(s).
96
+
97
+ This is a Jinja2 template callable that renders script/link tags
98
+ for Vite-managed assets. Also works for HTMX partial responses.
99
+
100
+ Args:
101
+ context: The template context containing the request.
102
+ path: Single path or list of paths to assets.
103
+ scripts_attrs: Optional attributes for script tags.
104
+
105
+ Returns:
106
+ HTML markup for the asset tags, or empty markup if VitePlugin
107
+ is not registered.
108
+
109
+ Example:
110
+ In a Jinja2 template:
111
+ {{ vite_asset("src/main.ts") }}
112
+ {{ vite_asset("src/components/UserProfile.tsx") }}
113
+ """
114
+ vite_plugin = _get_vite_plugin(context)
115
+ if vite_plugin is None:
116
+ return markupsafe.Markup("")
117
+ return vite_plugin.asset_loader.render_asset_tag(path, scripts_attrs)
118
+
119
+
120
+ def render_static_asset(context: "Mapping[str, Any]", /, path: str) -> str:
121
+ """Render a static asset URL.
122
+
123
+ This is a Jinja2 template callable that returns the URL for a static asset.
12
124
 
13
- T = TypeVar("T", bound=TemplateEngineProtocol)
125
+ Args:
126
+ context: The template context containing the request.
127
+ path: Path to the static asset.
128
+
129
+ Returns:
130
+ The full URL to the static asset, or empty string if VitePlugin
131
+ is not registered.
132
+ """
133
+ vite_plugin = _get_vite_plugin(context)
134
+ if vite_plugin is None:
135
+ return ""
136
+ return vite_plugin.asset_loader.get_static_asset(path)
137
+
138
+
139
+ def render_routes(
140
+ context: "Mapping[str, Any]",
141
+ /,
142
+ *,
143
+ only: "list[str] | None" = None,
144
+ exclude: "list[str] | None" = None,
145
+ include_components: bool = False,
146
+ ) -> "markupsafe.Markup":
147
+ """Render inline script tag with route definitions.
148
+
149
+ This is a Jinja2 template callable that renders an inline script tag
150
+ containing route metadata for client-side type-safe routing.
151
+
152
+ The script defines a global `window.Litestar.routes` object that can be
153
+ used by frontend routers.
154
+
155
+ Uses Litestar's built-in serializers, picking up any custom type encoders
156
+ configured on the app.
157
+
158
+ Args:
159
+ context: The template context containing the request.
160
+ only: Optional list of route patterns to include.
161
+ exclude: Optional list of route patterns to exclude.
162
+ include_components: Include Inertia component names.
163
+
164
+ Returns:
165
+ HTML markup for the inline routes script containing route metadata
166
+ as a JSON object.
167
+
168
+ Example:
169
+ In a Jinja2 template:
170
+ {{ vite_routes() }}
171
+ {{ vite_routes(exclude=['/api/internal']) }}
172
+ """
173
+ from litestar.serialization import encode_json, get_serializer
174
+
175
+ from litestar_vite.codegen import generate_routes_json
176
+
177
+ request = _get_request_from_context(context)
178
+ app = request.app
179
+
180
+ routes_data = generate_routes_json(app, only=only, exclude=exclude, include_components=include_components)
181
+
182
+ serializer = get_serializer(app.type_encoders)
183
+ routes_json = encode_json(routes_data, serializer=serializer).decode("utf-8")
184
+
185
+ script = dedent(f"""\
186
+ <script type="text/javascript">
187
+ (function() {{
188
+ window.Litestar = window.Litestar || {{}};
189
+ window.Litestar.routes = {routes_json};
190
+ }})();
191
+ </script>""")
192
+
193
+ return markupsafe.Markup(script)
14
194
 
15
195
 
16
196
  class ViteAssetLoader:
17
- """Vite manifest loader.
197
+ """Vite asset loader for managing frontend assets.
198
+
199
+ This class handles loading and rendering of Vite-managed assets.
200
+ It supports both development mode (with HMR) and production mode
201
+ (with manifest-based asset resolution).
202
+
203
+ The loader is designed to be instantiated per-app (not a singleton)
204
+ and supports async initialization for non-blocking file I/O.
18
205
 
19
- Please see: https://vitejs.dev/guide/backend-integration.html
206
+ Attributes:
207
+ config: The Vite configuration.
208
+
209
+ Example:
210
+ loader = ViteAssetLoader(config)
211
+ await loader.initialize()
212
+ html = loader.render_asset_tag("src/main.ts")
20
213
  """
21
214
 
22
- _instance: ClassVar[ViteAssetLoader | None] = None
215
+ def __init__(self, config: "ViteConfig") -> None:
216
+ """Initialize the asset loader.
23
217
 
24
- def __init__(self, config: ViteConfig) -> None:
218
+ Args:
219
+ config: The Vite configuration.
220
+ """
25
221
  self._config = config
26
222
  self._manifest: dict[str, Any] = {}
223
+ self._manifest_content: str = ""
224
+ self._vite_base_path: "str | None" = None
225
+ self._initialized: bool = False
226
+ self._is_hot_dev = self._config.hot_reload and self._config.is_dev_mode
27
227
 
28
228
  @classmethod
29
- def initialize_loader(cls, config: ViteConfig) -> ViteAssetLoader:
30
- """Singleton manifest loader."""
31
- if cls._instance is None:
32
- cls._instance = cls(config=config)
33
- cls._instance.parse_manifest()
34
- return cls._instance
229
+ def initialize_loader(cls, config: "ViteConfig") -> "ViteAssetLoader":
230
+ """Synchronously initialize a loader instance.
231
+
232
+ This is a convenience method for synchronous initialization.
233
+ For async contexts, prefer using `initialize()` after construction.
234
+
235
+ Args:
236
+ config: The Vite configuration.
237
+
238
+ Returns:
239
+ An initialized ViteAssetLoader instance.
240
+ """
241
+ loader = cls(config=config)
242
+ loader.parse_manifest()
243
+ return loader
244
+
245
+ async def initialize(self) -> None:
246
+ """Asynchronously initialize the loader.
247
+
248
+ This method performs async file I/O to load the manifest or hot file.
249
+ Call this during app startup in an async context.
250
+ """
251
+ if self._initialized:
252
+ return
253
+
254
+ await (self._load_hot_file_async() if self._is_hot_dev else self._load_manifest_async())
255
+ self._initialized = True
35
256
 
36
257
  def parse_manifest(self) -> None:
37
- """Read and parse the Vite manifest file.
38
-
39
- Example manifest:
40
- ```json
41
- {
42
- "main.js": {
43
- "file": "assets/main.4889e940.js",
44
- "src": "main.js",
45
- "isEntry": true,
46
- "dynamicImports": ["views/foo.js"],
47
- "css": ["assets/main.b82dbe22.css"],
48
- "assets": ["assets/asset.0ab0f9cd.png"]
49
- },
50
- "views/foo.js": {
51
- "file": "assets/foo.869aea0d.js",
52
- "src": "views/foo.js",
53
- "isDynamicEntry": true,
54
- "imports": ["_shared.83069a53.js"]
55
- },
56
- "_shared.83069a53.js": {
57
- "file": "assets/shared.83069a53.js"
58
- }
59
- }
60
- ```
258
+ """Synchronously parse the Vite manifest file.
259
+
260
+ This method reads the manifest.json file in production mode
261
+ or the hot file in development mode.
262
+
263
+ Note: For async contexts, use `initialize()` instead.
264
+ """
265
+ (self._load_hot_file_sync() if self._is_hot_dev else self._load_manifest_sync())
266
+
267
+ def _get_manifest_path(self) -> Path:
268
+ """Get the path to the manifest file.
269
+
270
+ Returns:
271
+ Absolute path to the Vite manifest file.
272
+ """
273
+ return self._config.resolve_manifest_path()
274
+
275
+ def _get_hot_file_path(self) -> Path:
276
+ """Get the path to the hot file.
277
+
278
+ Returns:
279
+ Path to the Vite hot file used for dev server URL discovery.
280
+ """
281
+ bundle_dir = self._config.bundle_dir
282
+ if not bundle_dir.is_absolute():
283
+ bundle_dir = self._config.root_dir / bundle_dir
284
+ return bundle_dir / self._config.hot_file
285
+
286
+ async def _load_manifest_async(self) -> None:
287
+ """Asynchronously load and parse the Vite manifest file.
288
+
289
+ Raises:
290
+ ManifestNotFoundError: If the manifest file cannot be read or parsed.
291
+ """
292
+ manifest_path = anyio.Path(self._get_manifest_path())
293
+ try:
294
+ if await manifest_path.exists():
295
+ content = await manifest_path.read_text()
296
+ self._manifest_content = content
297
+ self._manifest = decode_json(content)
298
+ else:
299
+ self._manifest = {}
300
+ except (OSError, UnicodeDecodeError, SerializationException) as exc:
301
+ raise ManifestNotFoundError(str(manifest_path)) from exc
302
+
303
+ def _load_manifest_sync(self) -> None:
304
+ """Synchronously load and parse the Vite manifest file.
61
305
 
62
306
  Raises:
63
- RuntimeError: if cannot load the file or JSON in file is malformed.
307
+ ManifestNotFoundError: If the manifest file cannot be read or parsed.
64
308
  """
309
+ manifest_path = self._get_manifest_path()
310
+ try:
311
+ if manifest_path.exists():
312
+ self._manifest_content = manifest_path.read_text()
313
+ self._manifest = decode_json(self._manifest_content)
314
+ else:
315
+ self._manifest = {}
316
+ except (OSError, UnicodeDecodeError, SerializationException) as exc:
317
+ raise ManifestNotFoundError(str(manifest_path)) from exc
318
+
319
+ async def _load_hot_file_async(self) -> None:
320
+ """Asynchronously read the hot file for dev server URL."""
321
+ hot_file_path = anyio.Path(self._get_hot_file_path())
322
+ if await hot_file_path.exists():
323
+ self._vite_base_path = await hot_file_path.read_text()
324
+
325
+ def _load_hot_file_sync(self) -> None:
326
+ """Synchronously read the hot file for dev server URL."""
327
+ hot_file_path = self._get_hot_file_path()
328
+ if hot_file_path.exists():
329
+ self._vite_base_path = hot_file_path.read_text()
330
+
331
+ @property
332
+ def manifest_content(self) -> str:
333
+ """Get the raw manifest content.
65
334
 
66
- if not self._config.hot_reload:
67
- with Path(self._config.static_dir / self._config.manifest_name).open() as manifest_file:
68
- manifest_content = manifest_file.read()
69
- try:
70
- self._manifest = json.loads(manifest_content)
71
- except Exception as exc: # noqa: BLE001
72
- msg = "Cannot read Vite manifest file at %s"
73
- raise RuntimeError(
74
- msg,
75
- Path(self._config.static_dir / self._config.manifest_name),
76
- ) from exc
335
+ Returns:
336
+ The raw JSON string content of the Vite manifest file.
337
+ """
338
+ return self._manifest_content
77
339
 
78
- def generate_ws_client_tags(self) -> str:
79
- """Generate the script tag for the Vite WS client for HMR.
340
+ @manifest_content.setter
341
+ def manifest_content(self, value: str) -> None:
342
+ """Set the manifest content.
343
+
344
+ Args:
345
+ value: The raw JSON string content to set.
346
+ """
347
+ self._manifest_content = value
348
+
349
+ @cached_property
350
+ def version_id(self) -> str:
351
+ """Get the version ID of the manifest.
352
+
353
+ The version ID is used for cache busting and Inertia.js asset versioning.
354
+
355
+ Returns:
356
+ A hash of the manifest content, or "1.0" if no manifest.
357
+ """
358
+ if self._manifest_content:
359
+ return hashlib.sha256(self._manifest_content.encode("utf-8")).hexdigest()
360
+ return "1.0"
361
+
362
+ def render_hmr_client(self) -> "markupsafe.Markup":
363
+ """Render the HMR client script tags.
364
+
365
+ Returns:
366
+ HTML markup containing React HMR and Vite client script tags.
367
+ """
368
+ return markupsafe.Markup(f"{self.generate_react_hmr_tags()}{self.generate_ws_client_tags()}")
369
+
370
+ def render_asset_tag(
371
+ self, path: "str | list[str]", scripts_attrs: "dict[str, str] | None" = None
372
+ ) -> "markupsafe.Markup":
373
+ """Render asset tags for the specified path(s).
374
+
375
+ Args:
376
+ path: Single path or list of paths to assets.
377
+ scripts_attrs: Optional attributes for script tags.
378
+
379
+ Returns:
380
+ HTML markup for script and link tags.
381
+ """
382
+ paths = [str(p) for p in path] if isinstance(path, list) else [str(path)]
383
+ return markupsafe.Markup("".join(self.generate_asset_tags(p, scripts_attrs=scripts_attrs) for p in paths))
384
+
385
+ def get_static_asset(self, path: str) -> str:
386
+ """Get the URL for a static asset.
80
387
 
81
- Only used when hot module reloading is enabled, in production this method returns an empty string.
388
+ Args:
389
+ path: The path to the asset.
82
390
 
83
391
  Returns:
84
- str: The script tag or an empty string.
392
+ The full URL to the asset.
393
+
394
+ Raises:
395
+ AssetNotFoundError: If the asset is not in the manifest.
85
396
  """
86
- if not self._config.hot_reload:
87
- return ""
397
+ if self._is_hot_dev:
398
+ return self._vite_server_url(path)
399
+
400
+ if path not in self._manifest:
401
+ raise AssetNotFoundError(path, str(self._get_manifest_path()))
402
+
403
+ return urljoin(self._config.asset_url, self._manifest[path]["file"])
404
+
405
+ def generate_ws_client_tags(self) -> str:
406
+ """Generate the Vite HMR client script tag.
88
407
 
89
- return self._script_tag(
90
- self._vite_server_url("@vite/client"),
91
- {"type": "module"},
92
- )
408
+ Only generates output in development mode with hot reload enabled.
409
+
410
+ Returns:
411
+ Script tag HTML or empty string in production.
412
+ """
413
+ if self._is_hot_dev:
414
+ return self._script_tag(self._vite_server_url("@vite/client"), {"type": "module"})
415
+ return ""
93
416
 
94
417
  def generate_react_hmr_tags(self) -> str:
95
- """Generate the script tag for the Vite WS client for HMR.
418
+ """Generate React Fast Refresh preamble script.
96
419
 
97
- Only used when hot module reloading is enabled, in production this method returns an empty string.
420
+ Only generates output when React mode is enabled in development.
98
421
 
99
422
  Returns:
100
- str: The script tag or an empty string.
423
+ React refresh script HTML or empty string.
101
424
  """
102
- if self._config.is_react and self._config.hot_reload:
103
- return f"""
104
- <script type="module">
425
+ if self._config.is_react and self._is_hot_dev:
426
+ nonce = self._config.csp_nonce
427
+ nonce_attr = f' nonce="{html.escape(nonce, quote=True)}"' if nonce else ""
428
+ return dedent(f"""
429
+ <script type="module"{nonce_attr}>
105
430
  import RefreshRuntime from '{self._vite_server_url()}@react-refresh'
106
431
  RefreshRuntime.injectIntoGlobalHook(window)
107
432
  window.$RefreshReg$ = () => {{}}
108
433
  window.$RefreshSig$ = () => (type) => type
109
434
  window.__vite_plugin_react_preamble_installed__=true
110
435
  </script>
111
- """
436
+ """)
112
437
  return ""
113
438
 
114
- def generate_asset_tags(self, path: str, scripts_attrs: dict[str, str] | None = None) -> str:
115
- """Generate all assets include tags for the file in argument.
439
+ def generate_asset_tags(self, path: "str | list[str]", scripts_attrs: "dict[str, str] | None" = None) -> str:
440
+ """Generate all asset tags for the specified file(s).
441
+
442
+ Args:
443
+ path: Path or list of paths to assets.
444
+ scripts_attrs: Optional attributes for script tags.
116
445
 
117
446
  Returns:
118
- str: All tags to import this asset in your HTML page.
447
+ HTML string with all necessary script and link tags.
448
+
449
+ Raises:
450
+ ImproperlyConfiguredException: If asset not found in manifest.
119
451
  """
120
- if self._config.hot_reload:
121
- return self._script_tag(
122
- self._vite_server_url(path),
123
- {"type": "module", "async": "", "defer": ""},
124
- )
452
+ from litestar.exceptions import ImproperlyConfiguredException
125
453
 
126
- if path not in self._manifest:
127
- msg = "Cannot find %s in Vite manifest at %s"
128
- raise RuntimeError(
129
- msg,
130
- path,
131
- Path(self._config.static_dir / self._config.manifest_name),
454
+ paths = [path] if isinstance(path, str) else list(path)
455
+
456
+ if self._is_hot_dev:
457
+ return "".join(
458
+ self._style_tag(self._vite_server_url(p))
459
+ if p.endswith(".css")
460
+ else self._script_tag(self._vite_server_url(p), {"type": "module", "async": "", "defer": ""})
461
+ for p in paths
132
462
  )
133
463
 
464
+ missing = [p for p in paths if p not in self._manifest]
465
+ if missing:
466
+ msg = "Cannot find %s in Vite manifest at %s. Did you forget to build your assets after an update?"
467
+ raise ImproperlyConfiguredException(msg, missing, self._get_manifest_path())
468
+
134
469
  tags: list[str] = []
135
- manifest_entry: dict = self._manifest[path]
470
+ manifest_entries = {p: self._manifest[p] for p in paths if p}
471
+
136
472
  if not scripts_attrs:
137
473
  scripts_attrs = {"type": "module", "async": "", "defer": ""}
138
474
 
139
- # Add dependent CSS
140
- if "css" in manifest_entry:
141
- tags.extend(
142
- self._style_tag(urljoin(self._config.static_url, css_path))
143
- for css_path in manifest_entry.get("css", {})
144
- )
145
- # Add dependent "vendor"
146
- if "imports" in manifest_entry:
147
- tags.extend(
148
- self.generate_asset_tags(vendor_path, scripts_attrs=scripts_attrs)
149
- for vendor_path in manifest_entry.get("imports", {})
150
- )
151
- # Add the script by itself
152
- tags.append(
153
- self._script_tag(
154
- urljoin(self._config.static_url, manifest_entry["file"]),
155
- attrs=scripts_attrs,
156
- ),
157
- )
475
+ asset_url_base = self._config.asset_url
476
+
477
+ for manifest in manifest_entries.values():
478
+ if "css" in manifest:
479
+ tags.extend(self._style_tag(urljoin(asset_url_base, css_path)) for css_path in manifest.get("css", []))
480
+
481
+ if "imports" in manifest:
482
+ tags.extend(
483
+ self.generate_asset_tags(vendor_path, scripts_attrs=scripts_attrs)
484
+ for vendor_path in manifest.get("imports", [])
485
+ )
486
+
487
+ file_path = manifest.get("file", "")
488
+ if file_path.endswith(".css"):
489
+ tags.append(self._style_tag(urljoin(asset_url_base, file_path)))
490
+ else:
491
+ tags.append(self._script_tag(urljoin(asset_url_base, file_path), attrs=scripts_attrs))
158
492
 
159
493
  return "".join(tags)
160
494
 
161
- def _vite_server_url(self, path: str | None = None) -> str:
162
- """Generate an URL to and asset served by the Vite development server.
495
+ def _vite_server_url(self, path: "str | None" = None) -> str:
496
+ """Generate a URL to an asset on the Vite development server.
163
497
 
164
- Keyword Arguments:
165
- path: Path to the asset. (default: {None})
498
+ Args:
499
+ path: Optional path to append to the base URL.
166
500
 
167
501
  Returns:
168
- str: Full URL to the asset.
502
+ Full URL to the asset on the dev server.
169
503
  """
170
- base_path = f"{self._config.protocol}://{self._config.host}:{self._config.port}"
171
- return urljoin(
172
- base_path,
173
- urljoin(self._config.static_url, path if path is not None else ""),
174
- )
504
+ base_path = self._vite_base_path or f"{self._config.protocol}://{self._config.host}:{self._config.port}"
505
+ return urljoin(base_path, urljoin(self._config.asset_url, path if path is not None else ""))
175
506
 
176
- def _script_tag(self, src: str, attrs: dict[str, str] | None = None) -> str:
177
- """Generate an HTML script tag."""
178
- attrs_str = ""
179
- if attrs is not None:
180
- attrs_str = " ".join([f'{key}="{value}"' for key, value in attrs.items()])
507
+ @staticmethod
508
+ def _script_tag(src: str, attrs: "dict[str, str] | None" = None) -> str:
509
+ """Generate an HTML script tag.
181
510
 
182
- return f'<script {attrs_str} src="{src}"></script>'
511
+ Args:
512
+ src: The source URL for the script.
513
+ attrs: Optional attributes for the script tag.
514
+
515
+ Returns:
516
+ HTML script tag string.
517
+ """
518
+ if attrs is None:
519
+ attrs = {}
520
+ attrs_str = " ".join(f'{key}="{value}"' for key, value in attrs.items())
521
+ attrs_prefix = f"{attrs_str} " if attrs_str else ""
522
+ return f'<script {attrs_prefix}src="{src}"></script>'
183
523
 
184
- def _style_tag(self, href: str) -> str:
185
- """Generate and HTML <link> stylesheet tag for CSS.
524
+ @staticmethod
525
+ def _style_tag(href: str) -> str:
526
+ """Generate an HTML link tag for CSS.
186
527
 
187
528
  Args:
188
- href: CSS file URL.
529
+ href: The URL to the CSS file.
189
530
 
190
531
  Returns:
191
- str: CSS link tag.
532
+ HTML link tag string.
192
533
  """
193
534
  return f'<link rel="stylesheet" href="{href}" />'