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
litestar_vite/doctor.py
ADDED
|
@@ -0,0 +1,1181 @@
|
|
|
1
|
+
"""Vite Doctor - Diagnostic Tool.
|
|
2
|
+
|
|
3
|
+
This module provides a best-effort diagnostic utility that checks Litestar ↔ Vite configuration alignment.
|
|
4
|
+
Regex patterns used for vite.config parsing are compiled at import time to avoid repeated compilation overhead.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import re
|
|
9
|
+
import socket
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import TYPE_CHECKING, Any, Literal
|
|
13
|
+
|
|
14
|
+
from litestar.serialization import decode_json, encode_json
|
|
15
|
+
from rich.console import Console, Group
|
|
16
|
+
from rich.panel import Panel
|
|
17
|
+
from rich.prompt import Confirm
|
|
18
|
+
from rich.syntax import Syntax
|
|
19
|
+
from rich.table import Table
|
|
20
|
+
|
|
21
|
+
from litestar_vite.config import ExternalDevServer, TypeGenConfig
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from litestar_vite.config import ViteConfig
|
|
25
|
+
|
|
26
|
+
console = Console()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _str_list_factory() -> list[str]:
|
|
30
|
+
"""Return an empty ``list[str]`` (typed for pyright).
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
An empty list.
|
|
34
|
+
"""
|
|
35
|
+
return []
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
_VITE_CONFIG_PATTERNS: dict[str, re.Pattern[str]] = {
|
|
39
|
+
"asset_url": re.compile(r"""assetUrl\s*:\s*['"]([^'"]+)['"]"""),
|
|
40
|
+
"bundle_dir": re.compile(r"""bundleDir\s*:\s*['"]([^'"]+)['"]"""),
|
|
41
|
+
"resource_dir": re.compile(r"""resourceDir\s*:\s*['"]([^'"]+)['"]"""),
|
|
42
|
+
"static_dir": re.compile(r"""staticDir\s*:\s*['"]([^'"]+)['"]"""),
|
|
43
|
+
"hot_file": re.compile(r"""hotFile\s*:\s*['"]([^'"]+)['"]"""),
|
|
44
|
+
"inertia_mode": re.compile(r"""inertiaMode\s*:\s*(true|false)"""),
|
|
45
|
+
"types_enabled": re.compile(r"""types\s*:\s*{\s*enabled\s*:\s*(true|false)"""),
|
|
46
|
+
"types_output": re.compile(r"""output\s*:\s*['"]([^'"]+)['"]"""),
|
|
47
|
+
"types_openapi": re.compile(r"""openapiPath\s*:\s*['"]([^'"]+)['"]"""),
|
|
48
|
+
"types_routes": re.compile(r"""routesPath\s*:\s*['"]([^'"]+)['"]"""),
|
|
49
|
+
"types_generate_zod": re.compile(r"""generateZod\s*:\s*(true|false)"""),
|
|
50
|
+
"types_generate_sdk": re.compile(r"""generateSdk\s*:\s*(true|false)"""),
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
_LITESTAR_CONFIG_START = re.compile(r"\blitestar\s*\(\s*{", re.MULTILINE)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _format_ts_literal(value: Any) -> str:
|
|
57
|
+
"""Format a Python value as a TypeScript literal for basic primitives.
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
A TypeScript literal string.
|
|
61
|
+
"""
|
|
62
|
+
if isinstance(value, bool):
|
|
63
|
+
return "true" if value else "false"
|
|
64
|
+
if isinstance(value, (int, float)):
|
|
65
|
+
return str(value)
|
|
66
|
+
s = str(value).replace("\\", "\\\\").replace("'", "\\'")
|
|
67
|
+
return f"'{s}'"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _extract_braced_block(source: str, open_brace_index: int) -> tuple[str, int, int] | None:
|
|
71
|
+
"""Extract a balanced braced block from ``source`` starting at ``open_brace_index``.
|
|
72
|
+
|
|
73
|
+
Best-effort parser intended for typical vite.config.* formatting. Ignores braces
|
|
74
|
+
inside quoted strings.
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
A tuple of (extracted block, start index, end index), or None if not found.
|
|
78
|
+
"""
|
|
79
|
+
if open_brace_index < 0 or open_brace_index >= len(source) or source[open_brace_index] != "{":
|
|
80
|
+
return None
|
|
81
|
+
|
|
82
|
+
depth = 0
|
|
83
|
+
in_string: str | None = None
|
|
84
|
+
escape = False
|
|
85
|
+
|
|
86
|
+
for i in range(open_brace_index, len(source)):
|
|
87
|
+
ch = source[i]
|
|
88
|
+
if in_string is not None:
|
|
89
|
+
if escape:
|
|
90
|
+
escape = False
|
|
91
|
+
continue
|
|
92
|
+
if ch == "\\":
|
|
93
|
+
escape = True
|
|
94
|
+
continue
|
|
95
|
+
if ch == in_string:
|
|
96
|
+
in_string = None
|
|
97
|
+
continue
|
|
98
|
+
|
|
99
|
+
if ch in {"'", '"', "`"}:
|
|
100
|
+
in_string = ch
|
|
101
|
+
continue
|
|
102
|
+
|
|
103
|
+
if ch == "{":
|
|
104
|
+
depth += 1
|
|
105
|
+
continue
|
|
106
|
+
if ch == "}":
|
|
107
|
+
depth -= 1
|
|
108
|
+
if depth == 0:
|
|
109
|
+
return source[open_brace_index : i + 1], open_brace_index, i + 1
|
|
110
|
+
|
|
111
|
+
return None
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _rel_to_root(path: Path | None, root: Path) -> str:
|
|
115
|
+
"""Return a stable string representation of ``path`` relative to ``root`` when possible.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
path: Candidate path to render.
|
|
119
|
+
root: Root directory for relative rendering.
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
A relative path string when ``path`` is under ``root``, otherwise a short fallback representation.
|
|
123
|
+
"""
|
|
124
|
+
if path is None:
|
|
125
|
+
return ""
|
|
126
|
+
try:
|
|
127
|
+
return str(path.resolve().relative_to(root.resolve()))
|
|
128
|
+
except ValueError:
|
|
129
|
+
parts = path.resolve().parts
|
|
130
|
+
return "/".join(parts[-3:])
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@dataclass
|
|
134
|
+
class DoctorIssue:
|
|
135
|
+
"""Represents a detected configuration issue."""
|
|
136
|
+
|
|
137
|
+
check: str
|
|
138
|
+
severity: Literal["error", "warning"]
|
|
139
|
+
message: str
|
|
140
|
+
fix_hint: str
|
|
141
|
+
auto_fixable: bool = False
|
|
142
|
+
context: dict[str, Any] | None = None
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
@dataclass
|
|
146
|
+
class ParsedViteConfig:
|
|
147
|
+
"""Parsed values from vite.config.* file."""
|
|
148
|
+
|
|
149
|
+
path: Path
|
|
150
|
+
content: str
|
|
151
|
+
litestar_config_block: str | None = None
|
|
152
|
+
has_litestar_config: bool = False
|
|
153
|
+
asset_url: str | None = None
|
|
154
|
+
bundle_dir: str | None = None
|
|
155
|
+
resource_dir: str | None = None
|
|
156
|
+
static_dir: str | None = None
|
|
157
|
+
hot_file: str | None = None
|
|
158
|
+
inertia_mode: bool | None = None
|
|
159
|
+
types_setting: Literal["auto"] | bool | None = None
|
|
160
|
+
inputs: list[str] = field(default_factory=_str_list_factory)
|
|
161
|
+
types_enabled: bool | None = None
|
|
162
|
+
types_output: str | None = None
|
|
163
|
+
types_openapi_path: str | None = None
|
|
164
|
+
types_routes_path: str | None = None
|
|
165
|
+
types_generate_zod: bool | None = None
|
|
166
|
+
types_generate_sdk: bool | None = None
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
class ViteDoctor:
|
|
170
|
+
"""Diagnose and fix Vite configuration issues."""
|
|
171
|
+
|
|
172
|
+
def __init__(self, config: "ViteConfig", verbose: bool = False) -> None:
|
|
173
|
+
self.config = config
|
|
174
|
+
self.verbose = verbose
|
|
175
|
+
self.issues: list[DoctorIssue] = []
|
|
176
|
+
self.vite_config_path: Path | None = None
|
|
177
|
+
self.parsed_config: ParsedViteConfig | None = None
|
|
178
|
+
self.bridge_path: Path | None = None
|
|
179
|
+
self.bridge_config: dict[str, Any] | None = None
|
|
180
|
+
|
|
181
|
+
def run(
|
|
182
|
+
self, fix: bool = False, no_prompt: bool = False, *, show_config: bool = False, runtime_checks: bool = False
|
|
183
|
+
) -> bool:
|
|
184
|
+
"""Run diagnostics and optionally fix issues.
|
|
185
|
+
|
|
186
|
+
When ``fix=True``, auto-fixable issues may be applied and the checks will be run again to produce an accurate
|
|
187
|
+
final status.
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
True if healthy (after fixes), False if issues remain.
|
|
191
|
+
"""
|
|
192
|
+
self.issues = []
|
|
193
|
+
self.vite_config_path = None
|
|
194
|
+
self.parsed_config = None
|
|
195
|
+
self.bridge_path = None
|
|
196
|
+
self.bridge_config = None
|
|
197
|
+
|
|
198
|
+
console.rule("[yellow]Vite Doctor Diagnostics[/]", align="left")
|
|
199
|
+
|
|
200
|
+
self._locate_vite_config()
|
|
201
|
+
if not self.vite_config_path or not self.parsed_config:
|
|
202
|
+
console.print("[red]✗ Could not locate or parse vite.config.* file[/]")
|
|
203
|
+
return False
|
|
204
|
+
|
|
205
|
+
self._locate_bridge_file()
|
|
206
|
+
self._maybe_load_bridge_config()
|
|
207
|
+
|
|
208
|
+
self._print_config_snapshot(show_bridge=show_config)
|
|
209
|
+
|
|
210
|
+
self._check_litestar_plugin_config()
|
|
211
|
+
self._check_bridge_file()
|
|
212
|
+
self._check_paths_exist()
|
|
213
|
+
self._check_asset_url()
|
|
214
|
+
self._check_hot_file()
|
|
215
|
+
self._check_bundle_dir()
|
|
216
|
+
self._check_resource_dir()
|
|
217
|
+
self._check_static_dir()
|
|
218
|
+
self._check_inertia_mode()
|
|
219
|
+
self._check_types_setting_alignment()
|
|
220
|
+
self._check_input_paths()
|
|
221
|
+
self._check_typegen_paths()
|
|
222
|
+
self._check_typegen_flags()
|
|
223
|
+
self._check_plugin_spread()
|
|
224
|
+
self._check_dist_files()
|
|
225
|
+
self._check_hotfile_presence()
|
|
226
|
+
self._check_manifest_presence()
|
|
227
|
+
self._check_typegen_artifacts()
|
|
228
|
+
self._check_env_alignment()
|
|
229
|
+
|
|
230
|
+
self._check_node_modules()
|
|
231
|
+
if runtime_checks:
|
|
232
|
+
self._check_hotfile_presence()
|
|
233
|
+
self._check_vite_server_reachable()
|
|
234
|
+
|
|
235
|
+
self._print_report()
|
|
236
|
+
|
|
237
|
+
errors = [i for i in self.issues if i.severity == "error"]
|
|
238
|
+
warnings = [i for i in self.issues if i.severity != "error"]
|
|
239
|
+
|
|
240
|
+
if not self.issues:
|
|
241
|
+
console.print("\n[green]✓ No issues found. Configuration looks healthy.[/]")
|
|
242
|
+
return True
|
|
243
|
+
|
|
244
|
+
if not errors and warnings:
|
|
245
|
+
console.print(f"\n[green]✓ No errors found.[/] [yellow]{len(warnings)} warning(s) detected.[/]")
|
|
246
|
+
|
|
247
|
+
if fix:
|
|
248
|
+
fixed = self._apply_fixes(no_prompt)
|
|
249
|
+
if not fixed:
|
|
250
|
+
return False
|
|
251
|
+
return self.run(fix=False, no_prompt=no_prompt, show_config=show_config)
|
|
252
|
+
|
|
253
|
+
return not errors
|
|
254
|
+
|
|
255
|
+
def _locate_vite_config(self) -> None:
|
|
256
|
+
"""Find and parse the vite.config file."""
|
|
257
|
+
root = self.config.root_dir or Path.cwd()
|
|
258
|
+
for ext in [".ts", ".js", ".mts", ".mjs"]:
|
|
259
|
+
path = root / f"vite.config{ext}"
|
|
260
|
+
if path.exists():
|
|
261
|
+
self.vite_config_path = path
|
|
262
|
+
content = path.read_text()
|
|
263
|
+
self.parsed_config = self._parse_vite_config(path, content)
|
|
264
|
+
if self.verbose:
|
|
265
|
+
console.print(f"[dim]Found config at {path}[/]")
|
|
266
|
+
return
|
|
267
|
+
|
|
268
|
+
def _locate_bridge_file(self) -> None:
|
|
269
|
+
"""Locate the .litestar.json bridge file."""
|
|
270
|
+
root = self.config.root_dir or Path.cwd()
|
|
271
|
+
self.bridge_path = root / ".litestar.json"
|
|
272
|
+
|
|
273
|
+
def _maybe_load_bridge_config(self) -> None:
|
|
274
|
+
"""Load .litestar.json if present."""
|
|
275
|
+
if self.bridge_path is None or not self.bridge_path.exists():
|
|
276
|
+
return
|
|
277
|
+
try:
|
|
278
|
+
decoded = decode_json(self.bridge_path.read_bytes())
|
|
279
|
+
if isinstance(decoded, dict):
|
|
280
|
+
self.bridge_config = decoded
|
|
281
|
+
except Exception as e: # noqa: BLE001 - diagnostic tool
|
|
282
|
+
self.bridge_config = {"__error__": str(e)}
|
|
283
|
+
|
|
284
|
+
def _parse_vite_config(self, path: Path, content: str) -> ParsedViteConfig:
|
|
285
|
+
"""Regex-based parsing of vite.config content.
|
|
286
|
+
|
|
287
|
+
Parsing is restricted to the ``litestar({ ... })`` config block when present to reduce false positives.
|
|
288
|
+
Inputs and type settings are parsed best-effort to allow slightly different formatting styles.
|
|
289
|
+
|
|
290
|
+
Returns:
|
|
291
|
+
ParsedViteConfig instance with extracted values.
|
|
292
|
+
"""
|
|
293
|
+
parsed = ParsedViteConfig(path=path, content=content, inputs=[])
|
|
294
|
+
|
|
295
|
+
config_source = content
|
|
296
|
+
match = _LITESTAR_CONFIG_START.search(content)
|
|
297
|
+
if match:
|
|
298
|
+
open_brace_index = content.find("{", match.start())
|
|
299
|
+
extracted = _extract_braced_block(content, open_brace_index)
|
|
300
|
+
if extracted is not None:
|
|
301
|
+
block, _, _ = extracted
|
|
302
|
+
parsed.litestar_config_block = block
|
|
303
|
+
parsed.has_litestar_config = True
|
|
304
|
+
config_source = block
|
|
305
|
+
|
|
306
|
+
input_single = re.search(r"""input\s*:\s*['"]([^'"]+)['"]""", config_source)
|
|
307
|
+
if input_single:
|
|
308
|
+
parsed.inputs.append(input_single.group(1))
|
|
309
|
+
input_array = re.search(r"""input\s*:\s*\[([^\]]*)\]""", config_source, flags=re.DOTALL)
|
|
310
|
+
if input_array:
|
|
311
|
+
parsed.inputs.extend(re.findall(r"""['"]([^'"]+)['"]""", input_array.group(1)))
|
|
312
|
+
|
|
313
|
+
if re.search(r"""types\s*:\s*['"]auto['"]""", config_source):
|
|
314
|
+
parsed.types_setting = "auto"
|
|
315
|
+
else:
|
|
316
|
+
types_bool = re.search(r"""types\s*:\s*(true|false)\b""", config_source)
|
|
317
|
+
if types_bool:
|
|
318
|
+
parsed.types_setting = types_bool.group(1) == "true"
|
|
319
|
+
|
|
320
|
+
str_key_map: dict[str, str] = {
|
|
321
|
+
"asset_url": "asset_url",
|
|
322
|
+
"bundle_dir": "bundle_dir",
|
|
323
|
+
"resource_dir": "resource_dir",
|
|
324
|
+
"static_dir": "static_dir",
|
|
325
|
+
"hot_file": "hot_file",
|
|
326
|
+
"types_output": "types_output",
|
|
327
|
+
"types_openapi": "types_openapi_path",
|
|
328
|
+
"types_routes": "types_routes_path",
|
|
329
|
+
}
|
|
330
|
+
bool_key_map: dict[str, str] = {
|
|
331
|
+
"inertia_mode": "inertia_mode",
|
|
332
|
+
"types_enabled": "types_enabled",
|
|
333
|
+
"types_generate_zod": "types_generate_zod",
|
|
334
|
+
"types_generate_sdk": "types_generate_sdk",
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
for key, pattern in _VITE_CONFIG_PATTERNS.items():
|
|
338
|
+
found = pattern.search(config_source)
|
|
339
|
+
if not found:
|
|
340
|
+
continue
|
|
341
|
+
val = found.group(1)
|
|
342
|
+
|
|
343
|
+
attr_name = str_key_map.get(key)
|
|
344
|
+
if attr_name is not None:
|
|
345
|
+
setattr(parsed, attr_name, val)
|
|
346
|
+
continue
|
|
347
|
+
|
|
348
|
+
attr_name = bool_key_map.get(key)
|
|
349
|
+
if attr_name is not None:
|
|
350
|
+
setattr(parsed, attr_name, val == "true")
|
|
351
|
+
|
|
352
|
+
return parsed
|
|
353
|
+
|
|
354
|
+
def _apply_write_bridge_fix(self) -> bool:
|
|
355
|
+
if self.bridge_path is None:
|
|
356
|
+
return False
|
|
357
|
+
expected = self._expected_bridge_payload()
|
|
358
|
+
if self.bridge_path.exists():
|
|
359
|
+
bridge_backup = self.bridge_path.with_suffix(self.bridge_path.suffix + ".bak")
|
|
360
|
+
bridge_backup.write_bytes(self.bridge_path.read_bytes())
|
|
361
|
+
console.print(f"[dim]Created backup at {bridge_backup}[/]")
|
|
362
|
+
self.bridge_path.write_bytes(encode_json(expected))
|
|
363
|
+
console.print("[green]✓ Wrote .litestar.json[/]")
|
|
364
|
+
return True
|
|
365
|
+
|
|
366
|
+
def _apply_vite_key_fix(self, content: str, *, key: str, expected: Any) -> tuple[str, bool]:
|
|
367
|
+
expected_literal = _format_ts_literal(expected)
|
|
368
|
+
expected_str = str(expected)
|
|
369
|
+
expected_bool = "true" if expected is True else "false" if expected is False else None
|
|
370
|
+
|
|
371
|
+
bool_pattern = rf"({key}\s*:\s*)(true|false)\b"
|
|
372
|
+
if expected_bool is not None and re.search(bool_pattern, content):
|
|
373
|
+
content = re.sub(bool_pattern, rf"\\g<1>{expected_bool}", content, count=1)
|
|
374
|
+
return content, True
|
|
375
|
+
|
|
376
|
+
quoted_pattern = rf"({key}\s*:\s*['\"])([^'\"]+)(['\"])"
|
|
377
|
+
if re.search(quoted_pattern, content):
|
|
378
|
+
content = re.sub(quoted_pattern, rf"\\g<1>{expected_str}\\g<3>", content, count=1)
|
|
379
|
+
return content, True
|
|
380
|
+
|
|
381
|
+
insert_match = _LITESTAR_CONFIG_START.search(content)
|
|
382
|
+
if insert_match:
|
|
383
|
+
brace_index = content.find("{", insert_match.start())
|
|
384
|
+
line_start = content.rfind("\n", 0, brace_index) + 1
|
|
385
|
+
indent_match = re.match(r"\s*", content[line_start:brace_index])
|
|
386
|
+
indent = indent_match.group(0) if indent_match else ""
|
|
387
|
+
insertion = f"\n{indent} {key}: {expected_literal},"
|
|
388
|
+
content = content[: brace_index + 1] + insertion + content[brace_index + 1 :]
|
|
389
|
+
return content, True
|
|
390
|
+
|
|
391
|
+
return content, False
|
|
392
|
+
|
|
393
|
+
def _resolve_to_root(self, path: Path) -> Path:
|
|
394
|
+
root = self.config.root_dir or Path.cwd()
|
|
395
|
+
return path if path.is_absolute() else (root / path)
|
|
396
|
+
|
|
397
|
+
def _check_litestar_plugin_config(self) -> None:
|
|
398
|
+
"""Ensure the vite.config includes a litestar({ ... }) plugin config."""
|
|
399
|
+
if not self.parsed_config:
|
|
400
|
+
return
|
|
401
|
+
if self.parsed_config.has_litestar_config:
|
|
402
|
+
return
|
|
403
|
+
|
|
404
|
+
self.issues.append(
|
|
405
|
+
DoctorIssue(
|
|
406
|
+
check="Missing litestar() Plugin Config",
|
|
407
|
+
severity="error",
|
|
408
|
+
message=f"{self.parsed_config.path.name} does not appear to contain a litestar({{...}}) call",
|
|
409
|
+
fix_hint=(
|
|
410
|
+
"Add the plugin to vite.config, e.g. `import litestar from 'litestar-vite-plugin'` and "
|
|
411
|
+
"`plugins: [litestar({ input: ['src/main.ts'] })]`"
|
|
412
|
+
),
|
|
413
|
+
auto_fixable=False,
|
|
414
|
+
)
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
def _expected_bridge_payload(self) -> dict[str, Any]:
|
|
418
|
+
"""Build the expected .litestar.json payload from the active Python config.
|
|
419
|
+
|
|
420
|
+
Returns:
|
|
421
|
+
A dictionary representing the expected bridge configuration.
|
|
422
|
+
"""
|
|
423
|
+
types = self.config.types if isinstance(self.config.types, TypeGenConfig) else None
|
|
424
|
+
deploy = self.config.deploy_config
|
|
425
|
+
return {
|
|
426
|
+
"assetUrl": self.config.asset_url,
|
|
427
|
+
"deployAssetUrl": deploy.asset_url if deploy is not None and deploy.asset_url else None,
|
|
428
|
+
"bundleDir": str(self.config.bundle_dir),
|
|
429
|
+
"hotFile": self.config.hot_file,
|
|
430
|
+
"resourceDir": str(self.config.resource_dir),
|
|
431
|
+
"staticDir": str(self.config.static_dir),
|
|
432
|
+
"manifest": self.config.manifest_name,
|
|
433
|
+
"mode": self.config.mode,
|
|
434
|
+
"proxyMode": self.config.proxy_mode,
|
|
435
|
+
"port": self.config.port,
|
|
436
|
+
"host": self.config.host,
|
|
437
|
+
"ssrOutDir": str(self.config.ssr_output_dir) if self.config.ssr_output_dir else None,
|
|
438
|
+
"types": (
|
|
439
|
+
{
|
|
440
|
+
"enabled": True,
|
|
441
|
+
"output": str(types.output),
|
|
442
|
+
"openapiPath": str(types.openapi_path),
|
|
443
|
+
"routesPath": str(types.routes_path),
|
|
444
|
+
"generateZod": types.generate_zod,
|
|
445
|
+
"generateSdk": types.generate_sdk,
|
|
446
|
+
}
|
|
447
|
+
if types
|
|
448
|
+
else None
|
|
449
|
+
),
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
def _check_bridge_file(self) -> None:
|
|
453
|
+
"""Validate presence and consistency of .litestar.json when enabled."""
|
|
454
|
+
if not self.config.set_environment:
|
|
455
|
+
return
|
|
456
|
+
if self.bridge_path is None:
|
|
457
|
+
return
|
|
458
|
+
|
|
459
|
+
if not self.bridge_path.exists():
|
|
460
|
+
self.issues.append(
|
|
461
|
+
DoctorIssue(
|
|
462
|
+
check="Missing .litestar.json",
|
|
463
|
+
severity="warning",
|
|
464
|
+
message=f"{self.bridge_path} not found (Vite plugin auto-config may fall back to defaults)",
|
|
465
|
+
fix_hint=(
|
|
466
|
+
"Start your app (`litestar run`) or `litestar assets serve` to generate it, or run "
|
|
467
|
+
"`litestar assets doctor --fix` to write it now"
|
|
468
|
+
),
|
|
469
|
+
auto_fixable=True,
|
|
470
|
+
context={"action": "write_bridge"},
|
|
471
|
+
)
|
|
472
|
+
)
|
|
473
|
+
return
|
|
474
|
+
|
|
475
|
+
if self.bridge_config is None:
|
|
476
|
+
return
|
|
477
|
+
|
|
478
|
+
if "__error__" in self.bridge_config:
|
|
479
|
+
self.issues.append(
|
|
480
|
+
DoctorIssue(
|
|
481
|
+
check="Invalid .litestar.json",
|
|
482
|
+
severity="error",
|
|
483
|
+
message=str(self.bridge_config.get("__error__")),
|
|
484
|
+
fix_hint=(
|
|
485
|
+
"Run `litestar assets doctor --fix` to rewrite it (a .bak backup is created). If you prefer, "
|
|
486
|
+
"starting your app (`litestar run`) will overwrite it on startup when `runtime.set_environment=True`"
|
|
487
|
+
),
|
|
488
|
+
auto_fixable=True,
|
|
489
|
+
context={"action": "write_bridge"},
|
|
490
|
+
)
|
|
491
|
+
)
|
|
492
|
+
return
|
|
493
|
+
|
|
494
|
+
expected = self._expected_bridge_payload()
|
|
495
|
+
mismatched: list[str] = []
|
|
496
|
+
root = self.config.root_dir or Path.cwd()
|
|
497
|
+
for key, exp in expected.items():
|
|
498
|
+
actual = self.bridge_config.get(key)
|
|
499
|
+
if key in {"bundleDir", "resourceDir", "staticDir", "ssrOutDir"}:
|
|
500
|
+
exp_path = (root / exp) if isinstance(exp, str) and exp and not Path(exp).is_absolute() else exp
|
|
501
|
+
act_path = (
|
|
502
|
+
(root / actual) if isinstance(actual, str) and actual and not Path(actual).is_absolute() else actual
|
|
503
|
+
)
|
|
504
|
+
if str(exp_path).rstrip("/") != str(act_path).rstrip("/"):
|
|
505
|
+
mismatched.append(key)
|
|
506
|
+
elif actual != exp:
|
|
507
|
+
mismatched.append(key)
|
|
508
|
+
|
|
509
|
+
if mismatched:
|
|
510
|
+
self.issues.append(
|
|
511
|
+
DoctorIssue(
|
|
512
|
+
check="Stale .litestar.json",
|
|
513
|
+
severity="warning",
|
|
514
|
+
message=f"Bridge config differs from Python config for: {', '.join(sorted(mismatched))}",
|
|
515
|
+
fix_hint=(
|
|
516
|
+
"Run `litestar assets doctor --fix` to rewrite it (a .bak backup is created). "
|
|
517
|
+
"Or just restart your app (`litestar run`) to overwrite it on startup when `runtime.set_environment=True`"
|
|
518
|
+
),
|
|
519
|
+
auto_fixable=True,
|
|
520
|
+
context={"action": "write_bridge"},
|
|
521
|
+
)
|
|
522
|
+
)
|
|
523
|
+
|
|
524
|
+
def _check_paths_exist(self) -> None:
|
|
525
|
+
"""Validate core Python paths exist."""
|
|
526
|
+
root = self.config.root_dir or Path.cwd()
|
|
527
|
+
resource_dir = self._resolve_to_root(self.config.resource_dir)
|
|
528
|
+
static_dir = self._resolve_to_root(self.config.static_dir)
|
|
529
|
+
|
|
530
|
+
if not resource_dir.exists():
|
|
531
|
+
self.issues.append(
|
|
532
|
+
DoctorIssue(
|
|
533
|
+
check="Missing resource_dir",
|
|
534
|
+
severity="error",
|
|
535
|
+
message=f"resource_dir does not exist: {_rel_to_root(resource_dir, root)}",
|
|
536
|
+
fix_hint="Create the directory or update ViteConfig.paths.resource_dir to the correct source folder",
|
|
537
|
+
auto_fixable=False,
|
|
538
|
+
)
|
|
539
|
+
)
|
|
540
|
+
|
|
541
|
+
if not static_dir.exists():
|
|
542
|
+
self.issues.append(
|
|
543
|
+
DoctorIssue(
|
|
544
|
+
check="Missing static_dir",
|
|
545
|
+
severity="warning",
|
|
546
|
+
message=f"static_dir does not exist: {_rel_to_root(static_dir, root)}",
|
|
547
|
+
fix_hint=f"Create the directory or update ViteConfig.paths.static_dir (often `${resource_dir}/public`)",
|
|
548
|
+
auto_fixable=False,
|
|
549
|
+
)
|
|
550
|
+
)
|
|
551
|
+
|
|
552
|
+
def _check_asset_url(self) -> None:
|
|
553
|
+
"""Check if Python asset_url matches JS assetUrl."""
|
|
554
|
+
if not self.parsed_config:
|
|
555
|
+
return
|
|
556
|
+
|
|
557
|
+
py_url = self.config.asset_url
|
|
558
|
+
js_url = self.parsed_config.asset_url
|
|
559
|
+
|
|
560
|
+
py_norm = py_url.rstrip("/")
|
|
561
|
+
js_norm = (js_url or "").rstrip("/")
|
|
562
|
+
|
|
563
|
+
if js_url and py_norm != js_norm:
|
|
564
|
+
self.issues.append(
|
|
565
|
+
DoctorIssue(
|
|
566
|
+
check="Asset URL Mismatch",
|
|
567
|
+
severity="error",
|
|
568
|
+
message=f"Python asset_url '{py_url}' != JS assetUrl '{js_url}'",
|
|
569
|
+
fix_hint=f"Update vite.config to use assetUrl: '{py_url}'",
|
|
570
|
+
auto_fixable=True,
|
|
571
|
+
context={"key": "assetUrl", "expected": py_url},
|
|
572
|
+
)
|
|
573
|
+
)
|
|
574
|
+
|
|
575
|
+
def _check_hot_file(self) -> None:
|
|
576
|
+
"""Check if Python hot_file matches JS hotFile.
|
|
577
|
+
|
|
578
|
+
Litestar's Python config stores ``hot_file`` as a filename (default ``hot``), while the JS plugin commonly
|
|
579
|
+
uses a full path defaulting to ``${bundleDir}/hot``. This check only warns when JS explicitly sets ``hotFile``
|
|
580
|
+
to a value that diverges from the Python expectation.
|
|
581
|
+
"""
|
|
582
|
+
if not self.parsed_config:
|
|
583
|
+
return
|
|
584
|
+
expected_hot = f"{self.config.bundle_dir}/{self.config.hot_file}".replace("//", "/")
|
|
585
|
+
js_hot = self.parsed_config.hot_file
|
|
586
|
+
|
|
587
|
+
if js_hot and js_hot != expected_hot:
|
|
588
|
+
self.issues.append(
|
|
589
|
+
DoctorIssue(
|
|
590
|
+
check="Hot File Mismatch",
|
|
591
|
+
severity="warning",
|
|
592
|
+
message=f"JS hotFile '{js_hot}' differs from Python default '{expected_hot}'",
|
|
593
|
+
fix_hint=f"Update vite.config to use hotFile: '{expected_hot}'",
|
|
594
|
+
auto_fixable=True,
|
|
595
|
+
context={"key": "hotFile", "expected": expected_hot},
|
|
596
|
+
)
|
|
597
|
+
)
|
|
598
|
+
|
|
599
|
+
def _check_bundle_dir(self) -> None:
|
|
600
|
+
"""Check bundle directory match."""
|
|
601
|
+
if not self.parsed_config:
|
|
602
|
+
return
|
|
603
|
+
|
|
604
|
+
py_dir = str(self.config.bundle_dir)
|
|
605
|
+
js_dir = self.parsed_config.bundle_dir
|
|
606
|
+
|
|
607
|
+
if js_dir and py_dir != js_dir:
|
|
608
|
+
self.issues.append(
|
|
609
|
+
DoctorIssue(
|
|
610
|
+
check="Bundle Directory Mismatch",
|
|
611
|
+
severity="error",
|
|
612
|
+
message=f"Python bundle_dir '{py_dir}' != JS bundleDir '{js_dir}'",
|
|
613
|
+
fix_hint=f"Update vite.config to use bundleDir: '{py_dir}'",
|
|
614
|
+
auto_fixable=True,
|
|
615
|
+
context={"key": "bundleDir", "expected": py_dir},
|
|
616
|
+
)
|
|
617
|
+
)
|
|
618
|
+
|
|
619
|
+
def _check_resource_dir(self) -> None:
|
|
620
|
+
"""Check resource directory match when explicitly set in vite.config."""
|
|
621
|
+
if not self.parsed_config:
|
|
622
|
+
return
|
|
623
|
+
|
|
624
|
+
py_dir = str(self.config.resource_dir)
|
|
625
|
+
js_dir = self.parsed_config.resource_dir
|
|
626
|
+
|
|
627
|
+
if js_dir and py_dir != js_dir:
|
|
628
|
+
self.issues.append(
|
|
629
|
+
DoctorIssue(
|
|
630
|
+
check="Resource Directory Mismatch",
|
|
631
|
+
severity="warning",
|
|
632
|
+
message=f"Python resource_dir '{py_dir}' != JS resourceDir '{js_dir}'",
|
|
633
|
+
fix_hint=f"Update vite.config to use resourceDir: '{py_dir}' (or remove it to auto-read from .litestar.json)",
|
|
634
|
+
auto_fixable=True,
|
|
635
|
+
context={"key": "resourceDir", "expected": py_dir},
|
|
636
|
+
)
|
|
637
|
+
)
|
|
638
|
+
|
|
639
|
+
def _check_static_dir(self) -> None:
|
|
640
|
+
"""Check static directory match when explicitly set in vite.config."""
|
|
641
|
+
if not self.parsed_config:
|
|
642
|
+
return
|
|
643
|
+
|
|
644
|
+
py_dir = str(self.config.static_dir)
|
|
645
|
+
js_dir = self.parsed_config.static_dir
|
|
646
|
+
|
|
647
|
+
if js_dir and py_dir != js_dir:
|
|
648
|
+
self.issues.append(
|
|
649
|
+
DoctorIssue(
|
|
650
|
+
check="Static Directory Mismatch",
|
|
651
|
+
severity="warning",
|
|
652
|
+
message=f"Python static_dir '{py_dir}' != JS staticDir '{js_dir}'",
|
|
653
|
+
fix_hint=f"Update vite.config to use staticDir: '{py_dir}' (or remove it to auto-read from .litestar.json)",
|
|
654
|
+
auto_fixable=True,
|
|
655
|
+
context={"key": "staticDir", "expected": py_dir},
|
|
656
|
+
)
|
|
657
|
+
)
|
|
658
|
+
|
|
659
|
+
def _check_inertia_mode(self) -> None:
|
|
660
|
+
"""Warn when vite.config inertiaMode conflicts with Python mode."""
|
|
661
|
+
if not self.parsed_config or self.parsed_config.inertia_mode is None:
|
|
662
|
+
return
|
|
663
|
+
|
|
664
|
+
py_inertia = self.config.mode == "inertia"
|
|
665
|
+
js_inertia = self.parsed_config.inertia_mode
|
|
666
|
+
|
|
667
|
+
if py_inertia != js_inertia:
|
|
668
|
+
self.issues.append(
|
|
669
|
+
DoctorIssue(
|
|
670
|
+
check="Inertia Mode Mismatch",
|
|
671
|
+
severity="warning",
|
|
672
|
+
message=(
|
|
673
|
+
f"Python mode={self.config.mode!r} implies inertiaMode={py_inertia}, "
|
|
674
|
+
f"but vite.config sets inertiaMode={js_inertia}"
|
|
675
|
+
),
|
|
676
|
+
fix_hint="Remove inertiaMode from vite.config to auto-detect, or set it to match your Python mode",
|
|
677
|
+
auto_fixable=False,
|
|
678
|
+
)
|
|
679
|
+
)
|
|
680
|
+
|
|
681
|
+
def _check_types_setting_alignment(self) -> None:
|
|
682
|
+
"""Warn when vite.config explicitly enables/disables types against Python config."""
|
|
683
|
+
if not self.parsed_config:
|
|
684
|
+
return
|
|
685
|
+
|
|
686
|
+
py_types_enabled = isinstance(self.config.types, TypeGenConfig)
|
|
687
|
+
js_setting = self.parsed_config.types_setting
|
|
688
|
+
|
|
689
|
+
if py_types_enabled and js_setting is False:
|
|
690
|
+
self.issues.append(
|
|
691
|
+
DoctorIssue(
|
|
692
|
+
check="TypeGen Disabled in vite.config",
|
|
693
|
+
severity="warning",
|
|
694
|
+
message="Python types are enabled, but vite.config sets `types: false`",
|
|
695
|
+
fix_hint="Remove `types: false` or set `types: 'auto'` to read from .litestar.json",
|
|
696
|
+
auto_fixable=False,
|
|
697
|
+
)
|
|
698
|
+
)
|
|
699
|
+
|
|
700
|
+
if not py_types_enabled and js_setting is True:
|
|
701
|
+
self.issues.append(
|
|
702
|
+
DoctorIssue(
|
|
703
|
+
check="TypeGen Enabled in vite.config",
|
|
704
|
+
severity="warning",
|
|
705
|
+
message="Python types are disabled, but vite.config sets `types: true`",
|
|
706
|
+
fix_hint="Disable types in vite.config, or enable TypeGenConfig in Python to keep both sides aligned",
|
|
707
|
+
auto_fixable=False,
|
|
708
|
+
)
|
|
709
|
+
)
|
|
710
|
+
|
|
711
|
+
def _check_input_paths(self) -> None:
|
|
712
|
+
"""Check that configured entrypoints exist on disk (best-effort)."""
|
|
713
|
+
if not self.parsed_config or not self.parsed_config.inputs:
|
|
714
|
+
return
|
|
715
|
+
|
|
716
|
+
root = self.config.root_dir or Path.cwd()
|
|
717
|
+
missing: list[str] = []
|
|
718
|
+
for input_path in self.parsed_config.inputs:
|
|
719
|
+
p = Path(input_path)
|
|
720
|
+
resolved = p if p.is_absolute() else (root / p)
|
|
721
|
+
if not resolved.exists():
|
|
722
|
+
missing.append(input_path)
|
|
723
|
+
|
|
724
|
+
if missing:
|
|
725
|
+
self.issues.append(
|
|
726
|
+
DoctorIssue(
|
|
727
|
+
check="Missing Vite Inputs",
|
|
728
|
+
severity="error",
|
|
729
|
+
message=f"Entry point(s) not found: {', '.join(missing)}",
|
|
730
|
+
fix_hint="Update the `input` paths in vite.config or ensure the files exist under your frontend source folder",
|
|
731
|
+
auto_fixable=False,
|
|
732
|
+
)
|
|
733
|
+
)
|
|
734
|
+
|
|
735
|
+
def _check_typegen_paths(self) -> None:
|
|
736
|
+
"""Check type generation paths when TypeGen is enabled on both sides.
|
|
737
|
+
|
|
738
|
+
Only compares OpenAPI and routes output paths when the corresponding JS settings are explicitly present to
|
|
739
|
+
avoid warning on JS defaults that may differ in representation (absolute vs relative).
|
|
740
|
+
"""
|
|
741
|
+
if not self.parsed_config:
|
|
742
|
+
return
|
|
743
|
+
|
|
744
|
+
if not isinstance(self.config.types, TypeGenConfig):
|
|
745
|
+
return
|
|
746
|
+
|
|
747
|
+
if self.parsed_config.types_enabled:
|
|
748
|
+
root = self.config.root_dir or Path.cwd()
|
|
749
|
+
|
|
750
|
+
py_openapi_path = self.config.types.openapi_path
|
|
751
|
+
if py_openapi_path is None:
|
|
752
|
+
py_openapi_path = self.config.types.output / "openapi.json"
|
|
753
|
+
js_openapi_path = (
|
|
754
|
+
(root / self.parsed_config.types_openapi_path) if self.parsed_config.types_openapi_path else None
|
|
755
|
+
)
|
|
756
|
+
|
|
757
|
+
if self.parsed_config.types_openapi_path and _rel_to_root(py_openapi_path, root) != _rel_to_root(
|
|
758
|
+
js_openapi_path, root
|
|
759
|
+
):
|
|
760
|
+
self.issues.append(
|
|
761
|
+
DoctorIssue(
|
|
762
|
+
check="TypeGen OpenAPI Path Mismatch",
|
|
763
|
+
severity="warning",
|
|
764
|
+
message=f"Python '{py_openapi_path}' != JS '{js_openapi_path}'",
|
|
765
|
+
fix_hint=f"Update vite.config openapiPath to '{py_openapi_path}'",
|
|
766
|
+
auto_fixable=True,
|
|
767
|
+
context={"key": "openapiPath", "expected": str(py_openapi_path)},
|
|
768
|
+
)
|
|
769
|
+
)
|
|
770
|
+
|
|
771
|
+
py_routes_path = self.config.types.routes_path
|
|
772
|
+
if py_routes_path is None:
|
|
773
|
+
py_routes_path = self.config.types.output / "routes.json"
|
|
774
|
+
js_routes_path = (
|
|
775
|
+
(root / self.parsed_config.types_routes_path) if self.parsed_config.types_routes_path else None
|
|
776
|
+
)
|
|
777
|
+
|
|
778
|
+
if self.parsed_config.types_routes_path and _rel_to_root(py_routes_path, root) != _rel_to_root(
|
|
779
|
+
js_routes_path, root
|
|
780
|
+
):
|
|
781
|
+
self.issues.append(
|
|
782
|
+
DoctorIssue(
|
|
783
|
+
check="TypeGen Routes Path Mismatch",
|
|
784
|
+
severity="warning",
|
|
785
|
+
message=f"Python '{py_routes_path}' != JS '{js_routes_path}'",
|
|
786
|
+
fix_hint=f"Update vite.config routesPath to '{py_routes_path}'",
|
|
787
|
+
auto_fixable=True,
|
|
788
|
+
context={"key": "routesPath", "expected": str(py_routes_path)},
|
|
789
|
+
)
|
|
790
|
+
)
|
|
791
|
+
|
|
792
|
+
def _check_typegen_flags(self) -> None:
|
|
793
|
+
"""Check TypeGen flags when enabled on both sides.
|
|
794
|
+
|
|
795
|
+
Only compares flags that are explicitly set in JS to avoid warning on JS defaults.
|
|
796
|
+
"""
|
|
797
|
+
if not self.parsed_config:
|
|
798
|
+
return
|
|
799
|
+
|
|
800
|
+
if not isinstance(self.config.types, TypeGenConfig):
|
|
801
|
+
return
|
|
802
|
+
|
|
803
|
+
if self.parsed_config.types_enabled:
|
|
804
|
+
py_zod = self.config.types.generate_zod
|
|
805
|
+
js_zod = self.parsed_config.types_generate_zod
|
|
806
|
+
|
|
807
|
+
if js_zod is not None and py_zod != js_zod:
|
|
808
|
+
self.issues.append(
|
|
809
|
+
DoctorIssue(
|
|
810
|
+
check="TypeGen generateZod Mismatch",
|
|
811
|
+
severity="warning",
|
|
812
|
+
message=f"Python generate_zod={py_zod} != JS generateZod={js_zod}",
|
|
813
|
+
fix_hint=f"Update vite.config generateZod to {str(py_zod).lower()}",
|
|
814
|
+
auto_fixable=True,
|
|
815
|
+
context={"key": "generateZod", "expected": str(py_zod).lower()},
|
|
816
|
+
)
|
|
817
|
+
)
|
|
818
|
+
|
|
819
|
+
py_sdk = self.config.types.generate_sdk
|
|
820
|
+
js_sdk = self.parsed_config.types_generate_sdk
|
|
821
|
+
|
|
822
|
+
if js_sdk is not None and py_sdk != js_sdk:
|
|
823
|
+
self.issues.append(
|
|
824
|
+
DoctorIssue(
|
|
825
|
+
check="TypeGen generateSdk Mismatch",
|
|
826
|
+
severity="warning",
|
|
827
|
+
message=f"Python generate_sdk={py_sdk} != JS generateSdk={js_sdk}",
|
|
828
|
+
fix_hint=f"Update vite.config generateSdk to {str(py_sdk).lower()}",
|
|
829
|
+
auto_fixable=True,
|
|
830
|
+
context={"key": "generateSdk", "expected": str(py_sdk).lower()},
|
|
831
|
+
)
|
|
832
|
+
)
|
|
833
|
+
|
|
834
|
+
def _check_plugin_spread(self) -> None:
|
|
835
|
+
"""No-op check kept for backwards compatibility.
|
|
836
|
+
|
|
837
|
+
This check is intentionally disabled because Vite plugin arrays allow nested arrays; reliably detecting
|
|
838
|
+
missing spread introduces false positives.
|
|
839
|
+
"""
|
|
840
|
+
return
|
|
841
|
+
|
|
842
|
+
def _check_dist_files(self) -> None:
|
|
843
|
+
"""Verify JS plugin dist files exist."""
|
|
844
|
+
root = self.config.root_dir or Path.cwd()
|
|
845
|
+
pkg_paths = [
|
|
846
|
+
root / "node_modules" / "@litestar" / "vite-plugin" / "dist",
|
|
847
|
+
root / "node_modules" / "litestar-vite-plugin" / "dist",
|
|
848
|
+
]
|
|
849
|
+
|
|
850
|
+
dist_path = next((p for p in pkg_paths if p.exists()), None)
|
|
851
|
+
|
|
852
|
+
if dist_path is None:
|
|
853
|
+
self.issues.append(
|
|
854
|
+
DoctorIssue(
|
|
855
|
+
check="Plugin Dist Missing",
|
|
856
|
+
severity="warning",
|
|
857
|
+
message="Could not find @litestar/vite-plugin dist files in node_modules",
|
|
858
|
+
fix_hint="Run `litestar assets install` to install frontend dependencies",
|
|
859
|
+
auto_fixable=False,
|
|
860
|
+
)
|
|
861
|
+
)
|
|
862
|
+
return
|
|
863
|
+
|
|
864
|
+
required_files = ["index.js", "install-hint.js", "litestar-meta.js"]
|
|
865
|
+
missing = [f for f in required_files if not (dist_path / "js" / f).exists()]
|
|
866
|
+
|
|
867
|
+
if missing:
|
|
868
|
+
self.issues.append(
|
|
869
|
+
DoctorIssue(
|
|
870
|
+
check="Corrupt Plugin Installation",
|
|
871
|
+
severity="error",
|
|
872
|
+
message=f"Missing required plugin files: {', '.join(missing)}",
|
|
873
|
+
fix_hint="Reinstall frontend dependencies with `litestar assets install` (or reinstall your package manager deps)",
|
|
874
|
+
auto_fixable=False,
|
|
875
|
+
)
|
|
876
|
+
)
|
|
877
|
+
|
|
878
|
+
def _check_node_modules(self) -> None:
|
|
879
|
+
"""Check if node_modules directory exists."""
|
|
880
|
+
root = self.config.root_dir or Path.cwd()
|
|
881
|
+
node_modules = root / "node_modules"
|
|
882
|
+
|
|
883
|
+
if not node_modules.exists():
|
|
884
|
+
self.issues.append(
|
|
885
|
+
DoctorIssue(
|
|
886
|
+
check="Node Modules Missing",
|
|
887
|
+
severity="error",
|
|
888
|
+
message="node_modules directory not found",
|
|
889
|
+
fix_hint="Run `litestar assets install` to install frontend dependencies",
|
|
890
|
+
auto_fixable=False,
|
|
891
|
+
)
|
|
892
|
+
)
|
|
893
|
+
elif self.verbose:
|
|
894
|
+
console.print("[dim]✓ node_modules directory exists[/]")
|
|
895
|
+
|
|
896
|
+
def _check_vite_server_reachable(self) -> None:
|
|
897
|
+
"""Check if Vite dev server is reachable (only in dev mode)."""
|
|
898
|
+
if not self.config.is_dev_mode:
|
|
899
|
+
return
|
|
900
|
+
|
|
901
|
+
host = self.config.host
|
|
902
|
+
port = self.config.port
|
|
903
|
+
|
|
904
|
+
if self.verbose:
|
|
905
|
+
console.print(f"[dim]Checking Vite server at {host}:{port}...[/]")
|
|
906
|
+
|
|
907
|
+
try:
|
|
908
|
+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
909
|
+
sock.settimeout(2)
|
|
910
|
+
result = sock.connect_ex((host, port))
|
|
911
|
+
sock.close()
|
|
912
|
+
|
|
913
|
+
if result != 0:
|
|
914
|
+
self.issues.append(
|
|
915
|
+
DoctorIssue(
|
|
916
|
+
check="Vite Server Not Running",
|
|
917
|
+
severity="warning",
|
|
918
|
+
message=f"Cannot connect to Vite dev server at {host}:{port}",
|
|
919
|
+
fix_hint="Start the dev server with `litestar assets serve` (and your backend with `litestar run` if needed)",
|
|
920
|
+
auto_fixable=False,
|
|
921
|
+
)
|
|
922
|
+
)
|
|
923
|
+
elif self.verbose:
|
|
924
|
+
console.print(f"[dim]✓ Vite server reachable at {host}:{port}[/]")
|
|
925
|
+
except OSError as e:
|
|
926
|
+
self.issues.append(
|
|
927
|
+
DoctorIssue(
|
|
928
|
+
check="Vite Server Check Failed",
|
|
929
|
+
severity="warning",
|
|
930
|
+
message=f"Could not check Vite server: {e}",
|
|
931
|
+
fix_hint="Ensure Vite dev server is running",
|
|
932
|
+
auto_fixable=False,
|
|
933
|
+
)
|
|
934
|
+
)
|
|
935
|
+
|
|
936
|
+
def _check_hotfile_presence(self) -> None:
|
|
937
|
+
"""Warn if hotfile is missing when it's required for dynamic proxy targets."""
|
|
938
|
+
if not self.config.is_dev_mode:
|
|
939
|
+
return
|
|
940
|
+
|
|
941
|
+
ext = self.config.runtime.external_dev_server
|
|
942
|
+
needs_hotfile = self.config.proxy_mode == "proxy" or (
|
|
943
|
+
isinstance(ext, ExternalDevServer) and ext.enabled and ext.target is None
|
|
944
|
+
)
|
|
945
|
+
if not needs_hotfile:
|
|
946
|
+
return
|
|
947
|
+
|
|
948
|
+
hot_path = self._resolve_to_root(self.config.bundle_dir) / self.config.hot_file
|
|
949
|
+
if not hot_path.exists():
|
|
950
|
+
self.issues.append(
|
|
951
|
+
DoctorIssue(
|
|
952
|
+
check="Hotfile Missing",
|
|
953
|
+
severity="warning",
|
|
954
|
+
message=f"Hotfile not found at {hot_path}",
|
|
955
|
+
fix_hint=(
|
|
956
|
+
"If you're running in proxy mode (SSR/external dev server), start the dev server with "
|
|
957
|
+
"`litestar assets serve` so it can write the hotfile. Otherwise, ignore this check or run "
|
|
958
|
+
"`litestar assets doctor` without `--runtime-checks`."
|
|
959
|
+
),
|
|
960
|
+
auto_fixable=False,
|
|
961
|
+
)
|
|
962
|
+
)
|
|
963
|
+
|
|
964
|
+
def _check_manifest_presence(self) -> None:
|
|
965
|
+
"""Ensure manifest exists in non-dev mode."""
|
|
966
|
+
if self.config.is_dev_mode:
|
|
967
|
+
return
|
|
968
|
+
|
|
969
|
+
candidates = self.config.candidate_manifest_paths()
|
|
970
|
+
if not any(path.exists() for path in candidates):
|
|
971
|
+
manifest_locations = " or ".join(str(path) for path in candidates)
|
|
972
|
+
self.issues.append(
|
|
973
|
+
DoctorIssue(
|
|
974
|
+
check="Manifest Missing",
|
|
975
|
+
severity="warning",
|
|
976
|
+
message=f"Manifest not found at {manifest_locations} (expected in production; ok during dev)",
|
|
977
|
+
fix_hint="Run `litestar assets build` before starting in production",
|
|
978
|
+
auto_fixable=False,
|
|
979
|
+
)
|
|
980
|
+
)
|
|
981
|
+
|
|
982
|
+
def _check_typegen_artifacts(self) -> None:
|
|
983
|
+
"""Verify exported OpenAPI/routes when typegen is enabled."""
|
|
984
|
+
if not isinstance(self.config.types, TypeGenConfig):
|
|
985
|
+
return
|
|
986
|
+
|
|
987
|
+
openapi_path = self.config.types.openapi_path
|
|
988
|
+
if openapi_path is None:
|
|
989
|
+
openapi_path = self.config.types.output / "openapi.json"
|
|
990
|
+
routes_path = self.config.types.routes_path
|
|
991
|
+
if routes_path is None:
|
|
992
|
+
routes_path = self.config.types.output / "routes.json"
|
|
993
|
+
|
|
994
|
+
if not openapi_path.exists():
|
|
995
|
+
self.issues.append(
|
|
996
|
+
DoctorIssue(
|
|
997
|
+
check="OpenAPI Export Missing",
|
|
998
|
+
severity="warning",
|
|
999
|
+
message=f"{openapi_path} not found",
|
|
1000
|
+
fix_hint="Run litestar assets generate-types (or start the app with types enabled)",
|
|
1001
|
+
auto_fixable=False,
|
|
1002
|
+
)
|
|
1003
|
+
)
|
|
1004
|
+
|
|
1005
|
+
if not routes_path.exists():
|
|
1006
|
+
self.issues.append(
|
|
1007
|
+
DoctorIssue(
|
|
1008
|
+
check="Routes Export Missing",
|
|
1009
|
+
severity="warning",
|
|
1010
|
+
message=f"{routes_path} not found",
|
|
1011
|
+
fix_hint="Run litestar assets generate-types (or start the app with types enabled)",
|
|
1012
|
+
auto_fixable=False,
|
|
1013
|
+
)
|
|
1014
|
+
)
|
|
1015
|
+
|
|
1016
|
+
def _check_env_alignment(self) -> None:
|
|
1017
|
+
"""Compare key env vars to active config to surface surprises."""
|
|
1018
|
+
env_mismatches: list[tuple[str, str, str]] = []
|
|
1019
|
+
comparisons = {
|
|
1020
|
+
"VITE_PORT": str(self.config.port),
|
|
1021
|
+
"VITE_HOST": self.config.host,
|
|
1022
|
+
"VITE_PROXY_MODE": self.config.proxy_mode,
|
|
1023
|
+
"VITE_PROTOCOL": self.config.protocol,
|
|
1024
|
+
}
|
|
1025
|
+
if self.config.base_url:
|
|
1026
|
+
comparisons["VITE_BASE_URL"] = self.config.base_url
|
|
1027
|
+
|
|
1028
|
+
for key, expected in comparisons.items():
|
|
1029
|
+
actual = os.getenv(key)
|
|
1030
|
+
if actual is None:
|
|
1031
|
+
continue
|
|
1032
|
+
if str(actual).rstrip("/") != str(expected).rstrip("/"):
|
|
1033
|
+
env_mismatches.append((key, str(actual), str(expected)))
|
|
1034
|
+
|
|
1035
|
+
if env_mismatches:
|
|
1036
|
+
mismatch_lines = ", ".join(f"{k}={a} (expected {e})" for k, a, e in env_mismatches)
|
|
1037
|
+
self.issues.append(
|
|
1038
|
+
DoctorIssue(
|
|
1039
|
+
check="Env / Config Mismatch",
|
|
1040
|
+
severity="warning",
|
|
1041
|
+
message=mismatch_lines,
|
|
1042
|
+
fix_hint="Unset conflicting env vars or align ViteConfig/runtime before running",
|
|
1043
|
+
auto_fixable=False,
|
|
1044
|
+
)
|
|
1045
|
+
)
|
|
1046
|
+
|
|
1047
|
+
def _print_config_snapshot(self, *, show_bridge: bool) -> None:
|
|
1048
|
+
"""Print a detailed view of Python, .litestar.json, and vite.config settings."""
|
|
1049
|
+
if not self.parsed_config:
|
|
1050
|
+
return
|
|
1051
|
+
|
|
1052
|
+
root = self.config.root_dir or Path.cwd()
|
|
1053
|
+
python_cfg = {
|
|
1054
|
+
"root": str(root),
|
|
1055
|
+
"mode": self.config.mode,
|
|
1056
|
+
"asset_url": self.config.asset_url,
|
|
1057
|
+
"bundle_dir": str(self.config.bundle_dir),
|
|
1058
|
+
"resource_dir": str(self.config.resource_dir),
|
|
1059
|
+
"static_dir": str(self.config.static_dir),
|
|
1060
|
+
"manifest": self.config.manifest_name,
|
|
1061
|
+
"hot_file": self.config.hot_file,
|
|
1062
|
+
"proxy_mode": self.config.proxy_mode,
|
|
1063
|
+
"dev_mode": self.config.is_dev_mode,
|
|
1064
|
+
"host": self.config.host,
|
|
1065
|
+
"port": self.config.port,
|
|
1066
|
+
"executor": self.config.runtime.executor,
|
|
1067
|
+
"set_environment": self.config.set_environment,
|
|
1068
|
+
"types_enabled": isinstance(self.config.types, TypeGenConfig),
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
js_cfg = {
|
|
1072
|
+
"has_litestar_config": self.parsed_config.has_litestar_config,
|
|
1073
|
+
"assetUrl": self.parsed_config.asset_url,
|
|
1074
|
+
"bundleDir": self.parsed_config.bundle_dir,
|
|
1075
|
+
"resourceDir": self.parsed_config.resource_dir,
|
|
1076
|
+
"staticDir": self.parsed_config.static_dir,
|
|
1077
|
+
"hotFile": self.parsed_config.hot_file,
|
|
1078
|
+
"inertiaMode": self.parsed_config.inertia_mode,
|
|
1079
|
+
"types": self.parsed_config.types_setting,
|
|
1080
|
+
"types.enabled": self.parsed_config.types_enabled,
|
|
1081
|
+
"types.output": self.parsed_config.types_output,
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
blocks: list[Any] = [
|
|
1085
|
+
"[bold]Python (effective)[/]",
|
|
1086
|
+
Syntax(encode_json(python_cfg).decode(), "json", theme="ansi_dark", word_wrap=True),
|
|
1087
|
+
]
|
|
1088
|
+
|
|
1089
|
+
if show_bridge and self.bridge_path is not None:
|
|
1090
|
+
bridge_obj: dict[str, Any] = {
|
|
1091
|
+
"path": str(self.bridge_path),
|
|
1092
|
+
"exists": self.bridge_path.exists(),
|
|
1093
|
+
"content": self.bridge_config or {},
|
|
1094
|
+
}
|
|
1095
|
+
blocks.extend([
|
|
1096
|
+
"[bold].litestar.json[/]",
|
|
1097
|
+
Syntax(encode_json(bridge_obj).decode(), "json", theme="ansi_dark", word_wrap=True),
|
|
1098
|
+
])
|
|
1099
|
+
|
|
1100
|
+
blocks.extend([
|
|
1101
|
+
"[bold]vite.config (litestar plugin)[/]",
|
|
1102
|
+
Syntax(encode_json(js_cfg).decode(), "json", theme="ansi_dark", word_wrap=True),
|
|
1103
|
+
])
|
|
1104
|
+
|
|
1105
|
+
if show_bridge and self.parsed_config.litestar_config_block:
|
|
1106
|
+
blocks.extend([
|
|
1107
|
+
"[bold]Extracted litestar({ ... }) block[/]",
|
|
1108
|
+
Syntax(self.parsed_config.litestar_config_block, "typescript", theme="ansi_dark", word_wrap=True),
|
|
1109
|
+
])
|
|
1110
|
+
|
|
1111
|
+
console.print(Panel(Group(*blocks), title="Config snapshot", border_style="dim"))
|
|
1112
|
+
|
|
1113
|
+
def _print_report(self) -> None:
|
|
1114
|
+
"""Print a table of detected issues."""
|
|
1115
|
+
if not self.issues:
|
|
1116
|
+
return
|
|
1117
|
+
|
|
1118
|
+
table = Table(show_header=True, header_style="bold magenta")
|
|
1119
|
+
table.add_column("Severity", style="dim")
|
|
1120
|
+
table.add_column("Check")
|
|
1121
|
+
table.add_column("Message")
|
|
1122
|
+
table.add_column("Fix Hint")
|
|
1123
|
+
|
|
1124
|
+
for issue in self.issues:
|
|
1125
|
+
severity_style = "red" if issue.severity == "error" else "yellow"
|
|
1126
|
+
table.add_row(f"[{severity_style}]{issue.severity.upper()}[/]", issue.check, issue.message, issue.fix_hint)
|
|
1127
|
+
|
|
1128
|
+
console.print(table)
|
|
1129
|
+
|
|
1130
|
+
def _apply_fixes(self, no_prompt: bool) -> bool:
|
|
1131
|
+
"""Apply auto-fixes.
|
|
1132
|
+
|
|
1133
|
+
Returns:
|
|
1134
|
+
True if fixes were applied, otherwise False.
|
|
1135
|
+
"""
|
|
1136
|
+
if not self.parsed_config:
|
|
1137
|
+
return False
|
|
1138
|
+
|
|
1139
|
+
fixable_issues = [i for i in self.issues if i.auto_fixable]
|
|
1140
|
+
if not fixable_issues:
|
|
1141
|
+
console.print("\n[yellow]No auto-fixable issues found.[/]")
|
|
1142
|
+
return False
|
|
1143
|
+
|
|
1144
|
+
console.print(f"\n[bold]Found {len(fixable_issues)} auto-fixable issues.[/]")
|
|
1145
|
+
|
|
1146
|
+
if not no_prompt and not Confirm.ask("Apply fixes?"):
|
|
1147
|
+
return False
|
|
1148
|
+
|
|
1149
|
+
content = self.parsed_config.content
|
|
1150
|
+
|
|
1151
|
+
will_edit_vite = self.vite_config_path is not None and any((i.context or {}).get("key") for i in fixable_issues)
|
|
1152
|
+
if will_edit_vite and self.vite_config_path is not None:
|
|
1153
|
+
backup_path = self.vite_config_path.with_suffix(self.vite_config_path.suffix + ".bak")
|
|
1154
|
+
backup_path.write_text(content)
|
|
1155
|
+
console.print(f"[dim]Created backup at {backup_path}[/]")
|
|
1156
|
+
|
|
1157
|
+
for issue in fixable_issues:
|
|
1158
|
+
context = issue.context
|
|
1159
|
+
if not context:
|
|
1160
|
+
continue
|
|
1161
|
+
|
|
1162
|
+
action = context.get("action")
|
|
1163
|
+
if action == "write_bridge":
|
|
1164
|
+
self._apply_write_bridge_fix()
|
|
1165
|
+
continue
|
|
1166
|
+
|
|
1167
|
+
key = context.get("key")
|
|
1168
|
+
expected = context.get("expected")
|
|
1169
|
+
if not key or expected is None:
|
|
1170
|
+
continue
|
|
1171
|
+
|
|
1172
|
+
content, updated = self._apply_vite_key_fix(content, key=key, expected=expected)
|
|
1173
|
+
if updated:
|
|
1174
|
+
console.print(f"[green]✓ Fixed {key}[/]")
|
|
1175
|
+
else:
|
|
1176
|
+
console.print(f"[red]✗ Failed to apply fix for {key} (pattern match failed)[/]")
|
|
1177
|
+
|
|
1178
|
+
if will_edit_vite and self.vite_config_path is not None:
|
|
1179
|
+
self.vite_config_path.write_text(content)
|
|
1180
|
+
console.print("\n[bold green]Fixes applied. Please verify configuration.[/]")
|
|
1181
|
+
return True
|