peezy-cli
Version:
Production-ready CLI for scaffolding modern applications with curated full-stack templates, intelligent migrations, and enterprise security.
1,225 lines (1,177 loc) • 37.8 kB
JavaScript
import fs from "node:fs/promises";
import path from "node:path";
import prompts from "prompts";
import { log } from "../utils/logger.js";
import { createSuccessOutput, createErrorOutput, outputJson, } from "../utils/json-output.js";
/**
* Analyze the current project to understand its context
*/
async function analyzeProject(projectPath) {
const context = {
hasPackageJson: false,
hasTypeScript: false,
hasReact: false,
hasVue: false,
hasNextJs: false,
hasExpress: false,
hasVite: false,
hasTailwind: false,
hasESLint: false,
hasPrettier: false,
hasJest: false,
hasVitest: false,
hasDocker: false,
hasPython: false,
framework: "unknown",
language: "javascript",
packageManager: "npm",
};
try {
// Check for package.json
const packageJsonPath = path.join(projectPath, "package.json");
if (await fileExists(packageJsonPath)) {
context.hasPackageJson = true;
const packageJson = JSON.parse(await fs.readFile(packageJsonPath, "utf-8"));
// Analyze dependencies
const allDeps = {
...packageJson.dependencies,
...packageJson.devDependencies,
};
context.hasReact = "react" in allDeps;
context.hasVue = "vue" in allDeps;
context.hasNextJs = "next" in allDeps;
context.hasExpress = "express" in allDeps;
context.hasVite =
"@vitejs/plugin-react" in allDeps || "@vitejs/plugin-vue" in allDeps;
context.hasTailwind = "tailwindcss" in allDeps;
context.hasESLint = "eslint" in allDeps;
context.hasPrettier = "prettier" in allDeps;
context.hasJest = "jest" in allDeps;
context.hasVitest = "vitest" in allDeps;
// Determine framework
if (context.hasNextJs)
context.framework = "nextjs";
else if (context.hasReact && context.hasVite)
context.framework = "react-vite";
else if (context.hasReact)
context.framework = "react";
else if (context.hasVue)
context.framework = "vue";
else if (context.hasExpress)
context.framework = "express";
// Determine package manager
if (await fileExists(path.join(projectPath, "bun.lockb")))
context.packageManager = "bun";
else if (await fileExists(path.join(projectPath, "pnpm-lock.yaml")))
context.packageManager = "pnpm";
else if (await fileExists(path.join(projectPath, "yarn.lock")))
context.packageManager = "yarn";
}
// Check for TypeScript
context.hasTypeScript =
(await fileExists(path.join(projectPath, "tsconfig.json"))) ||
(await fileExists(path.join(projectPath, "tsconfig.ts")));
if (context.hasTypeScript)
context.language = "typescript";
// Check for Docker
context.hasDocker = await fileExists(path.join(projectPath, "Dockerfile"));
// Check for Python
context.hasPython =
(await fileExists(path.join(projectPath, "requirements.txt"))) ||
(await fileExists(path.join(projectPath, "pyproject.toml"))) ||
(await fileExists(path.join(projectPath, "main.py"))) ||
(await fileExists(path.join(projectPath, "app.py")));
if (context.hasPython)
context.language = "python";
}
catch (error) {
log.debug(`Error analyzing project: ${error}`);
}
return context;
}
/**
* Get available file templates based on project context
*/
function getAvailableFiles(context) {
const files = [];
// TypeScript Configuration Files
if (context.hasTypeScript && context.hasPackageJson) {
if (context.hasNextJs) {
files.push({
name: "tsconfig.json",
description: "Next.js TypeScript configuration",
category: "TypeScript",
path: "configs/nextjs-tsconfig.json",
framework: ["nextjs"],
language: ["typescript"],
});
files.push({
name: "next-env.d.ts",
description: "Next.js environment type declarations",
category: "TypeScript",
path: "configs/next-env.d.ts",
framework: ["nextjs"],
language: ["typescript"],
});
}
else if (context.hasReact && context.hasVite) {
files.push({
name: "tsconfig.json",
description: "React + Vite TypeScript configuration",
category: "TypeScript",
path: "configs/react-vite-tsconfig.json",
framework: ["react-vite"],
language: ["typescript"],
});
files.push({
name: "tsconfig.node.json",
description: "Node.js TypeScript configuration for Vite",
category: "TypeScript",
path: "configs/tsconfig.node.json",
framework: ["react-vite", "vue"],
language: ["typescript"],
});
}
else if (context.hasVue) {
files.push({
name: "tsconfig.json",
description: "Vue TypeScript configuration",
category: "TypeScript",
path: "configs/vue-tsconfig.json",
framework: ["vue"],
language: ["typescript"],
});
}
else if (context.hasExpress) {
files.push({
name: "tsconfig.json",
description: "Express TypeScript configuration",
category: "TypeScript",
path: "configs/express-tsconfig.json",
framework: ["express"],
language: ["typescript"],
});
}
}
// ESLint Configuration Files
if (context.hasPackageJson) {
if (context.hasNextJs) {
files.push({
name: ".eslintrc.json",
description: "Next.js ESLint configuration",
category: "Code Quality",
path: "configs/nextjs-eslintrc.json",
dependencies: ["eslint", "eslint-config-next"],
devDependencies: [
"eslint",
"eslint-config-next",
"@typescript-eslint/eslint-plugin",
"@typescript-eslint/parser",
],
scripts: { lint: "next lint" },
framework: ["nextjs"],
});
}
else if (context.hasReact) {
files.push({
name: ".eslintrc.json",
description: "React ESLint configuration",
category: "Code Quality",
path: "configs/react-eslintrc.json",
devDependencies: [
"eslint",
"eslint-plugin-react",
"eslint-plugin-react-hooks",
"@typescript-eslint/eslint-plugin",
"@typescript-eslint/parser",
],
scripts: {
lint: "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
},
framework: ["react", "react-vite"],
});
}
else if (context.hasVue) {
files.push({
name: ".eslintrc.json",
description: "Vue ESLint configuration",
category: "Code Quality",
path: "configs/vue-eslintrc.json",
devDependencies: [
"eslint",
"eslint-plugin-vue",
"@typescript-eslint/eslint-plugin",
"@typescript-eslint/parser",
],
scripts: {
lint: "eslint . --ext ts,vue --report-unused-disable-directives --max-warnings 0",
},
framework: ["vue"],
});
}
else if (context.hasExpress) {
files.push({
name: ".eslintrc.json",
description: "Express ESLint configuration",
category: "Code Quality",
path: "configs/express-eslintrc.json",
devDependencies: [
"eslint",
"@typescript-eslint/eslint-plugin",
"@typescript-eslint/parser",
],
scripts: { lint: "eslint src --ext .ts" },
framework: ["express"],
});
}
}
// Prettier Configuration
if (context.hasPackageJson) {
files.push({
name: ".prettierrc",
description: "Prettier code formatting configuration",
category: "Code Quality",
path: "configs/prettierrc.json",
devDependencies: ["prettier"],
scripts: { format: "prettier --write ." },
});
}
// PostCSS Configuration
if (context.hasTailwind) {
files.push({
name: "postcss.config.js",
description: "PostCSS configuration for Tailwind CSS",
category: "Styling",
path: "configs/postcss.config.js",
requiredFiles: ["tailwind.config.js"],
});
}
// Testing Configuration
if (context.hasPackageJson) {
if (context.hasReact || context.hasVue) {
files.push({
name: "vitest.config.ts",
description: "Vitest testing configuration",
category: "Testing",
path: "configs/vitest.config.ts",
devDependencies: [
"vitest",
"@testing-library/react",
"@testing-library/jest-dom",
"jsdom",
],
scripts: { test: "vitest", "test:ui": "vitest --ui" },
framework: ["react", "react-vite", "vue"],
});
files.push({
name: "src/test/setup.ts",
description: "Test environment setup file",
category: "Testing",
path: "configs/test-setup.ts",
requiredFiles: ["vitest.config.ts"],
framework: ["react", "react-vite", "vue"],
});
}
if (context.hasExpress || (!context.hasReact && !context.hasVue)) {
files.push({
name: "jest.config.js",
description: "Jest testing configuration",
category: "Testing",
path: "configs/jest.config.js",
devDependencies: ["jest", "@types/jest", "ts-jest"],
scripts: { test: "jest", "test:watch": "jest --watch" },
framework: ["express"],
});
}
}
// Docker Configuration
if (context.hasPackageJson) {
if (context.hasNextJs) {
files.push({
name: "Dockerfile",
description: "Next.js optimized Docker configuration",
category: "Docker",
path: "configs/nextjs-Dockerfile",
framework: ["nextjs"],
});
files.push({
name: ".dockerignore",
description: "Docker ignore patterns for Next.js",
category: "Docker",
path: "configs/nextjs-dockerignore",
framework: ["nextjs"],
});
}
else if (context.hasReact && context.hasVite) {
files.push({
name: "Dockerfile",
description: "React SPA with Nginx Docker configuration",
category: "Docker",
path: "configs/react-spa-Dockerfile",
framework: ["react-vite"],
});
files.push({
name: "nginx.conf",
description: "Nginx configuration for React SPA",
category: "Docker",
path: "configs/nginx.conf",
requiredFiles: ["Dockerfile"],
framework: ["react-vite"],
});
files.push({
name: ".dockerignore",
description: "Docker ignore patterns for React SPA",
category: "Docker",
path: "configs/react-dockerignore",
framework: ["react-vite"],
});
}
else if (context.hasExpress) {
files.push({
name: "Dockerfile",
description: "Express.js Docker configuration",
category: "Docker",
path: "configs/express-Dockerfile",
framework: ["express"],
});
files.push({
name: ".dockerignore",
description: "Docker ignore patterns for Express",
category: "Docker",
path: "configs/express-dockerignore",
framework: ["express"],
});
}
// Docker Compose
files.push({
name: "docker-compose.yml",
description: "Docker Compose for production",
category: "Docker",
path: "configs/docker-compose.yml",
requiredFiles: ["Dockerfile"],
});
files.push({
name: "docker-compose.dev.yml",
description: "Docker Compose for development",
category: "Docker",
path: "configs/docker-compose.dev.yml",
});
}
// Environment Files
files.push({
name: ".env.example",
description: "Environment variables template",
category: "Configuration",
path: "configs/env.example",
});
// Git Configuration
if (context.hasPackageJson) {
files.push({
name: ".gitignore",
description: "Git ignore patterns for Node.js projects",
category: "Git",
path: "configs/nodejs-gitignore",
language: ["javascript", "typescript"],
});
}
else if (context.hasPython) {
files.push({
name: ".gitignore",
description: "Git ignore patterns for Python projects",
category: "Git",
path: "configs/python-gitignore",
language: ["python"],
});
}
// GitHub Actions
if (context.hasPackageJson) {
files.push({
name: ".github/workflows/ci.yml",
description: "GitHub Actions CI/CD workflow",
category: "CI/CD",
path: "configs/github-ci.yml",
});
}
return files;
}
/**
* Filter files based on project context and existing files
*/
async function filterAvailableFiles(files, context, projectPath) {
const filtered = [];
for (const file of files) {
// Skip if file already exists
const filePath = path.join(projectPath, file.name);
if (await fileExists(filePath)) {
continue;
}
// Skip if framework doesn't match
if (file.framework && !file.framework.includes(context.framework)) {
continue;
}
// Skip if language doesn't match
if (file.language && !file.language.includes(context.language)) {
continue;
}
// Skip if required files are missing
if (file.requiredFiles) {
const hasAllRequired = await Promise.all(file.requiredFiles.map((reqFile) => fileExists(path.join(projectPath, reqFile))));
if (!hasAllRequired.every(Boolean)) {
continue;
}
}
// Skip if conflicts with existing files
if (file.conflictsWith) {
const hasConflicts = await Promise.all(file.conflictsWith.map((conflictFile) => fileExists(path.join(projectPath, conflictFile))));
if (hasConflicts.some(Boolean)) {
continue;
}
}
filtered.push(file);
}
return filtered;
}
/**
* Check if a file exists
*/
async function fileExists(filePath) {
try {
await fs.access(filePath);
return true;
}
catch {
return false;
}
}
/**
* Add selected files to the project
*/
export async function addFiles(projectPath = process.cwd(), options = {}) {
try {
// Analyze the current project
log.info("Analyzing project structure...");
const context = await analyzeProject(projectPath);
if (options.json) {
log.debug(`Project context: ${JSON.stringify(context, null, 2)}`);
}
else {
log.info(`Detected: ${context.framework} project with ${context.language}`);
}
// Get available files
const allFiles = getAvailableFiles(context);
const availableFiles = await filterAvailableFiles(allFiles, context, projectPath);
if (availableFiles.length === 0) {
if (options.json) {
const output = createSuccessOutput({
message: "No additional files available for this project",
context,
});
outputJson(output);
}
else {
log.info("🎉 Your project already has all recommended files!");
log.info("No additional configuration files are needed.");
}
return;
}
// Group files by category
const filesByCategory = availableFiles.reduce((acc, file) => {
if (!acc[file.category])
acc[file.category] = [];
acc[file.category].push(file);
return acc;
}, {});
if (!options.json) {
log.info(`Found ${availableFiles.length} files that can be added to your project:`);
console.log();
}
// Interactive file selection
const choices = availableFiles.map((file) => ({
title: `${file.name}`,
description: file.description,
value: file,
selected: false,
}));
if (options.json) {
const output = createSuccessOutput({
availableFiles: availableFiles.map((f) => ({
name: f.name,
description: f.description,
category: f.category,
})),
context,
});
outputJson(output);
return;
}
const response = await prompts({
type: "multiselect",
name: "selectedFiles",
message: "Select files to add (use spacebar to select, enter to confirm):",
choices,
hint: "- Space to select. Return to submit",
instructions: false,
});
if (!response.selectedFiles || response.selectedFiles.length === 0) {
log.info("No files selected. Exiting.");
return;
}
const selectedFiles = response.selectedFiles;
log.info(`Adding ${selectedFiles.length} file(s) to your project...`);
console.log();
// Add selected files
const results = [];
for (const file of selectedFiles) {
try {
await addSingleFile(file, projectPath, context, options);
results.push({ file: file.name, success: true });
log.ok(`Added ${file.name}`);
}
catch (error) {
results.push({
file: file.name,
success: false,
error: error instanceof Error ? error.message : String(error),
});
log.err(`Failed to add ${file.name}: ${error instanceof Error ? error.message : String(error)}`);
}
}
// Update package.json if needed
const needsPackageUpdate = selectedFiles.some((f) => f.devDependencies || f.scripts);
if (needsPackageUpdate && context.hasPackageJson) {
await updatePackageJson(selectedFiles, projectPath, context);
}
console.log();
log.ok(`Successfully added ${results.filter((r) => r.success).length} file(s)`);
if (results.some((r) => !r.success)) {
log.warn(`Failed to add ${results.filter((r) => !r.success).length} file(s)`);
}
// Show next steps
showNextSteps(selectedFiles, context);
}
catch (error) {
if (options.json) {
const output = createErrorOutput([
error instanceof Error ? error.message : String(error),
]);
outputJson(output);
}
else {
log.err(`Failed to add files: ${error instanceof Error ? error.message : String(error)}`);
}
process.exit(1);
}
}
/**
* Add a single file to the project
*/
async function addSingleFile(file, projectPath, context, options) {
const targetPath = path.join(projectPath, file.name);
const targetDir = path.dirname(targetPath);
// Create directory if it doesn't exist
await fs.mkdir(targetDir, { recursive: true });
// Get the template file content
const templateContent = await getTemplateContent(file, context);
// Write the file
await fs.writeFile(targetPath, templateContent, "utf-8");
}
/**
* Get template content for a file
*/
async function getTemplateContent(file, context) {
// This would normally read from template files, but for now we'll generate content
// In a full implementation, you'd have actual template files
switch (file.name) {
case "tsconfig.json":
return generateTsConfig(context);
case ".eslintrc.json":
return generateEslintConfig(context);
case ".prettierrc":
return generatePrettierConfig();
case "postcss.config.js":
return generatePostCssConfig();
case ".env.example":
return generateEnvExample(context);
case ".gitignore":
return generateGitignore(context);
case "vitest.config.ts":
return generateVitestConfig(context);
case "src/test/setup.ts":
return generateTestSetup();
case "jest.config.js":
return generateJestConfig();
case "Dockerfile":
return generateDockerfile(context);
case ".dockerignore":
return generateDockerignore(context);
case "nginx.conf":
return generateNginxConfig();
case "docker-compose.yml":
return generateDockerCompose(context);
case "docker-compose.dev.yml":
return generateDockerComposeDev(context);
case ".github/workflows/ci.yml":
return generateGithubCI(context);
case "next-env.d.ts":
return generateNextEnv();
default:
return `# ${file.description}\n# Generated by Peezy CLI\n`;
}
}
/**
* Update package.json with new dependencies and scripts
*/
async function updatePackageJson(files, projectPath, context) {
const packageJsonPath = path.join(projectPath, "package.json");
const packageJson = JSON.parse(await fs.readFile(packageJsonPath, "utf-8"));
let updated = false;
// Add dev dependencies
const newDevDeps = files.flatMap((f) => f.devDependencies || []);
if (newDevDeps.length > 0) {
if (!packageJson.devDependencies)
packageJson.devDependencies = {};
for (const dep of newDevDeps) {
if (!packageJson.devDependencies[dep]) {
packageJson.devDependencies[dep] = getLatestVersion(dep);
updated = true;
}
}
}
// Add scripts
const newScripts = files.reduce((acc, f) => ({ ...acc, ...f.scripts }), {});
if (Object.keys(newScripts).length > 0) {
if (!packageJson.scripts)
packageJson.scripts = {};
for (const [script, command] of Object.entries(newScripts)) {
if (!packageJson.scripts[script]) {
packageJson.scripts[script] = command;
updated = true;
}
}
}
if (updated) {
await fs.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2) + "\n");
log.ok("Updated package.json with new dependencies and scripts");
}
}
/**
* Get latest version for a package (simplified)
*/
function getLatestVersion(packageName) {
// In a real implementation, you'd fetch from npm registry
// For now, return common versions
const versions = {
eslint: "^8.57.0",
prettier: "^3.0.0",
"@typescript-eslint/eslint-plugin": "^7.15.0",
"@typescript-eslint/parser": "^7.15.0",
"eslint-plugin-react": "^7.35.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-vue": "^9.27.0",
vitest: "^1.6.0",
"@testing-library/react": "^16.0.0",
"@testing-library/jest-dom": "^6.4.0",
jsdom: "^24.1.0",
jest: "^29.7.0",
"@types/jest": "^29.5.12",
"ts-jest": "^29.1.2",
};
return versions[packageName] || "^1.0.0";
}
/**
* Show next steps after adding files
*/
function showNextSteps(files, context) {
console.log();
log.info("🎯 Next Steps:");
const hasNewDeps = files.some((f) => f.devDependencies?.length);
if (hasNewDeps) {
console.log(` 1. Install new dependencies: ${context.packageManager} install`);
}
const hasLinting = files.some((f) => f.name === ".eslintrc.json");
if (hasLinting) {
console.log(` 2. Run linting: ${context.packageManager} run lint`);
}
const hasTesting = files.some((f) => f.name.includes("test") ||
f.name.includes("jest") ||
f.name.includes("vitest"));
if (hasTesting) {
console.log(` 3. Run tests: ${context.packageManager} run test`);
}
const hasDocker = files.some((f) => f.name === "Dockerfile");
if (hasDocker) {
console.log(" 4. Build Docker image: docker build -t my-app .");
}
console.log();
log.info("💡 Tip: Run 'peezy doctor' to check your project health");
}
// Template generators (simplified versions)
function generateTsConfig(context) {
if (context.hasNextJs) {
return JSON.stringify({
compilerOptions: {
target: "es5",
lib: ["dom", "dom.iterable", "es6"],
allowJs: true,
skipLibCheck: true,
strict: true,
noEmit: true,
esModuleInterop: true,
module: "esnext",
moduleResolution: "bundler",
resolveJsonModule: true,
isolatedModules: true,
jsx: "preserve",
incremental: true,
plugins: [{ name: "next" }],
baseUrl: ".",
paths: { "@/*": ["./src/*"] },
},
include: [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
],
exclude: ["node_modules"],
}, null, 2);
}
// Default React/Vite config
return JSON.stringify({
compilerOptions: {
target: "ES2020",
useDefineForClassFields: true,
lib: ["ES2020", "DOM", "DOM.Iterable"],
module: "ESNext",
skipLibCheck: true,
moduleResolution: "bundler",
allowImportingTsExtensions: true,
resolveJsonModule: true,
isolatedModules: true,
noEmit: true,
jsx: "react-jsx",
strict: true,
noUnusedLocals: true,
noUnusedParameters: true,
noFallthroughCasesInSwitch: true,
},
include: ["src"],
references: [{ path: "./tsconfig.node.json" }],
}, null, 2);
}
function generateEslintConfig(context) {
if (context.hasNextJs) {
return JSON.stringify({
extends: ["next/core-web-vitals", "@typescript-eslint/recommended"],
parser: "@typescript-eslint/parser",
plugins: ["@typescript-eslint"],
rules: {
"@typescript-eslint/no-unused-vars": "error",
"@typescript-eslint/no-explicit-any": "warn",
"prefer-const": "error",
"no-var": "error",
},
ignorePatterns: ["node_modules/", ".next/", "out/", "build/", "dist/"],
}, null, 2);
}
if (context.hasReact) {
return JSON.stringify({
parser: "@typescript-eslint/parser",
extends: [
"@typescript-eslint/recommended",
"plugin:react/recommended",
"plugin:react-hooks/recommended",
"plugin:react/jsx-runtime",
],
plugins: ["@typescript-eslint", "react", "react-hooks"],
parserOptions: {
ecmaVersion: 2022,
sourceType: "module",
ecmaFeatures: { jsx: true },
},
settings: { react: { version: "detect" } },
rules: {
"@typescript-eslint/no-unused-vars": "error",
"@typescript-eslint/no-explicit-any": "warn",
"react/react-in-jsx-scope": "off",
"react/prop-types": "off",
"prefer-const": "error",
"no-var": "error",
},
ignorePatterns: ["node_modules/", "dist/", "build/", "*.js"],
}, null, 2);
}
// Default config
return JSON.stringify({
parser: "@typescript-eslint/parser",
extends: ["@typescript-eslint/recommended"],
plugins: ["@typescript-eslint"],
rules: {
"@typescript-eslint/no-unused-vars": "error",
"@typescript-eslint/no-explicit-any": "warn",
"prefer-const": "error",
"no-var": "error",
},
ignorePatterns: ["node_modules/", "dist/", "*.js"],
}, null, 2);
}
function generatePrettierConfig() {
return JSON.stringify({
semi: true,
trailingComma: "es5",
singleQuote: false,
printWidth: 80,
tabWidth: 2,
useTabs: false,
}, null, 2);
}
function generatePostCssConfig() {
return `export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};`;
}
function generateEnvExample(context) {
let content = "# Environment Variables\n\n";
if (context.hasNextJs) {
content += "# Next.js\nNEXT_PUBLIC_APP_URL=http://localhost:3000\n\n";
}
if (context.hasReact && context.hasVite) {
content +=
"# Vite\nVITE_API_URL=http://localhost:3001/api\nVITE_APP_NAME=My App\n\n";
}
content +=
"# Database\n# DATABASE_URL=postgresql://user:password@localhost:5432/mydb\n\n";
content += "# API Keys\n# API_KEY=your-api-key-here\n";
return content;
}
function generateGitignore(context) {
if (context.language === "python") {
return `# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# Virtual environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# IDE
.vscode/
.idea/
*.swp
*.swo
# OS
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db`;
}
return `# Dependencies
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
# Build outputs
dist/
build/
.vite/
# Environment variables
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Coverage directory used by tools like istanbul
coverage/
*.lcov
# nyc test coverage
.nyc_output
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Editor directories and files
.vscode/
.idea/
*.swp
*.swo
*~
# OS generated files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# Logs
logs
*.log`;
}
function generateVitestConfig(context) {
return `/// <reference types="vitest" />
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'path'
export default defineConfig({
plugins: [react()],
test: {
globals: true,
environment: 'jsdom',
setupFiles: ['./src/test/setup.ts'],
css: true,
},
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
})`;
}
function generateTestSetup() {
return `import '@testing-library/jest-dom'`;
}
function generateJestConfig() {
return `module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
roots: ['<rootDir>/src'],
testMatch: ['**/__tests__/**/*.ts', '**/?(*.)+(spec|test).ts'],
transform: {
'^.+\\.ts$': 'ts-jest',
},
collectCoverageFrom: [
'src/**/*.ts',
'!src/**/*.d.ts',
],
};`;
}
function generateDockerfile(context) {
if (context.hasNextJs) {
return `FROM node:20-alpine AS base
FROM base AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production && npm cache clean --force
FROM base AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
ENV NEXT_TELEMETRY_DISABLED 1
RUN npm run build
FROM base AS runner
WORKDIR /app
ENV NODE_ENV production
ENV NEXT_TELEMETRY_DISABLED 1
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
RUN mkdir .next
RUN chown nextjs:nodejs .next
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"]`;
}
return `FROM node:20-alpine AS base
FROM base AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production && npm cache clean --force
FROM base AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM base AS runner
WORKDIR /app
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 appuser
COPY --from=builder --chown=appuser:nodejs /app/dist ./dist
COPY --from=builder --chown=appuser:nodejs /app/package*.json ./
COPY --from=deps --chown=appuser:nodejs /app/node_modules ./node_modules
USER appuser
EXPOSE 3000
CMD ["node", "dist/index.js"]`;
}
function generateDockerignore(context) {
return `node_modules
npm-debug.log
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
.git
.gitignore
README.md
Dockerfile*
docker-compose*.yml
.dockerignore
coverage
.nyc_output
.vscode
.idea
*.log
${context.hasNextJs ? ".next\nout" : "dist\nbuild"}
*.md
.eslintrc*
.prettierrc*
tsconfig*.json`;
}
function generateNginxConfig() {
return `events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types
text/plain
text/css
text/xml
text/javascript
application/json
application/javascript
application/xml+rss
application/atom+xml
image/svg+xml;
server {
listen 8080;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
location ~* \\.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
}
}`;
}
function generateDockerCompose(context) {
return `version: '3.8'
services:
app:
build: .
ports:
- "3000:3000"
environment:
- NODE_ENV=production
restart: unless-stopped
postgres:
image: postgres:16-alpine
environment:
POSTGRES_DB: myapp
POSTGRES_USER: user
POSTGRES_PASSWORD: password
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
postgres_data:`;
}
function generateDockerComposeDev(context) {
return `version: '3.8'
services:
postgres:
image: postgres:16-alpine
environment:
POSTGRES_DB: myapp_dev
POSTGRES_USER: user
POSTGRES_PASSWORD: password
ports:
- "5432:5432"
volumes:
- postgres_dev_data:/var/lib/postgresql/data
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redis_dev_data:/data
volumes:
postgres_dev_data:
redis_dev_data:`;
}
function generateGithubCI(context) {
return `name: CI
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18.x, 20.x]
steps:
- uses: actions/checkout@v4
- name: Use Node.js \${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: \${{ matrix.node-version }}
cache: 'npm'
- run: npm ci
- run: npm run build --if-present
- run: npm run lint --if-present
- run: npm test --if-present`;
}
function generateNextEnv() {
return `/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.`;
}
//# sourceMappingURL=addfile.js.map