commit
c77c620964
|
@ -46,3 +46,4 @@ e2e/build
|
||||||
!/desktop/assets/
|
!/desktop/assets/
|
||||||
!/desktop/electron.js
|
!/desktop/electron.js
|
||||||
src/configconstants.ts
|
src/configconstants.ts
|
||||||
|
.moodleapp-dev-config
|
||||||
|
|
|
@ -64,6 +64,7 @@ jobs:
|
||||||
- node --version
|
- node --version
|
||||||
- npm --version
|
- npm --version
|
||||||
- nvm --version
|
- nvm --version
|
||||||
|
- sudo apt-get install -y libsecret-1-dev > /dev/null
|
||||||
- npm ci
|
- npm ci
|
||||||
- npm install -g gulp
|
- npm install -g gulp
|
||||||
script: scripts/aot.sh
|
script: scripts/aot.sh
|
||||||
|
@ -83,6 +84,8 @@ jobs:
|
||||||
- ELECTRON_CACHE=$HOME/.cache/electron
|
- ELECTRON_CACHE=$HOME/.cache/electron
|
||||||
- ELECTRON_BUILDER_CACHE=$HOME/.cache/electron-builder
|
- ELECTRON_BUILDER_CACHE=$HOME/.cache/electron-builder
|
||||||
- BUILD_PLATFORM='linux'
|
- BUILD_PLATFORM='linux'
|
||||||
|
before_install:
|
||||||
|
- sudo apt-get install -y libsecret-1-dev > /dev/null
|
||||||
script: scripts/aot.sh
|
script: scripts/aot.sh
|
||||||
- stage: build
|
- stage: build
|
||||||
name: "Build MacOS"
|
name: "Build MacOS"
|
||||||
|
|
|
@ -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();
|
|
@ -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();
|
|
@ -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();
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
469
gulpfile.js
469
gulpfile.js
|
@ -1,437 +1,68 @@
|
||||||
var gulp = require('gulp'),
|
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||||
fs = require('fs'),
|
//
|
||||||
through = require('through'),
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
rename = require('gulp-rename'),
|
// you may not use this file except in compliance with the License.
|
||||||
path = require('path'),
|
// You may obtain a copy of the License at
|
||||||
slash = require('gulp-slash'),
|
//
|
||||||
clipEmptyFiles = require('gulp-clip-empty-files'),
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
File = require('vinyl'),
|
//
|
||||||
flatten = require('gulp-flatten'),
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
npmPath = require('path'),
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
concat = require('gulp-concat'),
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
htmlmin = require('gulp-htmlmin'),
|
// See the License for the specific language governing permissions and
|
||||||
bufferFrom = require('buffer-from'),
|
// limitations under the License.
|
||||||
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';
|
|
||||||
|
|
||||||
/**
|
const BuildConfigTask = require('./gulp/task-build-config');
|
||||||
* Copy a property from one object to another, adding a prefix to the key if needed.
|
const BuildLangTask = require('./gulp/task-build-lang');
|
||||||
* @param {Object} target Object to copy the properties to.
|
const CombineScssTask = require('./gulp/task-combine-scss');
|
||||||
* @param {Object} source Object to copy the properties from.
|
const CopyComponentTemplatesTask = require('./gulp/task-copy-component-templates');
|
||||||
* @param {String} prefix Prefix to add to the keys.
|
const PushTask = require('./gulp/task-push');
|
||||||
*/
|
const Utils = require('./gulp/utils');
|
||||||
function addProperties(target, source, prefix) {
|
const gulp = require('gulp');
|
||||||
for (var property in source) {
|
const pathLib = require('path');
|
||||||
target[prefix + property] = source[property];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
const paths = {
|
||||||
* Treats a file to merge JSONs. This function is based on gulp-jsoncombine module.
|
lang: [
|
||||||
* https://github.com/reflog/gulp-jsoncombine
|
'./src/lang/',
|
||||||
* @param {Object} file File treated.
|
'./src/core/**/lang/',
|
||||||
*/
|
'./src/addon/**/lang/',
|
||||||
function treatFile(file, data) {
|
'./src/assets/countries/',
|
||||||
if (file.isNull() || file.isStream()) {
|
'./src/assets/mimetypes/'
|
||||||
return; // ignore
|
],
|
||||||
}
|
config: './src/config.json',
|
||||||
try {
|
};
|
||||||
var srcPos = file.path.lastIndexOf('/src/');
|
|
||||||
if (srcPos == -1) {
|
|
||||||
// It's probably a Windows environment.
|
|
||||||
srcPos = file.path.lastIndexOf('\\src\\');
|
|
||||||
}
|
|
||||||
|
|
||||||
var path = file.path.substr(srcPos + 5);
|
const args = Utils.getCommandLineArguments();
|
||||||
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',
|
|
||||||
};
|
|
||||||
|
|
||||||
// Build the language files into a single file per language.
|
// Build the language files into a single file per language.
|
||||||
gulp.task('lang', function(done) {
|
gulp.task('lang', (done) => {
|
||||||
buildLang('en', paths.lang, path.join(paths.assets, 'lang'), done);
|
new BuildLangTask().run('en', paths.lang, done);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Convert config.json into a TypeScript class.
|
// Convert config.json into a TypeScript class.
|
||||||
gulp.task('config', function(done) {
|
gulp.task('config', (done) => {
|
||||||
// Get the last commit.
|
new BuildConfigTask().run(paths.config, done);
|
||||||
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.src(paths.config)
|
// Copy component templates to www to make compile-html work in AOT.
|
||||||
.pipe(through(function(file) {
|
gulp.task('copy-component-templates', (done) => {
|
||||||
// Convert the contents of the file into a TypeScript class.
|
new CopyComponentTemplatesTask().run(done);
|
||||||
// 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;
|
|
||||||
|
|
||||||
for (var key in config) {
|
// Combine SCSS files.
|
||||||
var value = config[key];
|
gulp.task('combine-scss', (done) => {
|
||||||
if (typeof value == 'string') {
|
new CombineScssTask().run(done);
|
||||||
// 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'");
|
|
||||||
|
|
||||||
// Check if the keys have "-" in it.
|
gulp.task('push', (done) => {
|
||||||
var matches = value.match(/"([^"]*\-[^"]*)":/g);
|
new PushTask().run(args, done);
|
||||||
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('default', gulp.parallel('lang', 'config'));
|
gulp.task('default', gulp.parallel('lang', 'config'));
|
||||||
|
|
||||||
gulp.task('watch', function() {
|
gulp.task('watch', () => {
|
||||||
var langsPaths = paths.lang.map(function(path) {
|
const langsPaths = paths.lang.map(path => path + 'en.json');
|
||||||
return path + 'en.json';
|
|
||||||
});
|
|
||||||
gulp.watch(langsPaths, { interval: 500 }, gulp.parallel('lang'));
|
gulp.watch(langsPaths, { interval: 500 }, gulp.parallel('lang'));
|
||||||
gulp.watch(paths.config, { interval: 500 }, gulp.parallel('config'));
|
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);
|
|
||||||
});
|
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -121,6 +121,7 @@
|
||||||
"cordova-support-google-services": "^1.3.2",
|
"cordova-support-google-services": "^1.3.2",
|
||||||
"es6-promise-plugin": "^4.2.2",
|
"es6-promise-plugin": "^4.2.2",
|
||||||
"font-awesome": "^4.7.0",
|
"font-awesome": "^4.7.0",
|
||||||
|
"inquirer": "^7.3.2",
|
||||||
"ionic-angular": "3.9.9",
|
"ionic-angular": "3.9.9",
|
||||||
"ionicons": "3.0.0",
|
"ionicons": "3.0.0",
|
||||||
"jszip": "^3.1.5",
|
"jszip": "^3.1.5",
|
||||||
|
@ -155,10 +156,12 @@
|
||||||
"gulp-htmlmin": "^5.0.1",
|
"gulp-htmlmin": "^5.0.1",
|
||||||
"gulp-rename": "^2.0.0",
|
"gulp-rename": "^2.0.0",
|
||||||
"gulp-slash": "^1.1.3",
|
"gulp-slash": "^1.1.3",
|
||||||
|
"keytar": "^6.0.1",
|
||||||
"lodash.template": "^4.5.0",
|
"lodash.template": "^4.5.0",
|
||||||
"minimist": "^1.2.5",
|
"minimist": "^1.2.5",
|
||||||
"native-run": "^1.0.0",
|
"native-run": "^1.0.0",
|
||||||
"node-loader": "^0.6.0",
|
"node-loader": "^0.6.0",
|
||||||
|
"request": "^2.88.2",
|
||||||
"through": "^2.3.8",
|
"through": "^2.3.8",
|
||||||
"typescript": "~2.6.2",
|
"typescript": "~2.6.2",
|
||||||
"vinyl": "^2.2.0",
|
"vinyl": "^2.2.0",
|
||||||
|
|
Loading…
Reference in New Issue