@fluojs/cli 1.0.1 → 1.0.2
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 +7 -5
- package/README.md +7 -5
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +21 -16
- package/dist/commands/generate.js +1 -1
- package/dist/commands/inspect.d.ts.map +1 -1
- package/dist/commands/inspect.js +12 -30
- package/dist/dev-runner/node-restart-runner.d.ts.map +1 -1
- package/dist/dev-runner/node-restart-runner.js +48 -2
- package/dist/fixtures/inspect-app.module.d.ts +24 -0
- package/dist/fixtures/inspect-app.module.d.ts.map +1 -0
- package/dist/fixtures/inspect-app.module.js +21 -0
- package/dist/generators/e2e.js +1 -1
- package/dist/new/scaffold.d.ts.map +1 -1
- package/dist/new/scaffold.js +254 -155
- package/package.json +1 -1
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`를
|
|
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 --
|
|
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
|
|
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 --
|
|
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
|
|
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;
|
|
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
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
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(
|
|
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(
|
|
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
|
|
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,
|
|
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"}
|
package/dist/commands/inspect.js
CHANGED
|
@@ -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: '
|
|
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 && !
|
|
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
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
|
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;
|
|
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
|
|
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
|
-
|
|
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];
|
package/dist/generators/e2e.js
CHANGED
|
@@ -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
|
|
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;
|
|
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"}
|
package/dist/new/scaffold.js
CHANGED
|
@@ -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`
|
|
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
|
|
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`
|
|
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
|
|
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`
|
|
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
|
|
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
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
}
|
|
1063
|
-
})();
|
|
1043
|
+
class LazyNatsTransport implements MicroserviceTransport {
|
|
1044
|
+
private connection: NatsConnection | undefined;
|
|
1045
|
+
private transport: NatsMicroserviceTransport | undefined;
|
|
1064
1046
|
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
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
|
|
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
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
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
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1158
|
+
async emit(pattern: string, payload: unknown) {
|
|
1159
|
+
await (await this.getTransport()).emit(pattern, payload);
|
|
1160
|
+
}
|
|
1139
1161
|
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
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
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1166
|
+
async send(pattern: string, payload: unknown, signal?: AbortSignal) {
|
|
1167
|
+
return await (await this.getTransport()).send(pattern, payload, signal);
|
|
1168
|
+
}
|
|
1148
1169
|
|
|
1149
|
-
|
|
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
|
-
|
|
1157
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1283
|
+
async emit(pattern: string, payload: unknown) {
|
|
1284
|
+
await (await this.getTransport()).emit(pattern, payload);
|
|
1285
|
+
}
|
|
1223
1286
|
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1287
|
+
async listen(handler: Parameters<MicroserviceTransport['listen']>[0]) {
|
|
1288
|
+
await (await this.getTransport()).listen(handler);
|
|
1289
|
+
}
|
|
1227
1290
|
|
|
1228
|
-
|
|
1229
|
-
await
|
|
1291
|
+
async send(pattern: string, payload: unknown, signal?: AbortSignal) {
|
|
1292
|
+
return await (await this.getTransport()).send(pattern, payload, signal);
|
|
1293
|
+
}
|
|
1230
1294
|
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1295
|
+
private async getTransport() {
|
|
1296
|
+
if (this.transport) {
|
|
1297
|
+
return this.transport;
|
|
1234
1298
|
}
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
const
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
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
|
-
|
|
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
|
|
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],
|