create-forgeon 0.1.38 → 0.2.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/README.md +12 -4
- package/package.json +1 -1
- package/src/cli/help.mjs +3 -2
- package/src/cli/options.mjs +142 -121
- package/src/cli/options.test.mjs +13 -10
- package/src/constants.mjs +11 -9
- package/src/core/docs.mjs +44 -23
- package/src/core/docs.test.mjs +21 -15
- package/src/core/scaffold.mjs +27 -15
- package/src/modules/db-prisma.mjs +134 -32
- package/src/modules/executor.test.mjs +44 -13
- package/src/modules/i18n.mjs +11 -28
- package/src/modules/logger.mjs +4 -1
- package/src/modules/swagger.mjs +7 -2
- package/src/presets/i18n.mjs +63 -40
- package/src/run-add-module.mjs +87 -17
- package/src/run-create-forgeon.mjs +33 -24
- package/templates/base/README.md +16 -3
- package/templates/base/apps/api/Dockerfile +6 -11
- package/templates/base/apps/api/package.json +13 -24
- package/templates/base/apps/api/src/app.module.ts +3 -5
- package/templates/base/apps/api/src/health/health.controller.ts +1 -19
- package/templates/base/apps/web/src/App.tsx +0 -5
- package/templates/base/docs/AI/MODULE_CHECKS.md +1 -1
- package/templates/base/docs/AI/ROADMAP.md +1 -1
- package/templates/base/infra/docker/.env.example +1 -6
- package/templates/base/infra/docker/compose.caddy.yml +13 -37
- package/templates/base/infra/docker/compose.nginx.yml +13 -37
- package/templates/base/infra/docker/compose.none.yml +8 -32
- package/templates/base/infra/docker/compose.yml +16 -40
- package/templates/base/package.json +12 -9
- package/templates/base/scripts/forgeon-sync-integrations.mjs +399 -0
- package/templates/docs-fragments/AI_ARCHITECTURE/20_env_base.md +0 -1
- package/templates/docs-fragments/AI_ARCHITECTURE/20b_env_db_prisma.md +1 -0
- package/templates/docs-fragments/AI_ARCHITECTURE/30_default_db.md +2 -2
- package/templates/docs-fragments/AI_ARCHITECTURE/30_default_db_none.md +7 -0
- package/templates/docs-fragments/AI_PROJECT/20_structure_base.md +0 -1
- package/templates/docs-fragments/AI_PROJECT/20b_structure_db_none.md +2 -0
- package/templates/docs-fragments/AI_PROJECT/20b_structure_db_prisma.md +1 -0
- package/templates/docs-fragments/README/10_stack.md +4 -4
- package/templates/docs-fragments/README/21_quick_start_dev_no_db.md +6 -0
- package/templates/module-presets/i18n/apps/web/src/App.tsx +0 -5
- /package/templates/{base → module-presets/db-prisma}/apps/api/prisma/migrations/0001_init/migration.sql +0 -0
- /package/templates/{base → module-presets/db-prisma}/apps/api/prisma/migrations/migration_lock.toml +0 -0
- /package/templates/{base → module-presets/db-prisma}/apps/api/prisma/schema.prisma +0 -0
- /package/templates/{base → module-presets/db-prisma}/apps/api/prisma/seed.ts +0 -0
- /package/templates/{base → module-presets/db-prisma}/packages/db-prisma/README.md +0 -0
- /package/templates/{base → module-presets/db-prisma}/packages/db-prisma/package.json +0 -0
- /package/templates/{base → module-presets/db-prisma}/packages/db-prisma/src/db-prisma-config.loader.ts +0 -0
- /package/templates/{base → module-presets/db-prisma}/packages/db-prisma/src/db-prisma-config.service.ts +0 -0
- /package/templates/{base → module-presets/db-prisma}/packages/db-prisma/src/db-prisma-env.schema.ts +0 -0
- /package/templates/{base → module-presets/db-prisma}/packages/db-prisma/src/db-prisma.module.ts +0 -0
- /package/templates/{base → module-presets/db-prisma}/packages/db-prisma/src/index.ts +0 -0
- /package/templates/{base → module-presets/db-prisma}/packages/db-prisma/src/prisma.service.ts +0 -0
- /package/templates/{base → module-presets/db-prisma}/packages/db-prisma/tsconfig.json +0 -0
package/src/presets/i18n.mjs
CHANGED
|
@@ -7,16 +7,31 @@ export function applyI18nDisabled(targetRoot) {
|
|
|
7
7
|
removeIfExists(path.join(targetRoot, 'packages', 'i18n-contracts'));
|
|
8
8
|
removeIfExists(path.join(targetRoot, 'packages', 'i18n-web'));
|
|
9
9
|
removeIfExists(path.join(targetRoot, 'resources', 'i18n'));
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
removeIfExists(path.join(targetRoot, 'scripts', 'i18n-add.mjs'));
|
|
11
|
+
|
|
12
|
+
const apiPackagePath = path.join(targetRoot, 'apps', 'api', 'package.json');
|
|
12
13
|
if (fs.existsSync(apiPackagePath)) {
|
|
13
14
|
const apiPackage = JSON.parse(fs.readFileSync(apiPackagePath, 'utf8'));
|
|
14
15
|
|
|
15
16
|
if (apiPackage.scripts) {
|
|
16
|
-
apiPackage.scripts.predev
|
|
17
|
-
|
|
17
|
+
const currentPredev = typeof apiPackage.scripts.predev === 'string' ? apiPackage.scripts.predev : '';
|
|
18
|
+
const nextSteps = currentPredev
|
|
19
|
+
.split('&&')
|
|
20
|
+
.map((item) => item.trim())
|
|
21
|
+
.filter(Boolean)
|
|
22
|
+
.filter(
|
|
23
|
+
(step) =>
|
|
24
|
+
step !== 'pnpm --filter @forgeon/i18n-contracts build' &&
|
|
25
|
+
step !== 'pnpm --filter @forgeon/i18n build',
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
if (nextSteps.length > 0) {
|
|
29
|
+
apiPackage.scripts.predev = nextSteps.join(' && ');
|
|
30
|
+
} else {
|
|
31
|
+
delete apiPackage.scripts.predev;
|
|
32
|
+
}
|
|
18
33
|
}
|
|
19
|
-
|
|
34
|
+
|
|
20
35
|
if (apiPackage.dependencies) {
|
|
21
36
|
delete apiPackage.dependencies['@forgeon/i18n'];
|
|
22
37
|
delete apiPackage.dependencies['@forgeon/i18n-contracts'];
|
|
@@ -70,8 +85,37 @@ export function applyI18nDisabled(targetRoot) {
|
|
|
70
85
|
const webPackage = JSON.parse(fs.readFileSync(webPackagePath, 'utf8'));
|
|
71
86
|
|
|
72
87
|
if (webPackage.scripts) {
|
|
73
|
-
|
|
74
|
-
|
|
88
|
+
const removeI18nBuildSteps = (scriptValue) => {
|
|
89
|
+
if (typeof scriptValue !== 'string') {
|
|
90
|
+
return scriptValue;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const next = scriptValue
|
|
94
|
+
.split('&&')
|
|
95
|
+
.map((item) => item.trim())
|
|
96
|
+
.filter(Boolean)
|
|
97
|
+
.filter(
|
|
98
|
+
(step) =>
|
|
99
|
+
step !== 'pnpm --filter @forgeon/i18n-contracts build' &&
|
|
100
|
+
step !== 'pnpm --filter @forgeon/i18n-web build',
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
return next.length > 0 ? next.join(' && ') : undefined;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const nextPredev = removeI18nBuildSteps(webPackage.scripts.predev);
|
|
107
|
+
if (nextPredev) {
|
|
108
|
+
webPackage.scripts.predev = nextPredev;
|
|
109
|
+
} else {
|
|
110
|
+
delete webPackage.scripts.predev;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const nextPrebuild = removeI18nBuildSteps(webPackage.scripts.prebuild);
|
|
114
|
+
if (nextPrebuild) {
|
|
115
|
+
webPackage.scripts.prebuild = nextPrebuild;
|
|
116
|
+
} else {
|
|
117
|
+
delete webPackage.scripts.prebuild;
|
|
118
|
+
}
|
|
75
119
|
}
|
|
76
120
|
|
|
77
121
|
if (webPackage.dependencies) {
|
|
@@ -107,12 +151,11 @@ export function applyI18nDisabled(targetRoot) {
|
|
|
107
151
|
writeJson(rootPackagePath, rootPackage);
|
|
108
152
|
}
|
|
109
153
|
|
|
110
|
-
const appModulePath = path.join(targetRoot, 'apps', 'api', 'src', 'app.module.ts');
|
|
154
|
+
const appModulePath = path.join(targetRoot, 'apps', 'api', 'src', 'app.module.ts');
|
|
111
155
|
fs.writeFileSync(
|
|
112
156
|
appModulePath,
|
|
113
157
|
`import { Module } from '@nestjs/common';
|
|
114
158
|
import { ConfigModule } from '@nestjs/config';
|
|
115
|
-
import { dbPrismaConfig, dbPrismaEnvSchema, DbPrismaModule } from '@forgeon/db-prisma';
|
|
116
159
|
import { CoreConfigModule, CoreErrorsModule, coreConfig, coreEnvSchema, createEnvValidator } from '@forgeon/core';
|
|
117
160
|
import { HealthController } from './health/health.controller';
|
|
118
161
|
|
|
@@ -120,13 +163,12 @@ import { HealthController } from './health/health.controller';
|
|
|
120
163
|
imports: [
|
|
121
164
|
ConfigModule.forRoot({
|
|
122
165
|
isGlobal: true,
|
|
123
|
-
load: [coreConfig
|
|
124
|
-
validate: createEnvValidator([coreEnvSchema
|
|
166
|
+
load: [coreConfig],
|
|
167
|
+
validate: createEnvValidator([coreEnvSchema]),
|
|
125
168
|
envFilePath: '.env',
|
|
126
169
|
}),
|
|
127
170
|
CoreConfigModule,
|
|
128
171
|
CoreErrorsModule,
|
|
129
|
-
DbPrismaModule,
|
|
130
172
|
],
|
|
131
173
|
controllers: [HealthController],
|
|
132
174
|
})
|
|
@@ -137,27 +179,24 @@ export class AppModule {}
|
|
|
137
179
|
|
|
138
180
|
const healthControllerPath = path.join(
|
|
139
181
|
targetRoot,
|
|
140
|
-
'apps',
|
|
141
|
-
'api',
|
|
142
|
-
'src',
|
|
143
|
-
'health',
|
|
144
|
-
'health.controller.ts',
|
|
145
|
-
);
|
|
182
|
+
'apps',
|
|
183
|
+
'api',
|
|
184
|
+
'src',
|
|
185
|
+
'health',
|
|
186
|
+
'health.controller.ts',
|
|
187
|
+
);
|
|
146
188
|
fs.writeFileSync(
|
|
147
189
|
healthControllerPath,
|
|
148
|
-
`import { BadRequestException, ConflictException, Controller, Get,
|
|
149
|
-
import { PrismaService } from '@forgeon/db-prisma';
|
|
190
|
+
`import { BadRequestException, ConflictException, Controller, Get, Query } from '@nestjs/common';
|
|
150
191
|
|
|
151
192
|
@Controller('health')
|
|
152
193
|
export class HealthController {
|
|
153
|
-
constructor(private readonly prisma: PrismaService) {}
|
|
154
|
-
|
|
155
194
|
@Get()
|
|
156
|
-
getHealth(
|
|
195
|
+
getHealth() {
|
|
157
196
|
return {
|
|
158
197
|
status: 'ok',
|
|
159
198
|
message: 'OK',
|
|
160
|
-
i18n: '
|
|
199
|
+
i18n: 'disabled',
|
|
161
200
|
};
|
|
162
201
|
}
|
|
163
202
|
|
|
@@ -188,22 +227,6 @@ export class HealthController {
|
|
|
188
227
|
value,
|
|
189
228
|
};
|
|
190
229
|
}
|
|
191
|
-
|
|
192
|
-
@Post('db')
|
|
193
|
-
async getDbProbe() {
|
|
194
|
-
const token = \`\${Date.now()}-\${Math.floor(Math.random() * 1_000_000)}\`;
|
|
195
|
-
const email = \`health-probe-\${token}@example.local\`;
|
|
196
|
-
const user = await this.prisma.user.create({
|
|
197
|
-
data: { email },
|
|
198
|
-
select: { id: true, email: true, createdAt: true },
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
return {
|
|
202
|
-
status: 'ok',
|
|
203
|
-
feature: 'db-prisma',
|
|
204
|
-
user,
|
|
205
|
-
};
|
|
206
|
-
}
|
|
207
230
|
}
|
|
208
231
|
`,
|
|
209
232
|
'utf8',
|
package/src/run-add-module.mjs
CHANGED
|
@@ -1,20 +1,88 @@
|
|
|
1
|
-
import path from 'node:path';
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { printAddHelp } from './cli/add-help.mjs';
|
|
6
|
+
import { parseAddCliArgs } from './cli/add-options.mjs';
|
|
7
|
+
import { addModule } from './modules/executor.mjs';
|
|
8
|
+
import { listModulePresets } from './modules/registry.mjs';
|
|
9
|
+
import { writeJson } from './utils/fs.mjs';
|
|
7
10
|
|
|
8
|
-
function printModuleList() {
|
|
11
|
+
function printModuleList() {
|
|
9
12
|
const modules = listModulePresets();
|
|
10
13
|
console.log('Available modules:');
|
|
11
14
|
for (const moduleItem of modules) {
|
|
12
15
|
const status = moduleItem.implemented ? 'implemented' : 'planned';
|
|
13
16
|
console.log(`- ${moduleItem.id} (${status}) - ${moduleItem.description}`);
|
|
14
17
|
}
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function ensureSyncTooling({ packageRoot, targetRoot }) {
|
|
21
|
+
const sourceScript = path.join(
|
|
22
|
+
packageRoot,
|
|
23
|
+
'templates',
|
|
24
|
+
'base',
|
|
25
|
+
'scripts',
|
|
26
|
+
'forgeon-sync-integrations.mjs',
|
|
27
|
+
);
|
|
28
|
+
const targetScript = path.join(targetRoot, 'scripts', 'forgeon-sync-integrations.mjs');
|
|
29
|
+
|
|
30
|
+
if (fs.existsSync(sourceScript) && !fs.existsSync(targetScript)) {
|
|
31
|
+
fs.mkdirSync(path.dirname(targetScript), { recursive: true });
|
|
32
|
+
fs.copyFileSync(sourceScript, targetScript);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const packagePath = path.join(targetRoot, 'package.json');
|
|
36
|
+
if (!fs.existsSync(packagePath)) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
|
|
41
|
+
if (!packageJson.scripts) {
|
|
42
|
+
packageJson.scripts = {};
|
|
43
|
+
}
|
|
44
|
+
if (!packageJson.devDependencies) {
|
|
45
|
+
packageJson.devDependencies = {};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
packageJson.scripts['forgeon:sync-integrations'] = 'node scripts/forgeon-sync-integrations.mjs';
|
|
49
|
+
if (!packageJson.devDependencies['ts-morph']) {
|
|
50
|
+
packageJson.devDependencies['ts-morph'] = '^24.0.0';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
writeJson(packagePath, packageJson);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function runIntegrationSync(targetRoot) {
|
|
57
|
+
const scriptPath = path.join(targetRoot, 'scripts', 'forgeon-sync-integrations.mjs');
|
|
58
|
+
if (!fs.existsSync(scriptPath)) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const tsMorphPackagePath = path.join(targetRoot, 'node_modules', 'ts-morph', 'package.json');
|
|
63
|
+
if (!fs.existsSync(tsMorphPackagePath)) {
|
|
64
|
+
console.warn(
|
|
65
|
+
'[create-forgeon add] sync-integrations skipped (dependencies are not installed yet). ' +
|
|
66
|
+
'Run `pnpm install` then `pnpm forgeon:sync-integrations` inside the project.',
|
|
67
|
+
);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const result = spawnSync(process.execPath, [scriptPath], {
|
|
72
|
+
cwd: targetRoot,
|
|
73
|
+
stdio: 'inherit',
|
|
74
|
+
env: process.env,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
if (result.status !== 0) {
|
|
78
|
+
console.warn(
|
|
79
|
+
'[create-forgeon add] sync-integrations failed. ' +
|
|
80
|
+
'Run `pnpm install` then `pnpm forgeon:sync-integrations` inside the project.',
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function runAddModule(argv = process.argv.slice(2)) {
|
|
18
86
|
const options = parseAddCliArgs(argv);
|
|
19
87
|
|
|
20
88
|
if (options.help) {
|
|
@@ -35,13 +103,15 @@ export async function runAddModule(argv = process.argv.slice(2)) {
|
|
|
35
103
|
const packageRoot = path.resolve(srcDir, '..');
|
|
36
104
|
const targetRoot = path.resolve(process.cwd(), options.project);
|
|
37
105
|
|
|
38
|
-
const result = addModule({
|
|
39
|
-
moduleId: options.moduleId,
|
|
40
|
-
targetRoot,
|
|
41
|
-
packageRoot,
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
|
|
106
|
+
const result = addModule({
|
|
107
|
+
moduleId: options.moduleId,
|
|
108
|
+
targetRoot,
|
|
109
|
+
packageRoot,
|
|
110
|
+
});
|
|
111
|
+
ensureSyncTooling({ packageRoot, targetRoot });
|
|
112
|
+
runIntegrationSync(targetRoot);
|
|
113
|
+
|
|
114
|
+
console.log(result.message);
|
|
45
115
|
console.log(`- module: ${result.preset.id}`);
|
|
46
116
|
console.log(`- docs: ${result.docsPath}`);
|
|
47
117
|
}
|
|
@@ -1,13 +1,19 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { fileURLToPath } from 'node:url';
|
|
4
|
-
import { printHelp } from './cli/help.mjs';
|
|
5
|
-
import { parseCliArgs, promptForMissingOptions } from './cli/options.mjs';
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { printHelp } from './cli/help.mjs';
|
|
5
|
+
import { parseCliArgs, promptForMissingOptions } from './cli/options.mjs';
|
|
6
|
+
import {
|
|
7
|
+
DEFAULT_DB,
|
|
8
|
+
DEFAULT_DB_PRISMA_ENABLED,
|
|
9
|
+
DEFAULT_FRONTEND,
|
|
10
|
+
DEFAULT_PROXY,
|
|
11
|
+
FIXED_DOCKER_ENABLED,
|
|
12
|
+
} from './constants.mjs';
|
|
13
|
+
import { runInstall } from './core/install.mjs';
|
|
14
|
+
import { scaffoldProject } from './core/scaffold.mjs';
|
|
15
|
+
import { validatePresetSupport } from './core/validate.mjs';
|
|
16
|
+
import { parseBoolean } from './utils/values.mjs';
|
|
11
17
|
|
|
12
18
|
export async function runCreateForgeon(argv = process.argv.slice(2)) {
|
|
13
19
|
const { options: parsedOptions, positional } = parseCliArgs(argv);
|
|
@@ -28,11 +34,12 @@ export async function runCreateForgeon(argv = process.argv.slice(2)) {
|
|
|
28
34
|
throw new Error('Project name is required.');
|
|
29
35
|
}
|
|
30
36
|
|
|
31
|
-
const frontend = (promptedOptions.frontend ?? DEFAULT_FRONTEND).toString().toLowerCase();
|
|
32
|
-
const db = (promptedOptions.db ?? DEFAULT_DB).toString().toLowerCase();
|
|
33
|
-
const
|
|
34
|
-
const
|
|
35
|
-
const
|
|
37
|
+
const frontend = (promptedOptions.frontend ?? DEFAULT_FRONTEND).toString().toLowerCase();
|
|
38
|
+
const db = (promptedOptions.db ?? DEFAULT_DB).toString().toLowerCase();
|
|
39
|
+
const dbPrismaEnabled = parseBoolean(promptedOptions.dbPrisma, DEFAULT_DB_PRISMA_ENABLED);
|
|
40
|
+
const i18nEnabled = parseBoolean(promptedOptions.i18n, true);
|
|
41
|
+
const dockerEnabled = FIXED_DOCKER_ENABLED;
|
|
42
|
+
const proxy = (promptedOptions.proxy ?? DEFAULT_PROXY).toString().toLowerCase();
|
|
36
43
|
const installEnabled = parseBoolean(promptedOptions.install, false);
|
|
37
44
|
|
|
38
45
|
validatePresetSupport({ frontend, db, dockerEnabled, proxy });
|
|
@@ -51,12 +58,13 @@ export async function runCreateForgeon(argv = process.argv.slice(2)) {
|
|
|
51
58
|
templateRoot,
|
|
52
59
|
packageRoot,
|
|
53
60
|
targetRoot,
|
|
54
|
-
projectName,
|
|
55
|
-
frontend,
|
|
56
|
-
db,
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
61
|
+
projectName,
|
|
62
|
+
frontend,
|
|
63
|
+
db,
|
|
64
|
+
dbPrismaEnabled,
|
|
65
|
+
i18nEnabled,
|
|
66
|
+
proxy,
|
|
67
|
+
});
|
|
60
68
|
|
|
61
69
|
if (installEnabled) {
|
|
62
70
|
runInstall(targetRoot);
|
|
@@ -64,9 +72,10 @@ export async function runCreateForgeon(argv = process.argv.slice(2)) {
|
|
|
64
72
|
|
|
65
73
|
console.log('Forgeon scaffold generated.');
|
|
66
74
|
console.log(`- path: ${targetRoot}`);
|
|
67
|
-
console.log(`- frontend: ${frontend}`);
|
|
68
|
-
console.log(`- db: ${db}`);
|
|
69
|
-
console.log(`-
|
|
75
|
+
console.log(`- frontend: ${frontend}`);
|
|
76
|
+
console.log(`- db: ${dbPrismaEnabled ? db : 'none'}`);
|
|
77
|
+
console.log(`- db-prisma: ${dbPrismaEnabled}`);
|
|
78
|
+
console.log(`- i18n: ${i18nEnabled}`);
|
|
70
79
|
console.log(`- docker: ${dockerEnabled}`);
|
|
71
80
|
console.log(`- proxy: ${proxy}`);
|
|
72
81
|
}
|
package/templates/base/README.md
CHANGED
|
@@ -20,14 +20,27 @@ Canonical monorepo scaffold for NestJS + frontend with shared packages, built-in
|
|
|
20
20
|
- Web: `http://localhost:5173`
|
|
21
21
|
- API health: `http://localhost:3000/api/health`
|
|
22
22
|
|
|
23
|
-
## Quick Start (Docker)
|
|
23
|
+
## Quick Start (Docker)
|
|
24
24
|
|
|
25
25
|
```bash
|
|
26
26
|
docker compose --env-file infra/docker/.env.example -f infra/docker/compose.yml up --build
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
-
Open `http://localhost:8080`.
|
|
30
|
-
|
|
29
|
+
Open `http://localhost:8080`.
|
|
30
|
+
|
|
31
|
+
## Integration Sync
|
|
32
|
+
|
|
33
|
+
Use integration sync to reconcile module cross-wiring when modules are installed in any order.
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pnpm forgeon:sync-integrations
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Current sync coverage:
|
|
40
|
+
- `jwt-auth + swagger`: adds OpenAPI decorators for auth controller/DTOs.
|
|
41
|
+
|
|
42
|
+
`create-forgeon add <module>` also runs integration sync automatically (best effort).
|
|
43
|
+
|
|
31
44
|
## i18n Configuration
|
|
32
45
|
|
|
33
46
|
Set in env (when i18n module is installed):
|
|
@@ -5,24 +5,19 @@ RUN corepack enable
|
|
|
5
5
|
|
|
6
6
|
COPY package.json pnpm-workspace.yaml tsconfig.base.json tsconfig.base.node.json tsconfig.base.esm.json ./
|
|
7
7
|
COPY apps/api/package.json apps/api/package.json
|
|
8
|
-
COPY apps/api/prisma apps/api/prisma
|
|
9
8
|
COPY packages/core/package.json packages/core/package.json
|
|
10
|
-
COPY packages/db-prisma/package.json packages/db-prisma/package.json
|
|
11
9
|
COPY packages/i18n/package.json packages/i18n/package.json
|
|
12
|
-
|
|
13
|
-
RUN pnpm install --frozen-lockfile=false
|
|
14
|
-
|
|
10
|
+
|
|
11
|
+
RUN pnpm install --frozen-lockfile=false
|
|
12
|
+
|
|
15
13
|
COPY apps/api apps/api
|
|
16
14
|
COPY packages/core packages/core
|
|
17
|
-
COPY packages/db-prisma packages/db-prisma
|
|
18
15
|
COPY packages/i18n packages/i18n
|
|
19
16
|
COPY resources resources
|
|
20
17
|
|
|
21
18
|
RUN pnpm --filter @forgeon/core build
|
|
22
|
-
RUN pnpm --filter @forgeon/db-prisma build
|
|
23
19
|
RUN pnpm --filter @forgeon/i18n build
|
|
24
|
-
RUN pnpm --filter @forgeon/api prisma:generate
|
|
25
20
|
RUN pnpm --filter @forgeon/api build
|
|
26
|
-
|
|
27
|
-
EXPOSE 3000
|
|
28
|
-
CMD ["
|
|
21
|
+
|
|
22
|
+
EXPOSE 3000
|
|
23
|
+
CMD ["node", "apps/api/dist/main.js"]
|
|
@@ -3,40 +3,29 @@
|
|
|
3
3
|
"version": "0.1.0",
|
|
4
4
|
"private": true,
|
|
5
5
|
"scripts": {
|
|
6
|
-
"predev": "pnpm --filter @forgeon/core build && pnpm --filter @forgeon/
|
|
7
|
-
"build": "tsc -p tsconfig.build.json",
|
|
8
|
-
"dev": "ts-node --transpile-only src/main.ts",
|
|
9
|
-
"start": "node dist/main.js"
|
|
10
|
-
|
|
11
|
-
"prisma:migrate:dev": "prisma migrate dev --schema prisma/schema.prisma",
|
|
12
|
-
"prisma:migrate:deploy": "prisma migrate deploy --schema prisma/schema.prisma",
|
|
13
|
-
"prisma:studio": "prisma studio --schema prisma/schema.prisma",
|
|
14
|
-
"prisma:seed": "ts-node --transpile-only prisma/seed.ts"
|
|
15
|
-
},
|
|
6
|
+
"predev": "pnpm --filter @forgeon/core build && pnpm --filter @forgeon/i18n build",
|
|
7
|
+
"build": "tsc -p tsconfig.build.json",
|
|
8
|
+
"dev": "ts-node --transpile-only src/main.ts",
|
|
9
|
+
"start": "node dist/main.js"
|
|
10
|
+
},
|
|
16
11
|
"dependencies": {
|
|
17
|
-
"@forgeon/db-prisma": "workspace:*",
|
|
18
12
|
"@forgeon/core": "workspace:*",
|
|
19
13
|
"@forgeon/i18n": "workspace:*",
|
|
20
14
|
"@nestjs/common": "^11.0.1",
|
|
21
15
|
"@nestjs/config": "^4.0.2",
|
|
22
16
|
"@nestjs/core": "^11.0.1",
|
|
23
|
-
"@nestjs/platform-express": "^11.0.1",
|
|
24
|
-
"@prisma/client": "^6.18.0",
|
|
17
|
+
"@nestjs/platform-express": "^11.0.1",
|
|
25
18
|
"class-transformer": "^0.5.1",
|
|
26
19
|
"class-validator": "^0.14.1",
|
|
27
20
|
"nestjs-i18n": "^10.5.1",
|
|
28
21
|
"reflect-metadata": "^0.2.2",
|
|
29
22
|
"rxjs": "^7.8.1"
|
|
30
23
|
},
|
|
31
|
-
"devDependencies": {
|
|
32
|
-
"@types/express": "^5.0.0",
|
|
33
|
-
"@types/node": "^22.10.7",
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
"prisma": {
|
|
39
|
-
"seed": "ts-node --transpile-only prisma/seed.ts"
|
|
40
|
-
}
|
|
41
|
-
}
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@types/express": "^5.0.0",
|
|
26
|
+
"@types/node": "^22.10.7",
|
|
27
|
+
"ts-node": "^10.9.2",
|
|
28
|
+
"typescript": "^5.7.3"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
42
31
|
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { Module } from '@nestjs/common';
|
|
2
2
|
import { ConfigModule } from '@nestjs/config';
|
|
3
|
-
import { dbPrismaConfig, dbPrismaEnvSchema, DbPrismaModule } from '@forgeon/db-prisma';
|
|
4
3
|
import {
|
|
5
4
|
CoreConfigModule,
|
|
6
5
|
CoreErrorsModule,
|
|
@@ -15,16 +14,15 @@ import { HealthController } from './health/health.controller';
|
|
|
15
14
|
const i18nPath = join(__dirname, '..', '..', '..', 'resources', 'i18n');
|
|
16
15
|
|
|
17
16
|
@Module({
|
|
18
|
-
imports: [
|
|
17
|
+
imports: [
|
|
19
18
|
ConfigModule.forRoot({
|
|
20
19
|
isGlobal: true,
|
|
21
|
-
load: [coreConfig,
|
|
22
|
-
validate: createEnvValidator([coreEnvSchema,
|
|
20
|
+
load: [coreConfig, i18nConfig],
|
|
21
|
+
validate: createEnvValidator([coreEnvSchema, i18nEnvSchema]),
|
|
23
22
|
envFilePath: '.env',
|
|
24
23
|
}),
|
|
25
24
|
CoreConfigModule,
|
|
26
25
|
CoreErrorsModule,
|
|
27
|
-
DbPrismaModule,
|
|
28
26
|
ForgeonI18nModule.register({
|
|
29
27
|
path: i18nPath,
|
|
30
28
|
}),
|
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
import { BadRequestException, ConflictException, Controller, Get,
|
|
2
|
-
import { PrismaService } from '@forgeon/db-prisma';
|
|
1
|
+
import { BadRequestException, ConflictException, Controller, Get, Query } from '@nestjs/common';
|
|
3
2
|
import { I18nService } from 'nestjs-i18n';
|
|
4
3
|
|
|
5
4
|
@Controller('health')
|
|
6
5
|
export class HealthController {
|
|
7
6
|
constructor(
|
|
8
|
-
private readonly prisma: PrismaService,
|
|
9
7
|
private readonly i18n: I18nService,
|
|
10
8
|
) {}
|
|
11
9
|
|
|
@@ -47,22 +45,6 @@ export class HealthController {
|
|
|
47
45
|
};
|
|
48
46
|
}
|
|
49
47
|
|
|
50
|
-
@Post('db')
|
|
51
|
-
async getDbProbe() {
|
|
52
|
-
const token = `${Date.now()}-${Math.floor(Math.random() * 1_000_000)}`;
|
|
53
|
-
const email = `health-probe-${token}@example.local`;
|
|
54
|
-
const user = await this.prisma.user.create({
|
|
55
|
-
data: { email },
|
|
56
|
-
select: { id: true, email: true, createdAt: true },
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
return {
|
|
60
|
-
status: 'ok',
|
|
61
|
-
feature: 'db-prisma',
|
|
62
|
-
user,
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
|
|
66
48
|
private translate(key: string, lang?: string): string {
|
|
67
49
|
const value = this.i18n.t(key, { lang, defaultValue: key });
|
|
68
50
|
return typeof value === 'string' ? value : key;
|
|
@@ -10,7 +10,6 @@ export default function App() {
|
|
|
10
10
|
const [healthResult, setHealthResult] = useState<ProbeResult | null>(null);
|
|
11
11
|
const [errorProbeResult, setErrorProbeResult] = useState<ProbeResult | null>(null);
|
|
12
12
|
const [validationProbeResult, setValidationProbeResult] = useState<ProbeResult | null>(null);
|
|
13
|
-
const [dbProbeResult, setDbProbeResult] = useState<ProbeResult | null>(null);
|
|
14
13
|
const [networkError, setNetworkError] = useState<string | null>(null);
|
|
15
14
|
|
|
16
15
|
const requestProbe = async (url: string, init?: RequestInit): Promise<ProbeResult> => {
|
|
@@ -62,14 +61,10 @@ export default function App() {
|
|
|
62
61
|
<button onClick={() => runProbe(setValidationProbeResult, '/api/health/validation')}>
|
|
63
62
|
Check validation (expect 400)
|
|
64
63
|
</button>
|
|
65
|
-
<button onClick={() => runProbe(setDbProbeResult, '/api/health/db', { method: 'POST' })}>
|
|
66
|
-
Check database (create user)
|
|
67
|
-
</button>
|
|
68
64
|
</div>
|
|
69
65
|
{renderResult('Health response', healthResult)}
|
|
70
66
|
{renderResult('Error probe response', errorProbeResult)}
|
|
71
67
|
{renderResult('Validation probe response', validationProbeResult)}
|
|
72
|
-
{renderResult('DB probe response', dbProbeResult)}
|
|
73
68
|
{networkError ? <p className="error">{networkError}</p> : null}
|
|
74
69
|
</main>
|
|
75
70
|
);
|
|
@@ -14,7 +14,7 @@ If a module can be validated through a safe API call, it must provide:
|
|
|
14
14
|
|
|
15
15
|
- `core-errors`: `GET /api/health/error` (returns error envelope, expected `409`)
|
|
16
16
|
- `core-validation`: `GET /api/health/validation` without `value` (expected `400`)
|
|
17
|
-
- `db-prisma
|
|
17
|
+
- `db-prisma` (when installed): `POST /api/health/db` (creates probe user and returns it, expected `201`)
|
|
18
18
|
|
|
19
19
|
## Rules For Future Modules
|
|
20
20
|
|
|
@@ -4,7 +4,7 @@ This is a living plan. Scope and priorities may change.
|
|
|
4
4
|
|
|
5
5
|
## Current Foundation (Implemented)
|
|
6
6
|
|
|
7
|
-
- [x] Canonical scaffold: NestJS API + React web +
|
|
7
|
+
- [x] Canonical scaffold: NestJS API + React web + Docker (+ default-on `db-prisma` module)
|
|
8
8
|
- [x] Proxy preset selection: `caddy | nginx | none`
|
|
9
9
|
- [x] `@forgeon/core`:
|
|
10
10
|
- [x] `core-config` (typed env config + validation)
|
|
@@ -1,45 +1,21 @@
|
|
|
1
|
-
services:
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
|
8
|
-
POSTGRES_DB: ${POSTGRES_DB}
|
|
9
|
-
ports:
|
|
10
|
-
- "5432:5432"
|
|
11
|
-
volumes:
|
|
12
|
-
- db_data:/var/lib/postgresql/data
|
|
13
|
-
healthcheck:
|
|
14
|
-
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"]
|
|
15
|
-
interval: 10s
|
|
16
|
-
timeout: 5s
|
|
17
|
-
retries: 10
|
|
18
|
-
|
|
19
|
-
api:
|
|
20
|
-
build:
|
|
21
|
-
context: ../..
|
|
22
|
-
dockerfile: apps/api/Dockerfile
|
|
23
|
-
restart: unless-stopped
|
|
1
|
+
services:
|
|
2
|
+
api:
|
|
3
|
+
build:
|
|
4
|
+
context: ../..
|
|
5
|
+
dockerfile: apps/api/Dockerfile
|
|
6
|
+
restart: unless-stopped
|
|
24
7
|
environment:
|
|
25
8
|
PORT: ${PORT}
|
|
26
9
|
API_PREFIX: ${API_PREFIX}
|
|
27
|
-
DATABASE_URL: ${DATABASE_URL}
|
|
28
10
|
I18N_DEFAULT_LANG: ${I18N_DEFAULT_LANG}
|
|
29
11
|
I18N_FALLBACK_LANG: ${I18N_FALLBACK_LANG}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
condition: service_healthy
|
|
33
|
-
|
|
34
|
-
caddy:
|
|
12
|
+
|
|
13
|
+
caddy:
|
|
35
14
|
build:
|
|
36
15
|
context: ../..
|
|
37
16
|
dockerfile: infra/docker/caddy.Dockerfile
|
|
38
|
-
restart: unless-stopped
|
|
39
|
-
depends_on:
|
|
40
|
-
- api
|
|
41
|
-
ports:
|
|
42
|
-
- "8080:80"
|
|
43
|
-
|
|
44
|
-
volumes:
|
|
45
|
-
db_data:
|
|
17
|
+
restart: unless-stopped
|
|
18
|
+
depends_on:
|
|
19
|
+
- api
|
|
20
|
+
ports:
|
|
21
|
+
- "8080:80"
|