@fluojs/cli 1.0.0-beta.6 → 1.0.0-beta.8

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 CHANGED
@@ -76,6 +76,8 @@ pnpm dev
76
76
 
77
77
  생성된 non-Deno starter의 `vite.config.ts`는 `@fluojs/vite`에서 `fluoDecoratorsPlugin()`을 import합니다. 따라서 decorator transform 업데이트는 각 신규 프로젝트에 inline 복사되는 대신 유지보수되는 Vite 패키지를 통해 전달됩니다.
78
78
 
79
+ 생성된 non-Deno HTTP starter는 TDD-first Vitest 레이아웃을 사용합니다. 빠른 greeting unit test와 `greeting.slice.test.ts`는 `src/greeting/` 아래에 colocate하고, 앱 dispatch test는 `src/app.test.ts`에 유지하며, 기본 e2e 스타일 request-pipeline test는 `createTestApp({ rootModule })`와 `app.request(...).send()`를 사용해 `test/app.e2e.test.ts`에 둡니다. 생성된 `vitest.config.ts`는 `src/**/*.test.ts`와 `test/**/*.test.ts`를 모두 포함하고, package script는 `test`, `test:watch`, `test:cov`, `test:e2e`를 노출합니다. 기존 `src/app.e2e.test.ts` 테스트는 request helper를 바꾸지 않고 `test/app.e2e.test.ts`로 이동할 수 있습니다.
80
+
79
81
  생성된 Node.js 애플리케이션 프로젝트에서 `fluo dev`는 기본적으로 fluo가 소유한 restart boundary를 거칩니다. 이 runner는 source와 주요 config 입력을 watch하고, atomic-save event burst를 debounce하며, restart 전에 파일 content hash를 비교하고, spawn하는 각 Node 앱 child process마다 `.env`를 로드하며, `node_modules`, `dist`, `.git`, `.fluo`, coverage, cache 폴더, editor swap file 같은 noisy output/cache 경로를 무시합니다. 파일 내용이 바뀌지 않은 Ctrl+S 저장은 앱을 재시작하지 않아야 합니다. 계획된 restart가 아닌 terminal 앱 child exit 또는 crash가 발생하면 runner는 watcher를 닫고, pending restart timer와 path를 비우며, `SIGINT`/`SIGTERM` handler를 등록 해제하고, child의 terminal code로 종료합니다. 이 동작은 full-process restart-on-watch이며 module-level HMR이 아닙니다. Config watch reload는 별도의 in-process config 관심사이고, 향후 HMR 작업은 어떤 모듈을 안전하게 hot-swap할 수 있는지 따로 문서화해야 합니다. 디버깅에 runtime-native Node watcher가 필요하면 `fluo dev --raw-watch` 또는 `FLUO_DEV_RAW_WATCH=1`을 사용하세요. 생성된 Bun/Deno/Workers 프로젝트는 기본적으로 watch/reload를 `bun --watch`, `deno run --watch`, `wrangler dev`에 위임합니다. 해당 프로젝트에서 fluo 소유 restart runner로 되돌리려면 `fluo dev --runner fluo` 또는 `FLUO_DEV_RUNNER=fluo`를 사용하고, 그 runner에 추가 ignore 경로가 필요하면 `FLUO_DEV_WATCH_IGNORE=path,pattern`으로 지정하세요.
80
82
 
81
83
  `fluo new`는 같은 Node 기반 설치/빌드 흐름 위에서 Node.js + Fastify, Express, raw Node.js HTTP 애플리케이션 스타터를 제공합니다.
@@ -135,18 +137,23 @@ feature slice를 생성합니다. 일부 schematic은 모듈에 자동 등록되
135
137
 
136
138
  ```bash
137
139
  fluo generate module users
140
+ fluo generate module users --with-test
138
141
  fluo generate resource users
142
+ fluo generate resource users --with-slice-test
143
+ fluo generate e2e users
139
144
  fluo generate controller users
140
145
  fluo generate service users
141
146
  fluo generate request-dto users CreateUser
142
147
  fluo generate service users --dry-run
143
148
  ```
144
149
 
145
- 지원되는 generator kind와 alias는 `controller`/`co`, `guard`/`gu`, `interceptor`/`in`, `middleware`/`mi`, `module`/`mo`, `repo`/`repository`, `request-dto`/`req`, `resource`/`resrc`, `response-dto`/`res`, `service`/`s`입니다.
150
+ 지원되는 generator kind와 alias는 `controller`/`co`, `e2e`, `guard`/`gu`, `interceptor`/`in`, `middleware`/`mi`, `module`/`mo`, `repo`/`repository`, `request-dto`/`req`, `resource`/`resrc`, `response-dto`/`res`, `service`/`s`입니다.
151
+
152
+ 자동 등록되는 generator는 `controller`, `service`, `repo`, `guard`, `interceptor`, `middleware`입니다. 파일만 생성하는 generator는 `e2e`, `module`, `request-dto`, `response-dto`, `resource`입니다.
146
153
 
147
- 자동 등록되는 generator는 `controller`, `service`, `repo`, `guard`, `interceptor`, `middleware`입니다. 파일만 생성하는 generator는 `module`, `request-dto`, `response-dto`, `resource`입니다.
154
+ `fluo generate module <name> --with-test`는 작성한 module을 `createTestingModule({ rootModule })`로 컴파일하는 `*.slice.test.ts`를 추가합니다. `fluo generate resource <name>`는 module, controller, service, repository, request DTO, response DTO, test를 포함하는 완전한 feature slice를 생성합니다. `--with-slice-test`를 추가하면 provider override와 service resolution을 보여 주는 resource-level slice test도 포함합니다. 생성된 resource module은 parent module에 자동으로 연결하지 않으므로, slice를 활성화할 준비가 되었을 때 직접 import하세요.
148
155
 
149
- `fluo generate resource <name>`는 module, controller, service, repository, request DTO, response DTO, test 포함하는 완전한 feature slice 생성합니다. 생성된 resource module은 parent module에 자동으로 연결하지 않으므로, slice를 활성화할 준비가 되었을 직접 import하세요.
156
+ `fluo generate e2e <name>`는 generated starter와 같은 app-level test 영역에 request-pipeline test를 두도록 `createTestApp({ rootModule: AppModule })`을 사용하는 `test/<name>.e2e.test.ts`를 작성합니다. 생성된 unit test는 직접 class 동작 검증에, slice test는 DI wiring과 override 검증에, e2e test는 virtual app을 통과하는 route, guard, interceptor, DTO validation, response writing 검증에 사용하세요.
150
157
 
151
158
  Request DTO 생성은 feature 디렉터리와 DTO 클래스 이름을 분리해서 받습니다. 따라서 `CreateUser`, `UpdateUser` 같은 여러 입력 계약을 같은 `src/users/` 슬라이스 안에 둘 수 있습니다.
152
159
 
