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.
- litestar_vite/__init__.py +54 -4
- litestar_vite/__metadata__.py +12 -7
- litestar_vite/cli.py +1048 -10
- litestar_vite/codegen/__init__.py +48 -0
- litestar_vite/codegen/_export.py +229 -0
- litestar_vite/codegen/_inertia.py +619 -0
- litestar_vite/codegen/_openapi.py +280 -0
- litestar_vite/codegen/_routes.py +720 -0
- litestar_vite/codegen/_ts.py +235 -0
- litestar_vite/codegen/_utils.py +141 -0
- litestar_vite/commands.py +73 -0
- litestar_vite/config/__init__.py +997 -0
- litestar_vite/config/_constants.py +97 -0
- litestar_vite/config/_deploy.py +70 -0
- litestar_vite/config/_inertia.py +241 -0
- litestar_vite/config/_paths.py +63 -0
- litestar_vite/config/_runtime.py +235 -0
- litestar_vite/config/_spa.py +93 -0
- litestar_vite/config/_types.py +94 -0
- litestar_vite/deploy.py +366 -0
- litestar_vite/doctor.py +1181 -0
- litestar_vite/exceptions.py +78 -0
- litestar_vite/executor.py +360 -0
- litestar_vite/handler/__init__.py +9 -0
- litestar_vite/handler/_app.py +612 -0
- litestar_vite/handler/_routing.py +130 -0
- litestar_vite/html_transform.py +569 -0
- litestar_vite/inertia/__init__.py +77 -0
- litestar_vite/inertia/_utils.py +119 -0
- litestar_vite/inertia/exception_handler.py +178 -0
- litestar_vite/inertia/helpers.py +1571 -0
- litestar_vite/inertia/middleware.py +54 -0
- litestar_vite/inertia/plugin.py +199 -0
- litestar_vite/inertia/precognition.py +274 -0
- litestar_vite/inertia/request.py +334 -0
- litestar_vite/inertia/response.py +802 -0
- litestar_vite/inertia/types.py +335 -0
- litestar_vite/loader.py +464 -123
- litestar_vite/plugin/__init__.py +687 -0
- litestar_vite/plugin/_process.py +185 -0
- litestar_vite/plugin/_proxy.py +689 -0
- litestar_vite/plugin/_proxy_headers.py +244 -0
- litestar_vite/plugin/_static.py +37 -0
- litestar_vite/plugin/_utils.py +489 -0
- litestar_vite/py.typed +0 -0
- litestar_vite/scaffolding/__init__.py +20 -0
- litestar_vite/scaffolding/generator.py +270 -0
- litestar_vite/scaffolding/templates.py +437 -0
- litestar_vite/templates/__init__.py +0 -0
- litestar_vite/templates/addons/tailwindcss/tailwind.css.j2 +1 -0
- litestar_vite/templates/angular/index.html.j2 +12 -0
- litestar_vite/templates/angular/openapi-ts.config.ts.j2 +18 -0
- litestar_vite/templates/angular/package.json.j2 +36 -0
- litestar_vite/templates/angular/src/app/app.component.css.j2 +3 -0
- litestar_vite/templates/angular/src/app/app.component.html.j2 +1 -0
- litestar_vite/templates/angular/src/app/app.component.ts.j2 +9 -0
- litestar_vite/templates/angular/src/app/app.config.ts.j2 +5 -0
- litestar_vite/templates/angular/src/main.ts.j2 +9 -0
- litestar_vite/templates/angular/src/styles.css.j2 +9 -0
- litestar_vite/templates/angular/tsconfig.app.json.j2 +34 -0
- litestar_vite/templates/angular/tsconfig.json.j2 +20 -0
- litestar_vite/templates/angular/vite.config.ts.j2 +21 -0
- litestar_vite/templates/angular-cli/.postcssrc.json.j2 +5 -0
- litestar_vite/templates/angular-cli/angular.json.j2 +36 -0
- litestar_vite/templates/angular-cli/openapi-ts.config.ts.j2 +18 -0
- litestar_vite/templates/angular-cli/package.json.j2 +28 -0
- litestar_vite/templates/angular-cli/proxy.conf.json.j2 +18 -0
- litestar_vite/templates/angular-cli/src/app/app.component.css.j2 +3 -0
- litestar_vite/templates/angular-cli/src/app/app.component.html.j2 +1 -0
- litestar_vite/templates/angular-cli/src/app/app.component.ts.j2 +9 -0
- litestar_vite/templates/angular-cli/src/app/app.config.ts.j2 +5 -0
- litestar_vite/templates/angular-cli/src/index.html.j2 +13 -0
- litestar_vite/templates/angular-cli/src/main.ts.j2 +6 -0
- litestar_vite/templates/angular-cli/src/styles.css.j2 +10 -0
- litestar_vite/templates/angular-cli/tailwind.config.js.j2 +4 -0
- litestar_vite/templates/angular-cli/tsconfig.app.json.j2 +16 -0
- litestar_vite/templates/angular-cli/tsconfig.json.j2 +26 -0
- litestar_vite/templates/angular-cli/tsconfig.spec.json.j2 +9 -0
- litestar_vite/templates/astro/astro.config.mjs.j2 +28 -0
- litestar_vite/templates/astro/openapi-ts.config.ts.j2 +15 -0
- litestar_vite/templates/astro/src/layouts/Layout.astro.j2 +63 -0
- litestar_vite/templates/astro/src/pages/index.astro.j2 +36 -0
- litestar_vite/templates/astro/src/styles/global.css.j2 +1 -0
- litestar_vite/templates/base/.gitignore.j2 +42 -0
- litestar_vite/templates/base/openapi-ts.config.ts.j2 +15 -0
- litestar_vite/templates/base/package.json.j2 +39 -0
- litestar_vite/templates/base/resources/vite-env.d.ts.j2 +1 -0
- litestar_vite/templates/base/tsconfig.json.j2 +37 -0
- litestar_vite/templates/htmx/src/main.js.j2 +8 -0
- litestar_vite/templates/htmx/templates/base.html.j2.j2 +56 -0
- litestar_vite/templates/htmx/templates/index.html.j2.j2 +13 -0
- litestar_vite/templates/htmx/vite.config.ts.j2 +40 -0
- litestar_vite/templates/nuxt/app.vue.j2 +29 -0
- litestar_vite/templates/nuxt/composables/useApi.ts.j2 +33 -0
- litestar_vite/templates/nuxt/nuxt.config.ts.j2 +31 -0
- litestar_vite/templates/nuxt/openapi-ts.config.ts.j2 +15 -0
- litestar_vite/templates/nuxt/pages/index.vue.j2 +54 -0
- litestar_vite/templates/react/index.html.j2 +13 -0
- litestar_vite/templates/react/src/App.css.j2 +56 -0
- litestar_vite/templates/react/src/App.tsx.j2 +19 -0
- litestar_vite/templates/react/src/main.tsx.j2 +10 -0
- litestar_vite/templates/react/vite.config.ts.j2 +39 -0
- litestar_vite/templates/react-inertia/index.html.j2 +14 -0
- litestar_vite/templates/react-inertia/package.json.j2 +47 -0
- litestar_vite/templates/react-inertia/resources/App.css.j2 +68 -0
- litestar_vite/templates/react-inertia/resources/main.tsx.j2 +17 -0
- litestar_vite/templates/react-inertia/resources/pages/Home.tsx.j2 +18 -0
- litestar_vite/templates/react-inertia/resources/ssr.tsx.j2 +19 -0
- litestar_vite/templates/react-inertia/vite.config.ts.j2 +59 -0
- litestar_vite/templates/react-router/index.html.j2 +12 -0
- litestar_vite/templates/react-router/src/App.css.j2 +17 -0
- litestar_vite/templates/react-router/src/App.tsx.j2 +7 -0
- litestar_vite/templates/react-router/src/main.tsx.j2 +10 -0
- litestar_vite/templates/react-router/vite.config.ts.j2 +39 -0
- litestar_vite/templates/react-tanstack/index.html.j2 +12 -0
- litestar_vite/templates/react-tanstack/openapi-ts.config.ts.j2 +18 -0
- litestar_vite/templates/react-tanstack/src/App.css.j2 +17 -0
- litestar_vite/templates/react-tanstack/src/main.tsx.j2 +21 -0
- litestar_vite/templates/react-tanstack/src/routeTree.gen.ts.j2 +7 -0
- litestar_vite/templates/react-tanstack/src/routes/__root.tsx.j2 +9 -0
- litestar_vite/templates/react-tanstack/src/routes/books.tsx.j2 +9 -0
- litestar_vite/templates/react-tanstack/src/routes/index.tsx.j2 +9 -0
- litestar_vite/templates/react-tanstack/vite.config.ts.j2 +39 -0
- litestar_vite/templates/svelte/index.html.j2 +13 -0
- litestar_vite/templates/svelte/src/App.svelte.j2 +30 -0
- litestar_vite/templates/svelte/src/app.css.j2 +45 -0
- litestar_vite/templates/svelte/src/main.ts.j2 +8 -0
- litestar_vite/templates/svelte/src/vite-env.d.ts.j2 +2 -0
- litestar_vite/templates/svelte/svelte.config.js.j2 +5 -0
- litestar_vite/templates/svelte/vite.config.ts.j2 +39 -0
- litestar_vite/templates/svelte-inertia/index.html.j2 +14 -0
- litestar_vite/templates/svelte-inertia/resources/app.css.j2 +21 -0
- litestar_vite/templates/svelte-inertia/resources/main.ts.j2 +11 -0
- litestar_vite/templates/svelte-inertia/resources/pages/Home.svelte.j2 +43 -0
- litestar_vite/templates/svelte-inertia/resources/vite-env.d.ts.j2 +2 -0
- litestar_vite/templates/svelte-inertia/svelte.config.js.j2 +5 -0
- litestar_vite/templates/svelte-inertia/vite.config.ts.j2 +37 -0
- litestar_vite/templates/sveltekit/openapi-ts.config.ts.j2 +15 -0
- litestar_vite/templates/sveltekit/src/app.css.j2 +40 -0
- litestar_vite/templates/sveltekit/src/app.html.j2 +12 -0
- litestar_vite/templates/sveltekit/src/hooks.server.ts.j2 +55 -0
- litestar_vite/templates/sveltekit/src/routes/+layout.svelte.j2 +12 -0
- litestar_vite/templates/sveltekit/src/routes/+page.svelte.j2 +34 -0
- litestar_vite/templates/sveltekit/svelte.config.js.j2 +12 -0
- litestar_vite/templates/sveltekit/tsconfig.json.j2 +14 -0
- litestar_vite/templates/sveltekit/vite.config.ts.j2 +31 -0
- litestar_vite/templates/vue/env.d.ts.j2 +7 -0
- litestar_vite/templates/vue/index.html.j2 +13 -0
- litestar_vite/templates/vue/src/App.vue.j2 +28 -0
- litestar_vite/templates/vue/src/main.ts.j2 +5 -0
- litestar_vite/templates/vue/src/style.css.j2 +45 -0
- litestar_vite/templates/vue/vite.config.ts.j2 +39 -0
- litestar_vite/templates/vue-inertia/env.d.ts.j2 +7 -0
- litestar_vite/templates/vue-inertia/index.html.j2 +14 -0
- litestar_vite/templates/vue-inertia/package.json.j2 +50 -0
- litestar_vite/templates/vue-inertia/resources/main.ts.j2 +18 -0
- litestar_vite/templates/vue-inertia/resources/pages/Home.vue.j2 +22 -0
- litestar_vite/templates/vue-inertia/resources/ssr.ts.j2 +21 -0
- litestar_vite/templates/vue-inertia/resources/style.css.j2 +21 -0
- litestar_vite/templates/vue-inertia/vite.config.ts.j2 +59 -0
- litestar_vite-0.15.0.dist-info/METADATA +230 -0
- litestar_vite-0.15.0.dist-info/RECORD +164 -0
- {litestar_vite-0.1.1.dist-info → litestar_vite-0.15.0.dist-info}/WHEEL +1 -1
- litestar_vite/config.py +0 -100
- litestar_vite/plugin.py +0 -45
- litestar_vite/template_engine.py +0 -103
- litestar_vite-0.1.1.dist-info/METADATA +0 -68
- litestar_vite-0.1.1.dist-info/RECORD +0 -11
- {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)
|