@fluojs/cli 1.0.0 → 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 +10 -6
- package/README.md +10 -6
- 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 +82 -7
- 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 +257 -156
- package/package.json +3 -3
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
|
|
|
@@ -74,11 +74,13 @@ pnpm dev
|
|
|
74
74
|
|
|
75
75
|
생성된 Node.js `dev`, `build`, `start` package script는 각각 `fluo dev`, `fluo build`, `fluo start`로 위임합니다. CLI가 Node 지향 lifecycle 명령을 소유하고 local toolchain binary를 실행할 때 project-local `node_modules/.bin`을 앞에 붙이며, 호출자가 명시하지 않은 경우 `dev`는 `NODE_ENV=development`, `build`/`start`는 `NODE_ENV=production`을 기본값으로 사용합니다. Bun, Deno, Workers의 생성된 `dev` script는 같은 `fluo dev` 추상성을 유지하되 Node-supervised dev process를 줄이도록 Bun, Deno, Wrangler의 native watch loop를 기본값으로 사용합니다. fluo가 소유한 restart boundary의 debounce/hash reporter 계약이 필요하면 `fluo dev --runner fluo` 또는 `FLUO_DEV_RUNNER=fluo`를 사용하세요. production/deployment script는 runtime-native입니다. Bun은 `bun build ./src/main.ts --outdir ./dist --target bun`과 `bun dist/main.js`를 사용하고, Deno는 `deno compile --allow-env --allow-net --output dist/app src/main.ts`와 `./dist/app`을 사용하며, Workers는 `start` 대신 Wrangler `preview`/`deploy` script를 노출합니다. 기본적으로 `fluo dev`와 `fluo start`는 CLI가 process boundary를 소유하는 경로에서 앱 로그만(애플리케이션 stdout/stderr) 표시합니다. Interactive terminal에서 fluo lifecycle status와 `app │` prefix가 붙은 애플리케이션 출력이 필요하면 `--reporter pretty`를 사용하고, 런타임/도구 watcher 원본 출력이 필요하면 `--verbose`(또는 `FLUO_VERBOSE=1`)를 사용하세요.
|
|
76
76
|
|
|
77
|
+
생성된 starter는 프로젝트를 만든 generator CLI package version을 기준으로 `@fluojs/cli` `devDependency`를 설정합니다. 따라서 `pnpm dev`, `pnpm build`, `pnpm start` 같은 lifecycle script는 오래된 hard-coded range가 아니라 starter를 스캐폴딩한 CLI 동작과 같은 기준을 사용합니다.
|
|
78
|
+
|
|
77
79
|
생성된 non-Deno starter의 `vite.config.ts`는 `@fluojs/vite`에서 `fluoDecoratorsPlugin()`을 import합니다. 따라서 decorator transform 업데이트는 각 신규 프로젝트에 inline 복사되는 대신 유지보수되는 Vite 패키지를 통해 전달됩니다.
|
|
78
80
|
|
|
79
81
|
생성된 non-Deno HTTP starter는 TDD-first Vitest 레이아웃을 사용합니다. 빠른 greeting unit test와 `greeting.slice.test.ts`는 `src/greeting/` 아래에 colocate하고, 앱 dispatch test는 `src/app.test.ts`에 유지하며, 기본 e2e 스타일 request-pipeline test는 `createTestApp({ rootModule })`와 `app.request(...).send()`를 사용해 `test/app.e2e.test.ts`에 둡니다. 생성된 `vitest.config.ts`는 `src/**/*.test.ts`와 `test/**/*.test.ts`를 모두 포함하고, package script는 `test`, `test:watch`, `test:cov`, `test:e2e`를 노출합니다. 기존 `src/app.e2e.test.ts` 테스트는 request helper를 바꾸지 않고 `test/app.e2e.test.ts`로 이동할 수 있습니다.
|
|
80
82
|
|
|
81
|
-
생성된 Node.js 애플리케이션 프로젝트에서 `fluo dev`는 기본적으로 fluo가 소유한 restart boundary를 거칩니다. 이 runner는 source와 주요 config 입력을 watch하고, atomic-save event burst를 debounce하며, restart 전에 파일 content hash를 비교하고, spawn하는 각 Node 앱 child process마다 `.env`를 로드하며, `node_modules`, `dist`, `.git`, `.fluo`, coverage, cache 폴더, editor swap file 같은 noisy output/cache 경로를 무시합니다. 파일 내용이 바뀌지 않은 Ctrl+S 저장은 앱을 재시작하지 않아야 합니다. 계획된 restart가 아닌 terminal 앱 child exit 또는 crash가 발생하면 runner는 watcher를 닫고, pending restart timer와 path를 비우며, `SIGINT`/`SIGTERM` handler를 등록 해제하고, child의 terminal code로 종료합니다. 이 동작은 full-process restart-on-watch이며 module-level HMR이 아닙니다. Config watch reload는 별도의 in-process config 관심사이고, 향후 HMR 작업은 어떤 모듈을 안전하게 hot-swap할 수 있는지 따로 문서화해야 합니다. 디버깅에 runtime-native Node watcher가 필요하면 `fluo dev --raw-watch` 또는 `FLUO_DEV_RAW_WATCH=1`을 사용하세요. 생성된 Bun/Deno/Workers 프로젝트는 기본적으로 watch/reload를 `bun --watch`, `deno run --watch`, `wrangler dev`에 위임합니다. 해당 프로젝트에서 fluo 소유 restart runner로 되돌리려면 `fluo dev --runner fluo` 또는 `FLUO_DEV_RUNNER=fluo`를 사용하고, 그 runner에 추가 ignore 경로가 필요하면 `FLUO_DEV_WATCH_IGNORE=path,pattern`으로 지정하세요.
|
|
83
|
+
생성된 Node.js 애플리케이션 프로젝트에서 `fluo dev`는 기본적으로 fluo가 소유한 restart boundary를 거칩니다. 이 runner는 source와 주요 config 입력을 watch하고, atomic-save event burst를 debounce하며, restart 전에 파일 content hash를 비교하고, spawn하는 각 Node 앱 child process마다 `.env`를 로드하며, `node_modules`, `dist`, `.git`, `.fluo`, coverage, cache 폴더, editor swap file 같은 noisy output/cache 경로를 무시합니다. 파일 내용이 바뀌지 않은 Ctrl+S 저장은 앱을 재시작하지 않아야 합니다. 계획된 restart와 terminal shutdown은 현재 앱 child에 먼저 `SIGTERM`을 보내고, 제한된 grace period 뒤에도 종료되지 않으면 force-kill하므로 비협조적인 child가 restart supervisor를 무기한 멈추게 할 수 없습니다. 계획된 restart가 아닌 terminal 앱 child exit 또는 crash가 발생하면 runner는 watcher를 닫고, pending restart timer와 path를 비우며, `SIGINT`/`SIGTERM` handler를 등록 해제하고, child의 terminal code로 종료합니다. 이 동작은 full-process restart-on-watch이며 module-level HMR이 아닙니다. Config watch reload는 별도의 in-process config 관심사이고, 향후 HMR 작업은 어떤 모듈을 안전하게 hot-swap할 수 있는지 따로 문서화해야 합니다. 디버깅에 runtime-native Node watcher가 필요하면 `fluo dev --raw-watch` 또는 `FLUO_DEV_RAW_WATCH=1`을 사용하세요. 생성된 Bun/Deno/Workers 프로젝트는 기본적으로 watch/reload를 `bun --watch`, `deno run --watch`, `wrangler dev`에 위임합니다. 해당 프로젝트에서 fluo 소유 restart runner로 되돌리려면 `fluo dev --runner fluo` 또는 `FLUO_DEV_RUNNER=fluo`를 사용하고, 그 runner에 추가 ignore 경로가 필요하면 `FLUO_DEV_WATCH_IGNORE=path,pattern`으로 지정하세요.
|
|
82
84
|
|
|
83
85
|
`fluo new`는 같은 Node 기반 설치/빌드 흐름 위에서 Node.js + Fastify, Express, raw Node.js HTTP 애플리케이션 스타터를 제공합니다.
|
|
84
86
|
|
|
@@ -110,7 +112,7 @@ fluo new my-grpc-service --shape microservice --transport grpc --runtime node --
|
|
|
110
112
|
|
|
111
113
|
지원되는 `--shape microservice --transport` 스타터 값은 정확히 `tcp`, `redis-streams`, `nats`, `kafka`, `rabbitmq`, `mqtt`, `grpc`입니다. 유지보수되는 Redis 기반 스타터가 필요하면 `redis-streams`를 사용하고, 더 넓은 Redis 통합 패턴이 필요하면 스캐폴딩 후 `@fluojs/redis`를 수동으로 추가하세요.
|
|
112
114
|
|
|
113
|
-
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을 소유하기 전에 외부 리소스를 열지 않습니다.
|
|
114
116
|
|
|
115
117
|
starter 매트릭스에는 mixed single-package starter도 포함됩니다. 하나의 Fastify HTTP 앱과 attached TCP microservice를 같은 생성 프로젝트 안에 함께 배치합니다.
|
|
116
118
|
|
|
@@ -153,7 +155,7 @@ fluo generate service users --dry-run
|
|
|
153
155
|
|
|
154
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하세요.
|
|
155
157
|
|
|
156
|
-
`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 검증에 사용하세요.
|
|
157
159
|
|
|
158
160
|
Request DTO 생성은 feature 디렉터리와 DTO 클래스 이름을 분리해서 받습니다. 따라서 `CreateUser`, `UpdateUser` 같은 여러 입력 계약을 같은 `src/users/` 슬라이스 안에 둘 수 있습니다.
|
|
159
161
|
|
|
@@ -248,7 +250,7 @@ fluo inspect ./src/app.module.ts --json > snapshot.json
|
|
|
248
250
|
fluo inspect ./src/app.module.ts --json --output artifacts/inspect-snapshot.json
|
|
249
251
|
|
|
250
252
|
# 런타임이 생산한 snapshot 옆에 bootstrap timing 포함하기
|
|
251
|
-
fluo inspect ./src/app.module.ts --
|
|
253
|
+
fluo inspect ./src/app.module.ts --timing
|
|
252
254
|
|
|
253
255
|
# 요약, snapshot, diagnostics, timing을 포함한 support triage report 내보내기
|
|
254
256
|
fluo inspect ./src/app.module.ts --report --output artifacts/inspect-report.json
|
|
@@ -257,7 +259,7 @@ fluo inspect ./src/app.module.ts --report --output artifacts/inspect-report.json
|
|
|
257
259
|
fluo inspect ./src/app.module.ts --export AdminModule --json
|
|
258
260
|
```
|
|
259
261
|
|
|
260
|
-
런타임이 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를 설치하세요:
|
|
261
263
|
|
|
262
264
|
```bash
|
|
263
265
|
pnpm add -D @fluojs/studio
|
|
@@ -272,8 +274,10 @@ Studio가 없으면 CI와 non-interactive 실행은 prompt나 package manager
|
|
|
272
274
|
| 익스포트 | 설명 |
|
|
273
275
|
|---|---|
|
|
274
276
|
| `runCli(argv?, options?)` | 모든 CLI 명령을 실행하는 메인 진입점입니다. |
|
|
277
|
+
| `CliRuntimeOptions` | stream, cwd, environment, registry metadata, update-check hook 같은 `runCli(...)` runtime override 타입입니다. |
|
|
275
278
|
| `newUsage()` | help surface와 test에서 사용하는 현재 `fluo new` usage text를 반환합니다. |
|
|
276
279
|
| `runNewCommand(argv, options?)` | 프로젝트 스캐폴딩 로직에 대한 프로그래밍적 접근을 제공합니다. |
|
|
280
|
+
| `NewCommandRuntimeOptions` | prompt, filesystem write, dependency install, git initialization 같은 `runNewCommand(...)` runtime override 타입입니다. |
|
|
277
281
|
| `CliPromptCancelledError` | 호출자가 제공한 prompt hook이 정상 취소를 알리기 위해 throw할 수 있는 안정적인 sentinel입니다. |
|
|
278
282
|
| `GenerateOptions` | 프로그래밍 방식 generator 옵션 타입입니다. |
|
|
279
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
|
|
|
@@ -74,11 +74,13 @@ pnpm dev
|
|
|
74
74
|
|
|
75
75
|
Generated Node.js `dev`, `build`, and `start` package scripts delegate to `fluo dev`, `fluo build`, and `fluo start`. The CLI owns the Node-oriented lifecycle command, prepends the project-local `node_modules/.bin` when invoking local toolchain binaries, and defaults `NODE_ENV` to `development` for `dev` and `production` for `build`/`start` when the caller has not set it explicitly. Bun, Deno, and Workers generated `dev` scripts keep the same `fluo dev` abstraction but default to Bun, Deno, or Wrangler native watch loops to reduce Node-supervised dev processes; use `fluo dev --runner fluo` (or `FLUO_DEV_RUNNER=fluo`) when you need the fluo-owned restart boundary for its debounce/hash reporter contract. Their production/deployment scripts are runtime-native: Bun uses `bun build ./src/main.ts --outdir ./dist --target bun` and `bun dist/main.js`, Deno uses `deno compile --allow-env --allow-net --output dist/app src/main.ts` and `./dist/app`, and Workers exposes Wrangler `preview`/`deploy` scripts instead of `start`. By default, `fluo dev` and `fluo start` show app logs only (application stdout/stderr) so the lifecycle output shape is unified where the CLI owns the process boundary. Use `--reporter pretty` when you want concise fluo-branded lifecycle status and `app │`-prefixed application stdout/stderr, and use `--verbose` (or `FLUO_VERBOSE=1`) when you need raw runtime/tooling watcher output for debugging.
|
|
76
76
|
|
|
77
|
+
Generated starters set their `@fluojs/cli` `devDependency` from the generator CLI package version that created the project, so lifecycle scripts such as `pnpm dev`, `pnpm build`, and `pnpm start` keep using the same CLI behavior that scaffolded the starter instead of a stale hard-coded range.
|
|
78
|
+
|
|
77
79
|
Generated non-Deno starter `vite.config.ts` files import `fluoDecoratorsPlugin()` from `@fluojs/vite`, so decorator transform updates ship through the maintained Vite package instead of being copied inline into every new project.
|
|
78
80
|
|
|
79
81
|
Generated non-Deno HTTP starters use a TDD-first Vitest layout: fast greeting unit tests and `greeting.slice.test.ts` stay colocated under `src/greeting/`, app dispatch tests stay in `src/app.test.ts`, and the default e2e-style request-pipeline tests live in `test/app.e2e.test.ts` with `createTestApp({ rootModule })` plus `app.request(...).send()`. The generated `vitest.config.ts` includes both `src/**/*.test.ts` and `test/**/*.test.ts`, while generated package scripts expose `test`, `test:watch`, `test:cov`, and `test:e2e`; existing `src/app.e2e.test.ts` tests can move to `test/app.e2e.test.ts` without changing the request helper.
|
|
80
82
|
|
|
81
|
-
For generated Node.js application projects, `fluo dev` runs through a fluo-owned restart boundary by default. The runner watches source and common config inputs, debounces atomic-save bursts, hashes file content before restarting, loads `.env` for each Node app child process it spawns, and ignores noisy output/cache paths such as `node_modules`, `dist`, `.git`, `.fluo`, coverage, cache folders, and editor swap files. Pressing Ctrl+S without changing file content should not restart the app. On terminal app child exit or crash outside a planned restart, the runner closes watchers, clears the pending restart timer and paths, unregisters its `SIGINT`/`SIGTERM` handlers, and exits with the child terminal code. This is full-process restart-on-watch, not module-level HMR; config watch reloads are a separate in-process config concern, and future HMR work must document which modules can be safely hot-swapped. Use `fluo dev --raw-watch` or `FLUO_DEV_RAW_WATCH=1` when you need the runtime-native Node watcher for debugging. Generated Bun/Deno/Workers projects delegate watch/reload behavior to `bun --watch`, `deno run --watch`, or `wrangler dev` by default; use `fluo dev --runner fluo` or `FLUO_DEV_RUNNER=fluo` when those projects should return to the fluo-owned restart runner, and use `FLUO_DEV_WATCH_IGNORE=path,pattern` to add extra ignored paths for that runner.
|
|
83
|
+
For generated Node.js application projects, `fluo dev` runs through a fluo-owned restart boundary by default. The runner watches source and common config inputs, debounces atomic-save bursts, hashes file content before restarting, loads `.env` for each Node app child process it spawns, and ignores noisy output/cache paths such as `node_modules`, `dist`, `.git`, `.fluo`, coverage, cache folders, and editor swap files. Pressing Ctrl+S without changing file content should not restart the app. Planned restarts and terminal shutdown first send `SIGTERM` to the current app child, then force-kill it after a bounded grace period so a non-cooperative child cannot hang the restart supervisor indefinitely. On terminal app child exit or crash outside a planned restart, the runner closes watchers, clears the pending restart timer and paths, unregisters its `SIGINT`/`SIGTERM` handlers, and exits with the child terminal code. This is full-process restart-on-watch, not module-level HMR; config watch reloads are a separate in-process config concern, and future HMR work must document which modules can be safely hot-swapped. Use `fluo dev --raw-watch` or `FLUO_DEV_RAW_WATCH=1` when you need the runtime-native Node watcher for debugging. Generated Bun/Deno/Workers projects delegate watch/reload behavior to `bun --watch`, `deno run --watch`, or `wrangler dev` by default; use `fluo dev --runner fluo` or `FLUO_DEV_RUNNER=fluo` when those projects should return to the fluo-owned restart runner, and use `FLUO_DEV_WATCH_IGNORE=path,pattern` to add extra ignored paths for that runner.
|
|
82
84
|
|
|
83
85
|
`fluo new` supports Node.js + Fastify, Express, and raw Node.js HTTP application starters on the same Node-oriented install/build flow:
|
|
84
86
|
|
|
@@ -110,7 +112,7 @@ fluo new my-grpc-service --shape microservice --transport grpc --runtime node --
|
|
|
110
112
|
|
|
111
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.
|
|
112
114
|
|
|
113
|
-
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.
|
|
114
116
|
|
|
115
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.
|
|
116
118
|
|
|
@@ -153,7 +155,7 @@ Auto-registered generators are `controller`, `service`, `repo`, `guard`, `interc
|
|
|
153
155
|
|
|
154
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.
|
|
155
157
|
|
|
156
|
-
`fluo generate e2e <name>` writes `test/<name>.e2e.test.ts` with `createTestApp({ rootModule: AppModule })` so request-pipeline tests live in the same app-level test area as generated starters. Use generated unit tests for direct class behavior, slice tests for DI wiring and overrides, and e2e tests for routes, guards, interceptors, DTO validation, and response writing through the virtual app.
|
|
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.
|
|
157
159
|
|
|
158
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.
|
|
159
161
|
|
|
@@ -248,7 +250,7 @@ fluo inspect ./src/app.module.ts --json > snapshot.json
|
|
|
248
250
|
fluo inspect ./src/app.module.ts --json --output artifacts/inspect-snapshot.json
|
|
249
251
|
|
|
250
252
|
# Include bootstrap timing next to the runtime-produced snapshot
|
|
251
|
-
fluo inspect ./src/app.module.ts --
|
|
253
|
+
fluo inspect ./src/app.module.ts --timing
|
|
252
254
|
|
|
253
255
|
# Emit a support triage report with summary, snapshot, diagnostics, and timing
|
|
254
256
|
fluo inspect ./src/app.module.ts --report --output artifacts/inspect-report.json
|
|
@@ -257,7 +259,7 @@ fluo inspect ./src/app.module.ts --report --output artifacts/inspect-report.json
|
|
|
257
259
|
fluo inspect ./src/app.module.ts --export AdminModule --json
|
|
258
260
|
```
|
|
259
261
|
|
|
260
|
-
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:
|
|
261
263
|
|
|
262
264
|
```bash
|
|
263
265
|
pnpm add -D @fluojs/studio
|
|
@@ -272,8 +274,10 @@ The package can be used programmatically to trigger CLI actions from within othe
|
|
|
272
274
|
| Export | Description |
|
|
273
275
|
|---|---|
|
|
274
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. |
|
|
275
278
|
| `newUsage()` | Returns the current `fluo new` usage text for help surfaces and tests. |
|
|
276
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. |
|
|
277
281
|
| `CliPromptCancelledError` | Stable sentinel that caller-supplied prompt hooks can throw to report normal cancellation. |
|
|
278
282
|
| `GenerateOptions` | Type for programmatic generator options. |
|
|
279
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"}
|
|
@@ -9,6 +9,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
9
9
|
/** Options used to configure the fluo-owned Node restart-on-watch process boundary. */
|
|
10
10
|
|
|
11
11
|
const DEFAULT_DEBOUNCE_MS = 100;
|
|
12
|
+
const DEFAULT_CHILD_SHUTDOWN_TIMEOUT_MS = 5_000;
|
|
12
13
|
const PRETTY_TTY_COLOR_ENV = 'FLUO_DEV_PRETTY_TTY_COLOR';
|
|
13
14
|
const DEFAULT_IGNORES = ['.cache', '.fluo', '.git', '.turbo', 'coverage', 'dist', 'node_modules', '*.swp', '*.swo', '*~', '.#*'];
|
|
14
15
|
const WATCH_FILES = ['.env', 'package.json', 'tsconfig.json', 'tsconfig.build.json'];
|
|
@@ -75,6 +76,28 @@ function collectWatchedContentPaths(paths, projectDirectory, ignorePatterns) {
|
|
|
75
76
|
}
|
|
76
77
|
return collected;
|
|
77
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
|
+
}
|
|
78
101
|
|
|
79
102
|
/**
|
|
80
103
|
* Creates a content-diff gate for fluo-owned dev restarts.
|
|
@@ -107,9 +130,20 @@ export function createContentChangeGate(projectDirectory, ignorePatterns = DEFAU
|
|
|
107
130
|
function getWatchTargets(projectDirectory) {
|
|
108
131
|
return [join(projectDirectory, 'src'), ...WATCH_FILES.map(fileName => join(projectDirectory, fileName))].filter(target => existsSync(target));
|
|
109
132
|
}
|
|
110
|
-
function stopChild(child) {
|
|
111
|
-
if (child && child.exitCode === null
|
|
112
|
-
|
|
133
|
+
function stopChild(child, shutdownTimeoutMs = DEFAULT_CHILD_SHUTDOWN_TIMEOUT_MS) {
|
|
134
|
+
if (child && child.exitCode === null) {
|
|
135
|
+
const forceKillTimer = setTimeout(() => {
|
|
136
|
+
if (child.exitCode === null) {
|
|
137
|
+
child.kill('SIGKILL');
|
|
138
|
+
}
|
|
139
|
+
}, shutdownTimeoutMs);
|
|
140
|
+
forceKillTimer.unref?.();
|
|
141
|
+
child.once('close', () => {
|
|
142
|
+
clearTimeout(forceKillTimer);
|
|
143
|
+
});
|
|
144
|
+
if (!child.killed) {
|
|
145
|
+
child.kill('SIGTERM');
|
|
146
|
+
}
|
|
113
147
|
}
|
|
114
148
|
}
|
|
115
149
|
function getPreserveColorTtyImport() {
|
|
@@ -184,7 +218,9 @@ export async function runNodeRestartRunner(options) {
|
|
|
184
218
|
const watchTarget = options.watchTarget ?? watch;
|
|
185
219
|
const appArgs = options.appArgs ?? [];
|
|
186
220
|
const debounceMs = options.debounceMs ?? Number(env.FLUO_DEV_RELOAD_DEBOUNCE_MS ?? DEFAULT_DEBOUNCE_MS);
|
|
187
|
-
const
|
|
221
|
+
const childShutdownTimeoutMs = Number(env.FLUO_DEV_CHILD_SHUTDOWN_TIMEOUT_MS ?? DEFAULT_CHILD_SHUTDOWN_TIMEOUT_MS);
|
|
222
|
+
const ignorePatterns = parseIgnorePatterns(env);
|
|
223
|
+
const gate = createContentChangeGate(projectDirectory, ignorePatterns);
|
|
188
224
|
const watchTargets = getWatchTargets(projectDirectory);
|
|
189
225
|
let child;
|
|
190
226
|
const pendingRestartPaths = new Set();
|
|
@@ -252,7 +288,7 @@ export async function runNodeRestartRunner(options) {
|
|
|
252
288
|
startChild(resolveExitCode, cleanup);
|
|
253
289
|
gate.commitBaseline(committedRestartPaths);
|
|
254
290
|
});
|
|
255
|
-
stopChild(previousChild);
|
|
291
|
+
stopChild(previousChild, childShutdownTimeoutMs);
|
|
256
292
|
return;
|
|
257
293
|
}
|
|
258
294
|
try {
|
|
@@ -266,10 +302,26 @@ export async function runNodeRestartRunner(options) {
|
|
|
266
302
|
return new Promise(resolveExitCode => {
|
|
267
303
|
const watchers = [];
|
|
268
304
|
let cleanedUp = false;
|
|
305
|
+
let resolved = false;
|
|
306
|
+
const resolveOnce = code => {
|
|
307
|
+
if (resolved) {
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
resolved = true;
|
|
311
|
+
resolveExitCode(code);
|
|
312
|
+
};
|
|
269
313
|
const stop = () => {
|
|
270
314
|
stopping = true;
|
|
271
315
|
cleanup();
|
|
272
|
-
|
|
316
|
+
const stoppingChild = child;
|
|
317
|
+
if (!stoppingChild || stoppingChild.exitCode !== null) {
|
|
318
|
+
resolveOnce(stoppingChild?.exitCode ?? 0);
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
stoppingChild.once('close', code => {
|
|
322
|
+
resolveOnce(code ?? 0);
|
|
323
|
+
});
|
|
324
|
+
stopChild(child, childShutdownTimeoutMs);
|
|
273
325
|
};
|
|
274
326
|
const cleanup = () => {
|
|
275
327
|
if (cleanedUp) {
|
|
@@ -289,6 +341,27 @@ export async function runNodeRestartRunner(options) {
|
|
|
289
341
|
signalTarget.off('SIGTERM', stop);
|
|
290
342
|
};
|
|
291
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
|
+
};
|
|
292
365
|
for (const target of watchTargets) {
|
|
293
366
|
try {
|
|
294
367
|
const stats = statSync(target);
|
|
@@ -305,7 +378,9 @@ export async function runNodeRestartRunner(options) {
|
|
|
305
378
|
if (!stats.isDirectory()) {
|
|
306
379
|
throw error;
|
|
307
380
|
}
|
|
308
|
-
|
|
381
|
+
for (const directoryPath of getFallbackWatchDirectories(target, projectDirectory, ignorePatterns)) {
|
|
382
|
+
watchFallbackDirectory(directoryPath);
|
|
383
|
+
}
|
|
309
384
|
}
|
|
310
385
|
} catch (error) {
|
|
311
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
|
@@ -28,7 +28,6 @@ const PUBLISHED_RUNTIME_DEPENDENCIES = {
|
|
|
28
28
|
nats: '^2.29.3'
|
|
29
29
|
};
|
|
30
30
|
const PUBLISHED_INTERNAL_DEPENDENCIES = {
|
|
31
|
-
'@fluojs/cli': '^1.0.0',
|
|
32
31
|
'@fluojs/config': '^1.0.0',
|
|
33
32
|
'@fluojs/core': '^1.0.0',
|
|
34
33
|
'@fluojs/di': '^1.0.0',
|
|
@@ -116,6 +115,9 @@ function writeTextFile(filePath, content) {
|
|
|
116
115
|
writeFileSync(filePath, content, 'utf8');
|
|
117
116
|
}
|
|
118
117
|
function createDependencySpec(packageName, releaseVersion, packageSpecs) {
|
|
118
|
+
if (packageName === '@fluojs/cli') {
|
|
119
|
+
return packageSpecs[packageName] ?? createPublishedInternalDependencySpec(releaseVersion);
|
|
120
|
+
}
|
|
119
121
|
return packageSpecs[packageName] ?? PUBLISHED_RUNTIME_DEPENDENCIES[packageName] ?? PUBLISHED_INTERNAL_DEPENDENCIES[packageName] ?? createPublishedInternalDependencySpec(releaseVersion);
|
|
120
122
|
}
|
|
121
123
|
function createPublishedInternalDependencySpec(version) {
|
|
@@ -448,38 +450,38 @@ function describeMicroserviceStarter(options) {
|
|
|
448
450
|
case 'nats':
|
|
449
451
|
return {
|
|
450
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'],
|
|
451
|
-
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',
|
|
452
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.',
|
|
453
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',
|
|
454
456
|
pattern: 'math.sum',
|
|
455
457
|
readmeTransportLabel: 'nats',
|
|
456
458
|
runtimeDependencyNote: 'runtime dependency set: `@fluojs/microservices` plus `nats` for the broker client and codec that `NatsMicroserviceTransport` expects the caller to supply',
|
|
457
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',
|
|
458
|
-
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.'
|
|
459
461
|
};
|
|
460
462
|
case 'kafka':
|
|
461
463
|
return {
|
|
462
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'],
|
|
463
|
-
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',
|
|
464
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.',
|
|
465
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',
|
|
466
468
|
pattern: 'math.sum',
|
|
467
469
|
readmeTransportLabel: 'kafka',
|
|
468
470
|
runtimeDependencyNote: 'runtime dependency set: `@fluojs/microservices` plus `kafkajs` for the generated producer/consumer/bootstrap contract used by `KafkaMicroserviceTransport`',
|
|
469
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',
|
|
470
|
-
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.'
|
|
471
473
|
};
|
|
472
474
|
case 'rabbitmq':
|
|
473
475
|
return {
|
|
474
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'],
|
|
475
|
-
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',
|
|
476
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.',
|
|
477
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',
|
|
478
480
|
pattern: 'math.sum',
|
|
479
481
|
readmeTransportLabel: 'rabbitmq',
|
|
480
482
|
runtimeDependencyNote: 'runtime dependency set: `@fluojs/microservices`, `amqplib`, and `@types/amqplib` for the generated queue client/bootstrap contract used by `RabbitMqMicroserviceTransport`',
|
|
481
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',
|
|
482
|
-
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.'
|
|
483
485
|
};
|
|
484
486
|
default:
|
|
485
487
|
return {
|
|
@@ -1025,8 +1027,8 @@ export class AppModule {}
|
|
|
1025
1027
|
case 'nats':
|
|
1026
1028
|
return `import { Module } from '@fluojs/core';
|
|
1027
1029
|
import { ConfigModule } from '@fluojs/config';
|
|
1028
|
-
import { MicroservicesModule, NatsMicroserviceTransport } from '@fluojs/microservices';
|
|
1029
|
-
import { JSONCodec, connect } from 'nats';
|
|
1030
|
+
import { MicroservicesModule, NatsMicroserviceTransport, type MicroserviceTransport } from '@fluojs/microservices';
|
|
1031
|
+
import { JSONCodec, connect, type NatsConnection } from 'nats';
|
|
1030
1032
|
|
|
1031
1033
|
import { MathHandler } from './math/math.handler';
|
|
1032
1034
|
|
|
@@ -1037,36 +1039,73 @@ const servers = (process.env.NATS_SERVERS ?? 'nats://127.0.0.1:4222')
|
|
|
1037
1039
|
const eventSubject = process.env.NATS_EVENT_SUBJECT ?? 'fluo.microservices.events';
|
|
1038
1040
|
const messageSubject = process.env.NATS_MESSAGE_SUBJECT ?? 'fluo.microservices.messages';
|
|
1039
1041
|
const codec = JSONCodec();
|
|
1040
|
-
const connection = await connect({
|
|
1041
|
-
name: 'fluo-microservice-starter',
|
|
1042
|
-
servers,
|
|
1043
|
-
});
|
|
1044
|
-
const client = {
|
|
1045
|
-
close() {
|
|
1046
|
-
void connection.close();
|
|
1047
|
-
},
|
|
1048
|
-
publish(subject: string, payload: Uint8Array) {
|
|
1049
|
-
connection.publish(subject, payload);
|
|
1050
|
-
},
|
|
1051
|
-
request(subject: string, payload: Uint8Array, options?: { timeout?: number }) {
|
|
1052
|
-
return connection.request(subject, payload, options);
|
|
1053
|
-
},
|
|
1054
|
-
subscribe(subject: string, handler: (message: { data: Uint8Array; respond(data: Uint8Array): void }) => void) {
|
|
1055
|
-
const subscription = connection.subscribe(subject);
|
|
1056
1042
|
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
}
|
|
1061
|
-
})();
|
|
1043
|
+
class LazyNatsTransport implements MicroserviceTransport {
|
|
1044
|
+
private connection: NatsConnection | undefined;
|
|
1045
|
+
private transport: NatsMicroserviceTransport | undefined;
|
|
1062
1046
|
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
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
|
+
},
|
|
1066
1099
|
},
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1100
|
+
codec,
|
|
1101
|
+
eventSubject,
|
|
1102
|
+
messageSubject,
|
|
1103
|
+
requestTimeoutMs: 3_000,
|
|
1104
|
+
});
|
|
1105
|
+
|
|
1106
|
+
return this.transport;
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1070
1109
|
|
|
1071
1110
|
@Module({
|
|
1072
1111
|
imports: [
|
|
@@ -1075,13 +1114,7 @@ const client = {
|
|
|
1075
1114
|
processEnv: process.env,
|
|
1076
1115
|
}),
|
|
1077
1116
|
MicroservicesModule.forRoot({
|
|
1078
|
-
transport: new
|
|
1079
|
-
client,
|
|
1080
|
-
codec,
|
|
1081
|
-
eventSubject,
|
|
1082
|
-
messageSubject,
|
|
1083
|
-
requestTimeoutMs: 3_000,
|
|
1084
|
-
}),
|
|
1117
|
+
transport: new LazyNatsTransport(),
|
|
1085
1118
|
}),
|
|
1086
1119
|
],
|
|
1087
1120
|
providers: [MathHandler],
|
|
@@ -1091,8 +1124,8 @@ export class AppModule {}
|
|
|
1091
1124
|
case 'kafka':
|
|
1092
1125
|
return `import { Module } from '@fluojs/core';
|
|
1093
1126
|
import { ConfigModule } from '@fluojs/config';
|
|
1094
|
-
import { Kafka, logLevel } from 'kafkajs';
|
|
1095
|
-
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';
|
|
1096
1129
|
|
|
1097
1130
|
import { MathHandler } from './math/math.handler';
|
|
1098
1131
|
|
|
@@ -1105,61 +1138,105 @@ const consumerGroup = process.env.KAFKA_CONSUMER_GROUP ?? 'fluo-handlers';
|
|
|
1105
1138
|
const eventTopic = process.env.KAFKA_EVENT_TOPIC ?? 'fluo.microservices.events';
|
|
1106
1139
|
const messageTopic = process.env.KAFKA_MESSAGE_TOPIC ?? 'fluo.microservices.messages';
|
|
1107
1140
|
const responseTopic = process.env.KAFKA_RESPONSE_TOPIC ?? 'fluo.microservices.responses';
|
|
1108
|
-
const kafka = new Kafka({
|
|
1109
|
-
brokers,
|
|
1110
|
-
clientId,
|
|
1111
|
-
logLevel: logLevel.NOTHING,
|
|
1112
|
-
});
|
|
1113
|
-
const producer = kafka.producer();
|
|
1114
|
-
const consumer = kafka.consumer({ groupId: consumerGroup });
|
|
1115
|
-
await Promise.all([producer.connect(), consumer.connect()]);
|
|
1116
|
-
|
|
1117
|
-
const handlers = new Map<string, (message: string) => Promise<void> | void>();
|
|
1118
|
-
let consumerRunning = false;
|
|
1119
|
-
|
|
1120
|
-
const producerClient = {
|
|
1121
|
-
async publish(topic: string, message: string) {
|
|
1122
|
-
await producer.send({
|
|
1123
|
-
messages: [{ value: message }],
|
|
1124
|
-
topic,
|
|
1125
|
-
});
|
|
1126
|
-
},
|
|
1127
|
-
};
|
|
1128
1141
|
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
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
|
+
}
|
|
1133
1157
|
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1158
|
+
async emit(pattern: string, payload: unknown) {
|
|
1159
|
+
await (await this.getTransport()).emit(pattern, payload);
|
|
1160
|
+
}
|
|
1137
1161
|
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
const value = message.value?.toString();
|
|
1162
|
+
async listen(handler: Parameters<MicroserviceTransport['listen']>[0]) {
|
|
1163
|
+
await (await this.getTransport()).listen(handler);
|
|
1164
|
+
}
|
|
1142
1165
|
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1166
|
+
async send(pattern: string, payload: unknown, signal?: AbortSignal) {
|
|
1167
|
+
return await (await this.getTransport()).send(pattern, payload, signal);
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
private async getTransport() {
|
|
1171
|
+
if (this.transport) {
|
|
1172
|
+
return this.transport;
|
|
1173
|
+
}
|
|
1146
1174
|
|
|
1147
|
-
|
|
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
|
+
},
|
|
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
|
+
},
|
|
1148
1232
|
},
|
|
1233
|
+
requestTimeoutMs: 3_000,
|
|
1234
|
+
responseTopic,
|
|
1149
1235
|
});
|
|
1150
|
-
},
|
|
1151
|
-
async unsubscribe(topic: string) {
|
|
1152
|
-
handlers.delete(topic);
|
|
1153
|
-
|
|
1154
|
-
if (handlers.size > 0) {
|
|
1155
|
-
return;
|
|
1156
|
-
}
|
|
1157
1236
|
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
},
|
|
1162
|
-
};
|
|
1237
|
+
return this.transport;
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1163
1240
|
|
|
1164
1241
|
@Module({
|
|
1165
1242
|
imports: [
|
|
@@ -1168,14 +1245,7 @@ const consumerClient = {
|
|
|
1168
1245
|
processEnv: process.env,
|
|
1169
1246
|
}),
|
|
1170
1247
|
MicroservicesModule.forRoot({
|
|
1171
|
-
transport: new
|
|
1172
|
-
consumer: consumerClient,
|
|
1173
|
-
eventTopic,
|
|
1174
|
-
messageTopic,
|
|
1175
|
-
producer: producerClient,
|
|
1176
|
-
requestTimeoutMs: 3_000,
|
|
1177
|
-
responseTopic,
|
|
1178
|
-
}),
|
|
1248
|
+
transport: new LazyKafkaTransport(),
|
|
1179
1249
|
}),
|
|
1180
1250
|
],
|
|
1181
1251
|
providers: [MathHandler],
|
|
@@ -1187,7 +1257,7 @@ export class AppModule {}
|
|
|
1187
1257
|
|
|
1188
1258
|
import { Module } from '@fluojs/core';
|
|
1189
1259
|
import { ConfigModule } from '@fluojs/config';
|
|
1190
|
-
import { MicroservicesModule, RabbitMqMicroserviceTransport } from '@fluojs/microservices';
|
|
1260
|
+
import { MicroservicesModule, RabbitMqMicroserviceTransport, type MicroserviceTransport } from '@fluojs/microservices';
|
|
1191
1261
|
|
|
1192
1262
|
import { MathHandler } from './math/math.handler';
|
|
1193
1263
|
|
|
@@ -1195,61 +1265,99 @@ const url = process.env.RABBITMQ_URL ?? 'amqp://127.0.0.1:5672';
|
|
|
1195
1265
|
const eventQueue = process.env.RABBITMQ_EVENT_QUEUE ?? 'fluo.microservices.events';
|
|
1196
1266
|
const messageQueue = process.env.RABBITMQ_MESSAGE_QUEUE ?? 'fluo.microservices.messages';
|
|
1197
1267
|
const responseQueue = process.env.RABBITMQ_RESPONSE_QUEUE ?? 'fluo.microservices.responses';
|
|
1198
|
-
const connection = await connect(url);
|
|
1199
|
-
const channel = await connection.createConfirmChannel();
|
|
1200
|
-
const consumerTags = new Map<string, string>();
|
|
1201
|
-
|
|
1202
|
-
const publisher = {
|
|
1203
|
-
async publish(queue: string, message: string) {
|
|
1204
|
-
await channel.assertQueue(queue, { durable: true });
|
|
1205
|
-
await new Promise<void>((resolve, reject) => {
|
|
1206
|
-
channel.sendToQueue(queue, Buffer.from(message), { persistent: true }, (error) => {
|
|
1207
|
-
if (error) {
|
|
1208
|
-
reject(error);
|
|
1209
|
-
return;
|
|
1210
|
-
}
|
|
1211
1268
|
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
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
|
+
}
|
|
1217
1282
|
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1283
|
+
async emit(pattern: string, payload: unknown) {
|
|
1284
|
+
await (await this.getTransport()).emit(pattern, payload);
|
|
1285
|
+
}
|
|
1221
1286
|
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1287
|
+
async listen(handler: Parameters<MicroserviceTransport['listen']>[0]) {
|
|
1288
|
+
await (await this.getTransport()).listen(handler);
|
|
1289
|
+
}
|
|
1225
1290
|
|
|
1226
|
-
|
|
1227
|
-
await
|
|
1291
|
+
async send(pattern: string, payload: unknown, signal?: AbortSignal) {
|
|
1292
|
+
return await (await this.getTransport()).send(pattern, payload, signal);
|
|
1293
|
+
}
|
|
1228
1294
|
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1295
|
+
private async getTransport() {
|
|
1296
|
+
if (this.transport) {
|
|
1297
|
+
return this.transport;
|
|
1232
1298
|
}
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
const
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
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,
|
|
1248
1356
|
});
|
|
1249
1357
|
|
|
1250
|
-
|
|
1251
|
-
}
|
|
1252
|
-
}
|
|
1358
|
+
return this.transport;
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1253
1361
|
|
|
1254
1362
|
@Module({
|
|
1255
1363
|
imports: [
|
|
@@ -1258,14 +1366,7 @@ const consumer = {
|
|
|
1258
1366
|
processEnv: process.env,
|
|
1259
1367
|
}),
|
|
1260
1368
|
MicroservicesModule.forRoot({
|
|
1261
|
-
transport: new
|
|
1262
|
-
consumer,
|
|
1263
|
-
eventQueue,
|
|
1264
|
-
messageQueue,
|
|
1265
|
-
publisher,
|
|
1266
|
-
requestTimeoutMs: 3_000,
|
|
1267
|
-
responseQueue,
|
|
1268
|
-
}),
|
|
1369
|
+
transport: new LazyRabbitMqTransport(),
|
|
1269
1370
|
}),
|
|
1270
1371
|
],
|
|
1271
1372
|
providers: [MathHandler],
|
package/package.json
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"migration",
|
|
10
10
|
"diagnostics"
|
|
11
11
|
],
|
|
12
|
-
"version": "1.0.
|
|
12
|
+
"version": "1.0.2",
|
|
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.
|
|
47
|
+
"@fluojs/runtime": "^1.0.1"
|
|
48
48
|
},
|
|
49
49
|
"peerDependencies": {
|
|
50
|
-
"@fluojs/studio": "^1.0.
|
|
50
|
+
"@fluojs/studio": "^1.0.1"
|
|
51
51
|
},
|
|
52
52
|
"peerDependenciesMeta": {
|
|
53
53
|
"@fluojs/studio": {
|