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.
Files changed (55) hide show
  1. package/README.md +12 -4
  2. package/package.json +1 -1
  3. package/src/cli/help.mjs +3 -2
  4. package/src/cli/options.mjs +142 -121
  5. package/src/cli/options.test.mjs +13 -10
  6. package/src/constants.mjs +11 -9
  7. package/src/core/docs.mjs +44 -23
  8. package/src/core/docs.test.mjs +21 -15
  9. package/src/core/scaffold.mjs +27 -15
  10. package/src/modules/db-prisma.mjs +134 -32
  11. package/src/modules/executor.test.mjs +44 -13
  12. package/src/modules/i18n.mjs +11 -28
  13. package/src/modules/logger.mjs +4 -1
  14. package/src/modules/swagger.mjs +7 -2
  15. package/src/presets/i18n.mjs +63 -40
  16. package/src/run-add-module.mjs +87 -17
  17. package/src/run-create-forgeon.mjs +33 -24
  18. package/templates/base/README.md +16 -3
  19. package/templates/base/apps/api/Dockerfile +6 -11
  20. package/templates/base/apps/api/package.json +13 -24
  21. package/templates/base/apps/api/src/app.module.ts +3 -5
  22. package/templates/base/apps/api/src/health/health.controller.ts +1 -19
  23. package/templates/base/apps/web/src/App.tsx +0 -5
  24. package/templates/base/docs/AI/MODULE_CHECKS.md +1 -1
  25. package/templates/base/docs/AI/ROADMAP.md +1 -1
  26. package/templates/base/infra/docker/.env.example +1 -6
  27. package/templates/base/infra/docker/compose.caddy.yml +13 -37
  28. package/templates/base/infra/docker/compose.nginx.yml +13 -37
  29. package/templates/base/infra/docker/compose.none.yml +8 -32
  30. package/templates/base/infra/docker/compose.yml +16 -40
  31. package/templates/base/package.json +12 -9
  32. package/templates/base/scripts/forgeon-sync-integrations.mjs +399 -0
  33. package/templates/docs-fragments/AI_ARCHITECTURE/20_env_base.md +0 -1
  34. package/templates/docs-fragments/AI_ARCHITECTURE/20b_env_db_prisma.md +1 -0
  35. package/templates/docs-fragments/AI_ARCHITECTURE/30_default_db.md +2 -2
  36. package/templates/docs-fragments/AI_ARCHITECTURE/30_default_db_none.md +7 -0
  37. package/templates/docs-fragments/AI_PROJECT/20_structure_base.md +0 -1
  38. package/templates/docs-fragments/AI_PROJECT/20b_structure_db_none.md +2 -0
  39. package/templates/docs-fragments/AI_PROJECT/20b_structure_db_prisma.md +1 -0
  40. package/templates/docs-fragments/README/10_stack.md +4 -4
  41. package/templates/docs-fragments/README/21_quick_start_dev_no_db.md +6 -0
  42. package/templates/module-presets/i18n/apps/web/src/App.tsx +0 -5
  43. /package/templates/{base → module-presets/db-prisma}/apps/api/prisma/migrations/0001_init/migration.sql +0 -0
  44. /package/templates/{base → module-presets/db-prisma}/apps/api/prisma/migrations/migration_lock.toml +0 -0
  45. /package/templates/{base → module-presets/db-prisma}/apps/api/prisma/schema.prisma +0 -0
  46. /package/templates/{base → module-presets/db-prisma}/apps/api/prisma/seed.ts +0 -0
  47. /package/templates/{base → module-presets/db-prisma}/packages/db-prisma/README.md +0 -0
  48. /package/templates/{base → module-presets/db-prisma}/packages/db-prisma/package.json +0 -0
  49. /package/templates/{base → module-presets/db-prisma}/packages/db-prisma/src/db-prisma-config.loader.ts +0 -0
  50. /package/templates/{base → module-presets/db-prisma}/packages/db-prisma/src/db-prisma-config.service.ts +0 -0
  51. /package/templates/{base → module-presets/db-prisma}/packages/db-prisma/src/db-prisma-env.schema.ts +0 -0
  52. /package/templates/{base → module-presets/db-prisma}/packages/db-prisma/src/db-prisma.module.ts +0 -0
  53. /package/templates/{base → module-presets/db-prisma}/packages/db-prisma/src/index.ts +0 -0
  54. /package/templates/{base → module-presets/db-prisma}/packages/db-prisma/src/prisma.service.ts +0 -0
  55. /package/templates/{base → module-presets/db-prisma}/packages/db-prisma/tsconfig.json +0 -0
@@ -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
- const apiPackagePath = path.join(targetRoot, 'apps', 'api', 'package.json');
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
- 'pnpm --filter @forgeon/core build && pnpm --filter @forgeon/db-prisma build';
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
- delete webPackage.scripts.predev;
74
- delete webPackage.scripts.prebuild;
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, dbPrismaConfig],
124
- validate: createEnvValidator([coreEnvSchema, dbPrismaEnvSchema]),
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, Post, Query } from '@nestjs/common';
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(@Query('lang') _lang?: string) {
195
+ getHealth() {
157
196
  return {
158
197
  status: 'ok',
159
198
  message: 'OK',
160
- i18n: 'English',
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',
@@ -1,20 +1,88 @@
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';
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
- export async function runAddModule(argv = process.argv.slice(2)) {
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
- console.log(result.message);
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 { DEFAULT_DB, DEFAULT_FRONTEND, DEFAULT_PROXY, FIXED_DOCKER_ENABLED } from './constants.mjs';
7
- import { runInstall } from './core/install.mjs';
8
- import { scaffoldProject } from './core/scaffold.mjs';
9
- import { validatePresetSupport } from './core/validate.mjs';
10
- import { parseBoolean } from './utils/values.mjs';
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 i18nEnabled = parseBoolean(promptedOptions.i18n, true);
34
- const dockerEnabled = FIXED_DOCKER_ENABLED;
35
- const proxy = (promptedOptions.proxy ?? DEFAULT_PROXY).toString().toLowerCase();
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
- i18nEnabled,
58
- proxy,
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(`- i18n: ${i18nEnabled}`);
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
  }
@@ -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 ["sh", "-c", "pnpm --filter @forgeon/api prisma:migrate:deploy && node apps/api/dist/main.js"]
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/db-prisma 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
- "prisma:generate": "prisma generate --schema prisma/schema.prisma",
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
- "prisma": "^6.18.0",
35
- "ts-node": "^10.9.2",
36
- "typescript": "^5.7.3"
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, dbPrismaConfig, i18nConfig],
22
- validate: createEnvValidator([coreEnvSchema, dbPrismaEnvSchema, i18nEnvSchema]),
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, Post, Query } from '@nestjs/common';
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`: `POST /api/health/db` (creates probe user and returns it, expected `201`)
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 + Prisma/Postgres + Docker
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,10 +1,5 @@
1
- POSTGRES_USER=postgres
2
- POSTGRES_PASSWORD=postgres
3
- POSTGRES_DB=app
4
-
5
1
  PORT=3000
6
2
  API_PREFIX=api
7
- DATABASE_URL=postgresql://postgres:postgres@db:5432/app?schema=public
8
-
3
+
9
4
  I18N_DEFAULT_LANG=en
10
5
  I18N_FALLBACK_LANG=en
@@ -1,45 +1,21 @@
1
- services:
2
- db:
3
- image: postgres:16-alpine
4
- restart: unless-stopped
5
- environment:
6
- POSTGRES_USER: ${POSTGRES_USER}
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
- depends_on:
31
- db:
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"