create-forgeon 0.3.15 → 0.3.17
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/cli/add-options.test.mjs +5 -2
- package/src/cli/options.test.mjs +1 -0
- package/src/cli/prompt-select.test.mjs +1 -0
- package/src/core/docs.test.mjs +80 -40
- package/src/core/scaffold.test.mjs +100 -0
- package/src/core/validate.test.mjs +1 -0
- package/src/modules/accounts.mjs +416 -0
- package/src/modules/db-prisma.mjs +23 -55
- package/src/modules/dependencies.test.mjs +71 -29
- package/src/modules/executor.mjs +3 -2
- package/src/modules/executor.test.mjs +631 -500
- package/src/modules/files-access.mjs +36 -105
- package/src/modules/files-image.mjs +35 -107
- package/src/modules/files-local.mjs +15 -6
- package/src/modules/files-quotas.mjs +75 -93
- package/src/modules/files-s3.mjs +17 -6
- package/src/modules/files.mjs +56 -125
- package/src/modules/i18n.mjs +17 -121
- package/src/modules/idempotency.test.mjs +180 -0
- package/src/modules/logger.mjs +0 -9
- package/src/modules/probes.test.mjs +204 -0
- package/src/modules/queue.mjs +325 -440
- package/src/modules/rate-limit.mjs +36 -76
- package/src/modules/rbac.mjs +39 -78
- package/src/modules/registry.mjs +22 -35
- package/src/modules/scheduler.mjs +51 -171
- package/src/modules/shared/files-runtime-wiring.mjs +81 -0
- package/src/modules/shared/nest-runtime-wiring.mjs +110 -0
- package/src/modules/shared/patch-utils.mjs +29 -1
- package/src/modules/shared/probes.mjs +235 -0
- package/src/modules/sync-integrations.mjs +109 -396
- package/src/modules/sync-integrations.test.mjs +141 -0
- package/src/run-add-module.test.mjs +154 -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 +65 -281
- package/templates/module-fragments/{jwt-auth → accounts}/00_title.md +2 -1
- package/templates/module-fragments/{jwt-auth → accounts}/10_overview.md +5 -5
- package/templates/module-fragments/accounts/20_scope.md +29 -0
- package/templates/module-fragments/accounts/90_status_implemented.md +8 -0
- package/templates/module-fragments/accounts/90_status_planned.md +7 -0
- package/templates/module-fragments/rbac/30_what_it_adds.md +3 -2
- package/templates/module-fragments/rbac/40_how_it_works.md +2 -1
- package/templates/module-fragments/rbac/50_how_to_use.md +2 -1
- package/templates/module-fragments/swagger/20_scope.md +2 -1
- package/templates/module-presets/accounts/apps/api/prisma/migrations/0002_accounts_core/migration.sql +97 -0
- package/templates/module-presets/accounts/apps/api/src/accounts/forgeon-accounts-db-prisma.module.ts +17 -0
- package/templates/module-presets/accounts/apps/api/src/accounts/prisma-accounts-persistence.store.ts +332 -0
- package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/package.json +5 -5
- package/templates/module-presets/accounts/packages/accounts-api/src/accounts-email.port.ts +13 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/accounts-persistence.port.ts +67 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/accounts-rbac.port.ts +14 -0
- package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/src/auth-config.loader.ts +7 -7
- package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/src/auth-config.service.ts +7 -7
- package/templates/module-presets/accounts/packages/accounts-api/src/auth-core.service.ts +318 -0
- package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/src/auth-env.schema.ts +4 -4
- package/templates/module-presets/accounts/packages/accounts-api/src/auth-jwt.service.ts +58 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/auth-password.service.ts +21 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/auth.controller.ts +93 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/auth.service.ts +48 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/auth.types.ts +17 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/dto/change-password.dto.ts +13 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/dto/confirm-password-reset.dto.ts +12 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/dto/index.ts +10 -0
- package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/src/dto/login.dto.ts +1 -1
- package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/src/dto/refresh.dto.ts +1 -1
- package/templates/module-presets/accounts/packages/accounts-api/src/dto/register.dto.ts +23 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/dto/request-password-reset.dto.ts +7 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/dto/update-user-profile.dto.ts +16 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/dto/update-user-settings.dto.ts +16 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/dto/update-user.dto.ts +8 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/dto/verify-email.dto.ts +8 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/forgeon-accounts.module.ts +82 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/index.ts +24 -0
- package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/src/jwt.strategy.ts +3 -3
- package/templates/module-presets/accounts/packages/accounts-api/src/owner-access.guard.ts +39 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/users-config.ts +13 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/users.controller.ts +65 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/users.service.ts +87 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/users.types.ts +65 -0
- package/templates/module-presets/{jwt-auth/packages/auth-contracts → accounts/packages/accounts-contracts}/package.json +1 -1
- package/templates/module-presets/accounts/packages/accounts-contracts/src/index.ts +119 -0
- package/templates/module-presets/files/apps/api/src/files/forgeon-files-db-prisma.module.ts +17 -0
- package/templates/module-presets/files/apps/api/src/files/prisma-files-persistence.store.ts +164 -0
- package/templates/module-presets/files/packages/files/package.json +1 -2
- package/templates/module-presets/files/packages/files/src/files.ports.ts +107 -0
- package/templates/module-presets/files/packages/files/src/files.service.ts +81 -395
- package/templates/module-presets/files/packages/files/src/forgeon-files.module.ts +126 -2
- package/templates/module-presets/files/packages/files/src/index.ts +2 -1
- package/templates/module-presets/files-local/packages/files-local/src/forgeon-files-local-storage.module.ts +18 -0
- package/templates/module-presets/files-local/packages/files-local/src/index.ts +2 -0
- package/templates/module-presets/files-local/packages/files-local/src/local-files-storage.adapter.ts +53 -0
- package/templates/module-presets/files-quotas/packages/files-quotas/src/forgeon-files-quotas.module.ts +12 -4
- package/templates/module-presets/files-s3/packages/files-s3/src/forgeon-files-s3-storage.module.ts +18 -0
- package/templates/module-presets/files-s3/packages/files-s3/src/index.ts +2 -0
- package/templates/module-presets/files-s3/packages/files-s3/src/s3-files-storage.adapter.ts +130 -0
- 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/src/modules/jwt-auth.mjs +0 -390
- 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-fragments/jwt-auth/20_scope.md +0 -19
- package/templates/module-fragments/jwt-auth/90_status_implemented.md +0 -8
- package/templates/module-fragments/jwt-auth/90_status_planned.md +0 -3
- package/templates/module-presets/jwt-auth/apps/api/prisma/migrations/0002_auth_refresh_token_hash/migration.sql +0 -3
- package/templates/module-presets/jwt-auth/apps/api/src/auth/prisma-auth-refresh-token.store.ts +0 -36
- package/templates/module-presets/jwt-auth/packages/auth-api/src/auth-refresh-token.store.ts +0 -23
- package/templates/module-presets/jwt-auth/packages/auth-api/src/auth.controller.ts +0 -71
- package/templates/module-presets/jwt-auth/packages/auth-api/src/auth.service.ts +0 -175
- package/templates/module-presets/jwt-auth/packages/auth-api/src/auth.types.ts +0 -6
- package/templates/module-presets/jwt-auth/packages/auth-api/src/dto/index.ts +0 -2
- package/templates/module-presets/jwt-auth/packages/auth-api/src/forgeon-auth.module.ts +0 -47
- package/templates/module-presets/jwt-auth/packages/auth-api/src/index.ts +0 -12
- package/templates/module-presets/jwt-auth/packages/auth-contracts/src/index.ts +0 -47
- package/templates/module-presets/logger/packages/logger/src/http-logging.interceptor.ts +0 -94
- /package/templates/module-presets/{jwt-auth/packages/auth-api/src/jwt-auth.guard.ts → accounts/packages/accounts-api/src/access-token.guard.ts} +0 -0
- /package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/src/auth-config.module.ts +0 -0
- /package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/tsconfig.json +0 -0
- /package/templates/module-presets/{jwt-auth/packages/auth-contracts → accounts/packages/accounts-contracts}/tsconfig.json +0 -0
package/src/modules/i18n.mjs
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
ensureValidatorSchema,
|
|
14
14
|
upsertEnvLines,
|
|
15
15
|
} from './shared/patch-utils.mjs';
|
|
16
|
+
import { ensureWebProbeDefinition, readManagedWebProbeDefinitions } from './shared/probes.mjs';
|
|
16
17
|
|
|
17
18
|
function copyFromBase(packageRoot, targetRoot, relativePath) {
|
|
18
19
|
const source = path.join(packageRoot, 'templates', 'base', relativePath);
|
|
@@ -39,10 +40,6 @@ function patchApiDockerfile(targetRoot) {
|
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
let content = fs.readFileSync(dockerfilePath, 'utf8').replace(/\r\n/g, '\n');
|
|
42
|
-
content = content.replace(
|
|
43
|
-
/^COPY package\.json pnpm-workspace\.yaml tsconfig\.base\.json \.\/$/m,
|
|
44
|
-
'COPY package.json pnpm-workspace.yaml tsconfig.base.json tsconfig.base.node.json tsconfig.base.esm.json ./',
|
|
45
|
-
);
|
|
46
43
|
|
|
47
44
|
content = ensureLineAfter(
|
|
48
45
|
content,
|
|
@@ -95,22 +92,6 @@ function patchProxyDockerfile(filePath) {
|
|
|
95
92
|
}
|
|
96
93
|
|
|
97
94
|
let content = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
|
|
98
|
-
|
|
99
|
-
content = ensureLineAfter(
|
|
100
|
-
content,
|
|
101
|
-
'COPY package.json pnpm-workspace.yaml ./',
|
|
102
|
-
'COPY tsconfig.base.json ./',
|
|
103
|
-
);
|
|
104
|
-
content = ensureLineAfter(
|
|
105
|
-
content,
|
|
106
|
-
'COPY tsconfig.base.json ./',
|
|
107
|
-
'COPY tsconfig.base.node.json ./',
|
|
108
|
-
);
|
|
109
|
-
content = ensureLineAfter(
|
|
110
|
-
content,
|
|
111
|
-
'COPY tsconfig.base.node.json ./',
|
|
112
|
-
'COPY tsconfig.base.esm.json ./',
|
|
113
|
-
);
|
|
114
95
|
content = ensureLineAfter(
|
|
115
96
|
content,
|
|
116
97
|
'COPY apps/web/package.json apps/web/package.json',
|
|
@@ -449,118 +430,32 @@ Module env:
|
|
|
449
430
|
fs.writeFileSync(readmePath, `${content.trimEnd()}\n`, 'utf8');
|
|
450
431
|
}
|
|
451
432
|
|
|
452
|
-
function
|
|
453
|
-
if (!
|
|
433
|
+
function restoreManagedWebProbes(targetRoot, definitions) {
|
|
434
|
+
if (!Array.isArray(definitions) || definitions.length === 0) {
|
|
454
435
|
return;
|
|
455
436
|
}
|
|
456
437
|
|
|
457
|
-
const
|
|
458
|
-
if (!fs.existsSync(
|
|
438
|
+
const probesFilePath = path.join(targetRoot, 'apps', 'web', 'src', 'probes.ts');
|
|
439
|
+
if (!fs.existsSync(probesFilePath)) {
|
|
459
440
|
return;
|
|
460
441
|
}
|
|
461
442
|
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
if (content.includes(stateLine)) {
|
|
466
|
-
return;
|
|
467
|
-
}
|
|
468
|
-
const anchors = [
|
|
469
|
-
' const [rbacProbeResult, setRbacProbeResult] = useState<ProbeResult | null>(null);',
|
|
470
|
-
' const [rateLimitProbeResult, setRateLimitProbeResult] = useState<ProbeResult | null>(null);',
|
|
471
|
-
' const [authProbeResult, setAuthProbeResult] = useState<ProbeResult | null>(null);',
|
|
472
|
-
' const [dbProbeResult, setDbProbeResult] = useState<ProbeResult | null>(null);',
|
|
473
|
-
' const [validationProbeResult, setValidationProbeResult] = useState<ProbeResult | null>(null);',
|
|
474
|
-
];
|
|
475
|
-
const anchor = anchors.find((line) => content.includes(line));
|
|
476
|
-
if (anchor) {
|
|
477
|
-
content = ensureLineAfter(content, anchor, stateLine);
|
|
478
|
-
}
|
|
479
|
-
};
|
|
480
|
-
|
|
481
|
-
const ensureProbeButton = (buttonText, buttonCode) => {
|
|
482
|
-
if (content.includes(buttonText)) {
|
|
483
|
-
return;
|
|
484
|
-
}
|
|
485
|
-
const actionsStart = content.indexOf('<div className="actions">');
|
|
486
|
-
if (actionsStart < 0) {
|
|
487
|
-
return;
|
|
488
|
-
}
|
|
489
|
-
const actionsEnd = content.indexOf('\n </div>', actionsStart);
|
|
490
|
-
if (actionsEnd < 0) {
|
|
491
|
-
return;
|
|
492
|
-
}
|
|
493
|
-
content = `${content.slice(0, actionsEnd)}\n${buttonCode}${content.slice(actionsEnd)}`;
|
|
494
|
-
};
|
|
495
|
-
|
|
496
|
-
const ensureProbeResult = (resultLine) => {
|
|
497
|
-
if (content.includes(resultLine)) {
|
|
498
|
-
return;
|
|
499
|
-
}
|
|
500
|
-
const networkLine = ' {networkError ? <p className="error">{networkError}</p> : null}';
|
|
501
|
-
if (content.includes(networkLine)) {
|
|
502
|
-
content = content.replace(networkLine, `${resultLine}\n${networkLine}`);
|
|
503
|
-
return;
|
|
504
|
-
}
|
|
505
|
-
const anchors = [
|
|
506
|
-
" {renderResult('RBAC probe response', rbacProbeResult)}",
|
|
507
|
-
" {renderResult('Rate limit probe response', rateLimitProbeResult)}",
|
|
508
|
-
" {renderResult('Auth probe response', authProbeResult)}",
|
|
509
|
-
" {renderResult('DB probe response', dbProbeResult)}",
|
|
510
|
-
" {renderResult('Validation probe response', validationProbeResult)}",
|
|
511
|
-
];
|
|
512
|
-
const anchor = anchors.find((line) => content.includes(line));
|
|
513
|
-
if (anchor) {
|
|
514
|
-
content = ensureLineAfter(content, anchor, resultLine);
|
|
515
|
-
}
|
|
443
|
+
const probeTargets = {
|
|
444
|
+
allowWeb: true,
|
|
445
|
+
probesFilePath,
|
|
516
446
|
};
|
|
517
447
|
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
);
|
|
524
|
-
ensureProbeResult(" {renderResult('DB probe response', dbProbeResult)}");
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
if (previousAppContent.includes('Check JWT auth probe')) {
|
|
528
|
-
ensureProbeState(' const [authProbeResult, setAuthProbeResult] = useState<ProbeResult | null>(null);');
|
|
529
|
-
ensureProbeButton(
|
|
530
|
-
'Check JWT auth probe',
|
|
531
|
-
" <button onClick={() => runProbe(setAuthProbeResult, '/health/auth')}>Check JWT auth probe</button>",
|
|
532
|
-
);
|
|
533
|
-
ensureProbeResult(" {renderResult('Auth probe response', authProbeResult)}");
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
if (previousAppContent.includes('Check rate limit (click repeatedly)')) {
|
|
537
|
-
ensureProbeState(
|
|
538
|
-
' const [rateLimitProbeResult, setRateLimitProbeResult] = useState<ProbeResult | null>(null);',
|
|
539
|
-
);
|
|
540
|
-
ensureProbeButton(
|
|
541
|
-
'Check rate limit (click repeatedly)',
|
|
542
|
-
" <button onClick={() => runProbe(setRateLimitProbeResult, '/health/rate-limit')}>\n Check rate limit (click repeatedly)\n </button>",
|
|
543
|
-
);
|
|
544
|
-
ensureProbeResult(" {renderResult('Rate limit probe response', rateLimitProbeResult)}");
|
|
448
|
+
for (const definition of definitions) {
|
|
449
|
+
ensureWebProbeDefinition({
|
|
450
|
+
targetRoot,
|
|
451
|
+
probeTargets,
|
|
452
|
+
definition,
|
|
453
|
+
});
|
|
545
454
|
}
|
|
546
|
-
|
|
547
|
-
if (previousAppContent.includes('Check RBAC access')) {
|
|
548
|
-
ensureProbeState(' const [rbacProbeResult, setRbacProbeResult] = useState<ProbeResult | null>(null);');
|
|
549
|
-
ensureProbeButton(
|
|
550
|
-
'Check RBAC access',
|
|
551
|
-
" <button\n onClick={() =>\n runProbe(setRbacProbeResult, '/health/rbac', {\n headers: { 'x-forgeon-permissions': 'health.rbac' },\n })\n }\n >\n Check RBAC access\n </button>",
|
|
552
|
-
);
|
|
553
|
-
ensureProbeResult(" {renderResult('RBAC probe response', rbacProbeResult)}");
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
fs.writeFileSync(filePath, `${content.trimEnd()}\n`, 'utf8');
|
|
557
455
|
}
|
|
558
456
|
|
|
559
457
|
export function applyI18nModule({ packageRoot, targetRoot }) {
|
|
560
|
-
const
|
|
561
|
-
const previousAppContent = fs.existsSync(existingWebAppPath)
|
|
562
|
-
? fs.readFileSync(existingWebAppPath, 'utf8')
|
|
563
|
-
: '';
|
|
458
|
+
const previousProbeDefinitions = readManagedWebProbeDefinitions(targetRoot);
|
|
564
459
|
|
|
565
460
|
copyFromBase(packageRoot, targetRoot, path.join('scripts', 'i18n-add.mjs'));
|
|
566
461
|
copyFromBase(packageRoot, targetRoot, path.join('packages', 'i18n'));
|
|
@@ -568,10 +463,11 @@ export function applyI18nModule({ packageRoot, targetRoot }) {
|
|
|
568
463
|
|
|
569
464
|
copyFromPreset(packageRoot, targetRoot, path.join('packages', 'i18n-contracts'));
|
|
570
465
|
copyFromPreset(packageRoot, targetRoot, path.join('packages', 'i18n-web'));
|
|
466
|
+
copyFromBase(packageRoot, targetRoot, path.join('apps', 'web', 'src', 'probes.ts'));
|
|
571
467
|
copyFromPreset(packageRoot, targetRoot, path.join('apps', 'web', 'src', 'App.tsx'));
|
|
572
468
|
copyFromPreset(packageRoot, targetRoot, path.join('apps', 'web', 'src', 'i18n.ts'));
|
|
573
469
|
copyFromPreset(packageRoot, targetRoot, path.join('apps', 'web', 'src', 'main.tsx'));
|
|
574
|
-
|
|
470
|
+
restoreManagedWebProbes(targetRoot, previousProbeDefinitions);
|
|
575
471
|
|
|
576
472
|
patchI18nPackage(targetRoot);
|
|
577
473
|
patchApiPackage(targetRoot);
|
|
@@ -0,0 +1,180 @@
|
|
|
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 { addModule } from './executor.mjs';
|
|
8
|
+
import { scaffoldProject } from '../core/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
|
+
function scaffoldBaseProject({ packageRoot, targetRoot, projectName, dbPrismaEnabled, proxy = 'caddy' }) {
|
|
19
|
+
const templateRoot = path.join(packageRoot, 'templates', 'base');
|
|
20
|
+
scaffoldProject({
|
|
21
|
+
templateRoot,
|
|
22
|
+
packageRoot,
|
|
23
|
+
targetRoot,
|
|
24
|
+
projectName,
|
|
25
|
+
frontend: 'react',
|
|
26
|
+
db: 'prisma',
|
|
27
|
+
dbPrismaEnabled,
|
|
28
|
+
i18nEnabled: false,
|
|
29
|
+
proxy,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function readProjectSnapshot(projectRoot) {
|
|
34
|
+
const snapshot = {};
|
|
35
|
+
const queue = [projectRoot];
|
|
36
|
+
const skipDirs = new Set(['node_modules', '.git', 'dist', 'build']);
|
|
37
|
+
|
|
38
|
+
while (queue.length > 0) {
|
|
39
|
+
const currentDir = queue.shift();
|
|
40
|
+
const entries = fs
|
|
41
|
+
.readdirSync(currentDir, { withFileTypes: true })
|
|
42
|
+
.sort((left, right) => left.name.localeCompare(right.name));
|
|
43
|
+
|
|
44
|
+
for (const entry of entries) {
|
|
45
|
+
const nextPath = path.join(currentDir, entry.name);
|
|
46
|
+
if (entry.isDirectory()) {
|
|
47
|
+
if (!skipDirs.has(entry.name)) {
|
|
48
|
+
queue.push(nextPath);
|
|
49
|
+
}
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (!entry.isFile()) {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
snapshot[path.relative(projectRoot, nextPath)] = readFile(nextPath);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return Object.fromEntries(
|
|
62
|
+
Object.entries(snapshot).sort(([left], [right]) => left.localeCompare(right)),
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
describe('addModule idempotency', () => {
|
|
67
|
+
const modulesDir = path.dirname(fileURLToPath(import.meta.url));
|
|
68
|
+
const packageRoot = path.resolve(modulesDir, '..', '..');
|
|
69
|
+
const scenarios = [
|
|
70
|
+
{
|
|
71
|
+
name: 'logger',
|
|
72
|
+
moduleId: 'logger',
|
|
73
|
+
dbPrismaEnabled: true,
|
|
74
|
+
setup: [],
|
|
75
|
+
verify(projectRoot) {
|
|
76
|
+
assert.equal(fs.existsSync(path.join(projectRoot, 'packages', 'logger', 'package.json')), true);
|
|
77
|
+
assert.match(
|
|
78
|
+
readFile(path.join(projectRoot, 'apps', 'api', 'src', 'main.ts')),
|
|
79
|
+
/app\.useLogger\(app\.get\(ForgeonLoggerService\)\);/,
|
|
80
|
+
);
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
name: 'accounts',
|
|
85
|
+
moduleId: 'accounts',
|
|
86
|
+
dbPrismaEnabled: true,
|
|
87
|
+
setup: [],
|
|
88
|
+
verify(projectRoot) {
|
|
89
|
+
assert.equal(fs.existsSync(path.join(projectRoot, 'packages', 'accounts-api', 'package.json')), true);
|
|
90
|
+
assert.equal(
|
|
91
|
+
fs.existsSync(
|
|
92
|
+
path.join(projectRoot, 'apps', 'api', 'src', 'accounts', 'prisma-accounts-persistence.store.ts'),
|
|
93
|
+
),
|
|
94
|
+
true,
|
|
95
|
+
);
|
|
96
|
+
assert.match(
|
|
97
|
+
readFile(path.join(projectRoot, 'apps', 'api', 'src', 'health', 'health.controller.ts')),
|
|
98
|
+
/@Get\('auth'\)/,
|
|
99
|
+
);
|
|
100
|
+
assert.match(
|
|
101
|
+
readFile(path.join(projectRoot, 'apps', 'api', 'src', 'app.module.ts')),
|
|
102
|
+
/ForgeonAccountsModule\.register\(/,
|
|
103
|
+
);
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
name: 'files-local',
|
|
108
|
+
moduleId: 'files-local',
|
|
109
|
+
dbPrismaEnabled: true,
|
|
110
|
+
setup: [],
|
|
111
|
+
verify(projectRoot) {
|
|
112
|
+
assert.equal(fs.existsSync(path.join(projectRoot, 'packages', 'files-local', 'package.json')), true);
|
|
113
|
+
assert.match(readFile(path.join(projectRoot, 'apps', 'api', '.env.example')), /FILES_LOCAL_ROOT=storage\/uploads/);
|
|
114
|
+
assert.match(readFile(path.join(projectRoot, 'infra', 'docker', 'compose.yml')), /^\s{2}files_data:\s*$/m);
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
name: 'files',
|
|
119
|
+
moduleId: 'files',
|
|
120
|
+
dbPrismaEnabled: true,
|
|
121
|
+
setup: ['files-local'],
|
|
122
|
+
verify(projectRoot) {
|
|
123
|
+
assert.equal(fs.existsSync(path.join(projectRoot, 'packages', 'files', 'package.json')), true);
|
|
124
|
+
assert.match(readFile(path.join(projectRoot, 'apps', 'api', '.env.example')), /FILES_STORAGE_DRIVER=local/);
|
|
125
|
+
assert.match(
|
|
126
|
+
readFile(path.join(projectRoot, 'apps', 'api', 'src', 'health', 'health.controller.ts')),
|
|
127
|
+
/@Post\('files'\)/,
|
|
128
|
+
);
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
name: 'queue',
|
|
133
|
+
moduleId: 'queue',
|
|
134
|
+
dbPrismaEnabled: false,
|
|
135
|
+
setup: [],
|
|
136
|
+
verify(projectRoot) {
|
|
137
|
+
assert.equal(fs.existsSync(path.join(projectRoot, 'packages', 'queue', 'package.json')), true);
|
|
138
|
+
assert.match(readFile(path.join(projectRoot, 'infra', 'docker', 'compose.yml')), /^\s{2}redis:\s*$/m);
|
|
139
|
+
assert.match(
|
|
140
|
+
readFile(path.join(projectRoot, 'apps', 'api', 'src', 'health', 'health.controller.ts')),
|
|
141
|
+
/@Get\('queue'\)/,
|
|
142
|
+
);
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
];
|
|
146
|
+
|
|
147
|
+
it('reapplying representative implemented modules is a no-op on project files', () => {
|
|
148
|
+
for (const scenario of scenarios) {
|
|
149
|
+
const tempRoot = makeTempDir(`forgeon-idempotent-${scenario.name}-`);
|
|
150
|
+
const projectRoot = path.join(tempRoot, `demo-${scenario.name}`);
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
scaffoldBaseProject({
|
|
154
|
+
packageRoot,
|
|
155
|
+
targetRoot: projectRoot,
|
|
156
|
+
projectName: `demo-${scenario.name}`,
|
|
157
|
+
dbPrismaEnabled: scenario.dbPrismaEnabled,
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
for (const moduleId of scenario.setup) {
|
|
161
|
+
addModule({ moduleId, targetRoot: projectRoot, packageRoot });
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const firstResult = addModule({ moduleId: scenario.moduleId, targetRoot: projectRoot, packageRoot });
|
|
165
|
+
assert.equal(firstResult.applied, true);
|
|
166
|
+
scenario.verify(projectRoot);
|
|
167
|
+
|
|
168
|
+
const before = readProjectSnapshot(projectRoot);
|
|
169
|
+
const secondResult = addModule({ moduleId: scenario.moduleId, targetRoot: projectRoot, packageRoot });
|
|
170
|
+
const after = readProjectSnapshot(projectRoot);
|
|
171
|
+
|
|
172
|
+
assert.equal(secondResult.applied, true);
|
|
173
|
+
assert.deepEqual(after, before);
|
|
174
|
+
scenario.verify(projectRoot);
|
|
175
|
+
} finally {
|
|
176
|
+
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
});
|
package/src/modules/logger.mjs
CHANGED
|
@@ -44,10 +44,6 @@ function patchMain(targetRoot) {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
let content = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
|
|
47
|
-
content = content.replace(
|
|
48
|
-
"import { ForgeonHttpLoggingInterceptor, ForgeonLoggerService } from '@forgeon/logger';",
|
|
49
|
-
"import { ForgeonLoggerService } from '@forgeon/logger';",
|
|
50
|
-
);
|
|
51
47
|
content = ensureLineBefore(
|
|
52
48
|
content,
|
|
53
49
|
"import { NestFactory } from '@nestjs/core';",
|
|
@@ -59,11 +55,6 @@ function patchMain(targetRoot) {
|
|
|
59
55
|
'const app = await NestFactory.create(AppModule, { bufferLogs: true });',
|
|
60
56
|
);
|
|
61
57
|
|
|
62
|
-
content = content.replace(
|
|
63
|
-
/\n\s*app\.useGlobalInterceptors\(app\.get\(ForgeonHttpLoggingInterceptor\)\);\s*/g,
|
|
64
|
-
'\n',
|
|
65
|
-
);
|
|
66
|
-
|
|
67
58
|
if (!content.includes('app.useLogger(app.get(ForgeonLoggerService));')) {
|
|
68
59
|
content = content.replace(
|
|
69
60
|
' const coreConfigService = app.get(CoreConfigService);',
|
|
@@ -0,0 +1,204 @@
|
|
|
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 { addModule } from './executor.mjs';
|
|
8
|
+
import { scaffoldProject } from '../core/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
|
+
function writeJson(filePath, value) {
|
|
19
|
+
fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function scaffoldBaseProject({ packageRoot, targetRoot, projectName }) {
|
|
23
|
+
const templateRoot = path.join(packageRoot, 'templates', 'base');
|
|
24
|
+
scaffoldProject({
|
|
25
|
+
templateRoot,
|
|
26
|
+
packageRoot,
|
|
27
|
+
targetRoot,
|
|
28
|
+
projectName,
|
|
29
|
+
frontend: 'react',
|
|
30
|
+
db: 'prisma',
|
|
31
|
+
dbPrismaEnabled: false,
|
|
32
|
+
i18nEnabled: false,
|
|
33
|
+
proxy: 'caddy',
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function readManagedProbeIds(projectRoot) {
|
|
38
|
+
const probesTs = readFile(path.join(projectRoot, 'apps', 'web', 'src', 'probes.ts'));
|
|
39
|
+
const match = probesTs.match(/forgeon:module-probes:start(?:\n([\s\S]*?))?\n \/\/ forgeon:module-probes:end/);
|
|
40
|
+
const block = match?.[1] ?? '';
|
|
41
|
+
return [...block.matchAll(/"id": "([^"]+)"/g)].map((item) => item[1]);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function captureLogs(work) {
|
|
45
|
+
const lines = [];
|
|
46
|
+
const originalLog = console.log;
|
|
47
|
+
console.log = (...args) => {
|
|
48
|
+
lines.push(args.join(' '));
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
work();
|
|
53
|
+
} finally {
|
|
54
|
+
console.log = originalLog;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return lines.join('\n');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
describe('probe wiring', () => {
|
|
61
|
+
const modulesDir = path.dirname(fileURLToPath(import.meta.url));
|
|
62
|
+
const packageRoot = path.resolve(modulesDir, '..', '..');
|
|
63
|
+
|
|
64
|
+
it('skips API and web probe wiring when probes are disabled in package.json', () => {
|
|
65
|
+
const tempRoot = makeTempDir('forgeon-probes-disabled-');
|
|
66
|
+
const projectRoot = path.join(tempRoot, 'demo-probes-disabled');
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
scaffoldBaseProject({
|
|
70
|
+
packageRoot,
|
|
71
|
+
targetRoot: projectRoot,
|
|
72
|
+
projectName: 'demo-probes-disabled',
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const packagePath = path.join(projectRoot, 'package.json');
|
|
76
|
+
const packageJson = JSON.parse(readFile(packagePath));
|
|
77
|
+
packageJson.forgeon.diagnostics.probes.enabled = false;
|
|
78
|
+
writeJson(packagePath, packageJson);
|
|
79
|
+
|
|
80
|
+
const output = captureLogs(() => {
|
|
81
|
+
addModule({ moduleId: 'queue', targetRoot: projectRoot, packageRoot });
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const healthController = readFile(path.join(projectRoot, 'apps', 'api', 'src', 'health', 'health.controller.ts'));
|
|
85
|
+
assert.doesNotMatch(healthController, /@Get\('queue'\)/);
|
|
86
|
+
assert.doesNotMatch(readFile(path.join(projectRoot, 'apps', 'web', 'src', 'probes.ts')), /"id": "queue"/);
|
|
87
|
+
assert.match(output, /forgeon\.diagnostics\.probes\.enabled=false/);
|
|
88
|
+
} finally {
|
|
89
|
+
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('skips probe wiring entirely when HealthController is missing', () => {
|
|
94
|
+
const tempRoot = makeTempDir('forgeon-probes-no-health-');
|
|
95
|
+
const projectRoot = path.join(tempRoot, 'demo-probes-no-health');
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
scaffoldBaseProject({
|
|
99
|
+
packageRoot,
|
|
100
|
+
targetRoot: projectRoot,
|
|
101
|
+
projectName: 'demo-probes-no-health',
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
fs.rmSync(path.join(projectRoot, 'apps', 'api', 'src', 'health', 'health.controller.ts'));
|
|
105
|
+
|
|
106
|
+
const output = captureLogs(() => {
|
|
107
|
+
addModule({ moduleId: 'queue', targetRoot: projectRoot, packageRoot });
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
assert.equal(
|
|
111
|
+
fs.existsSync(path.join(projectRoot, 'apps', 'api', 'src', 'health', 'health.controller.ts')),
|
|
112
|
+
false,
|
|
113
|
+
);
|
|
114
|
+
assert.doesNotMatch(readFile(path.join(projectRoot, 'apps', 'web', 'src', 'probes.ts')), /"id": "queue"/);
|
|
115
|
+
assert.match(output, /health\.controller\.ts is missing/);
|
|
116
|
+
assert.doesNotMatch(output, /App\.tsx/);
|
|
117
|
+
assert.doesNotMatch(output, /#probes/);
|
|
118
|
+
} finally {
|
|
119
|
+
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('adds only API probes when App.tsx is missing', () => {
|
|
124
|
+
const tempRoot = makeTempDir('forgeon-probes-no-app-');
|
|
125
|
+
const projectRoot = path.join(tempRoot, 'demo-probes-no-app');
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
scaffoldBaseProject({
|
|
129
|
+
packageRoot,
|
|
130
|
+
targetRoot: projectRoot,
|
|
131
|
+
projectName: 'demo-probes-no-app',
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
fs.rmSync(path.join(projectRoot, 'apps', 'web', 'src', 'App.tsx'));
|
|
135
|
+
|
|
136
|
+
const output = captureLogs(() => {
|
|
137
|
+
addModule({ moduleId: 'queue', targetRoot: projectRoot, packageRoot });
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const healthController = readFile(path.join(projectRoot, 'apps', 'api', 'src', 'health', 'health.controller.ts'));
|
|
141
|
+
assert.match(healthController, /@Get\('queue'\)/);
|
|
142
|
+
assert.doesNotMatch(readFile(path.join(projectRoot, 'apps', 'web', 'src', 'probes.ts')), /"id": "queue"/);
|
|
143
|
+
assert.match(output, /App\.tsx is missing/);
|
|
144
|
+
} finally {
|
|
145
|
+
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('adds only API probes when the #probes container is missing', () => {
|
|
150
|
+
const tempRoot = makeTempDir('forgeon-probes-no-container-');
|
|
151
|
+
const projectRoot = path.join(tempRoot, 'demo-probes-no-container');
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
scaffoldBaseProject({
|
|
155
|
+
packageRoot,
|
|
156
|
+
targetRoot: projectRoot,
|
|
157
|
+
projectName: 'demo-probes-no-container',
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
const appPath = path.join(projectRoot, 'apps', 'web', 'src', 'App.tsx');
|
|
161
|
+
const appTsx = readFile(appPath).replace('id="probes"', 'id="diagnostics"');
|
|
162
|
+
fs.writeFileSync(appPath, appTsx, 'utf8');
|
|
163
|
+
|
|
164
|
+
const output = captureLogs(() => {
|
|
165
|
+
addModule({ moduleId: 'rate-limit', targetRoot: projectRoot, packageRoot });
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
const healthController = readFile(path.join(projectRoot, 'apps', 'api', 'src', 'health', 'health.controller.ts'));
|
|
169
|
+
assert.match(healthController, /@Get\('rate-limit'\)/);
|
|
170
|
+
assert.doesNotMatch(readFile(path.join(projectRoot, 'apps', 'web', 'src', 'probes.ts')), /"id": "rate-limit"/);
|
|
171
|
+
assert.match(output, /#probes container/);
|
|
172
|
+
} finally {
|
|
173
|
+
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('keeps managed probe order stable regardless of install order', () => {
|
|
178
|
+
const tempRoot = makeTempDir('forgeon-probes-order-');
|
|
179
|
+
const projectRoot = path.join(tempRoot, 'demo-probes-order');
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
scaffoldBaseProject({
|
|
183
|
+
packageRoot,
|
|
184
|
+
targetRoot: projectRoot,
|
|
185
|
+
projectName: 'demo-probes-order',
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
addModule({ moduleId: 'queue', targetRoot: projectRoot, packageRoot });
|
|
189
|
+
addModule({ moduleId: 'scheduler', targetRoot: projectRoot, packageRoot });
|
|
190
|
+
addModule({ moduleId: 'db-prisma', targetRoot: projectRoot, packageRoot });
|
|
191
|
+
addModule({ moduleId: 'accounts', targetRoot: projectRoot, packageRoot });
|
|
192
|
+
addModule({ moduleId: 'rate-limit', targetRoot: projectRoot, packageRoot });
|
|
193
|
+
|
|
194
|
+
assert.deepEqual(readManagedProbeIds(projectRoot), ['db', 'auth', 'rate-limit', 'queue', 'scheduler']);
|
|
195
|
+
} finally {
|
|
196
|
+
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
|