node-project-gen 0.0.4

⚡ Blazing fast, architecture-aware Node.js backend generator. Scaffold production-ready projects in under 1 second.
use crate::generator::ProjectConfig;
use std::path::PathBuf;

pub fn generate(
    project_path: &PathBuf,
    config: &ProjectConfig,
) -> Result<(), Box<dyn std::error::Error>> {
    generate_dockerfile(project_path)?;
    generate_dockerignore(project_path)?;
    generate_docker_compose(project_path, config)?;
    generate_eslint(project_path)?;
    generate_prettier(project_path)?;
    generate_gitignore(project_path)?;
    generate_readme(project_path, config)?;
    generate_nodemon(project_path)?;
    Ok(())
}

fn generate_dockerfile(project_path: &PathBuf) -> Result<(), Box<dyn std::error::Error>> {
    let content = r#"FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build

FROM node:20-alpine
WORKDIR /app
RUN addgroup -g 1001 -S nodejs && adduser -S nodeapp -u 1001
COPY --from=builder --chown=nodeapp:nodejs /app/dist ./dist
COPY --from=builder --chown=nodeapp:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=nodeapp:nodejs /app/package.json ./
USER nodeapp
EXPOSE 3000
ENV NODE_ENV=production
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD node -e "require('http').get('http://localhost:3000/health', (r) => { if (r.statusCode !== 200) throw new Error(); })"
CMD ["node", "dist/server.js"]
"#;
    std::fs::write(project_path.join("Dockerfile"), content)?;
    Ok(())
}

fn generate_dockerignore(project_path: &PathBuf) -> Result<(), Box<dyn std::error::Error>> {
    let content = "node_modules\ndist\n.env\n.env.*\n*.log\n.git\ncoverage\ntests\n";
    std::fs::write(project_path.join(".dockerignore"), content)?;
    Ok(())
}

fn generate_docker_compose(
    project_path: &PathBuf,
    config: &ProjectConfig,
) -> Result<(), Box<dyn std::error::Error>> {
    let mut services = String::from(
        r#"version: '3.8'

services:
  app:
    build: .
    ports:
      - "3000:3000"
    env_file:
      - .env
    restart: unless-stopped
    depends_on:
"#,
    );

    if let Some(db) = config.db {
        match db {
            crate::cli::Database::Postgres => {
                services.push_str("      - postgres\n\n  postgres:\n    image: postgres:16-alpine\n    ports:\n      - \"5432:5432\"\n    environment:\n      POSTGRES_USER: user\n      POSTGRES_PASSWORD: password\n      POSTGRES_DB: mydb\n    volumes:\n      - pgdata:/var/lib/postgresql/data\n\nvolumes:\n  pgdata:\n");
            }
            crate::cli::Database::Mongodb => {
                services.push_str("      - mongo\n\n  mongo:\n    image: mongo:7\n    ports:\n      - \"27017:27017\"\n    volumes:\n      - mongodata:/data/db\n\nvolumes:\n  mongodata:\n");
            }
            crate::cli::Database::Mysql => {
                services.push_str("      - mysql\n\n  mysql:\n    image: mysql:8\n    ports:\n      - \"3306:3306\"\n    environment:\n      MYSQL_ROOT_PASSWORD: password\n      MYSQL_DATABASE: mydb\n    volumes:\n      - mysqldata:/var/lib/mysql\n\nvolumes:\n  mysqldata:\n");
            }
            _ => {
                services.push_str("      []\n");
            }
        }
    } else {
        services.push_str("      []\n");
    }

    std::fs::write(project_path.join("docker-compose.yml"), services)?;
    Ok(())
}

fn generate_eslint(project_path: &PathBuf) -> Result<(), Box<dyn std::error::Error>> {
    let content = r#"{
  "parser": "@typescript-eslint/parser",
  "plugins": ["@typescript-eslint"],
  "extends": [
    "eslint:recommended",
    "plugin:@typescript-eslint/recommended"
  ],
  "env": { "node": true, "es2022": true },
  "parserOptions": { "ecmaVersion": 2022, "sourceType": "module" },
  "rules": {
    "@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_" }],
    "@typescript-eslint/no-explicit-any": "warn",
    "no-console": "warn"
  },
  "ignorePatterns": ["dist/", "node_modules/", "coverage/"]
}
"#;
    std::fs::write(project_path.join(".eslintrc.json"), content)?;
    Ok(())
}

fn generate_prettier(project_path: &PathBuf) -> Result<(), Box<dyn std::error::Error>> {
    let content = r#"{
  "semi": true,
  "trailingComma": "all",
  "singleQuote": true,
  "printWidth": 100,
  "tabWidth": 2,
  "endOfLine": "lf",
  "arrowParens": "always"
}
"#;
    std::fs::write(project_path.join(".prettierrc"), content)?;
    Ok(())
}

fn generate_gitignore(project_path: &PathBuf) -> Result<(), Box<dyn std::error::Error>> {
    let content = r#"# Dependencies
node_modules/

# Build
dist/

# Environment
.env
.env.local
.env.*.local

# Logs
logs/
*.log
npm-debug.log*

# IDE
.vscode/
.idea/
*.swp
*.swo

# OS
.DS_Store
Thumbs.db

# Test
coverage/

# Prisma
prisma/*.db
prisma/migrations/
"#;
    std::fs::write(project_path.join(".gitignore"), content)?;
    Ok(())
}

fn generate_readme(
    project_path: &PathBuf,
    config: &ProjectConfig,
) -> Result<(), Box<dyn std::error::Error>> {
    let content = format!(
        r#"# {name}

> Generated by [Acrocoder CLI](https://github.com/naimishverma/nodegen-cli) ⚡

## Stack

- **Framework:** {framework}
- **Architecture:** {arch}
- **Language:** TypeScript
- **Testing:** {test}
- **Validation:** {validation}
- **Logger:** {logger}

## Getting Started

```bash
# Install dependencies
npm install

# Copy environment file
cp .env.example .env

# Start development server
npm run dev
```

## Scripts

| Script | Description |
|--------|-------------|
| `npm run dev` | Start development server |
| `npm run build` | Build for production |
| `npm start` | Start production server |
| `npm test` | Run tests |
| `npm run lint` | Lint code |
| `npm run format` | Format code |

## API Documentation

Swagger docs available at: `http://localhost:3000/api-docs`

## Docker

```bash
docker-compose up -d
```

## License

MIT
"#,
        name = config.name,
        framework = config.framework,
        arch = config.arch,
        test = config.test,
        validation = config.validation,
        logger = config.logger,
    );

    std::fs::write(project_path.join("README.md"), content)?;
    Ok(())
}

fn generate_nodemon(project_path: &PathBuf) -> Result<(), Box<dyn std::error::Error>> {
    let content = r#"{
  "watch": ["src"],
  "ext": "ts,json",
  "ignore": ["dist", "node_modules"],
  "exec": "ts-node-dev --respawn --transpile-only src/server.ts"
}
"#;
    std::fs::write(project_path.join("nodemon.json"), content)?;
    Ok(())
}