package/README.md CHANGED
@@ -76,6 +76,8 @@ Generated Node.js `dev`, `build`, and `start` package scripts delegate to `fluo
76
76
 
77
77
  Generated non-Deno starter `vite.config.ts` files import `fluoDecoratorsPlugin()` from `@fluojs/vite`, so decorator transform updates ship through the maintained Vite package instead of being copied inline into every new project.
78
78
 
79
+ Generated non-Deno HTTP starters use a TDD-first Vitest layout: fast greeting unit tests and `greeting.slice.test.ts` stay colocated under `src/greeting/`, app dispatch tests stay in `src/app.test.ts`, and the default e2e-style request-pipeline tests live in `test/app.e2e.test.ts` with `createTestApp({ rootModule })` plus `app.request(...).send()`. The generated `vitest.config.ts` includes both `src/**/*.test.ts` and `test/**/*.test.ts`, while generated package scripts expose `test`, `test:watch`, `test:cov`, and `test:e2e`; existing `src/app.e2e.test.ts` tests can move to `test/app.e2e.test.ts` without changing the request helper.
80
+
79
81
  For generated Node.js application projects, `fluo dev` runs through a fluo-owned restart boundary by default. The runner watches source and common config inputs, debounces atomic-save bursts, hashes file content before restarting, loads `.env` for each Node app child process it spawns, and ignores noisy output/cache paths such as `node_modules`, `dist`, `.git`, `.fluo`, coverage, cache folders, and editor swap files. Pressing Ctrl+S without changing file content should not restart the app. On terminal app child exit or crash outside a planned restart, the runner closes watchers, clears the pending restart timer and paths, unregisters its `SIGINT`/`SIGTERM` handlers, and exits with the child terminal code. This is full-process restart-on-watch, not module-level HMR; config watch reloads are a separate in-process config concern, and future HMR work must document which modules can be safely hot-swapped. Use `fluo dev --raw-watch` or `FLUO_DEV_RAW_WATCH=1` when you need the runtime-native Node watcher for debugging. Generated Bun/Deno/Workers projects delegate watch/reload behavior to `bun --watch`, `deno run --watch`, or `wrangler dev` by default; use `fluo dev --runner fluo` or `FLUO_DEV_RUNNER=fluo` when those projects should return to the fluo-owned restart runner, and use `FLUO_DEV_WATCH_IGNORE=path,pattern` to add extra ignored paths for that runner.
80
82
 
81
83
  `fluo new` supports Node.js + Fastify, Express, and raw Node.js HTTP application starters on the same Node-oriented install/build flow:
@@ -135,18 +137,23 @@ Generate a feature slice; some schematics auto-register in the module, while oth
135
137
 
136
138
  ```bash
137
139
  fluo generate module users
140
+ fluo generate module users --with-test
138
141
  fluo generate resource users
142
+ fluo generate resource users --with-slice-test
143
+ fluo generate e2e users
139
144
  fluo generate controller users
140
145
  fluo generate service users
141
146
  fluo generate request-dto users CreateUser
142
147
  fluo generate service users --dry-run
