diff --git a/.gitignore b/.gitignore index be5a2b54d..14e5e7e4f 100644 --- a/.gitignore +++ b/.gitignore @@ -46,3 +46,4 @@ e2e/build !/desktop/assets/ !/desktop/electron.js src/configconstants.ts +.moodleapp-dev-config diff --git a/.travis.yml b/.travis.yml index dde8041a3..5cf443cd2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -64,6 +64,7 @@ jobs: - node --version - npm --version - nvm --version + - sudo apt-get install -y libsecret-1-dev > /dev/null - npm ci - npm install -g gulp script: scripts/aot.sh @@ -83,6 +84,8 @@ jobs: - ELECTRON_CACHE=$HOME/.cache/electron - ELECTRON_BUILDER_CACHE=$HOME/.cache/electron-builder - BUILD_PLATFORM='linux' + before_install: + - sudo apt-get install -y libsecret-1-dev > /dev/null script: scripts/aot.sh - stage: build name: "Build MacOS" diff --git a/gulp/dev-config.js b/gulp/dev-config.js new file mode 100644 index 000000000..b270bf51f --- /dev/null +++ b/gulp/dev-config.js @@ -0,0 +1,69 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +const fs = require('fs'); + +const DEV_CONFIG_FILE = '.moodleapp-dev-config'; + +/** + * Class to read and write dev-config data from a file. + */ +class DevConfig { + + constructor() { + this.loadFileData(); + } + + /** + * Get a setting. + * + * @param name Name of the setting to get. + * @param defaultValue Value to use if not found. + */ + get(name, defaultValue) { + return typeof this.config[name] != 'undefined' ? this.config[name] : defaultValue; + } + + /** + * Load file data to memory. + */ + loadFileData() { + if (!fs.existsSync(DEV_CONFIG_FILE)) { + this.config = {}; + + return; + } + + try { + this.config = JSON.parse(fs.readFileSync(DEV_CONFIG_FILE)); + } catch (error) { + console.error('Error reading dev config file.', error); + this.config = {}; + } + } + + /** + * Save some settings. + * + * @param settings Object with the settings to save. + */ + save(settings) { + this.config = Object.assign(this.config, settings); + + // Save the data in the dev file. + fs.writeFileSync(DEV_CONFIG_FILE, JSON.stringify(this.config, null, 4)); + } +} + +module.exports = new DevConfig(); diff --git a/gulp/git.js b/gulp/git.js new file mode 100644 index 000000000..f3f66298a --- /dev/null +++ b/gulp/git.js @@ -0,0 +1,237 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +const exec = require('child_process').exec; +const fs = require('fs'); +const DevConfig = require('./dev-config'); +const Utils = require('./utils'); + +/** + * Class to run git commands. + */ +class Git { + + /** + * Create a patch. + * + * @param range Show only commits in the specified revision range. + * @param saveTo Path to the file to save the patch to. If not defined, the patch contents will be returned. + * @return Promise resolved when done. If saveTo not provided, it will return the patch contents. + */ + createPatch(range, saveTo) { + return new Promise((resolve, reject) => { + exec(`git format-patch ${range} --stdout`, (err, result) => { + if (err) { + reject(err || 'Cannot create patch.'); + return; + } + + if (!saveTo) { + resolve(result); + return; + } + + // Save it to a file. + const directory = saveTo.substring(0, saveTo.lastIndexOf('/')); + if (directory && directory != '.' && directory != '..' && !fs.existsSync(directory)) { + fs.mkdirSync(directory); + } + fs.writeFileSync(saveTo, result); + + resolve(); + }); + }); + } + + /** + * Get current branch. + * + * @return Promise resolved with the branch name. + */ + getCurrentBranch() { + return new Promise((resolve, reject) => { + exec('git branch --show-current', (err, branch) => { + if (branch) { + resolve(branch.replace('\n', '')); + } else { + reject (err || 'Current branch not found.'); + } + }); + }); + } + + /** + * Get the HEAD commit for a certain branch. + * + * @param branch Name of the branch. + * @param branchData Parsed branch data. If not provided it will be calculated. + * @return HEAD commit. + */ + async getHeadCommit(branch, branchData) { + if (!branchData) { + // Parse the branch to get the project and issue number. + branchData = Utils.parseBranch(branch); + } + + // Loop over the last commits to find the first commit messages that doesn't belong to the issue. + const commitsString = await this.log(50, branch, '%s_____%H'); + const commits = commitsString.split('\n'); + commits.pop(); // Remove last element, it's an empty string. + + for (let i = 0; i < commits.length; i++) { + const commit = commits[i]; + const match = Utils.getIssueFromCommitMessage(commit) == branchData.issue; + + if (i === 0 && !match) { + // Most recent commit doesn't belong to the issue. Stop looking. + break; + } + + if (!match) { + // The commit does not match any more, we found it! + return commit.split('_____')[1]; + } + } + + // Couldn't find the commit using the commit names, get the last commit in the integration branch. + const remote = DevConfig.get('upstreamRemote', 'origin'); + console.log(`Head commit not found using commit messages. Get last commit from ${remote}/integration`); + const hashes = await this.hashes(1, `${remote}/integration`); + + return hashes[0]; + } + + /** + * Get the URL of a certain remote. + * + * @param remote Remote name. + * @return Promise resolved with the remote URL. + */ + getRemoteUrl(remote) { + return new Promise((resolve, reject) => { + exec(`git remote get-url ${remote}`, (err, url) => { + if (url) { + resolve(url.replace('\n', '')); + } else { + reject (err || 'Remote not found.'); + } + }); + }); + } + + /** + * Return the latest hashes from git log. + * + * @param count Number of commits to display. + * @param range Show only commits in the specified revision range. + * @param format Pretty-print the contents of the commit logs in a given format. + * @return Promise resolved with the list of hashes. + */ + async hashes(count, range, format) { + format = format || '%H'; + + const hashList = await this.log(count, range, format); + + const hashes = hashList.split('\n'); + hashes.pop(); // Remove last element, it's an empty string. + + return hashes; + } + + /** + * Calls the log command and returns the raw output. + * + * @param count Number of commits to display. + * @param range Show only commits in the specified revision range. + * @param format Pretty-print the contents of the commit logs in a given format. + * @param path Show only commits that are enough to explain how the files that match the specified paths came to be. + * @return Promise resolved with the result. + */ + log(count, range, format, path) { + if (typeof count == 'undefined') { + count = 10; + } + + let command = 'git log'; + + if (count > 0) { + command += ` -n ${count} `; + } + if (format) { + command += ` --format=${format} `; + } + if (range){ + command += ` ${range} `; + } + if (path) { + command += ` -- ${path}`; + } + + return new Promise((resolve, reject) => { + exec(command, (err, result, stderr) => { + if (err) { + reject(err); + } else { + resolve(result); + } + }); + }); + } + + /** + * Return the latest titles of the commit messages. + * + * @param count Number of commits to display. + * @param range Show only commits in the specified revision range. + * @param path Show only commits that are enough to explain how the files that match the specified paths came to be. + * @return Promise resolved with the list of titles. + */ + async messages(count, range, path) { + count = typeof count != 'undefined' ? count : 10; + + const messageList = await this.log(count, range, '%s', path); + + const messages = messageList.split('\n'); + messages.pop(); // Remove last element, it's an empty string. + + return messages; + } + + /** + * Push a branch. + * + * @param remote Remote to use. + * @param branch Branch to push. + * @param force Whether to force the push. + * @return Promise resolved when done. + */ + push(remote, branch, force) { + return new Promise((resolve, reject) => { + let command = `git push ${remote} ${branch}`; + if (force) { + command += ' -f'; + } + + exec(command, (err, result, stderr) => { + if (err) { + reject(err); + } else { + resolve(); + } + }); + }); + } +} + +module.exports = new Git(); diff --git a/gulp/jira.js b/gulp/jira.js new file mode 100644 index 000000000..317fda5df --- /dev/null +++ b/gulp/jira.js @@ -0,0 +1,474 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +const exec = require('child_process').exec; +const https = require('https'); +const keytar = require('keytar'); +const inquirer = require('inquirer'); +const fs = require('fs'); +const request = require('request'); // This lib is deprecated, but it was the only way I found to make upload files work. +const DevConfig = require('./dev-config'); +const Git = require('./git'); +const Url = require('./url'); +const Utils = require('./utils'); + +const apiVersion = 2; + +/** + * Class to interact with Jira. + */ +class Jira { + + /** + * Ask the password to the user. + * + * @return Promise resolved with the password. + */ + async askPassword() { + const data = await inquirer.prompt([ + { + type: 'password', + name: 'password', + message: `Please enter the password for the username ${this.username}.`, + }, + ]); + + return data.password; + } + + /** + * Ask the user the tracker data. + * + * @return Promise resolved with the data, rejected if cannot get. + */ + async askTrackerData() { + const data = await inquirer.prompt([ + { + type: 'input', + name: 'url', + message: 'Please enter the tracker URL.', + default: 'https://tracker.moodle.org/', + }, + { + type: 'input', + name: 'username', + message: 'Please enter your tracker username.', + }, + ]); + + DevConfig.save({ + 'tracker.url': data.url, + 'tracker.username': data.username, + }); + + return data; + } + + /** + * Build URL to perform requests to Jira. + * + * @param uri URI to add the the Jira URL. + * @return URL. + */ + buildRequestUrl(uri) { + return Utils.concatenatePaths([this.url, this.uri, '/rest/api/', apiVersion, uri]); + } + + /** + * Delete an attachment. + * + * @param attachmentId Attachment ID. + * @return Promise resolved when done. + */ + async deleteAttachment(attachmentId) { + const response = await this.request(`attachment/${attachmentId}`, 'DELETE'); + + if (response.status != 204) { + throw new Error('Could not delete the attachment'); + } + } + + /** + * Load the issue info from jira server using a REST API call. + * + * @param key Key to identify the issue. E.g. MOBILE-1234. + * @param fields Fields to get. + * @return Promise resolved with the issue data. + */ + async getIssue(key, fields) { + fields = fields || '*all,-comment'; + + await this.init(); // Initialize data if needed. + + const response = await this.request(`issue/${key}`, 'GET', {'fields': fields, 'expand': 'names'}); + + if (response.status == 404) { + throw new Error('Issue could not be found.'); + } else if (response.status != 200) { + throw new Error('The tracker is not available.') + } + + const issue = response.data; + issue.named = {}; + + // Populate the named fields in a separate key. Allows us to easily find them without knowing the field ID. + const nameList = issue.names || {}; + for (const fieldKey in issue.fields) { + if (nameList[fieldKey]) { + issue.named[nameList[fieldKey]] = issue.fields[fieldKey]; + } + } + + return issue + } + + /** + * Load the version info from the jira server using a rest api call. + * + * @return Promise resolved when done. + */ + async getServerInfo() { + const response = await this.request('serverInfo'); + + if (response.status != 200) { + throw new Error(`Unexpected response code: ${response.status}`, response); + } + + this.version = response.data; + } + + /** + * Get tracker data to push an issue. + * + * @return Promise resolved with the data. + */ + async getTrackerData() { + // Check dev-config file first. + let data = this.getTrackerDataFromDevConfig(); + + if (data) { + console.log('Using tracker data from dev-config file'); + return data; + } + + // Try to use mdk now. + try { + data = await this.getTrackerDataFromMdk(); + + console.log('Using tracker data from mdk'); + + return data; + } catch (error) { + // MDK not available or not configured. Ask for the data. + const data = await this.askTrackerData(); + + data.fromInput = true; + + return data; + } + } + + /** + * Get tracker data from dev config file. + * + * @return Data, undefined if cannot get. + */ + getTrackerDataFromDevConfig() { + const url = DevConfig.get('tracker.url'); + const username = DevConfig.get('tracker.username'); + + if (url && username) { + return { + url, + username, + }; + } + } + + /** + * Get tracker URL and username from mdk. + * + * @return Promise resolved with the data, rejected if cannot get. + */ + getTrackerDataFromMdk() { + return new Promise((resolve, reject) => { + exec('mdk config show tracker.url', (err, url) => { + if (!url) { + reject(err || 'URL not found.'); + return; + } + + exec('mdk config show tracker.username', (err, username) => { + if (username) { + resolve({ + url: url.replace('\n', ''), + username: username.replace('\n', ''), + }); + } else { + reject(err | 'Username not found.'); + } + }); + }); + }); + } + + /** + * Initialize some data. + * + * @return Promise resolved when done. + */ + async init() { + if (this.initialized) { + // Already initialized. + return; + } + + // Get tracker URL and username. + const trackerData = await this.getTrackerData(); + + this.url = trackerData.url; + this.username = trackerData.username; + + const parsed = Url.parse(this.url); + this.ssl = parsed.protocol == 'https'; + this.host = parsed.domain; + this.uri = parsed.path; + + // Get the password. + this.password = await keytar.getPassword('mdk-jira-password', this.username); // Use same service name as mdk. + + if (!this.password) { + // Ask the user. + this.password = await this.askPassword(); + } + + while (!this.initialized) { + try { + await this.getServerInfo(); + + this.initialized = true; + keytar.setPassword('mdk-jira-password', this.username, this.password); + } catch (error) { + console.log('Error connecting to the server. Please make sure you entered the data correctly.', error); + if (trackerData.fromInput) { + // User entered the data manually, ask him again. + trackerData = await this.askTrackerData(); + + this.url = trackerData.url; + this.username = trackerData.username; + } + + this.password = await this.askPassword(); + } + } + } + + /** + * Check if a certain issue could be a security issue. + * + * @param key Key to identify the issue. E.g. MOBILE-1234. + * @return Promise resolved with boolean: whether it's a security issue. + */ + async isSecurityIssue(key) { + const issue = await this.getIssue(key, 'security'); + + return issue.fields && !!issue.fields.security; + } + + /** + * Sends a request to the server and returns the data. + * + * @param uri URI to add the the Jira URL. + * @param method Method to use. Defaults to 'GET'. + * @param params Params to send as GET params (in the URL). + * @param data JSON string with the data to send as POST/PUT params. + * @param headers Headers to send. + * @return Promise resolved with the result. + */ + request(uri, method, params, data, headers) { + uri = uri || ''; + method = (method || 'GET').toUpperCase(); + data = data || ''; + params = params || {}; + headers = headers || {}; + headers['Content-Type'] = 'application/json'; + + return new Promise((resolve, reject) => { + + // Build the request URL. + const url = Url.addParamsToUrl(this.buildRequestUrl(uri), params); + + // Initialize the request. + const options = { + method: method, + auth: `${this.username}:${this.password}`, + headers: headers, + }; + const request = https.request(url, options); + + // Add data. + if (data) { + request.write(data); + } + + // Treat response. + request.on('response', (response) => { + // Read the result. + let result = ''; + response.on('data', (chunk) => { + result += chunk; + }); + response.on('end', () => { + try { + result = JSON.parse(result); + } catch (error) { + // Leave it as text. + } + + resolve({ + status: response.statusCode, + data: result, + }); + }); + }); + + request.on('error', (e) => { + reject(e); + }); + + // Send the request. + request.end(); + }); + } + + /** + * Sets a set of fields for a certain issue in Jira. + * + * @param key Key to identify the issue. E.g. MOBILE-1234. + * @param updates Object with the fields to update. + * @return Promise resolved when done. + */ + async setCustomFields(key, updates) { + const issue = await this.getIssue(key); + const update = {'fields': {}}; + + // Detect which fields have changed. + for (const updateName in updates) { + const updateValue = updates[updateName]; + const remoteValue = issue.named[updateName]; + + if (!remoteValue || remoteValue != updateValue) { + // Map the label of the field with the field code. + let fieldKey; + for (const key in issue.names) { + if (issue.names[key] == updateName) { + fieldKey = key; + break; + } + } + + if (!fieldKey) { + throw new Error(`Could not find the field named ${updateName}.`); + } + + update.fields[fieldKey] = updateValue; + } + } + + if (!Object.keys(update.fields).length) { + // No fields to update. + console.log('No updates required.') + return; + } + + const response = await this.request(`issue/${key}`, 'PUT', null, JSON.stringify(update)); + + if (response.status != 204) { + throw new Error(`Issue was not updated: ${response.status}`, response.data); + } + + console.log('Issue updated successfully.'); + } + + /** + * Upload a new attachment to an issue. + * + * @param key Key to identify the issue. E.g. MOBILE-1234. + * @param filePath Path to the file to upload. + * @return Promise resolved when done. + */ + async upload(key, filePath) { + + const uri = `issue/${key}/attachments`; + const headers = { + 'X-Atlassian-Token': 'nocheck', + } + + const response = await this.uploadFile(uri, 'file', filePath, headers); + + if (response.status != 200) { + throw new Error('Could not upload file to Jira issue'); + } + + console.log('File successfully uploaded.') + } + + /** + * Upload a file to Jira. + * + * @param uri URI to add the the Jira URL. + * @param fieldName Name of the form field where to put the file. + * @param filePath Path to the file. + * @param headers Headers. + * @return Promise resolved with the result. + */ + async uploadFile(uri, fieldName, filePath, headers) { + uri = uri || ''; + headers = headers || {}; + headers['Content-Type'] = 'multipart/form-data'; + + return new Promise((resolve, reject) => { + // Add the file to the form data. + const formData = {}; + formData[fieldName] = { + value: fs.createReadStream(filePath), + options: { + filename: filePath.substr(filePath.lastIndexOf('/') + 1), + contentType: 'multipart/form-data', + }, + }; + + // Perform the request. + const options = { + url: this.buildRequestUrl(uri), + method: 'POST', + headers: headers, + auth: { + user: this.username, + pass: this.password, + }, + formData: formData, + }; + + request(options, (err, httpResponse, body) => { + resolve({ + status: httpResponse.statusCode, + data: body, + }); + }); + }); + } +} + +module.exports = new Jira(); diff --git a/gulp/task-build-config.js b/gulp/task-build-config.js new file mode 100644 index 000000000..98d380140 --- /dev/null +++ b/gulp/task-build-config.js @@ -0,0 +1,113 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +const gulp = require('gulp'); +const through = require('through'); +const bufferFrom = require('buffer-from'); +const rename = require('gulp-rename'); +const exec = require('child_process').exec; + +const LICENSE = '' + + '// (C) Copyright 2015 Moodle Pty Ltd.\n' + + '//\n' + + '// Licensed under the Apache License, Version 2.0 (the "License");\n' + + '// you may not use this file except in compliance with the License.\n' + + '// You may obtain a copy of the License at\n' + + '//\n' + + '// http://www.apache.org/licenses/LICENSE-2.0\n' + + '//\n' + + '// Unless required by applicable law or agreed to in writing, software\n' + + '// distributed under the License is distributed on an "AS IS" BASIS,\n' + + '// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n' + + '// See the License for the specific language governing permissions and\n' + + '// limitations under the License.\n\n'; + +/** + * Task to convert config.json into a TypeScript class. + */ +class BuildConfigTask { + + /** + * Run the task. + * + * @param path Path to the config file. + * @param done Function to call when done. + */ + run(path, done) { + // Get the last commit. + exec('git log -1 --pretty=format:"%H"', (err, commit, stderr) => { + if (err) { + console.error('An error occurred while getting the last commit: ' + err); + } else if (stderr) { + console.error('An error occurred while getting the last commit: ' + stderr); + } + + gulp.src(path) + .pipe(through(function(file) { + // Convert the contents of the file into a TypeScript class. + // Disable the rule variable-name in the file. + const config = JSON.parse(file.contents.toString()); + let contents = LICENSE + '// tslint:disable: variable-name\n' + 'export class CoreConfigConstants {\n'; + + for (let key in config) { + let value = config[key]; + + if (typeof value == 'string') { + // Wrap the string in ' and escape them. + value = "'" + value.replace(/([^\\])'/g, "$1\\'") + "'"; + } else if (typeof value != 'number' && typeof value != 'boolean') { + // Stringify with 4 spaces of indentation, and then add 4 more spaces in each line. + value = JSON.stringify(value, null, 4).replace(/^(?: )/gm, ' ').replace(/^(?:})/gm, ' }'); + // Replace " by ' in values. + value = value.replace(/: "([^"]*)"/g, ": '$1'"); + + // Check if the keys have "-" in it. + const matches = value.match(/"([^"]*\-[^"]*)":/g); + if (matches) { + // Replace " by ' in keys. We cannot remove them because keys have chars like '-'. + value = value.replace(/"([^"]*)":/g, "'$1':"); + } else { + // Remove ' in keys. + value = value.replace(/"([^"]*)":/g, "$1:"); + } + + // Add type any to the key. + key = key + ': any'; + } + + // If key has quotation marks, remove them. + if (key[0] == '"') { + key = key.substr(1, key.length - 2); + } + contents += ' static ' + key + ' = ' + value + ';\n'; + } + + // Add compilation info. + contents += ' static compilationtime = ' + Date.now() + ';\n'; + contents += ' static lastcommit = \'' + commit + '\';\n'; + + contents += '}\n'; + + file.contents = bufferFrom(contents); + + this.emit('data', file); + })) + .pipe(rename('configconstants.ts')) + .pipe(gulp.dest('./src')) + .on('end', done); + }); + } +} + +module.exports = BuildConfigTask; diff --git a/gulp/task-build-lang.js b/gulp/task-build-lang.js new file mode 100644 index 000000000..daa5d3126 --- /dev/null +++ b/gulp/task-build-lang.js @@ -0,0 +1,176 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +const gulp = require('gulp'); +const slash = require('gulp-slash'); +const clipEmptyFiles = require('gulp-clip-empty-files'); +const through = require('through'); +const bufferFrom = require('buffer-from'); +const File = require('vinyl'); +const pathLib = require('path'); + +/** + * Task to build the language files into a single file per language. + */ +class BuildLangTask { + + /** + * Copy a property from one object to another, adding a prefix to the key if needed. + * + * @param target Object to copy the properties to. + * @param source Object to copy the properties from. + * @param prefix Prefix to add to the keys. + */ + addProperties(target, source, prefix) { + for (let property in source) { + target[prefix + property] = source[property]; + } + } + + /** + * Run the task. + * + * @param language Language to treat. + * @param langPaths Paths to the possible language files. + * @param done Function to call when done. + */ + run(language, langPaths, done) { + const filename = language + '.json'; + const data = {}; + let firstFile = null; + const self = this; + + const paths = langPaths.map((path) => { + if (path.slice(-1) != '/') { + path = path + '/'; + } + + return path + language + '.json'; + }); + + gulp.src(paths, { allowEmpty: true }) + .pipe(slash()) + .pipe(clipEmptyFiles()) + .pipe(through(function(file) { + if (!firstFile) { + firstFile = file; + } + + return self.treatFile(file, data); + }, function() { + /* This implementation is based on gulp-jsoncombine module. + * https://github.com/reflog/gulp-jsoncombine */ + if (firstFile) { + const joinedPath = pathLib.join(firstFile.base, language + '.json'); + + const joinedFile = new File({ + cwd: firstFile.cwd, + base: firstFile.base, + path: joinedPath, + contents: self.treatMergedData(data), + }); + + this.emit('data', joinedFile); + } + + this.emit('end'); + })) + .pipe(gulp.dest(pathLib.join('./src/assets', 'lang'))) + .on('end', done); + } + + /** + * Treats a file to merge JSONs. This function is based on gulp-jsoncombine module. + * https://github.com/reflog/gulp-jsoncombine + * + * @param file File treated. + * @param data Object where to store the data. + */ + treatFile(file, data) { + if (file.isNull() || file.isStream()) { + return; // ignore + } + + try { + let srcPos = file.path.lastIndexOf('/src/'); + if (srcPos == -1) { + // It's probably a Windows environment. + srcPos = file.path.lastIndexOf('\\src\\'); + } + + const path = file.path.substr(srcPos + 5); + data[path] = JSON.parse(file.contents.toString()); + } catch (err) { + console.log('Error parsing JSON: ' + err); + } + } + + /** + * Treats the merged JSON data, adding prefixes depending on the component. + * + * @param data Merged data. + * @return Buffer with the treated data. + */ + treatMergedData(data) { + const merged = {}; + const mergedOrdered = {}; + + for (let filepath in data) { + const pathSplit = filepath.split(/[\/\\]/); + let prefix; + + pathSplit.pop(); + + switch (pathSplit[0]) { + case 'lang': + prefix = 'core'; + break; + case 'core': + if (pathSplit[1] == 'lang') { + // Not used right now. + prefix = 'core'; + } else { + prefix = 'core.' + pathSplit[1]; + } + break; + case 'addon': + // Remove final item 'lang'. + pathSplit.pop(); + // Remove first item 'addon'. + pathSplit.shift(); + + // For subplugins. We'll use plugin_subfolder_subfolder2_... + // E.g. 'mod_assign_feedback_comments'. + prefix = 'addon.' + pathSplit.join('_'); + break; + case 'assets': + prefix = 'assets.' + pathSplit[1]; + break; + } + + if (prefix) { + this.addProperties(merged, data[filepath], prefix + '.'); + } + } + + // Force ordering by string key. + Object.keys(merged).sort().forEach((key) => { + mergedOrdered[key] = merged[key]; + }); + + return bufferFrom(JSON.stringify(mergedOrdered, null, 4)); + } +} + +module.exports = BuildLangTask; diff --git a/gulp/task-combine-scss.js b/gulp/task-combine-scss.js new file mode 100644 index 000000000..0f0a28003 --- /dev/null +++ b/gulp/task-combine-scss.js @@ -0,0 +1,164 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +const gulp = require('gulp'); +const through = require('through'); +const bufferFrom = require('buffer-from'); +const concat = require('gulp-concat'); +const pathLib = require('path'); +const fs = require('fs'); + +/** + * Task to combine scss into a single file. + */ +class CombineScssTask { + + /** + * Finds the file and returns its content. + * + * @param capture Import file path. + * @param baseDir Directory where the file was found. + * @param paths Alternative paths where to find the imports. + * @param parsedFiles Already parsed files to reduce size of the result. + * @return Partially combined scss. + */ + getReplace(capture, baseDir, paths, parsedFiles) { + let parse = pathLib.parse(pathLib.resolve(baseDir, capture + '.scss')); + let file = parse.dir + '/' + parse.name; + + if (file.slice(-3) === '.wp') { + console.log('Windows Phone not supported "' + capture); + // File was already parsed, leave the import commented. + return '// @import "' + capture + '";'; + } + + if (!fs.existsSync(file + '.scss')) { + // File not found, might be a partial file. + file = parse.dir + '/_' + parse.name; + } + + // If file still not found, try to find the file in the alternative paths. + let x = 0; + while (!fs.existsSync(file + '.scss') && paths.length > x) { + parse = pathLib.parse(pathLib.resolve(paths[x], capture + '.scss')); + file = parse.dir + '/' + parse.name; + + x++; + } + + file = file + '.scss'; + + if (!fs.existsSync(file)) { + // File not found. Leave the import there. + console.log('File "' + capture + '" not found'); + return '@import "' + capture + '";'; + } + + if (parsedFiles.indexOf(file) >= 0) { + console.log('File "' + capture + '" already parsed'); + // File was already parsed, leave the import commented. + return '// @import "' + capture + '";'; + } + + parsedFiles.push(file); + const text = fs.readFileSync(file); + + // Recursive call. + return this.scssCombine(text, parse.dir, paths, parsedFiles); + } + + /** + * Run the task. + * + * @param done Function to call when done. + */ + run(done) { + const paths = [ + 'node_modules/ionic-angular/themes/', + 'node_modules/font-awesome/scss/', + 'node_modules/ionicons/dist/scss/' + ]; + const parsedFiles = []; + const self = this; + + gulp.src([ + './src/theme/variables.scss', + './node_modules/ionic-angular/themes/ionic.globals.*.scss', + './node_modules/ionic-angular/themes/ionic.components.scss', + './src/**/*.scss', + ]).pipe(through(function(file) { // Combine them based on @import and save it to stream. + if (file.isNull()) { + return; + } + + parsedFiles.push(file); + file.contents = bufferFrom(self.scssCombine( + file.contents, pathLib.dirname(file.path), paths, parsedFiles)); + + this.emit('data', file); + })).pipe(concat('combined.scss')) // Concat the stream output in single file. + .pipe(gulp.dest('.')) // Save file to destination. + .on('end', done); + } + + /** + * Combine scss files with its imports + * + * @param content Scss string to treat. + * @param baseDir Directory where the file was found. + * @param paths Alternative paths where to find the imports. + * @param parsedFiles Already parsed files to reduce size of the result. + * @return Scss string with the replaces done. + */ + scssCombine(content, baseDir, paths, parsedFiles) { + // Content is a Buffer, convert to string. + if (typeof content != "string") { + content = content.toString(); + } + + // Search of single imports. + let regex = /@import[ ]*['"](.*)['"][ ]*;/g; + + if (regex.test(content)) { + return content.replace(regex, (m, capture) => { + if (capture == "bmma") { + return m; + } + + return this.getReplace(capture, baseDir, paths, parsedFiles); + }); + } + + // Search of multiple imports. + regex = /@import(?:[ \n]+['"](.*)['"][,]?[ \n]*)+;/gm; + if (regex.test(content)) { + return content.replace(regex, (m, capture) => { + let text = ''; + + // Divide the import into multiple files. + const captures = m.match(/['"]([^'"]*)['"]/g); + + for (let x in captures) { + text += this.getReplace(captures[x].replace(/['"]+/g, ''), baseDir, paths, parsedFiles) + '\n'; + } + + return text; + }); + } + + return content; + } +} + +module.exports = CombineScssTask; diff --git a/gulp/task-copy-component-templates.js b/gulp/task-copy-component-templates.js new file mode 100644 index 000000000..2773a07b7 --- /dev/null +++ b/gulp/task-copy-component-templates.js @@ -0,0 +1,79 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +const fs = require('fs'); +const gulp = require('gulp'); +const flatten = require('gulp-flatten'); +const htmlmin = require('gulp-htmlmin'); +const pathLib = require('path'); + +const TEMPLATES_SRC = [ + './src/components/**/*.html', + './src/core/**/components/**/*.html', + './src/core/**/component/**/*.html', + // Copy all addon components because any component can be injected using extraImports. + './src/addon/**/components/**/*.html', + './src/addon/**/component/**/*.html' +]; +const TEMPLATES_DEST = './www/templates'; + +/** + * Task to copy component templates to www to make compile-html work in AOT. + */ +class CopyComponentTemplatesTask { + + /** + * Delete a folder and all its contents. + * + * @param path [description] + * @return {[type]} [description] + */ + deleteFolderRecursive(path) { + if (fs.existsSync(path)) { + fs.readdirSync(path).forEach((file) => { + var curPath = pathLib.join(path, file); + + if (fs.lstatSync(curPath).isDirectory()) { + this.deleteFolderRecursive(curPath); + } else { + fs.unlinkSync(curPath); + } + }); + + fs.rmdirSync(path); + } + } + + /** + * Run the task. + * + * @param done Callback to call once done. + */ + run(done) { + this.deleteFolderRecursive(TEMPLATES_DEST); + + gulp.src(TEMPLATES_SRC, { allowEmpty: true }) + .pipe(flatten()) + // Check options here: https://github.com/kangax/html-minifier + .pipe(htmlmin({ + collapseWhitespace: true, + removeComments: true, + caseSensitive: true + })) + .pipe(gulp.dest(TEMPLATES_DEST)) + .on('end', done); + } +} + +module.exports = CopyComponentTemplatesTask; diff --git a/gulp/task-push.js b/gulp/task-push.js new file mode 100644 index 000000000..72a697329 --- /dev/null +++ b/gulp/task-push.js @@ -0,0 +1,280 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +const gulp = require('gulp'); +const inquirer = require('inquirer'); +const DevConfig = require('./dev-config'); +const Git = require('./git'); +const Jira = require('./jira'); +const Utils = require('./utils'); + +/** + * Task to push a git branch and update tracker issue. + */ +class PushTask { + + /** + * Ask the user whether he wants to continue. + * + * @return Promise resolved with boolean: true if he wants to continue. + */ + async askConfirmContinue() { + const answer = await inquirer.prompt([ + { + type: 'input', + name: 'confirm', + message: 'Are you sure you want to continue?', + default: 'n', + }, + ]); + + return answer.confirm == 'y'; + } + + /** + * Push a patch to the tracker and remove the previous one. + * + * @param branch Branch name. + * @param branchData Parsed branch data. + * @param remote Remote used. + * @return Promise resolved when done. + */ + async pushPatch(branch, branchData, remote) { + const headCommit = await Git.getHeadCommit(branch, branchData); + + if (!headCommit) { + throw new Error('Head commit not resolved, abort pushing patch.'); + } + + // Create the patch file. + const fileName = branch + '.patch'; + const tmpPatchPath = `./tmp/${fileName}`; + + await Git.createPatch(`${headCommit}...${branch}`, tmpPatchPath); + console.log('Git patch created'); + + // Check if there is an attachment with same name in the issue. + const issue = await Jira.getIssue(branchData.issue, 'attachment'); + + let existingAttachmentId; + const attachments = (issue.fields && issue.fields.attachment) || []; + for (const i in attachments) { + if (attachments[i].filename == fileName) { + // Found an existing attachment with the same name, we keep track of it. + existingAttachmentId = attachments[i].id; + break + } + } + + // Push the patch to the tracker. + console.log(`Uploading patch ${fileName} to the tracker...`); + await Jira.upload(branchData.issue, tmpPatchPath); + + if (existingAttachmentId) { + // On success, deleting file that was there before. + try { + console.log('Deleting older patch...') + await Jira.deleteAttachment(existingAttachmentId); + } catch (error) { + console.log('Could not delete older attachment.'); + } + } + } + + /** + * Run the task. + * + * @param args Command line arguments. + * @param done Function to call when done. + */ + async run(args, done) { + try { + const remote = args.remote || DevConfig.get('upstreamRemote', 'origin'); + let branch = args.branch; + const force = !!args.force; + + if (!branch) { + branch = await Git.getCurrentBranch(); + } + + if (!branch) { + throw new Error('Cannot determine the current branch. Please make sure youu aren\'t in detached HEAD state'); + } else if (branch == 'HEAD') { + throw new Error('Cannot push HEAD branch'); + } + + // Parse the branch to get the project and issue number. + const branchData = Utils.parseBranch(branch); + const keepRunning = await this.validateCommitMessages(branchData); + + if (!keepRunning) { + // Last commit not valid, stop. + console.log('Exiting...'); + done(); + return; + } + + if (!args.patch) { + // Check if it's a security issue to force patch mode. + try { + args.patch = await Jira.isSecurityIssue(branchData.issue); + + if (args.patch) { + console.log(`${branchData.issue} appears to be a security issue, switching to patch mode...`); + } + } catch (error) { + console.log(`Could not check if ${branchData.issue} is a security issue.`); + } + } + + if (args.patch) { + // Create and upload a patch file. + await this.pushPatch(branch, branchData, remote); + } else { + // Push the branch. + console.log(`Pushing branch ${branch} to remote ${remote}...`); + await Git.push(remote, branch, force); + + // Update tracker info. + console.log(`Branch pushed, update tracker info...`); + await this.updateTrackerGitInfo(branch, branchData, remote); + } + } catch (error) { + console.error(error); + } + + done(); + } + + /** + * Update git info in the tracker issue. + * + * @param branch Branch name. + * @param branchData Parsed branch data. + * @param remote Remote used. + * @return Promise resolved when done. + */ + async updateTrackerGitInfo(branch, branchData, remote) { + // Get the repository data for the project. + let repositoryUrl = DevConfig.get(branchData.project + '.repositoryUrl'); + let diffUrlTemplate = DevConfig.get(branchData.project + '.diffUrlTemplate', ''); + let remoteUrl; + + if (!repositoryUrl) { + // Calculate the repositoryUrl based on the remote URL. + remoteUrl = await Git.getRemoteUrl(remote); + + repositoryUrl = remoteUrl.replace(/^https?:\/\//, 'git://'); + if (!repositoryUrl.match(/\.git$/)) { + repositoryUrl += '.git'; + } + } + + if (!diffUrlTemplate) { + // Calculate the diffUrlTemplate based on the remote URL. + if (!remoteUrl) { + remoteUrl = await Git.getRemoteUrl(remoteUrl); + } + + diffUrlTemplate = remoteUrl + '/compare/%headcommit%...%branch%'; + } + + // Search HEAD commit to put in the diff URL. + console.log ('Searching for head commit...'); + let headCommit = await Git.getHeadCommit(branch, branchData); + + if (!headCommit) { + throw new Error('Head commit not resolved, aborting update of tracker fields'); + } + + headCommit = headCommit.substr(0, 10); + console.log(`Head commit resolved to ${headCommit}`); + + // Calculate last properties needed. + const diffUrl = diffUrlTemplate.replace('%branch%', branch).replace('%headcommit%', headCommit); + const fieldRepositoryUrl = DevConfig.get('tracker.fieldnames.repositoryurl', 'Pull from Repository'); + const fieldBranch = DevConfig.get('tracker.fieldnames.branch', 'Pull Master Branch'); + const fieldDiffUrl = DevConfig.get('tracker.fieldnames.diffurl', 'Pull Master Diff URL'); + + // Update tracker fields. + const updates = {}; + updates[fieldRepositoryUrl] = repositoryUrl; + updates[fieldBranch] = branch; + updates[fieldDiffUrl] = diffUrl; + + console.log('Setting tracker fields...'); + await Jira.setCustomFields(branchData.issue, updates); + } + + /** + * Validate commit messages comparing them with the branch name. + * + * @param branchData Parsed branch data. + * @return True if value is ok or the user wants to continue anyway, false to stop. + */ + async validateCommitMessages(branchData) { + const messages = await Git.messages(30); + + let numConsecutive = 0; + let wrongCommitCandidate = null; + + for (let i = 0; i < messages.length; i++) { + const message = messages[i]; + const issue = Utils.getIssueFromCommitMessage(message); + + if (!issue || issue != branchData.issue) { + if (i === 0) { + // Last commit is wrong, it shouldn't happen. Ask the user if he wants to continue. + if (!issue) { + console.log('The issue number could not be found in the last commit message.'); + console.log(`Commit: ${message}`); + } else if (issue != branchData.issue) { + console.log('The issue number in the last commit does not match the branch being pushed to.'); + console.log(`Branch: ${branchData.issue} vs. commit: ${issue}`); + } + + return this.askConfirmContinue(); + } + + numConsecutive++; + if (numConsecutive > 2) { + // 3 consecutive commits with different branch, probably the branch commits are over. Everything OK. + return true; + } else if (!wrongCommitCandidate) { + wrongCommitCandidate = { + message: message, + issue: issue, + index: i, + }; + } + } else if (wrongCommitCandidate) { + // We've found a commit with the branch name after a commit with a different branch. Probably wrong commit. + if (!wrongCommitCandidate.issue) { + console.log('The issue number could not be found in one of the commit messages.'); + console.log(`Commit: ${wrongCommitCandidate.message}`); + } else { + console.log('The issue number in a certain commit does not match the branch being pushed to.'); + console.log(`Branch: ${branchData.issue} vs. commit: ${wrongCommitCandidate.issue}`); + console.log(`Commit message: ${wrongCommitCandidate.message}`); + } + + return this.askConfirmContinue(); + } + } + + return true; + } +} + +module.exports = PushTask; diff --git a/gulp/url.js b/gulp/url.js new file mode 100644 index 000000000..8b46409f6 --- /dev/null +++ b/gulp/url.js @@ -0,0 +1,79 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/** + * Class with helper functions for urls. + */ +class Url { + + /** + * Add params to a URL. + * + * @param url URL to add the params to. + * @param params Object with the params to add. + * @return URL with params. + */ + static addParamsToUrl(url, params) { + let separator = url.indexOf('?') != -1 ? '&' : '?'; + + for (const key in params) { + let value = params[key]; + + // Ignore objects. + if (typeof value != 'object') { + url += separator + key + '=' + value; + separator = '&'; + } + } + + return url; + } + + /** + * Parse parts of a url, using an implicit protocol if it is missing from the url. + * + * @param url Url. + * @return Url parts. + */ + static parse(url) { + // Parse url with regular expression taken from RFC 3986: https://tools.ietf.org/html/rfc3986#appendix-B. + const match = url.trim().match(/^(([^:/?#]+):)?(\/\/([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?/); + + if (!match) { + return null; + } + + const host = match[4] || ''; + + // Get the credentials and the port from the host. + const [domainAndPort, credentials] = host.split('@').reverse(); + const [domain, port] = domainAndPort.split(':'); + const [username, password] = credentials ? credentials.split(':') : []; + + // Prepare parts replacing empty strings with undefined. + return { + protocol: match[2] || undefined, + domain: domain || undefined, + port: port || undefined, + credentials: credentials || undefined, + username: username || undefined, + password: password || undefined, + path: match[5] || undefined, + query: match[7] || undefined, + fragment: match[9] || undefined, + }; + } +} + +module.exports = Url; diff --git a/gulp/utils.js b/gulp/utils.js new file mode 100644 index 000000000..db4a66413 --- /dev/null +++ b/gulp/utils.js @@ -0,0 +1,119 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +const DevConfig = require('./dev-config'); +const DEFAULT_ISSUE_REGEX = '(MOBILE)[-_]([0-9]+)'; + +/** + * Class with some utility functions. + */ +class Utils { + /** + * Concatenate several paths, adding a slash between them if needed. + * + * @param paths List of paths. + * @return Concatenated path. + */ + static concatenatePaths(paths) { + if (!paths.length) { + return ''; + } + + // Remove all slashes between paths. + for (let i = 0; i < paths.length; i++) { + if (!paths[i]) { + continue; + } + + if (i === 0) { + paths[i] = String(paths[i]).replace(/\/+$/g, ''); + } else if (i === paths.length - 1) { + paths[i] = String(paths[i]).replace(/^\/+/g, ''); + } else { + paths[i] = String(paths[i]).replace(/^\/+|\/+$/g, ''); + } + } + + // Remove empty paths. + paths = paths.filter(path => !!path); + + return paths.join('/'); + } + + /** + * Get command line arguments. + * + * @return Object with command line arguments. + */ + static getCommandLineArguments() { + + let args = {}, opt, thisOpt, curOpt; + for (let a = 0; a < process.argv.length; a++) { + + thisOpt = process.argv[a].trim(); + opt = thisOpt.replace(/^\-+/, ''); + + if (opt === thisOpt) { + // argument value + if (curOpt) { + args[curOpt] = opt; + } + curOpt = null; + } + else { + // Argument name. + curOpt = opt; + args[curOpt] = true; + } + } + + return args; + } + + /** + * Given a commit message, return the issue name (e.g. MOBILE-1234). + * + * @param commit Commit message. + * @return Issue name. + */ + static getIssueFromCommitMessage(commit) { + const regex = new RegExp(DevConfig.get('wording.branchRegex', DEFAULT_ISSUE_REGEX), 'i'); + const matches = commit.match(regex); + + return matches && matches[0]; + } + + /** + * Parse a branch name to extract some data. + * + * @param branch Branch name to parse. + * @return Data. + */ + static parseBranch(branch) { + const regex = new RegExp(DevConfig.get('wording.branchRegex', DEFAULT_ISSUE_REGEX), 'i'); + + const matches = branch.match(regex); + if (!matches || matches.length < 3) { + throw new Error(`Error parsing branch ${branch}`); + } + + return { + issue: matches[0], + project: matches[1], + issueNumber: matches[2], + }; + } +} + +module.exports = Utils; diff --git a/gulpfile.js b/gulpfile.js index fbeead754..dcc2afde8 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -1,437 +1,68 @@ -var gulp = require('gulp'), - fs = require('fs'), - through = require('through'), - rename = require('gulp-rename'), - path = require('path'), - slash = require('gulp-slash'), - clipEmptyFiles = require('gulp-clip-empty-files'), - File = require('vinyl'), - flatten = require('gulp-flatten'), - npmPath = require('path'), - concat = require('gulp-concat'), - htmlmin = require('gulp-htmlmin'), - bufferFrom = require('buffer-from'), - exec = require('child_process').exec, - license = '' + - '// (C) Copyright 2015 Moodle Pty Ltd.\n' + - '//\n' + - '// Licensed under the Apache License, Version 2.0 (the "License");\n' + - '// you may not use this file except in compliance with the License.\n' + - '// You may obtain a copy of the License at\n' + - '//\n' + - '// http://www.apache.org/licenses/LICENSE-2.0\n' + - '//\n' + - '// Unless required by applicable law or agreed to in writing, software\n' + - '// distributed under the License is distributed on an "AS IS" BASIS,\n' + - '// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n' + - '// See the License for the specific language governing permissions and\n' + - '// limitations under the License.\n\n'; +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. -/** - * Copy a property from one object to another, adding a prefix to the key if needed. - * @param {Object} target Object to copy the properties to. - * @param {Object} source Object to copy the properties from. - * @param {String} prefix Prefix to add to the keys. - */ -function addProperties(target, source, prefix) { - for (var property in source) { - target[prefix + property] = source[property]; - } -} +const BuildConfigTask = require('./gulp/task-build-config'); +const BuildLangTask = require('./gulp/task-build-lang'); +const CombineScssTask = require('./gulp/task-combine-scss'); +const CopyComponentTemplatesTask = require('./gulp/task-copy-component-templates'); +const PushTask = require('./gulp/task-push'); +const Utils = require('./gulp/utils'); +const gulp = require('gulp'); +const pathLib = require('path'); -/** - * Treats a file to merge JSONs. This function is based on gulp-jsoncombine module. - * https://github.com/reflog/gulp-jsoncombine - * @param {Object} file File treated. - */ -function treatFile(file, data) { - if (file.isNull() || file.isStream()) { - return; // ignore - } - try { - var srcPos = file.path.lastIndexOf('/src/'); - if (srcPos == -1) { - // It's probably a Windows environment. - srcPos = file.path.lastIndexOf('\\src\\'); - } +const paths = { + lang: [ + './src/lang/', + './src/core/**/lang/', + './src/addon/**/lang/', + './src/assets/countries/', + './src/assets/mimetypes/' + ], + config: './src/config.json', +}; - var path = file.path.substr(srcPos + 5); - data[path] = JSON.parse(file.contents.toString()); - } catch (err) { - console.log('Error parsing JSON: ' + err); - } -} - -/** - * Treats the merged JSON data, adding prefixes depending on the component. Used in lang tasks. - * - * @param {Object} data Merged data. - * @return {Buffer} Buffer with the treated data. - */ -function treatMergedData(data) { - var merged = {}; - var mergedOrdered = {}; - - for (var filepath in data) { - var pathSplit = filepath.split(/[\/\\]/), - prefix; - - pathSplit.pop(); - - switch (pathSplit[0]) { - case 'lang': - prefix = 'core'; - break; - case 'core': - if (pathSplit[1] == 'lang') { - // Not used right now. - prefix = 'core'; - } else { - prefix = 'core.' + pathSplit[1]; - } - break; - case 'addon': - // Remove final item 'lang'. - pathSplit.pop(); - // Remove first item 'addon'. - pathSplit.shift(); - - // For subplugins. We'll use plugin_subfolder_subfolder2_... - // E.g. 'mod_assign_feedback_comments'. - prefix = 'addon.' + pathSplit.join('_'); - break; - case 'assets': - prefix = 'assets.' + pathSplit[1]; - break; - } - - if (prefix) { - addProperties(merged, data[filepath], prefix + '.'); - } - } - - // Force ordering by string key. - Object.keys(merged).sort().forEach(function(k){ - mergedOrdered[k] = merged[k]; - }); - - return bufferFrom(JSON.stringify(mergedOrdered, null, 4)); -} - -/** - * Build lang file. - * - * @param {String} language Language to translate. - * @param {String[]} langPaths Paths to the possible language files. - * @param {String} buildDest Path where to leave the built files. - * @param {Function} done Function to call when done. - * @return {Void} - */ -function buildLang(language, langPaths, buildDest, done) { - var filename = language + '.json', - data = {}, - firstFile = null; - - var paths = langPaths.map(function(path) { - if (path.slice(-1) != '/') { - path = path + '/'; - } - return path + language + '.json'; - }); - - gulp.src(paths, { allowEmpty: true }) - .pipe(slash()) - .pipe(clipEmptyFiles()) - .pipe(through(function(file) { - if (!firstFile) { - firstFile = file; - } - return treatFile(file, data); - }, function() { - /* This implementation is based on gulp-jsoncombine module. - * https://github.com/reflog/gulp-jsoncombine */ - if (firstFile) { - var joinedPath = path.join(firstFile.base, language+'.json'); - - var joinedFile = new File({ - cwd: firstFile.cwd, - base: firstFile.base, - path: joinedPath, - contents: treatMergedData(data) - }); - - this.emit('data', joinedFile); - } - this.emit('end'); - })) - .pipe(gulp.dest(buildDest)) - .on('end', done); -} - -// Delete a folder and all its contents. -function deleteFolderRecursive(path) { - if (fs.existsSync(path)) { - fs.readdirSync(path).forEach(function(file) { - var curPath = npmPath.join(path, file); - if (fs.lstatSync(curPath).isDirectory()) { - deleteFolderRecursive(curPath); - } else { - fs.unlinkSync(curPath); - } - }); - - fs.rmdirSync(path); - } -} - -// List of app lang files. To be used only if cannot get it from filesystem. -var paths = { - src: './src', - assets: './src/assets', - lang: [ - './src/lang/', - './src/core/**/lang/', - './src/addon/**/lang/', - './src/assets/countries/', - './src/assets/mimetypes/' - ], - config: './src/config.json', - }; +const args = Utils.getCommandLineArguments(); // Build the language files into a single file per language. -gulp.task('lang', function(done) { - buildLang('en', paths.lang, path.join(paths.assets, 'lang'), done); +gulp.task('lang', (done) => { + new BuildLangTask().run('en', paths.lang, done); }); // Convert config.json into a TypeScript class. -gulp.task('config', function(done) { - // Get the last commit. - exec('git log -1 --pretty=format:"%H"', function (err, commit, stderr) { - if (err) { - console.error('An error occurred while getting the last commit: ' + err); - } else if (stderr) { - console.error('An error occurred while getting the last commit: ' + stderr); - } +gulp.task('config', (done) => { + new BuildConfigTask().run(paths.config, done); +}); - gulp.src(paths.config) - .pipe(through(function(file) { - // Convert the contents of the file into a TypeScript class. - // Disable the rule variable-name in the file. - var config = JSON.parse(file.contents.toString()), - contents = license + '// tslint:disable: variable-name\n' + 'export class CoreConfigConstants {\n', - that = this; +// Copy component templates to www to make compile-html work in AOT. +gulp.task('copy-component-templates', (done) => { + new CopyComponentTemplatesTask().run(done); +}); - for (var key in config) { - var value = config[key]; - if (typeof value == 'string') { - // Wrap the string in ' and scape them. - value = "'" + value.replace(/([^\\])'/g, "$1\\'") + "'"; - } else if (typeof value != 'number' && typeof value != 'boolean') { - // Stringify with 4 spaces of indentation, and then add 4 more spaces in each line. - value = JSON.stringify(value, null, 4).replace(/^(?: )/gm, ' ').replace(/^(?:})/gm, ' }'); - // Replace " by ' in values. - value = value.replace(/: "([^"]*)"/g, ": '$1'"); +// Combine SCSS files. +gulp.task('combine-scss', (done) => { + new CombineScssTask().run(done); +}); - // Check if the keys have "-" in it. - var matches = value.match(/"([^"]*\-[^"]*)":/g); - if (matches) { - // Replace " by ' in keys. We cannot remove them because keys have chars like '-'. - value = value.replace(/"([^"]*)":/g, "'$1':"); - } else { - // Remove ' in keys. - value = value.replace(/"([^"]*)":/g, "$1:"); - } - - // Add type any to the key. - key = key + ': any'; - } - - // If key has quotation marks, remove them. - if (key[0] == '"') { - key = key.substr(1, key.length - 2); - } - contents += ' static ' + key + ' = ' + value + ';\n'; - } - - // Add compilation info. - contents += ' static compilationtime = ' + Date.now() + ';\n'; - contents += ' static lastcommit = \'' + commit + '\';\n'; - - contents += '}\n'; - - file.contents = bufferFrom(contents); - this.emit('data', file); - })) - .pipe(rename('configconstants.ts')) - .pipe(gulp.dest(paths.src)) - .on('end', done); - }); +gulp.task('push', (done) => { + new PushTask().run(args, done); }); gulp.task('default', gulp.parallel('lang', 'config')); -gulp.task('watch', function() { - var langsPaths = paths.lang.map(function(path) { - return path + 'en.json'; - }); +gulp.task('watch', () => { + const langsPaths = paths.lang.map(path => path + 'en.json'); + gulp.watch(langsPaths, { interval: 500 }, gulp.parallel('lang')); gulp.watch(paths.config, { interval: 500 }, gulp.parallel('config')); }); - -var templatesSrc = [ - './src/components/**/*.html', - './src/core/**/components/**/*.html', - './src/core/**/component/**/*.html', - // Copy all addon components because any component can be injected using extraImports. - './src/addon/**/components/**/*.html', - './src/addon/**/component/**/*.html' - ], - templatesDest = './www/templates'; - -// Copy component templates to www to make compile-html work in AOT. -gulp.task('copy-component-templates', function(done) { - deleteFolderRecursive(templatesDest); - - gulp.src(templatesSrc, { allowEmpty: true }) - .pipe(flatten()) - // Check options here: https://github.com/kangax/html-minifier - .pipe(htmlmin({ - collapseWhitespace: true, - removeComments: true, - caseSensitive: true - })) - .pipe(gulp.dest(templatesDest)) - .on('end', done); -}); - -/** - * Finds the file and returns its content. - * - * @param {string} capture Import file path. - * @param {string} baseDir Directory where the file was found. - * @param {string} paths Alternative paths where to find the imports. - * @param {Array} parsedFiles Yet parsed files to reduce size of the result. - * @return {string} Partially combined scss. - */ -function getReplace(capture, baseDir, paths, parsedFiles) { - var parse = path.parse(path.resolve(baseDir, capture + '.scss')); - var file = parse.dir + '/' + parse.name; - - if (file.slice(-3) === '.wp') { - console.log('Windows Phone not supported "' + capture); - // File was already parsed, leave the import commented. - return '// @import "' + capture + '";'; - } - - if (!fs.existsSync(file + '.scss')) { - // File not found, might be a partial file. - file = parse.dir + '/_' + parse.name; - } - - // If file still not found, try to find the file in the alternative paths. - var x = 0; - while (!fs.existsSync(file + '.scss') && paths.length > x) { - parse = path.parse(path.resolve(paths[x], capture + '.scss')); - file = parse.dir + '/' + parse.name; - - x++; - } - - file = file + '.scss'; - - if (!fs.existsSync(file)) { - // File not found. Leave the import there. - console.log('File "' + capture + '" not found'); - return '@import "' + capture + '";'; - } - - if (parsedFiles.indexOf(file) >= 0) { - console.log('File "' + capture + '" already parsed'); - // File was already parsed, leave the import commented. - return '// @import "' + capture + '";'; - } - - parsedFiles.push(file); - var text = fs.readFileSync(file); - - // Recursive call. - return scssCombine(text, parse.dir, paths, parsedFiles); -} - -/** - * Combine scss files with its imports - * - * @param {string} content Scss string to read. - * @param {string} baseDir Directory where the file was found. - * @param {string} paths Alternative paths where to find the imports. - * @param {Array} parsedFiles Yet parsed files to reduce size of the result. - * @return {string} Scss string with the replaces done. - */ -function scssCombine(content, baseDir, paths, parsedFiles) { - - // Content is a Buffer, convert to string. - if (typeof content != "string") { - content = content.toString(); - } - - // Search of single imports. - var regex = /@import[ ]*['"](.*)['"][ ]*;/g; - - if (regex.test(content)) { - return content.replace(regex, function(m, capture) { - if (capture == "bmma") { - return m; - } - - return getReplace(capture, baseDir, paths, parsedFiles); - }); - } - - // Search of multiple imports. - regex = /@import(?:[ \n]+['"](.*)['"][,]?[ \n]*)+;/gm; - if (regex.test(content)) { - return content.replace(regex, function(m, capture) { - var text = ""; - - // Divide the import into multiple files. - regex = /['"]([^'"]*)['"]/g; - var captures = m.match(regex); - for (var x in captures) { - text += getReplace(captures[x].replace(/['"]+/g, ''), baseDir, paths, parsedFiles) + "\n"; - } - - return text; - }); - } - - return content; -} - -gulp.task('combine-scss', function(done) { - var paths = [ - 'node_modules/ionic-angular/themes/', - 'node_modules/font-awesome/scss/', - 'node_modules/ionicons/dist/scss/' - ]; - - var parsedFiles = []; - - gulp.src([ - './src/theme/variables.scss', - './node_modules/ionic-angular/themes/ionic.globals.*.scss', - './node_modules/ionic-angular/themes/ionic.components.scss', - './src/**/*.scss']) // define a source files - .pipe(through(function(file, encoding, callback) { - if (file.isNull()) { - return; - } - - parsedFiles.push(file); - file.contents = bufferFrom(scssCombine(file.contents, path.dirname(file.path), paths, parsedFiles)); - - this.emit('data', file); - })) // combine them based on @import and save it to stream - .pipe(concat('combined.scss')) // concat the stream output in single file - .pipe(gulp.dest('.')) // save file to destination. - .on('end', done); -}); diff --git a/package-lock.json b/package-lock.json index 7144ebf97..ff7dc336e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1632,8 +1632,7 @@ "@types/color-name": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", - "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", - "dev": true + "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==" }, "@types/cordova": { "version": "0.0.34", @@ -1869,9 +1868,12 @@ } }, "ansi-escapes": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", - "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==" + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.1.tgz", + "integrity": "sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA==", + "requires": { + "type-fest": "^0.11.0" + } }, "ansi-gray": { "version": "0.1.1", @@ -2262,9 +2264,9 @@ "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" }, "aws4": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", - "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==" + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.10.0.tgz", + "integrity": "sha512-3YDiu347mtVtjpyV3u5kVqQLP242c06zwDOgpeRnybmXlYYsLbtTrUBUm8i8srONt+FWobl5aibnU1030PeeuA==" }, "babel-code-frame": { "version": "6.26.0", @@ -2430,6 +2432,46 @@ "file-uri-to-path": "1.0.0" } }, + "bl": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.0.2.tgz", + "integrity": "sha512-j4OH8f6Qg2bGuWfRiltT2HYGx0e1QcBTrK9KAHNMwMZdQnDZFk0ZSYIpADjYCB3U12nicC5tVJwSIhwOWjb4RQ==", + "dev": true, + "requires": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + }, + "dependencies": { + "buffer": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.6.0.tgz", + "integrity": "sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==", + "dev": true, + "requires": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } + } + }, "block-stream": { "version": "0.0.9", "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", @@ -3973,6 +4015,15 @@ "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=" }, + "decompress-response": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", + "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", + "dev": true, + "requires": { + "mimic-response": "^2.0.0" + } + }, "dedent": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", @@ -5253,6 +5304,12 @@ "fill-range": "^2.1.0" } }, + "expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "dev": true + }, "expand-tilde": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", @@ -5309,8 +5366,7 @@ "extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "dev": true + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" }, "extend-shallow": { "version": "3.0.2", @@ -5711,9 +5767,9 @@ } }, "figures": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", - "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", "requires": { "escape-string-regexp": "^1.0.5" } @@ -6132,16 +6188,6 @@ "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" }, - "form-data": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - } - }, "formidable": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.2.tgz", @@ -6166,6 +6212,12 @@ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" }, + "fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true + }, "fs-extra": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-4.0.3.tgz", @@ -6216,6 +6268,18 @@ "requires": { "graceful-fs": "^4.1.11", "through2": "^2.0.3" + }, + "dependencies": { + "through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "requires": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + } } }, "fs.realpath": { @@ -6907,6 +6971,12 @@ } } }, + "github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4=", + "dev": true + }, "glob": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", @@ -8150,6 +8220,18 @@ "dev": true, "requires": { "through2": "~2.0.1" + }, + "dependencies": { + "through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "requires": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + } } }, "gulp-concat": { @@ -8161,6 +8243,18 @@ "concat-with-sourcemaps": "^1.0.0", "through2": "^2.0.0", "vinyl": "^2.0.0" + }, + "dependencies": { + "through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "requires": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + } } }, "gulp-flatten": { @@ -8171,6 +8265,18 @@ "requires": { "plugin-error": "^0.1.2", "through2": "^2.0.0" + }, + "dependencies": { + "through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "requires": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + } } }, "gulp-htmlmin": { @@ -8201,6 +8307,16 @@ "arr-union": "^3.1.0", "extend-shallow": "^3.0.2" } + }, + "through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "requires": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } } } }, @@ -8283,29 +8399,6 @@ "requires": { "ajv": "^6.5.5", "har-schema": "^2.0.0" - }, - "dependencies": { - "ajv": { - "version": "6.9.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.9.1.tgz", - "integrity": "sha512-XDN92U311aINL77ieWHmqCcNlwjoP5cHXDxIxbf2MaPYuCXOHS7gHH8jktxeK5omgd52XbSTX6a4Piwd1pQmzA==", - "requires": { - "fast-deep-equal": "^2.0.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "fast-deep-equal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", - "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=" - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" - } } }, "has": { @@ -8668,25 +8761,173 @@ } }, "inquirer": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.5.2.tgz", - "integrity": "sha512-cntlB5ghuB0iuO65Ovoi8ogLHiWGs/5yNrtUcKjFhSSiVeAIVpD7koaSU9RM8mpXw5YDi9RdYXGQMaOURB7ycQ==", + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.3.2.tgz", + "integrity": "sha512-DF4osh1FM6l0RJc5YWYhSDB6TawiBRlbV9Cox8MWlidU218Tb7fm3lQTULyUJDfJ0tjbzl0W4q651mrCCEM55w==", "requires": { - "ansi-escapes": "^3.2.0", - "chalk": "^2.4.2", - "cli-cursor": "^2.1.0", - "cli-width": "^2.0.0", + "ansi-escapes": "^4.2.1", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-width": "^3.0.0", "external-editor": "^3.0.3", - "figures": "^2.0.0", - "lodash": "^4.17.12", - "mute-stream": "0.0.7", - "run-async": "^2.2.0", - "rxjs": "^6.4.0", - "string-width": "^2.1.0", - "strip-ansi": "^5.1.0", + "figures": "^3.0.0", + "lodash": "^4.17.16", + "mute-stream": "0.0.8", + "run-async": "^2.4.0", + "rxjs": "^6.6.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", "through": "^2.3.6" }, "dependencies": { + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==" + }, + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "requires": { + "restore-cursor": "^3.1.0" + } + }, + "cli-width": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", + "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==" + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" + }, + "lodash": { + "version": "4.17.19", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", + "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==" + }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==" + }, + "onetime": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.0.tgz", + "integrity": "sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q==", + "requires": { + "mimic-fn": "^2.1.0" + } + }, + "restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "requires": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + } + }, + "rxjs": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.0.tgz", + "integrity": "sha512-3HMA8z/Oz61DUHe+SdOiQyzIf4tOx5oQHmMir7IZEu6TMqCLHT4LRcmNaUS0NwOz8VLvmmBduMsoaUvMaIiqzg==", + "requires": { + "tslib": "^1.9.0" + } + }, + "string-width": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + } + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "requires": { + "ansi-regex": "^5.0.0" + } + }, + "supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "insight": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/insight/-/insight-0.10.3.tgz", + "integrity": "sha512-YOncxSN6Omh+1Oqxt+OJAvJVMDKw7l6IEG0wT2cTMGxjsTcroOGW4IR926QDzxg/uZHcFZ2cZbckDWdZhc2pZw==", + "requires": { + "async": "^2.6.2", + "chalk": "^2.4.2", + "conf": "^1.4.0", + "inquirer": "^6.3.1", + "lodash.debounce": "^4.0.8", + "os-name": "^3.1.0", + "request": "^2.88.0", + "tough-cookie": "^3.0.1", + "uuid": "^3.3.2" + }, + "dependencies": { + "ansi-escapes": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", + "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==" + }, "ansi-regex": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", @@ -8702,20 +8943,67 @@ "supports-color": "^5.3.0" } }, + "figures": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", + "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=", + "requires": { + "escape-string-regexp": "^1.0.5" + } + }, + "inquirer": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.5.2.tgz", + "integrity": "sha512-cntlB5ghuB0iuO65Ovoi8ogLHiWGs/5yNrtUcKjFhSSiVeAIVpD7koaSU9RM8mpXw5YDi9RdYXGQMaOURB7ycQ==", + "requires": { + "ansi-escapes": "^3.2.0", + "chalk": "^2.4.2", + "cli-cursor": "^2.1.0", + "cli-width": "^2.0.0", + "external-editor": "^3.0.3", + "figures": "^2.0.0", + "lodash": "^4.17.12", + "mute-stream": "0.0.7", + "run-async": "^2.2.0", + "rxjs": "^6.4.0", + "string-width": "^2.1.0", + "strip-ansi": "^5.1.0", + "through": "^2.3.6" + } + }, "is-fullwidth-code-point": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" }, + "macos-release": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/macos-release/-/macos-release-2.3.0.tgz", + "integrity": "sha512-OHhSbtcviqMPt7yfw5ef5aghS2jzFVKEFyCJndQt2YpSQ9qRVSEv2axSJI1paVThEu+FFGs584h/1YhxjVqajA==" + }, "mute-stream": { "version": "0.0.7", "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=" }, + "os-name": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/os-name/-/os-name-3.1.0.tgz", + "integrity": "sha512-h8L+8aNjNcMpo/mAIBPn5PXCM16iyPGjHNWo6U1YO8sJTMHtEtyczI6QJnLoplswm6goopQkqc7OAnjhWcugVg==", + "requires": { + "macos-release": "^2.2.0", + "windows-release": "^3.1.0" + } + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + }, "rxjs": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.3.tgz", - "integrity": "sha512-wuYsAYYFdWTAnAaPoKGNhfpWwKZbJW+HgAJ+mImp+Epl7BG8oNWBCTyRM8gba9k4lk8BgWdoYm21Mo/RYhhbgA==", + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.0.tgz", + "integrity": "sha512-3HMA8z/Oz61DUHe+SdOiQyzIf4tOx5oQHmMir7IZEu6TMqCLHT4LRcmNaUS0NwOz8VLvmmBduMsoaUvMaIiqzg==", "requires": { "tslib": "^1.9.0" } @@ -8753,53 +9041,6 @@ "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" } } - } - } - }, - "insight": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/insight/-/insight-0.10.3.tgz", - "integrity": "sha512-YOncxSN6Omh+1Oqxt+OJAvJVMDKw7l6IEG0wT2cTMGxjsTcroOGW4IR926QDzxg/uZHcFZ2cZbckDWdZhc2pZw==", - "requires": { - "async": "^2.6.2", - "chalk": "^2.4.2", - "conf": "^1.4.0", - "inquirer": "^6.3.1", - "lodash.debounce": "^4.0.8", - "os-name": "^3.1.0", - "request": "^2.88.0", - "tough-cookie": "^3.0.1", - "uuid": "^3.3.2" - }, - "dependencies": { - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "macos-release": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/macos-release/-/macos-release-2.3.0.tgz", - "integrity": "sha512-OHhSbtcviqMPt7yfw5ef5aghS2jzFVKEFyCJndQt2YpSQ9qRVSEv2axSJI1paVThEu+FFGs584h/1YhxjVqajA==" - }, - "os-name": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/os-name/-/os-name-3.1.0.tgz", - "integrity": "sha512-h8L+8aNjNcMpo/mAIBPn5PXCM16iyPGjHNWo6U1YO8sJTMHtEtyczI6QJnLoplswm6goopQkqc7OAnjhWcugVg==", - "requires": { - "macos-release": "^2.2.0", - "windows-release": "^3.1.0" - } - }, - "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" }, "tough-cookie": { "version": "3.0.1", @@ -9139,11 +9380,6 @@ "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-2.0.0.tgz", "integrity": "sha1-IHurkWOEmcB7Kt8kCkGochADRXU=" }, - "is-promise": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", - "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=" - }, "is-redirect": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-redirect/-/is-redirect-1.0.0.tgz", @@ -9392,6 +9628,16 @@ "integrity": "sha1-h/zPrv/AtozRnVX2cilD+SnqNeo=", "dev": true }, + "keytar": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/keytar/-/keytar-6.0.1.tgz", + "integrity": "sha512-1Ihpf2tdM3sLwGMkYHXYhVC/hx5BDR7CWFL4IrBA3IDZo0xHhS2nM+tU9Y+u/U7okNfbVkwmKsieLkcWRMh93g==", + "dev": true, + "requires": { + "node-addon-api": "^3.0.0", + "prebuild-install": "5.3.4" + } + }, "kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -10382,6 +10628,12 @@ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==" }, + "mimic-response": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", + "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==", + "dev": true + }, "minimalistic-assert": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", @@ -10458,6 +10710,12 @@ "minimist": "^1.2.5" } }, + "mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "dev": true + }, "moment": { "version": "2.24.0", "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz", @@ -10521,6 +10779,12 @@ } } }, + "napi-build-utils": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", + "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==", + "dev": true + }, "native-run": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/native-run/-/native-run-1.0.0.tgz", @@ -10612,6 +10876,21 @@ "lower-case": "^1.1.1" } }, + "node-abi": { + "version": "2.18.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.18.0.tgz", + "integrity": "sha512-yi05ZoiuNNEbyT/xXfSySZE+yVnQW6fxPZuFbLyS1s6b5Kw3HzV2PHOM4XR+nsjzkHxByK+2Wg+yCQbe35l8dw==", + "dev": true, + "requires": { + "semver": "^5.4.1" + } + }, + "node-addon-api": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.0.0.tgz", + "integrity": "sha512-sSHCgWfJ+Lui/u+0msF3oyCgvdkhxDbkCS6Q8uiJquzOimkJBvX6hl5aSSA7DR1XbMpdM8r7phjcF63sF4rkKg==", + "dev": true + }, "node-gyp": { "version": "3.8.0", "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-3.8.0.tgz", @@ -10774,6 +11053,12 @@ } } }, + "noop-logger": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/noop-logger/-/noop-logger-0.1.1.tgz", + "integrity": "sha1-lKKxYzxPExdVMAfYlm/Q6EG2pMI=", + "dev": true + }, "nopt": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", @@ -11591,6 +11876,41 @@ "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", "dev": true }, + "prebuild-install": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-5.3.4.tgz", + "integrity": "sha512-AkKN+pf4fSEihjapLEEj8n85YIw/tN6BQqkhzbDc0RvEZGdkpJBGMUYx66AAMcPG2KzmPQS7Cm16an4HVBRRMA==", + "dev": true, + "requires": { + "detect-libc": "^1.0.3", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp": "^0.5.1", + "napi-build-utils": "^1.0.1", + "node-abi": "^2.7.0", + "noop-logger": "^0.1.1", + "npmlog": "^4.0.1", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^3.0.3", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0", + "which-pm-runs": "^1.0.0" + }, + "dependencies": { + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + } + } + }, "prelude-ls": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", @@ -11773,7 +12093,8 @@ "punycode": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", + "dev": true }, "q": { "version": "1.5.1", @@ -12100,6 +12421,18 @@ "remove-bom-buffer": "^3.0.0", "safe-buffer": "^5.1.0", "through2": "^2.0.3" + }, + "dependencies": { + "through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "requires": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + } } }, "remove-trailing-separator": { @@ -12144,9 +12477,9 @@ } }, "request": { - "version": "2.88.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", - "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", "requires": { "aws-sign2": "~0.7.0", "aws4": "^1.8.0", @@ -12155,7 +12488,7 @@ "extend": "~3.0.2", "forever-agent": "~0.6.1", "form-data": "~2.3.2", - "har-validator": "~5.1.0", + "har-validator": "~5.1.3", "http-signature": "~1.2.0", "is-typedarray": "~1.0.0", "isstream": "~0.1.2", @@ -12165,15 +12498,20 @@ "performance-now": "^2.1.0", "qs": "~6.5.2", "safe-buffer": "^5.1.2", - "tough-cookie": "~2.4.3", + "tough-cookie": "~2.5.0", "tunnel-agent": "^0.6.0", "uuid": "^3.3.2" }, "dependencies": { - "extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + "form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } } } }, @@ -12623,12 +12961,9 @@ "dev": true }, "run-async": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz", - "integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA=", - "requires": { - "is-promise": "^2.1.0" - } + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==" }, "rxjs": { "version": "5.5.12", @@ -13028,6 +13363,23 @@ } } }, + "simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "dev": true + }, + "simple-get": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.0.tgz", + "integrity": "sha512-bCR6cP+aTdScaQCnQKbPKtJOKDp/hj9EDLJo3Nw4y1QksqaovlW/bnptB6/c1e+qmNIDHRK+oXFDdEqBT8WzUA==", + "dev": true, + "requires": { + "decompress-response": "^4.2.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "simple-plist": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/simple-plist/-/simple-plist-1.0.0.tgz", @@ -13779,6 +14131,56 @@ "inherits": "2" } }, + "tar-fs": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.0.tgz", + "integrity": "sha512-9uW5iDvrIMCVpvasdFHW0wJPez0K4JnMZtsuIeDI7HyMGJNxmDZDOCQROr7lXyS+iL/QMpj07qcjGYTSdRFXUg==", + "dev": true, + "requires": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.0.0" + }, + "dependencies": { + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + } + } + }, + "tar-stream": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.1.3.tgz", + "integrity": "sha512-Z9yri56Dih8IaK8gncVPx4Wqt86NDmQTSh49XLZgjWpGZL9GK9HKParS2scqHCC4w6X9Gh2jwaU45V47XTKwVA==", + "dev": true, + "requires": { + "bl": "^4.0.1", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "dependencies": { + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } + } + }, "temp-file": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/temp-file/-/temp-file-3.1.3.tgz", @@ -13804,16 +14206,6 @@ "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" }, - "through2": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.3.tgz", - "integrity": "sha1-AARWmzfHx0ujnEPzzteNGtlBQL4=", - "dev": true, - "requires": { - "readable-stream": "^2.1.5", - "xtend": "~4.0.1" - } - }, "through2-filter": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/through2-filter/-/through2-filter-3.0.0.tgz", @@ -13822,6 +14214,18 @@ "requires": { "through2": "~2.0.0", "xtend": "~4.0.0" + }, + "dependencies": { + "through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "requires": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + } } }, "thunkify": { @@ -13970,15 +14374,34 @@ "dev": true, "requires": { "through2": "^2.0.3" + }, + "dependencies": { + "through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "requires": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + } } }, "tough-cookie": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", - "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", "requires": { - "psl": "^1.1.24", - "punycode": "^1.4.1" + "psl": "^1.1.28", + "punycode": "^2.1.1" + }, + "dependencies": { + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + } } }, "tree-kill": { @@ -14112,8 +14535,7 @@ "type-fest": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.11.0.tgz", - "integrity": "sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ==", - "dev": true + "integrity": "sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ==" }, "type-is": { "version": "1.6.16", @@ -14597,6 +15019,18 @@ "value-or-function": "^3.0.0", "vinyl": "^2.0.0", "vinyl-sourcemap": "^1.1.0" + }, + "dependencies": { + "through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "requires": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + } } }, "vinyl-sourcemap": { @@ -15479,6 +15913,12 @@ "integrity": "sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8=", "dev": true }, + "which-pm-runs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/which-pm-runs/-/which-pm-runs-1.0.0.tgz", + "integrity": "sha1-Zws6+8VS4LVd9rd4DKdGFfI60cs=", + "dev": true + }, "wide-align": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", diff --git a/package.json b/package.json index 9d33f3e83..255bc6425 100644 --- a/package.json +++ b/package.json @@ -121,6 +121,7 @@ "cordova-support-google-services": "^1.3.2", "es6-promise-plugin": "^4.2.2", "font-awesome": "^4.7.0", + "inquirer": "^7.3.2", "ionic-angular": "3.9.9", "ionicons": "3.0.0", "jszip": "^3.1.5", @@ -155,10 +156,12 @@ "gulp-htmlmin": "^5.0.1", "gulp-rename": "^2.0.0", "gulp-slash": "^1.1.3", + "keytar": "^6.0.1", "lodash.template": "^4.5.0", "minimist": "^1.2.5", "native-run": "^1.0.0", "node-loader": "^0.6.0", + "request": "^2.88.2", "through": "^2.3.8", "typescript": "~2.6.2", "vinyl": "^2.2.0",