function registerModules(loader, modules, callback) {
  const compatible = modules.filter((m) => ['noflo', 'noflo-nodejs'].includes(m.runtime));
  /** @type {string[]} */
  const componentLoaders = [];
  Promise.all(compatible.map((m) => {
    if (m.icon) {
      loader.setLibraryIcon(m.name, m.icon);
    }

    if (m.noflo && m.noflo.loader) {
      const loaderPath = path.resolve(loader.baseDir, m.base, m.noflo.loader);
      componentLoaders.push(loaderPath);
    }

    return Promise.all(m.components.map((c) => new Promise((resolve, reject) => {
      const language = utils.guessLanguageFromFilename(c.path);
      if (language === 'typescript' || language === 'coffeescript') {
        // We can't require a module that requires transpilation, go the setSource route
        readFile(path.resolve(loader.baseDir, c.path), 'utf-8')
          .then((source) => transpileAndRegisterForModule(
            loader,
            m,
            c,
            source,
            language,
          ))
          .then(resolve, reject);
        return;
      }
      registerSpecs(loader, m.name, c.name, c.tests);
      loader.registerComponent(m.name, c.name, path.resolve(loader.baseDir, c.path), (err) => {
        if (err) {
          reject(err);
          return;
        }
        resolve();
      });
    })));
  }))
    .then(
      () => {
        registerCustomLoaders(loader, componentLoaders, callback);
      },
      callback,
    );
}
@param {import("../ComponentLoader").ComponentLoader} loader
@param {Array<import("fbp-manifest/dist/lib/list").FbpManifestModule>} modules
@param {ErrorableCallback} callback