@titanium/turbo
Version:
🚀 Turbo is the awesome framework for turbo charging your Titanium cross-platform native mobile app development!
369 lines (321 loc) • 12.2 kB
JavaScript
/*
Converts a Classic Titanium project to an Alloy project.
Run `alloy new [path]` for basic operation.
As of Alloy 1.7, you can create an app from one of the testing
apps by using:
`alloy new [path] --testapp ui/tableview `
*/
var path = require('path'),
fs = require('fs-extra'),
chmodr = require('chmodr'),
_ = require('lodash'),
U = require('../../utils'),
CONST = require('../../common/constants'),
logger = require('../../logger'),
https = require('https'),
{ spawn } = require('child_process');
var BASE_ERR = 'Project creation failed. ';
var platformsDir = path.join(__dirname, '..', '..', '..', 'platforms');
var templatesDir = path.join(__dirname, '..', '..', '..', 'templates');
var sampleAppsDir = path.join(__dirname, '..', '..', '..', 'samples', 'apps');
module.exports = async function(args, program) {
var appDirs = ['controllers', 'styles', 'views', 'models', 'assets'];
var templateName = args[1] || 'default';
var paths = getPaths(args[0] || '.', templateName, program.testapp);
// only overwrite existing app path if given the force option
if (fs.existsSync(paths.app)) {
if (!program.force) {
U.die(BASE_ERR + '"app" directory already exists at "' + paths.app + '"');
} else {
fs.removeSync(paths.app);
}
}
fs.mkdirpSync(paths.app);
chmodr.sync(paths.app, 0755);
// copy platform-specific folders from Resources to app/assets
_.each(CONST.PLATFORM_FOLDERS, function(platform) {
var rPath = path.join(paths.resources, platform);
if (fs.existsSync(rPath)) {
var aPath = path.join(paths.app, CONST.DIR.ASSETS, platform);
fs.mkdirpSync(aPath);
chmodr.sync(aPath, 0755);
fs.copySync(rPath, aPath, {preserveTimestamps:true});
}
});
// add alloy-specific folders
_.each(appDirs, function(dir) {
fs.mkdirpSync(path.join(paths.app, dir));
chmodr.sync(path.join(paths.app, dir), 0755);
});
// move existing i18n and platform directories into app directory
['i18n', 'platform'].forEach(function (name) {
var src = path.join(paths.project, name);
if (fs.existsSync(src)) {
fs.renameSync(src, path.join(paths.app, name));
}
});
// add the default alloy.js file
U.copyFileSync(path.join(paths.template, 'alloy.js'), path.join(paths.app, 'alloy.js'));
// webpack builds don't require the alloy plugin
if (!templateName.includes('webpack')) {
// install ti.alloy compiler plugin
U.installPlugin(path.join(paths.alloy, '..'), paths.project);
}
// replace the classic webpack plugin with the alloy one
if (templateName.includes('webpack')) {
let pkg = {
devDependencies: { },
scripts: { }
};
// if there is an existing package.json then we'll just update it, if not then we'll make a
// best attempt to get a working project
if (await fs.exists(paths.packageJson)) {
pkg = await fs.readJSON(paths.packageJson);
// Make sure the required stanzas exist in the package.json
if (!pkg.dependencies) {
pkg.dependencies = {};
}
if (!pkg.devDependencies) {
pkg.devDependencies = {};
}
if (!pkg.scripts) {
pkg.scripts = {};
}
// remove the classic plugin if it exists
if (pkg.dependencies['@titanium-sdk/webpack-plugin-classic']) {
delete pkg.dependencies['@titanium-sdk/webpack-plugin-classic'];
}
if (pkg.devDependencies['@titanium-sdk/webpack-plugin-classic']) {
delete pkg.devDependencies['@titanium-sdk/webpack-plugin-classic'];
}
} else {
logger.warn(`No package.json file exists in ${paths.project} so creating one`);
logger.warn('Please visit https://github.com/appcelerator/webpack-plugin-alloy#readme to make sure your project is fully up to date');
// specify this exact version of webpack as using a new version requires all things to be updated
pkg.devDependencies.webpack = '^4.43.0';
pkg.devDependencies.eslint = '^7.5.0';
pkg.devDependencies['eslint-config-axway'] = '4.7.0';
pkg.devDependencies['babel-eslint'] = '10.1.0';
pkg.private = true;
}
const srcFolder = path.join(paths.project, 'src');
if (await fs.exists(srcFolder)) {
logger.info('Removing src folder');
await fs.remove(srcFolder);
}
// add the required dependencies, checking for the latest version if possible
Object.assign(pkg.devDependencies, {
'@titanium-sdk/webpack-plugin-alloy': `^${await getLatestPackageVersion('@titanium-sdk/webpack-plugin-alloy', '0.2.1')}`,
'@titanium-sdk/webpack-plugin-babel': `^${await getLatestPackageVersion('@titanium-sdk/webpack-plugin-babel', '0.1.2')}`,
'alloy': `^${await getLatestPackageVersion('alloy', '1.15.0')}`,
'alloy-compiler': `^${await getLatestPackageVersion('alloy-compiler', '0.2.4')}`,
'eslint-plugin-alloy': `^${await getLatestPackageVersion('eslint-plugin-alloy', '1.1.1')}`
});
Object.assign(pkg.scripts, {
lint: 'eslint app/'
});
await fs.writeJSON(paths.packageJson, pkg, { spaces: 2 });
// If the template has a readme then read it in and append it to the existing README.md file,
// if that file doesn't exist then it'll get created
if (await fs.exists(paths.readme)) {
const readmeContents = await fs.readFile(paths.readme);
await fs.appendFile(paths.appReadme, `\n${readmeContents}`);
}
}
// add the default app.tss file
U.copyFileSync(path.join(paths.template, CONST.GLOBAL_STYLE), path.join(paths.app, CONST.DIR.STYLE, CONST.GLOBAL_STYLE));
// copy DefaultIcon.png to project root
fs.readdirSync(templatesDir).forEach(function (name) {
if (/^DefaultIcon(\-\w+)?\.png$/.test(name)) {
U.copyFileSync(path.join(templatesDir, name), path.join(paths.project, name));
}
});
// copy Resources platform-specific directories to assets
U.copyFileSync(
path.join(paths.template, 'gitignore.txt'),
path.join(paths.project, '.gitignore')
);
_.each(CONST.PLATFORM_FOLDERS, function(dir) {
var rDir = path.join(paths.resources, dir);
if (!fs.existsSync(rDir)) {
return;
}
var p = path.join(paths.app, 'assets', dir);
fs.mkdirpSync(p);
chmodr.sync(p, 0755);
fs.copySync(rDir, p);
});
// copy in any Alloy-specific Resources files
// fs.copySync(paths.alloyResources,paths.assets,{preserveTimestamps:true});
_.each(CONST.PLATFORMS, function(p) {
var pDir = path.join(platformsDir, p, 'project');
if (!fs.existsSync(pDir)) {
return;
}
fs.copySync(
pDir,
paths.project,
{preserveTimestamps:true}
);
});
// add alloy project template files
var tplPath = (!program.testapp) ? path.join(paths.projectTemplate, 'app') : paths.projectTemplate;
fs.copySync(tplPath, paths.app, {preserveTimestamps:true});
// Don't copy across the default README if it's a webpack project
if (!templateName.includes('webpack')) {
fs.writeFileSync(path.join(paths.app, 'README'), fs.readFileSync(paths.readme, 'utf8'));
}
// if creating from one of the test apps...
if (program.testapp) {
// remove _generated folder,
// TODO: once we update wrench (ALOY-1001), add an exclude regex to the
// copyDirSynRecursive() statements above rather than deleting the folder here
fs.removeSync(path.join(paths.app, '_generated'));
if (fs.existsSync(path.join(sampleAppsDir, program.testapp, 'specs'))) {
// copy in the test harness
fs.mkdirpSync(path.join(paths.app, 'lib'));
chmodr.sync(path.join(paths.app, 'lib'), 0755);
fs.copySync(path.join(path.resolve(sampleAppsDir, '..'), 'lib'), path.join(paths.app, 'lib'), {preserveTimestamps:true});
}
}
// delete the build folder to give us a fresh run
fs.removeSync(paths.build);
if (templateName.includes('webpack')) {
logger.info('Installing project dependencies');
try {
await installDependencies(paths.project, logger);
} catch (error) {
logger.error('Failed to install project dependencies');
logger.error(error);
}
}
if (await fs.exists(paths.eslintTemplate)) {
await fs.copy(path.join(paths.eslintTemplate), path.join(paths.eslintApp));
}
// Copy across the settings.json to have VS Code hide certain directories
const vscodeDir = path.join(paths.project, '.vscode');
if (!await fs.exists(vscodeDir)) {
await fs.ensureDir(vscodeDir);
}
await fs.copy(path.join(paths.template, 'settings.json'), path.join(vscodeDir, 'settings.json'));
logger.info('Generated new project at: ' + paths.app);
};
function getPaths(project, templateName, testapp) {
var alloy = path.join(__dirname, '..', '..');
var template = path.join(alloy, 'template');
var projectTemplates = path.join(alloy, '..', 'templates');
var templateReadme = path.join(projectTemplates, templateName, 'README.md');
var customTemplateDir;
var customAppDir;
var readMeFile;
if (fs.existsSync(templateName) && !testapp) {
customTemplateDir = templateName;
customAppDir = path.join(templateName, 'app');
readMeFile = path.join(customTemplateDir, 'README');
} else if (fs.existsSync(templateReadme)) {
readMeFile = templateReadme;
}
var paths = {
// alloy paths
alloy: alloy,
template: path.join(alloy, 'template'),
readme: fs.existsSync(readMeFile) ? readMeFile : path.join(template, 'README'),
appTemplate: (!testapp) ? customAppDir || path.join(projectTemplates, templateName, 'app') : path.join(sampleAppsDir, testapp),
projectTemplate: (!testapp) ? customTemplateDir || path.join(projectTemplates, templateName) : path.join(sampleAppsDir, testapp),
// project paths
project: project,
resources: path.join(project, 'Resources'),
build: path.join(project, 'build')
};
// validate the existence of the paths
_.each(paths, function(v, k) {
if (!fs.existsSync(v)) {
var errs = [BASE_ERR];
switch (k) {
case 'build':
// skip
return;
case 'projectTemplate':
var projError = (!testapp) ? 'Project template "' + templateName : 'Test app "' + testapp;
errs.push(projError + '" not found at "' + v + '"');
break;
case 'appTemplate':
var appError = (!testapp) ? 'Application template "' + v : 'Test app "' + testapp;
errs.push(appError + '" not found');
break;
case 'project':
errs.push('Project path not found at "' + v + '"');
break;
default:
errs.push('"' + v + '" not found.');
break;
}
U.die(errs);
}
});
// Added after validation, since they won't exist yet
_.extend(paths, {
app: path.join(paths.project, 'app'),
assets: path.join(paths.project, 'app', 'assets'),
plugins: path.join(paths.project, 'plugins'),
packageJson: path.join(paths.project, 'package.json'),
eslintTemplate: path.join(paths.appTemplate, 'eslintrc_js'),
eslintApp: path.join(paths.project, '.eslintrc.js'),
appReadme: path.join(paths.project, 'README.md')
});
return paths;
}
/**
* Check what the "latest" dist-tag on npm is for the provided package, falling back to the
* default version provided if the request errors
*
* @param {String} packageName - Name of the package to lookup
* @param {String} defaultVersion - If the latest version can't be resolved, the version to fallback to
*
* @returns {Promise<String>} Either the latest version of the defaultVersion if the request errors
*/
async function getLatestPackageVersion (packageName, defaultVersion) {
return new Promise((resolve) => {
try {
https.get(`https://registry.npmjs.org/-/package/${packageName}/dist-tags`, res => {
if (res.statusCode === 200) {
let body = '';
res.on('data', data => body += data);
res.on('end', () => {
return resolve(JSON.parse(body).latest);
});
} else {
return resolve(defaultVersion);
}
});
} catch (error) {
return resolve(defaultVersion);
}
});
}
/**
* Runs "npm i" in the provided project directory, code is lifted from the post-create hook in the
* angular project template
*
* @param {String} projectPath - path to the project
*/
async function installDependencies(projectPath) {
let npmExecutable = 'npm';
const spawnOptions = {
cwd: projectPath,
stdio: 'inherit'
};
if (process.platform === 'win32') {
spawnOptions.shell = true;
npmExecutable += '.cmd';
}
return new Promise((resolve, reject) => {
const child = spawn(npmExecutable, [ 'i' ], spawnOptions);
child.on('close', code => {
if (code !== 0) {
return reject(new Error('Failed to install project dependencies.'));
}
resolve();
});
});
}