npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@nage-api/compat

v1.0.0-beta.4

Published

Legacy envelope shim and migration codemods for moving a nest-core-v2 app onto @nage-api

Readme

@nage-api/compat

Migration tooling for an application moving off nest-core-v2 (PLAN.md §23, §25 P1).

Two halves that share nothing but a release:

  • a runtime shim that lets a ported controller keep answering old clients in the old shape, so the controller and its callers can move weeks apart;
  • codemods, a build-time tool that an application never loads.

The package is temporary by design. When no route carries @LegacyEnvelope() any more, delete the import and the dependency; nothing else holds it in place.

Which output to trust

Every codemod declares a kind, and the distinction is the whole point of this surface. A codemod that is right most of the time is worse than no codemod: it produces a large, plausible diff that gets approved by scrolling.

| Codemod | Kind | What it does | | ------------------ | ----------- | --------------------------------------------------------------------------------------------------------------------------------------- | | import-paths | rewrite | Repoints …/core.responses at @nage-api/compat and …/core.decorators at @nage-api/core. Reports the other eleven legacy modules. | | env-rename | rewrite | Fixes the RECAPTCHA_SECRET_KET typo in source and .env. Reports JWT_SECRET and APP_ENGINE. | | res-envelope | report | Locates every @Res() handler and manual envelope build, and says which of three ports each one is. | | config-inventory | report | Inventories every environment variable and config namespace the app reads — the input to env.schema.ts. | | job-generics | report | Locates the anys that defeated the legacy Job / ModelService generics. |

A rewrite edits the file, and only where the result is what a careful developer would have typed: the two rewritten specifiers move name for name onto successors this repository ships, and the rewrite is skipped entirely if any name on the import has no successor. A report never edits anything; it prints path:line, what is wrong there and what to do, because the decision needs a fact the source does not carry.

runCodemods enforces that: a codemod declared as a report that returns a file change throws rather than writing it.

Running them

npx nage-codemod ../nest-core-v2            # report only; nothing is written
npx nage-codemod ../nest-core-v2 --write    # apply the rewrites
npx nage-codemod ../nest-core-v2 --only import-paths --write
npx nage-codemod --list

Reporting is the default and --write is opt-in, which is the inverse of most codemod runners. Three of the five produce nothing but notes, so the useful first run is a read-only one.

The same thing programmatically, for a runner of your own:

import { runCodemods } from '@nage-api/compat';

const report = runCodemods({
  files: [
    {
      path: 'src/order.controller.ts',
      content: "import { Result } from '../../core/core.responses';\n",
    },
  ],
});

// `changed` lists the paths whose content differs; `files` carries every file,
// edited or not, so a caller can write the whole set back or diff it.
export const rewritten: readonly string[] = report.changed;
export const toRead: number = report.notes.length;

Serving the old envelope, one route at a time

NageCompatModule.forRoot() changes nothing on its own — the interceptor it registers only acts on routes marked @LegacyEnvelope():

import { Module } from '@nestjs/common';
import { NageCompatModule } from '@nage-api/compat';

@Module({ imports: [NageCompatModule.forRoot()] })
export class AppModule {}
import { Controller, Get } from '@nestjs/common';
import { LegacyEnvelope } from '@nage-api/compat';
import type { Paginated } from '@nage-api/contracts';

interface Order {
  readonly id: number;
  readonly reference: string;
}

declare const orders: { findAll(): Promise<Paginated<Order>> };

@Controller('orders')
export class OrderController {
  /** Ported: returns data, never touches `res`. Old clients still read `records`. */
  @Get()
  @LegacyEnvelope()
  findAll(): Promise<Paginated<Order>> {
    return orders.findAll();
  }
}

That route answers { records, offset, limit, count, timestamp } — the counters back at the top level, undoing the meta.pagination nesting §16.1 introduced. Remove the decorator when the clients have moved and the route rejoins the standard envelope.