143
148
  ```
144
149
 
145
- Supported generator kinds and aliases are `controller`/`co`, `guard`/`gu`, `interceptor`/`in`, `middleware`/`mi`, `module`/`mo`, `repo`/`repository`, `request-dto`/`req`, `resource`/`resrc`, `response-dto`/`res`, and `service`/`s`.
150
+ Supported generator kinds and aliases are `controller`/`co`, `e2e`, `guard`/`gu`, `interceptor`/`in`, `middleware`/`mi`, `module`/`mo`, `repo`/`repository`, `request-dto`/`req`, `resource`/`resrc`, `response-dto`/`res`, and `service`/`s`.
151
+
152
+ Auto-registered generators are `controller`, `service`, `repo`, `guard`, `interceptor`, and `middleware`. Files-only generators are `e2e`, `module`, `request-dto`, `response-dto`, and `resource`.
146
153
 
147
- Auto-registered generators are `controller`, `service`, `repo`, `guard`, `interceptor`, and `middleware`. Files-only generators are `module`, `request-dto`, `response-dto`, and `resource`.
154
+ `fluo generate module <name> --with-test` adds a `*.slice.test.ts` that compiles the authored module with `createTestingModule({ rootModule })`. `fluo generate resource <name>` creates a complete feature slice with a module, controller, service, repository, request DTO, response DTO, and tests; add `--with-slice-test` to include a resource-level slice test that demonstrates provider override and service resolution. It does not wire the resource module into a parent module automatically; import the generated module when you are ready to activate the slice.
148
155
 
149
- `fluo generate resource <name>` creates a complete feature slice with a module, controller, service, repository, request DTO, response DTO, and tests. It does not wire the resource module into a parent module automatically; import the generated module when you are ready to activate the slice.
156
+ `fluo generate e2e <name>` writes `test/<name>.e2e.test.ts` with `createTestApp({ rootModule: AppModule })` so request-pipeline tests live in the same app-level test area as generated starters. Use generated unit tests for direct class behavior, slice tests for DI wiring and overrides, and e2e tests for routes, guards, interceptors, DTO validation, and response writing through the virtual app.
150
157
 
151
158
  Request DTO generation accepts the feature directory separately from the DTO class name, so multiple input contracts such as `CreateUser` and `UpdateUser` can live inside the same `src/users/` slice.
152
159
 
package/dist/cli.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,KAAK,4BAA4B,EAAmC,MAAM,uBAAuB,CAAC;AAE3G,OAAO,EAAE,KAAK,wBAAwB,EAA2B,MAAM,mBAAmB,CAAC;AAO3F,OAAO,EAAE,KAAK,4BAA4B,EAA6C,MAAM,mBAAmB,CAAC;AAEjH,KAAK,SAAS,GAAG;IACf,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC;CACjC,CAAC;AAEF,KAAK,iBAAiB,GAAG;IACvB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB,CAAC;AAEF;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,EAAE,CAAC,EAAE,OAAO,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,aAAa,CAAC,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,CAAC,CAAC;IACrF,YAAY,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;QAAC,MAAM,CAAC,EAAE,SAAS,CAAC;QAAC,KAAK,EAAE,SAAS,GAAG,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,SAAS,CAAA;KAAE,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACzL,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,KAAK,CAAC,EAAE,iBAAiB,CAAC;IAC1B,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,WAAW,CAAC,EAAE,KAAK,GAAG,4BAA4B,CAAC;CACpD;AAyXD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAsB,MAAM,CAC1B,IAAI,WAAwB,EAC5B,OAAO,GAAE,iBAAiB,GAAG,wBAAwB,GAAG,4BAAiC,GACxF,OAAO,CAAC,MAAM,CAAC,CAqNjB"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,KAAK,4BAA4B,EAAmC,MAAM,uBAAuB,CAAC;AAE3G,OAAO,EAAE,KAAK,wBAAwB,EAA2B,MAAM,mBAAmB,CAAC;AAO3F,OAAO,EAAE,KAAK,4BAA4B,EAA6C,MAAM,mBAAmB,CAAC;AAEjH,KAAK,SAAS,GAAG;IACf,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC;CACjC,CAAC;AAEF,KAAK,iBAAiB,GAAG;IACvB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB,CAAC;AAEF;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,EAAE,CAAC,EAAE,OAAO,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,aAAa,CAAC,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,CAAC,CAAC;IACrF,YAAY,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;QAAC,MAAM,CAAC,EAAE,SAAS,CAAC;QAAC,KAAK,EAAE,SAAS,GAAG,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,SAAS,CAAA;KAAE,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACzL,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,KAAK,CAAC,EAAE,iBAAiB,CAAC;IAC1B,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,WAAW,CAAC,EAAE,KAAK,GAAG,4BAA4B,CAAC;CACpD;AAwZD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAsB,MAAM,CAC1B,IAAI,WAAwB,EAC5B,OAAO,GAAE,iBAAiB,GAAG,wBAAwB,GAAG,4BAAiC,GACxF,OAAO,CAAC,MAAM,CAAC,CAqNjB"}
package/dist/cli.js CHANGED
@@ -134,7 +134,7 @@ function readCliVersion() {
134
134
  return manifest.version;
135
135
  }
136
136
  function generateUsage() {
137
- return ['Usage: fluo generate|g <kind> <name> [options]', ' fluo generate|g request-dto|req <feature> <name> [options]', '', 'Schematics', renderHelpTable(GENERATE_KIND_HELP, [{
137
+ return ['Usage: fluo generate|g <kind> <name> [options]', ' fluo generate|g request-dto|req <feature> <name> [options]', ' fluo generate|g e2e <name> [options]', '', 'Schematics', renderHelpTable(GENERATE_KIND_HELP, [{
138
138
  header: 'Schematic',
139
139
  render: entry => entry.schematic
140
140
  }, {
@@ -206,6 +206,8 @@ function parseGenerateArgs(argv) {
206
206
  let seenForce = false;
207
207
  let seenDryRun = false;
208
208
  let seenTargetDirectory = false;
209
+ let seenWithSliceTest = false;
210
+ let seenWithTest = false;
209
211
  for (let index = 0; index < optionArgs.length; index += 1) {
210
212
  const option = optionArgs[index];
211
213
  const next = optionArgs[index + 1];
@@ -243,8 +245,30 @@ function parseGenerateArgs(argv) {
243
245
  seenDryRun = true;
244
246
  continue;
245
247
  }
248
+ if (option === '--with-test') {
249
+ if (seenWithTest) {
250
+ throw new Error('Duplicate --with-test option.');
251
+ }
252
+ parsedOptions.withTest = true;
253
+ seenWithTest = true;
254
+ continue;
255
+ }
256
+ if (option === '--with-slice-test') {
257
+ if (seenWithSliceTest) {
258
+ throw new Error('Duplicate --with-slice-test option.');
259
+ }
260
+ parsedOptions.withSliceTest = true;
261
+ seenWithSliceTest = true;
262
+ continue;
263
+ }
246
264
  throw new Error(`Unknown option: ${option}`);
247
265
  }
266
+ if (parsedOptions.withTest && kind !== 'module') {
267
+ throw new Error('--with-test is only supported for module generation. Use --with-slice-test for resource generation.');
268
+ }
269
+ if (parsedOptions.withSliceTest && kind !== 'resource') {
270
+ throw new Error('--with-slice-test is only supported for resource generation.');
271
+ }
248
272
  return {
249
273
  kind,
250
274
  name,
@@ -472,7 +496,7 @@ export async function runCli(argv = process.argv.slice(2), runtime = {}) {
472
496
  return runInfoCommand(parsedCommand.argv, commandRuntime);
473
497
  }
474
498
  if (parsedCommand.command === 'build' || parsedCommand.command === 'dev' || parsedCommand.command === 'start') {
475
- return runScriptCommand(parsedCommand.command, parsedCommand.argv, commandRuntime);
499
+ return await runScriptCommand(parsedCommand.command, parsedCommand.argv, commandRuntime);
476
500
  }
477
501
  if (parsedCommand.command === 'upgrade') {
478
502
  return runUpgradeCommand(parsedCommand.argv, commandRuntime);
@@ -1 +1 @@
1
- {"version":3,"file":"generate.d.ts","sourceRoot":"","sources":["../../src/commands/generate.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAClE,OAAO,KAAK,EAAE,sBAAsB,EAAkB,MAAM,2BAA2B,CAAC;AAexF,8EAA8E;AAC9E,MAAM,MAAM,kBAAkB,GAAG,QAAQ,GAAG,eAAe,GAAG,kBAAkB,GAAG,eAAe,GAAG,WAAW,GAAG,MAAM,GAAG,WAAW,CAAC;AAExI,0FAA0F;AAC1F,MAAM,MAAM,iBAAiB,GAAG;IAC9B,oCAAoC;IACpC,MAAM,EAAE,kBAAkB,CAAC;IAC3B,gDAAgD;IAChD,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AA0HF;;;;;;GAMG;AACH,MAAM,MAAM,cAAc,GAAG;IAC3B,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,gBAAgB,EAAE,OAAO,CAAC;IAC1B,UAAU,EAAE,MAAM,GAAG,SAAS,CAAC;IAC/B,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,iBAAiB,EAAE,CAAC;IAClC,cAAc,EAAE,sBAAsB,CAAC,gBAAgB,CAAC,CAAC;CAC1D,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,aAAa,EAAE,IAAI,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,EAAE,OAAO,GAAE,eAAoB,GAAG,cAAc,CAgE1I"}
1
+ {"version":3,"file":"generate.d.ts","sourceRoot":"","sources":["../../src/commands/generate.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAClE,OAAO,KAAK,EAAE,sBAAsB,EAAkB,MAAM,2BAA2B,CAAC;AAexF,8EAA8E;AAC9E,MAAM,MAAM,kBAAkB,GAAG,QAAQ,GAAG,eAAe,GAAG,kBAAkB,GAAG,eAAe,GAAG,WAAW,GAAG,MAAM,GAAG,WAAW,CAAC;AAExI,0FAA0F;AAC1F,MAAM,MAAM,iBAAiB,GAAG;IAC9B,oCAAoC;IACpC,MAAM,EAAE,kBAAkB,CAAC;IAC3B,gDAAgD;IAChD,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAyIF;;;;;;GAMG;AACH,MAAM,MAAM,cAAc,GAAG;IAC3B,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,gBAAgB,EAAE,OAAO,CAAC;IAC1B,UAAU,EAAE,MAAM,GAAG,SAAS,CAAC;IAC/B,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,iBAAiB,EAAE,CAAC;IAClC,cAAc,EAAE,sBAAsB,CAAC,gBAAgB,CAAC,CAAC;CAC1D,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,aAAa,EAAE,IAAI,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,EAAE,OAAO,GAAE,eAAoB,GAAG,cAAc,CAgE1I"}
@@ -1,5 +1,5 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
- import { join, normalize, resolve } from 'node:path';
2
+ import { basename, dirname, join, normalize, relative, resolve, sep } from 'node:path';
3
3
  import { findGeneratorDefinition } from '../generators/manifest.js';
4
4
  import { ensureModuleImport, generateModuleFiles, registerInModule } from '../generators/module.js';
5
5
  import { toKebabCase, toPascalCase, toPlural } from '../generators/utils.js';
@@ -57,14 +57,25 @@ function planModuleWrite(modulePath, content) {
57
57
  path: modulePath
58
58
  };
59
59
  }
60
- function createGeneratorOptions(kind, domainDirectory, kebab, options) {
60
+ function createGeneratorOptions(kind, domainDirectory, kebab, options, resolvedBase) {
61
61
  return {
62
62
  ...options,
63
+ e2eRootModuleImport: options.e2eRootModuleImport ?? (kind === 'e2e' ? resolveE2eRootModuleImport(domainDirectory, resolvedBase) : undefined),
63
64
  hasRepo: options.hasRepo ?? (kind === 'service' ? existsSync(join(domainDirectory, `${kebab}.repo.ts`)) : undefined),
64
65
  hasService: options.hasService ?? (kind === 'controller' ? existsSync(join(domainDirectory, `${kebab}.service.ts`)) : undefined)
65
66
  };
66
67
  }
68
+ function toImportSpecifier(path) {
69
+ const normalized = path.split(sep).join('/');
70
+ return normalized.startsWith('.') ? normalized : `./${normalized}`;
71
+ }
72
+ function resolveE2eRootModuleImport(domainDirectory, resolvedBase) {
73
+ return toImportSpecifier(relative(domainDirectory, join(resolvedBase, 'app.module')));
74
+ }
67
75
  function resolveDomainDirectory(kind, resolvedBase, kebab, options) {
76
+ if (kind === 'e2e') {
77
+ return basename(resolvedBase) === 'src' ? join(dirname(resolvedBase), 'test') : join(resolvedBase, 'test');
78
+ }
68
79
  if (kind === 'request-dto' && options.targetFeature !== undefined) {
69
80
  const normalizedFeature = options.targetFeature.trim();
70
81
  const featureKebab = assertValidResourceName(normalizedFeature);
@@ -154,7 +165,7 @@ export function runGenerateCommand(kind, name, baseDirectory, options = {}) {
154
165
  const generator = findGeneratorDefinition(kind);
155
166
  const resolvedBase = resolve(baseDirectory);
156
167
  const domainDirectory = resolveDomainDirectory(kind, resolvedBase, kebab, options);
157
- const generatorOptions = createGeneratorOptions(kind, domainDirectory, kebab, options);
168
+ const generatorOptions = createGeneratorOptions(kind, domainDirectory, kebab, options, resolvedBase);
158
169
  const files = generator.factory(normalizedName, generatorOptions);
159
170
  const moduleRegistration = 'moduleRegistration' in generator ? generator.moduleRegistration : undefined;
160
171
  const moduleUpdate = moduleRegistration ? prepareModuleUpdate(domainDirectory, normalizedName, kind, moduleRegistration.classSuffix, moduleRegistration.arrayKey) : undefined;
@@ -7,13 +7,22 @@ export interface GeneratedFile {
7
7
  export interface GenerateOptions {
8
8
  /** Preview planned writes and module updates without mutating the workspace. */
9
9
  dryRun?: boolean;
10
+ /** Import specifier used by generated e2e tests to load the application root module. */
11
+ e2eRootModuleImport?: string;
12
+ /** Overwrite existing generated files instead of skipping them. */
10
13
  force?: boolean;
14
+ /** Indicates that a repository sibling exists and should be imported by service templates. */
11
15
  hasRepo?: boolean;
16
+ /** Indicates that a service sibling exists and should be imported by controller templates. */
12
17
  hasService?: boolean;
13
18
  /**
14
19
  * Feature or slice directory that should receive feature-local files such as request DTOs.
15
20
  */
16
21
  targetFeature?: string;
22
+ /** Emit resource-level slice test coverage with provider override examples. */
23
+ withSliceTest?: boolean;
24
+ /** Emit module-level test coverage for schematics that support companion tests. */
25
+ withTest?: boolean;
17
26
  }
18
27
  /**
19
28
  * Produces the in-memory files for one schematic/resource pair.
@@ -1 +1 @@
1
- {"version":3,"file":"generator-types.d.ts","sourceRoot":"","sources":["../src/generator-types.ts"],"names":[],"mappings":"AAAA,sFAAsF;AACtF,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,iIAAiI;AACjI,MAAM,WAAW,eAAe;IAC9B,gFAAgF;IAChF,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;OAEG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;GAEG;AACH,MAAM,MAAM,gBAAgB,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,eAAe,KAAK,aAAa,EAAE,CAAC;AAE5F,oFAAoF;AACpF,MAAM,WAAW,qBAAqB;IACpC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,gBAAgB,CAAC;IAC1B,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,kGAAkG;AAClG,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3B,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,SAAS,GAAG,MAAM,CAAC;CAC3B"}
1
+ {"version":3,"file":"generator-types.d.ts","sourceRoot":"","sources":["../src/generator-types.ts"],"names":[],"mappings":"AAAA,sFAAsF;AACtF,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,iIAAiI;AACjI,MAAM,WAAW,eAAe;IAC9B,gFAAgF;IAChF,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,wFAAwF;IACxF,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,mEAAmE;IACnE,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,8FAA8F;IAC9F,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,8FAA8F;IAC9F,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;OAEG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,+EAA+E;IAC/E,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,mFAAmF;IACnF,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED;;GAEG;AACH,MAAM,MAAM,gBAAgB,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,eAAe,KAAK,aAAa,EAAE,CAAC;AAE5F,oFAAoF;AACpF,MAAM,WAAW,qBAAqB;IACpC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,gBAAgB,CAAC;IAC1B,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,kGAAkG;AAClG,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3B,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,SAAS,GAAG,MAAM,CAAC;CAC3B"}
@@ -0,0 +1,10 @@
1
+ import type { GenerateOptions, GeneratedFile } from '../types.js';
2
+ /**
3
+ * Generate an app-level e2e-style test file.
4
+ *
5
+ * @param name The e2e test name.
6
+ * @param options The generation options.
7
+ * @returns The generated e2e test files.
8
+ */
9
+ export declare function generateE2eFiles(name: string, options?: GenerateOptions): GeneratedFile[];
10
+ //# sourceMappingURL=e2e.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"e2e.d.ts","sourceRoot":"","sources":["../../src/generators/e2e.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAKlE;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,eAAoB,GAAG,aAAa,EAAE,CAU7F"}
@@ -0,0 +1,21 @@
1
+ import { renderTemplate } from './render.js';
2
+ import { toKebabCase } from './utils.js';
3
+
4
+ /**
5
+ * Generate an app-level e2e-style test file.
6
+ *
7
+ * @param name The e2e test name.
8
+ * @param options The generation options.
9
+ * @returns The generated e2e test files.
10
+ */
11
+ export function generateE2eFiles(name, options = {}) {
12
+ const kebab = toKebabCase(name);
13
+ const rootModuleImport = options.e2eRootModuleImport ?? '../src/app.module';
14
+ return [{
15
+ content: renderTemplate('e2e.test.ts.ejs', {
16
+ kebab,
17
+ rootModuleImport
18
+ }),
19
+ path: `${kebab}.e2e.test.ts`
20
+ }];
21
+ }
@@ -40,6 +40,16 @@ export declare const generatorOptionSchemas: readonly [{
40
40
  readonly description: "Preview planned writes, skips, and module wiring without touching files.";
41
41
  readonly name: "--dry-run";
42
42
  readonly value: "boolean";
43
+ }, {
44
+ readonly aliases: readonly [];
45
+ readonly description: "Emit a module-level slice test when generating module schematics.";
46
+ readonly name: "--with-test";
47
+ readonly value: "boolean";
48
+ }, {
49
+ readonly aliases: readonly [];
50
+ readonly description: "Emit the resource slice test with createTestingModule provider override coverage.";
51
+ readonly name: "--with-slice-test";
52
+ readonly value: "boolean";
43
53
  }, {
44
54
  readonly aliases: readonly ["-h"];
45
55
  readonly description: "Show help for the generate command.";
@@ -61,6 +71,14 @@ export declare const builtInGeneratorCollection: {
61
71
  readonly nextStepHint: "Run 'pnpm typecheck' to verify module wiring, then add route handlers.";
62
72
  readonly schematic: "controller";
63
73
  readonly wiringBehavior: "auto-registered";
74
+ }, {
75
+ readonly aliases: readonly [];
76
+ readonly description: "Generate an app-level e2e-style test with createTestApp({ rootModule }).";
77
+ readonly factory: (name: string, options: import("../generator-types.js").GenerateOptions | undefined) => import("../generator-types.js").GeneratedFile[];
78
+ readonly kind: "e2e";
79
+ readonly nextStepHint: "Run 'pnpm test:e2e' after wiring the route into AppModule, or update the generated path expectation first.";
80
+ readonly schematic: "e2e";
81
+ readonly wiringBehavior: "files-only";
64
82
  }, {
65
83
  readonly aliases: readonly ["gu"];
66
84
  readonly description: "Generate a guard (auto-registered as a provider in the module).";
@@ -99,8 +117,8 @@ export declare const builtInGeneratorCollection: {
99
117
  readonly wiringBehavior: "auto-registered";
100
118
  }, {
101
119
  readonly aliases: readonly ["mo"];
102
- readonly description: "Generate a standalone module (import it in a parent module to activate).";
103
- readonly factory: (name: string) => import("../generator-types.js").GeneratedFile[];
120
+ readonly description: "Generate a standalone module (add --with-test for a module graph slice test).";
121
+ readonly factory: (name: string, options: import("../generator-types.js").GenerateOptions | undefined) => import("../generator-types.js").GeneratedFile[];
104
122
  readonly kind: "module";
105
123
  readonly nextStepHint: "Import the new module in a parent module's imports array, then run 'pnpm typecheck'.";
106
124
  readonly schematic: "module";
@@ -128,7 +146,7 @@ export declare const builtInGeneratorCollection: {
128
146
  readonly wiringBehavior: "files-only";
129
147
  }, {
130
148
  readonly aliases: readonly ["resrc"];
131
- readonly description: "Generate a full resource slice with module, controller, service, repository, and DTO stubs.";
149
+ readonly description: "Generate a full resource slice with module, controller, service, repository, DTO stubs, and optional --with-slice-test.";
132
150
  readonly factory: (name: string, options: import("../generator-types.js").GenerateOptions | undefined) => import("../generator-types.js").GeneratedFile[];
133
151
  readonly kind: "resource";
134
152
  readonly nextStepHint: "Run 'pnpm typecheck' and wire the resource module into a parent module when ready.";
@@ -173,6 +191,14 @@ export declare const generatorCollections: readonly [{
173
191
  readonly nextStepHint: "Run 'pnpm typecheck' to verify module wiring, then add route handlers.";
174
192
  readonly schematic: "controller";
175
193
  readonly wiringBehavior: "auto-registered";
194
+ }, {
195
+ readonly aliases: readonly [];
196
+ readonly description: "Generate an app-level e2e-style test with createTestApp({ rootModule }).";
197
+ readonly factory: (name: string, options: import("../generator-types.js").GenerateOptions | undefined) => import("../generator-types.js").GeneratedFile[];
198
+ readonly kind: "e2e";
199
+ readonly nextStepHint: "Run 'pnpm test:e2e' after wiring the route into AppModule, or update the generated path expectation first.";
200
+ readonly schematic: "e2e";
201
+ readonly wiringBehavior: "files-only";
176
202
  }, {
177
203
  readonly aliases: readonly ["gu"];
178
204
  readonly description: "Generate a guard (auto-registered as a provider in the module).";
@@ -211,8 +237,8 @@ export declare const generatorCollections: readonly [{
211
237
  readonly wiringBehavior: "auto-registered";
212
238
  }, {
213
239
  readonly aliases: readonly ["mo"];
214
- readonly description: "Generate a standalone module (import it in a parent module to activate).";
215
- readonly factory: (name: string) => import("../generator-types.js").GeneratedFile[];
240
+ readonly description: "Generate a standalone module (add --with-test for a module graph slice test).";
241
+ readonly factory: (name: string, options: import("../generator-types.js").GenerateOptions | undefined) => import("../generator-types.js").GeneratedFile[];
216
242
  readonly kind: "module";
217
243
  readonly nextStepHint: "Import the new module in a parent module's imports array, then run 'pnpm typecheck'.";
218
244
  readonly schematic: "module";
@@ -240,7 +266,7 @@ export declare const generatorCollections: readonly [{
240
266
  readonly wiringBehavior: "files-only";
241
267
  }, {
242
268
  readonly aliases: readonly ["resrc"];
243
- readonly description: "Generate a full resource slice with module, controller, service, repository, and DTO stubs.";
269
+ readonly description: "Generate a full resource slice with module, controller, service, repository, DTO stubs, and optional --with-slice-test.";
244
270
  readonly factory: (name: string, options: import("../generator-types.js").GenerateOptions | undefined) => import("../generator-types.js").GeneratedFile[];
245
271
  readonly kind: "resource";
246
272
  readonly nextStepHint: "Run 'pnpm typecheck' and wire the resource module into a parent module when ready.";
@@ -283,6 +309,14 @@ export declare const generatorManifest: readonly [{
283
309
  readonly nextStepHint: "Run 'pnpm typecheck' to verify module wiring, then add route handlers.";
284
310
  readonly schematic: "controller";
285
311
  readonly wiringBehavior: "auto-registered";
312
+ }, {
313
+ readonly aliases: readonly [];
314
+ readonly description: "Generate an app-level e2e-style test with createTestApp({ rootModule }).";
315
+ readonly factory: (name: string, options: import("../generator-types.js").GenerateOptions | undefined) => import("../generator-types.js").GeneratedFile[];
316
+ readonly kind: "e2e";
317
+ readonly nextStepHint: "Run 'pnpm test:e2e' after wiring the route into AppModule, or update the generated path expectation first.";
318
+ readonly schematic: "e2e";
319
+ readonly wiringBehavior: "files-only";
286
320
  }, {
287
321
  readonly aliases: readonly ["gu"];
288
322
  readonly description: "Generate a guard (auto-registered as a provider in the module).";
@@ -321,8 +355,8 @@ export declare const generatorManifest: readonly [{
321
355
  readonly wiringBehavior: "auto-registered";
322
356
  }, {
323
357
  readonly aliases: readonly ["mo"];
324
- readonly description: "Generate a standalone module (import it in a parent module to activate).";
325
- readonly factory: (name: string) => import("../generator-types.js").GeneratedFile[];
358
+ readonly description: "Generate a standalone module (add --with-test for a module graph slice test).";
359
+ readonly factory: (name: string, options: import("../generator-types.js").GenerateOptions | undefined) => import("../generator-types.js").GeneratedFile[];
326
360
  readonly kind: "module";
327
361
  readonly nextStepHint: "Import the new module in a parent module's imports array, then run 'pnpm typecheck'.";
328
362
  readonly schematic: "module";
@@ -350,7 +384,7 @@ export declare const generatorManifest: readonly [{
350
384
  readonly wiringBehavior: "files-only";
351
385
  }, {
352
386
  readonly aliases: readonly ["resrc"];
353
- readonly description: "Generate a full resource slice with module, controller, service, repository, and DTO stubs.";
387
+ readonly description: "Generate a full resource slice with module, controller, service, repository, DTO stubs, and optional --with-slice-test.";
354
388
  readonly factory: (name: string, options: import("../generator-types.js").GenerateOptions | undefined) => import("../generator-types.js").GeneratedFile[];
355
389
  readonly kind: "resource";
356
390
  readonly nextStepHint: "Run 'pnpm typecheck' and wire the resource module into a parent module when ready.";
@@ -1 +1 @@
1
- {"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../../src/generators/manifest.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAyB,MAAM,uBAAuB,CAAC;AAarF,yFAAyF;AACzF,MAAM,MAAM,cAAc,GAAG,aAAa,GAAG,WAAW,GAAG,YAAY,CAAC;AAExE,KAAK,4BAA4B,GAAG;IAClC,QAAQ,EAAE,cAAc,CAAC;IACzB,WAAW,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,8EAA8E;AAC9E,MAAM,MAAM,sBAAsB,GAAG;IACnC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3B,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,gBAAgB,CAAC;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,kBAAkB,CAAC,EAAE,4BAA4B,CAAC;IAClD,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,iBAAiB,GAAG,YAAY,CAAC;CAClD,CAAC;AAEF,4FAA4F;AAC5F,MAAM,MAAM,mBAAmB,GAAG;IAChC,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,SAAS,sBAAsB,EAAE,CAAC;IAC9C,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,UAAU,CAAC;CACpB,CAAC;AAEF,wFAAwF;AACxF,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;EAKkB,CAAC;AAsGtD,6EAA6E;AAC7E,eAAO,MAAM,0BAA0B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAKC,CAAC;AAEzC,2FAA2F;AAC3F,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAAiF,CAAC;AAEnH,2FAA2F;AAC3F,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAAwC,CAAC;AAEvE,kFAAkF;AAClF,MAAM,MAAM,aAAa,GAAG,CAAC,OAAO,iBAAiB,CAAC,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,CAAC;AAEvE,uEAAuE;AACvE,MAAM,MAAM,mBAAmB,GAAG,CAAC,OAAO,iBAAiB,CAAC,CAAC,MAAM,CAAC,CAAC;AAcrE;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,aAAa,GAAG,mBAAmB,CAOhF;AAED;;;;GAIG;AACH,wBAAgB,wBAAwB,IAAI,SAAS,mBAAmB,EAAE,CAEzE;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,YAAY,GAAE,MAAsC,GAAG,SAAS,mBAAmB,EAAE,CAO7H;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,aAAa,GAAG,SAAS,CAMzF"}
1
+ {"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../../src/generators/manifest.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAyB,MAAM,uBAAuB,CAAC;AAcrF,yFAAyF;AACzF,MAAM,MAAM,cAAc,GAAG,aAAa,GAAG,WAAW,GAAG,YAAY,CAAC;AAExE,KAAK,4BAA4B,GAAG;IAClC,QAAQ,EAAE,cAAc,CAAC;IACzB,WAAW,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,8EAA8E;AAC9E,MAAM,MAAM,sBAAsB,GAAG;IACnC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3B,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,gBAAgB,CAAC;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,kBAAkB,CAAC,EAAE,4BAA4B,CAAC;IAClD,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,iBAAiB,GAAG,YAAY,CAAC;CAClD,CAAC;AAEF,4FAA4F;AAC5F,MAAM,MAAM,mBAAmB,GAAG;IAChC,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,SAAS,sBAAsB,EAAE,CAAC;IAC9C,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,UAAU,CAAC;CACpB,CAAC;AAEF,wFAAwF;AACxF,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAOkB,CAAC;AA+GtD,6EAA6E;AAC7E,eAAO,MAAM,0BAA0B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAKC,CAAC;AAEzC,2FAA2F;AAC3F,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAAiF,CAAC;AAEnH,2FAA2F;AAC3F,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAAwC,CAAC;AAEvE,kFAAkF;AAClF,MAAM,MAAM,aAAa,GAAG,CAAC,OAAO,iBAAiB,CAAC,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,CAAC;AAEvE,uEAAuE;AACvE,MAAM,MAAM,mBAAmB,GAAG,CAAC,OAAO,iBAAiB,CAAC,CAAC,MAAM,CAAC,CAAC;AAcrE;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,aAAa,GAAG,mBAAmB,CAOhF;AAED;;;;GAIG;AACH,wBAAgB,wBAAwB,IAAI,SAAS,mBAAmB,EAAE,CAEzE;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,YAAY,GAAE,MAAsC,GAAG,SAAS,mBAAmB,EAAE,CAO7H;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,aAAa,GAAG,SAAS,CAMzF"}
@@ -1,4 +1,5 @@
1
1
  import { generateControllerFiles } from './controller.js';
2
+ import { generateE2eFiles } from './e2e.js';
2
3
  import { generateGuardFiles } from './guard.js';
3
4
  import { generateInterceptorFiles } from './interceptor.js';
4
5
  import { generateMiddlewareFiles } from './middleware.js';
@@ -31,6 +32,16 @@ export const generatorOptionSchemas = [{
31
32
  description: 'Preview planned writes, skips, and module wiring without touching files.',
32
33
  name: '--dry-run',
33
34
  value: 'boolean'
35
+ }, {
36
+ aliases: [],
37
+ description: 'Emit a module-level slice test when generating module schematics.',
38
+ name: '--with-test',
39
+ value: 'boolean'
40
+ }, {
41
+ aliases: [],
42
+ description: 'Emit the resource slice test with createTestingModule provider override coverage.',
43
+ name: '--with-slice-test',
44
+ value: 'boolean'
34
45
  }, {
35
46
  aliases: ['-h'],
36
47
  description: 'Show help for the generate command.',
@@ -49,6 +60,14 @@ const builtInGeneratorDefinitions = [{
49
60
  nextStepHint: "Run 'pnpm typecheck' to verify module wiring, then add route handlers.",
50
61
  schematic: 'controller',
51
62
  wiringBehavior: 'auto-registered'
63
+ }, {
64
+ aliases: [],
65
+ description: 'Generate an app-level e2e-style test with createTestApp({ rootModule }).',
66
+ factory: (name, options) => generateE2eFiles(name, options),
67
+ kind: 'e2e',
68
+ nextStepHint: "Run 'pnpm test:e2e' after wiring the route into AppModule, or update the generated path expectation first.",
69
+ schematic: 'e2e',
70
+ wiringBehavior: 'files-only'
52
71
  }, {
53
72
  aliases: ['gu'],
54
73
  description: 'Generate a guard (auto-registered as a provider in the module).',
@@ -87,8 +106,8 @@ const builtInGeneratorDefinitions = [{
87
106
  wiringBehavior: 'auto-registered'
88
107
  }, {
89
108
  aliases: ['mo'],
90
- description: 'Generate a standalone module (import it in a parent module to activate).',
91
- factory: name => generateModuleFiles(name),
109
+ description: 'Generate a standalone module (add --with-test for a module graph slice test).',
110
+ factory: (name, options) => generateModuleFiles(name, options),
92
111
  kind: 'module',
93
112
  nextStepHint: "Import the new module in a parent module's imports array, then run 'pnpm typecheck'.",
94
113
  schematic: 'module',
@@ -116,7 +135,7 @@ const builtInGeneratorDefinitions = [{
116
135
  wiringBehavior: 'files-only'
117
136
  }, {
118
137
  aliases: ['resrc'],
119
- description: 'Generate a full resource slice with module, controller, service, repository, and DTO stubs.',
138
+ description: 'Generate a full resource slice with module, controller, service, repository, DTO stubs, and optional --with-slice-test.',
120
139
  factory: (name, options) => generateResourceFiles(name, options),
121
140
  kind: 'resource',
122
141
  nextStepHint: "Run 'pnpm typecheck' and wire the resource module into a parent module when ready.",
@@ -1,4 +1,4 @@
1
- import type { GeneratedFile } from '../types.js';
1
+ import type { GenerateOptions, GeneratedFile } from '../types.js';
2
2
  import type { ModuleArrayKey } from './manifest.js';
3
3
  /**
4
4
  * Ensure module import.
@@ -15,7 +15,7 @@ export declare function ensureModuleImport(source: string, className: string, im
15
15
  * @param name The name.
16
16
  * @returns The generate module files result.
17
17
  */
18
- export declare function generateModuleFiles(name: string): GeneratedFile[];
18
+ export declare function generateModuleFiles(name: string, options?: GenerateOptions): GeneratedFile[];
19
19
  /**
20
20
  * Register in module.
21
21
  *
@@ -1 +1 @@
1
- {"version":3,"file":"module.d.ts","sourceRoot":"","sources":["../../src/generators/module.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAGjD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AA6FpD;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAyDhG;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,aAAa,EAAE,CAUjE;AAwDD;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAEpG"}
1
+ {"version":3,"file":"module.d.ts","sourceRoot":"","sources":["../../src/generators/module.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAGlE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AA6FpD;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAyDhG;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,eAAoB,GAAG,aAAa,EAAE,CAmBhG;AAwDD;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAEpG"}
@@ -112,16 +112,26 @@ export function ensureModuleImport(source, className, importPath) {
112
112
  * @param name The name.
113
113
  * @returns The generate module files result.
114
114
  */
115
- export function generateModuleFiles(name) {
115
+ export function generateModuleFiles(name, options = {}) {
116
116
  const kebab = toKebabCase(name);
117
117
  const pascal = `${toPascalCase(name)}Module`;
118
- return [{
118
+ const files = [{
119
119
  content: renderTemplate('module.ts.ejs', {
120
120
  kebab,
121
121
  pascal
122
122
  }),
123
123
  path: `${kebab}.module.ts`
124
124
  }];
125
+ if (options.withTest) {
126
+ files.push({
127
+ content: renderTemplate('module.slice.test.ts.ejs', {
128
+ kebab,
129
+ pascal
130
+ }),
131
+ path: `${kebab}.slice.test.ts`
132
+ });
133
+ }
134
+ return files;
125
135
  }
126
136
  function insertIntoModuleArray(source, arrayKey, className) {
127
137
  const sourceFile = parseSource(source);
@@ -1 +1 @@
1
- {"version":3,"file":"resource.d.ts","sourceRoot":"","sources":["../../src/generators/resource.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AASlE;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,eAAoB,GAAG,aAAa,EAAE,CASlG"}
1
+ {"version":3,"file":"resource.d.ts","sourceRoot":"","sources":["../../src/generators/resource.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAUlE;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,eAAoB,GAAG,aAAa,EAAE,CA2BlG"}
@@ -1,9 +1,10 @@
1
1
  import { generateControllerFiles } from './controller.js';
2
- import { generateModuleFiles } from './module.js';
3
2
  import { generateRepoFiles } from './repository.js';
4
3
  import { generateRequestDtoFiles } from './request-dto.js';
5
4
  import { generateResponseDtoFiles } from './response-dto.js';
5
+ import { renderTemplate } from './render.js';
6
6
  import { generateServiceFiles } from './service.js';
7
+ import { toKebabCase, toPascalCase } from './utils.js';
7
8
 
8
9
  /**
9
10
  * Generate a complete feature resource slice.
@@ -13,11 +14,38 @@ import { generateServiceFiles } from './service.js';
13
14
  * @returns The generated resource files.
14
15
  */
15
16
  export function generateResourceFiles(name, options = {}) {
16
- return [...generateModuleFiles(name), ...generateRepoFiles(name, options), ...generateServiceFiles(name, {
17
+ const kebab = toKebabCase(name);
18
+ const resource = toPascalCase(name);
19
+ const module = `${resource}Module`;
20
+ const controller = `${resource}Controller`;
21
+ const repo = `${resource}Repo`;
22
+ const service = `${resource}Service`;
23
+ const files = [{
24
+ content: renderTemplate('resource.module.ts.ejs', {
25
+ controller,
26
+ kebab,
27
+ module,
28
+ repo,
29
+ service
30
+ }),
31
+ path: `${kebab}.module.ts`
32
+ }, ...generateRepoFiles(name, options), ...generateServiceFiles(name, {
17
33
  ...options,
18
34
  hasRepo: true
19
35
  }), ...generateControllerFiles(name, {
20
36
  ...options,
21
37
  hasService: true
22
38
  }), ...generateRequestDtoFiles(`Create ${name}`), ...generateResponseDtoFiles(name)];
39
+ if (options.withSliceTest) {
40
+ files.push({
41
+ content: renderTemplate('resource.slice.test.ts.ejs', {
42
+ kebab,
43
+ repo,
44
+ resource,
45
+ service
46
+ }),
47
+ path: `${kebab}.slice.test.ts`
48
+ });
49
+ }
50
+ return files;
23
51
  }
@@ -0,0 +1,19 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { createTestApp } from '@fluojs/testing';
4
+
5
+ import { AppModule } from '<%- rootModuleImport %>';
6
+
7
+ describe('<%- kebab %> e2e', () => {
8
+ it('dispatches through the app request pipeline', async () => {
9
+ const app = await createTestApp({ rootModule: AppModule });
10
+
11
+ try {
12
+ const response = await app.request('GET', '/<%- kebab %>').send();
13
+
14
+ expect(response.status).toBe(200);
15
+ } finally {
16
+ await app.close();
17
+ }
18
+ });
19
+ });
@@ -0,0 +1,13 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { createTestingModule } from '@fluojs/testing';
4
+
5
+ import { <%- pascal %> } from './<%- kebab %>.module';
6
+
7
+ describe('<%- pascal %> slice', () => {
8
+ it('compiles the authored module graph', async () => {
9
+ const testingModule = await createTestingModule({ rootModule: <%- pascal %> }).compile();
10
+
11
+ expect(testingModule.rootModule).toBe(<%- pascal %>);
12
+ });
13
+ });
@@ -0,0 +1,13 @@
1
+ import { Module } from '@fluojs/core';
2
+
3
+ import { <%- controller %> } from './<%- kebab %>.controller';
4
+ import { <%- repo %> } from './<%- kebab %>.repo';
5
+ import { <%- service %> } from './<%- kebab %>.service';
6
+
7
+ @Module({
8
+ controllers: [<%- controller %>],
9
+ providers: [<%- repo %>, <%- service %>],
10
+ })
11
+ class <%- module %> {}
12
+
13
+ export { <%- module %> };
@@ -0,0 +1,21 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { createTestingModule } from '@fluojs/testing';
4
+
5
+ import { <%- resource %>Module } from './<%- kebab %>.module';
6
+ import { <%- repo %> } from './<%- kebab %>.repo';
7
+ import { <%- service %> } from './<%- kebab %>.service';
8
+
9
+ describe('<%- resource %>Module slice', () => {
10
+ it('compiles provider wiring with an explicit override', async () => {
11
+ const testingModule = await createTestingModule({ rootModule: <%- resource %>Module })
12
+ .overrideProvider(<%- repo %>, {
13
+ list<%- resource %>s: async () => [{ id: '<%- kebab %>-1' }],
14
+ })
15
+ .compile();
16
+
17
+ const service = await testingModule.resolve<<%- service %>>(<%- service %>);
18
+
19
+ await expect(service.list<%- resource %>s()).resolves.toEqual([{ id: '<%- kebab %>-1' }]);
20
+ });
21
+ });
@@ -1 +1 @@
1
- {"version":3,"file":"scaffold.d.ts","sourceRoot":"","sources":["../../src/new/scaffold.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,gBAAgB,EAAkB,MAAM,YAAY,CAAC;AAkoEnE;;;;;;GAMG;AACH,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,gBAAgB,EACzB,aAAa,SAAkB,GAC9B,OAAO,CAAC,IAAI,CAAC,CA6Bf;AAED;;GAEG;AACH,eAAO,MAAM,eAAe,6BAAuB,CAAC"}
1
+ {"version":3,"file":"scaffold.d.ts","sourceRoot":"","sources":["../../src/new/scaffold.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,gBAAgB,EAAkB,MAAM,YAAY,CAAC;AA+qEnE;;;;;;GAMG;AACH,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,gBAAgB,EACzB,aAAa,SAAkB,GAC9B,OAAO,CAAC,IAAI,CAAC,CA6Bf;AAED;;GAEG;AACH,eAAO,MAAM,eAAe,6BAAuB,CAAC"}
@@ -11,6 +11,7 @@ const PUBLISHED_DEV_DEPENDENCIES = {
11
11
  '@babel/preset-typescript': '^7.27.1',
12
12
  '@types/babel__core': '^7.20.5',
13
13
  '@types/node': '^22.13.10',
14
+ '@vitest/coverage-v8': '^3.0.8',
14
15
  tsx: '^4.20.4',
15
16
  typescript: '^6.0.2',
16
17
  vite: '^6.2.1',
@@ -27,22 +28,22 @@ const PUBLISHED_RUNTIME_DEPENDENCIES = {
27
28
  nats: '^2.29.3'
28
29
  };
29
30
  const PUBLISHED_INTERNAL_DEPENDENCIES = {
30
- '@fluojs/cli': '^1.0.0-beta.3',
31
- '@fluojs/config': '^1.0.0-beta.5',
32
- '@fluojs/core': '^1.0.0-beta.3',
31
+ '@fluojs/cli': '^1.0.0-beta.7',
32
+ '@fluojs/config': '^1.0.0-beta.7',
33
+ '@fluojs/core': '^1.0.0-beta.4',
33
34
  '@fluojs/di': '^1.0.0-beta.6',
34
- '@fluojs/http': '^1.0.0-beta.9',
35
- '@fluojs/microservices': '^1.0.0-beta.3',
35
+ '@fluojs/http': '^1.0.0-beta.10',
36
+ '@fluojs/microservices': '^1.0.0-beta.5',
36
37
  '@fluojs/platform-bun': '^1.0.0-beta.6',
37
38
  '@fluojs/platform-cloudflare-workers': '^1.0.0-beta.2',
38
39
  '@fluojs/platform-deno': '^1.0.0-beta.3',
39
40
  '@fluojs/platform-express': '^1.0.0-beta.6',
40
41
  '@fluojs/platform-fastify': '^1.0.0-beta.8',
41
42
  '@fluojs/platform-nodejs': '^1.0.0-beta.4',
42
- '@fluojs/runtime': '^1.0.0-beta.9',
43
+ '@fluojs/runtime': '^1.0.0-beta.11',
43
44
  '@fluojs/testing': '^1.0.0-beta.2',
44
- '@fluojs/validation': '^1.0.0-beta.2',
45
- '@fluojs/vite': '^1.0.0-beta.1'
45
+ '@fluojs/validation': '^1.0.0-beta.3',
46
+ '@fluojs/vite': '^1.0.0-beta.2'
46
47
  };
47
48
  function describeApplicationStarter(options) {
48
49
  if (options.runtime === 'bun') {
@@ -152,6 +153,8 @@ function createProjectScripts(bootstrapPlan) {
152
153
  dev: 'fluo dev',
153
154
  start: 'bun dist/main.js',
154
155
  test: 'vitest run',
156
+ 'test:cov': 'vitest run --coverage',
157
+ 'test:e2e': 'vitest run test/**/*.e2e.test.ts',
155
158
  'test:watch': 'vitest',
156
159
  typecheck: 'tsc -p tsconfig.json --noEmit'
157
160
  };
@@ -171,6 +174,8 @@ function createProjectScripts(bootstrapPlan) {
171
174
  dev: 'fluo dev',
172
175
  preview: 'wrangler dev --remote --show-interactive-dev-session=false',
173
176
  test: 'vitest run',
177
+ 'test:cov': 'vitest run --coverage',
178
+ 'test:e2e': 'vitest run test/**/*.e2e.test.ts',
174
179
  'test:watch': 'vitest',
175
180
  typecheck: 'tsc -p tsconfig.json --noEmit'
176
181
  };
@@ -180,6 +185,10 @@ function createProjectScripts(bootstrapPlan) {
180
185
  dev: 'fluo dev',
181
186
  start: 'fluo start',
182
187
  test: 'vitest run',
188
+ ...(bootstrapPlan.profile.emitter.type === 'http' ? {
189
+ 'test:cov': 'vitest run --coverage',
190
+ 'test:e2e': 'vitest run test/**/*.e2e.test.ts'
191
+ } : {}),
183
192
  'test:watch': 'vitest',
184
193
  typecheck: 'tsc -p tsconfig.json --noEmit'
185
194
  };
@@ -325,7 +334,7 @@ export default defineConfig({
325
334
  plugins: [fluoBabelDecoratorsPlugin()],
326
335
  test: {
327
336
  environment: 'node',
328
- include: ['src/**/*.test.ts'],
337
+ include: ['src/**/*.test.ts', 'test/**/*.test.ts'],
329
338
  },
330
339
  });
331
340
  `;
