@fluojs/cli 1.0.0-beta.5 → 1.0.0-beta.7
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/README.ko.md +40 -9
- package/README.md +40 -9
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +34 -4
- package/dist/commands/scripts.d.ts.map +1 -1
- package/dist/commands/scripts.js +187 -35
- package/dist/dev-runner/node-restart-runner.d.ts +5 -0
- package/dist/dev-runner/node-restart-runner.d.ts.map +1 -1
- package/dist/dev-runner/node-restart-runner.js +72 -3
- package/dist/dev-runner/preserve-color-tty.d.ts +2 -0
- package/dist/dev-runner/preserve-color-tty.d.ts.map +1 -0
- package/dist/dev-runner/preserve-color-tty.js +12 -0
- package/dist/new/scaffold.d.ts.map +1 -1
- package/dist/new/scaffold.js +186 -183
- package/dist/new/starter-profiles.d.ts.map +1 -1
- package/dist/new/starter-profiles.js +13 -13
- package/package.json +2 -2
package/dist/new/scaffold.js
CHANGED
|
@@ -27,21 +27,22 @@ const PUBLISHED_RUNTIME_DEPENDENCIES = {
|
|
|
27
27
|
nats: '^2.29.3'
|
|
28
28
|
};
|
|
29
29
|
const PUBLISHED_INTERNAL_DEPENDENCIES = {
|
|
30
|
-
'@fluojs/cli': '^1.0.0-beta.
|
|
31
|
-
'@fluojs/config': '^1.0.0-beta.
|
|
32
|
-
'@fluojs/core': '^1.0.0-beta.
|
|
30
|
+
'@fluojs/cli': '^1.0.0-beta.7',
|
|
31
|
+
'@fluojs/config': '^1.0.0-beta.7',
|
|
32
|
+
'@fluojs/core': '^1.0.0-beta.4',
|
|
33
33
|
'@fluojs/di': '^1.0.0-beta.6',
|
|
34
|
-
'@fluojs/http': '^1.0.0-beta.
|
|
35
|
-
'@fluojs/microservices': '^1.0.0-beta.
|
|
34
|
+
'@fluojs/http': '^1.0.0-beta.10',
|
|
35
|
+
'@fluojs/microservices': '^1.0.0-beta.5',
|
|
36
36
|
'@fluojs/platform-bun': '^1.0.0-beta.6',
|
|
37
37
|
'@fluojs/platform-cloudflare-workers': '^1.0.0-beta.2',
|
|
38
38
|
'@fluojs/platform-deno': '^1.0.0-beta.3',
|
|
39
39
|
'@fluojs/platform-express': '^1.0.0-beta.6',
|
|
40
40
|
'@fluojs/platform-fastify': '^1.0.0-beta.8',
|
|
41
41
|
'@fluojs/platform-nodejs': '^1.0.0-beta.4',
|
|
42
|
-
'@fluojs/runtime': '^1.0.0-beta.
|
|
42
|
+
'@fluojs/runtime': '^1.0.0-beta.11',
|
|
43
43
|
'@fluojs/testing': '^1.0.0-beta.2',
|
|
44
|
-
'@fluojs/validation': '^1.0.0-beta.
|
|
44
|
+
'@fluojs/validation': '^1.0.0-beta.3',
|
|
45
|
+
'@fluojs/vite': '^1.0.0-beta.2'
|
|
45
46
|
};
|
|
46
47
|
function describeApplicationStarter(options) {
|
|
47
48
|
if (options.runtime === 'bun') {
|
|
@@ -147,27 +148,28 @@ function createProjectScripts(bootstrapPlan) {
|
|
|
147
148
|
switch (bootstrapPlan.profile.id) {
|
|
148
149
|
case 'application-bun-bun-http':
|
|
149
150
|
return {
|
|
150
|
-
build: '
|
|
151
|
+
build: 'bun build ./src/main.ts --outdir ./dist --target bun',
|
|
151
152
|
dev: 'fluo dev',
|
|
152
|
-
start: '
|
|
153
|
+
start: 'bun dist/main.js',
|
|
153
154
|
test: 'vitest run',
|
|
154
155
|
'test:watch': 'vitest',
|
|
155
156
|
typecheck: 'tsc -p tsconfig.json --noEmit'
|
|
156
157
|
};
|
|
157
158
|
case 'application-deno-deno-http':
|
|
158
159
|
return {
|
|
159
|
-
build: '
|
|
160
|
+
build: 'deno compile --allow-env --allow-net --output dist/app src/main.ts',
|
|
160
161
|
dev: 'fluo dev',
|
|
161
|
-
start: '
|
|
162
|
+
start: './dist/app',
|
|
162
163
|
test: 'deno test --allow-env --allow-net',
|
|
163
164
|
'test:watch': 'deno test --allow-env --allow-net --watch',
|
|
164
165
|
typecheck: 'deno check src/main.ts src/app.test.ts'
|
|
165
166
|
};
|
|
166
167
|
case 'application-cloudflare-workers-cloudflare-workers-http':
|
|
167
168
|
return {
|
|
168
|
-
build: '
|
|
169
|
+
build: 'wrangler deploy --dry-run',
|
|
170
|
+
deploy: 'wrangler deploy',
|
|
169
171
|
dev: 'fluo dev',
|
|
170
|
-
|
|
172
|
+
preview: 'wrangler dev --remote --show-interactive-dev-session=false',
|
|
171
173
|
test: 'vitest run',
|
|
172
174
|
'test:watch': 'vitest',
|
|
173
175
|
typecheck: 'tsc -p tsconfig.json --noEmit'
|
|
@@ -203,6 +205,12 @@ function createPublishedDevDependencies(bootstrapPlan) {
|
|
|
203
205
|
if (bootstrapPlan.profile.id === 'application-deno-deno-http') {
|
|
204
206
|
return {};
|
|
205
207
|
}
|
|
208
|
+
if (bootstrapPlan.profile.id === 'application-bun-bun-http') {
|
|
209
|
+
return {
|
|
210
|
+
...PUBLISHED_DEV_DEPENDENCIES,
|
|
211
|
+
'@types/bun': '^1.2.5'
|
|
212
|
+
};
|
|
213
|
+
}
|
|
206
214
|
if (bootstrapPlan.profile.id === 'application-cloudflare-workers-cloudflare-workers-http') {
|
|
207
215
|
return {
|
|
208
216
|
...PUBLISHED_DEV_DEPENDENCIES,
|
|
@@ -243,7 +251,8 @@ function createProjectPackageJson(options, bootstrapPlan, releaseVersion, packag
|
|
|
243
251
|
}
|
|
244
252
|
}, null, 2);
|
|
245
253
|
}
|
|
246
|
-
function createProjectTsconfig() {
|
|
254
|
+
function createProjectTsconfig(bootstrapPlan) {
|
|
255
|
+
const types = bootstrapPlan.profile.id === 'application-bun-bun-http' ? ['node', 'bun'] : ['node'];
|
|
247
256
|
return `{
|
|
248
257
|
"compilerOptions": {
|
|
249
258
|
"target": "ES2022",
|
|
@@ -257,7 +266,7 @@ function createProjectTsconfig() {
|
|
|
257
266
|
"forceConsistentCasingInFileNames": true,
|
|
258
267
|
"resolveJsonModule": true,
|
|
259
268
|
"rootDir": "src",
|
|
260
|
-
"types":
|
|
269
|
+
"types": ${JSON.stringify(types)}
|
|
261
270
|
},
|
|
262
271
|
"include": ["src/**/*.ts"]
|
|
263
272
|
}
|
|
@@ -285,36 +294,9 @@ function createBabelConfig() {
|
|
|
285
294
|
`;
|
|
286
295
|
}
|
|
287
296
|
function createViteConfig() {
|
|
288
|
-
return `import {
|
|
289
|
-
import type { Plugin } from 'vite';
|
|
297
|
+
return `import { fluoDecoratorsPlugin } from '@fluojs/vite';
|
|
290
298
|
import { defineConfig } from 'vite';
|
|
291
299
|
|
|
292
|
-
function fluoDecoratorsPlugin(): Plugin {
|
|
293
|
-
return {
|
|
294
|
-
name: 'fluo-babel-decorators',
|
|
295
|
-
async transform(code: string, id: string) {
|
|
296
|
-
if (!id.endsWith('.ts') || id.includes('.test.')) {
|
|
297
|
-
return null;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
const result = await transformAsync(code, {
|
|
301
|
-
babelrc: false,
|
|
302
|
-
configFile: false,
|
|
303
|
-
filename: id,
|
|
304
|
-
plugins: [['@babel/plugin-proposal-decorators', { version: '2023-11' }]],
|
|
305
|
-
presets: [['@babel/preset-typescript', { allowDeclareFields: true }]],
|
|
306
|
-
sourceMaps: true,
|
|
307
|
-
});
|
|
308
|
-
|
|
309
|
-
if (!result?.code) {
|
|
310
|
-
return null;
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
return { code: result.code, map: result.map };
|
|
314
|
-
},
|
|
315
|
-
};
|
|
316
|
-
}
|
|
317
|
-
|
|
318
300
|
export default defineConfig({
|
|
319
301
|
plugins: [fluoDecoratorsPlugin()],
|
|
320
302
|
build: {
|
|
@@ -359,12 +341,37 @@ dist
|
|
|
359
341
|
coverage
|
|
360
342
|
`;
|
|
361
343
|
}
|
|
344
|
+
function createHttpPackageManagerLine(options) {
|
|
345
|
+
switch (options.runtime) {
|
|
346
|
+
case 'bun':
|
|
347
|
+
return `- Package manager: install/run commands can use ${options.packageManager}; ${createRunCommand(options.packageManager, 'dev')} keeps the \`fluo dev\` abstraction while defaulting to Bun's native watch loop (\`bun --watch src/main.ts\`) for fewer Node-supervised dev processes, \`fluo dev --runner fluo\` restores the Node-backed fluo restart supervisor when you need its debounce/hash reporter contract, and ${createRunCommand(options.packageManager, 'build')} plus ${createRunCommand(options.packageManager, 'start')} run Bun-native production commands (\`bun build ./src/main.ts --outdir ./dist --target bun\` then \`bun dist/main.js\`) so deployment targets do not need Node just to start the built app`;
|
|
348
|
+
case 'deno':
|
|
349
|
+
return `- Package manager: install/run commands can use ${options.packageManager}; ${createRunCommand(options.packageManager, 'dev')} keeps the \`fluo dev\` abstraction while defaulting to Deno's native watch loop (\`deno run --watch --allow-env --allow-net src/main.ts\`) for fewer Node-supervised dev processes, \`fluo dev --runner fluo\` restores the Node-backed fluo restart supervisor when you need its debounce/hash reporter contract, and ${createRunCommand(options.packageManager, 'build')} plus ${createRunCommand(options.packageManager, 'start')} run Deno-native production commands (\`deno compile --allow-env --allow-net --output dist/app src/main.ts\` then \`./dist/app\`) so deployment targets can run the compiled app without Node`;
|
|
350
|
+
case 'cloudflare-workers':
|
|
351
|
+
return `- Package manager: install/run commands can use ${options.packageManager}; ${createRunCommand(options.packageManager, 'dev')} keeps the \`fluo dev\` abstraction while defaulting to Wrangler's native dev loop (\`wrangler dev --show-interactive-dev-session=false\`) for Wrangler-owned reloads, \`fluo dev --runner fluo\` restores the Node-backed fluo restart supervisor when you need its debounce/hash reporter contract, and ${createRunCommand(options.packageManager, 'preview')} plus ${createRunCommand(options.packageManager, 'deploy')} run Wrangler-native preview/deploy commands; Wrangler tooling requires Node/npm-compatible tooling locally, but deployed Workers run on Cloudflare's isolate runtime`;
|
|
352
|
+
default:
|
|
353
|
+
return `- Package manager: install/run commands can use ${options.packageManager}; ${createRunCommand(options.packageManager, 'dev')}, ${createRunCommand(options.packageManager, 'build')}, and ${createRunCommand(options.packageManager, 'start')} delegate to \`fluo dev\`, \`fluo build\`, and \`fluo start\`, which own the generated runtime lifecycle and default \`NODE_ENV\` when unset`;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
function createHttpCommandsSection(options) {
|
|
357
|
+
const commands = [`- Dev: ${createRunCommand(options.packageManager, 'dev')}`, `- Build: ${createRunCommand(options.packageManager, 'build')}`];
|
|
358
|
+
if (options.runtime === 'bun' || options.runtime === 'deno' || options.runtime === 'cloudflare-workers') {
|
|
359
|
+
commands.push('- Dev fluo restart fallback: fluo dev --runner fluo');
|
|
360
|
+
}
|
|
361
|
+
if (options.runtime === 'cloudflare-workers') {
|
|
362
|
+
commands.push(`- Preview: ${createRunCommand(options.packageManager, 'preview')}`, `- Deploy: ${createRunCommand(options.packageManager, 'deploy')}`);
|
|
363
|
+
} else {
|
|
364
|
+
commands.push(`- Start: ${createRunCommand(options.packageManager, 'start')}`);
|
|
365
|
+
}
|
|
366
|
+
commands.push(`- Typecheck: ${createRunCommand(options.packageManager, 'typecheck')}`, `- Test: ${createRunCommand(options.packageManager, 'test')}`);
|
|
367
|
+
return commands.join('\n');
|
|
368
|
+
}
|
|
362
369
|
function createHttpProjectReadme(options) {
|
|
363
370
|
const starter = describeApplicationStarter(options);
|
|
364
371
|
const entrypointLabel = starter.entrypoint;
|
|
365
372
|
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('(... )', '(...)');
|
|
366
373
|
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`;
|
|
367
|
-
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/
|
|
374
|
+
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/greeting/*.test.ts\` — unit templates for the starter-owned greeting 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.`;
|
|
368
375
|
return `# ${options.projectName}
|
|
369
376
|
|
|
370
377
|
Generated by @fluojs/cli.
|
|
@@ -372,20 +379,16 @@ Generated by @fluojs/cli.
|
|
|
372
379
|
- Starter contract: ${starterContract}
|
|
373
380
|
- Default baseline: when you omit \`--platform\`, \`fluo new\` still generates the Node.js + Fastify HTTP starter by default
|
|
374
381
|
- 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
|
|
375
|
-
|
|
382
|
+
${createHttpPackageManagerLine(options)}
|
|
376
383
|
- Runtime dependency set: generated manifest entries match the selected runtime contract instead of inheriting the Node-only starter recipe
|
|
377
384
|
${corsLine}
|
|
378
385
|
- Observability: /health and /ready endpoints are included by default
|
|
379
386
|
- Runtime path: bootstrapApplication -> handler mapping -> dispatcher -> middleware -> guard -> interceptor -> controller
|
|
380
|
-
- Naming policy: runtime module entrypoints use governed canonical names (\`forRoot(...)\`, optional \`forRootAsync(...)\`, \`register(...)\`, \`forFeature(...)\`); helper/builders stay \`create*\` (for example \`
|
|
387
|
+
- Naming policy: runtime module entrypoints use governed canonical names (\`forRoot(...)\`, optional \`forRootAsync(...)\`, \`register(...)\`, \`forFeature(...)\`); helper/builders stay \`create*\` (for example \`createTestingModule(...)\`)
|
|
381
388
|
|
|
382
389
|
## Commands
|
|
383
390
|
|
|
384
|
-
|
|
385
|
-
- Start: ${createRunCommand(options.packageManager, 'start')}
|
|
386
|
-
- Build: ${createRunCommand(options.packageManager, 'build')}
|
|
387
|
-
- Typecheck: ${createRunCommand(options.packageManager, 'typecheck')}
|
|
388
|
-
- Test: ${createRunCommand(options.packageManager, 'test')}
|
|
391
|
+
${createHttpCommandsSection(options)}
|
|
389
392
|
|
|
390
393
|
## Generator example
|
|
391
394
|
|
|
@@ -548,30 +551,26 @@ function createAppFile(options) {
|
|
|
548
551
|
const importSuffix = options.runtime === 'deno' ? '.ts' : '';
|
|
549
552
|
if (options.runtime === 'cloudflare-workers') {
|
|
550
553
|
return `import { Global, Module } from '@fluojs/core';
|
|
551
|
-
import {
|
|
552
|
-
|
|
553
|
-
import { HealthModule } from './health/health.module';
|
|
554
|
+
import { HealthModule } from '@fluojs/runtime';
|
|
554
555
|
|
|
555
|
-
|
|
556
|
+
import { GreetingModule } from './greeting/greeting.module';
|
|
556
557
|
|
|
557
558
|
@Global()
|
|
558
559
|
@Module({
|
|
559
560
|
imports: [
|
|
560
|
-
|
|
561
|
-
|
|
561
|
+
GreetingModule,
|
|
562
|
+
HealthModule.forRoot(),
|
|
562
563
|
],
|
|
563
564
|
})
|
|
564
565
|
export class AppModule {}
|
|
565
566
|
`;
|
|
566
567
|
}
|
|
567
|
-
const processEnvValue = options.runtime === '
|
|
568
|
+
const processEnvValue = options.runtime === 'deno' ? 'Deno.env.toObject()' : 'process.env';
|
|
568
569
|
return `import { Global, Module } from '@fluojs/core';
|
|
569
570
|
import { ConfigModule } from '@fluojs/config';
|
|
570
|
-
import {
|
|
571
|
+
import { HealthModule } from '@fluojs/runtime';
|
|
571
572
|
|
|
572
|
-
import {
|
|
573
|
-
|
|
574
|
-
const RuntimeHealthModule = createHealthModule();
|
|
573
|
+
import { GreetingModule } from './greeting/greeting.module${importSuffix}';
|
|
575
574
|
|
|
576
575
|
@Global()
|
|
577
576
|
@Module({
|
|
@@ -580,134 +579,134 @@ const RuntimeHealthModule = createHealthModule();
|
|
|
580
579
|
envFile: '.env',
|
|
581
580
|
processEnv: ${processEnvValue},
|
|
582
581
|
}),
|
|
583
|
-
|
|
584
|
-
|
|
582
|
+
GreetingModule,
|
|
583
|
+
HealthModule.forRoot(),
|
|
585
584
|
],
|
|
586
585
|
})
|
|
587
586
|
export class AppModule {}
|
|
588
587
|
`;
|
|
589
588
|
}
|
|
590
|
-
function
|
|
591
|
-
return `export class
|
|
592
|
-
|
|
593
|
-
|
|
589
|
+
function createGreetingResponseDtoFile() {
|
|
590
|
+
return `export class GreetingResponseDto {
|
|
591
|
+
message!: string;
|
|
592
|
+
framework!: 'fluo';
|
|
593
|
+
project!: string;
|
|
594
594
|
}
|
|
595
595
|
`;
|
|
596
596
|
}
|
|
597
|
-
function
|
|
598
|
-
return `import type {
|
|
597
|
+
function createGreetingRepoFile(projectName, importSuffix = '') {
|
|
598
|
+
return `import type { GreetingResponseDto } from './greeting.response.dto${importSuffix}';
|
|
599
599
|
|
|
600
|
-
export class
|
|
601
|
-
|
|
600
|
+
export class GreetingRepo {
|
|
601
|
+
findGreeting(): GreetingResponseDto {
|
|
602
602
|
return {
|
|
603
|
-
|
|
604
|
-
|
|
603
|
+
message: 'Hello from fluo',
|
|
604
|
+
framework: 'fluo',
|
|
605
|
+
project: ${JSON.stringify(projectName)},
|
|
605
606
|
};
|
|
606
607
|
}
|
|
607
608
|
}
|
|
608
609
|
`;
|
|
609
610
|
}
|
|
610
|
-
function
|
|
611
|
+
function createGreetingRepoTestFile() {
|
|
611
612
|
return `import { describe, expect, it } from 'vitest';
|
|
612
613
|
|
|
613
|
-
import {
|
|
614
|
+
import { GreetingRepo } from './greeting.repo';
|
|
614
615
|
|
|
615
|
-
describe('
|
|
616
|
-
it('returns
|
|
617
|
-
const repo = new
|
|
618
|
-
expect(repo.
|
|
616
|
+
describe('GreetingRepo', () => {
|
|
617
|
+
it('returns greeting data', () => {
|
|
618
|
+
const repo = new GreetingRepo();
|
|
619
|
+
expect(repo.findGreeting()).toEqual({ message: 'Hello from fluo', framework: 'fluo', project: expect.any(String) });
|
|
619
620
|
});
|
|
620
621
|
});
|
|
621
622
|
`;
|
|
622
623
|
}
|
|
623
|
-
function
|
|
624
|
+
function createGreetingServiceFile(importSuffix = '') {
|
|
624
625
|
return `import { Inject } from '@fluojs/core';
|
|
625
|
-
import type {
|
|
626
|
+
import type { GreetingResponseDto } from './greeting.response.dto${importSuffix}';
|
|
626
627
|
|
|
627
|
-
import {
|
|
628
|
+
import { GreetingRepo } from './greeting.repo${importSuffix}';
|
|
628
629
|
|
|
629
|
-
@Inject(
|
|
630
|
-
export class
|
|
631
|
-
constructor(private readonly repo:
|
|
630
|
+
@Inject(GreetingRepo)
|
|
631
|
+
export class GreetingService {
|
|
632
|
+
constructor(private readonly repo: GreetingRepo) {}
|
|
632
633
|
|
|
633
|
-
|
|
634
|
-
return this.repo.
|
|
634
|
+
getGreeting(): GreetingResponseDto {
|
|
635
|
+
return this.repo.findGreeting();
|
|
635
636
|
}
|
|
636
637
|
}
|
|
637
638
|
`;
|
|
638
639
|
}
|
|
639
|
-
function
|
|
640
|
+
function createGreetingServiceTestFile() {
|
|
640
641
|
return `import { describe, expect, it } from 'vitest';
|
|
641
642
|
|
|
642
|
-
import {
|
|
643
|
-
import {
|
|
643
|
+
import { GreetingService } from './greeting.service';
|
|
644
|
+
import { GreetingRepo } from './greeting.repo';
|
|
644
645
|
|
|
645
|
-
class
|
|
646
|
-
|
|
647
|
-
return {
|
|
646
|
+
class FakeGreetingRepo {
|
|
647
|
+
findGreeting() {
|
|
648
|
+
return { message: 'Hello from fluo', framework: 'fluo' as const, project: 'test' };
|
|
648
649
|
}
|
|
649
650
|
}
|
|
650
651
|
|
|
651
|
-
describe('
|
|
652
|
+
describe('GreetingService', () => {
|
|
652
653
|
it('delegates to the repo', () => {
|
|
653
|
-
const service = new
|
|
654
|
-
expect(service.
|
|
654
|
+
const service = new GreetingService(new FakeGreetingRepo() as GreetingRepo);
|
|
655
|
+
expect(service.getGreeting()).toEqual({ message: 'Hello from fluo', framework: 'fluo', project: 'test' });
|
|
655
656
|
});
|
|
656
657
|
});
|
|
657
658
|
`;
|
|
658
659
|
}
|
|
659
|
-
function
|
|
660
|
-
return `import {
|
|
660
|
+
function createGreetingControllerFile(importSuffix = '') {
|
|
661
|
+
return `import { Inject } from '@fluojs/core';
|
|
661
662
|
import { Controller, Get } from '@fluojs/http';
|
|
662
663
|
|
|
663
|
-
import {
|
|
664
|
-
import {
|
|
665
|
-
|
|
666
|
-
ensureMetadataSymbol();
|
|
664
|
+
import { GreetingService } from './greeting.service${importSuffix}';
|
|
665
|
+
import { GreetingResponseDto } from './greeting.response.dto${importSuffix}';
|
|
667
666
|
|
|
668
|
-
@Inject(
|
|
669
|
-
@Controller('/
|
|
670
|
-
export class
|
|
671
|
-
constructor(private readonly service:
|
|
667
|
+
@Inject(GreetingService)
|
|
668
|
+
@Controller('/greeting')
|
|
669
|
+
export class GreetingController {
|
|
670
|
+
constructor(private readonly service: GreetingService) {}
|
|
672
671
|
|
|
673
672
|
@Get('/')
|
|
674
|
-
|
|
675
|
-
return this.service.
|
|
673
|
+
getGreeting(): GreetingResponseDto {
|
|
674
|
+
return this.service.getGreeting();
|
|
676
675
|
}
|
|
677
676
|
}
|
|
678
677
|
`;
|
|
679
678
|
}
|
|
680
|
-
function
|
|
679
|
+
function createGreetingControllerTestFile() {
|
|
681
680
|
return `import { describe, expect, it } from 'vitest';
|
|
682
681
|
|
|
683
|
-
import {
|
|
682
|
+
import { GreetingController } from './greeting.controller';
|
|
684
683
|
|
|
685
|
-
class
|
|
686
|
-
|
|
687
|
-
return {
|
|
684
|
+
class FakeGreetingService {
|
|
685
|
+
getGreeting() {
|
|
686
|
+
return { message: 'Hello from fluo', framework: 'fluo' as const, project: 'test' };
|
|
688
687
|
}
|
|
689
688
|
}
|
|
690
689
|
|
|
691
|
-
describe('
|
|
690
|
+
describe('GreetingController', () => {
|
|
692
691
|
it('delegates to the service', () => {
|
|
693
|
-
const controller = new
|
|
694
|
-
expect(controller.
|
|
692
|
+
const controller = new GreetingController(new FakeGreetingService() as never);
|
|
693
|
+
expect(controller.getGreeting()).toEqual({ message: 'Hello from fluo', framework: 'fluo', project: 'test' });
|
|
695
694
|
});
|
|
696
695
|
});
|
|
697
696
|
`;
|
|
698
697
|
}
|
|
699
|
-
function
|
|
698
|
+
function createGreetingModuleFile(importSuffix = '') {
|
|
700
699
|
return `import { Module } from '@fluojs/core';
|
|
701
700
|
|
|
702
|
-
import {
|
|
703
|
-
import {
|
|
704
|
-
import {
|
|
701
|
+
import { GreetingController } from './greeting.controller${importSuffix}';
|
|
702
|
+
import { GreetingRepo } from './greeting.repo${importSuffix}';
|
|
703
|
+
import { GreetingService } from './greeting.service${importSuffix}';
|
|
705
704
|
|
|
706
705
|
@Module({
|
|
707
|
-
controllers: [
|
|
708
|
-
providers: [
|
|
706
|
+
controllers: [GreetingController],
|
|
707
|
+
providers: [GreetingRepo, GreetingService],
|
|
709
708
|
})
|
|
710
|
-
export class
|
|
709
|
+
export class GreetingModule {}
|
|
711
710
|
`;
|
|
712
711
|
}
|
|
713
712
|
function createMainFile(options) {
|
|
@@ -742,6 +741,12 @@ export default {
|
|
|
742
741
|
`;
|
|
743
742
|
}
|
|
744
743
|
const portExpression = options.runtime === 'bun' ? "Bun.env.PORT ?? '3000'" : "process.env.PORT ?? '3000'";
|
|
744
|
+
const loggerGuidance = options.runtime === 'node' ? `
|
|
745
|
+
// Application logging defaults to the pretty console logger when logger is omitted.
|
|
746
|
+
// JSON logs are opt-in:
|
|
747
|
+
// import { createJsonApplicationLogger } from '@fluojs/runtime/node';
|
|
748
|
+
// Then pass \`logger: createJsonApplicationLogger()\` to FluoFactory.create(...).
|
|
749
|
+
` : '';
|
|
745
750
|
return `import { ${starter.adapterFactory} } from '${starter.packageName}';
|
|
746
751
|
import { FluoFactory } from '@fluojs/runtime';
|
|
747
752
|
|
|
@@ -749,6 +754,7 @@ import { AppModule } from './app';
|
|
|
749
754
|
|
|
750
755
|
// The generated starter wires the selected first-class fluo new application path:
|
|
751
756
|
// ${starter.runtimeLabel} + ${starter.platformLabel} via ${starter.adapterFactory}(...).
|
|
757
|
+
${loggerGuidance}
|
|
752
758
|
|
|
753
759
|
const parsedPort = Number.parseInt(${portExpression}, 10);
|
|
754
760
|
const port = Number.isFinite(parsedPort) ? parsedPort : 3000;
|
|
@@ -1428,12 +1434,12 @@ Generated by @fluojs/cli.
|
|
|
1428
1434
|
|
|
1429
1435
|
- \`src/app.ts\` — shared application module with config, runtime health endpoints, and the TCP microservice registration.
|
|
1430
1436
|
- \`src/main.ts\` — Fastify HTTP bootstrap that also starts the attached TCP microservice.
|
|
1431
|
-
- \`src/
|
|
1437
|
+
- \`src/greeting/*\` — starter-owned HTTP greeting slice.
|
|
1432
1438
|
- \`src/math/*\` — starter-owned message handler slice that proves the microservice half of the topology.
|
|
1433
1439
|
|
|
1434
1440
|
## Official generated testing templates
|
|
1435
1441
|
|
|
1436
|
-
- \`src/
|
|
1442
|
+
- \`src/greeting/*.test.ts\` — unit templates for the HTTP slice.
|
|
1437
1443
|
- \`src/math/math.handler.test.ts\` — unit template for the microservice handler.
|
|
1438
1444
|
- \`src/app.test.ts\` — hybrid verification template covering HTTP dispatch plus in-memory message routing with the same shared module contract.
|
|
1439
1445
|
|
|
@@ -1444,12 +1450,11 @@ function createMixedAppFile() {
|
|
|
1444
1450
|
return `import { Global, Module } from '@fluojs/core';
|
|
1445
1451
|
import { ConfigModule } from '@fluojs/config';
|
|
1446
1452
|
import { MicroservicesModule, TcpMicroserviceTransport } from '@fluojs/microservices';
|
|
1447
|
-
import {
|
|
1453
|
+
import { HealthModule } from '@fluojs/runtime';
|
|
1448
1454
|
|
|
1449
|
-
import {
|
|
1455
|
+
import { GreetingModule } from './greeting/greeting.module';
|
|
1450
1456
|
import { MathHandler } from './math/math.handler';
|
|
1451
1457
|
|
|
1452
|
-
const RuntimeHealthModule = createHealthModule();
|
|
1453
1458
|
const parsedMicroservicePort = Number.parseInt(process.env.MICROSERVICE_PORT ?? '4000', 10);
|
|
1454
1459
|
const microservicePort = Number.isFinite(parsedMicroservicePort) ? parsedMicroservicePort : 4000;
|
|
1455
1460
|
const microserviceHost = process.env.MICROSERVICE_HOST ?? '127.0.0.1';
|
|
@@ -1461,8 +1466,8 @@ const microserviceHost = process.env.MICROSERVICE_HOST ?? '127.0.0.1';
|
|
|
1461
1466
|
envFile: '.env',
|
|
1462
1467
|
processEnv: process.env,
|
|
1463
1468
|
}),
|
|
1464
|
-
|
|
1465
|
-
|
|
1469
|
+
GreetingModule,
|
|
1470
|
+
HealthModule.forRoot(),
|
|
1466
1471
|
MicroservicesModule.forRoot({
|
|
1467
1472
|
transport: new TcpMicroserviceTransport({ host: microserviceHost, port: microservicePort }),
|
|
1468
1473
|
}),
|
|
@@ -1499,9 +1504,9 @@ import {
|
|
|
1499
1504
|
MicroservicesModule,
|
|
1500
1505
|
type MicroserviceTransport,
|
|
1501
1506
|
} from '@fluojs/microservices';
|
|
1502
|
-
import { FluoFactory,
|
|
1507
|
+
import { FluoFactory, HealthModule } from '@fluojs/runtime';
|
|
1503
1508
|
|
|
1504
|
-
import {
|
|
1509
|
+
import { GreetingModule } from './greeting/greeting.module';
|
|
1505
1510
|
import { MathHandler } from './math/math.handler';
|
|
1506
1511
|
|
|
1507
1512
|
type TransportHandler = Parameters<MicroserviceTransport['listen']>[0];
|
|
@@ -1576,8 +1581,6 @@ function createResponse(): FrameworkResponse & { body?: unknown } {
|
|
|
1576
1581
|
describe('AppModule mixed starter', () => {
|
|
1577
1582
|
it('keeps HTTP routes and message handlers in one explicit topology contract', async () => {
|
|
1578
1583
|
const transport = new InMemoryLoopbackTransport();
|
|
1579
|
-
const RuntimeHealthModule = createHealthModule();
|
|
1580
|
-
|
|
1581
1584
|
@Global()
|
|
1582
1585
|
@Module({
|
|
1583
1586
|
imports: [
|
|
@@ -1585,8 +1588,8 @@ describe('AppModule mixed starter', () => {
|
|
|
1585
1588
|
envFile: '.env',
|
|
1586
1589
|
processEnv: process.env,
|
|
1587
1590
|
}),
|
|
1588
|
-
|
|
1589
|
-
|
|
1591
|
+
GreetingModule,
|
|
1592
|
+
HealthModule.forRoot(),
|
|
1590
1593
|
MicroservicesModule.forRoot({ transport }),
|
|
1591
1594
|
],
|
|
1592
1595
|
providers: [MathHandler],
|
|
@@ -1594,13 +1597,13 @@ describe('AppModule mixed starter', () => {
|
|
|
1594
1597
|
class TestAppModule {}
|
|
1595
1598
|
|
|
1596
1599
|
const app = await FluoFactory.create(TestAppModule, {});
|
|
1597
|
-
const
|
|
1600
|
+
const greetingResponse = createResponse();
|
|
1598
1601
|
|
|
1599
1602
|
const microservice = await app.connectMicroservice();
|
|
1600
1603
|
|
|
1601
|
-
await Promise.all([app.startAllMicroservices(), app.dispatch(createRequest('/
|
|
1604
|
+
await Promise.all([app.startAllMicroservices(), app.dispatch(createRequest('/greeting/'), greetingResponse)]);
|
|
1602
1605
|
|
|
1603
|
-
expect(
|
|
1606
|
+
expect(greetingResponse.body).toEqual({ message: 'Hello from fluo', framework: 'fluo', project: expect.any(String) });
|
|
1604
1607
|
await expect(transport.send('math.sum', { a: 20, b: 22 })).resolves.toBe(42);
|
|
1605
1608
|
expect(microservice.state).toBe('ready');
|
|
1606
1609
|
|
|
@@ -1671,13 +1674,13 @@ describe('AppModule', () => {
|
|
|
1671
1674
|
await app.close();
|
|
1672
1675
|
});
|
|
1673
1676
|
|
|
1674
|
-
it('dispatches the
|
|
1677
|
+
it('dispatches the greeting route', async () => {
|
|
1675
1678
|
const app = await FluoFactory.create(AppModule, {});
|
|
1676
1679
|
const response = createResponse();
|
|
1677
1680
|
|
|
1678
|
-
await app.dispatch(createRequest('/
|
|
1681
|
+
await app.dispatch(createRequest('/greeting/'), response);
|
|
1679
1682
|
|
|
1680
|
-
expect(response.body).toEqual({
|
|
1683
|
+
expect(response.body).toEqual({ message: 'Hello from fluo', framework: 'fluo', project: expect.any(String) });
|
|
1681
1684
|
|
|
1682
1685
|
await app.close();
|
|
1683
1686
|
});
|
|
@@ -1703,8 +1706,8 @@ describe('AppModule e2e', () => {
|
|
|
1703
1706
|
body: { status: 'ready' },
|
|
1704
1707
|
status: 200,
|
|
1705
1708
|
});
|
|
1706
|
-
await expect(app.dispatch({ method: 'GET', path: '/
|
|
1707
|
-
body: {
|
|
1709
|
+
await expect(app.dispatch({ method: 'GET', path: '/greeting/' })).resolves.toMatchObject({
|
|
1710
|
+
body: { message: 'Hello from fluo', framework: 'fluo', project: expect.any(String) },
|
|
1708
1711
|
status: 200,
|
|
1709
1712
|
});
|
|
1710
1713
|
|
|
@@ -1763,7 +1766,7 @@ Deno.test('AppModule dispatches the generated Deno starter routes', async () =>
|
|
|
1763
1766
|
|
|
1764
1767
|
await app.dispatch({ body: undefined, cookies: {}, headers: {}, method: 'GET', params: {}, path: '/health', query: {}, raw: {}, url: '/health' }, healthResponse as never);
|
|
1765
1768
|
await app.dispatch({ body: undefined, cookies: {}, headers: {}, method: 'GET', params: {}, path: '/ready', query: {}, raw: {}, url: '/ready' }, readyResponse as never);
|
|
1766
|
-
await app.dispatch({ body: undefined, cookies: {}, headers: {}, method: 'GET', params: {}, path: '/
|
|
1769
|
+
await app.dispatch({ body: undefined, cookies: {}, headers: {}, method: 'GET', params: {}, path: '/greeting/', query: {}, raw: {}, url: '/greeting/' }, infoResponse as never);
|
|
1767
1770
|
|
|
1768
1771
|
if (JSON.stringify(healthResponse.body) !== JSON.stringify({ status: 'ok' })) {
|
|
1769
1772
|
throw new Error('Expected /health to return status ok.');
|
|
@@ -1774,12 +1777,12 @@ Deno.test('AppModule dispatches the generated Deno starter routes', async () =>
|
|
|
1774
1777
|
}
|
|
1775
1778
|
|
|
1776
1779
|
if (typeof infoResponse.body !== 'object' || infoResponse.body === null) {
|
|
1777
|
-
throw new Error('Expected /
|
|
1780
|
+
throw new Error('Expected /greeting/ to return an object body.');
|
|
1778
1781
|
}
|
|
1779
1782
|
|
|
1780
|
-
const infoBody = infoResponse.body as {
|
|
1781
|
-
if (infoBody.
|
|
1782
|
-
throw new Error('Expected /
|
|
1783
|
+
const infoBody = infoResponse.body as { framework?: unknown; message?: unknown; project?: unknown };
|
|
1784
|
+
if (infoBody.message !== 'Hello from fluo' || infoBody.framework !== 'fluo' || typeof infoBody.project !== 'string') {
|
|
1785
|
+
throw new Error('Expected /greeting/ to return the generated service payload.');
|
|
1783
1786
|
}
|
|
1784
1787
|
|
|
1785
1788
|
await app.close();
|
|
@@ -1865,7 +1868,7 @@ function emitSharedScaffoldFiles(options, bootstrapPlan, releaseVersion, package
|
|
|
1865
1868
|
}];
|
|
1866
1869
|
if (bootstrapPlan.profile.id !== 'application-deno-deno-http') {
|
|
1867
1870
|
sharedFiles.push({
|
|
1868
|
-
content: createProjectTsconfig(),
|
|
1871
|
+
content: createProjectTsconfig(bootstrapPlan),
|
|
1869
1872
|
path: 'tsconfig.json'
|
|
1870
1873
|
}, {
|
|
1871
1874
|
content: createProjectTsconfigBuild(),
|
|
@@ -1905,20 +1908,20 @@ function emitApplicationScaffoldFiles(options) {
|
|
|
1905
1908
|
content: createMainFile(options),
|
|
1906
1909
|
path: entrypointPath
|
|
1907
1910
|
}, {
|
|
1908
|
-
content:
|
|
1909
|
-
path: 'src/
|
|
1911
|
+
content: createGreetingResponseDtoFile(),
|
|
1912
|
+
path: 'src/greeting/greeting.response.dto.ts'
|
|
1910
1913
|
}, {
|
|
1911
|
-
content:
|
|
1912
|
-
path: 'src/
|
|
1914
|
+
content: createGreetingRepoFile(options.projectName, importSuffix),
|
|
1915
|
+
path: 'src/greeting/greeting.repo.ts'
|
|
1913
1916
|
}, {
|
|
1914
|
-
content:
|
|
1915
|
-
path: 'src/
|
|
1917
|
+
content: createGreetingServiceFile(importSuffix),
|
|
1918
|
+
path: 'src/greeting/greeting.service.ts'
|
|
1916
1919
|
}, {
|
|
1917
|
-
content:
|
|
1918
|
-
path: 'src/
|
|
1920
|
+
content: createGreetingControllerFile(importSuffix),
|
|
1921
|
+
path: 'src/greeting/greeting.controller.ts'
|
|
1919
1922
|
}, {
|
|
1920
|
-
content:
|
|
1921
|
-
path: 'src/
|
|
1923
|
+
content: createGreetingModuleFile(importSuffix),
|
|
1924
|
+
path: 'src/greeting/greeting.module.ts'
|
|
1922
1925
|
}];
|
|
1923
1926
|
if (options.runtime === 'deno') {
|
|
1924
1927
|
files.push({
|
|
@@ -1928,14 +1931,14 @@ function emitApplicationScaffoldFiles(options) {
|
|
|
1928
1931
|
return files;
|
|
1929
1932
|
}
|
|
1930
1933
|
files.push({
|
|
1931
|
-
content:
|
|
1932
|
-
path: 'src/
|
|
1934
|
+
content: createGreetingRepoTestFile(),
|
|
1935
|
+
path: 'src/greeting/greeting.repo.test.ts'
|
|
1933
1936
|
}, {
|
|
1934
|
-
content:
|
|
1935
|
-
path: 'src/
|
|
1937
|
+
content: createGreetingServiceTestFile(),
|
|
1938
|
+
path: 'src/greeting/greeting.service.test.ts'
|
|
1936
1939
|
}, {
|
|
1937
|
-
content:
|
|
1938
|
-
path: 'src/
|
|
1940
|
+
content: createGreetingControllerTestFile(),
|
|
1941
|
+
path: 'src/greeting/greeting.controller.test.ts'
|
|
1939
1942
|
}, {
|
|
1940
1943
|
content: createAppTestFile(importSuffix),
|
|
1941
1944
|
path: 'src/app.test.ts'
|
|
@@ -1991,29 +1994,29 @@ function emitScaffoldFilesForRecipe(options, recipeId) {
|
|
|
1991
1994
|
content: createMixedMainFile(),
|
|
1992
1995
|
path: 'src/main.ts'
|
|
1993
1996
|
}, {
|
|
1994
|
-
content:
|
|
1995
|
-
path: 'src/
|
|
1997
|
+
content: createGreetingResponseDtoFile(),
|
|
1998
|
+
path: 'src/greeting/greeting.response.dto.ts'
|
|
1996
1999
|
}, {
|
|
1997
|
-
content:
|
|
1998
|
-
path: 'src/
|
|
2000
|
+
content: createGreetingRepoFile(options.projectName),
|
|
2001
|
+
path: 'src/greeting/greeting.repo.ts'
|
|
1999
2002
|
}, {
|
|
2000
|
-
content:
|
|
2001
|
-
path: 'src/
|
|
2003
|
+
content: createGreetingRepoTestFile(),
|
|
2004
|
+
path: 'src/greeting/greeting.repo.test.ts'
|
|
2002
2005
|
}, {
|
|
2003
|
-
content:
|
|
2004
|
-
path: 'src/
|
|
2006
|
+
content: createGreetingServiceFile(),
|
|
2007
|
+
path: 'src/greeting/greeting.service.ts'
|
|
2005
2008
|
}, {
|
|
2006
|
-
content:
|
|
2007
|
-
path: 'src/
|
|
2009
|
+
content: createGreetingServiceTestFile(),
|
|
2010
|
+
path: 'src/greeting/greeting.service.test.ts'
|
|
2008
2011
|
}, {
|
|
2009
|
-
content:
|
|
2010
|
-
path: 'src/
|
|
2012
|
+
content: createGreetingControllerFile(),
|
|
2013
|
+
path: 'src/greeting/greeting.controller.ts'
|
|
2011
2014
|
}, {
|
|
2012
|
-
content:
|
|
2013
|
-
path: 'src/
|
|
2015
|
+
content: createGreetingControllerTestFile(),
|
|
2016
|
+
path: 'src/greeting/greeting.controller.test.ts'
|
|
2014
2017
|
}, {
|
|
2015
|
-
content:
|
|
2016
|
-
path: 'src/
|
|
2018
|
+
content: createGreetingModuleFile(),
|
|
2019
|
+
path: 'src/greeting/greeting.module.ts'
|
|
2017
2020
|
}, {
|
|
2018
2021
|
content: createMathHandlerFile({
|
|
2019
2022
|
transport: 'tcp'
|