"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.detachAsync = detachAsync;
exports.prepareDetachedBuildAsync = prepareDetachedBuildAsync;
exports.bundleAssetsAsync = bundleAssetsAsync;

function _fsExtra() {
  const data = _interopRequireDefault(require("fs-extra"));

  _fsExtra = function () {
    return data;
  };

  return data;
}

function _jsonFile() {
  const data = _interopRequireDefault(require("@expo/json-file"));

  _jsonFile = function () {
    return data;
  };

  return data;
}

function _path() {
  const data = _interopRequireDefault(require("path"));

  _path = function () {
    return data;
  };

  return data;
}

function _process() {
  const data = _interopRequireDefault(require("process"));

  _process = function () {
    return data;
  };

  return data;
}

function _rimraf() {
  const data = _interopRequireDefault(require("rimraf"));

  _rimraf = function () {
    return data;
  };

  return data;
}

function _globPromise() {
  const data = _interopRequireDefault(require("glob-promise"));

  _globPromise = function () {
    return data;
  };

  return data;
}

function _uuid() {
  const data = _interopRequireDefault(require("uuid"));

  _uuid = function () {
    return data;
  };

  return data;
}

function _inquirer() {
  const data = _interopRequireDefault(require("inquirer"));

  _inquirer = function () {
    return data;
  };

  return data;
}

function _spawnAsync() {
  const data = _interopRequireDefault(require("@expo/spawn-async"));

  _spawnAsync = function () {
    return data;
  };

  return data;
}

function ConfigUtils() {
  const data = _interopRequireWildcard(require("@expo/config"));

  ConfigUtils = function () {
    return data;
  };

  return data;
}

function _isPlainObject() {
  const data = _interopRequireDefault(require("lodash/isPlainObject"));

  _isPlainObject = function () {
    return data;
  };

  return data;
}

function _ExponentTools() {
  const data = require("./ExponentTools");

  _ExponentTools = function () {
    return data;
  };

  return data;
}

function AssetBundle() {
  const data = _interopRequireWildcard(require("./AssetBundle"));

  AssetBundle = function () {
    return data;
  };

  return data;
}

function IosPlist() {
  const data = _interopRequireWildcard(require("./IosPlist"));

  IosPlist = function () {
    return data;
  };

  return data;
}

function IosNSBundle() {
  const data = _interopRequireWildcard(require("./IosNSBundle"));

  IosNSBundle = function () {
    return data;
  };

  return data;
}

function IosWorkspace() {
  const data = _interopRequireWildcard(require("./IosWorkspace"));

  IosWorkspace = function () {
    return data;
  };

  return data;
}

function AndroidShellApp() {
  const data = _interopRequireWildcard(require("./AndroidShellApp"));

  AndroidShellApp = function () {
    return data;
  };

  return data;
}

function _Api() {
  const data = _interopRequireDefault(require("../Api"));

  _Api = function () {
    return data;
  };

  return data;
}

function ProjectUtils() {
  const data = _interopRequireWildcard(require("../project/ProjectUtils"));

  ProjectUtils = function () {
    return data;
  };

  return data;
}

function _User() {
  const data = _interopRequireDefault(require("../User"));

  _User = function () {
    return data;
  };

  return data;
}

function _XDLError() {
  const data = _interopRequireDefault(require("../XDLError"));

  _XDLError = function () {
    return data;
  };

  return data;
}

function _StandaloneBuildFlags() {
  const data = _interopRequireDefault(require("./StandaloneBuildFlags"));

  _StandaloneBuildFlags = function () {
    return data;
  };

  return data;
}

function _StandaloneContext() {
  const data = _interopRequireDefault(require("./StandaloneContext"));

  _StandaloneContext = function () {
    return data;
  };

  return data;
}

function UrlUtils() {
  const data = _interopRequireWildcard(require("../UrlUtils"));

  UrlUtils = function () {
    return data;
  };

  return data;
}

function Versions() {
  const data = _interopRequireWildcard(require("../Versions"));

  Versions = function () {
    return data;
  };

  return data;
}

function _installPackagesAsync() {
  const data = _interopRequireDefault(require("./installPackagesAsync"));

  _installPackagesAsync = function () {
    return data;
  };

  return data;
}

