render-create 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -421,17 +421,24 @@ async function scaffoldFrontend(projectDir, _componentId, component, projectName
421
421
  /**
422
422
  * Scaffold an API component
423
423
  */
424
- async function scaffoldApi(projectDir, _componentId, component, projectName, skipInstall) {
424
+ async function scaffoldApi(projectDir, _componentId, component, projectName, skipInstall, hasDatabase) {
425
425
  const subdir = join(projectDir, component.subdir);
426
426
  ensureDir(subdir);
427
427
  console.log(chalk.blue(`\nScaffolding API: ${component.name}...\n`));
428
+ // Choose files based on database selection
429
+ const scaffoldFiles = hasDatabase && component.scaffoldFilesWithDb
430
+ ? component.scaffoldFilesWithDb
431
+ : component.scaffoldFiles;
428
432
  if (component.runtime === "python") {
429
433
  // Python API
430
- const pythonDeps = component.pythonDependencies ?? [];
434
+ const pythonDeps = [
435
+ ...(component.pythonDependencies ?? []),
436
+ ...(hasDatabase ? (component.pythonDependenciesWithDb ?? []) : []),
437
+ ];
431
438
  writeFileSync(join(subdir, "requirements.txt"), `${pythonDeps.join("\n")}\n`);
432
439
  console.log(chalk.green(` Created requirements.txt`));
433
- if (component.scaffoldFiles) {
434
- copyPostCreateFiles(subdir, component.scaffoldFiles, projectName);
440
+ if (scaffoldFiles) {
441
+ copyPostCreateFiles(subdir, scaffoldFiles, projectName);
435
442
  }
436
443
  if (!skipInstall) {
437
444
  installPythonDependencies(subdir);
@@ -439,23 +446,33 @@ async function scaffoldApi(projectDir, _componentId, component, projectName, ski
439
446
  }
440
447
  else {
441
448
  // Node API
449
+ const scripts = {
450
+ ...(component.scripts ?? {}),
451
+ ...(hasDatabase ? (component.scriptsWithDb ?? {}) : {}),
452
+ };
442
453
  const packageJson = {
443
454
  name: `${projectName}-${component.subdir}`,
444
455
  version: "0.1.0",
445
456
  private: true,
446
457
  type: "module",
447
- scripts: component.scripts ?? {},
458
+ scripts,
448
459
  dependencies: {},
449
460
  devDependencies: {},
450
461
  };
451
462
  writeFileSync(join(subdir, "package.json"), `${JSON.stringify(packageJson, null, 2)}\n`);
452
463
  console.log(chalk.green(` Created package.json`));
453
- if (component.scaffoldFiles) {
454
- copyPostCreateFiles(subdir, component.scaffoldFiles, projectName);
464
+ if (scaffoldFiles) {
465
+ copyPostCreateFiles(subdir, scaffoldFiles, projectName);
455
466
  }
456
467
  if (!skipInstall) {
457
- const deps = component.dependencies ?? [];
458
- const devDeps = component.devDependencies ?? [];
468
+ const deps = [
469
+ ...(component.dependencies ?? []),
470
+ ...(hasDatabase ? (component.dependenciesWithDb ?? []) : []),
471
+ ];
472
+ const devDeps = [
473
+ ...(component.devDependencies ?? []),
474
+ ...(hasDatabase ? (component.devDependenciesWithDb ?? []) : []),
475
+ ];
459
476
  if (deps.length > 0) {
460
477
  console.log(chalk.gray(` npm install ${deps.join(" ")}`));
461
478
  execSync(`npm install ${deps.join(" ")}`, { cwd: subdir, stdio: "inherit" });
@@ -671,6 +688,7 @@ function hasWorkflowSelected(selection, components) {
671
688
  function collectRules(selection, components) {
672
689
  const rules = new Set(["general"]);
673
690
  const hasWorkflows = hasWorkflowSelected(selection, components);
691
+ const hasDatabase = !!selection.database;
674
692
  if (selection.frontend) {
675
693
  const comp = components.frontends[selection.frontend];
676
694
  for (const rule of comp?.rules ?? []) {
@@ -682,6 +700,12 @@ function collectRules(selection, components) {
682
700
  for (const rule of comp?.rules ?? []) {
683
701
  rules.add(rule);
684
702
  }
703
+ // Add database rules (ORM) when database is selected
704
+ if (hasDatabase && comp?.rulesWithDb) {
705
+ for (const rule of comp.rulesWithDb) {
706
+ rules.add(rule);
707
+ }
708
+ }
685
709
  // Add workflows rule to APIs when workflows are selected
686
710
  if (hasWorkflows) {
687
711
  rules.add("workflows");
@@ -753,11 +777,15 @@ ${structureLines.join("\n")}
753
777
 
754
778
  1. Set up environment variables (copy \`.env.example\` to \`.env\` in each service)
755
779
  2. Install dependencies in each service directory
756
- 3. Run \`render blueprint launch\` to deploy to Render
780
+ 3. Push to GitHub and deploy via the Render Dashboard
757
781
 
758
782
  ## Deploy to Render
759
783
 
760
- This project includes a \`render.yaml\` Blueprint for easy deployment.
784
+ This project includes a \`render.yaml\` Blueprint for easy deployment:
785
+
786
+ 1. Push this repo to GitHub
787
+ 2. Go to [dashboard.render.com/blueprints](https://dashboard.render.com/select-repo?type=blueprint)
788
+ 3. Connect your repo and deploy
761
789
  `;
762
790
  writeFileSync(join(projectDir, "README.md"), readmeContent);
763
791
  // Create root .gitignore
@@ -797,6 +825,7 @@ Thumbs.db
797
825
  }
798
826
  // Scaffold APIs
799
827
  const hasWorkflows = hasWorkflowSelected(selection, components);
828
+ const hasDatabase = !!selection.database;
800
829
  for (const apiId of selection.apis) {
801
830
  const comp = components.apis[apiId];
802
831
  if (comp) {
@@ -812,7 +841,7 @@ Thumbs.db
812
841
  : comp.pythonDependencies,
813
842
  }
814
843
  : comp;
815
- await scaffoldApi(projectDir, apiId, apiComp, selection.projectName, skipInstall);
844
+ await scaffoldApi(projectDir, apiId, apiComp, selection.projectName, skipInstall, hasDatabase);
816
845
  }
817
846
  }
818
847
  // Scaffold workers
@@ -911,7 +940,8 @@ RENDER_API_KEY=
911
940
  console.log(chalk.white("\nNext steps:\n"));
912
941
  console.log(chalk.cyan(` cd ${selection.projectName}`));
913
942
  console.log(chalk.cyan(` # Start each service in its directory`));
914
- console.log(chalk.cyan(` render blueprint launch # Deploy to Render`));
943
+ console.log(chalk.cyan(` git init && git add . && git commit -m "Initial commit"`));
944
+ console.log(chalk.cyan(` # Push to GitHub, then deploy at dashboard.render.com/blueprints`));
915
945
  console.log();
916
946
  }
917
947
  /**
@@ -953,24 +983,13 @@ export async function init(nameArg, options) {
953
983
  message: "Select a stack preset:",
954
984
  choices: presetChoices,
955
985
  });
956
- questions.push({
957
- type: "checkbox",
958
- name: "extras",
959
- message: "Include extras:",
960
- choices: [
961
- { name: ".env.example template", value: "env", checked: true },
962
- { name: "docker-compose.yml", value: "docker", checked: false },
963
- ],
964
- });
965
986
  }
966
987
  const answers = await inquirer.prompt(questions);
967
988
  projectName = projectName ?? answers.projectName;
968
989
  selectedPresetId = options.preset ?? answers.preset;
969
- selectedExtras = answers.extras ?? (options.yes ? ["env"] : []);
970
990
  }
971
991
  else {
972
992
  selectedPresetId = options.preset;
973
- selectedExtras = options.yes ? ["env"] : [];
974
993
  }
975
994
  // Validate preset
976
995
  if (selectedPresetId && selectedPresetId !== "custom") {
@@ -980,6 +999,24 @@ export async function init(nameArg, options) {
980
999
  console.log(chalk.yellow(`Available presets: ${Object.keys(presetsConfig.presets).join(", ")}`));
981
1000
  process.exit(1);
982
1001
  }
1002
+ // Ask for extras for non-custom presets
1003
+ if (!options.yes) {
1004
+ const extrasAnswer = await inquirer.prompt([
1005
+ {
1006
+ type: "checkbox",
1007
+ name: "extras",
1008
+ message: "Include extras (Space to select, Enter to confirm):",
1009
+ choices: [
1010
+ { name: ".env.example template", value: "env", checked: true },
1011
+ { name: "docker-compose.yml", value: "docker", checked: false },
1012
+ ],
1013
+ },
1014
+ ]);
1015
+ selectedExtras = extrasAnswer.extras;
1016
+ }
1017
+ else {
1018
+ selectedExtras = ["env"];
1019
+ }
983
1020
  }
984
1021
  // Handle custom/composable preset
985
1022
  if (selectedPresetId === "custom") {
@@ -1054,20 +1091,8 @@ export async function init(nameArg, options) {
1054
1091
  frontendDeployType = "static";
1055
1092
  }
1056
1093
  }
1057
- // Step 3: Rest of the prompts
1058
- const restAnswers = await inquirer.prompt([
1059
- {
1060
- type: "checkbox",
1061
- name: "apis",
1062
- message: "Select API backends (optional, press Enter to skip):",
1063
- choices: apiChoices,
1064
- },
1065
- {
1066
- type: "checkbox",
1067
- name: "workers",
1068
- message: "Select background workers (optional, press Enter to skip):",
1069
- choices: workerChoices,
1070
- },
1094
+ // Step 3: Database and cache
1095
+ const { database, cache } = await inquirer.prompt([
1071
1096
  {
1072
1097
  type: "list",
1073
1098
  name: "database",
@@ -1080,6 +1105,53 @@ export async function init(nameArg, options) {
1080
1105
  message: "Add cache?",
1081
1106
  choices: cacheChoices,
1082
1107
  },
1108
+ ]);
1109
+ // Step 4: API backends (two-step)
1110
+ const { wantApi } = await inquirer.prompt([
1111
+ {
1112
+ type: "confirm",
1113
+ name: "wantApi",
1114
+ message: "Add API backend?",
1115
+ default: true,
1116
+ },
1117
+ ]);
1118
+ let selectedApis = [];
1119
+ if (wantApi) {
1120
+ const { apis } = await inquirer.prompt([
1121
+ {
1122
+ type: "checkbox",
1123
+ name: "apis",
1124
+ message: "Select API backends:",
1125
+ choices: apiChoices,
1126
+ validate: (input) => input.length > 0 || "Please select at least one API",
1127
+ },
1128
+ ]);
1129
+ selectedApis = apis;
1130
+ }
1131
+ // Step 5: Background workers (two-step)
1132
+ const { wantWorker } = await inquirer.prompt([
1133
+ {
1134
+ type: "confirm",
1135
+ name: "wantWorker",
1136
+ message: "Add background worker?",
1137
+ default: true,
1138
+ },
1139
+ ]);
1140
+ let selectedWorkers = [];
1141
+ if (wantWorker) {
1142
+ const { workers } = await inquirer.prompt([
1143
+ {
1144
+ type: "checkbox",
1145
+ name: "workers",
1146
+ message: "Select background workers:",
1147
+ choices: workerChoices,
1148
+ validate: (input) => input.length > 0 || "Please select at least one worker",
1149
+ },
1150
+ ]);
1151
+ selectedWorkers = workers;
1152
+ }
1153
+ // Step 6: Extras
1154
+ const { extras } = await inquirer.prompt([
1083
1155
  {
1084
1156
  type: "checkbox",
1085
1157
  name: "extras",
@@ -1094,11 +1166,11 @@ export async function init(nameArg, options) {
1094
1166
  projectName,
1095
1167
  frontend: frontend === "none" ? null : frontend,
1096
1168
  frontendDeployType,
1097
- apis: restAnswers.apis,
1098
- workers: restAnswers.workers,
1099
- database: restAnswers.database === "none" ? null : restAnswers.database,
1100
- cache: restAnswers.cache === "none" ? null : restAnswers.cache,
1101
- extras: restAnswers.extras,
1169
+ apis: selectedApis,
1170
+ workers: selectedWorkers,
1171
+ database: database === "none" ? null : database,
1172
+ cache: cache === "none" ? null : cache,
1173
+ extras,
1102
1174
  };
1103
1175
  // Scaffold the composable project
1104
1176
  await scaffoldComposableProject(selection, components, options.skipInstall ?? false);
package/dist/types.d.ts CHANGED
@@ -155,6 +155,16 @@ export interface ApiComponent extends BaseComponent {
155
155
  /** For Python APIs */
156
156
  pythonDependencies?: string[];
157
157
  scaffoldFiles: Record<string, string>;
158
+ /** Additional files when database is selected */
159
+ scaffoldFilesWithDb?: Record<string, string>;
160
+ /** Additional dependencies when database is selected */
161
+ dependenciesWithDb?: string[];
162
+ devDependenciesWithDb?: string[];
163
+ pythonDependenciesWithDb?: string[];
164
+ /** Additional scripts when database is selected */
165
+ scriptsWithDb?: Record<string, string>;
166
+ /** Additional rules when database is selected */
167
+ rulesWithDb?: string[];
158
168
  blueprint: {
159
169
  type: "web";
160
170
  runtime: "node" | "python";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "render-create",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "CLI to scaffold and deploy applications on Render with best practices, Cursor rules, and Infrastructure as Code",
5
5
  "type": "module",
6
6
  "bin": {
@@ -30,7 +30,8 @@
30
30
  "postversion": "git push && git push --tags",
31
31
  "release:patch": "npm version patch",
32
32
  "release:minor": "npm version minor",
33
- "release:major": "npm version major"
33
+ "release:major": "npm version major",
34
+ "publish:npm": "npm publish --access public"
34
35
  },
35
36
  "keywords": [
36
37
  "render",
@@ -0,0 +1,29 @@
1
+ from datetime import datetime
2
+ from fastapi import FastAPI
3
+ from fastapi.middleware.cors import CORSMiddleware
4
+
5
+ app = FastAPI(title="API")
6
+
7
+ # CORS
8
+ app.add_middleware(
9
+ CORSMiddleware,
10
+ allow_origins=["*"],
11
+ allow_credentials=True,
12
+ allow_methods=["*"],
13
+ allow_headers=["*"],
14
+ )
15
+
16
+
17
+ @app.get("/health")
18
+ async def health():
19
+ """Health check endpoint."""
20
+ return {"status": "ok"}
21
+
22
+
23
+ @app.get("/api/hello")
24
+ async def hello():
25
+ """Hello endpoint."""
26
+ return {
27
+ "message": "Hello from FastAPI!",
28
+ "timestamp": datetime.now().isoformat(),
29
+ }
@@ -0,0 +1,39 @@
1
+ import Fastify from "fastify";
2
+ import cors from "@fastify/cors";
3
+
4
+ const fastify = Fastify({
5
+ logger: true,
6
+ });
7
+
8
+ // Register CORS
9
+ await fastify.register(cors, {
10
+ origin: process.env.CORS_ORIGIN || "*",
11
+ });
12
+
13
+ // Health check endpoint
14
+ fastify.get("/health", async () => {
15
+ return { status: "ok" };
16
+ });
17
+
18
+ // API routes
19
+ fastify.get("/api/hello", async () => {
20
+ return {
21
+ message: "Hello from Fastify!",
22
+ timestamp: new Date().toISOString(),
23
+ };
24
+ });
25
+
26
+ // Start server
27
+ const start = async () => {
28
+ try {
29
+ const host = process.env.HOST || "0.0.0.0";
30
+ const port = parseInt(process.env.PORT || "3000", 10);
31
+ await fastify.listen({ port, host });
32
+ console.log(`Server running at http://${host}:${port}`);
33
+ } catch (err) {
34
+ fastify.log.error(err);
35
+ process.exit(1);
36
+ }
37
+ };
38
+
39
+ start();
@@ -74,29 +74,37 @@
74
74
  "apis": {
75
75
  "fastify": {
76
76
  "name": "Fastify (Node.js)",
77
- "description": "Fastify + Drizzle + Zod",
77
+ "description": "Fast Node.js API framework",
78
78
  "subdir": "node-api",
79
79
  "runtime": "node",
80
- "rules": ["typescript", "fastify", "drizzle"],
80
+ "rules": ["typescript", "fastify"],
81
81
  "configs": ["biome", "tsconfig", "gitignore-node"],
82
- "dependencies": ["fastify", "@fastify/cors", "@fastify/env", "drizzle-orm", "zod", "postgres"],
83
- "devDependencies": ["typescript", "@types/node", "tsx", "drizzle-kit", "@biomejs/biome"],
82
+ "dependencies": ["fastify", "@fastify/cors", "@fastify/env", "zod"],
83
+ "devDependencies": ["typescript", "@types/node", "tsx", "@biomejs/biome"],
84
84
  "scripts": {
85
85
  "dev": "tsx watch src/index.ts",
86
86
  "build": "tsc",
87
87
  "start": "node dist/index.js",
88
88
  "lint": "biome check .",
89
- "format": "biome check --write .",
90
- "db:generate": "drizzle-kit generate",
91
- "db:migrate": "drizzle-kit migrate",
92
- "db:studio": "drizzle-kit studio"
89
+ "format": "biome check --write ."
93
90
  },
94
91
  "scaffoldFiles": {
92
+ "src/index.ts": "fastify/index-simple.ts"
93
+ },
94
+ "scaffoldFilesWithDb": {
95
95
  "src/index.ts": "fastify/index.ts",
96
96
  "src/db/index.ts": "drizzle/db-index.ts",
97
97
  "src/db/schema.ts": "drizzle/schema.ts",
98
98
  "drizzle.config.ts": "drizzle/drizzle.config.ts"
99
99
  },
100
+ "dependenciesWithDb": ["drizzle-orm", "postgres"],
101
+ "devDependenciesWithDb": ["drizzle-kit"],
102
+ "scriptsWithDb": {
103
+ "db:generate": "drizzle-kit generate",
104
+ "db:migrate": "drizzle-kit migrate",
105
+ "db:studio": "drizzle-kit studio"
106
+ },
107
+ "rulesWithDb": ["drizzle"],
100
108
  "blueprint": {
101
109
  "type": "web",
102
110
  "runtime": "node",
@@ -112,19 +120,24 @@
112
120
  },
113
121
  "fastapi": {
114
122
  "name": "FastAPI (Python)",
115
- "description": "FastAPI + SQLAlchemy + Pydantic",
123
+ "description": "Fast Python API framework",
116
124
  "subdir": "python-api",
117
125
  "runtime": "python",
118
- "rules": ["python", "sqlalchemy"],
126
+ "rules": ["python"],
119
127
  "configs": ["ruff", "gitignore-python"],
120
- "pythonDependencies": ["fastapi", "uvicorn[standard]", "sqlalchemy", "psycopg2-binary", "pydantic", "pydantic-settings", "python-dotenv", "alembic"],
128
+ "pythonDependencies": ["fastapi", "uvicorn[standard]", "pydantic", "python-dotenv"],
121
129
  "scaffoldFiles": {
130
+ "main.py": "fastapi/main-simple.py"
131
+ },
132
+ "scaffoldFilesWithDb": {
122
133
  "main.py": "fastapi/main.py",
123
134
  "app/__init__.py": "fastapi/app/__init__.py",
124
135
  "app/config.py": "fastapi/app/config.py",
125
136
  "app/database.py": "fastapi/app/database.py",
126
137
  "app/models.py": "fastapi/app/models.py"
127
138
  },
139
+ "pythonDependenciesWithDb": ["sqlalchemy", "psycopg2-binary", "pydantic-settings", "alembic"],
140
+ "rulesWithDb": ["sqlalchemy"],
128
141
  "blueprint": {
129
142
  "type": "web",
130
143
  "runtime": "python",