@google/clasp
Version:
Develop Apps Script Projects locally
307 lines (306 loc) • 15 kB
JavaScript
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __generator = (this && this.__generator) || function (thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (_) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
};
Object.defineProperty(exports, "__esModule", { value: true });
var fs = require("fs");
var os = require("os");
var cli_spinner_1 = require("cli-spinner");
var pluralize = require("pluralize");
var path = require('path');
var findParentDir = require('find-parent-dir');
var splitLines = require('split-lines');
var dotf = require('dotf');
var read = require('read-file');
var isOnline = require('is-online');
// Names / Paths
exports.PROJECT_NAME = 'clasp';
exports.PROJECT_MANIFEST_BASENAME = 'appsscript';
// Dotfile names
exports.DOT = {
IGNORE: {
DIR: '~',
NAME: exports.PROJECT_NAME + "ignore",
PATH: "." + exports.PROJECT_NAME + "ignore",
},
PROJECT: {
DIR: path.join('.', '/'),
NAME: exports.PROJECT_NAME + ".json",
PATH: "." + exports.PROJECT_NAME + ".json",
},
RC: {
DIR: '~',
LOCAL_DIR: './',
NAME: exports.PROJECT_NAME + "rc.json",
PATH: path.join('~', "." + exports.PROJECT_NAME + "rc.json"),
ABSOLUTE_PATH: path.join(os.homedir(), "." + exports.PROJECT_NAME + "rc.json"),
ABSOLUTE_LOCAL_PATH: path.join('.', "." + exports.PROJECT_NAME + "rc.json"),
},
};
exports.DOTFILE = {
/**
* Reads DOT.IGNORE.PATH to get a glob pattern of ignored paths.
* @return {Promise<string[]>} A list of file glob patterns
*/
IGNORE: function () {
var projectDirectory = findParentDir.sync(process.cwd(), exports.DOT.PROJECT.PATH) || exports.DOT.PROJECT.DIR;
return new Promise(function (res, rej) {
if (fs.existsSync(path.join(projectDirectory, exports.DOT.IGNORE.PATH))) {
var buffer = read.sync(exports.DOT.IGNORE.PATH, 'utf8');
res(splitLines(buffer).filter(function (name) { return name; }));
}
else {
res([]);
}
});
},
/**
* Gets the closest DOT.PROJECT.NAME in the parent directory of the directory
* that the command was run in.
* @return {dotf} A dotf with that dotfile. Null if there is no file
*/
PROJECT: function () {
var projectDirectory = findParentDir.sync(process.cwd(), exports.DOT.PROJECT.PATH) || exports.DOT.PROJECT.DIR;
return dotf(projectDirectory, exports.DOT.PROJECT.NAME);
},
// See `login`: Stores { accessToken, refreshToken }
RC: dotf(exports.DOT.RC.DIR, exports.DOT.RC.NAME),
RC_LOCAL: dotf(exports.DOT.RC.LOCAL_DIR, exports.DOT.RC.NAME),
};
// Error messages (some errors take required params)
exports.ERROR = {
ACCESS_TOKEN: "Error retrieving access token: ",
BAD_CREDENTIALS_FILE: 'Incorrect credentials file format.',
COMMAND_DNE: function (command) { return "\uD83E\uDD14 Unknown command \"" + command + "\"\n\nForgot " + exports.PROJECT_NAME + " commands? Get help:\n " + exports.PROJECT_NAME + " --help"; },
CONFLICTING_FILE_EXTENSION: function (name) { return "File names: " + name + ".js/" + name + ".gs conflict. Only keep one."; },
CREATE: 'Error creating script.',
CREDENTIALS_DNE: 'Credentials file not found.',
DEPLOYMENT_COUNT: "Unable to deploy; Scripts may only have up to 20 versioned deployments at a time.",
FOLDER_EXISTS: "Project file (" + exports.DOT.PROJECT.PATH + ") already exists.",
FS_DIR_WRITE: 'Could not create directory.',
FS_FILE_WRITE: 'Could not write file.',
LOGGED_IN: "You seem to already be logged in. Did you mean to 'logout'?",
LOGGED_OUT: "\nCommand failed. Please login. (" + exports.PROJECT_NAME + " login)",
LOGS_UNAVAILABLE: 'StackDriver logs are getting ready, try again soon.',
OFFLINE: 'Error: Looks like you are offline.',
ONE_DEPLOYMENT_CREATE: 'Currently just one deployment can be created at a time.',
NO_FUNCTION_NAME: 'N/A',
NO_GCLOUD_PROJECT: "\nPlease set your projectId in your .clasp.json file to your Google Cloud project ID. \n\n You can find your projectId by following the instructions in the README here: \n\n https://github.com/google/clasp#get-project-id",
NO_NESTED_PROJECTS: '\nNested clasp projects are not supported.',
READ_ONLY_DELETE: 'Unable to delete read-only deployment.',
PAYLOAD_UNKNOWN: 'Unknown StackDriver payload.',
PERMISSION_DENIED: "Error: Permission denied. Enable the Apps Script API:\nhttps://script.google.com/home/usersettings",
RATE_LIMIT: 'Rate limit exceeded. Check quota.',
SCRIPT_ID: '\n> Did you provide the correct scriptId?\n',
SCRIPT_ID_DNE: "\nNo " + exports.DOT.PROJECT.PATH + " settings found. `create` or `clone` a project first.",
SCRIPT_ID_INCORRECT: function (scriptId) { return "The scriptId \"" + scriptId + "\" looks incorrect.\nDid you provide the correct scriptId?"; },
UNAUTHENTICATED: 'Error: Unauthenticated request: Please try again.',
};
// Log messages (some logs take required params)
exports.LOG = {
AUTH_CODE: 'Enter the code from that page here: ',
AUTH_PAGE_SUCCESSFUL: "Logged in! You may close this page.",
AUTH_SUCCESSFUL: "Saved the credentials to " + exports.DOT.RC.PATH + ". You may close the page.",
AUTHORIZE: function (authUrl) { return "\uD83D\uDD11 Authorize " + exports.PROJECT_NAME + " by visiting this url:\n" + authUrl + "\n"; },
CLONE_SUCCESS: function (fileNum) { return "Cloned " + fileNum + " " + pluralize('files', fileNum) + "."; },
CLONING: 'Cloning files...',
CREATE_PROJECT_FINISH: function (scriptId) { return "Created new script: " + exports.getScriptURL(scriptId); },
CREATE_PROJECT_START: function (title) { return "Creating new script: " + title + "..."; },
CREDENTIALS_FOUND: 'Credentials found, using those to login...',
DEFAULT_CREDENTIALS: 'No credentials given, continuing with default...',
DEPLOYMENT_CREATE: 'Creating deployment...',
DEPLOYMENT_DNE: 'No deployed versions of script.',
DEPLOYMENT_LIST: function (scriptId) { return "Listing deployments..."; },
DEPLOYMENT_START: function (scriptId) { return "Deploying project..."; },
FILES_TO_PUSH: 'Files to push were:',
FINDING_SCRIPTS: 'Finding your scripts...',
FINDING_SCRIPTS_DNE: 'No script files found.',
OPEN_PROJECT: function (scriptId) { return "Opening script: " + scriptId; },
PULLING: 'Pulling files...',
STATUS_PUSH: 'The following files will be pushed by clasp push:',
STATUS_IGNORE: 'Untracked files:',
PUSH_SUCCESS: function (numFiles) { return "Pushed " + numFiles + " " + pluralize('files', numFiles) + "."; },
PUSH_FAILURE: 'Push failed. Errors:',
PUSH_WATCH: 'Watching for changed files...\n',
PUSH_WATCH_UPDATED: function (filename) { return "- Updated: " + filename; },
PUSHING: 'Pushing files...',
REDEPLOY_END: 'Updated deployment.',
REDEPLOY_START: 'Updating deployment...',
STACKDRIVER_SETUP: 'Setting up StackDriver Logging.',
UNDEPLOYMENT_FINISH: function (deploymentId) { return "Undeployed " + deploymentId + "."; },
UNDEPLOYMENT_START: function (deploymentId) { return "Undeploy " + deploymentId + "..."; },
UNTITLED_SCRIPT_TITLE: 'Untitled Script',
VERSION_CREATE: 'Creating a new version...',
VERSION_CREATED: function (versionNumber) { return "Created version " + versionNumber + "."; },
VERSION_DESCRIPTION: function (_a) {
var versionNumber = _a.versionNumber, description = _a.description;
return versionNumber + " - " +
(description || '(no description)');
},
VERSION_NUM: function (numVersions) { return "~ " + numVersions + " " + pluralize('Version', numVersions) + " ~"; },
};
exports.spinner = new cli_spinner_1.Spinner();
/**
* Logs errors to the user such as unauthenticated or permission denied
* @param {object} err The object from the request's error
* @param {string} description The description of the error
*/
exports.logError = function (err, description) {
if (description === void 0) { description = ''; }
// Errors are weird. The API returns interesting error structures.
// TODO(timmerman) This will need to be standardized. Waiting for the API to
// change error model. Don't review this method now.
if (err && typeof err.error === 'string') {
exports.logError(null, JSON.parse(err.error).error);
}
else if (err && err.statusCode === 401 || err && err.error &&
err.error.error && err.error.error.code === 401) {
exports.logError(null, exports.ERROR.UNAUTHENTICATED);
}
else if (err && (err.error && err.error.code === 403 || err.code === 403)) {
exports.logError(null, exports.ERROR.PERMISSION_DENIED);
}
else if (err && err.code === 429) {
exports.logError(null, exports.ERROR.RATE_LIMIT);
}
else {
if (err && err.error) {
console.error("~~ API ERROR (" + (err.statusCode || err.error.code) + ")");
console.error(err.error);
}
if (description)
console.error(description);
process.exit(1);
}
};
/**
* Gets the script URL from a script ID.
*
* It is too expensive to get the script URL from the Drive API. (Async/not offline)
* @param {string} scriptId The script ID
* @return {string} The URL of the script in the online script editor.
*/
exports.getScriptURL = function (scriptId) { return "https://script.google.com/d/" + scriptId + "/edit"; };
/**
* Gets the project settings from the project dotfile. Logs errors.
* Should be used instead of `DOTFILE.PROJECT().read()`
* @param {boolean} failSilently Don't err when dot file DNE.
* @return {Promise} A promise to get the project script ID.
*/
function getProjectSettings(failSilently) {
var promise = new Promise(function (resolve, reject) {
var fail = function (failSilently) {
if (!failSilently) {
exports.logError(null, exports.ERROR.SCRIPT_ID_DNE);
reject();
}
resolve();
};
var dotfile = exports.DOTFILE.PROJECT();
if (dotfile) {
// Found a dotfile, but does it have the settings, or is it corrupted?
dotfile.read().then(function (settings) {
// Settings must have the script ID. Otherwise we err.
if (settings.scriptId) {
resolve(settings);
}
else {
// TODO: Better error message
fail(); // Script ID DNE
}
}).catch(function (err) {
fail(failSilently); // Failed to read dotfile
});
}
else {
fail(); // Never found a dotfile
}
});
promise.catch(function (err) {
exports.logError(err);
exports.spinner.stop(true);
});
return promise;
}
exports.getProjectSettings = getProjectSettings;
/**
* Gets the API FileType. Assumes the path is valid.
* @param {string} path The file path
* @return {string} The API's FileType enum (uppercase), null if not valid.
*/
function getAPIFileType(path) {
var extension = path.substr(path.lastIndexOf('.') + 1).toUpperCase();
return (extension === 'GS' || extension === 'JS') ? 'SERVER_JS' : extension.toUpperCase();
}
exports.getAPIFileType = getAPIFileType;
/**
* Checks if the network is available. Gracefully exits if not.
*/
function checkIfOnline() {
return __awaiter(this, void 0, void 0, function () {
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, isOnline()];
case 1:
if (!(_a.sent())) {
exports.logError(null, exports.ERROR.OFFLINE);
process.exit(1);
}
return [2 /*return*/];
}
});
});
}
exports.checkIfOnline = checkIfOnline;
/**
* Saves the script ID in the project dotfile.
* @param {string} scriptId The script ID
*/
function saveProjectId(scriptId) {
return __awaiter(this, void 0, void 0, function () {
return __generator(this, function (_a) {
return [2 /*return*/, exports.DOTFILE.PROJECT().write({ scriptId: scriptId })]; // Save the script id
});
});
}
exports.saveProjectId = saveProjectId;
/**
* Checks if the current directory appears to be a valid project.
* @return {boolean} True if valid project, false otherwise
*/
function manifestExists() {
return fs.existsSync(exports.PROJECT_MANIFEST_BASENAME + ".json");
}
exports.manifestExists = manifestExists;
;