feat(elysia): add isolated Elysia 2 integration - #12
Conversation
There was a problem hiding this comment.
10 findings, verified against the published elysia@2.0.0-beta.4 dist, details inline. The four in plugin-next.ts matter most (span leak on abort, misreported early returns, statusFrom() body/status confusion, throw status(4xx) logged as error); the rest are packaging/tooling issues.
| } | ||
| }) | ||
| .afterResponse(({ request, responseValue, set }) => { | ||
| const client = hub.getClient(); |
There was a problem hiding this comment.
Span leak on client abort. Elysia 2.0.0-beta.4 skips afterResponse when the client aborts (both fetch.mjs and the JIT lanes return early), so finishSpan never runs and every aborted request leaves a Span forever in the unbounded activeSpans Map. Fix: also finalize on request.signal abort, or sweep stale entries.
| if (!client || !ctx || ctx.finalized) return; | ||
|
|
||
| const status = statusFrom(set.status) ?? statusFrom(responseValue) ?? 200; | ||
| const { scope, spanId, startTime } = ctx; |
There was a problem hiding this comment.
Early returns recorded as 200/'ok' and lose traceparent. When another plugin's request hook returns early (CORS 204, auth 401, rate-limit 429), Elysia never assigns context.responseValue nor writes back set.status, so the span falls back to 200/'ok'. That lane also bypasses afterHandle, so the traceparent header is never set.
| } | ||
|
|
||
| function statusFrom(value: unknown): number | undefined { | ||
| if (typeof value === 'number') return value; |
There was a problem hiding this comment.
statusFrom() treats response bodies as HTTP statuses. () => 503 produces a healthy 200 response (503 becomes the body), but the span records http.status_code=503 and status 'error'; () => 'Unauthorized' records 401. Also value in StatusMap walks the prototype chain, so a body like 'toString' resolves to a Function that ends up in span attributes. Fix: rely on set.status (already normalized by afterResponse time) plus Response/ElysiaStatus instances, drop the number and StatusMap-string branches.
| set.headers.traceparent = createTraceparent(ctx.traceId, ctx.spanId, true); | ||
| }) | ||
| .error(({ request, error, set }) => { | ||
| const client = hub.getClient(); |
There was a problem hiding this comment.
throw status(4xx) treated as failure. A thrown ElysiaStatus goes through user .error() hooks first and has no message, so it gets logged as '{"code":401,"response":"Unauthorized"}' at error level, and ctx.errored forces the span to 'error'. Apps using throw status(401) guards flood LogTide with errors for routine rejections. Fix: skip or downgrade ElysiaStatus with code < 500.
| }, | ||
| "peerDependencies": { | ||
| "elysia": ">=1.0.0" | ||
| "elysia": ">=1.4.0 <2.0.0 || >=2.0.0-beta.4 <3.0.0" |
There was a problem hiding this comment.
Peer range wrong at both ends. It drops elysia 1.0-1.3 users (undocumented breaking change, the v1 plugin needs nothing newer), and npm semver only matches prereleases on the same major.minor.patch, so >=2.0.0-beta.4 <3.0.0 rejects future betas like 2.0.1-beta.1 or 2.1.0-beta.0.
| "devDependencies": { | ||
| "@sinclair/typebox": "^0.34.48", | ||
| "elysia": "^1.2.0", | ||
| "elysia": "npm:elysia@latest", |
There was a problem hiding this comment.
Floating dist-tags decide what gets built and tested. Once Elysia promotes 2.0.0 to latest, any lockfile refresh retargets the v1 plugin's build/tests/dts at Elysia 2, and next will drift to 2.1/3.0 betas. Pin ^1.4.29 and an exact or tilde beta instead.
| "@hono/node-server": ">=1.19.10", | ||
| "defu": ">=6.1.5", | ||
| "elysia": ">=1.4.27", | ||
| "elysia@<1.4.27": "1.4.29", |
There was a problem hiding this comment.
The security override no longer guards anything. pnpm's pkg@range selector only matches semver-range specifiers, so elysia@<1.4.27 does not apply to npm:elysia@latest/@next, and the exact 1.4.29 pin will not pick up a future 1.4.30 fix. Suggest "elysia@<2": ">=1.4.27 <2".
| import { defineConfig } from 'vitest/config'; | ||
|
|
||
| export default defineConfig({ | ||
| resolve: { |
There was a problem hiding this comment.
The fixture only works thanks to resolve.dedupe. The built dist/next.js imports bare 'elysia', which outside vitest (bun run src/app.ts, tsx) resolves to the package's own elysia 1.4.29 devDep, so a v1 plugin gets used in a v2 app. Related: the instanceof ElysiaStatus/Response checks break whenever two elysia copies coexist; duck-typing .code/.status would be robust.
| await Promise.all( | ||
| ['dist/next.d.ts', 'dist/next.d.cts'].map(async (path) => { | ||
| const declaration = await readFile(path, 'utf8'); | ||
| const patched = declaration.replaceAll('elysia-next', 'elysia'); |
There was a problem hiding this comment.
Fragile in both directions. The blind replaceAll rewrites any future 'elysia-next' occurrence in the dts output, and the throw-on-no-match fails the whole build if tsup's dts shape changes (dts.resolve, different chunking, or an explicit return type eliding the import). Promise.all can also leave dist half-patched.
| content-disposition: 0.5.4 | ||
| content-type: 1.0.5 | ||
| cookie: 0.7.2 | ||
| cookie: 1.1.1 |
There was a problem hiding this comment.
Side effect: express 4 now resolves cookie 1.1.1. The unbounded root override "cookie": ">=0.7.0" bumped express@4.22.1 from cookie 0.7.2 to 1.1.1 (express 4 pins ~0.7.1 upstream), so the express test apps run a graph no real express 4 user has. express@5.2.1 stays on 0.7.2 in this same diff, confirming it is accidental. Bound the override instead, e.g. cookie@<0.7.0.
Summary
Adds an isolated Elysia 2 integration at
@logtide/elysia/nextwhile preserving the existing Elysia 1.4 contract at@logtide/elysia.Elysia 2 is currently published under the
nextnpm tag (2.0.0-beta.4) and introduces breaking lifecycle and type changes. The two adapters are therefore compiled and tested against separate npm aliases instead of sharing framework types.Changes
@logtide/elysia/next: new Elysia 2 entry point using therequest,afterHandle,error, andafterResponselifecycle APIs.elysia@latest), while the preview export compiles againstelysia@nextthrough theelysia-nextdevelopment alias.afterResponse, after downstream response/error mapping has selected the actual status.status()wrappers, WebResponse, named statuses, mapped errors, and late lifecycle failures are covered.traceparentis injected for both successful and error responses..d.ts, and.d.ctstargets for./next. Published runtime and declaration output references the consumer peer packageelysia, never the development-only alias.test-app-elysia-v2, including typechecking and mock-server smoke coverage through the built@logtide/elysia/nextexport.Tests
pnpm buildpnpm typecheckpnpm testpnpm test:smoke@logtide/elysia: 32 tests pass (11 Elysia 1.4 + 21 Elysia 2)test-app-elysia-v2: typecheck and 3 smoke tests passelysiarather thanelysia-next.