2020-11-02 16:13:24 +00:00
/ *
*
* Usage -- see README . md
*
* Normal usage : node bin / plugins / checkPlugins . js ep _whatever
* Auto fix the things it can : node bin / plugins / checkPlugins . js ep _whatever autofix
* Auto commit , push and publish ( to npm ) * highly dangerous :
node bin / plugins / checkPlugins . js ep _whatever autofix autocommit
* /
2020-11-23 18:21:51 +00:00
const fs = require ( 'fs' ) ;
const { exec } = require ( 'child_process' ) ;
2020-11-02 16:13:24 +00:00
// get plugin name & path from user input
const pluginName = process . argv [ 2 ] ;
2020-11-23 18:21:51 +00:00
if ( ! pluginName ) {
console . error ( 'no plugin name specified' ) ;
2020-11-02 16:13:24 +00:00
process . exit ( 1 ) ;
}
2020-11-23 18:21:51 +00:00
const pluginPath = ` node_modules/ ${ pluginName } ` ;
2020-11-02 16:13:24 +00:00
2020-11-23 18:21:51 +00:00
console . log ( ` Checking the plugin: ${ pluginName } ` ) ;
2020-11-02 16:13:24 +00:00
// Should we autofix?
2020-11-23 18:21:51 +00:00
if ( process . argv [ 3 ] && process . argv [ 3 ] === 'autofix' ) var autoFix = true ;
2020-11-02 16:13:24 +00:00
// Should we update files where possible?
2020-11-23 18:21:51 +00:00
if ( process . argv [ 5 ] && process . argv [ 5 ] === 'autoupdate' ) var autoUpdate = true ;
2020-11-02 16:13:24 +00:00
// Should we automcommit and npm publish?!
2020-11-23 18:21:51 +00:00
if ( process . argv [ 4 ] && process . argv [ 4 ] === 'autocommit' ) var autoCommit = true ;
2020-11-02 16:13:24 +00:00
2020-11-23 18:21:51 +00:00
if ( autoCommit ) {
console . warn ( 'Auto commit is enabled, I hope you know what you are doing...' ) ;
2020-11-02 16:13:24 +00:00
}
2020-11-23 18:21:51 +00:00
fs . readdir ( pluginPath , ( err , rootFiles ) => {
// handling error
2020-11-02 16:13:24 +00:00
if ( err ) {
2020-11-23 18:21:51 +00:00
return console . log ( ` Unable to scan directory: ${ err } ` ) ;
2020-11-02 16:13:24 +00:00
}
// rewriting files to lower case
2020-11-23 18:21:51 +00:00
const files = [ ] ;
2020-11-02 16:13:24 +00:00
// some files we need to know the actual file name. Not compulsory but might help in the future.
2020-11-23 18:21:51 +00:00
let readMeFileName ;
let repository ;
let hasAutoFixed = false ;
2020-11-02 16:13:24 +00:00
2020-11-23 18:21:51 +00:00
for ( let i = 0 ; i < rootFiles . length ; i ++ ) {
if ( rootFiles [ i ] . toLowerCase ( ) . indexOf ( 'readme' ) !== - 1 ) readMeFileName = rootFiles [ i ] ;
2020-11-02 16:13:24 +00:00
files . push ( rootFiles [ i ] . toLowerCase ( ) ) ;
}
2020-11-23 18:21:51 +00:00
if ( files . indexOf ( '.git' ) === - 1 ) {
console . error ( 'No .git folder, aborting' ) ;
2020-11-02 16:13:24 +00:00
process . exit ( 1 ) ;
}
// do a git pull...
var child _process = require ( 'child_process' ) ;
2020-11-23 18:21:51 +00:00
try {
child _process . execSync ( 'git pull ' , { cwd : ` ${ pluginPath } / ` } ) ;
} catch ( e ) {
console . error ( 'Error git pull' , e ) ;
}
2020-11-02 16:13:24 +00:00
try {
2020-11-23 18:21:51 +00:00
const path = ` ${ pluginPath } /.github/workflows/npmpublish.yml ` ;
2020-11-02 16:13:24 +00:00
if ( ! fs . existsSync ( path ) ) {
console . log ( 'no .github/workflows/npmpublish.yml, 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' } ) ;
2020-11-23 18:21:51 +00:00
fs . mkdirSync ( ` ${ pluginPath } /.github/workflows ` , { recursive : true } ) ;
2020-11-02 16:13:24 +00:00
fs . writeFileSync ( path , npmpublish ) ;
2020-11-23 16:34:26 +00:00
hasAutoFixed = true ;
2020-11-02 16:13:24 +00:00
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' ) ;
}
2020-11-23 18:21:51 +00:00
} else {
2020-11-23 16:34:26 +00:00
// 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' } ) ;
2020-11-23 18:21:51 +00:00
const existingConfigLocation = currVersionFile . indexOf ( '##ETHERPAD_NPM_V=' ) ;
const existingValue = parseInt ( currVersionFile . substr ( existingConfigLocation + 17 , existingConfigLocation . length ) ) ;
2020-11-23 16:34:26 +00:00
const reqVersionFile = fs . readFileSync ( 'bin/plugins/lib/npmpublish.yml' , { encoding : 'utf8' , flag : 'r' } ) ;
2020-11-23 18:21:51 +00:00
const reqConfigLocation = reqVersionFile . indexOf ( '##ETHERPAD_NPM_V=' ) ;
const reqValue = parseInt ( reqVersionFile . substr ( reqConfigLocation + 17 , reqConfigLocation . length ) ) ;
2020-11-23 16:34:26 +00:00
2020-11-23 18:21:51 +00:00
if ( ! existingValue || ( reqValue > existingValue ) ) {
2020-11-23 16:34:26 +00:00
const npmpublish =
fs . readFileSync ( 'bin/plugins/lib/npmpublish.yml' , { encoding : 'utf8' , flag : 'r' } ) ;
2020-11-23 18:21:51 +00:00
fs . mkdirSync ( ` ${ pluginPath } /.github/workflows ` , { recursive : true } ) ;
2020-11-23 16:34:26 +00:00
fs . writeFileSync ( path , npmpublish ) ;
hasAutoFixed = true ;
}
2020-11-02 16:13:24 +00:00
}
} catch ( err ) {
console . error ( err ) ;
}
2020-11-23 18:21:51 +00:00
if ( files . indexOf ( 'package.json' ) === - 1 ) {
console . warn ( 'no package.json, please create' ) ;
2020-11-02 16:13:24 +00:00
}
2020-11-23 18:21:51 +00:00
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 ) {
2020-11-02 16:13:24 +00:00
updatedPackageJSON = true ;
parsedPackageJSON . funding = {
2020-11-23 18:21:51 +00:00
type : 'individual' ,
url : 'http://etherpad.org/' ,
} ;
2020-11-02 16:13:24 +00:00
}
2020-11-23 18:21:51 +00:00
if ( updatedPackageJSON ) {
2020-11-22 14:58:11 +00:00
hasAutoFixed = true ;
2020-11-23 18:21:51 +00:00
fs . writeFileSync ( ` ${ pluginPath } /package.json ` , JSON . stringify ( parsedPackageJSON , null , 2 ) ) ;
2020-11-02 16:13:24 +00:00
}
}
2020-11-23 18:21:51 +00:00
if ( packageJSON . toLowerCase ( ) . indexOf ( 'repository' ) === - 1 ) {
console . warn ( 'No repository in package.json' ) ;
if ( autoFix ) {
console . warn ( 'Repository not detected in package.json. Please add repository section manually.' ) ;
2020-11-02 16:13:24 +00:00
}
2020-11-23 18:21:51 +00:00
} else {
2020-11-02 16:13:24 +00:00
// useful for creating README later.
repository = parsedPackageJSON . repository . url ;
}
2020-11-22 14:58:11 +00:00
// include lint config
2020-11-23 18:21:51 +00:00
if ( packageJSON . toLowerCase ( ) . indexOf ( 'devdependencies' ) === - 1 || ! parsedPackageJSON . devDependencies . eslint ) {
console . warn ( 'Missing eslint reference in devDependencies' ) ;
if ( autoFix ) {
const devDependencies = {
'eslint' : '^7.14.0' ,
'eslint-config-etherpad' : '^1.0.10' ,
'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' ,
} ;
2020-11-22 14:58:11 +00:00
hasAutoFixed = true ;
parsedPackageJSON . devDependencies = devDependencies ;
2020-11-23 18:21:51 +00:00
fs . writeFileSync ( ` ${ pluginPath } /package.json ` , JSON . stringify ( parsedPackageJSON , null , 2 ) ) ;
2020-11-22 14:58:11 +00:00
2020-11-23 18:21:51 +00:00
const child _process = require ( 'child_process' ) ;
try {
child _process . execSync ( 'npm install' , { cwd : ` ${ pluginPath } / ` } ) ;
2020-11-22 14:58:11 +00:00
hasAutoFixed = true ;
2020-11-23 18:21:51 +00:00
} catch ( e ) {
console . error ( 'Failed to create package-lock.json' ) ;
2020-11-22 14:58:11 +00:00
}
}
}
2020-11-23 18:21:51 +00:00
if ( packageJSON . toLowerCase ( ) . indexOf ( 'eslintconfig' ) === - 1 ) {
console . warn ( 'No esLintConfig in package.json' ) ;
if ( autoFix ) {
const eslintConfig = {
root : true ,
extends : 'etherpad/plugin' ,
} ;
2020-11-22 14:58:11 +00:00
hasAutoFixed = true ;
parsedPackageJSON . eslintConfig = eslintConfig ;
2020-11-23 18:21:51 +00:00
fs . writeFileSync ( ` ${ pluginPath } /package.json ` , JSON . stringify ( parsedPackageJSON , null , 2 ) ) ;
2020-11-22 14:58:11 +00:00
}
}
2020-11-23 18:21:51 +00:00
if ( packageJSON . toLowerCase ( ) . indexOf ( 'scripts' ) === - 1 ) {
console . warn ( 'No scripts in package.json' ) ;
if ( autoFix ) {
const scripts = {
'lint' : 'eslint .' ,
'lint:fix' : 'eslint --fix .' ,
} ;
2020-11-22 14:58:11 +00:00
hasAutoFixed = true ;
parsedPackageJSON . scripts = scripts ;
2020-11-23 18:21:51 +00:00
fs . writeFileSync ( ` ${ pluginPath } /package.json ` , JSON . stringify ( parsedPackageJSON , null , 2 ) ) ;
2020-11-22 14:58:11 +00:00
}
}
2020-11-23 18:21:51 +00:00
if ( packageJSON . toLowerCase ( ) . indexOf ( 'engines' ) === - 1 ) {
console . warn ( 'No engines in package.json' ) ;
if ( autoFix ) {
const engines = {
lint : 'eslint .' ,
} ;
2020-11-22 14:58:11 +00:00
hasAutoFixed = true ;
parsedPackageJSON . engines = engines ;
2020-11-23 18:21:51 +00:00
fs . writeFileSync ( ` ${ pluginPath } /package.json ` , JSON . stringify ( parsedPackageJSON , null , 2 ) ) ;
2020-11-22 14:58:11 +00:00
}
}
2020-11-02 16:13:24 +00:00
}
2020-11-23 18:21:51 +00:00
if ( files . indexOf ( 'package-lock.json' ) === - 1 ) {
console . warn ( 'package-lock.json file not found. Please run npm install in the plugin folder and commit the package-lock.json file.' ) ;
if ( autoFix ) {
2020-11-02 16:13:24 +00:00
var child _process = require ( 'child_process' ) ;
2020-11-23 18:21:51 +00:00
try {
child _process . execSync ( 'npm install' , { cwd : ` ${ pluginPath } / ` } ) ;
console . log ( 'Making package-lock.json' ) ;
2020-11-22 14:58:11 +00:00
hasAutoFixed = true ;
2020-11-23 18:21:51 +00:00
} catch ( e ) {
console . error ( 'Failed to create package-lock.json' ) ;
2020-11-02 16:13:24 +00:00
}
}
}
2020-11-23 18:21:51 +00:00
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, 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' } ) ;
2020-11-02 16:13:24 +00:00
readme = readme . replace ( /\[plugin_name\]/g , pluginName ) ;
2020-11-23 18:21:51 +00:00
if ( repository ) {
const org = repository . split ( '/' ) [ 3 ] ;
const name = repository . split ( '/' ) [ 4 ] ;
2020-11-02 16:13:24 +00:00
readme = readme . replace ( /\[org_name\]/g , org ) ;
readme = readme . replace ( /\[repo_url\]/g , name ) ;
2020-11-23 18:21:51 +00:00
fs . writeFileSync ( ` ${ pluginPath } /README.md ` , readme ) ;
} else {
console . warn ( 'Unable to find repository in package.json, aborting.' ) ;
2020-11-02 16:13:24 +00:00
}
}
}
2020-11-23 18:21:51 +00:00
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.' ) ;
2020-11-02 16:13:24 +00:00
}
}
}
2020-11-23 18:21:51 +00:00
if ( files . indexOf ( 'license' ) === - 1 && files . indexOf ( 'license.md' ) === - 1 ) {
console . warn ( 'LICENSE.md file not found, please create' ) ;
if ( autoFix ) {
2020-11-22 14:58:11 +00:00
hasAutoFixed = true ;
2020-11-23 18:21:51 +00:00
console . log ( 'Autofixing missing LICENSE.md file, including Apache 2 license.' ) ;
exec ( 'git config user.name' , ( error , name , stderr ) => {
2020-11-02 16:13:24 +00:00
if ( error ) {
console . log ( ` error: ${ error . message } ` ) ;
return ;
}
if ( stderr ) {
console . log ( ` stderr: ${ stderr } ` ) ;
return ;
}
2020-11-23 18:21:51 +00:00
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]' , name ) ;
fs . writeFileSync ( ` ${ pluginPath } /LICENSE.md ` , license ) ;
2020-11-02 16:13:24 +00:00
} ) ;
}
}
2020-11-23 18:21:51 +00:00
let travisConfig = fs . readFileSync ( 'bin/plugins/lib/travis.yml' , { encoding : 'utf8' , flag : 'r' } ) ;
2020-11-02 16:13:24 +00:00
travisConfig = travisConfig . replace ( /\[plugin_name\]/g , pluginName ) ;
2020-11-23 18:21:51 +00:00
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.' ) ;
2020-11-02 16:13:24 +00:00
// TODO: Make it check version of the .travis file to see if it needs an update.
2020-11-23 18:21:51 +00:00
if ( autoFix ) {
2020-11-22 14:58:11 +00:00
hasAutoFixed = true ;
2020-11-23 18:21:51 +00:00
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' ) ;
2020-11-02 16:13:24 +00:00
}
}
2020-11-23 18:21:51 +00:00
if ( autoFix && autoUpdate ) {
2020-11-02 16:13:24 +00:00
// checks the file versioning of .travis and updates it to the latest.
2020-11-23 18:21:51 +00:00
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.' ) ;
2020-11-02 16:13:24 +00:00
// we will write the newTravisConfig to the location.
2020-11-23 18:21:51 +00:00
fs . writeFileSync ( ` ${ pluginPath } /.travis.yml ` , travisConfig ) ;
} else if ( newValue > existingValue ) {
console . log ( 'updating .travis.yml' ) ;
fs . writeFileSync ( ` ${ pluginPath } /.travis.yml ` , travisConfig ) ;
hasAutoFixed = true ;
2020-11-02 16:13:24 +00:00
}
}
2020-11-23 18:21:51 +00:00
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 ) {
2020-11-22 14:58:11 +00:00
hasAutoFixed = true ;
2020-11-23 18:21:51 +00:00
console . log ( 'Autofixing missing .gitignore file' ) ;
const gitignore = fs . readFileSync ( 'bin/plugins/lib/gitignore' , { encoding : 'utf8' , flag : 'r' } ) ;
fs . writeFileSync ( ` ${ pluginPath } /.gitignore ` , gitignore ) ;
2020-11-02 16:13:24 +00:00
}
}
// if we include templates but don't have translations...
2020-11-23 18:21:51 +00:00
if ( files . indexOf ( 'templates' ) !== - 1 && files . indexOf ( 'locales' ) === - 1 ) {
console . warn ( 'Translations not found, please create. Translation files help with Etherpad accessibility.' ) ;
2020-11-02 16:13:24 +00:00
}
2020-11-23 18:21:51 +00:00
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 ) {
2020-11-22 14:58:11 +00:00
hasAutoFixed = true ;
2020-11-23 18:21:51 +00:00
console . log ( 'Autofixing incorrectly existing .ep_initialized file' ) ;
fs . unlinkSync ( ` ${ pluginPath } /.ep_initialized ` ) ;
2020-11-02 16:13:24 +00:00
}
}
2020-11-23 18:21:51 +00:00
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 ) {
2020-11-22 14:58:11 +00:00
hasAutoFixed = true ;
2020-11-23 18:21:51 +00:00
console . log ( 'Autofixing incorrectly existing npm-debug.log file' ) ;
fs . unlinkSync ( ` ${ pluginPath } /npm-debug.log ` ) ;
2020-11-02 16:13:24 +00:00
}
}
2020-11-23 18:21:51 +00:00
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' ) ;
2020-11-02 16:13:24 +00:00
}
2020-11-23 18:21:51 +00:00
} ) ;
} 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' ) ;
2020-11-02 16:13:24 +00:00
}
2020-11-22 14:58:11 +00:00
// linting begins
2020-11-23 18:21:51 +00:00
if ( autoFix ) {
2020-11-22 14:58:11 +00:00
var lintCmd = 'npm run lint:fix' ;
2020-11-23 18:21:51 +00:00
} else {
2020-11-22 14:58:11 +00:00
var lintCmd = 'npm run lint' ;
}
2020-11-23 18:21:51 +00:00
try {
child _process . execSync ( lintCmd , { cwd : ` ${ pluginPath } / ` } ) ;
console . log ( 'Linting...' ) ;
if ( autoFix ) {
2020-11-22 14:58:11 +00:00
// todo: if npm run lint doesn't do anything no need for...
hasAutoFixed = true ;
}
2020-11-23 18:21:51 +00:00
} catch ( e ) {
2020-11-22 14:58:11 +00:00
// it is gonna throw an error anyway
2020-11-23 18:21:51 +00:00
console . log ( 'Manual linting probably required, check with: npm run lint' ) ;
2020-11-22 14:58:11 +00:00
}
// linting ends.
2020-11-23 18:21:51 +00:00
if ( hasAutoFixed ) {
console . log ( 'Fixes applied, please check git diff then run the following command:\n\n' ) ;
2020-11-02 16:13:24 +00:00
// bump npm Version
2020-11-23 18:21:51 +00:00
if ( autoCommit ) {
2020-11-02 16:13:24 +00:00
// holy shit you brave.
2020-11-23 18:21:51 +00:00
console . log ( 'Attempting autocommit and auto publish to npm' ) ;
2020-11-02 16:13:24 +00:00
// github should push to npm for us :)
2020-11-23 18:21:51 +00:00
exec ( ` cd node_modules/ ${ pluginName } && git add -A && git commit --allow-empty -m 'autofixes from Etherpad checkPlugins.js' && git push && cd ../.. ` , ( error , name , stderr ) => {
2020-11-02 16:13:24 +00:00
if ( error ) {
console . log ( ` error: ${ error . message } ` ) ;
return ;
}
if ( stderr ) {
console . log ( ` stderr: ${ stderr } ` ) ;
return ;
}
2020-11-23 18:21:51 +00:00
console . log ( "I think she's got it! By George she's got it!" ) ;
process . exit ( 0 ) ;
2020-11-02 16:13:24 +00:00
} ) ;
2020-11-23 18:21:51 +00:00
} else {
console . log ( ` cd node_modules/ ${ pluginName } && git add -A && git commit --allow-empty -m 'autofixes from Etherpad checkPlugins.js' && npm version patch && git add package.json && git commit --allow-empty -m 'bump version' && git push && npm publish && cd ../.. ` ) ;
2020-11-02 16:13:24 +00:00
}
}
2020-11-23 18:21:51 +00:00
console . log ( 'Finished' ) ;
2020-11-02 16:13:24 +00:00
} ) ;