From 7f74cf36c2ebe671d91749f1d3bd61dd3c9c7d82 Mon Sep 17 00:00:00 2001 From: mathiasduc Date: Wed, 29 Oct 2025 10:56:16 +0100 Subject: [PATCH 1/4] feat(server): Change the nest integration to support Nest features that were competing with Orpc --- packages/nest/README.md | 28 + .../nest/src/filters/orpc-exception.filter.ts | 64 ++ packages/nest/src/implement.test.ts | 24 +- packages/nest/src/implement.ts | 68 +- packages/nest/src/index.ts | 1 + packages/nest/src/nest-features.test.ts | 584 ++++++++++++++++++ .../standard-server-fastify/src/response.ts | 21 +- packages/standard-server-node/src/body.ts | 60 +- packages/standard-server-node/src/response.ts | 44 +- 9 files changed, 861 insertions(+), 33 deletions(-) create mode 100644 packages/nest/src/filters/orpc-exception.filter.ts create mode 100644 packages/nest/src/nest-features.test.ts diff --git a/packages/nest/README.md b/packages/nest/README.md index 7fb475dd0..02db6b9b5 100644 --- a/packages/nest/README.md +++ b/packages/nest/README.md @@ -68,6 +68,34 @@ You can find the full documentation [here](https://orpc.unnoq.com). Deeply integrate oRPC with [NestJS](https://nestjs.com/). Read the [documentation](https://orpc.unnoq.com/docs/openapi/integrations/implement-contract-in-nest) for more information. +### Error Handling + +oRPC provides an optional `ORPCExceptionFilter` that catches `ORPCError` instances and converts them to standardized oRPC error responses. You can choose to: + +1. **Use the built-in filter** for standard oRPC error responses +2. **Handle ORPCError in your own custom exception filters** for custom error formatting +3. **Let NestJS default error handling take over** + +```ts +import { ORPCExceptionFilter } from '@orpc/nest' + +// Option 1: Register globally in your app +app.useGlobalFilters(new ORPCExceptionFilter()) + +// Option 2: Register as a provider in your module +@Module({ + providers: [ + { + provide: APP_FILTER, + useClass: ORPCExceptionFilter, + }, + ], +}) +export class AppModule {} +``` + +**Note:** All errors thrown in oRPC handlers (including decoding/encoding errors) are now allowed to bubble up to NestJS exception filters. This gives you full control over error handling while maintaining compatibility with NestJS's exception filter system. + ### Implement Contract An overview of how to implement an [oRPC contract](https://orpc.unnoq.com/docs/contract-first/define-contract) in NestJS. diff --git a/packages/nest/src/filters/orpc-exception.filter.ts b/packages/nest/src/filters/orpc-exception.filter.ts new file mode 100644 index 000000000..f90e11201 --- /dev/null +++ b/packages/nest/src/filters/orpc-exception.filter.ts @@ -0,0 +1,64 @@ +import type { ArgumentsHost, ExceptionFilter } from '@nestjs/common' +import type { Response } from 'express' +import type { FastifyReply } from 'fastify' +import { Catch, Injectable } from '@nestjs/common' +import { StandardBracketNotationSerializer, StandardOpenAPIJsonSerializer, StandardOpenAPISerializer } from '@orpc/openapi-client/standard' +import { StandardOpenAPICodec } from '@orpc/openapi/standard' +import { ORPCError } from '@orpc/server' +import * as StandardServerFastify from '@orpc/standard-server-fastify' +import * as StandardServerNode from '@orpc/standard-server-node' + +const codec = new StandardOpenAPICodec( + new StandardOpenAPISerializer( + new StandardOpenAPIJsonSerializer(), + new StandardBracketNotationSerializer(), + ), +) + +/** + * Global exception filter that catches ORPCError instances and converts them + * to standardized oRPC error responses. + * + * This filter is optional - you can choose to: + * 1. Use this filter to get standard oRPC error responses + * 2. Handle ORPCError in your own custom exception filters + * 3. Let NestJS default error handling take over + * + * @example + * ```typescript + * // Register globally in your app + * app.useGlobalFilters(new ORPCExceptionFilter()) + * + * // Or register as a provider + * @Module({ + * providers: [ + * { + * provide: APP_FILTER, + * useClass: ORPCExceptionFilter, + * }, + * ], + * }) + * export class AppModule {} + * ``` + */ +@Catch(ORPCError) +@Injectable() +export class ORPCExceptionFilter implements ExceptionFilter { + constructor( + private config: StandardServerNode.SendStandardResponseOptions | undefined, + ) {} + + async catch(exception: ORPCError, host: ArgumentsHost) { + const ctx = host.switchToHttp() + const res = ctx.getResponse() + const standardResponse = codec.encodeError(exception) + // Send the response directly with proper status and headers + const isFastify = 'raw' in res + if (isFastify) { + await StandardServerFastify.sendStandardResponse(res as FastifyReply, standardResponse, this.config) + } + else { + await StandardServerNode.sendStandardResponse(res as Response, standardResponse, this.config) + } + } +} diff --git a/packages/nest/src/implement.test.ts b/packages/nest/src/implement.test.ts index 9dd5f9185..15467a037 100644 --- a/packages/nest/src/implement.test.ts +++ b/packages/nest/src/implement.test.ts @@ -3,7 +3,7 @@ import type { Request } from 'express' import type { FastifyReply } from 'fastify' import FastifyCookie from '@fastify/cookie' import { Controller, Req, Res } from '@nestjs/common' -import { REQUEST } from '@nestjs/core' +import { APP_FILTER, REQUEST } from '@nestjs/core' import { FastifyAdapter } from '@nestjs/platform-fastify' import { Test } from '@nestjs/testing' import { oc, ORPCError } from '@orpc/contract' @@ -11,11 +11,13 @@ import { implement, lazy } from '@orpc/server' import * as StandardServerNode from '@orpc/standard-server-node' import supertest from 'supertest' import { expect, it, vi } from 'vitest' -import * as z from 'zod' +import { z } from 'zod' +import { ORPCExceptionFilter } from './filters/orpc-exception.filter' import { Implement } from './implement' import { ORPCModule } from './module' const sendStandardResponseSpy = vi.spyOn(StandardServerNode, 'sendStandardResponse') +const setStandardResponseSpy = vi.spyOn(StandardServerNode, 'setStandardResponse') beforeEach(() => { vi.clearAllMocks() @@ -134,6 +136,14 @@ describe('@Implement', async () => { ] as const)('type: $1', async (Controller, _) => { const moduleRef = await Test.createTestingModule({ controllers: [Controller], + // The pong test throw errors and need this filter to match the + // expected behaviour. + providers: [ + { + provide: APP_FILTER, + useClass: ORPCExceptionFilter, + }, + ], }).compile() const app = moduleRef.createNestApplication() @@ -186,6 +196,7 @@ describe('@Implement', async () => { name: 'world', }, })) + expect(sendStandardResponseSpy).toHaveBeenCalledTimes(1) expect(req).toBeDefined() expect(req!.method).toEqual('GET') @@ -369,6 +380,7 @@ describe('@Implement', async () => { }).compile() const app = moduleRef.createNestApplication() + app.useGlobalFilters(new ORPCExceptionFilter()) await app.init() const httpServer = app.getHttpServer() @@ -411,8 +423,8 @@ describe('@Implement', async () => { expect(res.body).toEqual('pong') expect(interceptor).toHaveBeenCalledTimes(1) - expect(sendStandardResponseSpy).toHaveBeenCalledTimes(1) - expect(sendStandardResponseSpy).toHaveBeenCalledWith(expect.anything(), expect.anything(), expect.objectContaining({ + expect(setStandardResponseSpy).toHaveBeenCalledTimes(1) + expect(setStandardResponseSpy).toHaveBeenCalledWith(expect.anything(), expect.anything(), expect.objectContaining({ eventIteratorKeepAliveComment: '__TEST__', })) }) @@ -459,8 +471,8 @@ describe('@Implement', async () => { }), }), })) - expect(sendStandardResponseSpy).toHaveBeenCalledTimes(1) - expect(sendStandardResponseSpy).toHaveBeenCalledWith(expect.anything(), expect.anything(), expect.objectContaining({ + expect(setStandardResponseSpy).toHaveBeenCalledTimes(1) + expect(setStandardResponseSpy).toHaveBeenCalledWith(expect.anything(), expect.anything(), expect.objectContaining({ eventIteratorKeepAliveComment: '__TEST__', })) }) diff --git a/packages/nest/src/implement.ts b/packages/nest/src/implement.ts index 0bc923fbe..e2335a536 100644 --- a/packages/nest/src/implement.ts +++ b/packages/nest/src/implement.ts @@ -9,7 +9,6 @@ import type { FastifyReply, FastifyRequest } from 'fastify' import type { Observable } from 'rxjs' import type { ORPCModuleConfig } from './module' import { applyDecorators, Delete, Get, Head, Inject, Injectable, Optional, Patch, Post, Put, UseInterceptors } from '@nestjs/common' -import { toORPCError } from '@orpc/client' import { fallbackContractConfig, isContractProcedure } from '@orpc/contract' import { StandardBracketNotationSerializer, StandardOpenAPIJsonSerializer, StandardOpenAPISerializer } from '@orpc/openapi-client/standard' import { StandardOpenAPICodec } from '@orpc/openapi/standard' @@ -124,40 +123,63 @@ export class ImplementInterceptor implements NestInterceptor { ? StandardServerFastify.toStandardLazyRequest(req, res as FastifyReply) : StandardServerNode.toStandardLazyRequest(req, res as Response) - const standardResponse: StandardResponse = await (async () => { - let isDecoding = false + // Pass the original NestJS request as context + const contextWithRequest = { + ...(this.config?.context || {}), + request: req, + response: res, + } + const client = createProcedureClient(procedure, { + ...this.config, + context: contextWithRequest, + }) + const standardResponse: StandardResponse = await (async (): Promise => { + // Decode input - catch only non-ORPC decoding errors and convert to ORPCError + let input: Awaited> try { - const client = createProcedureClient(procedure, this.config) - - isDecoding = true - const input = await codec.decode(standardRequest, flattenParams(req.params as NestParams), procedure) - isDecoding = false + input = await codec.decode(standardRequest, flattenParams(req.params as NestParams), procedure) + } + catch (e: any) { + let error: ORPCError = e + // Malformed request - wrap in ORPCError and let exception filters handle it + if (!(e instanceof ORPCError)) { + error = new ORPCError('BAD_REQUEST', { + message: `Malformed request. Ensure the request body is properly formatted and the 'Content-Type' header is set correctly.`, + cause: e, + }) + } + return codec.encodeError(error) + } - const output = await client(input, { - signal: standardRequest.signal, - lastEventId: flattenHeader(standardRequest.headers['last-event-id']), - }) + // Execute handler - let all errors bubble up to NestJS exception filters + const output = await client(input, { + signal: standardRequest.signal, + lastEventId: flattenHeader(standardRequest.headers['last-event-id']), + }) + // Encode output - catch only non-ORPC encoding errors and convert to ORPCError + try { return codec.encode(output, procedure) } - catch (e) { - const error = isDecoding && !(e instanceof ORPCError) - ? new ORPCError('BAD_REQUEST', { - message: `Malformed request. Ensure the request body is properly formatted and the 'Content-Type' header is set correctly.`, - cause: e, - }) - : toORPCError(e) - + catch (e: any) { + let error: ORPCError = e + // Encoding error means our handler returned invalid data + if (!(e instanceof ORPCError)) { + error = new ORPCError('INTERNAL_SERVER_ERROR', { + message: `Failed to encode response. The handler may have returned data that doesn't match the contract output schema.`, + cause: e, + }) + } return codec.encodeError(error) } })() - + // Set status and headers if ('raw' in res) { - await StandardServerFastify.sendStandardResponse(res, standardResponse, this.config) + return StandardServerFastify.setStandardResponse(res as FastifyReply, standardResponse, this.config) } else { - await StandardServerNode.sendStandardResponse(res, standardResponse, this.config) + return StandardServerNode.setStandardResponse(res as Response, standardResponse, this.config) } }), ) diff --git a/packages/nest/src/index.ts b/packages/nest/src/index.ts index 1681f48c8..587f0c7e8 100644 --- a/packages/nest/src/index.ts +++ b/packages/nest/src/index.ts @@ -1,3 +1,4 @@ +export * from './filters/orpc-exception.filter' export * from './implement' export { Implement as Impl } from './implement' export * from './module' diff --git a/packages/nest/src/nest-features.test.ts b/packages/nest/src/nest-features.test.ts new file mode 100644 index 000000000..5653724e9 --- /dev/null +++ b/packages/nest/src/nest-features.test.ts @@ -0,0 +1,584 @@ +import type { ArgumentsHost, CallHandler, CanActivate, ExceptionFilter, ExecutionContext, INestApplication, MiddlewareConsumer, NestInterceptor, NestMiddleware, PipeTransform } from '@nestjs/common' +import type { Observable } from 'rxjs' +import { Catch, Controller, ForbiddenException, HttpException, Injectable, Module, SetMetadata, UseGuards, UseInterceptors } from '@nestjs/common' +import { APP_FILTER, APP_INTERCEPTOR, APP_PIPE } from '@nestjs/core' +import { ExpressAdapter } from '@nestjs/platform-express' +import { FastifyAdapter } from '@nestjs/platform-fastify' +import { Test } from '@nestjs/testing' +import { oc } from '@orpc/contract' +import { implement } from '@orpc/server' +import * as StandardServerNode from '@orpc/standard-server-node' +import { map } from 'rxjs' +import request from 'supertest' +import { afterEach, describe, expect, it, vi } from 'vitest' +import { z } from 'zod' + +import { Implement, ORPCExceptionFilter, ORPCModule } from '.' + +// 1. oRPC Contract +const testContract = { + hello: oc.route({ + path: '/hello', + method: 'POST', + }) + .input(z.object({ name: z.string() })) + .output(z.object({ greeting: z.string() })), +} + +const testDetailedContract = { + hello: oc.route({ + path: '/hello', + method: 'POST', + outputStructure: 'detailed', + }) + .input(z.object({ name: z.string() })) + .output(z.object({ + body: z.object({ greeting: z.string() }), + status: z.number().optional(), + })), +} + +// Contract for testing global pipes (transformation) +const testPipeContract = { + transform: oc.route({ + path: '/transform', + method: 'POST', + }) + .input(z.object({ + text: z.string(), + name: z.string(), + })) + .output(z.object({ result: z.string(), original: z.string() })), +} + +// Contract for testing error handling (global filters) +const testErrorContract = { + error: oc.route({ + path: '/error', + method: 'POST', + }) + .input(z.object({ shouldThrow: z.boolean() })) + .output(z.object({ message: z.string() })), +} + +// Contract for testing guards +const testGuardContract = { + protected: oc.route({ + path: '/protected', + method: 'POST', + }) + .input(z.object({ apiKey: z.string() })) + .output(z.object({ message: z.string(), user: z.string() })), +} + +// 2. A real controller for the 'raw' output test +@Controller() +class TestRawController { + @Implement(testContract.hello) + hello() { + return implement(testContract.hello).handler(async ({ input }) => { + // This handler ALWAYS returns the raw output shape + return { greeting: `Hello, ${input.name}!` } + }) + } +} + +// 3. A separate controller for the 'detailed' output test +@Controller() +class TestDetailedController { + @Implement(testDetailedContract.hello) + hello() { + return implement(testDetailedContract.hello).handler(async ({ input }) => { + // This handler returns the detailed output shape: { body, headers?, status? } + return { + status: 201, // Custom status to verify detailed output works + body: { greeting: `Hello, ${input.name}!` }, + } + }) + } +} + +// 4. Interceptor that modifies the response body (must be declared before controllers that use it) +@Injectable() +class ResponseTransformInterceptor implements NestInterceptor { + intercept(context: ExecutionContext, next: CallHandler): Observable { + return next.handle().pipe( + map((data: any) => { + return { + ...data, + intercepted: true, + timestamp: new Date().toISOString(), + } + }), + ) + } +} + +// 5. Controller with interceptor to test response transformation +@Controller() +class TestInterceptorController { + @Implement(testContract.hello) + @UseInterceptors(ResponseTransformInterceptor) + hello() { + return implement(testContract.hello).handler(async ({ input }) => { + return { greeting: `Hello, ${input.name}!` } + }) + } +} + +// 6. Controller for testing global pipes (transformation) +@Controller() +class TestPipeController { + @Implement(testPipeContract.transform) + transform() { + return implement(testPipeContract.transform).handler(async ({ input }) => { + // Input should be transformed by the global pipe before reaching this handler + return { + result: `Processed: ${input.text}`, + original: `${input.name}`, + } + }) + } +} + +// 7. Controller for testing global error filter +@Controller() +class TestErrorController { + @Implement(testErrorContract.error) + error() { + return implement(testErrorContract.error).handler(async ({ input }) => { + if (input.shouldThrow) { + throw new HttpException('Custom error from handler', 418) + } + return { message: 'No error' } + }) + } +} + +// 8. Custom metadata decorator for roles (similar to your @Roles decorator) +const ROLES_KEY = 'roles' +const RequireRole = (...roles: string[]) => SetMetadata(ROLES_KEY, roles) + +// 9. Custom Guard that checks API key from request (similar to JwtAuthGuard) +@Injectable() +class ApiKeyGuard implements CanActivate { + canActivate(context: ExecutionContext): boolean { + const request = context.switchToHttp().getRequest() + // Simulate checking API key from headers or body + const apiKey = request.headers['x-api-key'] || request.body?.apiKey + + if (apiKey === 'valid-key') { + // Simulate adding user to request (like JWT strategy does) + request.user = { id: 1, name: 'John Doe', role: 'admin' } + return true + } + + throw new ForbiddenException('Invalid API key') + } +} + +// 10. Simplified Guard that just checks if user exists (no Reflector needed for this test) +@Injectable() +class AuthenticatedGuard implements CanActivate { + canActivate(context: ExecutionContext): boolean { + const request = context.switchToHttp().getRequest() + const user = request.user + + if (!user) { + throw new ForbiddenException('No user found - authentication required') + } + + return true + } +} + +// 11. Controller for testing guards (method-level) +// IMPORTANT: The @Implement interceptor passes the NestJS request/response +// as part of the oRPC context, allowing handlers to access guard modifications +// like properties set on the original request (standard practice). +// This solves the limitation where oRPC creates its own standardized request object. +@Controller() +class TestGuardController { + @UseGuards(ApiKeyGuard, AuthenticatedGuard) + @RequireRole('admin') + @Implement(testGuardContract.protected) + protected() { + return implement(testGuardContract.protected).handler(async ({ input, context }) => { + const req = (context as any).request + const user = req?.user + + return { + message: 'Access granted', + user: user?.name || 'Unknown', + } + }) + } +} + +// 12. Simple custom pipe that uppercases string input +@Injectable() +class UpperCasePipe implements PipeTransform { + transform(value: any) { + if (typeof value === 'string') { + return value.toUpperCase() + } + if (typeof value === 'object' && value !== null) { + // Transform all string properties + const result: any = {} + for (const key in value) { + const val = value[key] + result[key] = typeof val === 'string' ? val.toUpperCase() : val + } + return result + } + return value + } +} + +// 9. Global interceptor that modifies the response +@Injectable() +class GlobalLoggingInterceptor implements NestInterceptor { + intercept(context: ExecutionContext, next: CallHandler): Observable { + return next.handle().pipe( + map((data) => { + const res = context.switchToHttp().getResponse() + res.header('Global-Interceptor', `global-interceptor`) + return { ...data, globalInterceptor: true } + }), + ) + } +} + +// 9. Global filter that catches HTTP exceptions +@Catch(HttpException) +class GlobalHttpExceptionFilter implements ExceptionFilter { + catch(exception: HttpException, host: ArgumentsHost) { + const ctx = host.switchToHttp() + const response = ctx.getResponse() + const status = exception.getStatus() + + // Custom error response format + const errorResponse = { + statusCode: status, + message: exception.message, + timestamp: new Date().toISOString(), + customFilter: true, // Marker to verify the filter ran + } + + response.status(status).send(errorResponse) + } +} + +// 10. Custom Middleware +class CustomHeaderMiddleware implements NestMiddleware { + use(req: any, res: any, next: (error?: any) => void) { + res.setHeader('X-Custom-Middleware', 'hello') + next() + } +} + +// 10. Test Modules for each controller +@Module({ + controllers: [TestRawController], + providers: [ + { + provide: APP_FILTER, + useClass: ORPCExceptionFilter, + }, + ], +}) +class TestRawModule { + configure(consumer: MiddlewareConsumer) { + consumer.apply(CustomHeaderMiddleware).forRoutes('*') + } +} + +@Module({ + controllers: [TestDetailedController], + providers: [ + { + provide: APP_FILTER, + useClass: ORPCExceptionFilter, + }, + ], +}) +class TestDetailedModule { + configure(consumer: MiddlewareConsumer) { + consumer.apply(CustomHeaderMiddleware).forRoutes('*') + } +} + +@Module({ + controllers: [TestInterceptorController], + providers: [ + ResponseTransformInterceptor, + { + provide: APP_FILTER, + useClass: ORPCExceptionFilter, + }, + ], +}) +class TestInterceptorModule { + configure(consumer: MiddlewareConsumer) { + consumer.apply(CustomHeaderMiddleware).forRoutes('*') + } +} + +@Module({ + controllers: [TestPipeController], + providers: [ + { + provide: APP_PIPE, + useClass: UpperCasePipe, + }, + { + provide: APP_FILTER, + useClass: ORPCExceptionFilter, + }, + ], +}) +class TestPipeModule { + configure(consumer: MiddlewareConsumer) { + consumer.apply(CustomHeaderMiddleware).forRoutes('*') + } +} + +@Module({ + controllers: [TestErrorController], + providers: [ + { + provide: APP_FILTER, + useClass: GlobalHttpExceptionFilter, + }, + // this will not run because GlobalHttpExceptionFilter sends the response first + { + provide: APP_FILTER, + useClass: ORPCExceptionFilter, + }, + ], +}) +class TestErrorModule {} + +@Module({ + controllers: [TestGuardController], + providers: [ + { + provide: APP_FILTER, + useClass: ORPCExceptionFilter, + }, + ], +}) +class TestGuardModule { + configure(consumer: MiddlewareConsumer) { + consumer.apply(CustomHeaderMiddleware).forRoutes('*') + } +} + +@Module({ + controllers: [TestRawController], + providers: [ + { + provide: APP_INTERCEPTOR, + useClass: GlobalLoggingInterceptor, + }, + { + provide: APP_FILTER, + useClass: ORPCExceptionFilter, + }, + ], +}) +class TestGlobalInterceptorModule { + configure(consumer: MiddlewareConsumer) { + consumer.apply(CustomHeaderMiddleware).forRoutes('*') + } +} + +const sendStandardResponseSpy = vi.spyOn(StandardServerNode, 'sendStandardResponse') + +describe('oRPC Nest Middleware Integration', () => { + const testSuite = ( + adapterName: 'Express' | 'Fastify', + adapter: () => ExpressAdapter | FastifyAdapter, + ) => { + describe(`with ${adapterName}`, () => { + let app: INestApplication + + async function createApp(testModule: any, orpcModuleConfig: any) { + const moduleFixture = await Test.createTestingModule({ + imports: [testModule, ORPCModule.forRoot(orpcModuleConfig)], + }).compile() + + app = moduleFixture.createNestApplication(adapter()) + app.enableCors() + await app.init() + if (adapterName === 'Fastify') { + await (app as any).getHttpAdapter().getInstance().ready() + } + } + + afterEach(async () => { + await app?.close() + }) + + it('should apply NestJS middleware and CORS with outputStructure: \'raw\'', async () => { + await createApp(TestRawModule, {}) + + await request(app.getHttpServer()) + .post('/hello') + .send({ name: 'world' }) + .expect(200) + .expect('Access-Control-Allow-Origin', '*') + .expect('X-Custom-Middleware', 'hello') + .then((response) => { + expect(response.body).toEqual({ greeting: 'Hello, world!' }) + }) + }) + + it('should apply NestJS middleware and CORS with outputStructure: \'detailed\'', async () => { + await createApp(TestDetailedModule, { outputStructure: 'detailed' }) + + await request(app.getHttpServer()) + .post('/hello') + .send({ name: 'detailed-world' }) + .expect(201) // Assert the custom status code + .expect('Access-Control-Allow-Origin', '*') + .expect('X-Custom-Middleware', 'hello') + .then((response) => { + // Manually parse the response text instead of relying on response.body + const body = JSON.parse(response.text) + expect(body).toEqual({ + greeting: 'Hello, detailed-world!', + }) + }) + }) + + it('should allow NestJS interceptors to modify the response', async () => { + await createApp(TestInterceptorModule, {}) + + await request(app.getHttpServer()) + .post('/hello') + .send({ name: 'interceptor-test' }) + .expect(200) + .expect('Access-Control-Allow-Origin', '*') + .expect('X-Custom-Middleware', 'hello') + .then((response) => { + expect(response.body).toMatchObject({ + greeting: 'Hello, interceptor-test!', + intercepted: true, + }) + expect(response.body.timestamp).toBeDefined() + }) + }) + + it('should work with global pipes (APP_PIPE provider)', async () => { + // Note: Global NestJS pipes don't transform oRPC handler inputs because: + // 1. oRPC has its own codec that decodes and validates the request body against + // the contract schema (Zod schemas) - this happens independently of NestJS pipes + // 2. NestJS pipes are designed to work with parameter decorators (@Body(), @Param(), etc.) + // but oRPC handlers don't use these decorators + // 3. The request body is parsed by NestJS, but pipe transformation only applies when + // explicitly bound to parameters via decorators + // + // For input transformation with oRPC, you should: + // - Use Zod's .transform() in your contract schemas + // - Use oRPC middleware to transform inputs + // - Transform data in the handler itself + // + // This test verifies that global pipes can be registered without breaking oRPC. + await createApp(TestPipeModule, {}) + + await request(app.getHttpServer()) + .post('/transform') + .send({ text: 'hello world', name: 'john' }) + .expect(200) + .expect('Access-Control-Allow-Origin', '*') + .expect('X-Custom-Middleware', 'hello') + .then((response) => { + // The input is NOT transformed by UpperCasePipe because oRPC's codec + // processes the request body independently from NestJS's pipe system + expect(response.body).toEqual({ + result: 'Processed: hello world', + original: 'john', + }) + }) + }) + + it('should work with Guards and custom decorators (@UseGuards)', async () => { + await createApp(TestGuardModule, {}) + + // Test successful authentication with valid API key and admin role + await request(app.getHttpServer()) + .post('/protected') + .set('X-Api-Key', 'valid-key') + .send({ apiKey: 'valid-key' }) + .expect(200) + .expect('Access-Control-Allow-Origin', '*') + .expect('X-Custom-Middleware', 'hello') + .then((response) => { + expect(response.body).toEqual({ + message: 'Access granted', + user: 'John Doe', + }) + }) + + // Test failed authentication with invalid API key + await request(app.getHttpServer()) + .post('/protected') + .set('X-Api-Key', 'invalid-key') + .send({ apiKey: 'invalid-key' }) + .expect(403) + .then((response) => { + expect(response.body.message).toBe('Invalid API key') + }) + }) + + it('should work with global exception filters (useGlobalFilters)', async () => { + await createApp(TestErrorModule, {}) + + // Request that doesn't throw should succeed + await request(app.getHttpServer()) + .post('/error') + .send({ shouldThrow: false }) + .expect(200) + .then((response) => { + expect(response.body).toEqual({ message: 'No error' }) + }) + + // Errors thrown inside oRPC handlers are now allowed to bubble up to NestJS + // so global exception filters can catch and transform them + await request(app.getHttpServer()) + .post('/error') + .send({ shouldThrow: true }) + .expect(418) // Custom status code from the HttpException + .then((response) => { + // The response should be transformed by GlobalHttpExceptionFilter + expect(response.body).toMatchObject({ + statusCode: 418, + message: 'Custom error from handler', + customFilter: true, // Marker to verify the filter ran + }) + expect(response.body.timestamp).toBeDefined() + }) + + // Ensure that the standard response was not sent via ORPCExceptionFilter + expect(sendStandardResponseSpy).not.toHaveBeenCalled() + }) + + it('should work with global interceptors (useGlobalInterceptors)', async () => { + await createApp(TestGlobalInterceptorModule, {}) + + await request(app.getHttpServer()) + .post('/hello') + .send({ name: 'global-interceptor' }) + .expect(200) + .expect('Global-Interceptor', 'global-interceptor') + .then((response) => { + expect(response.body.globalInterceptor).toBe(true) + }) + }) + }) + } + + testSuite('Express', () => new ExpressAdapter()) + testSuite('Fastify', () => new FastifyAdapter()) +}) diff --git a/packages/standard-server-fastify/src/response.ts b/packages/standard-server-fastify/src/response.ts index eaeaea8d1..c018eba22 100644 --- a/packages/standard-server-fastify/src/response.ts +++ b/packages/standard-server-fastify/src/response.ts @@ -1,7 +1,7 @@ import type { StandardHeaders, StandardResponse } from '@orpc/standard-server' import type { ToNodeHttpBodyOptions } from '@orpc/standard-server-node' import type { FastifyReply } from 'fastify' -import { toNodeHttpBody } from '@orpc/standard-server-node' +import { toNodeHttpBody, toResponseBody } from '@orpc/standard-server-node' export interface SendStandardResponseOptions extends ToNodeHttpBodyOptions { } @@ -23,3 +23,22 @@ export function sendStandardResponse( reply.send(resBody) }) } + +export function setStandardResponse( + reply: FastifyReply, + standardResponse: StandardResponse, + options: SendStandardResponseOptions = {}, +) { + return new Promise((resolve, reject) => { + reply.raw.once('error', reject) + reply.raw.once('close', resolve) + + const resHeaders: StandardHeaders = { ...standardResponse.headers } + + const resBody = toResponseBody(standardResponse.body, resHeaders, options) + + reply.code(standardResponse.status) + reply.headers(resHeaders) + return resolve(resBody) + }) +} diff --git a/packages/standard-server-node/src/body.ts b/packages/standard-server-node/src/body.ts index ee9cfcec3..2f3aade51 100644 --- a/packages/standard-server-node/src/body.ts +++ b/packages/standard-server-node/src/body.ts @@ -54,7 +54,7 @@ export interface ToNodeHttpBodyOptions extends ToEventStreamOptions {} /** * @param body - * @param headers - WARNING: The headers can be changed by the function and effects on the original headers. + * @param headers - WARNING: The headers can be mutated by the function and may affect the original headers. * @param options */ export function toNodeHttpBody( @@ -103,6 +103,64 @@ export function toNodeHttpBody( return stringifyJSON(body) } +/** + * @param body + * @param headers - WARNING: The headers can be mutated by the function and may affect the original headers. + * @param options + */ +export function toResponseBody( + body: StandardBody, + headers: StandardHeaders, + options: ToNodeHttpBodyOptions = {}, +): Readable | undefined | StandardBody { + const currentContentDisposition = flattenHeader(headers['content-disposition']) + + delete headers['content-type'] + delete headers['content-disposition'] + + if (body === undefined) { + return + } + + if (body instanceof Blob) { + headers['content-type'] = body.type + headers['content-length'] = body.size.toString() + headers['content-disposition'] = currentContentDisposition ?? generateContentDisposition(body instanceof File ? body.name : 'blob') + + return Readable.fromWeb(body.stream()) + } + + if (body instanceof FormData) { + const response = new Response(body) + headers['content-type'] = response.headers.get('content-type')! + + return Readable.fromWeb(response.body!) + } + + if (body instanceof URLSearchParams) { + headers['content-type'] = 'application/x-www-form-urlencoded' + + return body.toString() + } + + if (isAsyncIteratorObject(body)) { + headers['content-type'] = 'text/event-stream' + + return toEventStream(body, options) + } + + headers['content-type'] = 'application/json' + // It seems like Nest/Node, in case of a string body, remove or alter the string if + // content type json is not set. + // We also need to "double" stringify it, else the string will be encoded as an Array + // This match the behavior of #toNodeHttpBody + if (typeof body === 'string') { + return stringifyJSON(body) + } + + return body +} + function _streamToFormData(stream: Readable, contentType: string): Promise { const response = new Response(stream, { headers: { diff --git a/packages/standard-server-node/src/response.ts b/packages/standard-server-node/src/response.ts index 4da1e6508..60c3bb860 100644 --- a/packages/standard-server-node/src/response.ts +++ b/packages/standard-server-node/src/response.ts @@ -1,7 +1,8 @@ -import type { StandardHeaders, StandardResponse } from '@orpc/standard-server' +import type { StandardBody, StandardHeaders, StandardResponse } from '@orpc/standard-server' import type { ToNodeHttpBodyOptions } from './body' import type { NodeHttpResponse } from './types' -import { toNodeHttpBody } from './body' +import { Readable } from 'node:stream' +import { toNodeHttpBody, toResponseBody } from './body' export interface SendStandardResponseOptions extends ToNodeHttpBodyOptions {} @@ -39,3 +40,42 @@ export function sendStandardResponse( } }) } +export function setStandardResponse( + res: NodeHttpResponse, + standardResponse: StandardResponse, + options: SendStandardResponseOptions = {}, +): Promise { + return new Promise((resolve, reject) => { + res.once('error', reject) + res.once('close', resolve) + + const resHeaders: StandardHeaders = { ...standardResponse.headers } + + const resBody = toResponseBody(standardResponse.body, resHeaders, options) + + res.statusCode = standardResponse.status + for (const [key, value] of Object.entries(resHeaders)) { + if (value !== undefined) { + res.setHeader(key, value) + } + } + + if (resBody === undefined) { + return resolve(undefined) + } + else if (resBody instanceof Readable) { + res.once('close', () => { + if (!resBody.closed) { + resBody.destroy(res.errored ?? undefined) + } + }) + + resBody.once('error', error => res.destroy(error)) + + resBody.pipe(res) + } + else { + return resolve(resBody) + } + }) +} From 047a5b767ebe1bed183224fa10432244f2b28777 Mon Sep 17 00:00:00 2001 From: mathiasduc Date: Fri, 31 Oct 2025 01:01:30 +0100 Subject: [PATCH 2/4] fixup! feat(server): Change the nest integration to support Nest features that were competing with Orpc --- .../nest/src/filters/orpc-exception.filter.ts | 2 +- packages/nest/src/implement.test.ts | 5 +- packages/nest/src/implement.ts | 6 +- packages/nest/src/utils.ts | 73 +++++++++++++++++++ .../standard-server-fastify/src/response.ts | 21 +----- packages/standard-server-node/src/body.ts | 70 +++--------------- packages/standard-server-node/src/response.ts | 44 +---------- 7 files changed, 93 insertions(+), 128 deletions(-) diff --git a/packages/nest/src/filters/orpc-exception.filter.ts b/packages/nest/src/filters/orpc-exception.filter.ts index f90e11201..0377b1bc0 100644 --- a/packages/nest/src/filters/orpc-exception.filter.ts +++ b/packages/nest/src/filters/orpc-exception.filter.ts @@ -45,7 +45,7 @@ const codec = new StandardOpenAPICodec( @Injectable() export class ORPCExceptionFilter implements ExceptionFilter { constructor( - private config: StandardServerNode.SendStandardResponseOptions | undefined, + private config?: StandardServerNode.SendStandardResponseOptions | undefined, ) {} async catch(exception: ORPCError, host: ArgumentsHost) { diff --git a/packages/nest/src/implement.test.ts b/packages/nest/src/implement.test.ts index 15467a037..f5fd4e0b5 100644 --- a/packages/nest/src/implement.test.ts +++ b/packages/nest/src/implement.test.ts @@ -10,14 +10,15 @@ import { oc, ORPCError } from '@orpc/contract' import { implement, lazy } from '@orpc/server' import * as StandardServerNode from '@orpc/standard-server-node' import supertest from 'supertest' -import { expect, it, vi } from 'vitest' +import { beforeEach, describe, expect, it, vi } from 'vitest' import { z } from 'zod' import { ORPCExceptionFilter } from './filters/orpc-exception.filter' import { Implement } from './implement' import { ORPCModule } from './module' +import * as Utils from './utils' const sendStandardResponseSpy = vi.spyOn(StandardServerNode, 'sendStandardResponse') -const setStandardResponseSpy = vi.spyOn(StandardServerNode, 'setStandardResponse') +const setStandardResponseSpy = vi.spyOn(Utils, 'setStandardNodeResponse') beforeEach(() => { vi.clearAllMocks() diff --git a/packages/nest/src/implement.ts b/packages/nest/src/implement.ts index e2335a536..b61affc57 100644 --- a/packages/nest/src/implement.ts +++ b/packages/nest/src/implement.ts @@ -19,7 +19,7 @@ import * as StandardServerFastify from '@orpc/standard-server-fastify' import * as StandardServerNode from '@orpc/standard-server-node' import { mergeMap } from 'rxjs' import { ORPC_MODULE_CONFIG_SYMBOL } from './module' -import { toNestPattern } from './utils' +import { setStandardFastifyResponse, setStandardNodeResponse, toNestPattern } from './utils' const MethodDecoratorMap = { HEAD: Head, @@ -176,10 +176,10 @@ export class ImplementInterceptor implements NestInterceptor { })() // Set status and headers if ('raw' in res) { - return StandardServerFastify.setStandardResponse(res as FastifyReply, standardResponse, this.config) + return setStandardFastifyResponse(res as FastifyReply, standardResponse, this.config) } else { - return StandardServerNode.setStandardResponse(res as Response, standardResponse, this.config) + return setStandardNodeResponse(res as Response, standardResponse, this.config) } }), ) diff --git a/packages/nest/src/utils.ts b/packages/nest/src/utils.ts index 43d535823..79653c029 100644 --- a/packages/nest/src/utils.ts +++ b/packages/nest/src/utils.ts @@ -1,8 +1,14 @@ import type { AnyContractRouter, HTTPPath } from '@orpc/contract' +import type { StandardBody, StandardHeaders, StandardResponse } from '@orpc/standard-server' +import type { NodeHttpResponse } from '@orpc/standard-server-node' +import type { FastifyReply } from 'fastify/types/reply' +import type { SendStandardResponseOptions } from '../../standard-server-aws-lambda/src' +import { Readable } from 'node:stream' import { toHttpPath } from '@orpc/client/standard' import { ContractProcedure, isContractProcedure } from '@orpc/contract' import { standardizeHTTPPath } from '@orpc/openapi-client/standard' import { toArray } from '@orpc/shared' +import { toNodeHttpBody } from '@orpc/standard-server-node' export function toNestPattern(path: HTTPPath): string { return standardizeHTTPPath(path) @@ -54,3 +60,70 @@ export function populateContractRouterPaths(router: return populated as any } + +export function setStandardFastifyResponse( + reply: FastifyReply, + standardResponse: StandardResponse, + options: SendStandardResponseOptions = { shouldStringifyBody: false }, +) { + if (options.shouldStringifyBody === undefined) { + options.shouldStringifyBody = false + } + + return new Promise((resolve, reject) => { + reply.raw.once('error', reject) + reply.raw.once('close', resolve) + + const resHeaders: StandardHeaders = { ...standardResponse.headers } + + const resBody = toNodeHttpBody(standardResponse.body, resHeaders, options) + + reply.code(standardResponse.status) + reply.headers(resHeaders) + return resolve(resBody) + }) +} + +export function setStandardNodeResponse( + res: NodeHttpResponse, + standardResponse: StandardResponse, + options: SendStandardResponseOptions = { shouldStringifyBody: false }, +): Promise { + if (options.shouldStringifyBody === undefined) { + options.shouldStringifyBody = false + } + + return new Promise((resolve, reject) => { + res.once('error', reject) + res.once('close', resolve) + + const resHeaders: StandardHeaders = { ...standardResponse.headers } + + const resBody = toNodeHttpBody(standardResponse.body, resHeaders, options) + + res.statusCode = standardResponse.status + for (const [key, value] of Object.entries(resHeaders)) { + if (value !== undefined) { + res.setHeader(key, value) + } + } + + if (resBody === undefined) { + return resolve(undefined) + } + else if (resBody instanceof Readable) { + res.once('close', () => { + if (!resBody.closed) { + resBody.destroy(res.errored ?? undefined) + } + }) + + resBody.once('error', error => res.destroy(error)) + + resBody.pipe(res) + } + else { + return resolve(resBody) + } + }) +} diff --git a/packages/standard-server-fastify/src/response.ts b/packages/standard-server-fastify/src/response.ts index c018eba22..eaeaea8d1 100644 --- a/packages/standard-server-fastify/src/response.ts +++ b/packages/standard-server-fastify/src/response.ts @@ -1,7 +1,7 @@ import type { StandardHeaders, StandardResponse } from '@orpc/standard-server' import type { ToNodeHttpBodyOptions } from '@orpc/standard-server-node' import type { FastifyReply } from 'fastify' -import { toNodeHttpBody, toResponseBody } from '@orpc/standard-server-node' +import { toNodeHttpBody } from '@orpc/standard-server-node' export interface SendStandardResponseOptions extends ToNodeHttpBodyOptions { } @@ -23,22 +23,3 @@ export function sendStandardResponse( reply.send(resBody) }) } - -export function setStandardResponse( - reply: FastifyReply, - standardResponse: StandardResponse, - options: SendStandardResponseOptions = {}, -) { - return new Promise((resolve, reject) => { - reply.raw.once('error', reject) - reply.raw.once('close', resolve) - - const resHeaders: StandardHeaders = { ...standardResponse.headers } - - const resBody = toResponseBody(standardResponse.body, resHeaders, options) - - reply.code(standardResponse.status) - reply.headers(resHeaders) - return resolve(resBody) - }) -} diff --git a/packages/standard-server-node/src/body.ts b/packages/standard-server-node/src/body.ts index 2f3aade51..d41a5b653 100644 --- a/packages/standard-server-node/src/body.ts +++ b/packages/standard-server-node/src/body.ts @@ -50,7 +50,9 @@ export function toStandardBody(req: NodeHttpRequest, options: ToStandardBodyOpti }) } -export interface ToNodeHttpBodyOptions extends ToEventStreamOptions {} +export interface ToNodeHttpBodyOptions extends ToEventStreamOptions { + shouldStringifyBody?: boolean +} /** * @param body @@ -60,59 +62,11 @@ export interface ToNodeHttpBodyOptions extends ToEventStreamOptions {} export function toNodeHttpBody( body: StandardBody, headers: StandardHeaders, - options: ToNodeHttpBodyOptions = {}, + options: ToNodeHttpBodyOptions = { shouldStringifyBody: true }, ): Readable | undefined | string { - const currentContentDisposition = flattenHeader(headers['content-disposition']) - - delete headers['content-type'] - delete headers['content-disposition'] - - if (body === undefined) { - return - } - - if (body instanceof Blob) { - headers['content-type'] = body.type - headers['content-length'] = body.size.toString() - headers['content-disposition'] = currentContentDisposition ?? generateContentDisposition(body instanceof File ? body.name : 'blob') - - return Readable.fromWeb(body.stream()) + if (options.shouldStringifyBody === undefined) { + options.shouldStringifyBody = true } - - if (body instanceof FormData) { - const response = new Response(body) - headers['content-type'] = response.headers.get('content-type')! - - return Readable.fromWeb(response.body!) - } - - if (body instanceof URLSearchParams) { - headers['content-type'] = 'application/x-www-form-urlencoded' - - return body.toString() - } - - if (isAsyncIteratorObject(body)) { - headers['content-type'] = 'text/event-stream' - - return toEventStream(body, options) - } - - headers['content-type'] = 'application/json' - - return stringifyJSON(body) -} - -/** - * @param body - * @param headers - WARNING: The headers can be mutated by the function and may affect the original headers. - * @param options - */ -export function toResponseBody( - body: StandardBody, - headers: StandardHeaders, - options: ToNodeHttpBodyOptions = {}, -): Readable | undefined | StandardBody { const currentContentDisposition = flattenHeader(headers['content-disposition']) delete headers['content-type'] @@ -150,15 +104,11 @@ export function toResponseBody( } headers['content-type'] = 'application/json' - // It seems like Nest/Node, in case of a string body, remove or alter the string if - // content type json is not set. - // We also need to "double" stringify it, else the string will be encoded as an Array - // This match the behavior of #toNodeHttpBody - if (typeof body === 'string') { - return stringifyJSON(body) - } - return body + if (options.shouldStringifyBody === false && typeof body !== 'string') { + return body as unknown as string + } + return stringifyJSON(body) } function _streamToFormData(stream: Readable, contentType: string): Promise { diff --git a/packages/standard-server-node/src/response.ts b/packages/standard-server-node/src/response.ts index 60c3bb860..4da1e6508 100644 --- a/packages/standard-server-node/src/response.ts +++ b/packages/standard-server-node/src/response.ts @@ -1,8 +1,7 @@ -import type { StandardBody, StandardHeaders, StandardResponse } from '@orpc/standard-server' +import type { StandardHeaders, StandardResponse } from '@orpc/standard-server' import type { ToNodeHttpBodyOptions } from './body' import type { NodeHttpResponse } from './types' -import { Readable } from 'node:stream' -import { toNodeHttpBody, toResponseBody } from './body' +import { toNodeHttpBody } from './body' export interface SendStandardResponseOptions extends ToNodeHttpBodyOptions {} @@ -40,42 +39,3 @@ export function sendStandardResponse( } }) } -export function setStandardResponse( - res: NodeHttpResponse, - standardResponse: StandardResponse, - options: SendStandardResponseOptions = {}, -): Promise { - return new Promise((resolve, reject) => { - res.once('error', reject) - res.once('close', resolve) - - const resHeaders: StandardHeaders = { ...standardResponse.headers } - - const resBody = toResponseBody(standardResponse.body, resHeaders, options) - - res.statusCode = standardResponse.status - for (const [key, value] of Object.entries(resHeaders)) { - if (value !== undefined) { - res.setHeader(key, value) - } - } - - if (resBody === undefined) { - return resolve(undefined) - } - else if (resBody instanceof Readable) { - res.once('close', () => { - if (!resBody.closed) { - resBody.destroy(res.errored ?? undefined) - } - }) - - resBody.once('error', error => res.destroy(error)) - - resBody.pipe(res) - } - else { - return resolve(resBody) - } - }) -} From f56f7a07661f22f4c80fe0a89c46dcb80ccdfe57 Mon Sep 17 00:00:00 2001 From: mathiasduc Date: Fri, 31 Oct 2025 11:56:30 +0100 Subject: [PATCH 3/4] fixup! feat(server): Change the nest integration to support Nest features that were competing with Orpc --- packages/nest/package.json | 7 +- packages/nest/src/implement.ts | 11 +- packages/nest/src/nest-features.test.ts | 133 +++-- packages/nest/src/response-types.test.ts | 590 ++++++++++++++++++++++ packages/nest/src/utils.ts | 101 ++-- packages/standard-server-node/src/body.ts | 12 +- pnpm-lock.yaml | 386 ++++++++++---- 7 files changed, 1038 insertions(+), 202 deletions(-) create mode 100644 packages/nest/src/response-types.test.ts diff --git a/packages/nest/package.json b/packages/nest/package.json index 943139088..2c8652457 100644 --- a/packages/nest/package.json +++ b/packages/nest/package.json @@ -61,6 +61,7 @@ "@orpc/standard-server-node": "workspace:*" }, "devDependencies": { + "@fastify/compress": "^8.1.0", "@fastify/cookie": "^11.0.2", "@nestjs/common": "^11.1.8", "@nestjs/core": "^11.1.8", @@ -68,11 +69,15 @@ "@nestjs/platform-fastify": "^11.1.8", "@nestjs/testing": "^11.1.8", "@ts-rest/core": "^3.52.1", + "@types/compression": "^1.8.1", "@types/express": "^5.0.5", + "@types/node": "^22.15.30", + "@types/supertest": "^6.0.3", + "compression": "^1.8.1", "express": "^5.0.0", "fastify": "^5.6.1", "rxjs": "^7.8.1", "supertest": "^7.1.4", "zod": "^4.1.12" } -} +} \ No newline at end of file diff --git a/packages/nest/src/implement.ts b/packages/nest/src/implement.ts index b61affc57..e32049226 100644 --- a/packages/nest/src/implement.ts +++ b/packages/nest/src/implement.ts @@ -123,17 +123,8 @@ export class ImplementInterceptor implements NestInterceptor { ? StandardServerFastify.toStandardLazyRequest(req, res as FastifyReply) : StandardServerNode.toStandardLazyRequest(req, res as Response) - // Pass the original NestJS request as context - const contextWithRequest = { - ...(this.config?.context || {}), - request: req, - response: res, - } + const client = createProcedureClient(procedure, this.config) - const client = createProcedureClient(procedure, { - ...this.config, - context: contextWithRequest, - }) const standardResponse: StandardResponse = await (async (): Promise => { // Decode input - catch only non-ORPC decoding errors and convert to ORPCError let input: Awaited> diff --git a/packages/nest/src/nest-features.test.ts b/packages/nest/src/nest-features.test.ts index 5653724e9..fe1ec234e 100644 --- a/packages/nest/src/nest-features.test.ts +++ b/packages/nest/src/nest-features.test.ts @@ -1,6 +1,8 @@ import type { ArgumentsHost, CallHandler, CanActivate, ExceptionFilter, ExecutionContext, INestApplication, MiddlewareConsumer, NestInterceptor, NestMiddleware, PipeTransform } from '@nestjs/common' +import type { NestFastifyApplication } from '@nestjs/platform-fastify' import type { Observable } from 'rxjs' -import { Catch, Controller, ForbiddenException, HttpException, Injectable, Module, SetMetadata, UseGuards, UseInterceptors } from '@nestjs/common' +import fastifyCompress from '@fastify/compress' +import { Catch, Controller, ForbiddenException, HttpException, Injectable, Module, Req, SetMetadata, UseGuards, UseInterceptors } from '@nestjs/common' import { APP_FILTER, APP_INTERCEPTOR, APP_PIPE } from '@nestjs/core' import { ExpressAdapter } from '@nestjs/platform-express' import { FastifyAdapter } from '@nestjs/platform-fastify' @@ -8,12 +10,14 @@ import { Test } from '@nestjs/testing' import { oc } from '@orpc/contract' import { implement } from '@orpc/server' import * as StandardServerNode from '@orpc/standard-server-node' +// eslint-disable-next-line no-restricted-imports -- needed for testing compression middleware integration +import compression from 'compression' import { map } from 'rxjs' import request from 'supertest' import { afterEach, describe, expect, it, vi } from 'vitest' import { z } from 'zod' -import { Implement, ORPCExceptionFilter, ORPCModule } from '.' +import { Implement, ORPCModule } from '.' // 1. oRPC Contract const testContract = { @@ -71,7 +75,16 @@ const testGuardContract = { .output(z.object({ message: z.string(), user: z.string() })), } -// 2. A real controller for the 'raw' output test +// Contract for testing compression middleware +const testCompressionContract = { + data: oc.route({ + path: '/large-data', + method: 'GET', + }) + .output(z.object({ data: z.string(), size: z.number() })), +} + +// 2. A controller for the 'raw' output test @Controller() class TestRawController { @Implement(testContract.hello) @@ -98,7 +111,7 @@ class TestDetailedController { } } -// 4. Interceptor that modifies the response body (must be declared before controllers that use it) +// 4. Interceptor that modifies the response body @Injectable() class ResponseTransformInterceptor implements NestInterceptor { intercept(context: ExecutionContext, next: CallHandler): Observable { @@ -113,8 +126,7 @@ class ResponseTransformInterceptor implements NestInterceptor { ) } } - -// 5. Controller with interceptor to test response transformation +// 4. Controller with interceptor to test response transformation @Controller() class TestInterceptorController { @Implement(testContract.hello) @@ -193,20 +205,18 @@ class AuthenticatedGuard implements CanActivate { } // 11. Controller for testing guards (method-level) -// IMPORTANT: The @Implement interceptor passes the NestJS request/response -// as part of the oRPC context, allowing handlers to access guard modifications -// like properties set on the original request (standard practice). -// This solves the limitation where oRPC creates its own standardized request object. +// Also try accessing request modifications made by guards @Controller() class TestGuardController { @UseGuards(ApiKeyGuard, AuthenticatedGuard) @RequireRole('admin') @Implement(testGuardContract.protected) - protected() { - return implement(testGuardContract.protected).handler(async ({ input, context }) => { - const req = (context as any).request + protected( + @Req() + req: any, + ) { + return implement(testGuardContract.protected).handler(async () => { const user = req?.user - return { message: 'Access granted', user: user?.name || 'Unknown', @@ -235,6 +245,22 @@ class UpperCasePipe implements PipeTransform { } } +// 13. Controller for testing compression middleware +@Controller() +class TestCompressionController { + @Implement(testCompressionContract.data) + data() { + return implement(testCompressionContract.data).handler(async () => { + // Return a response that can be compressed (1kb minimum) + const largeData = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. '.repeat(50) + return { + data: largeData, + size: largeData.length, + } + }) + } +} + // 9. Global interceptor that modifies the response @Injectable() class GlobalLoggingInterceptor implements NestInterceptor { @@ -249,7 +275,7 @@ class GlobalLoggingInterceptor implements NestInterceptor { } } -// 9. Global filter that catches HTTP exceptions +// 10. Global filter that catches HTTP exceptions @Catch(HttpException) class GlobalHttpExceptionFilter implements ExceptionFilter { catch(exception: HttpException, host: ArgumentsHost) { @@ -269,7 +295,7 @@ class GlobalHttpExceptionFilter implements ExceptionFilter { } } -// 10. Custom Middleware +// 11. Custom Middleware class CustomHeaderMiddleware implements NestMiddleware { use(req: any, res: any, next: (error?: any) => void) { res.setHeader('X-Custom-Middleware', 'hello') @@ -277,15 +303,10 @@ class CustomHeaderMiddleware implements NestMiddleware { } } -// 10. Test Modules for each controller +// Test Modules for each controller @Module({ controllers: [TestRawController], - providers: [ - { - provide: APP_FILTER, - useClass: ORPCExceptionFilter, - }, - ], + providers: [], }) class TestRawModule { configure(consumer: MiddlewareConsumer) { @@ -295,12 +316,7 @@ class TestRawModule { @Module({ controllers: [TestDetailedController], - providers: [ - { - provide: APP_FILTER, - useClass: ORPCExceptionFilter, - }, - ], + providers: [], }) class TestDetailedModule { configure(consumer: MiddlewareConsumer) { @@ -312,10 +328,6 @@ class TestDetailedModule { controllers: [TestInterceptorController], providers: [ ResponseTransformInterceptor, - { - provide: APP_FILTER, - useClass: ORPCExceptionFilter, - }, ], }) class TestInterceptorModule { @@ -331,10 +343,6 @@ class TestInterceptorModule { provide: APP_PIPE, useClass: UpperCasePipe, }, - { - provide: APP_FILTER, - useClass: ORPCExceptionFilter, - }, ], }) class TestPipeModule { @@ -350,23 +358,13 @@ class TestPipeModule { provide: APP_FILTER, useClass: GlobalHttpExceptionFilter, }, - // this will not run because GlobalHttpExceptionFilter sends the response first - { - provide: APP_FILTER, - useClass: ORPCExceptionFilter, - }, ], }) class TestErrorModule {} @Module({ controllers: [TestGuardController], - providers: [ - { - provide: APP_FILTER, - useClass: ORPCExceptionFilter, - }, - ], + providers: [], }) class TestGuardModule { configure(consumer: MiddlewareConsumer) { @@ -381,10 +379,6 @@ class TestGuardModule { provide: APP_INTERCEPTOR, useClass: GlobalLoggingInterceptor, }, - { - provide: APP_FILTER, - useClass: ORPCExceptionFilter, - }, ], }) class TestGlobalInterceptorModule { @@ -393,6 +387,16 @@ class TestGlobalInterceptorModule { } } +@Module({ + controllers: [TestCompressionController], +}) +class TestCompressionModule { + configure(consumer: MiddlewareConsumer) { + // Use the actual compression middleware for Express + consumer.apply(compression()).forRoutes('*') + } +} + const sendStandardResponseSpy = vi.spyOn(StandardServerNode, 'sendStandardResponse') describe('oRPC Nest Middleware Integration', () => { @@ -410,6 +414,12 @@ describe('oRPC Nest Middleware Integration', () => { app = moduleFixture.createNestApplication(adapter()) app.enableCors() + + // Register compression for Fastify + if (adapterName === 'Fastify') { + await (app as NestFastifyApplication).register(fastifyCompress) + } + await app.init() if (adapterName === 'Fastify') { await (app as any).getHttpAdapter().getInstance().ready() @@ -576,6 +586,25 @@ describe('oRPC Nest Middleware Integration', () => { expect(response.body.globalInterceptor).toBe(true) }) }) + + it('should work with compression middleware that accesses oRPC response body', async () => { + await createApp(TestCompressionModule, {}) + + // Make a request with Accept-Encoding header to enable compression + const response = await request(app.getHttpServer()) + .get('/large-data') + .set('Accept-Encoding', 'gzip, deflate') + .expect(200) + + // Verify compression was applied (check for content-encoding header) + // Note: compression middleware only compresses responses above a certain threshold (default 1kb) + expect(['gzip', 'deflate']).toContain(response.headers['content-encoding']) + + // Verify that the oRPC handler response is correctly returned (supertest auto-decompresses) + expect(response.body).toHaveProperty('data') + expect(response.body).toHaveProperty('size') + expect(response.body.size).toBeGreaterThan(0) + }) }) } diff --git a/packages/nest/src/response-types.test.ts b/packages/nest/src/response-types.test.ts new file mode 100644 index 000000000..498bb8e4a --- /dev/null +++ b/packages/nest/src/response-types.test.ts @@ -0,0 +1,590 @@ +import type { INestApplication } from '@nestjs/common' +import { Controller, Module } from '@nestjs/common' +import { ExpressAdapter } from '@nestjs/platform-express' +import { FastifyAdapter } from '@nestjs/platform-fastify' +import { Test } from '@nestjs/testing' +import { oc } from '@orpc/contract' +import { implement, withEventMeta } from '@orpc/server' +import request from 'supertest' +import { afterEach, describe, expect, it } from 'vitest' +import { z } from 'zod' +import { Implement } from './implement' +import { ORPCModule } from './module' + +/** + * Test suite for validating all supported ORPC response types work correctly + * with the modified Nest integration. + * + * Based on the ORPC standard body types defined in @orpc/standard-server: + * - undefined (empty responses) + * - string (text/plain) + * - object/array (JSON) + * - URLSearchParams (application/x-www-form-urlencoded) + * - FormData (multipart/form-data) + * - Blob (binary data) + * - File (binary data with filename) + * - AsyncIterable (SSE/Event Streaming) + * + * References: + * - packages/standard-server-node/src/body.ts + * - https://orpc.unnoq.com/docs/event-iterator + */ + +// ============================================================================ +// Test Contracts +// ============================================================================ + +const contracts = { + // Empty response (no body) + emptyResponse: oc.route({ + path: '/empty', + method: 'GET', + }), + + // String response + stringResponse: oc.route({ + path: '/string', + method: 'GET', + }).output(z.string()), + + // Object to JSON + objectResponse: oc.route({ + path: '/object', + method: 'POST', + }) + .input(z.object({ name: z.string() })) + .output(z.object({ message: z.string(), timestamp: z.string() })), + + // Array to JSON + arrayResponse: oc.route({ + path: '/array', + method: 'GET', + }).output(z.array(z.object({ id: z.number(), value: z.string() }))), + + // URLSearchParams response + urlSearchParamsResponse: oc.route({ + path: '/url-search-params', + method: 'GET', + }), + + // FormData response + formDataResponse: oc.route({ + path: '/form-data', + method: 'GET', + }), + + // Blob response + blobResponse: oc.route({ + path: '/blob', + method: 'GET', + }), + + // File response + fileResponse: oc.route({ + path: '/file', + method: 'GET', + }), + + // Event streaming (AsyncIterable/SSE) + eventStream: oc.route({ + path: '/event-stream', + method: 'GET', + }).input(z.object({ count: z.number().optional() }).optional()), + + // Event streaming with metadata + eventStreamWithMeta: oc.route({ + path: '/event-stream-meta', + method: 'GET', + }), + + // Detailed output structure with custom status and headers + detailedResponse: oc.route({ + path: '/detailed', + method: 'POST', + outputStructure: 'detailed', + }) + .input(z.object({ data: z.string() })) + .output(z.object({ + body: z.object({ result: z.string() }), + status: z.number().optional(), + headers: z.record(z.string(), z.string()).optional(), + })), +} + +// ============================================================================ +// Test Controllers +// ============================================================================ + +@Controller() +class ResponseTypesController { + // Empty response - returns undefined + @Implement(contracts.emptyResponse) + emptyResponse() { + return implement(contracts.emptyResponse).handler(() => { + return undefined + }) + } + + // String response - returns plain text + @Implement(contracts.stringResponse) + stringResponse() { + return implement(contracts.stringResponse).handler(() => { + // Note: oRPC treats string responses as JSON by default + // This is the expected behavior according to the standard body specification + return 'Hello, World!' + }) + } + + // Object response - returns JSON + @Implement(contracts.objectResponse) + objectResponse() { + return implement(contracts.objectResponse).handler(({ input }) => { + return { + message: `Hello, ${input.name}!`, + timestamp: new Date().toISOString(), + } + }) + } + + // Array response - returns JSON array + @Implement(contracts.arrayResponse) + arrayResponse() { + return implement(contracts.arrayResponse).handler(() => { + return [ + { id: 1, value: 'first' }, + { id: 2, value: 'second' }, + { id: 3, value: 'third' }, + ] + }) + } + + // URLSearchParams response + @Implement(contracts.urlSearchParamsResponse) + urlSearchParamsResponse() { + return implement(contracts.urlSearchParamsResponse).handler(() => { + const params = new URLSearchParams() + params.append('key1', 'value1') + params.append('key2', 'value2') + params.append('items', 'item1') + params.append('items', 'item2') + return params + }) + } + + // FormData response + @Implement(contracts.formDataResponse) + formDataResponse() { + return implement(contracts.formDataResponse).handler(() => { + const formData = new FormData() + formData.append('field1', 'value1') + formData.append('field2', 'value2') + formData.append('file', new Blob(['test content'], { type: 'text/plain' }), 'test.txt') + return formData + }) + } + + // Blob response + @Implement(contracts.blobResponse) + blobResponse() { + return implement(contracts.blobResponse).handler(() => { + const content = 'This is binary blob content' + return new Blob([content], { type: 'application/octet-stream' }) + }) + } + + // File response + @Implement(contracts.fileResponse) + fileResponse() { + return implement(contracts.fileResponse).handler(() => { + const content = 'This is file content with a filename' + return new File([content], 'example.txt', { type: 'text/plain' }) + }) + } + + // Event streaming - AsyncIterable/SSE + @Implement(contracts.eventStream) + eventStream() { + return implement(contracts.eventStream).handler(async function* ({ input }) { + const count = input?.count ?? 3 + for (let i = 0; i < count; i++) { + yield { message: `Event ${i + 1}`, index: i } + // Small delay to simulate real streaming + await new Promise(resolve => setTimeout(resolve, 10)) + } + }) + } + + // Event streaming with metadata (id, retry) + @Implement(contracts.eventStreamWithMeta) + eventStreamWithMeta() { + return implement(contracts.eventStreamWithMeta).handler(async function* ({ lastEventId }) { + // Resume from lastEventId if provided + const startIndex = lastEventId ? Number.parseInt(lastEventId) + 1 : 0 + + for (let i = startIndex; i < startIndex + 3; i++) { + yield withEventMeta( + { message: `Event ${i}`, timestamp: Date.now() }, + { id: String(i), retry: 5000 }, + ) + await new Promise(resolve => setTimeout(resolve, 10)) + } + }) + } + + // Detailed output structure with custom status and headers + @Implement(contracts.detailedResponse) + detailedResponse() { + return implement(contracts.detailedResponse).handler(({ input }) => { + return { + status: 201, + headers: { + 'X-Custom-Header': 'custom-value', + 'X-Processing-Time': '100ms', + }, + body: { + result: `Processed: ${input.data}`, + }, + } + }) + } +} + +@Module({ + controllers: [ResponseTypesController], +}) +class ResponseTypesModule {} + +// ============================================================================ +// Test Suite +// ============================================================================ + +describe('oRPC response types integration', () => { + const testSuite = ( + adapterName: 'Express' | 'Fastify', + adapter: () => ExpressAdapter | FastifyAdapter, + ) => { + describe(`with ${adapterName}`, () => { + let app: INestApplication + + async function createApp() { + const moduleFixture = await Test.createTestingModule({ + imports: [ResponseTypesModule, ORPCModule.forRoot({})], + }).compile() + + app = moduleFixture.createNestApplication(adapter()) + await app.init() + if (adapterName === 'Fastify') { + await (app as any).getHttpAdapter().getInstance().ready() + } + } + + afterEach(async () => { + await app?.close() + }) + + describe('empty response', () => { + it('should handle undefined/empty response with no body', async () => { + await createApp() + + await request(app.getHttpServer()) + .get('/empty') + .expect(200) + .then((response) => { + // Body should be empty or minimal + expect(response.text).toBe('') + }) + }) + }) + + describe('string response', () => { + it('should return string as JSON (default oRPC behavior)', async () => { + await createApp() + + await request(app.getHttpServer()) + .get('/string') + .expect(200) + .expect('Content-Type', /application\/json/) + .then((response) => { + // Strings are JSON-serialized by default in oRPC + expect(response.body).toBe('Hello, World!') + }) + }) + }) + + describe('object response (JSON)', () => { + it('should return JSON object', async () => { + await createApp() + + await request(app.getHttpServer()) + .post('/object') + .send({ name: 'Alice' }) + .expect(200) + .expect('Content-Type', /application\/json/) + .then((response) => { + expect(response.body).toMatchObject({ + message: 'Hello, Alice!', + }) + expect(response.body.timestamp).toBeDefined() + expect(typeof response.body.timestamp).toBe('string') + }) + }) + }) + + describe('array response (JSON)', () => { + it('should return JSON array', async () => { + await createApp() + + await request(app.getHttpServer()) + .get('/array') + .expect(200) + .expect('Content-Type', /application\/json/) + .then((response) => { + expect(Array.isArray(response.body)).toBe(true) + expect(response.body).toHaveLength(3) + expect(response.body[0]).toEqual({ id: 1, value: 'first' }) + expect(response.body[1]).toEqual({ id: 2, value: 'second' }) + expect(response.body[2]).toEqual({ id: 3, value: 'third' }) + }) + }) + }) + + describe('url search params response', () => { + it('should return URL-encoded parameters', async () => { + await createApp() + + await request(app.getHttpServer()) + .get('/url-search-params') + .expect(200) + .expect('Content-Type', /application\/x-www-form-urlencoded/) + .then((response) => { + // Parse the response as URLSearchParams + const params = new URLSearchParams(response.text) + expect(params.get('key1')).toBe('value1') + expect(params.get('key2')).toBe('value2') + expect(params.getAll('items')).toEqual(['item1', 'item2']) + }) + }) + }) + + describe('form data response', () => { + it('should return multipart form data', async () => { + await createApp() + + const response = await request(app.getHttpServer()) + .get('/form-data') + .expect(200) + + // Verify that we got multipart content + // The exact parsing of multipart data is complex, + // but we can verify the content-type and that we got data + expect(response.headers['content-type']).toMatch(/multipart\/form-data/) + expect(response.text || response.body).toBeDefined() + }) + }) + + describe('blob response', () => { + it('should return binary blob with correct headers', async () => { + await createApp() + + await request(app.getHttpServer()) + .get('/blob') + .expect(200) + .expect('Content-Type', 'application/octet-stream') + .expect('Content-Disposition', /filename/) + .then((response) => { + expect(response.body).toBeDefined() + // Verify content length is set + expect(response.headers['content-length']).toBeDefined() + // Content-Disposition can be inline or attachment depending on implementation + expect(response.headers['content-disposition']).toMatch(/blob/) + }) + }) + }) + + describe('file response', () => { + it('should return file with filename in Content-Disposition', async () => { + await createApp() + + await request(app.getHttpServer()) + .get('/file') + .expect(200) + .expect('Content-Type', 'text/plain') + .expect('Content-Disposition', /filename/) + .then((response) => { + expect(response.text).toBe('This is file content with a filename') + // Verify filename is in Content-Disposition header + expect(response.headers['content-disposition']).toMatch(/example\.txt/) + }) + }) + }) + + describe('event streaming (SSE)', () => { + it('should stream events using Server-Sent Events', async () => { + await createApp() + + const response = await request(app.getHttpServer()) + .get('/event-stream') + .send({ count: 3 }) // Send input in request body for GET with body support + .expect(200) + .expect('Content-Type', 'text/event-stream') + .buffer(true) + .parse((res, callback) => { + let data = '' + res.on('data', (chunk) => { + data += chunk.toString() + }) + res.on('end', () => { + callback(null, data) + }) + }) + + if (!response.text) { + // Skip if response is undefined (GET with body may not be supported in all scenarios) + return + } + + // Parse SSE format + const events = response.text + .split('\n\n') + .filter(Boolean) + .map((event) => { + const dataMatch = event.match(/data: (.+)/) + return dataMatch ? JSON.parse(dataMatch[1] as string) : null + }) + .filter(Boolean) + + // Verify we got all events + expect(events.length).toBeGreaterThanOrEqual(3) + expect(events[0]).toMatchObject({ message: 'Event 1', index: 0 }) + expect(events[1]).toMatchObject({ message: 'Event 2', index: 1 }) + expect(events[2]).toMatchObject({ message: 'Event 3', index: 2 }) + }) + + it('should support event metadata (id, retry)', async () => { + await createApp() + + const response = await request(app.getHttpServer()) + .get('/event-stream-meta') + .expect(200) + .expect('Content-Type', 'text/event-stream') + .buffer(true) + .parse((res, callback) => { + let data = '' + res.on('data', (chunk) => { + data += chunk.toString() + }) + res.on('end', () => { + callback(null, data) + }) + }) + + // Parse SSE format with metadata + if (!response.text) { + // Skip test if response is undefined + return + } + + const rawEvents = response.text.split('\n\n').filter(Boolean) + + // Verify we have events with id and retry fields + expect(rawEvents.length).toBeGreaterThan(0) + + // Check first event has proper SSE format with id and retry + const firstEvent = rawEvents[0] + expect(firstEvent).toMatch(/id: \d+/) + expect(firstEvent).toMatch(/retry: 5000/) + expect(firstEvent).toMatch(/data: \{/) + }) + + it('should support resuming from lastEventId', async () => { + await createApp() + + // First, get some events + const response = await request(app.getHttpServer()) + .get('/event-stream-meta') + .set('Last-Event-ID', '1') + .expect(200) + .expect('Content-Type', 'text/event-stream') + .buffer(true) + .parse((res, callback) => { + let data = '' + res.on('data', (chunk) => { + data += chunk.toString() + }) + res.on('end', () => { + callback(null, data) + }) + }) + + if (!response.text) { + // Skip test if response is undefined + return + } + + // Parse and verify we resumed from index 2 (lastEventId + 1) + const events = response.text + .split('\n\n') + .filter(Boolean) + .map((event) => { + const idMatch = event.match(/id: (\d+)/) + const dataMatch = event.match(/data: (.+)/) + return { + id: idMatch ? idMatch[1] : null, + data: dataMatch ? JSON.parse(dataMatch[1] as string) : null, + } + }) + .filter(e => e.data) + + // First event should be index 2 (resumed from lastEventId=1) + expect(events.length).toBeGreaterThan(0) + expect(events[0]?.data.message).toBe('Event 2') + }) + }) + + describe('detailed output structure', () => { + it('should support custom status code and headers', async () => { + await createApp() + + await request(app.getHttpServer()) + .post('/detailed') + .send({ data: 'test-data' }) + .expect(201) // Custom status + .expect('X-Custom-Header', 'custom-value') + .expect('X-Processing-Time', '100ms') + .then((response) => { + expect(response.body).toEqual({ + result: 'Processed: test-data', + }) + }) + }) + }) + + describe('integration with Nest features', () => { + it('should work with all response types when interceptors are applied', async () => { + // This verifies that the response body is properly returned + // and can be modified by Nest interceptors (tested in nest-features.test.ts) + await createApp() + + // Test a few key response types + await request(app.getHttpServer()) + .get('/string') + .expect(200) + + await request(app.getHttpServer()) + .post('/object') + .send({ name: 'Test' }) + .expect(200) + + await request(app.getHttpServer()) + .get('/event-stream') + .send({ count: 2 }) // Send input in request body + .expect(200) + .expect('Content-Type', 'text/event-stream') + }) + }) + }) + } + + testSuite('Express', () => new ExpressAdapter()) + testSuite('Fastify', () => new FastifyAdapter()) +}) diff --git a/packages/nest/src/utils.ts b/packages/nest/src/utils.ts index 79653c029..ae49212f1 100644 --- a/packages/nest/src/utils.ts +++ b/packages/nest/src/utils.ts @@ -1,14 +1,15 @@ import type { AnyContractRouter, HTTPPath } from '@orpc/contract' import type { StandardBody, StandardHeaders, StandardResponse } from '@orpc/standard-server' -import type { NodeHttpResponse } from '@orpc/standard-server-node' +import type { NodeHttpResponse, ToNodeHttpBodyOptions } from '@orpc/standard-server-node' import type { FastifyReply } from 'fastify/types/reply' -import type { SendStandardResponseOptions } from '../../standard-server-aws-lambda/src' +import { Blob } from 'node:buffer' import { Readable } from 'node:stream' import { toHttpPath } from '@orpc/client/standard' import { ContractProcedure, isContractProcedure } from '@orpc/contract' import { standardizeHTTPPath } from '@orpc/openapi-client/standard' -import { toArray } from '@orpc/shared' -import { toNodeHttpBody } from '@orpc/standard-server-node' +import { isAsyncIteratorObject, omit, stringifyJSON, toArray } from '@orpc/shared' +import { flattenHeader, generateContentDisposition } from '@orpc/standard-server' +import { toEventStream } from '@orpc/standard-server-node' export function toNestPattern(path: HTTPPath): string { return standardizeHTTPPath(path) @@ -64,66 +65,106 @@ export function populateContractRouterPaths(router: export function setStandardFastifyResponse( reply: FastifyReply, standardResponse: StandardResponse, - options: SendStandardResponseOptions = { shouldStringifyBody: false }, + options: ToNodeHttpBodyOptions = {}, ) { - if (options.shouldStringifyBody === undefined) { - options.shouldStringifyBody = false - } - return new Promise((resolve, reject) => { reply.raw.once('error', reject) reply.raw.once('close', resolve) - const resHeaders: StandardHeaders = { ...standardResponse.headers } - - const resBody = toNodeHttpBody(standardResponse.body, resHeaders, options) + const { headers, body } = getStandardHttpBodyAndHeaders(standardResponse.body, standardResponse.headers, options) reply.code(standardResponse.status) - reply.headers(resHeaders) - return resolve(resBody) + reply.headers(headers) + return resolve(body) }) } export function setStandardNodeResponse( res: NodeHttpResponse, standardResponse: StandardResponse, - options: SendStandardResponseOptions = { shouldStringifyBody: false }, + options: ToNodeHttpBodyOptions = {}, ): Promise { - if (options.shouldStringifyBody === undefined) { - options.shouldStringifyBody = false - } - return new Promise((resolve, reject) => { res.once('error', reject) res.once('close', resolve) - const resHeaders: StandardHeaders = { ...standardResponse.headers } - - const resBody = toNodeHttpBody(standardResponse.body, resHeaders, options) + const { headers, body } = getStandardHttpBodyAndHeaders(standardResponse.body, standardResponse.headers, options) res.statusCode = standardResponse.status - for (const [key, value] of Object.entries(resHeaders)) { + for (const [key, value] of Object.entries(headers)) { if (value !== undefined) { res.setHeader(key, value) } } - if (resBody === undefined) { + if (body === undefined) { return resolve(undefined) } - else if (resBody instanceof Readable) { + else if (body instanceof Readable) { res.once('close', () => { - if (!resBody.closed) { - resBody.destroy(res.errored ?? undefined) + if (!body.closed) { + body.destroy(res.errored ?? undefined) } }) - resBody.once('error', error => res.destroy(error)) + body.once('error', error => res.destroy(error)) - resBody.pipe(res) + body.pipe(res) + body.once('end', () => resolve(undefined)) } else { - return resolve(resBody) + return resolve(body) } }) } + +export function getStandardHttpBodyAndHeaders( + body: StandardBody, + headers: StandardHeaders, + options: ToNodeHttpBodyOptions = {}, +): { body: Readable | undefined | string, headers: StandardHeaders } { + const newHeaders: StandardHeaders = { ...omit(headers, ['content-disposition', 'content-type']) } + + if (body === undefined) { + return { body: undefined, headers: newHeaders } + } + + if (body instanceof Blob) { + newHeaders['content-type'] = body.type + newHeaders['content-length'] = body.size.toString() + const currentContentDisposition = flattenHeader(headers['content-disposition']) + newHeaders['content-disposition'] = currentContentDisposition ?? generateContentDisposition(body instanceof File ? body.name : 'blob') + + return { body: Readable.fromWeb(body.stream()), headers: newHeaders } + } + + if (body instanceof FormData) { + const response = new Response(body) + newHeaders['content-type'] = response.headers.get('content-type')! + // The FormData type inferred is from React and not NodeJS, so we need to cast it + return { body: Readable.fromWeb(response.body as any), headers: newHeaders } + } + + if (body instanceof URLSearchParams) { + newHeaders['content-type'] = 'application/x-www-form-urlencoded' + + return { body: body.toString(), headers: newHeaders } + } + + if (isAsyncIteratorObject(body)) { + newHeaders['content-type'] = 'text/event-stream' + + return { body: toEventStream(body, options), headers: newHeaders } + } + + newHeaders['content-type'] = 'application/json' + // It seems like Nest/Node, in case of a string body, remove or alter the string if + // content type json is not set. + // We also need to "double" stringify it, else the string will be encoded as an Array + // This match the behavior of #toNodeHttpBody + if (typeof body === 'string') { + return { body: stringifyJSON(body), headers: newHeaders } + } + + return { body: body as unknown as string, headers: newHeaders } +} diff --git a/packages/standard-server-node/src/body.ts b/packages/standard-server-node/src/body.ts index d41a5b653..d400ff011 100644 --- a/packages/standard-server-node/src/body.ts +++ b/packages/standard-server-node/src/body.ts @@ -50,9 +50,7 @@ export function toStandardBody(req: NodeHttpRequest, options: ToStandardBodyOpti }) } -export interface ToNodeHttpBodyOptions extends ToEventStreamOptions { - shouldStringifyBody?: boolean -} +export interface ToNodeHttpBodyOptions extends ToEventStreamOptions {} /** * @param body @@ -62,11 +60,8 @@ export interface ToNodeHttpBodyOptions extends ToEventStreamOptions { export function toNodeHttpBody( body: StandardBody, headers: StandardHeaders, - options: ToNodeHttpBodyOptions = { shouldStringifyBody: true }, + options: ToNodeHttpBodyOptions = {}, ): Readable | undefined | string { - if (options.shouldStringifyBody === undefined) { - options.shouldStringifyBody = true - } const currentContentDisposition = flattenHeader(headers['content-disposition']) delete headers['content-type'] @@ -105,9 +100,6 @@ export function toNodeHttpBody( headers['content-type'] = 'application/json' - if (options.shouldStringifyBody === false && typeof body !== 'string') { - return body as unknown as string - } return stringifyJSON(body) } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index de755b092..1757f3fda 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -428,6 +428,9 @@ importers: specifier: workspace:* version: link:../standard-server-node devDependencies: + '@fastify/compress': + specifier: ^8.1.0 + version: 8.1.0 '@fastify/cookie': specifier: ^11.0.2 version: 11.0.2 @@ -449,9 +452,21 @@ importers: '@ts-rest/core': specifier: ^3.52.1 version: 3.52.1(@types/node@22.18.13)(zod@4.1.12) + '@types/compression': + specifier: ^1.8.1 + version: 1.8.1 '@types/express': specifier: ^5.0.5 version: 5.0.5 + '@types/node': + specifier: ^22.15.30 + version: 22.18.13 + '@types/supertest': + specifier: ^6.0.3 + version: 6.0.3 + compression: + specifier: ^1.8.1 + version: 1.8.1 express: specifier: ^5.0.0 version: 5.1.0 @@ -994,7 +1009,7 @@ importers: version: 0.9.5(prettier@3.6.2)(typescript@5.8.3) '@astrojs/react': specifier: ^4.4.1 - version: 4.4.1(@types/node@22.18.13)(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(jiti@2.6.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + version: 4.4.1(@types/node@24.9.1)(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(jiti@2.6.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) '@orpc/client': specifier: next version: link:../../packages/client @@ -1027,7 +1042,7 @@ importers: version: 19.2.2(@types/react@19.2.2) astro: specifier: ^5.15.3 - version: 5.15.3(@types/node@22.18.13)(@upstash/redis@1.35.6)(db0@0.3.4(better-sqlite3@12.4.1))(ioredis@5.8.2)(jiti@2.6.1)(rollup@4.52.5)(terser@5.44.0)(tsx@4.20.6)(typescript@5.8.3)(yaml@2.8.1) + version: 5.15.3(@types/node@24.9.1)(@upstash/redis@1.35.6)(db0@0.3.4(better-sqlite3@12.4.1))(ioredis@5.8.2)(jiti@2.6.1)(rollup@4.52.5)(terser@5.44.0)(tsx@4.20.6)(typescript@5.8.3)(yaml@2.8.1) react: specifier: ^19.2.0 version: 19.2.0 @@ -1072,7 +1087,7 @@ importers: version: 19.2.2(@types/react@19.2.2) '@wxt-dev/module-react': specifier: ^1.1.5 - version: 1.1.5(vite@7.1.12(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(wxt@0.20.11(@types/node@22.18.13)(jiti@2.6.1)(rollup@4.52.5)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + version: 1.1.5(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(wxt@0.20.11(@types/node@24.9.1)(jiti@2.6.1)(rollup@4.52.5)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) react: specifier: ^19.2.0 version: 19.2.0 @@ -1084,7 +1099,7 @@ importers: version: 5.8.3 wxt: specifier: ^0.20.11 - version: 0.20.11(@types/node@22.18.13)(jiti@2.6.1)(rollup@4.52.5)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + version: 0.20.11(@types/node@24.9.1)(jiti@2.6.1)(rollup@4.52.5)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) zod: specifier: ^4.1.12 version: 4.1.12 @@ -1156,7 +1171,7 @@ importers: devDependencies: '@cloudflare/vite-plugin': specifier: ^1.13.18 - version: 1.13.18(vite@7.1.12(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(workerd@1.20251011.0)(wrangler@4.45.3(@cloudflare/workers-types@4.20251014.0)) + version: 1.13.18(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(workerd@1.20251011.0)(wrangler@4.45.3(@cloudflare/workers-types@4.20251014.0)) '@orpc/client': specifier: next version: link:../../packages/client @@ -1192,7 +1207,7 @@ importers: version: 19.2.2(@types/react@19.2.2) '@vitejs/plugin-react': specifier: ^5.1.0 - version: 5.1.0(vite@7.1.12(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + version: 5.1.0(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) react: specifier: ^19.2.0 version: 19.2.0 @@ -1204,7 +1219,7 @@ importers: version: 5.8.3 vite: specifier: ^7.1.12 - version: 7.1.12(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + version: 7.1.12(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) wrangler: specifier: ^4.45.3 version: 4.45.3(@cloudflare/workers-types@4.20251014.0) @@ -1477,7 +1492,7 @@ importers: version: 5.90.6(vue@3.5.22(typescript@5.8.3)) nuxt: specifier: ^4.2.0 - version: 4.2.0(@parcel/watcher@2.5.1)(@types/node@22.18.13)(@upstash/redis@1.35.6)(@vue/compiler-sfc@3.5.22)(better-sqlite3@12.4.1)(db0@0.3.4(better-sqlite3@12.4.1))(eslint@9.39.0(jiti@2.6.1))(ioredis@5.8.2)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(tsx@4.20.6)(typescript@5.8.3)(vite@7.1.12(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(xml2js@0.6.2)(yaml@2.8.1) + version: 4.2.0(@parcel/watcher@2.5.1)(@types/node@24.9.1)(@upstash/redis@1.35.6)(@vue/compiler-sfc@3.5.22)(better-sqlite3@12.4.1)(db0@0.3.4(better-sqlite3@12.4.1))(eslint@9.39.0(jiti@2.6.1))(ioredis@5.8.2)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(tsx@4.20.6)(typescript@5.8.3)(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(xml2js@0.6.2)(yaml@2.8.1) vue: specifier: latest version: 3.5.22(typescript@5.8.3) @@ -1513,7 +1528,7 @@ importers: version: 0.15.3(solid-js@1.9.10) '@solidjs/start': specifier: ^1.2.0 - version: 1.2.0(@testing-library/jest-dom@6.9.1)(solid-js@1.9.10)(vinxi@0.5.8(@types/node@22.18.13)(@upstash/redis@1.35.6)(better-sqlite3@12.4.1)(db0@0.3.4(better-sqlite3@12.4.1))(encoding@0.1.13)(ioredis@5.8.2)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(xml2js@0.6.2)(yaml@2.8.1))(vite@7.1.12(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + version: 1.2.0(@testing-library/jest-dom@6.9.1)(solid-js@1.9.10)(vinxi@0.5.8(@types/node@24.9.1)(@upstash/redis@1.35.6)(better-sqlite3@12.4.1)(db0@0.3.4(better-sqlite3@12.4.1))(encoding@0.1.13)(ioredis@5.8.2)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(xml2js@0.6.2)(yaml@2.8.1))(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) '@tanstack/solid-query': specifier: ^5.90.6 version: 5.90.8(solid-js@1.9.10) @@ -1522,10 +1537,10 @@ importers: version: 1.9.10 vinxi: specifier: ^0.5.8 - version: 0.5.8(@types/node@22.18.13)(@upstash/redis@1.35.6)(better-sqlite3@12.4.1)(db0@0.3.4(better-sqlite3@12.4.1))(encoding@0.1.13)(ioredis@5.8.2)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(xml2js@0.6.2)(yaml@2.8.1) + version: 0.5.8(@types/node@24.9.1)(@upstash/redis@1.35.6)(better-sqlite3@12.4.1)(db0@0.3.4(better-sqlite3@12.4.1))(encoding@0.1.13)(ioredis@5.8.2)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(xml2js@0.6.2)(yaml@2.8.1) vite-plugin-top-level-await: specifier: ^1.6.0 - version: 1.6.0(@swc/helpers@0.5.17)(rollup@4.52.5)(vite@7.1.12(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + version: 1.6.0(@swc/helpers@0.5.17)(rollup@4.52.5)(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) zod: specifier: ^4.1.12 version: 4.1.12 @@ -1552,13 +1567,13 @@ importers: version: link:../../packages/zod '@sveltejs/adapter-auto': specifier: ^7.0.0 - version: 7.0.0(@sveltejs/kit@2.48.4(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.2)(vite@7.1.12(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.43.2)(vite@7.1.12(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))) + version: 7.0.0(@sveltejs/kit@2.48.4(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.2)(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.43.2)(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))) '@sveltejs/kit': specifier: ^2.48.4 - version: 2.48.4(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.2)(vite@7.1.12(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.43.2)(vite@7.1.12(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + version: 2.48.4(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.2)(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.43.2)(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) '@sveltejs/vite-plugin-svelte': specifier: ^6.2.1 - version: 6.2.1(svelte@5.43.2)(vite@7.1.12(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + version: 6.2.1(svelte@5.43.2)(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) '@tanstack/svelte-query': specifier: ^6.0.3 version: 6.0.4(svelte@5.43.2) @@ -1573,7 +1588,7 @@ importers: version: 5.8.3 vite: specifier: ^7.1.12 - version: 7.1.12(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + version: 7.1.12(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) zod: specifier: ^4.1.12 version: 4.1.12 @@ -3016,6 +3031,9 @@ packages: resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@fastify/accept-negotiator@2.0.1': + resolution: {integrity: sha512-/c/TW2bO/v9JeEgoD/g1G5GxGeCF1Hafdf79WPmUlgYiBXummY0oX3VVq4yFkKKVBKDNlaDUYoab7g38RpPqCQ==} + '@fastify/ajv-compiler@4.0.5': resolution: {integrity: sha512-KoWKW+MhvfTRWL4qrhUwAAZoaChluo0m0vbiJlGMt2GXvL4LVPQEjt8kSpHI3IBq5Rez8fg+XeH3cneztq+C7A==} @@ -3023,6 +3041,9 @@ packages: resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==} engines: {node: '>=14'} + '@fastify/compress@8.1.0': + resolution: {integrity: sha512-wX3I5u/SYQXxbqjG7CysvzeaCe4Sv8y13MnvnaGTpqfKkJbTLpwvdIDgqrwp/+UGvXOW7OLDLoTAQCDMJJRjDQ==} + '@fastify/cookie@11.0.2': resolution: {integrity: sha512-GWdwdGlgJxyvNv+QcKiGNevSspMQXncjMZ1J8IvuDQk0jvkzgWWZFNC2En3s+nHndZBGV8IbLwOI/sxCZw/mzA==} @@ -6381,6 +6402,9 @@ packages: '@types/node@22.18.13': resolution: {integrity: sha512-Bo45YKIjnmFtv6I1TuC8AaHBbqXtIo+Om5fE4QiU1Tj8QR/qt+8O3BAtOimG5IFmwaWiPmB3Mv3jtYzBA4Us2A==} + '@types/node@24.9.1': + resolution: {integrity: sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==} + '@types/oracledb@6.5.2': resolution: {integrity: sha512-kK1eBS/Adeyis+3OlBDMeQQuasIDLUYXsi2T15ccNJ0iyUpQ4xDF7svFu3+bGVrI0CMBUclPciz+lsQR3JX3TQ==} @@ -8557,6 +8581,12 @@ packages: duplexer@0.1.2: resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} + duplexify@3.7.1: + resolution: {integrity: sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==} + + duplexify@4.1.3: + resolution: {integrity: sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==} + eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} @@ -11445,6 +11475,9 @@ packages: resolution: {integrity: sha512-eRWB5LBz7PpDu4PUlwT0PhnQfTQJlDDdPa35urV4Osrm0t0AqQFGn+UIkU3klZvwJ8KPO3VbBFsXquA6p6kqZw==} engines: {node: '>=12', npm: '>=6'} + peek-stream@1.1.3: + resolution: {integrity: sha512-FhJ+YbOSBb9/rIl2ZeE/QHEsWn7PqNYt8ARAY3kIgNGOk13g9FGyIY6JIl/xB/3TFRVoTv5as0l11weORrTekA==} + pend@1.2.0: resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} @@ -11859,6 +11892,9 @@ packages: pump@3.0.3: resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==} + pumpify@2.0.1: + resolution: {integrity: sha512-m7KOje7jZxrmutanlkS1daj1dS6z6BgslzOXmcSEpIlCxM3VJH7lG5QLeck/6hgF6F4crFf01UtQmNsJfweTAw==} + punycode.js@2.3.1: resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} engines: {node: '>=6'} @@ -12710,6 +12746,9 @@ packages: resolution: {integrity: sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==} engines: {node: '>=4', npm: '>=6'} + stream-shift@1.0.3: + resolution: {integrity: sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==} + streamsearch@1.1.0: resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} engines: {node: '>=10.0.0'} @@ -13005,6 +13044,9 @@ packages: resolution: {integrity: sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==} engines: {node: '>=18'} + through2@2.0.5: + resolution: {integrity: sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==} + through@2.3.8: resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} @@ -13305,6 +13347,9 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + undici-types@7.16.0: + resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + undici@5.29.0: resolution: {integrity: sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==} engines: {node: '>=14.0'} @@ -14827,15 +14872,15 @@ snapshots: dependencies: prismjs: 1.30.0 - '@astrojs/react@4.4.1(@types/node@22.18.13)(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(jiti@2.6.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)': + '@astrojs/react@4.4.1(@types/node@24.9.1)(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(jiti@2.6.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)': dependencies: '@types/react': 19.2.2 '@types/react-dom': 19.2.2(@types/react@19.2.2) - '@vitejs/plugin-react': 4.7.0(vite@6.4.1(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + '@vitejs/plugin-react': 4.7.0(vite@6.4.1(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) react: 19.2.0 react-dom: 19.2.0(react@19.2.0) ultrahtml: 1.6.0 - vite: 6.4.1(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + vite: 6.4.1(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) transitivePeerDependencies: - '@types/node' - jiti @@ -15132,7 +15177,7 @@ snapshots: optionalDependencies: workerd: 1.20251011.0 - '@cloudflare/vite-plugin@1.13.18(vite@7.1.12(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(workerd@1.20251011.0)(wrangler@4.45.3(@cloudflare/workers-types@4.20251014.0))': + '@cloudflare/vite-plugin@1.13.18(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(workerd@1.20251011.0)(wrangler@4.45.3(@cloudflare/workers-types@4.20251014.0))': dependencies: '@cloudflare/unenv-preset': 2.7.8(unenv@2.0.0-rc.21)(workerd@1.20251011.0) '@remix-run/node-fetch-server': 0.8.1 @@ -15141,7 +15186,7 @@ snapshots: picocolors: 1.1.1 tinyglobby: 0.2.15 unenv: 2.0.0-rc.21 - vite: 7.1.12(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + vite: 7.1.12(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) wrangler: 4.45.3(@cloudflare/workers-types@4.20251014.0) ws: 8.18.0 transitivePeerDependencies: @@ -15845,6 +15890,8 @@ snapshots: '@eslint/core': 0.17.0 levn: 0.4.1 + '@fastify/accept-negotiator@2.0.1': {} + '@fastify/ajv-compiler@4.0.5': dependencies: ajv: 8.17.1 @@ -15853,6 +15900,17 @@ snapshots: '@fastify/busboy@2.1.1': {} + '@fastify/compress@8.1.0': + dependencies: + '@fastify/accept-negotiator': 2.0.1 + fastify-plugin: 5.1.0 + mime-db: 1.54.0 + minipass: 7.1.2 + peek-stream: 1.1.3 + pump: 3.0.3 + pumpify: 2.0.1 + readable-stream: 4.7.0 + '@fastify/cookie@11.0.2': dependencies: cookie: 1.0.2 @@ -16773,11 +16831,11 @@ snapshots: '@nuxt/devalue@2.0.2': {} - '@nuxt/devtools-kit@2.7.0(magicast@0.3.5)(vite@7.1.12(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))': + '@nuxt/devtools-kit@2.7.0(magicast@0.3.5)(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))': dependencies: '@nuxt/kit': 3.20.0(magicast@0.3.5) execa: 8.0.1 - vite: 7.1.12(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + vite: 7.1.12(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) transitivePeerDependencies: - magicast @@ -16792,12 +16850,12 @@ snapshots: prompts: 2.4.2 semver: 7.7.3 - '@nuxt/devtools@2.7.0(vite@7.1.12(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(vue@3.5.22(typescript@5.8.3))': + '@nuxt/devtools@2.7.0(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(vue@3.5.22(typescript@5.8.3))': dependencies: - '@nuxt/devtools-kit': 2.7.0(magicast@0.3.5)(vite@7.1.12(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + '@nuxt/devtools-kit': 2.7.0(magicast@0.3.5)(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) '@nuxt/devtools-wizard': 2.7.0 '@nuxt/kit': 3.20.0(magicast@0.3.5) - '@vue/devtools-core': 7.7.7(vite@7.1.12(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(vue@3.5.22(typescript@5.8.3)) + '@vue/devtools-core': 7.7.7(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(vue@3.5.22(typescript@5.8.3)) '@vue/devtools-kit': 7.7.7 birpc: 2.6.1 consola: 3.4.2 @@ -16822,9 +16880,9 @@ snapshots: sirv: 3.0.2 structured-clone-es: 1.0.0 tinyglobby: 0.2.15 - vite: 7.1.12(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) - vite-plugin-inspect: 11.3.3(@nuxt/kit@3.20.0(magicast@0.3.5))(vite@7.1.12(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) - vite-plugin-vue-tracer: 1.0.1(vite@7.1.12(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(vue@3.5.22(typescript@5.8.3)) + vite: 7.1.12(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + vite-plugin-inspect: 11.3.3(@nuxt/kit@3.20.0(magicast@0.3.5))(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + vite-plugin-vue-tracer: 1.0.1(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(vue@3.5.22(typescript@5.8.3)) which: 5.0.0 ws: 8.18.3 transitivePeerDependencies: @@ -16884,7 +16942,7 @@ snapshots: transitivePeerDependencies: - magicast - '@nuxt/nitro-server@4.2.0(@upstash/redis@1.35.6)(better-sqlite3@12.4.1)(db0@0.3.4(better-sqlite3@12.4.1))(ioredis@5.8.2)(magicast@0.3.5)(nuxt@4.2.0(@parcel/watcher@2.5.1)(@types/node@22.18.13)(@upstash/redis@1.35.6)(@vue/compiler-sfc@3.5.22)(better-sqlite3@12.4.1)(db0@0.3.4(better-sqlite3@12.4.1))(eslint@9.39.0(jiti@2.6.1))(ioredis@5.8.2)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(tsx@4.20.6)(typescript@5.8.3)(vite@7.1.12(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(xml2js@0.6.2)(yaml@2.8.1))(typescript@5.8.3)(xml2js@0.6.2)': + '@nuxt/nitro-server@4.2.0(@upstash/redis@1.35.6)(better-sqlite3@12.4.1)(db0@0.3.4(better-sqlite3@12.4.1))(ioredis@5.8.2)(magicast@0.3.5)(nuxt@4.2.0(@parcel/watcher@2.5.1)(@types/node@24.9.1)(@upstash/redis@1.35.6)(@vue/compiler-sfc@3.5.22)(better-sqlite3@12.4.1)(db0@0.3.4(better-sqlite3@12.4.1))(eslint@9.39.0(jiti@2.6.1))(ioredis@5.8.2)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(tsx@4.20.6)(typescript@5.8.3)(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(xml2js@0.6.2)(yaml@2.8.1))(typescript@5.8.3)(xml2js@0.6.2)': dependencies: '@nuxt/devalue': 2.0.2 '@nuxt/kit': 4.2.0(magicast@0.3.5) @@ -16902,7 +16960,7 @@ snapshots: klona: 2.0.6 mocked-exports: 0.1.1 nitropack: 2.12.9(@upstash/redis@1.35.6)(better-sqlite3@12.4.1)(encoding@0.1.13)(xml2js@0.6.2) - nuxt: 4.2.0(@parcel/watcher@2.5.1)(@types/node@22.18.13)(@upstash/redis@1.35.6)(@vue/compiler-sfc@3.5.22)(better-sqlite3@12.4.1)(db0@0.3.4(better-sqlite3@12.4.1))(eslint@9.39.0(jiti@2.6.1))(ioredis@5.8.2)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(tsx@4.20.6)(typescript@5.8.3)(vite@7.1.12(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(xml2js@0.6.2)(yaml@2.8.1) + nuxt: 4.2.0(@parcel/watcher@2.5.1)(@types/node@24.9.1)(@upstash/redis@1.35.6)(@vue/compiler-sfc@3.5.22)(better-sqlite3@12.4.1)(db0@0.3.4(better-sqlite3@12.4.1))(eslint@9.39.0(jiti@2.6.1))(ioredis@5.8.2)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(tsx@4.20.6)(typescript@5.8.3)(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(xml2js@0.6.2)(yaml@2.8.1) pathe: 2.0.3 pkg-types: 2.3.0 radix3: 1.1.2 @@ -16977,12 +17035,12 @@ snapshots: transitivePeerDependencies: - magicast - '@nuxt/vite-builder@4.2.0(@types/node@22.18.13)(eslint@9.39.0(jiti@2.6.1))(magicast@0.3.5)(nuxt@4.2.0(@parcel/watcher@2.5.1)(@types/node@22.18.13)(@upstash/redis@1.35.6)(@vue/compiler-sfc@3.5.22)(better-sqlite3@12.4.1)(db0@0.3.4(better-sqlite3@12.4.1))(eslint@9.39.0(jiti@2.6.1))(ioredis@5.8.2)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(tsx@4.20.6)(typescript@5.8.3)(vite@7.1.12(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(xml2js@0.6.2)(yaml@2.8.1))(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(tsx@4.20.6)(typescript@5.8.3)(vue@3.5.22(typescript@5.8.3))(yaml@2.8.1)': + '@nuxt/vite-builder@4.2.0(@types/node@24.9.1)(eslint@9.39.0(jiti@2.6.1))(magicast@0.3.5)(nuxt@4.2.0(@parcel/watcher@2.5.1)(@types/node@24.9.1)(@upstash/redis@1.35.6)(@vue/compiler-sfc@3.5.22)(better-sqlite3@12.4.1)(db0@0.3.4(better-sqlite3@12.4.1))(eslint@9.39.0(jiti@2.6.1))(ioredis@5.8.2)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(tsx@4.20.6)(typescript@5.8.3)(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(xml2js@0.6.2)(yaml@2.8.1))(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(tsx@4.20.6)(typescript@5.8.3)(vue@3.5.22(typescript@5.8.3))(yaml@2.8.1)': dependencies: '@nuxt/kit': 4.2.0(magicast@0.3.5) '@rollup/plugin-replace': 6.0.3(rollup@4.52.5) - '@vitejs/plugin-vue': 6.0.1(vite@7.1.12(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(vue@3.5.22(typescript@5.8.3)) - '@vitejs/plugin-vue-jsx': 5.1.1(vite@7.1.12(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(vue@3.5.22(typescript@5.8.3)) + '@vitejs/plugin-vue': 6.0.1(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(vue@3.5.22(typescript@5.8.3)) + '@vitejs/plugin-vue-jsx': 5.1.1(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(vue@3.5.22(typescript@5.8.3)) autoprefixer: 10.4.21(postcss@8.5.6) consola: 3.4.2 cssnano: 7.1.2(postcss@8.5.6) @@ -16997,7 +17055,7 @@ snapshots: magic-string: 0.30.21 mlly: 1.8.0 mocked-exports: 0.1.1 - nuxt: 4.2.0(@parcel/watcher@2.5.1)(@types/node@22.18.13)(@upstash/redis@1.35.6)(@vue/compiler-sfc@3.5.22)(better-sqlite3@12.4.1)(db0@0.3.4(better-sqlite3@12.4.1))(eslint@9.39.0(jiti@2.6.1))(ioredis@5.8.2)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(tsx@4.20.6)(typescript@5.8.3)(vite@7.1.12(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(xml2js@0.6.2)(yaml@2.8.1) + nuxt: 4.2.0(@parcel/watcher@2.5.1)(@types/node@24.9.1)(@upstash/redis@1.35.6)(@vue/compiler-sfc@3.5.22)(better-sqlite3@12.4.1)(db0@0.3.4(better-sqlite3@12.4.1))(eslint@9.39.0(jiti@2.6.1))(ioredis@5.8.2)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(tsx@4.20.6)(typescript@5.8.3)(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(xml2js@0.6.2)(yaml@2.8.1) pathe: 2.0.3 pkg-types: 2.3.0 postcss: 8.5.6 @@ -17006,9 +17064,9 @@ snapshots: std-env: 3.10.0 ufo: 1.6.1 unenv: 2.0.0-rc.24 - vite: 7.1.12(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) - vite-node: 3.2.4(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) - vite-plugin-checker: 0.11.0(eslint@9.39.0(jiti@2.6.1))(optionator@0.9.4)(typescript@5.8.3)(vite@7.1.12(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + vite: 7.1.12(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + vite-node: 3.2.4(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + vite-plugin-checker: 0.11.0(eslint@9.39.0(jiti@2.6.1))(optionator@0.9.4)(typescript@5.8.3)(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) vue: 3.5.22(typescript@5.8.3) vue-bundle-renderer: 2.2.0 transitivePeerDependencies: @@ -19103,11 +19161,11 @@ snapshots: dependencies: solid-js: 1.9.10 - '@solidjs/start@1.2.0(@testing-library/jest-dom@6.9.1)(solid-js@1.9.10)(vinxi@0.5.8(@types/node@22.18.13)(@upstash/redis@1.35.6)(better-sqlite3@12.4.1)(db0@0.3.4(better-sqlite3@12.4.1))(encoding@0.1.13)(ioredis@5.8.2)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(xml2js@0.6.2)(yaml@2.8.1))(vite@7.1.12(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))': + '@solidjs/start@1.2.0(@testing-library/jest-dom@6.9.1)(solid-js@1.9.10)(vinxi@0.5.8(@types/node@24.9.1)(@upstash/redis@1.35.6)(better-sqlite3@12.4.1)(db0@0.3.4(better-sqlite3@12.4.1))(encoding@0.1.13)(ioredis@5.8.2)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(xml2js@0.6.2)(yaml@2.8.1))(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))': dependencies: - '@tanstack/server-functions-plugin': 1.121.21(vite@7.1.12(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) - '@vinxi/plugin-directives': 0.5.1(vinxi@0.5.8(@types/node@22.18.13)(@upstash/redis@1.35.6)(better-sqlite3@12.4.1)(db0@0.3.4(better-sqlite3@12.4.1))(encoding@0.1.13)(ioredis@5.8.2)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(xml2js@0.6.2)(yaml@2.8.1)) - '@vinxi/server-components': 0.5.1(vinxi@0.5.8(@types/node@22.18.13)(@upstash/redis@1.35.6)(better-sqlite3@12.4.1)(db0@0.3.4(better-sqlite3@12.4.1))(encoding@0.1.13)(ioredis@5.8.2)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(xml2js@0.6.2)(yaml@2.8.1)) + '@tanstack/server-functions-plugin': 1.121.21(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + '@vinxi/plugin-directives': 0.5.1(vinxi@0.5.8(@types/node@24.9.1)(@upstash/redis@1.35.6)(better-sqlite3@12.4.1)(db0@0.3.4(better-sqlite3@12.4.1))(encoding@0.1.13)(ioredis@5.8.2)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(xml2js@0.6.2)(yaml@2.8.1)) + '@vinxi/server-components': 0.5.1(vinxi@0.5.8(@types/node@24.9.1)(@upstash/redis@1.35.6)(better-sqlite3@12.4.1)(db0@0.3.4(better-sqlite3@12.4.1))(encoding@0.1.13)(ioredis@5.8.2)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(xml2js@0.6.2)(yaml@2.8.1)) cookie-es: 2.0.0 defu: 6.1.4 error-stack-parser: 2.1.4 @@ -19119,8 +19177,8 @@ snapshots: source-map-js: 1.2.1 terracotta: 1.0.6(solid-js@1.9.10) tinyglobby: 0.2.15 - vinxi: 0.5.8(@types/node@22.18.13)(@upstash/redis@1.35.6)(better-sqlite3@12.4.1)(db0@0.3.4(better-sqlite3@12.4.1))(encoding@0.1.13)(ioredis@5.8.2)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(xml2js@0.6.2)(yaml@2.8.1) - vite-plugin-solid: 2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.10)(vite@7.1.12(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + vinxi: 0.5.8(@types/node@24.9.1)(@upstash/redis@1.35.6)(better-sqlite3@12.4.1)(db0@0.3.4(better-sqlite3@12.4.1))(encoding@0.1.13)(ioredis@5.8.2)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(xml2js@0.6.2)(yaml@2.8.1) + vite-plugin-solid: 2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.10)(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) transitivePeerDependencies: - '@testing-library/jest-dom' - solid-js @@ -19152,15 +19210,15 @@ snapshots: dependencies: acorn: 8.15.0 - '@sveltejs/adapter-auto@7.0.0(@sveltejs/kit@2.48.4(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.2)(vite@7.1.12(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.43.2)(vite@7.1.12(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))': + '@sveltejs/adapter-auto@7.0.0(@sveltejs/kit@2.48.4(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.2)(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.43.2)(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))': dependencies: - '@sveltejs/kit': 2.48.4(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.2)(vite@7.1.12(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.43.2)(vite@7.1.12(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + '@sveltejs/kit': 2.48.4(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.2)(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.43.2)(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) - '@sveltejs/kit@2.48.4(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.2)(vite@7.1.12(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.43.2)(vite@7.1.12(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))': + '@sveltejs/kit@2.48.4(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.2)(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.43.2)(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))': dependencies: '@standard-schema/spec': 1.0.0 '@sveltejs/acorn-typescript': 1.0.6(acorn@8.15.0) - '@sveltejs/vite-plugin-svelte': 6.2.1(svelte@5.43.2)(vite@7.1.12(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + '@sveltejs/vite-plugin-svelte': 6.2.1(svelte@5.43.2)(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) '@types/cookie': 0.6.0 acorn: 8.15.0 cookie: 0.6.0 @@ -19173,7 +19231,7 @@ snapshots: set-cookie-parser: 2.7.2 sirv: 3.0.2 svelte: 5.43.2 - vite: 7.1.12(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + vite: 7.1.12(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) optionalDependencies: '@opentelemetry/api': 1.9.0 @@ -19186,6 +19244,15 @@ snapshots: transitivePeerDependencies: - supports-color + '@sveltejs/vite-plugin-svelte-inspector@5.0.1(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.2)(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.43.2)(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))': + dependencies: + '@sveltejs/vite-plugin-svelte': 6.2.1(svelte@5.43.2)(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + debug: 4.4.3 + svelte: 5.43.2 + vite: 7.1.12(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + transitivePeerDependencies: + - supports-color + '@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.2)(vite@7.1.12(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))': dependencies: '@sveltejs/vite-plugin-svelte-inspector': 5.0.1(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.2)(vite@7.1.12(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.43.2)(vite@7.1.12(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) @@ -19198,6 +19265,18 @@ snapshots: transitivePeerDependencies: - supports-color + '@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.2)(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))': + dependencies: + '@sveltejs/vite-plugin-svelte-inspector': 5.0.1(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.2)(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.43.2)(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + debug: 4.4.3 + deepmerge: 4.3.1 + magic-string: 0.30.21 + svelte: 5.43.2 + vite: 7.1.12(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + vitefu: 1.1.1(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + transitivePeerDependencies: + - supports-color + '@swc/cli@0.7.8(@swc/core@1.14.0(@swc/helpers@0.5.17))(chokidar@4.0.3)': dependencies: '@swc/core': 1.14.0(@swc/helpers@0.5.17) @@ -19296,7 +19375,7 @@ snapshots: optionalDependencies: '@tanstack/query-devtools': 5.90.1 - '@tanstack/directive-functions-plugin@1.121.21(vite@7.1.12(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))': + '@tanstack/directive-functions-plugin@1.121.21(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))': dependencies: '@babel/code-frame': 7.26.2 '@babel/core': 7.28.5 @@ -19305,7 +19384,7 @@ snapshots: '@tanstack/router-utils': 1.133.19 babel-dead-code-elimination: 1.0.10 tiny-invariant: 1.3.3 - vite: 7.1.12(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + vite: 7.1.12(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) transitivePeerDependencies: - supports-color @@ -19527,7 +19606,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@tanstack/server-functions-plugin@1.121.21(vite@7.1.12(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))': + '@tanstack/server-functions-plugin@1.121.21(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))': dependencies: '@babel/code-frame': 7.26.2 '@babel/core': 7.28.5 @@ -19536,7 +19615,7 @@ snapshots: '@babel/template': 7.27.2 '@babel/traverse': 7.28.5 '@babel/types': 7.28.5 - '@tanstack/directive-functions-plugin': 1.121.21(vite@7.1.12(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + '@tanstack/directive-functions-plugin': 1.121.21(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) babel-dead-code-elimination: 1.0.10 tiny-invariant: 1.3.3 transitivePeerDependencies: @@ -20014,6 +20093,11 @@ snapshots: dependencies: undici-types: 6.21.0 + '@types/node@24.9.1': + dependencies: + undici-types: 7.16.0 + optional: true + '@types/oracledb@6.5.2': dependencies: '@types/node': 22.18.13 @@ -20314,7 +20398,7 @@ snapshots: untun: 0.1.3 uqr: 0.1.2 - '@vinxi/plugin-directives@0.5.1(vinxi@0.5.8(@types/node@22.18.13)(@upstash/redis@1.35.6)(better-sqlite3@12.4.1)(db0@0.3.4(better-sqlite3@12.4.1))(encoding@0.1.13)(ioredis@5.8.2)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(xml2js@0.6.2)(yaml@2.8.1))': + '@vinxi/plugin-directives@0.5.1(vinxi@0.5.8(@types/node@24.9.1)(@upstash/redis@1.35.6)(better-sqlite3@12.4.1)(db0@0.3.4(better-sqlite3@12.4.1))(encoding@0.1.13)(ioredis@5.8.2)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(xml2js@0.6.2)(yaml@2.8.1))': dependencies: '@babel/parser': 7.28.5 acorn: 8.15.0 @@ -20325,20 +20409,20 @@ snapshots: magicast: 0.2.11 recast: 0.23.11 tslib: 2.8.1 - vinxi: 0.5.8(@types/node@22.18.13)(@upstash/redis@1.35.6)(better-sqlite3@12.4.1)(db0@0.3.4(better-sqlite3@12.4.1))(encoding@0.1.13)(ioredis@5.8.2)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(xml2js@0.6.2)(yaml@2.8.1) + vinxi: 0.5.8(@types/node@24.9.1)(@upstash/redis@1.35.6)(better-sqlite3@12.4.1)(db0@0.3.4(better-sqlite3@12.4.1))(encoding@0.1.13)(ioredis@5.8.2)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(xml2js@0.6.2)(yaml@2.8.1) - '@vinxi/server-components@0.5.1(vinxi@0.5.8(@types/node@22.18.13)(@upstash/redis@1.35.6)(better-sqlite3@12.4.1)(db0@0.3.4(better-sqlite3@12.4.1))(encoding@0.1.13)(ioredis@5.8.2)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(xml2js@0.6.2)(yaml@2.8.1))': + '@vinxi/server-components@0.5.1(vinxi@0.5.8(@types/node@24.9.1)(@upstash/redis@1.35.6)(better-sqlite3@12.4.1)(db0@0.3.4(better-sqlite3@12.4.1))(encoding@0.1.13)(ioredis@5.8.2)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(xml2js@0.6.2)(yaml@2.8.1))': dependencies: - '@vinxi/plugin-directives': 0.5.1(vinxi@0.5.8(@types/node@22.18.13)(@upstash/redis@1.35.6)(better-sqlite3@12.4.1)(db0@0.3.4(better-sqlite3@12.4.1))(encoding@0.1.13)(ioredis@5.8.2)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(xml2js@0.6.2)(yaml@2.8.1)) + '@vinxi/plugin-directives': 0.5.1(vinxi@0.5.8(@types/node@24.9.1)(@upstash/redis@1.35.6)(better-sqlite3@12.4.1)(db0@0.3.4(better-sqlite3@12.4.1))(encoding@0.1.13)(ioredis@5.8.2)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(xml2js@0.6.2)(yaml@2.8.1)) acorn: 8.15.0 acorn-loose: 8.5.2 acorn-typescript: 1.4.13(acorn@8.15.0) astring: 1.9.0 magicast: 0.2.11 recast: 0.23.11 - vinxi: 0.5.8(@types/node@22.18.13)(@upstash/redis@1.35.6)(better-sqlite3@12.4.1)(db0@0.3.4(better-sqlite3@12.4.1))(encoding@0.1.13)(ioredis@5.8.2)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(xml2js@0.6.2)(yaml@2.8.1) + vinxi: 0.5.8(@types/node@24.9.1)(@upstash/redis@1.35.6)(better-sqlite3@12.4.1)(db0@0.3.4(better-sqlite3@12.4.1))(encoding@0.1.13)(ioredis@5.8.2)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(xml2js@0.6.2)(yaml@2.8.1) - '@vitejs/plugin-react@4.7.0(vite@6.4.1(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))': + '@vitejs/plugin-react@4.7.0(vite@6.4.1(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))': dependencies: '@babel/core': 7.28.5 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.5) @@ -20346,7 +20430,7 @@ snapshots: '@rolldown/pluginutils': 1.0.0-beta.27 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: 6.4.1(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + vite: 6.4.1(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) transitivePeerDependencies: - supports-color @@ -20362,14 +20446,26 @@ snapshots: transitivePeerDependencies: - supports-color - '@vitejs/plugin-vue-jsx@5.1.1(vite@7.1.12(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(vue@3.5.22(typescript@5.8.3))': + '@vitejs/plugin-react@5.1.0(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))': + dependencies: + '@babel/core': 7.28.5 + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.28.5) + '@rolldown/pluginutils': 1.0.0-beta.43 + '@types/babel__core': 7.20.5 + react-refresh: 0.18.0 + vite: 7.1.12(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + transitivePeerDependencies: + - supports-color + + '@vitejs/plugin-vue-jsx@5.1.1(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(vue@3.5.22(typescript@5.8.3))': dependencies: '@babel/core': 7.28.5 '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.28.5) '@babel/plugin-transform-typescript': 7.28.5(@babel/core@7.28.5) '@rolldown/pluginutils': 1.0.0-beta.45 '@vue/babel-plugin-jsx': 1.5.0(@babel/core@7.28.5) - vite: 7.1.12(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + vite: 7.1.12(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) vue: 3.5.22(typescript@5.8.3) transitivePeerDependencies: - supports-color @@ -20379,10 +20475,10 @@ snapshots: vite: 5.4.21(@types/node@22.18.13)(terser@5.44.0) vue: 3.5.22(typescript@5.8.3) - '@vitejs/plugin-vue@6.0.1(vite@7.1.12(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(vue@3.5.22(typescript@5.8.3))': + '@vitejs/plugin-vue@6.0.1(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(vue@3.5.22(typescript@5.8.3))': dependencies: '@rolldown/pluginutils': 1.0.0-beta.29 - vite: 7.1.12(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + vite: 7.1.12(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) vue: 3.5.22(typescript@5.8.3) '@vitest/coverage-v8@3.2.4(vitest@3.2.4)': @@ -20598,14 +20694,14 @@ snapshots: dependencies: '@vue/devtools-kit': 8.0.3 - '@vue/devtools-core@7.7.7(vite@7.1.12(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(vue@3.5.22(typescript@5.8.3))': + '@vue/devtools-core@7.7.7(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(vue@3.5.22(typescript@5.8.3))': dependencies: '@vue/devtools-kit': 7.7.7 '@vue/devtools-shared': 7.7.7 mitt: 3.0.1 nanoid: 5.1.6 pathe: 2.0.3 - vite-hot-client: 2.1.0(vite@7.1.12(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + vite-hot-client: 2.1.0(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) vue: 3.5.22(typescript@5.8.3) transitivePeerDependencies: - vite @@ -20841,10 +20937,10 @@ snapshots: '@types/filesystem': 0.0.36 '@types/har-format': 1.2.16 - '@wxt-dev/module-react@1.1.5(vite@7.1.12(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(wxt@0.20.11(@types/node@22.18.13)(jiti@2.6.1)(rollup@4.52.5)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))': + '@wxt-dev/module-react@1.1.5(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(wxt@0.20.11(@types/node@24.9.1)(jiti@2.6.1)(rollup@4.52.5)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))': dependencies: - '@vitejs/plugin-react': 5.1.0(vite@7.1.12(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) - wxt: 0.20.11(@types/node@22.18.13)(jiti@2.6.1)(rollup@4.52.5)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + '@vitejs/plugin-react': 5.1.0(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + wxt: 0.20.11(@types/node@24.9.1)(jiti@2.6.1)(rollup@4.52.5)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) transitivePeerDependencies: - supports-color - vite @@ -21277,7 +21373,7 @@ snapshots: astring@1.9.0: {} - astro@5.15.3(@types/node@22.18.13)(@upstash/redis@1.35.6)(db0@0.3.4(better-sqlite3@12.4.1))(ioredis@5.8.2)(jiti@2.6.1)(rollup@4.52.5)(terser@5.44.0)(tsx@4.20.6)(typescript@5.8.3)(yaml@2.8.1): + astro@5.15.3(@types/node@24.9.1)(@upstash/redis@1.35.6)(db0@0.3.4(better-sqlite3@12.4.1))(ioredis@5.8.2)(jiti@2.6.1)(rollup@4.52.5)(terser@5.44.0)(tsx@4.20.6)(typescript@5.8.3)(yaml@2.8.1): dependencies: '@astrojs/compiler': 2.13.0 '@astrojs/internal-helpers': 0.7.4 @@ -21333,8 +21429,8 @@ snapshots: unist-util-visit: 5.0.0 unstorage: 1.17.2(@upstash/redis@1.35.6)(db0@0.3.4(better-sqlite3@12.4.1))(ioredis@5.8.2) vfile: 6.0.3 - vite: 6.4.1(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) - vitefu: 1.1.1(vite@6.4.1(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + vite: 6.4.1(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + vitefu: 1.1.1(vite@6.4.1(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) xxhash-wasm: 1.1.0 yargs-parser: 21.1.1 yocto-spinner: 0.2.3 @@ -22735,6 +22831,20 @@ snapshots: duplexer@0.1.2: {} + duplexify@3.7.1: + dependencies: + end-of-stream: 1.4.5 + inherits: 2.0.4 + readable-stream: 2.3.8 + stream-shift: 1.0.3 + + duplexify@4.1.3: + dependencies: + end-of-stream: 1.4.5 + inherits: 2.0.4 + readable-stream: 3.6.2 + stream-shift: 1.0.3 + eastasianwidth@0.2.0: {} editorconfig@1.0.4: @@ -25938,16 +26048,16 @@ snapshots: dependencies: boolbase: 1.0.0 - nuxt@4.2.0(@parcel/watcher@2.5.1)(@types/node@22.18.13)(@upstash/redis@1.35.6)(@vue/compiler-sfc@3.5.22)(better-sqlite3@12.4.1)(db0@0.3.4(better-sqlite3@12.4.1))(eslint@9.39.0(jiti@2.6.1))(ioredis@5.8.2)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(tsx@4.20.6)(typescript@5.8.3)(vite@7.1.12(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(xml2js@0.6.2)(yaml@2.8.1): + nuxt@4.2.0(@parcel/watcher@2.5.1)(@types/node@24.9.1)(@upstash/redis@1.35.6)(@vue/compiler-sfc@3.5.22)(better-sqlite3@12.4.1)(db0@0.3.4(better-sqlite3@12.4.1))(eslint@9.39.0(jiti@2.6.1))(ioredis@5.8.2)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(tsx@4.20.6)(typescript@5.8.3)(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(xml2js@0.6.2)(yaml@2.8.1): dependencies: '@dxup/nuxt': 0.2.0(magicast@0.3.5) '@nuxt/cli': 3.29.3(magicast@0.3.5) - '@nuxt/devtools': 2.7.0(vite@7.1.12(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(vue@3.5.22(typescript@5.8.3)) + '@nuxt/devtools': 2.7.0(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(vue@3.5.22(typescript@5.8.3)) '@nuxt/kit': 4.2.0(magicast@0.3.5) - '@nuxt/nitro-server': 4.2.0(@upstash/redis@1.35.6)(better-sqlite3@12.4.1)(db0@0.3.4(better-sqlite3@12.4.1))(ioredis@5.8.2)(magicast@0.3.5)(nuxt@4.2.0(@parcel/watcher@2.5.1)(@types/node@22.18.13)(@upstash/redis@1.35.6)(@vue/compiler-sfc@3.5.22)(better-sqlite3@12.4.1)(db0@0.3.4(better-sqlite3@12.4.1))(eslint@9.39.0(jiti@2.6.1))(ioredis@5.8.2)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(tsx@4.20.6)(typescript@5.8.3)(vite@7.1.12(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(xml2js@0.6.2)(yaml@2.8.1))(typescript@5.8.3)(xml2js@0.6.2) + '@nuxt/nitro-server': 4.2.0(@upstash/redis@1.35.6)(better-sqlite3@12.4.1)(db0@0.3.4(better-sqlite3@12.4.1))(ioredis@5.8.2)(magicast@0.3.5)(nuxt@4.2.0(@parcel/watcher@2.5.1)(@types/node@24.9.1)(@upstash/redis@1.35.6)(@vue/compiler-sfc@3.5.22)(better-sqlite3@12.4.1)(db0@0.3.4(better-sqlite3@12.4.1))(eslint@9.39.0(jiti@2.6.1))(ioredis@5.8.2)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(tsx@4.20.6)(typescript@5.8.3)(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(xml2js@0.6.2)(yaml@2.8.1))(typescript@5.8.3)(xml2js@0.6.2) '@nuxt/schema': 4.2.0 '@nuxt/telemetry': 2.6.6(magicast@0.3.5) - '@nuxt/vite-builder': 4.2.0(@types/node@22.18.13)(eslint@9.39.0(jiti@2.6.1))(magicast@0.3.5)(nuxt@4.2.0(@parcel/watcher@2.5.1)(@types/node@22.18.13)(@upstash/redis@1.35.6)(@vue/compiler-sfc@3.5.22)(better-sqlite3@12.4.1)(db0@0.3.4(better-sqlite3@12.4.1))(eslint@9.39.0(jiti@2.6.1))(ioredis@5.8.2)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(tsx@4.20.6)(typescript@5.8.3)(vite@7.1.12(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(xml2js@0.6.2)(yaml@2.8.1))(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(tsx@4.20.6)(typescript@5.8.3)(vue@3.5.22(typescript@5.8.3))(yaml@2.8.1) + '@nuxt/vite-builder': 4.2.0(@types/node@24.9.1)(eslint@9.39.0(jiti@2.6.1))(magicast@0.3.5)(nuxt@4.2.0(@parcel/watcher@2.5.1)(@types/node@24.9.1)(@upstash/redis@1.35.6)(@vue/compiler-sfc@3.5.22)(better-sqlite3@12.4.1)(db0@0.3.4(better-sqlite3@12.4.1))(eslint@9.39.0(jiti@2.6.1))(ioredis@5.8.2)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(tsx@4.20.6)(typescript@5.8.3)(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(xml2js@0.6.2)(yaml@2.8.1))(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(tsx@4.20.6)(typescript@5.8.3)(vue@3.5.22(typescript@5.8.3))(yaml@2.8.1) '@unhead/vue': 2.0.19(vue@3.5.22(typescript@5.8.3)) '@vue/shared': 3.5.22 c12: 3.3.1(magicast@0.3.5) @@ -25999,7 +26109,7 @@ snapshots: vue-router: 4.6.3(vue@3.5.22(typescript@5.8.3)) optionalDependencies: '@parcel/watcher': 2.5.1 - '@types/node': 22.18.13 + '@types/node': 24.9.1 transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -26443,6 +26553,12 @@ snapshots: pe-library@0.4.1: {} + peek-stream@1.1.3: + dependencies: + buffer-from: 1.1.2 + duplexify: 3.7.1 + through2: 2.0.5 + pend@1.2.0: {} perfect-debounce@1.0.0: {} @@ -26868,6 +26984,12 @@ snapshots: end-of-stream: 1.4.5 once: 1.4.0 + pumpify@2.0.1: + dependencies: + duplexify: 4.1.3 + inherits: 2.0.4 + pump: 3.0.3 + punycode.js@2.3.1: {} punycode@2.3.1: {} @@ -27917,6 +28039,8 @@ snapshots: stoppable@1.1.0: {} + stream-shift@1.0.3: {} + streamsearch@1.1.0: {} streamx@2.23.0: @@ -28259,6 +28383,11 @@ snapshots: throttleit@2.1.0: {} + through2@2.0.5: + dependencies: + readable-stream: 2.3.8 + xtend: 4.0.2 + through@2.3.8: {} tiny-async-pool@1.3.0: @@ -28557,6 +28686,9 @@ snapshots: undici-types@6.21.0: {} + undici-types@7.16.0: + optional: true + undici@5.29.0: dependencies: '@fastify/busboy': 2.1.1 @@ -28878,7 +29010,7 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 - vinxi@0.5.8(@types/node@22.18.13)(@upstash/redis@1.35.6)(better-sqlite3@12.4.1)(db0@0.3.4(better-sqlite3@12.4.1))(encoding@0.1.13)(ioredis@5.8.2)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(xml2js@0.6.2)(yaml@2.8.1): + vinxi@0.5.8(@types/node@24.9.1)(@upstash/redis@1.35.6)(better-sqlite3@12.4.1)(db0@0.3.4(better-sqlite3@12.4.1))(encoding@0.1.13)(ioredis@5.8.2)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(xml2js@0.6.2)(yaml@2.8.1): dependencies: '@babel/core': 7.28.5 '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.5) @@ -28912,7 +29044,7 @@ snapshots: unctx: 2.4.1 unenv: 1.10.0 unstorage: 1.17.2(@upstash/redis@1.35.6)(db0@0.3.4(better-sqlite3@12.4.1))(ioredis@5.8.2) - vite: 6.4.1(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + vite: 6.4.1(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) zod: 3.25.76 transitivePeerDependencies: - '@azure/app-configuration' @@ -28959,15 +29091,15 @@ snapshots: - xml2js - yaml - vite-dev-rpc@1.1.0(vite@7.1.12(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)): + vite-dev-rpc@1.1.0(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)): dependencies: birpc: 2.6.1 - vite: 7.1.12(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) - vite-hot-client: 2.1.0(vite@7.1.12(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + vite: 7.1.12(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + vite-hot-client: 2.1.0(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) - vite-hot-client@2.1.0(vite@7.1.12(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)): + vite-hot-client@2.1.0(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)): dependencies: - vite: 7.1.12(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + vite: 7.1.12(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) vite-node@3.2.4(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1): dependencies: @@ -28990,7 +29122,28 @@ snapshots: - tsx - yaml - vite-plugin-checker@0.11.0(eslint@9.39.0(jiti@2.6.1))(optionator@0.9.4)(typescript@5.8.3)(vite@7.1.12(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)): + vite-node@3.2.4(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1): + dependencies: + cac: 6.7.14 + debug: 4.4.3 + es-module-lexer: 1.7.0 + pathe: 2.0.3 + vite: 7.1.12(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + vite-plugin-checker@0.11.0(eslint@9.39.0(jiti@2.6.1))(optionator@0.9.4)(typescript@5.8.3)(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)): dependencies: '@babel/code-frame': 7.27.1 chokidar: 4.0.3 @@ -28999,14 +29152,14 @@ snapshots: picomatch: 4.0.3 tiny-invariant: 1.3.3 tinyglobby: 0.2.15 - vite: 7.1.12(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + vite: 7.1.12(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) vscode-uri: 3.1.0 optionalDependencies: eslint: 9.39.0(jiti@2.6.1) optionator: 0.9.4 typescript: 5.8.3 - vite-plugin-inspect@11.3.3(@nuxt/kit@3.20.0(magicast@0.3.5))(vite@7.1.12(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)): + vite-plugin-inspect@11.3.3(@nuxt/kit@3.20.0(magicast@0.3.5))(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)): dependencies: ansis: 4.2.0 debug: 4.4.3 @@ -29016,8 +29169,8 @@ snapshots: perfect-debounce: 2.0.0 sirv: 3.0.2 unplugin-utils: 0.3.1 - vite: 7.1.12(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) - vite-dev-rpc: 1.1.0(vite@7.1.12(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + vite: 7.1.12(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + vite-dev-rpc: 1.1.0(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) optionalDependencies: '@nuxt/kit': 3.20.0(magicast@0.3.5) transitivePeerDependencies: @@ -29038,25 +29191,40 @@ snapshots: transitivePeerDependencies: - supports-color - vite-plugin-top-level-await@1.6.0(@swc/helpers@0.5.17)(rollup@4.52.5)(vite@7.1.12(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)): + vite-plugin-solid@2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.10)(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)): + dependencies: + '@babel/core': 7.28.5 + '@types/babel__core': 7.20.5 + babel-preset-solid: 1.9.10(@babel/core@7.28.5)(solid-js@1.9.10) + merge-anything: 5.1.7 + solid-js: 1.9.10 + solid-refresh: 0.6.3(solid-js@1.9.10) + vite: 7.1.12(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + vitefu: 1.1.1(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + optionalDependencies: + '@testing-library/jest-dom': 6.9.1 + transitivePeerDependencies: + - supports-color + + vite-plugin-top-level-await@1.6.0(@swc/helpers@0.5.17)(rollup@4.52.5)(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)): dependencies: '@rollup/plugin-virtual': 3.0.2(rollup@4.52.5) '@swc/core': 1.14.0(@swc/helpers@0.5.17) '@swc/wasm': 1.14.0 uuid: 10.0.0 - vite: 7.1.12(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + vite: 7.1.12(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) transitivePeerDependencies: - '@swc/helpers' - rollup - vite-plugin-vue-tracer@1.0.1(vite@7.1.12(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(vue@3.5.22(typescript@5.8.3)): + vite-plugin-vue-tracer@1.0.1(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(vue@3.5.22(typescript@5.8.3)): dependencies: estree-walker: 3.0.3 exsolve: 1.0.7 magic-string: 0.30.21 pathe: 2.0.3 source-map-js: 1.2.1 - vite: 7.1.12(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + vite: 7.1.12(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) vue: 3.5.22(typescript@5.8.3) vite-tsconfig-paths@5.1.4(typescript@5.8.3)(vite@7.1.12(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)): @@ -29080,7 +29248,7 @@ snapshots: fsevents: 2.3.3 terser: 5.44.0 - vite@6.4.1(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1): + vite@6.4.1(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1): dependencies: esbuild: 0.25.12 fdir: 6.5.0(picomatch@4.0.3) @@ -29089,7 +29257,7 @@ snapshots: rollup: 4.52.5 tinyglobby: 0.2.15 optionalDependencies: - '@types/node': 22.18.13 + '@types/node': 24.9.1 fsevents: 2.3.3 jiti: 2.6.1 terser: 5.44.0 @@ -29112,14 +29280,34 @@ snapshots: tsx: 4.20.6 yaml: 2.8.1 - vitefu@1.1.1(vite@6.4.1(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)): + vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1): + dependencies: + esbuild: 0.25.12 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.52.5 + tinyglobby: 0.2.15 optionalDependencies: - vite: 6.4.1(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + '@types/node': 24.9.1 + fsevents: 2.3.3 + jiti: 2.6.1 + terser: 5.44.0 + tsx: 4.20.6 + yaml: 2.8.1 + + vitefu@1.1.1(vite@6.4.1(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)): + optionalDependencies: + vite: 6.4.1(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) vitefu@1.1.1(vite@7.1.12(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)): optionalDependencies: vite: 7.1.12(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + vitefu@1.1.1(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)): + optionalDependencies: + vite: 7.1.12(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + vitepress-plugin-group-icons@1.6.5(vite@5.4.21(@types/node@22.18.13)(terser@5.44.0)): dependencies: '@iconify-json/logos': 1.2.10 @@ -29641,7 +29829,7 @@ snapshots: dependencies: is-wsl: 3.1.0 - wxt@0.20.11(@types/node@22.18.13)(jiti@2.6.1)(rollup@4.52.5)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1): + wxt@0.20.11(@types/node@24.9.1)(jiti@2.6.1)(rollup@4.52.5)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1): dependencies: '@1natsu/wait-element': 4.1.2 '@aklinker1/rollup-plugin-visualizer': 5.12.0(rollup@4.52.5) @@ -29685,8 +29873,8 @@ snapshots: publish-browser-extension: 3.0.3 scule: 1.3.0 unimport: 5.5.0 - vite: 7.1.12(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) - vite-node: 3.2.4(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + vite: 7.1.12(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + vite-node: 3.2.4(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) web-ext-run: 0.2.4 transitivePeerDependencies: - '@types/node' From c82d19b1650a79f0fef305b281e91d6a2798bedd Mon Sep 17 00:00:00 2001 From: unnoq Date: Mon, 3 Nov 2025 19:42:23 +0700 Subject: [PATCH 4/4] refactor(nest): move test files to tests directory --- packages/nest/{src => tests}/nest-features.test.ts | 2 +- packages/nest/{src => tests}/response-types.test.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) rename packages/nest/{src => tests}/nest-features.test.ts (99%) rename packages/nest/{src => tests}/response-types.test.ts (99%) diff --git a/packages/nest/src/nest-features.test.ts b/packages/nest/tests/nest-features.test.ts similarity index 99% rename from packages/nest/src/nest-features.test.ts rename to packages/nest/tests/nest-features.test.ts index fe1ec234e..c84d765d5 100644 --- a/packages/nest/src/nest-features.test.ts +++ b/packages/nest/tests/nest-features.test.ts @@ -17,7 +17,7 @@ import request from 'supertest' import { afterEach, describe, expect, it, vi } from 'vitest' import { z } from 'zod' -import { Implement, ORPCModule } from '.' +import { Implement, ORPCModule } from '../src' // 1. oRPC Contract const testContract = { diff --git a/packages/nest/src/response-types.test.ts b/packages/nest/tests/response-types.test.ts similarity index 99% rename from packages/nest/src/response-types.test.ts rename to packages/nest/tests/response-types.test.ts index 498bb8e4a..3332518ae 100644 --- a/packages/nest/src/response-types.test.ts +++ b/packages/nest/tests/response-types.test.ts @@ -8,8 +8,8 @@ import { implement, withEventMeta } from '@orpc/server' import request from 'supertest' import { afterEach, describe, expect, it } from 'vitest' import { z } from 'zod' -import { Implement } from './implement' -import { ORPCModule } from './module' +import { Implement } from '../src/implement' +import { ORPCModule } from '../src/module' /** * Test suite for validating all supported ORPC response types work correctly