create-forgeon 0.3.14 → 0.3.16

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 +4 -2
  2. package/src/core/docs.test.mjs +79 -40
  3. package/src/core/scaffold.test.mjs +99 -0
  4. package/src/modules/db-prisma.mjs +23 -55
  5. package/src/modules/executor.test.mjs +2575 -2419
  6. package/src/modules/files-access.mjs +27 -98
  7. package/src/modules/files-image.mjs +26 -100
  8. package/src/modules/files-quotas.mjs +67 -87
  9. package/src/modules/files.mjs +35 -104
  10. package/src/modules/i18n.mjs +17 -121
  11. package/src/modules/idempotency.test.mjs +174 -0
  12. package/src/modules/jwt-auth.mjs +90 -209
  13. package/src/modules/logger.mjs +0 -9
  14. package/src/modules/probes.test.mjs +202 -0
  15. package/src/modules/queue.mjs +325 -412
  16. package/src/modules/rate-limit.mjs +22 -66
  17. package/src/modules/rbac.mjs +27 -67
  18. package/src/modules/scheduler.mjs +44 -167
  19. package/src/modules/shared/nest-runtime-wiring.mjs +110 -0
  20. package/src/modules/shared/probes.mjs +235 -0
  21. package/src/modules/sync-integrations.mjs +54 -21
  22. package/src/modules/sync-integrations.test.mjs +220 -0
  23. package/src/run-add-module.test.mjs +153 -0
  24. package/templates/base/README.md +7 -55
  25. package/templates/base/apps/web/src/App.tsx +70 -42
  26. package/templates/base/apps/web/src/probes.ts +61 -0
  27. package/templates/base/apps/web/src/styles.css +86 -25
  28. package/templates/base/package.json +21 -15
  29. package/templates/base/scripts/forgeon-sync-integrations.mjs +55 -11
  30. package/templates/module-presets/files-quotas/packages/files-quotas/src/forgeon-files-quotas.module.ts +12 -4
  31. package/templates/module-presets/i18n/apps/web/src/App.tsx +68 -41
  32. package/templates/module-presets/logger/packages/logger/src/index.ts +0 -1
  33. package/templates/base/docs/AI/ARCHITECTURE.md +0 -85
  34. package/templates/base/docs/AI/MODULE_CHECKS.md +0 -28
  35. package/templates/base/docs/AI/MODULE_SPEC.md +0 -77
  36. package/templates/base/docs/AI/PROJECT.md +0 -43
  37. package/templates/base/docs/AI/ROADMAP.md +0 -171
  38. package/templates/base/docs/AI/TASKS.md +0 -60
  39. package/templates/base/docs/AI/VALIDATION.md +0 -31
  40. package/templates/base/docs/README.md +0 -18
  41. package/templates/module-presets/logger/packages/logger/src/http-logging.interceptor.ts +0 -94
