function replaceModules(replaceMap, opts = {disableUnusedError: false}) {
  // Allow callers to specifier an unresolved path, but normalize things
  // by resolving those paths now.
  // TODO: really this should use import.meta.resolve, but... that's not a thing yet!
  const require = createRequire(import.meta.url);
  for (const [k, v] of Object.entries(replaceMap)) {
    try {
      const resolvedPath = require.resolve(k);
      if (resolvedPath !== k) {
        replaceMap[resolvedPath] = v;
        delete replaceMap[k];
      }
    } catch {}
  }

  return {
    name: 'replace-modules',
    setup(build) {
      // Capture modules of interest and resolve them to their absolute paths.
      // This handles real-files on disk, and builtin specifiers.
      build.onResolve({filter: /.*/}, (args) => {
        let resolvedPath;
        try {
          resolvedPath = require.resolve(args.path, {paths: [args.resolveDir]});
        } catch {
          // We should append .js and .ts and .tsx to try and find the correct file...
          // but we aren't shimming such modules at the moment, so whatever.
          return;
        }

        // `resolvedPath` is now either an absolute path on disk, or a builtin module (like `url`).
        if (!(resolvedPath in replaceMap)) return;

        // Put everything we see here into our namespace.
        return {path: resolvedPath, namespace: 'replace-modules'};
      });

      const modulesNotSeen = new Set(Object.keys(replaceMap));
      build.onLoad({filter: /.*/, namespace: 'replace-modules'}, async (args) => {
        // Anything in our namespace is guaranteed to be something in replaceMap.
        modulesNotSeen.delete(args.path);
        return {contents: replaceMap[args.path], resolveDir: path.dirname(args.path)};
      });

      // Handle the third case - when a module is created by some other plugin, and the user of this
      // plugin wishes to replace it.
      build.onLoad({filter: /.*/}, async (args) => {
        // The `onResolve` hook moved all the real modules (builtins and real files on disk) to the `replace-modules`
        // namespace. What remains here are modules that were injected by other plugins. Example: __zlib-lib/inflate
        if (args.path in replaceMap) {
          modulesNotSeen.delete(args.path);
          return {contents: replaceMap[args.path], resolveDir: path.dirname(args.path)};
        }
        return null;
      });

      if (!opts.disableUnusedError) {
        build.onEnd(() => {
          if (modulesNotSeen.size > 0) {
            throw new Error('Unused module replacements: ' + [...modulesNotSeen]);
          }
        });
      }
    },
  };
}
Given a module path, replace the contents with the provided text.

- If the module is a file on disk, the path MUST be absolute.
- Bare builtin specifiers (ex: 'fs', 'path') work too.
- Other loaders may give a resolved path that doesn't reference a filepath.
- In all cases where a module is replaced, no other loaders will process that module.
  If this is ever problematic, this plugin should be converted to be a partial loader.
- This plugin should always be the first loader plugin.

@param {Record<string, string>} replaceMap
@param {{disableUnusedError: boolean}} opts
@return {esbuild.Plugin}