function _Logger() {
  const data = _interopRequireDefault(require("./Logger"));

  _Logger = function () {
    return data;
  };

  return data;
}

function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = Object.defineProperty && Object.getOwnPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : {}; if (desc.get || desc.set) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } } newObj.default = obj; return newObj; } }

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

// Set EXPO_VIEW_DIR to universe/exponent to test locally
async function yesnoAsync(message) {
  const {
    ok
  } = await _inquirer().default.prompt([{
    type: 'confirm',
    name: 'ok',
    message
  }]);
  return ok;
}

async function detachAsync(projectRoot, options = {}) {
  let originalLogger = _Logger().default.loggerObj;

  _Logger().default.configure({
    trace: options.verbose ? console.trace.bind(console) : () => {},
    debug: options.verbose ? console.debug.bind(console) : () => {},
    info: options.verbose ? console.info.bind(console) : () => {},
    warn: console.warn.bind(console),
    error: console.error.bind(console),
    fatal: console.error.bind(console)
  });

  try {
    return await _detachAsync(projectRoot, options);
  } finally {
    _Logger().default.configure(originalLogger);
  }
}

async function _detachAsync(projectRoot, options) {
  let user = await _User().default.ensureLoggedInAsync();

  if (!user) {
    throw new Error('Internal error -- somehow detach is being run in offline mode.');
  }

  let username = user.username;
  const {
    configName,
    configPath,
    configNamespace
  } = await ConfigUtils().findConfigFileAsync(projectRoot);
  let {
    exp,
    pkg
  } = await ProjectUtils().readConfigJsonAsync(projectRoot);
  if (!exp) throw new Error(`Couldn't read ${configName}`);
  if (!pkg) throw new Error(`Couldn't read package.json`);
  let experienceName = `@${username}/${exp.slug}`;
  let experienceUrl = `exp://exp.host/${experienceName}`; // Check to make sure project isn't fully detached already

  let hasIosDirectory = (0, _ExponentTools().isDirectory)(_path().default.join(projectRoot, 'ios'));
  let hasAndroidDirectory = (0, _ExponentTools().isDirectory)(_path().default.join(projectRoot, 'android'));

  if (hasIosDirectory && hasAndroidDirectory) {
    throw new (_XDLError().default)('DIRECTORY_ALREADY_EXISTS', 'Error detaching. `ios` and `android` directories already exist.');
  } // Project was already detached on Windows or Linux


  if (!hasIosDirectory && hasAndroidDirectory && _process().default.platform === 'darwin') {
    let response = await yesnoAsync(`This will add an Xcode project and leave your existing Android project alone. Enter 'yes' to continue:`);

    if (!response) {
      _Logger().default.info('Exiting...');

      return false;
    }
  }

  if (hasIosDirectory && !hasAndroidDirectory) {
    throw new Error('`ios` directory already exists. Please remove it and try again.');
  }

  _Logger().default.info('Validating project manifest...');

  if (!exp.name) {
    throw new Error(`${configName} is missing \`name\``);
  }

  if (!exp.sdkVersion) {
    throw new Error(`${configName} is missing \`sdkVersion\``);
  }

  if (!Versions().gteSdkVersion(exp, '25.0.0')) {
    throw new Error(`The app must be updated to SDK 25.0.0 or newer to be compatible with this tool.`);
  }

  const versions = await Versions().versionsAsync();
  let sdkVersionConfig = versions.sdkVersions[exp.sdkVersion];

  if (!sdkVersionConfig || !sdkVersionConfig.androidExpoViewUrl || !sdkVersionConfig.iosExpoViewUrl) {
    if (_process().default.env.EXPO_VIEW_DIR) {
      _Logger().default.warn(`Detaching is not supported for SDK ${exp.sdkVersion}; ignoring this because you provided EXPO_VIEW_DIR`);

      sdkVersionConfig = {};
    } else {
      throw new Error(`Detaching is not supported for SDK version ${exp.sdkVersion}`);
    }
  } // Modify exp.json


  exp.isDetached = true;

  if (!exp.detach) {
    exp.detach = {};
  }

  let detachedUUID = _uuid().default.v4().replace(/-/g, '');

  let generatedScheme = `exp${detachedUUID}`;

  if (!exp.detach.scheme && !Versions().gteSdkVersion(exp, '27.0.0')) {
    // set this for legacy purposes
    exp.detach.scheme = generatedScheme;
  }

  if (!exp.scheme) {
    _Logger().default.info(`You have not specified a custom scheme for deep linking. A default value of ${generatedScheme} will be used. You can change this later by following the instructions in this guide: https://docs.expo.io/versions/latest/workflow/linking/`);

    exp.scheme = generatedScheme;
  }

  let expoDirectory = _path().default.join(projectRoot, '.expo-source');

  _fsExtra().default.mkdirpSync(expoDirectory);

  const context = _StandaloneContext().default.createUserContext(projectRoot, exp, experienceUrl); // iOS


  let isIosSupported = true;

  if (_process().default.platform !== 'darwin') {
    if (options && options.force) {
      _Logger().default.warn(`You are not running macOS, but have provided the --force option, so we will attempt to generate an iOS project anyway. This might fail.`);
    } else {
      _Logger().default.warn(`Skipping iOS because you are not running macOS.`);

      isIosSupported = false;
    }
  }

  if (!hasIosDirectory && isIosSupported) {
    if (!exp.ios) {
      exp.ios = {};
    }

    if (!exp.ios.bundleIdentifier) {
      _Logger().default.info(`You'll need to specify an iOS bundle identifier. See: https://docs.expo.io/versions/latest/workflow/configuration/#ios`);

      const {
        iosBundleIdentifier
      } = await _inquirer().default.prompt([{
        name: 'iosBundleIdentifier',
        message: 'What would you like your iOS bundle identifier to be?',
        validate: value => /^[a-zA-Z][a-zA-Z0-9\-.]+$/.test(value)
      }]);
      exp.ios.bundleIdentifier = iosBundleIdentifier;
    }

    await detachIOSAsync(context);
    exp = IosWorkspace().addDetachedConfigToExp(exp, context);
    exp.detach.iosExpoViewUrl = sdkVersionConfig.iosExpoViewUrl;
  } // Android


  if (!hasAndroidDirectory) {
    if (!exp.android) {
      exp.android = {};
    }

    if (!exp.android.package) {
      _Logger().default.info(`You'll need to specify an Android package name. See: https://docs.expo.io/versions/latest/workflow/configuration/#android`);

      const {
        androidPackage
      } = await _inquirer().default.prompt([{
        name: 'androidPackage',
        message: 'What would you like your Android package name to be?',
        validate: value => /^[a-zA-Z][a-zA-Z0-9_]*(\.[a-zA-Z][a-zA-Z0-9_]*)+$/.test(value) ? true : "Invalid format of Android package name (only alphanumeric characters, '.' and '_' are allowed, and each '.' must be followed by a letter)"
      }]);
      exp.android.package = androidPackage;
    }

    let androidDirectory = _path().default.join(expoDirectory, 'android');

    _rimraf().default.sync(androidDirectory);

    _fsExtra().default.mkdirpSync(androidDirectory);

    await detachAndroidAsync(context, sdkVersionConfig.androidExpoViewUrl);
    exp = AndroidShellApp().addDetachedConfigToExp(exp, context);
    exp.detach.androidExpoViewUrl = sdkVersionConfig.androidExpoViewUrl;
  }

  _Logger().default.info('Writing ExpoKit configuration...'); // Update exp.json/app.json
  // if we're writing to app.json, we need to place the configuration under the expo key


  const config = configNamespace ? {
    [configNamespace]: exp
  } : exp;
  await _fsExtra().default.writeFile(configPath, JSON.stringify(config, null, 2));
  const packagesToInstall = [];
  const nodeModulesPath = exp.nodeModulesPath ? _path().default.resolve(projectRoot, exp.nodeModulesPath) : projectRoot;

  if (sdkVersionConfig && sdkVersionConfig.expoReactNativeTag) {
    packagesToInstall.push(`react-native@https://github.com/expo/react-native/archive/${sdkVersionConfig.expoReactNativeTag}.tar.gz`);
  } else if (_process().default.env.EXPO_VIEW_DIR) {// ignore, using test directory
  } else {
    throw new Error(`Expo's React Native fork does not support this SDK version.`);
  } // Add expokitNpmPackage if it is supported. Was added before SDK 29.


  if (_process().default.env.EXPO_VIEW_DIR) {
    _Logger().default.info(`Linking 'expokit' package...`);

    await (0, _spawnAsync().default)('yarn', ['link'], {
      cwd: _path().default.join(_process().default.env.EXPO_VIEW_DIR, 'expokit-npm-package')
    });
    await (0, _spawnAsync().default)('yarn', ['link', 'expokit'], {
      cwd: nodeModulesPath
    });
  } else if (sdkVersionConfig.expokitNpmPackage) {
    packagesToInstall.push(sdkVersionConfig.expokitNpmPackage);
  }

  const {
    packagesToInstallWhenEjecting
  } = sdkVersionConfig;

  if ((0, _isPlainObject().default)(packagesToInstallWhenEjecting)) {
    Object.keys(packagesToInstallWhenEjecting).forEach(packageName => {
      packagesToInstall.push(`${packageName}@${packagesToInstallWhenEjecting[packageName]}`);
    });
  }

  if (packagesToInstall.length) {
    await (0, _installPackagesAsync().default)(projectRoot, packagesToInstall, {
      packageManager: options.packageManager
    });
  }

  return true;
}
/**
 *  Create a detached Expo iOS app pointing at the given project.
 */


async function detachIOSAsync(context) {
  await IosWorkspace().createDetachedAsync(context);

  _Logger().default.info('Configuring iOS project...');

  await IosNSBundle().configureAsync(context);

  _Logger().default.info(`iOS detach is complete!`);
}

async function detachAndroidAsync(context, expoViewUrl) {
  if (context.type !== 'user') {
    throw new Error(`detachAndroidAsync only supports user standalone contexts`);
  }

  _Logger().default.info('Moving Android project files...');

  let androidProjectDirectory = _path().default.join(context.data.projectPath, 'android');

  let tmpExpoDirectory;

  if (_process().default.env.EXPO_VIEW_DIR) {
    // Only for testing
    await AndroidShellApp().copyInitialShellAppFilesAsync(_path().default.join(_process().default.env.EXPO_VIEW_DIR, 'android'), androidProjectDirectory, true, context.data.exp.sdkVersion);
  } else {
    tmpExpoDirectory = _path().default.join(context.data.projectPath, 'temp-android-directory');

    _fsExtra().default.mkdirpSync(tmpExpoDirectory);

    _Logger().default.info('Downloading Android code...');

    await _Api().default.downloadAsync(expoViewUrl, tmpExpoDirectory, {
      extract: true
    });
    await AndroidShellApp().copyInitialShellAppFilesAsync(tmpExpoDirectory, androidProjectDirectory, true, context.data.exp.sdkVersion);
  }

  _Logger().default.info('Updating Android app...');

  await AndroidShellApp().runShellAppModificationsAsync(context, context.data.exp.sdkVersion); // Clean up

  _Logger().default.info('Cleaning up Android...');

  if (!_process().default.env.EXPO_VIEW_DIR) {
    (0, _ExponentTools().rimrafDontThrow)(tmpExpoDirectory);
  }

  _Logger().default.info('Android detach is complete!\n');
}

