@fluojs/cli 1.0.0-beta.1
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/LICENSE +21 -0
- package/README.ko.md +155 -0
- package/README.md +155 -0
- package/bin/fluo.mjs +5 -0
- package/dist/cli.d.ts +37 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +292 -0
- package/dist/commands/generate.d.ts +40 -0
- package/dist/commands/generate.d.ts.map +1 -0
- package/dist/commands/generate.js +134 -0
- package/dist/commands/inspect.d.ts +30 -0
- package/dist/commands/inspect.d.ts.map +1 -0
- package/dist/commands/inspect.js +221 -0
- package/dist/commands/migrate.d.ts +30 -0
- package/dist/commands/migrate.d.ts.map +1 -0
- package/dist/commands/migrate.js +173 -0
- package/dist/commands/new.d.ts +45 -0
- package/dist/commands/new.d.ts.map +1 -0
- package/dist/commands/new.js +353 -0
- package/dist/generator-types.d.ts +21 -0
- package/dist/generator-types.d.ts.map +1 -0
- package/dist/generator-types.js +1 -0
- package/dist/generators/controller.d.ts +3 -0
- package/dist/generators/controller.d.ts.map +1 -0
- package/dist/generators/controller.js +22 -0
- package/dist/generators/guard.d.ts +3 -0
- package/dist/generators/guard.d.ts.map +1 -0
- package/dist/generators/guard.js +15 -0
- package/dist/generators/interceptor.d.ts +3 -0
- package/dist/generators/interceptor.d.ts.map +1 -0
- package/dist/generators/interceptor.js +15 -0
- package/dist/generators/manifest.d.ts +121 -0
- package/dist/generators/manifest.d.ts.map +1 -0
- package/dist/generators/manifest.js +130 -0
- package/dist/generators/middleware.d.ts +3 -0
- package/dist/generators/middleware.d.ts.map +1 -0
- package/dist/generators/middleware.js +15 -0
- package/dist/generators/module.d.ts +6 -0
- package/dist/generators/module.d.ts.map +1 -0
- package/dist/generators/module.js +143 -0
- package/dist/generators/render.d.ts +2 -0
- package/dist/generators/render.d.ts.map +1 -0
- package/dist/generators/render.js +17 -0
- package/dist/generators/repository.d.ts +3 -0
- package/dist/generators/repository.d.ts.map +1 -0
- package/dist/generators/repository.js +29 -0
- package/dist/generators/request-dto.d.ts +3 -0
- package/dist/generators/request-dto.d.ts.map +1 -0
- package/dist/generators/request-dto.js +17 -0
- package/dist/generators/response-dto.d.ts +3 -0
- package/dist/generators/response-dto.d.ts.map +1 -0
- package/dist/generators/response-dto.js +17 -0
- package/dist/generators/service.d.ts +3 -0
- package/dist/generators/service.d.ts.map +1 -0
- package/dist/generators/service.js +22 -0
- package/dist/generators/templates/controller.test.ts.ejs +21 -0
- package/dist/generators/templates/controller.ts.ejs +29 -0
- package/dist/generators/templates/guard.ts.ejs +7 -0
- package/dist/generators/templates/interceptor.ts.ejs +7 -0
- package/dist/generators/templates/middleware.ts.ejs +11 -0
- package/dist/generators/templates/module.ts.ejs +9 -0
- package/dist/generators/templates/repository.slice.test.ts.ejs +15 -0
- package/dist/generators/templates/repository.test.ts.ejs +9 -0
- package/dist/generators/templates/repository.ts.ejs +10 -0
- package/dist/generators/templates/request-dto.ts.ejs +9 -0
- package/dist/generators/templates/response-dto.ts.ejs +3 -0
- package/dist/generators/templates/service.test.ts.ejs +21 -0
- package/dist/generators/templates/service.ts.ejs +24 -0
- package/dist/generators/utils.d.ts +4 -0
- package/dist/generators/utils.d.ts.map +1 -0
- package/dist/generators/utils.js +18 -0
- package/dist/help.d.ts +8 -0
- package/dist/help.d.ts.map +1 -0
- package/dist/help.js +16 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/new/install.d.ts +51 -0
- package/dist/new/install.d.ts.map +1 -0
- package/dist/new/install.js +140 -0
- package/dist/new/package-spec-resolver.d.ts +4 -0
- package/dist/new/package-spec-resolver.d.ts.map +1 -0
- package/dist/new/package-spec-resolver.js +397 -0
- package/dist/new/prompt.d.ts +56 -0
- package/dist/new/prompt.d.ts.map +1 -0
- package/dist/new/prompt.js +278 -0
- package/dist/new/resolver.d.ts +32 -0
- package/dist/new/resolver.d.ts.map +1 -0
- package/dist/new/resolver.js +93 -0
- package/dist/new/scaffold.d.ts +14 -0
- package/dist/new/scaffold.d.ts.map +1 -0
- package/dist/new/scaffold.js +2010 -0
- package/dist/new/starter-profiles.d.ts +91 -0
- package/dist/new/starter-profiles.d.ts.map +1 -0
- package/dist/new/starter-profiles.js +347 -0
- package/dist/new/types.d.ts +63 -0
- package/dist/new/types.d.ts.map +1 -0
- package/dist/new/types.js +1 -0
- package/dist/registry.d.ts +10 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +30 -0
- package/dist/transforms/nestjs-migrate.d.ts +33 -0
- package/dist/transforms/nestjs-migrate.d.ts.map +1 -0
- package/dist/transforms/nestjs-migrate.js +891 -0
- package/dist/types.d.ts +12 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/package.json +65 -0
|
@@ -0,0 +1,2010 @@
|
|
|
1
|
+
import { mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join, resolve } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { initializeGitRepository, installDependencies } from './install.js';
|
|
5
|
+
import { resolvePackageSpecs } from './package-spec-resolver.js';
|
|
6
|
+
import { resolveBootstrapPlan } from './resolver.js';
|
|
7
|
+
const PUBLISHED_DEV_DEPENDENCIES = {
|
|
8
|
+
'@babel/cli': '^7.26.4',
|
|
9
|
+
'@babel/core': '^7.26.10',
|
|
10
|
+
'@babel/plugin-proposal-decorators': '^7.28.0',
|
|
11
|
+
'@babel/preset-typescript': '^7.27.1',
|
|
12
|
+
'@types/babel__core': '^7.20.5',
|
|
13
|
+
'@types/node': '^22.13.10',
|
|
14
|
+
tsx: '^4.20.4',
|
|
15
|
+
typescript: '^6.0.2',
|
|
16
|
+
vite: '^6.2.1',
|
|
17
|
+
vitest: '^3.0.8'
|
|
18
|
+
};
|
|
19
|
+
const PUBLISHED_RUNTIME_DEPENDENCIES = {
|
|
20
|
+
'@types/amqplib': '^0.10.7',
|
|
21
|
+
'@grpc/grpc-js': '^1.0.0',
|
|
22
|
+
'@grpc/proto-loader': '^0.8.0',
|
|
23
|
+
amqplib: '^0.10.5',
|
|
24
|
+
ioredis: '^5.0.0',
|
|
25
|
+
kafkajs: '^2.2.4',
|
|
26
|
+
mqtt: '^5.0.0',
|
|
27
|
+
nats: '^2.29.3'
|
|
28
|
+
};
|
|
29
|
+
function describeApplicationStarter(options) {
|
|
30
|
+
if (options.runtime === 'bun') {
|
|
31
|
+
return {
|
|
32
|
+
adapterCall: 'createBunAdapter({ port })',
|
|
33
|
+
adapterFactory: 'createBunAdapter',
|
|
34
|
+
entrypoint: 'src/main.ts',
|
|
35
|
+
packageName: '@fluojs/platform-bun',
|
|
36
|
+
platformLabel: 'Bun native HTTP',
|
|
37
|
+
runtimeLabel: 'Bun runtime'
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
if (options.runtime === 'deno') {
|
|
41
|
+
return {
|
|
42
|
+
entrypoint: 'src/main.ts',
|
|
43
|
+
packageName: '@fluojs/platform-deno',
|
|
44
|
+
platformLabel: 'Deno native HTTP',
|
|
45
|
+
runtimeLabel: 'Deno runtime'
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
if (options.runtime === 'cloudflare-workers') {
|
|
49
|
+
return {
|
|
50
|
+
entrypoint: 'src/worker.ts',
|
|
51
|
+
packageName: '@fluojs/platform-cloudflare-workers',
|
|
52
|
+
platformLabel: 'Cloudflare Workers HTTP',
|
|
53
|
+
runtimeLabel: 'Cloudflare Workers runtime'
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
switch (options.platform) {
|
|
57
|
+
case 'express':
|
|
58
|
+
return {
|
|
59
|
+
adapterCall: 'createExpressAdapter({ port })',
|
|
60
|
+
adapterFactory: 'createExpressAdapter',
|
|
61
|
+
entrypoint: 'src/main.ts',
|
|
62
|
+
packageName: '@fluojs/platform-express',
|
|
63
|
+
platformLabel: 'Express HTTP',
|
|
64
|
+
runtimeLabel: 'Node.js runtime'
|
|
65
|
+
};
|
|
66
|
+
case 'nodejs':
|
|
67
|
+
return {
|
|
68
|
+
adapterCall: 'createNodejsAdapter({ port })',
|
|
69
|
+
adapterFactory: 'createNodejsAdapter',
|
|
70
|
+
entrypoint: 'src/main.ts',
|
|
71
|
+
packageName: '@fluojs/platform-nodejs',
|
|
72
|
+
platformLabel: 'raw Node.js HTTP',
|
|
73
|
+
runtimeLabel: 'Node.js runtime'
|
|
74
|
+
};
|
|
75
|
+
default:
|
|
76
|
+
return {
|
|
77
|
+
adapterCall: 'createFastifyAdapter({ port })',
|
|
78
|
+
adapterFactory: 'createFastifyAdapter',
|
|
79
|
+
entrypoint: 'src/main.ts',
|
|
80
|
+
packageName: '@fluojs/platform-fastify',
|
|
81
|
+
platformLabel: 'Fastify HTTP',
|
|
82
|
+
runtimeLabel: 'Node.js runtime'
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function packageRootFromImportMeta(importMetaUrl) {
|
|
87
|
+
return resolve(dirname(fileURLToPath(importMetaUrl)), '..', '..');
|
|
88
|
+
}
|
|
89
|
+
function readOwnPackageVersion(importMetaUrl) {
|
|
90
|
+
const packageJson = JSON.parse(readFileSync(join(packageRootFromImportMeta(importMetaUrl), 'package.json'), 'utf8'));
|
|
91
|
+
return packageJson.version;
|
|
92
|
+
}
|
|
93
|
+
function writeTextFile(filePath, content) {
|
|
94
|
+
mkdirSync(dirname(filePath), {
|
|
95
|
+
recursive: true
|
|
96
|
+
});
|
|
97
|
+
writeFileSync(filePath, content, 'utf8');
|
|
98
|
+
}
|
|
99
|
+
function createDependencySpec(packageName, releaseVersion, packageSpecs) {
|
|
100
|
+
return packageSpecs[packageName] ?? PUBLISHED_RUNTIME_DEPENDENCIES[packageName] ?? createPublishedInternalDependencySpec(releaseVersion);
|
|
101
|
+
}
|
|
102
|
+
function createPublishedInternalDependencySpec(version) {
|
|
103
|
+
return `^${version}`;
|
|
104
|
+
}
|
|
105
|
+
function createRunCommand(packageManager, command) {
|
|
106
|
+
switch (packageManager) {
|
|
107
|
+
case 'bun':
|
|
108
|
+
return `bun run ${command}`;
|
|
109
|
+
case 'npm':
|
|
110
|
+
return `npm run ${command}`;
|
|
111
|
+
case 'yarn':
|
|
112
|
+
return `yarn ${command}`;
|
|
113
|
+
default:
|
|
114
|
+
return `pnpm ${command}`;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function createExecCommand(packageManager, command) {
|
|
118
|
+
switch (packageManager) {
|
|
119
|
+
case 'bun':
|
|
120
|
+
return `bun x ${command}`;
|
|
121
|
+
case 'npm':
|
|
122
|
+
return `npm exec -- ${command}`;
|
|
123
|
+
case 'yarn':
|
|
124
|
+
return `yarn ${command}`;
|
|
125
|
+
default:
|
|
126
|
+
return `pnpm exec ${command}`;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
function createProjectScripts(bootstrapPlan) {
|
|
130
|
+
switch (bootstrapPlan.profile.id) {
|
|
131
|
+
case 'application-bun-bun-http':
|
|
132
|
+
return {
|
|
133
|
+
build: 'bun build ./src/main.ts --outdir ./dist --target bun',
|
|
134
|
+
dev: 'bun --watch src/main.ts',
|
|
135
|
+
test: 'vitest run',
|
|
136
|
+
'test:watch': 'vitest',
|
|
137
|
+
typecheck: 'tsc -p tsconfig.json --noEmit'
|
|
138
|
+
};
|
|
139
|
+
case 'application-deno-deno-http':
|
|
140
|
+
return {
|
|
141
|
+
build: 'mkdir -p dist && deno compile --allow-env --allow-net --output dist/app src/main.ts',
|
|
142
|
+
dev: 'deno run --allow-env --allow-net --watch src/main.ts',
|
|
143
|
+
test: 'deno test --allow-env --allow-net',
|
|
144
|
+
'test:watch': 'deno test --allow-env --allow-net --watch',
|
|
145
|
+
typecheck: 'deno check src/main.ts src/app.test.ts'
|
|
146
|
+
};
|
|
147
|
+
case 'application-cloudflare-workers-cloudflare-workers-http':
|
|
148
|
+
return {
|
|
149
|
+
build: 'wrangler deploy --dry-run',
|
|
150
|
+
dev: 'wrangler dev',
|
|
151
|
+
test: 'vitest run',
|
|
152
|
+
'test:watch': 'vitest',
|
|
153
|
+
typecheck: 'tsc -p tsconfig.json --noEmit'
|
|
154
|
+
};
|
|
155
|
+
default:
|
|
156
|
+
return {
|
|
157
|
+
build: 'babel src --extensions .ts --out-dir dist --config-file ./babel.config.cjs && tsc -p tsconfig.build.json',
|
|
158
|
+
dev: 'node --env-file=.env --watch --watch-preserve-output --import tsx src/main.ts',
|
|
159
|
+
test: 'vitest run',
|
|
160
|
+
'test:watch': 'vitest',
|
|
161
|
+
typecheck: 'tsc -p tsconfig.json --noEmit'
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
function createProjectEngines(bootstrapPlan) {
|
|
166
|
+
switch (bootstrapPlan.profile.id) {
|
|
167
|
+
case 'application-bun-bun-http':
|
|
168
|
+
return {
|
|
169
|
+
bun: '>=1.2.5'
|
|
170
|
+
};
|
|
171
|
+
case 'application-deno-deno-http':
|
|
172
|
+
return {
|
|
173
|
+
deno: '>=2.0.0'
|
|
174
|
+
};
|
|
175
|
+
default:
|
|
176
|
+
return {
|
|
177
|
+
node: '>=20.0.0'
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
function createPublishedDevDependencies(bootstrapPlan) {
|
|
182
|
+
if (bootstrapPlan.profile.id === 'application-deno-deno-http') {
|
|
183
|
+
return {};
|
|
184
|
+
}
|
|
185
|
+
if (bootstrapPlan.profile.id === 'application-cloudflare-workers-cloudflare-workers-http') {
|
|
186
|
+
return {
|
|
187
|
+
...PUBLISHED_DEV_DEPENDENCIES,
|
|
188
|
+
wrangler: '^4.11.1'
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
return {
|
|
192
|
+
...PUBLISHED_DEV_DEPENDENCIES
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
function createProjectPackageJson(options, bootstrapPlan, releaseVersion, packageSpecs) {
|
|
196
|
+
const packageManagerField = options.packageManager === 'pnpm' ? {
|
|
197
|
+
packageManager: 'pnpm@10.4.1'
|
|
198
|
+
} : options.packageManager === 'bun' ? {
|
|
199
|
+
packageManager: 'bun@1.2.5'
|
|
200
|
+
} : options.packageManager === 'yarn' ? {
|
|
201
|
+
packageManager: 'yarn@1.22.22'
|
|
202
|
+
} : {};
|
|
203
|
+
const localOverrideConfig = Object.keys(packageSpecs).length ? {
|
|
204
|
+
overrides: packageSpecs,
|
|
205
|
+
resolutions: packageSpecs
|
|
206
|
+
} : {};
|
|
207
|
+
const dependencyEntries = Object.fromEntries(bootstrapPlan.dependencies.dependencies.map(packageName => [packageName, createDependencySpec(packageName, releaseVersion, packageSpecs)]));
|
|
208
|
+
const devDependencyEntries = Object.fromEntries(bootstrapPlan.dependencies.devDependencies.map(packageName => [packageName, createDependencySpec(packageName, releaseVersion, packageSpecs)]));
|
|
209
|
+
return JSON.stringify({
|
|
210
|
+
name: options.projectName,
|
|
211
|
+
version: '0.1.0',
|
|
212
|
+
private: true,
|
|
213
|
+
type: 'module',
|
|
214
|
+
engines: createProjectEngines(bootstrapPlan),
|
|
215
|
+
...packageManagerField,
|
|
216
|
+
...localOverrideConfig,
|
|
217
|
+
scripts: createProjectScripts(bootstrapPlan),
|
|
218
|
+
dependencies: dependencyEntries,
|
|
219
|
+
devDependencies: {
|
|
220
|
+
...devDependencyEntries,
|
|
221
|
+
...createPublishedDevDependencies(bootstrapPlan)
|
|
222
|
+
}
|
|
223
|
+
}, null, 2);
|
|
224
|
+
}
|
|
225
|
+
function createProjectTsconfig() {
|
|
226
|
+
return `{
|
|
227
|
+
"compilerOptions": {
|
|
228
|
+
"target": "ES2022",
|
|
229
|
+
"module": "ESNext",
|
|
230
|
+
"moduleResolution": "Bundler",
|
|
231
|
+
"strict": true,
|
|
232
|
+
"declaration": true,
|
|
233
|
+
"declarationMap": true,
|
|
234
|
+
"skipLibCheck": true,
|
|
235
|
+
"esModuleInterop": true,
|
|
236
|
+
"forceConsistentCasingInFileNames": true,
|
|
237
|
+
"resolveJsonModule": true,
|
|
238
|
+
"rootDir": "src",
|
|
239
|
+
"types": ["node"]
|
|
240
|
+
},
|
|
241
|
+
"include": ["src/**/*.ts"]
|
|
242
|
+
}
|
|
243
|
+
`;
|
|
244
|
+
}
|
|
245
|
+
function createProjectTsconfigBuild() {
|
|
246
|
+
return `{
|
|
247
|
+
"extends": "./tsconfig.json",
|
|
248
|
+
"compilerOptions": {
|
|
249
|
+
"declaration": true,
|
|
250
|
+
"declarationMap": true,
|
|
251
|
+
"emitDeclarationOnly": true,
|
|
252
|
+
"outDir": "dist"
|
|
253
|
+
},
|
|
254
|
+
"exclude": ["src/**/*.test.ts"]
|
|
255
|
+
}
|
|
256
|
+
`;
|
|
257
|
+
}
|
|
258
|
+
function createBabelConfig() {
|
|
259
|
+
return `module.exports = {
|
|
260
|
+
ignore: ['src/**/*.test.ts'],
|
|
261
|
+
presets: [['@babel/preset-typescript', { allowDeclareFields: true }]],
|
|
262
|
+
plugins: [['@babel/plugin-proposal-decorators', { version: '2023-11' }]],
|
|
263
|
+
};
|
|
264
|
+
`;
|
|
265
|
+
}
|
|
266
|
+
function createViteConfig() {
|
|
267
|
+
return `import { defineConfig } from 'vite';
|
|
268
|
+
|
|
269
|
+
export default defineConfig({
|
|
270
|
+
server: {
|
|
271
|
+
port: 5173,
|
|
272
|
+
},
|
|
273
|
+
});
|
|
274
|
+
`;
|
|
275
|
+
}
|
|
276
|
+
function createVitestConfig() {
|
|
277
|
+
return `import { defineConfig } from 'vitest/config';
|
|
278
|
+
|
|
279
|
+
import { fluoBabelDecoratorsPlugin } from '@fluojs/testing/vitest';
|
|
280
|
+
|
|
281
|
+
export default defineConfig({
|
|
282
|
+
plugins: [fluoBabelDecoratorsPlugin()],
|
|
283
|
+
test: {
|
|
284
|
+
environment: 'node',
|
|
285
|
+
include: ['src/**/*.test.ts'],
|
|
286
|
+
},
|
|
287
|
+
});
|
|
288
|
+
`;
|
|
289
|
+
}
|
|
290
|
+
function createGitignore() {
|
|
291
|
+
return `node_modules
|
|
292
|
+
dist
|
|
293
|
+
.fluo
|
|
294
|
+
.env
|
|
295
|
+
.env.local
|
|
296
|
+
.wrangler
|
|
297
|
+
.dev.vars
|
|
298
|
+
coverage
|
|
299
|
+
`;
|
|
300
|
+
}
|
|
301
|
+
function createHttpProjectReadme(options) {
|
|
302
|
+
const starter = describeApplicationStarter(options);
|
|
303
|
+
const entrypointLabel = starter.entrypoint;
|
|
304
|
+
const starterContract = options.runtime === 'deno' ? `\`${entrypointLabel}\` boots the selected first-class application starter: ${starter.runtimeLabel} + ${starter.platformLabel} via \`runDenoApplication(...)\`` : options.runtime === 'cloudflare-workers' ? `\`${entrypointLabel}\` exports the selected first-class application starter: ${starter.runtimeLabel} + ${starter.platformLabel} via \`createCloudflareWorkerEntrypoint(...)\`` : `\`${entrypointLabel}\` wires the selected first-class application starter: ${starter.runtimeLabel} + ${starter.platformLabel} via \`${starter.adapterFactory}(... )\``.replace('(... )', '(...)');
|
|
305
|
+
const corsLine = options.runtime === 'cloudflare-workers' ? '- CORS: defaults to allowOrigin `*`; pass a `cors` option to `createCloudflareWorkerEntrypoint(..., { cors })` when you need edge-specific restrictions' : options.runtime === 'deno' ? '- CORS: defaults to allowOrigin `*`; configure it through the Deno HTTP bootstrap path before exposing the adapter in production' : `- CORS: defaults to allowOrigin '*'; pass a \`cors\` option to \`FluoFactory.create(..., { cors, adapter: ${starter.adapterFactory}(...) })\` to restrict origins`;
|
|
306
|
+
const testingSection = options.runtime === 'deno' ? `## Official generated testing templates\n\n- \`src/app.test.ts\` — Deno-native integration-style dispatch verification for the generated runtime + starter routes.\n\nUse this test when you need confidence that the generated Deno entrypoint and module graph still agree on the same HTTP contract.` : `## Official generated testing templates\n\n- \`src/health/*.test.ts\` — unit templates for the starter-owned health slice.\n- \`src/app.test.ts\` — integration-style dispatch template for runtime + starter routes.\n- \`src/app.e2e.test.ts\` — e2e-style template powered by \`createTestApp\` from \`@fluojs/testing\`.\n- \`${createExecCommand(options.packageManager, 'fluo g repo User')}\` also adds:\n - \`src/users/user.repo.test.ts\` (unit template)\n - \`src/users/user.repo.slice.test.ts\` (slice/integration template via \`createTestingModule\`)\n\nUse unit templates for fast logic checks. Use slice/e2e templates when you need module wiring and route-level confidence.`;
|
|
307
|
+
return `# ${options.projectName}
|
|
308
|
+
|
|
309
|
+
Generated by @fluojs/cli.
|
|
310
|
+
|
|
311
|
+
- Starter contract: ${starterContract}
|
|
312
|
+
- Default baseline: when you omit \`--platform\`, \`fluo new\` still generates the Node.js + Fastify HTTP starter by default
|
|
313
|
+
- Broader runtime/adapter package coverage is documented in the fluo docs and package READMEs; this generated starter intentionally describes only the wired starter path above
|
|
314
|
+
- Package manager: install/run commands can use ${options.packageManager}; runtime choice stays explicit and is independent from the package manager you picked
|
|
315
|
+
- Runtime dependency set: generated manifest entries match the selected runtime contract instead of inheriting the Node-only starter recipe
|
|
316
|
+
${corsLine}
|
|
317
|
+
- Observability: /health and /ready endpoints are included by default
|
|
318
|
+
- Runtime path: bootstrapApplication -> handler mapping -> dispatcher -> middleware -> guard -> interceptor -> controller
|
|
319
|
+
- Naming policy: runtime module entrypoints use governed canonical names (\`forRoot(...)\`, optional \`forRootAsync(...)\`, \`register(...)\`, \`forFeature(...)\`); helper/builders stay \`create*\` (for example \`createHealthModule()\`, \`createTestingModule(...)\`)
|
|
320
|
+
|
|
321
|
+
## Commands
|
|
322
|
+
|
|
323
|
+
- Dev: ${createRunCommand(options.packageManager, 'dev')}
|
|
324
|
+
- Build: ${createRunCommand(options.packageManager, 'build')}
|
|
325
|
+
- Typecheck: ${createRunCommand(options.packageManager, 'typecheck')}
|
|
326
|
+
- Test: ${createRunCommand(options.packageManager, 'test')}
|
|
327
|
+
|
|
328
|
+
## Generator example
|
|
329
|
+
|
|
330
|
+
- Repo generator: ${createExecCommand(options.packageManager, 'fluo g repo User')}
|
|
331
|
+
|
|
332
|
+
${testingSection}
|
|
333
|
+
`;
|
|
334
|
+
}
|
|
335
|
+
function describeMicroserviceStarter(options) {
|
|
336
|
+
switch (options.transport) {
|
|
337
|
+
case 'redis-streams':
|
|
338
|
+
return {
|
|
339
|
+
configLines: ['- Redis Streams broker: configure `REDIS_URL` in `.env` before you start the service', '- Optional stream tuning: `REDIS_STREAMS_NAMESPACE` and `REDIS_STREAMS_CONSUMER_GROUP` let you align stream names and the consumer group with your broker contract'],
|
|
340
|
+
entrypointNote: '`src/app.ts` wires `RedisStreamsMicroserviceTransport` with dedicated reader/writer `ioredis` clients configured for lazy connection startup',
|
|
341
|
+
generatedProjectVerification: 'The generated-project verification path typechecks, builds, and tests the scaffold while asserting the Redis Streams starter keeps the `ioredis` dependency, `.env` contract, and transport entrypoint wiring intact.',
|
|
342
|
+
packageManagerNote: 'runtime choice stays explicit and independent from the package manager you picked; the generated manifest adds the `ioredis` dependency because Redis Streams transport support lives outside the base fluo packages',
|
|
343
|
+
pattern: 'math.sum',
|
|
344
|
+
readmeTransportLabel: 'redis-streams',
|
|
345
|
+
runtimeDependencyNote: 'runtime dependency set: `@fluojs/microservices` plus `ioredis` for the broker client pair used by `RedisStreamsMicroserviceTransport`',
|
|
346
|
+
starterNote: 'the Redis Streams starter keeps TCP as the default microservice path when you omit `--transport`, but this scaffold becomes runnable as soon as `REDIS_URL` points at a broker',
|
|
347
|
+
testNote: '`src/app.test.ts` preserves a broker-free integration template via an in-memory transport, while generated-project verification still checks the Redis Streams entrypoint/build contract.'
|
|
348
|
+
};
|
|
349
|
+
case 'mqtt':
|
|
350
|
+
return {
|
|
351
|
+
configLines: ['- MQTT broker: configure `MQTT_URL` in `.env` before you start the service', '- Topic namespace: adjust `MQTT_NAMESPACE` when you need transport-owned topics to coexist with other services on the same broker'],
|
|
352
|
+
entrypointNote: '`src/app.ts` wires `MqttMicroserviceTransport` with the generated broker URL and namespace settings so the starter can own its MQTT client at runtime',
|
|
353
|
+
generatedProjectVerification: 'The generated-project verification path typechecks, builds, and tests the scaffold while asserting the MQTT starter keeps the `mqtt` dependency, `.env` contract, and transport entrypoint wiring intact.',
|
|
354
|
+
packageManagerNote: 'runtime choice stays explicit and independent from the package manager you picked; the generated manifest adds the `mqtt` dependency because MQTT transport support lives outside the base fluo packages',
|
|
355
|
+
pattern: 'math.sum',
|
|
356
|
+
readmeTransportLabel: 'mqtt',
|
|
357
|
+
runtimeDependencyNote: 'runtime dependency set: `@fluojs/microservices` plus `mqtt` for the broker client that `MqttMicroserviceTransport` loads at runtime',
|
|
358
|
+
starterNote: 'the MQTT starter keeps TCP as the default microservice path when you omit `--transport`, but this scaffold becomes runnable as soon as `MQTT_URL` points at a broker',
|
|
359
|
+
testNote: '`src/app.test.ts` preserves a broker-free integration template via an in-memory transport, while generated-project verification still checks the MQTT entrypoint/build contract.'
|
|
360
|
+
};
|
|
361
|
+
case 'grpc':
|
|
362
|
+
return {
|
|
363
|
+
configLines: ['- gRPC listener: configure `GRPC_URL` in `.env` before you start the service', '- Protobuf contract: `proto/math.proto` stays in the generated project root and `src/app.ts` resolves it from `process.cwd()` so builds and runtime launches share the same schema path'],
|
|
364
|
+
entrypointNote: '`src/app.ts` wires `GrpcMicroserviceTransport` against `proto/math.proto`, the `fluo.microservices` package, and the generated `MathService` RPC contract',
|
|
365
|
+
extraFiles: ['- `proto/math.proto` — protobuf contract for the generated `MathService.Sum` RPC'],
|
|
366
|
+
generatedProjectVerification: 'The generated-project verification path typechecks, builds, and tests the scaffold while asserting the gRPC starter keeps the protobuf file, gRPC peer dependencies, `.env` contract, and transport entrypoint wiring intact.',
|
|
367
|
+
packageManagerNote: 'runtime choice stays explicit and independent from the package manager you picked; the generated manifest adds `@grpc/grpc-js` and `@grpc/proto-loader` because gRPC transport support lives outside the base fluo packages',
|
|
368
|
+
pattern: 'MathService.Sum',
|
|
369
|
+
readmeTransportLabel: 'grpc',
|
|
370
|
+
runtimeDependencyNote: 'runtime dependency set: `@fluojs/microservices` plus `@grpc/grpc-js` and `@grpc/proto-loader` for the generated protobuf-backed RPC listener',
|
|
371
|
+
starterNote: 'the gRPC starter keeps TCP as the default microservice path when you omit `--transport`, but this scaffold becomes runnable as soon as `GRPC_URL` is reachable for the generated protobuf contract',
|
|
372
|
+
testNote: '`src/app.test.ts` preserves a broker-free integration template via an in-memory transport, while generated-project verification still checks the gRPC entrypoint/build contract and the generated `proto/math.proto` file.'
|
|
373
|
+
};
|
|
374
|
+
case 'nats':
|
|
375
|
+
return {
|
|
376
|
+
configLines: ['- NATS broker: configure `NATS_SERVERS` in `.env` before you start the service', '- Subject contract: keep `NATS_MESSAGE_SUBJECT` and `NATS_EVENT_SUBJECT` aligned with the peer services that share the broker namespace'],
|
|
377
|
+
entrypointNote: '`src/app.ts` opens one caller-owned `nats` client plus `JSONCodec()` and passes both into `NatsMicroserviceTransport` as the canonical starter bootstrap contract',
|
|
378
|
+
generatedProjectVerification: 'The generated-project verification path typechecks, builds, and tests the scaffold while asserting the NATS starter keeps the `nats` dependency, `.env` contract, and transport entrypoint wiring intact.',
|
|
379
|
+
packageManagerNote: 'runtime choice stays explicit and independent from the package manager you picked; the generated manifest adds the `nats` client because the NATS starter depends on an external broker plus a caller-owned client/bootstrap pair',
|
|
380
|
+
pattern: 'math.sum',
|
|
381
|
+
readmeTransportLabel: 'nats',
|
|
382
|
+
runtimeDependencyNote: 'runtime dependency set: `@fluojs/microservices` plus `nats` for the broker client and codec that `NatsMicroserviceTransport` expects the caller to supply',
|
|
383
|
+
starterNote: 'the NATS starter keeps TCP as the default microservice path when you omit `--transport`, but this scaffold becomes runnable as soon as `NATS_SERVERS` points at a reachable broker cluster',
|
|
384
|
+
testNote: '`src/app.test.ts` preserves a broker-free integration template via an in-memory transport, while generated-project verification still checks the NATS entrypoint/build contract and the caller-owned client bootstrap wiring.'
|
|
385
|
+
};
|
|
386
|
+
case 'kafka':
|
|
387
|
+
return {
|
|
388
|
+
configLines: ['- Kafka brokers: configure `KAFKA_BROKERS` in `.env` before you start the service', '- Topic/group contract: `KAFKA_CLIENT_ID`, `KAFKA_CONSUMER_GROUP`, `KAFKA_MESSAGE_TOPIC`, `KAFKA_EVENT_TOPIC`, and `KAFKA_RESPONSE_TOPIC` stay explicit so the starter never hides its shared broker topology'],
|
|
389
|
+
entrypointNote: '`src/app.ts` opens canonical `kafkajs` producer/consumer collaborators and passes them into `KafkaMicroserviceTransport`, keeping the response-topic contract explicit in starter-owned config',
|
|
390
|
+
generatedProjectVerification: 'The generated-project verification path typechecks, builds, and tests the scaffold while asserting the Kafka starter keeps the `kafkajs` dependency, `.env` contract, and transport entrypoint wiring intact.',
|
|
391
|
+
packageManagerNote: 'runtime choice stays explicit and independent from the package manager you picked; the generated manifest adds `kafkajs` because the Kafka starter depends on an external broker plus caller-owned producer/consumer collaborators',
|
|
392
|
+
pattern: 'math.sum',
|
|
393
|
+
readmeTransportLabel: 'kafka',
|
|
394
|
+
runtimeDependencyNote: 'runtime dependency set: `@fluojs/microservices` plus `kafkajs` for the generated producer/consumer/bootstrap contract used by `KafkaMicroserviceTransport`',
|
|
395
|
+
starterNote: 'the Kafka starter keeps TCP as the default microservice path when you omit `--transport`, but this scaffold becomes runnable as soon as `KAFKA_BROKERS` points at a reachable broker set',
|
|
396
|
+
testNote: '`src/app.test.ts` preserves a broker-free integration template via an in-memory transport, while generated-project verification still checks the Kafka entrypoint/build contract and the explicit topic/client bootstrap wiring.'
|
|
397
|
+
};
|
|
398
|
+
case 'rabbitmq':
|
|
399
|
+
return {
|
|
400
|
+
configLines: ['- RabbitMQ broker: configure `RABBITMQ_URL` in `.env` before you start the service', '- Queue contract: `RABBITMQ_MESSAGE_QUEUE`, `RABBITMQ_EVENT_QUEUE`, and `RABBITMQ_RESPONSE_QUEUE` stay explicit so the starter advertises exactly which queues and reply path it owns'],
|
|
401
|
+
entrypointNote: '`src/app.ts` opens a canonical `amqplib` connection/channel pair and passes caller-owned publisher/consumer collaborators into `RabbitMqMicroserviceTransport`',
|
|
402
|
+
generatedProjectVerification: 'The generated-project verification path typechecks, builds, and tests the scaffold while asserting the RabbitMQ starter keeps the `amqplib` dependency, `.env` contract, and transport entrypoint wiring intact.',
|
|
403
|
+
packageManagerNote: 'runtime choice stays explicit and independent from the package manager you picked; the generated manifest adds `amqplib` because the RabbitMQ starter depends on an external broker plus caller-owned publisher/consumer collaborators',
|
|
404
|
+
pattern: 'math.sum',
|
|
405
|
+
readmeTransportLabel: 'rabbitmq',
|
|
406
|
+
runtimeDependencyNote: 'runtime dependency set: `@fluojs/microservices`, `amqplib`, and `@types/amqplib` for the generated queue client/bootstrap contract used by `RabbitMqMicroserviceTransport`',
|
|
407
|
+
starterNote: 'the RabbitMQ starter keeps TCP as the default microservice path when you omit `--transport`, but this scaffold becomes runnable as soon as `RABBITMQ_URL` points at a reachable broker',
|
|
408
|
+
testNote: '`src/app.test.ts` preserves a broker-free integration template via an in-memory transport, while generated-project verification still checks the RabbitMQ entrypoint/build contract and the explicit queue/bootstrap wiring.'
|
|
409
|
+
};
|
|
410
|
+
default:
|
|
411
|
+
return {
|
|
412
|
+
configLines: ['- Local TCP listener: configure `MICROSERVICE_HOST` and `MICROSERVICE_PORT` in `.env`'],
|
|
413
|
+
entrypointNote: '`src/app.ts` wires `TcpMicroserviceTransport` directly with the generated host/port environment settings',
|
|
414
|
+
generatedProjectVerification: 'The generated-project verification path typechecks, builds, and tests the scaffold while asserting the TCP starter keeps the default `.env` contract and transport entrypoint wiring intact.',
|
|
415
|
+
packageManagerNote: 'transport choice stays explicit and is independent from the package manager you picked',
|
|
416
|
+
pattern: 'math.sum',
|
|
417
|
+
readmeTransportLabel: 'tcp',
|
|
418
|
+
runtimeDependencyNote: 'runtime dependency set: `@fluojs/microservices` with no extra transport peer dependencies beyond the base fluo packages',
|
|
419
|
+
starterNote: 'TCP remains the simplest default microservice path when you omit `--transport`.',
|
|
420
|
+
testNote: '`src/app.test.ts` preserves a broker-free integration template via an in-memory transport that matches the generated message pattern contract.'
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
function createMicroserviceResponseExpectation(options) {
|
|
425
|
+
return options.transport === 'grpc' ? '{ result: 42 }' : '42';
|
|
426
|
+
}
|
|
427
|
+
function createMicroserviceProjectReadme(options) {
|
|
428
|
+
const starter = describeMicroserviceStarter(options);
|
|
429
|
+
const extraFilesSection = starter.extraFiles?.length ? `${starter.extraFiles.join('\n')}\n` : '';
|
|
430
|
+
return `# ${options.projectName}
|
|
431
|
+
|
|
432
|
+
Generated by @fluojs/cli.
|
|
433
|
+
|
|
434
|
+
- Shape: \`microservice\`
|
|
435
|
+
- Transport: \`${starter.readmeTransportLabel}\` is the generated runnable starter contract for this project
|
|
436
|
+
- Runtime: \`node\`
|
|
437
|
+
- Platform: \`none\` because the microservice starter boots through \`@fluojs/microservices\`, not an HTTP adapter
|
|
438
|
+
- Package manager: install/run commands can use ${options.packageManager}; ${starter.packageManagerNote}
|
|
439
|
+
- Messaging contract: \`src/math/math.handler.ts\` exposes a \`${starter.pattern}\` message pattern and the generated tests verify it through an in-memory transport so the starter stays testable without external brokers
|
|
440
|
+
- Entrypoint contract: ${starter.entrypointNote}
|
|
441
|
+
- Starter contract note: ${starter.starterNote}
|
|
442
|
+
- ${starter.runtimeDependencyNote}
|
|
443
|
+
|
|
444
|
+
## Commands
|
|
445
|
+
|
|
446
|
+
- Dev: ${createRunCommand(options.packageManager, 'dev')}
|
|
447
|
+
- Build: ${createRunCommand(options.packageManager, 'build')}
|
|
448
|
+
- Typecheck: ${createRunCommand(options.packageManager, 'typecheck')}
|
|
449
|
+
- Test: ${createRunCommand(options.packageManager, 'test')}
|
|
450
|
+
|
|
451
|
+
## Starter transport notes
|
|
452
|
+
|
|
453
|
+
${starter.configLines.join('\n')}
|
|
454
|
+
- Default transport behavior: omitting \`--transport\` still resolves to the TCP starter path
|
|
455
|
+
- Broader messaging packages such as \`@fluojs/redis\` remain package-level integration choices, not additional \`fluo new --transport\` starter flags
|
|
456
|
+
|
|
457
|
+
## Official generated testing templates
|
|
458
|
+
|
|
459
|
+
- \`src/math/math.handler.test.ts\` — unit template for the starter-owned message handler.
|
|
460
|
+
- \`src/app.test.ts\` — integration-style microservice test via an in-memory transport implementation.
|
|
461
|
+
|
|
462
|
+
## Generated files that define the starter contract
|
|
463
|
+
|
|
464
|
+
- \`src/app.ts\` — transport registration and environment-driven runtime wiring.
|
|
465
|
+
- \`src/main.ts\` — microservice bootstrap entrypoint.
|
|
466
|
+
- \`src/math/*\` — starter-owned message handler slice proving the generated transport contract.
|
|
467
|
+
${extraFilesSection}
|
|
468
|
+
## Generated-project verification
|
|
469
|
+
|
|
470
|
+
- ${starter.generatedProjectVerification}
|
|
471
|
+
|
|
472
|
+
Use the unit template for handler logic and the integration template when you need runtime wiring confidence. ${starter.testNote}
|
|
473
|
+
`;
|
|
474
|
+
}
|
|
475
|
+
function createProjectReadme(options, bootstrapPlan) {
|
|
476
|
+
if (bootstrapPlan.profile.emitter.type === 'microservice') {
|
|
477
|
+
return createMicroserviceProjectReadme(options);
|
|
478
|
+
}
|
|
479
|
+
if (bootstrapPlan.profile.id === 'mixed-node-fastify-tcp') {
|
|
480
|
+
return createMixedProjectReadme(options);
|
|
481
|
+
}
|
|
482
|
+
return createHttpProjectReadme(options);
|
|
483
|
+
}
|
|
484
|
+
function createAppFile(options) {
|
|
485
|
+
const importSuffix = options.runtime === 'deno' ? '.ts' : '';
|
|
486
|
+
if (options.runtime === 'cloudflare-workers') {
|
|
487
|
+
return `import { Global, Module } from '@fluojs/core';
|
|
488
|
+
import { createHealthModule } from '@fluojs/runtime';
|
|
489
|
+
|
|
490
|
+
import { HealthModule } from './health/health.module';
|
|
491
|
+
|
|
492
|
+
const RuntimeHealthModule = createHealthModule();
|
|
493
|
+
|
|
494
|
+
@Global()
|
|
495
|
+
@Module({
|
|
496
|
+
imports: [
|
|
497
|
+
HealthModule,
|
|
498
|
+
RuntimeHealthModule,
|
|
499
|
+
],
|
|
500
|
+
})
|
|
501
|
+
export class AppModule {}
|
|
502
|
+
`;
|
|
503
|
+
}
|
|
504
|
+
const processEnvValue = options.runtime === 'bun' ? 'Bun.env' : options.runtime === 'deno' ? 'Deno.env.toObject()' : 'process.env';
|
|
505
|
+
return `import { Global, Module } from '@fluojs/core';
|
|
506
|
+
import { ConfigModule } from '@fluojs/config';
|
|
507
|
+
import { createHealthModule } from '@fluojs/runtime';
|
|
508
|
+
|
|
509
|
+
import { HealthModule } from './health/health.module${importSuffix}';
|
|
510
|
+
|
|
511
|
+
const RuntimeHealthModule = createHealthModule();
|
|
512
|
+
|
|
513
|
+
@Global()
|
|
514
|
+
@Module({
|
|
515
|
+
imports: [
|
|
516
|
+
ConfigModule.forRoot({
|
|
517
|
+
envFile: '.env',
|
|
518
|
+
processEnv: ${processEnvValue},
|
|
519
|
+
}),
|
|
520
|
+
HealthModule,
|
|
521
|
+
RuntimeHealthModule,
|
|
522
|
+
],
|
|
523
|
+
})
|
|
524
|
+
export class AppModule {}
|
|
525
|
+
`;
|
|
526
|
+
}
|
|
527
|
+
function createHealthResponseDtoFile() {
|
|
528
|
+
return `export class HealthResponseDto {
|
|
529
|
+
ok!: boolean;
|
|
530
|
+
service!: string;
|
|
531
|
+
}
|
|
532
|
+
`;
|
|
533
|
+
}
|
|
534
|
+
function createHealthRepoFile(projectName, importSuffix = '') {
|
|
535
|
+
return `import type { HealthResponseDto } from './health.response.dto${importSuffix}';
|
|
536
|
+
|
|
537
|
+
export class HealthRepo {
|
|
538
|
+
findHealth(): HealthResponseDto {
|
|
539
|
+
return {
|
|
540
|
+
ok: true,
|
|
541
|
+
service: ${JSON.stringify(projectName)},
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
`;
|
|
546
|
+
}
|
|
547
|
+
function createHealthRepoTestFile() {
|
|
548
|
+
return `import { describe, expect, it } from 'vitest';
|
|
549
|
+
|
|
550
|
+
import { HealthRepo } from './health.repo';
|
|
551
|
+
|
|
552
|
+
describe('HealthRepo', () => {
|
|
553
|
+
it('returns health data', () => {
|
|
554
|
+
const repo = new HealthRepo();
|
|
555
|
+
expect(repo.findHealth()).toEqual({ ok: true, service: expect.any(String) });
|
|
556
|
+
});
|
|
557
|
+
});
|
|
558
|
+
`;
|
|
559
|
+
}
|
|
560
|
+
function createHealthServiceFile(importSuffix = '') {
|
|
561
|
+
return `import { Inject } from '@fluojs/core';
|
|
562
|
+
import type { HealthResponseDto } from './health.response.dto${importSuffix}';
|
|
563
|
+
|
|
564
|
+
import { HealthRepo } from './health.repo${importSuffix}';
|
|
565
|
+
|
|
566
|
+
@Inject(HealthRepo)
|
|
567
|
+
export class HealthService {
|
|
568
|
+
constructor(private readonly repo: HealthRepo) {}
|
|
569
|
+
|
|
570
|
+
getHealth(): HealthResponseDto {
|
|
571
|
+
return this.repo.findHealth();
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
`;
|
|
575
|
+
}
|
|
576
|
+
function createHealthServiceTestFile() {
|
|
577
|
+
return `import { describe, expect, it } from 'vitest';
|
|
578
|
+
|
|
579
|
+
import { HealthService } from './health.service';
|
|
580
|
+
import { HealthRepo } from './health.repo';
|
|
581
|
+
|
|
582
|
+
class FakeHealthRepo {
|
|
583
|
+
findHealth() {
|
|
584
|
+
return { ok: true, service: 'test' };
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
describe('HealthService', () => {
|
|
589
|
+
it('delegates to the repo', () => {
|
|
590
|
+
const service = new HealthService(new FakeHealthRepo() as HealthRepo);
|
|
591
|
+
expect(service.getHealth()).toEqual({ ok: true, service: 'test' });
|
|
592
|
+
});
|
|
593
|
+
});
|
|
594
|
+
`;
|
|
595
|
+
}
|
|
596
|
+
function createHealthControllerFile(importSuffix = '') {
|
|
597
|
+
return `import { Inject } from '@fluojs/core';
|
|
598
|
+
import { Controller, Get } from '@fluojs/http';
|
|
599
|
+
|
|
600
|
+
import { HealthService } from './health.service${importSuffix}';
|
|
601
|
+
import { HealthResponseDto } from './health.response.dto${importSuffix}';
|
|
602
|
+
|
|
603
|
+
@Inject(HealthService)
|
|
604
|
+
@Controller('/health-info')
|
|
605
|
+
export class HealthController {
|
|
606
|
+
constructor(private readonly service: HealthService) {}
|
|
607
|
+
|
|
608
|
+
@Get('/')
|
|
609
|
+
getHealth(): HealthResponseDto {
|
|
610
|
+
return this.service.getHealth();
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
`;
|
|
614
|
+
}
|
|
615
|
+
function createHealthControllerTestFile() {
|
|
616
|
+
return `import { describe, expect, it } from 'vitest';
|
|
617
|
+
|
|
618
|
+
import { HealthController } from './health.controller';
|
|
619
|
+
|
|
620
|
+
class FakeHealthService {
|
|
621
|
+
getHealth() {
|
|
622
|
+
return { ok: true, service: 'test' };
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
describe('HealthController', () => {
|
|
627
|
+
it('delegates to the service', () => {
|
|
628
|
+
const controller = new HealthController(new FakeHealthService() as never);
|
|
629
|
+
expect(controller.getHealth()).toEqual({ ok: true, service: 'test' });
|
|
630
|
+
});
|
|
631
|
+
});
|
|
632
|
+
`;
|
|
633
|
+
}
|
|
634
|
+
function createHealthModuleFile(importSuffix = '') {
|
|
635
|
+
return `import { Module } from '@fluojs/core';
|
|
636
|
+
|
|
637
|
+
import { HealthController } from './health.controller${importSuffix}';
|
|
638
|
+
import { HealthRepo } from './health.repo${importSuffix}';
|
|
639
|
+
import { HealthService } from './health.service${importSuffix}';
|
|
640
|
+
|
|
641
|
+
@Module({
|
|
642
|
+
controllers: [HealthController],
|
|
643
|
+
providers: [HealthRepo, HealthService],
|
|
644
|
+
})
|
|
645
|
+
export class HealthModule {}
|
|
646
|
+
`;
|
|
647
|
+
}
|
|
648
|
+
function createMainFile(options) {
|
|
649
|
+
const starter = describeApplicationStarter(options);
|
|
650
|
+
if (options.runtime === 'deno') {
|
|
651
|
+
return `import { runDenoApplication } from '@fluojs/platform-deno';
|
|
652
|
+
|
|
653
|
+
import { AppModule } from './app.ts';
|
|
654
|
+
|
|
655
|
+
// The generated starter wires the selected first-class fluo new application path:
|
|
656
|
+
// Deno runtime + Deno native HTTP via runDenoApplication(...).
|
|
657
|
+
|
|
658
|
+
const parsedPort = Number.parseInt(Deno.env.get('PORT') ?? '3000', 10);
|
|
659
|
+
const port = Number.isFinite(parsedPort) ? parsedPort : 3000;
|
|
660
|
+
|
|
661
|
+
await runDenoApplication(AppModule, { port });
|
|
662
|
+
`;
|
|
663
|
+
}
|
|
664
|
+
if (options.runtime === 'cloudflare-workers') {
|
|
665
|
+
return `import { createCloudflareWorkerEntrypoint } from '@fluojs/platform-cloudflare-workers';
|
|
666
|
+
|
|
667
|
+
import { AppModule } from './app';
|
|
668
|
+
|
|
669
|
+
// The generated starter wires the selected first-class fluo new application path:
|
|
670
|
+
// Cloudflare Workers runtime + Cloudflare Workers HTTP via createCloudflareWorkerEntrypoint(...).
|
|
671
|
+
|
|
672
|
+
const worker = createCloudflareWorkerEntrypoint(AppModule);
|
|
673
|
+
|
|
674
|
+
export default {
|
|
675
|
+
fetch: worker.fetch,
|
|
676
|
+
};
|
|
677
|
+
`;
|
|
678
|
+
}
|
|
679
|
+
const portExpression = options.runtime === 'bun' ? "Bun.env.PORT ?? '3000'" : "process.env.PORT ?? '3000'";
|
|
680
|
+
return `import { ${starter.adapterFactory} } from '${starter.packageName}';
|
|
681
|
+
import { FluoFactory } from '@fluojs/runtime';
|
|
682
|
+
|
|
683
|
+
import { AppModule } from './app';
|
|
684
|
+
|
|
685
|
+
// The generated starter wires the selected first-class fluo new application path:
|
|
686
|
+
// ${starter.runtimeLabel} + ${starter.platformLabel} via ${starter.adapterFactory}(...).
|
|
687
|
+
|
|
688
|
+
const parsedPort = Number.parseInt(${portExpression}, 10);
|
|
689
|
+
const port = Number.isFinite(parsedPort) ? parsedPort : 3000;
|
|
690
|
+
|
|
691
|
+
const app = await FluoFactory.create(AppModule, {
|
|
692
|
+
adapter: ${starter.adapterCall},
|
|
693
|
+
});
|
|
694
|
+
await app.listen();
|
|
695
|
+
`;
|
|
696
|
+
}
|
|
697
|
+
function createMicroserviceAppFile(options) {
|
|
698
|
+
switch (options.transport) {
|
|
699
|
+
case 'redis-streams':
|
|
700
|
+
return `import Redis from 'ioredis';
|
|
701
|
+
import { Module } from '@fluojs/core';
|
|
702
|
+
import { ConfigModule } from '@fluojs/config';
|
|
703
|
+
import { MicroservicesModule, RedisStreamsMicroserviceTransport, type RedisStreamClientLike } from '@fluojs/microservices';
|
|
704
|
+
|
|
705
|
+
import { MathHandler } from './math/math.handler';
|
|
706
|
+
|
|
707
|
+
const redisUrl = process.env.REDIS_URL ?? 'redis://127.0.0.1:6379';
|
|
708
|
+
const namespace = process.env.REDIS_STREAMS_NAMESPACE ?? 'fluo:streams';
|
|
709
|
+
const consumerGroup = process.env.REDIS_STREAMS_CONSUMER_GROUP ?? 'fluo-handlers';
|
|
710
|
+
const readerRedis = new Redis(redisUrl, { lazyConnect: true, maxRetriesPerRequest: 1 });
|
|
711
|
+
const writerRedis = new Redis(redisUrl, { lazyConnect: true, maxRetriesPerRequest: 1 });
|
|
712
|
+
|
|
713
|
+
const readerClient: RedisStreamClientLike = {
|
|
714
|
+
async decr(key) {
|
|
715
|
+
return await readerRedis.decr(key);
|
|
716
|
+
},
|
|
717
|
+
async del(key) {
|
|
718
|
+
await readerRedis.del(key);
|
|
719
|
+
},
|
|
720
|
+
async get(key) {
|
|
721
|
+
return await readerRedis.get(key);
|
|
722
|
+
},
|
|
723
|
+
async incr(key) {
|
|
724
|
+
return await readerRedis.incr(key);
|
|
725
|
+
},
|
|
726
|
+
async xack(stream, group, id) {
|
|
727
|
+
await readerRedis.xack(stream, group, id);
|
|
728
|
+
},
|
|
729
|
+
async xadd(stream, fields) {
|
|
730
|
+
return await readerRedis.xadd(stream, '*', ...Object.entries(fields).flatMap(([key, value]) => [key, value]));
|
|
731
|
+
},
|
|
732
|
+
async xdel(stream, id) {
|
|
733
|
+
await readerRedis.xdel(stream, id);
|
|
734
|
+
},
|
|
735
|
+
async xgroupCreate(stream, group, startId, mkstream) {
|
|
736
|
+
if (mkstream) {
|
|
737
|
+
await readerRedis.xgroup('CREATE', stream, group, startId, 'MKSTREAM');
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
await readerRedis.xgroup('CREATE', stream, group, startId);
|
|
742
|
+
},
|
|
743
|
+
async xgroupDestroy(stream, group) {
|
|
744
|
+
await readerRedis.xgroup('DESTROY', stream, group);
|
|
745
|
+
},
|
|
746
|
+
async set(key, value) {
|
|
747
|
+
return await readerRedis.set(key, value);
|
|
748
|
+
},
|
|
749
|
+
async xreadgroup(group, consumer, streams, options) {
|
|
750
|
+
const response = await readerRedis.xreadgroup(
|
|
751
|
+
'GROUP',
|
|
752
|
+
group,
|
|
753
|
+
consumer,
|
|
754
|
+
options?.count ? 'COUNT' : undefined,
|
|
755
|
+
options?.count ? String(options.count) : undefined,
|
|
756
|
+
options?.blockMs ? 'BLOCK' : undefined,
|
|
757
|
+
options?.blockMs ? String(options.blockMs) : undefined,
|
|
758
|
+
'STREAMS',
|
|
759
|
+
...streams,
|
|
760
|
+
...streams.map(() => '>'),
|
|
761
|
+
);
|
|
762
|
+
|
|
763
|
+
if (!response) {
|
|
764
|
+
return null;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
return response.flatMap(([, entries]) => entries.map(([id, values]) => ({
|
|
768
|
+
fields: Object.fromEntries(values.reduce<string[][]>((pairs, value, index, source) => {
|
|
769
|
+
if (index % 2 === 0) {
|
|
770
|
+
pairs.push([value, source[index + 1] ?? '']);
|
|
771
|
+
}
|
|
772
|
+
return pairs;
|
|
773
|
+
}, [])),
|
|
774
|
+
id,
|
|
775
|
+
})));
|
|
776
|
+
},
|
|
777
|
+
};
|
|
778
|
+
|
|
779
|
+
const writerClient: RedisStreamClientLike = {
|
|
780
|
+
async decr(key) {
|
|
781
|
+
return await writerRedis.decr(key);
|
|
782
|
+
},
|
|
783
|
+
async del(key) {
|
|
784
|
+
await writerRedis.del(key);
|
|
785
|
+
},
|
|
786
|
+
async get(key) {
|
|
787
|
+
return await writerRedis.get(key);
|
|
788
|
+
},
|
|
789
|
+
async incr(key) {
|
|
790
|
+
return await writerRedis.incr(key);
|
|
791
|
+
},
|
|
792
|
+
async xack(stream, group, id) {
|
|
793
|
+
await writerRedis.xack(stream, group, id);
|
|
794
|
+
},
|
|
795
|
+
async xadd(stream, fields) {
|
|
796
|
+
return await writerRedis.xadd(stream, '*', ...Object.entries(fields).flatMap(([key, value]) => [key, value]));
|
|
797
|
+
},
|
|
798
|
+
async xdel(stream, id) {
|
|
799
|
+
await writerRedis.xdel(stream, id);
|
|
800
|
+
},
|
|
801
|
+
async xgroupCreate(stream, group, startId, mkstream) {
|
|
802
|
+
if (mkstream) {
|
|
803
|
+
await writerRedis.xgroup('CREATE', stream, group, startId, 'MKSTREAM');
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
await writerRedis.xgroup('CREATE', stream, group, startId);
|
|
808
|
+
},
|
|
809
|
+
async xgroupDestroy(stream, group) {
|
|
810
|
+
await writerRedis.xgroup('DESTROY', stream, group);
|
|
811
|
+
},
|
|
812
|
+
async set(key, value) {
|
|
813
|
+
return await writerRedis.set(key, value);
|
|
814
|
+
},
|
|
815
|
+
async xreadgroup(group, consumer, streams, options) {
|
|
816
|
+
const response = await writerRedis.xreadgroup(
|
|
817
|
+
'GROUP',
|
|
818
|
+
group,
|
|
819
|
+
consumer,
|
|
820
|
+
options?.count ? 'COUNT' : undefined,
|
|
821
|
+
options?.count ? String(options.count) : undefined,
|
|
822
|
+
options?.blockMs ? 'BLOCK' : undefined,
|
|
823
|
+
options?.blockMs ? String(options.blockMs) : undefined,
|
|
824
|
+
'STREAMS',
|
|
825
|
+
...streams,
|
|
826
|
+
...streams.map(() => '>'),
|
|
827
|
+
);
|
|
828
|
+
|
|
829
|
+
if (!response) {
|
|
830
|
+
return null;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
return response.flatMap(([, entries]) => entries.map(([id, values]) => ({
|
|
834
|
+
fields: Object.fromEntries(values.reduce<string[][]>((pairs, value, index, source) => {
|
|
835
|
+
if (index % 2 === 0) {
|
|
836
|
+
pairs.push([value, source[index + 1] ?? '']);
|
|
837
|
+
}
|
|
838
|
+
return pairs;
|
|
839
|
+
}, [])),
|
|
840
|
+
id,
|
|
841
|
+
})));
|
|
842
|
+
},
|
|
843
|
+
};
|
|
844
|
+
|
|
845
|
+
@Module({
|
|
846
|
+
imports: [
|
|
847
|
+
ConfigModule.forRoot({
|
|
848
|
+
envFile: '.env',
|
|
849
|
+
processEnv: process.env,
|
|
850
|
+
}),
|
|
851
|
+
MicroservicesModule.forRoot({
|
|
852
|
+
transport: new RedisStreamsMicroserviceTransport({
|
|
853
|
+
consumerGroup,
|
|
854
|
+
namespace,
|
|
855
|
+
readerClient,
|
|
856
|
+
writerClient,
|
|
857
|
+
}),
|
|
858
|
+
}),
|
|
859
|
+
],
|
|
860
|
+
providers: [MathHandler],
|
|
861
|
+
})
|
|
862
|
+
export class AppModule {}
|
|
863
|
+
`;
|
|
864
|
+
case 'mqtt':
|
|
865
|
+
return `import { Module } from '@fluojs/core';
|
|
866
|
+
import { ConfigModule } from '@fluojs/config';
|
|
867
|
+
import { MicroservicesModule, MqttMicroserviceTransport } from '@fluojs/microservices';
|
|
868
|
+
|
|
869
|
+
import { MathHandler } from './math/math.handler';
|
|
870
|
+
|
|
871
|
+
const url = process.env.MQTT_URL ?? 'mqtt://127.0.0.1:1883';
|
|
872
|
+
const namespace = process.env.MQTT_NAMESPACE ?? 'fluo.microservices';
|
|
873
|
+
|
|
874
|
+
@Module({
|
|
875
|
+
imports: [
|
|
876
|
+
ConfigModule.forRoot({
|
|
877
|
+
envFile: '.env',
|
|
878
|
+
processEnv: process.env,
|
|
879
|
+
}),
|
|
880
|
+
MicroservicesModule.forRoot({
|
|
881
|
+
transport: new MqttMicroserviceTransport({
|
|
882
|
+
namespace,
|
|
883
|
+
requestTimeoutMs: 3_000,
|
|
884
|
+
url,
|
|
885
|
+
}),
|
|
886
|
+
}),
|
|
887
|
+
],
|
|
888
|
+
providers: [MathHandler],
|
|
889
|
+
})
|
|
890
|
+
export class AppModule {}
|
|
891
|
+
`;
|
|
892
|
+
case 'grpc':
|
|
893
|
+
return `import { resolve } from 'node:path';
|
|
894
|
+
|
|
895
|
+
import { Module } from '@fluojs/core';
|
|
896
|
+
import { ConfigModule } from '@fluojs/config';
|
|
897
|
+
import { GrpcMicroserviceTransport, MicroservicesModule } from '@fluojs/microservices';
|
|
898
|
+
|
|
899
|
+
import { MathHandler } from './math/math.handler';
|
|
900
|
+
|
|
901
|
+
const url = process.env.GRPC_URL ?? '127.0.0.1:50051';
|
|
902
|
+
const protoPath = resolve(process.cwd(), 'proto', 'math.proto');
|
|
903
|
+
|
|
904
|
+
@Module({
|
|
905
|
+
imports: [
|
|
906
|
+
ConfigModule.forRoot({
|
|
907
|
+
envFile: '.env',
|
|
908
|
+
processEnv: process.env,
|
|
909
|
+
}),
|
|
910
|
+
MicroservicesModule.forRoot({
|
|
911
|
+
transport: new GrpcMicroserviceTransport({
|
|
912
|
+
packageName: 'fluo.microservices',
|
|
913
|
+
protoPath,
|
|
914
|
+
services: ['MathService'],
|
|
915
|
+
url,
|
|
916
|
+
}),
|
|
917
|
+
}),
|
|
918
|
+
],
|
|
919
|
+
providers: [MathHandler],
|
|
920
|
+
})
|
|
921
|
+
export class AppModule {}
|
|
922
|
+
`;
|
|
923
|
+
case 'nats':
|
|
924
|
+
return `import { Module } from '@fluojs/core';
|
|
925
|
+
import { ConfigModule } from '@fluojs/config';
|
|
926
|
+
import { MicroservicesModule, NatsMicroserviceTransport } from '@fluojs/microservices';
|
|
927
|
+
import { JSONCodec, connect } from 'nats';
|
|
928
|
+
|
|
929
|
+
import { MathHandler } from './math/math.handler';
|
|
930
|
+
|
|
931
|
+
const servers = (process.env.NATS_SERVERS ?? 'nats://127.0.0.1:4222')
|
|
932
|
+
.split(',')
|
|
933
|
+
.map((value) => value.trim())
|
|
934
|
+
.filter((value) => value.length > 0);
|
|
935
|
+
const eventSubject = process.env.NATS_EVENT_SUBJECT ?? 'fluo.microservices.events';
|
|
936
|
+
const messageSubject = process.env.NATS_MESSAGE_SUBJECT ?? 'fluo.microservices.messages';
|
|
937
|
+
const codec = JSONCodec();
|
|
938
|
+
const connection = await connect({
|
|
939
|
+
name: 'fluo-microservice-starter',
|
|
940
|
+
servers,
|
|
941
|
+
});
|
|
942
|
+
const client = {
|
|
943
|
+
close() {
|
|
944
|
+
void connection.close();
|
|
945
|
+
},
|
|
946
|
+
publish(subject: string, payload: Uint8Array) {
|
|
947
|
+
connection.publish(subject, payload);
|
|
948
|
+
},
|
|
949
|
+
request(subject: string, payload: Uint8Array, options?: { timeout?: number }) {
|
|
950
|
+
return connection.request(subject, payload, options);
|
|
951
|
+
},
|
|
952
|
+
subscribe(subject: string, handler: (message: { data: Uint8Array; respond(data: Uint8Array): void }) => void) {
|
|
953
|
+
const subscription = connection.subscribe(subject);
|
|
954
|
+
|
|
955
|
+
void (async () => {
|
|
956
|
+
for await (const message of subscription) {
|
|
957
|
+
handler(message);
|
|
958
|
+
}
|
|
959
|
+
})();
|
|
960
|
+
|
|
961
|
+
return {
|
|
962
|
+
unsubscribe() {
|
|
963
|
+
subscription.unsubscribe();
|
|
964
|
+
},
|
|
965
|
+
};
|
|
966
|
+
},
|
|
967
|
+
};
|
|
968
|
+
|
|
969
|
+
@Module({
|
|
970
|
+
imports: [
|
|
971
|
+
ConfigModule.forRoot({
|
|
972
|
+
envFile: '.env',
|
|
973
|
+
processEnv: process.env,
|
|
974
|
+
}),
|
|
975
|
+
MicroservicesModule.forRoot({
|
|
976
|
+
transport: new NatsMicroserviceTransport({
|
|
977
|
+
client,
|
|
978
|
+
codec,
|
|
979
|
+
eventSubject,
|
|
980
|
+
messageSubject,
|
|
981
|
+
requestTimeoutMs: 3_000,
|
|
982
|
+
}),
|
|
983
|
+
}),
|
|
984
|
+
],
|
|
985
|
+
providers: [MathHandler],
|
|
986
|
+
})
|
|
987
|
+
export class AppModule {}
|
|
988
|
+
`;
|
|
989
|
+
case 'kafka':
|
|
990
|
+
return `import { Module } from '@fluojs/core';
|
|
991
|
+
import { ConfigModule } from '@fluojs/config';
|
|
992
|
+
import { Kafka, logLevel } from 'kafkajs';
|
|
993
|
+
import { KafkaMicroserviceTransport, MicroservicesModule } from '@fluojs/microservices';
|
|
994
|
+
|
|
995
|
+
import { MathHandler } from './math/math.handler';
|
|
996
|
+
|
|
997
|
+
const brokers = (process.env.KAFKA_BROKERS ?? '127.0.0.1:9092')
|
|
998
|
+
.split(',')
|
|
999
|
+
.map((value) => value.trim())
|
|
1000
|
+
.filter((value) => value.length > 0);
|
|
1001
|
+
const clientId = process.env.KAFKA_CLIENT_ID ?? 'fluo-microservice-starter';
|
|
1002
|
+
const consumerGroup = process.env.KAFKA_CONSUMER_GROUP ?? 'fluo-handlers';
|
|
1003
|
+
const eventTopic = process.env.KAFKA_EVENT_TOPIC ?? 'fluo.microservices.events';
|
|
1004
|
+
const messageTopic = process.env.KAFKA_MESSAGE_TOPIC ?? 'fluo.microservices.messages';
|
|
1005
|
+
const responseTopic = process.env.KAFKA_RESPONSE_TOPIC ?? 'fluo.microservices.responses';
|
|
1006
|
+
const kafka = new Kafka({
|
|
1007
|
+
brokers,
|
|
1008
|
+
clientId,
|
|
1009
|
+
logLevel: logLevel.NOTHING,
|
|
1010
|
+
});
|
|
1011
|
+
const producer = kafka.producer();
|
|
1012
|
+
const consumer = kafka.consumer({ groupId: consumerGroup });
|
|
1013
|
+
await Promise.all([producer.connect(), consumer.connect()]);
|
|
1014
|
+
|
|
1015
|
+
const handlers = new Map<string, (message: string) => Promise<void> | void>();
|
|
1016
|
+
let consumerRunning = false;
|
|
1017
|
+
|
|
1018
|
+
const producerClient = {
|
|
1019
|
+
async publish(topic: string, message: string) {
|
|
1020
|
+
await producer.send({
|
|
1021
|
+
messages: [{ value: message }],
|
|
1022
|
+
topic,
|
|
1023
|
+
});
|
|
1024
|
+
},
|
|
1025
|
+
};
|
|
1026
|
+
|
|
1027
|
+
const consumerClient = {
|
|
1028
|
+
async subscribe(topic: string, handler: (message: string) => Promise<void> | void) {
|
|
1029
|
+
handlers.set(topic, handler);
|
|
1030
|
+
await consumer.subscribe({ fromBeginning: false, topic });
|
|
1031
|
+
|
|
1032
|
+
if (consumerRunning) {
|
|
1033
|
+
return;
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
consumerRunning = true;
|
|
1037
|
+
void consumer.run({
|
|
1038
|
+
eachMessage: async ({ topic, message }) => {
|
|
1039
|
+
const value = message.value?.toString();
|
|
1040
|
+
|
|
1041
|
+
if (!value) {
|
|
1042
|
+
return;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
await handlers.get(topic)?.(value);
|
|
1046
|
+
},
|
|
1047
|
+
});
|
|
1048
|
+
},
|
|
1049
|
+
async unsubscribe(topic: string) {
|
|
1050
|
+
handlers.delete(topic);
|
|
1051
|
+
|
|
1052
|
+
if (handlers.size > 0) {
|
|
1053
|
+
return;
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
consumerRunning = false;
|
|
1057
|
+
await consumer.stop();
|
|
1058
|
+
await Promise.all([consumer.disconnect(), producer.disconnect()]);
|
|
1059
|
+
},
|
|
1060
|
+
};
|
|
1061
|
+
|
|
1062
|
+
@Module({
|
|
1063
|
+
imports: [
|
|
1064
|
+
ConfigModule.forRoot({
|
|
1065
|
+
envFile: '.env',
|
|
1066
|
+
processEnv: process.env,
|
|
1067
|
+
}),
|
|
1068
|
+
MicroservicesModule.forRoot({
|
|
1069
|
+
transport: new KafkaMicroserviceTransport({
|
|
1070
|
+
consumer: consumerClient,
|
|
1071
|
+
eventTopic,
|
|
1072
|
+
messageTopic,
|
|
1073
|
+
producer: producerClient,
|
|
1074
|
+
requestTimeoutMs: 3_000,
|
|
1075
|
+
responseTopic,
|
|
1076
|
+
}),
|
|
1077
|
+
}),
|
|
1078
|
+
],
|
|
1079
|
+
providers: [MathHandler],
|
|
1080
|
+
})
|
|
1081
|
+
export class AppModule {}
|
|
1082
|
+
`;
|
|
1083
|
+
case 'rabbitmq':
|
|
1084
|
+
return `import { connect } from 'amqplib';
|
|
1085
|
+
|
|
1086
|
+
import { Module } from '@fluojs/core';
|
|
1087
|
+
import { ConfigModule } from '@fluojs/config';
|
|
1088
|
+
import { MicroservicesModule, RabbitMqMicroserviceTransport } from '@fluojs/microservices';
|
|
1089
|
+
|
|
1090
|
+
import { MathHandler } from './math/math.handler';
|
|
1091
|
+
|
|
1092
|
+
const url = process.env.RABBITMQ_URL ?? 'amqp://127.0.0.1:5672';
|
|
1093
|
+
const eventQueue = process.env.RABBITMQ_EVENT_QUEUE ?? 'fluo.microservices.events';
|
|
1094
|
+
const messageQueue = process.env.RABBITMQ_MESSAGE_QUEUE ?? 'fluo.microservices.messages';
|
|
1095
|
+
const responseQueue = process.env.RABBITMQ_RESPONSE_QUEUE ?? 'fluo.microservices.responses';
|
|
1096
|
+
const connection = await connect(url);
|
|
1097
|
+
const channel = await connection.createConfirmChannel();
|
|
1098
|
+
const consumerTags = new Map<string, string>();
|
|
1099
|
+
|
|
1100
|
+
const publisher = {
|
|
1101
|
+
async publish(queue: string, message: string) {
|
|
1102
|
+
await channel.assertQueue(queue, { durable: true });
|
|
1103
|
+
await new Promise<void>((resolve, reject) => {
|
|
1104
|
+
channel.sendToQueue(queue, Buffer.from(message), { persistent: true }, (error) => {
|
|
1105
|
+
if (error) {
|
|
1106
|
+
reject(error);
|
|
1107
|
+
return;
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
resolve();
|
|
1111
|
+
});
|
|
1112
|
+
});
|
|
1113
|
+
},
|
|
1114
|
+
};
|
|
1115
|
+
|
|
1116
|
+
const consumer = {
|
|
1117
|
+
async cancel(queue: string) {
|
|
1118
|
+
const consumerTag = consumerTags.get(queue);
|
|
1119
|
+
|
|
1120
|
+
if (!consumerTag) {
|
|
1121
|
+
return;
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
consumerTags.delete(queue);
|
|
1125
|
+
await channel.cancel(consumerTag);
|
|
1126
|
+
|
|
1127
|
+
if (consumerTags.size === 0) {
|
|
1128
|
+
await channel.close();
|
|
1129
|
+
await connection.close();
|
|
1130
|
+
}
|
|
1131
|
+
},
|
|
1132
|
+
async consume(queue: string, handler: (message: string) => Promise<void> | void) {
|
|
1133
|
+
await channel.assertQueue(queue, { durable: true });
|
|
1134
|
+
const result = await channel.consume(queue, (message) => {
|
|
1135
|
+
if (!message) {
|
|
1136
|
+
return;
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
void Promise.resolve(handler(message.content.toString()))
|
|
1140
|
+
.then(() => {
|
|
1141
|
+
channel.ack(message);
|
|
1142
|
+
})
|
|
1143
|
+
.catch(() => {
|
|
1144
|
+
channel.nack(message, false, false);
|
|
1145
|
+
});
|
|
1146
|
+
});
|
|
1147
|
+
|
|
1148
|
+
consumerTags.set(queue, result.consumerTag);
|
|
1149
|
+
},
|
|
1150
|
+
};
|
|
1151
|
+
|
|
1152
|
+
@Module({
|
|
1153
|
+
imports: [
|
|
1154
|
+
ConfigModule.forRoot({
|
|
1155
|
+
envFile: '.env',
|
|
1156
|
+
processEnv: process.env,
|
|
1157
|
+
}),
|
|
1158
|
+
MicroservicesModule.forRoot({
|
|
1159
|
+
transport: new RabbitMqMicroserviceTransport({
|
|
1160
|
+
consumer,
|
|
1161
|
+
eventQueue,
|
|
1162
|
+
messageQueue,
|
|
1163
|
+
publisher,
|
|
1164
|
+
requestTimeoutMs: 3_000,
|
|
1165
|
+
responseQueue,
|
|
1166
|
+
}),
|
|
1167
|
+
}),
|
|
1168
|
+
],
|
|
1169
|
+
providers: [MathHandler],
|
|
1170
|
+
})
|
|
1171
|
+
export class AppModule {}
|
|
1172
|
+
`;
|
|
1173
|
+
default:
|
|
1174
|
+
return `import { Module } from '@fluojs/core';
|
|
1175
|
+
import { ConfigModule } from '@fluojs/config';
|
|
1176
|
+
import { MicroservicesModule, TcpMicroserviceTransport } from '@fluojs/microservices';
|
|
1177
|
+
|
|
1178
|
+
import { MathHandler } from './math/math.handler';
|
|
1179
|
+
|
|
1180
|
+
const parsedPort = Number.parseInt(process.env.MICROSERVICE_PORT ?? '4000', 10);
|
|
1181
|
+
const port = Number.isFinite(parsedPort) ? parsedPort : 4000;
|
|
1182
|
+
const host = process.env.MICROSERVICE_HOST ?? '127.0.0.1';
|
|
1183
|
+
|
|
1184
|
+
@Module({
|
|
1185
|
+
imports: [
|
|
1186
|
+
ConfigModule.forRoot({
|
|
1187
|
+
envFile: '.env',
|
|
1188
|
+
processEnv: process.env,
|
|
1189
|
+
}),
|
|
1190
|
+
MicroservicesModule.forRoot({
|
|
1191
|
+
transport: new TcpMicroserviceTransport({ host, port }),
|
|
1192
|
+
}),
|
|
1193
|
+
],
|
|
1194
|
+
providers: [MathHandler],
|
|
1195
|
+
})
|
|
1196
|
+
export class AppModule {}
|
|
1197
|
+
`;
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
function createMicroserviceMainFile() {
|
|
1201
|
+
return `import { FluoFactory } from '@fluojs/runtime';
|
|
1202
|
+
|
|
1203
|
+
import { AppModule } from './app';
|
|
1204
|
+
|
|
1205
|
+
const microservice = await FluoFactory.createMicroservice(AppModule);
|
|
1206
|
+
await microservice.listen();
|
|
1207
|
+
`;
|
|
1208
|
+
}
|
|
1209
|
+
function createMathHandlerFile(options) {
|
|
1210
|
+
const pattern = describeMicroserviceStarter(options).pattern;
|
|
1211
|
+
if (options.transport === 'grpc') {
|
|
1212
|
+
return `import { MessagePattern } from '@fluojs/microservices';
|
|
1213
|
+
|
|
1214
|
+
type SumInput = {
|
|
1215
|
+
a: number;
|
|
1216
|
+
b: number;
|
|
1217
|
+
};
|
|
1218
|
+
|
|
1219
|
+
type SumResponse = {
|
|
1220
|
+
result: number;
|
|
1221
|
+
};
|
|
1222
|
+
|
|
1223
|
+
export class MathHandler {
|
|
1224
|
+
@MessagePattern(${JSON.stringify(pattern)})
|
|
1225
|
+
sum(input: SumInput): SumResponse {
|
|
1226
|
+
return { result: input.a + input.b };
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
`;
|
|
1230
|
+
}
|
|
1231
|
+
return `import { MessagePattern } from '@fluojs/microservices';
|
|
1232
|
+
|
|
1233
|
+
type SumInput = {
|
|
1234
|
+
a: number;
|
|
1235
|
+
b: number;
|
|
1236
|
+
};
|
|
1237
|
+
|
|
1238
|
+
export class MathHandler {
|
|
1239
|
+
@MessagePattern(${JSON.stringify(pattern)})
|
|
1240
|
+
sum(input: SumInput): number {
|
|
1241
|
+
return input.a + input.b;
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
`;
|
|
1245
|
+
}
|
|
1246
|
+
function createMathHandlerTestFile(options) {
|
|
1247
|
+
const expectation = createMicroserviceResponseExpectation(options);
|
|
1248
|
+
return `import { describe, expect, it } from 'vitest';
|
|
1249
|
+
|
|
1250
|
+
import { MathHandler } from './math.handler';
|
|
1251
|
+
|
|
1252
|
+
describe('MathHandler', () => {
|
|
1253
|
+
it('sums message payload values', () => {
|
|
1254
|
+
const handler = new MathHandler();
|
|
1255
|
+
|
|
1256
|
+
expect(handler.sum({ a: 20, b: 22 })).toEqual(${expectation});
|
|
1257
|
+
});
|
|
1258
|
+
});
|
|
1259
|
+
`;
|
|
1260
|
+
}
|
|
1261
|
+
function createMicroserviceAppTestFile(options) {
|
|
1262
|
+
const pattern = describeMicroserviceStarter(options).pattern;
|
|
1263
|
+
const expectation = createMicroserviceResponseExpectation(options);
|
|
1264
|
+
return `import { describe, expect, it } from 'vitest';
|
|
1265
|
+
|
|
1266
|
+
import { Module } from '@fluojs/core';
|
|
1267
|
+
import {
|
|
1268
|
+
MicroservicesModule,
|
|
1269
|
+
type MicroserviceTransport,
|
|
1270
|
+
} from '@fluojs/microservices';
|
|
1271
|
+
import { FluoFactory } from '@fluojs/runtime';
|
|
1272
|
+
|
|
1273
|
+
import { MathHandler } from './math/math.handler';
|
|
1274
|
+
|
|
1275
|
+
type TransportHandler = Parameters<MicroserviceTransport['listen']>[0];
|
|
1276
|
+
|
|
1277
|
+
class InMemoryLoopbackTransport implements MicroserviceTransport {
|
|
1278
|
+
private handler: TransportHandler | undefined;
|
|
1279
|
+
|
|
1280
|
+
async close(): Promise<void> {
|
|
1281
|
+
this.handler = undefined;
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
async emit(pattern: string, payload: unknown): Promise<void> {
|
|
1285
|
+
if (!this.handler) {
|
|
1286
|
+
throw new Error('Transport handler is not listening.');
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
await this.handler({ kind: 'event', pattern, payload });
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
async listen(handler: TransportHandler): Promise<void> {
|
|
1293
|
+
this.handler = handler;
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
async send(pattern: string, payload: unknown): Promise<unknown> {
|
|
1297
|
+
if (!this.handler) {
|
|
1298
|
+
throw new Error('Transport handler is not listening.');
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
return await this.handler({ kind: 'message', pattern, payload });
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
describe('AppModule microservice starter', () => {
|
|
1306
|
+
it('routes message patterns through the generated transport contract', async () => {
|
|
1307
|
+
const transport = new InMemoryLoopbackTransport();
|
|
1308
|
+
|
|
1309
|
+
@Module({
|
|
1310
|
+
imports: [MicroservicesModule.forRoot({ transport })],
|
|
1311
|
+
providers: [MathHandler],
|
|
1312
|
+
})
|
|
1313
|
+
class TestAppModule {}
|
|
1314
|
+
|
|
1315
|
+
const microservice = await FluoFactory.createMicroservice(TestAppModule);
|
|
1316
|
+
await microservice.listen();
|
|
1317
|
+
|
|
1318
|
+
await expect(transport.send(${JSON.stringify(pattern)}, { a: 19, b: 23 })).resolves.toEqual(${expectation});
|
|
1319
|
+
|
|
1320
|
+
await microservice.close();
|
|
1321
|
+
});
|
|
1322
|
+
});
|
|
1323
|
+
`;
|
|
1324
|
+
}
|
|
1325
|
+
function createGrpcProtoFile() {
|
|
1326
|
+
return `syntax = "proto3";
|
|
1327
|
+
|
|
1328
|
+
package fluo.microservices;
|
|
1329
|
+
|
|
1330
|
+
service MathService {
|
|
1331
|
+
rpc Sum (SumRequest) returns (SumResponse);
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
message SumRequest {
|
|
1335
|
+
int32 a = 1;
|
|
1336
|
+
int32 b = 2;
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
message SumResponse {
|
|
1340
|
+
int32 result = 1;
|
|
1341
|
+
}
|
|
1342
|
+
`;
|
|
1343
|
+
}
|
|
1344
|
+
function createMixedProjectReadme(options) {
|
|
1345
|
+
return `# ${options.projectName}
|
|
1346
|
+
|
|
1347
|
+
Generated by @fluojs/cli.
|
|
1348
|
+
|
|
1349
|
+
- Shape: \`mixed\`
|
|
1350
|
+
- Supported topology: \`single-package\` generates one shared Node.js package where the Fastify HTTP application also starts an attached TCP microservice from the same \`src/main.ts\`
|
|
1351
|
+
- Runtime contract: \`src/app.ts\` owns one shared module graph, and \`src/main.ts\` bootstraps the HTTP API before calling \`connectMicroservice()\` and \`startAllMicroservices()\`
|
|
1352
|
+
- Intentional limitation: the first mixed release supports only the explicit Fastify HTTP + attached TCP microservice contract; other mixed transports or separate multi-entrypoint layouts fail validation instead of generating partial scaffolds
|
|
1353
|
+
|
|
1354
|
+
## Commands
|
|
1355
|
+
|
|
1356
|
+
- Dev: ${createRunCommand(options.packageManager, 'dev')}
|
|
1357
|
+
- Build: ${createRunCommand(options.packageManager, 'build')}
|
|
1358
|
+
- Typecheck: ${createRunCommand(options.packageManager, 'typecheck')}
|
|
1359
|
+
- Test: ${createRunCommand(options.packageManager, 'test')}
|
|
1360
|
+
|
|
1361
|
+
## Generated topology
|
|
1362
|
+
|
|
1363
|
+
- \`src/app.ts\` — shared application module with config, runtime health endpoints, and the TCP microservice registration.
|
|
1364
|
+
- \`src/main.ts\` — Fastify HTTP bootstrap that also starts the attached TCP microservice.
|
|
1365
|
+
- \`src/health/*\` — starter-owned HTTP health slice.
|
|
1366
|
+
- \`src/math/*\` — starter-owned message handler slice that proves the microservice half of the topology.
|
|
1367
|
+
|
|
1368
|
+
## Official generated testing templates
|
|
1369
|
+
|
|
1370
|
+
- \`src/health/*.test.ts\` — unit templates for the HTTP slice.
|
|
1371
|
+
- \`src/math/math.handler.test.ts\` — unit template for the microservice handler.
|
|
1372
|
+
- \`src/app.test.ts\` — hybrid verification template covering HTTP dispatch plus in-memory message routing with the same shared module contract.
|
|
1373
|
+
|
|
1374
|
+
Use the unit templates for fast logic checks. Use the mixed verification template when you need confidence that both generated entrypoints still share one behavioral contract.
|
|
1375
|
+
`;
|
|
1376
|
+
}
|
|
1377
|
+
function createMixedAppFile() {
|
|
1378
|
+
return `import { Global, Module } from '@fluojs/core';
|
|
1379
|
+
import { ConfigModule } from '@fluojs/config';
|
|
1380
|
+
import { MicroservicesModule, TcpMicroserviceTransport } from '@fluojs/microservices';
|
|
1381
|
+
import { createHealthModule } from '@fluojs/runtime';
|
|
1382
|
+
|
|
1383
|
+
import { HealthModule } from './health/health.module';
|
|
1384
|
+
import { MathHandler } from './math/math.handler';
|
|
1385
|
+
|
|
1386
|
+
const RuntimeHealthModule = createHealthModule();
|
|
1387
|
+
const parsedMicroservicePort = Number.parseInt(process.env.MICROSERVICE_PORT ?? '4000', 10);
|
|
1388
|
+
const microservicePort = Number.isFinite(parsedMicroservicePort) ? parsedMicroservicePort : 4000;
|
|
1389
|
+
const microserviceHost = process.env.MICROSERVICE_HOST ?? '127.0.0.1';
|
|
1390
|
+
|
|
1391
|
+
@Global()
|
|
1392
|
+
@Module({
|
|
1393
|
+
imports: [
|
|
1394
|
+
ConfigModule.forRoot({
|
|
1395
|
+
envFile: '.env',
|
|
1396
|
+
processEnv: process.env,
|
|
1397
|
+
}),
|
|
1398
|
+
HealthModule,
|
|
1399
|
+
RuntimeHealthModule,
|
|
1400
|
+
MicroservicesModule.forRoot({
|
|
1401
|
+
transport: new TcpMicroserviceTransport({ host: microserviceHost, port: microservicePort }),
|
|
1402
|
+
}),
|
|
1403
|
+
],
|
|
1404
|
+
providers: [MathHandler],
|
|
1405
|
+
})
|
|
1406
|
+
export class AppModule {}
|
|
1407
|
+
`;
|
|
1408
|
+
}
|
|
1409
|
+
function createMixedMainFile() {
|
|
1410
|
+
return `import { createFastifyAdapter } from '@fluojs/platform-fastify';
|
|
1411
|
+
import { FluoFactory } from '@fluojs/runtime';
|
|
1412
|
+
|
|
1413
|
+
import { AppModule } from './app';
|
|
1414
|
+
|
|
1415
|
+
const parsedPort = Number.parseInt(process.env.PORT ?? '3000', 10);
|
|
1416
|
+
const port = Number.isFinite(parsedPort) ? parsedPort : 3000;
|
|
1417
|
+
|
|
1418
|
+
const app = await FluoFactory.create(AppModule, {
|
|
1419
|
+
adapter: createFastifyAdapter({ port }),
|
|
1420
|
+
});
|
|
1421
|
+
await app.connectMicroservice();
|
|
1422
|
+
await app.startAllMicroservices();
|
|
1423
|
+
await app.listen();
|
|
1424
|
+
`;
|
|
1425
|
+
}
|
|
1426
|
+
function createMixedAppTestFile() {
|
|
1427
|
+
return `import { describe, expect, it } from 'vitest';
|
|
1428
|
+
|
|
1429
|
+
import { Global, Module } from '@fluojs/core';
|
|
1430
|
+
import { ConfigModule } from '@fluojs/config';
|
|
1431
|
+
import type { FrameworkRequest, FrameworkResponse } from '@fluojs/http';
|
|
1432
|
+
import {
|
|
1433
|
+
MicroservicesModule,
|
|
1434
|
+
type MicroserviceTransport,
|
|
1435
|
+
} from '@fluojs/microservices';
|
|
1436
|
+
import { FluoFactory, createHealthModule } from '@fluojs/runtime';
|
|
1437
|
+
|
|
1438
|
+
import { HealthModule } from './health/health.module';
|
|
1439
|
+
import { MathHandler } from './math/math.handler';
|
|
1440
|
+
|
|
1441
|
+
type TransportHandler = Parameters<MicroserviceTransport['listen']>[0];
|
|
1442
|
+
|
|
1443
|
+
class InMemoryLoopbackTransport implements MicroserviceTransport {
|
|
1444
|
+
private handler: TransportHandler | undefined;
|
|
1445
|
+
|
|
1446
|
+
async close(): Promise<void> {
|
|
1447
|
+
this.handler = undefined;
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
async emit(pattern: string, payload: unknown): Promise<void> {
|
|
1451
|
+
if (!this.handler) {
|
|
1452
|
+
throw new Error('Transport handler is not listening.');
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
await this.handler({ kind: 'event', pattern, payload });
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
async listen(handler: TransportHandler): Promise<void> {
|
|
1459
|
+
this.handler = handler;
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
async send(pattern: string, payload: unknown): Promise<unknown> {
|
|
1463
|
+
if (!this.handler) {
|
|
1464
|
+
throw new Error('Transport handler is not listening.');
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
return await this.handler({ kind: 'message', pattern, payload });
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
function createRequest(path: string): FrameworkRequest {
|
|
1472
|
+
return {
|
|
1473
|
+
body: undefined,
|
|
1474
|
+
cookies: {},
|
|
1475
|
+
headers: {},
|
|
1476
|
+
method: 'GET',
|
|
1477
|
+
params: {},
|
|
1478
|
+
path,
|
|
1479
|
+
query: {},
|
|
1480
|
+
raw: {},
|
|
1481
|
+
url: path,
|
|
1482
|
+
};
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
function createResponse(): FrameworkResponse & { body?: unknown } {
|
|
1486
|
+
return {
|
|
1487
|
+
committed: false,
|
|
1488
|
+
headers: {},
|
|
1489
|
+
redirect(status, location) {
|
|
1490
|
+
this.setStatus(status);
|
|
1491
|
+
this.setHeader('Location', location);
|
|
1492
|
+
this.committed = true;
|
|
1493
|
+
},
|
|
1494
|
+
send(body) {
|
|
1495
|
+
this.body = body;
|
|
1496
|
+
this.committed = true;
|
|
1497
|
+
},
|
|
1498
|
+
setHeader(name, value) {
|
|
1499
|
+
this.headers[name] = value;
|
|
1500
|
+
},
|
|
1501
|
+
setStatus(code) {
|
|
1502
|
+
this.statusCode = code;
|
|
1503
|
+
this.statusSet = true;
|
|
1504
|
+
},
|
|
1505
|
+
statusCode: undefined,
|
|
1506
|
+
statusSet: false,
|
|
1507
|
+
};
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
describe('AppModule mixed starter', () => {
|
|
1511
|
+
it('keeps HTTP routes and message handlers in one explicit topology contract', async () => {
|
|
1512
|
+
const transport = new InMemoryLoopbackTransport();
|
|
1513
|
+
const RuntimeHealthModule = createHealthModule();
|
|
1514
|
+
|
|
1515
|
+
@Global()
|
|
1516
|
+
@Module({
|
|
1517
|
+
imports: [
|
|
1518
|
+
ConfigModule.forRoot({
|
|
1519
|
+
envFile: '.env',
|
|
1520
|
+
processEnv: process.env,
|
|
1521
|
+
}),
|
|
1522
|
+
HealthModule,
|
|
1523
|
+
RuntimeHealthModule,
|
|
1524
|
+
MicroservicesModule.forRoot({ transport }),
|
|
1525
|
+
],
|
|
1526
|
+
providers: [MathHandler],
|
|
1527
|
+
})
|
|
1528
|
+
class TestAppModule {}
|
|
1529
|
+
|
|
1530
|
+
const app = await FluoFactory.create(TestAppModule, {});
|
|
1531
|
+
const healthResponse = createResponse();
|
|
1532
|
+
|
|
1533
|
+
const microservice = await app.connectMicroservice();
|
|
1534
|
+
|
|
1535
|
+
await Promise.all([app.startAllMicroservices(), app.dispatch(createRequest('/health-info/'), healthResponse)]);
|
|
1536
|
+
|
|
1537
|
+
expect(healthResponse.body).toEqual({ ok: true, service: expect.any(String) });
|
|
1538
|
+
await expect(transport.send('math.sum', { a: 20, b: 22 })).resolves.toBe(42);
|
|
1539
|
+
expect(microservice.state).toBe('ready');
|
|
1540
|
+
|
|
1541
|
+
await app.close();
|
|
1542
|
+
});
|
|
1543
|
+
});
|
|
1544
|
+
`;
|
|
1545
|
+
}
|
|
1546
|
+
function createAppTestFile(importSuffix = '') {
|
|
1547
|
+
return `import { describe, expect, it } from 'vitest';
|
|
1548
|
+
|
|
1549
|
+
import type { FrameworkRequest, FrameworkResponse } from '@fluojs/http';
|
|
1550
|
+
import { FluoFactory } from '@fluojs/runtime';
|
|
1551
|
+
|
|
1552
|
+
import { AppModule } from './app${importSuffix}';
|
|
1553
|
+
|
|
1554
|
+
function createRequest(path: string): FrameworkRequest {
|
|
1555
|
+
return {
|
|
1556
|
+
body: undefined,
|
|
1557
|
+
cookies: {},
|
|
1558
|
+
headers: {},
|
|
1559
|
+
method: 'GET',
|
|
1560
|
+
params: {},
|
|
1561
|
+
path,
|
|
1562
|
+
query: {},
|
|
1563
|
+
raw: {},
|
|
1564
|
+
url: path,
|
|
1565
|
+
};
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
function createResponse(): FrameworkResponse & { body?: unknown } {
|
|
1569
|
+
return {
|
|
1570
|
+
committed: false,
|
|
1571
|
+
headers: {},
|
|
1572
|
+
redirect(status, location) {
|
|
1573
|
+
this.setStatus(status);
|
|
1574
|
+
this.setHeader('Location', location);
|
|
1575
|
+
this.committed = true;
|
|
1576
|
+
},
|
|
1577
|
+
send(body) {
|
|
1578
|
+
this.body = body;
|
|
1579
|
+
this.committed = true;
|
|
1580
|
+
},
|
|
1581
|
+
setHeader(name, value) {
|
|
1582
|
+
this.headers[name] = value;
|
|
1583
|
+
},
|
|
1584
|
+
setStatus(code) {
|
|
1585
|
+
this.statusCode = code;
|
|
1586
|
+
this.statusSet = true;
|
|
1587
|
+
},
|
|
1588
|
+
statusCode: undefined,
|
|
1589
|
+
statusSet: false,
|
|
1590
|
+
};
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
describe('AppModule', () => {
|
|
1594
|
+
it('dispatches the runtime health and readiness routes', async () => {
|
|
1595
|
+
const app = await FluoFactory.create(AppModule, {});
|
|
1596
|
+
const healthResponse = createResponse();
|
|
1597
|
+
const readyResponse = createResponse();
|
|
1598
|
+
|
|
1599
|
+
await app.dispatch(createRequest('/health'), healthResponse);
|
|
1600
|
+
await app.dispatch(createRequest('/ready'), readyResponse);
|
|
1601
|
+
|
|
1602
|
+
expect(healthResponse.body).toEqual({ status: 'ok' });
|
|
1603
|
+
expect(readyResponse.body).toEqual({ status: 'ready' });
|
|
1604
|
+
|
|
1605
|
+
await app.close();
|
|
1606
|
+
});
|
|
1607
|
+
|
|
1608
|
+
it('dispatches the health-info route', async () => {
|
|
1609
|
+
const app = await FluoFactory.create(AppModule, {});
|
|
1610
|
+
const response = createResponse();
|
|
1611
|
+
|
|
1612
|
+
await app.dispatch(createRequest('/health-info/'), response);
|
|
1613
|
+
|
|
1614
|
+
expect(response.body).toEqual({ ok: true, service: expect.any(String) });
|
|
1615
|
+
|
|
1616
|
+
await app.close();
|
|
1617
|
+
});
|
|
1618
|
+
});
|
|
1619
|
+
`;
|
|
1620
|
+
}
|
|
1621
|
+
function createAppE2eTestFile(importSuffix = '') {
|
|
1622
|
+
return `import { describe, expect, it } from 'vitest';
|
|
1623
|
+
|
|
1624
|
+
import { createTestApp } from '@fluojs/testing';
|
|
1625
|
+
|
|
1626
|
+
import { AppModule } from './app${importSuffix}';
|
|
1627
|
+
|
|
1628
|
+
describe('AppModule e2e', () => {
|
|
1629
|
+
it('serves runtime and starter routes through createTestApp', async () => {
|
|
1630
|
+
const app = await createTestApp({ rootModule: AppModule });
|
|
1631
|
+
|
|
1632
|
+
await expect(app.dispatch({ method: 'GET', path: '/health' })).resolves.toMatchObject({
|
|
1633
|
+
body: { status: 'ok' },
|
|
1634
|
+
status: 200,
|
|
1635
|
+
});
|
|
1636
|
+
await expect(app.dispatch({ method: 'GET', path: '/ready' })).resolves.toMatchObject({
|
|
1637
|
+
body: { status: 'ready' },
|
|
1638
|
+
status: 200,
|
|
1639
|
+
});
|
|
1640
|
+
await expect(app.dispatch({ method: 'GET', path: '/health-info/' })).resolves.toMatchObject({
|
|
1641
|
+
body: { ok: true, service: expect.any(String) },
|
|
1642
|
+
status: 200,
|
|
1643
|
+
});
|
|
1644
|
+
|
|
1645
|
+
await app.close();
|
|
1646
|
+
});
|
|
1647
|
+
});
|
|
1648
|
+
`;
|
|
1649
|
+
}
|
|
1650
|
+
function createDenoAppTestFile() {
|
|
1651
|
+
return `import { FluoFactory } from '@fluojs/runtime';
|
|
1652
|
+
|
|
1653
|
+
import { AppModule } from './app.ts';
|
|
1654
|
+
|
|
1655
|
+
type ResponseStub = {
|
|
1656
|
+
body?: unknown;
|
|
1657
|
+
committed: boolean;
|
|
1658
|
+
headers: Record<string, string>;
|
|
1659
|
+
redirect(status: number, location: string): void;
|
|
1660
|
+
send(body: unknown): void;
|
|
1661
|
+
setHeader(name: string, value: string): void;
|
|
1662
|
+
setStatus(code: number): void;
|
|
1663
|
+
statusCode?: number;
|
|
1664
|
+
statusSet: boolean;
|
|
1665
|
+
};
|
|
1666
|
+
|
|
1667
|
+
function createResponse(): ResponseStub {
|
|
1668
|
+
return {
|
|
1669
|
+
committed: false,
|
|
1670
|
+
headers: {},
|
|
1671
|
+
redirect(status, location) {
|
|
1672
|
+
this.setStatus(status);
|
|
1673
|
+
this.setHeader('Location', location);
|
|
1674
|
+
this.committed = true;
|
|
1675
|
+
},
|
|
1676
|
+
send(body) {
|
|
1677
|
+
this.body = body;
|
|
1678
|
+
this.committed = true;
|
|
1679
|
+
},
|
|
1680
|
+
setHeader(name, value) {
|
|
1681
|
+
this.headers[name] = value;
|
|
1682
|
+
},
|
|
1683
|
+
setStatus(code) {
|
|
1684
|
+
this.statusCode = code;
|
|
1685
|
+
this.statusSet = true;
|
|
1686
|
+
},
|
|
1687
|
+
statusCode: undefined,
|
|
1688
|
+
statusSet: false,
|
|
1689
|
+
};
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
Deno.test('AppModule dispatches the generated Deno starter routes', async () => {
|
|
1693
|
+
const app = await FluoFactory.create(AppModule, {});
|
|
1694
|
+
const healthResponse = createResponse();
|
|
1695
|
+
const readyResponse = createResponse();
|
|
1696
|
+
const infoResponse = createResponse();
|
|
1697
|
+
|
|
1698
|
+
await app.dispatch({ body: undefined, cookies: {}, headers: {}, method: 'GET', params: {}, path: '/health', query: {}, raw: {}, url: '/health' }, healthResponse as never);
|
|
1699
|
+
await app.dispatch({ body: undefined, cookies: {}, headers: {}, method: 'GET', params: {}, path: '/ready', query: {}, raw: {}, url: '/ready' }, readyResponse as never);
|
|
1700
|
+
await app.dispatch({ body: undefined, cookies: {}, headers: {}, method: 'GET', params: {}, path: '/health-info/', query: {}, raw: {}, url: '/health-info/' }, infoResponse as never);
|
|
1701
|
+
|
|
1702
|
+
if (JSON.stringify(healthResponse.body) !== JSON.stringify({ status: 'ok' })) {
|
|
1703
|
+
throw new Error('Expected /health to return status ok.');
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
if (JSON.stringify(readyResponse.body) !== JSON.stringify({ status: 'ready' })) {
|
|
1707
|
+
throw new Error('Expected /ready to return status ready.');
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
if (typeof infoResponse.body !== 'object' || infoResponse.body === null) {
|
|
1711
|
+
throw new Error('Expected /health-info/ to return an object body.');
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
const infoBody = infoResponse.body as { ok?: unknown; service?: unknown };
|
|
1715
|
+
if (infoBody.ok !== true || typeof infoBody.service !== 'string') {
|
|
1716
|
+
throw new Error('Expected /health-info/ to return the generated service payload.');
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
await app.close();
|
|
1720
|
+
});
|
|
1721
|
+
`;
|
|
1722
|
+
}
|
|
1723
|
+
function createEnvFile(bootstrapPlan) {
|
|
1724
|
+
if (bootstrapPlan.profile.id === 'application-cloudflare-workers-cloudflare-workers-http') {
|
|
1725
|
+
return undefined;
|
|
1726
|
+
}
|
|
1727
|
+
if (bootstrapPlan.profile.id === 'microservice-node-none-tcp' || bootstrapPlan.profile.id === 'mixed-node-fastify-tcp') {
|
|
1728
|
+
return `MICROSERVICE_HOST=127.0.0.1
|
|
1729
|
+
MICROSERVICE_PORT=4000
|
|
1730
|
+
PORT=3000
|
|
1731
|
+
`;
|
|
1732
|
+
}
|
|
1733
|
+
if (bootstrapPlan.profile.id === 'microservice-node-none-redis-streams') {
|
|
1734
|
+
return `REDIS_URL=redis://127.0.0.1:6379
|
|
1735
|
+
REDIS_STREAMS_NAMESPACE=fluo:streams
|
|
1736
|
+
REDIS_STREAMS_CONSUMER_GROUP=fluo-handlers
|
|
1737
|
+
PORT=3000
|
|
1738
|
+
`;
|
|
1739
|
+
}
|
|
1740
|
+
if (bootstrapPlan.profile.id === 'microservice-node-none-mqtt') {
|
|
1741
|
+
return `MQTT_URL=mqtt://127.0.0.1:1883
|
|
1742
|
+
MQTT_NAMESPACE=fluo.microservices
|
|
1743
|
+
PORT=3000
|
|
1744
|
+
`;
|
|
1745
|
+
}
|
|
1746
|
+
if (bootstrapPlan.profile.id === 'microservice-node-none-grpc') {
|
|
1747
|
+
return `GRPC_URL=127.0.0.1:50051
|
|
1748
|
+
PORT=3000
|
|
1749
|
+
`;
|
|
1750
|
+
}
|
|
1751
|
+
if (bootstrapPlan.profile.id === 'microservice-node-none-nats') {
|
|
1752
|
+
return `NATS_SERVERS=nats://127.0.0.1:4222
|
|
1753
|
+
NATS_EVENT_SUBJECT=fluo.microservices.events
|
|
1754
|
+
NATS_MESSAGE_SUBJECT=fluo.microservices.messages
|
|
1755
|
+
PORT=3000
|
|
1756
|
+
`;
|
|
1757
|
+
}
|
|
1758
|
+
if (bootstrapPlan.profile.id === 'microservice-node-none-kafka') {
|
|
1759
|
+
return `KAFKA_BROKERS=127.0.0.1:9092
|
|
1760
|
+
KAFKA_CLIENT_ID=fluo-microservice-starter
|
|
1761
|
+
KAFKA_CONSUMER_GROUP=fluo-handlers
|
|
1762
|
+
KAFKA_EVENT_TOPIC=fluo.microservices.events
|
|
1763
|
+
KAFKA_MESSAGE_TOPIC=fluo.microservices.messages
|
|
1764
|
+
KAFKA_RESPONSE_TOPIC=fluo.microservices.responses
|
|
1765
|
+
PORT=3000
|
|
1766
|
+
`;
|
|
1767
|
+
}
|
|
1768
|
+
if (bootstrapPlan.profile.id === 'microservice-node-none-rabbitmq') {
|
|
1769
|
+
return `RABBITMQ_URL=amqp://127.0.0.1:5672
|
|
1770
|
+
RABBITMQ_EVENT_QUEUE=fluo.microservices.events
|
|
1771
|
+
RABBITMQ_MESSAGE_QUEUE=fluo.microservices.messages
|
|
1772
|
+
RABBITMQ_RESPONSE_QUEUE=fluo.microservices.responses
|
|
1773
|
+
PORT=3000
|
|
1774
|
+
`;
|
|
1775
|
+
}
|
|
1776
|
+
return `PORT=3000
|
|
1777
|
+
`;
|
|
1778
|
+
}
|
|
1779
|
+
function createWranglerConfig(projectName) {
|
|
1780
|
+
return `{
|
|
1781
|
+
"$schema": "node_modules/wrangler/config-schema.json",
|
|
1782
|
+
"name": ${JSON.stringify(projectName)},
|
|
1783
|
+
"main": "src/worker.ts",
|
|
1784
|
+
"compatibility_date": "2026-04-11"
|
|
1785
|
+
}
|
|
1786
|
+
`;
|
|
1787
|
+
}
|
|
1788
|
+
function emitSharedScaffoldFiles(options, bootstrapPlan, releaseVersion, packageSpecs) {
|
|
1789
|
+
const envFile = createEnvFile(bootstrapPlan);
|
|
1790
|
+
const sharedFiles = [{
|
|
1791
|
+
content: createProjectPackageJson(options, bootstrapPlan, releaseVersion, packageSpecs),
|
|
1792
|
+
path: 'package.json'
|
|
1793
|
+
}, {
|
|
1794
|
+
content: createProjectReadme(options, bootstrapPlan),
|
|
1795
|
+
path: 'README.md'
|
|
1796
|
+
}, {
|
|
1797
|
+
content: createGitignore(),
|
|
1798
|
+
path: '.gitignore'
|
|
1799
|
+
}];
|
|
1800
|
+
if (bootstrapPlan.profile.id !== 'application-deno-deno-http') {
|
|
1801
|
+
sharedFiles.push({
|
|
1802
|
+
content: createProjectTsconfig(),
|
|
1803
|
+
path: 'tsconfig.json'
|
|
1804
|
+
}, {
|
|
1805
|
+
content: createProjectTsconfigBuild(),
|
|
1806
|
+
path: 'tsconfig.build.json'
|
|
1807
|
+
}, {
|
|
1808
|
+
content: createBabelConfig(),
|
|
1809
|
+
path: 'babel.config.cjs'
|
|
1810
|
+
}, {
|
|
1811
|
+
content: createViteConfig(),
|
|
1812
|
+
path: 'vite.config.ts'
|
|
1813
|
+
}, {
|
|
1814
|
+
content: createVitestConfig(),
|
|
1815
|
+
path: 'vitest.config.ts'
|
|
1816
|
+
});
|
|
1817
|
+
}
|
|
1818
|
+
if (bootstrapPlan.profile.id === 'application-cloudflare-workers-cloudflare-workers-http') {
|
|
1819
|
+
sharedFiles.push({
|
|
1820
|
+
content: createWranglerConfig(options.projectName),
|
|
1821
|
+
path: 'wrangler.jsonc'
|
|
1822
|
+
});
|
|
1823
|
+
}
|
|
1824
|
+
if (envFile) {
|
|
1825
|
+
sharedFiles.push({
|
|
1826
|
+
content: envFile,
|
|
1827
|
+
path: '.env'
|
|
1828
|
+
});
|
|
1829
|
+
}
|
|
1830
|
+
return sharedFiles;
|
|
1831
|
+
}
|
|
1832
|
+
function emitApplicationScaffoldFiles(options) {
|
|
1833
|
+
const importSuffix = options.runtime === 'deno' ? '.ts' : '';
|
|
1834
|
+
const entrypointPath = describeApplicationStarter(options).entrypoint;
|
|
1835
|
+
const files = [{
|
|
1836
|
+
content: createAppFile(options),
|
|
1837
|
+
path: 'src/app.ts'
|
|
1838
|
+
}, {
|
|
1839
|
+
content: createMainFile(options),
|
|
1840
|
+
path: entrypointPath
|
|
1841
|
+
}, {
|
|
1842
|
+
content: createHealthResponseDtoFile(),
|
|
1843
|
+
path: 'src/health/health.response.dto.ts'
|
|
1844
|
+
}, {
|
|
1845
|
+
content: createHealthRepoFile(options.projectName, importSuffix),
|
|
1846
|
+
path: 'src/health/health.repo.ts'
|
|
1847
|
+
}, {
|
|
1848
|
+
content: createHealthServiceFile(importSuffix),
|
|
1849
|
+
path: 'src/health/health.service.ts'
|
|
1850
|
+
}, {
|
|
1851
|
+
content: createHealthControllerFile(importSuffix),
|
|
1852
|
+
path: 'src/health/health.controller.ts'
|
|
1853
|
+
}, {
|
|
1854
|
+
content: createHealthModuleFile(importSuffix),
|
|
1855
|
+
path: 'src/health/health.module.ts'
|
|
1856
|
+
}];
|
|
1857
|
+
if (options.runtime === 'deno') {
|
|
1858
|
+
files.push({
|
|
1859
|
+
content: createDenoAppTestFile(),
|
|
1860
|
+
path: 'src/app.test.ts'
|
|
1861
|
+
});
|
|
1862
|
+
return files;
|
|
1863
|
+
}
|
|
1864
|
+
files.push({
|
|
1865
|
+
content: createHealthRepoTestFile(),
|
|
1866
|
+
path: 'src/health/health.repo.test.ts'
|
|
1867
|
+
}, {
|
|
1868
|
+
content: createHealthServiceTestFile(),
|
|
1869
|
+
path: 'src/health/health.service.test.ts'
|
|
1870
|
+
}, {
|
|
1871
|
+
content: createHealthControllerTestFile(),
|
|
1872
|
+
path: 'src/health/health.controller.test.ts'
|
|
1873
|
+
}, {
|
|
1874
|
+
content: createAppTestFile(importSuffix),
|
|
1875
|
+
path: 'src/app.test.ts'
|
|
1876
|
+
}, {
|
|
1877
|
+
content: createAppE2eTestFile(importSuffix),
|
|
1878
|
+
path: 'src/app.e2e.test.ts'
|
|
1879
|
+
});
|
|
1880
|
+
return files;
|
|
1881
|
+
}
|
|
1882
|
+
function emitScaffoldFilesForRecipe(options, recipeId) {
|
|
1883
|
+
if (recipeId === 'application-bun-bun-http' || recipeId === 'application-cloudflare-workers-cloudflare-workers-http' || recipeId === 'application-deno-deno-http' || recipeId === 'application-node-fastify-http' || recipeId === 'application-node-express-http' || recipeId === 'application-node-nodejs-http') {
|
|
1884
|
+
return emitApplicationScaffoldFiles(options);
|
|
1885
|
+
}
|
|
1886
|
+
if (recipeId === 'microservice-node-none-tcp' || recipeId === 'microservice-node-none-redis-streams' || recipeId === 'microservice-node-none-mqtt' || recipeId === 'microservice-node-none-grpc' || recipeId === 'microservice-node-none-nats' || recipeId === 'microservice-node-none-kafka' || recipeId === 'microservice-node-none-rabbitmq') {
|
|
1887
|
+
const transport = recipeId === 'microservice-node-none-redis-streams' ? 'redis-streams' : recipeId === 'microservice-node-none-mqtt' ? 'mqtt' : recipeId === 'microservice-node-none-nats' ? 'nats' : recipeId === 'microservice-node-none-kafka' ? 'kafka' : recipeId === 'microservice-node-none-rabbitmq' ? 'rabbitmq' : recipeId === 'microservice-node-none-grpc' ? 'grpc' : 'tcp';
|
|
1888
|
+
const files = [{
|
|
1889
|
+
content: createMicroserviceAppFile({
|
|
1890
|
+
transport
|
|
1891
|
+
}),
|
|
1892
|
+
path: 'src/app.ts'
|
|
1893
|
+
}, {
|
|
1894
|
+
content: createMicroserviceMainFile(),
|
|
1895
|
+
path: 'src/main.ts'
|
|
1896
|
+
}, {
|
|
1897
|
+
content: createMathHandlerFile({
|
|
1898
|
+
transport
|
|
1899
|
+
}),
|
|
1900
|
+
path: 'src/math/math.handler.ts'
|
|
1901
|
+
}, {
|
|
1902
|
+
content: createMathHandlerTestFile({
|
|
1903
|
+
transport
|
|
1904
|
+
}),
|
|
1905
|
+
path: 'src/math/math.handler.test.ts'
|
|
1906
|
+
}, {
|
|
1907
|
+
content: createMicroserviceAppTestFile({
|
|
1908
|
+
transport
|
|
1909
|
+
}),
|
|
1910
|
+
path: 'src/app.test.ts'
|
|
1911
|
+
}];
|
|
1912
|
+
if (recipeId === 'microservice-node-none-grpc') {
|
|
1913
|
+
files.push({
|
|
1914
|
+
content: createGrpcProtoFile(),
|
|
1915
|
+
path: 'proto/math.proto'
|
|
1916
|
+
});
|
|
1917
|
+
}
|
|
1918
|
+
return files;
|
|
1919
|
+
}
|
|
1920
|
+
if (recipeId === 'mixed-node-fastify-tcp') {
|
|
1921
|
+
return [{
|
|
1922
|
+
content: createMixedAppFile(),
|
|
1923
|
+
path: 'src/app.ts'
|
|
1924
|
+
}, {
|
|
1925
|
+
content: createMixedMainFile(),
|
|
1926
|
+
path: 'src/main.ts'
|
|
1927
|
+
}, {
|
|
1928
|
+
content: createHealthResponseDtoFile(),
|
|
1929
|
+
path: 'src/health/health.response.dto.ts'
|
|
1930
|
+
}, {
|
|
1931
|
+
content: createHealthRepoFile(options.projectName),
|
|
1932
|
+
path: 'src/health/health.repo.ts'
|
|
1933
|
+
}, {
|
|
1934
|
+
content: createHealthRepoTestFile(),
|
|
1935
|
+
path: 'src/health/health.repo.test.ts'
|
|
1936
|
+
}, {
|
|
1937
|
+
content: createHealthServiceFile(),
|
|
1938
|
+
path: 'src/health/health.service.ts'
|
|
1939
|
+
}, {
|
|
1940
|
+
content: createHealthServiceTestFile(),
|
|
1941
|
+
path: 'src/health/health.service.test.ts'
|
|
1942
|
+
}, {
|
|
1943
|
+
content: createHealthControllerFile(),
|
|
1944
|
+
path: 'src/health/health.controller.ts'
|
|
1945
|
+
}, {
|
|
1946
|
+
content: createHealthControllerTestFile(),
|
|
1947
|
+
path: 'src/health/health.controller.test.ts'
|
|
1948
|
+
}, {
|
|
1949
|
+
content: createHealthModuleFile(),
|
|
1950
|
+
path: 'src/health/health.module.ts'
|
|
1951
|
+
}, {
|
|
1952
|
+
content: createMathHandlerFile({
|
|
1953
|
+
transport: 'tcp'
|
|
1954
|
+
}),
|
|
1955
|
+
path: 'src/math/math.handler.ts'
|
|
1956
|
+
}, {
|
|
1957
|
+
content: createMathHandlerTestFile({
|
|
1958
|
+
transport: 'tcp'
|
|
1959
|
+
}),
|
|
1960
|
+
path: 'src/math/math.handler.test.ts'
|
|
1961
|
+
}, {
|
|
1962
|
+
content: createMixedAppTestFile(),
|
|
1963
|
+
path: 'src/app.test.ts'
|
|
1964
|
+
}];
|
|
1965
|
+
}
|
|
1966
|
+
return [];
|
|
1967
|
+
}
|
|
1968
|
+
function emitScaffoldFilesForPlan(options, bootstrapPlan) {
|
|
1969
|
+
return emitScaffoldFilesForRecipe(options, bootstrapPlan.profile.id);
|
|
1970
|
+
}
|
|
1971
|
+
function buildScaffoldFiles(options, bootstrapPlan, releaseVersion, packageSpecs) {
|
|
1972
|
+
return [...emitSharedScaffoldFiles(options, bootstrapPlan, releaseVersion, packageSpecs), ...emitScaffoldFilesForPlan(options, bootstrapPlan)];
|
|
1973
|
+
}
|
|
1974
|
+
|
|
1975
|
+
/**
|
|
1976
|
+
* Scaffolds a new fluo application into the target directory.
|
|
1977
|
+
*
|
|
1978
|
+
* @param options Bootstrap configuration for the new project.
|
|
1979
|
+
* @param importMetaUrl Optional URL of the importing module for relative path resolution.
|
|
1980
|
+
* @returns A promise that resolves when the project has been scaffolded.
|
|
1981
|
+
*/
|
|
1982
|
+
export async function scaffoldBootstrapApp(options, importMetaUrl = import.meta.url) {
|
|
1983
|
+
const targetDirectory = resolve(options.targetDirectory);
|
|
1984
|
+
const releaseVersion = readOwnPackageVersion(importMetaUrl);
|
|
1985
|
+
const bootstrapPlan = resolveBootstrapPlan(options);
|
|
1986
|
+
const packageSpecs = await resolvePackageSpecs(options, bootstrapPlan);
|
|
1987
|
+
mkdirSync(targetDirectory, {
|
|
1988
|
+
recursive: true
|
|
1989
|
+
});
|
|
1990
|
+
if (!options.force) {
|
|
1991
|
+
const existingFiles = readdirSync(targetDirectory);
|
|
1992
|
+
if (existingFiles.length > 0) {
|
|
1993
|
+
throw new Error(`Target directory "${targetDirectory}" is not empty. ` + 'Remove the existing files or use --force to overwrite.');
|
|
1994
|
+
}
|
|
1995
|
+
}
|
|
1996
|
+
for (const file of buildScaffoldFiles(options, bootstrapPlan, releaseVersion, packageSpecs)) {
|
|
1997
|
+
writeTextFile(join(targetDirectory, file.path), file.content);
|
|
1998
|
+
}
|
|
1999
|
+
if (options.initializeGit) {
|
|
2000
|
+
await initializeGitRepository(targetDirectory);
|
|
2001
|
+
}
|
|
2002
|
+
if (options.installDependencies ?? !options.skipInstall) {
|
|
2003
|
+
await installDependencies(targetDirectory, options.packageManager);
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
2006
|
+
|
|
2007
|
+
/**
|
|
2008
|
+
* Alias for {@link scaffoldBootstrapApp}.
|
|
2009
|
+
*/
|
|
2010
|
+
export const scaffoldFluoApp = scaffoldBootstrapApp;
|