@elysia/codemod
v2.0.0-exp.49
Published
Readme
Elysia 1.4.x → 2.0 codemod
Codemod for moving Elysia project from 1.4.x to 2.0 syntax
Usage
bunx @elysia/codemodFlags: --check (preview diffs + warnings, no prompt, no writes), --to <major> (target version
to migrate up to, e.g. --to 2; defaults to the newest available — the run chains every config
between the detected version and the target), -y/--yes (skip the prompt, for CI), --force
(run even if the version can't be detected), -h/--help. With no paths the current directory is
scanned (node_modules is skipped via .gitignore); pass paths to narrow it.
# from a checkout of this repo:
bun install
bun run migrate:check ./path # dry run
bun run migrate ./path # apply
bun run build # bundle the CLI to bin/index.js
bun testWhat it does automatically
| # | 1.x | 2.0 | Rule |
|---|-----|-----|------|
| 1 | get(path, handler, hook) | get(path, hook, handler) | 02-verb-arg-swap |
| 3 | onRequest/onParse/onTransform/onBeforeHandle/onAfterHandle/onAfterResponse/onMapResponse/onError | bare request/parse/…/error | 01-event-prefix |
| 3 | onStart / onStop | setup / cleanup | 01-event-prefix |
| 5 | .resolve(fn) | .derive(fn) | 03-resolve-to-derive |
| 5 | macro/guard { resolve: fn } hook | { derive: fn } | 13-resolve-key-to-derive |
| 6 | .as('scoped') | .as('plugin') | 04-scope-as |
| 6 | hook({ as: 'scoped' }, fn) (lifecycle) | hook('plugin', fn) (global/local pass through) | 05-scope-object |
| 6 | .decorate({ as: 'override' }, …) / .state({ as: 'append' }, …) | leading string | 06-decorate-state-scope |
| 6 | .guard({ as: 'scoped', …rest }) | .guard('plugin', { …rest }) (scope-only → guard('plugin', {})) | 12-guard-scope |
| 7 | .guard({ body, … }) | .guard({ schema: 'standalone', body, … }) | 10-guard-standalone |
| 8 | t.Transform(…) | t.Codec(…) | 07-typebox-codec |
| 9 | .macro('auth', def) | .macro({ auth: def }) | 09-macro-object |
| 10 | NotFoundError | NotFound | 08-notfound |
| 10 | getSchemaValidator(s) + its import | Validator.create(s) + import { Validator } | 11-getschemavalidator |
| 10 | .prefix(name, { models }) | .model(prefix.capitalize(name, { models })) + import { prefix } | 14-prefix-to-model |
| 15 | .ws(…) | .use(websocket()) + import { websocket } from 'elysia/websocket' | 15-websocket |
| 15 | new Elysia({ websocket: opts }) | new Elysia().use(websocket(opts)) (config key removed) | 15-websocket |
| 16 | .trace(…) | .use(trace()) + import { trace } from 'elysia/trace' (extends an existing elysia/trace import) | 16-trace |
What it only warns about (manual migration)
These are semantic or context-dependent and would be unsafe to rewrite blindly. migrate:check
prints each one with a warning[…] id:
| Warning | Why manual |
|---|---|
| error-code | error.code is gone — dispatch with instanceof / error(SomeError, fn) |
| set-redirect | set.redirect = x → ({ redirect }) => redirect(x) (needs context destructuring) |
| response-field | response field on after/mapResponse → responseValue |
| parse-contenttype | parse handler lost its 2nd contentType param → use ctx.contentType |
| mount-instance | .mount(instance) → .use(instance) (a .mount(fetchHandler) is still fine) |
| verb-ambiguous | 3-arg verb call whose handler isn't an inline fn and hook isn't an object literal (e.g. get(p, listUsers, hooks.auth)) — swap by hand |
| verb-comment | a comment between verb arguments can block the auto-swap — verify the order |
| resolve-manual | .resolve(namedFn) (bare reference) isn't auto-migrated → .derive(namedFn) |
| guard-scope-callback | a 2-arg guard({ as }, run) — 2.0 has no guard(scope, hook, run); restructure by hand |
| scope-lifecycle-merged | a lifecycle hook object mixing as with other keys (e.g. insert, removed in 2.0) |
| scope-only-object | a scope-only { as: 'scoped' } on a lifecycle hook → use the bare string form |
| removed-methods | .affix()/.suffix() → new Elysia({ name, prefix }) |
| prefix-manual | .prefix(name, models) where models isn't an object literal (only that form auto-migrates) |
| websocket | ws.data is inlined onto ws; generator/yield is preferred over ws.send |
| typebox-removed | t.Recursive / t.Not / t.RegExp removed — no drop-in replacement |
| typebox-novalidate | t.NoValidate now skips Check only; Encode still runs |
| file-type | t.File({ type }) → call setFileTypeDetector() once at your app entry (any detector lib; nothing installed for you) |
Behavior-only changes from MIGRATION.md §11 (afterHandle short-circuit, bodyless GET/HEAD skip
parse, 422 body echo, shared-Response mutation trap, streamResponse raw bytes, WS query
parsing, codec validation order) have no syntactic signature and are documentation-only.
Limitations
- Formatting. Rewrites that span a method chain re-emit the receiver on one line, so a few chained calls can collapse onto a single line. It's recommend to run formatter after.
- Receiver assumption. The verb swap and
on*rename assume the receiver is an Elysia instance. A 3-arg.post()on an Express app, or.onError()on an EventEmitter, would be rewritten too. Review the diff; these are rare in an Elysia codebase. .tsonly. Rules uselanguage: typescript. For.tsxroute files, duplicate the rules withlanguage: tsx(ast-grep treats tsx as a separate grammar).- 2-arg
guard({ as }, run)(a scoped group with a callback) has noguard(scope, hook, run)form in 2.0
Layout
src/
index.ts CLI. detect version, confirm, dispatch to a migration (built to bin/index.js)
detect.ts reads the project's Elysia version (installed copy, then package.json)
migrations.ts registry of version migrations, oldest first
runner.ts the ast-grep runner (node-compatible; resolves the ast-grep binary)
2-0/ the Elysia 1.4 -> 2.0 migration
sgconfig.yml ast-grep config (points at rules/)
rules/*.yml auto-fix rules
rules/warn/*.yml manual-review warnings (no fix)
bin/index.js bundled CLI (the npx/pnpx/bunx entry; `bun run build`)
test/ fixtures + bun test (intent, idempotency, warning coverage)