async function ensureBuildConstantsExistsIOSAsync(configFilePath) {
  // EXBuildConstants is included in newer ExpoKit projects.
  // create it if it doesn't exist.
  const doesBuildConstantsExist = _fsExtra().default.existsSync(_path().default.join(configFilePath, 'EXBuildConstants.plist'));

  if (!doesBuildConstantsExist) {
    await IosPlist().createBlankAsync(configFilePath, 'EXBuildConstants');

    _Logger().default.info('Created `EXBuildConstants.plist` because it did not exist yet');
  }
}

async function _getIosExpoKitVersionThrowErrorAsync(iosProjectDirectory) {
  let expoKitVersion = '';

  const podfileLockPath = _path().default.join(iosProjectDirectory, 'Podfile.lock');

  try {
    const podfileLock = await _fsExtra().default.readFile(podfileLockPath, 'utf8');
    const expoKitVersionRegex = /ExpoKit\/Core\W?\(([0-9.]+)\)/gi;
    let match = expoKitVersionRegex.exec(podfileLock);
    expoKitVersion = match[1];
  } catch (e) {
    throw new Error(`Unable to read ExpoKit version from Podfile.lock. Make sure your project depends on ExpoKit. (${e})`);
  }

  return expoKitVersion;
}

async function prepareDetachedBuildIosAsync(projectDir, args) {
  const {
    exp
  } = await ProjectUtils().readConfigJsonAsync(projectDir);

  if (exp) {
    return prepareDetachedUserContextIosAsync(projectDir, exp, args);
  } else {
    return prepareDetachedServiceContextIosAsync(projectDir, args);
  }
}

async function prepareDetachedServiceContextIosAsync(projectDir, args) {
  // service context
  // TODO: very brittle hack: the paths here are hard coded to match the single workspace
  // path generated inside IosShellApp. When we support more than one path, this needs to
  // be smarter.
  const expoRootDir = _path().default.join(projectDir, '..', '..');

  const workspaceSourcePath = _path().default.join(projectDir, 'ios');

  const buildFlags = _StandaloneBuildFlags().default.createIos('Release', {
    workspaceSourcePath
  });

  const context = _StandaloneContext().default.createServiceContext(expoRootDir, null, null, null,
  /* testEnvironment */
  'none', buildFlags, null, null);

  const {
    iosProjectDirectory,
    supportingDirectory
  } = IosWorkspace().getPaths(context);
  const expoKitVersion = await _getIosExpoKitVersionThrowErrorAsync(iosProjectDirectory); // use prod api keys if available

  const prodApiKeys = await _readDefaultApiKeysAsync(_path().default.join(context.data.expoSourcePath, '__internal__', 'keys.json'));
  const {
    exp
  } = await ProjectUtils().readConfigJsonAsync(expoRootDir);
  await IosPlist().modifyAsync(supportingDirectory, 'EXBuildConstants', constantsConfig => {
    // verify that we are actually in a service context and not a misconfigured project
    const contextType = constantsConfig.STANDALONE_CONTEXT_TYPE;

    if (contextType !== 'service') {
      throw new Error('Unable to configure a project which has no app.json and also no STANDALONE_CONTEXT_TYPE.');
    }

    constantsConfig.EXPO_RUNTIME_VERSION = expoKitVersion;
    constantsConfig.API_SERVER_ENDPOINT = _process().default.env.ENVIRONMENT === 'staging' ? 'https://staging.exp.host/--/api/v2/' : 'https://exp.host/--/api/v2/';

    if (prodApiKeys) {
      constantsConfig.DEFAULT_API_KEYS = prodApiKeys;
    }

    if (exp && exp.sdkVersion) {
      constantsConfig.TEMPORARY_SDK_VERSION = exp.sdkVersion;
    }

    return constantsConfig;
  });
}

async function _readDefaultApiKeysAsync(jsonFilePath) {
  if (_fsExtra().default.existsSync(jsonFilePath)) {
    let keys = {};
    const allKeys = await new (_jsonFile().default)(jsonFilePath).readAsync();
    const validKeys = ['AMPLITUDE_KEY', 'GOOGLE_MAPS_IOS_API_KEY'];

    for (const key in allKeys) {
      if (allKeys.hasOwnProperty(key) && validKeys.includes(key)) {
        keys[key] = allKeys[key];
      }
    }

    return keys;
  }

  return null;
}

