Skip to content

migration:generate / seed:generate always emit .js, which breaks in "type": "module" projects — allow configuring the file extension (e.g. .cjs) #1575

Open

Description

@NaufalK25

What you are doing?

In a project whose package.json has "type": "module" (common with TypeScript ESM setups), generating a migration and running it fails, because the generated file is CommonJS but has a .js extension, which Node treats as ESM in that scope.

// package.json
{ "type": "module" }
npx sequelize-cli migration:generate --name add-users-table
# -> migrations/20261001000000-add-users-table.js   (content: module.exports = { up, down })

npx sequelize-cli db:migrate

What do you expect to happen?

A way to tell the CLI which extension to use for generated migrations and seeders, for example:

npx sequelize-cli migration:generate --name add-users-table --extension cjs

or once in .sequelizerc (whose keys are already passed to yargs as config, see src/core/yargs.js#L6-L17):

// .sequelizerc
module.exports = {
  'migrations-path': 'migrations',
  extension: 'cjs',
};

so the generated file is 20261001000000-add-users-table.cjs, which db:migrate already loads correctly.

What is actually happening?

The extension is hardcoded to js:

Migrations are then loaded with a plain require() by Umzug v2 (umzug/src/migration.js#L68). Under "type": "module" a .js file is ESM, so loading it fails:

# Node 22
module is not defined in ES module scope

# Node < 20.19 / < 22.12
ERR_REQUIRE_ESM

The loader side already supports .cjs: the migrator pattern is /^(?!.*\.d\.ts$).*\.(cjs|js|cts|ts)$/ (src/core/migrator.js#L49), added in #905. Only generation is missing.

Current workaround

We wrap the generate command in a small script that renames the output from .js to .cjs:

#!/usr/bin/env bash
# bin/generate-migration.sh — usage: bin/generate-migration.sh --name <name>
set -euo pipefail

MIGRATIONS_DIR="migrations"
before=$(ls "$MIGRATIONS_DIR")

npx sequelize-cli migration:generate "$@"

for f in "$MIGRATIONS_DIR"/*.js; do
  name=$(basename "$f")
  if ! grep -qxF "$name" <<< "$before"; then
    mv "$f" "${f%.js}.cjs"
    echo "Renamed $name -> ${name%.js}.cjs"
  fi
done

This works, but every project using "type": "module" has to reinvent it, and it's easy to forget when someone runs sequelize-cli migration:generate directly. A built-in option would remove the need for it.

Proposed change

  • Add an --extension option to migration:generate and seed:generate, settable from .sequelizerc like other options.
  • Accept only extensions the migrator actually loads (js, cjs, ts, cts) via yargs choices, default js, so behaviour is unchanged for existing users.
  • Reject mjs, since the migrator pattern skips .mjs files silently and Umzug v2 loads with require().
  • Pass the extension explicitly through getMigrationPath / getSeederPath → getFileName → getFileExtension(extension) (returning extension || 'js'), rather than reading global args inside getFileExtension(). The same helper is also used by getModelPath() (path-helper.js#L76) and init (init-helper.js#L58), and the generated models/index.js only loads .js files (models/index.js#L25), so a global setting would silently break model loading.
  • Template content stays CommonJS, since every accepted extension is loaded through require().

Related

Dialect: any
Database version: N/A
Sequelize CLI version: 6.6.5
Sequelize version: 6.37.7
Node version: 22.x

Would you resolve this issue by submitting a Pull Request?

  • Yes, I have the time and I know how to start.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions