etherpad-lite/bin/plugins/checkPlugin.js
John McLear 2fdc737355
bugfix, lint and refactor all bin scripts (#4617)
* bugfix, lint and refactor all bin scripts

* for squash: throw Error(message) rather than log(message); throw Error()

* for squash: Exit non-0 on unhandled Promise rejection

Many of the recent lint changes have converted normal functions to
async functions, and an error thrown in an async function does not
cause Node.js to exit by default.

* for squash: fix `require()` paths

* for squash: remove erroneous `Object.keys()` call

* for squash: fix missing `continue` statements

* for squash: Fix HTTP method for deleteSession

* for squash: delete erroneous throw

Throw is only for errors, not successful completion.

* for squash: redo migrateDirtyDBtoRealDB.js to fix async bugs

* for squash: fix erroneous use of `for..of`

* for squash: Add line break between statements

* for squash: put closing paren on same line as last arg

* for squash: Move `log()` back up where it was

to minimize the diff to develop

* for squash: indentation fixes

* for squash: typo fix

* for squash: wrap long lines

* for squash: use `util.callbackify` to silence promise/no-callback-in-promise warning

* for squash: use double quotes to improve readability

Co-authored-by: Richard Hansen <rhansen@rhansen.org>
2021-01-18 08:53:15 +00:00

476 lines
19 KiB
JavaScript
Executable file

'use strict';
/*
* Usage -- see README.md
*
* Normal usage: node bin/plugins/checkPlugin.js ep_whatever
* Auto fix the things it can: node bin/plugins/checkPlugin.js ep_whatever autofix
* Auto commit, push and publish to npm (highly dangerous):
* node bin/plugins/checkPlugin.js ep_whatever autocommit
*/
// As of v14, Node.js does not exit when there is an unhandled Promise rejection. Convert an
// unhandled rejection into an uncaught exception, which does cause Node.js to exit.
process.on('unhandledRejection', (err) => { throw err; });
const fs = require('fs');
const childProcess = require('child_process');
// get plugin name & path from user input
const pluginName = process.argv[2];
if (!pluginName) throw new Error('no plugin name specified');
const pluginPath = `node_modules/${pluginName}`;
console.log(`Checking the plugin: ${pluginName}`);
const optArgs = process.argv.slice(3);
const autoCommit = optArgs.indexOf('autocommit') !== -1;
const autoFix = autoCommit || optArgs.indexOf('autofix') !== -1;
const execSync = (cmd, opts = {}) => (childProcess.execSync(cmd, {
cwd: `${pluginPath}/`,
...opts,
}) || '').toString().replace(/\n+$/, '');
const writePackageJson = (obj) => {
let s = JSON.stringify(obj, null, 2);
if (s.length && s.slice(s.length - 1) !== '\n') s += '\n';
return fs.writeFileSync(`${pluginPath}/package.json`, s);
};
const updateDeps = (parsedPackageJson, key, wantDeps) => {
const {[key]: deps = {}} = parsedPackageJson;
let changed = false;
for (const [pkg, verInfo] of Object.entries(wantDeps)) {
const {ver, overwrite = true} = typeof verInfo === 'string' ? {ver: verInfo} : verInfo;
if (deps[pkg] === ver) continue;
if (deps[pkg] == null) {
console.warn(`Missing dependency in ${key}: '${pkg}': '${ver}'`);
} else {
if (!overwrite) continue;
console.warn(`Dependency mismatch in ${key}: '${pkg}': '${ver}' (current: ${deps[pkg]})`);
}
if (autoFix) {
deps[pkg] = ver;
changed = true;
}
}
if (changed) {
parsedPackageJson[key] = deps;
writePackageJson(parsedPackageJson);
}
};
const prepareRepo = () => {
let branch = execSync('git symbolic-ref HEAD');
if (branch !== 'refs/heads/master' && branch !== 'refs/heads/main') {
throw new Error('master/main must be checked out');
}
branch = branch.replace(/^refs\/heads\//, '');
execSync('git rev-parse --verify -q HEAD^0 || ' +
`{ echo "Error: no commits on ${branch}" >&2; exit 1; }`);
execSync('git rev-parse --verify @{u}'); // Make sure there's a remote tracking branch.
const modified = execSync('git diff-files --name-status');
if (modified !== '') throw new Error(`working directory has modifications:\n${modified}`);
const untracked = execSync('git ls-files -o --exclude-standard');
if (untracked !== '') throw new Error(`working directory has untracked files:\n${untracked}`);
const indexStatus = execSync('git diff-index --cached --name-status HEAD');
if (indexStatus !== '') throw new Error(`uncommitted staged changes to files:\n${indexStatus}`);
execSync('git pull --ff-only', {stdio: 'inherit'});
if (execSync('git rev-list @{u}...') !== '') throw new Error('repo contains unpushed commits');
if (autoCommit) {
execSync('git config --get user.name');
execSync('git config --get user.email');
}
};
if (autoCommit) {
console.warn('Auto commit is enabled, I hope you know what you are doing...');
}
fs.readdir(pluginPath, (err, rootFiles) => {
// handling error
if (err) {
return console.log(`Unable to scan directory: ${err}`);
}
// rewriting files to lower case
const files = [];
// some files we need to know the actual file name. Not compulsory but might help in the future.
let readMeFileName;
let repository;
for (let i = 0; i < rootFiles.length; i++) {
if (rootFiles[i].toLowerCase().indexOf('readme') !== -1) readMeFileName = rootFiles[i];
files.push(rootFiles[i].toLowerCase());
}
if (files.indexOf('.git') === -1) throw new Error('No .git folder, aborting');
prepareRepo();
try {
const path = `${pluginPath}/.github/workflows/npmpublish.yml`;
if (!fs.existsSync(path)) {
console.log('no .github/workflows/npmpublish.yml');
console.log('create one and set npm secret to auto publish to npm on commit');
if (autoFix) {
const npmpublish =
fs.readFileSync('bin/plugins/lib/npmpublish.yml', {encoding: 'utf8', flag: 'r'});
fs.mkdirSync(`${pluginPath}/.github/workflows`, {recursive: true});
fs.writeFileSync(path, npmpublish);
console.log("If you haven't already, setup autopublish for this plugin https://github.com/ether/etherpad-lite/wiki/Plugins:-Automatically-publishing-to-npm-on-commit-to-Github-Repo");
} else {
console.log('Setup autopublish for this plugin https://github.com/ether/etherpad-lite/wiki/Plugins:-Automatically-publishing-to-npm-on-commit-to-Github-Repo');
}
} else {
// autopublish exists, we should check the version..
// checkVersion takes two file paths and checks for a version string in them.
const currVersionFile = fs.readFileSync(path, {encoding: 'utf8', flag: 'r'});
const existingConfigLocation = currVersionFile.indexOf('##ETHERPAD_NPM_V=');
const existingValue = parseInt(
currVersionFile.substr(existingConfigLocation + 17, existingConfigLocation.length));
const reqVersionFile =
fs.readFileSync('bin/plugins/lib/npmpublish.yml', {encoding: 'utf8', flag: 'r'});
const reqConfigLocation = reqVersionFile.indexOf('##ETHERPAD_NPM_V=');
const reqValue =
parseInt(reqVersionFile.substr(reqConfigLocation + 17, reqConfigLocation.length));
if (!existingValue || (reqValue > existingValue)) {
const npmpublish =
fs.readFileSync('bin/plugins/lib/npmpublish.yml', {encoding: 'utf8', flag: 'r'});
fs.mkdirSync(`${pluginPath}/.github/workflows`, {recursive: true});
fs.writeFileSync(path, npmpublish);
}
}
} catch (err) {
console.error(err);
}
try {
const path = `${pluginPath}/.github/workflows/backend-tests.yml`;
if (!fs.existsSync(path)) {
console.log('no .github/workflows/backend-tests.yml');
console.log('create one and set npm secret to auto publish to npm on commit');
if (autoFix) {
const backendTests =
fs.readFileSync('bin/plugins/lib/backend-tests.yml', {encoding: 'utf8', flag: 'r'});
fs.mkdirSync(`${pluginPath}/.github/workflows`, {recursive: true});
fs.writeFileSync(path, backendTests);
}
} else {
// autopublish exists, we should check the version..
// checkVersion takes two file paths and checks for a version string in them.
const currVersionFile = fs.readFileSync(path, {encoding: 'utf8', flag: 'r'});
const existingConfigLocation = currVersionFile.indexOf('##ETHERPAD_NPM_V=');
const existingValue = parseInt(
currVersionFile.substr(existingConfigLocation + 17, existingConfigLocation.length));
const reqVersionFile =
fs.readFileSync('bin/plugins/lib/backend-tests.yml', {encoding: 'utf8', flag: 'r'});
const reqConfigLocation = reqVersionFile.indexOf('##ETHERPAD_NPM_V=');
const reqValue =
parseInt(reqVersionFile.substr(reqConfigLocation + 17, reqConfigLocation.length));
if (!existingValue || (reqValue > existingValue)) {
const backendTests =
fs.readFileSync('bin/plugins/lib/backend-tests.yml', {encoding: 'utf8', flag: 'r'});
fs.mkdirSync(`${pluginPath}/.github/workflows`, {recursive: true});
fs.writeFileSync(path, backendTests);
}
}
} catch (err) {
console.error(err);
}
if (files.indexOf('package.json') === -1) {
console.warn('no package.json, please create');
}
if (files.indexOf('package.json') !== -1) {
const packageJSON =
fs.readFileSync(`${pluginPath}/package.json`, {encoding: 'utf8', flag: 'r'});
const parsedPackageJSON = JSON.parse(packageJSON);
if (autoFix) {
let updatedPackageJSON = false;
if (!parsedPackageJSON.funding) {
updatedPackageJSON = true;
parsedPackageJSON.funding = {
type: 'individual',
url: 'https://etherpad.org/',
};
}
if (updatedPackageJSON) {
writePackageJson(parsedPackageJSON);
}
}
if (packageJSON.toLowerCase().indexOf('repository') === -1) {
console.warn('No repository in package.json');
if (autoFix) {
console.warn('Repository not detected in package.json. Add repository section.');
}
} else {
// useful for creating README later.
repository = parsedPackageJSON.repository.url;
}
updateDeps(parsedPackageJSON, 'devDependencies', {
'eslint': '^7.17.0',
'eslint-config-etherpad': '^1.0.22',
'eslint-plugin-eslint-comments': '^3.2.0',
'eslint-plugin-mocha': '^8.0.0',
'eslint-plugin-node': '^11.1.0',
'eslint-plugin-prefer-arrow': '^1.2.2',
'eslint-plugin-promise': '^4.2.1',
'eslint-plugin-you-dont-need-lodash-underscore': '^6.10.0',
});
updateDeps(parsedPackageJSON, 'peerDependencies', {
// Some plugins require a newer version of Etherpad so don't overwrite if already set.
'ep_etherpad-lite': {ver: '>=1.8.6', overwrite: false},
});
if (packageJSON.toLowerCase().indexOf('eslintconfig') === -1) {
console.warn('No esLintConfig in package.json');
if (autoFix) {
const eslintConfig = {
root: true,
extends: 'etherpad/plugin',
};
parsedPackageJSON.eslintConfig = eslintConfig;
writePackageJson(parsedPackageJSON);
}
}
if (packageJSON.toLowerCase().indexOf('scripts') === -1) {
console.warn('No scripts in package.json');
if (autoFix) {
const scripts = {
'lint': 'eslint .',
'lint:fix': 'eslint --fix .',
};
parsedPackageJSON.scripts = scripts;
writePackageJson(parsedPackageJSON);
}
}
if ((packageJSON.toLowerCase().indexOf('engines') === -1) || !parsedPackageJSON.engines.node) {
console.warn('No engines or node engine in package.json');
if (autoFix) {
const engines = {
node: '>=10.13.0',
};
parsedPackageJSON.engines = engines;
writePackageJson(parsedPackageJSON);
}
}
}
if (files.indexOf('package-lock.json') === -1) {
console.warn('package-lock.json not found');
if (!autoFix) {
console.warn('Run npm install in the plugin folder and commit the package-lock.json file.');
}
}
if (files.indexOf('readme') === -1 && files.indexOf('readme.md') === -1) {
console.warn('README.md file not found, please create');
if (autoFix) {
console.log('Autofixing missing README.md file');
console.log('please edit the README.md file further to include plugin specific details.');
let readme = fs.readFileSync('bin/plugins/lib/README.md', {encoding: 'utf8', flag: 'r'});
readme = readme.replace(/\[plugin_name\]/g, pluginName);
if (repository) {
const org = repository.split('/')[3];
const name = repository.split('/')[4];
readme = readme.replace(/\[org_name\]/g, org);
readme = readme.replace(/\[repo_url\]/g, name);
fs.writeFileSync(`${pluginPath}/README.md`, readme);
} else {
console.warn('Unable to find repository in package.json, aborting.');
}
}
}
if (files.indexOf('contributing') === -1 && files.indexOf('contributing.md') === -1) {
console.warn('CONTRIBUTING.md file not found, please create');
if (autoFix) {
console.log('Autofixing missing CONTRIBUTING.md file, please edit the CONTRIBUTING.md ' +
'file further to include plugin specific details.');
let contributing =
fs.readFileSync('bin/plugins/lib/CONTRIBUTING.md', {encoding: 'utf8', flag: 'r'});
contributing = contributing.replace(/\[plugin_name\]/g, pluginName);
fs.writeFileSync(`${pluginPath}/CONTRIBUTING.md`, contributing);
}
}
if (files.indexOf('readme') !== -1 && files.indexOf('readme.md') !== -1) {
const readme =
fs.readFileSync(`${pluginPath}/${readMeFileName}`, {encoding: 'utf8', flag: 'r'});
if (readme.toLowerCase().indexOf('license') === -1) {
console.warn('No license section in README');
if (autoFix) {
console.warn('Please add License section to README manually.');
}
}
}
if (files.indexOf('license') === -1 && files.indexOf('license.md') === -1) {
console.warn('LICENSE.md file not found, please create');
if (autoFix) {
console.log('Autofixing missing LICENSE.md file, including Apache 2 license.');
let license = fs.readFileSync('bin/plugins/lib/LICENSE.md', {encoding: 'utf8', flag: 'r'});
license = license.replace('[yyyy]', new Date().getFullYear());
license = license.replace('[name of copyright owner]', execSync('git config user.name'));
fs.writeFileSync(`${pluginPath}/LICENSE.md`, license);
}
}
let travisConfig = fs.readFileSync('bin/plugins/lib/travis.yml', {encoding: 'utf8', flag: 'r'});
travisConfig = travisConfig.replace(/\[plugin_name\]/g, pluginName);
if (files.indexOf('.travis.yml') === -1) {
console.warn('.travis.yml file not found, please create. ' +
'.travis.yml is used for automatically CI testing Etherpad. ' +
'It is useful to know if your plugin breaks another feature for example.');
// TODO: Make it check version of the .travis file to see if it needs an update.
if (autoFix) {
console.log('Autofixing missing .travis.yml file');
fs.writeFileSync(`${pluginPath}/.travis.yml`, travisConfig);
console.log('Travis file created, please sign into travis and enable this repository');
}
}
if (autoFix) {
// checks the file versioning of .travis and updates it to the latest.
const existingConfig =
fs.readFileSync(`${pluginPath}/.travis.yml`, {encoding: 'utf8', flag: 'r'});
const existingConfigLocation = existingConfig.indexOf('##ETHERPAD_TRAVIS_V=');
const existingValue =
parseInt(existingConfig.substr(existingConfigLocation + 20, existingConfig.length));
const newConfigLocation = travisConfig.indexOf('##ETHERPAD_TRAVIS_V=');
const newValue = parseInt(travisConfig.substr(newConfigLocation + 20, travisConfig.length));
if (existingConfigLocation === -1) {
console.warn('no previous .travis.yml version found so writing new.');
// we will write the newTravisConfig to the location.
fs.writeFileSync(`${pluginPath}/.travis.yml`, travisConfig);
} else if (newValue > existingValue) {
console.log('updating .travis.yml');
fs.writeFileSync(`${pluginPath}/.travis.yml`, travisConfig);
}//
}
if (files.indexOf('.gitignore') === -1) {
console.warn('.gitignore file not found, please create. .gitignore files are useful to ' +
"ensure files aren't incorrectly commited to a repository.");
if (autoFix) {
console.log('Autofixing missing .gitignore file');
const gitignore = fs.readFileSync('bin/plugins/lib/gitignore', {encoding: 'utf8', flag: 'r'});
fs.writeFileSync(`${pluginPath}/.gitignore`, gitignore);
}
} else {
let gitignore =
fs.readFileSync(`${pluginPath}/.gitignore`, {encoding: 'utf8', flag: 'r'});
if (gitignore.indexOf('node_modules/') === -1) {
console.warn('node_modules/ missing from .gitignore');
if (autoFix) {
gitignore += 'node_modules/';
fs.writeFileSync(`${pluginPath}/.gitignore`, gitignore);
}
}
}
// if we include templates but don't have translations...
if (files.indexOf('templates') !== -1 && files.indexOf('locales') === -1) {
console.warn('Translations not found, please create. ' +
'Translation files help with Etherpad accessibility.');
}
if (files.indexOf('.ep_initialized') !== -1) {
console.warn(
'.ep_initialized found, please remove. .ep_initialized should never be commited to git ' +
'and should only exist once the plugin has been executed one time.');
if (autoFix) {
console.log('Autofixing incorrectly existing .ep_initialized file');
fs.unlinkSync(`${pluginPath}/.ep_initialized`);
}
}
if (files.indexOf('npm-debug.log') !== -1) {
console.warn('npm-debug.log found, please remove. npm-debug.log should never be commited to ' +
'your repository.');
if (autoFix) {
console.log('Autofixing incorrectly existing npm-debug.log file');
fs.unlinkSync(`${pluginPath}/npm-debug.log`);
}
}
if (files.indexOf('static') !== -1) {
fs.readdir(`${pluginPath}/static`, (errRead, staticFiles) => {
if (staticFiles.indexOf('tests') === -1) {
console.warn('Test files not found, please create tests. https://github.com/ether/etherpad-lite/wiki/Creating-a-plugin#writing-and-running-front-end-tests-for-your-plugin');
}
});
} else {
console.warn('Test files not found, please create tests. https://github.com/ether/etherpad-lite/wiki/Creating-a-plugin#writing-and-running-front-end-tests-for-your-plugin');
}
// Install dependencies so we can run ESLint. This should also create or update package-lock.json
// if autoFix is enabled.
const npmInstall = `npm install${autoFix ? '' : ' --no-package-lock'}`;
execSync(npmInstall, {stdio: 'inherit'});
// The ep_etherpad-lite peer dep must be installed last otherwise `npm install` will nuke it. An
// absolute path to etherpad-lite/src is used here so that pluginPath can be a symlink.
execSync(
`${npmInstall} --no-save ep_etherpad-lite@file:${__dirname}/../../src`, {stdio: 'inherit'});
// linting begins
try {
console.log('Linting...');
const lintCmd = autoFix ? 'npx eslint --fix .' : 'npx eslint';
execSync(lintCmd, {stdio: 'inherit'});
} catch (e) {
// it is gonna throw an error anyway
console.log('Manual linting probably required, check with: npm run lint');
}
// linting ends.
if (autoFix) {
const unchanged = JSON.parse(execSync(
'untracked=$(git ls-files -o --exclude-standard) || exit 1; ' +
'git diff-files --quiet && [ -z "$untracked" ] && echo true || echo false'));
if (!unchanged) {
// Display a diff of changes. Git doesn't diff untracked files, so they must be added to the
// index. Use a temporary index file to avoid modifying Git's default index file.
execSync('git read-tree HEAD; git add -A && git diff-index -p --cached HEAD && echo ""', {
env: {...process.env, GIT_INDEX_FILE: '.git/checkPlugin.index'},
stdio: 'inherit',
});
fs.unlinkSync(`${pluginPath}/.git/checkPlugin.index`);
const cmd = [
'git add -A',
'git commit -m "autofixes from Etherpad checkPlugin.js"',
'git push',
].join(' && ');
if (autoCommit) {
console.log('Attempting autocommit and auto publish to npm');
execSync(cmd, {stdio: 'inherit'});
} else {
console.log('Fixes applied. Check the above git diff then run the following command:');
console.log(`(cd node_modules/${pluginName} && ${cmd})`);
}
} else {
console.log('No changes.');
}
}
console.log('Finished');
});