Spaces:
Running
Running
| """Dockerfile and package.json generator for JS/TS frameworks. | |
| Auto-generates Dockerfile, package.json, and .dockerignore for | |
| React, Next.js, Vue.js, Express, NestJS, and plain Node.js projects | |
| so they can be pushed to HuggingFace Docker Spaces. | |
| """ | |
| from __future__ import annotations | |
| import re | |
| from typing import Any | |
| # βββ Framework Detection ββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Keywords in code that identify a framework | |
| FRAMEWORK_SIGNALS: dict[str, list[str]] = { | |
| "nextjs": [ | |
| "from 'next", 'from "next', | |
| "next/link", "next/image", "next/router", "next/head", | |
| "NextResponse", "NextRequest", "next/navigation", | |
| "getServerSideProps", "getStaticProps", | |
| ], | |
| "react": [ | |
| "from 'react", 'from "react', | |
| "react-dom", "ReactDOM", "useState", "useEffect", | |
| "jsx", "tsx", "React.Component", "React.createElement", | |
| ], | |
| "vue": [ | |
| "from 'vue", 'from "vue', | |
| "createApp", "Vue.createApp", "<template>", | |
| "defineComponent", "ref(", "reactive(", | |
| ], | |
| "express": [ | |
| "require('express')", 'require("express")', | |
| "from 'express", 'from "express', | |
| "express()", "express.Router", | |
| ], | |
| "nestjs": [ | |
| "@Module", "@Controller", "@Get", "@Post", "@Put", "@Delete", | |
| "from '@nestjs", 'from "@nestjs', | |
| "NestFactory.create", | |
| ], | |
| "nodejs": [ | |
| "require('http')", "http.createServer", | |
| "const http = require", "import http from", | |
| ], | |
| } | |
| def detect_framework(files: dict[str, str]) -> str: | |
| """Detect the JS/TS framework from project files. | |
| Returns one of: 'nextjs', 'react', 'vue', 'express', 'nestjs', 'nodejs', 'static' | |
| """ | |
| all_code = "\n".join(files.values()) | |
| # Check file names first (strong signal) | |
| has_next_config = any( | |
| f.startswith("next.config") for f in files | |
| ) | |
| if has_next_config: | |
| return "nextjs" | |
| # Check package.json if present | |
| for fname, content in files.items(): | |
| if fname == "package.json" or fname.endswith("/package.json"): | |
| try: | |
| import json | |
| pkg = json.loads(content) | |
| deps = {**pkg.get("dependencies", {}), **pkg.get("devDependencies", {})} | |
| if "next" in deps: | |
| return "nextjs" | |
| if "vue" in deps or "@vue/cli-service" in deps: | |
| return "vue" | |
| if "@nestjs/core" in deps: | |
| return "nestjs" | |
| if "express" in deps: | |
| return "express" | |
| if "react" in deps or "react-dom" in deps: | |
| return "react" | |
| except Exception: | |
| pass | |
| # Check code content for framework signals | |
| for fw, signals in FRAMEWORK_SIGNALS.items(): | |
| for signal in signals: | |
| if signal in all_code: | |
| return fw | |
| # Check file extensions | |
| has_jsx_tsx = any(f.endswith((".jsx", ".tsx")) for f in files) | |
| has_vue = any(f.endswith(".vue") for f in files) | |
| if has_vue: | |
| return "vue" | |
| if has_jsx_tsx: | |
| return "react" | |
| return "static" | |
| def is_js_project(files: dict[str, str]) -> bool: | |
| """Check if the project is a JavaScript/TypeScript project.""" | |
| js_extensions = {".js", ".jsx", ".ts", ".tsx", ".vue", ".mjs", ".cjs"} | |
| has_package_json = any("package.json" in f for f in files) | |
| has_js_files = any( | |
| any(f.endswith(ext) for ext in js_extensions) | |
| for f in files | |
| ) | |
| return has_package_json or has_js_files | |
| # βββ Dockerfile Templates βββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _dockerfile_nextjs() -> str: | |
| """Dockerfile for Next.js projects.""" | |
| return """FROM node:20-slim AS base | |
| # Install dependencies only when needed | |
| FROM base AS deps | |
| WORKDIR /app | |
| COPY package.json package-lock.json* yarn.lock* pnpm-lock.yaml* ./ | |
| RUN \\ | |
| if [ -f yarn.lock ]; then yarn --frozen-lockfile; \\ | |
| elif [ -f package-lock.json ]; then npm ci; \\ | |
| elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; \\ | |
| else npm i; \\ | |
| fi | |
| # Rebuild source code only when needed | |
| FROM base AS builder | |
| WORKDIR /app | |
| COPY --from=deps /app/node_modules ./node_modules | |
| COPY . . | |
| RUN npm run build | |
| # Production image | |
| FROM base AS runner | |
| WORKDIR /app | |
| ENV NODE_ENV=production | |
| RUN addgroup --system --gid 1001 nodejs | |
| RUN adduser --system --uid 1001 nextjs | |
| COPY --from=builder /app/public ./public | |
| COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ | |
| COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static | |
| USER nextjs | |
| EXPOSE 3000 | |
| ENV PORT=3000 | |
| ENV HOSTNAME="0.0.0.0" | |
| CMD ["node", "server.js"] | |
| """ | |
| def _dockerfile_react() -> str: | |
| """Dockerfile for React (Vite/CRA) projects β served with nginx.""" | |
| return """FROM node:20-slim AS build | |
| WORKDIR /app | |
| COPY package.json package-lock.json* yarn.lock* pnpm-lock.yaml* ./ | |
| RUN \\ | |
| if [ -f yarn.lock ]; then yarn --frozen-lockfile; \\ | |
| elif [ -f package-lock.json ]; then npm ci; \\ | |
| elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; \\ | |
| else npm i; \\ | |
| fi | |
| COPY . . | |
| RUN npm run build | |
| FROM nginx:alpine | |
| COPY --from=build /app/build /usr/share/nginx/html | |
| COPY --from=build /app/dist /usr/share/nginx/html 2>/dev/null || true | |
| RUN cat > /etc/nginx/conf.d/default.conf << 'EOF' | |
| server { | |
| listen 7860; | |
| server_name localhost; | |
| root /usr/share/nginx/html; | |
| index index.html; | |
| location / { | |
| try_files $uri $uri/ /index.html; | |
| } | |
| } | |
| EOF | |
| EXPOSE 7860 | |
| CMD ["nginx", "-g", "daemon off;"] | |
| """ | |
| def _dockerfile_vue() -> str: | |
| """Dockerfile for Vue.js projects β served with nginx.""" | |
| return """FROM node:20-slim AS build | |
| WORKDIR /app | |
| COPY package.json package-lock.json* yarn.lock* pnpm-lock.yaml* ./ | |
| RUN \\ | |
| if [ -f yarn.lock ]; then yarn --frozen-lockfile; \\ | |
| elif [ -f package-lock.json ]; then npm ci; \\ | |
| elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; \\ | |
| else npm i; \\ | |
| fi | |
| COPY . . | |
| RUN npm run build | |
| FROM nginx:alpine | |
| COPY --from=build /app/dist /usr/share/nginx/html | |
| RUN cat > /etc/nginx/conf.d/default.conf << 'EOF' | |
| server { | |
| listen 7860; | |
| server_name localhost; | |
| root /usr/share/nginx/html; | |
| index index.html; | |
| location / { | |
| try_files $uri $uri/ /index.html; | |
| } | |
| } | |
| EOF | |
| EXPOSE 7860 | |
| CMD ["nginx", "-g", "daemon off;"] | |
| """ | |
| def _dockerfile_express() -> str: | |
| """Dockerfile for Express/Node.js server projects.""" | |
| return """FROM node:20-slim | |
| WORKDIR /app | |
| COPY package.json package-lock.json* yarn.lock* pnpm-lock.yaml* ./ | |
| RUN \\ | |
| if [ -f yarn.lock ]; then yarn --frozen-lockfile; \\ | |
| elif [ -f package-lock.json ]; then npm ci; \\ | |
| elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; \\ | |
| else npm i; \\ | |
| fi | |
| COPY . . | |
| RUN addgroup --system --gid 1001 appuser && adduser --system --uid 1001 appuser | |
| USER appuser | |
| EXPOSE 7860 | |
| ENV PORT=7860 | |
| ENV HOST=0.0.0.0 | |
| CMD ["node", "index.js"] | |
| """ | |
| def _dockerfile_nestjs() -> str: | |
| """Dockerfile for NestJS projects.""" | |
| return """FROM node:20-slim AS build | |
| WORKDIR /app | |
| COPY package.json package-lock.json* yarn.lock* pnpm-lock.yaml* ./ | |
| RUN \\ | |
| if [ -f yarn.lock ]; then yarn --frozen-lockfile; \\ | |
| elif [ -f package-lock.json ]; then npm ci; \\ | |
| elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; \\ | |
| else npm i; \\ | |
| fi | |
| COPY . . | |
| RUN npm run build | |
| FROM node:20-slim | |
| WORKDIR /app | |
| COPY package.json package-lock.json* yarn.lock* pnpm-lock.yaml* ./ | |
| RUN \\ | |
| if [ -f yarn.lock ]; then yarn --frozen-lockfile --production; \\ | |
| elif [ -f package-lock.json ]; then npm ci --only=production; \\ | |
| elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile --prod; \\ | |
| else npm i --only=production; \\ | |
| fi | |
| COPY --from=build /app/dist ./dist | |
| RUN addgroup --system --gid 1001 appuser && adduser --system --uid 1001 appuser | |
| USER appuser | |
| EXPOSE 7860 | |
| ENV PORT=7860 | |
| CMD ["node", "dist/main.js"] | |
| """ | |
| def generate_dockerfile(framework: str) -> str: | |
| """Generate a Dockerfile for the given framework. | |
| Args: | |
| framework: One of 'nextjs', 'react', 'vue', 'express', 'nestjs', 'nodejs', 'static' | |
| Returns: | |
| Dockerfile content as string. | |
| """ | |
| templates = { | |
| "nextjs": _dockerfile_nextjs, | |
| "react": _dockerfile_react, | |
| "vue": _dockerfile_vue, | |
| "express": _dockerfile_express, | |
| "nestjs": _dockerfile_nestjs, | |
| "nodejs": _dockerfile_express, # Same as express for plain Node | |
| } | |
| gen = templates.get(framework) | |
| if gen: | |
| return gen() | |
| # Fallback: generic node server | |
| return _dockerfile_express() | |
| # βββ package.json Generator βββββββββββββββββββββββββββββββββββββββββββββ | |
| def _scan_js_imports(code: str) -> set[str]: | |
| """Scan JS/TS code for import/require statements and return package names.""" | |
| packages = set() | |
| # ESM: import xxx from 'pkg' / import 'pkg' | |
| for m in re.finditer(r"import\s+.*?\s+from\s+['\"](@?[\w-]+/[\w-]+|[\w-]+)", code): | |
| packages.add(m.group(1)) | |
| # ESM: import 'pkg' | |
| for m in re.finditer(r"import\s+['\"](@?[\w-]+/[\w-]+|[\w-]+)['\"]", code): | |
| packages.add(m.group(1)) | |
| # CJS: require('pkg') | |
| for m in re.finditer(r"require\s*\(\s*['\"](@?[\w-]+/[\w-]+|[\w-]+)['\"]\s*\)", code): | |
| packages.add(m.group(1)) | |
| return packages | |
| # Known packages that should NOT go in dependencies (built-in or types) | |
| _SKIP_PACKAGES = { | |
| "react", "react-dom", "next", "vue", "express", | |
| "path", "fs", "http", "https", "url", "os", "crypto", | |
| "stream", "util", "events", "buffer", "child_process", | |
| "net", "tls", "zlib", "assert", "querystring", | |
| } | |
| # Framework-specific dependency sets | |
| _FRAMEWORK_DEPS: dict[str, dict[str, str]] = { | |
| "nextjs": { | |
| "next": "14.2.0", | |
| "react": "^18.3.0", | |
| "react-dom": "^18.3.0", | |
| }, | |
| "react": { | |
| "react": "^18.3.0", | |
| "react-dom": "^18.3.0", | |
| "react-scripts": "5.0.1", | |
| }, | |
| "vue": { | |
| "vue": "^3.4.0", | |
| }, | |
| "express": { | |
| "express": "^4.19.0", | |
| }, | |
| "nestjs": { | |
| "@nestjs/core": "^10.3.0", | |
| "@nestjs/common": "^10.3.0", | |
| "@nestjs/platform-express": "^10.3.0", | |
| "reflect-metadata": "^0.2.0", | |
| "rxjs": "^7.8.0", | |
| }, | |
| "nodejs": {}, | |
| } | |
| # Common package version mapping | |
| _PACKAGE_VERSIONS: dict[str, str] = { | |
| "axios": "^1.6.0", | |
| "lodash": "^4.17.21", | |
| "cors": "^2.8.5", | |
| "dotenv": "^16.4.0", | |
| "mongoose": "^8.2.0", | |
| "prisma": "^5.10.0", | |
| "@prisma/client": "^5.10.0", | |
| "zod": "^3.22.0", | |
| "socket.io": "^4.7.0", | |
| "multer": "^1.4.4", | |
| "cookie-parser": "^1.4.6", | |
| "express-session": "^1.18.0", | |
| "jsonwebtoken": "^9.0.0", | |
| "bcrypt": "^5.1.0", | |
| "uuid": "^9.0.0", | |
| "dayjs": "^1.11.10", | |
| "chart.js": "^4.4.0", | |
| "framer-motion": "^11.0.0", | |
| "lucide-react": "^0.350.0", | |
| "tailwindcss": "^3.4.0", | |
| "postcss": "^8.4.0", | |
| "autoprefixer": "^10.4.0", | |
| "@vitejs/plugin-react": "^4.2.0", | |
| "vite": "^5.1.0", | |
| "typescript": "^5.3.0", | |
| "@types/react": "^18.3.0", | |
| "@types/react-dom": "^18.3.0", | |
| "@types/node": "^20.11.0", | |
| "tailwind-merge": "^2.2.0", | |
| "clsx": "^2.1.0", | |
| "class-variance-authority": "^0.7.0", | |
| "@radix-ui/react-slot": "^1.0.2", | |
| "next-themes": "^0.3.0", | |
| "recharts": "^2.12.0", | |
| "react-hook-form": "^7.50.0", | |
| "@hookform/resolvers": "^3.3.0", | |
| "zustand": "^4.5.0", | |
| "jotai": "^2.6.0", | |
| "tanstack": "^5.24.0", | |
| "@tanstack/react-query": "^5.24.0", | |
| "swr": "^2.2.0", | |
| "nodemon": "^3.1.0", | |
| "ts-node": "^10.9.0", | |
| "ts-node-dev": "^2.0.0", | |
| } | |
| def generate_package_json( | |
| framework: str, | |
| project_name: str = "my-app", | |
| extra_deps: set[str] | None = None, | |
| existing_content: str | None = None, | |
| ) -> str: | |
| """Generate a package.json for the given framework. | |
| If existing_content is provided, merges dependencies into it. | |
| """ | |
| import json | |
| # Start with existing or fresh | |
| if existing_content: | |
| try: | |
| pkg = json.loads(existing_content) | |
| except Exception: | |
| pkg = {} | |
| else: | |
| pkg = {} | |
| pkg.setdefault("name", project_name) | |
| pkg.setdefault("version", "1.0.0") | |
| pkg.setdefault("private", True) | |
| deps = pkg.get("dependencies", {}) | |
| dev_deps = pkg.get("devDependencies", {}) | |
| # Add framework core deps | |
| fw_deps = _FRAMEWORK_DEPS.get(framework, {}) | |
| for name, version in fw_deps.items(): | |
| deps[name] = version | |
| # Add scanned extra deps | |
| if extra_deps: | |
| for dep in extra_deps: | |
| if dep in _SKIP_PACKAGES: | |
| continue | |
| if dep in deps or dep in dev_deps: | |
| continue | |
| version = _PACKAGE_VERSIONS.get(dep, "^1.0.0") | |
| # Dev deps | |
| if dep.startswith("@types/") or dep in {"typescript", "nodemon", "ts-node", "ts-node-dev"}: | |
| dev_deps[dep] = version | |
| else: | |
| deps[dep] = version | |
| pkg["dependencies"] = deps | |
| if dev_deps: | |
| pkg["devDependencies"] = dev_deps | |
| # Add scripts based on framework | |
| scripts = pkg.get("scripts", {}) | |
| if framework == "nextjs": | |
| scripts.setdefault("dev", "next dev") | |
| scripts.setdefault("build", "next build") | |
| scripts.setdefault("start", "next start -p 7860") | |
| elif framework in ("react",): | |
| scripts.setdefault("dev", "vite") | |
| scripts.setdefault("build", "vite build") | |
| scripts.setdefault("start", "vite preview --port 7860 --host 0.0.0.0") | |
| # Ensure vite is in devDeps | |
| if "vite" not in dev_deps and "vite" not in deps: | |
| dev_deps["vite"] = "^5.1.0" | |
| if "@vitejs/plugin-react" not in dev_deps: | |
| dev_deps["@vitejs/plugin-react"] = "^4.2.0" | |
| elif framework == "vue": | |
| scripts.setdefault("dev", "vite") | |
| scripts.setdefault("build", "vite build") | |
| scripts.setdefault("start", "vite preview --port 7860 --host 0.0.0.0") | |
| if "vite" not in dev_deps and "vite" not in deps: | |
| dev_deps["vite"] = "^5.1.0" | |
| elif framework in ("express", "nodejs"): | |
| scripts.setdefault("dev", "node index.js") | |
| scripts.setdefault("start", "node index.js") | |
| elif framework == "nestjs": | |
| scripts.setdefault("build", "nest build") | |
| scripts.setdefault("start", "node dist/main.js") | |
| pkg["scripts"] = scripts | |
| pkg["dependencies"] = deps | |
| if dev_deps: | |
| pkg["devDependencies"] = dev_deps | |
| return json.dumps(pkg, indent=2) + "\n" | |
| # βββ .dockerignore ββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| DOCKERIGNORE = """node_modules | |
| npm-debug.log* | |
| yarn-debug.log* | |
| yarn-error.log* | |
| .next | |
| .git | |
| .gitignore | |
| README.md | |
| .env | |
| .env.local | |
| .env.production | |
| .DS_Store | |
| """ | |
| # βββ Vite Config Generators βββββββββββββββββββββββββββββββββββββββββββββ | |
| def generate_vite_config(framework: str) -> str | None: | |
| """Generate a vite.config.js/ts if needed for React or Vue.""" | |
| if framework == "react": | |
| return """import { defineConfig } from 'vite' | |
| import react from '@vitejs/plugin-react' | |
| export default defineConfig({ | |
| plugins: [react()], | |
| server: { | |
| host: '0.0.0.0', | |
| port: 7860, | |
| }, | |
| }) | |
| """ | |
| if framework == "vue": | |
| return """import { defineConfig } from 'vite' | |
| import vue from '@vitejs/plugin-vue' | |
| export default defineConfig({ | |
| plugins: [vue()], | |
| server: { | |
| host: '0.0.0.0', | |
| port: 7860, | |
| }, | |
| }) | |
| """ | |
| return None | |
| # βββ Next.js Config ββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def generate_next_config() -> str: | |
| """Generate next.config.js with standalone output for Docker.""" | |
| return """/** @type {import('next').NextConfig} */ | |
| const nextConfig = { | |
| output: 'standalone', | |
| } | |
| module.exports = nextConfig | |
| """ | |
| # βββ Public index.html for Vite βββββββββββββββββββββββββββββββββββββββββ | |
| def generate_index_html(title: str = "App") -> str: | |
| """Generate a minimal index.html for Vite projects.""" | |
| return f"""<!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
| <title>{title}</title> | |
| </head> | |
| <body> | |
| <div id="root"></div> | |
| <script type="module" src="/src/main.jsx"></script> | |
| </body> | |
| </html> | |
| """ | |
| # βββ Full Project Scaffold ββββββββββββββββββββββββββββββββββββββββββββββ | |
| def scaffold_js_project( | |
| files: dict[str, str], | |
| framework: str, | |
| project_name: str = "my-app", | |
| ) -> dict[str, str]: | |
| """Add Dockerfile, package.json, and config files to a JS project. | |
| Takes existing generated files and returns an augmented dict with | |
| Docker support files added. | |
| """ | |
| augmented = dict(files) | |
| # Detect imports from all JS/TS files | |
| all_js_code = "\n".join( | |
| content for fname, content in files.items() | |
| if fname.endswith((".js", ".jsx", ".ts", ".tsx", ".vue", ".mjs")) | |
| ) | |
| extra_deps = _scan_js_imports(all_js_code) | |
| # Add Dockerfile | |
| if "Dockerfile" not in augmented: | |
| augmented["Dockerfile"] = generate_dockerfile(framework) | |
| # Add .dockerignore | |
| if ".dockerignore" not in augmented: | |
| augmented[".dockerignore"] = DOCKERIGNORE | |
| # Add or merge package.json | |
| if "package.json" in augmented: | |
| augmented["package.json"] = generate_package_json( | |
| framework=framework, | |
| project_name=project_name, | |
| extra_deps=extra_deps, | |
| existing_content=augmented["package.json"], | |
| ) | |
| else: | |
| augmented["package.json"] = generate_package_json( | |
| framework=framework, | |
| project_name=project_name, | |
| extra_deps=extra_deps, | |
| ) | |
| # Framework-specific config files | |
| if framework == "nextjs" and "next.config.js" not in augmented and "next.config.mjs" not in augmented: | |
| augmented["next.config.js"] = generate_next_config() | |
| if framework in ("react", "vue"): | |
| vite_cfg = generate_vite_config(framework) | |
| if vite_cfg and "vite.config.js" not in augmented and "vite.config.ts" not in augmented: | |
| augmented["vite.config.js"] = vite_cfg | |
| # Add index.html entry point for Vite if not present | |
| if "index.html" not in augmented: | |
| augmented["index.html"] = generate_index_html(project_name) | |
| return augmented | |