Initial commit for uvify UI
Browse files- .dockerignore +68 -0
- .gitignore +11 -0
- DEPLOYMENT.md +89 -0
- Dockerfile +72 -0
- README.md +111 -12
- eslint.config.js +23 -0
- index.html +13 -0
- package-lock.json +0 -0
- package.json +35 -0
- postcss.config.js +6 -0
- public/vite.svg +1 -0
- requirements.txt +4 -0
- server.py +60 -0
- src/App.tsx +39 -0
- src/assets/react.svg +1 -0
- src/components/Header.tsx +31 -0
- src/components/Hero.tsx +100 -0
- src/components/Results.tsx +167 -0
- src/index.css +3 -0
- src/main.tsx +10 -0
- src/services/uvifyApi.ts +36 -0
- src/types/uvify.ts +16 -0
- src/vite-env.d.ts +1 -0
- start.sh +63 -0
- tailwind.config.js +19 -0
- tsconfig.app.json +27 -0
- tsconfig.json +7 -0
- tsconfig.node.json +25 -0
- uvify_cors_wrapper.py +23 -0
- vite.config.ts +19 -0
.dockerignore
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Node.js
|
| 2 |
+
node_modules/
|
| 3 |
+
npm-debug.log*
|
| 4 |
+
.npm
|
| 5 |
+
|
| 6 |
+
# Python
|
| 7 |
+
.venv/
|
| 8 |
+
__pycache__/
|
| 9 |
+
*.pyc
|
| 10 |
+
*.pyo
|
| 11 |
+
*.pyd
|
| 12 |
+
.Python
|
| 13 |
+
build/
|
| 14 |
+
develop-eggs/
|
| 15 |
+
dist/
|
| 16 |
+
downloads/
|
| 17 |
+
eggs/
|
| 18 |
+
.eggs/
|
| 19 |
+
lib/
|
| 20 |
+
lib64/
|
| 21 |
+
parts/
|
| 22 |
+
sdist/
|
| 23 |
+
var/
|
| 24 |
+
wheels/
|
| 25 |
+
*.egg-info/
|
| 26 |
+
.installed.cfg
|
| 27 |
+
*.egg
|
| 28 |
+
|
| 29 |
+
# IDEs
|
| 30 |
+
.vscode/
|
| 31 |
+
.idea/
|
| 32 |
+
*.swp
|
| 33 |
+
*.swo
|
| 34 |
+
*~
|
| 35 |
+
|
| 36 |
+
# OS
|
| 37 |
+
.DS_Store
|
| 38 |
+
Thumbs.db
|
| 39 |
+
|
| 40 |
+
# Git
|
| 41 |
+
.git/
|
| 42 |
+
.gitignore
|
| 43 |
+
|
| 44 |
+
# Logs
|
| 45 |
+
*.log
|
| 46 |
+
logs/
|
| 47 |
+
|
| 48 |
+
# Runtime data
|
| 49 |
+
pids/
|
| 50 |
+
*.pid
|
| 51 |
+
*.seed
|
| 52 |
+
*.pid.lock
|
| 53 |
+
|
| 54 |
+
# Environment
|
| 55 |
+
.env
|
| 56 |
+
.env.local
|
| 57 |
+
.env.development.local
|
| 58 |
+
.env.test.local
|
| 59 |
+
.env.production.local
|
| 60 |
+
|
| 61 |
+
# Build outputs (will be copied from build stage)
|
| 62 |
+
dist/
|
| 63 |
+
|
| 64 |
+
# Development scripts
|
| 65 |
+
start.sh
|
| 66 |
+
|
| 67 |
+
# Documentation
|
| 68 |
+
README.md
|
.gitignore
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Python-generated files
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.py[oc]
|
| 4 |
+
build/
|
| 5 |
+
dist/
|
| 6 |
+
wheels/
|
| 7 |
+
*.egg-info
|
| 8 |
+
|
| 9 |
+
# Virtual environments
|
| 10 |
+
.venv
|
| 11 |
+
node_modules/
|
DEPLOYMENT.md
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# HuggingFace Deployment Guide
|
| 2 |
+
|
| 3 |
+
This directory contains the uvify application configured for deployment on HuggingFace Spaces.
|
| 4 |
+
|
| 5 |
+
## Quick Deploy to HuggingFace Spaces
|
| 6 |
+
|
| 7 |
+
### Method 1: Direct Upload
|
| 8 |
+
|
| 9 |
+
1. Create a new HuggingFace Space:
|
| 10 |
+
- Go to https://huggingface.co/new-space
|
| 11 |
+
- Choose "Docker" as the SDK
|
| 12 |
+
- Set visibility as needed
|
| 13 |
+
|
| 14 |
+
2. Upload files:
|
| 15 |
+
- Copy all files from this directory to your HuggingFace Space repository
|
| 16 |
+
- Commit and push the changes
|
| 17 |
+
|
| 18 |
+
3. The Space will automatically build and deploy using the Dockerfile
|
| 19 |
+
|
| 20 |
+
### Method 2: Git Integration
|
| 21 |
+
|
| 22 |
+
1. Create a new HuggingFace Space with Git integration
|
| 23 |
+
2. Clone your HuggingFace Space repository locally
|
| 24 |
+
3. Copy the contents of this directory to the cloned repository
|
| 25 |
+
4. Commit and push:
|
| 26 |
+
```bash
|
| 27 |
+
git add .
|
| 28 |
+
git commit -m "Initial uvify deployment"
|
| 29 |
+
git push
|
| 30 |
+
```
|
| 31 |
+
|
| 32 |
+
## Files Overview
|
| 33 |
+
|
| 34 |
+
- `Dockerfile` - Multi-stage build configuration for HuggingFace
|
| 35 |
+
- `server.py` - Production FastAPI server that serves both API and frontend
|
| 36 |
+
- `requirements.txt` - Python dependencies
|
| 37 |
+
- `.dockerignore` - Excludes unnecessary files from Docker build
|
| 38 |
+
- Frontend files (`src/`, `package.json`, etc.) - React application
|
| 39 |
+
|
| 40 |
+
## Architecture
|
| 41 |
+
|
| 42 |
+
The deployment uses a multi-stage Docker build:
|
| 43 |
+
|
| 44 |
+
1. **Frontend Stage**: Builds the React application using Node.js and Vite
|
| 45 |
+
2. **Production Stage**:
|
| 46 |
+
- Uses Python 3.10 base image
|
| 47 |
+
- Installs uvify and FastAPI dependencies using `uv`
|
| 48 |
+
- Copies built frontend assets
|
| 49 |
+
- Serves both API and static files on port 7860
|
| 50 |
+
|
| 51 |
+
## API Endpoints
|
| 52 |
+
|
| 53 |
+
Once deployed, your application will be available at:
|
| 54 |
+
- Frontend: `https://your-space-name.hf.space/`
|
| 55 |
+
- API docs: `https://your-space-name.hf.space/docs`
|
| 56 |
+
- API: `https://your-space-name.hf.space/api/`
|
| 57 |
+
|
| 58 |
+
## Environment Variables
|
| 59 |
+
|
| 60 |
+
The application uses these environment variables:
|
| 61 |
+
- `PORT` - Server port (default: 7860 for HuggingFace)
|
| 62 |
+
|
| 63 |
+
## Local Testing
|
| 64 |
+
|
| 65 |
+
To test the Docker build locally:
|
| 66 |
+
|
| 67 |
+
```bash
|
| 68 |
+
# Build the image
|
| 69 |
+
docker build -t uvify-app .
|
| 70 |
+
|
| 71 |
+
# Run the container
|
| 72 |
+
docker run -p 7860:7860 uvify-app
|
| 73 |
+
```
|
| 74 |
+
|
| 75 |
+
Then visit http://localhost:7860 to test the application.
|
| 76 |
+
|
| 77 |
+
## Troubleshooting
|
| 78 |
+
|
| 79 |
+
### Build Issues
|
| 80 |
+
- Ensure all dependencies are correctly specified in `requirements.txt`
|
| 81 |
+
- Check that the frontend builds successfully with `npm run build`
|
| 82 |
+
|
| 83 |
+
### Runtime Issues
|
| 84 |
+
- Check the HuggingFace Space logs for Python/FastAPI errors
|
| 85 |
+
- Verify that the uvify package is correctly installed and accessible
|
| 86 |
+
|
| 87 |
+
### CORS Issues
|
| 88 |
+
- The server.py is configured to allow all origins for HuggingFace deployment
|
| 89 |
+
- If you need to restrict origins, modify the CORS middleware in `server.py`
|
Dockerfile
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Multi-stage build for uvify HuggingFace app
|
| 2 |
+
FROM node:20-slim AS frontend-builder
|
| 3 |
+
|
| 4 |
+
# Set working directory for frontend build
|
| 5 |
+
WORKDIR /app
|
| 6 |
+
|
| 7 |
+
# Copy package files
|
| 8 |
+
COPY package*.json ./
|
| 9 |
+
|
| 10 |
+
# Install Node.js dependencies
|
| 11 |
+
RUN npm ci
|
| 12 |
+
|
| 13 |
+
# Copy frontend source code
|
| 14 |
+
COPY src/ ./src/
|
| 15 |
+
COPY public/ ./public/
|
| 16 |
+
COPY index.html ./
|
| 17 |
+
COPY vite.config.ts ./
|
| 18 |
+
COPY tsconfig*.json ./
|
| 19 |
+
COPY tailwind.config.js ./
|
| 20 |
+
COPY postcss.config.js ./
|
| 21 |
+
COPY eslint.config.js ./
|
| 22 |
+
|
| 23 |
+
# Build the frontend for production
|
| 24 |
+
RUN npm run build
|
| 25 |
+
|
| 26 |
+
# Production stage
|
| 27 |
+
FROM python:3.10-slim
|
| 28 |
+
|
| 29 |
+
# Create a non-root user
|
| 30 |
+
RUN useradd --create-home --shell /bin/bash user
|
| 31 |
+
|
| 32 |
+
# Set working directory
|
| 33 |
+
WORKDIR /app
|
| 34 |
+
|
| 35 |
+
# Install system dependencies and uv
|
| 36 |
+
RUN apt-get update && apt-get install -y \
|
| 37 |
+
curl \
|
| 38 |
+
git \
|
| 39 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 40 |
+
|
| 41 |
+
# Install uv for fast Python package management
|
| 42 |
+
RUN pip install uv
|
| 43 |
+
|
| 44 |
+
# Copy frontend build from previous stage
|
| 45 |
+
COPY --chown=user --from=frontend-builder /app/dist ./static
|
| 46 |
+
|
| 47 |
+
# Copy Python backend files
|
| 48 |
+
COPY --chown=user server.py ./
|
| 49 |
+
COPY --chown=user requirements.txt ./
|
| 50 |
+
|
| 51 |
+
# Install Python dependencies using uv
|
| 52 |
+
RUN uv pip install --system -r requirements.txt
|
| 53 |
+
|
| 54 |
+
# Change ownership of the app directory to user
|
| 55 |
+
RUN chown -R user:user /app
|
| 56 |
+
|
| 57 |
+
# Switch to non-root user
|
| 58 |
+
USER user
|
| 59 |
+
|
| 60 |
+
# Expose port 7860 (HuggingFace default)
|
| 61 |
+
EXPOSE 7860
|
| 62 |
+
|
| 63 |
+
# Set environment variables
|
| 64 |
+
ENV PORT=7860
|
| 65 |
+
ENV PYTHONPATH=/app
|
| 66 |
+
|
| 67 |
+
# Health check
|
| 68 |
+
HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \
|
| 69 |
+
CMD curl -f http://localhost:7860/docs || exit 1
|
| 70 |
+
|
| 71 |
+
# Run the production server
|
| 72 |
+
CMD ["python", "server.py"]
|
README.md
CHANGED
|
@@ -1,12 +1,111 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Uvify UI
|
| 2 |
+
|
| 3 |
+
A modern web interface for [uvify](https://github.com/avilum/uvify) - Turn Python repositories into uv environment oneliners.
|
| 4 |
+
|
| 5 |
+
## Features
|
| 6 |
+
|
| 7 |
+
- 🚀 Analyze GitHub repositories and local directories
|
| 8 |
+
- 📦 Parse dependencies from requirements.txt, pyproject.toml, and setup.py
|
| 9 |
+
- 🎯 Generate ready-to-use `uv` commands
|
| 10 |
+
- 📋 Copy commands and download results as JSON
|
| 11 |
+
- 🎨 Beautiful, GitIngest-inspired UI
|
| 12 |
+
|
| 13 |
+
## Quick Start
|
| 14 |
+
|
| 15 |
+
### Prerequisites
|
| 16 |
+
|
| 17 |
+
- Node.js 18+
|
| 18 |
+
- Python 3.8+
|
| 19 |
+
- [uv](https://github.com/astral-sh/uv) package manager
|
| 20 |
+
|
| 21 |
+
Install uv if you haven't already:
|
| 22 |
+
```bash
|
| 23 |
+
curl -LsSf https://astral.sh/uv/install.sh | sh
|
| 24 |
+
# or on macOS:
|
| 25 |
+
brew install uv
|
| 26 |
+
```
|
| 27 |
+
|
| 28 |
+
### Installation & Running
|
| 29 |
+
|
| 30 |
+
The easiest way to run both the UI and API servers:
|
| 31 |
+
|
| 32 |
+
```bash
|
| 33 |
+
npm start
|
| 34 |
+
```
|
| 35 |
+
|
| 36 |
+
This will:
|
| 37 |
+
1. Install all dependencies (if needed)
|
| 38 |
+
2. Start the uvify API server on http://localhost:8000
|
| 39 |
+
3. Start the UI development server on http://localhost:5173
|
| 40 |
+
|
| 41 |
+
### Manual Setup
|
| 42 |
+
|
| 43 |
+
If you prefer to run the servers separately:
|
| 44 |
+
|
| 45 |
+
1. Install dependencies:
|
| 46 |
+
```bash
|
| 47 |
+
npm install
|
| 48 |
+
uv venv
|
| 49 |
+
source .venv/bin/activate
|
| 50 |
+
uv pip install 'uvify[api]'
|
| 51 |
+
```
|
| 52 |
+
|
| 53 |
+
2. Start the API server:
|
| 54 |
+
```bash
|
| 55 |
+
npm run start:api
|
| 56 |
+
```
|
| 57 |
+
|
| 58 |
+
3. In another terminal, start the UI:
|
| 59 |
+
```bash
|
| 60 |
+
npm run start:ui
|
| 61 |
+
```
|
| 62 |
+
|
| 63 |
+
## Usage
|
| 64 |
+
|
| 65 |
+
1. Enter a GitHub repository URL or username/repo format
|
| 66 |
+
2. Click "Analyze" to process the repository
|
| 67 |
+
3. View the results showing:
|
| 68 |
+
- Parsed dependencies per file
|
| 69 |
+
- Ready-to-use `uv` commands
|
| 70 |
+
- Python version requirements
|
| 71 |
+
- Directory structure
|
| 72 |
+
|
| 73 |
+
## Development
|
| 74 |
+
|
| 75 |
+
### Project Structure
|
| 76 |
+
|
| 77 |
+
```
|
| 78 |
+
uvifyUI/
|
| 79 |
+
├── src/
|
| 80 |
+
│ ├── components/ # React components
|
| 81 |
+
│ ├── services/ # API integration
|
| 82 |
+
│ └── types/ # TypeScript types
|
| 83 |
+
├── public/ # Static assets
|
| 84 |
+
└── start.sh # Unified start script
|
| 85 |
+
```
|
| 86 |
+
|
| 87 |
+
### Available Scripts
|
| 88 |
+
|
| 89 |
+
- `npm start` - Run both UI and API servers
|
| 90 |
+
- `npm run dev` - Run UI development server only
|
| 91 |
+
- `npm run build` - Build for production
|
| 92 |
+
- `npm run lint` - Run ESLint
|
| 93 |
+
|
| 94 |
+
## Environment Variables
|
| 95 |
+
|
| 96 |
+
Copy `.env.example` to `.env` and configure:
|
| 97 |
+
|
| 98 |
+
```
|
| 99 |
+
VITE_API_URL=http://localhost:8000
|
| 100 |
+
```
|
| 101 |
+
|
| 102 |
+
## Built With
|
| 103 |
+
|
| 104 |
+
- React + TypeScript
|
| 105 |
+
- Vite
|
| 106 |
+
- Tailwind CSS
|
| 107 |
+
- uvify API backend
|
| 108 |
+
|
| 109 |
+
## License
|
| 110 |
+
|
| 111 |
+
This project is built on top of uvify. See the [uvify repository](https://github.com/avilum/uvify) for license information.
|
eslint.config.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import js from '@eslint/js'
|
| 2 |
+
import globals from 'globals'
|
| 3 |
+
import reactHooks from 'eslint-plugin-react-hooks'
|
| 4 |
+
import reactRefresh from 'eslint-plugin-react-refresh'
|
| 5 |
+
import tseslint from 'typescript-eslint'
|
| 6 |
+
import { globalIgnores } from 'eslint/config'
|
| 7 |
+
|
| 8 |
+
export default tseslint.config([
|
| 9 |
+
globalIgnores(['dist']),
|
| 10 |
+
{
|
| 11 |
+
files: ['**/*.{ts,tsx}'],
|
| 12 |
+
extends: [
|
| 13 |
+
js.configs.recommended,
|
| 14 |
+
tseslint.configs.recommended,
|
| 15 |
+
reactHooks.configs['recommended-latest'],
|
| 16 |
+
reactRefresh.configs.vite,
|
| 17 |
+
],
|
| 18 |
+
languageOptions: {
|
| 19 |
+
ecmaVersion: 2020,
|
| 20 |
+
globals: globals.browser,
|
| 21 |
+
},
|
| 22 |
+
},
|
| 23 |
+
])
|
index.html
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 7 |
+
<title>Vite + React + TS</title>
|
| 8 |
+
</head>
|
| 9 |
+
<body>
|
| 10 |
+
<div id="root"></div>
|
| 11 |
+
<script type="module" src="/src/main.tsx"></script>
|
| 12 |
+
</body>
|
| 13 |
+
</html>
|
package-lock.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "uvify-ui",
|
| 3 |
+
"private": true,
|
| 4 |
+
"version": "0.0.0",
|
| 5 |
+
"type": "module",
|
| 6 |
+
"scripts": {
|
| 7 |
+
"dev": "vite",
|
| 8 |
+
"build": "tsc -b && vite build",
|
| 9 |
+
"lint": "eslint .",
|
| 10 |
+
"preview": "vite preview",
|
| 11 |
+
"start": "./start.sh",
|
| 12 |
+
"start:ui": "vite",
|
| 13 |
+
"start:api": "source .venv/bin/activate && python -m uvicorn src.uvify:api --host 0.0.0.0 --port 8000"
|
| 14 |
+
},
|
| 15 |
+
"dependencies": {
|
| 16 |
+
"react": "^19.1.0",
|
| 17 |
+
"react-dom": "^19.1.0"
|
| 18 |
+
},
|
| 19 |
+
"devDependencies": {
|
| 20 |
+
"@eslint/js": "^9.30.1",
|
| 21 |
+
"@types/react": "^19.1.8",
|
| 22 |
+
"@types/react-dom": "^19.1.6",
|
| 23 |
+
"@vitejs/plugin-react": "^4.6.0",
|
| 24 |
+
"autoprefixer": "^10.4.21",
|
| 25 |
+
"eslint": "^9.30.1",
|
| 26 |
+
"eslint-plugin-react-hooks": "^5.2.0",
|
| 27 |
+
"eslint-plugin-react-refresh": "^0.4.20",
|
| 28 |
+
"globals": "^16.3.0",
|
| 29 |
+
"postcss": "^8.5.6",
|
| 30 |
+
"tailwindcss": "^3.4.17",
|
| 31 |
+
"typescript": "~5.8.3",
|
| 32 |
+
"typescript-eslint": "^8.35.1",
|
| 33 |
+
"vite": "^7.0.4"
|
| 34 |
+
}
|
| 35 |
+
}
|
postcss.config.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export default {
|
| 2 |
+
plugins: {
|
| 3 |
+
tailwindcss: {},
|
| 4 |
+
autoprefixer: {},
|
| 5 |
+
},
|
| 6 |
+
}
|
public/vite.svg
ADDED
|
|
requirements.txt
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
uvify[api]
|
| 2 |
+
fastapi
|
| 3 |
+
uvicorn
|
| 4 |
+
python-multipart
|
server.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Production server for uvify HuggingFace deployment
|
| 4 |
+
Serves both React frontend and FastAPI backend together
|
| 5 |
+
"""
|
| 6 |
+
import os
|
| 7 |
+
from fastapi import FastAPI
|
| 8 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 9 |
+
from fastapi.staticfiles import StaticFiles
|
| 10 |
+
from fastapi.responses import FileResponse
|
| 11 |
+
import uvify
|
| 12 |
+
|
| 13 |
+
# Create a new FastAPI app that combines everything
|
| 14 |
+
app = FastAPI(
|
| 15 |
+
title="Uvify",
|
| 16 |
+
description="Analyze GitHub repositories to generate uv commands",
|
| 17 |
+
version="1.0.0"
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
# Add CORS middleware for HuggingFace deployment
|
| 21 |
+
app.add_middleware(
|
| 22 |
+
CORSMiddleware,
|
| 23 |
+
allow_origins=["*"], # Allow all origins for HuggingFace
|
| 24 |
+
allow_credentials=True,
|
| 25 |
+
allow_methods=["*"],
|
| 26 |
+
allow_headers=["*"],
|
| 27 |
+
)
|
| 28 |
+
|
| 29 |
+
# Mount the uvify API under /api prefix to avoid conflicts
|
| 30 |
+
app.mount("/api", uvify.api, name="uvify_api")
|
| 31 |
+
|
| 32 |
+
# Mount static files for the built React app
|
| 33 |
+
app.mount("/static", StaticFiles(directory="static"), name="static")
|
| 34 |
+
|
| 35 |
+
# Serve the React app for the root route
|
| 36 |
+
@app.get("/")
|
| 37 |
+
async def serve_index():
|
| 38 |
+
"""Serve the React app index page"""
|
| 39 |
+
return FileResponse("static/index.html")
|
| 40 |
+
|
| 41 |
+
# Serve the React app for all other frontend routes
|
| 42 |
+
@app.get("/{path:path}")
|
| 43 |
+
async def serve_frontend(path: str):
|
| 44 |
+
"""Serve static files or fallback to React app for client-side routing"""
|
| 45 |
+
# Skip API routes - they're handled by the mounted API
|
| 46 |
+
if path.startswith("api/"):
|
| 47 |
+
return {"error": "API route not found"}
|
| 48 |
+
|
| 49 |
+
# Try to serve static file first (CSS, JS, images, etc.)
|
| 50 |
+
file_path = f"static/{path}"
|
| 51 |
+
if os.path.exists(file_path) and os.path.isfile(file_path):
|
| 52 |
+
return FileResponse(file_path)
|
| 53 |
+
|
| 54 |
+
# Fallback to React app index.html for client-side routing
|
| 55 |
+
return FileResponse("static/index.html")
|
| 56 |
+
|
| 57 |
+
if __name__ == "__main__":
|
| 58 |
+
import uvicorn
|
| 59 |
+
port = int(os.environ.get("PORT", 7860))
|
| 60 |
+
uvicorn.run(app, host="0.0.0.0", port=port)
|
src/App.tsx
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useState } from 'react';
|
| 2 |
+
import Header from './components/Header';
|
| 3 |
+
import Hero from './components/Hero';
|
| 4 |
+
import Results from './components/Results';
|
| 5 |
+
import { analyzeRepository } from './services/uvifyApi';
|
| 6 |
+
import type { UvifyResult } from './types/uvify';
|
| 7 |
+
|
| 8 |
+
function App() {
|
| 9 |
+
const [results, setResults] = useState<UvifyResult[]>([]);
|
| 10 |
+
const [isLoading, setIsLoading] = useState(false);
|
| 11 |
+
const [error, setError] = useState<string>('');
|
| 12 |
+
const [currentSource, setCurrentSource] = useState<string>('');
|
| 13 |
+
|
| 14 |
+
const handleAnalyze = async (source: string) => {
|
| 15 |
+
setIsLoading(true);
|
| 16 |
+
setError('');
|
| 17 |
+
setResults([]);
|
| 18 |
+
setCurrentSource(source);
|
| 19 |
+
|
| 20 |
+
try {
|
| 21 |
+
const data = await analyzeRepository(source);
|
| 22 |
+
setResults(data);
|
| 23 |
+
} catch (err) {
|
| 24 |
+
setError(err instanceof Error ? err.message : 'Failed to analyze repository');
|
| 25 |
+
} finally {
|
| 26 |
+
setIsLoading(false);
|
| 27 |
+
}
|
| 28 |
+
};
|
| 29 |
+
|
| 30 |
+
return (
|
| 31 |
+
<div className="min-h-screen bg-gray-900 text-white">
|
| 32 |
+
<Header />
|
| 33 |
+
<Hero onAnalyze={handleAnalyze} isLoading={isLoading} />
|
| 34 |
+
<Results results={results} source={currentSource} error={error} />
|
| 35 |
+
</div>
|
| 36 |
+
);
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
export default App
|
src/assets/react.svg
ADDED
|
|
src/components/Header.tsx
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const Header = () => {
|
| 2 |
+
return (
|
| 3 |
+
<header className="bg-gray-900 border-b border-gray-800">
|
| 4 |
+
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
| 5 |
+
<div className="flex justify-between items-center h-16">
|
| 6 |
+
<h1 className="text-2xl font-bold">
|
| 7 |
+
<a href="/" className="flex items-center gap-1">
|
| 8 |
+
<span className="text-uvify-blue">Uv</span>
|
| 9 |
+
<span className="text-uvify-purple">ify</span>
|
| 10 |
+
</a>
|
| 11 |
+
</h1>
|
| 12 |
+
<nav className="flex items-center gap-6">
|
| 13 |
+
<a
|
| 14 |
+
href="https://github.com/avilum/uvify"
|
| 15 |
+
target="_blank"
|
| 16 |
+
rel="noopener noreferrer"
|
| 17 |
+
className="flex items-center gap-2 text-gray-300 hover:text-white transition-colors"
|
| 18 |
+
>
|
| 19 |
+
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
| 20 |
+
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
|
| 21 |
+
</svg>
|
| 22 |
+
GitHub
|
| 23 |
+
</a>
|
| 24 |
+
</nav>
|
| 25 |
+
</div>
|
| 26 |
+
</div>
|
| 27 |
+
</header>
|
| 28 |
+
);
|
| 29 |
+
};
|
| 30 |
+
|
| 31 |
+
export default Header;
|
src/components/Hero.tsx
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useState } from 'react';
|
| 2 |
+
|
| 3 |
+
interface HeroProps {
|
| 4 |
+
onAnalyze: (source: string) => void;
|
| 5 |
+
isLoading: boolean;
|
| 6 |
+
}
|
| 7 |
+
|
| 8 |
+
const Hero = ({ onAnalyze, isLoading }: HeroProps) => {
|
| 9 |
+
const [source, setSource] = useState('');
|
| 10 |
+
|
| 11 |
+
const exampleRepos = [
|
| 12 |
+
{ name: 'requests', url: 'psf/requests' },
|
| 13 |
+
{ name: 'FastAPI', url: 'fastapi/fastapi' },
|
| 14 |
+
{ name: 'Flask', url: 'pallets/flask' },
|
| 15 |
+
{ name: 'Black', url: 'psf/black' },
|
| 16 |
+
{ name: 'Poetry', url: 'python-poetry/poetry' },
|
| 17 |
+
];
|
| 18 |
+
|
| 19 |
+
const handleSubmit = (e: React.FormEvent) => {
|
| 20 |
+
e.preventDefault();
|
| 21 |
+
if (source.trim()) {
|
| 22 |
+
onAnalyze(source.trim());
|
| 23 |
+
}
|
| 24 |
+
};
|
| 25 |
+
|
| 26 |
+
const handleExample = (url: string) => {
|
| 27 |
+
setSource(url);
|
| 28 |
+
onAnalyze(url);
|
| 29 |
+
};
|
| 30 |
+
|
| 31 |
+
return (
|
| 32 |
+
<section className="py-12 px-4">
|
| 33 |
+
<div className="max-w-4xl mx-auto">
|
| 34 |
+
<div className="text-center mb-10">
|
| 35 |
+
<div className="flex justify-center items-center gap-4 mb-6">
|
| 36 |
+
<span className="text-6xl">⚡</span>
|
| 37 |
+
<h1 className="text-5xl font-bold">
|
| 38 |
+
<span className="text-gray-100">Python Environment</span>
|
| 39 |
+
<br />
|
| 40 |
+
<span className="bg-gradient-to-r from-uvify-blue to-uvify-purple bg-clip-text text-transparent">
|
| 41 |
+
Simplified
|
| 42 |
+
</span>
|
| 43 |
+
</h1>
|
| 44 |
+
<span className="text-6xl">🐍</span>
|
| 45 |
+
</div>
|
| 46 |
+
<p className="text-xl text-gray-400 mb-2">
|
| 47 |
+
Turn any Python repository into a uv environment oneliner.
|
| 48 |
+
</p>
|
| 49 |
+
<p className="text-lg text-gray-500">
|
| 50 |
+
Analyze dependencies from requirements.txt, pyproject.toml, and setup.py files.
|
| 51 |
+
</p>
|
| 52 |
+
</div>
|
| 53 |
+
|
| 54 |
+
<div className="bg-gray-800 rounded-2xl p-8 shadow-2xl border border-gray-700">
|
| 55 |
+
<form onSubmit={handleSubmit} className="space-y-6">
|
| 56 |
+
<div className="flex gap-3">
|
| 57 |
+
<input
|
| 58 |
+
type="text"
|
| 59 |
+
value={source}
|
| 60 |
+
onChange={(e) => setSource(e.target.value)}
|
| 61 |
+
placeholder="https://github.com/owner/repo or owner/repo"
|
| 62 |
+
className="flex-1 px-4 py-3 bg-gray-900 border border-gray-700 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:border-uvify-blue focus:ring-1 focus:ring-uvify-blue"
|
| 63 |
+
disabled={isLoading}
|
| 64 |
+
/>
|
| 65 |
+
<button
|
| 66 |
+
type="submit"
|
| 67 |
+
disabled={isLoading || !source.trim()}
|
| 68 |
+
className="px-8 py-3 bg-gradient-to-r from-uvify-blue to-uvify-purple text-white font-semibold rounded-lg hover:shadow-lg transform hover:-translate-y-0.5 transition-all disabled:opacity-50 disabled:cursor-not-allowed disabled:transform-none"
|
| 69 |
+
>
|
| 70 |
+
{isLoading ? 'Analyzing...' : 'Analyze'}
|
| 71 |
+
</button>
|
| 72 |
+
</div>
|
| 73 |
+
</form>
|
| 74 |
+
|
| 75 |
+
<div className="mt-6">
|
| 76 |
+
<p className="text-sm text-gray-500 mb-3">Try these example repositories:</p>
|
| 77 |
+
<div className="flex flex-wrap gap-2">
|
| 78 |
+
{exampleRepos.map((repo) => (
|
| 79 |
+
<button
|
| 80 |
+
key={repo.name}
|
| 81 |
+
onClick={() => handleExample(repo.url)}
|
| 82 |
+
className="px-4 py-2 bg-gray-700 hover:bg-gray-600 text-gray-300 rounded-md text-sm transition-colors"
|
| 83 |
+
disabled={isLoading}
|
| 84 |
+
>
|
| 85 |
+
{repo.name}
|
| 86 |
+
</button>
|
| 87 |
+
))}
|
| 88 |
+
</div>
|
| 89 |
+
</div>
|
| 90 |
+
</div>
|
| 91 |
+
|
| 92 |
+
<p className="text-center text-gray-500 text-sm mt-6">
|
| 93 |
+
Supports GitHub repositories and local directories
|
| 94 |
+
</p>
|
| 95 |
+
</div>
|
| 96 |
+
</section>
|
| 97 |
+
);
|
| 98 |
+
};
|
| 99 |
+
|
| 100 |
+
export default Hero;
|
src/components/Results.tsx
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useState } from 'react';
|
| 2 |
+
import type { UvifyResult } from '../types/uvify';
|
| 3 |
+
|
| 4 |
+
interface ResultsProps {
|
| 5 |
+
results: UvifyResult[];
|
| 6 |
+
source: string;
|
| 7 |
+
error?: string;
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
const Results = ({ results, source, error }: ResultsProps) => {
|
| 11 |
+
const [copiedIndex, setCopiedIndex] = useState<number | null>(null);
|
| 12 |
+
|
| 13 |
+
const copyToClipboard = (text: string, index: number) => {
|
| 14 |
+
navigator.clipboard.writeText(text).then(() => {
|
| 15 |
+
setCopiedIndex(index);
|
| 16 |
+
setTimeout(() => setCopiedIndex(null), 2000);
|
| 17 |
+
});
|
| 18 |
+
};
|
| 19 |
+
|
| 20 |
+
const downloadResults = () => {
|
| 21 |
+
const dataStr = JSON.stringify(results, null, 2);
|
| 22 |
+
const dataUri = 'data:application/json;charset=utf-8,'+ encodeURIComponent(dataStr);
|
| 23 |
+
|
| 24 |
+
const exportFileDefaultName = `uvify-${source.replace(/[^a-z0-9]/gi, '-')}.json`;
|
| 25 |
+
|
| 26 |
+
const linkElement = document.createElement('a');
|
| 27 |
+
linkElement.setAttribute('href', dataUri);
|
| 28 |
+
linkElement.setAttribute('download', exportFileDefaultName);
|
| 29 |
+
linkElement.click();
|
| 30 |
+
};
|
| 31 |
+
|
| 32 |
+
if (error) {
|
| 33 |
+
return (
|
| 34 |
+
<div className="max-w-4xl mx-auto px-4 py-8">
|
| 35 |
+
<div className="bg-red-900/20 border border-red-800 rounded-lg p-6">
|
| 36 |
+
<h3 className="text-red-400 font-semibold mb-2">Error</h3>
|
| 37 |
+
<p className="text-gray-300">{error}</p>
|
| 38 |
+
</div>
|
| 39 |
+
</div>
|
| 40 |
+
);
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
if (results.length === 0) {
|
| 44 |
+
return null;
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
const totalDeps = results.reduce((acc, r) => acc + r.dependencies.length, 0);
|
| 48 |
+
const pythonVersions = [...new Set(results.map(r => r.pythonVersion).filter(Boolean))];
|
| 49 |
+
|
| 50 |
+
return (
|
| 51 |
+
<div className="max-w-6xl mx-auto px-4 py-8">
|
| 52 |
+
<div className="grid md:grid-cols-2 gap-6 mb-8">
|
| 53 |
+
<div className="bg-gray-800 rounded-lg p-6 border border-gray-700">
|
| 54 |
+
<h3 className="text-lg font-semibold mb-4">Repository Info</h3>
|
| 55 |
+
<div className="space-y-2 text-sm">
|
| 56 |
+
<div className="flex justify-between">
|
| 57 |
+
<span className="text-gray-400">Repository:</span>
|
| 58 |
+
<span className="font-mono">{source}</span>
|
| 59 |
+
</div>
|
| 60 |
+
<div className="flex justify-between">
|
| 61 |
+
<span className="text-gray-400">Files analyzed:</span>
|
| 62 |
+
<span>{results.length}</span>
|
| 63 |
+
</div>
|
| 64 |
+
<div className="flex justify-between">
|
| 65 |
+
<span className="text-gray-400">Total dependencies:</span>
|
| 66 |
+
<span>{totalDeps}</span>
|
| 67 |
+
</div>
|
| 68 |
+
{pythonVersions.length > 0 && (
|
| 69 |
+
<div className="flex justify-between">
|
| 70 |
+
<span className="text-gray-400">Python versions:</span>
|
| 71 |
+
<span className="font-mono">{pythonVersions.join(', ')}</span>
|
| 72 |
+
</div>
|
| 73 |
+
)}
|
| 74 |
+
</div>
|
| 75 |
+
<div className="mt-4 flex gap-3">
|
| 76 |
+
<button
|
| 77 |
+
onClick={() => copyToClipboard(JSON.stringify(results, null, 2), -1)}
|
| 78 |
+
className="flex-1 px-4 py-2 bg-gray-700 hover:bg-gray-600 text-sm rounded-md transition-colors flex items-center justify-center gap-2"
|
| 79 |
+
>
|
| 80 |
+
{copiedIndex === -1 ? '✓ Copied!' : '📋 Copy all'}
|
| 81 |
+
</button>
|
| 82 |
+
<button
|
| 83 |
+
onClick={downloadResults}
|
| 84 |
+
className="flex-1 px-4 py-2 bg-gray-700 hover:bg-gray-600 text-sm rounded-md transition-colors flex items-center justify-center gap-2"
|
| 85 |
+
>
|
| 86 |
+
💾 Download
|
| 87 |
+
</button>
|
| 88 |
+
</div>
|
| 89 |
+
</div>
|
| 90 |
+
|
| 91 |
+
<div className="bg-gray-800 rounded-lg p-6 border border-gray-700">
|
| 92 |
+
<h3 className="text-lg font-semibold mb-4">Directory Structure</h3>
|
| 93 |
+
<div className="font-mono text-sm text-gray-400 space-y-1">
|
| 94 |
+
<div>└── {source.split('/').pop()}/</div>
|
| 95 |
+
{results.map((result, idx) => (
|
| 96 |
+
<div key={idx} className="ml-6">
|
| 97 |
+
{idx === results.length - 1 ? '└── ' : '├── '}
|
| 98 |
+
{result.file}
|
| 99 |
+
</div>
|
| 100 |
+
))}
|
| 101 |
+
</div>
|
| 102 |
+
</div>
|
| 103 |
+
</div>
|
| 104 |
+
|
| 105 |
+
<h2 className="text-2xl font-bold mb-6">Analysis Results</h2>
|
| 106 |
+
|
| 107 |
+
<div className="space-y-6">
|
| 108 |
+
{results.map((result, index) => (
|
| 109 |
+
<div key={index} className="bg-gray-800 rounded-lg p-6 border border-gray-700">
|
| 110 |
+
<div className="flex items-start justify-between mb-4">
|
| 111 |
+
<div>
|
| 112 |
+
<h3 className="text-xl font-semibold mb-1">
|
| 113 |
+
{result.packageName || result.file}
|
| 114 |
+
</h3>
|
| 115 |
+
<p className="text-sm text-gray-400">
|
| 116 |
+
{result.fileType} • {result.dependencies.length} dependencies
|
| 117 |
+
{result.pythonVersion && ` • Python ${result.pythonVersion}`}
|
| 118 |
+
</p>
|
| 119 |
+
</div>
|
| 120 |
+
<button
|
| 121 |
+
onClick={() => copyToClipboard(result.oneLiner, index)}
|
| 122 |
+
className="px-3 py-1 bg-gray-700 hover:bg-gray-600 text-sm rounded-md transition-colors"
|
| 123 |
+
>
|
| 124 |
+
{copiedIndex === index ? '✓' : '📋'}
|
| 125 |
+
</button>
|
| 126 |
+
</div>
|
| 127 |
+
|
| 128 |
+
<div className="space-y-4">
|
| 129 |
+
<div className="bg-gray-900 rounded-lg p-4 overflow-x-auto">
|
| 130 |
+
<p className="text-xs text-gray-500 mb-2">One-liner command:</p>
|
| 131 |
+
<code className="text-sm text-green-400 font-mono whitespace-pre">
|
| 132 |
+
{result.oneLiner}
|
| 133 |
+
</code>
|
| 134 |
+
</div>
|
| 135 |
+
|
| 136 |
+
{result.uvInstallFromSource && (
|
| 137 |
+
<div className="bg-gray-900 rounded-lg p-4 overflow-x-auto">
|
| 138 |
+
<p className="text-xs text-gray-500 mb-2">Install from source:</p>
|
| 139 |
+
<code className="text-sm text-blue-400 font-mono whitespace-pre">
|
| 140 |
+
{result.uvInstallFromSource}
|
| 141 |
+
</code>
|
| 142 |
+
</div>
|
| 143 |
+
)}
|
| 144 |
+
|
| 145 |
+
{result.dependencies.length > 0 && (
|
| 146 |
+
<div>
|
| 147 |
+
<p className="text-sm text-gray-400 mb-2">Dependencies:</p>
|
| 148 |
+
<div className="bg-gray-900 rounded-lg p-4">
|
| 149 |
+
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
| 150 |
+
{result.dependencies.map((dep, depIndex) => (
|
| 151 |
+
<code key={depIndex} className="text-sm text-gray-300 font-mono">
|
| 152 |
+
{dep}
|
| 153 |
+
</code>
|
| 154 |
+
))}
|
| 155 |
+
</div>
|
| 156 |
+
</div>
|
| 157 |
+
</div>
|
| 158 |
+
)}
|
| 159 |
+
</div>
|
| 160 |
+
</div>
|
| 161 |
+
))}
|
| 162 |
+
</div>
|
| 163 |
+
</div>
|
| 164 |
+
);
|
| 165 |
+
};
|
| 166 |
+
|
| 167 |
+
export default Results;
|
src/index.css
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@tailwind base;
|
| 2 |
+
@tailwind components;
|
| 3 |
+
@tailwind utilities;
|
src/main.tsx
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { StrictMode } from 'react'
|
| 2 |
+
import { createRoot } from 'react-dom/client'
|
| 3 |
+
import './index.css'
|
| 4 |
+
import App from './App.tsx'
|
| 5 |
+
|
| 6 |
+
createRoot(document.getElementById('root')!).render(
|
| 7 |
+
<StrictMode>
|
| 8 |
+
<App />
|
| 9 |
+
</StrictMode>,
|
| 10 |
+
)
|
src/services/uvifyApi.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { UvifyResult } from '../types/uvify';
|
| 2 |
+
|
| 3 |
+
const API_BASE_URL = import.meta.env.VITE_API_URL || (import.meta.env.PROD ? '' : 'http://localhost:8000');
|
| 4 |
+
|
| 5 |
+
export const analyzeRepository = async (source: string): Promise<UvifyResult[]> => {
|
| 6 |
+
try {
|
| 7 |
+
// Clean up the source input
|
| 8 |
+
let cleanSource = source.trim();
|
| 9 |
+
|
| 10 |
+
// Remove https://github.com/ prefix if present
|
| 11 |
+
if (cleanSource.startsWith('https://github.com/')) {
|
| 12 |
+
cleanSource = cleanSource.replace('https://github.com/', '');
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
// Remove trailing .git if present
|
| 16 |
+
if (cleanSource.endsWith('.git')) {
|
| 17 |
+
cleanSource = cleanSource.slice(0, -4);
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
// Make the API call to the mounted API endpoint
|
| 21 |
+
const response = await fetch(`${API_BASE_URL}/api/${cleanSource}`);
|
| 22 |
+
|
| 23 |
+
if (!response.ok) {
|
| 24 |
+
const errorText = await response.text();
|
| 25 |
+
throw new Error(errorText || `HTTP error! status: ${response.status}`);
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
const data = await response.json();
|
| 29 |
+
return data;
|
| 30 |
+
} catch (error) {
|
| 31 |
+
if (error instanceof Error) {
|
| 32 |
+
throw error;
|
| 33 |
+
}
|
| 34 |
+
throw new Error('An unexpected error occurred');
|
| 35 |
+
}
|
| 36 |
+
};
|
src/types/uvify.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export interface UvifyResult {
|
| 2 |
+
file: string;
|
| 3 |
+
fileType: string;
|
| 4 |
+
oneLiner: string;
|
| 5 |
+
uvInstallFromSource?: string;
|
| 6 |
+
dependencies: string[];
|
| 7 |
+
packageName?: string;
|
| 8 |
+
pythonVersion?: string;
|
| 9 |
+
isLocal: boolean;
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
export interface UvifyRequest {
|
| 13 |
+
source: string;
|
| 14 |
+
excludePatterns?: string[];
|
| 15 |
+
includePatterns?: string[];
|
| 16 |
+
}
|
src/vite-env.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
/// <reference types="vite/client" />
|
start.sh
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
|
| 3 |
+
# Colors for output
|
| 4 |
+
RED='\033[0;31m'
|
| 5 |
+
GREEN='\033[0;32m'
|
| 6 |
+
YELLOW='\033[1;33m'
|
| 7 |
+
NC='\033[0m' # No Color
|
| 8 |
+
|
| 9 |
+
echo -e "${GREEN}Starting Uvify UI Server...${NC}"
|
| 10 |
+
|
| 11 |
+
# Check if node_modules exists
|
| 12 |
+
if [ ! -d "node_modules" ]; then
|
| 13 |
+
echo -e "${YELLOW}Installing Node dependencies...${NC}"
|
| 14 |
+
npm install
|
| 15 |
+
fi
|
| 16 |
+
|
| 17 |
+
# Activate Python virtual environment
|
| 18 |
+
if [ -d ".venv" ]; then
|
| 19 |
+
source .venv/bin/activate
|
| 20 |
+
else
|
| 21 |
+
echo -e "${YELLOW}Creating Python virtual environment...${NC}"
|
| 22 |
+
uv venv
|
| 23 |
+
source .venv/bin/activate
|
| 24 |
+
echo -e "${YELLOW}Installing Python dependencies...${NC}"
|
| 25 |
+
uv pip install 'uvify[api]'
|
| 26 |
+
fi
|
| 27 |
+
|
| 28 |
+
# Create .env file if it doesn't exist
|
| 29 |
+
if [ ! -f ".env" ]; then
|
| 30 |
+
cp .env.example .env
|
| 31 |
+
echo -e "${YELLOW}Created .env file from .env.example${NC}"
|
| 32 |
+
fi
|
| 33 |
+
|
| 34 |
+
# Function to cleanup on exit
|
| 35 |
+
cleanup() {
|
| 36 |
+
echo -e "\n${YELLOW}Shutting down servers...${NC}"
|
| 37 |
+
kill $UVIFY_PID $VITE_PID 2>/dev/null
|
| 38 |
+
exit
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
trap cleanup EXIT INT TERM
|
| 42 |
+
|
| 43 |
+
# Start uvify backend server
|
| 44 |
+
echo -e "${GREEN}Starting Uvify backend server on http://localhost:8000${NC}"
|
| 45 |
+
# Use our CORS wrapper to handle cross-origin requests
|
| 46 |
+
python uvify_cors_wrapper.py &
|
| 47 |
+
UVIFY_PID=$!
|
| 48 |
+
|
| 49 |
+
# Wait a moment for the backend to start
|
| 50 |
+
sleep 2
|
| 51 |
+
|
| 52 |
+
# Start Vite frontend server
|
| 53 |
+
echo -e "${GREEN}Starting Vite frontend server on http://localhost:5173${NC}"
|
| 54 |
+
npm run dev &
|
| 55 |
+
VITE_PID=$!
|
| 56 |
+
|
| 57 |
+
echo -e "${GREEN}Both servers are running!${NC}"
|
| 58 |
+
echo -e "Frontend: ${GREEN}http://localhost:5173${NC}"
|
| 59 |
+
echo -e "Backend API: ${GREEN}http://localhost:8000${NC}"
|
| 60 |
+
echo -e "\nPress ${YELLOW}Ctrl+C${NC} to stop both servers"
|
| 61 |
+
|
| 62 |
+
# Wait for both processes
|
| 63 |
+
wait $UVIFY_PID $VITE_PID
|
tailwind.config.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/** @type {import('tailwindcss').Config} */
|
| 2 |
+
export default {
|
| 3 |
+
content: [
|
| 4 |
+
"./index.html",
|
| 5 |
+
"./src/**/*.{js,ts,jsx,tsx}",
|
| 6 |
+
],
|
| 7 |
+
theme: {
|
| 8 |
+
extend: {
|
| 9 |
+
colors: {
|
| 10 |
+
'uvify-blue': '#3B82F6',
|
| 11 |
+
'uvify-purple': '#8B5CF6',
|
| 12 |
+
},
|
| 13 |
+
fontFamily: {
|
| 14 |
+
'mono': ['Consolas', 'Monaco', 'Courier New', 'monospace'],
|
| 15 |
+
},
|
| 16 |
+
},
|
| 17 |
+
},
|
| 18 |
+
plugins: [],
|
| 19 |
+
}
|
tsconfig.app.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"compilerOptions": {
|
| 3 |
+
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
| 4 |
+
"target": "ES2022",
|
| 5 |
+
"useDefineForClassFields": true,
|
| 6 |
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
| 7 |
+
"module": "ESNext",
|
| 8 |
+
"skipLibCheck": true,
|
| 9 |
+
|
| 10 |
+
/* Bundler mode */
|
| 11 |
+
"moduleResolution": "bundler",
|
| 12 |
+
"allowImportingTsExtensions": true,
|
| 13 |
+
"verbatimModuleSyntax": true,
|
| 14 |
+
"moduleDetection": "force",
|
| 15 |
+
"noEmit": true,
|
| 16 |
+
"jsx": "react-jsx",
|
| 17 |
+
|
| 18 |
+
/* Linting */
|
| 19 |
+
"strict": true,
|
| 20 |
+
"noUnusedLocals": true,
|
| 21 |
+
"noUnusedParameters": true,
|
| 22 |
+
"erasableSyntaxOnly": true,
|
| 23 |
+
"noFallthroughCasesInSwitch": true,
|
| 24 |
+
"noUncheckedSideEffectImports": true
|
| 25 |
+
},
|
| 26 |
+
"include": ["src"]
|
| 27 |
+
}
|
tsconfig.json
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"files": [],
|
| 3 |
+
"references": [
|
| 4 |
+
{ "path": "./tsconfig.app.json" },
|
| 5 |
+
{ "path": "./tsconfig.node.json" }
|
| 6 |
+
]
|
| 7 |
+
}
|
tsconfig.node.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"compilerOptions": {
|
| 3 |
+
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
| 4 |
+
"target": "ES2023",
|
| 5 |
+
"lib": ["ES2023"],
|
| 6 |
+
"module": "ESNext",
|
| 7 |
+
"skipLibCheck": true,
|
| 8 |
+
|
| 9 |
+
/* Bundler mode */
|
| 10 |
+
"moduleResolution": "bundler",
|
| 11 |
+
"allowImportingTsExtensions": true,
|
| 12 |
+
"verbatimModuleSyntax": true,
|
| 13 |
+
"moduleDetection": "force",
|
| 14 |
+
"noEmit": true,
|
| 15 |
+
|
| 16 |
+
/* Linting */
|
| 17 |
+
"strict": true,
|
| 18 |
+
"noUnusedLocals": true,
|
| 19 |
+
"noUnusedParameters": true,
|
| 20 |
+
"erasableSyntaxOnly": true,
|
| 21 |
+
"noFallthroughCasesInSwitch": true,
|
| 22 |
+
"noUncheckedSideEffectImports": true
|
| 23 |
+
},
|
| 24 |
+
"include": ["vite.config.ts"]
|
| 25 |
+
}
|
uvify_cors_wrapper.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
CORS wrapper for uvify API
|
| 4 |
+
"""
|
| 5 |
+
from fastapi import FastAPI
|
| 6 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 7 |
+
import uvify
|
| 8 |
+
|
| 9 |
+
# Get the original FastAPI app from uvify
|
| 10 |
+
app = uvify.api
|
| 11 |
+
|
| 12 |
+
# Add CORS middleware
|
| 13 |
+
app.add_middleware(
|
| 14 |
+
CORSMiddleware,
|
| 15 |
+
allow_origins=["http://localhost:5173", "http://localhost:5174", "http://localhost:5175"],
|
| 16 |
+
allow_credentials=True,
|
| 17 |
+
allow_methods=["*"],
|
| 18 |
+
allow_headers=["*"],
|
| 19 |
+
)
|
| 20 |
+
|
| 21 |
+
if __name__ == "__main__":
|
| 22 |
+
import uvicorn
|
| 23 |
+
uvicorn.run(app, host="0.0.0.0", port=8000)
|
vite.config.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { defineConfig } from 'vite'
|
| 2 |
+
import react from '@vitejs/plugin-react'
|
| 3 |
+
|
| 4 |
+
// https://vite.dev/config/
|
| 5 |
+
export default defineConfig({
|
| 6 |
+
plugins: [react()],
|
| 7 |
+
server: {
|
| 8 |
+
proxy: {
|
| 9 |
+
'/api': {
|
| 10 |
+
target: 'http://localhost:8000',
|
| 11 |
+
changeOrigin: true,
|
| 12 |
+
}
|
| 13 |
+
}
|
| 14 |
+
},
|
| 15 |
+
build: {
|
| 16 |
+
outDir: 'dist',
|
| 17 |
+
assetsDir: 'assets',
|
| 18 |
+
}
|
| 19 |
+
})
|