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
@@ -0,0 +1,235 @@
1
+ """TypeScript conversion helpers for code generation."""
2
+
3
+ import re
4
+ from pathlib import PurePosixPath
5
+ from typing import Any, cast
6
+
7
+ _PATH_PARAM_TYPE_PATTERN = re.compile(r"\{([^:}]+):[^}]+\}")
8
+
9
+ _OPENAPI_STRING_FORMAT_TO_TS_ALIAS: dict[str, str] = {
10
+ "uuid": "UUID",
11
+ "date-time": "DateTime",
12
+ "date": "DateOnly",
13
+ "time": "TimeOnly",
14
+ "duration": "Duration",
15
+ "email": "Email",
16
+ "idn-email": "Email",
17
+ "uri": "URI",
18
+ "url": "URI",
19
+ "iri": "URI",
20
+ "iri-reference": "URI",
21
+ "uri-reference": "URI",
22
+ "uri-template": "URI",
23
+ "ipv4": "IPv4",
24
+ "ipv6": "IPv6",
25
+ }
26
+
27
+
28
+ def normalize_path(path: str) -> str:
29
+ """Normalize route path to use {param} syntax.
30
+
31
+ Returns:
32
+ Normalized path string.
33
+ """
34
+ if not path or path == "/":
35
+ return path
36
+ return _PATH_PARAM_TYPE_PATTERN.sub(r"{\1}", str(PurePosixPath(path)))
37
+
38
+
39
+ def ts_type_from_openapi(schema_dict: dict[str, Any]) -> str:
40
+ """Convert an OpenAPI schema dict to a TypeScript type string.
41
+
42
+ This function is intentionally lightweight and mirrors the historical
43
+ behavior used in this project's unit tests (OpenAPI 3.1 union types,
44
+ oneOf nullable patterns, etc.). It is not a full OpenAPI-to-TypeScript
45
+ compiler.
46
+
47
+ Returns:
48
+ A TypeScript type expression string.
49
+ """
50
+ if not schema_dict:
51
+ return "any"
52
+
53
+ ref = schema_dict.get("$ref")
54
+ if isinstance(ref, str) and ref:
55
+ return ref.split("/")[-1]
56
+
57
+ if "anyOf" in schema_dict and isinstance(schema_dict["anyOf"], list) and schema_dict["anyOf"]:
58
+ schemas = cast("list[Any]", schema_dict["anyOf"])
59
+ union = {ts_type_from_subschema(s) for s in schemas}
60
+ return join_union(union)
61
+
62
+ result = "any"
63
+ match schema_dict:
64
+ case {"const": const} if const is not None:
65
+ result = "any" if const is False else ts_literal(const)
66
+ case {"enum": enum} if isinstance(enum, list) and enum:
67
+ enum_values = cast("list[Any]", enum)
68
+ result = " | ".join(ts_literal(v) for v in enum_values)
69
+ case {"oneOf": one_of} if isinstance(one_of, list) and one_of:
70
+ schemas = cast("list[Any]", one_of)
71
+ union = {ts_type_from_subschema(s) for s in schemas}
72
+ result = join_union(union)
73
+ case {"allOf": all_of} if isinstance(all_of, list) and all_of:
74
+ schemas = cast("list[Any]", all_of)
75
+ parts = [wrap_union_for_intersection(ts_type_from_subschema(s)) for s in schemas]
76
+ parts = [p for p in parts if p and p != "any"]
77
+ result = " & ".join(parts) if parts else "any"
78
+ case {"type": list()}:
79
+ type_entries_list: list[Any] = schema_dict["type"]
80
+ parts = [ts_type_from_openapi_type_entry(t, schema_dict) for t in type_entries_list if isinstance(t, str)]
81
+ result = join_union(set(parts)) if parts else "any"
82
+ case {"type": str() as schema_type}:
83
+ result = ts_type_from_openapi_type_entry(schema_type, schema_dict)
84
+ case _:
85
+ pass
86
+
87
+ return result
88
+
89
+
90
+ def python_type_to_typescript(py_type: str, *, fallback: str = "unknown") -> tuple[str, bool]:
91
+ """Convert a Python typing string representation into a TS type and optionality flag.
92
+
93
+ Returns:
94
+ A tuple of the TypeScript type string and a boolean indicating if the type is optional.
95
+ """
96
+ if not py_type:
97
+ return fallback, False
98
+
99
+ normalized = py_type.replace("typing.", "").replace("types.", "")
100
+ optional = "None" in normalized or "NoneType" in normalized or "Optional[" in normalized
101
+
102
+ normalized = normalized.replace("NoneType", "None").replace("None", "null")
103
+
104
+ mapping: dict[str, str] = {
105
+ "str": "string",
106
+ "int": "number",
107
+ "float": "number",
108
+ "bool": "boolean",
109
+ "dict": "Record<string, unknown>",
110
+ "Dict": "Record<string, unknown>",
111
+ "list": f"{fallback}[]",
112
+ "List": f"{fallback}[]",
113
+ "tuple": f"{fallback}[]",
114
+ "Tuple": f"{fallback}[]",
115
+ "set": f"{fallback}[]",
116
+ "Set": f"{fallback}[]",
117
+ "Any": fallback,
118
+ "unknown": "unknown",
119
+ "null": "null",
120
+ }
121
+
122
+ for k, v in mapping.items():
123
+ if normalized == k:
124
+ return v, optional
125
+
126
+ return normalized, optional
127
+
128
+
129
+ def collect_ref_names(schema_dict: Any) -> set[str]:
130
+ """Collect referenced component names from an OpenAPI schema dict.
131
+
132
+ Returns:
133
+ A set of referenced component names.
134
+ """
135
+ refs: set[str] = set()
136
+ if isinstance(schema_dict, dict):
137
+ schema_dict_t = cast("dict[str, Any]", schema_dict)
138
+ ref_any = schema_dict_t.get("$ref")
139
+ if isinstance(ref_any, str) and ref_any.startswith("#/components/schemas/"):
140
+ refs.add(ref_any.split("/")[-1])
141
+ for v in schema_dict_t.values():
142
+ refs.update(collect_ref_names(v))
143
+ elif isinstance(schema_dict, list):
144
+ for item in cast("list[Any]", schema_dict):
145
+ refs.update(collect_ref_names(item))
146
+ return refs
147
+
148
+
149
+ def ts_type_from_subschema(schema: Any) -> str:
150
+ if isinstance(schema, dict):
151
+ return ts_type_from_openapi(cast("dict[str, Any]", schema))
152
+ return "any"
153
+
154
+
155
+ def ts_type_from_openapi_type_entry(type_name: str, schema_dict: dict[str, Any]) -> str:
156
+ primitive_types: dict[str, str] = {
157
+ "string": "string",
158
+ "integer": "number",
159
+ "number": "number",
160
+ "boolean": "boolean",
161
+ "null": "null",
162
+ }
163
+
164
+ result = primitive_types.get(type_name, "any")
165
+ if type_name == "string":
166
+ fmt = schema_dict.get("format")
167
+ if isinstance(fmt, str) and fmt:
168
+ result = _OPENAPI_STRING_FORMAT_TO_TS_ALIAS.get(fmt, result)
169
+ if type_name == "array":
170
+ items = schema_dict.get("items")
171
+ item_type = ts_type_from_subschema(items) if isinstance(items, dict) else "unknown"
172
+ result = f"{wrap_for_array(item_type)}[]"
173
+ elif type_name == "object":
174
+ properties = schema_dict.get("properties")
175
+ if not isinstance(properties, dict) or not properties:
176
+ result = "{}"
177
+ else:
178
+ required_list = schema_dict.get("required")
179
+ required: set[str] = set()
180
+ if isinstance(required_list, list):
181
+ required = {v for v in cast("list[Any]", required_list) if isinstance(v, str)}
182
+
183
+ lines: list[str] = ["{"]
184
+ for name, prop_schema in cast("dict[str, Any]", properties).items():
185
+ ts_type = ts_type_from_subschema(prop_schema)
186
+ optional = "" if name in required else "?"
187
+ lines.append(f" {name}{optional}: {ts_type};")
188
+ lines.append("}")
189
+ result = "\n".join(lines)
190
+
191
+ return result
192
+
193
+
194
+ def wrap_for_array(type_expr: str) -> str:
195
+ expr = type_expr.strip()
196
+ if not expr:
197
+ return "unknown"
198
+ if expr.startswith("(") and expr.endswith(")"):
199
+ return expr
200
+ # Parenthesize unions/intersections so `(A | B)[]` / `(A & B)[]` is emitted correctly.
201
+ if " | " in expr or (" & " in expr and not expr.startswith("{")):
202
+ return f"({expr})"
203
+ return expr
204
+
205
+
206
+ def wrap_union_for_intersection(type_expr: str) -> str:
207
+ expr = type_expr.strip()
208
+ if not expr:
209
+ return "any"
210
+ if expr.startswith("(") and expr.endswith(")"):
211
+ return expr
212
+ if " | " in expr:
213
+ return f"({expr})"
214
+ return expr
215
+
216
+
217
+ def join_union(types: set[str]) -> str:
218
+ if not types:
219
+ return "any"
220
+ if len(types) == 1:
221
+ return next(iter(types))
222
+ return " | ".join(sorted(types))
223
+
224
+
225
+ def ts_literal(value: Any) -> str:
226
+ if value is None:
227
+ return "null"
228
+ if isinstance(value, bool):
229
+ return "true" if value else "false"
230
+ if isinstance(value, int | float):
231
+ return str(value)
232
+ if isinstance(value, str):
233
+ escaped = value.replace("\\\\", "\\\\\\\\").replace('"', '\\"')
234
+ return f'"{escaped}"'
235
+ return "any"
@@ -0,0 +1,141 @@
1
+ """Utilities for deterministic code generation and file output."""
2
+
3
+ import hashlib
4
+ import json
5
+ from collections.abc import Callable
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+
10
+ def deep_sort_dict(obj: Any) -> Any:
11
+ """Recursively sort all dictionary keys for deterministic JSON output.
12
+
13
+ Args:
14
+ obj: Any Python object (dict, list, or primitive).
15
+
16
+ Returns:
17
+ The object with all nested dict keys sorted.
18
+ """
19
+ if isinstance(obj, dict):
20
+ # pyright: ignore - intentionally working with Any types for generic dict sorting
21
+ return {k: deep_sort_dict(v) for k, v in sorted(obj.items())} # pyright: ignore[reportUnknownVariableType,reportUnknownArgumentType]
22
+ if isinstance(obj, list):
23
+ return [deep_sort_dict(item) for item in obj] # pyright: ignore[reportUnknownVariableType]
24
+ return obj
25
+
26
+
27
+ def strip_timestamp_for_comparison(content: bytes) -> bytes:
28
+ """Remove generatedAt and other timestamp fields for content comparison.
29
+
30
+ This allows comparing file content while ignoring fields that change
31
+ on every generation (like timestamps).
32
+
33
+ Args:
34
+ content: JSON content as bytes.
35
+
36
+ Returns:
37
+ JSON content with timestamp fields removed, sorted keys.
38
+ """
39
+ try:
40
+ data = json.loads(content)
41
+ # Remove fields that change on every generation
42
+ data.pop("generatedAt", None)
43
+ # Return sorted JSON for consistent comparison
44
+ return json.dumps(data, sort_keys=True, separators=(",", ":")).encode("utf-8")
45
+ except (json.JSONDecodeError, TypeError, AttributeError):
46
+ # If we can't parse the content, return as-is
47
+ return content
48
+
49
+
50
+ def write_if_changed(
51
+ path: Path,
52
+ content: bytes | str,
53
+ *,
54
+ normalize_for_comparison: Callable[[bytes], bytes] | None = None,
55
+ encoding: str = "utf-8",
56
+ ) -> bool:
57
+ """Write content to file only if it differs from the existing content.
58
+
59
+ Uses hash comparison to avoid unnecessary writes that would trigger
60
+ file watchers and unnecessary rebuilds. Optionally normalizes content
61
+ before comparison (e.g., to strip timestamps).
62
+
63
+ Args:
64
+ path: The file path to write to.
65
+ content: The content to write (bytes or str).
66
+ normalize_for_comparison: Optional callback to normalize content before
67
+ comparison (e.g., strip timestamps). The file is written with the
68
+ original content, not the normalized version.
69
+ encoding: Encoding for string content.
70
+
71
+ Returns:
72
+ True if file was written (content changed), False if skipped (unchanged).
73
+ """
74
+ # Ensure trailing newline for POSIX compliance
75
+ if isinstance(content, str):
76
+ if not content.endswith("\n"):
77
+ content += "\n"
78
+ content_bytes = content.encode(encoding)
79
+ else:
80
+ if not content.endswith(b"\n"):
81
+ content += b"\n"
82
+ content_bytes = content
83
+
84
+ if path.exists():
85
+ try:
86
+ existing = path.read_bytes()
87
+
88
+ # Normalize both for comparison if a normalizer is provided
89
+ if normalize_for_comparison:
90
+ existing_normalized = normalize_for_comparison(existing)
91
+ new_normalized = normalize_for_comparison(content_bytes)
92
+ else:
93
+ existing_normalized = existing
94
+ new_normalized = content_bytes
95
+
96
+ # Compare using MD5 hash for efficiency
97
+ existing_hash = hashlib.md5(existing_normalized).hexdigest() # noqa: S324
98
+ new_hash = hashlib.md5(new_normalized).hexdigest() # noqa: S324
99
+ if existing_hash == new_hash:
100
+ return False
101
+ except OSError:
102
+ pass
103
+
104
+ path.parent.mkdir(parents=True, exist_ok=True)
105
+ if isinstance(content, str):
106
+ path.write_text(content, encoding=encoding)
107
+ else:
108
+ path.write_bytes(content)
109
+ return True
110
+
111
+
112
+ def encode_deterministic_json(
113
+ data: dict[str, Any], *, indent: int = 2, serializer: Callable[[Any], bytes] | None = None
114
+ ) -> bytes:
115
+ """Encode JSON with sorted keys for deterministic output.
116
+
117
+ This is a wrapper that ensures all nested dict keys are sorted
118
+ before serialization, producing byte-identical output for the
119
+ same input data regardless of insertion order.
120
+
121
+ Args:
122
+ data: Dictionary to encode.
123
+ indent: Indentation level for formatting.
124
+ serializer: Optional custom serializer function. If not provided,
125
+ uses litestar's default encode_json.
126
+
127
+ Returns:
128
+ Formatted JSON bytes with sorted keys.
129
+ """
130
+ import msgspec
131
+ from litestar.serialization import encode_json
132
+
133
+ sorted_data = deep_sort_dict(data)
134
+ if serializer is not None:
135
+ content = msgspec.json.format(serializer(sorted_data), indent=indent)
136
+ else:
137
+ content = msgspec.json.format(encode_json(sorted_data), indent=indent)
138
+ # Ensure trailing newline for POSIX compliance
139
+ if not content.endswith(b"\n"):
140
+ content += b"\n"
141
+ return content
@@ -0,0 +1,73 @@
1
+ """Vite commands module.
2
+
3
+ This module provides utility functions for Vite project initialization.
4
+ The main scaffolding functionality has moved to litestar_vite.scaffolding.
5
+ """
6
+
7
+ from typing import TYPE_CHECKING
8
+
9
+ from litestar_vite.config import JINJA_INSTALLED
10
+ from litestar_vite.exceptions import MissingDependencyError
11
+
12
+ if TYPE_CHECKING:
13
+ from pathlib import Path
14
+
15
+
16
+ def init_vite(
17
+ root_path: "Path",
18
+ resource_path: "Path",
19
+ asset_url: "str",
20
+ static_path: "Path",
21
+ bundle_path: "Path",
22
+ enable_ssr: "bool",
23
+ vite_port: int,
24
+ litestar_port: int,
25
+ framework: str = "react",
26
+ ) -> None:
27
+ """Initialize a new Vite project using the scaffolding system.
28
+
29
+ Args:
30
+ root_path: Root directory for the Vite project.
31
+ resource_path: Directory containing source files.
32
+ asset_url: Base URL for serving assets.
33
+ static_path: Directory for static (unprocessed) frontend assets.
34
+ bundle_path: Output directory for built files.
35
+ enable_ssr: Enable server-side rendering.
36
+ vite_port: Port for Vite dev server.
37
+ litestar_port: Port for Litestar server.
38
+ framework: Framework template to use (default: react).
39
+
40
+ Raises:
41
+ MissingDependencyError: If Jinja2 is not installed.
42
+ ValueError: If the specified framework template is not found.
43
+ """
44
+ if not JINJA_INSTALLED:
45
+ raise MissingDependencyError(package="jinja2", install_package="jinja")
46
+
47
+ from litestar_vite.scaffolding import TemplateContext, generate_project
48
+ from litestar_vite.scaffolding.templates import FrameworkType, get_template
49
+
50
+ template = get_template(framework)
51
+ if template is None:
52
+ template = get_template(FrameworkType.REACT)
53
+ if template is None: # pragma: no cover
54
+ msg = f"Could not find template for framework: {framework}"
55
+ raise ValueError(msg)
56
+
57
+ context = TemplateContext(
58
+ project_name=root_path.name or "my-project",
59
+ framework=template,
60
+ use_typescript=template.uses_typescript,
61
+ use_tailwind=False,
62
+ vite_port=vite_port,
63
+ litestar_port=litestar_port,
64
+ asset_url=asset_url,
65
+ resource_dir=str(resource_path),
66
+ bundle_dir=str(bundle_path),
67
+ static_dir=str(static_path),
68
+ enable_ssr=enable_ssr,
69
+ enable_inertia=template.inertia_compatible and "inertia" in framework,
70
+ enable_types=True,
71
+ )
72
+
73
+ generate_project(root_path, context, overwrite=True)