async function prepareDetachedUserContextIosAsync(projectDir, exp, args) {
  const context = _StandaloneContext().default.createUserContext(projectDir, exp);

  let {
    iosProjectDirectory,
    supportingDirectory
  } = IosWorkspace().getPaths(context);

  _Logger().default.info(`Preparing iOS build at ${iosProjectDirectory}...`); // These files cause @providesModule naming collisions
  // but are not available until after `pod install` has run.


  let podsDirectory = _path().default.join(iosProjectDirectory, 'Pods');

  if (!(0, _ExponentTools().isDirectory)(podsDirectory)) {
    throw new Error(`Can't find directory ${podsDirectory}, make sure you've run pod install.`);
  }

  let rnPodDirectory = _path().default.join(podsDirectory, 'React');

  if ((0, _ExponentTools().isDirectory)(rnPodDirectory)) {
    let rnFilesToDelete = await (0, _globPromise().default)(rnPodDirectory + '/**/*.@(js|json)');

    if (rnFilesToDelete) {
      for (let i = 0; i < rnFilesToDelete.length; i++) {
        await _fsExtra().default.unlink(rnFilesToDelete[i]);
      }
    }
  } // insert expo development url into iOS config


  if (!args.skipXcodeConfig) {
    // populate EXPO_RUNTIME_VERSION from ExpoKit pod version
    const expoKitVersion = await _getIosExpoKitVersionThrowErrorAsync(iosProjectDirectory); // populate development url

    let devUrl = await UrlUtils().constructManifestUrlAsync(projectDir); // populate default api keys

    const defaultApiKeys = await _readDefaultApiKeysAsync(_path().default.join(podsDirectory, 'ExpoKit', 'template-files', 'keys.json'));
    await ensureBuildConstantsExistsIOSAsync(supportingDirectory);
    await IosPlist().modifyAsync(supportingDirectory, 'EXBuildConstants', constantsConfig => {
      constantsConfig.developmentUrl = devUrl;
      constantsConfig.EXPO_RUNTIME_VERSION = expoKitVersion;

      if (defaultApiKeys) {
        constantsConfig.DEFAULT_API_KEYS = defaultApiKeys;
      }

      if (exp.sdkVersion) {
        constantsConfig.TEMPORARY_SDK_VERSION = exp.sdkVersion;
      }

      return constantsConfig;
    });
  }
}

async function prepareDetachedBuildAsync(projectDir, args) {
  if (args.platform === 'ios') {
    await prepareDetachedBuildIosAsync(projectDir, args);
  } else {
    let androidProjectDirectory = _path().default.join(projectDir, 'android');

    let expoBuildConstantsMatches = await (0, _globPromise().default)(androidProjectDirectory + '/**/DetachBuildConstants.java');

    if (expoBuildConstantsMatches && expoBuildConstantsMatches.length) {
      let expoBuildConstants = expoBuildConstantsMatches[0];
      let devUrl = await UrlUtils().constructManifestUrlAsync(projectDir);
      await (0, _ExponentTools().regexFileAsync)(/DEVELOPMENT_URL = "[^"]*";/, `DEVELOPMENT_URL = "${devUrl}";`, expoBuildConstants);
    }
  }
} // args.dest: string,
// This is the path where assets will be copied to. It should be
// `$CONFIGURATION_BUILD_DIR/$UNLOCALIZED_RESOURCES_FOLDER_PATH` on iOS
// (see `exponent-view-template.xcodeproj/project.pbxproj` for an example)
// and `$buildDir/intermediates/assets/$targetPath` on Android (see
// `android/app/expo.gradle` for an example).


async function bundleAssetsAsync(projectDir, args) {
  let {
    exp
  } = await ProjectUtils().readConfigJsonAsync(projectDir);

  if (!exp) {
    // Don't run assets bundling for the service context.
    return;
  }

  let publishManifestPath = args.platform === 'ios' ? exp.ios.publishManifestPath : exp.android.publishManifestPath;

  if (!publishManifestPath) {
    _Logger().default.warn(`Skipped assets bundling because the '${args.platform}.publishManifestPath' key is not specified in the app manifest.`);

    return;
  }

  let bundledManifestPath = _path().default.join(projectDir, publishManifestPath);

  let manifest;

  try {
    manifest = JSON.parse((await _fsExtra().default.readFile(bundledManifestPath, 'utf8')));
  } catch (ex) {
    throw new Error(`Error reading the manifest file. Make sure the path '${bundledManifestPath}' is correct.\n\nError: ${ex.message}`);
  }

  await AssetBundle().bundleAsync(null, manifest.bundledAssets, args.dest);
}
//# sourceMappingURL=../__sourcemaps__/detach/Detach.js.map