gazan-init 0.1.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.
Files changed (42) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +722 -0
  3. package/bin/gazan.js +17 -0
  4. package/package.json +54 -0
  5. package/src/cli/commands/init.js +126 -0
  6. package/src/cli/index.js +22 -0
  7. package/src/config/aliases.js +130 -0
  8. package/src/config/normalize.js +65 -0
  9. package/src/generators/aliasResolver.js +68 -0
  10. package/src/generators/aliasRuntime.js +79 -0
  11. package/src/generators/database/mongoNative.js +45 -0
  12. package/src/generators/database/mongoose.js +32 -0
  13. package/src/generators/database/prisma.js +61 -0
  14. package/src/generators/engine.js +88 -0
  15. package/src/generators/entity/sensitiveFields.js +12 -0
  16. package/src/generators/entity/toApp.js +325 -0
  17. package/src/generators/entity/toMongoose.js +180 -0
  18. package/src/generators/entity/toPrisma.js +233 -0
  19. package/src/generators/entity/toZod.js +70 -0
  20. package/src/generators/env.js +123 -0
  21. package/src/generators/errors.js +67 -0
  22. package/src/generators/features/auth.js +163 -0
  23. package/src/generators/features/bullmq.js +96 -0
  24. package/src/generators/features/redis.js +34 -0
  25. package/src/generators/features/socket.js +45 -0
  26. package/src/generators/gitignore.js +19 -0
  27. package/src/generators/index.js +317 -0
  28. package/src/generators/middlewares.js +167 -0
  29. package/src/generators/packageJson.js +107 -0
  30. package/src/generators/paths.js +45 -0
  31. package/src/generators/project.js +189 -0
  32. package/src/generators/readme.js +177 -0
  33. package/src/generators/shutdown.js +113 -0
  34. package/src/generators/stripSensitiveFieldsHelper.js +50 -0
  35. package/src/generators/syntax.js +65 -0
  36. package/src/generators/tsconfig.js +32 -0
  37. package/src/parser/entity/errors.js +21 -0
  38. package/src/parser/entity/parse.js +417 -0
  39. package/src/parser/entity/schema.js +104 -0
  40. package/src/prompts/index.js +186 -0
  41. package/src/utils/fsSafety.js +17 -0
  42. package/src/utils/strings.js +110 -0
