mirror of
https://github.com/ether/etherpad-lite.git
synced 2025-01-20 06:29:53 +01:00
04063d664b
* fix bin folder and workflows as far its possible cleanup of dockerfile changed paths of scripts add lock file fix working directory for workflows fix windows bin fix travis (is travis used anyway?) fix package refs remove pnpm-lock file in root as these conflicts with the docker volume setup optimize comments use install again refactor prod image call to run fix --workspace can only be used inside a workspace correct comment try fix pipeline try fix pipeline for upgrade-from-latest-release install all deps smaller adjustments save update dockerfile remove workspace command fix run test command start repair latest release workflow start repair latest release workflow start repair latest release workflow further repairs * remove test plugin from docker compose
420 lines
16 KiB
JavaScript
Executable file
420 lines
16 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 fix and commit: node bin/plugins/checkPlugin.js ep_whatever autocommit
|
|
* Auto fix, commit, push and publish to npm (highly dangerous):
|
|
* node bin/plugins/checkPlugin.js ep_whatever autopush
|
|
*/
|
|
|
|
const process = require('process');
|
|
|
|
// 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 assert = require('assert').strict;
|
|
const fs = require('fs');
|
|
const fsp = fs.promises;
|
|
const childProcess = require('child_process');
|
|
const log4js = require('log4js');
|
|
const path = require('path');
|
|
|
|
const logger = log4js.getLogger('checkPlugin');
|
|
|
|
(async () => {
|
|
// get plugin name & path from user input
|
|
const pluginName = process.argv[2];
|
|
|
|
if (!pluginName) throw new Error('no plugin name specified');
|
|
logger.info(`Checking the plugin: ${pluginName}`);
|
|
|
|
const epRootDir = await fsp.realpath(path.join(await fsp.realpath(__dirname), '../..'));
|
|
logger.info(`Etherpad root directory: ${epRootDir}`);
|
|
process.chdir(epRootDir);
|
|
const pluginPath = await fsp.realpath(`node_modules/${pluginName}`);
|
|
logger.info(`Plugin directory: ${pluginPath}`);
|
|
const epSrcDir = await fsp.realpath(path.join(epRootDir, 'src'));
|
|
|
|
const optArgs = process.argv.slice(3);
|
|
const autoPush = optArgs.includes('autopush');
|
|
const autoCommit = autoPush || optArgs.includes('autocommit');
|
|
const autoFix = autoCommit || optArgs.includes('autofix');
|
|
|
|
const execSync = (cmd, opts = {}) => (childProcess.execSync(cmd, {
|
|
cwd: `${pluginPath}/`,
|
|
...opts,
|
|
}) || '').toString().replace(/\n+$/, '');
|
|
|
|
const writePackageJson = async (obj) => {
|
|
let s = JSON.stringify(obj, null, 2);
|
|
if (s.length && s.slice(s.length - 1) !== '\n') s += '\n';
|
|
return await fsp.writeFile(`${pluginPath}/package.json`, s);
|
|
};
|
|
|
|
const checkEntries = (got, want) => {
|
|
let changed = false;
|
|
for (const [key, val] of Object.entries(want)) {
|
|
try {
|
|
assert.deepEqual(got[key], val);
|
|
} catch (err) {
|
|
logger.warn(`${key} possibly outdated.`);
|
|
logger.warn(err.message);
|
|
if (autoFix) {
|
|
got[key] = val;
|
|
changed = true;
|
|
}
|
|
}
|
|
}
|
|
return changed;
|
|
};
|
|
|
|
const updateDeps = async (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' || verInfo == null ? {ver: verInfo} : verInfo;
|
|
if (deps[pkg] === ver || (deps[pkg] == null && ver == null)) continue;
|
|
if (deps[pkg] == null) {
|
|
logger.warn(`Missing dependency in ${key}: '${pkg}': '${ver}'`);
|
|
} else {
|
|
if (!overwrite) continue;
|
|
logger.warn(`Dependency mismatch in ${key}: '${pkg}': '${ver}' (current: ${deps[pkg]})`);
|
|
}
|
|
if (autoFix) {
|
|
if (ver == null) delete deps[pkg];
|
|
else deps[pkg] = ver;
|
|
changed = true;
|
|
}
|
|
}
|
|
if (changed) {
|
|
parsedPackageJson[key] = deps;
|
|
await writePackageJson(parsedPackageJson);
|
|
}
|
|
};
|
|
|
|
const prepareRepo = () => {
|
|
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}`);
|
|
let br;
|
|
if (autoCommit) {
|
|
br = execSync('git symbolic-ref HEAD');
|
|
if (!br.startsWith('refs/heads/')) throw new Error('detached HEAD');
|
|
br = br.replace(/^refs\/heads\//, '');
|
|
execSync('git rev-parse --verify -q HEAD^0 || ' +
|
|
`{ echo "Error: no commits on ${br}" >&2; exit 1; }`);
|
|
execSync('git config --get user.name');
|
|
execSync('git config --get user.email');
|
|
}
|
|
if (autoPush) {
|
|
if (!['master', 'main'].includes(br)) throw new Error('master/main not checked out');
|
|
execSync('git rev-parse --verify @{u}');
|
|
execSync('git pull --ff-only', {stdio: 'inherit'});
|
|
if (execSync('git rev-list @{u}...') !== '') throw new Error('repo contains unpushed commits');
|
|
}
|
|
};
|
|
|
|
const checkFile = async (srcFn, dstFn, overwrite = true) => {
|
|
const outFn = path.join(pluginPath, dstFn);
|
|
const wantContents = await fsp.readFile(srcFn, {encoding: 'utf8'});
|
|
let gotContents = null;
|
|
try {
|
|
gotContents = await fsp.readFile(outFn, {encoding: 'utf8'});
|
|
} catch (err) { /* treat as if the file doesn't exist */ }
|
|
try {
|
|
assert.equal(gotContents, wantContents);
|
|
} catch (err) {
|
|
logger.warn(`File ${dstFn} does not match the default`);
|
|
logger.warn(err.message);
|
|
if (!overwrite && gotContents != null) {
|
|
logger.warn('Leaving existing contents alone.');
|
|
return;
|
|
}
|
|
if (autoFix) {
|
|
await fsp.mkdir(path.dirname(outFn), {recursive: true});
|
|
await fsp.writeFile(outFn, wantContents);
|
|
}
|
|
}
|
|
};
|
|
|
|
if (autoPush) {
|
|
logger.warn('Auto push is enabled, I hope you know what you are doing...');
|
|
}
|
|
|
|
const files = await fsp.readdir(pluginPath);
|
|
|
|
// some files we need to know the actual file name. Not compulsory but might help in the future.
|
|
const readMeFileName = files.filter((f) => f === 'README' || f === 'README.md')[0];
|
|
|
|
if (!files.includes('.git')) throw new Error('No .git folder, aborting');
|
|
prepareRepo();
|
|
|
|
const workflows = ['backend-tests.yml', 'frontend-tests.yml', 'npmpublish.yml'];
|
|
await Promise.all(workflows.map(async (fn) => {
|
|
await checkFile(`bin/plugins/lib/${fn}`, `.github/workflows/${fn}`);
|
|
}));
|
|
await checkFile('bin/plugins/lib/dependabot.yml', '.github/dependabot.yml');
|
|
|
|
if (!files.includes('package.json')) {
|
|
logger.warn('no package.json, please create');
|
|
} else {
|
|
const packageJSON =
|
|
await fsp.readFile(`${pluginPath}/package.json`, {encoding: 'utf8', flag: 'r'});
|
|
const parsedPackageJSON = JSON.parse(packageJSON);
|
|
|
|
await updateDeps(parsedPackageJSON, 'devDependencies', {
|
|
'eslint': '^8.14.0',
|
|
'eslint-config-etherpad': '^3.0.13',
|
|
// Changing the TypeScript version can break plugin code, so leave it alone if present.
|
|
'typescript': {ver: '^4.6.4', overwrite: false},
|
|
// These were moved to eslint-config-etherpad's dependencies so they can be removed:
|
|
'@typescript-eslint/eslint-plugin': null,
|
|
'@typescript-eslint/parser': null,
|
|
'eslint-import-resolver-typescript': null,
|
|
'eslint-plugin-cypress': null,
|
|
'eslint-plugin-eslint-comments': null,
|
|
'eslint-plugin-import': null,
|
|
'eslint-plugin-mocha': null,
|
|
'eslint-plugin-node': null,
|
|
'eslint-plugin-prefer-arrow': null,
|
|
'eslint-plugin-promise': null,
|
|
'eslint-plugin-you-dont-need-lodash-underscore': null,
|
|
});
|
|
|
|
await 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},
|
|
});
|
|
|
|
await updateDeps(parsedPackageJSON, 'engines', {
|
|
node: '>=12.17.0',
|
|
});
|
|
|
|
if (parsedPackageJSON.eslintConfig != null && autoFix) {
|
|
delete parsedPackageJSON.eslintConfig;
|
|
await writePackageJson(parsedPackageJSON);
|
|
}
|
|
if (files.includes('.eslintrc.js')) {
|
|
const [from, to] = [`${pluginPath}/.eslintrc.js`, `${pluginPath}/.eslintrc.cjs`];
|
|
if (!files.includes('.eslintrc.cjs')) {
|
|
if (autoFix) {
|
|
await fsp.rename(from, to);
|
|
} else {
|
|
logger.warn(`please rename ${from} to ${to}`);
|
|
}
|
|
} else {
|
|
logger.error(`both ${from} and ${to} exist; delete ${from}`);
|
|
}
|
|
} else {
|
|
checkFile('bin/plugins/lib/eslintrc.cjs', '.eslintrc.cjs', false);
|
|
}
|
|
|
|
if (checkEntries(parsedPackageJSON, {
|
|
funding: {
|
|
type: 'individual',
|
|
url: 'https://etherpad.org/',
|
|
},
|
|
})) await writePackageJson(parsedPackageJSON);
|
|
|
|
if (parsedPackageJSON.scripts == null) parsedPackageJSON.scripts = {};
|
|
if (checkEntries(parsedPackageJSON.scripts, {
|
|
'lint': 'eslint .',
|
|
'lint:fix': 'eslint --fix .',
|
|
})) await writePackageJson(parsedPackageJSON);
|
|
}
|
|
|
|
if (!files.includes('package-lock.json')) {
|
|
logger.warn('package-lock.json not found');
|
|
if (!autoFix) {
|
|
logger.warn('Run npm install in the plugin folder and commit the package-lock.json file.');
|
|
}
|
|
}
|
|
|
|
const fillTemplate = async (templateFilename, outputFilename) => {
|
|
const contents = (await fsp.readFile(templateFilename, 'utf8'))
|
|
.replace(/\[name of copyright owner\]/g, execSync('git config user.name'))
|
|
.replace(/\[plugin_name\]/g, pluginName)
|
|
.replace(/\[yyyy\]/g, new Date().getFullYear());
|
|
await fsp.writeFile(outputFilename, contents);
|
|
};
|
|
|
|
if (!readMeFileName) {
|
|
logger.warn('README.md file not found, please create');
|
|
if (autoFix) {
|
|
logger.info('Autofixing missing README.md file');
|
|
logger.info('please edit the README.md file further to include plugin specific details.');
|
|
await fillTemplate('bin/plugins/lib/README.md', `${pluginPath}/README.md`);
|
|
}
|
|
}
|
|
|
|
if (!files.includes('CONTRIBUTING') && !files.includes('CONTRIBUTING.md')) {
|
|
logger.warn('CONTRIBUTING.md file not found, please create');
|
|
if (autoFix) {
|
|
logger.info('Autofixing missing CONTRIBUTING.md file, please edit the CONTRIBUTING.md ' +
|
|
'file further to include plugin specific details.');
|
|
await fillTemplate('bin/plugins/lib/CONTRIBUTING.md', `${pluginPath}/CONTRIBUTING.md`);
|
|
}
|
|
}
|
|
|
|
|
|
if (readMeFileName) {
|
|
let readme =
|
|
await fsp.readFile(`${pluginPath}/${readMeFileName}`, {encoding: 'utf8', flag: 'r'});
|
|
if (!readme.toLowerCase().includes('license')) {
|
|
logger.warn('No license section in README');
|
|
if (autoFix) {
|
|
logger.warn('Please add License section to README manually.');
|
|
}
|
|
}
|
|
// eslint-disable-next-line max-len
|
|
const publishBadge = `![Publish Status](https://github.com/ether/${pluginName}/workflows/Node.js%20Package/badge.svg)`;
|
|
// eslint-disable-next-line max-len
|
|
const testBadge = `![Backend Tests Status](https://github.com/ether/${pluginName}/workflows/Backend%20tests/badge.svg)`;
|
|
if (readme.toLowerCase().includes('travis')) {
|
|
logger.warn('Remove Travis badges');
|
|
}
|
|
if (!readme.includes('workflows/Node.js%20Package/badge.svg')) {
|
|
logger.warn('No Github workflow badge detected');
|
|
if (autoFix) {
|
|
readme = `${publishBadge} ${testBadge}\n\n${readme}`;
|
|
// write readme to file system
|
|
await fsp.writeFile(`${pluginPath}/${readMeFileName}`, readme);
|
|
logger.info('Wrote Github workflow badges to README');
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!files.includes('LICENSE') && !files.includes('LICENSE.md')) {
|
|
logger.warn('LICENSE file not found, please create');
|
|
if (autoFix) {
|
|
logger.info('Autofixing missing LICENSE file (Apache 2.0).');
|
|
await fsp.copyFile('bin/plugins/lib/LICENSE', `${pluginPath}/LICENSE`);
|
|
}
|
|
}
|
|
|
|
if (!files.includes('.gitignore')) {
|
|
logger.warn('.gitignore file not found, please create. .gitignore files are useful to ' +
|
|
"ensure files aren't incorrectly commited to a repository.");
|
|
if (autoFix) {
|
|
logger.info('Autofixing missing .gitignore file');
|
|
const gitignore =
|
|
await fsp.readFile('bin/plugins/lib/gitignore', {encoding: 'utf8', flag: 'r'});
|
|
await fsp.writeFile(`${pluginPath}/.gitignore`, gitignore);
|
|
}
|
|
} else {
|
|
let gitignore =
|
|
await fsp.readFile(`${pluginPath}/.gitignore`, {encoding: 'utf8', flag: 'r'});
|
|
if (!gitignore.includes('node_modules/')) {
|
|
logger.warn('node_modules/ missing from .gitignore');
|
|
if (autoFix) {
|
|
gitignore += 'node_modules/';
|
|
await fsp.writeFile(`${pluginPath}/.gitignore`, gitignore);
|
|
}
|
|
}
|
|
}
|
|
|
|
// if we include templates but don't have translations...
|
|
if (files.includes('templates') && !files.includes('locales')) {
|
|
logger.warn('Translations not found, please create. ' +
|
|
'Translation files help with Etherpad accessibility.');
|
|
}
|
|
|
|
|
|
if (files.includes('.ep_initialized')) {
|
|
logger.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) {
|
|
logger.info('Autofixing incorrectly existing .ep_initialized file');
|
|
await fsp.unlink(`${pluginPath}/.ep_initialized`);
|
|
}
|
|
}
|
|
|
|
if (files.includes('npm-debug.log')) {
|
|
logger.warn('npm-debug.log found, please remove. npm-debug.log should never be commited to ' +
|
|
'your repository.');
|
|
if (autoFix) {
|
|
logger.info('Autofixing incorrectly existing npm-debug.log file');
|
|
await fsp.unlink(`${pluginPath}/npm-debug.log`);
|
|
}
|
|
}
|
|
|
|
if (files.includes('static')) {
|
|
const staticFiles = await fsp.readdir(`${pluginPath}/static`);
|
|
if (!staticFiles.includes('tests')) {
|
|
logger.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 {
|
|
logger.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'});
|
|
// Create the ep_etherpad-lite symlink if necessary. This must be done after running `npm install`
|
|
// because that command nukes the symlink.
|
|
try {
|
|
const d = await fsp.realpath(path.join(pluginPath, 'node_modules/ep_etherpad-lite'));
|
|
assert.equal(d, epSrcDir);
|
|
} catch (err) {
|
|
execSync(`${npmInstall} --no-save ep_etherpad-lite@file:${epSrcDir}`, {stdio: 'inherit'});
|
|
}
|
|
// linting begins
|
|
try {
|
|
logger.info('Linting...');
|
|
const lintCmd = autoFix ? 'npx eslint --fix .' : 'npx eslint';
|
|
execSync(lintCmd, {stdio: 'inherit'});
|
|
} catch (e) {
|
|
// it is gonna throw an error anyway
|
|
logger.info('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',
|
|
});
|
|
await fsp.unlink(`${pluginPath}/.git/checkPlugin.index`);
|
|
|
|
const commitCmd = [
|
|
'git add -A',
|
|
'git commit -m "autofixes from Etherpad checkPlugin.js"',
|
|
].join(' && ');
|
|
if (autoCommit) {
|
|
logger.info('Committing changes...');
|
|
execSync(commitCmd, {stdio: 'inherit'});
|
|
} else {
|
|
logger.info('Fixes applied. Check the above git diff then run the following command:');
|
|
logger.info(`(cd node_modules/${pluginName} && ${commitCmd})`);
|
|
}
|
|
const pushCmd = 'git push';
|
|
if (autoPush) {
|
|
logger.info('Pushing new commit...');
|
|
execSync(pushCmd, {stdio: 'inherit'});
|
|
} else {
|
|
logger.info('Changes committed. To push, run the following command:');
|
|
logger.info(`(cd node_modules/${pluginName} && ${pushCmd})`);
|
|
}
|
|
} else {
|
|
logger.info('No changes.');
|
|
}
|
|
}
|
|
|
|
logger.info('Finished');
|
|
})();
|