/**
* Command line module. Generate domain documentation into single HTML file.
* Command line usage:
ubcli generateDoc -?
* @author pavel.mash 04.02.14
* @module generateDoc
* @memberOf module:@unitybase/ubcli
*/
const fs = require('fs')
const argv = require('@unitybase/base').argv
const options = require('@unitybase/base').options
const _ = require('lodash')
const path = require('path')
const mustache = require('mustache')
const SNIPPETS_FN = '.jsdoc_snippets.json'
const MIXIN_METHODS = {
select: {
src: 'mStorage_api.select',
doc: null
},
insert: {
src: 'mStorage_api.insert',
doc: null
},
update: {
src: 'mStorage_api.update',
doc: null
},
delete: {
src: 'mStorage_api.delete',
doc: null
},
addnew: {
src: 'mStorage_api.addnew',
doc: null
},
lock: {
src: 'softLock_api.lock',
doc: null
},
unlock: {
src: 'softLock_api.unlock',
doc: null
},
renewLock: {
src: 'softLock_api.renewLock',
doc: null
},
isLocked: {
src: 'softLock_api.isLocked',
doc: null
},
getallroles: {
src: 'als_api.getallroles',
doc: null
},
getallstates: {
src: 'als_api.getallstates',
doc: null
},
newversion: {
src: 'dataHistory_api.newversion',
doc: null
},
fts: {
src: 'fts_api.fts',
doc: null
},
ftsreindex: {
src: 'fts_api.ftsreindex',
doc: null
}
}
module.exports = function generateDoc (cfg) {
let
domainI18n,
i, j, len, lenj, k, lenk
console.time('Generation time')
if (!cfg) {
const opts = options.describe('generateDoc',
'Generate domain documentation into single HTML file\nDocumentation generated using default language for user, specified in -u',
'ubcli'
)
.add(argv.establishConnectionFromCmdLineAttributes._cmdLineParams)
.add({
short: 'out',
long: 'out',
param: 'outputFileName',
defaultValue: './domainDocumentation.html',
help: 'Output file path'
})
.add({ short: 'su', long: 'skipUndocumented', defaultValue: false, help: 'Remove undocumented methods from API documentation' })
cfg = opts.parseVerbose({}, true)
if (!cfg) return
}
const session = argv.establishConnectionFromCmdLineAttributes(cfg)
// must be required for translation
// require('@unitybase/ub/i18n')
// console.log('Session.uData: ', session.uData, typeof session.uData)
const conn = session.connection
const outputFileName = cfg.out
try {
const domain = conn.getDomainInfo(true)
const snippets = generateJsDocSnippets(domain)
domainI18n = domain.entities
// add entityCode for each entity in domain
_.each(domainI18n, function (entity, entityCode) {
entity.entityCode = entityCode
})
domainI18n = _.groupBy(domainI18n, 'modelName')
// add modelCode for each model in domain
_.each(domainI18n, function (value, key) {
value.modelCode = key
value.entities = []
const packageJsonFn = path.join(domain.models[key].realPath, 'package.json')
value.modelPackage = require(packageJsonFn)
})
// transform domain to array of entity
const domainAsArray = _.values(domainI18n)
const undocumentedMethods = []
const fakeDocumentedMethods = []
let documentedMethodsCnt = 0
let stdMethodsCnt = 0
let isStdMethod = false
for (i = 0, len = domainAsArray.length; i < len; ++i) {
for (j = 0, lenj = domainAsArray[i].length; j < lenj; ++j) {
domainAsArray[i].entities[j] = domainAsArray[i][j]
const e = domainAsArray[i].entities[j]
_.each(e.attributes, function (value, key) {
value.attrCode = key
})
e.attributes = _.values(e.attributes)
for (k = 0, lenk = e.attributes.length; k < lenk; ++k) {
if (e.attributes[k].associatedEntity === '') {
e.attributes[k].associatedEntity = null
}
}
_.each(domainAsArray[i][j].mixins, function (value, key) {
value.mixinCode = key
})
e.mixins = _.values(e.mixins)
const methods = _.keys(e.entityMethods).sort()
e.methodsArray = []
methods.forEach(methodName => {
const m = {
name: methodName,
jsdoc: { description: '' }
}
// 1) search find methods declared as function methodName(){}; me.methodName = methodName
const snippetLongName = `${e.name}_ns#${methodName}`
let snippet = snippets.find(s => s.longname === snippetLongName)
if (!snippet) {
// 2) search for methods declared as me.methodName = function(..){}
const fn = e.name + '.js'
snippet = snippets.find(s => s.name === methodName && s.meta.filename === fn)
}
if (snippet) {
convertServerSideParamsToAPI(snippet, e.name, methodName)
}
isStdMethod = false
if (!snippet && MIXIN_METHODS[methodName]) { // check mixin methods in case method not already documented
snippet = MIXIN_METHODS[methodName].doc
isStdMethod = true
stdMethodsCnt++
}
let isDocumented = true
if (!snippet || !snippet.comment) {
isDocumented = false
undocumentedMethods.push(`${e.name}.${methodName}`)
} else if (snippet) {
if (snippet.comment && (!snippet.params || !snippet.params.length)) {
fakeDocumentedMethods.push(`${e.name}.${methodName}`)
}
if (!isStdMethod) documentedMethodsCnt++
}
if (snippet) {
m.jsdoc = snippet
}
if (isDocumented || !cfg.skipUndocumented) {
e.methodsArray.push(m)
}
})
}
}
const tpl = fs.readFileSync(path.join(__dirname, 'templates', 'generateDoc_template.mustache'), 'utf8')
const appInfo = conn.getAppInfo()
let appName = appInfo.uiSettings.adminUI.applicationName
if (typeof appName === 'object') {
appName = appName[Object.keys(appName)[0]]
appInfo.uiSettings.adminUI.applicationName = appName
}
const rendered = mustache.render(tpl, {
domain: domainAsArray,
appInfo: conn.getAppInfo(),
i18n: function () {
return function (word) {
// console.log('translate for ', word, 'to', userLang);
// return UB.i18n(word, userLang)
return word
}
}
})
if (!fs.writeFileSync(outputFileName, rendered)) {
console.error('Write to file ' + outputFileName + ' fail')
}
if (!snippets.length) {
console.warn('Methods documentation not added because of jsdoc errors')
} else {
if (undocumentedMethods.length) {
console.warn(`Detected ${undocumentedMethods.length} undocumented entity level methods:`)
console.warn('\t' + undocumentedMethods.join('\n\t'))
}
if (fakeDocumentedMethods.length) {
console.warn(`Documentation w/o parameters detected for ${fakeDocumentedMethods.length} methods:`)
console.warn('\t' + fakeDocumentedMethods.join('\n\t'))
}
console.timeEnd('Generation time')
console.info(`API methods statistics:
- total methods count: ${stdMethodsCnt + documentedMethodsCnt + undocumentedMethods.length}
- methods added by mixins: ${stdMethodsCnt}
- methods added by developers: ${documentedMethodsCnt + undocumentedMethods.length}
- documentation written for ${documentedMethodsCnt} custom methods
- undocumented ${undocumentedMethods.length} custom methods
- fake documentation (no parameters defined) detected for ${fakeDocumentedMethods.length} custom methods`)
}
console.info('Result file', outputFileName)
} finally {
if (session && session.logout) {
session.logout()
}
}
}
module.exports.shortDoc = 'Generate domain documentation into HTML file'
/**
* Generate JsDoc snippets for current domain. `npx ubcli generateDoc` is executed from folder with ubConfig
* @param {UBDomain} domain
*/
function generateJsDocSnippets (domain) {
const JSDOC_CONG_TMP = '.jsdoc_conf_tmp.json'
const JSDOC_CONF_TMP_PATH = path.join(process.cwd(), JSDOC_CONG_TMP)
// check jsdoc is installed
const jsdocPath = path.join(process.cwd(), 'node_modules', 'jsdoc', 'jsdoc.js')
if (!fs.existsSync(jsdocPath)) {
console.error(`Can't find jsdoc module (expected to be in ${jsdocPath}). API methods documentation generation is skipped`)
return []
}
// create config for jsdoc based on available domain models
const jsdocConf = {
recurseDepth: 2,
tags: {
allowUnknownTags: true
},
source: {
include: [
'./node_modules/@unitybase/stubs/_UBMixinsAPI-stub.js' // mixins doc: mStorage etc
],
includePattern: '.+\\.js(m|x)?$',
// "excludePattern": "(\\/_.*\\/|\\\\_.*\\\\|public)"
excludePattern: '(\\/public|\\\\public|\\/_autotest|\\\\/_autotest|\\/_migration|\\\\/_migration)'
},
plugins: [
'plugins/markdown',
'./node_modules/ub-jsdoc/plugins/sripPFromDescription',
'./node_modules/ub-jsdoc/plugins/memberOfModule.js',
'./node_modules/ub-jsdoc/plugins/publishedTag.js'
]
}
if (!fs.existsSync(path.join(process.cwd(), 'node_modules/@unitybase/stubs'))) {
console.warn('@unitybase/stubs package should be added ad dev dependency for mixin methods documentation generation')
}
domain.orderedModels.forEach(m => {
if (m.realPath) jsdocConf.source.include.push(m.realPath)
})
if (fs.existsSync(JSDOC_CONF_TMP_PATH)) fs.unlinkSync(JSDOC_CONF_TMP_PATH)
fs.writeFileSync(JSDOC_CONF_TMP_PATH, JSON.stringify(jsdocConf, null, '\t'))
const snippetsFullFn = path.join(process.cwd(), SNIPPETS_FN)
if (fs.existsSync(snippetsFullFn)) fs.unlinkSync(snippetsFullFn)
let cmd, shell
if (process.platform === 'win32') {
shell = 'cmd.exe'
cmd = `/c "node.exe ${jsdocPath} -r -c ./${JSDOC_CONG_TMP} -X > ./${SNIPPETS_FN}"`
} else {
shell = '/bin/sh'
cmd = `-c "${jsdocPath} -r -c ./${JSDOC_CONG_TMP} -X > ./${SNIPPETS_FN}"`
}
console.log(`Run jsdoc shell command: ${shell} ${cmd}`)
// eslint-disable-next-line no-undef
const res = shellExecute(shell, cmd)
if (res !== 0) {
console.error(`Got error from jsdoc while executing command: ${shell} ${cmd}`)
return []
}
const snippets = require(snippetsFullFn)
// fill build-in mixins
// TODO - how to add a custom mixin documentation (ldoc etc)?
Object.keys(MIXIN_METHODS).forEach(m => {
const mixinMethodDoc = MIXIN_METHODS[m]
mixinMethodDoc.doc = snippets.find(s => s.longname === mixinMethodDoc.src)
if (mixinMethodDoc.doc) convertServerSideParamsToAPI(mixinMethodDoc.doc, '', mixinMethodDoc.src.split('.')[1])
})
return snippets
}
/**
* Mutate a snipped what contains a server-side parameters description into client-side API
* - replace ubMethodParams type -> object
* - removes mParams
* - adds entity & method parameters if not already added
* @param {object} snippet
* @param {string} eName
* @param {string} mName
*/
function convertServerSideParamsToAPI (snippet, eName, mName) {
const prms = snippet.params
if (!prms || !prms.length) return
let topLevelObjPrm = null
prms.forEach(p => {
// ubMethodParams -> object
if (p.type && p.type.names) {
p.type.names = p.type.names.map(n => {
if (n === 'ubMethodParams') {
topLevelObjPrm = p
return 'object'
} else {
return n
}
})
}
// ctxt.mParams.newPwd -> ctxt.newPwd
p.name = p.name.replace('.mParams', '')
if (!p.description) p.description = ''
})
if (topLevelObjPrm) { // @param {ubMethodParam} pn is defined - use this parameter name to add a entity & method if not exists
let pn = `${topLevelObjPrm}.entity`
let prm = prms.find(p => p.name === pn)
let pp = 1
if (!prm) { // add entity parameter
prms.splice(pp, 0, {
type: {
names: ['string']
},
description: eName ? `Should be "${eName}"` : 'Entity code',
name: `${topLevelObjPrm.name}.entity`
})
pp++
}
pn = `${topLevelObjPrm}.method`
prm = prms.find(p => p.name === pn)
if (!prm) { // add method parameter
prms.splice(pp, 0, {
type: {
names: ['string']
},
description: mName ? `Should be "${mName}"` : 'This method code',
name: `${topLevelObjPrm.name}.method`
})
pp++
}
}
}