package/package.json CHANGED
@@ -1,12 +1,14 @@
1
1
  {
2
2
  "name": "create-forgeon",
3
- "version": "0.3.14",
3
+ "version": "0.3.16",
4
4
  "description": "Forgeon project generator CLI",
5
5
  "license": "MIT",
6
6
  "author": "Forgeon",
7
7
  "type": "module",
8
8
  "scripts": {
9
- "test": "node --test src/cli/options.test.mjs src/cli/add-options.test.mjs src/cli/prompt-select.test.mjs src/core/docs.test.mjs src/core/validate.test.mjs src/modules/executor.test.mjs"
9
+ "test": "node --test --test-concurrency=1 src/cli/options.test.mjs src/cli/add-options.test.mjs src/cli/prompt-select.test.mjs src/core/docs.test.mjs src/core/validate.test.mjs src/core/scaffold.test.mjs src/modules/dependencies.test.mjs src/modules/executor.test.mjs src/modules/probes.test.mjs src/modules/idempotency.test.mjs src/modules/sync-integrations.test.mjs src/run-add-module.test.mjs",
10
+ "smoke:generated-project": "node scripts/generated-project-smoke.mjs",
11
+ "smoke:generated-project:full": "node scripts/generated-project-full-smoke.mjs"
10
12
  },
11
13
  "bin": {
12
14
  "create-forgeon": "bin/create-forgeon.mjs"
@@ -1,29 +1,30 @@
1
- import { describe, it } from 'node:test';
2
- import assert from 'node:assert/strict';
3
- import fs from 'node:fs';
4
- import os from 'node:os';
5
- import path from 'node:path';
6
- import { fileURLToPath } from 'node:url';
7
- import { generateDocs } from './docs.mjs';
8
-
9
- function makeTempDir(prefix) {
10
- return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
11
- }
12
-
13
- function readFile(filePath) {
14
- return fs.readFileSync(filePath, 'utf8');
15
- }
16
-
17
- describe('generateDocs', () => {
18
- const thisDir = path.dirname(fileURLToPath(import.meta.url));
19
- const packageRoot = path.resolve(thisDir, '..', '..');
20
-
1
+ import { describe, it } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import fs from 'node:fs';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { generateDocs } from './docs.mjs';
8
+ import { scaffoldProject } from './scaffold.mjs';
9
+
10
+ function makeTempDir(prefix) {
11
+ return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
12
+ }
13
+
14
+ function readFile(filePath) {
15
+ return fs.readFileSync(filePath, 'utf8');
16
+ }
17
+
18
+ describe('generateDocs', () => {
19
+ const thisDir = path.dirname(fileURLToPath(import.meta.url));
20
+ const packageRoot = path.resolve(thisDir, '..', '..');
21
+
21
22
  it('generates docs for proxy=none without i18n section', () => {
22
- const targetRoot = makeTempDir('forgeon-docs-off-');
23
-
24
- try {
25
- generateDocs(
26
- targetRoot,
23
+ const targetRoot = makeTempDir('forgeon-docs-off-');
24
+
25
+ try {
26
+ generateDocs(
27
+ targetRoot,
27
28
  {
28
29
  frontend: 'react',
29
30
  db: 'prisma',
@@ -31,9 +32,9 @@ describe('generateDocs', () => {
31
32
  dockerEnabled: true,
32
33
  i18nEnabled: false,
33
34
  proxy: 'none',
34
- },
35
- packageRoot,
36
- );
35
+ },
36
+ packageRoot,
37
+ );
37
38
 
38
39
  const readme = readFile(path.join(targetRoot, 'README.md'));
39
40
 
@@ -45,18 +46,20 @@ describe('generateDocs', () => {
45
46
  assert.match(readme, /Module notes index: `modules\/README\.md`/);
46
47
  assert.doesNotMatch(readme, /i18n Configuration/);
47
48
  assert.doesNotMatch(readme, /Prisma In Container Start/);
49
+ assert.doesNotMatch(readme, /docs\/README\.md/);
50
+ assert.doesNotMatch(readme, /docs\/Agents\.md/);
48
51
  assert.equal(fs.existsSync(path.join(targetRoot, 'docs')), false);
49
52
  } finally {
50
53
  fs.rmSync(targetRoot, { recursive: true, force: true });
51
54
  }
52
- });
53
-
54
- it('generates docker and caddy notes when enabled', () => {
55
- const targetRoot = makeTempDir('forgeon-docs-on-');
56
-
57
- try {
58
- generateDocs(
59
- targetRoot,
55
+ });
56
+
57
+ it('generates docker and caddy notes when enabled', () => {
58
+ const targetRoot = makeTempDir('forgeon-docs-on-');
59
+
60
+ try {
61
+ generateDocs(
62
+ targetRoot,
60
63
  {
61
64
  frontend: 'react',
62
65
  db: 'prisma',
@@ -64,9 +67,9 @@ describe('generateDocs', () => {
64
67
  dockerEnabled: true,
65
68
  i18nEnabled: true,
66
69
  proxy: 'caddy',
67
- },
68
- packageRoot,
69
- );
70
+ },
71
+ packageRoot,
72
+ );
70
73
 
71
74
  const readme = readFile(path.join(targetRoot, 'README.md'));
72
75
 
@@ -77,9 +80,45 @@ describe('generateDocs', () => {
77
80
  assert.match(readme, /Prisma In Container Start/);
78
81
  assert.match(readme, /Error Handling \(`core-errors`\)/);
79
82
  assert.match(readme, /Module-specific notes: `modules\/<module-id>\/README\.md`/);
83
+ assert.doesNotMatch(readme, /docs\/README\.md/);
84
+ assert.doesNotMatch(readme, /docs\/Agents\.md/);
80
85
  assert.equal(fs.existsSync(path.join(targetRoot, 'docs')), false);
81
86
  } finally {
82
87
  fs.rmSync(targetRoot, { recursive: true, force: true });
83
88
  }
84
- });
89
+ });
90
+
91
+ it('scaffolds a generated project without copying internal docs payload', () => {
92
+ const tempRoot = makeTempDir('forgeon-scaffold-doc-boundary-');
93
+ const targetRoot = path.join(tempRoot, 'demo-doc-boundary');
94
+ const templateRoot = path.join(packageRoot, 'templates', 'base');
95
+
96
+ try {
97
+ scaffoldProject({
98
+ templateRoot,
99
+ packageRoot,
100
+ targetRoot,
101
+ projectName: 'demo-doc-boundary',
102
+ frontend: 'react',
103
+ db: 'prisma',
104
+ dbPrismaEnabled: true,
105
+ i18nEnabled: true,
106
+ proxy: 'caddy',
107
+ });
108
+
109
+ const readme = readFile(path.join(targetRoot, 'README.md'));
110
+ const packageJson = readFile(path.join(targetRoot, 'package.json'));
111
+
112
+ assert.equal(fs.existsSync(path.join(targetRoot, 'docs')), false);
113
+ assert.match(readme, /Module notes index: `modules\/README\.md`/);
114
+ assert.doesNotMatch(readme, /temporary template placeholder/i);
115
+ assert.doesNotMatch(readme, /built-in docs/i);
116
+ assert.doesNotMatch(readme, /docs\/README\.md/);
117
+ assert.doesNotMatch(readme, /docs\/Agents\.md/);
118
+ assert.doesNotMatch(packageJson, /"create:forgeon"/);
119
+ assert.match(packageJson, /"forgeon:sync-integrations"/);
120
+ } finally {
121
+ fs.rmSync(tempRoot, { recursive: true, force: true });
122
+ }
123
+ });
85
124
  });
@@ -0,0 +1,99 @@
1
+ import { describe, it } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import fs from 'node:fs';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { scaffoldProject } from './scaffold.mjs';
8
+
9
+ function makeTempDir(prefix) {
10
+ return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
11
+ }
12
+
13
+ function readFile(filePath) {
14
+ return fs.readFileSync(filePath, 'utf8');
15
+ }
16
+
17
+ function assertProxyPreset(targetRoot, proxy) {
18
+ const dockerDir = path.join(targetRoot, 'infra', 'docker');
19
+ const compose = readFile(path.join(dockerDir, 'compose.yml'));
20
+ const packageJson = readFile(path.join(targetRoot, 'package.json'));
21
+ const appTsx = readFile(path.join(targetRoot, 'apps', 'web', 'src', 'App.tsx'));
22
+ const probesTs = readFile(path.join(targetRoot, 'apps', 'web', 'src', 'probes.ts'));
23
+
24
+ assert.equal(fs.existsSync(path.join(targetRoot, 'docs')), false);
25
+ assert.equal(fs.existsSync(path.join(dockerDir, 'compose.caddy.yml')), false);
26
+ assert.equal(fs.existsSync(path.join(dockerDir, 'compose.nginx.yml')), false);
27
+ assert.equal(fs.existsSync(path.join(dockerDir, 'compose.none.yml')), false);
28
+ assert.match(packageJson, /"forgeon:sync-integrations"/);
29
+ assert.doesNotMatch(packageJson, /"create:forgeon"/);
30
+ assert.match(compose, /^services:\s*$/m);
31
+ assert.match(compose, /^\s{2}api:\s*$/m);
32
+
33
+ if (proxy === 'caddy') {
34
+ assert.match(compose, /^\s{2}caddy:\s*$/m);
35
+ assert.doesNotMatch(compose, /^\s{2}nginx:\s*$/m);
36
+ assert.equal(fs.existsSync(path.join(dockerDir, 'caddy.Dockerfile')), true);
37
+ assert.equal(fs.existsSync(path.join(dockerDir, 'nginx.Dockerfile')), false);
38
+ assert.equal(fs.existsSync(path.join(targetRoot, 'infra', 'caddy')), true);
39
+ assert.equal(fs.existsSync(path.join(targetRoot, 'infra', 'nginx')), false);
40
+ return;
41
+ }
42
+
43
+ if (proxy === 'nginx') {
44
+ assert.match(compose, /^\s{2}nginx:\s*$/m);
45
+ assert.doesNotMatch(compose, /^\s{2}caddy:\s*$/m);
46
+ assert.equal(fs.existsSync(path.join(dockerDir, 'nginx.Dockerfile')), true);
47
+ assert.equal(fs.existsSync(path.join(dockerDir, 'caddy.Dockerfile')), false);
48
+ assert.equal(fs.existsSync(path.join(targetRoot, 'infra', 'nginx')), true);
49
+ assert.equal(fs.existsSync(path.join(targetRoot, 'infra', 'caddy')), false);
50
+ return;
51
+ }
52
+
53
+ assert.doesNotMatch(compose, /^\s{2}caddy:\s*$/m);
54
+ assert.doesNotMatch(compose, /^\s{2}nginx:\s*$/m);
55
+ assert.match(compose, /- "3000:3000"/);
56
+ assert.equal(fs.existsSync(path.join(dockerDir, 'nginx.Dockerfile')), false);
57
+ assert.equal(fs.existsSync(path.join(dockerDir, 'caddy.Dockerfile')), false);
58
+ assert.equal(fs.existsSync(path.join(targetRoot, 'infra', 'nginx')), false);
59
+ assert.equal(fs.existsSync(path.join(targetRoot, 'infra', 'caddy')), false);
60
+ }
61
+
62
+ describe('scaffoldProject', () => {
63
+ const thisDir = path.dirname(fileURLToPath(import.meta.url));
64
+ const packageRoot = path.resolve(thisDir, '..', '..');
65
+ const templateRoot = path.join(packageRoot, 'templates', 'base');
66
+ const cases = [
67
+ { proxy: 'caddy', readmePattern: /Proxy Preset: Caddy/ },
68
+ { proxy: 'nginx', readmePattern: /Proxy Preset: Nginx/ },
69
+ { proxy: 'none', readmePattern: /Proxy Preset: none/ },
70
+ ];
71
+
72
+ it('applies proxy presets without leftover reverse-proxy assets', () => {
73
+ for (const testCase of cases) {
74
+ const tempRoot = makeTempDir(`forgeon-scaffold-proxy-${testCase.proxy}-`);
75
+ const targetRoot = path.join(tempRoot, `demo-${testCase.proxy}`);
76
+
77
+ try {
78
+ scaffoldProject({
79
+ templateRoot,
80
+ packageRoot,
81
+ targetRoot,
82
+ projectName: `demo-${testCase.proxy}`,
83
+ frontend: 'react',
84
+ db: 'prisma',
85
+ dbPrismaEnabled: false,
86
+ i18nEnabled: false,
87
+ proxy: testCase.proxy,
88
+ });
89
+
90
+ const readme = readFile(path.join(targetRoot, 'README.md'));
91
+ assert.match(readme, testCase.readmePattern);
92
+ assert.match(readme, /Module notes index: `modules\/README\.md`/);
93
+ assertProxyPreset(targetRoot, testCase.proxy);
94
+ } finally {
95
+ fs.rmSync(tempRoot, { recursive: true, force: true });
96
+ }
97
+ }
98
+ });
99
+ });
@@ -15,6 +15,7 @@ import {
15
15
  ensureValidatorSchema,
16
16
  upsertEnvLines,
17
17
  } from './shared/patch-utils.mjs';
18
+ import { ensureWebProbeDefinition, resolveProbeTargets } from './shared/probes.mjs';
18
19
 
19
20
  function copyFromPreset(packageRoot, targetRoot, relativePath) {
20
21
  const source = path.join(packageRoot, 'templates', 'module-presets', 'db-prisma', relativePath);
@@ -110,7 +111,11 @@ function patchAppModule(targetRoot) {
110
111
  fs.writeFileSync(filePath, `${content.trimEnd()}\n`, 'utf8');
111
112
  }
112
113
 
113
- function patchHealthController(targetRoot) {
114
+ function patchHealthController(targetRoot, probeTargets) {
115
+ if (!probeTargets.allowApi) {
116
+ return;
117
+ }
118
+
114
119
  const filePath = path.join(targetRoot, 'apps', 'api', 'src', 'health', 'health.controller.ts');
115
120
  if (!fs.existsSync(filePath)) {
116
121
  return;
@@ -171,58 +176,19 @@ function patchHealthController(targetRoot) {
171
176
  fs.writeFileSync(filePath, `${content.trimEnd()}\n`, 'utf8');
172
177
  }
173
178
 
174
- function patchWebApp(targetRoot) {
175
- const filePath = path.join(targetRoot, 'apps', 'web', 'src', 'App.tsx');
176
- if (!fs.existsSync(filePath)) {
177
- return;
178
- }
179
-
180
- let content = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
181
- content = content
182
- .replace(/^\s*\{\/\* forgeon:probes:actions:start \*\/\}\r?\n?/gm, '')
183
- .replace(/^\s*\{\/\* forgeon:probes:actions:end \*\/\}\r?\n?/gm, '')
184
- .replace(/^\s*\{\/\* forgeon:probes:results:start \*\/\}\r?\n?/gm, '')
185
- .replace(/^\s*\{\/\* forgeon:probes:results:end \*\/\}\r?\n?/gm, '');
186
-
187
- if (!content.includes('dbProbeResult')) {
188
- const stateAnchor = ' const [validationProbeResult, setValidationProbeResult] = useState<ProbeResult | null>(null);';
189
- if (content.includes(stateAnchor)) {
190
- content = ensureLineAfter(
191
- content,
192
- stateAnchor,
193
- ' const [dbProbeResult, setDbProbeResult] = useState<ProbeResult | null>(null);',
194
- );
195
- }
196
- }
197
-
198
- if (!content.includes('Check database (create user)')) {
199
- const dbButton = content.includes("runProbe(setHealthResult, '/health')")
200
- ? " <button onClick={() => runProbe(setDbProbeResult, '/health/db', { method: 'POST' })}>\n Check database (create user)\n </button>"
201
- : " <button onClick={() => runProbe(setDbProbeResult, '/api/health/db', { method: 'POST' })}>\n Check database (create user)\n </button>";
202
-
203
- const actionsStart = content.indexOf('<div className="actions">');
204
- if (actionsStart >= 0) {
205
- const actionsEnd = content.indexOf('\n </div>', actionsStart);
206
- if (actionsEnd >= 0) {
207
- content = `${content.slice(0, actionsEnd)}\n${dbButton}${content.slice(actionsEnd)}`;
208
- }
209
- }
210
- }
211
-
212
- if (!content.includes("{renderResult('DB probe response', dbProbeResult)}")) {
213
- const dbResultLine = " {renderResult('DB probe response', dbProbeResult)}";
214
- const networkLine = ' {networkError ? <p className="error">{networkError}</p> : null}';
215
- if (content.includes(networkLine)) {
216
- content = content.replace(networkLine, `${dbResultLine}\n${networkLine}`);
217
- } else {
218
- const resultAnchor = "{renderResult('Validation probe response', validationProbeResult)}";
219
- if (content.includes(resultAnchor)) {
220
- content = ensureLineAfter(content, resultAnchor, dbResultLine);
221
- }
222
- }
223
- }
224
-
225
- fs.writeFileSync(filePath, `${content.trimEnd()}\n`, 'utf8');
179
+ function registerWebProbe(targetRoot, probeTargets) {
180
+ ensureWebProbeDefinition({
181
+ targetRoot,
182
+ probeTargets,
183
+ definition: {
184
+ id: 'db',
185
+ title: 'Database',
186
+ buttonLabel: 'Check database (create user)',
187
+ resultTitle: 'DB probe response',
188
+ path: '/health/db',
189
+ request: { method: 'POST' },
190
+ },
191
+ });
226
192
  }
227
193
 
228
194
  function patchApiDockerfile(targetRoot) {
@@ -349,11 +315,13 @@ export function applyDbPrismaModule({ packageRoot, targetRoot }) {
349
315
  copyFromPreset(packageRoot, targetRoot, path.join('packages', 'db-prisma'));
350
316
  copyFromPreset(packageRoot, targetRoot, path.join('apps', 'api', 'prisma'));
351
317
 
318
+ const probeTargets = resolveProbeTargets({ targetRoot, moduleId: 'db-prisma' });
319
+
352
320
  patchApiPackage(targetRoot);
353
321
  patchRootPackage(targetRoot);
354
322
  patchAppModule(targetRoot);
355
- patchHealthController(targetRoot);
356
- patchWebApp(targetRoot);
323
+ patchHealthController(targetRoot, probeTargets);
324
+ registerWebProbe(targetRoot, probeTargets);
357
325
  patchApiDockerfile(targetRoot);
358
326
  patchCompose(targetRoot);
359
327
  patchReadme(targetRoot);