The flag is per route, not per application. NageCoreModule registers ResponseInterceptor unconditionally, so a global switch would have to win a race with it decided by module import order — a correctness property invisible at the call site. @LegacyEnvelope() instead rides on @NoEnvelope(), which makes the core interceptor stand down for exactly these routes whatever order the modules were imported in. It also matches how §23.3 phase 5 says to migrate: controller by controller.

The legacy body is reconstructed, so check it

nest-core-v2 is not a dependency of this repository and its src/core/core.responses.ts is not readable from here. The default body is rebuilt from the two call sites PLAN.md quotes — Result(res, { data, message }) (§4.1) and Result(res, { records, offset, limit, count }) (§22) — plus §16.1's note that the old envelope stamped timestamp on 200s and not on errors. So it spreads the payload and stamps a timestamp, which is the only behaviour both quoted call sites agree on.

Diff it against your own and, where it differs, supply the builder rather than patching this one:

import { NageCompatModule, type LegacyBodyBuilder } from '@nage-api/compat';
import { Module } from '@nestjs/common';

const ourBody: LegacyBodyBuilder = ({ payload, timestamp }) => ({
  status: 'success',
  result: payload,
  timestamp,
});

@Module({ imports: [NageCompatModule.forRoot({ body: ourBody })] })
export class AppModule {}

One thing is deliberately not reproduced: the legacy ErrorResponse echoed error.message || error to the client, which is how SQL text and upstream response bodies reached callers (§5.2). legacyErrorBody takes an already sanitised ErrorPayload, so the shape comes back and the leak does not.

Result and Created for controllers that still hold @Res()

The import-paths codemod repoints …/core.responses here, which makes a legacy controller compile and behave inside a @nage-api application before anyone touches its signature:

import { Controller, Get, Res } from '@nestjs/common';
import { Result, type LegacyResponse } from '@nage-api/compat';

@Controller('orders')
export class OrderController {
  @Get()
  findAll(@Res() res: LegacyResponse): unknown {
    return Result(res, { records: [], offset: 0, limit: 10, count: 0 });
  }
}

This is a step, not a destination. A handler with @Res() in its signature is in library-specific mode: Nest ignores what it returns, ResponseInterceptor never sees the value and AllExceptionsFilter never sees its errors. That is the defect §16.1 replaced, and res-envelope lists every site of it.

The response is typed as LegacyResponse — two methods — rather than Express's Response, so this package pulls no HTTP framework into an application running Fastify, and a test can pass an object literal.

What this deliberately does not do

  • It does not repeat nage doctor --legacy. That scan finds the insecure literals the legacy framework shipped — rejectUnauthorized: false, enableCors(), Math.random(, synchronize: true, the seeded credentials. Run it first; nothing here duplicates it.
  • It does not rewrite Result(res, x) to return x. Rewriting the body without removing the @Res() parameter produces a route that hangs, and removing a parameter changes the signature, its callers and its tests — none of which is decidable from the line being rewritten. res-envelope reports each site with which of the three shapes it is.
  • It does not repoint …/core.errors. NotFoundError and ValidationError exist in @nage-api/core under the same names, so that rewrite would compile — and change the status code and the stable error.code the client receives. A rewrite whose failure mode is a passing build is the one not to make.
  • It ships no entity/schema introspection codemod, though §23.4 names one. Its input is a live database's information_schema rather than source; its output is a migration in a format nage db migrate cannot yet apply; and nothing here could test it against a real schema. Written against a guess it would be the most dangerous artefact in the set — a plausible schema migration for a production database.

Not yet implemented

  • Errors on a @LegacyEnvelope() route still get the new error envelope. AllExceptionsFilter is global and this package does not replace it. Restoring the old error body would mean a second filter carrying its own copy of the status mapping, and the old shape is the one that leaked.
  • @NoEnvelope() and @LegacyEnvelope() cannot both be meaningful on one route; the second is built from the first.
  • None of this has been validated against a real nest-core-v2 application. §28.10 asks for the migration guide to be proved by porting one end to end, and no such application is reachable from this repository. The codemods are tested against before/after fixtures written from PLAN.md's evidence, which proves they do what they say — not that what they say covers a real codebase. Treat the first run on a real repository as the review it is.