346 lines
10 KiB
Plaintext
346 lines
10 KiB
Plaintext
/**
|
|
* @flow
|
|
*/
|
|
|
|
import promisify from 'util.promisify';
|
|
import existsAsync from 'exists-async';
|
|
import fs from 'fs';
|
|
import mkdirp from 'mkdirp';
|
|
import path from 'path';
|
|
import spawnAsync from '@expo/spawn-async';
|
|
import JsonFile from '@expo/json-file';
|
|
import joi from 'joi';
|
|
import rimraf from 'rimraf';
|
|
|
|
import * as Analytics from './Analytics';
|
|
import Api from './Api';
|
|
import * as Binaries from './Binaries';
|
|
import ErrorCode from './ErrorCode';
|
|
import * as Extract from './Extract';
|
|
import Logger from './Logger';
|
|
import NotificationCode from './NotificationCode';
|
|
import * as ProjectUtils from './project/ProjectUtils';
|
|
import UserManager from './User';
|
|
import * as UrlUtils from './UrlUtils';
|
|
import UserSettings from './UserSettings';
|
|
import XDLError from './XDLError';
|
|
import * as ProjectSettings from './ProjectSettings';
|
|
import MessageCode from './MessageCode';
|
|
|
|
// FIXME(perry) eliminate usage of this template
|
|
export const ENTRY_POINT_PLATFORM_TEMPLATE_STRING = 'PLATFORM_GOES_HERE';
|
|
|
|
export { default as convertProjectAsync } from './project/Convert';
|
|
|
|
const readFileAsync = promisify(fs.readFile);
|
|
const writeFileAsync = promisify(fs.writeFile);
|
|
const validateAsync = promisify(joi.validate);
|
|
const mkdirpAsync = promisify(mkdirp);
|
|
|
|
export async function determineEntryPointAsync(root: string) {
|
|
let { exp, pkg } = await ProjectUtils.readConfigJsonAsync(root);
|
|
|
|
// entryPoint is relative to the packager root and main is relative
|
|
// to the project root. So if your rn-cli.config.js points to a different
|
|
// root than the project root, these can be different. Most of the time
|
|
// you should use main.
|
|
let entryPoint = pkg.main || 'index.js';
|
|
if (exp && exp.entryPoint) {
|
|
entryPoint = exp.entryPoint;
|
|
}
|
|
|
|
return entryPoint;
|
|
}
|
|
|
|
function _starterAppCacheDirectory() {
|
|
let dotExpoHomeDirectory = UserSettings.dotExpoHomeDirectory();
|
|
let dir = path.join(dotExpoHomeDirectory, 'starter-app-cache');
|
|
mkdirp.sync(dir);
|
|
return dir;
|
|
}
|
|
|
|
async function _downloadStarterAppAsync(templateId, progressFunction, retryFunction) {
|
|
let versions = await Api.versionsAsync();
|
|
let templateApp = null;
|
|
for (let i = 0; i < versions.templatesv2.length; i++) {
|
|
if (templateId === versions.templatesv2[i].id) {
|
|
templateApp = versions.templatesv2[i];
|
|
}
|
|
}
|
|
|
|
if (!templateApp) {
|
|
throw new XDLError(ErrorCode.INVALID_OPTIONS, `No template app with id ${templateId}.`);
|
|
}
|
|
|
|
let starterAppVersion = templateApp.version;
|
|
let starterAppName = `${templateId}-${starterAppVersion}`;
|
|
let filename = `${starterAppName}.tar.gz`;
|
|
let starterAppPath = path.join(_starterAppCacheDirectory(), filename);
|
|
|
|
if (await existsAsync(starterAppPath)) {
|
|
return {
|
|
starterAppPath,
|
|
starterAppName,
|
|
};
|
|
}
|
|
|
|
let url = `https://s3.amazonaws.com/exp-starter-apps/${filename}`;
|
|
await Api.downloadAsync(
|
|
url,
|
|
path.join(_starterAppCacheDirectory(), filename),
|
|
{},
|
|
progressFunction,
|
|
retryFunction
|
|
);
|
|
return {
|
|
starterAppPath,
|
|
starterAppName,
|
|
};
|
|
}
|
|
|
|
export async function downloadTemplateApp(templateId: string, selectedDir: string, opts: any) {
|
|
// Validate
|
|
let schema = joi.object().keys({
|
|
name: joi.string().required(),
|
|
});
|
|
|
|
// Should we validate that name is a valid name here?
|
|
try {
|
|
await validateAsync({ name: opts.name }, schema);
|
|
} catch (e) {
|
|
throw new XDLError(ErrorCode.INVALID_OPTIONS, e.toString());
|
|
}
|
|
let name = opts.name;
|
|
let root = path.join(selectedDir, name);
|
|
|
|
Analytics.logEvent('New Project', {
|
|
selectedDir,
|
|
name,
|
|
});
|
|
|
|
let fileExists = true;
|
|
try {
|
|
// If file doesn't exist it will throw an error.
|
|
// Don't want to continue unless there is nothing there.
|
|
fs.statSync(root);
|
|
} catch (e) {
|
|
fileExists = false;
|
|
}
|
|
// This check is required because without it, the retry button would throw an error because the directory already exists,
|
|
// even though it is empty.
|
|
if (fileExists && fs.readdirSync(root).length !== 0) {
|
|
throw new XDLError(
|
|
ErrorCode.DIRECTORY_ALREADY_EXISTS,
|
|
`That directory already exists. Please choose a different parent directory or project name.`
|
|
);
|
|
}
|
|
|
|
// Download files
|
|
await mkdirpAsync(root);
|
|
Logger.notifications.info({ code: NotificationCode.PROGRESS }, MessageCode.DOWNLOADING);
|
|
let { starterAppPath } = await _downloadStarterAppAsync(
|
|
templateId,
|
|
opts.progressFunction,
|
|
opts.retryFunction
|
|
);
|
|
return { starterAppPath, name, root };
|
|
}
|
|
|
|
export async function extractTemplateApp(starterAppPath: string, name: string, root: string) {
|
|
Logger.notifications.info({ code: NotificationCode.PROGRESS }, MessageCode.EXTRACTING);
|
|
await Extract.extractAsync(starterAppPath, root);
|
|
|
|
// Update files
|
|
Logger.notifications.info({ code: NotificationCode.PROGRESS }, MessageCode.CUSTOMIZING);
|
|
|
|
// Update app.json
|
|
let appJson = await readFileAsync(path.join(root, 'app.json'), 'utf8');
|
|
let customAppJson = appJson
|
|
.replace(/\"My New Project\"/, `"${name}"`)
|
|
.replace(/\"my-new-project\"/, `"${name}"`);
|
|
await writeFileAsync(path.join(root, 'app.json'), customAppJson, 'utf8');
|
|
|
|
await initGitRepo(root);
|
|
|
|
Logger.notifications.info({ code: NotificationCode.PROGRESS }, 'Starting project...');
|
|
|
|
return root;
|
|
}
|
|
|
|
async function initGitRepo(root: string) {
|
|
if (process.platform === 'darwin' && !Binaries.isXcodeInstalled()) {
|
|
Logger.global.warn(`Unable to initialize git repo. \`git\` not installed.`);
|
|
return;
|
|
}
|
|
|
|
// let's see if we're in a git tree
|
|
let insideGit = true;
|
|
try {
|
|
await spawnAsync('git', ['rev-parse', '--is-inside-work-tree'], {
|
|
cwd: root,
|
|
});
|
|
Logger.global.debug('New project is already inside of a git repo, skipping git init.');
|
|
} catch (e) {
|
|
insideGit = false;
|
|
}
|
|
|
|
if (!insideGit) {
|
|
try {
|
|
await spawnAsync('git', ['init'], { cwd: root });
|
|
} catch (e) {
|
|
// no-op -- this is just a convenience and we don't care if it fails
|
|
}
|
|
}
|
|
}
|
|
|
|
export async function saveRecentExpRootAsync(root: string) {
|
|
root = path.resolve(root);
|
|
|
|
// Write the recent Exps JSON file
|
|
let recentExpsJsonFile = UserSettings.recentExpsJsonFile();
|
|
let recentExps = await recentExpsJsonFile.readAsync();
|
|
// Filter out copies of this so we don't get dupes in this list
|
|
recentExps = recentExps.filter(function(x) {
|
|
return x !== root;
|
|
});
|
|
recentExps.unshift(root);
|
|
return await recentExpsJsonFile.writeAsync(recentExps.slice(0, 100));
|
|
}
|
|
|
|
function getHomeDir(): string {
|
|
return process.env[process.platform === 'win32' ? 'USERPROFILE' : 'HOME'] || '';
|
|
}
|
|
|
|
function makePathReadable(pth) {
|
|
let homedir = getHomeDir();
|
|
if (pth.substr(0, homedir.length) === homedir) {
|
|
return `~${pth.substr(homedir.length)}`;
|
|
} else {
|
|
return pth;
|
|
}
|
|
}
|
|
|
|
export async function expInfoSafeAsync(root: string) {
|
|
try {
|
|
let { exp: { name, description, icon, iconUrl } } = await ProjectUtils.readConfigJsonAsync(
|
|
root
|
|
);
|
|
let pathOrUrl =
|
|
icon || iconUrl || 'https://d3lwq5rlu14cro.cloudfront.net/ExponentEmptyManifest_192.png';
|
|
let resolvedPath = path.resolve(root, pathOrUrl);
|
|
if (fs.existsSync(resolvedPath)) {
|
|
icon = `file://${resolvedPath}`;
|
|
} else {
|
|
icon = pathOrUrl; // Assume already a URL
|
|
}
|
|
|
|
return {
|
|
readableRoot: makePathReadable(root),
|
|
root,
|
|
name,
|
|
description,
|
|
icon,
|
|
};
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
type PublishInfo = {
|
|
args: {
|
|
username: string,
|
|
remoteUsername: string,
|
|
remotePackageName: string,
|
|
remoteFullPackageName: string,
|
|
bundleIdentifierIOS: ?string,
|
|
packageNameAndroid: ?string,
|
|
},
|
|
};
|
|
|
|
// TODO: remove / change, no longer publishInfo, this is just used for signing
|
|
export async function getPublishInfoAsync(root: string): Promise<PublishInfo> {
|
|
const user = await UserManager.ensureLoggedInAsync();
|
|
|
|
if (!user) {
|
|
throw new Error('Attempted to login in offline mode. This is a bug.');
|
|
}
|
|
|
|
const { username } = user;
|
|
|
|
const { exp } = await ProjectUtils.readConfigJsonAsync(root);
|
|
|
|
const name = exp.slug;
|
|
const version = exp.version;
|
|
|
|
const configName = await ProjectUtils.configFilenameAsync(root);
|
|
|
|
if (!exp || !exp.sdkVersion) {
|
|
throw new Error(`sdkVersion is missing from ${configName}`);
|
|
}
|
|
|
|
if (!name) {
|
|
// slug is made programmatically for app.json
|
|
throw new Error(`slug field is missing from exp.json.`);
|
|
}
|
|
|
|
if (!version) {
|
|
throw new Error(`Can't get version of package.`);
|
|
}
|
|
|
|
let remotePackageName = name;
|
|
let remoteUsername = username;
|
|
let remoteFullPackageName = `@${remoteUsername}/${remotePackageName}`;
|
|
let bundleIdentifierIOS = exp.ios ? exp.ios.bundleIdentifier : null;
|
|
let packageNameAndroid = exp.android ? exp.android.package : null;
|
|
|
|
return {
|
|
args: {
|
|
username,
|
|
remoteUsername,
|
|
remotePackageName,
|
|
remoteFullPackageName,
|
|
bundleIdentifierIOS,
|
|
packageNameAndroid, // TODO: this isn't used anywhere
|
|
},
|
|
};
|
|
}
|
|
|
|
export async function recentValidExpsAsync() {
|
|
let recentExpsJsonFile = UserSettings.recentExpsJsonFile();
|
|
let recentExps = await recentExpsJsonFile.readAsync();
|
|
|
|
let results = await Promise.all(recentExps.map(expInfoSafeAsync));
|
|
let filteredResults = results.filter(result => result);
|
|
return filteredResults;
|
|
}
|
|
|
|
export async function sendAsync(recipient: string, url_: string) {
|
|
let result = await Api.callMethodAsync('send', [recipient, url_]);
|
|
return result;
|
|
}
|
|
|
|
// TODO: figure out where these functions should live
|
|
export async function getProjectRandomnessAsync(projectRoot: string) {
|
|
let ps = await ProjectSettings.readAsync(projectRoot);
|
|
let randomness = ps.urlRandomness;
|
|
if (randomness) {
|
|
return randomness;
|
|
} else {
|
|
return resetProjectRandomnessAsync(projectRoot);
|
|
}
|
|
}
|
|
|
|
export async function resetProjectRandomnessAsync(projectRoot: string) {
|
|
let randomness = UrlUtils.someRandomness();
|
|
ProjectSettings.setAsync(projectRoot, { urlRandomness: randomness });
|
|
return randomness;
|
|
}
|
|
|
|
export async function clearXDLCacheAsync() {
|
|
let dotExpoHomeDirectory = UserSettings.dotExpoHomeDirectory();
|
|
rimraf.sync(path.join(dotExpoHomeDirectory, 'ios-simulator-app-cache'));
|
|
rimraf.sync(path.join(dotExpoHomeDirectory, 'android-apk-cache'));
|
|
rimraf.sync(path.join(dotExpoHomeDirectory, 'starter-app-cache'));
|
|
Logger.notifications.info(`Cleared cache`);
|
|
}
|