@fluojs/cli 1.0.1 → 1.0.3

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
@@ -48,7 +48,7 @@ fluo -v
48
48
 
49
49
  `fluo`가 interactive TTY에서 실행되면 로컬 캐시를 사용해 공개 npm `latest` dist-tag의 `@fluojs/cli` 버전을 확인하므로 매 invocation마다 registry를 호출하지 않습니다. 더 새로운 버전이 있으면 CLI가 설치 여부를 묻습니다. 거절하면 현재 설치된 버전으로 기존 명령을 계속 실행하고, 승인하면 현재 설치를 소유한 것으로 보이는 package manager의 전역 업데이트 명령(`npm install -g`, `pnpm add -g`, `bun add -g`, `yarn global add`)을 사용한 뒤 같은 인자로 업데이트된 `fluo` 바이너리를 다시 시작합니다. 설치 도구를 추론할 수 없으면 Node.js 기본 전역 설치 경로를 소유하는 npm 기준으로 `npm install -g @fluojs/cli@<latest>`를 fallback으로 사용합니다.
50
50
 
51
- `fluo new`와 alias인 `fluo create`는 일반 update-check cache가 아직 fresh하더라도 스캐폴딩 전에 interactive 최신 버전 확인을 새로 시도합니다. 이를 통해 첫 프로젝트 생성 경로가 방금 배포된 starter 동작과 더 잘 맞춰지며, `fluo dev`, `fluo build`, `fluo generate`, `fluo inspect` 같은 일상 명령은 기존처럼 일반 TTL이 만료될 때까지 cached latest-version 결과를 재사용합니다.
51
+ `fluo new`와 alias인 `fluo create`는 일반 update-check cache가 아직 fresh하더라도 스캐폴딩 전에 interactive 최신 버전 확인을 새로 시도합니다. 이를 통해 첫 프로젝트 생성 경로가 방금 배포된 starter 동작과 더 잘 맞춰지며, `fluo dev`, `fluo build`, `fluo generate`, `fluo inspect` 같은 일상 명령은 기존처럼 일반 TTL이 만료될 때까지 cached latest-version 결과를 재사용합니다. 순수 help 및 version inspection 경로(`fluo help <command>`, `<command> --help`, `fluo version`, `fluo --version`, `fluo -v`)는 interactive update check를 실행하지 않고 즉시 출력합니다.
52
52
 
53
53
  업데이트 확인은 CI, non-TTY 출력, npm-script context, 업데이트 후 재실행 context, registry/network 실패, 명시적 opt-out 경로에서는 건너뜁니다. 한 번만 끄려면 `--no-update-check`(또는 compatibility alias `--no-update-notifier`)를 사용하고, 자동화에서 절대 prompt가 뜨면 안 되는 경우에는 `FLUO_NO_UPDATE_CHECK=1`을 설정하세요.
54
54
 
@@ -112,7 +112,7 @@ fluo new my-grpc-service --shape microservice --transport grpc --runtime node --
112
112
 
113
113
  지원되는 `--shape microservice --transport` 스타터 값은 정확히 `tcp`, `redis-streams`, `nats`, `kafka`, `rabbitmq`, `mqtt`, `grpc`입니다. 유지보수되는 Redis 기반 스타터가 필요하면 `redis-streams`를 사용하고, 더 넓은 Redis 통합 패턴이 필요하면 스캐폴딩 후 `@fluojs/redis`를 수동으로 추가하세요.
114
114
 
115
- NATS/Kafka/RabbitMQ 스타터 계약은 외부 broker와 caller-owned client library 의존성을 숨기지 않고 명시적으로 유지합니다. 생성된 프로젝트는 `src/app.ts`에서 `nats` + `JSONCodec()`, `kafkajs` producer/consumer collaborator, `amqplib` publisher/consumer collaborator를 직접 연결하므로, 기본 fluo 패키지가 그 의존성을 감춘 것처럼 가장하지 않는 runnable starter 계약이 됩니다.
115
+ NATS/Kafka/RabbitMQ 스타터 계약은 외부 broker와 caller-owned client library 의존성을 숨기지 않고 명시적으로 유지합니다. 생성된 프로젝트는 `src/app.ts`에서 `nats` + `JSONCodec()`, `kafkajs` producer/consumer collaborator, `amqplib` publisher/consumer collaborator를 직접 연결하므로, 기본 fluo 패키지가 그 의존성을 감춘 것처럼 가장하지 않는 runnable starter 계약이 됩니다. 해당 broker client는 생성된 transport wrapper가 Fluo lifecycle에서 listen/send/emit을 시작할 때 lazy하게 생성합니다. 따라서 `fluo inspect`, 테스트, static tooling이 `src/app.ts`를 import하는 것만으로는 broker에 연결하거나 lifecycle이 teardown을 소유하기 전에 외부 리소스를 열지 않습니다.
116
116
 
117
117
  starter 매트릭스에는 mixed single-package starter도 포함됩니다. 하나의 Fastify HTTP 앱과 attached TCP microservice를 같은 생성 프로젝트 안에 함께 배치합니다.
118
118
 
@@ -155,7 +155,7 @@ fluo generate service users --dry-run
155
155
 
156
156
  `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하세요.
157
157
 
158
- `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 검증에 사용하세요.
158
+ `fluo generate e2e <name>`는 generated starter와 같은 app-level test 영역에 request-pipeline test를 두도록 `createTestApp({ rootModule: AppModule })`을 사용하는 `test/<name>.e2e.test.ts`를 작성하고, 기본 starter root module인 `../src/app`에서 `AppModule`을 import합니다. 생성된 unit test는 직접 class 동작 검증에, slice test는 DI wiring과 override 검증에, e2e test는 virtual app을 통과하는 route, guard, interceptor, DTO validation, response writing 검증에 사용하세요.
159
159
 
160
160
  Request DTO 생성은 feature 디렉터리와 DTO 클래스 이름을 분리해서 받습니다. 따라서 `CreateUser`, `UpdateUser` 같은 여러 입력 계약을 같은 `src/users/` 슬라이스 안에 둘 수 있습니다.
161
161
 
@@ -250,7 +250,7 @@ fluo inspect ./src/app.module.ts --json > snapshot.json
250
250
  fluo inspect ./src/app.module.ts --json --output artifacts/inspect-snapshot.json
251
251
 
252
252
  # 런타임이 생산한 snapshot 옆에 bootstrap timing 포함하기
253
- fluo inspect ./src/app.module.ts --json --timing
253
+ fluo inspect ./src/app.module.ts --timing
254
254
 
255
255
  # 요약, snapshot, diagnostics, timing을 포함한 support triage report 내보내기
256
256
  fluo inspect ./src/app.module.ts --report --output artifacts/inspect-report.json
@@ -259,7 +259,7 @@ fluo inspect ./src/app.module.ts --report --output artifacts/inspect-report.json
259
259
  fluo inspect ./src/app.module.ts --export AdminModule --json
260
260
  ```
261
261
 
262
- 런타임이 inspection snapshot을 생산합니다. `fluo inspect`는 output mode flag가 없을 때 기본적으로 그 snapshot을 JSON으로 직렬화하고, `fluo inspect --mermaid`는 snapshot-to-Mermaid 렌더링을 선택적 `@fluojs/studio` 계약에 위임합니다. `--export <name>`은 bootstrap할 module export를 선택하며 기본값은 `AppModule`입니다. `--timing`은 JSON 출력에 bootstrap timing diagnostics를 기록하고, `--report`는 CI/support triage를 위해 런타임이 생산한 snapshot을 안정적인 요약과 함께 감쌉니다. `--timing`은 Mermaid 출력과 함께 사용할 수 없습니다. `--output <path>`는 선택한 inspect payload를 stdout 대신 명시적 artifact 경로에 씁니다. 이 동작은 검사 대상 애플리케이션을 writable하게 만들지 않으며, 일반 bootstrap/close cycle 외에 module graph state를 바꾸지 않습니다. Mermaid 출력이 필요하면 명령을 실행하는 프로젝트에 Studio를 설치하세요:
262
+ 런타임이 inspection snapshot을 생산합니다. `fluo inspect`는 `./src/app.ts` 또는 `./src/app.module.ts` 같은 생성된 TypeScript source module을 명시적 TypeScript loader boundary로 받아들이며, 기존 `.js`와 `.mjs` module path는 계속 Node.js native ESM으로 로드합니다. `fluo inspect`는 output mode flag가 없을 때 기본적으로 그 snapshot을 JSON으로 직렬화하고, `fluo inspect --mermaid`는 snapshot-to-Mermaid 렌더링을 선택적 `@fluojs/studio` 계약에 위임합니다. `--export <name>`은 bootstrap할 module export를 선택하며 기본값은 `AppModule`입니다. `--timing`은 명시적인 `--json` flag 없이 제공된 경우를 포함해 JSON snapshot 출력 옆에 bootstrap timing diagnostics를 기록하고, `--report`는 CI/support triage를 위해 런타임이 생산한 snapshot을 안정적인 요약과 함께 감쌉니다. `--timing`은 Mermaid 출력과 함께 사용할 수 없습니다. `--output <path>`는 선택한 inspect payload를 stdout 대신 명시적 artifact 경로에 씁니다. 이 동작은 검사 대상 애플리케이션을 writable하게 만들지 않으며, 일반 bootstrap/close cycle 외에 module graph state를 바꾸지 않습니다. Mermaid 출력이 필요하면 명령을 실행하는 프로젝트에 Studio를 설치하세요:
263
263
 
264
264
  ```bash
