create-forgeon 0.1.24 → 0.1.26

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 (41) hide show
  1. package/package.json +1 -1
  2. package/src/core/docs.test.mjs +8 -6
  3. package/src/modules/executor.test.mjs +8 -0
  4. package/src/modules/i18n.mjs +19 -1
  5. package/src/presets/i18n.mjs +76 -34
  6. package/templates/base/apps/api/Dockerfile +7 -4
  7. package/templates/base/apps/api/package.json +3 -2
  8. package/templates/base/apps/api/src/app.module.ts +4 -4
  9. package/templates/base/apps/api/src/health/health.controller.ts +41 -8
  10. package/templates/base/apps/api/src/main.ts +6 -8
  11. package/templates/base/apps/web/src/App.tsx +76 -35
  12. package/templates/base/apps/web/src/styles.css +29 -17
  13. package/templates/base/docs/AI/ARCHITECTURE.md +48 -32
  14. package/templates/base/docs/AI/MODULE_CHECKS.md +25 -0
  15. package/templates/base/docs/AI/MODULE_SPEC.md +1 -0
  16. package/templates/base/docs/AI/PROJECT.md +27 -16
  17. package/templates/base/docs/AI/TASKS.md +8 -7
  18. package/templates/base/docs/AI/VALIDATION.md +6 -1
  19. package/templates/base/docs/README.md +1 -0
  20. package/templates/base/packages/core/README.md +1 -0
  21. package/templates/base/packages/core/src/errors/core-exception.filter.ts +11 -3
  22. package/templates/base/packages/core/src/index.ts +1 -0
  23. package/templates/base/packages/core/src/validation/core-validation.pipe.ts +56 -0
  24. package/templates/base/packages/core/src/validation/index.ts +1 -0
  25. package/templates/base/packages/db-prisma/README.md +9 -0
  26. package/templates/base/packages/db-prisma/package.json +20 -0
  27. package/templates/base/packages/db-prisma/src/db-prisma-config.loader.ts +18 -0
  28. package/templates/base/packages/db-prisma/src/db-prisma-config.service.ts +12 -0
  29. package/templates/base/packages/db-prisma/src/db-prisma-env.schema.ts +17 -0
  30. package/templates/base/packages/db-prisma/src/db-prisma.module.ts +13 -0
  31. package/templates/base/packages/db-prisma/src/index.ts +5 -0
  32. package/templates/base/{apps/api/src/prisma → packages/db-prisma/src}/prisma.service.ts +24 -27
  33. package/templates/base/packages/db-prisma/tsconfig.json +9 -0
  34. package/templates/base/resources/i18n/en/common.json +3 -0
  35. package/templates/base/resources/i18n/uk/common.json +3 -0
  36. package/templates/docs-fragments/AI_ARCHITECTURE/20_env_base.md +1 -1
  37. package/templates/docs-fragments/AI_ARCHITECTURE/30_default_db.md +9 -7
  38. package/templates/docs-fragments/AI_ARCHITECTURE/32_scope_freeze.md +6 -5
  39. package/templates/docs-fragments/AI_PROJECT/20_structure_base.md +2 -1
  40. package/templates/module-presets/i18n/apps/web/src/App.tsx +63 -22
  41. package/templates/base/apps/api/src/prisma/prisma.module.ts +0 -9
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-forgeon",
3
- "version": "0.1.24",
3
+ "version": "0.1.26",
4
4
  "description": "Forgeon project generator CLI",
5
5
  "license": "MIT",
6
6
  "author": "Forgeon",
@@ -55,9 +55,10 @@ describe('generateDocs', () => {
55
55
  assert.match(architectureDoc, /Config Strategy/);
56
56
  assert.match(architectureDoc, /TypeScript Module Policy/);
57
57
  assert.match(architectureDoc, /tsconfig\.base\.esm\.json/);
58
- } finally {
59
- fs.rmSync(targetRoot, { recursive: true, force: true });
60
- }
58
+ assert.match(architectureDoc, /DbPrismaModule/);
59
+ } finally {
60
+ fs.rmSync(targetRoot, { recursive: true, force: true });
61
+ }
61
62
  });
62
63
 
