async function bundle(opts: *) {
  const directory = process.cwd();
  const configPath = getWebpackConfigPath(directory, opts.config);

  const config = getConfig(
    configPath,
    {
      root: directory,
      dev: opts.dev,
      minify: opts.minify,
      bundle: true,
    },
    opts.platform,
    logger
  );

  if (opts.assetsDest) {
    config.output.path = path.isAbsolute(opts.assetsDest)
      ? opts.assetsDest
      : path.join(directory, opts.assetsDest);
  }

  if (opts.bundleOutput) {
    config.output.filename = path.isAbsolute(opts.bundleOutput)
      ? path.relative(config.output.path, opts.bundleOutput)
      : path.relative(
          config.output.path,
          path.join(directory, opts.bundleOutput)
        );
  }

  logger.info(`Assets Destination: ${config.output.path}`);
  logger.info(`Bundle output: ${config.output.filename}`);
  logger.info(
    `Bundle output (resolved): ${path.resolve(config.output.filename)}`
  );

  // attach progress plugin
  if (opts.progress !== 'none') {
    config.plugins = config.plugins.concat([
      new SimpleProgressWebpackPlugin({
        format: opts.progress,
      }),
    ]);
  }

  const compiler = webpack(config);

  logger.info(
    messages.initialBundleInformation({
      entry: config.entry,
      dev: opts.dev,
    })
  );

  const stats = await new Promise((resolve, reject) =>
    compiler.run((err, info) => {
      if (err || info.hasErrors()) {
        reject(
          new MessageError(
            messages.bundleFailed({
              errors: err
                ? [err.message]
                : info.toJson({ errorDetails: true }).errors,
            })
          )
        );
      } else if (info.hasWarnings()) {
        logger.warn(info.toJson().warnings);
        resolve(info);
      } else {
        resolve(info);
      }
    })
  );

  clear();

  logger.done(
    messages.bundleBuilt({
      stats,
      platform: opts.platform,
      assetsPath: config.output.path,
      bundlePath: config.output.filename,
    })
  );
}