create-forgeon 0.1.2 → 0.1.6
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/README.md +19 -17
- package/bin/create-forgeon.mjs +22 -22
- package/package.json +1 -1
- package/src/cli/add-help.mjs +12 -12
- package/src/cli/add-options.mjs +54 -54
- package/src/cli/add-options.test.mjs +24 -24
- package/src/cli/help.mjs +20 -20
- package/src/cli/options.mjs +121 -121
- package/src/cli/options.test.mjs +41 -41
- package/src/cli/prompt-select.mjs +94 -94
- package/src/cli/prompt-select.test.mjs +148 -148
- package/src/constants.mjs +13 -13
- package/src/core/docs.mjs +128 -128
- package/src/core/docs.test.mjs +91 -91
- package/src/core/install.mjs +14 -14
- package/src/core/scaffold.mjs +48 -45
- package/src/core/validate.mjs +12 -12
- package/src/core/validate.test.mjs +73 -73
- package/src/databases/index.mjs +26 -26
- package/src/frameworks/index.mjs +32 -32
- package/src/infrastructure/proxy.mjs +12 -12
- package/src/modules/docs.mjs +70 -70
- package/src/modules/executor.mjs +39 -21
- package/src/modules/executor.test.mjs +95 -45
- package/src/modules/i18n.mjs +283 -0
- package/src/modules/registry.mjs +43 -35
- package/src/presets/i18n.mjs +228 -180
- package/src/presets/index.mjs +2 -2
- package/src/presets/proxy.mjs +32 -32
- package/src/run-add-module.mjs +47 -47
- package/src/run-create-forgeon.mjs +72 -72
- package/src/utils/fs.mjs +26 -26
- package/src/utils/values.mjs +20 -20
- package/templates/base/.dockerignore +7 -7
- package/templates/base/.editorconfig +11 -11
- package/templates/base/README.md +46 -46
- package/templates/base/apps/api/Dockerfile +24 -24
- package/templates/base/apps/api/package.json +39 -39
- package/templates/base/apps/api/prisma/migrations/0001_init/migration.sql +11 -11
- package/templates/base/apps/api/prisma/schema.prisma +14 -14
- package/templates/base/apps/api/prisma/seed.ts +19 -19
- package/templates/base/apps/api/src/app.module.ts +32 -32
- package/templates/base/apps/api/src/common/dto/echo-query.dto.ts +5 -5
- package/templates/base/apps/api/src/common/filters/app-exception.filter.ts +129 -129
- package/templates/base/apps/api/src/config/app.config.ts +12 -12
- package/templates/base/apps/api/src/health/health.controller.ts +30 -30
- package/templates/base/apps/api/src/main.ts +25 -25
- package/templates/base/apps/api/src/prisma/prisma.module.ts +8 -8
- package/templates/base/apps/api/src/prisma/prisma.service.ts +26 -26
- package/templates/base/apps/api/tsconfig.build.json +8 -8
- package/templates/base/apps/api/tsconfig.json +8 -8
- package/templates/base/apps/web/Dockerfile +12 -12
- package/templates/base/apps/web/index.html +11 -11
- package/templates/base/apps/web/package.json +21 -21
- package/templates/base/apps/web/src/App.tsx +35 -35
- package/templates/base/apps/web/src/main.tsx +8 -8
- package/templates/base/apps/web/src/styles.css +32 -32
- package/templates/base/apps/web/tsconfig.json +17 -17
- package/templates/base/apps/web/vite.config.ts +14 -14
- package/templates/base/docs/AI/ARCHITECTURE.md +37 -37
- package/templates/base/docs/AI/MODULE_SPEC.md +56 -56
- package/templates/base/docs/AI/PROJECT.md +31 -31
- package/templates/base/docs/AI/TASKS.md +57 -57
- package/templates/base/docs/README.md +6 -6
- package/templates/base/infra/caddy/Caddyfile +15 -15
- package/templates/base/infra/docker/.env.example +9 -9
- package/templates/base/infra/docker/caddy.Dockerfile +15 -15
- package/templates/base/infra/docker/compose.caddy.yml +44 -44
- package/templates/base/infra/docker/compose.nginx.yml +44 -44
- package/templates/base/infra/docker/compose.none.yml +37 -37
- package/templates/base/infra/docker/compose.yml +44 -44
- package/templates/base/infra/docker/nginx.Dockerfile +15 -15
- package/templates/base/infra/nginx/nginx.conf +31 -31
- package/templates/base/package.json +23 -23
- package/templates/base/packages/core/README.md +2 -2
- package/templates/base/packages/core/package.json +13 -13
- package/templates/base/packages/core/tsconfig.json +7 -7
- package/templates/base/packages/i18n/package.json +18 -18
- package/templates/base/packages/i18n/src/forgeon-i18n.module.ts +45 -45
- package/templates/base/packages/i18n/tsconfig.json +8 -8
- package/templates/base/pnpm-workspace.yaml +2 -2
- package/templates/base/resources/i18n/en/common.json +4 -4
- package/templates/base/resources/i18n/en/validation.json +2 -2
- package/templates/base/resources/i18n/uk/common.json +4 -4
- package/templates/base/resources/i18n/uk/validation.json +2 -2
- package/templates/base/tsconfig.base.json +16 -16
- package/templates/docs-fragments/AI_ARCHITECTURE/00_title.md +1 -1
- package/templates/docs-fragments/AI_ARCHITECTURE/10_layout_base.md +6 -6
- package/templates/docs-fragments/AI_ARCHITECTURE/11_layout_infra.md +1 -1
- package/templates/docs-fragments/AI_ARCHITECTURE/12_layout_i18n_resources.md +1 -1
- package/templates/docs-fragments/AI_ARCHITECTURE/20_env_base.md +4 -4
- package/templates/docs-fragments/AI_ARCHITECTURE/21_env_i18n.md +3 -3
- package/templates/docs-fragments/AI_ARCHITECTURE/30_default_db.md +7 -7
- package/templates/docs-fragments/AI_ARCHITECTURE/31_docker_runtime.md +5 -5
- package/templates/docs-fragments/AI_ARCHITECTURE/32_scope_freeze.md +5 -5
- package/templates/docs-fragments/AI_ARCHITECTURE/40_docs_generation.md +9 -9
- package/templates/docs-fragments/AI_ARCHITECTURE/50_extension_points.md +8 -8
- package/templates/docs-fragments/AI_PROJECT/00_title.md +1 -1
- package/templates/docs-fragments/AI_PROJECT/10_what_is.md +3 -3
- package/templates/docs-fragments/AI_PROJECT/20_structure_base.md +5 -5
- package/templates/docs-fragments/AI_PROJECT/21_structure_i18n.md +2 -0
- package/templates/docs-fragments/AI_PROJECT/22_structure_docker.md +1 -1
- package/templates/docs-fragments/AI_PROJECT/23_structure_docs.md +1 -1
- package/templates/docs-fragments/AI_PROJECT/30_run_dev.md +8 -8
- package/templates/docs-fragments/AI_PROJECT/31_run_docker.md +5 -5
- package/templates/docs-fragments/AI_PROJECT/32_proxy_notes.md +5 -5
- package/templates/docs-fragments/AI_PROJECT/32_proxy_notes_none.md +5 -5
- package/templates/docs-fragments/AI_PROJECT/33_i18n_notes.md +2 -0
- package/templates/docs-fragments/AI_PROJECT/40_change_boundaries_base.md +3 -3
- package/templates/docs-fragments/AI_PROJECT/41_change_boundaries_docker.md +1 -1
- package/templates/docs-fragments/README/00_title.md +3 -3
- package/templates/docs-fragments/README/10_stack.md +8 -8
- package/templates/docs-fragments/README/20_quick_start_dev_intro.md +6 -6
- package/templates/docs-fragments/README/21_quick_start_dev_db_docker.md +4 -4
- package/templates/docs-fragments/README/21_quick_start_dev_db_local.md +1 -1
- package/templates/docs-fragments/README/22_quick_start_dev_outro.md +7 -7
- package/templates/docs-fragments/README/30_quick_start_docker.md +7 -7
- package/templates/docs-fragments/README/30_quick_start_docker_none.md +9 -9
- package/templates/docs-fragments/README/31_proxy_preset_caddy.md +9 -9
- package/templates/docs-fragments/README/31_proxy_preset_nginx.md +8 -8
- package/templates/docs-fragments/README/31_proxy_preset_none.md +6 -6
- package/templates/docs-fragments/README/32_prisma_container_start.md +5 -5
- package/templates/docs-fragments/README/40_i18n.md +14 -8
- package/templates/docs-fragments/README/90_next_steps.md +7 -7
- package/templates/module-fragments/i18n/00_title.md +5 -0
- package/templates/module-fragments/i18n/10_overview.md +9 -0
- package/templates/module-fragments/i18n/20_scope.md +7 -0
- package/templates/module-fragments/i18n/90_status_implemented.md +3 -0
- package/templates/module-fragments/jwt-auth/00_title.md +1 -1
- package/templates/module-fragments/jwt-auth/10_overview.md +6 -6
- package/templates/module-fragments/jwt-auth/20_scope.md +7 -7
- package/templates/module-fragments/jwt-auth/90_status_planned.md +3 -3
- package/templates/module-fragments/queue/00_title.md +1 -1
- package/templates/module-fragments/queue/10_overview.md +6 -6
- package/templates/module-fragments/queue/20_scope.md +7 -7
- package/templates/module-fragments/queue/90_status_planned.md +3 -3
- package/templates/module-presets/i18n/apps/web/src/App.tsx +61 -0
- package/templates/module-presets/i18n/packages/i18n-contracts/package.json +14 -0
- package/templates/module-presets/i18n/packages/i18n-contracts/src/index.ts +7 -0
- package/templates/module-presets/i18n/packages/i18n-contracts/tsconfig.json +8 -0
- package/templates/module-presets/i18n/packages/i18n-web/package.json +17 -0
- package/templates/module-presets/i18n/packages/i18n-web/src/index.ts +50 -0
- package/templates/module-presets/i18n/packages/i18n-web/tsconfig.json +8 -0
package/src/presets/i18n.mjs
CHANGED
|
@@ -1,203 +1,251 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { removeIfExists, writeJson } from '../utils/fs.mjs';
|
|
4
|
-
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { removeIfExists, writeJson } from '../utils/fs.mjs';
|
|
4
|
+
|
|
5
5
|
export function applyI18nDisabled(targetRoot) {
|
|
6
6
|
removeIfExists(path.join(targetRoot, 'packages', 'i18n'));
|
|
7
|
+
removeIfExists(path.join(targetRoot, 'packages', 'i18n-contracts'));
|
|
8
|
+
removeIfExists(path.join(targetRoot, 'packages', 'i18n-web'));
|
|
7
9
|
removeIfExists(path.join(targetRoot, 'resources', 'i18n'));
|
|
8
|
-
|
|
9
|
-
const apiPackagePath = path.join(targetRoot, 'apps', 'api', 'package.json');
|
|
10
|
-
if (fs.existsSync(apiPackagePath)) {
|
|
11
|
-
const apiPackage = JSON.parse(fs.readFileSync(apiPackagePath, 'utf8'));
|
|
12
|
-
|
|
13
|
-
if (apiPackage.scripts) {
|
|
14
|
-
delete apiPackage.scripts.predev;
|
|
15
|
-
}
|
|
16
|
-
|
|
10
|
+
|
|
11
|
+
const apiPackagePath = path.join(targetRoot, 'apps', 'api', 'package.json');
|
|
12
|
+
if (fs.existsSync(apiPackagePath)) {
|
|
13
|
+
const apiPackage = JSON.parse(fs.readFileSync(apiPackagePath, 'utf8'));
|
|
14
|
+
|
|
15
|
+
if (apiPackage.scripts) {
|
|
16
|
+
delete apiPackage.scripts.predev;
|
|
17
|
+
}
|
|
18
|
+
|
|
17
19
|
if (apiPackage.dependencies) {
|
|
18
20
|
delete apiPackage.dependencies['@forgeon/i18n'];
|
|
21
|
+
delete apiPackage.dependencies['@forgeon/i18n-contracts'];
|
|
19
22
|
delete apiPackage.dependencies['nestjs-i18n'];
|
|
20
23
|
}
|
|
21
|
-
|
|
22
|
-
writeJson(apiPackagePath, apiPackage);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const apiDockerfile = path.join(targetRoot, 'apps', 'api', 'Dockerfile');
|
|
24
|
+
|
|
25
|
+
writeJson(apiPackagePath, apiPackage);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const apiDockerfile = path.join(targetRoot, 'apps', 'api', 'Dockerfile');
|
|
26
29
|
if (fs.existsSync(apiDockerfile)) {
|
|
27
30
|
let content = fs.readFileSync(apiDockerfile, 'utf8');
|
|
28
31
|
content = content
|
|
29
32
|
.replace(/^COPY packages\/i18n\/package\.json packages\/i18n\/package\.json\r?\n/gm, '')
|
|
33
|
+
.replace(
|
|
34
|
+
/^COPY packages\/i18n-contracts\/package\.json packages\/i18n-contracts\/package\.json\r?\n/gm,
|
|
35
|
+
'',
|
|
36
|
+
)
|
|
30
37
|
.replace(/^COPY packages\/i18n packages\/i18n\r?\n/gm, '')
|
|
31
|
-
.replace(/^
|
|
38
|
+
.replace(/^COPY packages\/i18n-contracts packages\/i18n-contracts\r?\n/gm, '')
|
|
39
|
+
.replace(/^RUN pnpm --filter @forgeon\/i18n build\r?\n/gm, '')
|
|
40
|
+
.replace(/^RUN pnpm --filter @forgeon\/i18n-contracts build\r?\n/gm, '');
|
|
32
41
|
fs.writeFileSync(apiDockerfile, content, 'utf8');
|
|
33
42
|
}
|
|
34
43
|
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
import { AppExceptionFilter } from './common/filters/app-exception.filter';
|
|
44
|
-
|
|
45
|
-
@Module({
|
|
46
|
-
imports: [
|
|
47
|
-
ConfigModule.forRoot({
|
|
48
|
-
isGlobal: true,
|
|
49
|
-
load: [appConfig],
|
|
50
|
-
envFilePath: '.env',
|
|
51
|
-
}),
|
|
52
|
-
PrismaModule,
|
|
53
|
-
],
|
|
54
|
-
controllers: [HealthController],
|
|
55
|
-
providers: [AppExceptionFilter],
|
|
56
|
-
})
|
|
57
|
-
export class AppModule {}
|
|
58
|
-
`,
|
|
59
|
-
'utf8',
|
|
60
|
-
);
|
|
61
|
-
|
|
62
|
-
const healthControllerPath = path.join(
|
|
63
|
-
targetRoot,
|
|
64
|
-
'apps',
|
|
65
|
-
'api',
|
|
66
|
-
'src',
|
|
67
|
-
'health',
|
|
68
|
-
'health.controller.ts',
|
|
69
|
-
);
|
|
70
|
-
fs.writeFileSync(
|
|
71
|
-
healthControllerPath,
|
|
72
|
-
`import { Controller, Get, Query } from '@nestjs/common';
|
|
73
|
-
import { EchoQueryDto } from '../common/dto/echo-query.dto';
|
|
74
|
-
|
|
75
|
-
@Controller('health')
|
|
76
|
-
export class HealthController {
|
|
77
|
-
@Get()
|
|
78
|
-
getHealth(@Query('lang') _lang?: string) {
|
|
79
|
-
return {
|
|
80
|
-
status: 'ok',
|
|
81
|
-
message: 'OK',
|
|
82
|
-
};
|
|
83
|
-
}
|
|
44
|
+
const proxyDockerfiles = [
|
|
45
|
+
path.join(targetRoot, 'infra', 'docker', 'caddy.Dockerfile'),
|
|
46
|
+
path.join(targetRoot, 'infra', 'docker', 'nginx.Dockerfile'),
|
|
47
|
+
];
|
|
48
|
+
for (const dockerfilePath of proxyDockerfiles) {
|
|
49
|
+
if (!fs.existsSync(dockerfilePath)) {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
84
52
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
53
|
+
const content = fs
|
|
54
|
+
.readFileSync(dockerfilePath, 'utf8')
|
|
55
|
+
.replace(
|
|
56
|
+
/^COPY packages\/i18n-contracts\/package\.json packages\/i18n-contracts\/package\.json\r?\n/gm,
|
|
57
|
+
'',
|
|
58
|
+
)
|
|
59
|
+
.replace(/^COPY packages\/i18n-web\/package\.json packages\/i18n-web\/package\.json\r?\n/gm, '')
|
|
60
|
+
.replace(/^COPY packages\/i18n-contracts packages\/i18n-contracts\r?\n/gm, '')
|
|
61
|
+
.replace(/^COPY packages\/i18n-web packages\/i18n-web\r?\n/gm, '');
|
|
62
|
+
|
|
63
|
+
fs.writeFileSync(dockerfilePath, content, 'utf8');
|
|
88
64
|
}
|
|
89
|
-
}
|
|
90
|
-
`,
|
|
91
|
-
'utf8',
|
|
92
|
-
);
|
|
93
65
|
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
'
|
|
97
|
-
'api',
|
|
98
|
-
'src',
|
|
99
|
-
'common',
|
|
100
|
-
'filters',
|
|
101
|
-
'app-exception.filter.ts',
|
|
102
|
-
);
|
|
103
|
-
fs.writeFileSync(
|
|
104
|
-
filterPath,
|
|
105
|
-
`import {
|
|
106
|
-
ArgumentsHost,
|
|
107
|
-
Catch,
|
|
108
|
-
ExceptionFilter,
|
|
109
|
-
HttpException,
|
|
110
|
-
HttpStatus,
|
|
111
|
-
Injectable,
|
|
112
|
-
} from '@nestjs/common';
|
|
113
|
-
import { Response } from 'express';
|
|
66
|
+
const webPackagePath = path.join(targetRoot, 'apps', 'web', 'package.json');
|
|
67
|
+
if (fs.existsSync(webPackagePath)) {
|
|
68
|
+
const webPackage = JSON.parse(fs.readFileSync(webPackagePath, 'utf8'));
|
|
114
69
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
catch(exception: unknown, host: ArgumentsHost): void {
|
|
119
|
-
const context = host.switchToHttp();
|
|
120
|
-
const response = context.getResponse<Response>();
|
|
121
|
-
|
|
122
|
-
const status =
|
|
123
|
-
exception instanceof HttpException
|
|
124
|
-
? exception.getStatus()
|
|
125
|
-
: HttpStatus.INTERNAL_SERVER_ERROR;
|
|
126
|
-
|
|
127
|
-
const payload =
|
|
128
|
-
exception instanceof HttpException
|
|
129
|
-
? exception.getResponse()
|
|
130
|
-
: { message: 'Internal server error' };
|
|
131
|
-
|
|
132
|
-
const message =
|
|
133
|
-
typeof payload === 'object' && payload !== null && 'message' in payload
|
|
134
|
-
? Array.isArray((payload as { message?: unknown }).message)
|
|
135
|
-
? String((payload as { message: unknown[] }).message[0] ?? 'Internal server error')
|
|
136
|
-
: String((payload as { message?: unknown }).message ?? 'Internal server error')
|
|
137
|
-
: typeof payload === 'string'
|
|
138
|
-
? payload
|
|
139
|
-
: 'Internal server error';
|
|
140
|
-
|
|
141
|
-
response.status(status).json({
|
|
142
|
-
error: {
|
|
143
|
-
code: this.resolveCode(status),
|
|
144
|
-
message,
|
|
145
|
-
},
|
|
146
|
-
});
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
private resolveCode(status: number): string {
|
|
150
|
-
switch (status) {
|
|
151
|
-
case HttpStatus.BAD_REQUEST:
|
|
152
|
-
return 'validation_error';
|
|
153
|
-
case HttpStatus.UNAUTHORIZED:
|
|
154
|
-
return 'unauthorized';
|
|
155
|
-
case HttpStatus.FORBIDDEN:
|
|
156
|
-
return 'forbidden';
|
|
157
|
-
case HttpStatus.NOT_FOUND:
|
|
158
|
-
return 'not_found';
|
|
159
|
-
case HttpStatus.CONFLICT:
|
|
160
|
-
return 'conflict';
|
|
161
|
-
default:
|
|
162
|
-
return 'internal_error';
|
|
70
|
+
if (webPackage.scripts) {
|
|
71
|
+
delete webPackage.scripts.predev;
|
|
72
|
+
delete webPackage.scripts.prebuild;
|
|
163
73
|
}
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
`,
|
|
167
|
-
'utf8',
|
|
168
|
-
);
|
|
169
74
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
export default registerAs('app', () => ({
|
|
176
|
-
port: Number(process.env.PORT ?? 3000),
|
|
177
|
-
}));
|
|
178
|
-
`,
|
|
179
|
-
'utf8',
|
|
180
|
-
);
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
export function patchDockerEnvForI18n(targetRoot, i18nEnabled) {
|
|
184
|
-
const dockerEnvPath = path.join(targetRoot, 'infra', 'docker', '.env.example');
|
|
185
|
-
if (fs.existsSync(dockerEnvPath) && !i18nEnabled) {
|
|
186
|
-
const content = fs
|
|
187
|
-
.readFileSync(dockerEnvPath, 'utf8')
|
|
188
|
-
.replace(/^I18N_ENABLED=.*\r?\n/gm, '')
|
|
189
|
-
.replace(/^I18N_DEFAULT_LANG=.*\r?\n/gm, '')
|
|
190
|
-
.replace(/^I18N_FALLBACK_LANG=.*\r?\n/gm, '');
|
|
191
|
-
fs.writeFileSync(dockerEnvPath, content.trimEnd() + '\n', 'utf8');
|
|
192
|
-
}
|
|
75
|
+
if (webPackage.dependencies) {
|
|
76
|
+
delete webPackage.dependencies['@forgeon/i18n-contracts'];
|
|
77
|
+
delete webPackage.dependencies['@forgeon/i18n-web'];
|
|
78
|
+
}
|
|
193
79
|
|
|
194
|
-
|
|
195
|
-
if (fs.existsSync(composePath) && !i18nEnabled) {
|
|
196
|
-
const content = fs
|
|
197
|
-
.readFileSync(composePath, 'utf8')
|
|
198
|
-
.replace(/^\s+I18N_ENABLED:.*\r?\n/gm, '')
|
|
199
|
-
.replace(/^\s+I18N_DEFAULT_LANG:.*\r?\n/gm, '')
|
|
200
|
-
.replace(/^\s+I18N_FALLBACK_LANG:.*\r?\n/gm, '');
|
|
201
|
-
fs.writeFileSync(composePath, content, 'utf8');
|
|
80
|
+
writeJson(webPackagePath, webPackage);
|
|
202
81
|
}
|
|
203
|
-
|
|
82
|
+
|
|
83
|
+
const appModulePath = path.join(targetRoot, 'apps', 'api', 'src', 'app.module.ts');
|
|
84
|
+
fs.writeFileSync(
|
|
85
|
+
appModulePath,
|
|
86
|
+
`import { Module } from '@nestjs/common';
|
|
87
|
+
import { ConfigModule } from '@nestjs/config';
|
|
88
|
+
import appConfig from './config/app.config';
|
|
89
|
+
import { HealthController } from './health/health.controller';
|
|
90
|
+
import { PrismaModule } from './prisma/prisma.module';
|
|
91
|
+
import { AppExceptionFilter } from './common/filters/app-exception.filter';
|
|
92
|
+
|
|
93
|
+
@Module({
|
|
94
|
+
imports: [
|
|
95
|
+
ConfigModule.forRoot({
|
|
96
|
+
isGlobal: true,
|
|
97
|
+
load: [appConfig],
|
|
98
|
+
envFilePath: '.env',
|
|
99
|
+
}),
|
|
100
|
+
PrismaModule,
|
|
101
|
+
],
|
|
102
|
+
controllers: [HealthController],
|
|
103
|
+
providers: [AppExceptionFilter],
|
|
104
|
+
})
|
|
105
|
+
export class AppModule {}
|
|
106
|
+
`,
|
|
107
|
+
'utf8',
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
const healthControllerPath = path.join(
|
|
111
|
+
targetRoot,
|
|
112
|
+
'apps',
|
|
113
|
+
'api',
|
|
114
|
+
'src',
|
|
115
|
+
'health',
|
|
116
|
+
'health.controller.ts',
|
|
117
|
+
);
|
|
118
|
+
fs.writeFileSync(
|
|
119
|
+
healthControllerPath,
|
|
120
|
+
`import { Controller, Get, Query } from '@nestjs/common';
|
|
121
|
+
import { EchoQueryDto } from '../common/dto/echo-query.dto';
|
|
122
|
+
|
|
123
|
+
@Controller('health')
|
|
124
|
+
export class HealthController {
|
|
125
|
+
@Get()
|
|
126
|
+
getHealth(@Query('lang') _lang?: string) {
|
|
127
|
+
return {
|
|
128
|
+
status: 'ok',
|
|
129
|
+
message: 'OK',
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
@Get('echo')
|
|
134
|
+
getEcho(@Query() query: EchoQueryDto) {
|
|
135
|
+
return { value: query.value };
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
`,
|
|
139
|
+
'utf8',
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
const filterPath = path.join(
|
|
143
|
+
targetRoot,
|
|
144
|
+
'apps',
|
|
145
|
+
'api',
|
|
146
|
+
'src',
|
|
147
|
+
'common',
|
|
148
|
+
'filters',
|
|
149
|
+
'app-exception.filter.ts',
|
|
150
|
+
);
|
|
151
|
+
fs.writeFileSync(
|
|
152
|
+
filterPath,
|
|
153
|
+
`import {
|
|
154
|
+
ArgumentsHost,
|
|
155
|
+
Catch,
|
|
156
|
+
ExceptionFilter,
|
|
157
|
+
HttpException,
|
|
158
|
+
HttpStatus,
|
|
159
|
+
Injectable,
|
|
160
|
+
} from '@nestjs/common';
|
|
161
|
+
import { Response } from 'express';
|
|
162
|
+
|
|
163
|
+
@Injectable()
|
|
164
|
+
@Catch()
|
|
165
|
+
export class AppExceptionFilter implements ExceptionFilter {
|
|
166
|
+
catch(exception: unknown, host: ArgumentsHost): void {
|
|
167
|
+
const context = host.switchToHttp();
|
|
168
|
+
const response = context.getResponse<Response>();
|
|
169
|
+
|
|
170
|
+
const status =
|
|
171
|
+
exception instanceof HttpException
|
|
172
|
+
? exception.getStatus()
|
|
173
|
+
: HttpStatus.INTERNAL_SERVER_ERROR;
|
|
174
|
+
|
|
175
|
+
const payload =
|
|
176
|
+
exception instanceof HttpException
|
|
177
|
+
? exception.getResponse()
|
|
178
|
+
: { message: 'Internal server error' };
|
|
179
|
+
|
|
180
|
+
const message =
|
|
181
|
+
typeof payload === 'object' && payload !== null && 'message' in payload
|
|
182
|
+
? Array.isArray((payload as { message?: unknown }).message)
|
|
183
|
+
? String((payload as { message: unknown[] }).message[0] ?? 'Internal server error')
|
|
184
|
+
: String((payload as { message?: unknown }).message ?? 'Internal server error')
|
|
185
|
+
: typeof payload === 'string'
|
|
186
|
+
? payload
|
|
187
|
+
: 'Internal server error';
|
|
188
|
+
|
|
189
|
+
response.status(status).json({
|
|
190
|
+
error: {
|
|
191
|
+
code: this.resolveCode(status),
|
|
192
|
+
message,
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
private resolveCode(status: number): string {
|
|
198
|
+
switch (status) {
|
|
199
|
+
case HttpStatus.BAD_REQUEST:
|
|
200
|
+
return 'validation_error';
|
|
201
|
+
case HttpStatus.UNAUTHORIZED:
|
|
202
|
+
return 'unauthorized';
|
|
203
|
+
case HttpStatus.FORBIDDEN:
|
|
204
|
+
return 'forbidden';
|
|
205
|
+
case HttpStatus.NOT_FOUND:
|
|
206
|
+
return 'not_found';
|
|
207
|
+
case HttpStatus.CONFLICT:
|
|
208
|
+
return 'conflict';
|
|
209
|
+
default:
|
|
210
|
+
return 'internal_error';
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
`,
|
|
215
|
+
'utf8',
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
const appConfigPath = path.join(targetRoot, 'apps', 'api', 'src', 'config', 'app.config.ts');
|
|
219
|
+
fs.writeFileSync(
|
|
220
|
+
appConfigPath,
|
|
221
|
+
`import { registerAs } from '@nestjs/config';
|
|
222
|
+
|
|
223
|
+
export default registerAs('app', () => ({
|
|
224
|
+
port: Number(process.env.PORT ?? 3000),
|
|
225
|
+
}));
|
|
226
|
+
`,
|
|
227
|
+
'utf8',
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function patchDockerEnvForI18n(targetRoot, i18nEnabled) {
|
|
232
|
+
const dockerEnvPath = path.join(targetRoot, 'infra', 'docker', '.env.example');
|
|
233
|
+
if (fs.existsSync(dockerEnvPath) && !i18nEnabled) {
|
|
234
|
+
const content = fs
|
|
235
|
+
.readFileSync(dockerEnvPath, 'utf8')
|
|
236
|
+
.replace(/^I18N_ENABLED=.*\r?\n/gm, '')
|
|
237
|
+
.replace(/^I18N_DEFAULT_LANG=.*\r?\n/gm, '')
|
|
238
|
+
.replace(/^I18N_FALLBACK_LANG=.*\r?\n/gm, '');
|
|
239
|
+
fs.writeFileSync(dockerEnvPath, content.trimEnd() + '\n', 'utf8');
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const composePath = path.join(targetRoot, 'infra', 'docker', 'compose.yml');
|
|
243
|
+
if (fs.existsSync(composePath) && !i18nEnabled) {
|
|
244
|
+
const content = fs
|
|
245
|
+
.readFileSync(composePath, 'utf8')
|
|
246
|
+
.replace(/^\s+I18N_ENABLED:.*\r?\n/gm, '')
|
|
247
|
+
.replace(/^\s+I18N_DEFAULT_LANG:.*\r?\n/gm, '')
|
|
248
|
+
.replace(/^\s+I18N_FALLBACK_LANG:.*\r?\n/gm, '');
|
|
249
|
+
fs.writeFileSync(composePath, content, 'utf8');
|
|
250
|
+
}
|
|
251
|
+
}
|
package/src/presets/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { applyProxyPreset } from './proxy.mjs';
|
|
2
|
-
export { applyI18nDisabled, patchDockerEnvForI18n } from './i18n.mjs';
|
|
1
|
+
export { applyProxyPreset } from './proxy.mjs';
|
|
2
|
+
export { applyI18nDisabled, patchDockerEnvForI18n } from './i18n.mjs';
|
package/src/presets/proxy.mjs
CHANGED
|
@@ -1,32 +1,32 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { removeIfExists } from '../utils/fs.mjs';
|
|
4
|
-
|
|
5
|
-
export function applyProxyPreset(targetRoot, proxy) {
|
|
6
|
-
const dockerDir = path.join(targetRoot, 'infra', 'docker');
|
|
7
|
-
const composeTarget = path.join(dockerDir, 'compose.yml');
|
|
8
|
-
const composeSource = path.join(dockerDir, `compose.${proxy}.yml`);
|
|
9
|
-
|
|
10
|
-
if (!fs.existsSync(composeSource)) {
|
|
11
|
-
throw new Error(`Missing proxy compose preset: ${composeSource}`);
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
fs.copyFileSync(composeSource, composeTarget);
|
|
15
|
-
|
|
16
|
-
removeIfExists(path.join(dockerDir, 'compose.nginx.yml'));
|
|
17
|
-
removeIfExists(path.join(dockerDir, 'compose.caddy.yml'));
|
|
18
|
-
removeIfExists(path.join(dockerDir, 'compose.none.yml'));
|
|
19
|
-
|
|
20
|
-
if (proxy === 'nginx') {
|
|
21
|
-
removeIfExists(path.join(dockerDir, 'caddy.Dockerfile'));
|
|
22
|
-
removeIfExists(path.join(targetRoot, 'infra', 'caddy'));
|
|
23
|
-
} else if (proxy === 'caddy') {
|
|
24
|
-
removeIfExists(path.join(dockerDir, 'nginx.Dockerfile'));
|
|
25
|
-
removeIfExists(path.join(targetRoot, 'infra', 'nginx'));
|
|
26
|
-
} else if (proxy === 'none') {
|
|
27
|
-
removeIfExists(path.join(dockerDir, 'nginx.Dockerfile'));
|
|
28
|
-
removeIfExists(path.join(dockerDir, 'caddy.Dockerfile'));
|
|
29
|
-
removeIfExists(path.join(targetRoot, 'infra', 'nginx'));
|
|
30
|
-
removeIfExists(path.join(targetRoot, 'infra', 'caddy'));
|
|
31
|
-
}
|
|
32
|
-
}
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { removeIfExists } from '../utils/fs.mjs';
|
|
4
|
+
|
|
5
|
+
export function applyProxyPreset(targetRoot, proxy) {
|
|
6
|
+
const dockerDir = path.join(targetRoot, 'infra', 'docker');
|
|
7
|
+
const composeTarget = path.join(dockerDir, 'compose.yml');
|
|
8
|
+
const composeSource = path.join(dockerDir, `compose.${proxy}.yml`);
|
|
9
|
+
|
|
10
|
+
if (!fs.existsSync(composeSource)) {
|
|
11
|
+
throw new Error(`Missing proxy compose preset: ${composeSource}`);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
fs.copyFileSync(composeSource, composeTarget);
|
|
15
|
+
|
|
16
|
+
removeIfExists(path.join(dockerDir, 'compose.nginx.yml'));
|
|
17
|
+
removeIfExists(path.join(dockerDir, 'compose.caddy.yml'));
|
|
18
|
+
removeIfExists(path.join(dockerDir, 'compose.none.yml'));
|
|
19
|
+
|
|
20
|
+
if (proxy === 'nginx') {
|
|
21
|
+
removeIfExists(path.join(dockerDir, 'caddy.Dockerfile'));
|
|
22
|
+
removeIfExists(path.join(targetRoot, 'infra', 'caddy'));
|
|
23
|
+
} else if (proxy === 'caddy') {
|
|
24
|
+
removeIfExists(path.join(dockerDir, 'nginx.Dockerfile'));
|
|
25
|
+
removeIfExists(path.join(targetRoot, 'infra', 'nginx'));
|
|
26
|
+
} else if (proxy === 'none') {
|
|
27
|
+
removeIfExists(path.join(dockerDir, 'nginx.Dockerfile'));
|
|
28
|
+
removeIfExists(path.join(dockerDir, 'caddy.Dockerfile'));
|
|
29
|
+
removeIfExists(path.join(targetRoot, 'infra', 'nginx'));
|
|
30
|
+
removeIfExists(path.join(targetRoot, 'infra', 'caddy'));
|
|
31
|
+
}
|
|
32
|
+
}
|
package/src/run-add-module.mjs
CHANGED
|
@@ -1,47 +1,47 @@
|
|
|
1
|
-
import path from 'node:path';
|
|
2
|
-
import { fileURLToPath } from 'node:url';
|
|
3
|
-
import { printAddHelp } from './cli/add-help.mjs';
|
|
4
|
-
import { parseAddCliArgs } from './cli/add-options.mjs';
|
|
5
|
-
import { addModule } from './modules/executor.mjs';
|
|
6
|
-
import { listModulePresets } from './modules/registry.mjs';
|
|
7
|
-
|
|
8
|
-
function printModuleList() {
|
|
9
|
-
const modules = listModulePresets();
|
|
10
|
-
console.log('Available modules:');
|
|
11
|
-
for (const moduleItem of modules) {
|
|
12
|
-
const status = moduleItem.implemented ? 'implemented' : 'planned';
|
|
13
|
-
console.log(`- ${moduleItem.id} (${status}) - ${moduleItem.description}`);
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export async function runAddModule(argv = process.argv.slice(2)) {
|
|
18
|
-
const options = parseAddCliArgs(argv);
|
|
19
|
-
|
|
20
|
-
if (options.help) {
|
|
21
|
-
printAddHelp();
|
|
22
|
-
return;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
if (options.list) {
|
|
26
|
-
printModuleList();
|
|
27
|
-
return;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
if (!options.moduleId) {
|
|
31
|
-
throw new Error('Module id is required. Use `create-forgeon add --list` to see available modules.');
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const srcDir = path.dirname(fileURLToPath(import.meta.url));
|
|
35
|
-
const packageRoot = path.resolve(srcDir, '..');
|
|
36
|
-
const targetRoot = path.resolve(process.cwd(), options.project);
|
|
37
|
-
|
|
38
|
-
const result = addModule({
|
|
39
|
-
moduleId: options.moduleId,
|
|
40
|
-
targetRoot,
|
|
41
|
-
packageRoot,
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
console.log(result.message);
|
|
45
|
-
console.log(`- module: ${result.preset.id}`);
|
|
46
|
-
console.log(`- docs: ${result.docsPath}`);
|
|
47
|
-
}
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { printAddHelp } from './cli/add-help.mjs';
|
|
4
|
+
import { parseAddCliArgs } from './cli/add-options.mjs';
|
|
5
|
+
import { addModule } from './modules/executor.mjs';
|
|
6
|
+
import { listModulePresets } from './modules/registry.mjs';
|
|
7
|
+
|
|
8
|
+
function printModuleList() {
|
|
9
|
+
const modules = listModulePresets();
|
|
10
|
+
console.log('Available modules:');
|
|
11
|
+
for (const moduleItem of modules) {
|
|
12
|
+
const status = moduleItem.implemented ? 'implemented' : 'planned';
|
|
13
|
+
console.log(`- ${moduleItem.id} (${status}) - ${moduleItem.description}`);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function runAddModule(argv = process.argv.slice(2)) {
|
|
18
|
+
const options = parseAddCliArgs(argv);
|
|
19
|
+
|
|
20
|
+
if (options.help) {
|
|
21
|
+
printAddHelp();
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (options.list) {
|
|
26
|
+
printModuleList();
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (!options.moduleId) {
|
|
31
|
+
throw new Error('Module id is required. Use `create-forgeon add --list` to see available modules.');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const srcDir = path.dirname(fileURLToPath(import.meta.url));
|
|
35
|
+
const packageRoot = path.resolve(srcDir, '..');
|
|
36
|
+
const targetRoot = path.resolve(process.cwd(), options.project);
|
|
37
|
+
|
|
38
|
+
const result = addModule({
|
|
39
|
+
moduleId: options.moduleId,
|
|
40
|
+
targetRoot,
|
|
41
|
+
packageRoot,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
console.log(result.message);
|
|
45
|
+
console.log(`- module: ${result.preset.id}`);
|
|
46
|
+
console.log(`- docs: ${result.docsPath}`);
|
|
47
|
+
}
|