mirror of
https://github.com/ether/etherpad-lite.git
synced 2025-04-22 00:16:15 -04:00

file in the root directory that references ./src directory as the file source for `ep_etherpad-lite`. Remove --legacy-peer-deps and --no-save when invoking npm. There is no need for them anymore, as we are bumping npm now to v8. ./src/package.json contains all dependencies of Etherpad core (package name ep_etherpad-lite) as before. The root directory's package.json file references ep_etherpad-lite and also contains references to any installed plugins. Remove npm from package.json as we depend on a recent version now; PATH is still updated as before, so in the future we may install a custom npm version again lint package-lock: update exception for sqlite3 remove node_modules and package.json during installDeps.sh update Dockerfile adapt minify windows build Fixed installOnWindows.bat remove node_modules from git bump minimal node/npm version in src/bin/functions.sh add changelog notes update installdeps fix dockerfile docker: test npm prefix set to the etherpad directory workflow: upgrade-from-latest-release needs to be adapted until next release is out Revert "docker: test npm prefix set to the etherpad directory" This reverts commit b856a2488c9dbfb2acf35309cd1ee83016b631ad. use npm link --bin-links=false to prevent it from copying bin files temp fix for scripts as they are not installed to bin directory anymore adjust bin paths in Dockerfile Dockerfile add hint for npm link, dockerfile update dockerfile Revert "Fixed installOnWindows.bat" This reverts commit 70d0716bbedc4c0c1043155fcc5d157f01775c61. try installOnWindows; still TODO: no difference between production and development; no warning like in installDeps.sh before update - it just removes package* and node_modules so admins must be aware of the plugins they want to reinstall later update installOnWindows.bat update package-lock.json Dockerfile Dockerfile add file: scheme for lint check - needed as long as we have the plugin compatibility symlinks in ./src/node_modules fix installOnWindows upgrade-from-latest-release workflow: adapt cypress installation src/package.json: test-container fix path to _mocha; maybe revert this in case we enable bin-links again src/package.json: add test-on-windows script another try with test-on-windows, without using bin-links use bin-links on windows Revert "use bin-links on windows" This reverts commit f50ec2a9fabe3098d48e8f412b73c01edbe2140e. invoke mocha binary on windows run npm i once on windows, to make bin files available - why? remove supertest on windows production builds add symlink for mocha debug Revert "debug" This reverts commit 8916a0515ca2897c57ca65fef49fd0b3610d2989. Revert "add symlink for mocha" This reverts commit 3c60bef77d2a120d24fce14421fe638598cd849d. windows workflow: adapt cypress path frontend admin tests
419 lines
16 KiB
JavaScript
Executable file
419 lines
16 KiB
JavaScript
Executable file
'use strict';
|
|
|
|
/*
|
|
* Usage -- see README.md
|
|
*
|
|
* Normal usage: node src/bin/plugins/checkPlugin.js ep_whatever
|
|
* Auto fix the things it can: node src/bin/plugins/checkPlugin.js ep_whatever autofix
|
|
* Auto fix and commit: node src/bin/plugins/checkPlugin.js ep_whatever autocommit
|
|
* Auto fix, commit, push and publish to npm (highly dangerous):
|
|
* node src/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(`src/bin/plugins/lib/${fn}`, `.github/workflows/${fn}`);
|
|
}));
|
|
await checkFile('src/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('src/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('src/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('src/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 = ``;
|
|
// eslint-disable-next-line max-len
|
|
const testBadge = ``;
|
|
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('src/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('src/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');
|
|
}
|
|
|
|
try {
|
|
const d = await fsp.realpath(path.join(pluginPath, 'node_modules/ep_etherpad-lite'));
|
|
assert.equal(d, epSrcDir);
|
|
} catch (err) {
|
|
execSync('./src/bin/installDeps.sh', {stdio: 'inherit'});
|
|
}
|
|
|
|
// 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'});
|
|
// 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');
|
|
})();
|