Claude
Skills
Sign in
Back

modernizing-toolchain

Included with Lifetime
$97 forever

Migrates lenne.tech projects from the legacy jest+eslint+prettier toolchain to the current vitest+oxlint+oxfmt baseline used by nest-server-starter and nuxt-base-starter. Covers swc decoratorMetadata config, the @Prop union-type fix for SWC, supertest default-import correction, ESM/CJS interop, the Nitro PORT-vs-NITRO_PORT bug, ANSI escape stripping in workspace runners (lerna/nx), free-port logic for check-server-start.sh, the offers-pattern config.env.ts (NSC__-only + fail-fast + auto-derived appUrl), and the multi-phase check-envs.sh smoke test. Activates whenever someone is migrating an existing project to the new toolchain, debugging "Cannot determine a type for the X field" Mongoose errors, ERR_SOCKET_BAD_PORT crashes from check-server-start, or wants to align an existing project with the current starter conventions.

Code Review

What this skill does


# Modernizing the lenne.tech Toolchain

## When This Skill Activates

- Migrating an existing API/App from jest → vitest, eslint → oxlint, prettier → oxfmt
- Adopting the `check` / `check:fix` / `check:envs` pipeline used by the starters
- Debugging Mongoose `"Cannot determine a type for the X field (union/intersection/ambiguous type was used)"` after switching to vitest+SWC
- Debugging `ERR_SOCKET_BAD_PORT` from `node .output/server/index.mjs` in any check pipeline
- Debugging missing or stale `types.gen.ts` after a Nuxt update
- Aligning a project with the current `config.env.ts` shape (NSC__-only + fail-fast + helper functions)
- Cleaning up phantom Unix-domain-sockets named `[33m12345[39m` in the project root

## Reference Repositories (public)

All references in this skill are to the public lenne.tech repos. **Never reference local clone paths in skill output**:

- API starter: <https://github.com/lenneTech/nest-server-starter>
- API framework: <https://github.com/lenneTech/nest-server>
- App starter: <https://github.com/lenneTech/nuxt-base-starter>
- App framework: <https://github.com/lenneTech/nuxt-extensions>
- Monorepo template: <https://github.com/lenneTech/lt-monorepo>
- CLI: <https://github.com/lenneTech/cli>

## The Migration Checklist (full pipeline)

Apply these in order. Each phase has a clear "done" signal — proceed only when met.

### Phase 1 — Inventory

1. Detect project shape: monorepo (`projects/api`, `projects/app`) vs single-package?
2. Detect package manager: `npm` (check `package-lock.json`) vs `pnpm` (check `pnpm-lock.yaml`).
3. Detect baseline toolchain in EACH subproject:
   - jest? → `jest-e2e.json` or `jest.config.*` exists
   - eslint? → `eslint.config.*` or `.eslintrc.*` exists
   - prettier? → `.prettierrc*` exists
   - vitest? → `vitest.config.ts` or `vitest-e2e.config.ts` exists
   - oxlint? → `oxlint.json` exists
4. Detect deployment shape: GitLab CI? Docker Compose? both?

The migration is identical regardless of mode (monorepo / single, npm / pnpm) — only the invocation
syntax changes. Examples in this skill use `npm run` for npm projects and `pnpm run` for pnpm projects;
substitute as appropriate.

### Phase 2 — API: jest → vitest

1. **Install dev dependencies**:
   ```
   <pm> add -D vitest @vitest/coverage-v8 @vitest/ui unplugin-swc vite-plugin-node
   ```
   Pin `vitest` to the same version the upstream `nest-server-starter` uses (check
   `https://raw.githubusercontent.com/lenneTech/nest-server-starter/main/package.json`).

2. **Create `vitest-e2e.config.ts`** at the API root. The exact swc options are mandatory —
   anything missing causes silent test failures with confusing error messages:
   ```ts
   import swc from 'unplugin-swc';
   import { defineConfig } from 'vitest/config';

   export default defineConfig({
     plugins: [
       swc.vite({
         jsc: {
           target: 'es2022',
           transform: {
             decoratorMetadata: true,    // required: NestJS DI + Mongoose @Prop need this
             legacyDecorator: true,      // required: nest uses pre-stage-3 decorators
             useDefineForClassFields: true, // must MATCH tsconfig.useDefineForClassFields
           },
         },
       }),
     ],
     test: {
       environment: 'node',
       exclude: ['tests/helpers/**/*', 'tests/fixtures/**/*', 'tests/global-setup.ts', 'tests/report.js'],
       fileParallelism: false,           // sequential: each test boots a NestJS app
       globals: true,
       globalSetup: ['tests/global-setup.ts'],
       hookTimeout: 60000,
       include: ['tests/**/*.e2e-spec.ts'],
       isolate: true,
       maxConcurrency: 1,
       pool: 'forks',                    // forks > threads for NestJS
       reporters: ['default'],
       retry: 3,                         // mongo race conditions are flaky
       root: './',
       teardownTimeout: 30000,
       testTimeout: 30000,
       watch: false,
     },
   });
   ```

