UNPKG

@google/clasp

Version:

Develop Apps Script Projects locally

307 lines (306 loc) 15 kB
"use strict"; 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;