create-forgeon 0.3.15 → 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.
- package/package.json +4 -2
- package/src/core/docs.test.mjs +79 -40
- package/src/core/scaffold.test.mjs +99 -0
- package/src/modules/db-prisma.mjs +23 -55
- package/src/modules/executor.test.mjs +132 -36
- package/src/modules/files-access.mjs +27 -98
- package/src/modules/files-image.mjs +26 -100
- package/src/modules/files-quotas.mjs +67 -87
- package/src/modules/files.mjs +35 -104
- package/src/modules/i18n.mjs +17 -121
- package/src/modules/idempotency.test.mjs +174 -0
- package/src/modules/jwt-auth.mjs +90 -209
- package/src/modules/logger.mjs +0 -9
- package/src/modules/probes.test.mjs +202 -0
- package/src/modules/queue.mjs +325 -443
- package/src/modules/rate-limit.mjs +22 -66
- package/src/modules/rbac.mjs +27 -67
- package/src/modules/scheduler.mjs +44 -167
- package/src/modules/shared/nest-runtime-wiring.mjs +110 -0
- package/src/modules/shared/probes.mjs +235 -0
- package/src/modules/sync-integrations.mjs +54 -21
- package/src/modules/sync-integrations.test.mjs +220 -0
- package/src/run-add-module.test.mjs +153 -0
- package/templates/base/README.md +7 -55
- package/templates/base/apps/web/src/App.tsx +70 -42
- package/templates/base/apps/web/src/probes.ts +61 -0
- package/templates/base/apps/web/src/styles.css +86 -25
- package/templates/base/package.json +21 -15
- package/templates/base/scripts/forgeon-sync-integrations.mjs +55 -11
- package/templates/module-presets/files-quotas/packages/files-quotas/src/forgeon-files-quotas.module.ts +12 -4
- package/templates/module-presets/i18n/apps/web/src/App.tsx +68 -41
- package/templates/module-presets/logger/packages/logger/src/index.ts +0 -1
- package/templates/base/docs/AI/ARCHITECTURE.md +0 -85
- package/templates/base/docs/AI/MODULE_CHECKS.md +0 -28
- package/templates/base/docs/AI/MODULE_SPEC.md +0 -77
- package/templates/base/docs/AI/PROJECT.md +0 -43
- package/templates/base/docs/AI/ROADMAP.md +0 -171
- package/templates/base/docs/AI/TASKS.md +0 -60
- package/templates/base/docs/AI/VALIDATION.md +0 -31
- package/templates/base/docs/README.md +0 -18
- 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.
|
|
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"
|
package/src/core/docs.test.mjs
CHANGED
|
@@ -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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const
|
|
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
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
|
|
323
|
+
patchHealthController(targetRoot, probeTargets);
|
|
324
|
+
registerWebProbe(targetRoot, probeTargets);
|
|
357
325
|
patchApiDockerfile(targetRoot);
|
|
358
326
|
patchCompose(targetRoot);
|
|
359
327
|
patchReadme(targetRoot);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, it } from 'node:test';
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import os from 'node:os';
|
|
@@ -18,6 +18,21 @@ function createMinimalForgeonProject(targetRoot) {
|
|
|
18
18
|
fs.writeFileSync(path.join(targetRoot, 'pnpm-workspace.yaml'), 'packages:\n - apps/*\n', 'utf8');
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
function readFile(filePath) {
|
|
22
|
+
return fs.readFileSync(filePath, 'utf8');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function readWebProbes(projectRoot) {
|
|
26
|
+
return readFile(path.join(projectRoot, 'apps', 'web', 'src', 'probes.ts'));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function assertWebProbeShell(projectRoot) {
|
|
30
|
+
const appTsx = readFile(path.join(projectRoot, 'apps', 'web', 'src', 'App.tsx'));
|
|
31
|
+
assert.match(appTsx, /id="probes"/);
|
|
32
|
+
assert.match(appTsx, /from '\.\/probes'/);
|
|
33
|
+
return appTsx;
|
|
34
|
+
}
|
|
35
|
+
|
|
21
36
|
function assertDbPrismaWiring(projectRoot) {
|
|
22
37
|
const appModule = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'src', 'app.module.ts'), 'utf8');
|
|
23
38
|
assert.match(appModule, /dbPrismaConfig/);
|
|
@@ -75,10 +90,12 @@ function assertRateLimitWiring(projectRoot) {
|
|
|
75
90
|
assert.match(healthController, /@Get\('rate-limit'\)/);
|
|
76
91
|
assert.match(healthController, /TOO_MANY_REQUESTS/);
|
|
77
92
|
|
|
78
|
-
const appTsx =
|
|
93
|
+
const appTsx = assertWebProbeShell(projectRoot);
|
|
94
|
+
const probesTs = readWebProbes(projectRoot);
|
|
79
95
|
assert.match(appTsx, /cache: 'no-store'/);
|
|
80
|
-
assert.match(
|
|
81
|
-
assert.match(
|
|
96
|
+
assert.match(probesTs, /"id": "rate-limit"/);
|
|
97
|
+
assert.match(probesTs, /"buttonLabel": "Check rate limit \(click repeatedly\)"/);
|
|
98
|
+
assert.match(probesTs, /"resultTitle": "Rate limit probe response"/);
|
|
82
99
|
|
|
83
100
|
const readme = fs.readFileSync(path.join(projectRoot, 'README.md'), 'utf8');
|
|
84
101
|
assert.match(readme, /## Rate Limit Module/);
|
|
@@ -109,9 +126,11 @@ function assertQueueWiring(projectRoot) {
|
|
|
109
126
|
assert.match(healthController, /@Get\('queue'\)/);
|
|
110
127
|
assert.match(healthController, /queueService\.getProbeStatus/);
|
|
111
128
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
assert.match(
|
|
129
|
+
assertWebProbeShell(projectRoot);
|
|
130
|
+
const probesTs = readWebProbes(projectRoot);
|
|
131
|
+
assert.match(probesTs, /"id": "queue"/);
|
|
132
|
+
assert.match(probesTs, /"buttonLabel": "Check queue health"/);
|
|
133
|
+
assert.match(probesTs, /"resultTitle": "Queue probe response"/);
|
|
115
134
|
|
|
116
135
|
const apiEnv = fs.readFileSync(path.join(projectRoot, 'apps', 'api', '.env.example'), 'utf8');
|
|
117
136
|
assert.match(apiEnv, /QUEUE_ENABLED=true/);
|
|
@@ -156,9 +175,11 @@ function assertSchedulerWiring(projectRoot) {
|
|
|
156
175
|
assert.match(healthController, /@Get\('scheduler'\)/);
|
|
157
176
|
assert.match(healthController, /schedulerService\.getProbeStatus/);
|
|
158
177
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
assert.match(
|
|
178
|
+
assertWebProbeShell(projectRoot);
|
|
179
|
+
const probesTs = readWebProbes(projectRoot);
|
|
180
|
+
assert.match(probesTs, /"id": "scheduler"/);
|
|
181
|
+
assert.match(probesTs, /"buttonLabel": "Check scheduler health"/);
|
|
182
|
+
assert.match(probesTs, /"resultTitle": "Scheduler probe response"/);
|
|
162
183
|
|
|
163
184
|
const apiEnv = fs.readFileSync(path.join(projectRoot, 'apps', 'api', '.env.example'), 'utf8');
|
|
164
185
|
assert.match(apiEnv, /SCHEDULER_ENABLED=true/);
|
|
@@ -200,10 +221,12 @@ function assertRbacWiring(projectRoot) {
|
|
|
200
221
|
assert.match(healthController, /@Get\('rbac'\)/);
|
|
201
222
|
assert.match(healthController, /@Permissions\('health\.rbac'\)/);
|
|
202
223
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
assert.match(
|
|
206
|
-
assert.match(
|
|
224
|
+
assertWebProbeShell(projectRoot);
|
|
225
|
+
const probesTs = readWebProbes(projectRoot);
|
|
226
|
+
assert.match(probesTs, /"id": "rbac"/);
|
|
227
|
+
assert.match(probesTs, /"buttonLabel": "Check RBAC access"/);
|
|
228
|
+
assert.match(probesTs, /"resultTitle": "RBAC probe response"/);
|
|
229
|
+
assert.match(probesTs, /x-forgeon-permissions/);
|
|
207
230
|
|
|
208
231
|
const readme = fs.readFileSync(path.join(projectRoot, 'README.md'), 'utf8');
|
|
209
232
|
assert.match(readme, /## RBAC \/ Permissions Module/);
|
|
@@ -262,11 +285,14 @@ function assertFilesWiring(projectRoot, expectedStorageDriver = 'local') {
|
|
|
262
285
|
assert.match(filesService, /variants:\s*\{[\s\S]*?none:\s*\{[\s\S]*?\}/);
|
|
263
286
|
assert.match(filesService, /prisma\.fileBlob/);
|
|
264
287
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
assert.match(
|
|
268
|
-
assert.match(
|
|
269
|
-
assert.match(
|
|
288
|
+
assertWebProbeShell(projectRoot);
|
|
289
|
+
const probesTs = readWebProbes(projectRoot);
|
|
290
|
+
assert.match(probesTs, /"id": "files"/);
|
|
291
|
+
assert.match(probesTs, /"buttonLabel": "Check files probe \(create metadata\)"/);
|
|
292
|
+
assert.match(probesTs, /"resultTitle": "Files probe response"/);
|
|
293
|
+
assert.match(probesTs, /"id": "files-variants"/);
|
|
294
|
+
assert.match(probesTs, /"buttonLabel": "Check files variants capability"/);
|
|
295
|
+
assert.match(probesTs, /"resultTitle": "Files variants probe response"/);
|
|
270
296
|
|
|
271
297
|
const schema = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'prisma', 'schema.prisma'), 'utf8');
|
|
272
298
|
assert.match(schema, /model FileRecord \{/);
|
|
@@ -410,10 +436,12 @@ function assertFilesAccessWiring(projectRoot) {
|
|
|
410
436
|
assert.match(healthController, /extractFilesAccessSubject/);
|
|
411
437
|
assert.match(healthController, /filesAccessService\.canRead/);
|
|
412
438
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
assert.match(
|
|
416
|
-
assert.match(
|
|
439
|
+
assertWebProbeShell(projectRoot);
|
|
440
|
+
const probesTs = readWebProbes(projectRoot);
|
|
441
|
+
assert.match(probesTs, /"id": "files-access"/);
|
|
442
|
+
assert.match(probesTs, /"buttonLabel": "Check files access"/);
|
|
443
|
+
assert.match(probesTs, /"resultTitle": "Files access probe response"/);
|
|
444
|
+
assert.match(probesTs, /x-forgeon-user-id/);
|
|
417
445
|
|
|
418
446
|
const readme = fs.readFileSync(path.join(projectRoot, 'README.md'), 'utf8');
|
|
419
447
|
assert.match(readme, /## Files Access Module/);
|
|
@@ -436,7 +464,8 @@ function assertFilesQuotasWiring(projectRoot) {
|
|
|
436
464
|
);
|
|
437
465
|
|
|
438
466
|
const filesPackage = fs.readFileSync(path.join(projectRoot, 'packages', 'files', 'package.json'), 'utf8');
|
|
439
|
-
assert.
|
|
467
|
+
assert.doesNotMatch(filesPackage, /@forgeon\/files-quotas/);
|
|
468
|
+
assert.match(filesPackage, /@nestjs\/core/);
|
|
440
469
|
|
|
441
470
|
const apiDockerfile = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'Dockerfile'), 'utf8');
|
|
442
471
|
assert.match(
|
|
@@ -455,7 +484,9 @@ function assertFilesQuotasWiring(projectRoot) {
|
|
|
455
484
|
path.join(projectRoot, 'packages', 'files', 'src', 'files.controller.ts'),
|
|
456
485
|
'utf8',
|
|
457
486
|
);
|
|
458
|
-
assert.match(filesController, /
|
|
487
|
+
assert.match(filesController, /ModuleRef/);
|
|
488
|
+
assert.match(filesController, /FORGEON_FILES_UPLOAD_QUOTA_SERVICE/);
|
|
489
|
+
assert.match(filesController, /getFilesUploadQuotaService/);
|
|
459
490
|
assert.match(filesController, /filesQuotasService\.assertUploadAllowed/);
|
|
460
491
|
|
|
461
492
|
const healthController = fs.readFileSync(
|
|
@@ -465,9 +496,11 @@ function assertFilesQuotasWiring(projectRoot) {
|
|
|
465
496
|
assert.match(healthController, /@Get\('files-quotas'\)/);
|
|
466
497
|
assert.match(healthController, /filesQuotasService\.getProbeStatus/);
|
|
467
498
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
assert.match(
|
|
499
|
+
assertWebProbeShell(projectRoot);
|
|
500
|
+
const probesTs = readWebProbes(projectRoot);
|
|
501
|
+
assert.match(probesTs, /"id": "files-quotas"/);
|
|
502
|
+
assert.match(probesTs, /"buttonLabel": "Check files quotas"/);
|
|
503
|
+
assert.match(probesTs, /"resultTitle": "Files quotas probe response"/);
|
|
471
504
|
|
|
472
505
|
const apiEnv = fs.readFileSync(path.join(projectRoot, 'apps', 'api', '.env.example'), 'utf8');
|
|
473
506
|
assert.match(apiEnv, /FILES_QUOTAS_ENABLED=true/);
|
|
@@ -552,9 +585,11 @@ function assertFilesImageWiring(projectRoot) {
|
|
|
552
585
|
assert.match(healthController, /@Get\('files-image'\)/);
|
|
553
586
|
assert.match(healthController, /filesImageService\.getProbeStatus/);
|
|
554
587
|
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
assert.match(
|
|
588
|
+
assertWebProbeShell(projectRoot);
|
|
589
|
+
const probesTs = readWebProbes(projectRoot);
|
|
590
|
+
assert.match(probesTs, /"id": "files-image"/);
|
|
591
|
+
assert.match(probesTs, /"buttonLabel": "Check files image sanitize"/);
|
|
592
|
+
assert.match(probesTs, /"resultTitle": "Files image probe response"/);
|
|
558
593
|
|
|
559
594
|
const apiEnv = fs.readFileSync(path.join(projectRoot, 'apps', 'api', '.env.example'), 'utf8');
|
|
560
595
|
assert.match(apiEnv, /FILES_IMAGE_ENABLED=true/);
|
|
@@ -617,9 +652,11 @@ function assertJwtAuthWiring(projectRoot, withPrismaStore) {
|
|
|
617
652
|
assert.match(healthController, /authService\.getProbeStatus/);
|
|
618
653
|
assert.doesNotMatch(healthController, /,\s*,/);
|
|
619
654
|
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
assert.match(
|
|
655
|
+
assertWebProbeShell(projectRoot);
|
|
656
|
+
const probesTs = readWebProbes(projectRoot);
|
|
657
|
+
assert.match(probesTs, /"id": "auth"/);
|
|
658
|
+
assert.match(probesTs, /"buttonLabel": "Check JWT auth probe"/);
|
|
659
|
+
assert.match(probesTs, /"resultTitle": "Auth probe response"/);
|
|
623
660
|
|
|
624
661
|
const apiDockerfile = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'Dockerfile'), 'utf8');
|
|
625
662
|
assert.match(
|
|
@@ -1188,6 +1225,16 @@ describe('addModule', () => {
|
|
|
1188
1225
|
assert.match(loggerModule, /ForgeonHttpLoggingMiddleware/);
|
|
1189
1226
|
assert.match(loggerModule, /consumer\.apply\(RequestIdMiddleware, ForgeonHttpLoggingMiddleware\)\.forRoutes\('\*'\);/);
|
|
1190
1227
|
|
|
1228
|
+
const loggerIndex = fs.readFileSync(
|
|
1229
|
+
path.join(projectRoot, 'packages', 'logger', 'src', 'index.ts'),
|
|
1230
|
+
'utf8',
|
|
1231
|
+
);
|
|
1232
|
+
assert.doesNotMatch(loggerIndex, /http-logging\.interceptor/);
|
|
1233
|
+
assert.equal(
|
|
1234
|
+
fs.existsSync(path.join(projectRoot, 'packages', 'logger', 'src', 'http-logging.interceptor.ts')),
|
|
1235
|
+
false,
|
|
1236
|
+
);
|
|
1237
|
+
|
|
1191
1238
|
const apiEnv = fs.readFileSync(path.join(projectRoot, 'apps', 'api', '.env.example'), 'utf8');
|
|
1192
1239
|
assert.match(apiEnv, /LOGGER_LEVEL=log/);
|
|
1193
1240
|
assert.match(apiEnv, /LOGGER_HTTP_ENABLED=true/);
|
|
@@ -1604,8 +1651,9 @@ describe('addModule', () => {
|
|
|
1604
1651
|
assert.match(healthController, /@Get\('files-access'\)/);
|
|
1605
1652
|
assert.match(healthController, /@Get\('files-quotas'\)/);
|
|
1606
1653
|
|
|
1607
|
-
|
|
1608
|
-
const
|
|
1654
|
+
assertWebProbeShell(projectRoot);
|
|
1655
|
+
const probesTs = readWebProbes(projectRoot);
|
|
1656
|
+
const filesChecks = probesTs.match(/"buttonLabel": "Check files /g) ?? [];
|
|
1609
1657
|
assert.equal(filesChecks.length, 5);
|
|
1610
1658
|
} finally {
|
|
1611
1659
|
fs.rmSync(targetRoot, { recursive: true, force: true });
|
|
@@ -1925,6 +1973,51 @@ describe('addModule', () => {
|
|
|
1925
1973
|
}
|
|
1926
1974
|
});
|
|
1927
1975
|
|
|
1976
|
+
|
|
1977
|
+
it('applies i18n after probe modules and preserves managed probe registry entries', () => {
|
|
1978
|
+
const targetRoot = mkTmp('forgeon-module-i18n-probes-');
|
|
1979
|
+
const projectRoot = path.join(targetRoot, 'demo-i18n-probes');
|
|
1980
|
+
const templateRoot = path.join(packageRoot, 'templates', 'base');
|
|
1981
|
+
|
|
1982
|
+
try {
|
|
1983
|
+
scaffoldProject({
|
|
1984
|
+
templateRoot,
|
|
1985
|
+
packageRoot,
|
|
1986
|
+
targetRoot: projectRoot,
|
|
1987
|
+
projectName: 'demo-i18n-probes',
|
|
1988
|
+
frontend: 'react',
|
|
1989
|
+
db: 'prisma',
|
|
1990
|
+
dbPrismaEnabled: true,
|
|
1991
|
+
i18nEnabled: false,
|
|
1992
|
+
proxy: 'caddy',
|
|
1993
|
+
});
|
|
1994
|
+
|
|
1995
|
+
addModule({ moduleId: 'rate-limit', targetRoot: projectRoot, packageRoot });
|
|
1996
|
+
addModule({ moduleId: 'rbac', targetRoot: projectRoot, packageRoot });
|
|
1997
|
+
|
|
1998
|
+
const probesBeforeI18n = readWebProbes(projectRoot);
|
|
1999
|
+
assert.match(probesBeforeI18n, /"id": "rate-limit"/);
|
|
2000
|
+
assert.match(probesBeforeI18n, /"id": "rbac"/);
|
|
2001
|
+
|
|
2002
|
+
const i18nResult = addModule({
|
|
2003
|
+
moduleId: 'i18n',
|
|
2004
|
+
targetRoot: projectRoot,
|
|
2005
|
+
packageRoot,
|
|
2006
|
+
});
|
|
2007
|
+
assert.equal(i18nResult.applied, true);
|
|
2008
|
+
|
|
2009
|
+
const probesAfterI18n = readWebProbes(projectRoot);
|
|
2010
|
+
assert.match(probesAfterI18n, /"id": "rate-limit"/);
|
|
2011
|
+
assert.match(probesAfterI18n, /"id": "rbac"/);
|
|
2012
|
+
|
|
2013
|
+
const rateLimitIndex = probesAfterI18n.indexOf('"id": "rate-limit"');
|
|
2014
|
+
const rbacIndex = probesAfterI18n.indexOf('"id": "rbac"');
|
|
2015
|
+
assert.equal(rbacIndex >= 0 && rateLimitIndex > rbacIndex, true);
|
|
2016
|
+
} finally {
|
|
2017
|
+
fs.rmSync(targetRoot, { recursive: true, force: true });
|
|
2018
|
+
}
|
|
2019
|
+
});
|
|
2020
|
+
|
|
1928
2021
|
it('applies swagger -> logger -> i18n and keeps all module wiring', () => {
|
|
1929
2022
|
const targetRoot = mkTmp('forgeon-module-mixed-order-');
|
|
1930
2023
|
const projectRoot = path.join(targetRoot, 'demo-mixed-order');
|
|
@@ -2503,3 +2596,6 @@ describe('addModule', () => {
|
|
|
2503
2596
|
});
|
|
2504
2597
|
|
|
2505
2598
|
|
|
2599
|
+
|
|
2600
|
+
|
|
2601
|
+
|