| """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_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()) |
|
|
| |
| has_next_config = any( |
| f.startswith("next.config") for f in files |
| ) |
| if has_next_config: |
| return "nextjs" |
|
|
| |
| 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 |
|
|
| |
| for fw, signals in FRAMEWORK_SIGNALS.items(): |
| for signal in signals: |
| if signal in all_code: |
| return fw |
|
|
| |
| 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 |
|
|
|
|
| |
|
|
| 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, |
| } |
| gen = templates.get(framework) |
| if gen: |
| return gen() |
| |
| return _dockerfile_express() |
|
|
|
|
| |
|
|
| def _scan_js_imports(code: str) -> set[str]: |
| """Scan JS/TS code for import/require statements and return package names.""" |
| packages = set() |
|
|
| |
| for m in re.finditer(r"import\s+.*?\s+from\s+['\"](@?[\w-]+/[\w-]+|[\w-]+)", code): |
| packages.add(m.group(1)) |
|
|
| |
| for m in re.finditer(r"import\s+['\"](@?[\w-]+/[\w-]+|[\w-]+)['\"]", code): |
| packages.add(m.group(1)) |
|
|
| |
| for m in re.finditer(r"require\s*\(\s*['\"](@?[\w-]+/[\w-]+|[\w-]+)['\"]\s*\)", code): |
| packages.add(m.group(1)) |
|
|
| return packages |
|
|
|
|
| |
| _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_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": {}, |
| } |
|
|
| |
| _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 |
|
|
| |
| 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", {}) |
|
|
| |
| fw_deps = _FRAMEWORK_DEPS.get(framework, {}) |
| for name, version in fw_deps.items(): |
| deps[name] = version |
|
|
| |
| 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") |
| |
| 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 |
|
|
| |
| 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") |
| |
| 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 = """node_modules |
| npm-debug.log* |
| yarn-debug.log* |
| yarn-error.log* |
| .next |
| .git |
| .gitignore |
| README.md |
| .env |
| .env.local |
| .env.production |
| .DS_Store |
| """ |
|
|
|
|
| |
|
|
| 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 |
|
|
|
|
| |
|
|
| 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 |
| """ |
|
|
|
|
| |
|
|
| 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> |
| """ |
|
|
|
|
| |
|
|
| 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) |
|
|
| |
| 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) |
|
|
| |
| if "Dockerfile" not in augmented: |
| augmented["Dockerfile"] = generate_dockerfile(framework) |
|
|
| |
| if ".dockerignore" not in augmented: |
| augmented[".dockerignore"] = DOCKERIGNORE |
|
|
| |
| 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, |
| ) |
|
|
| |
| 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 |
|
|
| |
| if "index.html" not in augmented: |
| augmented["index.html"] = generate_index_html(project_name) |
|
|
| return augmented |
|
|