nodejs-quickstart-structure 1.13.0 → 1.15.1
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/CHANGELOG.md +32 -0
- package/README.md +4 -3
- package/bin/index.js +84 -80
- package/lib/generator.js +28 -4
- package/lib/modules/app-setup.js +111 -19
- package/lib/modules/caching-setup.js +13 -0
- package/lib/modules/config-files.js +50 -62
- package/lib/modules/database-setup.js +35 -30
- package/lib/modules/kafka-setup.js +78 -10
- package/package.json +8 -4
- package/templates/clean-architecture/js/src/errors/BadRequestError.js +1 -1
- package/templates/clean-architecture/js/src/errors/BadRequestError.spec.js.ejs +21 -0
- package/templates/clean-architecture/js/src/errors/NotFoundError.js +1 -1
- package/templates/clean-architecture/js/src/errors/NotFoundError.spec.js.ejs +21 -0
- package/templates/clean-architecture/js/src/infrastructure/log/logger.spec.js.ejs +63 -0
- package/templates/clean-architecture/js/src/infrastructure/repositories/UserRepository.js.ejs +2 -3
- package/templates/clean-architecture/js/src/infrastructure/repositories/UserRepository.spec.js.ejs +81 -0
- package/templates/clean-architecture/js/src/interfaces/controllers/userController.js.ejs +8 -4
- package/templates/clean-architecture/js/src/interfaces/controllers/userController.spec.js.ejs +102 -0
- package/templates/clean-architecture/js/src/interfaces/graphql/context.spec.js.ejs +31 -0
- package/templates/clean-architecture/js/src/interfaces/graphql/resolvers/user.resolvers.spec.js.ejs +49 -0
- package/templates/clean-architecture/js/src/interfaces/routes/api.spec.js.ejs +38 -0
- package/templates/clean-architecture/js/src/usecases/CreateUser.spec.js.ejs +51 -0
- package/templates/clean-architecture/js/src/usecases/GetAllUsers.spec.js.ejs +61 -0
- package/templates/clean-architecture/ts/src/errors/BadRequestError.spec.ts.ejs +21 -0
- package/templates/clean-architecture/ts/src/errors/BadRequestError.ts +1 -1
- package/templates/clean-architecture/ts/src/errors/NotFoundError.spec.ts.ejs +21 -0
- package/templates/clean-architecture/ts/src/errors/NotFoundError.ts +1 -1
- package/templates/clean-architecture/ts/src/infrastructure/log/logger.spec.ts.ejs +64 -0
- package/templates/clean-architecture/ts/src/infrastructure/repositories/UserRepository.spec.ts.ejs +85 -0
- package/templates/clean-architecture/ts/src/infrastructure/repositories/UserRepository.ts.ejs +2 -3
- package/templates/clean-architecture/ts/src/interfaces/controllers/userController.spec.ts.ejs +166 -0
- package/templates/clean-architecture/ts/src/interfaces/graphql/context.spec.ts.ejs +32 -0
- package/templates/clean-architecture/ts/src/interfaces/graphql/resolvers/user.resolvers.spec.ts.ejs +51 -0
- package/templates/clean-architecture/ts/src/interfaces/routes/userRoutes.spec.ts.ejs +40 -0
- package/templates/clean-architecture/ts/src/usecases/createUser.spec.ts.ejs +51 -0
- package/templates/clean-architecture/ts/src/usecases/getAllUsers.spec.ts.ejs +63 -0
- package/templates/clean-architecture/ts/src/utils/errorMiddleware.ts.ejs +1 -2
- package/templates/common/.cursorrules.ejs +60 -0
- package/templates/common/.dockerignore +2 -0
- package/templates/common/.gitlab-ci.yml.ejs +5 -5
- package/templates/common/Jenkinsfile.ejs +1 -1
- package/templates/common/README.md.ejs +11 -1
- package/templates/common/_github/workflows/ci.yml +7 -4
- package/templates/common/caching/js/memoryCache.spec.js.ejs +101 -0
- package/templates/common/caching/js/redisClient.spec.js.ejs +149 -0
- package/templates/common/caching/ts/memoryCache.spec.ts.ejs +102 -0
- package/templates/common/caching/ts/redisClient.spec.ts.ejs +157 -0
- package/templates/common/database/js/database.spec.js.ejs +56 -0
- package/templates/common/database/js/models/User.js.ejs +22 -0
- package/templates/common/database/js/models/User.spec.js.ejs +84 -0
- package/templates/common/database/js/mongoose.spec.js.ejs +43 -0
- package/templates/common/database/ts/database.spec.ts.ejs +56 -0
- package/templates/common/database/ts/models/User.spec.ts.ejs +84 -0
- package/templates/common/database/ts/models/User.ts.ejs +26 -0
- package/templates/common/database/ts/mongoose.spec.ts.ejs +42 -0
- package/templates/common/eslint.config.mjs.ejs +11 -2
- package/templates/common/health/js/healthRoute.spec.js.ejs +70 -0
- package/templates/common/health/ts/healthRoute.spec.ts.ejs +76 -0
- package/templates/common/jest.config.js.ejs +19 -5
- package/templates/common/kafka/js/config/kafka.spec.js.ejs +21 -0
- package/templates/common/kafka/js/services/kafkaService.js.ejs +9 -5
- package/templates/common/kafka/js/services/kafkaService.spec.js.ejs +60 -0
- package/templates/common/kafka/ts/config/kafka.spec.ts.ejs +21 -0
- package/templates/common/kafka/ts/services/kafkaService.spec.ts.ejs +61 -0
- package/templates/common/kafka/ts/services/kafkaService.ts.ejs +1 -1
- package/templates/common/package.json.ejs +0 -3
- package/templates/common/prompts/add-feature.md.ejs +26 -0
- package/templates/common/prompts/project-context.md.ejs +43 -0
- package/templates/common/prompts/troubleshoot.md.ejs +28 -0
- package/templates/common/shutdown/js/gracefulShutdown.spec.js.ejs +160 -0
- package/templates/common/shutdown/ts/gracefulShutdown.spec.ts.ejs +158 -0
- package/templates/common/src/utils/errorMiddleware.spec.js.ejs +79 -0
- package/templates/common/src/utils/errorMiddleware.spec.ts.ejs +94 -0
- package/templates/common/tsconfig.json +1 -1
- package/templates/mvc/js/src/controllers/userController.js.ejs +4 -31
- package/templates/mvc/js/src/controllers/userController.spec.js.ejs +170 -0
- package/templates/mvc/js/src/errors/BadRequestError.js +1 -1
- package/templates/mvc/js/src/errors/BadRequestError.spec.js.ejs +21 -0
- package/templates/mvc/js/src/errors/NotFoundError.js +1 -1
- package/templates/mvc/js/src/errors/NotFoundError.spec.js.ejs +21 -0
- package/templates/mvc/js/src/graphql/context.spec.js.ejs +29 -0
- package/templates/mvc/js/src/graphql/resolvers/user.resolvers.spec.js.ejs +47 -0
- package/templates/mvc/js/src/index.js.ejs +1 -1
- package/templates/mvc/js/src/routes/api.spec.js.ejs +36 -0
- package/templates/mvc/js/src/utils/logger.spec.js.ejs +63 -0
- package/templates/mvc/ts/src/controllers/userController.spec.ts.ejs +185 -0
- package/templates/mvc/ts/src/controllers/userController.ts.ejs +4 -31
- package/templates/mvc/ts/src/errors/BadRequestError.spec.ts.ejs +21 -0
- package/templates/mvc/ts/src/errors/BadRequestError.ts +1 -1
- package/templates/mvc/ts/src/errors/NotFoundError.spec.ts.ejs +21 -0
- package/templates/mvc/ts/src/errors/NotFoundError.ts +1 -1
- package/templates/mvc/ts/src/graphql/context.spec.ts.ejs +30 -0
- package/templates/mvc/ts/src/graphql/resolvers/user.resolvers.spec.ts.ejs +51 -0
- package/templates/mvc/ts/src/routes/api.spec.ts.ejs +40 -0
- package/templates/mvc/ts/src/utils/errorMiddleware.ts.ejs +1 -2
- package/templates/mvc/ts/src/utils/logger.spec.ts.ejs +64 -0
- package/docs/demo.gif +0 -0
- package/docs/generateCase.md +0 -265
- package/docs/generatorFlow.md +0 -233
- package/docs/releaseNoteRule.md +0 -42
- package/docs/ruleDevelop.md +0 -30
- package/templates/common/tests/health.test.ts.ejs +0 -24
|
@@ -3,127 +3,115 @@ import path from 'path';
|
|
|
3
3
|
import ejs from 'ejs';
|
|
4
4
|
|
|
5
5
|
export const renderPackageJson = async (templatesDir, targetDir, config) => {
|
|
6
|
-
const { projectName, database, communication, language, viewEngine, caching } = config;
|
|
7
6
|
const packageJsonPath = path.join(targetDir, 'package.json');
|
|
8
7
|
const packageTemplate = await fs.readFile(path.join(templatesDir, 'common', 'package.json.ejs'), 'utf-8');
|
|
9
|
-
const packageContent = ejs.render(packageTemplate, {
|
|
10
|
-
projectName,
|
|
11
|
-
database,
|
|
12
|
-
communication,
|
|
13
|
-
language,
|
|
14
|
-
communication,
|
|
15
|
-
language,
|
|
16
|
-
viewEngine,
|
|
17
|
-
caching
|
|
18
|
-
});
|
|
8
|
+
const packageContent = ejs.render(packageTemplate, { ...config });
|
|
19
9
|
await fs.writeFile(packageJsonPath, packageContent);
|
|
20
10
|
};
|
|
21
11
|
|
|
22
12
|
export const renderDockerCompose = async (templatesDir, targetDir, config) => {
|
|
23
|
-
const { projectName, database, dbName, communication, caching } = config;
|
|
24
13
|
const dockerComposePath = path.join(targetDir, 'docker-compose.yml');
|
|
25
14
|
const dockerTemplate = await fs.readFile(path.join(templatesDir, 'common', 'docker-compose.yml.ejs'), 'utf-8');
|
|
26
|
-
const dockerContent = ejs.render(dockerTemplate, {
|
|
27
|
-
projectName,
|
|
28
|
-
database,
|
|
29
|
-
dbName,
|
|
30
|
-
communication,
|
|
31
|
-
caching
|
|
32
|
-
});
|
|
15
|
+
const dockerContent = ejs.render(dockerTemplate, { ...config });
|
|
33
16
|
await fs.writeFile(dockerComposePath, dockerContent);
|
|
34
17
|
};
|
|
35
18
|
|
|
36
19
|
export const renderReadme = async (templatesDir, targetDir, config) => {
|
|
37
|
-
const { projectName, architecture, database, communication, language, ciProvider, caching } = config;
|
|
38
20
|
const readmePath = path.join(targetDir, 'README.md');
|
|
39
21
|
const readmeTemplate = await fs.readFile(path.join(templatesDir, 'common', 'README.md.ejs'), 'utf-8');
|
|
40
|
-
const readmeContent = ejs.render(readmeTemplate, {
|
|
41
|
-
projectName,
|
|
42
|
-
architecture,
|
|
43
|
-
database,
|
|
44
|
-
communication,
|
|
45
|
-
caching,
|
|
46
|
-
language,
|
|
47
|
-
ciProvider
|
|
48
|
-
});
|
|
22
|
+
const readmeContent = ejs.render(readmeTemplate, { ...config });
|
|
49
23
|
await fs.writeFile(readmePath, readmeContent);
|
|
50
24
|
};
|
|
51
25
|
|
|
52
26
|
export const renderDockerfile = async (templatesDir, targetDir, config) => {
|
|
53
|
-
const { language, viewEngine } = config;
|
|
54
27
|
const dockerfileTemplate = await fs.readFile(path.join(templatesDir, 'common', 'Dockerfile'), 'utf-8');
|
|
55
|
-
const dockerfileContent = ejs.render(dockerfileTemplate, {
|
|
56
|
-
language,
|
|
57
|
-
viewEngine
|
|
58
|
-
});
|
|
28
|
+
const dockerfileContent = ejs.render(dockerfileTemplate, { ...config });
|
|
59
29
|
await fs.writeFile(path.join(targetDir, 'Dockerfile'), dockerfileContent);
|
|
60
30
|
};
|
|
61
31
|
|
|
62
32
|
export const renderPm2Config = async (templatesDir, targetDir, config) => {
|
|
63
|
-
const { projectName, database, dbName, communication, language, caching } = config;
|
|
64
33
|
const pm2ConfigPath = path.join(targetDir, 'ecosystem.config.js');
|
|
65
34
|
const pm2Template = await fs.readFile(path.join(templatesDir, 'common', 'ecosystem.config.js.ejs'), 'utf-8');
|
|
66
|
-
const pm2Content = ejs.render(pm2Template, {
|
|
67
|
-
projectName,
|
|
68
|
-
database,
|
|
69
|
-
dbName,
|
|
70
|
-
communication,
|
|
71
|
-
language,
|
|
72
|
-
caching
|
|
73
|
-
});
|
|
35
|
+
const pm2Content = ejs.render(pm2Template, { ...config });
|
|
74
36
|
await fs.writeFile(pm2ConfigPath, pm2Content);
|
|
75
37
|
};
|
|
76
38
|
|
|
77
|
-
export const renderProfessionalConfig = async (templatesDir, targetDir,
|
|
39
|
+
export const renderProfessionalConfig = async (templatesDir, targetDir, config) => {
|
|
78
40
|
const eslintTemplate = await fs.readFile(path.join(templatesDir, 'common', 'eslint.config.mjs.ejs'), 'utf-8');
|
|
79
|
-
const eslintContent = ejs.render(eslintTemplate, {
|
|
41
|
+
const eslintContent = ejs.render(eslintTemplate, { ...config });
|
|
80
42
|
await fs.writeFile(path.join(targetDir, 'eslint.config.mjs'), eslintContent);
|
|
81
43
|
|
|
82
44
|
await fs.copy(path.join(templatesDir, 'common', '.prettierrc'), path.join(targetDir, '.prettierrc'));
|
|
83
45
|
await fs.copy(path.join(templatesDir, 'common', '.lintstagedrc'), path.join(targetDir, '.lintstagedrc'));
|
|
84
46
|
|
|
85
47
|
const jestTemplate = await fs.readFile(path.join(templatesDir, 'common', 'jest.config.js.ejs'), 'utf-8');
|
|
86
|
-
const jestContent = ejs.render(jestTemplate, {
|
|
48
|
+
const jestContent = ejs.render(jestTemplate, { ...config });
|
|
87
49
|
await fs.writeFile(path.join(targetDir, 'jest.config.js'), jestContent);
|
|
88
50
|
};
|
|
89
51
|
|
|
52
|
+
export const renderAiNativeFiles = async (templatesDir, targetDir, config) => {
|
|
53
|
+
// 1. .cursorrules
|
|
54
|
+
const cursorRulesPath = path.join(targetDir, '.cursorrules');
|
|
55
|
+
const cursorRulesTemplate = await fs.readFile(path.join(templatesDir, 'common', '.cursorrules.ejs'), 'utf-8');
|
|
56
|
+
const cursorRulesContent = ejs.render(cursorRulesTemplate, { ...config });
|
|
57
|
+
await fs.writeFile(cursorRulesPath, cursorRulesContent);
|
|
58
|
+
|
|
59
|
+
// 2. prompts/
|
|
60
|
+
const promptsDirTarget = path.join(targetDir, 'prompts');
|
|
61
|
+
await fs.ensureDir(promptsDirTarget);
|
|
62
|
+
|
|
63
|
+
const promptsSourceDir = path.join(templatesDir, 'common', 'prompts');
|
|
64
|
+
const promptFiles = await fs.readdir(promptsSourceDir);
|
|
65
|
+
|
|
66
|
+
for (const file of promptFiles) {
|
|
67
|
+
if (file.endsWith('.ejs')) {
|
|
68
|
+
const templatePath = path.join(promptsSourceDir, file);
|
|
69
|
+
const targetFilePath = path.join(promptsDirTarget, file.replace('.ejs', ''));
|
|
70
|
+
const templateContent = await fs.readFile(templatePath, 'utf-8');
|
|
71
|
+
const renderedContent = ejs.render(templateContent, { ...config });
|
|
72
|
+
await fs.writeFile(targetFilePath, renderedContent);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
90
77
|
export const setupCiCd = async (templatesDir, targetDir, config) => {
|
|
91
|
-
const { ciProvider
|
|
78
|
+
const { ciProvider } = config;
|
|
92
79
|
if (ciProvider === 'GitHub Actions') {
|
|
93
80
|
await fs.ensureDir(path.join(targetDir, '.github/workflows'));
|
|
94
81
|
await fs.copy(path.join(templatesDir, 'common', '_github/workflows/ci.yml'), path.join(targetDir, '.github/workflows/ci.yml'));
|
|
95
82
|
} else if (ciProvider === 'Jenkins') {
|
|
96
83
|
const jenkinsTemplate = await fs.readFile(path.join(templatesDir, 'common', 'Jenkinsfile.ejs'), 'utf-8');
|
|
97
|
-
const jenkinsContent = ejs.render(jenkinsTemplate, {
|
|
84
|
+
const jenkinsContent = ejs.render(jenkinsTemplate, { ...config });
|
|
98
85
|
await fs.writeFile(path.join(targetDir, 'Jenkinsfile'), jenkinsContent);
|
|
99
86
|
} else if (ciProvider === 'GitLab CI') {
|
|
100
87
|
const gitlabTemplate = await fs.readFile(path.join(templatesDir, 'common', '.gitlab-ci.yml.ejs'), 'utf-8');
|
|
101
|
-
const gitlabContent = ejs.render(gitlabTemplate, {
|
|
88
|
+
const gitlabContent = ejs.render(gitlabTemplate, { ...config });
|
|
102
89
|
await fs.writeFile(path.join(targetDir, '.gitlab-ci.yml'), gitlabContent);
|
|
103
90
|
}
|
|
104
91
|
};
|
|
105
92
|
|
|
106
|
-
export const renderTestSample = async (templatesDir, targetDir,
|
|
93
|
+
export const renderTestSample = async (templatesDir, targetDir, config) => {
|
|
107
94
|
await fs.ensureDir(path.join(targetDir, 'tests'));
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
95
|
+
if (config.language === 'TypeScript') {
|
|
96
|
+
const testsTsConfig = {
|
|
97
|
+
"extends": "../tsconfig.json",
|
|
98
|
+
"compilerOptions": {
|
|
99
|
+
"types": ["jest", "node"],
|
|
100
|
+
"rootDir": "../tests",
|
|
101
|
+
"noEmit": true
|
|
102
|
+
},
|
|
103
|
+
"include": ["**/*.ts"]
|
|
104
|
+
};
|
|
105
|
+
await fs.writeFile(path.join(targetDir, 'tests', 'tsconfig.json'), JSON.stringify(testsTsConfig, null, 4));
|
|
106
|
+
}
|
|
112
107
|
};
|
|
113
108
|
|
|
114
109
|
export const renderEnvExample = async (templatesDir, targetDir, config) => {
|
|
115
|
-
const { database, dbName, communication, projectName, caching } = config;
|
|
116
110
|
const envExamplePath = path.join(targetDir, '.env.example');
|
|
117
111
|
const envPath = path.join(targetDir, '.env');
|
|
118
112
|
const envTemplate = await fs.readFile(path.join(templatesDir, 'common', '.env.example.ejs'), 'utf-8');
|
|
119
113
|
|
|
120
|
-
const envContent = ejs.render(envTemplate, {
|
|
121
|
-
database,
|
|
122
|
-
dbName,
|
|
123
|
-
communication,
|
|
124
|
-
projectName,
|
|
125
|
-
caching
|
|
126
|
-
});
|
|
114
|
+
const envContent = ejs.render(envTemplate, { ...config });
|
|
127
115
|
|
|
128
116
|
await fs.writeFile(envExamplePath, envContent);
|
|
129
117
|
await fs.writeFile(envPath, envContent);
|
|
@@ -10,7 +10,7 @@ export const setupDatabase = async (templatesDir, targetDir, config) => {
|
|
|
10
10
|
if (database === 'MongoDB') {
|
|
11
11
|
// Copy migrate-mongo config
|
|
12
12
|
const migrateConfigTemplate = await fs.readFile(path.join(templatesDir, 'common', 'migrate-mongo-config.js.ejs'), 'utf-8');
|
|
13
|
-
const migrateConfigContent = ejs.render(migrateConfigTemplate, {
|
|
13
|
+
const migrateConfigContent = ejs.render(migrateConfigTemplate, { ...config });
|
|
14
14
|
await fs.writeFile(path.join(targetDir, 'migrate-mongo-config.js'), migrateConfigContent);
|
|
15
15
|
|
|
16
16
|
// Setup migrations directory
|
|
@@ -58,8 +58,19 @@ export const setupDatabase = async (templatesDir, targetDir, config) => {
|
|
|
58
58
|
|
|
59
59
|
if (await fs.pathExists(dbConfigTemplateSource)) {
|
|
60
60
|
const dbTemplate = await fs.readFile(dbConfigTemplateSource, 'utf-8');
|
|
61
|
-
const dbContent = ejs.render(dbTemplate, {
|
|
61
|
+
const dbContent = ejs.render(dbTemplate, { ...config });
|
|
62
62
|
await fs.writeFile(dbConfigTarget, dbContent);
|
|
63
|
+
|
|
64
|
+
// Render Spec
|
|
65
|
+
const specTemplateName = dbConfigFileName.replace(`.${langExt}`, `.spec.${langExt}.ejs`);
|
|
66
|
+
const specTemplateSource = path.join(templatesDir, 'common', 'database', langExt, specTemplateName);
|
|
67
|
+
if (await fs.pathExists(specTemplateSource)) {
|
|
68
|
+
const specTemplate = await fs.readFile(specTemplateSource, 'utf-8');
|
|
69
|
+
const specContent = ejs.render(specTemplate, { ...config });
|
|
70
|
+
const specTarget = dbConfigTarget.replace(`${path.sep}src${path.sep}`, `${path.sep}tests${path.sep}`).replace(`.${langExt}`, `.spec.${langExt}`);
|
|
71
|
+
await fs.ensureDir(path.dirname(specTarget));
|
|
72
|
+
await fs.writeFile(specTarget, specContent);
|
|
73
|
+
}
|
|
63
74
|
}
|
|
64
75
|
} else if (architecture === 'MVC') {
|
|
65
76
|
// Even if DB is None, MVC needs src/config for other things (like swagger or general config)
|
|
@@ -75,37 +86,31 @@ export const generateModels = async (templatesDir, targetDir, config) => {
|
|
|
75
86
|
const langExt = language === 'TypeScript' ? 'ts' : 'js';
|
|
76
87
|
const modelFileName = language === 'TypeScript' ? 'User.ts' : 'User.js';
|
|
77
88
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
let modelTarget;
|
|
82
|
-
|
|
83
|
-
if (architecture === 'MVC') {
|
|
84
|
-
await fs.ensureDir(path.join(targetDir, 'src/models'));
|
|
85
|
-
modelTarget = path.join(targetDir, 'src/models', modelFileName);
|
|
86
|
-
} else {
|
|
87
|
-
await fs.ensureDir(path.join(targetDir, 'src/infrastructure/database/models'));
|
|
88
|
-
modelTarget = path.join(targetDir, 'src/infrastructure/database/models', modelFileName);
|
|
89
|
-
}
|
|
89
|
+
const sourceModelName = database === 'MongoDB' ? `${modelFileName}.mongoose.ejs` : `${modelFileName}.ejs`;
|
|
90
|
+
const modelTemplateSource = path.join(templatesDir, 'common', 'database', langExt, 'models', sourceModelName);
|
|
91
|
+
let modelTarget;
|
|
90
92
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
await fs.writeFile(modelTarget, modelContent);
|
|
95
|
-
}
|
|
93
|
+
if (architecture === 'MVC') {
|
|
94
|
+
await fs.ensureDir(path.join(targetDir, 'src/models'));
|
|
95
|
+
modelTarget = path.join(targetDir, 'src/models', modelFileName);
|
|
96
96
|
} else {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
: `class UserModel {\n static mockData = [];\n}\n\nmodule.exports = UserModel;`;
|
|
97
|
+
await fs.ensureDir(path.join(targetDir, 'src/infrastructure/database/models'));
|
|
98
|
+
modelTarget = path.join(targetDir, 'src/infrastructure/database/models', modelFileName);
|
|
99
|
+
}
|
|
101
100
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
101
|
+
if (await fs.pathExists(modelTemplateSource)) {
|
|
102
|
+
const modelTemplate = await fs.readFile(modelTemplateSource, 'utf-8');
|
|
103
|
+
const modelContent = ejs.render(modelTemplate, { ...config });
|
|
104
|
+
await fs.writeFile(modelTarget, modelContent);
|
|
105
|
+
|
|
106
|
+
// Render Spec
|
|
107
|
+
const modelSpecTemplateSource = path.join(templatesDir, 'common', 'database', langExt, 'models', `User.spec.${langExt}.ejs`);
|
|
108
|
+
if (await fs.pathExists(modelSpecTemplateSource)) {
|
|
109
|
+
const modelSpecTemplate = await fs.readFile(modelSpecTemplateSource, 'utf-8');
|
|
110
|
+
const modelSpecContent = ejs.render(modelSpecTemplate, { ...config });
|
|
111
|
+
const modelSpecTarget = modelTarget.replace(`${path.sep}src${path.sep}`, `${path.sep}tests${path.sep}`).replace(`.${langExt}`, `.spec.${langExt}`);
|
|
112
|
+
await fs.ensureDir(path.dirname(modelSpecTarget));
|
|
113
|
+
await fs.writeFile(modelSpecTarget, modelSpecContent);
|
|
109
114
|
}
|
|
110
115
|
}
|
|
111
116
|
};
|
|
@@ -13,24 +13,68 @@ export const setupKafka = async (templatesDir, targetDir, config) => {
|
|
|
13
13
|
// Render Kafka Service with dynamic logger path
|
|
14
14
|
const kafkaServiceFileName = `kafkaService.${langExt}`;
|
|
15
15
|
const kafkaServiceTemplate = path.join(targetDir, 'src', 'services', `${kafkaServiceFileName}.ejs`);
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
let configPath = '../config/kafka';
|
|
16
|
+
// Render Kafka Service Spec
|
|
17
|
+
const kafkaSpecFileName = `kafkaService.spec.${langExt}`;
|
|
18
|
+
const kafkaSpecTemplate = path.join(targetDir, 'src', 'services', `${kafkaSpecFileName}.ejs`);
|
|
20
19
|
|
|
20
|
+
if (await fs.pathExists(kafkaServiceTemplate)) {
|
|
21
|
+
let serviceLoggerPath, serviceConfigPath;
|
|
21
22
|
if (language === 'TypeScript') {
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
serviceLoggerPath = architecture === 'Clean Architecture' ? '@/infrastructure/log/logger' : '@/utils/logger';
|
|
24
|
+
serviceConfigPath = architecture === 'Clean Architecture' ? '@/infrastructure/config/kafka' : '@/config/kafka';
|
|
25
|
+
} else {
|
|
26
|
+
serviceLoggerPath = architecture === 'Clean Architecture' ? '../../infrastructure/log/logger' : '../utils/logger';
|
|
27
|
+
serviceConfigPath = architecture === 'Clean Architecture' ? '../../infrastructure/config/kafka' : '../config/kafka';
|
|
24
28
|
}
|
|
25
29
|
|
|
26
|
-
const content = ejs.render(await fs.readFile(kafkaServiceTemplate, 'utf-8'), { loggerPath, configPath });
|
|
30
|
+
const content = ejs.render(await fs.readFile(kafkaServiceTemplate, 'utf-8'), { ...config, loggerPath: serviceLoggerPath, configPath: serviceConfigPath });
|
|
27
31
|
await fs.writeFile(path.join(targetDir, 'src', 'services', kafkaServiceFileName), content);
|
|
28
32
|
await fs.remove(kafkaServiceTemplate);
|
|
29
33
|
}
|
|
30
34
|
|
|
35
|
+
if (await fs.pathExists(kafkaSpecTemplate)) {
|
|
36
|
+
let specLoggerPath, specConfigPath, specServicePath;
|
|
37
|
+
if (language === 'TypeScript') {
|
|
38
|
+
specLoggerPath = architecture === 'Clean Architecture' ? '@/infrastructure/log/logger' : '@/utils/logger';
|
|
39
|
+
specConfigPath = architecture === 'Clean Architecture' ? '@/infrastructure/config/kafka' : '@/config/kafka';
|
|
40
|
+
specServicePath = architecture === 'Clean Architecture' ? '@/infrastructure/messaging/kafkaClient' : '@/services/kafkaService';
|
|
41
|
+
} else {
|
|
42
|
+
// For JS tests, we use @/ to ensure resolution after move to tests/
|
|
43
|
+
specLoggerPath = architecture === 'Clean Architecture' ? '@/infrastructure/log/logger' : '@/utils/logger';
|
|
44
|
+
specConfigPath = architecture === 'Clean Architecture' ? '@/infrastructure/config/kafka' : '@/config/kafka';
|
|
45
|
+
specServicePath = architecture === 'Clean Architecture' ? '@/infrastructure/messaging/kafkaClient' : '@/services/kafkaService';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const specContent = ejs.render(await fs.readFile(kafkaSpecTemplate, 'utf-8'), { ...config, loggerPath: specLoggerPath, configPath: specConfigPath, servicePath: specServicePath });
|
|
49
|
+
await fs.writeFile(path.join(targetDir, 'src', 'services', kafkaSpecFileName), specContent);
|
|
50
|
+
await fs.remove(kafkaSpecTemplate);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Render Kafka Config Spec
|
|
54
|
+
const kafkaConfigSpecFileName = `kafka.spec.${langExt}`;
|
|
55
|
+
const kafkaConfigSpecTemplate = path.join(templatesDir, 'common', 'kafka', langExt, 'config', `${kafkaConfigSpecFileName}.ejs`);
|
|
56
|
+
if (await fs.pathExists(kafkaConfigSpecTemplate)) {
|
|
57
|
+
const specContent = ejs.render(await fs.readFile(kafkaConfigSpecTemplate, 'utf-8'), { ...config });
|
|
58
|
+
let specTarget;
|
|
59
|
+
if (architecture === 'MVC') {
|
|
60
|
+
specTarget = path.join(targetDir, 'tests', 'config', kafkaConfigSpecFileName);
|
|
61
|
+
} else {
|
|
62
|
+
specTarget = path.join(targetDir, 'tests', 'infrastructure', 'config', kafkaConfigSpecFileName);
|
|
63
|
+
}
|
|
64
|
+
await fs.ensureDir(path.dirname(specTarget));
|
|
65
|
+
await fs.writeFile(specTarget, specContent);
|
|
66
|
+
|
|
67
|
+
// Remove the template from src in targetDir to avoid double processing by processAllTests
|
|
68
|
+
const targetSpecTemplate = path.join(targetDir, 'src', 'config', `${kafkaConfigSpecFileName}.ejs`);
|
|
69
|
+
if (await fs.pathExists(targetSpecTemplate)) {
|
|
70
|
+
await fs.remove(targetSpecTemplate);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
31
74
|
if (architecture === 'Clean Architecture') {
|
|
32
75
|
// Clean Architecture Restructuring
|
|
33
76
|
await fs.ensureDir(path.join(targetDir, 'src/infrastructure/messaging'));
|
|
77
|
+
await fs.ensureDir(path.join(targetDir, 'tests/infrastructure/messaging'));
|
|
34
78
|
await fs.ensureDir(path.join(targetDir, 'src/infrastructure/config'));
|
|
35
79
|
|
|
36
80
|
const serviceExt = language === 'TypeScript' ? 'ts' : 'js';
|
|
@@ -42,6 +86,15 @@ export const setupKafka = async (templatesDir, targetDir, config) => {
|
|
|
42
86
|
{ overwrite: true }
|
|
43
87
|
);
|
|
44
88
|
|
|
89
|
+
// Move Spec to Tests/Infrastructure/Messaging
|
|
90
|
+
if (await fs.pathExists(path.join(targetDir, `src/services/kafkaService.spec.${serviceExt}`))) {
|
|
91
|
+
await fs.move(
|
|
92
|
+
path.join(targetDir, `src/services/kafkaService.spec.${serviceExt}`),
|
|
93
|
+
path.join(targetDir, `tests/infrastructure/messaging/kafkaClient.spec.${serviceExt}`),
|
|
94
|
+
{ overwrite: true }
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
45
98
|
// Move Config to Infrastructure/Config
|
|
46
99
|
// Note: Check if config path exists before moving, though copy above should have put it there
|
|
47
100
|
if (await fs.pathExists(path.join(targetDir, `src/config/kafka.${serviceExt}`))) {
|
|
@@ -57,12 +110,27 @@ export const setupKafka = async (templatesDir, targetDir, config) => {
|
|
|
57
110
|
|
|
58
111
|
// Remove REST-specific folders (Interfaces) - Note: routes is kept for health endpoint
|
|
59
112
|
await fs.remove(path.join(targetDir, 'src/interfaces/controllers'));
|
|
113
|
+
await fs.remove(path.join(targetDir, 'tests/interfaces/controllers'));
|
|
60
114
|
|
|
61
115
|
// Original logic removed src/config entirely, but now we use it for Zod env validation in TS.
|
|
62
116
|
// We will no longer delete it.
|
|
63
|
-
} else if (architecture === 'MVC'
|
|
64
|
-
|
|
65
|
-
|
|
117
|
+
} else if (architecture === 'MVC') {
|
|
118
|
+
const serviceExt = language === 'TypeScript' ? 'ts' : 'js';
|
|
119
|
+
// Move Spec to Tests/Services
|
|
120
|
+
if (await fs.pathExists(path.join(targetDir, `src/services/kafkaService.spec.${serviceExt}`))) {
|
|
121
|
+
await fs.ensureDir(path.join(targetDir, 'tests/services'));
|
|
122
|
+
await fs.move(
|
|
123
|
+
path.join(targetDir, `src/services/kafkaService.spec.${serviceExt}`),
|
|
124
|
+
path.join(targetDir, `tests/services/kafkaService.spec.${serviceExt}`),
|
|
125
|
+
{ overwrite: true }
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (!config.viewEngine || config.viewEngine === 'None') {
|
|
130
|
+
// MVC Cleanup for Kafka Worker (No views) - Note: routes is kept for health endpoint
|
|
131
|
+
await fs.remove(path.join(targetDir, 'src/controllers'));
|
|
132
|
+
await fs.remove(path.join(targetDir, 'tests/controllers'));
|
|
133
|
+
}
|
|
66
134
|
}
|
|
67
135
|
};
|
|
68
136
|
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nodejs-quickstart-structure",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.15.1",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "
|
|
5
|
+
"description": "The ultimate nodejs quickstart structure CLI to scaffold Node.js microservices with MVC or Clean Architecture",
|
|
6
6
|
"main": "bin/index.js",
|
|
7
7
|
"bin": {
|
|
8
8
|
"nodejs-quickstart": "./bin/index.js"
|
|
@@ -16,12 +16,17 @@
|
|
|
16
16
|
},
|
|
17
17
|
"keywords": [
|
|
18
18
|
"nodejs",
|
|
19
|
+
"node",
|
|
20
|
+
"quickstart",
|
|
21
|
+
"structure",
|
|
19
22
|
"cli",
|
|
20
23
|
"scaffold",
|
|
21
24
|
"mvc",
|
|
22
25
|
"clean-architecture",
|
|
23
26
|
"microservices",
|
|
24
|
-
"backend"
|
|
27
|
+
"backend",
|
|
28
|
+
"generator",
|
|
29
|
+
"boilerplate"
|
|
25
30
|
],
|
|
26
31
|
"author": "Pau Dang <[EMAIL_ADDRESS]>",
|
|
27
32
|
"repository": {
|
|
@@ -44,7 +49,6 @@
|
|
|
44
49
|
"bin",
|
|
45
50
|
"lib",
|
|
46
51
|
"templates",
|
|
47
|
-
"docs",
|
|
48
52
|
"README.md",
|
|
49
53
|
"CHANGELOG.md"
|
|
50
54
|
]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const { BadRequestError } = require('@/errors/BadRequestError');
|
|
2
|
+
const { ApiError } = require('@/errors/ApiError');
|
|
3
|
+
const HTTP_STATUS = require('@/utils/httpCodes');
|
|
4
|
+
|
|
5
|
+
describe('BadRequestError', () => {
|
|
6
|
+
it('should extend ApiError', () => {
|
|
7
|
+
const error = new BadRequestError();
|
|
8
|
+
expect(error).toBeInstanceOf(ApiError);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('should have default message "Bad request"', () => {
|
|
12
|
+
const error = new BadRequestError();
|
|
13
|
+
expect(error.message).toBe('Bad request');
|
|
14
|
+
expect(error.statusCode).toBe(HTTP_STATUS.BAD_REQUEST);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('should accept a custom message', () => {
|
|
18
|
+
const error = new BadRequestError('Custom bad request');
|
|
19
|
+
expect(error.message).toBe('Custom bad request');
|
|
20
|
+
});
|
|
21
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const { NotFoundError } = require('@/errors/NotFoundError');
|
|
2
|
+
const { ApiError } = require('@/errors/ApiError');
|
|
3
|
+
const HTTP_STATUS = require('@/utils/httpCodes');
|
|
4
|
+
|
|
5
|
+
describe('NotFoundError', () => {
|
|
6
|
+
it('should extend ApiError', () => {
|
|
7
|
+
const error = new NotFoundError();
|
|
8
|
+
expect(error).toBeInstanceOf(ApiError);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('should have default message "Resource not found"', () => {
|
|
12
|
+
const error = new NotFoundError();
|
|
13
|
+
expect(error.message).toBe('Resource not found');
|
|
14
|
+
expect(error.statusCode).toBe(HTTP_STATUS.NOT_FOUND);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('should accept a custom message', () => {
|
|
18
|
+
const error = new NotFoundError('User not found');
|
|
19
|
+
expect(error.message).toBe('User not found');
|
|
20
|
+
});
|
|
21
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
jest.mock('winston-daily-rotate-file');
|
|
2
|
+
jest.mock('winston', () => {
|
|
3
|
+
const mockLogger = {
|
|
4
|
+
add: jest.fn(),
|
|
5
|
+
info: jest.fn(),
|
|
6
|
+
error: jest.fn(),
|
|
7
|
+
warn: jest.fn()
|
|
8
|
+
};
|
|
9
|
+
const format = {
|
|
10
|
+
combine: jest.fn(),
|
|
11
|
+
timestamp: jest.fn(),
|
|
12
|
+
json: jest.fn(),
|
|
13
|
+
simple: jest.fn()
|
|
14
|
+
};
|
|
15
|
+
const transports = {
|
|
16
|
+
Console: jest.fn(),
|
|
17
|
+
DailyRotateFile: jest.fn()
|
|
18
|
+
};
|
|
19
|
+
return {
|
|
20
|
+
format,
|
|
21
|
+
transports,
|
|
22
|
+
createLogger: jest.fn().mockReturnValue(mockLogger)
|
|
23
|
+
};
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
<% if (architecture === 'MVC') { -%>
|
|
27
|
+
const logger = require('@/utils/logger');
|
|
28
|
+
<% } else { -%>
|
|
29
|
+
const logger = require('@/infrastructure/log/logger');
|
|
30
|
+
<% } -%>
|
|
31
|
+
|
|
32
|
+
describe('Logger', () => {
|
|
33
|
+
it('should export a logger instance', () => {
|
|
34
|
+
expect(logger).toBeDefined();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should have info method', () => {
|
|
38
|
+
expect(typeof logger.info).toBe('function');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should have error method', () => {
|
|
42
|
+
expect(typeof logger.error).toBe('function');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should call info', () => {
|
|
46
|
+
logger.info('test message');
|
|
47
|
+
expect(logger.info).toHaveBeenCalledWith('test message');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should call error', () => {
|
|
51
|
+
logger.error('test error');
|
|
52
|
+
expect(logger.error).toHaveBeenCalledWith('test error');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should use JSON format in production environment', () => {
|
|
56
|
+
const winston = require('winston');
|
|
57
|
+
jest.resetModules();
|
|
58
|
+
process.env.NODE_ENV = 'production';
|
|
59
|
+
require('@/infrastructure/log/logger');
|
|
60
|
+
expect(winston.format.json).toHaveBeenCalled();
|
|
61
|
+
process.env.NODE_ENV = 'test';
|
|
62
|
+
});
|
|
63
|
+
});
|
package/templates/clean-architecture/js/src/infrastructure/repositories/UserRepository.js.ejs
CHANGED
|
@@ -3,8 +3,7 @@ const UserModel = require('../database/models/User');
|
|
|
3
3
|
class UserRepository {
|
|
4
4
|
async save(user) {
|
|
5
5
|
<%_ if (database === 'None') { -%>
|
|
6
|
-
const newUser =
|
|
7
|
-
UserModel.mockData.push(newUser);
|
|
6
|
+
const newUser = await UserModel.create(user);
|
|
8
7
|
return newUser;
|
|
9
8
|
<%_ } else { -%>
|
|
10
9
|
const newUser = await UserModel.create({ name: user.name, email: user.email });
|
|
@@ -18,7 +17,7 @@ class UserRepository {
|
|
|
18
17
|
|
|
19
18
|
async getUsers() {
|
|
20
19
|
<%_ if (database === 'None') { -%>
|
|
21
|
-
return UserModel.
|
|
20
|
+
return await UserModel.find();
|
|
22
21
|
<%_ } else if (database === 'MongoDB') { -%>
|
|
23
22
|
const users = await UserModel.find();
|
|
24
23
|
return users.map(user => ({
|