Created
April 9, 2018 23:51
-
-
Save sompylasar/378b402991a1f407c538cc9a5c2372de to your computer and use it in GitHub Desktop.
TODO.md generator
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env node | |
/* eslint-disable strict, no-console, prefer-arrow-callback, prefer-spread */ | |
'use strict'; | |
const glob = require('glob'); | |
const leasot = require('leasot'); // eslint-disable-line import/no-extraneous-dependencies | |
const path = require('path'); | |
const fs = require('fs'); | |
const gitAdd = require('../_binUtils').gitAdd; | |
const handleError = require('../_binUtils').handleError; | |
const ROOT_DIR = require('../_binUtils').ROOT_DIR; | |
const REPO_BASE_URL = ''; | |
const DEFAULT_USERNAME = 'sompylasar'; | |
const TAGS = 'HACK, QUESTION, WORKAROUND, TODO, FIXME, XXX, REVIEW'.split(/\s*,\s*/); | |
if (false) { // eslint-disable-line no-constant-condition | |
TAGS.push.apply(TAGS, 'WARNING, NOTE, CHANGED, IDEA'.split(/\s*,\s*/)); | |
} | |
const GLOB = '@(src|bin)/**/*'; | |
const GLOB_IGNORE = []; | |
const OUTPUT = path.join(ROOT_DIR, 'TODO.md'); | |
function processFile(file, next) { | |
fs.readFile(file, { encoding: 'utf8' }, function onFileRead(error, content) { | |
if (error) { | |
next(error); | |
return; | |
} | |
const contentString = content.toString(); | |
const todosForFile = leasot.parse({ | |
ext: path.extname(file), | |
content: contentString, | |
fileName: file, | |
customTags: TAGS, | |
}); | |
const isInTests = ( path.basename(path.dirname(file)).indexOf('test') === 0 ); | |
if ( isInTests ) { | |
const contentStringLines = contentString.split('\n'); | |
contentStringLines.forEach((line) => { | |
const skippedTestRegexp = /\bit\.skip\((['"])(.+)(\1),.*$/; | |
const skippedTestMatch = skippedTestRegexp.exec(line); | |
if (skippedTestMatch) { | |
const todoEnableSkippedTest = { | |
file: file, | |
line: 0, | |
kind: 'TODO', | |
ref: '@' + DEFAULT_USERNAME, | |
text: 'Enable skipped test: ' + skippedTestMatch[2].replace(/\\(.)/g, '$1'), | |
}; | |
todosForFile.push(todoEnableSkippedTest); | |
} | |
}); | |
} | |
next(null, todosForFile); | |
}); | |
} | |
function strcmp(left, right) { | |
return (left < right ? -1 : (left > right ? 1 : 0)); | |
} | |
function multicmp(cmpArray) { | |
for (let ic = cmpArray.length, i = 0; i < ic; ++i) { | |
if (cmpArray[i] !== 0) { | |
return cmpArray[i]; | |
} | |
} | |
return 0; | |
} | |
function compareTodos(left, right) { | |
const kindcmpres = ( TAGS.indexOf(left.kind) - TAGS.indexOf(right.kind) ); | |
const filecmpres = strcmp(left.file, right.file); | |
const linecmpres = ( left.line - right.line ); | |
const refcmpres = strcmp(left.ref, right.ref); | |
const textcmpres = strcmp(left.text, right.text); | |
const cmpArray = [ | |
kindcmpres, | |
filecmpres, | |
linecmpres, | |
refcmpres, | |
textcmpres, | |
]; | |
const multicmpres = multicmp(cmpArray); | |
return multicmpres; | |
} | |
function processAllFiles(files, next) { | |
const todosAll = []; | |
function nextFile(fileIndex) { | |
if (fileIndex >= files.length) { | |
todosAll.sort(compareTodos); | |
next(null, todosAll); | |
return; | |
} | |
processFile(files[fileIndex], function onFileProcessed(error, todosForFile) { | |
if (error) { | |
next(error); | |
return; | |
} | |
if (todosForFile) { | |
todosAll.push.apply(todosAll, todosForFile); | |
} | |
nextFile(fileIndex + 1); | |
}); | |
} | |
nextFile(0); | |
} | |
function generateMarkdown(todosArray, next) { | |
const todosMarkdown = (todosArray.length <= 0 ? '_Congratulations, the code is clean of TODOs!_' : todosArray.map(function onMapItem(item) { | |
/* | |
item ~ { | |
file: '/absolute/path/to/file.js', | |
kind: 'TODO', | |
line: 2, | |
text: 'Make this work with `foo-bar`.', | |
ref: '@sompylasar' | |
} | |
*/ | |
const fileRelative = path.relative(ROOT_DIR, item.file); | |
const text = item.text.replace(/^\s+|\s+$/g, ''); | |
const usernames = ( | |
item.ref.split(/[&|,;]+/g) | |
.map(function onUsernameTrim(username) { | |
username = username.replace(/(^\s+)|(\s+$)/g, ''); | |
if (username === 'any') { | |
username = DEFAULT_USERNAME; | |
} | |
return username; | |
}) | |
.filter(function onUsernameFilter(username) { | |
return !!username; | |
}) | |
); | |
const line = ( | |
' - ' + | |
'**' + item.kind.toUpperCase() + '**' + | |
' ' + | |
usernames.map(function onUsernameToMarkdown(username) { | |
if (/\s+/.test(username) || username.indexOf('@') > 0 || username.indexOf('+') >= 0) { return '**' + username + '**'; } | |
return '[**' + '@' + username.replace(/^[@]+/, '') + '**](https://github.com/' + username.replace(/^[@]+/, '') + ')'; | |
}).join(';') + | |
': ' + | |
(text || '_(no comment)_') + ' \n' + | |
'<sup>– [' + fileRelative + '#L' + item.line + '](' + REPO_BASE_URL + fileRelative + '#L' + item.line + ')</sup>' | |
); | |
return line; | |
}).join('\n')); | |
const todosMarkdownWithHeading = [ | |
'# TODOs', | |
'', | |
'> <sup>**Warning**: This file is auto-generated. Please do not edit it manually, use [npm run todo](bin/todo.js) to update it.</sup>', | |
'', | |
'_Found ' + todosArray.length + ' in `' + GLOB + '`, looked for ' + | |
TAGS.map(function onMapTag(tag) { | |
return ( | |
'`' + tag.toUpperCase() + '` (' + | |
todosArray.filter(function onFilterByTag(item) { | |
return (item.kind.toUpperCase() === tag.toUpperCase()); | |
}).length + | |
')' | |
); | |
}).join(', ') + | |
'._', | |
'', | |
todosMarkdown, | |
'', | |
].join('\n'); | |
next(null, todosMarkdownWithHeading); | |
} | |
glob(GLOB, { | |
cwd: ROOT_DIR, | |
nodir: true, | |
realpath: true, | |
ignore: GLOB_IGNORE, | |
}, function onGlob(globError, filesFromGlob) { | |
if (globError) { | |
handleError(globError); | |
return; | |
} | |
const files = filesFromGlob.filter(function onFilesFilter(file) { | |
const relativePath = path.relative(ROOT_DIR, file); | |
return ( | |
!/node_modules|bower_components|vendor/.test(relativePath) | |
&& leasot.isExtSupported(path.extname(relativePath)) | |
); | |
}); | |
console.log('Files found: ' + files.length); | |
processAllFiles(files, function onAllFilesProcessed(processAllFilesError, todosArray) { | |
if (processAllFilesError) { | |
handleError(processAllFilesError); | |
return; | |
} | |
console.log('TODOs found: ' + todosArray.length); | |
generateMarkdown(todosArray, function onMarkdownGenerated(generateMarkdownError, todosMarkdown) { | |
if (generateMarkdownError) { | |
handleError(generateMarkdownError); | |
return; | |
} | |
console.log('Markdown generated.'); | |
fs.writeFile(OUTPUT, todosMarkdown, { encoding: 'utf8' }, function onFileWritten(writeFileError) { | |
if (writeFileError) { | |
handleError(writeFileError); | |
return; | |
} | |
console.log('Markdown written to file: ' + OUTPUT); | |
gitAdd(OUTPUT, function onGitAdded(gitAddError) { | |
if (gitAddError) { | |
handleError(gitAddError); | |
return; | |
} | |
console.log('File added to git stage: ' + OUTPUT); | |
}); | |
}); | |
}); | |
}); | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment