`appName` may be null if config defines only 1 app
function getConfig(projectDir, appName, env) {
  if (!['dev', 'test', 'prod'].includes(env)) exit(`Incorrect env value: ${env}`);
  process.env.NODE_ENV = {dev: 'development', test: 'testing', prod: 'production'}[env];

  const configPath = path.join(projectDir, 'basys.json');
  let projectConfig;
  try {
    projectConfig = fs.readFileSync(configPath, 'utf8');
  } catch (e) {
    exit(`Couldn't read configuration file ${configPath}\n`);
  }

  try {
    projectConfig = JSON5.parse(projectConfig);
  } catch (e) {
    exit(
      `Syntax error in ${configPath}: ${e.message}\n` +
        'You can use JSON5 (https://json5.org) which is an extension of the JSON format, that includes:\n' +
        '  - comments,\n  - unquoted and single-quoted object keys,\n  - trailing commas,\n  - single-quoted and multi-line strings.\n',
    );
  }

  // BUG: Introduce some shortcuts for validating the configs, or use some library. App builder import function should do it.
  if (!projectConfig || projectConfig.constructor !== Object) {
    exit(`${configPath} value must be an object`);
  }

  if (!projectConfig.apps) exit("'apps' option must be provided in basys.json");
  const appNames = Object.keys(projectConfig.apps);
  if (appNames.length === 0) exit('At least one app must be defined in basys.json');
  // BUG: validate that all appNames use allowed symbols only

  if (!appName) {
    if (appNames.length > 1) exit('App name must be specified');
    appName = appNames[0];
  }

  if (!appNames.includes(appName)) {
    exit(`Incorrect app name: '${appName}'. Available names are: '${appNames.join("', '")}'.`);
  }

  let config = projectConfig.apps[appName];
  if (!['web', 'mobile', 'desktop'].includes(config.type)) {
    exit(
      `Incorrect ${appName} app type: '${
        config.type
      }'. Allowed values are: 'web', 'mobile', 'desktop'.`,
    );
  }

  // Default app configuration
  const defaultConfig = {
    entry: null, // Path to UI entry file (relative to src/ directory)
    favicon: null,
    styles: [],
    overrides: {},
    caseSensitive: false,
    sourceMap: false,

    host: 'localhost',
    // In dev env another free port will be determined if this one is occupied.
    // In other envs the server will fail to start.
    port: 8080,

    poll: false, // dev env only, see https://webpack.js.org/configuration/dev-server/#devserver-watchoptions-
    custom: {}, // Holds custom config options (shouldn't overlap with any built-in options)
  };

  if (config.type === 'web') {
    Object.assign(defaultConfig, {
      backendEntry: null, // Path to backend entry file (relative to src/ directory)
      backendPort: 3000,
      nodeVersion: env === 'dev' ? 'current' : '8.9',
      browsers: ['> 0.5%', 'last 2 versions', 'Firefox ESR', 'not dead'],

      // BUG: automatically detect the browsers available on developer's machine? (only relevant if web app is present)
      // BUG: allow to override it via CLI arguments?
      // BUG: support remote browsers
      // Available values: 'chromium', 'chrome', 'chrome:headless', 'chrome-canary', 'ie', 'edge',
      // 'firefox', 'firefox:headless', 'opera', 'safari'
      testBrowsers: [],
    });
  }
  // BUG: for mobile apps provide ios/android configuration, supported versions

  if (config[env]) {
    config = merge(config, config[env]);
    delete config.dev;
    delete config.test;
    delete config.prod;
  }

  config = merge(defaultConfig, config);

  // Validate that custom options don't overlap with any built-in options
  for (const key in config.custom) {
    if (key in config) exit(`Custom config option '${key}' is not allowed`);
  }

  const tempDir = path.join(projectDir, '.basys', appName, env);
  fs.ensureDirSync(tempDir);

  config = merge(config, {
    appName,
    env,
    assetsPublicPath: '/', // BUG: maybe set it to '/static/'? don't expose to user?
    tempDir,
    projectDir,
    distDir: env === 'prod' ? path.join(projectDir, 'dist', appName) : tempDir,
  });

  // BUG: Validate both config files (depending on env), if invalid raise a meaningful error (with a link to local or public docs?)
  //      Look at https://github.com/mozilla/node-convict , https://github.com/ianstormtaylor/superstruct

  return config;
}