fullstack-code-builder / code /huggingface /dockerfile_gen.py
R-Kentaren's picture
Upload folder using huggingface_hub
ff86d3d verified
raw
history blame contribute delete
19.2 kB
"""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