diff --git a/apps/jsonthing-api/CHANGELOG.md b/apps/jsonthing-api/CHANGELOG.md index 0589e76..33ce8c3 100644 --- a/apps/jsonthing-api/CHANGELOG.md +++ b/apps/jsonthing-api/CHANGELOG.md @@ -3,6 +3,17 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [0.4.0](https://github.com/lharti/jsonthing/compare/jsonthing-api@0.3.0...jsonthing-api@0.4.0) (2024-11-16) + + +### Features + +* **api:** add `/healthz` endpoint ([217927e](https://github.com/lharti/jsonthing/commit/217927e4910e9696fcbd816d6cd483687128dcec)) + + + + + # [0.3.0](https://github.com/lharti/jsonthing/compare/jsonthing-api@0.2.0...jsonthing-api@0.3.0) (2024-11-12) diff --git a/apps/jsonthing-api/cspell.json b/apps/jsonthing-api/cspell.json index 2ab4c10..e228a1c 100644 --- a/apps/jsonthing-api/cspell.json +++ b/apps/jsonthing-api/cspell.json @@ -5,6 +5,7 @@ "dictionaries": [], "words": [ "dtos", + "Healthz", "jsonthing", "mockingoose", "nestjs", diff --git a/apps/jsonthing-api/package.json b/apps/jsonthing-api/package.json index 2b66fc1..dac35c7 100644 --- a/apps/jsonthing-api/package.json +++ b/apps/jsonthing-api/package.json @@ -1,6 +1,6 @@ { "name": "jsonthing-api", - "version": "0.3.0", + "version": "0.4.0", "description": "", "author": "", "private": true, @@ -35,6 +35,7 @@ "@nestjs/core": "^10.4.6", "@nestjs/mongoose": "^10.1.0", "@nestjs/platform-express": "^10.4.6", + "@nestjs/terminus": "^10.2.3", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", "joi": "^17.13.3", diff --git a/apps/jsonthing-api/src/app.module.spec.ts b/apps/jsonthing-api/src/app.module.spec.ts index 1220bb1..80ef65d 100644 --- a/apps/jsonthing-api/src/app.module.spec.ts +++ b/apps/jsonthing-api/src/app.module.spec.ts @@ -1,6 +1,7 @@ import { AppModule } from '@/app.module' import { DatabaseModule } from '@/database.module' import { DocsModule } from '@/routes/docs' +import { HealthzModule } from '@/routes/healthz' import { ConfigModule } from '@nestjs/config' import { Test, TestingModule } from '@nestjs/testing' @@ -68,4 +69,12 @@ describe('appModule', () => { expect(docsModule).toBeInstanceOf(DocsModule) }) + + it('should import HealthzModule', () => { + expect.assertions(1) + + const healthzModule = appModule.get(HealthzModule) + + expect(healthzModule).toBeInstanceOf(HealthzModule) + }) }) diff --git a/apps/jsonthing-api/src/app.module.ts b/apps/jsonthing-api/src/app.module.ts index 58b9d43..b9dd563 100644 --- a/apps/jsonthing-api/src/app.module.ts +++ b/apps/jsonthing-api/src/app.module.ts @@ -1,9 +1,9 @@ import { appConfigValidationSchema } from '@/config/app.config' import { DatabaseModule } from '@/database.module' import { DocsModule } from '@/routes/docs' +import { HealthzModule } from '@/routes/healthz' import { Module } from '@nestjs/common' import { ConfigModule } from '@nestjs/config' - @Module({ imports: [ ConfigModule.forRoot({ @@ -17,6 +17,7 @@ import { ConfigModule } from '@nestjs/config' // API ROUTES DocsModule, + HealthzModule, ], }) export class AppModule {} diff --git a/apps/jsonthing-api/src/routes/healthz/healthz.controller.spec.ts b/apps/jsonthing-api/src/routes/healthz/healthz.controller.spec.ts new file mode 100644 index 0000000..e29b8ef --- /dev/null +++ b/apps/jsonthing-api/src/routes/healthz/healthz.controller.spec.ts @@ -0,0 +1,62 @@ +import { + HealthCheckService, + MongooseHealthIndicator, +} from '@nestjs/terminus' +import { Test, TestingModule } from '@nestjs/testing' +import { HealthzController } from './healthz.controller' + +describe('healthzController', () => { + let testModule: TestingModule + let controller: HealthzController + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [HealthzController], + providers: [ + { + provide: HealthCheckService, + + useValue: { + check: jest.fn(checksFuncs => + checksFuncs[0](), + ), + }, + }, + + { + provide: MongooseHealthIndicator, + + useValue: { + pingCheck: jest.fn(key => + Promise.resolve({ + [key]: { status: 'up' }, + }), + ), + }, + }, + ], + }).compile() + + controller = module.get(HealthzController) + + testModule = module + }) + + afterAll(async () => { + await testModule.close() + }) + + it('should call health check service with mongoose ping check', async () => { + expect.assertions(1) + + const checkResult = await controller.check() + + expect(checkResult).toMatchInlineSnapshot(` + { + "mongoose": { + "status": "up", + }, + } + `) + }) +}) diff --git a/apps/jsonthing-api/src/routes/healthz/healthz.controller.ts b/apps/jsonthing-api/src/routes/healthz/healthz.controller.ts new file mode 100644 index 0000000..4d0c24f --- /dev/null +++ b/apps/jsonthing-api/src/routes/healthz/healthz.controller.ts @@ -0,0 +1,22 @@ +import { Controller, Get } from '@nestjs/common' +import { + HealthCheck, + HealthCheckService, + MongooseHealthIndicator, +} from '@nestjs/terminus' + +@Controller('healthz') +export class HealthzController { + constructor( + private readonly health: HealthCheckService, + private readonly mongoose: MongooseHealthIndicator, + ) {} + + @Get() + @HealthCheck() + check() { + return this.health.check([ + () => this.mongoose.pingCheck('mongoose'), + ]) + } +} diff --git a/apps/jsonthing-api/src/routes/healthz/healthz.module.spec.ts b/apps/jsonthing-api/src/routes/healthz/healthz.module.spec.ts new file mode 100644 index 0000000..6b3f427 --- /dev/null +++ b/apps/jsonthing-api/src/routes/healthz/healthz.module.spec.ts @@ -0,0 +1,34 @@ +import { TerminusModule } from '@nestjs/terminus' +import { Test, TestingModule } from '@nestjs/testing' +import { HealthzController } from './healthz.controller' +import { HealthzModule } from './healthz.module' + +describe('healthzModule', () => { + let module: TestingModule + + beforeAll(async () => { + module = await Test.createTestingModule({ + imports: [HealthzModule], + }).compile() + }) + + afterAll(async () => { + await module.close() + }) + + it('should import TerminusModule', () => { + expect.assertions(1) + + const terminusModule = module.get(TerminusModule) + + expect(terminusModule).toBeDefined() + }) + + it('should have HealthzController', () => { + expect.assertions(1) + + const healthzController = module.get(HealthzController) + + expect(healthzController).toBeDefined() + }) +}) diff --git a/apps/jsonthing-api/src/routes/healthz/healthz.module.ts b/apps/jsonthing-api/src/routes/healthz/healthz.module.ts new file mode 100644 index 0000000..554afd6 --- /dev/null +++ b/apps/jsonthing-api/src/routes/healthz/healthz.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common' +import { TerminusModule } from '@nestjs/terminus' +import { HealthzController } from './healthz.controller' + +@Module({ + imports: [TerminusModule], + controllers: [HealthzController], +}) +export class HealthzModule {} diff --git a/apps/jsonthing-api/src/routes/healthz/index.ts b/apps/jsonthing-api/src/routes/healthz/index.ts new file mode 100644 index 0000000..3e505e2 --- /dev/null +++ b/apps/jsonthing-api/src/routes/healthz/index.ts @@ -0,0 +1 @@ +export * from './healthz.module' diff --git a/apps/jsonthing-web/CHANGELOG.md b/apps/jsonthing-web/CHANGELOG.md index d39b3fa..1469e87 100644 --- a/apps/jsonthing-web/CHANGELOG.md +++ b/apps/jsonthing-web/CHANGELOG.md @@ -3,6 +3,17 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [0.6.0](https://github.com/lharti/jsonthing/compare/jsonthing-web@0.5.8...jsonthing-web@0.6.0) (2024-11-16) + + +### Features + +* **web:** add `/healthz` endpoint ([16dbded](https://github.com/lharti/jsonthing/commit/16dbdedeebd1e79dd64566e8cfd2ec6f44651ea7)) + + + + + ## [0.5.8](https://github.com/lharti/jsonthing/compare/jsonthing-web@0.5.7...jsonthing-web@0.5.8) (2024-11-14) diff --git a/apps/jsonthing-web/next.config.mjs b/apps/jsonthing-web/next.config.ts similarity index 83% rename from apps/jsonthing-web/next.config.mjs rename to apps/jsonthing-web/next.config.ts index 35e1de7..c9c24cb 100644 --- a/apps/jsonthing-web/next.config.mjs +++ b/apps/jsonthing-web/next.config.ts @@ -1,5 +1,6 @@ -/** @type {import('next').NextConfig} */ -const nextConfig = { +import { NextConfig } from 'next' + +const nextConfig: NextConfig = { output: 'standalone', reactStrictMode: true, diff --git a/apps/jsonthing-web/package.json b/apps/jsonthing-web/package.json index 85fa51d..7469dad 100644 --- a/apps/jsonthing-web/package.json +++ b/apps/jsonthing-web/package.json @@ -1,6 +1,6 @@ { "name": "jsonthing-web", - "version": "0.5.8", + "version": "0.6.0", "private": true, "scripts": { "dev": "next dev --turbo", diff --git a/apps/jsonthing-web/src/app/healthz/route.ts b/apps/jsonthing-web/src/app/healthz/route.ts new file mode 100644 index 0000000..0ebab2c --- /dev/null +++ b/apps/jsonthing-web/src/app/healthz/route.ts @@ -0,0 +1,3 @@ +export function GET() { + return Response.json({ status: 'ok' }) +} diff --git a/apps/jsonthing-web/tsconfig.json b/apps/jsonthing-web/tsconfig.json index 75466f6..caa9c06 100644 --- a/apps/jsonthing-web/tsconfig.json +++ b/apps/jsonthing-web/tsconfig.json @@ -26,6 +26,7 @@ }, "include": [ "next-env.d.ts", + "next.config.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", diff --git a/cspell.json b/cspell.json index 9d94c8c..4b11477 100644 --- a/cspell.json +++ b/cspell.json @@ -3,7 +3,13 @@ "ignorePaths": [], "dictionaryDefinitions": [], "dictionaries": [], - "words": ["jsonthing"], + "words": [ + "healthcheck", + "Healthz", + "INITDB", + "jsonthing", + "lharti" + ], "ignoreWords": [], "import": [] } diff --git a/docker/stack/nginx.conf b/docker/stack/nginx.conf index 187a7b5..46d4826 100644 --- a/docker/stack/nginx.conf +++ b/docker/stack/nginx.conf @@ -13,7 +13,7 @@ http { } server { - listen 80; + listen 80 default_server; server_name jsonthing.com www.jsonthing.com; location / { @@ -27,6 +27,14 @@ http { proxy_read_timeout 90; proxy_connect_timeout 90; } + + location /healthz { + access_log off; + + return 200 '{"status": "OK"}'; + + add_header Content-Type application/json; + } } server { diff --git a/docker/stack/stack.compose.yml b/docker/stack/stack.compose.yml index bed882f..13c033b 100644 --- a/docker/stack/stack.compose.yml +++ b/docker/stack/stack.compose.yml @@ -16,8 +16,7 @@ x-base-deploy-config: &base-deploy-config services: webapp: - image: lharti/jsonthing-web:tmp - restart: always + image: lharti/jsonthing-web:0.5.8 networks: - private environment: @@ -25,29 +24,38 @@ services: - INT_API_URL=http://api:5000 depends_on: - mongo + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:3000/healthz"] + interval: 10s + timeout: 5s + retries: 3 deploy: <<: *base-deploy-config api: image: lharti/jsonthing-api - restart: always networks: - private environment: - PORT=5000 - DB_NAME=jsonthing - env_file: - - .env + - DB_URI=${DB_URI} depends_on: - mongo + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:5000/healthz"] + interval: 10s + timeout: 5s + retries: 3 deploy: <<: *base-deploy-config mongo: image: mongo:latest - restart: always - env_file: - - .env + environment: + - MONGO_INITDB_ROOT_USERNAME=${MONGO_INITDB_ROOT_USERNAME} + - MONGO_INITDB_ROOT_PASSWORD=${MONGO_INITDB_ROOT_PASSWORD} + - MONGO_INITDB_DATABASE=${MONGO_INITDB_DATABASE} networks: - private volumes: @@ -67,24 +75,29 @@ services: nginx: image: nginx:latest - restart: always networks: - public - private ports: - "80:80" configs: - - source: nginx.conf + - source: nginx target: /etc/nginx/nginx.conf depends_on: - webapp - api + + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost/healthz"] + interval: 10s + timeout: 5s + retries: 3 deploy: <<: *base-deploy-config configs: - nginx.conf: - file: ./nginx.conf + nginx: + external: true volumes: mongo-data: diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 37bab80..dd2117f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -35,6 +35,9 @@ importers: '@nestjs/platform-express': specifier: ^10.4.6 version: 10.4.6(@nestjs/common@10.4.6(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.6) + '@nestjs/terminus': + specifier: ^10.2.3 + version: 10.2.3(@nestjs/common@10.4.6(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.6)(@nestjs/mongoose@10.1.0(@nestjs/common@10.4.6(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.6)(mongoose@8.8.0(socks@2.8.3))(rxjs@7.8.1))(mongoose@8.8.0(socks@2.8.3))(reflect-metadata@0.2.2)(rxjs@7.8.1) class-transformer: specifier: ^0.5.1 version: 0.5.1 @@ -948,6 +951,54 @@ packages: peerDependencies: typescript: '>=4.8.2' + '@nestjs/terminus@10.2.3': + resolution: {integrity: sha512-iX7gXtAooePcyQqFt57aDke5MzgdkBeYgF5YsFNNFwOiAFdIQEhfv3PR0G+HlH9F6D7nBCDZt9U87Pks/qHijg==} + peerDependencies: + '@grpc/grpc-js': '*' + '@grpc/proto-loader': '*' + '@mikro-orm/core': '*' + '@mikro-orm/nestjs': '*' + '@nestjs/axios': ^1.0.0 || ^2.0.0 || ^3.0.0 + '@nestjs/common': ^9.0.0 || ^10.0.0 + '@nestjs/core': ^9.0.0 || ^10.0.0 + '@nestjs/microservices': ^9.0.0 || ^10.0.0 + '@nestjs/mongoose': ^9.0.0 || ^10.0.0 + '@nestjs/sequelize': ^9.0.0 || ^10.0.0 + '@nestjs/typeorm': ^9.0.0 || ^10.0.0 + '@prisma/client': '*' + mongoose: '*' + reflect-metadata: 0.1.x || 0.2.x + rxjs: 7.x + sequelize: '*' + typeorm: '*' + peerDependenciesMeta: + '@grpc/grpc-js': + optional: true + '@grpc/proto-loader': + optional: true + '@mikro-orm/core': + optional: true + '@mikro-orm/nestjs': + optional: true + '@nestjs/axios': + optional: true + '@nestjs/microservices': + optional: true + '@nestjs/mongoose': + optional: true + '@nestjs/sequelize': + optional: true + '@nestjs/typeorm': + optional: true + '@prisma/client': + optional: true + mongoose: + optional: true + sequelize: + optional: true + typeorm: + optional: true + '@nestjs/testing@10.4.6': resolution: {integrity: sha512-aiDicKhlGibVGNYuew399H5qZZXaseOBT/BS+ERJxxCmco7ZdAqaujsNjSaSbTK9ojDPf27crLT0C4opjqJe3A==} peerDependencies: @@ -1971,6 +2022,9 @@ packages: ajv@8.12.0: resolution: {integrity: sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==} + ansi-align@3.0.1: + resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==} + ansi-colors@4.1.3: resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} engines: {node: '>=6'} @@ -2177,6 +2231,10 @@ packages: resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + boxen@5.1.2: + resolution: {integrity: sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ==} + engines: {node: '>=10'} + brace-expansion@1.1.11: resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} @@ -2284,6 +2342,10 @@ packages: chardet@0.7.0: resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} + check-disk-space@3.4.0: + resolution: {integrity: sha512-drVkSqfwA+TvuEhFipiR1OC9boEGZL5RrWvVsOthdcvQNXyCCuKkEiTOTXZ7qxSf/GLwq4GvzfrQD/Wz325hgw==} + engines: {node: '>=16'} + chokidar@3.6.0: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} @@ -2320,6 +2382,10 @@ packages: resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} engines: {node: '>=6'} + cli-boxes@2.2.1: + resolution: {integrity: sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==} + engines: {node: '>=6'} + cli-cursor@3.1.0: resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} engines: {node: '>=8'} @@ -6215,6 +6281,10 @@ packages: wide-align@1.1.5: resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==} + widest-line@3.1.0: + resolution: {integrity: sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==} + engines: {node: '>=8'} + word-wrap@1.2.5: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} @@ -7223,6 +7293,18 @@ snapshots: transitivePeerDependencies: - chokidar + '@nestjs/terminus@10.2.3(@nestjs/common@10.4.6(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.6)(@nestjs/mongoose@10.1.0(@nestjs/common@10.4.6(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.6)(mongoose@8.8.0(socks@2.8.3))(rxjs@7.8.1))(mongoose@8.8.0(socks@2.8.3))(reflect-metadata@0.2.2)(rxjs@7.8.1)': + dependencies: + '@nestjs/common': 10.4.6(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) + '@nestjs/core': 10.4.6(@nestjs/common@10.4.6(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.6)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1) + boxen: 5.1.2 + check-disk-space: 3.4.0 + reflect-metadata: 0.2.2 + rxjs: 7.8.1 + optionalDependencies: + '@nestjs/mongoose': 10.1.0(@nestjs/common@10.4.6(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.6)(mongoose@8.8.0(socks@2.8.3))(rxjs@7.8.1) + mongoose: 8.8.0(socks@2.8.3) + '@nestjs/testing@10.4.6(@nestjs/common@10.4.6(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.6)(@nestjs/platform-express@10.4.6)': dependencies: '@nestjs/common': 10.4.6(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) @@ -8359,6 +8441,10 @@ snapshots: require-from-string: 2.0.2 uri-js: 4.4.1 + ansi-align@3.0.1: + dependencies: + string-width: 4.2.3 + ansi-colors@4.1.3: {} ansi-escapes@4.3.2: @@ -8613,6 +8699,17 @@ snapshots: transitivePeerDependencies: - supports-color + boxen@5.1.2: + dependencies: + ansi-align: 3.0.1 + camelcase: 6.3.0 + chalk: 4.1.2 + cli-boxes: 2.2.1 + string-width: 4.2.3 + type-fest: 0.20.2 + widest-line: 3.1.0 + wrap-ansi: 7.0.0 + brace-expansion@1.1.11: dependencies: balanced-match: 1.0.2 @@ -8746,6 +8843,8 @@ snapshots: chardet@0.7.0: {} + check-disk-space@3.4.0: {} + chokidar@3.6.0: dependencies: anymatch: 3.1.3 @@ -8782,6 +8881,8 @@ snapshots: clean-stack@2.2.0: {} + cli-boxes@2.2.1: {} + cli-cursor@3.1.0: dependencies: restore-cursor: 3.1.0 @@ -13388,6 +13489,10 @@ snapshots: dependencies: string-width: 4.2.3 + widest-line@3.1.0: + dependencies: + string-width: 4.2.3 + word-wrap@1.2.5: {} wordwrap@1.0.0: {}