@@ -371,7 +380,7 @@ function createHttpProjectReadme(options) {
371
380
  const entrypointLabel = starter.entrypoint;
372
381
  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('(... )', '(...)');
373
382
  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`;
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.`;
383
+ 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/greeting.repo.test.ts\`, \`src/greeting/greeting.service.test.ts\`, and \`src/greeting/greeting.controller.test.ts\` — unit templates for the starter-owned greeting slice.\n- \`src/greeting/greeting.slice.test.ts\` — module/slice template via \`createTestingModule\` for real DI graph confidence.\n- \`src/app.test.ts\` — integration-style dispatch template for runtime + starter routes.\n- \`test/app.e2e.test.ts\` — default HTTP/e2e-style template powered by \`createTestApp\` and \`app.request(...).send()\` from \`@fluojs/testing\`; older \`src/app.e2e.test.ts\` tests can be moved here without changing the request helper.\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, \`${createRunCommand(options.packageManager, 'test:e2e')}\` for the dedicated request-level e2e suite, and \`${createRunCommand(options.packageManager, 'test:cov')}\` when your Vitest runtime supports coverage.`;
375
384
  return `# ${options.projectName}
376
385
 
377
386
  Generated by @fluojs/cli.
@@ -695,6 +704,28 @@ describe('GreetingController', () => {
695
704
  });
696
705
  `;
697
706
  }
