Skip to content

Migrate backend from CommonJS to ES Modules#2134

Open
trillium wants to merge 10 commits intodevelopmentfrom
chore/backend-esm-migration
Open

Migrate backend from CommonJS to ES Modules#2134
trillium wants to merge 10 commits intodevelopmentfrom
chore/backend-esm-migration

Conversation

@trillium
Copy link
Copy Markdown
Member

@trillium trillium commented Apr 28, 2026

Fixes #2133

What changes did you make and why did you make them ?

  • Convert all require() calls to ESM import statements across 76 backend files (~160 occurrences)
  • Convert all module.exports to export default or named exports (~55 occurrences)
  • Add explicit .js extensions to all relative imports (required by Node ESM)
  • Replace require.main === module with import.meta.url equivalent (5 files)
  • Replace __dirname with fileURLToPath(import.meta.url) (1 file)
  • Add "type": "module" to backend/package.json
  • Migrate test runner from Jest to Vitest (aligns with client, which already uses Vitest)
  • Convert all jest.mock/jest.fn/jest.spyOn to Vitest equivalents (vi.mock/vi.fn/vi.spyOn)

Note: This PR must be merged before #2137 (test suite fixes), which depends on the ESM/Vitest changes made here.

Screenshots of Proposed Changes Of The Website (if any, please do not screen shot code changes)

N/A — no visual changes, backend only.

trillium and others added 10 commits July 12, 2023 15:33
…tle caching, and commit tracking

- Python skill (verify_stale_issue.py) with issue type detection
- Bash implementations for single and batch issue verification
- Label-based scope detection (dev vs non-dev roles)
- Title consistency caching system
- Git commit history analysis for PR tracking
- Decision tree logic for issue verdict categorization
Convert all require() calls for third-party npm packages (express, mongoose, jsonwebtoken, etc.) to ESM import syntax as the first step of the CJS-to-ESM migration.
…ensions

Convert all require() calls for relative paths (./x, ../x) to ESM import syntax, adding explicit .js file extensions as required by the ESM module resolution algorithm.
Replace all module.exports assignments with ESM export syntax. Barrel files (models/index.js, controllers/index.js, middleware/index.js, validators/index.js) use named exports; individual modules use export default.
…check

Convert the CJS entry-point guard pattern to its ESM equivalent using fileURLToPath(import.meta.url), which is needed because ESM has no require.main.
…configs to .cjs

Flip the ESM switch by adding "type": "module" to backend/package.json. Rename jest.config.js and jest-mongodb-config.js to .cjs since Jest config files must remain CommonJS until the Vitest migration.
Replace Jest with Vitest: add vitest.config.js with node environment and 30s timeout, update package.json scripts (test -> vitest run, test:watch -> vitest), convert jest.setup.js to use vi.mock, and remove jest/jest-mongodb config files and dependencies.
… test files

Replace all Jest test APIs with their Vitest equivalents (jest.mock -> vi.mock, jest.fn -> vi.fn) and add explicit vitest imports (describe, it, expect, vi, etc.) to every test file.
…-test.js

Vitest does not support the done() callback pattern; remove done parameter and done() calls from all test functions, using async/await instead. Add vitest lifecycle imports (beforeAll, afterEach, afterAll) to setup-test.js.
@trillium trillium force-pushed the chore/backend-esm-migration branch from abc5e7a to 140919e Compare April 28, 2026 07:28
@trillium
Copy link
Copy Markdown
Member Author

Code Review: Migrate backend from CommonJS to ES Modules

Overall Assessment

This is a well-structured, methodical migration. The commit history shows a disciplined approach — third-party imports first, then relative imports, then exports, then the require.main pattern, then the "type": "module" flip, then the Jest-to-Vitest migration. The scope is clear and the conversion patterns are applied consistently across ~76 files.

That said, I found a few issues ranging from potential runtime breakage to minor housekeeping.


Issues Found

1. vi.mock() paths missing .js extensions (Medium — potential test breakage)

All vi.mock() calls use extensionless paths, while all import statements correctly use .js extensions. In ESM mode, Vitest resolves mock paths to match import paths. If the resolution doesn't align, mocks may silently fail to intercept the real module.

Affected files:

  • backend/jest.setup.js — 4 occurrences:
    vi.mock('./workers/openCheckins');    // should be './workers/openCheckins.js'
    vi.mock('./workers/closeCheckins');   // should be './workers/closeCheckins.js'
    vi.mock('./workers/createRecurringEvents'); // should be './workers/createRecurringEvents.js'
    vi.mock('./workers/slackbot');        // should be './workers/slackbot.js'
  • backend/controllers/user.controller.test.js:
    vi.mock('../models/user.model');      // should be '../models/user.model.js'
  • backend/routers/auth.router.test.js:
    vi.mock('../controllers/email.controller');  // should be '../controllers/email.controller.js'

Recommendation: Add .js extensions to all vi.mock() paths to match the import paths and ensure consistent resolution.

2. vitest.config.js references jest.setup.js (Low — confusing but functional)

setupFiles: ['./jest.setup.js'],

The file jest.setup.js was updated in place (imports converted, jest.setTimeout removed, jest.mockvi.mock) but never renamed. It works, but the name jest.setup.js is misleading now that Jest is gone. Consider renaming to vitest.setup.js or test.setup.js.

3. jest still listed as a devDependency in package.json (Low — cleanup)

Looking at the diff, jest was removed from devDependencies but @shelf/jest-mongodb was also removed. However, the package-lock.json still shows jest as a transitive dependency. This is probably fine, but worth confirming that jest itself is fully removed from devDependencies (the diff shows it was removed, so this is likely just an observation about the lockfile).

Edit: On closer inspection, the diff does show jest removed from devDependencies. Good.

4. Unrelated files included in the PR (Housekeeping)

The PR includes changes unrelated to the ESM migration:

  • .github/workflows/New-issue-create-card.yml — project name change
  • ai/verify-stale-issues/ — 3 new files (shell scripts + Python), ~960 lines added

These should ideally be in separate PRs to keep the ESM migration reviewable and revertable as a single unit.

5. package-lock.json added alongside existing yarn.lock (Housekeeping)

The PR adds a new backend/package-lock.json (15,864 lines) while also modifying backend/yarn.lock. Having both lockfiles is a recipe for confusion — the project should pick one package manager. If Vitest was installed with npm, consider removing the package-lock.json and using yarn add instead, or vice versa.


What Looks Good

  • "type": "module" in package.json — correctly set.
  • .js extensions on all relative import paths — consistently applied across all source files (routers, controllers, models, middleware, validators, workers, config, setup-test).
  • Barrel files (index.js) use export { ... } (named re-exports) correctly, and consuming files destructure them properly (e.g., import { Event } from '../models/index.js').
  • export default used appropriately for single-export modules (controllers, middleware, routers, workers).
  • Individual model files use export { ModelName } (named exports), matching the original module.exports = { ModelName } pattern — this preserves the import API for consumers.
  • require.main === module replaced with process.argv[1] === fileURLToPath(import.meta.url) in server.js — correct ESM equivalent.
  • assert-env call pattern (import assertEnv from 'assert-env'; assertEnv([...])) is correct — the package uses module.exports = fn, which becomes the default export under ESM interop.
  • Worker side-effect imports in app.js properly split into import + invocation (e.g., import openCheckins from './workers/openCheckins.js'; const runOpenCheckinWorker = openCheckins(cron, fetch);).
  • done() callback removal in test files — correctly removed since Vitest (like modern Jest) handles async tests via returned promises.
  • setup-test.js properly imports beforeAll, afterEach, afterAll from vitest since globals: false in the config.
  • Vitest config has appropriate settings: testTimeout: 30000 (replacing jest.setTimeout(30000)), globals: false, environment: 'node'.
  • Jest config files and dependencies removed (jest.config.js, jest-mongodb-config.js, @shelf/jest-mongodb, jest).

Summary

The core ESM migration is solid. The main actionable item is adding .js extensions to all vi.mock() paths (6 occurrences) to prevent potential mock resolution mismatches. The rest are cleanup suggestions. Nice work on a clean, systematic conversion.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Migrate backend from CommonJS to ES Modules

1 participant