@@ -0,0 +1,186 @@
1
+ "use strict";
2
+
3
+ const p = require("@clack/prompts");
4
+ const fs = require("fs-extra");
5
+ const path = require("path");
6
+ const { EntityValidationError } = require("../parser/entity/errors");
7
+ const { readEntityFile } = require("../parser/entity/parse");
8
+
9
+ function onCancel() {
10
+ p.cancel("Operation cancelled.");
11
+ process.exit(1);
12
+ }
13
+
14
+ async function ask(fn, opts) {
15
+ const result = await fn(opts);
16
+ if (p.isCancel(result)) onCancel();
17
+ return result;
18
+ }
19
+
20
+ async function runPrompts(defaults = {}) {
21
+ p.intro("GAZAN — backend project initializer");
22
+
23
+ const projectName = await ask(p.text, {
24
+ message: "Project name",
25
+ placeholder: "my-app",
26
+ initialValue: defaults.projectName,
27
+ validate: (value) => {
28
+ if (!value) return "Project name is required";
29
+ if (!/^[a-z0-9][a-z0-9-_]*$/i.test(value)) return "Use letters, digits, - and _ only";
30
+ },
31
+ });
32
+
33
+ const moduleSystem = await ask(p.select, {
34
+ message: "Which module system do you want?",
35
+ options: [
36
+ { value: "cjs", label: "CommonJS (CJS)" },
37
+ { value: "mjs", label: "ES Modules (MJS)" },
38
+ ],
39
+ });
40
+
41
+ const language = await ask(p.select, {
42
+ message: "Which language do you want?",
43
+ options: [
44
+ { value: "js", label: "JavaScript" },
45
+ { value: "ts", label: "TypeScript" },
46
+ ],
47
+ });
48
+
49
+ const aliasesEnabled = await ask(p.confirm, {
50
+ message: "Do you want to enable module/path aliases? (e.g. @/services/x instead of ../../services/x)",
51
+ initialValue: true,
52
+ });
53
+
54
+ const databaseChoice = await ask(p.select, {
55
+ message: "Which database do you want?",
56
+ options: [
57
+ { value: "postgresql-prisma", label: "PostgreSQL + Prisma" },
58
+ { value: "mongodb-mongoose", label: "MongoDB + Mongoose" },
59
+ { value: "mongodb-native", label: "MongoDB + native mongosh/driver" },
60
+ { value: "none", label: "None" },
61
+ ],
62
+ });
63
+
64
+ const database = parseDatabaseChoice(databaseChoice);
65
+
66
+ const architecture = await ask(p.select, {
67
+ message: "Which architecture do you want?",
68
+ options: [
69
+ { value: "mvc", label: "MVC" },
70
+ { value: "hmvc", label: "HMVC" },
71
+ ],
72
+ });
73
+
74
+ const useSrc = await ask(p.confirm, {
75
+ message: "Use src/ directory?",
76
+ initialValue: true,
77
+ });
78
+
79
+ const socketIO = await ask(p.confirm, {
80
+ message: "Do you need Socket.IO?",
81
+ initialValue: false,
82
+ });
83
+
84
+ const bullMQ = await ask(p.confirm, {
85
+ message: "Do you need BullMQ workers?",
86
+ initialValue: false,
87
+ });
88
+
89
+ // BullMQ already requires Redis, so rate limiting rides on that connection for free. Only ask
90
+ // when it wouldn't otherwise exist, so a "Redis-only-for-rate-limiting" setup stays reachable.
91
+ let rateLimitStrategy = "memory";
92
+ if (!bullMQ) {
93
+ const useRedisForRateLimit = await ask(p.confirm, {
94
+ message: "Use Redis for rate limiting? (No = in-memory rate limiting)",
95
+ initialValue: false,
96
+ });
97
+ rateLimitStrategy = useRedisForRateLimit ? "redis" : "memory";
98
+ }
99
+
100
+ const authEnabled = await ask(p.confirm, {
101
+ message: "Do you need authentication?",
102
+ initialValue: false,
103
+ });
104
+
105
+ let authMethods = [];
106
+ if (authEnabled) {
107
+ authMethods = await ask(p.multiselect, {
108
+ message: "Which authentication methods?",
109
+ options: [
110
+ { value: "email-password", label: "Email/password" },
111
+ { value: "jwt", label: "JWT" },
112
+ { value: "oauth", label: "OAuth" },
113
+ { value: "refresh-token", label: "Refresh tokens" },
114
+ ],
115
+ required: false,
116
+ });
117
+ }
118
+
119
+ const hasEntityFile = await ask(p.confirm, {
120
+ message: "Do you have an entity.json file?",
121
+ initialValue: false,
122
+ });
123
+
124
+ let entityFile = null;
125
+ if (hasEntityFile) {
126
+ entityFile = await promptEntityPath(database.type);
127
+ }
128
+
129
+ p.outro("Configuration collected.");
130
+
131
+ return {
132
+ projectName,
133
+ moduleSystem,
134
+ language,
135
+ aliasesEnabled,
136
+ database,
137
+ architecture,
138
+ useSrc,
139
+ socketIO,
140
+ bullMQ,
141
+ rateLimitStrategy,
142
+ authentication: { enabled: authEnabled, methods: authMethods },
143
+ entityFile,
144
+ };
145
+ }
146
+
147
+ async function promptEntityPath(databaseType) {
148
+ for (;;) {
149
+ const candidate = await ask(p.text, {
150
+ message: "Enter entity.json path",
151
+ placeholder: "./entity.json",
152
+ validate: (value) => {
153
+ if (!value || !value.trim()) return "A path is required";
154
+ },
155
+ });
156
+
157
+ const absolute = path.resolve(candidate.trim());
158
+ if (!fs.existsSync(absolute)) {
159
+ p.log.error(`No file exists at: ${absolute}`);
160
+ continue;
161
+ }
162
+
163
+ try {
164
+ readEntityFile(absolute, { databaseType });
165
+ p.log.success(`entity.json is valid (${absolute})`);
166
+ return absolute;
167
+ } catch (error) {
168
+ if (error instanceof EntityValidationError) {
169
+ p.log.error(error.format());
170
+ } else {
171
+ p.log.error(error.message);
172
+ }
173
+ const retry = await ask(p.confirm, { message: "Try a different path?", initialValue: true });
174
+ if (!retry) return null;
175
+ }
176
+ }
177
+ }
178
+
179
+ function parseDatabaseChoice(choice) {
180
+ if (choice === "postgresql-prisma") return { type: "postgresql", orm: "prisma" };
181
+ if (choice === "mongodb-mongoose") return { type: "mongodb", orm: "mongoose" };
182
+ if (choice === "mongodb-native") return { type: "mongodb", orm: "native" };
183
+ return { type: "none", orm: null };
184
+ }
185
+
186
+ module.exports = { runPrompts };
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+
3
+ const fs = require("fs-extra");
4
+
5
+ // The only entries safe to ignore when deciding whether a directory is "empty enough" to
6
+ // generate into without asking. Deliberately NOT a blanket dotfile exclusion — a pre-existing
7
+ // .env (real secrets), .gitignore, or editor config must still trip the not-empty safety prompt,
8
+ // since GeneratorEngine.createFile overwrites unconditionally once generation proceeds.
9
+ const IGNORABLE_ENTRIES = new Set([".git", ".DS_Store"]);
10
+
11
+ function isDirEmpty(dir) {
12
+ if (!fs.existsSync(dir)) return true;
13
+ const entries = fs.readdirSync(dir).filter((e) => !IGNORABLE_ENTRIES.has(e));
14
+ return entries.length === 0;
15
+ }
16
+
17
+ module.exports = { isDirEmpty };
@@ -0,0 +1,110 @@
1
+ "use strict";
2
+
3
+ function splitWords(input) {
4
+ return String(input)
5
+ .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
6
+ .replace(/[_\-\s]+/g, " ")
7
+ .trim()
8
+ .split(" ")
9
+ .filter(Boolean);
10
+ }
11
+
12
+ function toPascalCase(input) {
13
+ return splitWords(input)
14
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase())
15
+ .join("");
16
+ }
17
+
18
+ function toCamelCase(input) {
19
+ const pascal = toPascalCase(input);
20
+ return pascal.charAt(0).toLowerCase() + pascal.slice(1);
21
+ }
22
+
23
+ function toKebabCase(input) {
24
+ return splitWords(input)
25
+ .map((w) => w.toLowerCase())
26
+ .join("-");
27
+ }
28
+
29
+ function toSnakeCase(input) {
30
+ return splitWords(input)
31
+ .map((w) => w.toLowerCase())
32
+ .join("_");
33
+ }
34
+
35
+ function toUpperSnakeCase(input) {
36
+ return toSnakeCase(input).toUpperCase();
37
+ }
38
+
39
+ const IRREGULAR_PLURALS = {
40
+ person: "people",
41
+ child: "children",
42
+ man: "men",
43
+ woman: "women",
44
+ tooth: "teeth",
45
+ foot: "feet",
46
+ mouse: "mice",
47
+ goose: "geese",
48
+ };
49
+
50
+ function pluralize(word) {
51
+ const lower = word.toLowerCase();
52
+ if (IRREGULAR_PLURALS[lower]) {
53
+ return matchCase(word, IRREGULAR_PLURALS[lower]);
54
+ }
55
+ if (/(s|x|z|ch|sh)$/i.test(word)) return `${word}es`;
56
+ if (/[^aeiou]y$/i.test(word)) return `${word.slice(0, -1)}ies`;
57
+ if (/fe?$/i.test(word) && !/(roof|belief|chef|chief)$/i.test(word)) {
58
+ return `${word.replace(/fe?$/i, "")}ves`;
59
+ }
60
+ return `${word}s`;
61
+ }
62
+
63
+ function matchCase(source, target) {
64
+ if (source[0] === source[0].toUpperCase()) {
65
+ return target.charAt(0).toUpperCase() + target.slice(1);
66
+ }
67
+ return target;
68
+ }
69
+
70
+ /** Levenshtein edit distance, used to power "did you mean X?" suggestions in error messages. */
71
+ function levenshtein(a, b) {
72
+ const m = a.length;
73
+ const n = b.length;
74
+ const dp = Array.from({ length: m + 1 }, (_, i) => [i, ...new Array(n).fill(0)]);
75
+ for (let j = 0; j <= n; j++) dp[0][j] = j;
76
+ for (let i = 1; i <= m; i++) {
77
+ for (let j = 1; j <= n; j++) {
78
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
79
+ dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost);
80
+ }
81
+ }
82
+ return dp[m][n];
83
+ }
84
+
85
+ /** Finds the closest match to `target` among `candidates`, or null if nothing is close enough to be useful. */
86
+ function didYouMean(target, candidates) {
87
+ if (!target || candidates.length === 0) return null;
88
+ let best = null;
89
+ let bestDistance = Infinity;
90
+ for (const candidate of candidates) {
91
+ const distance = levenshtein(target.toLowerCase(), candidate.toLowerCase());
92
+ if (distance < bestDistance) {
93
+ bestDistance = distance;
94
+ best = candidate;
95
+ }
96
+ }
97
+ const threshold = Math.max(2, Math.ceil(target.length / 2));
98
+ return bestDistance <= threshold ? best : null;
99
+ }
100
+
101
+ module.exports = {
102
+ toPascalCase,
103
+ toCamelCase,
104
+ toKebabCase,
105
+ toSnakeCase,
106
+ toUpperSnakeCase,
107
+ pluralize,
108
+ levenshtein,
109
+ didYouMean,
110
+ };