sequelize / umzug

Framework agnostic migration tool for Node.js
MIT License
1.98k stars 158 forks source link

Only URLs with a scheme in: file, data, and node are supported by the default ESM loader #672

Open mmakhalaf opened 1 month ago

mmakhalaf commented 1 month ago

I'm getting this error after switching to my package to a node module and Typescript module resolution to NodeNext,

My tsconfig module settings are as follows amongst other things, and uses project references but I don't think that's the issue,

    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",

I don't have the script that's running in the same directory as the migration directory, so I have to do something like this for glob to work.

migrations: {
    glob: [
      `${__dirname}/migrations/*.*ts`,
    ]
}

or

  migrations: {
    glob: [
      `migrations/*.*ts`,
      {
        cwd: __dirname,
      },
    ],
  },

The example says that I should define __dirname as const __dirname = new URL(".", import.meta.url).pathname.replace(/\/$/, ""). This puts a / at the start of the path, which breaks glob. I can remove it (i.e. const __dirname = new URL(".", import.meta.url).pathname.replace(/\/$/, "").replace(/^\//, "");) which allows glob to find the migration files, but then the subsequent dynamic import fails with this error,

Error: Migration 2023.08.16T14.29.36.init.ts (up) failed: Original error: Only URLs with a scheme in: file, data, and node are supported by the default ESM loader. On Windows, absolute paths must be valid file:// URLs. Received protocol 'd:'.

I can work around the issue by overriding the resolver and doing the dynamic import like this, I simplified it also for our case,

const defaultResolver: Resolver<unknown> = ({ name, path: filepath }) => {
  if (!filepath) {
    throw new Error(`Can't use default resolver for non-filesystem migrations`);
  }

  const loadModule: () => Promise<RunnableMigration<unknown>> = async () =>
    import(`file://${filepath}`) as Promise<RunnableMigration<unknown>>;

  const getModule = async () => {
    return await loadModule();
  };

  return {
    name,
    path: filepath,
    up: async ({ context }) =>
      (await getModule()).up({ path: filepath, name, context }),
    down: async ({ context }) =>
      (await getModule()).down?.({ path: filepath, name, context }),
  };
};