From a6bf3d866f6d4c57d21513a0e72c7d423d8cff9a Mon Sep 17 00:00:00 2001 From: saltyaom Date: Mon, 1 Jan 2024 23:38:41 +0700 Subject: [PATCH] :broom: chore: review issues --- CHANGELOG.md | 11 +++++++ build.ts | 3 +- example/a.ts | 26 ++++++++++------- package.json | 2 +- src/compose.ts | 51 ++++++++++++++++++++++++--------- src/cookie.ts | 4 +-- src/error.ts | 4 +-- src/handler.ts | 10 ++++++- src/index.ts | 7 +++-- src/utils.ts | 3 ++ test/core/elysia.test.ts | 19 ++++++++++++ test/core/handle-error.test.ts | 24 +++++++++++++++- test/lifecycle/request.test.ts | 16 +++++++++++ test/units/map-response.test.ts | 2 +- test/ws/message.test.ts | 20 +++++++++++++ 15 files changed, 167 insertions(+), 35 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f35b11dd..7426e32a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +# 0.8.7. - 29 Dec 2023 +Improvement: +- [#385](https://github.com/elysiajs/elysia/issues/385) If error is instanceof Response, respond with it + +Bug fix: +- onRequest doesn't early return +- handle thrown error function +- [#373](https://github.com/elysiajs/elysia/issues/373) cookie is not set when File is return +- [#379](https://github.com/elysiajs/elysia/issues/379) WebSocket: Sending a space character ' ' receives 0 +- [#317](https://github.com/elysiajs/elysia/issues/317) Exclude TypeBox from bundling + # 0.8.6. - 29 Dec 2023 Bug fix: - body without default value thrown Object.assign error diff --git a/build.ts b/build.ts index 5b5b9968..8a3e4564 100644 --- a/build.ts +++ b/build.ts @@ -5,7 +5,8 @@ await Bun.build({ outdir: './dist/bun', minify: true, target: 'bun', - sourcemap: 'external' + sourcemap: 'external', + 'external': ['@sinclair/typebox'] }) copyFileSync('dist/index.d.ts', 'dist/bun/index.d.ts') diff --git a/example/a.ts b/example/a.ts index ebbaf33a..568ada5f 100644 --- a/example/a.ts +++ b/example/a.ts @@ -1,14 +1,20 @@ -import { Elysia, t } from '../src' -import { cors } from '@elysiajs/cors' +import { Elysia, error, t } from '../src' +import { req } from '../test/utils' -export const cache = () => - new Elysia({ name: 'cache' }).mapResponse(({ response }) => { - return new Response('hello world') +const app = new Elysia().group('inbox', (app) => + app.ws('join', { + body: t.Object({ + inboxId: t.String() + }), + message(ws, { inboxId }) { + ws.send(inboxId) + } }) +).listen(3000) -export default cache +// const response = await app.handle(req('/a')).then((x) => x.text()) +// console.log(response) -const app = new Elysia() - .use(cache) - .get('/', () => 'A') - .listen(3000) +console.log( + `🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}` +) diff --git a/package.json b/package.json index a9bc4b1d..a8589275 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "elysia", "description": "Ergonomic Framework for Human", - "version": "0.8.6", + "version": "0.8.7", "author": { "name": "saltyAom", "url": "https://github.com/SaltyAom", diff --git a/src/compose.ts b/src/compose.ts index e8381871..f1a59fd0 100644 --- a/src/compose.ts +++ b/src/compose.ts @@ -21,7 +21,8 @@ import { NotFoundError, ValidationError, InternalServerError, - ERROR_CODE + ERROR_CODE, + ELYSIA_RESPONSE } from './error' import { CookieOptions, parseCookie } from './cookie' @@ -1612,8 +1613,11 @@ export const composeGeneralHandler = ( ` } - return ${maybeAsync ? 'async' : ''} function map(request) { - ` + return ${maybeAsync ? 'async' : ''} function map(request) {\n` + + if(app.event.request.length) + fnLiteral += `let re` + const traceLiteral = app.event.trace.map((x) => x.toString()) const report = createReport({ @@ -1670,17 +1674,16 @@ export const composeGeneralHandler = ( name: app.event.request[i].name }) - const name = `re${i}` - if (withReturn) { - fnLiteral += `const ${name} = mapEarlyResponse( + fnLiteral += `re = mapEarlyResponse( ${maybeAsync ? 'await' : ''} onRequest[${i}](ctx), ctx.set )\n` endUnit() - // fnLiteral += `if(${name}) return ${name}\n` + if(withReturn) + fnLiteral += `if(re !== undefined) return re\n` } else { fnLiteral += `${ maybeAsync ? 'await' : '' @@ -1810,17 +1813,25 @@ export const composeErrorHandler = ( let fnLiteral = `const { app: { event: { error: onError, onResponse: res } }, mapResponse, - ERROR_CODE + ERROR_CODE, + ELYSIA_RESPONSE } = inject return ${ app.event.error.find(isAsync) ? 'async' : '' } function(context, error) { + let r + const { set } = context context.code = error.code context.error = error - ` + + if(error[ELYSIA_RESPONSE]) { + error.status = error[ELYSIA_RESPONSE] + error.message = error.response + } +` for (let i = 0; i < app.event.error.length; i++) { const handler = app.event.error[i] @@ -1829,9 +1840,16 @@ export const composeErrorHandler = ( }onError[${i}](context)` if (hasReturn(handler.toString())) - fnLiteral += `const r${i} = ${response}; if(r${i} !== undefined) { + fnLiteral += `r = ${response}; if(r !== undefined) { + if(r instanceof Response) return r + + if(r[ELYSIA_RESPONSE]) { + error.status = error[ELYSIA_RESPONSE] + error.message = error.response + } + if(set.status === 200) set.status = error.status - return mapResponse(r${i}, set) + return mapResponse(r, set) }\n` else fnLiteral += response + '\n' } @@ -1843,7 +1861,13 @@ export const composeErrorHandler = ( { headers: set.headers, status: set.status } ) } else { - return new Response(error.message, { headers: set.headers, status: error.status ?? 500 }) + if(error.code && typeof error.status === "number") + return new Response( + error.message, + { headers: set.headers, status: error.status } + ) + + return mapResponse(error, set) } }` @@ -1853,6 +1877,7 @@ export const composeErrorHandler = ( )({ app, mapResponse, - ERROR_CODE + ERROR_CODE, + ELYSIA_RESPONSE }) } diff --git a/src/cookie.ts b/src/cookie.ts index 5edf9fde..a5049194 100644 --- a/src/cookie.ts +++ b/src/cookie.ts @@ -2,7 +2,7 @@ import { parse } from 'cookie' import type { Context } from './context' -import { unsignCookie } from './utils' +import { isNumericString, unsignCookie } from './utils' import { InvalidCookieSignature } from './error' export interface CookieOptions { @@ -431,7 +431,7 @@ export const parseCookie = async ( } // @ts-ignore - if (!Number.isNaN(+value)) value = +value + if (isNumericString(value)) value = +value // @ts-ignore else if (value === 'true') value = true // @ts-ignore diff --git a/src/error.ts b/src/error.ts index 93909ae9..02a6f0b9 100644 --- a/src/error.ts +++ b/src/error.ts @@ -12,7 +12,8 @@ const env = ? process?.env : undefined -export const ERROR_CODE = Symbol('ErrorCode') +export const ERROR_CODE = Symbol('ElysiaErrorCode') +export const ELYSIA_RESPONSE = Symbol('ElysiaResponse') export const isProduction = (env?.NODE_ENV ?? env?.ENV) === 'production' @@ -23,7 +24,6 @@ export type ElysiaErrors = | ValidationError | InvalidCookieSignature -export const ELYSIA_RESPONSE = Symbol('ElysiaResponse') export const error = < const Code extends number | keyof typeof StatusMap, diff --git a/src/handler.ts b/src/handler.ts index 82be2e1d..7ea8d391 100644 --- a/src/handler.ts +++ b/src/handler.ts @@ -20,6 +20,7 @@ export const isNotEmpty = (obj: Object) => { const handleFile = (response: File | Blob, set?: Context['set']) => { const size = response.size + if ( (size && set && @@ -29,7 +30,13 @@ const handleFile = (response: File | Blob, set?: Context['set']) => { set.status !== 416) || (!set && size) ) { - if (set) + if (set) { + if (set.headers instanceof Headers) + if (hasHeaderShorthand) set.headers = set.headers.toJSON() + else + for (const [key, value] of set.headers.entries()) + if (key in set.headers) set.headers[key] = value + return new Response(response as Blob, { status: set.status as number, headers: Object.assign( @@ -40,6 +47,7 @@ const handleFile = (response: File | Blob, set?: Context['set']) => { set.headers ) }) + } return new Response(response as Blob, { headers: { diff --git a/src/index.ts b/src/index.ts index 8f6d8dd3..e631465b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -28,7 +28,8 @@ import { asGlobal, traceBackMacro, replaceUrlPath, - primitiveHooks + primitiveHooks, + isNumericString } from './utils' import { @@ -40,11 +41,11 @@ import { import { isProduction, ERROR_CODE, + ELYSIA_RESPONSE, ValidationError, type ParseError, type NotFoundError, type InternalServerError, - ELYSIA_RESPONSE } from './error' import type { @@ -3199,7 +3200,7 @@ export default class Elysia< } catch { // Not empty } - else if (!Number.isNaN(+message)) message = +message + else if (isNumericString(message)) message = +message } if (transform?.length) diff --git a/src/utils.ts b/src/utils.ts index 1d877c15..c4c9e9e9 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -582,3 +582,6 @@ export const traceBackMacro = ( traceBackMacro(extension[key], value as any, hooks) } } + +export const isNumericString = (message: string) => + !Number.isNaN(parseInt(message)) diff --git a/test/core/elysia.test.ts b/test/core/elysia.test.ts index 91b078f3..21466b5b 100644 --- a/test/core/elysia.test.ts +++ b/test/core/elysia.test.ts @@ -105,4 +105,23 @@ describe('Edge Case', () => { expect(await strict.handle(req('/a')).then((x) => x.status)).toBe(404) expect(await strict.handle(req('/a/')).then((x) => x.status)).toBe(200) }) + + it('return cookie with file', async () => { + const kyuukararin = Bun.file('test/kyuukurarin.mp4') + + const app = new Elysia().get('/', ({ cookie: { name } }) => { + name.set({ + value: 'Rikuhachima Aru', + maxAge: new Date().setFullYear(new Date().getFullYear() + 1), + httpOnly: true + }) + + return kyuukararin + }) + + const response = await app.handle(req('/')).then((x) => x.headers.toJSON()) + + expect(response['set-cookie']).toHaveLength(1) + expect(response['content-type']).toBe('video/mp4') + }) }) diff --git a/test/core/handle-error.test.ts b/test/core/handle-error.test.ts index 8a6e6cc2..f4442540 100644 --- a/test/core/handle-error.test.ts +++ b/test/core/handle-error.test.ts @@ -1,4 +1,4 @@ -import { Elysia, InternalServerError, NotFoundError, t } from '../../src' +import { Elysia, InternalServerError, NotFoundError, error, t } from '../../src' import { describe, expect, it } from 'bun:test' import { req } from '../utils' @@ -146,4 +146,26 @@ describe('Handle Error', () => { expect(await response.text()).toEqual('handled') expect(response.status).toEqual(418) }) + + it('handle thrown error function', async () => { + const app = new Elysia().get('/', () => { + throw error(404, 'Not Found :(') + }) + + const response = await app.handle(req('/')) + + expect(await response.text()).toEqual('Not Found :(') + expect(response.status).toEqual(404) + }) + + it('handle thrown Response', async () => { + const app = new Elysia().get('/', () => { + throw error(404, 'Not Found :(') + }) + + const response = await app.handle(req('/')) + + expect(await response.text()).toEqual('Not Found :(') + expect(response.status).toEqual(404) + }) }) diff --git a/test/lifecycle/request.test.ts b/test/lifecycle/request.test.ts index d8f1840b..487da5eb 100644 --- a/test/lifecycle/request.test.ts +++ b/test/lifecycle/request.test.ts @@ -28,4 +28,20 @@ describe('On Request', () => { expect(res.headers.get('name')).toBe('llama') }) + + it('early return', async () => { + const app = new Elysia() + .onRequest(({ set }) => { + set.status = 401 + return 'Unauthorized' + }) + .get('/', () => { + console.log("This shouldn't be run") + return "You shouldn't see this" + }) + + const res = await app.handle(req('/')) + expect(await res.text()).toBe('Unauthorized') + expect(res.status).toBe(401) + }) }) diff --git a/test/units/map-response.test.ts b/test/units/map-response.test.ts index 02e0c460..0045cc67 100644 --- a/test/units/map-response.test.ts +++ b/test/units/map-response.test.ts @@ -332,7 +332,7 @@ describe('Map Response', () => { const response = mapResponse(kyuukararin, { ...defaultContext, - status: 304, + status: 304 }) expect(response).toBeInstanceOf(Response) diff --git a/test/ws/message.test.ts b/test/ws/message.test.ts index baf4004d..7729705e 100644 --- a/test/ws/message.test.ts +++ b/test/ws/message.test.ts @@ -233,9 +233,29 @@ describe('WebSocket message', () => { const { type, data } = await message expect(type).toBe('message') + // @ts-ignore expect(data).toEqual(new Uint8Array(3)) await wsClosed(ws) app.stop() }) + + it('should send & receive', async () => { + const app = new Elysia() + .ws('/ws', { + message(ws, message) { + ws.send(message) + } + }) + .listen(0) + const ws = newWebsocket(app.server!) + await wsOpen(ws) + const message = wsMessage(ws) + ws.send(' ') + const { type, data } = await message + expect(type).toBe('message') + expect(data).toBe(' ') + await wsClosed(ws) + app.stop() + }) })