neatnode 3.3.2 → 3.4.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.
package/bin/index.js CHANGED
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neatnode",
3
- "version": "3.3.2",
3
+ "version": "3.4.0",
4
4
  "description": "Plug & Play Node.js backend starter templates — build REST APIs, socket servers, and more in seconds.",
5
5
  "bin": {
6
6
  "neatnode": "bin/index.js"
@@ -45,7 +45,7 @@
45
45
  },
46
46
  "dependencies": {
47
47
  "axios": "^1.13.2",
48
- "extract-zip": "^2.0.1",
48
+ "decompress": "^4.2.1",
49
49
  "fs-extra": "^11.3.2",
50
50
  "inquirer": "^12.10.0"
51
51
  },
@@ -54,4 +54,4 @@
54
54
  "eslint": "^9.0.0",
55
55
  "globals": "^15.0.0"
56
56
  }
57
- }
57
+ }
@@ -1,76 +1,107 @@
1
1
  import fs from "fs";
2
2
  import path from "path";
3
3
  import os from "os";
4
- import { fileURLToPath } from "url";
5
4
  import { copyTemplate } from "../utils/copyTemplate.js";
6
- import { cleanupTemplateMarkers, removeCrud, removeCrudModule, removeCrudReferences } from "./removeCRUD.js";
7
- import { downloadTemplate } from "../utils/downloadRepoTemplateByVersionTags.js";
5
+ import {
6
+ cleanupTemplateMarkers,
7
+ removeCrud,
8
+ removeCrudModule,
9
+ removeCrudReferences,
10
+ } from "./removeCRUD.js";
11
+ import { downloadTemplate, getPackageVersion } from "../utils/downloadRepoTemplateByVersionTags.js";
8
12
  import { addEnv } from "./addEnv.js";
