UNPKG

@expo/xdl

Version:
851 lines (629 loc) 25.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.detachAsync = detachAsync; exports.prepareDetachedBuildAsync = prepareDetachedBuildAsync; exports.bundleAssetsAsync = bundleAssetsAsync; function _config() { const data = require("@expo/config"); _config = function () { return data; }; return data; } function _jsonFile() { const data = _interopRequireDefault(require("@expo/json-file")); _jsonFile = function () { return data; }; return data; } function _spawnAsync() { const data = _interopRequireDefault(require("@expo/spawn-async")); _spawnAsync = function () { return data; }; return data; } function _fsExtra() { const data = _interopRequireDefault(require("fs-extra")); _fsExtra = function () { return data; }; return data; } function _glob() { const data = require("glob"); _glob = function () { return data; }; return data; } function _isPlainObject() { const data = _interopRequireDefault(require("lodash/isPlainObject")); _isPlainObject = 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 _prompts() { const data = _interopRequireDefault(require("prompts")); _prompts = function () { return data; }; return data; } function _rimraf() { const data = _interopRequireDefault(require("rimraf")); _rimraf = function () { return data; }; return data; } function _uuid() { const data = _interopRequireDefault(require("uuid")); _uuid = function () { return data; }; return data; } function _Api() { const data = _interopRequireDefault(require("../Api")); _Api = function () { return data; }; return data; } function EmbeddedAssets() { const data = _interopRequireWildcard(require("../EmbeddedAssets")); EmbeddedAssets = function () { return data; }; return data; } function UrlUtils() { const data = _interopRequireWildcard(require("../UrlUtils")); UrlUtils = function () { return data; }; return data; } function _User() { const data = _interopRequireDefault(require("../User")); _User = function () { return data; }; return data; } function Versions() { const data = _interopRequireWildcard(require("../Versions")); Versions = function () { return data; }; return data; } function _XDLError() { const data = _interopRequireDefault(require("../XDLError")); _XDLError = function () { return data; }; return data; } function AndroidShellApp() { const data = _interopRequireWildcard(require("./AndroidShellApp")); AndroidShellApp = function () { return data; }; return data; } function AssetBundle() { const data = _interopRequireWildcard(require("./AssetBundle")); AssetBundle = function () { return data; }; return data; } function _ExponentTools() { const data = require("./ExponentTools"); _ExponentTools = function () { return data; }; return data; } function IosNSBundle() { const data = _interopRequireWildcard(require("./IosNSBundle")); IosNSBundle = function () { return data; }; return data; } function IosPlist() { const data = _interopRequireWildcard(require("./IosPlist")); IosPlist = function () { return data; }; return data; } function IosWorkspace() { const data = _interopRequireWildcard(require("./IosWorkspace")); IosWorkspace = function () { return data; }; return data; } function _Logger() { const data = _interopRequireDefault(require("./Logger")); _Logger = 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 _installPackagesAsync() { const data = _interopRequireDefault(require("./installPackagesAsync")); _installPackagesAsync = function () { return data; }; return data; } function _getRequireWildcardCache() { if (typeof WeakMap !== "function") return null; var cache = new WeakMap(); _getRequireWildcardCache = function () { return cache; }; return cache; } function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } // Set EXPO_VIEW_DIR to universe/exponent to test locally const SERVICE_CONTEXT_PROJECT_NAME = 'exponent-view-template'; async function yesnoAsync(message) { const { ok } = await (0, _prompts().default)({ type: 'confirm', name: 'ok', initial: true, message }); return ok; } async function detachAsync(projectRoot, options = {}) { const 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) { const user = await _User().default.ensureLoggedInAsync(); if (!user) { throw new Error('Internal error -- somehow detach is being run in offline mode.'); } const username = user.username; const { configName, configPath, configNamespace } = (0, _config().findConfigFile)(projectRoot); let { exp } = (0, _config().getConfig)(projectRoot); const experienceName = `@${username}/${exp.slug}`; const experienceUrl = `exp://exp.host/${experienceName}`; // Check to make sure project isn't fully detached already const hasIosDirectory = (0, _ExponentTools().isDirectory)(_path().default.join(projectRoot, 'ios')); const 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') { const 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}`); } } exp.isDetached = true; if (!exp.detach) { exp.detach = {}; } const detachedUUID = _uuid().default.v4().replace(/-/g, ''); const generatedScheme = `exp${detachedUUID}`; if (!exp.detach.scheme && !Versions().gteSdkVersion(exp, '27.0.0')) { // set this for legacy purposes exp.detach.scheme = generatedScheme; } const linkingWarning = `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/workflow/linking/`; if (!exp.scheme) { _Logger().default.info(linkingWarning); exp.scheme = generatedScheme; } else if (Array.isArray(exp.scheme) && exp.scheme.length === 0) { _Logger().default.info(linkingWarning); exp.scheme.push(generatedScheme); } const 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 && sdkVersionConfig.iosExpoViewUrl) { 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/config/app/#ios`); const { iosBundleIdentifier } = await (0, _prompts().default)({ type: 'text', 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 && sdkVersionConfig.androidExpoViewUrl) { 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/config/app/#android`); const { androidPackage } = await (0, _prompts().default)({ type: 'text', 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; } const 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...'); // 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 = []; 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 fork of react-native 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: projectRoot }); } else if (sdkVersionConfig.expokitNpmPackage) { packagesToInstall.push(sdkVersionConfig.expokitNpmPackage); } if (sdkVersionConfig) { 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...'); const 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; const 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 readNullableConfigJsonAsync(projectDir) { try { return (0, _config().getConfig)(projectDir); } catch (_) { return null; } } async function prepareDetachedBuildIosAsync(projectDir, args) { const config = await readNullableConfigJsonAsync(projectDir); if (config && config.exp.name !== SERVICE_CONTEXT_PROJECT_NAME) { return prepareDetachedUserContextIosAsync(projectDir, config.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, 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 } = (0, _config().getConfig)(expoRootDir, { skipSDKVersionRequirement: true }); 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)) { const 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); const { 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. const 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.`); } const rnPodDirectory = _path().default.join(podsDirectory, 'React'); if ((0, _ExponentTools().isDirectory)(rnPodDirectory)) { const rnFilesToDelete = (0, _glob().sync)('**/*.@(js|json)', { absolute: true, cwd: rnPodDirectory }); 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 const 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 { const expoBuildConstantsMatches = (0, _glob().sync)('android/**/DetachBuildConstants.java', { absolute: true, cwd: projectDir }); if (expoBuildConstantsMatches && expoBuildConstantsMatches.length) { const expoBuildConstants = expoBuildConstantsMatches[0]; const 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) { const options = await readNullableConfigJsonAsync(projectDir); if (!options || options.exp.name === SERVICE_CONTEXT_PROJECT_NAME) { // Don't run assets bundling for the service context. return; } const { exp } = options; const bundledManifestPath = EmbeddedAssets().getEmbeddedManifestPath(args.platform, projectDir, exp); if (!bundledManifestPath) { _Logger().default.warn(`Skipped assets bundling because the '${args.platform}.publishManifestPath' key is not specified in the app manifest.`); return; } 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}`); } if (!manifest || !Object.keys(manifest).length) { throw new Error(`The manifest at '${bundledManifestPath}' was empty or invalid.`); } await AssetBundle().bundleAsync(null, manifest.bundledAssets, args.dest, getExportUrl(manifest)); } /** * This function extracts the exported public URL that is set in the manifest * when the developer runs `expo export --public-url x`. We use this to ensure * that we fetch the resources from the appropriate place when doing builds * against self-hosted apps. */ function getExportUrl(manifest) { const { bundleUrl } = manifest; if (bundleUrl.includes(AssetBundle().DEFAULT_CDN_HOST)) { return null; } try { const bundleUrlParts = bundleUrl.split('/'); return bundleUrlParts.slice(0, bundleUrlParts.length - 2).join('/'); } catch (e) { throw Error(`Expected bundleUrl to be of the format https://domain/bundles/bundle-hash-id, ${bundleUrl} does not follow this format.`); } } //# sourceMappingURL=../__sourcemaps__/detach/Detach.js.map