From d9e6f39cd256b19a994e1996e76288c1bcf58586 Mon Sep 17 00:00:00 2001 From: saltyaom Date: Wed, 5 Nov 2025 15:57:22 +0700 Subject: [PATCH 01/10] :wrench: fix: #1528 error in parsing request body validation errors with Zod --- .github/workflows/ci.yml | 3 +++ .github/workflows/publish.yml | 3 +++ CHANGELOG.md | 8 ++++++ example/a.ts | 49 +++++++++++++++++++++++------------ package.json | 2 +- src/error.ts | 42 ++++++++++++++++++++++++++---- test/extends/error.test.ts | 37 +++++++++++++++++++++++++- 7 files changed, 121 insertions(+), 23 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b96c2a6a..6c7e7246 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,6 +27,9 @@ jobs: - name: Test run: bun run test + - name: Test + run: bun run test:cf + - name: Publish Preview if: github.event_name == 'pull_request' run: bunx pkg-pr-new publish diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index f20844b7..9b496044 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -43,6 +43,9 @@ jobs: - name: Test run: bun run test + - name: Test + run: bun run test:cf + - name: 'Publish' env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 248c2779..456f9752 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +# 1.4.16 - 5 Nov 2025 +Improvement: +- ValidationError: add `messageValue` as an alias of `errorValue` +- ValidationError.detail now accept optional 2nd parameter `allowUnsafeValidatorDetails` + +Bug fix: +- [#1528](https://github.com/elysiajs/elysia/issues/1528) error in parsing request body validation errors with Zod + # 1.4.15 - 3 Nov 2025 Bug fix: - 1.4.14 regression with Eden Treaty, and OpenAPI type gen diff --git a/example/a.ts b/example/a.ts index 3c6688cb..464009d6 100644 --- a/example/a.ts +++ b/example/a.ts @@ -1,21 +1,38 @@ -import Elysia, { t } from '../src' +import { Elysia } from '../src' +import * as z from 'zod' -new Elysia() +const sendOtpEmailSchema = z.object({ + channel: z.literal('email'), + otpTo: z.email({ error: 'Must be a valid email address' }) +}) + +const sendOtpSmsSchema = z.object({ + channel: z.literal('sms'), + otpTo: z.e164({ error: 'Must be a valid phone number with country code' }) +}) + +const sendOtpSchema = z.discriminatedUnion('channel', [ + sendOtpEmailSchema, + sendOtpSmsSchema +]) + +export const app = new Elysia() + .onError(({ code, error, status }) => { + switch (code) { + case 'VALIDATION': + return error.detail(error.message) + // console.log('error', {error: JSON.parse(error.message)}); + // console.log('error', {error, code, status}); + // return status(422, { type: 'VALIDATION', message: 'Validation error', userMessage: error.message } as OtpErrorInfo) + } + }) .post( - '/mirror', - async ({ status, body }) => status(201, { success: false }), + '/', + async ({ body, set }) => { + return 'ok' + }, { - body: t.Object({ - code: t.String() - }), - response: { - 200: t.Object({ - success: t.Literal(true) - }), - 201: t.Object({ - success: t.Literal(false) - }) - } + body: sendOtpSchema } ) - .listen(3333) + .listen(3000) diff --git a/package.json b/package.json index 621907b6..b7a06245 100644 --- a/package.json +++ b/package.json @@ -178,7 +178,7 @@ ], "license": "MIT", "scripts": { - "test": "bun run test:functionality && bun run test:types && bun run test:node && bun run test:cf", + "test": "bun run test:functionality && bun run test:types && bun run test:node", "test:functionality": "bun test && bun run test:imports", "test:imports": "bun run test/type-system/import.ts", "test:types": "tsc --project tsconfig.test.json", diff --git a/src/error.ts b/src/error.ts index d8d00864..3dabb29d 100644 --- a/src/error.ts +++ b/src/error.ts @@ -153,9 +153,15 @@ export const mapValueError = (error: ValueError | undefined): MapValueError => { summary: undefined } - const { message, path, value, type } = error + let { message, path, value, type } = error + + if (Array.isArray(path)) path = path[0] + + const property = + typeof path === 'string' + ? path.slice(1).replaceAll('/', '.') + : 'unknown' - const property = path.slice(1).replaceAll('/', '.') const isRoot = path === '' switch (type) { @@ -271,8 +277,29 @@ export class ValidationError extends Error { code = 'VALIDATION' status = 422 + /** + * An actual value of `message` + * + * Since `message` is string + * use this instead of message + */ valueError?: ValueError + + /** + * Alias of `valueError` + */ + get messageValue() { + return this.valueError + } + + /** + * Expected value of the schema + */ expected?: unknown + + /** + * Custom error if provided + */ customError?: string constructor( @@ -282,6 +309,9 @@ export class ValidationError extends Error { | TypeCheck | ElysiaTypeCheck | StandardSchemaV1Like, + /** + * Input value + */ public value: unknown, private allowUnsafeValidationDetails = false, errors?: ValueErrorIterator @@ -535,15 +565,17 @@ export class ValidationError extends Error { * }) * ``` */ - detail(message: unknown) { + detail( + message: unknown, + allowUnsafeValidatorDetails = this.allowUnsafeValidationDetails + ) { if (!this.customError) return this.message - const validator = this.validator const value = this.value const expected = this.expected const errors = this.all - return isProduction && !this.allowUnsafeValidationDetails + return isProduction && !allowUnsafeValidatorDetails ? { type: 'validation', on: this.type, diff --git a/test/extends/error.test.ts b/test/extends/error.test.ts index 4767264d..f6af7607 100644 --- a/test/extends/error.test.ts +++ b/test/extends/error.test.ts @@ -2,7 +2,8 @@ import { Elysia, t } from '../../src' import { describe, expect, it } from 'bun:test' -import { req } from '../utils' +import { post, req } from '../utils' +import z from 'zod' class CustomError extends Error { constructor() { @@ -90,4 +91,38 @@ describe('Error', () => { expect(response.status).toBe(422) expect(response.headers.get('content-type')).toBe('application/json') }) + + it('validation error should handle Standard Schema with error.detail', async () => { + const sendOtpEmailSchema = z.object({ + channel: z.literal('email'), + otpTo: z.email({ error: 'Must be a valid email address' }) + }) + + const sendOtpSmsSchema = z.object({ + channel: z.literal('sms'), + otpTo: z.e164({ + error: 'Must be a valid phone number with country code' + }) + }) + + const sendOtpSchema = z.discriminatedUnion('channel', [ + sendOtpEmailSchema, + sendOtpSmsSchema + ]) + + const app = new Elysia() + .onError(({ code, error, status }) => { + switch (code) { + case 'VALIDATION': + return error.detail(error.message) + } + }) + .post('/', ({ body, set }) => 'ok', { + body: sendOtpSchema + }) + + const response = await app.handle(post('/', {})) + + expect(response.status).toBe(422) + }) }) From d195ed59f82ce5cab17825864458a0f347850631 Mon Sep 17 00:00:00 2001 From: saltyaom Date: Wed, 5 Nov 2025 16:03:13 +0700 Subject: [PATCH 02/10] :broom: chore: update exact mirror ersion --- CHANGELOG.md | 1 + package.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 456f9752..cba02095 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ Improvement: Bug fix: - [#1528](https://github.com/elysiajs/elysia/issues/1528) error in parsing request body validation errors with Zod +- [#1527](https://github.com/elysiajs/elysia/issues/1527) bracket handling in exact mirror # 1.4.15 - 3 Nov 2025 Bug fix: diff --git a/package.json b/package.json index b7a06245..41ee96de 100644 --- a/package.json +++ b/package.json @@ -191,7 +191,7 @@ }, "dependencies": { "cookie": "^1.0.2", - "exact-mirror": "0.2.2", + "exact-mirror": "0.2.3", "fast-decode-uri-component": "^1.0.1", "memoirist": "^0.4.0" }, From 9c7856e891c1e16bb237bce1c6d66455badda828 Mon Sep 17 00:00:00 2001 From: saltyaom Date: Sun, 9 Nov 2025 19:40:04 +0700 Subject: [PATCH 03/10] :wrench: fix(websocket): ping/pong not being called --- example/a.ts | 55 +++++++++++++++----------------------- src/adapter/bun/index.ts | 5 ++-- src/ws/index.ts | 6 +++++ test/ws/connection.test.ts | 39 ++++++++++++++++++++++++++- test/ws/utils.ts | 2 +- 5 files changed, 69 insertions(+), 38 deletions(-) diff --git a/example/a.ts b/example/a.ts index 464009d6..98efa557 100644 --- a/example/a.ts +++ b/example/a.ts @@ -1,38 +1,27 @@ import { Elysia } from '../src' -import * as z from 'zod' +import { req } from '../test/utils' -const sendOtpEmailSchema = z.object({ - channel: z.literal('email'), - otpTo: z.email({ error: 'Must be a valid email address' }) -}) +new Elysia() + .ws("/", { + ping() { + console.log("onping") + }, -const sendOtpSmsSchema = z.object({ - channel: z.literal('sms'), - otpTo: z.e164({ error: 'Must be a valid phone number with country code' }) -}) + pong() { + console.log("onpong") + }, -const sendOtpSchema = z.discriminatedUnion('channel', [ - sendOtpEmailSchema, - sendOtpSmsSchema -]) + async message(ws) { + console.log("onmessage") + console.log(ws.body) + }, + }) + .listen(3005) -export const app = new Elysia() - .onError(({ code, error, status }) => { - switch (code) { - case 'VALIDATION': - return error.detail(error.message) - // console.log('error', {error: JSON.parse(error.message)}); - // console.log('error', {error, code, status}); - // return status(422, { type: 'VALIDATION', message: 'Validation error', userMessage: error.message } as OtpErrorInfo) - } - }) - .post( - '/', - async ({ body, set }) => { - return 'ok' - }, - { - body: sendOtpSchema - } - ) - .listen(3000) +const ws = new WebSocket("ws://localhost:3005") + +ws.addEventListener("open", () => { + ws.ping() + ws.send("df") + ws.ping() +}) diff --git a/src/adapter/bun/index.ts b/src/adapter/bun/index.ts index c6f1cbec..9dccd4ed 100644 --- a/src/adapter/bun/index.ts +++ b/src/adapter/bun/index.ts @@ -10,7 +10,7 @@ import { createBunRouteHandler } from './compose' import { createNativeStaticHandler } from './handler-native' import { serializeCookie } from '../../cookies' -import { isProduction, ValidationError } from '../../error' +import { isProduction, status, ValidationError } from '../../error' import { getSchemaValidator } from '../../schema' import { hasHeaderShorthand, @@ -627,8 +627,7 @@ export const BunAdapter: ElysiaAdapter = { ) return - set.status = 400 - return 'Expected a websocket connection' + return status(400, 'Expected a websocket connection') }, { ...rest, diff --git a/src/ws/index.ts b/src/ws/index.ts index 285df7ed..414058f1 100644 --- a/src/ws/index.ts +++ b/src/ws/index.ts @@ -27,6 +27,12 @@ export const websocket: WebSocketHandler = { }, close(ws, code, reason) { ws.data.close?.(ws, code, reason) + }, + ping(ws) { + ws.data.ping?.(ws) + }, + pong(ws) { + ws.data.pong?.(ws) } } diff --git a/test/ws/connection.test.ts b/test/ws/connection.test.ts index 296177bd..8bb62ac8 100644 --- a/test/ws/connection.test.ts +++ b/test/ws/connection.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from 'bun:test' -import { Elysia } from '../../src' +import { Elysia, t } from '../../src' import { newWebsocket, wsOpen, wsClose, wsClosed } from './utils' import { req } from '../utils' @@ -173,4 +173,41 @@ describe('WebSocket connection', () => { name: 'Jane Doe' }) }) + + it('call ping/pong', async () => { + let pinged = false + let ponged = false + + const app = new Elysia() + .ws('/', { + ping() { + pinged = true + }, + pong() { + ponged = true + }, + async message(ws) { + } + }) + .listen(0) + + const ws = new WebSocket(`ws://localhost:${app.server?.port}`) + + await new Promise((resolve) => { + ws.addEventListener('open', () => { + ws.ping() + ws.send('df') + ws.pong() + + resolve() + }, { + once: true + }) + }) + + await Bun.sleep(3) + + expect(pinged).toBe(true) + expect(ponged).toBe(true) + }) }) diff --git a/test/ws/utils.ts b/test/ws/utils.ts index d1571f97..bb20f9bf 100644 --- a/test/ws/utils.ts +++ b/test/ws/utils.ts @@ -1,6 +1,6 @@ import type { Server } from 'bun' -export const newWebsocket = (server: Server, path = '/ws') => +export const newWebsocket = (server: Server, path = '/ws') => new WebSocket(`ws://${server.hostname}:${server.port}${path}`, {}) export const wsOpen = (ws: WebSocket) => From d6630031f0a00d34993d958f279f017313950a9a Mon Sep 17 00:00:00 2001 From: saltyaom Date: Sun, 9 Nov 2025 19:53:18 +0700 Subject: [PATCH 04/10] :blue_book: doc: update changelog --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cba02095..1eea4af8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,13 @@ Improvement: - ValidationError.detail now accept optional 2nd parameter `allowUnsafeValidatorDetails` Bug fix: +- [#1537](https://github.com/elysiajs/elysia/issues/1537) websocket: ping/pong not being called +- [#1536](https://github.com/elysiajs/elysia/pull/1536) export ExtractErrorFromHandle +- [#1535](https://github.com/elysiajs/elysia/pull/1535) skip response validation for generators and streams +- [#1531](https://github.com/elysiajs/elysia/pull/1531) typo in ElysiaTypeCustomErrorCallback: valdation to validation - [#1528](https://github.com/elysiajs/elysia/issues/1528) error in parsing request body validation errors with Zod - [#1527](https://github.com/elysiajs/elysia/issues/1527) bracket handling in exact mirror +- [#1524](https://github.com/elysiajs/elysia/pull/1524) head request handler not working # 1.4.15 - 3 Nov 2025 Bug fix: From 54bf6ac44242566d534030e708a685dc59bdfc41 Mon Sep 17 00:00:00 2001 From: saltyaom Date: Sun, 9 Nov 2025 20:20:22 +0700 Subject: [PATCH 05/10] :tada: feat(macro): add introspect --- CHANGELOG.md | 1 + example/a.ts | 50 +++++++++++++++++++++++++++----------------------- src/index.ts | 14 ++++++++++++++ src/types.ts | 10 +++++++++- 4 files changed, 51 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1eea4af8..ff17f17f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ Improvement: - ValidationError: add `messageValue` as an alias of `errorValue` - ValidationError.detail now accept optional 2nd parameter `allowUnsafeValidatorDetails` +- macro: add `introspect` Bug fix: - [#1537](https://github.com/elysiajs/elysia/issues/1537) websocket: ping/pong not being called diff --git a/example/a.ts b/example/a.ts index 98efa557..c1244b86 100644 --- a/example/a.ts +++ b/example/a.ts @@ -1,27 +1,31 @@ import { Elysia } from '../src' import { req } from '../test/utils' -new Elysia() - .ws("/", { - ping() { - console.log("onping") - }, +const app = new Elysia() + .macro('a', { + introspect(option) { + console.log('a', option) + }, + beforeHandle() { + console.log('before handle a') + } + }) + .macro({ + b: { + introspect(option) { + console.log('b', option) + }, + beforeHandle() { + console.log('before handle a') + } + } + }) + .get('/', () => 'hello world', { + a: true, + b: true, + detail: { + description: 'a' + } + }) - pong() { - console.log("onpong") - }, - - async message(ws) { - console.log("onmessage") - console.log(ws.body) - }, - }) - .listen(3005) - -const ws = new WebSocket("ws://localhost:3005") - -ws.addEventListener("open", () => { - ws.ping() - ws.send("df") - ws.ping() -}) +app.handle(req('/')) diff --git a/src/index.ts b/src/index.ts index 1db3637d..279b22d6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5235,6 +5235,10 @@ export default class Elysia< >, const Property extends MaybeValueOrVoidFunction< MacroProperty< + Metadata['macro'] & + InputSchema & { + [name in Name]?: boolean + }, Schema & MacroContext, Singleton & { derive: Partial @@ -5277,6 +5281,8 @@ export default class Elysia< const Input extends Metadata['macro'] & InputSchema, const NewMacro extends Macro< + Metadata['macro'] & + InputSchema, Input, IntersectIfObjectSchema< MergeSchema< @@ -5321,6 +5327,7 @@ export default class Elysia< const NewMacro extends MaybeFunction< Macro< Input, + // @ts-ignore trust me bro IntersectIfObjectSchema< MergeSchema< UnwrapRoute, @@ -5417,6 +5424,13 @@ export default class Elysia< continue } + if (k === 'introspect') { + value?.(localHook) + + delete localHook[key] + continue + } + if (k === 'detail') { if (!localHook.detail) localHook.detail = {} localHook.detail = mergeDeep(localHook.detail, value, { diff --git a/src/types.ts b/src/types.ts index 93b4ba6f..e4cd4596 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1944,6 +1944,7 @@ export type BaseMacro = Record< export type MaybeValueOrVoidFunction = T | ((...a: any) => void | T) export interface MacroProperty< + in out Macro extends BaseMacro = {}, in out TypedRoute extends RouteSchema = {}, in out Singleton extends SingletonBase = { decorator: {} @@ -1966,9 +1967,16 @@ export interface MacroProperty< afterResponse?: MaybeArray> resolve?: MaybeArray> detail?: DocumentDecoration + /** + * Introspect hook option for documentation generation or analysis + * + * @param option + */ + introspect?(option: Prettify): unknown } export interface Macro< + in out Macro extends BaseMacro = {}, in out Input extends BaseMacro = {}, in out TypedRoute extends RouteSchema = {}, in out Singleton extends SingletonBase = { @@ -1980,7 +1988,7 @@ export interface Macro< in out Errors extends Record = {} > { [K: keyof any]: MaybeValueOrVoidFunction< - Input & MacroProperty + Input & MacroProperty > } From 1a170fddbb3b7c6b9da6b7e5a6b464c6f533dd7d Mon Sep 17 00:00:00 2001 From: saltyaom Date: Tue, 11 Nov 2025 09:00:17 +0700 Subject: [PATCH 06/10] :tada: feat: compile thing --- CHANGELOG.md | 1 + example/a.ts | 44 +++++++++++-------------------- src/adapter/web-standard/index.ts | 6 ++--- src/index.ts | 28 +++++++++++--------- 4 files changed, 35 insertions(+), 44 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ff17f17f..f0d42560 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ Improvement: - ValidationError: add `messageValue` as an alias of `errorValue` - ValidationError.detail now accept optional 2nd parameter `allowUnsafeValidatorDetails` - macro: add `introspect` +- prevent redundant route compilation Bug fix: - [#1537](https://github.com/elysiajs/elysia/issues/1537) websocket: ping/pong not being called diff --git a/example/a.ts b/example/a.ts index c1244b86..bbf2f3a5 100644 --- a/example/a.ts +++ b/example/a.ts @@ -1,31 +1,19 @@ -import { Elysia } from '../src' +import { InternalSymbolName } from 'typescript' +import { Elysia, t } from '../src' import { req } from '../test/utils' -const app = new Elysia() - .macro('a', { - introspect(option) { - console.log('a', option) - }, - beforeHandle() { - console.log('before handle a') - } - }) - .macro({ - b: { - introspect(option) { - console.log('b', option) - }, - beforeHandle() { - console.log('before handle a') - } - } - }) - .get('/', () => 'hello world', { - a: true, - b: true, - detail: { - description: 'a' - } - }) +const app = new Elysia().get('/', () => 'ok').compile() +for (const route of app.routes) route.compile() -app.handle(req('/')) +console.log(app.fetch.toString()) +console.log(app.routes[0].compile().toString()) + +// Bun.sleepSync(7) +// console.log('Slept') + +const res = app + .handle(req('/')) + .then((x) => x.text()) + .then(console.log) + +// process.exit(0) diff --git a/src/adapter/web-standard/index.ts b/src/adapter/web-standard/index.ts index 2d3973de..90c4ad39 100644 --- a/src/adapter/web-standard/index.ts +++ b/src/adapter/web-standard/index.ts @@ -93,10 +93,8 @@ export const WebStandardAdapter: ElysiaAdapter = { fnLiteral += `const u=r.url,` + `s=u.indexOf('/',${standardHostname ? 11 : 7}),` + - `qi=u.indexOf('?',s+1)\n` + - `let p\n` + - `if(qi===-1)p=u.substring(s)\n` + - `else p=u.substring(s, qi)\n` + `qi=u.indexOf('?',s+1),` + + `p=u.substring(s,qi===-1?undefined:qi)\n` if (hasTrace) fnLiteral += `const id=randomId()\n` diff --git a/src/index.ts b/src/index.ts index 279b22d6..f667387e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -909,11 +909,8 @@ export default class Elysia< addResponsePath(path) - let _compiled: ComposedHandler const compile = () => { - if (_compiled) return _compiled - - return (_compiled = composeHandler({ + const compiled = composeHandler({ app: this, path, method, @@ -926,7 +923,12 @@ export default class Elysia< : handle, allowMeta, inference: this.inference - })) + }) + + if (this.router.history[index]) + this.router.history[index].composed = compiled + + return compiled } let oldIndex: number | undefined @@ -941,14 +943,14 @@ export default class Elysia< else this.routeTree[`${method}_${path}`] = this.router.history.length const index = oldIndex ?? this.router.history.length + const route = this.router.history const mainHandler = shouldPrecompile ? compile() : (ctx: Context) => - ( - (this.router.history[index].composed = - compile!()) as ComposedHandler - )(ctx) + ((route[index].composed = compile!()) as ComposedHandler)( + ctx + ) if (oldIndex !== undefined) this.router.history[oldIndex] = Object.assign( @@ -956,7 +958,7 @@ export default class Elysia< method, path, composed: mainHandler, - compile: compile!, + compile, handler: handle, hooks }, @@ -976,7 +978,7 @@ export default class Elysia< method, path, composed: mainHandler, - compile: compile!, + compile, handler: handle, hooks }, @@ -987,7 +989,9 @@ export default class Elysia< ) const handler = { - handler: shouldPrecompile ? mainHandler : undefined, + handler: shouldPrecompile + ? (route[index].composed as ComposedHandler) + : undefined, compile() { return (this.handler = compile!()) } From 777318395d6f422bcc41a32cb9bbbd97968dce76 Mon Sep 17 00:00:00 2001 From: saltyaom Date: Thu, 13 Nov 2025 11:05:36 +0700 Subject: [PATCH 07/10] :tada: feat: merge multiple macro resolve response --- CHANGELOG.md | 1 + example/a.ts | 29 +++++----- src/error.ts | 4 +- src/types.ts | 4 +- test/types/lifecycle/soundness.ts | 88 +++++++++++++++++++++++++++++++ 5 files changed, 106 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f0d42560..5c781185 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ Improvement: - ValidationError.detail now accept optional 2nd parameter `allowUnsafeValidatorDetails` - macro: add `introspect` - prevent redundant route compilation +- merge multiple macro resolve response Bug fix: - [#1537](https://github.com/elysiajs/elysia/issues/1537) websocket: ping/pong not being called diff --git a/example/a.ts b/example/a.ts index bbf2f3a5..2725d880 100644 --- a/example/a.ts +++ b/example/a.ts @@ -1,19 +1,16 @@ -import { InternalSymbolName } from 'typescript' -import { Elysia, t } from '../src' -import { req } from '../test/utils' +import { Elysia, status, t } from '../src' -const app = new Elysia().get('/', () => 'ok').compile() -for (const route of app.routes) route.compile() +status(401) -console.log(app.fetch.toString()) -console.log(app.routes[0].compile().toString()) +const app = new Elysia() + .macro({ + multiple: { + resolve({ status }) { + if (Math.random() > 0.5) return status(401) + return status(403) + } + } + }) + .get('/multiple', () => 'Ok', { multiple: true }) -// Bun.sleepSync(7) -// console.log('Slept') - -const res = app - .handle(req('/')) - .then((x) => x.text()) - .then(console.log) - -// process.exit(0) +app['~Routes']['multiple']['get']['response'] diff --git a/src/error.ts b/src/error.ts index 3dabb29d..88ba2411 100644 --- a/src/error.ts +++ b/src/error.ts @@ -39,7 +39,7 @@ const emptyHttpStatus = { 308: undefined } as const -export type SelectiveStatus = < +export type SelectiveStatus = < const Code extends | keyof Res | InvertedStatusMap[Extract] @@ -91,7 +91,7 @@ export const status = < >( code: Code, response?: T -) => new ElysiaCustomStatusResponse(code, response as any) +) => new ElysiaCustomStatusResponse(code, response as T) export class InternalServerError extends Error { code = 'INTERNAL_SERVER_ERROR' diff --git a/src/types.ts b/src/types.ts index e4cd4596..22e72d17 100644 --- a/src/types.ts +++ b/src/types.ts @@ -966,7 +966,7 @@ type ExtractOnlyResponseFromMacro = ? IsNever extends true ? {} : { - return: A extends ElysiaCustomStatusResponse< + return: UnionToIntersect< A extends ElysiaCustomStatusResponse< any, infer Value, infer Status @@ -977,7 +977,7 @@ type ExtractOnlyResponseFromMacro = InvertedStatusMap[Status] : Value } - : {} + : {}> } : {} diff --git a/test/types/lifecycle/soundness.ts b/test/types/lifecycle/soundness.ts index 91134ad7..bd9df867 100644 --- a/test/types/lifecycle/soundness.ts +++ b/test/types/lifecycle/soundness.ts @@ -2192,3 +2192,91 @@ import { Prettify } from '../../../src/types' } }) } + +// intersect multiple resolve macro response +{ + const app = new Elysia() + .macro({ + multiple: { + resolve({ status }) { + if (Math.random() > 0.5) return status(401) + return status(403) + } + } + }) + .get('/multiple', () => 'Ok', { multiple: true }) + + expectTypeOf< + (typeof app)['~Routes']['multiple']['get']['response'] + >().toEqualTypeOf<{ + 200: string + 401: 'Unauthorized' + 403: 'Forbidden' + }>() +} + +// intersect multiple resolve macro response +{ + // intersect multiple resolve macro response + { + const app = new Elysia() + .macro({ + multiple: { + resolve({ status }) { + if (Math.random() > 0.5) return status(401) + return status(403) + } + } + }) + .get('/multiple', () => 'Ok', { multiple: true }) + + expectTypeOf< + (typeof app)['~Routes']['multiple']['get']['response'] + >().toEqualTypeOf<{ + 200: string + 401: 'Unauthorized' + 403: 'Forbidden' + }>() + + const app = new Elysia() + .macro('multiple', { + resolve({ status }) { + if (Math.random() > 0.5) return status(401) + return status(403) + } + }) + .get('/multiple', () => 'Ok', { multiple: true }) + + expectTypeOf< + (typeof app)['~Routes']['multiple']['get']['response'] + >().toEqualTypeOf<{ + 200: string + 401: 'Unauthorized' + 403: 'Forbidden' + }>() + } + const app = new Elysia() + .macro('multiple', { + resolve({ status }) { + if (Math.random() > 0.5) return status(401) + return status(403) + } + }) + .get('/multiple', () => 'Ok', { multiple: true }) + + expectTypeOf< + (typeof app)['~Routes']['multiple']['get']['response'] + >().toEqualTypeOf<{ + 200: string + 401: 'Unauthorized' + 403: 'Forbidden' + }>() + + expectTypeOf< + (typeof app)['~Routes']['multiple']['get']['response'] + >().toEqualTypeOf<{ + 200: string + 401: 'Unauthorized' + 403: 'Forbidden' + }>() +} From dd6afdde23fa5c5f33f9b0ad7b6f873011af320f Mon Sep 17 00:00:00 2001 From: saltyaom Date: Thu, 13 Nov 2025 11:07:03 +0700 Subject: [PATCH 08/10] :tada: feat: merge multiple macro resolve response --- test/types/lifecycle/soundness.ts | 58 +++++++++++-------------------- 1 file changed, 20 insertions(+), 38 deletions(-) diff --git a/test/types/lifecycle/soundness.ts b/test/types/lifecycle/soundness.ts index bd9df867..56853eb4 100644 --- a/test/types/lifecycle/soundness.ts +++ b/test/types/lifecycle/soundness.ts @@ -2217,44 +2217,26 @@ import { Prettify } from '../../../src/types' // intersect multiple resolve macro response { - // intersect multiple resolve macro response - { - const app = new Elysia() - .macro({ - multiple: { - resolve({ status }) { - if (Math.random() > 0.5) return status(401) - return status(403) - } - } - }) - .get('/multiple', () => 'Ok', { multiple: true }) - - expectTypeOf< - (typeof app)['~Routes']['multiple']['get']['response'] - >().toEqualTypeOf<{ - 200: string - 401: 'Unauthorized' - 403: 'Forbidden' - }>() - - const app = new Elysia() - .macro('multiple', { - resolve({ status }) { - if (Math.random() > 0.5) return status(401) - return status(403) - } - }) - .get('/multiple', () => 'Ok', { multiple: true }) - - expectTypeOf< - (typeof app)['~Routes']['multiple']['get']['response'] - >().toEqualTypeOf<{ - 200: string - 401: 'Unauthorized' - 403: 'Forbidden' - }>() - } + const app = new Elysia() + .macro('multiple', { + resolve({ status }) { + if (Math.random() > 0.5) return status(401) + return status(403) + } + }) + .get('/multiple', () => 'Ok', { multiple: true }) + + expectTypeOf< + (typeof app)['~Routes']['multiple']['get']['response'] + >().toEqualTypeOf<{ + 200: string + 401: 'Unauthorized' + 403: 'Forbidden' + }>() +} + +// intersect multiple resolve macro response +{ const app = new Elysia() .macro('multiple', { resolve({ status }) { From 58d9659cb89e67f8062905fa7678d37f829b1f65 Mon Sep 17 00:00:00 2001 From: saltyaom Date: Thu, 13 Nov 2025 11:26:05 +0700 Subject: [PATCH 09/10] :tada: feat: update changelog --- example/a.ts | 26 ++++++++++++++++---------- package.json | 2 +- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/example/a.ts b/example/a.ts index 2725d880..32191dbe 100644 --- a/example/a.ts +++ b/example/a.ts @@ -1,16 +1,22 @@ import { Elysia, status, t } from '../src' -status(401) - -const app = new Elysia() - .macro({ - multiple: { - resolve({ status }) { - if (Math.random() > 0.5) return status(401) - return status(403) +const auth = (app: Elysia) => + app.derive(({ headers, status }) => { + try { + const token = headers['authorization']?.replace('Bearer ', '') || '' + return { + isAuthenticated: true } + } catch (e) { + const error = e as Error + console.error('Authentication error:', error.message) + return status(401, 'Unauthorized') } }) - .get('/multiple', () => 'Ok', { multiple: true }) -app['~Routes']['multiple']['get']['response'] +const app = new Elysia() + .use(auth) + .get('/', ({ isAuthenticated }) => isAuthenticated) + .listen(5000) + +app['~Routes'] diff --git a/package.json b/package.json index 41ee96de..f026f3e7 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "elysia", "description": "Ergonomic Framework for Human", - "version": "1.4.15", + "version": "1.4.16", "author": { "name": "saltyAom", "url": "https://github.com/SaltyAom", From cc012817c6d99e8c59734f66537b085b0cc3a5a7 Mon Sep 17 00:00:00 2001 From: saltyaom Date: Thu, 13 Nov 2025 11:26:49 +0700 Subject: [PATCH 10/10] :blue_book: doc: merge main --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c781185..2dedc914 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -# 1.4.16 - 5 Nov 2025 +# 1.4.16 - 13 Nov 2025 Improvement: - ValidationError: add `messageValue` as an alias of `errorValue` - ValidationError.detail now accept optional 2nd parameter `allowUnsafeValidatorDetails` @@ -7,6 +7,7 @@ Improvement: - merge multiple macro resolve response Bug fix: +- [#1543](https://github.com/elysiajs/elysia/pull/1524) respect toResponse() method on Error classes - [#1537](https://github.com/elysiajs/elysia/issues/1537) websocket: ping/pong not being called - [#1536](https://github.com/elysiajs/elysia/pull/1536) export ExtractErrorFromHandle - [#1535](https://github.com/elysiajs/elysia/pull/1535) skip response validation for generators and streams