@intuitionrobotics/thunderstorm-codemod
v2.3.4
Published
One-shot migration tool for v0.x to v1.0 of the @intuitionrobotics/* framework. Rewrites legacy module.exports = ... API/aggregator/re-export files to ESM-style export default, and generates static RouteResolver manifests from api/ directories.
Readme
@intuitionrobotics/thunderstorm-codemod
One-shot migration tools for upgrading downstream consumers of the
@intuitionrobotics/* framework. Ships three CLI binaries, all designed
to be run once per repository (and then deleted from your dev deps).
pnpm dlx @intuitionrobotics/thunderstorm-codemod migrate ./src/main/api
pnpm dlx @intuitionrobotics/thunderstorm-codemod bootstrap-routes ./src/main/api --out ./src/main/api/routes.ts
pnpm dlx @intuitionrobotics/thunderstorm-codemod check-routes ./src/main/api --routes ./src/main/api/routes.tsThe
bootstrap-routes/check-routesinvocations above are short for the binsthunderstorm-bootstrap-routesandthunderstorm-check-routes. Both are published with their ownbinentry, so you can wirecheck-routesinto CI directly via:npx --package=@intuitionrobotics/thunderstorm-codemod -- thunderstorm-check-routes ...
What each tool does
thunderstorm-codemod migrate <api-dir>
Three transformations in one pass, all idempotent.
Pass 1 — module.exports rewrite (v0.x → v1). Safe to run on any tree.
| Before | After |
|---|---|
| module.exports = new ServerApi_X(); | export default new ServerApi_X(); |
| module.exports = [a, b, c]; | export default [a, b, c]; |
| module.exports = expr; (anything else) | export default expr; |
| module.exports = require("X"); | import _reexport from "X";export default _reexport; |
Regex-based; multi-line or conditional module.exports are left alone —
review with --dry-run first if you have an unusual layout.
Pass 2 — strip super("path") and collapse _Get/_Post (v1 → v2).
For every ServerApi_* subclass:
- Removes the URL-fragment argument from the
super(...)call. - If the parent is
ServerApi_GetorServerApi_Post, rewrites theextendsclause toextends ServerApi<T>and injectsHttpMethod.GET(or.POST) into the super() args. - Updates the import list: replaces
ServerApi_Get/ServerApi_PostwithServerApi, addsHttpMethod. Preservestypemodifiers on type-only imports (load-bearing underverbatimModuleSyntax).
| Parent class (before migration) | super() args captured |
|---|---|
| extends ServerApi_Get<...> | super("leaf") → arg 0 → leaf, method=GET |
| extends ServerApi_Post<...> | super("leaf") → arg 0 → leaf, method=POST |
| extends ServerApi_Redirect | super("leaf", code, url) → arg 0 |
| extends ServerApi<...> | super(METHOD, "leaf"[, tag]) → arg 1 |
Recording into the sidecar happens for every ServerApi-family
endpoint the codemod sees, even ones with nothing structural to rewrite.
That way bootstrap-routes always knows the method for each endpoint
when generating the routes file.
The sidecar lives at <api-dir>/.thunderstorm-routes-sidecar.json.
Delete it once bootstrap-routes has consumed it.
thunderstorm-bootstrap-routes <api-dir> --out <file.ts>
Reads the sidecar from migrate plus the filesystem layout of <api-dir>
and writes a single Express-Router-tree routes file:
// Auto-generated by thunderstorm-bootstrap-routes — initial seed.
// Hand-edit this from here. The tool runs ONCE; do not regenerate.
import {Router} from "express";
import register from "./v1/register.js";
import endpointExample from "./v1/types-testing/post-without-response-endpoint.js";
const typesTesting = Router();
typesTesting.post("/post-without-response-endpoint", endpointExample.handler);
const v1 = Router();
v1.post("/register", register.handler);
v1.use("/types-testing", typesTesting);
export const apiRoutes: Router = Router();
apiRoutes.use("/v1", v1);Each leaf gets router.<method>("/path", api.handler) — pure Express,
no framework helper. .handler is the cached RequestHandler on every
ServerApi instance.
Conventions inherited from the walker:
_<name>.tsfiles are skipped (private helpers — no v2 equivalent).&<name>.tsfiles were RouteResolver aggregators in v1. They are omitted from the generated routes file. Delete them from your repo after the seed is written; they're vestigial.- Directory names become Express Router subtrees mounted at
/{dirname}. - File basenames are the fallback for leaf URLs when the sidecar has
no captured
leafPath(e.g. files that were already v2-shape on a re-run). - JS reserved words (
delete,class, …) used as identifiers get an underscore prefix (_delete,_class) so the emitted file compiles.
After bootstrap-routes writes the file: edit it freely. The tool
runs once. From here on, adding an endpoint means writing the handler
file and adding one router.<method>("/path", api.handler) line in
this routes file.
thunderstorm-check-routes <api-dir> --routes <file.ts>
CI guard. Verifies every leaf endpoint in <api-dir> is mounted somewhere
in the routes file. Recognised mount patterns:
router.<verb>(path, api.handler)— the canonical case.router.<verb>(path, api)— bare identifier in the handler slot (TS would warn at compile time if this doesn't match the ExpressRequestHandlershape, so it's rare but accepted).const ... = importedX— destructure or alias from an imported identifier. The check trusts the destructured names are mounted further down without re-walking.for (const a of importedArray) ...— iteration over an imported array, typical for dynamic DB-API-generator output:for (const api of generatedApis) router[api.method](`/${api.relativePath}`, api.handler);
Exits 0 if all leaves are mounted; exits 1 with file:line lines for
the missing ones; exits 2 on bad CLI args.
End-to-end migration flow
# 1. Inside your consumer repo, on a clean branch:
pnpm dlx @intuitionrobotics/thunderstorm-codemod migrate ./src/main/api
# 2. Generate the initial Express routes file:
pnpm dlx @intuitionrobotics/thunderstorm-codemod bootstrap-routes \
./src/main/api --out ./src/main/api/routes.ts
# 3. Delete legacy aggregators the tool flagged but left in place:
find ./src/main/api -name '&*.ts' -delete
grep -l 'new RouteResolver(' ./src/main/api -r | xargs rm -f
# 4. Update your server entrypoint:
# - .setInitialRouteResolver(new RouteResolver())
# - .registerApis(...)
# + .setRoutes(apiRoutes) // import from ./api/routes.js
# + (everything else is unchanged)
#
# Plus: any ad-hoc endpoint class living outside api/ needs the same
# Get/Post collapse the codemod did automatically inside api/. See
# docs/migrating-to-v2.md "Endpoint class outside api/" for the
# grep + hand-edit checklist.
# 5. Add the CI guard. Once it passes, commit and remove the codemod
# from your devDependencies — its job is done.
pnpm dlx @intuitionrobotics/thunderstorm-codemod check-routes \
./src/main/api --routes ./src/main/api/routes.tsKnown limitations
- Array-of-endpoints files. A file like
export default [new ApiA(), new ApiB()];is captured in the sidecar with one entry per class, but the bootstrap emits a single import. Hand-edit the generated routes file:
Or, for dynamic DB-API-generator arrays, iterate inline:const [a, b] = arrayFile; router.get("/a", a.handler); router.post("/b", b.handler);for (const api of generatedApis) router[api.method](`/${api.relativePath}`, api.handler);check-routesaccepts both patterns. - Direct
new ServerApi_Redirect("path", code, url)constructions. Not yet rewritten by the codemod (only subclasses are). The bootstrap falls back to the filename for the mount path. Hand-edit if you need the original URL fragment preserved. - Non-string path literals. If
super(METHOD, somePath)uses a variable or expression instead of a string literal, the codemod leaves the file alone. Fix manually. - Endpoint classes outside
api/. The codemod only walks the directory you pass. Endpoint classes in module init code, util files, or anywhere else need a hand-edit (same shape — drop_Get/_Post, addHttpMethodto super, fix imports). - Bare
.tsfiles only..tsx,.js,.cjs,.d.ts, and.test.tsare skipped. - Skips
node_modules,dist,build,.git. Hidden directories (.<name>) too.
Exit codes
| Code | Meaning | |---|---| | 0 | success | | 1 | one or more files failed to read/transform/write, or an endpoint was unmounted | | 2 | invalid CLI arguments |
Bins
The package exposes four binaries:
| Bin | Purpose |
|---|---|
| thunderstorm-codemod | umbrella, currently supports migrate |
| thunderstorm-bootstrap-routes | one-shot Express routes-file seed |
| thunderstorm-check-routes | CI guard: every leaf is mounted |
| thunderstorm-gen-routes | legacy v1 RouteResolver-tree generator (deprecated; kept for repos that haven't migrated yet) |
