create-forgeon 0.3.19 → 0.3.21
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 +1 -1
- package/src/cli/add-help.mjs +3 -2
- package/src/core/docs.test.mjs +1 -0
- package/src/core/scaffold.test.mjs +1 -0
- package/src/modules/accounts.mjs +9 -18
- package/src/modules/dependencies.mjs +153 -4
- package/src/modules/dependencies.test.mjs +58 -0
- package/src/modules/executor.test.mjs +544 -515
- package/src/modules/files-access.mjs +375 -375
- package/src/modules/files-image.mjs +512 -510
- package/src/modules/files-quotas.mjs +365 -365
- package/src/modules/files.mjs +5 -6
- package/src/modules/idempotency.test.mjs +3 -2
- package/src/modules/registry.mjs +20 -0
- package/src/modules/shared/files-runtime-wiring.mjs +13 -10
- package/src/run-add-module.mjs +39 -26
- package/src/run-add-module.test.mjs +228 -152
- package/src/run-scan-integrations.mjs +1 -0
- package/templates/base/package.json +1 -0
- package/templates/module-presets/accounts/packages/accounts-api/package.json +1 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/auth-core.service.ts +15 -19
- package/templates/module-presets/accounts/{apps/api/src/accounts/prisma-accounts-persistence.store.ts → packages/accounts-api/src/auth.store.ts} +44 -166
- package/templates/module-presets/accounts/packages/accounts-api/src/forgeon-accounts.module.ts +7 -1
- package/templates/module-presets/accounts/packages/accounts-api/src/index.ts +3 -4
- package/templates/module-presets/accounts/packages/accounts-api/src/users.service.ts +10 -11
- package/templates/module-presets/accounts/packages/accounts-api/src/users.store.ts +113 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/users.types.ts +48 -0
- package/templates/module-presets/files/packages/files/package.json +1 -0
- package/templates/module-presets/files/packages/files/src/files.ports.ts +0 -95
- package/templates/module-presets/files/packages/files/src/files.service.ts +43 -36
- package/templates/module-presets/files/{apps/api/src/files/prisma-files-persistence.store.ts → packages/files/src/files.store.ts} +77 -13
- package/templates/module-presets/files/packages/files/src/forgeon-files.module.ts +7 -116
- package/templates/module-presets/files/packages/files/src/index.ts +1 -0
- package/templates/module-presets/files-quotas/packages/files-quotas/package.json +20 -20
- package/templates/module-presets/files-quotas/packages/files-quotas/src/files-quotas.service.ts +118 -118
- package/templates/module-presets/files-quotas/packages/files-quotas/src/forgeon-files-quotas.module.ts +18 -18
- package/templates/module-presets/accounts/packages/accounts-api/src/accounts-persistence.port.ts +0 -67
- package/templates/module-presets/files/apps/api/src/files/forgeon-files-db-prisma.module.ts +0 -17
package/src/modules/files.mjs
CHANGED
|
@@ -170,9 +170,9 @@ function patchAppModule(targetRoot) {
|
|
|
170
170
|
content,
|
|
171
171
|
"import { filesConfig, filesEnvSchema, ForgeonFilesModule } from '@forgeon/files';",
|
|
172
172
|
);
|
|
173
|
-
content =
|
|
174
|
-
|
|
175
|
-
|
|
173
|
+
content = content.replace(
|
|
174
|
+
/^import \{ ForgeonFilesDbPrismaModule \} from '\.\/files\/forgeon-files-db-prisma\.module';\r?\n/m,
|
|
175
|
+
'',
|
|
176
176
|
);
|
|
177
177
|
|
|
178
178
|
if (fs.existsSync(path.join(targetRoot, 'packages', 'files-local', 'package.json'))) {
|
|
@@ -188,7 +188,6 @@ function patchAppModule(targetRoot) {
|
|
|
188
188
|
|
|
189
189
|
fs.writeFileSync(filePath, `${content.trimEnd()}\n`, 'utf8');
|
|
190
190
|
}
|
|
191
|
-
|
|
192
191
|
function patchApiDockerfile(targetRoot) {
|
|
193
192
|
const dockerfilePath = path.join(targetRoot, 'apps', 'api', 'Dockerfile');
|
|
194
193
|
if (!fs.existsSync(dockerfilePath)) {
|
|
@@ -378,7 +377,7 @@ function patchReadme(targetRoot) {
|
|
|
378
377
|
|
|
379
378
|
const section = `## Files Module
|
|
380
379
|
|
|
381
|
-
The files module adds runtime file endpoints with
|
|
380
|
+
The files module adds runtime file endpoints with Prisma-backed metadata and storage-driver-aware behavior.
|
|
382
381
|
|
|
383
382
|
What it currently adds:
|
|
384
383
|
- \`@forgeon/files\` package with:
|
|
@@ -425,7 +424,6 @@ Key env:
|
|
|
425
424
|
|
|
426
425
|
export function applyFilesModule({ packageRoot, targetRoot }) {
|
|
427
426
|
copyFromPreset(packageRoot, targetRoot, path.join('packages', 'files'));
|
|
428
|
-
copyFromPreset(packageRoot, targetRoot, path.join('apps', 'api', 'src', 'files'));
|
|
429
427
|
const probeTargets = resolveProbeTargets({ targetRoot, moduleId: 'files' });
|
|
430
428
|
|
|
431
429
|
|
|
@@ -456,3 +454,4 @@ export function applyFilesModule({ packageRoot, targetRoot }) {
|
|
|
456
454
|
}
|
|
457
455
|
|
|
458
456
|
|
|
457
|
+
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
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';
|
|
@@ -91,7 +91,7 @@ describe('addModule idempotency', () => {
|
|
|
91
91
|
fs.existsSync(
|
|
92
92
|
path.join(projectRoot, 'apps', 'api', 'src', 'accounts', 'prisma-accounts-persistence.store.ts'),
|
|
93
93
|
),
|
|
94
|
-
|
|
94
|
+
false,
|
|
95
95
|
);
|
|
96
96
|
assert.match(
|
|
97
97
|
readFile(path.join(projectRoot, 'apps', 'api', 'src', 'health', 'health.controller.ts')),
|
|
@@ -178,3 +178,4 @@ describe('addModule idempotency', () => {
|
|
|
178
178
|
}
|
|
179
179
|
});
|
|
180
180
|
});
|
|
181
|
+
|
package/src/modules/registry.mjs
CHANGED
|
@@ -255,6 +255,10 @@ const MODULE_PRESETS = {
|
|
|
255
255
|
docFragments: ['00_title', '10_overview', '20_scope', '90_status_implemented'],
|
|
256
256
|
},
|
|
257
257
|
};
|
|
258
|
+
|
|
259
|
+
const RECOMMENDED_CAPABILITY_PROVIDERS = {
|
|
260
|
+
'files-storage-adapter': 'files-local',
|
|
261
|
+
};
|
|
258
262
|
|
|
259
263
|
export function listModulePresets() {
|
|
260
264
|
return Object.values(MODULE_PRESETS);
|
|
@@ -272,6 +276,22 @@ export function getCapabilityProviders(capabilityId, { implementedOnly = true }
|
|
|
272
276
|
return Array.isArray(preset.provides) && preset.provides.includes(capabilityId);
|
|
273
277
|
});
|
|
274
278
|
}
|
|
279
|
+
|
|
280
|
+
export function getRecommendedCapabilityProvider(capabilityId, providers = null) {
|
|
281
|
+
const availableProviders = Array.isArray(providers)
|
|
282
|
+
? providers
|
|
283
|
+
: getCapabilityProviders(capabilityId, { implementedOnly: true });
|
|
284
|
+
|
|
285
|
+
const recommendedProviderId = RECOMMENDED_CAPABILITY_PROVIDERS[capabilityId];
|
|
286
|
+
if (recommendedProviderId) {
|
|
287
|
+
const matchedProvider = availableProviders.find((provider) => provider.id === recommendedProviderId);
|
|
288
|
+
if (matchedProvider) {
|
|
289
|
+
return matchedProvider;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return availableProviders[0] ?? null;
|
|
294
|
+
}
|
|
275
295
|
|
|
276
296
|
export function ensureModuleExists(moduleId) {
|
|
277
297
|
const preset = getModulePreset(moduleId);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { ensureLineAfter, ensureLineBefore } from './patch-utils.mjs';
|
|
4
4
|
|
|
@@ -38,22 +38,25 @@ export function resolveFilesStorageRuntimeModule(targetRoot) {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
export function upsertFilesModuleRegistration(content, storageRuntimeModuleName = null) {
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const moduleBlock = ` ForgeonFilesModule.register({
|
|
47
|
-
imports: [${runtimeImports.join(', ')}],
|
|
48
|
-
}),`;
|
|
41
|
+
const moduleBlock = storageRuntimeModuleName
|
|
42
|
+
? ` ForgeonFilesModule.register({
|
|
43
|
+
imports: [${storageRuntimeModuleName}],
|
|
44
|
+
}),`
|
|
45
|
+
: ' ForgeonFilesModule.register(),';
|
|
49
46
|
|
|
50
47
|
if (content.includes('ForgeonFilesModule.register({')) {
|
|
51
48
|
return content.replace(
|
|
52
|
-
/ {4}ForgeonFilesModule\.register\(
|
|
49
|
+
/ {4}ForgeonFilesModule\.register\([\s\S]*? {4}\}\),/m,
|
|
53
50
|
moduleBlock,
|
|
54
51
|
);
|
|
55
52
|
}
|
|
56
53
|
|
|
54
|
+
if (content.includes(' ForgeonFilesModule.register(),')) {
|
|
55
|
+
return storageRuntimeModuleName
|
|
56
|
+
? content.replace(' ForgeonFilesModule.register(),', moduleBlock)
|
|
57
|
+
: content;
|
|
58
|
+
}
|
|
59
|
+
|
|
57
60
|
if (content.includes(' ForgeonFilesModule,')) {
|
|
58
61
|
return content.replace(' ForgeonFilesModule,', moduleBlock);
|
|
59
62
|
}
|
package/src/run-add-module.mjs
CHANGED
|
@@ -8,6 +8,7 @@ import { addModule } from './modules/executor.mjs';
|
|
|
8
8
|
import {
|
|
9
9
|
getPendingOptionalIntegrations,
|
|
10
10
|
getPendingRecommendedCompanions,
|
|
11
|
+
resolveAllModulesInstallPlan,
|
|
11
12
|
resolveModuleInstallPlan,
|
|
12
13
|
} from './modules/dependencies.mjs';
|
|
13
14
|
import { listModulePresets } from './modules/registry.mjs';
|
|
@@ -19,11 +20,10 @@ import {
|
|
|
19
20
|
import { readJson, writeJson } from './utils/fs.mjs';
|
|
20
21
|
|
|
21
22
|
function printModuleList() {
|
|
22
|
-
const modules = listModulePresets();
|
|
23
|
-
console.log('Available modules:');
|
|
24
|
-
for (const moduleItem of modules) {
|
|
25
|
-
|
|
26
|
-
console.log(`- ${moduleItem.id} (${status}) - ${moduleItem.description}`);
|
|
23
|
+
const modules = listModulePresets().filter((moduleItem) => moduleItem.implemented !== false);
|
|
24
|
+
console.log('Available modules:');
|
|
25
|
+
for (const moduleItem of modules) {
|
|
26
|
+
console.log(`- ${moduleItem.id} - ${moduleItem.description}`);
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
|
|
@@ -120,6 +120,7 @@ function ensureSyncTooling({ packageRoot, targetRoot }) {
|
|
|
120
120
|
}
|
|
121
121
|
|
|
122
122
|
packageJson.scripts['forgeon:sync-integrations'] = 'node scripts/forgeon-sync-integrations.mjs';
|
|
123
|
+
packageJson.scripts['add-all'] = 'npx create-forgeon@latest add all --project .';
|
|
123
124
|
|
|
124
125
|
writeJson(packagePath, packageJson);
|
|
125
126
|
}
|
|
@@ -230,12 +231,18 @@ export async function runAddModule(argv = process.argv.slice(2)) {
|
|
|
230
231
|
const packageRoot = path.resolve(srcDir, '..');
|
|
231
232
|
const targetRoot = path.resolve(process.cwd(), options.project);
|
|
232
233
|
const dependencyManifestStateBefore = collectDependencyManifestState(targetRoot);
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
234
|
+
const isAllModulesInstall = options.moduleId === 'all';
|
|
235
|
+
const plan = isAllModulesInstall
|
|
236
|
+
? await resolveAllModulesInstallPlan({
|
|
237
|
+
targetRoot,
|
|
238
|
+
providerSelections: options.providers,
|
|
239
|
+
})
|
|
240
|
+
: await resolveModuleInstallPlan({
|
|
241
|
+
moduleId: options.moduleId,
|
|
242
|
+
targetRoot,
|
|
243
|
+
withRequired: options.withRequired,
|
|
244
|
+
providerSelections: options.providers,
|
|
245
|
+
});
|
|
239
246
|
|
|
240
247
|
if (plan.cancelled) {
|
|
241
248
|
console.log('Installation cancelled.');
|
|
@@ -257,15 +264,19 @@ export async function runAddModule(argv = process.argv.slice(2)) {
|
|
|
257
264
|
printModuleAdded(currentResult.preset.id, currentResult.docsPath);
|
|
258
265
|
}
|
|
259
266
|
|
|
260
|
-
const pendingRecommendedCompanions =
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
267
|
+
const pendingRecommendedCompanions = isAllModulesInstall
|
|
268
|
+
? []
|
|
269
|
+
: getPendingRecommendedCompanions({
|
|
270
|
+
moduleId: options.moduleId,
|
|
271
|
+
targetRoot,
|
|
272
|
+
});
|
|
273
|
+
const selectedRecommendedCompanions = isAllModulesInstall
|
|
274
|
+
? []
|
|
275
|
+
: await chooseRecommendedCompanions({
|
|
276
|
+
requestedModuleId: options.moduleId,
|
|
277
|
+
companions: pendingRecommendedCompanions,
|
|
278
|
+
withRecommended: options.withRecommended,
|
|
279
|
+
});
|
|
269
280
|
|
|
270
281
|
for (const recommendedModuleId of selectedRecommendedCompanions) {
|
|
271
282
|
const recommendedPlan = await resolveModuleInstallPlan({
|
|
@@ -291,14 +302,16 @@ export async function runAddModule(argv = process.argv.slice(2)) {
|
|
|
291
302
|
await runIntegrationFlow({
|
|
292
303
|
targetRoot,
|
|
293
304
|
packageRoot,
|
|
294
|
-
relatedModuleId: selectedRecommendedCompanions.length > 0 ? null : options.moduleId,
|
|
305
|
+
relatedModuleId: isAllModulesInstall || selectedRecommendedCompanions.length > 0 ? null : options.moduleId,
|
|
295
306
|
});
|
|
296
307
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
308
|
+
if (!isAllModulesInstall) {
|
|
309
|
+
const pendingOptionalIntegrations = getPendingOptionalIntegrations({
|
|
310
|
+
moduleId: options.moduleId,
|
|
311
|
+
targetRoot,
|
|
312
|
+
});
|
|
313
|
+
printOptionalIntegrationsWarning(pendingOptionalIntegrations);
|
|
314
|
+
}
|
|
302
315
|
|
|
303
316
|
const dependencyManifestStateAfter = collectDependencyManifestState(targetRoot);
|
|
304
317
|
const changedDependencyManifestPaths = getChangedDependencyManifestPaths(
|
|
@@ -1,154 +1,230 @@
|
|
|
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 './core/scaffold.mjs';
|
|
8
|
-
import { runAddModule } from './run-add-module.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, proxy }) {
|
|
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,
|
|
34
|
-
});
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function stripSyncTooling(targetRoot) {
|
|
38
|
-
const packagePath = path.join(targetRoot, 'package.json');
|
|
39
|
-
const packageJson = JSON.parse(readFile(packagePath));
|
|
40
|
-
if (packageJson.scripts) {
|
|
41
|
-
delete packageJson.scripts['forgeon:sync-integrations'];
|
|
42
|
-
}
|
|
43
|
-
writeJson(packagePath, packageJson);
|
|
44
|
-
|
|
45
|
-
fs.rmSync(path.join(targetRoot, 'scripts', 'forgeon-sync-integrations.mjs'), { force: true });
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
async function captureLogs(work) {
|
|
49
|
-
const lines = [];
|
|
50
|
-
const originalLog = console.log;
|
|
51
|
-
console.log = (...args) => {
|
|
52
|
-
lines.push(args.join(' '));
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
try {
|
|
56
|
-
await work(lines);
|
|
57
|
-
} finally {
|
|
58
|
-
console.log = originalLog;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
return lines.join('\n');
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
describe('runAddModule', () => {
|
|
65
|
-
const thisDir = path.dirname(fileURLToPath(import.meta.url));
|
|
66
|
-
const packageRoot = path.resolve(thisDir, '..');
|
|
67
|
-
|
|
68
|
-
it('
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
assert.
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
assert.
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
assert.
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
assert.match(
|
|
113
|
-
assert.
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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 './core/scaffold.mjs';
|
|
8
|
+
import { runAddModule } from './run-add-module.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, proxy }) {
|
|
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,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function stripSyncTooling(targetRoot) {
|
|
38
|
+
const packagePath = path.join(targetRoot, 'package.json');
|
|
39
|
+
const packageJson = JSON.parse(readFile(packagePath));
|
|
40
|
+
if (packageJson.scripts) {
|
|
41
|
+
delete packageJson.scripts['forgeon:sync-integrations'];
|
|
42
|
+
}
|
|
43
|
+
writeJson(packagePath, packageJson);
|
|
44
|
+
|
|
45
|
+
fs.rmSync(path.join(targetRoot, 'scripts', 'forgeon-sync-integrations.mjs'), { force: true });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function captureLogs(work) {
|
|
49
|
+
const lines = [];
|
|
50
|
+
const originalLog = console.log;
|
|
51
|
+
console.log = (...args) => {
|
|
52
|
+
lines.push(args.join(' '));
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
await work(lines);
|
|
57
|
+
} finally {
|
|
58
|
+
console.log = originalLog;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return lines.join('\n');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
describe('runAddModule', () => {
|
|
65
|
+
const thisDir = path.dirname(fileURLToPath(import.meta.url));
|
|
66
|
+
const packageRoot = path.resolve(thisDir, '..');
|
|
67
|
+
|
|
68
|
+
it('lists implemented modules without status suffixes', async () => {
|
|
69
|
+
const output = await captureLogs(async () => {
|
|
70
|
+
await runAddModule(['--list']);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
assert.match(output, /Available modules:/);
|
|
74
|
+
assert.doesNotMatch(output, /\(implemented\)/);
|
|
75
|
+
assert.match(output, /- files - /);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('installs files stack non-interactively with provider selection and restores sync tooling', async () => {
|
|
79
|
+
const tempRoot = makeTempDir('forgeon-run-add-files-');
|
|
80
|
+
const targetRoot = path.join(tempRoot, 'demo-run-add-files');
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
scaffoldBaseProject({
|
|
84
|
+
packageRoot,
|
|
85
|
+
targetRoot,
|
|
86
|
+
projectName: 'demo-run-add-files',
|
|
87
|
+
proxy: 'nginx',
|
|
88
|
+
});
|
|
89
|
+
stripSyncTooling(targetRoot);
|
|
90
|
+
|
|
91
|
+
const output = await captureLogs(async () => {
|
|
92
|
+
await runAddModule([
|
|
93
|
+
'files',
|
|
94
|
+
'--project',
|
|
95
|
+
targetRoot,
|
|
96
|
+
'--with-required',
|
|
97
|
+
'--provider',
|
|
98
|
+
'files-storage-adapter=files-local',
|
|
99
|
+
]);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
assert.equal(fs.existsSync(path.join(targetRoot, 'packages', 'db-prisma', 'package.json')), true);
|
|
103
|
+
assert.equal(fs.existsSync(path.join(targetRoot, 'packages', 'files-local', 'package.json')), true);
|
|
104
|
+
assert.equal(fs.existsSync(path.join(targetRoot, 'packages', 'files', 'package.json')), true);
|
|
105
|
+
assert.equal(fs.existsSync(path.join(targetRoot, 'scripts', 'forgeon-sync-integrations.mjs')), true);
|
|
106
|
+
|
|
107
|
+
const packageJson = JSON.parse(readFile(path.join(targetRoot, 'package.json')));
|
|
108
|
+
assert.equal(packageJson.scripts['forgeon:sync-integrations'], 'node scripts/forgeon-sync-integrations.mjs');
|
|
109
|
+
assert.equal(packageJson.scripts['add-all'], 'npx create-forgeon@latest add all --project .');
|
|
110
|
+
|
|
111
|
+
const compose = readFile(path.join(targetRoot, 'infra', 'docker', 'compose.yml'));
|
|
112
|
+
assert.match(compose, /^\s{2}nginx:\s*$/m);
|
|
113
|
+
assert.doesNotMatch(compose, /^\s{2}caddy:\s*$/m);
|
|
114
|
+
|
|
115
|
+
const apiEnv = readFile(path.join(targetRoot, 'apps', 'api', '.env.example'));
|
|
116
|
+
assert.match(apiEnv, /DATABASE_URL=postgresql:\/\/postgres:postgres@localhost:5432\/app\?schema=public/);
|
|
117
|
+
assert.match(apiEnv, /FILES_STORAGE_DRIVER=local/);
|
|
118
|
+
|
|
119
|
+
const healthController = readFile(path.join(targetRoot, 'apps', 'api', 'src', 'health', 'health.controller.ts'));
|
|
120
|
+
assert.match(healthController, /@Post\('files'\)/);
|
|
121
|
+
|
|
122
|
+
assert.match(output, /Recommended companion modules are available:/);
|
|
123
|
+
assert.match(output, /No integration groups found\./);
|
|
124
|
+
assert.match(output, /Next: run pnpm install/);
|
|
125
|
+
} finally {
|
|
126
|
+
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('installs all implemented modules non-interactively with the recommended provider', async () => {
|
|
131
|
+
const tempRoot = makeTempDir('forgeon-run-add-all-');
|
|
132
|
+
const targetRoot = path.join(tempRoot, 'demo-run-add-all');
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
scaffoldBaseProject({
|
|
136
|
+
packageRoot,
|
|
137
|
+
targetRoot,
|
|
138
|
+
projectName: 'demo-run-add-all',
|
|
139
|
+
proxy: 'caddy',
|
|
140
|
+
});
|
|
141
|
+
stripSyncTooling(targetRoot);
|
|
142
|
+
|
|
143
|
+
const output = await captureLogs(async () => {
|
|
144
|
+
await runAddModule(['all', '--project', targetRoot]);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const expectedInstalledModules = [
|
|
148
|
+
'db-prisma',
|
|
149
|
+
'files-local',
|
|
150
|
+
'files',
|
|
151
|
+
'files-access',
|
|
152
|
+
'files-quotas',
|
|
153
|
+
'files-image',
|
|
154
|
+
'i18n',
|
|
155
|
+
'logger',
|
|
156
|
+
'swagger',
|
|
157
|
+
'accounts',
|
|
158
|
+
'rate-limit',
|
|
159
|
+
'rbac',
|
|
160
|
+
'queue',
|
|
161
|
+
'scheduler',
|
|
162
|
+
];
|
|
163
|
+
|
|
164
|
+
for (const moduleId of expectedInstalledModules) {
|
|
165
|
+
const packageDir =
|
|
166
|
+
moduleId === 'accounts'
|
|
167
|
+
? path.join(targetRoot, 'packages', 'accounts-api')
|
|
168
|
+
: path.join(targetRoot, 'packages', moduleId);
|
|
169
|
+
assert.equal(fs.existsSync(path.join(packageDir, 'package.json')), true, `expected ${moduleId}`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
assert.equal(fs.existsSync(path.join(targetRoot, 'packages', 'files-s3', 'package.json')), false);
|
|
173
|
+
|
|
174
|
+
const packageJson = JSON.parse(readFile(path.join(targetRoot, 'package.json')));
|
|
175
|
+
assert.equal(packageJson.scripts['forgeon:sync-integrations'], 'node scripts/forgeon-sync-integrations.mjs');
|
|
176
|
+
assert.equal(packageJson.scripts['add-all'], 'npx create-forgeon@latest add all --project .');
|
|
177
|
+
|
|
178
|
+
const healthController = readFile(path.join(targetRoot, 'apps', 'api', 'src', 'health', 'health.controller.ts'));
|
|
179
|
+
assert.match(healthController, /@Post\('files'\)/);
|
|
180
|
+
assert.match(healthController, /@Get\('files-image'\)/);
|
|
181
|
+
assert.match(healthController, /@Get\('queue'\)/);
|
|
182
|
+
assert.match(healthController, /@Get\('scheduler'\)/);
|
|
183
|
+
assert.match(healthController, /@Get\('rate-limit'\)/);
|
|
184
|
+
|
|
185
|
+
assert.match(output, /Found 1 integration group/);
|
|
186
|
+
assert.match(output, /Integration skipped\./);
|
|
187
|
+
assert.match(output, /Run later with: pnpm forgeon:sync-integrations/);
|
|
188
|
+
assert.match(output, /Next: run pnpm install/);
|
|
189
|
+
assert.doesNotMatch(output, /Recommended companion modules are available:/);
|
|
190
|
+
} finally {
|
|
191
|
+
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('installs scheduler with required queue on proxy=none scaffold', async () => {
|
|
196
|
+
const tempRoot = makeTempDir('forgeon-run-add-scheduler-');
|
|
197
|
+
const targetRoot = path.join(tempRoot, 'demo-run-add-scheduler');
|
|
198
|
+
|
|
199
|
+
try {
|
|
200
|
+
scaffoldBaseProject({
|
|
201
|
+
packageRoot,
|
|
202
|
+
targetRoot,
|
|
203
|
+
projectName: 'demo-run-add-scheduler',
|
|
204
|
+
proxy: 'none',
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
const output = await captureLogs(async () => {
|
|
208
|
+
await runAddModule(['scheduler', '--project', targetRoot, '--with-required']);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
assert.equal(fs.existsSync(path.join(targetRoot, 'packages', 'queue', 'package.json')), true);
|
|
212
|
+
assert.equal(fs.existsSync(path.join(targetRoot, 'packages', 'scheduler', 'package.json')), true);
|
|
213
|
+
|
|
214
|
+
const compose = readFile(path.join(targetRoot, 'infra', 'docker', 'compose.yml'));
|
|
215
|
+
assert.match(compose, /^\s{2}redis:\s*$/m);
|
|
216
|
+
assert.doesNotMatch(compose, /^\s{2}caddy:\s*$/m);
|
|
217
|
+
assert.doesNotMatch(compose, /^\s{2}nginx:\s*$/m);
|
|
218
|
+
|
|
219
|
+
const healthController = readFile(path.join(targetRoot, 'apps', 'api', 'src', 'health', 'health.controller.ts'));
|
|
220
|
+
assert.match(healthController, /@Get\('queue'\)/);
|
|
221
|
+
assert.match(healthController, /@Get\('scheduler'\)/);
|
|
222
|
+
|
|
223
|
+
assert.match(output, /No integration groups found\./);
|
|
224
|
+
assert.match(output, /Next: run pnpm install/);
|
|
225
|
+
} finally {
|
|
226
|
+
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
227
|
+
}
|
|
228
|
+
});
|
|
153
229
|
});
|
|
154
230
|
|
|
@@ -69,6 +69,7 @@ function ensureSyncTooling({ packageRoot, targetRoot }) {
|
|
|
69
69
|
packageJson.scripts = {};
|
|
70
70
|
}
|
|
71
71
|
packageJson.scripts['forgeon:sync-integrations'] = 'node scripts/forgeon-sync-integrations.mjs';
|
|
72
|
+
packageJson.scripts['add-all'] = 'npx create-forgeon@latest add all --project .';
|
|
72
73
|
writeJson(packagePath, packageJson);
|
|
73
74
|
}
|
|
74
75
|
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
"scripts": {
|
|
7
7
|
"dev": "pnpm --parallel --filter @forgeon/api --filter @forgeon/web dev",
|
|
8
8
|
"build": "pnpm -r build",
|
|
9
|
+
"add-all": "npx create-forgeon@latest add all --project .",
|
|
9
10
|
"forgeon:sync-integrations": "node scripts/forgeon-sync-integrations.mjs",
|
|
10
11
|
"create:forgeon": "node scripts/create-forgeon.mjs",
|
|
11
12
|
"docker:up": "docker compose --env-file infra/docker/.env.example -f infra/docker/compose.yml up --build",
|