265
265
  pnpm add -D @fluojs/studio
@@ -274,8 +274,10 @@ Studio가 없으면 CI와 non-interactive 실행은 prompt나 package manager
274
274
  | 익스포트 | 설명 |
275
275
  |---|---|
276
276
  | `runCli(argv?, options?)` | 모든 CLI 명령을 실행하는 메인 진입점입니다. |
277
+ | `CliRuntimeOptions` | stream, cwd, environment, registry metadata, update-check hook 같은 `runCli(...)` runtime override 타입입니다. |
277
278
  | `newUsage()` | help surface와 test에서 사용하는 현재 `fluo new` usage text를 반환합니다. |
278
279
  | `runNewCommand(argv, options?)` | 프로젝트 스캐폴딩 로직에 대한 프로그래밍적 접근을 제공합니다. |
280
+ | `NewCommandRuntimeOptions` | prompt, filesystem write, dependency install, git initialization 같은 `runNewCommand(...)` runtime override 타입입니다. |
279
281
  | `CliPromptCancelledError` | 호출자가 제공한 prompt hook이 정상 취소를 알리기 위해 throw할 수 있는 안정적인 sentinel입니다. |
280
282
  | `GenerateOptions` | 프로그래밍 방식 generator 옵션 타입입니다. |
281
283
  | `GeneratedFile` | 생성된 파일 경로, 내용, write status를 설명하는 타입입니다. |
package/README.md CHANGED
@@ -48,7 +48,7 @@ fluo -v
48
48
 
49
49
  When `fluo` runs in an interactive TTY, it checks the public npm `latest` dist-tag for `@fluojs/cli` using a local cache so every invocation does not hit the registry. If a newer version is available, the CLI asks whether to install it. Declining continues the current command with the installed version; accepting updates the global CLI with the package manager that appears to own the current installation (`npm install -g`, `pnpm add -g`, `bun add -g`, or `yarn global add`) and then restarts `fluo` with the same arguments under the updated binary. If the installer cannot be inferred, the CLI falls back to `npm install -g @fluojs/cli@<latest>` because npm owns the default Node.js global installation path.
50
50
 
51
- `fluo new` and its `fluo create` alias attempt a fresh interactive latest-version check before scaffolding, even when the normal update-check cache is still fresh. This keeps first-run project creation aligned with newly published starter behavior, while day-to-day commands such as `fluo dev`, `fluo build`, `fluo generate`, and `fluo inspect` continue to reuse the cached latest-version result until the normal TTL expires.
51
+ `fluo new` and its `fluo create` alias attempt a fresh interactive latest-version check before scaffolding, even when the normal update-check cache is still fresh. This keeps first-run project creation aligned with newly published starter behavior, while day-to-day commands such as `fluo dev`, `fluo build`, `fluo generate`, and `fluo inspect` continue to reuse the cached latest-version result until the normal TTL expires. Pure help and version inspection paths (`fluo help <command>`, `<command> --help`, `fluo version`, `fluo --version`, and `fluo -v`) print immediately without running the interactive update check.
52
52
 
53
53
  The update check is skipped in CI, non-TTY output, npm-script contexts, rerun-after-update contexts, registry/network failures, and explicit opt-out paths. Use `--no-update-check` (or the compatibility alias `--no-update-notifier`) for one invocation, or set `FLUO_NO_UPDATE_CHECK=1` when automation must never prompt.
54
54
 
@@ -112,7 +112,7 @@ fluo new my-grpc-service --shape microservice --transport grpc --runtime node --
112
112
 
113
113
  Supported `--shape microservice --transport` starter values are exactly `tcp`, `redis-streams`, `nats`, `kafka`, `rabbitmq`, `mqtt`, and `grpc`. Use `redis-streams` for the maintained Redis-backed starter, or add `@fluojs/redis` manually after scaffolding when you need broader Redis integration patterns.
114
114
 
115
- The NATS/Kafka/RabbitMQ starter contracts stay explicit about external brokers and caller-owned client libraries. Generated projects wire `nats` + `JSONCodec()`, `kafkajs` producer/consumer collaborators, and `amqplib` publisher/consumer collaborators directly in `src/app.ts` so the starter contract is runnable without pretending the base fluo packages hide those dependencies.
115
+ The NATS/Kafka/RabbitMQ starter contracts stay explicit about external brokers and caller-owned client libraries. Generated projects wire `nats` + `JSONCodec()`, `kafkajs` producer/consumer collaborators, and `amqplib` publisher/consumer collaborators directly in `src/app.ts` so the starter contract is runnable without pretending the base fluo packages hide those dependencies. Those broker clients are created lazily by the generated transport wrapper when the Fluo lifecycle starts listening, sends, or emits; importing `src/app.ts` for `fluo inspect`, tests, or static tooling does not connect to a broker or open external resources before the application lifecycle owns teardown.
116
116
 
117
117
  The starter matrix also includes a mixed single-package starter: one Fastify HTTP app with an attached TCP microservice in the same generated project.
118
118
 
@@ -155,7 +155,7 @@ Auto-registered generators are `controller`, `service`, `repo`, `guard`, `interc
155
155
 
156
156
  `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.
157
157
 
158
- `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.
158
+ `fluo generate e2e <name>` writes `test/<name>.e2e.test.ts` with `createTestApp({ rootModule: AppModule })` and imports `AppModule` from the default starter root module at `../src/app`, 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.
159
159
 
160
160
  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.
161
161
 
@@ -250,7 +250,7 @@ fluo inspect ./src/app.module.ts --json > snapshot.json
250
250
  fluo inspect ./src/app.module.ts --json --output artifacts/inspect-snapshot.json
251
251
 
252
252
  # Include bootstrap timing next to the runtime-produced snapshot
253
- fluo inspect ./src/app.module.ts --json --timing
253
+ fluo inspect ./src/app.module.ts --timing
254
254
 
255
255
  # Emit a support triage report with summary, snapshot, diagnostics, and timing
256
256
  fluo inspect ./src/app.module.ts --report --output artifacts/inspect-report.json
@@ -259,7 +259,7 @@ fluo inspect ./src/app.module.ts --report --output artifacts/inspect-report.json
259
259
  fluo inspect ./src/app.module.ts --export AdminModule --json
260
260
  ```
261
261
 
262
- The runtime produces the inspection snapshot. `fluo inspect` serializes that snapshot as JSON by default when no output mode flag is provided, and `fluo inspect --mermaid` delegates snapshot-to-Mermaid rendering to the optional `@fluojs/studio` contract. `--export <name>` selects the module export to bootstrap and defaults to `AppModule`; `--timing` records bootstrap timing diagnostics for JSON output, and `--report` wraps the runtime-produced snapshot with a stable summary for CI/support triage. `--timing` cannot be combined with Mermaid output. `--output <path>` writes the selected inspect payload to an explicit artifact path instead of stdout; it does not make the inspected application writable or change module graph state beyond the normal bootstrap/close cycle. Install Studio in the project that runs the command when you need Mermaid output:
262
+ The runtime produces the inspection snapshot. `fluo inspect` accepts generated TypeScript source modules such as `./src/app.ts` or `./src/app.module.ts` through an explicit TypeScript loader boundary, while existing `.js` and `.mjs` module paths continue to load through native Node.js ESM. `fluo inspect` serializes the snapshot as JSON by default when no output mode flag is provided, and `fluo inspect --mermaid` delegates snapshot-to-Mermaid rendering to the optional `@fluojs/studio` contract. `--export <name>` selects the module export to bootstrap and defaults to `AppModule`; `--timing` records bootstrap timing diagnostics next to the JSON snapshot output, including when `--timing` is provided without an explicit `--json` flag, and `--report` wraps the runtime-produced snapshot with a stable summary for CI/support triage. `--timing` cannot be combined with Mermaid output. `--output <path>` writes the selected inspect payload to an explicit artifact path instead of stdout; it does not make the inspected application writable or change module graph state beyond the normal bootstrap/close cycle. Install Studio in the project that runs the command when you need Mermaid output:
263
263
 
