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 +0 -0
- package/package.json +3 -3
- package/src/actions/createProject.js +61 -30
- package/src/cli.js +18 -4
- package/src/commands/generate.js +21 -0
- package/src/config/fileDefinitions.js +104 -0
- package/src/config/templates.js +60 -6
- package/src/generators/resource.js +77 -0
- package/src/generators/updateRouteRegistry.js +46 -0
- package/src/generators/verifyRouteRegistry.js +36 -0
- package/src/templates/config.js +39 -0
- package/src/templates/javascript/controller.hbs +14 -0
- package/src/templates/javascript/mongoose/model.hbs +12 -0
- package/src/templates/javascript/route.hbs +8 -0
- package/src/templates/javascript/service.hbs +16 -0
- package/src/templates/javascript/validation.hbs +5 -0
- package/src/templates/typescript/controller.hbs +14 -0
- package/src/templates/typescript/mongoose/model.hbs +16 -0
- package/src/templates/typescript/route.hbs +9 -0
- package/src/templates/typescript/service.hbs +17 -0
- package/src/templates/typescript/validation.hbs +5 -0
- package/src/utils/buildContext.js +66 -0
- package/src/utils/buildGenerationPlan.js +23 -0
- package/src/utils/downloadRepoTemplateByVersionTags.js +4 -5
- package/src/utils/loadConfig.js +33 -0
- package/src/utils/renderTemplate.js +20 -0
- package/src/utils/updatePackageJson.js +24 -0
- package/src/utils/writeFile.js +16 -0
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
|
+
"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
|
-
"
|
|
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 {
|
|
7
|
-
|
|
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 =
|
|
16
|
-
|
|
17
|
-
|
|
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":
|
|
34
|
-
|
|
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", `
|
|
95
|
+
path.join(targetPath, "src", "routes", `index.route.${langKey}`),
|
|
54
96
|
);
|
|
55
97
|
}
|
|
56
98
|
|
|
57
|
-
//
|
|
99
|
+
// Cleanup template markers
|
|
58
100
|
cleanupTemplateMarkers(
|
|
59
|
-
path.join(targetPath, "src", `
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
];
|
package/src/config/templates.js
CHANGED
|
@@ -1,19 +1,73 @@
|
|
|
1
1
|
export default {
|
|
2
|
+
// javascript templates
|
|
2
3
|
js: [
|
|
3
|
-
|
|
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
|
-
|
|
16
|
-
{
|
|
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,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,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,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,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
|
-
|
|
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
|
|
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
|
+
}
|