3. **Create `vitest.config.ts`** for unit specs (slim, mostly defaults):
   ```ts
   import swc from 'unplugin-swc';
   import { defineConfig } from 'vitest/config';

   export default defineConfig({
     plugins: [swc.vite()],
     test: {
       environment: 'node',
       globals: true,
       include: ['src/**/*.spec.ts'],
       root: './',
     },
   });
   ```

4. **Create `tests/global-setup.ts`** that drops the e2e DB before the run:
   ```ts
   import { MongoClient } from 'mongodb';

   export async function setup() {
     const uri = process.env.NSC__MONGOOSE__URI || 'mongodb://127.0.0.1/<your-db>-e2e';
     const c = await MongoClient.connect(uri);
     await c.db().dropDatabase();
     await c.close();
   }
   ```
   Do NOT import `src/config.env` here — it runs outside the swc pipeline and will fail with
   `Cannot find module './src/config.env'`.

5. **Update `tsconfig.json`**:
   ```jsonc
   {
     "compilerOptions": {
       "target": "es2022",                     // bump from es2020 — needed for `new Error(msg, { cause })`
       "useDefineForClassFields": true,        // MUST match swc setting above
       "types": ["vitest/globals"]              // describe/it/expect/vi available without per-file imports
     }
   }
   ```

6. **Migrate all `jest.*` calls** to `vi.*`:
   - `jest.spyOn` → `vi.spyOn`
   - `jest.fn` → `vi.fn`
   - `jest.mock` → `vi.mock`
   - `jest.restoreAllMocks` → `vi.restoreAllMocks`
   - `jest.clearAllMocks` → `vi.clearAllMocks`
   - `jest.resetAllMocks` → `vi.resetAllMocks`

   Watch for **multi-line patterns**:
   ```ts
   const fetchSpy = jest    // <- stays here on its own line
     .spyOn(globalThis, 'fetch')
   ```
   `sed 's/jest\.spyOn/vi.spyOn/g'` won't catch this. Run it again as
   `sed 's/^[[:space:]]*const \([a-zA-Z]*\) = jest$/  const \1 = vi/' || grep "jest$" tests/`.

7. **Replace `import * as supertest`** with default import:
   ```diff
   -import * as supertest from 'supertest';
   +import supertest from 'supertest';
   ```
   The namespace form resolves to `{ default: <function> }` under SWC's CJS↔ESM interop and breaks
   the call site.

8. **The @Prop union-type fix (CRITICAL)**: SWC's `decoratorMetadata` emits `Object` for TypeScript
   union types, where `ts-jest` emits `String`. Mongoose rejects `Object` with:
   ```
   Cannot determine a type for the "MyModelClass.statusField" field
   (union/intersection/ambiguous type was used). Make sure your property
   is decorated with a "@Prop({ type: TYPE_HERE })" decorator.
   ```

   **Fix every `@Prop`** whose property has a union/literal-union TypeScript type by adding
   `type: String` (or `type: Object` for record-like unions):
   ```diff
   -@Prop({ default: 'none' }) transform?: TransformKind;
   +@Prop({ default: 'none', type: String }) transform?: TransformKind;

   -@Prop({ enum: [...], required: true }) mode: AuthMode;
   +@Prop({ enum: [...], required: true, type: String }) mode: AuthMode;

   -@Prop({ default: null }) someId: null | string;
   +@Prop({ default: null, type: String }) someId: null | string;
   ```
   Sweep with: `grep -rn "@Prop" src/server/modules/ | grep -v "type:" | head`. Inspect each match,
   add `type:` if the property type is a literal union (e.g. `'a' | 'b'`), a renamed type alias for
   such a union, or `null | string`.

9. **Update package.json scripts**:
   ```jsonc
   {
     "scripts": {
       "test": "<pm> run vitest",
       "test:ci": "<pm> run vitest:ci",
       "test:e2e": "<pm> run vitest",
       "vitest": "NODE_ENV=e2e vitest run --config vitest-e2e.config.ts",
       "vitest:ci": "NODE_ENV=ci vitest run --config vitest-e2e.config.ts",
       "vitest:cov": "NODE_ENV=e2e vitest run --coverage --config vitest-e2e.config.ts",
       "vitest:watch": "NODE_ENV=e2e vitest --config vitest-e2e.config.ts",
       "vitest:unit": "vitest run --config vitest.config.ts"
     }
   }
   ```

10. **Remove jest artifacts**:
    - delete `jest-e2e.json`, `babel.config.js`, `tests/report.js`
    - uninstall `jest`, `@types/jest`, `babel-jest

Related in Code Review