264
264
  ```bash
265
265
  pnpm add -D @fluojs/studio
@@ -274,8 +274,10 @@ The package can be used programmatically to trigger CLI actions from within othe
274
274
  | Export | Description |
275
275
  |---|---|
276
276
  | `runCli(argv?, options?)` | Main entry point to execute any CLI command. |
277
+ | `CliRuntimeOptions` | Type for `runCli(...)` runtime overrides such as streams, cwd, environment, registry metadata, and update-check hooks. |
277
278
  | `newUsage()` | Returns the current `fluo new` usage text for help surfaces and tests. |
278
279
  | `runNewCommand(argv, options?)` | Programmatic access to the project scaffolding logic. |
280
+ | `NewCommandRuntimeOptions` | Type for `runNewCommand(...)` runtime overrides such as prompts, filesystem writes, dependency installation, and git initialization. |
279
281
  | `CliPromptCancelledError` | Stable sentinel that caller-supplied prompt hooks can throw to report normal cancellation. |
280
282
  | `GenerateOptions` | Type for programmatic generator options. |
281
283
  | `GeneratedFile` | Type describing generated file paths, content, and write status. |
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;AAwZD;;;;;;;;;;;;;;;;;;;;;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;AA4ZD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAsB,MAAM,CAC1B,IAAI,WAAwB,EAC5B,OAAO,GAAE,iBAAiB,GAAG,wBAAwB,GAAG,4BAAiC,GACxF,OAAO,CAAC,MAAM,CAAC,CAuNjB"}
package/dist/cli.js CHANGED
@@ -125,6 +125,9 @@ function isVersionCommand(value) {
125
125
  function isCreationCommand(value) {
126
126
  return value === 'new' || value === 'create';
127
127
  }
128
+ function isHelpInvocation(argv) {
129
+ return argv[0] === 'help' || argv.some(isHelpFlag);
130
+ }
128
131
  function readCliVersion() {
129
132
  const packageJsonPath = fileURLToPath(new URL('../package.json', import.meta.url));
130
133
  const manifest = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
@@ -387,20 +390,22 @@ export async function runCli(argv = process.argv.slice(2), runtime = {}) {
387
390
  stdout.write(`${readCliVersion()}\n`);
388
391
  return 0;
389
392
  }
390
- const updateCheckOptions = runtime.updateCheck === false ? undefined : runtime.updateCheck;
391
- const updateCheckResult = await runCliUpdateCheck(commandArgv, {
392
- ...updateCheckOptions,
393
- ci: runtime.ci,
394
- env,
395
- bypassCache: isCreationCommand(commandArgv[0]),
396
- interactive: runtime.interactive,
397
- skip: updateFlagResult.skipUpdateCheck || runtime.updateCheck === false,
398
- stderr,
399
- stdin: runtime.stdin,
400
- stdout
401
- });
402
- if (updateCheckResult.action === 'reran') {
403
- return updateCheckResult.exitCode;
393
+ if (!isHelpInvocation(commandArgv)) {
394
+ const updateCheckOptions = runtime.updateCheck === false ? undefined : runtime.updateCheck;
395
+ const updateCheckResult = await runCliUpdateCheck(commandArgv, {
396
+ ...updateCheckOptions,
397
+ ci: runtime.ci,
398
+ env,
399
+ bypassCache: isCreationCommand(commandArgv[0]),
400
+ interactive: runtime.interactive,
401
+ skip: updateFlagResult.skipUpdateCheck || runtime.updateCheck === false,
402
+ stderr,
403
+ stdin: runtime.stdin,
404
+ stdout
405
+ });
406
+ if (updateCheckResult.action === 'reran') {
407
+ return updateCheckResult.exitCode;
408
+ }
404
409
  }
405
410
  if (commandArgv.length === 0) {
406
411
  throw new Error(usage());
@@ -416,7 +421,7 @@ export async function runCli(argv = process.argv.slice(2), runtime = {}) {
416
421
  return 0;
417
422
  }
418
423
  if (topic === 'doctor' || topic === 'info') {
419
- stdout.write(`${diagnosticsUsage('doctor')}\n`);
424
+ stdout.write(`${diagnosticsUsage(topic)}\n`);
420
425
  return 0;
421
426
  }
422
427
  if (topic === 'analyze') {
@@ -455,7 +460,7 @@ export async function runCli(argv = process.argv.slice(2), runtime = {}) {
455
460
  return 0;
456
461
  }
457
462
  if ((commandArgv[0] === 'doctor' || commandArgv[0] === 'info') && commandArgv.slice(1).some(isHelpFlag)) {
458
- stdout.write(`${diagnosticsUsage('doctor')}\n`);
463
+ stdout.write(`${diagnosticsUsage(commandArgv[0])}\n`);
459
464
  return 0;
460
465
  }
461
466
  if (commandArgv[0] === 'analyze' && commandArgv.slice(1).some(isHelpFlag)) {
@@ -70,7 +70,7 @@ function toImportSpecifier(path) {
70
70
  return normalized.startsWith('.') ? normalized : `./${normalized}`;
71
71
  }
72
72
  function resolveE2eRootModuleImport(domainDirectory, resolvedBase) {
73
- return toImportSpecifier(relative(domainDirectory, join(resolvedBase, 'app.module')));
73
+ return toImportSpecifier(relative(domainDirectory, join(resolvedBase, 'app')));
74
74
  }
75
75
  function resolveDomainDirectory(kind, resolvedBase, kebab, options) {
76
76
  if (kind === 'e2e') {
@@ -1 +1 @@
1
- {"version":3,"file":"inspect.d.ts","sourceRoot":"","sources":["../../src/commands/inspect.ts"],"names":[],"mappings":"AAMA,OAAO,EAML,KAAK,qBAAqB,EAE3B,MAAM,iBAAiB,CAAC;AAKzB,KAAK,SAAS,GAAG;IACf,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC;CACjC,CAAC;AAEF,KAAK,eAAe,GAAG;IACrB,KAAK,CAAC,IAAI,IAAI,CAAC;IACf,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACnE,CAAC;AAEF,KAAK,cAAc,GAAG;IACpB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB,CAAC;AAEF,KAAK,qBAAqB,GAAG,CAAC,QAAQ,EAAE,qBAAqB,KAAK,MAAM,CAAC;AAEzE,KAAK,2BAA2B,GAAG,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,qBAAqB,GAAG,SAAS,CAAC,CAAC;AAE/F;;GAEG;AACH,MAAM,WAAW,4BAA4B;IAC3C,yEAAyE;IACzE,EAAE,CAAC,EAAE,OAAO,CAAC;IACb,uDAAuD;IACvD,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,yEAAyE;IACzE,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,yEAAyE;IACzE,yBAAyB,CAAC,EAAE,2BAA2B,CAAC;IACxD,wFAAwF;IACxF,MAAM,CAAC,EAAE,eAAe,CAAC;IACzB,sCAAsC;IACtC,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,4CAA4C;IAC5C,KAAK,CAAC,EAAE,cAAc,CAAC;IACvB,yCAAyC;IACzC,MAAM,CAAC,EAAE,SAAS,CAAC;CACpB;AAkFD;;;;GAIG;AACH,wBAAgB,YAAY,IAAI,MAAM,CAarC;AA+PD;;;;;;GAMG;AACH,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,OAAO,GAAE,4BAAiC,GAAG,OAAO,CAAC,MAAM,CAAC,CA4EnH"}
1
+ {"version":3,"file":"inspect.d.ts","sourceRoot":"","sources":["../../src/commands/inspect.ts"],"names":[],"mappings":"AAMA,OAAO,EAOL,KAAK,qBAAqB,EAC3B,MAAM,iBAAiB,CAAC;AAMzB,KAAK,SAAS,GAAG;IACf,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC;CACjC,CAAC;AAEF,KAAK,eAAe,GAAG;IACrB,KAAK,CAAC,IAAI,IAAI,CAAC;IACf,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACnE,CAAC;AAEF,KAAK,cAAc,GAAG;IACpB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB,CAAC;AAEF,KAAK,qBAAqB,GAAG,CAAC,QAAQ,EAAE,qBAAqB,KAAK,MAAM,CAAC;AAEzE,KAAK,2BAA2B,GAAG,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,qBAAqB,GAAG,SAAS,CAAC,CAAC;AAE/F;;GAEG;AACH,MAAM,WAAW,4BAA4B;IAC3C,yEAAyE;IACzE,EAAE,CAAC,EAAE,OAAO,CAAC;IACb,uDAAuD;IACvD,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,yEAAyE;IACzE,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,yEAAyE;IACzE,yBAAyB,CAAC,EAAE,2BAA2B,CAAC;IACxD,wFAAwF;IACxF,MAAM,CAAC,EAAE,eAAe,CAAC;IACzB,sCAAsC;IACtC,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,4CAA4C;IAC5C,KAAK,CAAC,EAAE,cAAc,CAAC;IACvB,yCAAyC;IACzC,MAAM,CAAC,EAAE,SAAS,CAAC;CACpB;AAmFD;;;;GAIG;AACH,wBAAgB,YAAY,IAAI,MAAM,CAarC;AA+PD;;;;;;GAMG;AACH,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,OAAO,GAAE,4BAAiC,GAAG,OAAO,CAAC,MAAM,CAAC,CAwDnH"}
@@ -1,9 +1,10 @@
1
1
  import { mkdir, writeFile } from 'node:fs/promises';
2
2
  import { createRequire } from 'node:module';
3
- import { dirname, resolve } from 'node:path';
3
+ import { dirname, extname, resolve } from 'node:path';
4
4
  import { pathToFileURL } from 'node:url';
5
5
  import * as clack from '@clack/prompts';
6
6
  import { FluoFactory, PLATFORM_SHELL } from '@fluojs/runtime';
7
+ import { tsImport } from 'tsx/esm/api';
7
8
  import { renderAliasList, renderHelpTable } from '../help.js';
8
9
  import { CliPromptCancelledError, isCliPromptCancelledError } from '../prompt-cancel.js';
9
10
 
@@ -21,7 +22,7 @@ const INSPECT_OPTION_HELP = [{
21
22
  option: '--mermaid'
22
23
  }, {
23
24
  aliases: [],
24
- description: 'Bootstrap the application context and emit versioned timing diagnostics.',
25
+ description: 'Include bootstrap timing diagnostics next to JSON inspect output.',
25
26
  option: '--timing'
26
27
  }, {
27
28
  aliases: [],
@@ -41,6 +42,7 @@ const INSPECT_OPTION_HELP = [{
41
42
  option: '--help'
42
43
  }];
43
44
  const STUDIO_CONTRACT_ENTRYPOINT = '@fluojs/studio/contracts';
45
+ const TYPESCRIPT_MODULE_EXTENSIONS = new Set(['.ts', '.tsx', '.mts', '.cts']);
44
46
  const STUDIO_MISSING_MESSAGE = ['Mermaid graph rendering is owned by @fluojs/studio, but @fluojs/studio is not resolvable from this project.', 'Install @fluojs/studio explicitly (for example: pnpm add -D @fluojs/studio) and rerun fluo inspect --mermaid.'].join('\n');
45
47
  function isHelpFlag(value) {
46
48
  return value === '--help' || value === '-h';
@@ -121,7 +123,7 @@ function parseInspectArgs(argv) {
121
123
  if (!modulePath) {
122
124
  throw new Error(inspectUsage());
123
125
  }
124
- if (!json && !mermaid && !timing && !report) {
126
+ if (!json && !mermaid && !report) {
125
127
  json = true;
126
128
  }
127
129
  const selectedModes = [json, mermaid, report].filter(Boolean).length;
@@ -147,13 +149,12 @@ function resolveRootModule(exportedValue, exportName) {
147
149
  }
148
150
  return exportedValue;
149
151
  }
150
- function stringifyTiming(timing) {
151
- const value = timing ?? {
152
- phases: [],
153
- totalMs: 0,
154
- version: 1
155
- };
156
- return JSON.stringify(value, null, 2);
152
+ async function importInspectModule(modulePath) {
153
+ const moduleUrl = pathToFileURL(modulePath).href;
154
+ if (TYPESCRIPT_MODULE_EXTENSIONS.has(extname(modulePath))) {
155
+ return tsImport(moduleUrl, import.meta.url);
156
+ }
157
+ return import(moduleUrl);
157
158
  }
158
159
  function stringifySnapshot(snapshot) {
159
160
  return JSON.stringify(snapshot, null, 2);
@@ -283,27 +284,8 @@ export async function runInspectCommand(argv, runtime = {}) {
283
284
  }
284
285
  const parsed = parseInspectArgs(argv);
285
286
  const modulePath = resolve(cwd, parsed.modulePath);
286
- const importedModule = await import(pathToFileURL(modulePath).href);
287
+ const importedModule = await importInspectModule(modulePath);
287
288
  const rootModule = resolveRootModule(importedModule[parsed.exportName], parsed.exportName);
288
- if (parsed.timing && !parsed.json && !parsed.report) {
289
- const context = await FluoFactory.createApplicationContext(rootModule, {
290
- diagnostics: {
291
- timing: true
292
- },
293
- logger: {
294
- debug() {},
295
- error() {},
296
- log() {},
297
- warn() {}
298
- }
299
- });
300
- try {
301
- await emitInspectPayload(stringifyTiming(context.bootstrapTiming), parsed, cwd, stdout);
302
- } finally {
303
- await context.close();
304
- }
305
- return 0;
306
- }
307
289
  const context = await FluoFactory.createApplicationContext(rootModule, {
308
290
  diagnostics: parsed.timing || parsed.report ? {
309
291
  timing: true
@@ -1 +1 @@
1
- {"version":3,"file":"node-restart-runner.d.ts","sourceRoot":"","sources":["../../src/dev-runner/node-restart-runner.ts"],"names":[],"mappings":"AAAA,OAAO,EAAS,KAAK,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAE9D,OAAO,EAA0D,KAAK,SAAS,EAAE,MAAM,SAAS,CAAC;AAIjG,KAAK,mBAAmB,GAAG;IACzB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC;CACjC,CAAC;AAEF,KAAK,mBAAmB,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;IAAC,KAAK,EAAE,SAAS,CAAA;CAAE,KAAK,YAAY,CAAC;AACjJ,2EAA2E;AAC3E,MAAM,MAAM,gBAAgB,GAAG,KAAK,GAAG,oBAAoB,GAAG,MAAM,GAAG,MAAM,CAAC;AAE9E,KAAK,aAAa,GAAG,QAAQ,GAAG,SAAS,CAAC;AAE1C,KAAK,mBAAmB,GAAG;IACzB,GAAG,CAAC,MAAM,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC;IAC1D,IAAI,CAAC,MAAM,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC;CAC5D,CAAC;AAEF,KAAK,qBAAqB,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,iBAAiB,EAAE;IAAE,SAAS,EAAE,OAAO,CAAA;CAAE,GAAG,CAAC,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC,EAAE,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,KAAK,IAAI,KAAK,SAAS,CAAC;AAE1O,KAAK,iBAAiB,GAAG;IACvB,cAAc,CAAC,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC;IAC9C,mBAAmB,CAAC,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC;CACvD,CAAC;AAEF,uFAAuF;AACvF,MAAM,MAAM,wBAAwB,GAAG;IACrC,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;IACvB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,OAAO,CAAC,EAAE,gBAAgB,CAAC;IAC3B,YAAY,CAAC,EAAE,mBAAmB,CAAC;IACnC,UAAU,CAAC,EAAE,mBAAmB,CAAC;IACjC,MAAM,CAAC,EAAE,mBAAmB,CAAC;IAC7B,MAAM,CAAC,EAAE,mBAAmB,CAAC;IAC7B,WAAW,CAAC,EAAE,qBAAqB,CAAC;CACrC,CAAC;AAiGF;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CAAC,gBAAgB,EAAE,MAAM,EAAE,cAAc,GAAE,MAAM,EAAoB,GAAG,iBAAiB,CAuB/H;AAiFD;;;;;GAKG;AACH,wBAAsB,oBAAoB,CAAC,OAAO,EAAE,wBAAwB,GAAG,OAAO,CAAC,MAAM,CAAC,CA+K7F"}
1
+ {"version":3,"file":"node-restart-runner.d.ts","sourceRoot":"","sources":["../../src/dev-runner/node-restart-runner.ts"],"names":[],"mappings":"AAAA,OAAO,EAAS,KAAK,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAE9D,OAAO,EAA0D,KAAK,SAAS,EAAE,MAAM,SAAS,CAAC;AAIjG,KAAK,mBAAmB,GAAG;IACzB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC;CACjC,CAAC;AAEF,KAAK,mBAAmB,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;IAAC,KAAK,EAAE,SAAS,CAAA;CAAE,KAAK,YAAY,CAAC;AACjJ,2EAA2E;AAC3E,MAAM,MAAM,gBAAgB,GAAG,KAAK,GAAG,oBAAoB,GAAG,MAAM,GAAG,MAAM,CAAC;AAE9E,KAAK,aAAa,GAAG,QAAQ,GAAG,SAAS,CAAC;AAE1C,KAAK,mBAAmB,GAAG;IACzB,GAAG,CAAC,MAAM,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC;IAC1D,IAAI,CAAC,MAAM,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC;CAC5D,CAAC;AAEF,KAAK,qBAAqB,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,iBAAiB,EAAE;IAAE,SAAS,EAAE,OAAO,CAAA;CAAE,GAAG,CAAC,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC,EAAE,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,KAAK,IAAI,KAAK,SAAS,CAAC;AAE1O,KAAK,iBAAiB,GAAG;IACvB,cAAc,CAAC,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC;IAC9C,mBAAmB,CAAC,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC;CACvD,CAAC;AAEF,uFAAuF;AACvF,MAAM,MAAM,wBAAwB,GAAG;IACrC,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;IACvB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,OAAO,CAAC,EAAE,gBAAgB,CAAC;IAC3B,YAAY,CAAC,EAAE,mBAAmB,CAAC;IACnC,UAAU,CAAC,EAAE,mBAAmB,CAAC;IACjC,MAAM,CAAC,EAAE,mBAAmB,CAAC;IAC7B,MAAM,CAAC,EAAE,mBAAmB,CAAC;IAC7B,WAAW,CAAC,EAAE,qBAAqB,CAAC;CACrC,CAAC;AA4HF;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CAAC,gBAAgB,EAAE,MAAM,EAAE,cAAc,GAAE,MAAM,EAAoB,GAAG,iBAAiB,CAuB/H;AAiFD;;;;;GAKG;AACH,wBAAsB,oBAAoB,CAAC,OAAO,EAAE,wBAAwB,GAAG,OAAO,CAAC,MAAM,CAAC,CA4M7F"}
@@ -76,6 +76,28 @@ function collectWatchedContentPaths(paths, projectDirectory, ignorePatterns) {
76
76
  }
77
77
  return collected;
78
78
  }
79
+ function collectWatchDirectories(directoryPath, projectDirectory, ignorePatterns, directories) {
80
+ if (shouldIgnorePath(directoryPath, projectDirectory, ignorePatterns)) {
81
+ return;
82
+ }
83
+ try {
84
+ const stats = statSync(directoryPath);
85
+ if (!stats.isDirectory()) {
86
+ return;
87
+ }
88
+ directories.add(directoryPath);
89
+ for (const entry of readdirSync(directoryPath)) {
90
+ collectWatchDirectories(join(directoryPath, entry), projectDirectory, ignorePatterns, directories);
91
+ }
92
+ } catch (_error) {
93
+ return;
94
+ }
95
+ }
96
+ function getFallbackWatchDirectories(directoryPath, projectDirectory, ignorePatterns) {
97
+ const directories = new Set();
98
+ collectWatchDirectories(directoryPath, projectDirectory, ignorePatterns, directories);
99
+ return [...directories];
100
+ }
79
101
 
80
102
  /**
81
103
  * Creates a content-diff gate for fluo-owned dev restarts.
@@ -197,7 +219,8 @@ export async function runNodeRestartRunner(options) {
197
219
  const appArgs = options.appArgs ?? [];
198
220
  const debounceMs = options.debounceMs ?? Number(env.FLUO_DEV_RELOAD_DEBOUNCE_MS ?? DEFAULT_DEBOUNCE_MS);
199
221
  const childShutdownTimeoutMs = Number(env.FLUO_DEV_CHILD_SHUTDOWN_TIMEOUT_MS ?? DEFAULT_CHILD_SHUTDOWN_TIMEOUT_MS);
200
- const gate = createContentChangeGate(projectDirectory, parseIgnorePatterns(env));
222
+ const ignorePatterns = parseIgnorePatterns(env);
223
+ const gate = createContentChangeGate(projectDirectory, ignorePatterns);
201
224
  const watchTargets = getWatchTargets(projectDirectory);
202
225
  let child;
203
226
  const pendingRestartPaths = new Set();
@@ -318,6 +341,27 @@ export async function runNodeRestartRunner(options) {
318
341
  signalTarget.off('SIGTERM', stop);
319
342
  };
320
343
  startChild(resolveExitCode, cleanup);
344
+ const watchedFallbackDirectories = new Set();
345
+ const watchFallbackDirectory = directoryPath => {
346
+ if (watchedFallbackDirectories.has(directoryPath) || shouldIgnorePath(directoryPath, projectDirectory, ignorePatterns)) {
347
+ return;
348
+ }
349
+ watchedFallbackDirectories.add(directoryPath);
350
+ const listener = (_event, filename) => {
351
+ const fileName = filename ? String(filename) : basename(directoryPath);
352
+ const changedPath = filename ? join(directoryPath, fileName) : directoryPath;
353
+ scheduleRestart(changedPath, resolveExitCode, cleanup);
354
+ for (const nextDirectoryPath of getFallbackWatchDirectories(changedPath, projectDirectory, ignorePatterns)) {
355
+ watchFallbackDirectory(nextDirectoryPath);
356
+ }
357
+ };
358
+ try {
359
+ watchers.push(watchTarget(directoryPath, listener));
360
+ } catch (error) {
361
+ watchedFallbackDirectories.delete(directoryPath);
362
+ stderr.write(`[fluo] unable to watch ${directoryPath}: ${error instanceof Error ? error.message : String(error)}\n`);
363
+ }
364
+ };
321
365
  for (const target of watchTargets) {
322
366
  try {
323
367
  const stats = statSync(target);
@@ -334,7 +378,9 @@ export async function runNodeRestartRunner(options) {
334
378
  if (!stats.isDirectory()) {
335
379
  throw error;
336
380
  }
337
- watchers.push(watchTarget(target, listener));
381
+ for (const directoryPath of getFallbackWatchDirectories(target, projectDirectory, ignorePatterns)) {
382
+ watchFallbackDirectory(directoryPath);
383
+ }
338
384
  }
339
385
  } catch (error) {
340
386
  stderr.write(`[fluo] unable to watch ${target}: ${error instanceof Error ? error.message : String(error)}\n`);
@@ -0,0 +1,24 @@
1
+ interface TypeScriptFixtureMetadata {
2
+ readonly source: 'typescript';
3
+ }
4
+ declare class TypeScriptSharedService {
5
+ readonly metadata: TypeScriptFixtureMetadata;
6
+ }
7
+ /**
8
+ * Represents a TypeScript-authored shared module fixture.
9
+ */
10
+ export declare class TypeScriptSharedModule {
11
+ }
12
+ /**
13
+ * Represents the default TypeScript-authored app module fixture.
14
+ */
15
+ export declare class AppModule {
16
+ }
17
+ /**
18
+ * Represents a named TypeScript-authored module fixture for --export coverage.
19
+ */
20
+ export declare class AdminModule {
21
+ }
22
+ export declare const inspectFixtureServices: (typeof TypeScriptSharedService)[];
23
+ export {};
24
+ //# sourceMappingURL=inspect-app.module.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inspect-app.module.d.ts","sourceRoot":"","sources":["../../src/fixtures/inspect-app.module.ts"],"names":[],"mappings":"AAAA,UAAU,yBAAyB;IACjC,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;CAC/B;AAED,cAAM,uBAAuB;IAC3B,QAAQ,CAAC,QAAQ,EAAE,yBAAyB,CAA4B;CACzE;AAED;;GAEG;AACH,qBAAa,sBAAsB;CAAG;AAEtC;;GAEG;AACH,qBAAa,SAAS;CAAG;AAEzB;;GAEG;AACH,qBAAa,WAAW;CAAG;AAE3B,eAAO,MAAM,sBAAsB,oCAA4B,CAAC"}
@@ -0,0 +1,21 @@
1
+ class TypeScriptSharedService {
2
+ metadata = {
3
+ source: 'typescript'
4
+ };
5
+ }
6
+
7
+ /**
8
+ * Represents a TypeScript-authored shared module fixture.
9
+ */
10
+ export class TypeScriptSharedModule {}
11
+
12
+ /**
13
+ * Represents the default TypeScript-authored app module fixture.
14
+ */
15
+ export class AppModule {}
16
+
17
+ /**
18
+ * Represents a named TypeScript-authored module fixture for --export coverage.
19
+ */
20
+ export class AdminModule {}
21
+ export const inspectFixtureServices = [TypeScriptSharedService];
@@ -10,7 +10,7 @@ import { toKebabCase } from './utils.js';
10
10
  */
11
11
  export function generateE2eFiles(name, options = {}) {
12
12
  const kebab = toKebabCase(name);
13
- const rootModuleImport = options.e2eRootModuleImport ?? '../src/app.module';
13
+ const rootModuleImport = options.e2eRootModuleImport ?? '../src/app';
14
14
  return [{
15
15
  content: renderTemplate('e2e.test.ts.ejs', {
16
16
  kebab,
@@ -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;AAkrEnE;;;;;;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;AAqxEnE;;;;;;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"}
@@ -450,38 +450,38 @@ function describeMicroserviceStarter(options) {
450
450
  case 'nats':
451
451
  return {
452
452
  configLines: ['- NATS broker: configure `NATS_SERVERS` in `.env` before you start the service', '- Subject contract: keep `NATS_MESSAGE_SUBJECT` and `NATS_EVENT_SUBJECT` aligned with the peer services that share the broker namespace'],
453
- entrypointNote: '`src/app.ts` opens one caller-owned `nats` client plus `JSONCodec()` and passes both into `NatsMicroserviceTransport` as the canonical starter bootstrap contract',
453
+ entrypointNote: '`src/app.ts` keeps the caller-owned `nats` client plus `JSONCodec()` contract explicit, but opens the client lazily from the generated transport wrapper when the Fluo lifecycle starts broker work',
454
454
  generatedProjectVerification: 'The generated-project verification path typechecks, builds, and tests the scaffold while asserting the NATS starter keeps the `nats` dependency, `.env` contract, and transport entrypoint wiring intact.',
455
455
  packageManagerNote: 'runtime choice stays explicit and independent from the package manager you picked; the generated manifest adds the `nats` client because the NATS starter depends on an external broker plus a caller-owned client/bootstrap pair',
456
456
  pattern: 'math.sum',
457
457
  readmeTransportLabel: 'nats',
458
458
  runtimeDependencyNote: 'runtime dependency set: `@fluojs/microservices` plus `nats` for the broker client and codec that `NatsMicroserviceTransport` expects the caller to supply',
459
459
  starterNote: 'the NATS starter keeps TCP as the default microservice path when you omit `--transport`, but this scaffold becomes runnable as soon as `NATS_SERVERS` points at a reachable broker cluster',
460
- testNote: '`src/app.test.ts` preserves a broker-free integration template via an in-memory transport, while generated-project verification still checks the NATS entrypoint/build contract and the caller-owned client bootstrap wiring.'
460
+ testNote: '`src/app.test.ts` preserves a broker-free integration template via an in-memory transport, while generated-project verification still checks the NATS entrypoint/build contract and import-safe lazy client bootstrap wiring.'
461
461
  };
462
462
  case 'kafka':
463
463
  return {
464
464
  configLines: ['- Kafka brokers: configure `KAFKA_BROKERS` in `.env` before you start the service', '- Topic/group contract: `KAFKA_CLIENT_ID`, `KAFKA_CONSUMER_GROUP`, `KAFKA_MESSAGE_TOPIC`, `KAFKA_EVENT_TOPIC`, and `KAFKA_RESPONSE_TOPIC` stay explicit so the starter never hides its shared broker topology'],
465
- entrypointNote: '`src/app.ts` opens canonical `kafkajs` producer/consumer collaborators and passes them into `KafkaMicroserviceTransport`, keeping the response-topic contract explicit in starter-owned config',
465
+ entrypointNote: '`src/app.ts` keeps the canonical `kafkajs` producer/consumer collaborator contract explicit, but creates them lazily from the generated transport wrapper when the Fluo lifecycle starts broker work',
466
466
  generatedProjectVerification: 'The generated-project verification path typechecks, builds, and tests the scaffold while asserting the Kafka starter keeps the `kafkajs` dependency, `.env` contract, and transport entrypoint wiring intact.',
467
467
  packageManagerNote: 'runtime choice stays explicit and independent from the package manager you picked; the generated manifest adds `kafkajs` because the Kafka starter depends on an external broker plus caller-owned producer/consumer collaborators',
468
468
  pattern: 'math.sum',
469
469
  readmeTransportLabel: 'kafka',
470
470
  runtimeDependencyNote: 'runtime dependency set: `@fluojs/microservices` plus `kafkajs` for the generated producer/consumer/bootstrap contract used by `KafkaMicroserviceTransport`',
471
471
  starterNote: 'the Kafka starter keeps TCP as the default microservice path when you omit `--transport`, but this scaffold becomes runnable as soon as `KAFKA_BROKERS` points at a reachable broker set',
472
- testNote: '`src/app.test.ts` preserves a broker-free integration template via an in-memory transport, while generated-project verification still checks the Kafka entrypoint/build contract and the explicit topic/client bootstrap wiring.'
472
+ testNote: '`src/app.test.ts` preserves a broker-free integration template via an in-memory transport, while generated-project verification still checks the Kafka entrypoint/build contract and import-safe lazy topic/client bootstrap wiring.'
473
473
  };
474
474
  case 'rabbitmq':
475
475
  return {
476
476
  configLines: ['- RabbitMQ broker: configure `RABBITMQ_URL` in `.env` before you start the service', '- Queue contract: `RABBITMQ_MESSAGE_QUEUE`, `RABBITMQ_EVENT_QUEUE`, and `RABBITMQ_RESPONSE_QUEUE` stay explicit so the starter advertises exactly which queues and reply path it owns'],
477
- entrypointNote: '`src/app.ts` opens a canonical `amqplib` connection/channel pair and passes caller-owned publisher/consumer collaborators into `RabbitMqMicroserviceTransport`',
477
+ entrypointNote: '`src/app.ts` keeps the canonical `amqplib` connection/channel pair and caller-owned publisher/consumer collaborator contract explicit, but opens them lazily from the generated transport wrapper when the Fluo lifecycle starts broker work',
478
478
  generatedProjectVerification: 'The generated-project verification path typechecks, builds, and tests the scaffold while asserting the RabbitMQ starter keeps the `amqplib` dependency, `.env` contract, and transport entrypoint wiring intact.',
479
479
  packageManagerNote: 'runtime choice stays explicit and independent from the package manager you picked; the generated manifest adds `amqplib` because the RabbitMQ starter depends on an external broker plus caller-owned publisher/consumer collaborators',
480
480
  pattern: 'math.sum',
481
481
  readmeTransportLabel: 'rabbitmq',
482
482
  runtimeDependencyNote: 'runtime dependency set: `@fluojs/microservices`, `amqplib`, and `@types/amqplib` for the generated queue client/bootstrap contract used by `RabbitMqMicroserviceTransport`',
483
483
  starterNote: 'the RabbitMQ starter keeps TCP as the default microservice path when you omit `--transport`, but this scaffold becomes runnable as soon as `RABBITMQ_URL` points at a reachable broker',
484
- testNote: '`src/app.test.ts` preserves a broker-free integration template via an in-memory transport, while generated-project verification still checks the RabbitMQ entrypoint/build contract and the explicit queue/bootstrap wiring.'
484
+ testNote: '`src/app.test.ts` preserves a broker-free integration template via an in-memory transport, while generated-project verification still checks the RabbitMQ entrypoint/build contract and import-safe lazy queue/bootstrap wiring.'
485
485
  };
486
486
  default:
487
487
  return {
@@ -1027,8 +1027,8 @@ export class AppModule {}
1027
1027
  case 'nats':
1028
1028
  return `import { Module } from '@fluojs/core';