707
+ function createGreetingSliceTestFile(importSuffix = '') {
708
+ return `import { describe, expect, it } from 'vitest';
709
+
710
+ import { createTestingModule } from '@fluojs/testing';
711
+
712
+ import { GreetingModule } from './greeting.module${importSuffix}';
713
+ import { GreetingRepo } from './greeting.repo${importSuffix}';
714
+ import { GreetingService } from './greeting.service${importSuffix}';
715
+
716
+ describe('Greeting slice', () => {
717
+ it('resolves starter providers from the module graph', async () => {
718
+ const testingModule = await createTestingModule({ rootModule: GreetingModule }).compile();
719
+
720
+ const repo = await testingModule.resolve(GreetingRepo);
721
+ const service = await testingModule.resolve(GreetingService);
722
+
723
+ expect(repo.findGreeting()).toEqual({ message: 'Hello from fluo', framework: 'fluo', project: expect.any(String) });
724
+ expect(service.getGreeting()).toEqual({ message: 'Hello from fluo', framework: 'fluo', project: expect.any(String) });
725
+ });
726
+ });
727
+ `;
728
+ }
698
729
  function createGreetingModuleFile(importSuffix = '') {
699
730
  return `import { Module } from '@fluojs/core';
700
731
 
@@ -741,6 +772,12 @@ export default {
741
772
  `;
742
773
  }
