litestar-vite 0.1.1__py3-none-any.whl → 0.15.0rc2__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 (154) hide show
  1. litestar_vite/__init__.py +54 -4
  2. litestar_vite/__metadata__.py +12 -7
  3. litestar_vite/_codegen/__init__.py +26 -0
  4. litestar_vite/_codegen/inertia.py +407 -0
  5. litestar_vite/_codegen/openapi.py +233 -0
  6. litestar_vite/_codegen/routes.py +653 -0
  7. litestar_vite/_codegen/ts.py +235 -0
  8. litestar_vite/_handler/__init__.py +8 -0
  9. litestar_vite/_handler/app.py +524 -0
  10. litestar_vite/_handler/routing.py +130 -0
  11. litestar_vite/cli.py +1147 -10
  12. litestar_vite/codegen.py +39 -0
  13. litestar_vite/commands.py +79 -0
  14. litestar_vite/config.py +1594 -70
  15. litestar_vite/deploy.py +355 -0
  16. litestar_vite/doctor.py +1179 -0
  17. litestar_vite/exceptions.py +78 -0
  18. litestar_vite/executor.py +316 -0
  19. litestar_vite/handler.py +9 -0
  20. litestar_vite/html_transform.py +426 -0
  21. litestar_vite/inertia/__init__.py +53 -0
  22. litestar_vite/inertia/_utils.py +114 -0
  23. litestar_vite/inertia/exception_handler.py +172 -0
  24. litestar_vite/inertia/helpers.py +1043 -0
  25. litestar_vite/inertia/middleware.py +54 -0
  26. litestar_vite/inertia/plugin.py +133 -0
  27. litestar_vite/inertia/request.py +286 -0
  28. litestar_vite/inertia/response.py +706 -0
  29. litestar_vite/inertia/types.py +316 -0
  30. litestar_vite/loader.py +462 -121
  31. litestar_vite/plugin.py +2160 -21
  32. litestar_vite/py.typed +0 -0
  33. litestar_vite/scaffolding/__init__.py +20 -0
  34. litestar_vite/scaffolding/generator.py +270 -0
  35. litestar_vite/scaffolding/templates.py +437 -0
  36. litestar_vite/templates/__init__.py +0 -0
  37. litestar_vite/templates/addons/tailwindcss/tailwind.css.j2 +1 -0
  38. litestar_vite/templates/angular/index.html.j2 +12 -0
  39. litestar_vite/templates/angular/openapi-ts.config.ts.j2 +18 -0
  40. litestar_vite/templates/angular/package.json.j2 +35 -0
  41. litestar_vite/templates/angular/src/app/app.component.css.j2 +3 -0
  42. litestar_vite/templates/angular/src/app/app.component.html.j2 +1 -0
  43. litestar_vite/templates/angular/src/app/app.component.ts.j2 +9 -0
  44. litestar_vite/templates/angular/src/app/app.config.ts.j2 +5 -0
  45. litestar_vite/templates/angular/src/main.ts.j2 +9 -0
  46. litestar_vite/templates/angular/src/styles.css.j2 +9 -0
  47. litestar_vite/templates/angular/tsconfig.app.json.j2 +34 -0
  48. litestar_vite/templates/angular/tsconfig.json.j2 +20 -0
  49. litestar_vite/templates/angular/vite.config.ts.j2 +21 -0
  50. litestar_vite/templates/angular-cli/.postcssrc.json.j2 +5 -0
  51. litestar_vite/templates/angular-cli/angular.json.j2 +36 -0
  52. litestar_vite/templates/angular-cli/openapi-ts.config.ts.j2 +18 -0
  53. litestar_vite/templates/angular-cli/package.json.j2 +27 -0
  54. litestar_vite/templates/angular-cli/proxy.conf.json.j2 +18 -0
  55. litestar_vite/templates/angular-cli/src/app/app.component.css.j2 +3 -0
  56. litestar_vite/templates/angular-cli/src/app/app.component.html.j2 +1 -0
  57. litestar_vite/templates/angular-cli/src/app/app.component.ts.j2 +9 -0
  58. litestar_vite/templates/angular-cli/src/app/app.config.ts.j2 +5 -0
  59. litestar_vite/templates/angular-cli/src/index.html.j2 +13 -0
  60. litestar_vite/templates/angular-cli/src/main.ts.j2 +6 -0
  61. litestar_vite/templates/angular-cli/src/styles.css.j2 +10 -0
  62. litestar_vite/templates/angular-cli/tailwind.config.js.j2 +4 -0
  63. litestar_vite/templates/angular-cli/tsconfig.app.json.j2 +16 -0
  64. litestar_vite/templates/angular-cli/tsconfig.json.j2 +26 -0
  65. litestar_vite/templates/angular-cli/tsconfig.spec.json.j2 +9 -0
  66. litestar_vite/templates/astro/astro.config.mjs.j2 +28 -0
  67. litestar_vite/templates/astro/openapi-ts.config.ts.j2 +15 -0
  68. litestar_vite/templates/astro/src/layouts/Layout.astro.j2 +63 -0
  69. litestar_vite/templates/astro/src/pages/index.astro.j2 +36 -0
  70. litestar_vite/templates/astro/src/styles/global.css.j2 +1 -0
  71. litestar_vite/templates/base/.gitignore.j2 +42 -0
  72. litestar_vite/templates/base/openapi-ts.config.ts.j2 +15 -0
  73. litestar_vite/templates/base/package.json.j2 +38 -0
  74. litestar_vite/templates/base/resources/vite-env.d.ts.j2 +1 -0
  75. litestar_vite/templates/base/tsconfig.json.j2 +37 -0
  76. litestar_vite/templates/htmx/src/main.js.j2 +8 -0
  77. litestar_vite/templates/htmx/templates/base.html.j2.j2 +56 -0
  78. litestar_vite/templates/htmx/templates/index.html.j2.j2 +13 -0
  79. litestar_vite/templates/htmx/vite.config.ts.j2 +40 -0
  80. litestar_vite/templates/nuxt/app.vue.j2 +29 -0
  81. litestar_vite/templates/nuxt/composables/useApi.ts.j2 +33 -0
  82. litestar_vite/templates/nuxt/nuxt.config.ts.j2 +31 -0
  83. litestar_vite/templates/nuxt/openapi-ts.config.ts.j2 +15 -0
  84. litestar_vite/templates/nuxt/pages/index.vue.j2 +54 -0
  85. litestar_vite/templates/react/index.html.j2 +13 -0
  86. litestar_vite/templates/react/src/App.css.j2 +56 -0
  87. litestar_vite/templates/react/src/App.tsx.j2 +19 -0
  88. litestar_vite/templates/react/src/main.tsx.j2 +10 -0
  89. litestar_vite/templates/react/vite.config.ts.j2 +39 -0
  90. litestar_vite/templates/react-inertia/index.html.j2 +14 -0
  91. litestar_vite/templates/react-inertia/package.json.j2 +46 -0
  92. litestar_vite/templates/react-inertia/resources/App.css.j2 +68 -0
  93. litestar_vite/templates/react-inertia/resources/main.tsx.j2 +17 -0
  94. litestar_vite/templates/react-inertia/resources/pages/Home.tsx.j2 +18 -0
  95. litestar_vite/templates/react-inertia/resources/ssr.tsx.j2 +19 -0
  96. litestar_vite/templates/react-inertia/vite.config.ts.j2 +59 -0
  97. litestar_vite/templates/react-router/index.html.j2 +12 -0
  98. litestar_vite/templates/react-router/src/App.css.j2 +17 -0
  99. litestar_vite/templates/react-router/src/App.tsx.j2 +7 -0
  100. litestar_vite/templates/react-router/src/main.tsx.j2 +10 -0
  101. litestar_vite/templates/react-router/vite.config.ts.j2 +39 -0
  102. litestar_vite/templates/react-tanstack/index.html.j2 +12 -0
  103. litestar_vite/templates/react-tanstack/openapi-ts.config.ts.j2 +18 -0
  104. litestar_vite/templates/react-tanstack/src/App.css.j2 +17 -0
  105. litestar_vite/templates/react-tanstack/src/main.tsx.j2 +21 -0
  106. litestar_vite/templates/react-tanstack/src/routeTree.gen.ts.j2 +7 -0
  107. litestar_vite/templates/react-tanstack/src/routes/__root.tsx.j2 +9 -0
  108. litestar_vite/templates/react-tanstack/src/routes/books.tsx.j2 +9 -0
  109. litestar_vite/templates/react-tanstack/src/routes/index.tsx.j2 +9 -0
  110. litestar_vite/templates/react-tanstack/vite.config.ts.j2 +39 -0
  111. litestar_vite/templates/svelte/index.html.j2 +13 -0
  112. litestar_vite/templates/svelte/src/App.svelte.j2 +30 -0
  113. litestar_vite/templates/svelte/src/app.css.j2 +45 -0
  114. litestar_vite/templates/svelte/src/main.ts.j2 +8 -0
  115. litestar_vite/templates/svelte/src/vite-env.d.ts.j2 +2 -0
  116. litestar_vite/templates/svelte/svelte.config.js.j2 +5 -0
  117. litestar_vite/templates/svelte/vite.config.ts.j2 +39 -0
  118. litestar_vite/templates/svelte-inertia/index.html.j2 +14 -0
  119. litestar_vite/templates/svelte-inertia/resources/app.css.j2 +21 -0
  120. litestar_vite/templates/svelte-inertia/resources/main.ts.j2 +11 -0
  121. litestar_vite/templates/svelte-inertia/resources/pages/Home.svelte.j2 +43 -0
  122. litestar_vite/templates/svelte-inertia/resources/vite-env.d.ts.j2 +2 -0
  123. litestar_vite/templates/svelte-inertia/svelte.config.js.j2 +5 -0
  124. litestar_vite/templates/svelte-inertia/vite.config.ts.j2 +37 -0
  125. litestar_vite/templates/sveltekit/openapi-ts.config.ts.j2 +15 -0
  126. litestar_vite/templates/sveltekit/src/app.css.j2 +40 -0
  127. litestar_vite/templates/sveltekit/src/app.html.j2 +12 -0
  128. litestar_vite/templates/sveltekit/src/hooks.server.ts.j2 +55 -0
  129. litestar_vite/templates/sveltekit/src/routes/+layout.svelte.j2 +12 -0
  130. litestar_vite/templates/sveltekit/src/routes/+page.svelte.j2 +34 -0
  131. litestar_vite/templates/sveltekit/svelte.config.js.j2 +12 -0
  132. litestar_vite/templates/sveltekit/tsconfig.json.j2 +14 -0
  133. litestar_vite/templates/sveltekit/vite.config.ts.j2 +31 -0
  134. litestar_vite/templates/vue/env.d.ts.j2 +7 -0
  135. litestar_vite/templates/vue/index.html.j2 +13 -0
  136. litestar_vite/templates/vue/src/App.vue.j2 +28 -0
  137. litestar_vite/templates/vue/src/main.ts.j2 +5 -0
  138. litestar_vite/templates/vue/src/style.css.j2 +45 -0
  139. litestar_vite/templates/vue/vite.config.ts.j2 +39 -0
  140. litestar_vite/templates/vue-inertia/env.d.ts.j2 +7 -0
  141. litestar_vite/templates/vue-inertia/index.html.j2 +14 -0
  142. litestar_vite/templates/vue-inertia/package.json.j2 +49 -0
  143. litestar_vite/templates/vue-inertia/resources/main.ts.j2 +18 -0
  144. litestar_vite/templates/vue-inertia/resources/pages/Home.vue.j2 +22 -0
  145. litestar_vite/templates/vue-inertia/resources/ssr.ts.j2 +21 -0
  146. litestar_vite/templates/vue-inertia/resources/style.css.j2 +21 -0
  147. litestar_vite/templates/vue-inertia/vite.config.ts.j2 +59 -0
  148. litestar_vite-0.15.0rc2.dist-info/METADATA +230 -0
  149. litestar_vite-0.15.0rc2.dist-info/RECORD +151 -0
  150. {litestar_vite-0.1.1.dist-info → litestar_vite-0.15.0rc2.dist-info}/WHEEL +1 -1
  151. litestar_vite/template_engine.py +0 -103
  152. litestar_vite-0.1.1.dist-info/METADATA +0 -68
  153. litestar_vite-0.1.1.dist-info/RECORD +0 -11
  154. {litestar_vite-0.1.1.dist-info → litestar_vite-0.15.0rc2.dist-info}/licenses/LICENSE +0 -0
