Merge pull request #45 from FirebasePrivate/mb-use

Implements the "use" command, deprecates "firebase" key
This commit is contained in:
Chris Raynor
2016-03-11 15:33:20 -08:00
15 changed files with 234 additions and 92 deletions

View File

@@ -37,7 +37,7 @@ logger.add(winston.transports.Console, {
});
var debugging = false;
if (_.contains(args, '--debug')) {
if (_.includes(args, '--debug')) {
logger.transports.console.level = 'debug';
debugging = true;
}

View File

@@ -36,5 +36,7 @@ module.exports = function(client) {
client.functions = {};
client.functions.log = loadCommand('functions-log');
client.use = loadCommand('use');
return client;
};

View File

@@ -55,9 +55,9 @@ module.exports = new Command('init')
{
type: 'list',
name: 'project',
message: 'What Firebase do you want to use?',
message: 'What Firebase project do you want to use?',
validate: function(answer) {
if (!_.contains(nameOptions, answer)) {
if (!_.includes(nameOptions, answer)) {
return 'Must specify a Firebase to which you have access';
}
return true;

156
commands/use.js Normal file
View File

@@ -0,0 +1,156 @@
'use strict';
var Command = require('../lib/command');
var logger = require('../lib/logger');
var configstore = require('../lib/configstore');
var requireAuth = require('../lib/requireAuth');
var api = require('../lib/api');
var chalk = require('chalk');
var utils = require('../lib/utils');
var _ = require('lodash');
var fs = require('fs');
var path = require('path');
var prompt = require('../lib/prompt');
var makeActive = function(projectDir, newActive) {
var activeProjects = configstore.get('activeProjects') || {};
if (newActive) {
activeProjects[projectDir] = newActive;
} else {
_.unset(activeProjects, projectDir);
}
configstore.set('activeProjects', activeProjects);
};
var writeAlias = function(projectDir, rc, alias, projectId) {
if (projectId) {
_.set(rc, ['projects', alias], projectId);
} else {
_.unset(rc, ['projects', alias], projectId);
}
fs.writeFileSync(path.resolve(projectDir, './.firebaserc'), JSON.stringify(rc, null, 2));
};
var listAliases = function(options) {
if (_.size(options.rc, 'projects') > 0) {
logger.info('Project aliases for', chalk.bold(options.projectRoot) + ':');
logger.info();
_.forEach(options.rc.projects, function(projectId, alias) {
var listing = alias + ' (' + projectId + ')';
if (options.project === projectId || options.projectAlias === alias) {
logger.info(chalk.cyan.bold('* ' + listing));
} else {
logger.info(' ' + listing);
}
});
} else {
logger.info('Run', chalk.bold('firebase use --add'), 'to define a new project alias.');
}
};
var verifyMessage = function(name) {
return 'please verify project ' + chalk.bold(name) + ' exists and you have access.';
};
module.exports = new Command('use [alias_or_project_id]')
.description('set an active Firebase project for your working directory')
.option('--add', 'create a new project alias interactively')
.option('--alias <name>', 'create a new alias for the provided project id')
.option('--unalias <name>', 'remove an already created project alias')
.option('--clear', 'clear the active project selection')
.before(requireAuth)
.action(function(newActive, options) {
// HACK: Commander.js silently swallows an option called alias >_<
var aliasOpt;
var i = process.argv.indexOf('--alias');
if (i >= 0 && process.argv.length > i + 1) {
aliasOpt = process.argv[i + 1];
}
if (!options.projectRoot) { // not in project directory
return utils.reject(chalk.bold('firebase use') + ' must be run from a Firebase project directory.\n\nRun ' + chalk.bold('firebase init') + ' to start a project directory in the current folder.');
}
if (newActive) { // firebase use [alias_or_project]
var aliasedProject = _.get(options.rc, ['projects', newActive]);
return api.getProjects().then(function(projects) {
if (aliasOpt) { // firebase use [project] --alias [alias]
if (!projects[newActive]) {
return utils.reject('Cannot create alias ' + chalk.bold(aliasOpt) + ', ' + verifyMessage(newActive));
}
writeAlias(options.projectRoot, options.rc, aliasOpt, newActive);
aliasedProject = newActive;
logger.info('Created alias', chalk.bold(aliasOpt), 'for', aliasedProject + '.');
}
if (aliasedProject) { // found alias
if (!projects[aliasedProject]) { // found alias, but not in project list
return utils.reject('Unable to use alias ' + chalk.bold(newActive) + ', ' + verifyMessage(aliasedProject));
}
makeActive(options.projectRoot, newActive);
logger.info('Now using alias', chalk.bold(newActive), '(' + aliasedProject + ')');
} else if (projects[newActive]) { // exact project id specified
makeActive(options.projectRoot, newActive);
logger.info('Now using project', chalk.bold(newActive));
} else { // no alias or project recognized
return utils.reject('Invalid project selection, ' + verifyMessage(newActive));
}
});
} else if (options.unalias) { // firebase use --unalias [alias]
if (_.has(options.rc, ['projects', options.unalias])) {
writeAlias(options.projectRoot, options.rc, options.unalias, null);
logger.info('Removed alias', chalk.bold(options.unalias));
logger.info();
listAliases(options);
}
} else if (options.add) { // firebase use --add
if (options.nonInteractive) {
return utils.reject('Cannot run ' + chalk.bold('firebase use --add') + ' in non-interactive mode. Use ' + chalk.bold('firebase use <project_id> --alias <alias>') + ' instead.');
}
return api.getProjects().then(function(projects) {
var results = {};
return prompt(results, [
{
type: 'list',
name: 'project',
message: 'Which project do you want to add?',
choices: Object.keys(projects).sort()
},
{
type: 'input',
name: 'alias',
message: 'What alias do you want to use for this project? (e.g. staging)'
}
]).then(function() {
writeAlias(options.projectRoot, options.rc, results.alias, results.project);
makeActive(options.projectRoot, results.alias);
logger.info();
logger.info('Created alias', chalk.bold(results.alias), 'for', results.project + '.');
logger.info('Now using alias', chalk.bold(results.alias) + ' (' + results.project + ')');
});
});
} else if (options.clear) { // firebase use --clear
makeActive(options.projectRoot, null);
options.projectAlias = null;
options.project = null;
logger.info('Cleared active project.');
logger.info();
listAliases(options);
} else { // fireabase use
if (options.projectAlias) {
logger.info('Active Project:', chalk.bold.cyan(options.projectAlias + ' (' + options.project + ')'));
} else if (options.project) {
logger.info('Active Project:', chalk.bold.cyan(options.project));
} else {
var msg = 'No project is currently active';
if (_.size(options.rc.projects === 0)) {
msg += ', and no aliases have been created.';
}
logger.info(msg + '.');
}
logger.info();
listAliases(options);
}
});

View File

@@ -58,7 +58,7 @@ var _request = function(options) {
var _appendQueryData = function(path, data) {
if (data && _.size(data) > 0) {
path += _.contains(path, '?') ? '&' : '?';
path += _.includes(path, '?') ? '&' : '?';
path += querystring.stringify(data);
}
return path;

View File

@@ -8,8 +8,10 @@ var utils = require('./utils');
var FirebaseError = require('./error');
var chalk = require('chalk');
var getProjectId = require('./getProjectId');
var loadRCFiles = require('./loadRCFiles');
var loadRCFile = require('./loadRCFile');
var Config = require('./config');
var detectProjectRoot = require('./detectProjectRoot');
var configstore = require('../lib/configstore');
var Command = function(cmd) {
this._cmd = cmd;
@@ -120,19 +122,28 @@ Command.prototype._prepare = function(options) {
options.configError = e;
}
this.applyPrefs(options);
options.projectRoot = detectProjectRoot(options.cwd);
this.applyRC(options);
return RSVP.resolve();
};
/**
* Apply configuration from .firebaserc files in the working directory tree.
*/
Command.prototype.applyPrefs = function(options) {
var prefs = loadRCFiles(options.cwd);
Command.prototype.applyRC = function(options) {
var rc = loadRCFile(options.cwd);
options.rc = rc;
var prefsProject = _.get(prefs, ['project', options.project || 'default']);
if (prefsProject) {
options.project = prefsProject;
options.project = options.project || (configstore.get('activeProjects') || {})[options.projectRoot];
// support deprecated "firebase" key in firebase.json
if (options.config && !options.project) {
options.project = options.config.defaults.project;
}
var rcProject = _.get(rc, ['projects', options.project]);
if (rcProject) {
options.projectAlias = options.project;
options.project = rcProject;
}
};

View File

@@ -11,7 +11,7 @@ var parseBoltRules = require('./parseBoltRules');
var triggerRulesGenerator = require('./triggerRulesGenerator');
var detectProjectRoot = require('./detectProjectRoot');
var chalk = require('chalk');
var utils = require('./utils');
var Config = function(src, options) {
this.options = options || {};
this.projectDir = detectProjectRoot(options.cwd);
@@ -23,6 +23,7 @@ var Config = function(src, options) {
if (this._src.firebase) {
this.defaults.project = this._src.firebase;
utils.logWarning(chalk.bold('"firebase"') + ' key in firebase.json is deprecated. Run ' + chalk.bold('firebase use --add') + ' instead');
}
Config.TARGETS.forEach(function(target) {
@@ -131,7 +132,7 @@ Config.prototype.has = function(key) {
Config.prototype.path = function(pathName) {
var outPath = path.normalize(path.join(this.projectDir, pathName));
if (_.contains(path.relative(this.projectDir, outPath), '..')) {
if (_.includes(path.relative(this.projectDir, outPath), '..')) {
throw new FirebaseError(chalk.bold(pathName) + ' is outside of project directory', {exit: 1});
}
return outPath;

View File

@@ -36,8 +36,8 @@ var deploy = function(targetNames, options) {
var projectId = getProjectId(options);
// --- TEMPORARY CHECKS FOR FUNCTION RULE COMPILATION
var deployingFunctions = _.contains(targetNames, 'functions');
var deployingRules = _.contains(targetNames, 'rules');
var deployingFunctions = _.includes(targetNames, 'functions');
var deployingRules = _.includes(targetNames, 'rules');
var warnText = chalk.yellow.bold('\n>--- ALPHA WARNING! ---<\n');
if (deployingFunctions && !deployingRules) {
console.log(warnText);
@@ -103,7 +103,7 @@ var deploy = function(targetNames, options) {
logger.info();
utils.logSuccess(chalk.underline.bold('Deploy complete!'));
logger.info();
var deployedHosting = _.contains(targetNames, 'hosting');
var deployedHosting = _.includes(targetNames, 'hosting');
if (deployedHosting) {
logger.info(chalk.bold('URL:'), utils.addSubdomain(api.hostingOrigin, projectId));
}

31
lib/loadRCFile.js Normal file
View File

@@ -0,0 +1,31 @@
'use strict';
var fsutils = require('./fsutils');
var path = require('path');
var cjson = require('cjson');
var utils = require('./utils');
var chalk = require('chalk');
var detectProjectRoot = require('./detectProjectRoot');
/**
* .firebaserc should always be a sibling of firebase.json. If it doesn't parse,
* it's considered a warning, not an error.
*/
module.exports = function(cwd) {
var out = {};
var dir = detectProjectRoot(cwd || process.cwd());
if (!dir) {
return out;
}
var potential = path.resolve(dir, './.firebaserc');
if (fsutils.fileExistsSync(potential)) {
try {
out = cjson.load(potential);
} catch (e) {
// a malformed .firebaserc is a warning, not an error
utils.logWarning('JSON parsing error while trying to load ' + chalk.bold(potential));
}
}
return out;
};

View File

@@ -1,32 +0,0 @@
'use strict';
var fsutils = require('./fsutils');
var path = require('path');
var cjson = require('cjson');
var utils = require('./utils');
var _ = require('lodash');
/**
* Detect all .firebaserc files traversing up from the current directory and
* merge them together (with closest to working dir winning conflicts).
*/
module.exports = function(cwd) {
var cur = cwd || process.cwd();
var out = [];
var prev;
while (cur !== prev) {
var potential = path.resolve(cur, './.firebaserc');
if (fsutils.fileExistsSync(potential)) {
try {
out.push(cjson.load(potential));
} catch (e) {
// a malformed .firebaserc is a warning, not an error
utils.logWarning('JSON parsing error while trying to load ' + potential);
}
}
prev = cur;
cur = path.dirname(cur);
}
return _.merge.apply(null, out.reverse());
};

View File

@@ -57,10 +57,10 @@
"fs-extra": "^0.23.1",
"fstream-ignore": "^1.0.2",
"googleapis": "^2.1.7",
"inquirer": "^0.8.5",
"inquirer": "^0.12.0",
"jsonschema": "^1.0.2",
"jsonwebtoken": "^5.4.0",
"lodash": "^3.10.0",
"lodash": "^4.6.1",
"node-uuid": "^1.4.3",
"open": "^0.0.5",
"portfinder": "^0.4.0",

1
test/fixtures/fbrc/firebase.json vendored Normal file
View File

@@ -0,0 +1 @@
{}

View File

@@ -0,0 +1 @@
{}

View File

@@ -5,8 +5,6 @@ var expect = chai.expect;
chai.use(require('chai-as-promised'));
var RSVP = require('rsvp');
var path = require('path');
var fixturesDir = path.resolve(__dirname, '../fixtures');
var Command = require('../../lib/command');
describe('Command', function() {
@@ -45,37 +43,6 @@ describe('Command', function() {
expect(command.action(action)._action).to.equal(action);
});
describe('.applyPrefs()', function() {
var run;
var rcdir = path.resolve(fixturesDir, 'fbrc');
beforeEach(function() {
run = command.action(function(options) {
return RSVP.resolve(options.project);
}).runner();
});
it('should use the specified project if no alias is found', function() {
return expect(run({
project: 'my-specific-project',
cwd: rcdir
})).to.eventually.eq('my-specific-project');
});
it('should use the provided alias if one is found in .firebaserc', function() {
return expect(run({
project: 'other',
cwd: rcdir
})).to.eventually.eq('top');
});
it('should use the default alias if no project is specified and one is found in .firebaserc', function() {
return expect(run({
cwd: rcdir
})).to.eventually.eq('top');
});
});
describe('.runner()', function() {
it('should work when no arguments are passed and options', function() {
var run = command.action(function(options) {

View File

@@ -4,19 +4,23 @@ var chai = require('chai');
var expect = chai.expect;
var path = require('path');
var loadRCFiles = require('../../lib/loadRCFiles');
var loadRCFile = require('../../lib/loadRCFile');
var fixturesDir = path.resolve(__dirname, '../fixtures');
describe('loadRCFiles', function() {
it('should merge all detected files', function() {
var result = loadRCFiles(path.resolve(fixturesDir, 'fbrc/conflict'));
expect(result.project.default).to.eq('conflict');
return expect(result.project.other).to.eq('top');
describe('loadRCFile', function() {
it('should load from nearest project directory', function() {
var result = loadRCFile(path.resolve(fixturesDir, 'fbrc/conflict'));
expect(result.project.default).to.eq('top');
});
it('should not crash when encountering malformed files', function() {
var result = loadRCFiles(path.resolve(fixturesDir, 'fbrc/invalid'));
return expect(result.project.other).to.eq('top');
it('should be an empty object when not in project dir', function() {
var result = loadRCFile(__dirname);
return expect(result).to.deep.eq({});
});
it('should not throw up on invalid json', function() {
var result = loadRCFile(path.resolve(fixturesDir, 'fbrc/invalid'));
return expect(result).to.deep.eq({});
});
});