743
774
  const portExpression = options.runtime === 'bun' ? "Bun.env.PORT ?? '3000'" : "process.env.PORT ?? '3000'";
775
+ const loggerGuidance = options.runtime === 'node' ? `
776
+ // Application logging defaults to the pretty console logger when logger is omitted.
777
+ // JSON logs are opt-in:
778
+ // import { createJsonApplicationLogger } from '@fluojs/runtime/node';
779
+ // Then pass \`logger: createJsonApplicationLogger()\` to FluoFactory.create(...).
780
+ ` : '';
744
781
  return `import { ${starter.adapterFactory} } from '${starter.packageName}';
745
782
  import { FluoFactory } from '@fluojs/runtime';
746
783
 
@@ -748,6 +785,7 @@ import { AppModule } from './app';
748
785
 
749
786
  // The generated starter wires the selected first-class fluo new application path:
750
787
  // ${starter.runtimeLabel} + ${starter.platformLabel} via ${starter.adapterFactory}(...).
788
+ ${loggerGuidance}
751
789
 
752
790
  const parsedPort = Number.parseInt(${portExpression}, 10);
753
791
  const port = Number.isFinite(parsedPort) ? parsedPort : 3000;
@@ -1685,21 +1723,21 @@ function createAppE2eTestFile(importSuffix = '') {
1685
1723
 
1686
1724
  import { createTestApp } from '@fluojs/testing';
1687
1725
 
1688
- import { AppModule } from './app${importSuffix}';
1726
+ import { AppModule } from '../src/app${importSuffix}';
1689
1727
 
1690
1728
  describe('AppModule e2e', () => {
1691
- it('serves runtime and starter routes through createTestApp', async () => {
1729
+ it('serves runtime and starter routes through createTestApp request helpers', async () => {
1692
1730
  const app = await createTestApp({ rootModule: AppModule });
1693
1731
 
1694
- await expect(app.dispatch({ method: 'GET', path: '/health' })).resolves.toMatchObject({
1732
+ await expect(app.request('GET', '/health').send()).resolves.toMatchObject({
1695
1733
  body: { status: 'ok' },
1696
1734
  status: 200,
1697
1735
  });
1698
- await expect(app.dispatch({ method: 'GET', path: '/ready' })).resolves.toMatchObject({
1736
+ await expect(app.request('GET', '/ready').send()).resolves.toMatchObject({
1699
1737
  body: { status: 'ready' },
1700
1738
  status: 200,
1701
1739
  });
1702
- await expect(app.dispatch({ method: 'GET', path: '/greeting/' })).resolves.toMatchObject({
1740
+ await expect(app.request('GET', '/greeting/').send()).resolves.toMatchObject({
1703
1741
  body: { message: 'Hello from fluo', framework: 'fluo', project: expect.any(String) },
1704
1742
  status: 200,
1705
1743
  });
@@ -1932,12 +1970,15 @@ function emitApplicationScaffoldFiles(options) {
1932
1970
  }, {
1933
1971
  content: createGreetingControllerTestFile(),
1934
1972
  path: 'src/greeting/greeting.controller.test.ts'
1973
+ }, {
1974
+ content: createGreetingSliceTestFile(importSuffix),
1975
+ path: 'src/greeting/greeting.slice.test.ts'
1935
1976
  }, {
1936
1977
  content: createAppTestFile(importSuffix),
1937
1978
  path: 'src/app.test.ts'
1938
1979
  }, {
1939
1980
  content: createAppE2eTestFile(importSuffix),
1940
- path: 'src/app.e2e.test.ts'
1981
+ path: 'test/app.e2e.test.ts'
1941
1982
  });
1942
1983
  return files;
1943
1984
  }
@@ -2010,6 +2051,9 @@ function emitScaffoldFilesForRecipe(options, recipeId) {
2010
2051
  }, {
2011
2052
  content: createGreetingModuleFile(),
2012
2053
  path: 'src/greeting/greeting.module.ts'
2054
+ }, {
2055
+ content: createGreetingSliceTestFile(),
2056
+ path: 'src/greeting/greeting.slice.test.ts'
2013
2057
  }, {
2014
2058
  content: createMathHandlerFile({
2015
2059
  transport: 'tcp'
package/package.json CHANGED
@@ -9,7 +9,7 @@
9
9
  "migration",
10
10
  "diagnostics"
11
11
  ],
12
- "version": "1.0.0-beta.6",
12
+ "version": "1.0.0-beta.8",
13
13
  "private": false,
14
14
  "license": "MIT",
15
15
  "repository": {
@@ -44,10 +44,10 @@
44
44
  "ejs": "^3.1.10",
45
45
  "tsx": "^4.20.4",
46
46
  "typescript": "^6.0.2",
47
- "@fluojs/runtime": "^1.0.0-beta.11"
47
+ "@fluojs/runtime": "^1.0.0-beta.12"
48
48
  },
49
49
  "peerDependencies": {
50
- "@fluojs/studio": "^1.0.0-beta.3"
50
+ "@fluojs/studio": "^1.0.0-beta.4"
51
51
  },
52
52
  "peerDependenciesMeta": {
53
53
  "@fluojs/studio": {