litestar_vite/loader.py CHANGED
@@ -1,106 +1,431 @@
1
- from __future__ import annotations
1
+ """Vite Asset Loader.
2
2
 
3
- import json
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
+ from functools import cached_property
4
16
  from pathlib import Path
5
- from typing import TYPE_CHECKING, Any, ClassVar, TypeVar
17
+ from textwrap import dedent
18
+ from typing import TYPE_CHECKING, Any
6
19
  from urllib.parse import urljoin
7
20
 
8
- from litestar.template import TemplateEngineProtocol
21
+ import anyio
22
+ import markupsafe
23
+ from litestar.exceptions import SerializationException
24
+ from litestar.serialization import decode_json
25
+
26
+ from litestar_vite.exceptions import AssetNotFoundError, ManifestNotFoundError
9
27
 
10
28
  if TYPE_CHECKING:
29
+ from collections.abc import Mapping
30
+
31
+ from litestar.connection import Request
32
+
11
33
  from litestar_vite.config import ViteConfig
34
+ from litestar_vite.plugin import VitePlugin
35
+
36
+
37
+ def _get_request_from_context(context: "Mapping[str, Any]") -> "Request[Any, Any, Any]":
38
+ """Get the request from the template context.
39
+
40
+ Args:
41
+ context: The template context.
42
+
43
+ Returns:
44
+ The request object from the template context.
45
+
46
+ Raises:
47
+ ValueError: If 'request' is not found in the template context.
48
+ TypeError: If 'request' is not a Litestar Request object.
49
+ """
50
+ from litestar.connection import Request
51
+
52
+ request = context.get("request")
53
+ if request is None:
54
+ msg = "Request not found in template context. Ensure 'request' is passed to the template."
55
+ raise ValueError(msg)
56
+ if not isinstance(request, Request): # pyright: ignore[reportUnknownVariableType]
57
+ msg = f"Expected Request object, got {type(request)}"
58
+ raise TypeError(msg)
59
+ return request # pyright: ignore[reportReturnType,reportUnknownVariableType]
60
+
61
+
62
+ def _get_vite_plugin(context: "Mapping[str, Any]") -> "VitePlugin | None":
63
+ """Return the VitePlugin from the template context, if registered.
64
+
65
+ Returns:
66
+ The VitePlugin instance, or None if not registered.
67
+ """
68
+ request = _get_request_from_context(context)
69
+ return request.app.plugins.get("VitePlugin")
12
70
 
13
- T = TypeVar("T", bound=TemplateEngineProtocol)
71
+
72
+ def render_hmr_client(context: "Mapping[str, Any]", /) -> "markupsafe.Markup":
73
+ """Render the HMR client script tag.
74
+
75
+ This is a Jinja2 template callable that renders the Vite HMR client
76
+ script tag for development mode.
77
+
78
+ Args:
79
+ context: The template context containing the request.
80
+
81
+ Returns:
82
+ HTML markup for the HMR client script, or empty markup if
83
+ VitePlugin is not registered.
84
+ """
85
+ vite_plugin = _get_vite_plugin(context)
86
+ if vite_plugin is None:
87
+ return markupsafe.Markup("")
88
+ return vite_plugin.asset_loader.render_hmr_client()
89
+
90
+
91
+ def render_asset_tag(
92
+ context: "Mapping[str, Any]", /, path: "str | list[str]", scripts_attrs: "dict[str, str] | None" = None
93
+ ) -> "markupsafe.Markup":
94
+ """Render asset tags for the specified path(s).
95
+
96
+ This is a Jinja2 template callable that renders script/link tags
97
+ for Vite-managed assets. Also works for HTMX partial responses.
98
+
99
+ Args:
100
+ context: The template context containing the request.
101
+ path: Single path or list of paths to assets.
102
+ scripts_attrs: Optional attributes for script tags.
103
+
104
+ Returns:
105
+ HTML markup for the asset tags, or empty markup if VitePlugin
106
+ is not registered.
107
+
108
+ Example:
109
+ In a Jinja2 template:
110
+ {{ vite_asset("src/main.ts") }}
111
+ {{ vite_asset("src/components/UserProfile.tsx") }}
112
+ """
113
+ vite_plugin = _get_vite_plugin(context)
114
+ if vite_plugin is None:
115
+ return markupsafe.Markup("")
116
+ return vite_plugin.asset_loader.render_asset_tag(path, scripts_attrs)
117
+
118
+
119
+ def render_static_asset(context: "Mapping[str, Any]", /, path: str) -> str:
120
+ """Render a static asset URL.
121
+
122
+ This is a Jinja2 template callable that returns the URL for a static asset.
123
+
124
+ Args:
125
+ context: The template context containing the request.
126
+ path: Path to the static asset.
127
+
128
+ Returns:
129
+ The full URL to the static asset, or empty string if VitePlugin
130
+ is not registered.
131
+ """
132
+ vite_plugin = _get_vite_plugin(context)
133
+ if vite_plugin is None:
134
+ return ""
135
+ return vite_plugin.asset_loader.get_static_asset(path)
136
+
137
+
138
+ def render_routes(
139
+ context: "Mapping[str, Any]",
140
+ /,
141
+ *,
142
+ only: "list[str] | None" = None,
143
+ exclude: "list[str] | None" = None,
144
+ include_components: bool = False,
145
+ ) -> "markupsafe.Markup":
146
+ """Render inline script tag with route definitions.
147
+
148
+ This is a Jinja2 template callable that renders an inline script tag
149
+ containing route metadata for client-side type-safe routing.
150
+
151
+ The script defines a global `window.Litestar.routes` object that can be
152
+ used by frontend routers.
153
+
154
+ Uses Litestar's built-in serializers, picking up any custom type encoders
155
+ configured on the app.
156
+
157
+ Args:
158
+ context: The template context containing the request.
159
+ only: Optional list of route patterns to include.
160
+ exclude: Optional list of route patterns to exclude.
161
+ include_components: Include Inertia component names.
162
+
163
+ Returns:
164
+ HTML markup for the inline routes script containing route metadata
165
+ as a JSON object.
166
+
167
+ Example:
168
+ In a Jinja2 template:
169
+ {{ vite_routes() }}
170
+ {{ vite_routes(exclude=['/api/internal']) }}
171
+ """
172
+ from litestar.serialization import encode_json, get_serializer
173
+
174
+ from litestar_vite.codegen import generate_routes_json
175
+
176
+ request = _get_request_from_context(context)
177
+ app = request.app
178
+
179
+ routes_data = generate_routes_json(app, only=only, exclude=exclude, include_components=include_components)
180
+
181
+ serializer = get_serializer(app.type_encoders)
182
+ routes_json = encode_json(routes_data, serializer=serializer).decode("utf-8")
183
+
184
+ script = dedent(f"""\
185
+ <script type="text/javascript">
186
+ (function() {{
187
+ window.Litestar = window.Litestar || {{}};
188
+ window.Litestar.routes = {routes_json};
189
+ }})();
190
+ </script>""")
191
+
192
+ return markupsafe.Markup(script)
14
193
 
15
194
 
16
195
  class ViteAssetLoader:
17
- """Vite manifest loader.
196
+ """Vite asset loader for managing frontend assets.
197
+
198
+ This class handles loading and rendering of Vite-managed assets.
199
+ It supports both development mode (with HMR) and production mode
200
+ (with manifest-based asset resolution).
201
+
202
+ The loader is designed to be instantiated per-app (not a singleton)
203
+ and supports async initialization for non-blocking file I/O.
18
204
 
19
- Please see: https://vitejs.dev/guide/backend-integration.html
205
+ Attributes:
206
+ config: The Vite configuration.
207
+
208
+ Example:
209
+ loader = ViteAssetLoader(config)
210
+ await loader.initialize()
211
+ html = loader.render_asset_tag("src/main.ts")
20
212
  """