1029
1029
  import { ConfigModule } from '@fluojs/config';
1030
- import { MicroservicesModule, NatsMicroserviceTransport } from '@fluojs/microservices';
1031
- import { JSONCodec, connect } from 'nats';
1030
+ import { MicroservicesModule, NatsMicroserviceTransport, type MicroserviceTransport } from '@fluojs/microservices';
1031
+ import { JSONCodec, connect, type NatsConnection } from 'nats';
1032
1032
 
1033
1033
  import { MathHandler } from './math/math.handler';
1034
1034
 
@@ -1039,36 +1039,73 @@ const servers = (process.env.NATS_SERVERS ?? 'nats://127.0.0.1:4222')
1039
1039
  const eventSubject = process.env.NATS_EVENT_SUBJECT ?? 'fluo.microservices.events';
1040
1040
  const messageSubject = process.env.NATS_MESSAGE_SUBJECT ?? 'fluo.microservices.messages';
1041
1041
  const codec = JSONCodec();
1042
- const connection = await connect({
1043
- name: 'fluo-microservice-starter',
1044
- servers,
1045
- });
1046
- const client = {
1047
- close() {
1048
- void connection.close();
1049
- },
1050
- publish(subject: string, payload: Uint8Array) {
1051
- connection.publish(subject, payload);
1052
- },
1053
- request(subject: string, payload: Uint8Array, options?: { timeout?: number }) {
1054
- return connection.request(subject, payload, options);
1055
- },
1056
- subscribe(subject: string, handler: (message: { data: Uint8Array; respond(data: Uint8Array): void }) => void) {
1057
- const subscription = connection.subscribe(subject);
1058
1042
 
1059
- void (async () => {
1060
- for await (const message of subscription) {
1061
- handler(message);
1062
- }
1063
- })();
1043
+ class LazyNatsTransport implements MicroserviceTransport {
1044
+ private connection: NatsConnection | undefined;
1045
+ private transport: NatsMicroserviceTransport | undefined;
1064
1046
 
1065
- return {
1066
- unsubscribe() {
1067
- subscription.unsubscribe();
1047
+ async close() {
1048
+ await this.transport?.close();
1049
+ await this.connection?.close();
1050
+ this.transport = undefined;
1051
+ this.connection = undefined;
1052
+ }
1053
+
1054
+ async emit(pattern: string, payload: unknown) {
1055
+ await (await this.getTransport()).emit(pattern, payload);
1056
+ }
1057
+
1058
+ async listen(handler: Parameters<MicroserviceTransport['listen']>[0]) {
1059
+ await (await this.getTransport()).listen(handler);
1060
+ }
1061
+
1062
+ async send(pattern: string, payload: unknown, signal?: AbortSignal) {
1063
+ return await (await this.getTransport()).send(pattern, payload, signal);
1064
+ }
1065
+
1066
+ private async getTransport() {
1067
+ if (this.transport) {
1068
+ return this.transport;
1069
+ }
1070
+
1071
+ const connection = await connect({
1072
+ name: 'fluo-microservice-starter',
1073
+ servers,
1074
+ });
1075
+ this.connection = connection;
1076
+ this.transport = new NatsMicroserviceTransport({
1077
+ client: {
1078
+ publish(subject: string, payload: Uint8Array) {
1079
+ connection.publish(subject, payload);
1080
+ },
1081
+ request(subject: string, payload: Uint8Array, options?: { timeout?: number }) {
1082
+ return connection.request(subject, payload, options);
1083
+ },
1084
+ subscribe(subject: string, handler: (message: { data: Uint8Array; respond(data: Uint8Array): void }) => void) {
1085
+ const subscription = connection.subscribe(subject);
1086
+
1087
+ void (async () => {
1088
+ for await (const message of subscription) {
1089
+ handler(message);
1090
+ }
1091
+ })();
1092
+
1093
+ return {
1094
+ unsubscribe() {
1095
+ subscription.unsubscribe();
1096
+ },
1097
+ };
1098
+ },
1068
1099
  },
1069
- };
1070
- },
1071
- };
1100
+ codec,
1101
+ eventSubject,
1102
+ messageSubject,
1103
+ requestTimeoutMs: 3_000,
1104
+ });
1105
+
1106
+ return this.transport;
1107
+ }
1108
+ }
1072
1109
 
1073
1110
  @Module({
1074
1111
  imports: [
@@ -1077,13 +1114,7 @@ const client = {
1077
1114
  processEnv: process.env,
1078
1115
  }),
1079
1116
  MicroservicesModule.forRoot({
1080
- transport: new NatsMicroserviceTransport({
1081
- client,
1082
- codec,
1083
- eventSubject,
1084
- messageSubject,
1085
- requestTimeoutMs: 3_000,
1086
- }),
1117
+ transport: new LazyNatsTransport(),
1087
1118
  }),
1088
1119
  ],
1089
1120
  providers: [MathHandler],
@@ -1093,8 +1124,8 @@ export class AppModule {}
1093
1124
  case 'kafka':
1094
1125
  return `import { Module } from '@fluojs/core';
1095
1126
  import { ConfigModule } from '@fluojs/config';
1096
- import { Kafka, logLevel } from 'kafkajs';
1097
- import { KafkaMicroserviceTransport, MicroservicesModule } from '@fluojs/microservices';
1127
+ import { Kafka, logLevel, type Consumer, type Producer } from 'kafkajs';
1128
+ import { KafkaMicroserviceTransport, MicroservicesModule, type MicroserviceTransport } from '@fluojs/microservices';
1098
1129
 
1099
1130
  import { MathHandler } from './math/math.handler';
1100
1131
 
@@ -1107,61 +1138,105 @@ const consumerGroup = process.env.KAFKA_CONSUMER_GROUP ?? 'fluo-handlers';
1107
1138
  const eventTopic = process.env.KAFKA_EVENT_TOPIC ?? 'fluo.microservices.events';
1108
1139
  const messageTopic = process.env.KAFKA_MESSAGE_TOPIC ?? 'fluo.microservices.messages';
1109
1140
  const responseTopic = process.env.KAFKA_RESPONSE_TOPIC ?? 'fluo.microservices.responses';
1110
- const kafka = new Kafka({
1111
- brokers,
1112
- clientId,
1113
- logLevel: logLevel.NOTHING,
1114
- });
1115
- const producer = kafka.producer();
1116
- const consumer = kafka.consumer({ groupId: consumerGroup });
1117
- await Promise.all([producer.connect(), consumer.connect()]);
1118
-
1119
- const handlers = new Map<string, (message: string) => Promise<void> | void>();
1120
- let consumerRunning = false;
1121
-
1122
- const producerClient = {
1123
- async publish(topic: string, message: string) {
1124
- await producer.send({
1125
- messages: [{ value: message }],
1126
- topic,
1127
- });
1128
- },
1129
- };
1130
1141
 
1131
- const consumerClient = {
1132
- async subscribe(topic: string, handler: (message: string) => Promise<void> | void) {
1133
- handlers.set(topic, handler);
1134
- await consumer.subscribe({ fromBeginning: false, topic });
1142
+ class LazyKafkaTransport implements MicroserviceTransport {
1143
+ private consumer: Consumer | undefined;
1144
+ private producer: Producer | undefined;
1145
+ private transport: KafkaMicroserviceTransport | undefined;
1146
+
1147
+ async close() {
1148
+ await this.transport?.close();
1149
+ await Promise.all([
1150
+ this.consumer?.disconnect().catch(() => undefined),
1151
+ this.producer?.disconnect().catch(() => undefined),
1152
+ ]);
1153
+ this.consumer = undefined;
1154
+ this.producer = undefined;
1155
+ this.transport = undefined;
1156
+ }
1135
1157
 
1136
- if (consumerRunning) {
1137
- return;
1138
- }
1158
+ async emit(pattern: string, payload: unknown) {
1159
+ await (await this.getTransport()).emit(pattern, payload);
1160
+ }
1139
1161
 
1140
- consumerRunning = true;
1141
- void consumer.run({
1142
- eachMessage: async ({ topic, message }) => {
1143
- const value = message.value?.toString();
1162
+ async listen(handler: Parameters<MicroserviceTransport['listen']>[0]) {
1163
+ await (await this.getTransport()).listen(handler);
1164
+ }
1144
1165
 
1145
- if (!value) {
1146
- return;
1147
- }
1166
+ async send(pattern: string, payload: unknown, signal?: AbortSignal) {
1167
+ return await (await this.getTransport()).send(pattern, payload, signal);
1168
+ }
1148
1169
 
1149
- await handlers.get(topic)?.(value);
1170
+ private async getTransport() {
1171
+ if (this.transport) {
1172
+ return this.transport;
1173
+ }
1174
+
1175
+ const kafka = new Kafka({
1176
+ brokers,
1177
+ clientId,
1178
+ logLevel: logLevel.NOTHING,
1179
+ });
1180
+ const producer = kafka.producer();
1181
+ const consumer = kafka.consumer({ groupId: consumerGroup });
1182
+ await Promise.all([producer.connect(), consumer.connect()]);
1183
+
1184
+ const handlers = new Map<string, (message: string) => Promise<void> | void>();
1185
+ let consumerRunning = false;
1186
+
1187
+ this.producer = producer;
1188
+ this.consumer = consumer;
1189
+ this.transport = new KafkaMicroserviceTransport({
1190
+ consumer: {
1191
+ async subscribe(topic: string, handler: (message: string) => Promise<void> | void) {
1192
+ handlers.set(topic, handler);
1193
+ await consumer.subscribe({ fromBeginning: false, topic });
1194
+
1195
+ if (consumerRunning) {
1196
+ return;
1197
+ }
1198
+
1199
+ consumerRunning = true;
1200
+ void consumer.run({
1201
+ eachMessage: async ({ topic, message }) => {
1202
+ const value = message.value?.toString();
1203
+
1204
+ if (!value) {
1205
+ return;
1206
+ }
1207
+
1208
+ await handlers.get(topic)?.(value);
1209
+ },
1210
+ });
1211
+ },
1212
+ async unsubscribe(topic: string) {
1213
+ handlers.delete(topic);
1214
+
1215
+ if (handlers.size > 0) {
1216
+ return;
1217
+ }
1218
+
1219
+ consumerRunning = false;
1220
+ await consumer.stop();
1221
+ },
1150
1222
  },
1223
+ eventTopic,
1224
+ messageTopic,
1225
+ producer: {
1226
+ async publish(topic: string, message: string) {
1227
+ await producer.send({
1228
+ messages: [{ value: message }],
1229
+ topic,
1230
+ });
1231
+ },
1232
+ },
1233
+ requestTimeoutMs: 3_000,
1234
+ responseTopic,
1151
1235
  });
1152
- },
1153
- async unsubscribe(topic: string) {
1154
- handlers.delete(topic);
1155
1236
 
1156
- if (handlers.size > 0) {
1157
- return;
1158
- }
1159
-
1160
- consumerRunning = false;
1161
- await consumer.stop();
1162
- await Promise.all([consumer.disconnect(), producer.disconnect()]);
1163
- },
1164
- };
1237
+ return this.transport;
1238
+ }
1239
+ }
1165
1240
 
1166
1241
  @Module({
1167
1242
  imports: [
@@ -1170,14 +1245,7 @@ const consumerClient = {
1170
1245
  processEnv: process.env,
1171
1246
  }),
1172
1247
  MicroservicesModule.forRoot({
1173
- transport: new KafkaMicroserviceTransport({
1174
- consumer: consumerClient,
1175
- eventTopic,
1176
- messageTopic,
1177
- producer: producerClient,
1178
- requestTimeoutMs: 3_000,
1179
- responseTopic,
1180
- }),
1248
+ transport: new LazyKafkaTransport(),
1181
1249
  }),
1182
1250
  ],
1183
1251
  providers: [MathHandler],
@@ -1189,7 +1257,7 @@ export class AppModule {}
1189
1257
 
1190
1258
  import { Module } from '@fluojs/core';
1191
1259
  import { ConfigModule } from '@fluojs/config';
1192
- import { MicroservicesModule, RabbitMqMicroserviceTransport } from '@fluojs/microservices';
1260
+ import { MicroservicesModule, RabbitMqMicroserviceTransport, type MicroserviceTransport } from '@fluojs/microservices';
1193
1261
 
1194
1262
  import { MathHandler } from './math/math.handler';
1195
1263
 
@@ -1197,61 +1265,99 @@ const url = process.env.RABBITMQ_URL ?? 'amqp://127.0.0.1:5672';
1197
1265
  const eventQueue = process.env.RABBITMQ_EVENT_QUEUE ?? 'fluo.microservices.events';
1198
1266
  const messageQueue = process.env.RABBITMQ_MESSAGE_QUEUE ?? 'fluo.microservices.messages';
1199
1267
  const responseQueue = process.env.RABBITMQ_RESPONSE_QUEUE ?? 'fluo.microservices.responses';
1200
- const connection = await connect(url);
1201
- const channel = await connection.createConfirmChannel();
1202
- const consumerTags = new Map<string, string>();
1203
-
1204
- const publisher = {
1205
- async publish(queue: string, message: string) {
1206
- await channel.assertQueue(queue, { durable: true });
1207
- await new Promise<void>((resolve, reject) => {
1208
- channel.sendToQueue(queue, Buffer.from(message), { persistent: true }, (error) => {
1209
- if (error) {
1210
- reject(error);
1211
- return;
1212
- }
1213
1268
 
1214
- resolve();
1215
- });
1216
- });
1217
- },
1218
- };
1269
+ class LazyRabbitMqTransport implements MicroserviceTransport {
1270
+ private channel: Awaited<ReturnType<Awaited<ReturnType<typeof connect>>['createConfirmChannel']>> | undefined;
1271
+ private connection: Awaited<ReturnType<typeof connect>> | undefined;
1272
+ private transport: RabbitMqMicroserviceTransport | undefined;
1273
+
1274
+ async close() {
1275
+ await this.transport?.close();
1276
+ await this.channel?.close().catch(() => undefined);
1277
+ await this.connection?.close().catch(() => undefined);
1278
+ this.channel = undefined;
1279
+ this.connection = undefined;
1280
+ this.transport = undefined;
1281
+ }
1219
1282
 
1220
- const consumer = {
1221
- async cancel(queue: string) {
1222
- const consumerTag = consumerTags.get(queue);
1283
+ async emit(pattern: string, payload: unknown) {
1284
+ await (await this.getTransport()).emit(pattern, payload);
1285
+ }
1223
1286
 
1224
- if (!consumerTag) {
1225
- return;
1226
- }
1287
+ async listen(handler: Parameters<MicroserviceTransport['listen']>[0]) {
1288
+ await (await this.getTransport()).listen(handler);
1289
+ }
1227
1290
 
1228
- consumerTags.delete(queue);
1229
- await channel.cancel(consumerTag);
1291
+ async send(pattern: string, payload: unknown, signal?: AbortSignal) {
1292
+ return await (await this.getTransport()).send(pattern, payload, signal);
1293
+ }
1230
1294
 
1231
- if (consumerTags.size === 0) {
1232
- await channel.close();
1233
- await connection.close();
1295
+ private async getTransport() {
1296
+ if (this.transport) {
1297
+ return this.transport;
1234
1298
  }
1235
- },
1236
- async consume(queue: string, handler: (message: string) => Promise<void> | void) {
1237
- await channel.assertQueue(queue, { durable: true });
1238
- const result = await channel.consume(queue, (message) => {
1239
- if (!message) {
1240
- return;
1241
- }
1242
-
1243
- void Promise.resolve(handler(message.content.toString()))
1244
- .then(() => {
1245
- channel.ack(message);
1246
- })
1247
- .catch(() => {
1248
- channel.nack(message, false, false);
1249
- });
1299
+
1300
+ const connection = await connect(url);
1301
+ const channel = await connection.createConfirmChannel();
1302
+ const consumerTags = new Map<string, string>();
1303
+
1304
+ this.connection = connection;
1305
+ this.channel = channel;
1306
+ this.transport = new RabbitMqMicroserviceTransport({
1307
+ consumer: {
1308
+ async cancel(queue: string) {
1309
+ const consumerTag = consumerTags.get(queue);
1310
+
1311
+ if (!consumerTag) {
1312
+ return;
1313
+ }
1314
+
1315
+ consumerTags.delete(queue);
1316
+ await channel.cancel(consumerTag);
1317
+ },
1318
+ async consume(queue: string, handler: (message: string) => Promise<void> | void) {
1319
+ await channel.assertQueue(queue, { durable: true });
1320
+ const result = await channel.consume(queue, (message) => {
1321
+ if (!message) {
1322
+ return;
1323
+ }
1324
+
1325
+ void Promise.resolve(handler(message.content.toString()))
1326
+ .then(() => {
1327
+ channel.ack(message);
1328
+ })
1329
+ .catch(() => {
1330
+ channel.nack(message, false, false);
1331
+ });
1332
+ });
1333
+
1334
+ consumerTags.set(queue, result.consumerTag);
1335
+ },
1336
+ },
1337
+ eventQueue,
1338
+ messageQueue,
1339
+ publisher: {
1340
+ async publish(queue: string, message: string) {
1341
+ await channel.assertQueue(queue, { durable: true });
1342
+ await new Promise<void>((resolve, reject) => {
1343
+ channel.sendToQueue(queue, Buffer.from(message), { persistent: true }, (error) => {
1344
+ if (error) {
1345
+ reject(error);
1346
+ return;
1347
+ }
1348
+
1349
+ resolve();
1350
+ });
1351
+ });
1352
+ },
1353
+ },
1354
+ requestTimeoutMs: 3_000,
1355
+ responseQueue,
1250
1356
  });
1251
1357
 
1252
- consumerTags.set(queue, result.consumerTag);
1253
- },
1254
- };
1358
+ return this.transport;
1359
+ }
1360
+ }
1255
1361
 
1256
1362
  @Module({
1257
1363
  imports: [
@@ -1260,14 +1366,7 @@ const consumer = {
1260
1366
  processEnv: process.env,
1261
1367
  }),
1262
1368
  MicroservicesModule.forRoot({
1263
- transport: new RabbitMqMicroserviceTransport({
1264
- consumer,
1265
- eventQueue,
1266
- messageQueue,
1267
- publisher,
1268
- requestTimeoutMs: 3_000,
1269
- responseQueue,
1270
- }),
1369
+ transport: new LazyRabbitMqTransport(),
1271
1370
  }),
1272
1371
  ],
1273
1372
  providers: [MathHandler],
package/package.json CHANGED
@@ -9,7 +9,7 @@
9
9
  "migration",
10
10
  "diagnostics"
11
11
  ],
12
- "version": "1.0.1",
12
+ "version": "1.0.3",
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.1"
47
+ "@fluojs/runtime": "^1.1.0"
48
48
  },
49
49
  "peerDependencies": {
50
- "@fluojs/studio": "^1.0.1"
50
+ "@fluojs/studio": "^1.0.2"
51
51
  },
52
52
  "peerDependenciesMeta": {
53
53
  "@fluojs/studio": {