63
64
  it('generates docker and caddy notes when enabled', () => {
@@ -95,8 +96,9 @@ describe('generateDocs', () => {
95
96
  assert.match(architectureDoc, /Active reverse proxy preset: `caddy`/);
96
97
  assert.match(architectureDoc, /TypeScript Module Policy/);
97
98
  assert.match(architectureDoc, /tsconfig\.base\.node\.json/);
98
- } finally {
99
- fs.rmSync(targetRoot, { recursive: true, force: true });
100
- }
99
+ assert.match(architectureDoc, /DbPrismaModule/);
100
+ } finally {
101
+ fs.rmSync(targetRoot, { recursive: true, force: true });
102
+ }
101
103
  });
102
104
  });
@@ -98,6 +98,7 @@ describe('addModule', () => {
98
98
  assert.equal(fs.existsSync(path.join(projectRoot, 'tsconfig.base.esm.json')), true);
99
99
 
100
100
  const apiPackage = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'package.json'), 'utf8');
101
+ assert.match(apiPackage, /@forgeon\/db-prisma/);
101
102
  assert.match(apiPackage, /@forgeon\/i18n/);
102
103
  assert.match(apiPackage, /@forgeon\/i18n-contracts/);
103
104
 
@@ -110,15 +111,20 @@ describe('addModule', () => {
110
111
 
111
112
  const appModule = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'src', 'app.module.ts'), 'utf8');
112
113
  assert.match(appModule, /coreConfig/);
114
+ assert.match(appModule, /dbPrismaConfig/);
115
+ assert.match(appModule, /dbPrismaEnvSchema/);
113
116
  assert.match(appModule, /createEnvValidator/);
114
117
  assert.match(appModule, /coreEnvSchema/);
115
118
  assert.match(appModule, /i18nConfig/);
116
119
  assert.match(appModule, /i18nEnvSchema/);
117
120
  assert.match(appModule, /CoreConfigModule/);
118
121
  assert.match(appModule, /CoreErrorsModule/);
122
+ assert.match(appModule, /DbPrismaModule/);
119
123
 
120
124
  const mainTs = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'src', 'main.ts'), 'utf8');
121
125
  assert.match(mainTs, /CoreExceptionFilter/);