13
+ import { generateNeatNodeConfig } from "../templates/config.js";
14
+ import { updatePackageJson } from "../utils/updatePackageJson.js";
15
+
16
+ export async function createProject({
17
+ projectName,
18
+ repoPath,
19
+ includeCrud,
20
+ crudName,
21
+ langKey,
22
+ isModular,
23
+ tempConfig,
24
+ }) {
25
+ // Project configuration based on user choices
26
+ const projectConfig = {
27
+ language: langKey === "ts" ? "typescript" : "javascript",
28
+ architecture: isModular ? "modular" : "mvc",
29
+ database: {
30
+ provider: "mongodb",
31
+ client: "mongoose",
32
+ },
33
+ tempConfig,
34
+ validation: langKey === "ts" ? "zod" : "joi",
35
+ srcDir: "src",
36
+ langKey,
37
+ };
38
+
39
+ const neatnodeVersion = getPackageVersion();
9
40
 
10
- const __filename = fileURLToPath(import.meta.url);
11
- const __dirname = path.dirname(__filename);
12
-
13
- export async function createProject({ projectName, repoPath, includeCrud, crudName, langKey, isModular }) {
14
41
  try {
15
- const targetPath = projectName === "."
16
- ? process.cwd()
17
- : path.join(process.cwd(), projectName);
42
+ const targetPath =
43
+ projectName === "."
44
+ ? process.cwd()
45
+ : path.join(process.cwd(), projectName);
18
46
 
47
+ // Check if the target directory already exists
19
48
  if (fs.existsSync(targetPath) && projectName !== ".") {
20
49
  console.error(`❌ Folder "${projectName}" already exists.`);
21
50
  process.exit(1);
22
51
  }
23
52
 
53
+ // Create the project folder if the project name is not "."
24
54
  if (projectName !== ".") {
25
55
  console.log("Creating project folder...");
26
56
  fs.mkdirSync(targetPath);
27
57
  }
28
58
 
59
+ // Download the template from the specified repository path
29
60
  console.log("Downloading template...");
30
61
  const localTemplatePath = await downloadTemplate(repoPath);
31
62
 
63
+ // Copy the template files to the target directory and replace placeholders
32
64
  await copyTemplate(localTemplatePath, targetPath, {
33
- "project-name": projectName === "." ? path.basename(process.cwd()) : projectName,
34
- "author": os.userInfo().username || "author",
65
+ "project-name":
66
+ projectName === "." ? path.basename(process.cwd()) : projectName,
67
+ author: os.userInfo().username || "author",
68
+ });
69
+
70
+ await updatePackageJson({
71
+ targetPath,
72
+ neatnodeVersion,
35
73
  });
36
74
 
75
+ // Generate the Neatnode configuration file based on the user's choices
76
+ await generateNeatNodeConfig({
77
+ targetPath,
78
+ ...projectConfig,
79
+ });
80
+
81
+ // Add environment variables to the project
37
82
  await addEnv({ targetPath });
38
83
 
84
+ // Handle CRUD removal if the user chose not to include it
39
85
  if (!includeCrud && crudName) {
40
86
  console.log("🗑 Removing CRUD files...");
41
87
 
42
88
  if (isModular) {
43
89
  removeCrudModule(targetPath, crudName);
44
-
45
- removeCrudReferences(
46
- path.join(targetPath, "src", `routes/index.route.${langKey}`)
47
- );
48
90
  }
49
91
 
50
92
  removeCrud(targetPath, crudName, langKey);
51
93
 
52
94
  removeCrudReferences(
53
- path.join(targetPath, "src", `app.${langKey}`)
95
+ path.join(targetPath, "src", "routes", `index.route.${langKey}`),
54
96
  );
55
97
  }
56
98
 
57
- // ALWAYS CLEANUP MARKERS
99
+ // Cleanup template markers
58
100
  cleanupTemplateMarkers(
59
- path.join(targetPath, "src", `app.${langKey}`)
101
+ path.join(targetPath, "src", "routes", `index.route.${langKey}`),
60
102
  );
61
-
62
- if (isModular) {
63
- cleanupTemplateMarkers(
64
- path.join(targetPath, "src", `routes/index.route.${langKey}`)
65
- );
66
- }
67
-
68
- console.log(`\n✅ Project "${projectName}" created successfully!\n`);
69
-
70
103
  } catch (err) {
71
104
  console.error("❌ Failed to create project:", err);
72
105
  process.exit(1);
73
106
  }
74
107
  }
75
-
76
-
package/src/cli.js CHANGED
@@ -2,6 +2,7 @@
2
2
  import inquirer from "inquirer";
3
3
  import templates from "./config/templates.js";
4
4
  import { createProject } from "./actions/createProject.js";
5
+ import { generate } from "./commands/generate.js";
5
6
 
6
7
  async function main() {
7
8
  console.log("\n🚀 Welcome to NeatNode CLI!\n");
@@ -59,7 +60,6 @@ async function main() {
59
60
  isModular = architecture === "modular";
60
61
 
61
62
  chosen.repoPath = chosen.architecture[architecture];
62
-
63
63
  }
64
64
 
65
65
  // STEP 4 — CRUD Optional (only for some templates)
@@ -126,10 +126,12 @@ async function main() {
126
126
  crudName,
127
127
  langKey,
128
128
  isModular,
129
+ tempConfig: chosen.config,
129
130
  });
130
131
 
131
-
132
- console.log(`\n✅ Project "${projectName}" created successfully using "${chosen.name}".\n`);
132
+ console.log(
133
+ `\n✅ Project "${projectName}" created successfully using "${chosen.name}".\n`,
134
+ );
133
135
 
134
136
  console.log("Next steps:");
135
137
  console.log(` cd ${projectName}`);
@@ -137,11 +139,23 @@ async function main() {
137
139
  console.log(" npm run dev\n");
138
140
 
139
141
  console.log("🎉 Happy Coding!\n");
142
+ }
140
143
 
144
+ async function run() {
145
+ const args = process.argv.slice(2);
146
+ const force = args.includes("--force");
141
147
 
148
+ if (args[0] === "g" || args[0] === "generate") {
149
+ return generate({
150
+ type: args[1],
151
+ name: args[2],
152
+ force,
153
+ });
154
+ }
142
155
 
156
+ return main();
143
157
  }
144
158
 
145
- main().catch((err) => {
159
+ run().catch((err) => {
146
160
  console.error("❌ Error:", err.message || err);
147
161
  });
@@ -0,0 +1,21 @@
1
+ import { generateResource } from "../generators/resource.js";
2
+ import { loadConfig } from "../utils/loadConfig.js";
3
+
4
+ export async function generate({ type, name, force }) {
5
+ if (!type) {
6
+ throw new Error("Missing generator type.");
7
+ }
8
+
9
+ if (!name) {
10
+ throw new Error("Missing resource name.");
11
+ }
12
+ const config = await loadConfig();
13
+
14
+ switch (type) {
15
+ case "resource":
16
+ return generateResource({ name, config, force });
17
+
18
+ default:
19
+ throw new Error(`Unknown generator: "${type}"`);
20
+ }
21
+ }
@@ -0,0 +1,104 @@
1
+ import path from "path";
2
+
3
+ export const FILE_DEFINITIONS = [
4
+ // controller
5
+ {
6
+ type: "controller",
7
+
8
+ template: "controller.hbs",
9
+
10
+ output(config, name, ext) {
11
+ if (config.architecture === "mvc") {
12
+ return path.join(
13
+ config.srcDir,
14
+ "controllers",
15
+ `${name}.controller.${ext}`,
16
+ );
17
+ }
18
+
19
+ return path.join(
20
+ config.srcDir,
21
+ "modules",
22
+ name,
23
+ `${name}.controller.${ext}`,
24
+ );
25
+ },
26
+ },
27
+
28
+ // service
29
+ {
30
+ type: "service",
31
+
32
+ template: "service.hbs",
33
+
34
+ output(config, name, ext) {
35
+ if (config.architecture === "mvc") {
36
+ return path.join(config.srcDir, "services", `${name}.service.${ext}`);
37
+ }
38
+
39
+ return path.join(
40
+ config.srcDir,
41
+ "modules",
42
+ name,
43
+ `${name}.service.${ext}`,
44
+ );
45
+ },
46
+ },
47
+
48
+ // route
49
+ {
50
+ type: "route",
51
+
52
+ template: "route.hbs",
53
+
54
+ output(config, name, ext) {
55
+ if (config.architecture === "mvc") {
56
+ return path.join(config.srcDir, "routes", `${name}.route.${ext}`);
57
+ }
58
+
59
+ return path.join(config.srcDir, "modules", name, `${name}.route.${ext}`);
60
+ },
61
+
62
+ },
63
+
64
+ // validation
65
+ {
66
+ type: "validation",
67
+
68
+ template: "validation.hbs",
69
+
70
+ output(config, name, ext) {
71
+ if (config.architecture === "mvc") {
72
+ return path.join(
73
+ config.srcDir,
74
+ "validations",
75
+ `${name}.validation.${ext}`,
76
+ );
77
+ }
78
+
79
+ return path.join(
80
+ config.srcDir,
81
+ "modules",
82
+ name,
83
+ `${name}.validation.${ext}`,
84
+ );
85
+ },
86
+ },
87
+
88
+ // model
89
+ {
90
+ type: "model",
91
+
92
+ template: "model.hbs",
93
+
94
+ database: "mongoose",
95
+
96
+ output(config, name, ext) {
97
+ if (config.architecture === "mvc") {
98
+ return path.join(config.srcDir, "models", `${name}.model.${ext}`);
99
+ }
100
+
101
+ return path.join(config.srcDir, "modules", name, `${name}.model.${ext}`);
102
+ },
103
+ },
104
+ ];
@@ -1,19 +1,73 @@
1
1
  export default {
2
+ // javascript templates
2
3
  js: [
3
- { name: "Basic Express", repoPath: "templates/js/express-basic" },
4
+ // basic express template
5
+ {
6
+ name: "Basic Express",
7
+ repoPath: "templates/js/express-basic",
8
+ config: {
9
+ template: "basic",
10
+ features: {
11
+ resourceGenerator: false,
12
+ },
13
+ },
14
+ },
15
+
16
+ // rest api template
4
17
  {
5
18
  name: "REST API",
6
19
  architecture: {
7
20
  mvc: "templates/js/express-rest-api",
8
- modular: "templates/js/express-modular-rest-api"
9
- }
21
+ modular: "templates/js/express-modular-rest-api",
22
+ },
23
+ config: {
24
+ template: "rest-api",
25
+ features: {
26
+ resourceGenerator: true,
27
+ },
28
+ },
29
+ },
30
+
31
+ // socket.io template
32
+ {
33
+ name: "Socket.IO",
34
+ repoPath: "templates/js/express-socket",
35
+ config: {
36
+ template: "socket",
37
+ features: {
38
+ resourceGenerator: false,
39
+ },
40
+ },
10
41
  },
11
- { name: "Socket.IO", repoPath: "templates/js/express-socket" },
12
42
  ],
13
43
 
44
+ // typescript templates
14
45
  ts: [
15
- { name: "Basic Express (TS)", repoPath: "templates/ts/basic-express" },
16
- { name: "REST API (TS)", repoPath: "templates/ts/express-rest-api", isModular: true },
46
+ // basic express template
47
+ {
48
+ name: "Basic Express (TS)",
49
+ repoPath: "templates/ts/basic-express",
50
+ config: {
51
+ template: "basic",
52
+ features: {
53
+ resourceGenerator: false,
54
+ },
55
+ },
56
+ },
57
+
58
+ // rest api template
59
+ {
60
+ name: "REST API (TS)",
61
+ repoPath: "templates/ts/express-rest-api",
62
+ isModular: true,
63
+ config: {
64
+ template: "rest-api",
65
+ features: {
66
+ resourceGenerator: true,
67
+ },
68
+ },
69
+ },
70
+
17
71
  // { name: "Socket.IO (TS)", repoPath: "templates/ts/express-socket" },
18
72
  ],
19
73
  };
@@ -0,0 +1,77 @@
1
+ import { buildContext } from "../utils/buildContext.js";
2
+ import { buildGenerationPlan } from "../utils/buildGenerationPlan.js";
3
+ import { renderTemplate } from "../utils/renderTemplate.js";
4
+ import { writeFile } from "../utils/writeFile.js";
5
+ import { updateRouteRegistry } from "./updateRouteRegistry.js";
6
+ import path from "path";
7
+ import fs from "fs";
8
+ import { validateRouteRegistry } from "./verifyRouteRegistry.js";
9
+
10
+ export async function generateResource({ name, config, force }) {
11
+ const files = ["controller", "service", "route", "validation", "model"];
12
+
13
+ if (!config.features.resourceGenerator) {
14
+ console.error(
15
+ `❌ The "${config.template}" template does not support resource generation.`,
16
+ );
17
+ process.exit(1);
18
+ }
19
+
20
+ const context = buildContext(name, config);
21
+
22
+ const plan = buildGenerationPlan({
23
+ config,
24
+ context,
25
+ files,
26
+ });
27
+
28
+ const createdFiles = [];
29
+
30
+ // Check if any of the files in the plan already exist
31
+ if (!force) {
32
+ for (const file of plan) {
33
+ if (fs.existsSync(file.output)) {
34
+ console.error(`❌ ${path.basename(file.output)} already exists.`);
35
+
36
+ console.log("\nUse --force to overwrite existing files.");
37
+
38
+ process.exit(1);
39
+ }
40
+ }
41
+ }
42
+
43
+ // Validate the route registry before proceeding with file generation
44
+ validateRouteRegistry({
45
+ targetPath: process.cwd(),
46
+ config,
47
+ });
48
+
49
+ // Generate files based on the plan
50
+ for (const file of plan) {
51
+ const content = renderTemplate(file.template, context);
52
+
53
+ await writeFile(file.output, content, { overwrite: force });
54
+
55
+ createdFiles.push(file.output);
56
+ }
57
+
58
+ // Update the route registry after generating files
59
+ updateRouteRegistry({
60
+ targetPath: process.cwd(),
61
+ context,
62
+ config,
63
+ });
64
+
65
+ console.log();
66
+
67
+ for (const file of createdFiles) {
68
+ console.log(`✔ Created ${path.basename(file)}`);
69
+ }
70
+
71
+ console.log();
72
+ console.log("✔ Updated routes/index.route.js");
73
+ console.log();
74
+ console.log(`✨ Resource "${name}" generated successfully.`);
75
+
76
+ return plan;
77
+ }
@@ -0,0 +1,46 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+
4
+ export function updateRouteRegistry({ targetPath, context, config }) {
5
+ const extension = config.language === "typescript" ? "ts" : "js";
6
+
7
+ const routeRegistry = path.join(
8
+ targetPath,
9
+ config.srcDir,
10
+ "routes",
11
+ `index.route.${extension}`,
12
+ );
13
+
14
+ let content = fs.readFileSync(routeRegistry, "utf8");
15
+
16
+ // Check for markers
17
+ const IMPORT_MARKER = "/* <NEATNODE_IMPORTS> */";
18
+ const ROUTE_MARKER = "/* <NEATNODE_ROUTES> */";
19
+
20
+ // Generate import and route statements based on architecture
21
+ const moduleImport = `import ${context.camelName}Route from "../modules/${context.rawName}/${context.rawName}.route.${extension}";`;
22
+ const mvcImport = `import ${context.camelName}Route from "./${context.rawName}.route.${extension}";`;
23
+
24
+ const importStatement =
25
+ config.architecture === "modular" ? moduleImport : mvcImport;
26
+
27
+ const routeStatement = `router.use("/${context.pluralName}", ${context.camelName}Route);`;
28
+
29
+ // Prevent duplicate imports
30
+ if (!content.includes(importStatement)) {
31
+ content = content.replace(
32
+ IMPORT_MARKER,
33
+ `${importStatement}\n${IMPORT_MARKER}`,
34
+ );
35
+ }
36
+
37
+ // Prevent duplicate routes
38
+ if (!content.includes(routeStatement)) {
39
+ content = content.replace(
40
+ ROUTE_MARKER,
41
+ `${routeStatement}\n${ROUTE_MARKER}`,
42
+ );
43
+ }
44
+
45
+ fs.writeFileSync(routeRegistry, content, "utf8");
46
+ }
@@ -0,0 +1,36 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+
4
+ export function validateRouteRegistry({ targetPath, config }) {
5
+ const extension = config.language === "typescript" ? "ts" : "js";
6
+
7
+ const routeRegistry = path.join(
8
+ targetPath,
9
+ config.srcDir,
10
+ "routes",
11
+ `index.route.${extension}`,
12
+ );
13
+
14
+ let content = fs.readFileSync(routeRegistry, "utf8");
15
+
16
+ // Check for markers
17
+ const IMPORT_MARKER = "/* <NEATNODE_IMPORTS> */";
18
+ const ROUTE_MARKER = "/* <NEATNODE_ROUTES> */";
19
+
20
+ // If the markers are not found, throw an error and exit the process
21
+ if (!content.includes(IMPORT_MARKER) || !content.includes(ROUTE_MARKER)) {
22
+ console.error(`
23
+ ❌ NeatNode route markers were not found.
24
+
25
+ Expected markers in:
26
+
27
+ ${routeRegistry}
28
+ ${IMPORT_MARKER}
29
+ ${ROUTE_MARKER}
30
+
31
+ Please restore them before generating resources.
32
+ `);
33
+
34
+ process.exit(1);
35
+ }
36
+ }
@@ -0,0 +1,39 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+
4
+ export async function generateNeatNodeConfig({
5
+ targetPath,
6
+ language,
7
+ architecture,
8
+ validation,
9
+ langKey,
10
+ database,
11
+ srcDir,
12
+ tempConfig,
13
+ }) {
14
+ const { provider, client } = database;
15
+
16
+ const content = `export default {
17
+ template: "${tempConfig.template}",
18
+
19
+ language: "${language}",
20
+ architecture: "${architecture}",
21
+
22
+ database: {
23
+ provider: "${provider}",
24
+ client: "${client}"
25
+ },
26
+ features: {
27
+ resourceGenerator: ${tempConfig.features.resourceGenerator},
28
+ },
29
+
30
+ validation: "${validation}",
31
+ srcDir: "${srcDir}",
32
+ };
33
+ `;
34
+
35
+ fs.writeFileSync(
36
+ path.join(targetPath, `neatnode.config.${langKey}`),
37
+ content,
38
+ );
39
+ }
@@ -0,0 +1,14 @@
1
+ import { StatusCodes } from "http-status-codes";
2
+ import { get{{pascalName}}Service } from "{{serviceImport}}{{camelName}}.service.js";
3
+ import sendResponse from "{{utilsImport}}ApiResponse.js";
4
+ import CatchAsync from "{{utilsImport}}CatchAsync.js";
5
+
6
+ export const get{{pascalName}} = CatchAsync(async (req, res) => {
7
+
8
+ const {{camelName}} = await get{{pascalName}}Service(req.params.id);
9
+
10
+ sendResponse(res, StatusCodes.OK, "{{pascalName}} fetched successfully",
11
+ {{camelName}},
12
+ );
13
+
14
+ });
@@ -0,0 +1,12 @@
1
+ import mongoose from "mongoose";
2
+
3
+ const {{camelName}}Schema = new mongoose.Schema(
4
+ {
5
+ // Define your schema fields here
6
+ },
7
+ { timestamps: true }
8
+ );
9
+
10
+ const {{pascalName}} = mongoose.model("{{pascalName}}", {{camelName}}Schema);
11
+
12
+ export default {{pascalName}};
@@ -0,0 +1,8 @@
1
+ import express from "express";
2
+ import { get{{pascalName}} } from "{{controllerImport}}{{camelName}}.controller.js";
3
+
4
+ const router = express.Router();
5
+
6
+ router.get("/", get{{pascalName}});
7
+
8
+ export default router;
@@ -0,0 +1,16 @@
1
+ import { StatusCodes } from "http-status-codes";
2
+ import {{pascalName}} from "{{modelImport}}{{camelName}}.model.js";
3
+ import ApiError from "{{utilsImport}}ApiError.js";
4
+
5
+ export const get{{pascalName}}Service = async (id) => {
6
+ const {{camelName}} = await {{pascalName}}.findById(id);
7
+
8
+ if (!{{camelName}}) {
9
+ throw new ApiError(
10
+ StatusCodes.NOT_FOUND,
11
+ "{{pascalName}} not found"
12
+ );
13
+ }
14
+
15
+ return {{camelName}};
16
+ };
@@ -0,0 +1,5 @@
1
+ import joi from "joi";
2
+
3
+ export const create{{pascalName}}Schema = joi.object({
4
+
5
+ });
@@ -0,0 +1,14 @@
1
+ import type { Request, Response } from "express";
2
+ import { StatusCodes } from "http-status-codes";
3
+ import { get{{pascalName}}Service } from "{{serviceImport}}{{camelName}}.service.js";
4
+ import sendResponse from "{{utilsImport}}ApiResponse.js";
5
+ import CatchAsync from "{{utilsImport}}CatchAsync.js";
6
+
7
+ export const get{{pascalName}} = CatchAsync(async (req: Request, res: Response) => {
8
+ const {{camelName}} = await get{{pascalName}}Service(req.params.id);
9
+
10
+ sendResponse(res, StatusCodes.OK, "{{pascalName}} fetched successfully",
11
+ {{camelName}},
12
+ );
13
+
14
+ });
@@ -0,0 +1,16 @@
1
+ import mongoose, { type Model, Schema, type InferSchemaType } from "mongoose";
2
+
3
+ const {{camelName}}Schema = new Schema(
4
+ {
5
+ // Define your schema fields here
6
+ },
7
+ {
8
+ timestamps: true,
9
+ versionKey: false,
10
+ },
11
+ );
12
+
13
+ type {{pascalName}}Document = InferSchemaType<typeof {{camelName}}Schema>;
14
+ type {{pascalName}}Model = Model<{{pascalName}}Document>;
15
+
16
+ export const {{pascalName}} = mongoose.model<{{pascalName}}Document, {{pascalName}}Model>("{{pascalName}}", {{camelName}}Schema);
@@ -0,0 +1,9 @@
1
+ import { Router } from "express";
2
+
3
+ import { get{{pascalName}} } from "{{controllerImport}}{{camelName}}.controller.js";
4
+
5
+ const router = Router();
6
+
7
+ router.get("/", get{{pascalName}});
8
+
9
+ export default router;
@@ -0,0 +1,17 @@
1
+ import ApiError from "{{utilsImport}}ApiError.js";
2
+ import { {{pascalName}} } from "{{modelImport}}{{camelName}}.model.js";
3
+
4
+ export const get{{pascalName}}Service = async (id: string) => {
5
+
6
+ const {{camelName}} = await {{pascalName}}.findById(id);
7
+
8
+ if (!{{camelName}}) {
9
+ throw new ApiError(
10
+ StatusCodes.NOT_FOUND,
11
+ "{{pascalName}} not found"
12
+ );
13
+ }
14
+
15
+ return {{camelName}};
16
+
17
+ };
@@ -0,0 +1,5 @@
1
+ import { z } from "zod";
2
+
3
+ export const create{{pascalName}}Schema = z.object({
4
+
5
+ });
@@ -0,0 +1,66 @@
1
+ function buildImportPaths(config) {
2
+ if (config.architecture === "mvc") {
3
+ return {
4
+ // Resource imports
5
+ serviceImport: "../services/",
6
+ controllerImport: "../controllers/",
7
+ validationImport: "../schemas/",
8
+ modelImport: "../models/",
9
+
10
+ // Shared imports
11
+ utilsImport: "../utils/",
12
+ middlewareImport: "../middleware/",
13
+ configImport: "../config/",
14
+ };
15
+ }
16
+
17
+ return {
18
+ // Resource imports
19
+ serviceImport: "./",
20
+ controllerImport: "./",
21
+ validationImport: "./",
22
+ modelImport: "./",
23
+
24
+ // Shared imports
25
+ utilsImport: "../../shared/utils/",
26
+ middlewareImport: "../../core/middleware/",
27
+ configImport: "../../core/config/",
28
+ };
29
+ }
30
+
31
+ function toPascalCase(value) {
32
+ return value
33
+ .split(/[-_\s]+/)
34
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
35
+ .join("");
36
+ }
37
+
38
+ function toCamelCase(value) {
39
+ const pascal = toPascalCase(value);
40
+
41
+ return pascal.charAt(0).toLowerCase() + pascal.slice(1);
42
+ }
43
+
44
+ function toKebabCase(value) {
45
+ return value.trim().toLowerCase().replace(/\s+/g, "-").replace(/_/g, "-");
46
+ }
47
+
48
+ export function buildContext(name, config) {
49
+ const camelName = toCamelCase(name);
50
+ const pascalName = toPascalCase(name);
51
+ const kebabName = toKebabCase(name);
52
+
53
+ return {
54
+ name,
55
+ rawName: name,
56
+
57
+ camelName,
58
+ pascalName,
59
+ kebabName,
60
+
61
+ pluralName: `${kebabName}s`,
62
+ camelPluralName: `${camelName}s`,
63
+ pascalPluralName: `${pascalName}s`,
64
+ ...buildImportPaths(config),
65
+ };
66
+ }
@@ -0,0 +1,23 @@
1
+ import { FILE_DEFINITIONS } from "../config/fileDefinitions.js";
2
+
3
+ export function buildGenerationPlan({ config, context, files }) {
4
+ const extension = config.language === "typescript" ? "ts" : "js";
5
+
6
+ const fileDefinitions = files
7
+ .map((type) => FILE_DEFINITIONS.find((file) => file.type === type))
8
+ .filter(Boolean)
9
+ .filter((file) => {
10
+ if (!file.database) return true;
11
+
12
+ return file.database === config.database.client;
13
+ })
14
+ .map((file) => ({
15
+ type: file.type,
16
+ template: file.database
17
+ ? `${config.language}/${config.database.client}/${file.template}`
18
+ : `${config.language}/${file.template}`,
19
+ output: file.output(config, context.camelName, extension),
20
+ }));
21
+
22
+ return fileDefinitions;
23
+ }
@@ -1,9 +1,9 @@
1
1
  import axios from "axios";
2
- import extract from "extract-zip";
3
2
  import fs from "fs";
4
3
  import path from "path";
5
4
  import os from "os";
6
5
  import { fileURLToPath } from "url";
6
+ import decompress from "decompress";
7
7
 
8
8
  const __filename = fileURLToPath(import.meta.url);
9
9
  const __dirname = path.dirname(__filename);
@@ -11,13 +11,13 @@ const __dirname = path.dirname(__filename);
11
11
  const owner = "aakash-gupta02";
12
12
  const repo = "NeatNode";
13
13
 
14
- const getPackageVersion = () => {
14
+ export const getPackageVersion = () => {
15
15
  try {
16
16
  const pkgPath = path.resolve(__dirname, "../../package.json");
17
17
  const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
18
18
  return pkg.version;
19
19
  } catch {
20
- return null;
20
+ throw new Error("Failed to read NeatNode package version.");
21
21
  }
22
22
  };
23
23
 
@@ -56,8 +56,7 @@ const downloadFromRef = async ({ repoPath, ref, refType }) => {
56
56
  });
57
57
 
58
58
  fs.writeFileSync(tempZip, response.data);
59
- await extract(tempZip, { dir: tempExtractDir });
60
-
59
+ await decompress(tempZip, tempExtractDir);
61
60
  const extractedRootDir = fs
62
61
  .readdirSync(tempExtractDir, { withFileTypes: true })
63
62
  .find((entry) => entry.isDirectory());
@@ -0,0 +1,33 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { pathToFileURL } from "url";
4
+
5
+ export async function loadConfig() {
6
+ const cwd = process.cwd();
7
+
8
+ const possibleConfigs = [
9
+ "neatnode.config.js",
10
+ "neatnode.config.ts",
11
+ ];
12
+
13
+ let configPath = null;
14
+
15
+ for (const file of possibleConfigs) {
16
+ const fullPath = path.join(cwd, file);
17
+
18
+ if (fs.existsSync(fullPath)) {
19
+ configPath = fullPath;
20
+ break;
21
+ }
22
+ }
23
+
24
+ if (!configPath) {
25
+ throw new Error(
26
+ "No neatnode.config.js or neatnode.config.ts found."
27
+ );
28
+ }
29
+
30
+ const config = await import(pathToFileURL(configPath).href);
31
+
32
+ return config.default;
33
+ }
@@ -0,0 +1,20 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { fileURLToPath } from "url";
4
+
5
+ const __filename = fileURLToPath(import.meta.url);
6
+ const __dirname = path.dirname(__filename);
7
+
8
+ const TEMPLATE_ROOT = path.join(__dirname, "../templates");
9
+
10
+ export function renderTemplate(templatePath, context) {
11
+ const fullPath = path.join(TEMPLATE_ROOT, templatePath);
12
+
13
+ let content = fs.readFileSync(fullPath, "utf8");
14
+
15
+ for (const [key, value] of Object.entries(context)) {
16
+ content = content.replaceAll(`{{${key}}}`, value);
17
+ }
18
+
19
+ return content;
20
+ }
@@ -0,0 +1,24 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+
4
+ export function updatePackageJson({
5
+ targetPath,
6
+ neatnodeVersion,
7
+ }) {
8
+ const packageJsonPath = path.join(targetPath, "package.json");
9
+
10
+ const packageJson = JSON.parse(
11
+ fs.readFileSync(packageJsonPath, "utf8"),
12
+ );
13
+
14
+ packageJson.devDependencies = {
15
+ ...packageJson.devDependencies,
16
+ neatnode: `^${neatnodeVersion}`,
17
+ };
18
+
19
+ fs.writeFileSync(
20
+ packageJsonPath,
21
+ JSON.stringify(packageJson, null, 2) + "\n",
22
+ "utf8",
23
+ );
24
+ }
@@ -0,0 +1,16 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+
4
+ export async function writeFile(filePath, content, options = {}) {
5
+ const { overwrite = false } = options;
6
+
7
+ if (fs.existsSync(filePath) && !overwrite) {
8
+ throw new Error(`File already exists: ${path.basename(filePath)}`);
9
+ }
10
+
11
+ await fs.promises.mkdir(path.dirname(filePath), {
12
+ recursive: true,
13
+ });
14
+
15
+ await fs.promises.writeFile(filePath, content);
16
+ }