render-create
Advanced tools
| from datetime import datetime | ||
| from fastapi import FastAPI | ||
| from fastapi.middleware.cors import CORSMiddleware | ||
| app = FastAPI(title="API") | ||
| # CORS | ||
| app.add_middleware( | ||
| CORSMiddleware, | ||
| allow_origins=["*"], | ||
| allow_credentials=True, | ||
| allow_methods=["*"], | ||
| allow_headers=["*"], | ||
| ) | ||
| @app.get("/health") | ||
| async def health(): | ||
| """Health check endpoint.""" | ||
| return {"status": "ok"} | ||
| @app.get("/api/hello") | ||
| async def hello(): | ||
| """Hello endpoint.""" | ||
| return { | ||
| "message": "Hello from FastAPI!", | ||
| "timestamp": datetime.now().isoformat(), | ||
| } |
| import Fastify from "fastify"; | ||
| import cors from "@fastify/cors"; | ||
| const fastify = Fastify({ | ||
| logger: true, | ||
| }); | ||
| // Register CORS | ||
| await fastify.register(cors, { | ||
| origin: process.env.CORS_ORIGIN || "*", | ||
| }); | ||
| // Health check endpoint | ||
| fastify.get("/health", async () => { | ||
| return { status: "ok" }; | ||
| }); | ||
| // API routes | ||
| fastify.get("/api/hello", async () => { | ||
| return { | ||
| message: "Hello from Fastify!", | ||
| timestamp: new Date().toISOString(), | ||
| }; | ||
| }); | ||
| // Start server | ||
| const start = async () => { | ||
| try { | ||
| const host = process.env.HOST || "0.0.0.0"; | ||
| const port = parseInt(process.env.PORT || "3000", 10); | ||
| await fastify.listen({ port, host }); | ||
| console.log(`Server running at http://${host}:${port}`); | ||
| } catch (err) { | ||
| fastify.log.error(err); | ||
| process.exit(1); | ||
| } | ||
| }; | ||
| start(); |
+115
-43
@@ -424,13 +424,20 @@ /** | ||
| */ | ||
| async function scaffoldApi(projectDir, _componentId, component, projectName, skipInstall) { | ||
| async function scaffoldApi(projectDir, _componentId, component, projectName, skipInstall, hasDatabase) { | ||
| const subdir = join(projectDir, component.subdir); | ||
| ensureDir(subdir); | ||
| console.log(chalk.blue(`\nScaffolding API: ${component.name}...\n`)); | ||
| // Choose files based on database selection | ||
| const scaffoldFiles = hasDatabase && component.scaffoldFilesWithDb | ||
| ? component.scaffoldFilesWithDb | ||
| : component.scaffoldFiles; | ||
| if (component.runtime === "python") { | ||
| // Python API | ||
| const pythonDeps = component.pythonDependencies ?? []; | ||
| const pythonDeps = [ | ||
| ...(component.pythonDependencies ?? []), | ||
| ...(hasDatabase ? (component.pythonDependenciesWithDb ?? []) : []), | ||
| ]; | ||
| writeFileSync(join(subdir, "requirements.txt"), `${pythonDeps.join("\n")}\n`); | ||
| console.log(chalk.green(` Created requirements.txt`)); | ||
| if (component.scaffoldFiles) { | ||
| copyPostCreateFiles(subdir, component.scaffoldFiles, projectName); | ||
| if (scaffoldFiles) { | ||
| copyPostCreateFiles(subdir, scaffoldFiles, projectName); | ||
| } | ||
@@ -443,2 +450,6 @@ if (!skipInstall) { | ||
| // Node API | ||
| const scripts = { | ||
| ...(component.scripts ?? {}), | ||
| ...(hasDatabase ? (component.scriptsWithDb ?? {}) : {}), | ||
| }; | ||
| const packageJson = { | ||
@@ -449,3 +460,3 @@ name: `${projectName}-${component.subdir}`, | ||
| type: "module", | ||
| scripts: component.scripts ?? {}, | ||
| scripts, | ||
| dependencies: {}, | ||
@@ -456,8 +467,14 @@ devDependencies: {}, | ||
| console.log(chalk.green(` Created package.json`)); | ||
| if (component.scaffoldFiles) { | ||
| copyPostCreateFiles(subdir, component.scaffoldFiles, projectName); | ||
| if (scaffoldFiles) { | ||
| copyPostCreateFiles(subdir, scaffoldFiles, projectName); | ||
| } | ||
| if (!skipInstall) { | ||
| const deps = component.dependencies ?? []; | ||
| const devDeps = component.devDependencies ?? []; | ||
| const deps = [ | ||
| ...(component.dependencies ?? []), | ||
| ...(hasDatabase ? (component.dependenciesWithDb ?? []) : []), | ||
| ]; | ||
| const devDeps = [ | ||
| ...(component.devDependencies ?? []), | ||
| ...(hasDatabase ? (component.devDependenciesWithDb ?? []) : []), | ||
| ]; | ||
| if (deps.length > 0) { | ||
@@ -678,2 +695,3 @@ console.log(chalk.gray(` npm install ${deps.join(" ")}`)); | ||
| const hasWorkflows = hasWorkflowSelected(selection, components); | ||
| const hasDatabase = !!selection.database; | ||
| if (selection.frontend) { | ||
@@ -690,2 +708,8 @@ const comp = components.frontends[selection.frontend]; | ||
| } | ||
| // Add database rules (ORM) when database is selected | ||
| if (hasDatabase && comp?.rulesWithDb) { | ||
| for (const rule of comp.rulesWithDb) { | ||
| rules.add(rule); | ||
| } | ||
| } | ||
| // Add workflows rule to APIs when workflows are selected | ||
@@ -762,7 +786,11 @@ if (hasWorkflows) { | ||
| 2. Install dependencies in each service directory | ||
| 3. Run \`render blueprint launch\` to deploy to Render | ||
| 3. Push to GitHub and deploy via the Render Dashboard | ||
| ## Deploy to Render | ||
| This project includes a \`render.yaml\` Blueprint for easy deployment. | ||
| This project includes a \`render.yaml\` Blueprint for easy deployment: | ||
| 1. Push this repo to GitHub | ||
| 2. Go to [dashboard.render.com/blueprints](https://dashboard.render.com/select-repo?type=blueprint) | ||
| 3. Connect your repo and deploy | ||
| `; | ||
@@ -807,2 +835,3 @@ writeFileSync(join(projectDir, "README.md"), readmeContent); | ||
| const hasWorkflows = hasWorkflowSelected(selection, components); | ||
| const hasDatabase = !!selection.database; | ||
| for (const apiId of selection.apis) { | ||
@@ -823,3 +852,3 @@ const comp = components.apis[apiId]; | ||
| : comp; | ||
| await scaffoldApi(projectDir, apiId, apiComp, selection.projectName, skipInstall); | ||
| await scaffoldApi(projectDir, apiId, apiComp, selection.projectName, skipInstall, hasDatabase); | ||
| } | ||
@@ -923,3 +952,4 @@ } | ||
| console.log(chalk.cyan(` # Start each service in its directory`)); | ||
| console.log(chalk.cyan(` render blueprint launch # Deploy to Render`)); | ||
| console.log(chalk.cyan(` git init && git add . && git commit -m "Initial commit"`)); | ||
| console.log(chalk.cyan(` # Push to GitHub, then deploy at dashboard.render.com/blueprints`)); | ||
| console.log(); | ||
@@ -966,11 +996,2 @@ } | ||
| }); | ||
| questions.push({ | ||
| type: "checkbox", | ||
| name: "extras", | ||
| message: "Include extras:", | ||
| choices: [ | ||
| { name: ".env.example template", value: "env", checked: true }, | ||
| { name: "docker-compose.yml", value: "docker", checked: false }, | ||
| ], | ||
| }); | ||
| } | ||
@@ -980,7 +1001,5 @@ const answers = await inquirer.prompt(questions); | ||
| selectedPresetId = options.preset ?? answers.preset; | ||
| selectedExtras = answers.extras ?? (options.yes ? ["env"] : []); | ||
| } | ||
| else { | ||
| selectedPresetId = options.preset; | ||
| selectedExtras = options.yes ? ["env"] : []; | ||
| } | ||
@@ -995,2 +1014,20 @@ // Validate preset | ||
| } | ||
| // Ask for extras for non-custom presets | ||
| if (!options.yes) { | ||
| const extrasAnswer = await inquirer.prompt([ | ||
| { | ||
| type: "checkbox", | ||
| name: "extras", | ||
| message: "Include extras (Space to select, Enter to confirm):", | ||
| choices: [ | ||
| { name: ".env.example template", value: "env", checked: true }, | ||
| { name: "docker-compose.yml", value: "docker", checked: false }, | ||
| ], | ||
| }, | ||
| ]); | ||
| selectedExtras = extrasAnswer.extras; | ||
| } | ||
| else { | ||
| selectedExtras = ["env"]; | ||
| } | ||
| } | ||
@@ -1070,17 +1107,5 @@ // Handle custom/composable preset | ||
| } | ||
| // Step 3: Rest of the prompts | ||
| const restAnswers = await inquirer.prompt([ | ||
| // Step 3: Database and cache | ||
| const { database, cache } = await inquirer.prompt([ | ||
| { | ||
| type: "checkbox", | ||
| name: "apis", | ||
| message: "Select API backends (optional, press Enter to skip):", | ||
| choices: apiChoices, | ||
| }, | ||
| { | ||
| type: "checkbox", | ||
| name: "workers", | ||
| message: "Select background workers (optional, press Enter to skip):", | ||
| choices: workerChoices, | ||
| }, | ||
| { | ||
| type: "list", | ||
@@ -1097,3 +1122,50 @@ name: "database", | ||
| }, | ||
| ]); | ||
| // Step 4: API backends (two-step) | ||
| const { wantApi } = await inquirer.prompt([ | ||
| { | ||
| type: "confirm", | ||
| name: "wantApi", | ||
| message: "Add API backend?", | ||
| default: true, | ||
| }, | ||
| ]); | ||
| let selectedApis = []; | ||
| if (wantApi) { | ||
| const { apis } = await inquirer.prompt([ | ||
| { | ||
| type: "checkbox", | ||
| name: "apis", | ||
| message: "Select API backends:", | ||
| choices: apiChoices, | ||
| validate: (input) => input.length > 0 || "Please select at least one API", | ||
| }, | ||
| ]); | ||
| selectedApis = apis; | ||
| } | ||
| // Step 5: Background workers (two-step) | ||
| const { wantWorker } = await inquirer.prompt([ | ||
| { | ||
| type: "confirm", | ||
| name: "wantWorker", | ||
| message: "Add background worker?", | ||
| default: true, | ||
| }, | ||
| ]); | ||
| let selectedWorkers = []; | ||
| if (wantWorker) { | ||
| const { workers } = await inquirer.prompt([ | ||
| { | ||
| type: "checkbox", | ||
| name: "workers", | ||
| message: "Select background workers:", | ||
| choices: workerChoices, | ||
| validate: (input) => input.length > 0 || "Please select at least one worker", | ||
| }, | ||
| ]); | ||
| selectedWorkers = workers; | ||
| } | ||
| // Step 6: Extras | ||
| const { extras } = await inquirer.prompt([ | ||
| { | ||
| type: "checkbox", | ||
@@ -1112,7 +1184,7 @@ name: "extras", | ||
| frontendDeployType, | ||
| apis: restAnswers.apis, | ||
| workers: restAnswers.workers, | ||
| database: restAnswers.database === "none" ? null : restAnswers.database, | ||
| cache: restAnswers.cache === "none" ? null : restAnswers.cache, | ||
| extras: restAnswers.extras, | ||
| apis: selectedApis, | ||
| workers: selectedWorkers, | ||
| database: database === "none" ? null : database, | ||
| cache: cache === "none" ? null : cache, | ||
| extras, | ||
| }; | ||
@@ -1119,0 +1191,0 @@ // Scaffold the composable project |
+10
-0
@@ -158,2 +158,12 @@ /** | ||
| scaffoldFiles: Record<string, string>; | ||
| /** Additional files when database is selected */ | ||
| scaffoldFilesWithDb?: Record<string, string>; | ||
| /** Additional dependencies when database is selected */ | ||
| dependenciesWithDb?: string[]; | ||
| devDependenciesWithDb?: string[]; | ||
| pythonDependenciesWithDb?: string[]; | ||
| /** Additional scripts when database is selected */ | ||
| scriptsWithDb?: Record<string, string>; | ||
| /** Additional rules when database is selected */ | ||
| rulesWithDb?: string[]; | ||
| blueprint: { | ||
@@ -160,0 +170,0 @@ type: "web"; |
+3
-2
| { | ||
| "name": "render-create", | ||
| "version": "0.1.0", | ||
| "version": "0.2.0", | ||
| "description": "CLI to scaffold and deploy applications on Render with best practices, Cursor rules, and Infrastructure as Code", | ||
@@ -33,3 +33,4 @@ "type": "module", | ||
| "release:minor": "npm version minor", | ||
| "release:major": "npm version major" | ||
| "release:major": "npm version major", | ||
| "publish:npm": "npm publish --access public" | ||
| }, | ||
@@ -36,0 +37,0 @@ "keywords": [ |
+24
-11
@@ -77,9 +77,9 @@ { | ||
| "name": "Fastify (Node.js)", | ||
| "description": "Fastify + Drizzle + Zod", | ||
| "description": "Fast Node.js API framework", | ||
| "subdir": "node-api", | ||
| "runtime": "node", | ||
| "rules": ["typescript", "fastify", "drizzle"], | ||
| "rules": ["typescript", "fastify"], | ||
| "configs": ["biome", "tsconfig", "gitignore-node"], | ||
| "dependencies": ["fastify", "@fastify/cors", "@fastify/env", "drizzle-orm", "zod", "postgres"], | ||
| "devDependencies": ["typescript", "@types/node", "tsx", "drizzle-kit", "@biomejs/biome"], | ||
| "dependencies": ["fastify", "@fastify/cors", "@fastify/env", "zod"], | ||
| "devDependencies": ["typescript", "@types/node", "tsx", "@biomejs/biome"], | ||
| "scripts": { | ||
@@ -90,8 +90,8 @@ "dev": "tsx watch src/index.ts", | ||
| "lint": "biome check .", | ||
| "format": "biome check --write .", | ||
| "db:generate": "drizzle-kit generate", | ||
| "db:migrate": "drizzle-kit migrate", | ||
| "db:studio": "drizzle-kit studio" | ||
| "format": "biome check --write ." | ||
| }, | ||
| "scaffoldFiles": { | ||
| "src/index.ts": "fastify/index-simple.ts" | ||
| }, | ||
| "scaffoldFilesWithDb": { | ||
| "src/index.ts": "fastify/index.ts", | ||
@@ -102,2 +102,10 @@ "src/db/index.ts": "drizzle/db-index.ts", | ||
| }, | ||
| "dependenciesWithDb": ["drizzle-orm", "postgres"], | ||
| "devDependenciesWithDb": ["drizzle-kit"], | ||
| "scriptsWithDb": { | ||
| "db:generate": "drizzle-kit generate", | ||
| "db:migrate": "drizzle-kit migrate", | ||
| "db:studio": "drizzle-kit studio" | ||
| }, | ||
| "rulesWithDb": ["drizzle"], | ||
| "blueprint": { | ||
@@ -118,9 +126,12 @@ "type": "web", | ||
| "name": "FastAPI (Python)", | ||
| "description": "FastAPI + SQLAlchemy + Pydantic", | ||
| "description": "Fast Python API framework", | ||
| "subdir": "python-api", | ||
| "runtime": "python", | ||
| "rules": ["python", "sqlalchemy"], | ||
| "rules": ["python"], | ||
| "configs": ["ruff", "gitignore-python"], | ||
| "pythonDependencies": ["fastapi", "uvicorn[standard]", "sqlalchemy", "psycopg2-binary", "pydantic", "pydantic-settings", "python-dotenv", "alembic"], | ||
| "pythonDependencies": ["fastapi", "uvicorn[standard]", "pydantic", "python-dotenv"], | ||
| "scaffoldFiles": { | ||
| "main.py": "fastapi/main-simple.py" | ||
| }, | ||
| "scaffoldFilesWithDb": { | ||
| "main.py": "fastapi/main.py", | ||
@@ -132,2 +143,4 @@ "app/__init__.py": "fastapi/app/__init__.py", | ||
| }, | ||
| "pythonDependenciesWithDb": ["sqlalchemy", "psycopg2-binary", "pydantic-settings", "alembic"], | ||
| "rulesWithDb": ["sqlalchemy"], | ||
| "blueprint": { | ||
@@ -134,0 +147,0 @@ "type": "web", |
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 4 instances in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 3 instances in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
200938
2.45%84
2.44%3822
4.08%16
23.08%