126
+ assert.match(mainTs, /createValidationPipe/);
127
+ assert.doesNotMatch(mainTs, /new ValidationPipe\(/);
122
128
 
123
129
  const forgeonI18nModule = fs.readFileSync(
124
130
  path.join(projectRoot, 'packages', 'i18n', 'src', 'forgeon-i18n.module.ts'),
@@ -225,6 +231,8 @@ describe('addModule', () => {
225
231
  'utf8',
226
232
  );
227
233
  assert.match(apiDockerfile, /RUN pnpm --filter @forgeon\/core build/);
234
+ assert.match(apiDockerfile, /RUN pnpm --filter @forgeon\/db-prisma build/);
235
+ assert.match(apiDockerfile, /COPY packages\/db-prisma\/package\.json packages\/db-prisma\/package\.json/);
228
236
  } finally {
229
237
  fs.rmSync(targetRoot, { recursive: true, force: true });
230
238
  }
@@ -102,6 +102,11 @@ function patchApiDockerfile(targetRoot) {
102
102
  content = ensureLineAfter(
103
103
  content,
104
104
  'COPY packages/core/package.json packages/core/package.json',
105
+ 'COPY packages/db-prisma/package.json packages/db-prisma/package.json',
106
+ );
107
+ content = ensureLineAfter(
108
+ content,
109
+ 'COPY packages/db-prisma/package.json packages/db-prisma/package.json',
105
110
  'COPY packages/i18n-contracts/package.json packages/i18n-contracts/package.json',
106
111
  );
107
112
  content = ensureLineAfter(
@@ -112,6 +117,11 @@ function patchApiDockerfile(targetRoot) {
112
117
  content = ensureLineAfter(
113
118
  content,
114
119
  'COPY packages/core packages/core',
120
+ 'COPY packages/db-prisma packages/db-prisma',
121
+ );
122
+ content = ensureLineAfter(
123
+ content,
124
+ 'COPY packages/db-prisma packages/db-prisma',
115
125
  'COPY packages/i18n-contracts packages/i18n-contracts',
116
126
  );
117
127
  content = ensureLineAfter(
@@ -122,6 +132,7 @@ function patchApiDockerfile(targetRoot) {
122
132
 
123
133
  content = content
124
134
  .replace(/^RUN pnpm --filter @forgeon\/core build\r?\n?/gm, '')
135
+ .replace(/^RUN pnpm --filter @forgeon\/db-prisma build\r?\n?/gm, '')
125
136
  .replace(/^RUN pnpm --filter @forgeon\/i18n-contracts build\r?\n?/gm, '')
126
137
  .replace(/^RUN pnpm --filter @forgeon\/i18n build\r?\n?/gm, '');
127
138
 
@@ -130,6 +141,11 @@ function patchApiDockerfile(targetRoot) {
130
141
  'RUN pnpm --filter @forgeon/api prisma:generate',
131
142
  'RUN pnpm --filter @forgeon/core build',
132
143
  );
144
+ content = ensureLineBefore(
145
+ content,
146
+ 'RUN pnpm --filter @forgeon/api prisma:generate',
147
+ 'RUN pnpm --filter @forgeon/db-prisma build',
148
+ );
133
149
  content = ensureLineBefore(
134
150
  content,
135
151
  'RUN pnpm --filter @forgeon/api prisma:generate',
@@ -229,10 +245,11 @@ function patchApiPackage(targetRoot) {
229
245
  ensureScript(
230
246
  packageJson,
231
247
  'predev',
232
- 'pnpm --filter @forgeon/i18n-contracts build && pnpm --filter @forgeon/i18n build',
248
+ 'pnpm --filter @forgeon/core build && pnpm --filter @forgeon/db-prisma build && pnpm --filter @forgeon/i18n-contracts build && pnpm --filter @forgeon/i18n build',
233
249
  );
234
250
  ensureDependency(packageJson, '@forgeon/i18n', 'workspace:*');
235
251
  ensureDependency(packageJson, '@forgeon/i18n-contracts', 'workspace:*');
252
+ ensureDependency(packageJson, '@forgeon/db-prisma', 'workspace:*');
236
253
  ensureDependency(packageJson, 'nestjs-i18n', '^10.5.1');
237
254
  writeJson(packagePath, packageJson);
238
255
  }
@@ -298,6 +315,7 @@ function patchRootPackage(targetRoot) {
298
315
  }
299
316
 
300
317
  export function applyI18nModule({ packageRoot, targetRoot }) {
318
+ copyFromBase(packageRoot, targetRoot, path.join('packages', 'db-prisma'));
301
319
  copyFromBase(packageRoot, targetRoot, path.join('packages', 'i18n'));
302
320
  copyFromBase(packageRoot, targetRoot, path.join('resources', 'i18n'));
303
321
 
@@ -9,12 +9,13 @@ export function applyI18nDisabled(targetRoot) {
9
9
  removeIfExists(path.join(targetRoot, 'resources', 'i18n'));
10
10
 
11
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
- }
12
+ if (fs.existsSync(apiPackagePath)) {
13
+ const apiPackage = JSON.parse(fs.readFileSync(apiPackagePath, 'utf8'));
14
+
15
+ if (apiPackage.scripts) {
16
+ apiPackage.scripts.predev =
17
+ 'pnpm --filter @forgeon/core build && pnpm --filter @forgeon/db-prisma build';
18
+ }
18
19
 
19
20
  if (apiPackage.dependencies) {
20
21
  delete apiPackage.dependencies['@forgeon/i18n'];
@@ -110,21 +111,21 @@ export function applyI18nDisabled(targetRoot) {
110
111
  appModulePath,
111
112
  `import { Module } from '@nestjs/common';
112
113
  import { ConfigModule } from '@nestjs/config';
113
- import { CoreConfigModule, CoreErrorsModule, coreConfig, validateCoreEnv } from '@forgeon/core';
114
+ import { dbPrismaConfig, dbPrismaEnvSchema, DbPrismaModule } from '@forgeon/db-prisma';
115
+ import { CoreConfigModule, CoreErrorsModule, coreConfig, coreEnvSchema, createEnvValidator } from '@forgeon/core';
114
116
  import { HealthController } from './health/health.controller';
115
- import { PrismaModule } from './prisma/prisma.module';
116
117
 
117
118
  @Module({
118
119
  imports: [
119
120
  ConfigModule.forRoot({
120
121
  isGlobal: true,
121
- load: [coreConfig],
122
- validate: validateCoreEnv,
122
+ load: [coreConfig, dbPrismaConfig],
123
+ validate: createEnvValidator([coreEnvSchema, dbPrismaEnvSchema]),
123
124
  envFilePath: '.env',
124
125
  }),
125
126
  CoreConfigModule,
126
127
  CoreErrorsModule,
127
- PrismaModule,
128
+ DbPrismaModule,
128
129
  ],
129
130
  controllers: [HealthController],
130
131
  })
@@ -141,29 +142,70 @@ export class AppModule {}
141
142
  'health',
142
143
  'health.controller.ts',
143
144
  );
144
- fs.writeFileSync(
145
- healthControllerPath,
146
- `import { Controller, Get, Query } from '@nestjs/common';
147
- import { EchoQueryDto } from '../common/dto/echo-query.dto';
148
-
149
- @Controller('health')
150
- export class HealthController {
151
- @Get()
152
- getHealth(@Query('lang') _lang?: string) {
153
- return {
154
- status: 'ok',
155
- message: 'OK',
156
- };
157
- }
158
-
159
- @Get('echo')
160
- getEcho(@Query() query: EchoQueryDto) {
161
- return { value: query.value };
162
- }
163
- }
164
- `,
165
- 'utf8',
166
- );
145
+ fs.writeFileSync(
146
+ healthControllerPath,
147
+ `import { ConflictException, Controller, Get, Post, Query } from '@nestjs/common';
148
+ import { PrismaService } from '@forgeon/db-prisma';
149
+ import { EchoQueryDto } from '../common/dto/echo-query.dto';
150
+
151
+ @Controller('health')
152
+ export class HealthController {
153
+ constructor(private readonly prisma: PrismaService) {}
154
+
155
+ @Get()
156
+ getHealth(@Query('lang') lang?: string) {
157
+ const locale = this.resolveLocale(lang);
158
+ return {
159
+ status: 'ok',
160
+ message: 'OK',
161
+ i18n: locale === 'uk' ? 'Ukrainian' : 'English',
162
+ };
163
+ }
164
+
165
+ @Get('error')
166
+ getErrorProbe() {
167
+ throw new ConflictException({
168
+ message: 'Email already exists',
169
+ details: {
170
+ feature: 'core-errors',
171
+ probe: 'health.error',
172
+ },
173
+ });
174
+ }
175
+
176
+ @Get('validation')
177
+ getValidationProbe(@Query() query: EchoQueryDto) {
178
+ return {
179
+ status: 'ok',
180
+ validated: true,
181
+ value: query.value,
182
+ };
183
+ }
184
+
185
+ @Post('db')
186
+ async getDbProbe() {
187
+ const token = \`\${Date.now()}-\${Math.floor(Math.random() * 1_000_000)}\`;
188
+ const email = \`health-probe-\${token}@example.local\`;
189
+ const user = await this.prisma.user.create({
190
+ data: { email },
191
+ select: { id: true, email: true, createdAt: true },
192
+ });
193
+
194
+ return {
195
+ status: 'ok',
196
+ feature: 'db-prisma',
197
+ user,
198
+ };
199
+ }
200
+
201
+ private resolveLocale(lang?: string): 'en' | 'uk' {
202
+ const normalized = (lang ?? '').toLowerCase();
203
+ return normalized.startsWith('uk') ? 'uk' : 'en';
204
+ }
205
+ }
206
+ `,
207
+ 'utf8',
208
+ );
167
209
 
168
210
  removeIfExists(
169
211
  path.join(targetRoot, 'apps', 'api', 'src', 'common', 'filters', 'app-exception.filter.ts'),
@@ -4,19 +4,22 @@ WORKDIR /app
4
4
  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
- COPY apps/api/package.json apps/api/package.json
8
- COPY apps/api/prisma apps/api/prisma
9
- COPY packages/core/package.json packages/core/package.json
10
- COPY packages/i18n/package.json packages/i18n/package.json
7
+ COPY apps/api/package.json apps/api/package.json
8
+ COPY apps/api/prisma apps/api/prisma
9
+ COPY packages/core/package.json packages/core/package.json
10
+ COPY packages/db-prisma/package.json packages/db-prisma/package.json
11
+ COPY packages/i18n/package.json packages/i18n/package.json
11
12
 
12
13
  RUN pnpm install --frozen-lockfile=false
13
14
 
14
15
  COPY apps/api apps/api
15
16
  COPY packages/core packages/core
17
+ COPY packages/db-prisma packages/db-prisma
16
18
  COPY packages/i18n packages/i18n
17
19
  COPY resources resources
18
20
 
19
21
  RUN pnpm --filter @forgeon/core build
22
+ RUN pnpm --filter @forgeon/db-prisma build
20
23
  RUN pnpm --filter @forgeon/i18n build
21
24
  RUN pnpm --filter @forgeon/api prisma:generate
22
25
  RUN pnpm --filter @forgeon/api build
@@ -2,8 +2,8 @@
2
2
  "name": "@forgeon/api",
3
3
  "version": "0.1.0",
4
4
  "private": true,
5
- "scripts": {
6
- "predev": "pnpm --filter @forgeon/i18n build",
5
+ "scripts": {
6
+ "predev": "pnpm --filter @forgeon/core build && pnpm --filter @forgeon/db-prisma build && pnpm --filter @forgeon/i18n build",
7
7
  "build": "tsc -p tsconfig.build.json",
8
8
  "dev": "ts-node --transpile-only src/main.ts",
9
9
  "start": "node dist/main.js",
@@ -14,6 +14,7 @@
14
14
  "prisma:seed": "ts-node --transpile-only prisma/seed.ts"
15
15
  },
16
16
  "dependencies": {
17
+ "@forgeon/db-prisma": "workspace:*",
17
18
  "@forgeon/core": "workspace:*",
18
19
  "@forgeon/i18n": "workspace:*",
19
20
  "@nestjs/common": "^11.0.1",
@@ -1,5 +1,6 @@
1
1
  import { Module } from '@nestjs/common';
2
2
  import { ConfigModule } from '@nestjs/config';
3
+ import { dbPrismaConfig, dbPrismaEnvSchema, DbPrismaModule } from '@forgeon/db-prisma';
3
4
  import {
4
5
  CoreConfigModule,
5
6
  CoreErrorsModule,
@@ -10,7 +11,6 @@ import {
10
11
  import { ForgeonI18nModule, i18nConfig, i18nEnvSchema } from '@forgeon/i18n';
11
12
  import { join } from 'path';
12
13
  import { HealthController } from './health/health.controller';
13
- import { PrismaModule } from './prisma/prisma.module';
14
14
 
15
15
  const i18nPath = join(__dirname, '..', '..', '..', 'resources', 'i18n');
16
16
 
@@ -18,16 +18,16 @@ const i18nPath = join(__dirname, '..', '..', '..', 'resources', 'i18n');
18
18
  imports: [
19
19
  ConfigModule.forRoot({
20
20
  isGlobal: true,
21
- load: [coreConfig, i18nConfig],
22
- validate: createEnvValidator([coreEnvSchema, i18nEnvSchema]),
21
+ load: [coreConfig, dbPrismaConfig, i18nConfig],
22
+ validate: createEnvValidator([coreEnvSchema, dbPrismaEnvSchema, i18nEnvSchema]),
23
23
  envFilePath: '.env',
24
24
  }),
25
25
  CoreConfigModule,
26
26
  CoreErrorsModule,
27
+ DbPrismaModule,
27
28
  ForgeonI18nModule.register({
28
29
  path: i18nPath,
29
30
  }),
30
- PrismaModule,
31
31
  ],
32
32
  controllers: [HealthController],
33
33
  })
@@ -1,10 +1,14 @@
1
- import { Controller, Get, Optional, Query } from '@nestjs/common';
1
+ import { ConflictException, Controller, Get, Optional, Post, Query } from '@nestjs/common';
2
+ import { PrismaService } from '@forgeon/db-prisma';
2
3
  import { I18nService } from 'nestjs-i18n';
3
4
  import { EchoQueryDto } from '../common/dto/echo-query.dto';
4
5
 
5
6
  @Controller('health')
6
7
  export class HealthController {
7
- constructor(@Optional() private readonly i18n?: I18nService) {}
8
+ constructor(
9
+ private readonly prisma: PrismaService,
10
+ @Optional() private readonly i18n?: I18nService,
11
+ ) {}
8
12
 
9
13
  @Get()
10
14
  getHealth(@Query('lang') lang?: string) {
@@ -16,16 +20,45 @@ export class HealthController {
16
20
  };
17
21
  }
18
22
 
19
- @Get('echo')
20
- getEcho(@Query() query: EchoQueryDto) {
21
- return { value: query.value };
23
+ @Get('error')
24
+ getErrorProbe() {
25
+ throw new ConflictException({
26
+ message: 'Email already exists',
27
+ details: {
28
+ feature: 'core-errors',
29
+ probe: 'health.error',
30
+ },
31
+ });
22
32
  }
23
33
 
24
- private translate(key: string, lang?: string): string {
34
+ @Get('validation')
35
+ getValidationProbe(@Query() query: EchoQueryDto) {
36
+ return {
37
+ status: 'ok',
38
+ validated: true,
39
+ value: query.value,
40
+ };
41
+ }
42
+
43
+ @Post('db')
44
+ async getDbProbe() {
45
+ const token = `${Date.now()}-${Math.floor(Math.random() * 1_000_000)}`;
46
+ const email = `health-probe-${token}@example.local`;
47
+ const user = await this.prisma.user.create({
48
+ data: { email },
49
+ select: { id: true, email: true, createdAt: true },
50
+ });
51
+
52
+ return {
53
+ status: 'ok',
54
+ feature: 'db-prisma',
55
+ user,
56
+ };
57
+ }
58
+
59
+ private translate(key: string, lang?: string): string {
25
60
  if (!this.i18n) {
26
61
  if (key === 'common.ok') return 'OK';
27
- if (key === 'common.checkApiHealth') return 'Check API health';
28
- if (key === 'common.language') return 'Language';
29
62
  if (key === 'common.languages.english') return 'English';
30
63
  if (key === 'common.languages.ukrainian') return 'Ukrainian';
31
64
  return key;
@@ -1,6 +1,9 @@
1
1
  import 'reflect-metadata';
2
- import { ValidationPipe } from '@nestjs/common';
3
- import { CoreConfigService, CoreExceptionFilter } from '@forgeon/core';
2
+ import {
3
+ CoreConfigService,
4
+ CoreExceptionFilter,
5
+ createValidationPipe,
6
+ } from '@forgeon/core';
4
7
  import { NestFactory } from '@nestjs/core';
5
8
  import { AppModule } from './app.module';
6
9
 
@@ -10,12 +13,7 @@ async function bootstrap() {
10
13
  const coreConfigService = app.get(CoreConfigService);
11
14
 
12
15
  app.setGlobalPrefix(coreConfigService.apiPrefix);
13
- app.useGlobalPipes(
14
- new ValidationPipe({
15
- whitelist: true,
16
- transform: true,
17
- }),
18
- );
16
+ app.useGlobalPipes(createValidationPipe());
19
17
  app.useGlobalFilters(app.get(CoreExceptionFilter));
20
18
 
21
19
  await app.listen(coreConfigService.port);
@@ -1,37 +1,78 @@
1
- import { useState } from 'react';
2
- import './styles.css';
1
+ import { useState } from 'react';
2
+ import './styles.css';
3
+
4
+ type ProbeResult = {
5
+ statusCode: number;
6
+ body: unknown;
7
+ };
8
+
9
+ export default function App() {
10
+ const [healthResult, setHealthResult] = useState<ProbeResult | null>(null);
11
+ const [errorProbeResult, setErrorProbeResult] = useState<ProbeResult | null>(null);
12
+ const [validationProbeResult, setValidationProbeResult] = useState<ProbeResult | null>(null);
13
+ const [dbProbeResult, setDbProbeResult] = useState<ProbeResult | null>(null);
14
+ const [networkError, setNetworkError] = useState<string | null>(null);
15
+
16
+ const requestProbe = async (url: string, init?: RequestInit): Promise<ProbeResult> => {
17
+ const response = await fetch(url, init);
18
+ let body: unknown = null;
19
+
20
+ try {
21
+ body = await response.json();
22
+ } catch {
23
+ body = { message: 'Non-JSON response' };
24
+ }
25
+
26
+ return {
27
+ statusCode: response.status,
28
+ body,
29
+ };
30
+ };
31
+
32
+ const runProbe = async (
33
+ setter: (value: ProbeResult | null) => void,
34
+ url: string,
35
+ init?: RequestInit,
36
+ ) => {
37
+ setNetworkError(null);
38
+ try {
39
+ const result = await requestProbe(url, init);
40
+ setter(result);
41
+ } catch (err) {
42
+ setNetworkError(err instanceof Error ? err.message : 'Unknown error');
43
+ }
44
+ };
45
+
46
+ const renderResult = (title: string, result: ProbeResult | null) => (
47
+ <section>
48
+ <h3>{title}</h3>
49
+ {result ? <pre>{JSON.stringify(result, null, 2)}</pre> : null}
50
+ </section>
51
+ );
52
+
53
+ return (
54
+ <main className="page">
55
+ <h1>Forgeon Fullstack Scaffold</h1>
56
+ <p>Default frontend preset: React + Vite + TypeScript.</p>
57
+ <div className="actions">
58
+ <button onClick={() => runProbe(setHealthResult, '/api/health')}>Check API health</button>
59
+ <button onClick={() => runProbe(setErrorProbeResult, '/api/health/error')}>
60
+ Check error envelope
61
+ </button>
62
+ <button onClick={() => runProbe(setValidationProbeResult, '/api/health/validation')}>
63
+ Check validation (expect 400)
64
+ </button>
65
+ <button onClick={() => runProbe(setDbProbeResult, '/api/health/db', { method: 'POST' })}>
66
+ Check database (create user)
67
+ </button>
68
+ </div>
69
+ {renderResult('Health response', healthResult)}
70
+ {renderResult('Error probe response', errorProbeResult)}
71
+ {renderResult('Validation probe response', validationProbeResult)}
72
+ {renderResult('DB probe response', dbProbeResult)}
73
+ {networkError ? <p className="error">{networkError}</p> : null}
74
+ </main>
75
+ );
76
+ }
3
77
 
4
- type HealthResponse = {
5
- status: string;
6
- message: string;
7
- };
8
-
9
- export default function App() {
10
- const [data, setData] = useState<HealthResponse | null>(null);
11
- const [error, setError] = useState<string | null>(null);
12
-
13
- const checkApi = async () => {
14
- setError(null);
15
- try {
16
- const response = await fetch('/api/health');
17
- if (!response.ok) {
18
- throw new Error(`HTTP ${response.status}`);
19
- }
20
- const payload = (await response.json()) as HealthResponse;
21
- setData(payload);
22
- } catch (err) {
23
- setError(err instanceof Error ? err.message : 'Unknown error');
24
- }
25
- };
26
-
27
- return (
28
- <main className="page">
29
- <h1>Forgeon Fullstack Scaffold</h1>
30
- <p>Default frontend preset: React + Vite + TypeScript.</p>
31
- <button onClick={checkApi}>Check API health</button>
32
- {data ? <pre>{JSON.stringify(data, null, 2)}</pre> : null}
33
- {error ? <p className="error">{error}</p> : null}
34
- </main>
35
- );
36
- }
37
78
 
@@ -8,23 +8,35 @@ body {
8
8
  color: #0f172a;
9
9
  }
10
10
 
11
- .page {
12
- max-width: 720px;
13
- margin: 3rem auto;
14
- padding: 0 1rem;
15
- }
16
-
17
- button {
18
- padding: 0.6rem 1rem;
19
- border: 0;
20
- border-radius: 0.5rem;
21
- cursor: pointer;
22
- }
23
-
24
- pre {
25
- background: #e2e8f0;
26
- padding: 1rem;
27
- border-radius: 0.5rem;
11
+ .page {
12
+ max-width: 720px;
13
+ margin: 3rem auto;
14
+ padding: 0 1rem;
15
+ }
16
+
17
+ .actions {
18
+ display: grid;
19
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
20
+ gap: 0.6rem;
21
+ margin: 1rem 0 1.25rem;
22
+ }
23
+
24
+ button {
25
+ padding: 0.6rem 1rem;
26
+ border: 0;
27
+ border-radius: 0.5rem;
28
+ cursor: pointer;
29
+ }
30
+
31
+ h3 {
32
+ margin: 1rem 0 0.5rem;
33
+ font-size: 1rem;
34
+ }
35
+
36
+ pre {
37
+ background: #e2e8f0;
38
+ padding: 1rem;
39
+ border-radius: 0.5rem;
28
40
  overflow: auto;
29
41
  }
30
42