async function buildApp(appNames, pathFolders, outFolder, minifyJS = code => code, customLoadModuleSource = descriptor => Promise.resolve(null)) {
  const descriptors = await loadAppDescriptors(appNames, pathFolders);
  const modules = await loadModules(descriptors, pathFolders, customLoadModuleSource);
  const fetchedImages = await loadImages(modules, pathFolders);
  const runtime = await loadSource(pathFolders, 'Runtime.js');

  const builtApps = [];
  const notAutoStartModules = new Set();
  for (const appName of appNames) {
    const appDescriptor = { modules: [], hasHtml: false };
    let current = descriptors.get(appName);
    while (current) {
      appDescriptor.modules.push(...current.modules);
      appDescriptor.hasHtml = appDescriptor.hasHtml || current.hasHtml;
      current = current.extends ? descriptors.get(current.extends) : null;
    }

    const moduleDescriptors = appDescriptor.modules.map(module => {
      const moduleName = module.name;
      const moduleDescriptor = modules.get(module.name);
      const descriptor = { name: moduleName, remote: false };
      if (module.type !== 'autostart')
        descriptor.scripts = [`${moduleName}_module.js`];
      if (moduleDescriptor.extensions)
        descriptor.extensions = moduleDescriptor.extensions;
      if (moduleDescriptor.dependencies)
        descriptor.dependencies = moduleDescriptor.dependencies;
      if (moduleDescriptor.experiment)
        descriptor.experiment = moduleDescriptor.experiment;
      return descriptor;
    });

    const autoStartModulesByName = new Map();
    appDescriptor.modules.map(module => {
      if (module.type === 'autostart')
        autoStartModulesByName.set(module.name, module);
      else
        notAutoStartModules.add(module.name);
    });

    const appScript = await loadSource(pathFolders, appName + '.js');

    let scriptContent = '';
    scriptContent += '/* Runtime.js */\n' + runtime + '\n';
    scriptContent += `allDescriptors.push(...${JSON.stringify(moduleDescriptors)});\n`;
    scriptContent += `applicationDescriptor = ${JSON.stringify(appDescriptor)};\n`;
    scriptContent += appScript;
    const visitedModule = new Set();
    for (const [, module] of autoStartModulesByName)
      scriptContent += writeModule(modules, module, autoStartModulesByName, visitedModule);

    let htmlContent = '';
    if (appDescriptor.hasHtml) {
      const content = await loadSource(pathFolders, appName + '.html');
      htmlContent = content.replace(/<script.*?src="Runtime.js"><\/script>/, '<!-- <script src="Runtime.js"></script> -->');
    }

    builtApps.push({
      scriptContent,
      htmlContent,
      name: appName
    });

    function writeModule(modules, module, autoStartModulesByName, visitedModule) {
      if (visitedModule.has(module.name))
        return '';
      visitedModule.add(module.name);
      const builtModule = modules.get(module.name);
      let content = '';
      for (const dep of builtModule.dependencies) {
        const depModule = autoStartModulesByName.get(dep);
        if (!depModule)
          console.error(`Autostart module ${module.name} depends on not autostart module ${dep}`);
        content += writeModule(modules, depModule, autoStartModulesByName, visitedModule);
      }
      return content + builtModule.content;
    }
  }

  const favicon = await fs.readFile(lookupFile(pathFolders, 'favicon.png')[0]);

  const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'devtools-frontend-'));
  const promises = [];
  for (const app of builtApps) {
    promises.push(fs.writeFile(path.join(tmpDir, app.name + '.js'), await minifyJS(app.scriptContent)));
    if (app.htmlContent)
      promises.push(fs.writeFile(path.join(tmpDir, app.name + '.html'), app.htmlContent));
  }

  promises.push(...Array.from(notAutoStartModules).map(async moduleName => {
    await fs.mkdir(path.join(tmpDir, moduleName));
    return fs.writeFile(path.join(tmpDir, moduleName, moduleName + '_module.js'), await minifyJS(modules.get(moduleName).content));
  }));

  const createImageFolder = fs.mkdir(path.join(tmpDir, 'Images'));
  promises.push(...fetchedImages.map(async image => {
    await createImageFolder;
    if (image.content)
      return fs.writeFile(path.join(tmpDir, 'Images', image.name.substr('Images/'.length)), image.content);
  }));
  if (favicon)
    promises.push(fs.writeFile(path.join(tmpDir, 'favicon.png'), favicon));
  await Promise.all(promises);

  await fs.rename(tmpDir, outFolder);
}
@param {!Array<string>} appNames
@param {!Array<string>} pathFolders
@param {string} outFolder
@param {function(string):string=} minifyJS
@return {!Promise}