@expo/xdl
Version:
The Expo Development Library
851 lines (629 loc) • 25.2 kB
JavaScript
;
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