21
213
 
22
- _instance: ClassVar[ViteAssetLoader | None] = None
214
+ def __init__(self, config: "ViteConfig") -> None:
215
+ """Initialize the asset loader.
23
216
 
24
- def __init__(self, config: ViteConfig) -> None:
217
+ Args:
218
+ config: The Vite configuration.
219
+ """
25
220
  self._config = config
26
221
  self._manifest: dict[str, Any] = {}
222
+ self._manifest_content: str = ""
223
+ self._vite_base_path: "str | None" = None
224
+ self._initialized: bool = False
225
+ self._is_hot_dev = self._config.hot_reload and self._config.is_dev_mode
27
226
 
28
227
  @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
228
+ def initialize_loader(cls, config: "ViteConfig") -> "ViteAssetLoader":
229
+ """Synchronously initialize a loader instance.
230
+
231
+ This is a convenience method for synchronous initialization.
232
+ For async contexts, prefer using `initialize()` after construction.
233
+
234
+ Args:
235
+ config: The Vite configuration.
236
+
237
+ Returns:
238
+ An initialized ViteAssetLoader instance.
239
+ """
240
+ loader = cls(config=config)
241
+ loader.parse_manifest()
242
+ return loader
243
+
244
+ async def initialize(self) -> None:
245
+ """Asynchronously initialize the loader.
246
+
247
+ This method performs async file I/O to load the manifest or hot file.
248
+ Call this during app startup in an async context.
249
+ """
250
+ if self._initialized:
251
+ return
252
+
253
+ await (self._load_hot_file_async() if self._is_hot_dev else self._load_manifest_async())
254
+ self._initialized = True
35
255
 
