1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-21 09:38:01 +02:00
joplin/packages/tools/tool-utils.ts

466 lines
12 KiB
TypeScript
Raw Normal View History

2021-01-18 16:37:27 +02:00
import * as fs from 'fs-extra';
import { readCredentialFile } from '@joplin/lib/utils/credentialFiles';
2021-01-18 16:37:27 +02:00
const fetch = require('node-fetch');
2020-11-06 20:45:45 +02:00
const execa = require('execa');
const { splitCommandString } = require('@joplin/lib/string-utils');
const moment = require('moment');
2021-01-18 16:37:27 +02:00
function quotePath(path: string) {
if (!path) return '';
if (path.indexOf('"') < 0 && path.indexOf(' ') < 0) return path;
path = path.replace(/"/, '\\"');
return `"${path}"`;
}
function commandToString(commandName: string, args: string[] = []) {
const output = [quotePath(commandName)];
for (const arg of args) {
output.push(quotePath(arg));
}
return output.join(' ');
}
2021-05-14 17:17:02 +02:00
async function insertChangelog(tag: string, changelogPath: string, changelog: string, isPrerelease: boolean) {
const currentText = await fs.readFile(changelogPath, 'UTF-8');
const lines = currentText.split('\n');
const beforeLines = [];
const afterLines = [];
for (const line of lines) {
if (afterLines.length) {
afterLines.push(line);
continue;
}
if (line.indexOf('##') === 0) {
afterLines.push(line);
continue;
}
beforeLines.push(line);
}
const header = [
'##',
`[${tag}](https://github.com/laurent22/joplin/releases/tag/${tag})`,
];
2021-05-14 17:17:02 +02:00
if (isPrerelease) header.push('(Pre-release)');
header.push('-');
// eslint-disable-next-line no-useless-escape
header.push(`${moment.utc().format('YYYY-MM-DD\THH:mm:ss')}Z`);
let newLines = [];
newLines.push(header.join(' '));
newLines.push('');
newLines = newLines.concat(changelog.split('\n'));
newLines.push('');
const output = beforeLines.concat(newLines).concat(afterLines);
return output.join('\n');
}
2021-05-14 17:17:02 +02:00
export async function completeReleaseWithChangelog(changelogPath: string, newVersion: string, newTag: string, appName: string, isPreRelease: boolean) {
const changelog = (await execCommand2(`node ${rootDir}/packages/tools/git-changelog ${newTag} --publish-format full`, { })).trim();
2021-05-14 17:17:02 +02:00
const newChangelog = await insertChangelog(newTag, changelogPath, changelog, isPreRelease);
await fs.writeFile(changelogPath, newChangelog);
const finalCmds = [
'git pull',
'git add -A',
`git commit -m "${appName} ${newVersion}"`,
`git tag "${newTag}"`,
'git push',
'git push --tags',
];
console.info('');
console.info('Verify that the changelog is correct:');
console.info('');
console.info(`${process.env.EDITOR} "${changelogPath}"`);
console.info('');
console.info('Then run these commands:');
console.info('');
console.info(finalCmds.join(' && '));
}
2021-01-18 16:37:27 +02:00
async function loadGitHubUsernameCache() {
const path = `${__dirname}/github_username_cache.json`;
if (await fs.pathExists(path)) {
const jsonString = await fs.readFile(path, 'utf8');
return JSON.parse(jsonString);
}
return {};
}
async function saveGitHubUsernameCache(cache: any) {
const path = `${__dirname}/github_username_cache.json`;
await fs.writeFile(path, JSON.stringify(cache));
}
// Returns the project root dir
export const rootDir = require('path').dirname(require('path').dirname(__dirname));
2017-12-04 20:16:14 +02:00
export function execCommand(command: string, options: any = null): Promise<string> {
options = options || {};
const exec = require('child_process').exec;
2017-12-04 20:16:14 +02:00
return new Promise((resolve, reject) => {
exec(command, options, (error: any, stdout: any, stderr: any) => {
2017-12-04 20:16:14 +02:00
if (error) {
if (error.signal == 'SIGTERM') {
resolve('Process was killed');
} else {
reject(error);
}
} else {
resolve([stdout.trim(), stderr.trim()].join('\n'));
2017-12-04 20:16:14 +02:00
}
});
});
2020-11-06 20:45:45 +02:00
}
2021-01-18 16:37:27 +02:00
export function resolveRelativePathWithinDir(baseDir: string, ...relativePath: string[]) {
const path = require('path');
const resolvedBaseDir = path.resolve(baseDir);
const resolvedPath = path.resolve(baseDir, ...relativePath);
if (resolvedPath.indexOf(resolvedBaseDir) !== 0) throw new Error(`Resolved path for relative path "${JSON.stringify(relativePath)}" is not within base directory "${baseDir}" (Was resolved to ${resolvedPath})`);
return resolvedPath;
2021-01-18 16:37:27 +02:00
}
2021-01-18 16:37:27 +02:00
export function execCommandVerbose(commandName: string, args: string[] = []) {
2020-11-06 20:45:45 +02:00
console.info(`> ${commandToString(commandName, args)}`);
const promise = execa(commandName, args);
promise.stdout.pipe(process.stdout);
return promise;
2021-01-18 16:37:27 +02:00
}
interface ExecCommandOptions {
showInput?: boolean;
showOutput?: boolean;
quiet?: boolean;
}
2020-11-06 20:45:45 +02:00
// There's lot of execCommandXXX functions, but eventually all scripts should
// use the one below, which supports:
//
// - Printing the command being executed
// - Printing the output in real time (piping to stdout)
// - Returning the command result as string
2021-01-18 16:37:27 +02:00
export async function execCommand2(command: string | string[], options: ExecCommandOptions = null): Promise<string> {
options = {
showInput: true,
showOutput: true,
2021-01-18 16:37:27 +02:00
quiet: false,
...options,
};
2021-01-18 16:37:27 +02:00
if (options.quiet) {
options.showInput = false;
options.showOutput = false;
}
if (options.showInput) {
if (typeof command === 'string') {
console.info(`> ${command}`);
} else {
console.info(`> ${commandToString(command[0], command.slice(1))}`);
}
}
const args: string[] = typeof command === 'string' ? splitCommandString(command) : command as string[];
const executableName = args[0];
args.splice(0, 1);
const promise = execa(executableName, args);
if (options.showOutput) promise.stdout.pipe(process.stdout);
const result = await promise;
2021-01-18 16:37:27 +02:00
return result.stdout.trim();
}
2021-01-18 16:37:27 +02:00
export function execCommandWithPipes(executable: string, args: string[]) {
const spawn = require('child_process').spawn;
return new Promise((resolve, reject) => {
const child = spawn(executable, args, { stdio: 'inherit' });
2021-01-18 16:37:27 +02:00
child.on('error', (error: any) => {
reject(error);
});
2021-01-18 16:37:27 +02:00
child.on('close', (code: any) => {
if (code !== 0) {
reject(`Ended with code ${code}`);
} else {
2021-01-18 16:37:27 +02:00
resolve(null);
}
});
2017-12-04 20:16:14 +02:00
});
2021-01-18 16:37:27 +02:00
}
2017-12-04 20:16:14 +02:00
2021-01-18 16:37:27 +02:00
export function toSystemSlashes(path: string) {
2020-11-06 20:45:45 +02:00
const os = process.platform;
if (os === 'win32') return path.replace(/\//g, '\\');
return path.replace(/\\/g, '/');
2021-01-18 16:37:27 +02:00
}
2020-11-06 20:45:45 +02:00
2021-01-18 16:37:27 +02:00
export async function setPackagePrivateField(filePath: string, value: any) {
2020-12-28 00:22:29 +02:00
const text = await fs.readFile(filePath, 'utf8');
const obj = JSON.parse(text);
if (!value) {
delete obj.private;
} else {
obj.private = true;
}
await fs.writeFile(filePath, JSON.stringify(obj, null, 2), 'utf8');
2021-01-18 16:37:27 +02:00
}
2020-12-28 00:22:29 +02:00
2021-01-18 16:37:27 +02:00
export async function downloadFile(url: string, targetPath: string) {
2017-12-04 20:16:14 +02:00
const https = require('https');
const fs = require('fs');
return new Promise((resolve, reject) => {
const file = fs.createWriteStream(targetPath);
2021-01-18 16:37:27 +02:00
https.get(url, function(response: any) {
2019-09-19 23:51:18 +02:00
if (response.statusCode !== 200) reject(new Error(`HTTP error ${response.statusCode}`));
2017-12-04 20:16:14 +02:00
response.pipe(file);
file.on('finish', function() {
2019-10-09 21:35:13 +02:00
// file.close();
2021-01-18 16:37:27 +02:00
resolve(null);
2017-12-04 20:16:14 +02:00
});
2021-01-18 16:37:27 +02:00
}).on('error', (error: any) => {
2017-12-04 20:16:14 +02:00
reject(error);
});
});
2021-01-18 16:37:27 +02:00
}
2017-12-04 20:16:14 +02:00
2021-01-18 16:37:27 +02:00
export function fileSha256(filePath: string) {
2017-12-04 20:16:14 +02:00
return new Promise((resolve, reject) => {
const crypto = require('crypto');
const fs = require('fs');
const algo = 'sha256';
const shasum = crypto.createHash(algo);
const s = fs.ReadStream(filePath);
2021-01-18 16:37:27 +02:00
s.on('data', function(d: any) { shasum.update(d); });
2017-12-04 20:16:14 +02:00
s.on('end', function() {
const d = shasum.digest('hex');
resolve(d);
});
2021-01-18 16:37:27 +02:00
s.on('error', function(error: any) {
2017-12-04 20:16:14 +02:00
reject(error);
});
});
2021-01-18 16:37:27 +02:00
}
2017-12-04 20:16:14 +02:00
2021-01-18 16:37:27 +02:00
export async function unlinkForce(filePath: string) {
2017-12-04 20:16:14 +02:00
const fs = require('fs-extra');
try {
await fs.unlink(filePath);
} catch (error) {
if (error.code === 'ENOENT') return;
throw error;
}
2021-01-18 16:37:27 +02:00
}
2017-12-04 20:16:14 +02:00
2021-01-18 16:37:27 +02:00
export function fileExists(filePath: string) {
2018-10-13 12:09:03 +02:00
const fs = require('fs-extra');
2017-12-04 20:16:14 +02:00
return new Promise((resolve, reject) => {
2021-01-18 16:37:27 +02:00
fs.stat(filePath, function(err: any) {
2017-12-04 20:16:14 +02:00
if (err == null) {
resolve(true);
2019-10-09 21:35:13 +02:00
} else if (err.code == 'ENOENT') {
2017-12-04 20:16:14 +02:00
resolve(false);
} else {
reject(err);
}
});
});
2021-01-18 16:37:27 +02:00
}
2021-01-18 16:37:27 +02:00
export async function gitRepoClean(): Promise<boolean> {
const output = await execCommand2('git status --porcelain', { quiet: true });
return !output.trim();
}
2021-01-18 16:37:27 +02:00
export async function gitRepoCleanTry() {
if (!(await gitRepoClean())) throw new Error(`There are pending changes in the repository: ${process.cwd()}`);
}
export async function gitPullTry(ignoreIfNotBranch = true) {
try {
2021-01-18 16:37:27 +02:00
await execCommand('git pull');
} catch (error) {
if (ignoreIfNotBranch && error.message.includes('no tracking information for the current branch')) {
console.info('Skipping git pull because no tracking information on current branch');
} else {
throw error;
}
}
2021-01-18 16:37:27 +02:00
}
2021-01-18 16:37:27 +02:00
export async function githubUsername(email: string, name: string) {
const cache = await loadGitHubUsernameCache();
2020-01-25 00:43:55 +02:00
const cacheKey = `${email}:${name}`;
if (cacheKey in cache) return cache[cacheKey];
let output = null;
2021-01-18 16:37:27 +02:00
const oauthToken = await githubOauthToken();
2020-01-25 00:43:55 +02:00
const urlsToTry = [
`https://api.github.com/search/users?q=${encodeURI(email)}+in:email`,
// Note that this can fail if the email could not be found and the user
// shares a name with someone else. It's rare enough that we can leave
// it for now.
// https://github.com/laurent22/joplin/pull/5390
2020-01-25 00:43:55 +02:00
`https://api.github.com/search/users?q=user:${encodeURI(name)}`,
];
for (const url of urlsToTry) {
const response = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': `token ${oauthToken}`,
},
});
const responseText = await response.text();
2020-01-25 00:43:55 +02:00
if (!response.ok) continue;
const responseJson = JSON.parse(responseText);
2020-01-25 00:43:55 +02:00
if (!responseJson || !responseJson.items || responseJson.items.length !== 1) continue;
output = responseJson.items[0].login;
break;
}
2020-01-25 00:43:55 +02:00
cache[cacheKey] = output;
await saveGitHubUsernameCache(cache);
2020-01-25 00:43:55 +02:00
return output;
2021-01-18 16:37:27 +02:00
}
2021-01-18 16:37:27 +02:00
export function patreonOauthToken() {
return readCredentialFile('patreon_oauth_token.txt');
}
2021-01-18 16:37:27 +02:00
export function githubOauthToken() {
return readCredentialFile('github_oauth_token.txt');
}
2021-01-18 16:37:27 +02:00
export async function githubRelease(project: string, tagName: string, options: any = null) {
2019-01-12 00:07:23 +02:00
options = Object.assign({}, {
isDraft: false,
isPreRelease: false,
}, options);
2021-01-18 16:37:27 +02:00
const oauthToken = await githubOauthToken();
2019-09-19 23:51:18 +02:00
const response = await fetch(`https://api.github.com/repos/laurent22/${project}/releases`, {
method: 'POST',
body: JSON.stringify({
tag_name: tagName,
name: tagName,
2019-01-12 00:07:23 +02:00
draft: options.isDraft,
prerelease: options.isPreRelease,
}),
headers: {
'Content-Type': 'application/json',
2019-09-19 23:51:18 +02:00
'Authorization': `token ${oauthToken}`,
},
});
const responseText = await response.text();
2019-09-19 23:51:18 +02:00
if (!response.ok) throw new Error(`Cannot create GitHub release: ${responseText}`);
const responseJson = JSON.parse(responseText);
2019-09-19 23:51:18 +02:00
if (!responseJson.url) throw new Error(`No URL for release: ${responseText}`);
return responseJson;
2021-01-18 16:37:27 +02:00
}
2021-01-18 16:37:27 +02:00
export function readline(question: string) {
return new Promise((resolve) => {
2019-06-15 19:58:09 +02:00
const readline = require('readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
2019-06-15 19:58:09 +02:00
});
2021-01-18 16:37:27 +02:00
rl.question(`${question} `, (answer: string) => {
2019-06-15 19:58:09 +02:00
resolve(answer);
rl.close();
});
});
2021-01-18 16:37:27 +02:00
}
2019-06-15 19:58:09 +02:00
2021-01-18 16:37:27 +02:00
export function isLinux() {
return process && process.platform === 'linux';
2021-01-18 16:37:27 +02:00
}
2021-01-18 16:37:27 +02:00
export function isWindows() {
return process && process.platform === 'win32';
2021-01-18 16:37:27 +02:00
}
2021-01-18 16:37:27 +02:00
export function isMac() {
return process && process.platform === 'darwin';
2021-01-18 16:37:27 +02:00
}
2021-01-18 16:37:27 +02:00
export async function insertContentIntoFile(filePath: string, markerOpen: string, markerClose: string, contentToInsert: string) {
2019-07-18 19:36:29 +02:00
const fs = require('fs-extra');
let content = await fs.readFile(filePath, 'utf-8');
// [^]* matches any character including new lines
2019-09-19 23:51:18 +02:00
const regex = new RegExp(`${markerOpen}[^]*?${markerClose}`);
2019-07-18 19:36:29 +02:00
content = content.replace(regex, markerOpen + contentToInsert + markerClose);
await fs.writeFile(filePath, content);
2021-01-18 16:37:27 +02:00
}
2019-07-18 19:36:29 +02:00
2021-01-18 16:37:27 +02:00
export function dirname(path: string) {
2020-11-05 18:58:23 +02:00
if (!path) throw new Error('Path is empty');
const s = path.split(/\/|\\/);
s.pop();
return s.join('/');
2021-01-18 16:37:27 +02:00
}
2020-11-05 18:58:23 +02:00
2021-01-18 16:37:27 +02:00
export function basename(path: string) {
2020-11-05 18:58:23 +02:00
if (!path) throw new Error('Path is empty');
const s = path.split(/\/|\\/);
return s[s.length - 1];
2021-01-18 16:37:27 +02:00
}
2020-11-05 18:58:23 +02:00
2021-01-18 16:37:27 +02:00
export function filename(path: string, includeDir = false) {
2020-11-05 18:58:23 +02:00
if (!path) throw new Error('Path is empty');
2021-01-18 16:37:27 +02:00
const output = includeDir ? path : basename(path);
2020-11-05 18:58:23 +02:00
if (output.indexOf('.') < 0) return output;
const splitted = output.split('.');
splitted.pop();
return splitted.join('.');
2021-01-18 16:37:27 +02:00
}
2020-11-05 18:58:23 +02:00
2021-01-18 16:37:27 +02:00
export function fileExtension(path: string) {
2020-11-05 18:58:23 +02:00
if (!path) throw new Error('Path is empty');
const output = path.split('.');
if (output.length <= 1) return '';
return output[output.length - 1];
2021-01-18 16:37:27 +02:00
}