From 1453f6a40c21c1c5843aa0924476f393f84d6231 Mon Sep 17 00:00:00 2001 From: Shinigami92 Date: Sun, 17 Aug 2025 13:25:54 +0200 Subject: [PATCH] feat: OpenAPI plugin --- examples/cat-rest-api/package.json | 3 +- examples/cat-rest-api/src/app.ts | 6 +- .../src/cats/cat.controller.spec.ts | 4 +- packages/core/src/auralis.plugin.ts | 12 + packages/core/src/auralis.ts | 227 +++++++++++------- packages/core/src/index.ts | 2 + packages/openapi/README.md | 14 ++ packages/openapi/package.json | 49 ++++ packages/openapi/src/index.ts | 134 +++++++++++ packages/openapi/tsconfig.json | 8 + pnpm-lock.yaml | 61 +++-- 11 files changed, 409 insertions(+), 111 deletions(-) create mode 100644 packages/core/src/auralis.plugin.ts create mode 100644 packages/openapi/README.md create mode 100644 packages/openapi/package.json create mode 100644 packages/openapi/src/index.ts create mode 100644 packages/openapi/tsconfig.json diff --git a/examples/cat-rest-api/package.json b/examples/cat-rest-api/package.json index 780f688..cad06ff 100644 --- a/examples/cat-rest-api/package.json +++ b/examples/cat-rest-api/package.json @@ -12,7 +12,8 @@ "type": "module", "module": "./dist/main.js", "dependencies": { - "@auralis/core": "workspace:*" + "@auralis/core": "workspace:*", + "@auralis/openapi": "workspace:*" }, "devDependencies": { "@types/supertest": "6.0.3", diff --git a/examples/cat-rest-api/src/app.ts b/examples/cat-rest-api/src/app.ts index 43e9317..20f9388 100644 --- a/examples/cat-rest-api/src/app.ts +++ b/examples/cat-rest-api/src/app.ts @@ -1,3 +1,7 @@ import { AuralisFactory } from "@auralis/core"; +import { OpenAPI } from "@auralis/openapi"; -export const app = await AuralisFactory.create(); +const app = await AuralisFactory.create(); +app.use(OpenAPI, {}); + +export { app }; diff --git a/examples/cat-rest-api/src/cats/cat.controller.spec.ts b/examples/cat-rest-api/src/cats/cat.controller.spec.ts index d2278fc..906d1b7 100644 --- a/examples/cat-rest-api/src/cats/cat.controller.spec.ts +++ b/examples/cat-rest-api/src/cats/cat.controller.spec.ts @@ -18,14 +18,14 @@ describe("CatController /cats", () => { it("POST /", async () => { const response = await supertest(app.server).post("/cats").send({ - name: "Mr Kettles", + name: "Mr Kittles", age: 5, }); expect(response.status).toBe(200); expect(response.body).toEqual({ id: expect.any(String), - name: "Mr Kettles", + name: "Mr Kittles", age: 5, }); }); diff --git a/packages/core/src/auralis.plugin.ts b/packages/core/src/auralis.plugin.ts new file mode 100644 index 0000000..66ada37 --- /dev/null +++ b/packages/core/src/auralis.plugin.ts @@ -0,0 +1,12 @@ +import type { Auralis } from "./auralis.ts"; + +export interface AuralisPlugin { + readonly name: string; + register(app: Auralis, options?: TPluginOptions): void; +} + +export function definePlugin( + plugin: AuralisPlugin +): AuralisPlugin { + return plugin; +} diff --git a/packages/core/src/auralis.ts b/packages/core/src/auralis.ts index fb662f8..09fb20e 100644 --- a/packages/core/src/auralis.ts +++ b/packages/core/src/auralis.ts @@ -1,9 +1,10 @@ import { glob } from "node:fs/promises"; -import type { IncomingMessage } from "node:http"; +import type { IncomingMessage, ServerResponse } from "node:http"; import { createServer } from "node:http"; import type { Server } from "node:net"; import { resolve } from "node:path"; import { pathToFileURL } from "node:url"; +import type { AuralisPlugin } from "./auralis.plugin.ts"; import type { HttpMethod } from "./decorators/http-method.decorator.ts"; import { AuralisResponseError } from "./errors/auralis-response.error.ts"; import { InternalServerError } from "./errors/internal-server-response.error.ts"; @@ -45,41 +46,56 @@ export interface ControllerMetadata { handlers?: Map; } +interface AuralisControllerHandler { + controller: Constructor; + fn: Function; + name: string; + method: HttpMethod; + path: string; + responseHeaders?: Record; + pathVariables?: Map< + string, + { + type: Function; + index: number; + } + >; + requestBody?: { + paramName: string; + type: Constructor; + index: number; + }; + passRequest?: { + paramName: string; + index: number; + }; + passResponse?: { + paramName: string; + index: number; + }; +} + +export interface AuralisPluginHandler { + name: string; + method: HttpMethod; + path: string; + fn: (req: IncomingMessage, res: ServerResponse) => Promise | void; +} + +export type AuralisHandler = AuralisControllerHandler | AuralisPluginHandler; + export class Auralis { static [AURALIS_REGISTRY_SYMBOL]: Map = new Map(); - #handlers: Array<{ - controller: Constructor; - fn: Function; - name: string; - method: HttpMethod; - path: string; - responseHeaders?: Record; - pathVariables?: Map< - string, - { - type: Function; - index: number; - } - >; - requestBody?: { - paramName: string; - type: Constructor; - index: number; - }; - passRequest?: { - paramName: string; - index: number; - }; - passResponse?: { - paramName: string; - index: number; - }; - }> = []; + #handlers: AuralisHandler[] = []; #server?: Server; + get handlers(): ReadonlyArray { + return this.#handlers; + } + get server(): Server { if (!this.#server) { throw new Error("[Auralis]: Server is not initialized"); @@ -185,79 +201,83 @@ export class Auralis { console.debug("[Auralis]: Found handler for", req.method, req.url); } - const parametersForHandler: unknown[] = []; - - // Extract path variables if they exist - if (handlerRef.pathVariables) { - const regexPattern = handlerRef.path.replaceAll( - /:(\w+)/g, - (_, name) => `(?<${name as string}>[^/]+)` - ); - const regex = new RegExp(regexPattern); - const match = regex.exec(req.url!); - - if (match) { - if (process.env.AURALIS_DEBUG) { - console.debug("[Auralis]: Extracted path variables", match); - } - - for (const [ - pathVariableName, - pathVariableRef, - ] of handlerRef.pathVariables) { - parametersForHandler[pathVariableRef.index] = - pathVariableRef.type(match.groups![pathVariableName]); + if ("controller" in handlerRef) { + const parametersForHandler: unknown[] = []; + + // Extract path variables if they exist + if (handlerRef.pathVariables) { + const regexPattern = handlerRef.path.replaceAll( + /:(\w+)/g, + (_, name) => `(?<${name as string}>[^/]+)` + ); + const regex = new RegExp(regexPattern); + const match = regex.exec(req.url!); + + if (match) { + if (process.env.AURALIS_DEBUG) { + console.debug("[Auralis]: Extracted path variables", match); + } + + for (const [ + pathVariableName, + pathVariableRef, + ] of handlerRef.pathVariables) { + parametersForHandler[pathVariableRef.index] = + pathVariableRef.type(match.groups![pathVariableName]); + } } } - } - // Extract request body if it exists - if (handlerRef.requestBody) { - const { type, index } = handlerRef.requestBody; - - const rawBody = await new Promise((resolve) => { - let data = ""; - req - .on("data", (chunk) => { - data += chunk as string; - }) - .on("end", () => { - resolve(data); - }); - }); - - parametersForHandler[index] = new type(JSON.parse(rawBody)); - } + // Extract request body if it exists + if (handlerRef.requestBody) { + const { type, index } = handlerRef.requestBody; + + const rawBody = await new Promise((resolve) => { + let data = ""; + req + .on("data", (chunk) => { + data += chunk as string; + }) + .on("end", () => { + resolve(data); + }); + }); + + parametersForHandler[index] = new type(JSON.parse(rawBody)); + } - if (handlerRef.responseHeaders) { - for (const [name, value] of Object.entries( - handlerRef.responseHeaders - )) { - res.setHeader(name, value); + if (handlerRef.responseHeaders) { + for (const [name, value] of Object.entries( + handlerRef.responseHeaders + )) { + res.setHeader(name, value); + } } - } - if (handlerRef.passRequest) { - const { index } = handlerRef.passRequest; - parametersForHandler[index] = req; - } + if (handlerRef.passRequest) { + const { index } = handlerRef.passRequest; + parametersForHandler[index] = req; + } - if (handlerRef.passResponse) { - const { index } = handlerRef.passResponse; - parametersForHandler[index] = res; - } + if (handlerRef.passResponse) { + const { index } = handlerRef.passResponse; + parametersForHandler[index] = res; + } - const controllerInstance = new handlerRef.controller(); - const boundFn = handlerRef.fn.bind(controllerInstance); + const controllerInstance = new handlerRef.controller(); + const boundFn = handlerRef.fn.bind(controllerInstance); - const responseBody = await boundFn(...parametersForHandler); + const responseBody = await boundFn(...parametersForHandler); - if (!res.writableEnded && responseBody) { - res.write(JSON.stringify(responseBody)); - } + if (!res.writableEnded && responseBody) { + res.write(JSON.stringify(responseBody)); + } - if (!res.headersSent && responseBody === void 0) { - res.statusCode = 204; + if (!res.headersSent && responseBody === void 0) { + res.statusCode = 204; + } + } else { + await handlerRef.fn(req, res); } } else { const notFoundResponse = new NotFoundResponseError( @@ -285,6 +305,18 @@ export class Auralis { }); } + use( + plugin: AuralisPlugin, + options?: TPluginOptions + ): this { + if (process.env.AURALIS_DEBUG) { + console.debug("[Auralis] Using plugin:", plugin.name); + } + + plugin.register(this, options); + return this; + } + // eslint-disable-next-line @typescript-eslint/require-await async listen(port: number): Promise { this.#server?.listen(port); @@ -316,6 +348,19 @@ export class Auralis { return `http://${host}:${address.port.toString()}`; } + + addPluginHandler( + metadata: Pick, + fn: AuralisPluginHandler["fn"] + ): this { + this.#handlers.push({ + name: metadata.name, + method: metadata.method, + path: metadata.path, + fn, + }); + return this; + } } function pathMatches(path: string, req: IncomingMessage): boolean { diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 3196d11..bab6a5c 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,3 +1,5 @@ +export { definePlugin } from "./auralis.plugin.ts"; +export type { AuralisPlugin } from "./auralis.plugin.ts"; export type { Auralis } from "./auralis.ts"; export { Controller } from "./decorators/controller.decorator.ts"; export { Delete } from "./decorators/delete.decorator.ts"; diff --git a/packages/openapi/README.md b/packages/openapi/README.md new file mode 100644 index 0000000..9259974 --- /dev/null +++ b/packages/openapi/README.md @@ -0,0 +1,14 @@ +[![NPM package](https://img.shields.io/npm/v/@auralis/openapi.svg)](https://www.npmjs.com/package/@auralis/openapi) +[![Downloads](https://img.shields.io/npm/dt/@auralis/openapi.svg)](https://www.npmjs.com/package/@auralis/openapi) +[![Build Status](https://github.com/auralisjs/auralis/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/auralisjs/auralis/actions/workflows/ci.yml) +[![License: MIT](https://img.shields.io/github/license/auralisjs/auralis.svg)](https://github.com/auralisjs/auralis/blob/main/LICENSE) + +# @auralis/openapi + +`@auralis/openapi` is a plugin for the Auralis framework that adds support for OpenAPI specifications. + +# UNDER CONSTRUCTION + +Right now there is no stable version of Auralis available. We just working on it. + + diff --git a/packages/openapi/package.json b/packages/openapi/package.json new file mode 100644 index 0000000..25a7128 --- /dev/null +++ b/packages/openapi/package.json @@ -0,0 +1,49 @@ +{ + "name": "@auralis/openapi", + "version": "0.1.0", + "description": "OpenAPI package for Auralis framework", + "scripts": { + "build": "tsc", + "ts-check": "tsc --noEmit" + }, + "homepage": "https://github.com/auralisjs/auralis/tree/main/packages/openapi", + "bugs": { + "url": "https://github.com/auralisjs/auralis/issues" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/auralisjs/auralis.git", + "directory": "packages/openapi" + }, + "funding": [ + { + "type": "github", + "url": "https://github.com/Shinigami92" + }, + { + "type": "paypal", + "url": "https://www.paypal.com/donate/?hosted_button_id=L7GY729FBKTZY" + } + ], + "license": "MIT", + "type": "module", + "exports": { + ".": "./dist/index.js", + "./package.json": "./package.json" + }, + "files": [ + "dist" + ], + "dependencies": { + "yaml": "^2.8.1" + }, + "devDependencies": { + "openapi-types": "12.1.3" + }, + "peerDependencies": { + "@auralis/core": "workspace:^" + }, + "engines": { + "node": ">=24.0.0" + } +} diff --git a/packages/openapi/src/index.ts b/packages/openapi/src/index.ts new file mode 100644 index 0000000..9431bf3 --- /dev/null +++ b/packages/openapi/src/index.ts @@ -0,0 +1,134 @@ +import type { Auralis } from "@auralis/core"; +import { definePlugin } from "@auralis/core"; +import type { ServerResponse } from "node:http"; +import type { OpenAPIV3_1 } from "openapi-types"; +import { stringify } from "yaml"; + +const HttpMethods = { + GET: "get", + PUT: "put", + POST: "post", + DELETE: "delete", + OPTIONS: "options", + HEAD: "head", + PATCH: "patch", + TRACE: "trace", +} as const; + +function generateOpenAPISpec(app: Auralis): OpenAPIV3_1.Document { + // https://swagger.io/docs/specification/v3_0/basic-structure/ + + const paths: OpenAPIV3_1.PathsObject = {}; + const components: OpenAPIV3_1.ComponentsObject = { + schemas: {}, + }; + + for (const handler of app.handlers) { + const { method, path, name } = handler; + + if (method === "CONNECT") { + continue; // Skip CONNECT method for OpenAPI + } + + paths[path] ??= {}; + + if ("controller" in handler) { + const contentType = + handler.responseHeaders?.["Content-Type"] ?? "application/json"; + + paths[path][HttpMethods[method]] = { + tags: [handler.controller.name], + operationId: name, + responses: { + 200: { + description: "Successful response", + content: { + [contentType]: { + // TODO @Shinigami92 2025-08-18: Implement response schema + }, + }, + }, + }, + }; + } + } + + return { + openapi: "3.1.0", + info: { + // TODO @Shinigami92 2025-08-18: Pass info from outside + title: "Auralis API", + description: "API documentation for Auralis", + version: "1.0.0", + }, + + servers: [ + { + url: app.getUrl(), + // TODO @Shinigami92 2025-08-18: pass server description from outside + description: "Auralis server", + }, + ], + + paths, + components, + }; +} + +function cors(res: ServerResponse): void { + // TODO @Shinigami92 2025-08-18: add cors property to app + res.setHeader("Access-Control-Allow-Origin", "*"); + res.setHeader("Access-Control-Allow-Methods", "OPTIONS, GET"); + res.setHeader("Access-Control-Max-Age", 2592000); // 30 days + res.setHeader("Access-Control-Allow-Headers", "content-type"); +} + +export const OpenAPI = definePlugin({ + name: "auralis:openapi", + register(app, options) { + console.log("Registering OpenAPI plugin with options:", options); + + let openapiSpec: OpenAPIV3_1.Document | null = null; + let openapiSpecYaml: string | null = null; + + app.addPluginHandler( + { + name: "auralis:openapi:generate", + method: "GET", + path: "/openapi.json", + }, + (req, res) => { + if (!openapiSpec) { + openapiSpec = generateOpenAPISpec(app); + } + + cors(res); + + res.setHeader("Content-Type", "application/json; charset=utf-8"); + res.end(JSON.stringify(openapiSpec)); + } + ); + + app.addPluginHandler( + { + name: "auralis:openapi:generate", + method: "GET", + path: "/openapi.yaml", + }, + (req, res) => { + if (!openapiSpec) { + openapiSpec = generateOpenAPISpec(app); + } + + if (!openapiSpecYaml) { + openapiSpecYaml = stringify(openapiSpec); + } + + cors(res); + + res.setHeader("Content-Type", "application/yaml; charset=utf-8"); + res.end(openapiSpecYaml); + } + ); + }, +}); diff --git a/packages/openapi/tsconfig.json b/packages/openapi/tsconfig.json new file mode 100644 index 0000000..5048d57 --- /dev/null +++ b/packages/openapi/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "noEmit": false, + "outDir": "./dist", + "declaration": true + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 732eecd..5c78959 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -31,10 +31,10 @@ importers: version: 24.3.0 '@vitest/coverage-v8': specifier: 3.2.4 - version: 3.2.4(vitest@3.2.4(@types/node@24.3.0)(jiti@2.5.1)) + version: 3.2.4(vitest@3.2.4(@types/node@24.3.0)(jiti@2.5.1)(yaml@2.8.1)) '@vitest/eslint-plugin': specifier: 1.3.4 - version: 1.3.4(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2)(vitest@3.2.4(@types/node@24.3.0)(jiti@2.5.1)) + version: 1.3.4(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2)(vitest@3.2.4(@types/node@24.3.0)(jiti@2.5.1)(yaml@2.8.1)) eslint: specifier: 9.33.0 version: 9.33.0(jiti@2.5.1) @@ -76,13 +76,16 @@ importers: version: 8.39.1(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2) vitest: specifier: 3.2.4 - version: 3.2.4(@types/node@24.3.0)(jiti@2.5.1) + version: 3.2.4(@types/node@24.3.0)(jiti@2.5.1)(yaml@2.8.1) examples/cat-rest-api: dependencies: '@auralis/core': specifier: workspace:* version: link:../../packages/core + '@auralis/openapi': + specifier: workspace:* + version: link:../../packages/openapi devDependencies: '@types/supertest': specifier: 6.0.3 @@ -93,6 +96,19 @@ importers: packages/core: {} + packages/openapi: + dependencies: + '@auralis/core': + specifier: workspace:^ + version: link:../core + yaml: + specifier: ^2.8.1 + version: 2.8.1 + devDependencies: + openapi-types: + specifier: 12.1.3 + version: 12.1.3 + packages: '@ampproject/remapping@2.3.0': @@ -1621,6 +1637,9 @@ packages: once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + openapi-types@12.1.3: + resolution: {integrity: sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==} + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -2108,6 +2127,11 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + yaml@2.8.1: + resolution: {integrity: sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==} + engines: {node: '>= 14.6'} + hasBin: true + yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} @@ -2793,7 +2817,7 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.11.1': optional: true - '@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/node@24.3.0)(jiti@2.5.1))': + '@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/node@24.3.0)(jiti@2.5.1)(yaml@2.8.1))': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 1.0.2 @@ -2808,17 +2832,17 @@ snapshots: std-env: 3.9.0 test-exclude: 7.0.1 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/node@24.3.0)(jiti@2.5.1) + vitest: 3.2.4(@types/node@24.3.0)(jiti@2.5.1)(yaml@2.8.1) transitivePeerDependencies: - supports-color - '@vitest/eslint-plugin@1.3.4(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2)(vitest@3.2.4(@types/node@24.3.0)(jiti@2.5.1))': + '@vitest/eslint-plugin@1.3.4(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2)(vitest@3.2.4(@types/node@24.3.0)(jiti@2.5.1)(yaml@2.8.1))': dependencies: '@typescript-eslint/utils': 8.39.1(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2) eslint: 9.33.0(jiti@2.5.1) optionalDependencies: typescript: 5.9.2 - vitest: 3.2.4(@types/node@24.3.0)(jiti@2.5.1) + vitest: 3.2.4(@types/node@24.3.0)(jiti@2.5.1)(yaml@2.8.1) transitivePeerDependencies: - supports-color @@ -2830,13 +2854,13 @@ snapshots: chai: 5.3.1 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.4(vite@7.1.2(@types/node@24.3.0)(jiti@2.5.1))': + '@vitest/mocker@3.2.4(vite@7.1.2(@types/node@24.3.0)(jiti@2.5.1)(yaml@2.8.1))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.17 optionalDependencies: - vite: 7.1.2(@types/node@24.3.0)(jiti@2.5.1) + vite: 7.1.2(@types/node@24.3.0)(jiti@2.5.1)(yaml@2.8.1) '@vitest/pretty-format@3.2.4': dependencies: @@ -3632,6 +3656,8 @@ snapshots: dependencies: wrappy: 1.0.2 + openapi-types@12.1.3: {} + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -4008,13 +4034,13 @@ snapshots: dependencies: punycode: 2.3.1 - vite-node@3.2.4(@types/node@24.3.0)(jiti@2.5.1): + vite-node@3.2.4(@types/node@24.3.0)(jiti@2.5.1)(yaml@2.8.1): dependencies: cac: 6.7.14 debug: 4.4.1 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 7.1.2(@types/node@24.3.0)(jiti@2.5.1) + vite: 7.1.2(@types/node@24.3.0)(jiti@2.5.1)(yaml@2.8.1) transitivePeerDependencies: - '@types/node' - jiti @@ -4029,7 +4055,7 @@ snapshots: - tsx - yaml - vite@7.1.2(@types/node@24.3.0)(jiti@2.5.1): + vite@7.1.2(@types/node@24.3.0)(jiti@2.5.1)(yaml@2.8.1): dependencies: esbuild: 0.25.9 fdir: 6.5.0(picomatch@4.0.3) @@ -4041,12 +4067,13 @@ snapshots: '@types/node': 24.3.0 fsevents: 2.3.3 jiti: 2.5.1 + yaml: 2.8.1 - vitest@3.2.4(@types/node@24.3.0)(jiti@2.5.1): + vitest@3.2.4(@types/node@24.3.0)(jiti@2.5.1)(yaml@2.8.1): dependencies: '@types/chai': 5.2.2 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@7.1.2(@types/node@24.3.0)(jiti@2.5.1)) + '@vitest/mocker': 3.2.4(vite@7.1.2(@types/node@24.3.0)(jiti@2.5.1)(yaml@2.8.1)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -4064,8 +4091,8 @@ snapshots: tinyglobby: 0.2.14 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 7.1.2(@types/node@24.3.0)(jiti@2.5.1) - vite-node: 3.2.4(@types/node@24.3.0)(jiti@2.5.1) + vite: 7.1.2(@types/node@24.3.0)(jiti@2.5.1)(yaml@2.8.1) + vite-node: 3.2.4(@types/node@24.3.0)(jiti@2.5.1)(yaml@2.8.1) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 24.3.0 @@ -4119,4 +4146,6 @@ snapshots: wrappy@1.0.2: {} + yaml@2.8.1: {} + yocto-queue@0.1.0: {}