36
256
  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
- ```
257
+ """Synchronously parse the Vite manifest file.
258
+
259
+ This method reads the manifest.json file in production mode
260
+ or the hot file in development mode.
261
+
262
+ Note: For async contexts, use `initialize()` instead.
263
+ """
264
+ (self._load_hot_file_sync() if self._is_hot_dev else self._load_manifest_sync())
265
+
266
+ def _get_manifest_path(self) -> Path:
267
+ """Get the path to the manifest file.
268
+
269
+ Returns:
270
+ Absolute path to the Vite manifest file.
271
+ """
272
+ bundle_dir = self._config.bundle_dir
273
+ if not bundle_dir.is_absolute():
274
+ bundle_dir = self._config.root_dir / bundle_dir
275
+ return bundle_dir / self._config.manifest_name
276
+
277
+ def _get_hot_file_path(self) -> Path:
278
+ """Get the path to the hot file.
279
+
280
+ Returns:
281
+ Path to the Vite hot file used for dev server URL discovery.
282
+ """
283
+ bundle_dir = self._config.bundle_dir
284
+ if not bundle_dir.is_absolute():
285
+ bundle_dir = self._config.root_dir / bundle_dir
286
+ return bundle_dir / self._config.hot_file
287
+
288
+ async def _load_manifest_async(self) -> None:
289
+ """Asynchronously load and parse the Vite manifest file.
290
+
291
+ Raises:
292
+ ManifestNotFoundError: If the manifest file cannot be read or parsed.
293
+ """
294
+ manifest_path = anyio.Path(self._get_manifest_path())
295
+ try:
296
+ if await manifest_path.exists():
297
+ content = await manifest_path.read_text()
298
+ self._manifest_content = content
299
+ self._manifest = decode_json(content)
300
+ else:
301
+ self._manifest = {}
302
+ except (OSError, UnicodeDecodeError, SerializationException) as exc:
303
+ raise ManifestNotFoundError(str(manifest_path)) from exc
304
+
305
+ def _load_manifest_sync(self) -> None:
306
+ """Synchronously load and parse the Vite manifest file.
61
307
 
62
308
  Raises:
63
- RuntimeError: if cannot load the file or JSON in file is malformed.
309
+ ManifestNotFoundError: If the manifest file cannot be read or parsed.
64
310
  """
311
+ manifest_path = self._get_manifest_path()
312
+ try:
313
+ if manifest_path.exists():
314
+ self._manifest_content = manifest_path.read_text()
315
+ self._manifest = decode_json(self._manifest_content)
316
+ else:
317
+ self._manifest = {}
318
+ except (OSError, UnicodeDecodeError, SerializationException) as exc:
319
+ raise ManifestNotFoundError(str(manifest_path)) from exc
320
+
321
+ async def _load_hot_file_async(self) -> None:
322
+ """Asynchronously read the hot file for dev server URL."""
323
+ hot_file_path = anyio.Path(self._get_hot_file_path())
324
+ if await hot_file_path.exists():
325
+ self._vite_base_path = await hot_file_path.read_text()
326
+
327
+ def _load_hot_file_sync(self) -> None:
328
+ """Synchronously read the hot file for dev server URL."""
329
+ hot_file_path = self._get_hot_file_path()
330
+ if hot_file_path.exists():
331
+ self._vite_base_path = hot_file_path.read_text()
332
+
333
+ @property
334
+ def manifest_content(self) -> str:
335
+ """Get the raw manifest content.
65
336
 
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
337
+ Returns:
338
+ The raw JSON string content of the Vite manifest file.
339
+ """
340
+ return self._manifest_content
77
341
 
78
- def generate_ws_client_tags(self) -> str:
79
- """Generate the script tag for the Vite WS client for HMR.
342
+ @manifest_content.setter
343
+ def manifest_content(self, value: str) -> None:
344
+ """Set the manifest content.
345
+
346
+ Args:
347
+ value: The raw JSON string content to set.
348
+ """
349
+ self._manifest_content = value
350
+
351
+ @cached_property
352
+ def version_id(self) -> str:
353
+ """Get the version ID of the manifest.
354
+
355
+ The version ID is used for cache busting and Inertia.js asset versioning.
356
+
357
+ Returns:
358
+ A hash of the manifest content, or "1.0" if no manifest.
359
+ """
360
+ if self._manifest_content:
361
+ return hashlib.sha256(self._manifest_content.encode("utf-8")).hexdigest()
362
+ return "1.0"
363
+
364
+ def render_hmr_client(self) -> "markupsafe.Markup":
365
+ """Render the HMR client script tags.
366
+
367
+ Returns:
368
+ HTML markup containing React HMR and Vite client script tags.
369
+ """
370
+ return markupsafe.Markup(f"{self.generate_react_hmr_tags()}{self.generate_ws_client_tags()}")
371
+
372
+ def render_asset_tag(
373
+ self, path: "str | list[str]", scripts_attrs: "dict[str, str] | None" = None
374
+ ) -> "markupsafe.Markup":
375
+ """Render asset tags for the specified path(s).
376
+
377
+ Args:
378
+ path: Single path or list of paths to assets.
379
+ scripts_attrs: Optional attributes for script tags.
380
+
381
+ Returns:
382
+ HTML markup for script and link tags.
383
+ """
384
+ paths = [str(p) for p in path] if isinstance(path, list) else [str(path)]
385
+ return markupsafe.Markup("".join(self.generate_asset_tags(p, scripts_attrs=scripts_attrs) for p in paths))
386
+
387
+ def get_static_asset(self, path: str) -> str:
388
+ """Get the URL for a static asset.
80
389
 
81
- Only used when hot module reloading is enabled, in production this method returns an empty string.
390
+ Args:
391
+ path: The path to the asset.
82
392
 
83
393
  Returns:
84
- str: The script tag or an empty string.
394
+ The full URL to the asset.
395
+
396
+ Raises:
397
+ AssetNotFoundError: If the asset is not in the manifest.
85
398
  """
86
- if not self._config.hot_reload:
87
- return ""
399
+ if self._is_hot_dev:
400
+ return self._vite_server_url(path)
401
+
402
+ if path not in self._manifest:
403
+ raise AssetNotFoundError(path, str(self._get_manifest_path()))
404
+
405
+ return urljoin(self._config.base_url or self._config.asset_url, self._manifest[path]["file"])
406
+
407
+ def generate_ws_client_tags(self) -> str:
408
+ """Generate the Vite HMR client script tag.
88
409
 
89
- return self._script_tag(
90
- self._vite_server_url("@vite/client"),
91
- {"type": "module"},
92
- )
410
+ Only generates output in development mode with hot reload enabled.
411
+
412
+ Returns:
413
+ Script tag HTML or empty string in production.
414
+ """
415
+ if self._is_hot_dev:
416
+ return self._script_tag(self._vite_server_url("@vite/client"), {"type": "module"})
417
+ return ""
93
418
 
94
419
  def generate_react_hmr_tags(self) -> str:
95
- """Generate the script tag for the Vite WS client for HMR.
420
+ """Generate React Fast Refresh preamble script.
96
421
 
97
- Only used when hot module reloading is enabled, in production this method returns an empty string.
422
+ Only generates output when React mode is enabled in development.
98
423
 
99
424
  Returns:
100
- str: The script tag or an empty string.
425
+ React refresh script HTML or empty string.
101
426
  """
102
- if self._config.is_react and self._config.hot_reload:
103
- return f"""
427
+ if self._config.is_react and self._is_hot_dev:
428
+ return dedent(f"""
104
429
  <script type="module">
105
430
  import RefreshRuntime from '{self._vite_server_url()}@react-refresh'
106
431
  RefreshRuntime.injectIntoGlobalHook(window)
@@ -108,86 +433,102 @@ class ViteAssetLoader:
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.base_url or 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}" />'