clan-app: Working swagger
This commit is contained in:
@@ -63,8 +63,13 @@ def app_run(app_opts: ClanAppOptions) -> int:
|
|||||||
if app_opts.http_api:
|
if app_opts.http_api:
|
||||||
from clan_app.deps.http.http_server import HttpApiServer
|
from clan_app.deps.http.http_server import HttpApiServer
|
||||||
|
|
||||||
|
openapi_file = os.getenv("OPENAPI_FILE", None)
|
||||||
|
swagger_dist = os.getenv("SWAGGER_UI_DIST", None)
|
||||||
|
|
||||||
http_server = HttpApiServer(
|
http_server = HttpApiServer(
|
||||||
api=API,
|
api=API,
|
||||||
|
openapi_file=Path(openapi_file) if openapi_file else None,
|
||||||
|
swagger_dist=Path(swagger_dist) if swagger_dist else None,
|
||||||
host=app_opts.http_host,
|
host=app_opts.http_host,
|
||||||
port=app_opts.http_port,
|
port=app_opts.http_port,
|
||||||
)
|
)
|
||||||
@@ -85,6 +90,7 @@ def app_run(app_opts: ClanAppOptions) -> int:
|
|||||||
log.info(
|
log.info(
|
||||||
f"Example request: curl -X POST http://{app_opts.http_host}:{app_opts.http_port}/api/v1/list_log_days"
|
f"Example request: curl -X POST http://{app_opts.http_host}:{app_opts.http_port}/api/v1/list_log_days"
|
||||||
)
|
)
|
||||||
|
|
||||||
log.info("Press Ctrl+C to stop the server")
|
log.info("Press Ctrl+C to stop the server")
|
||||||
try:
|
try:
|
||||||
# Keep the main thread alive
|
# Keep the main thread alive
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import logging
|
|||||||
import threading
|
import threading
|
||||||
import uuid
|
import uuid
|
||||||
from http.server import BaseHTTPRequestHandler
|
from http.server import BaseHTTPRequestHandler
|
||||||
|
from pathlib import Path
|
||||||
from typing import TYPE_CHECKING, Any
|
from typing import TYPE_CHECKING, Any
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
@@ -31,36 +32,43 @@ class HttpBridge(ApiBridge, BaseHTTPRequestHandler):
|
|||||||
request: Any,
|
request: Any,
|
||||||
client_address: Any,
|
client_address: Any,
|
||||||
server: Any,
|
server: Any,
|
||||||
|
*,
|
||||||
|
openapi_file: Path | None = None,
|
||||||
|
swagger_dist: Path | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
# Initialize the API bridge fields
|
# Initialize the API bridge fields
|
||||||
self.api = api
|
self.api = api
|
||||||
|
self.openapi_file = openapi_file
|
||||||
|
self.swagger_dist = swagger_dist
|
||||||
self.middleware_chain = middleware_chain
|
self.middleware_chain = middleware_chain
|
||||||
self.threads: dict[str, WebThread] = {}
|
self.threads: dict[str, WebThread] = {}
|
||||||
self._current_response: BackendResponse | None = None
|
|
||||||
|
|
||||||
# Initialize the HTTP handler
|
# Initialize the HTTP handler
|
||||||
super(BaseHTTPRequestHandler, self).__init__(request, client_address, server)
|
super(BaseHTTPRequestHandler, self).__init__(request, client_address, server)
|
||||||
|
|
||||||
def send_api_response(self, response: BackendResponse) -> None:
|
def send_api_response(self, response: BackendResponse) -> None:
|
||||||
"""Send HTTP response directly to the client."""
|
"""Send HTTP response directly to the client."""
|
||||||
self._current_response = response
|
|
||||||
|
|
||||||
# Send HTTP response
|
try:
|
||||||
self.send_response_only(200)
|
# Send HTTP response
|
||||||
self.send_header("Content-Type", "application/json")
|
self.send_response_only(200)
|
||||||
self.send_header("Access-Control-Allow-Origin", "*")
|
self.send_header("Content-Type", "application/json")
|
||||||
self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
self.send_header("Access-Control-Allow-Origin", "*")
|
||||||
self.send_header("Access-Control-Allow-Headers", "Content-Type")
|
self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
||||||
self.end_headers()
|
self.send_header("Access-Control-Allow-Headers", "Content-Type")
|
||||||
|
self.end_headers()
|
||||||
|
|
||||||
# Write response data
|
# Write response data
|
||||||
response_data = json.dumps(
|
response_data = json.dumps(
|
||||||
dataclass_to_dict(response), indent=2, ensure_ascii=False
|
dataclass_to_dict(response), indent=2, ensure_ascii=False
|
||||||
)
|
)
|
||||||
self.wfile.write(response_data.encode("utf-8"))
|
self.wfile.write(response_data.encode("utf-8"))
|
||||||
|
|
||||||
# Log the response for debugging
|
# Log the response for debugging
|
||||||
log.debug(f"HTTP response for {response._op_key}: {response_data}") # noqa: SLF001
|
log.debug(f"HTTP response for {response._op_key}: {response_data}") # noqa: SLF001
|
||||||
|
except BrokenPipeError as e:
|
||||||
|
# Handle broken pipe errors gracefully
|
||||||
|
log.warning(f"Client disconnected before we could send a response: {e!s}")
|
||||||
|
|
||||||
def do_OPTIONS(self) -> None: # noqa: N802
|
def do_OPTIONS(self) -> None: # noqa: N802
|
||||||
"""Handle CORS preflight requests."""
|
"""Handle CORS preflight requests."""
|
||||||
@@ -86,6 +94,65 @@ class HttpBridge(ApiBridge, BaseHTTPRequestHandler):
|
|||||||
_op_key="info",
|
_op_key="info",
|
||||||
)
|
)
|
||||||
self.send_api_response(response)
|
self.send_api_response(response)
|
||||||
|
elif path.startswith("/api/swagger"):
|
||||||
|
if self.swagger_dist and self.swagger_dist.exists():
|
||||||
|
# Serve static files from swagger_dist
|
||||||
|
rel_path = parsed_url.path[len("/api/swagger") :].lstrip("/")
|
||||||
|
# Redirect /api/swagger (no trailing slash or file) to /api/swagger/index.html
|
||||||
|
if rel_path == "":
|
||||||
|
self.send_response(302)
|
||||||
|
self.send_header("Location", "/api/swagger/index.html")
|
||||||
|
self.end_headers()
|
||||||
|
return
|
||||||
|
file_path = self.swagger_dist / rel_path
|
||||||
|
if rel_path == "index.html":
|
||||||
|
file_path = Path(__file__).parent / "swagger.html"
|
||||||
|
elif rel_path == "openapi.json":
|
||||||
|
if not self.openapi_file:
|
||||||
|
self.send_error(404, "OpenAPI file not found")
|
||||||
|
return
|
||||||
|
file_path = self.openapi_file
|
||||||
|
|
||||||
|
if file_path.exists() and file_path.is_file():
|
||||||
|
try:
|
||||||
|
# Guess content type
|
||||||
|
if file_path.suffix == ".html":
|
||||||
|
content_type = "text/html"
|
||||||
|
elif file_path.suffix == ".js":
|
||||||
|
content_type = "application/javascript"
|
||||||
|
elif file_path.suffix == ".css":
|
||||||
|
content_type = "text/css"
|
||||||
|
elif file_path.suffix == ".json":
|
||||||
|
content_type = "application/json"
|
||||||
|
elif file_path.suffix == ".png":
|
||||||
|
content_type = "image/png"
|
||||||
|
elif file_path.suffix == ".svg":
|
||||||
|
content_type = "image/svg+xml"
|
||||||
|
else:
|
||||||
|
content_type = "application/octet-stream"
|
||||||
|
with file_path.open("rb") as f:
|
||||||
|
file_data = f.read()
|
||||||
|
if rel_path == "openapi.json":
|
||||||
|
json_data = json.loads(file_data.decode("utf-8"))
|
||||||
|
json_data["servers"] = [
|
||||||
|
{
|
||||||
|
"url": f"http://{getattr(self.server, 'server_address', ('localhost', 80))[0]}:{getattr(self.server, 'server_address', ('localhost', 80))[1]}/api/v1/"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
file_data = json.dumps(json_data, indent=2).encode("utf-8")
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header("Content-Type", content_type)
|
||||||
|
self.end_headers()
|
||||||
|
|
||||||
|
self.wfile.write(file_data)
|
||||||
|
except Exception as e:
|
||||||
|
log.error(f"Error reading Swagger file: {e!s}")
|
||||||
|
self.send_error(500, "Internal Server Error")
|
||||||
|
else:
|
||||||
|
self.send_error(404, "Swagger file not found")
|
||||||
|
else:
|
||||||
|
self.send_error(404, "Swagger file not found")
|
||||||
|
|
||||||
elif path == "/api/methods":
|
elif path == "/api/methods":
|
||||||
response = BackendResponse(
|
response = BackendResponse(
|
||||||
body=SuccessDataClass(
|
body=SuccessDataClass(
|
||||||
@@ -97,8 +164,6 @@ class HttpBridge(ApiBridge, BaseHTTPRequestHandler):
|
|||||||
_op_key="methods",
|
_op_key="methods",
|
||||||
)
|
)
|
||||||
self.send_api_response(response)
|
self.send_api_response(response)
|
||||||
else:
|
|
||||||
self.send_api_error_response("info", "Not Found", ["http_bridge", "GET"])
|
|
||||||
|
|
||||||
def do_POST(self) -> None: # noqa: N802
|
def do_POST(self) -> None: # noqa: N802
|
||||||
"""Handle POST requests."""
|
"""Handle POST requests."""
|
||||||
@@ -107,7 +172,9 @@ class HttpBridge(ApiBridge, BaseHTTPRequestHandler):
|
|||||||
|
|
||||||
# Check if this is an API call
|
# Check if this is an API call
|
||||||
if not path.startswith("/api/v1/"):
|
if not path.startswith("/api/v1/"):
|
||||||
self.send_api_error_response("post", "Not Found", ["http_bridge", "POST"])
|
self.send_api_error_response(
|
||||||
|
"post", f"Path not found {path}", ["http_bridge", "POST"]
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Extract method name from path
|
# Extract method name from path
|
||||||
@@ -151,15 +218,15 @@ class HttpBridge(ApiBridge, BaseHTTPRequestHandler):
|
|||||||
return
|
return
|
||||||
|
|
||||||
# Generate a unique operation key
|
# Generate a unique operation key
|
||||||
op_key = str(uuid.uuid4())
|
gen_op_key = str(uuid.uuid4())
|
||||||
|
|
||||||
# Handle the API request
|
# Handle the API request
|
||||||
try:
|
try:
|
||||||
self._handle_api_request(method_name, request_data, op_key)
|
self._handle_api_request(method_name, request_data, gen_op_key)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.exception(f"Error processing API request {method_name}")
|
log.exception(f"Error processing API request {method_name}")
|
||||||
self.send_api_error_response(
|
self.send_api_error_response(
|
||||||
op_key,
|
gen_op_key,
|
||||||
f"Internal server error: {e!s}",
|
f"Internal server error: {e!s}",
|
||||||
["http_bridge", "POST", method_name],
|
["http_bridge", "POST", method_name],
|
||||||
)
|
)
|
||||||
@@ -168,7 +235,7 @@ class HttpBridge(ApiBridge, BaseHTTPRequestHandler):
|
|||||||
self,
|
self,
|
||||||
method_name: str,
|
method_name: str,
|
||||||
request_data: dict[str, Any],
|
request_data: dict[str, Any],
|
||||||
op_key: str,
|
gen_op_key: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Handle an API request by processing it through middleware."""
|
"""Handle an API request by processing it through middleware."""
|
||||||
try:
|
try:
|
||||||
@@ -183,6 +250,22 @@ class HttpBridge(ApiBridge, BaseHTTPRequestHandler):
|
|||||||
msg = f"Expected body to be a dict, got {type(body)}"
|
msg = f"Expected body to be a dict, got {type(body)}"
|
||||||
raise TypeError(msg)
|
raise TypeError(msg)
|
||||||
|
|
||||||
|
op_key = header.get("op_key", gen_op_key)
|
||||||
|
if not isinstance(op_key, str):
|
||||||
|
msg = f"Expected op_key to be a string, got {type(op_key)}"
|
||||||
|
raise TypeError(msg)
|
||||||
|
|
||||||
|
# Check if op_key is a valid UUID
|
||||||
|
try:
|
||||||
|
uuid.UUID(op_key)
|
||||||
|
except ValueError as e:
|
||||||
|
msg = f"op_key '{op_key}' is not a valid UUID"
|
||||||
|
raise TypeError(msg) from e
|
||||||
|
|
||||||
|
if op_key in self.threads:
|
||||||
|
msg = f"Operation key '{op_key}' is already in use. Please try again."
|
||||||
|
raise ValueError(msg)
|
||||||
|
|
||||||
# Create API request
|
# Create API request
|
||||||
api_request = BackendRequest(
|
api_request = BackendRequest(
|
||||||
method_name=method_name, args=body, header=header, op_key=op_key
|
method_name=method_name, args=body, header=header, op_key=op_key
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import logging
|
import logging
|
||||||
import threading
|
import threading
|
||||||
from http.server import HTTPServer
|
from http.server import HTTPServer
|
||||||
|
from pathlib import Path
|
||||||
from typing import TYPE_CHECKING, Any
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
from clan_lib.api import MethodRegistry
|
from clan_lib.api import MethodRegistry
|
||||||
@@ -21,8 +22,12 @@ class HttpApiServer:
|
|||||||
api: MethodRegistry,
|
api: MethodRegistry,
|
||||||
host: str = "127.0.0.1",
|
host: str = "127.0.0.1",
|
||||||
port: int = 8080,
|
port: int = 8080,
|
||||||
|
openapi_file: Path | None = None,
|
||||||
|
swagger_dist: Path | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.api = api
|
self.api = api
|
||||||
|
self.openapi = openapi_file
|
||||||
|
self.swagger_dist = swagger_dist
|
||||||
self.host = host
|
self.host = host
|
||||||
self.port = port
|
self.port = port
|
||||||
self._server: HTTPServer | None = None
|
self._server: HTTPServer | None = None
|
||||||
@@ -51,6 +56,8 @@ class HttpApiServer:
|
|||||||
"""Create a request handler class with injected dependencies."""
|
"""Create a request handler class with injected dependencies."""
|
||||||
api = self.api
|
api = self.api
|
||||||
middleware_chain = tuple(self._middleware)
|
middleware_chain = tuple(self._middleware)
|
||||||
|
openapi_file = self.openapi
|
||||||
|
swagger_dist = self.swagger_dist
|
||||||
|
|
||||||
class RequestHandler(HttpBridge):
|
class RequestHandler(HttpBridge):
|
||||||
def __init__(self, request: Any, client_address: Any, server: Any) -> None:
|
def __init__(self, request: Any, client_address: Any, server: Any) -> None:
|
||||||
@@ -60,6 +67,8 @@ class HttpApiServer:
|
|||||||
request=request,
|
request=request,
|
||||||
client_address=client_address,
|
client_address=client_address,
|
||||||
server=server,
|
server=server,
|
||||||
|
openapi_file=openapi_file,
|
||||||
|
swagger_dist=swagger_dist,
|
||||||
)
|
)
|
||||||
|
|
||||||
return RequestHandler
|
return RequestHandler
|
||||||
|
|||||||
38
pkgs/clan-app/clan_app/deps/http/swagger.html
Normal file
38
pkgs/clan-app/clan_app/deps/http/swagger.html
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<title>Swagger UI</title>
|
||||||
|
<link rel="stylesheet" type="text/css" href="./swagger-ui.css" />
|
||||||
|
<link rel="stylesheet" type="text/css" href="index.css" />
|
||||||
|
<link
|
||||||
|
rel="icon"
|
||||||
|
type="image/png"
|
||||||
|
href="./favicon-32x32.png"
|
||||||
|
sizes="32x32"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="icon"
|
||||||
|
type="image/png"
|
||||||
|
href="./favicon-16x16.png"
|
||||||
|
sizes="16x16"
|
||||||
|
/>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="swagger-ui"></div>
|
||||||
|
<script src="./swagger-ui-bundle.js" charset="UTF-8"></script>
|
||||||
|
<script src="./swagger-ui-standalone-preset.js" charset="UTF-8"></script>
|
||||||
|
<script src="./swagger-initializer.js" charset="UTF-8"></script>
|
||||||
|
<script>
|
||||||
|
window.onload = () => {
|
||||||
|
SwaggerUIBundle({
|
||||||
|
url: "./openapi.json", // Path to your OpenAPI 3 spec (YAML or JSON)
|
||||||
|
dom_id: "#swagger-ui",
|
||||||
|
presets: [SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset],
|
||||||
|
layout: "StandaloneLayout",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -32,7 +32,12 @@
|
|||||||
|
|
||||||
devShells.clan-app = pkgs.callPackage ./shell.nix {
|
devShells.clan-app = pkgs.callPackage ./shell.nix {
|
||||||
inherit self';
|
inherit self';
|
||||||
inherit (self'.packages) clan-app webview-lib clan-app-ui;
|
inherit (self'.packages)
|
||||||
|
clan-app
|
||||||
|
webview-lib
|
||||||
|
clan-app-ui
|
||||||
|
clan-lib-openapi
|
||||||
|
;
|
||||||
inherit (config.packages) clan-ts-api;
|
inherit (config.packages) clan-ts-api;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -7,9 +7,10 @@
|
|||||||
webview-lib,
|
webview-lib,
|
||||||
clan-app-ui,
|
clan-app-ui,
|
||||||
clan-ts-api,
|
clan-ts-api,
|
||||||
|
clan-lib-openapi,
|
||||||
ps,
|
ps,
|
||||||
|
fetchzip,
|
||||||
process-compose,
|
process-compose,
|
||||||
go-swagger,
|
|
||||||
json2ts,
|
json2ts,
|
||||||
playwright-driver,
|
playwright-driver,
|
||||||
luakit,
|
luakit,
|
||||||
@@ -18,6 +19,12 @@
|
|||||||
let
|
let
|
||||||
GREEN = "\\033[1;32m";
|
GREEN = "\\033[1;32m";
|
||||||
NC = "\\033[0m";
|
NC = "\\033[0m";
|
||||||
|
|
||||||
|
swagger-ui-dist = fetchzip {
|
||||||
|
url = "https://github.com/swagger-api/swagger-ui/archive/refs/tags/v5.26.2.zip";
|
||||||
|
sha256 = "sha256-KoFOsCheR1N+7EigFDV3r7frMMQtT43HE5H1/xsKLG4=";
|
||||||
|
};
|
||||||
|
|
||||||
in
|
in
|
||||||
|
|
||||||
mkShell {
|
mkShell {
|
||||||
@@ -43,7 +50,6 @@ mkShell {
|
|||||||
nativeBuildInputs = clan-app.nativeBuildInputs ++ [
|
nativeBuildInputs = clan-app.nativeBuildInputs ++ [
|
||||||
ps
|
ps
|
||||||
process-compose
|
process-compose
|
||||||
go-swagger
|
|
||||||
];
|
];
|
||||||
|
|
||||||
buildInputs = [
|
buildInputs = [
|
||||||
@@ -77,6 +83,8 @@ mkShell {
|
|||||||
|
|
||||||
export XDG_DATA_DIRS=$GSETTINGS_SCHEMAS_PATH:$XDG_DATA_DIRS
|
export XDG_DATA_DIRS=$GSETTINGS_SCHEMAS_PATH:$XDG_DATA_DIRS
|
||||||
export WEBVIEW_LIB_DIR=${webview-lib}/lib
|
export WEBVIEW_LIB_DIR=${webview-lib}/lib
|
||||||
|
export OPENAPI_FILE="${clan-lib-openapi}"
|
||||||
|
export SWAGGER_UI_DIST="${swagger-ui-dist}/dist"
|
||||||
|
|
||||||
## Webview UI
|
## Webview UI
|
||||||
# Add clan-app-ui scripts to PATH
|
# Add clan-app-ui scripts to PATH
|
||||||
|
|||||||
19
pkgs/clan-app/ui/package-lock.json
generated
19
pkgs/clan-app/ui/package-lock.json
generated
@@ -54,6 +54,7 @@
|
|||||||
"prettier": "^3.2.5",
|
"prettier": "^3.2.5",
|
||||||
"solid-devtools": "^0.34.0",
|
"solid-devtools": "^0.34.0",
|
||||||
"storybook": "^9.0.8",
|
"storybook": "^9.0.8",
|
||||||
|
"swagger-ui-dist": "^5.26.2",
|
||||||
"tailwindcss": "^3.4.3",
|
"tailwindcss": "^3.4.3",
|
||||||
"typescript": "^5.4.5",
|
"typescript": "^5.4.5",
|
||||||
"typescript-eslint": "^8.32.1",
|
"typescript-eslint": "^8.32.1",
|
||||||
@@ -1756,6 +1757,14 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@scarf/scarf": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz",
|
||||||
|
"integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==",
|
||||||
|
"dev": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
"node_modules/@sideway/address": {
|
"node_modules/@sideway/address": {
|
||||||
"version": "4.1.5",
|
"version": "4.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz",
|
||||||
@@ -7431,6 +7440,16 @@
|
|||||||
"node": ">= 10"
|
"node": ">= 10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/swagger-ui-dist": {
|
||||||
|
"version": "5.26.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.26.2.tgz",
|
||||||
|
"integrity": "sha512-WmMS9iMlHQejNm/Uw5ZTo4e3M2QMmEavRz7WLWVsq7Mlx4PSHJbY+VCrLsAz9wLxyHVgrJdt7N8+SdQLa52Ykg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@scarf/scarf": "=1.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/symbol-tree": {
|
"node_modules/symbol-tree": {
|
||||||
"version": "3.2.4",
|
"version": "3.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
|
||||||
|
|||||||
@@ -54,6 +54,7 @@
|
|||||||
"prettier": "^3.2.5",
|
"prettier": "^3.2.5",
|
||||||
"solid-devtools": "^0.34.0",
|
"solid-devtools": "^0.34.0",
|
||||||
"storybook": "^9.0.8",
|
"storybook": "^9.0.8",
|
||||||
|
"swagger-ui-dist": "^5.26.2",
|
||||||
"tailwindcss": "^3.4.3",
|
"tailwindcss": "^3.4.3",
|
||||||
"typescript": "^5.4.5",
|
"typescript": "^5.4.5",
|
||||||
"typescript-eslint": "^8.32.1",
|
"typescript-eslint": "^8.32.1",
|
||||||
|
|||||||
Reference in New Issue
Block a user