#!/usr/bin/env node /** * @file * The script generates docs.json used as the source of truth * for the source code generators (FP, typings, etc.). * * It's a part of the build process. */ const os = require('os') const pLimit = require('p-limit') const fs = require('fs/promises') const path = require('path') const cloneDeep = require('lodash.clonedeep') const jsDocParser = require('jsdoc-to-markdown') const listFns = require('../_lib/listFns') const docsConfig = require('../../docs/index.js') const docsPath = path.resolve(process.cwd(), 'tmp/docs.json') generateDocsFromSource() .then(generatedDocsObj) .then(injectStaticDocsToDocsObj) .then(injectSharedDocsToDocsObj) .then(writeDocsFile) .catch(reportErrors) /** * Generates docs object from a list of functions using extended JSDoc format. */ async function generateDocsFromSource() { const fns = await listFns() const limit = pLimit(os.cpus().length) const configFile = path.resolve(process.cwd(), 'jsdoc2md.json') const jobs = fns.map((fn) => { return limit(() => jsDocParser .getTemplateData({ files: fn.fullPath, 'no-cache': true, configure: configFile, }) .then((result) => result[0]) ) }) const docsResult = await Promise.all(jobs) return docsResult .map((doc) => { const pureTag = doc.customTags && doc.customTags.find((t) => t.tag === 'pure') const pure = (pureTag && pureTag.value) !== 'false' return { type: 'jsdoc', kind: 'function', urlId: doc.name, category: doc.category, title: doc.name, description: doc.summary, content: doc, pure, } }) .reduce( (array, doc) => array .concat(generateFnDoc(doc)) .concat( doc.pure ? [generateFPFnDoc(doc)].concat( generateFPFnWithOptionsDoc(doc) || [] ) : [] ), [] ) } /** * Generates docs object. */ function generatedDocsObj(docs) { return groupDocs(docs, docsConfig.groups) } /** * Injects static docs (markdown documents specified in the config file) * to docs object. */ function injectStaticDocsToDocsObj(docsFileObj) { return getListOfStaticDocs() .then((staticDocs) => { staticDocs.forEach((staticDoc) => { docsFileObj[staticDoc.category].push(staticDoc) }) return docsFileObj }) .catch(reportErrors) } /** * Injects shared docs to docs object. */ function injectSharedDocsToDocsObj(docsFileObj) { return generateSharedDocs() .then((sharedDocs) => { sharedDocs.forEach((sharedDoc) => { docsFileObj[sharedDoc.category].push(sharedDoc) }) return docsFileObj }) .catch(reportErrors) } /** * Prints an error and exits the process with 1 status code. */ function reportErrors(err) { console.error(err.stack) process.exit(1) } /** * Writes docs file. */ function writeDocsFile(docsFileObj) { return fs.writeFile(docsPath, JSON.stringify(docsFileObj)) } /** * Groups passed docs list. */ function groupDocs(docs, groups) { return docs.reduce((acc, doc) => { ;(acc[doc.category] = acc[doc.category] || []).push(doc) return acc }, buildGroupsTemplate(groups)) } /** * Builds an object where the key is a group name and the value is * an empty array. Pre-generated docs object allows to preserve the desired * groups order. */ function buildGroupsTemplate(groups) { return groups.reduce((acc, group) => { acc[group] = [] return acc }, {}) } /** * Returns promise to list of static docs with its contents. */ function getListOfStaticDocs() { return Promise.all( docsConfig.staticDocs.map((staticDoc) => { return fs .readFile(staticDoc.path) .then((docContent) => docContent.toString()) .then((content) => Object.assign({ content }, staticDoc)) .catch(reportErrors) }) ) } /** * Returns promise to list of shared docs with its contents. */ function generateSharedDocs() { const docs = docsConfig.sharedDocs .map( (fn) => jsDocParser.getTemplateDataSync({ files: fn.fullPath, 'no-cache': true, })[0] ) .map((doc) => ({ type: 'jsdoc', kind: 'typedef', urlId: doc.name, category: doc.category, title: doc.name, description: doc.summary, content: doc, properties: paramsToTree(doc.properties), })) return Promise.resolve(docs) } function generateFnDoc(dirtyDoc) { const doc = cloneDeep(dirtyDoc) const isFPFn = false const { urlId, title } = doc const args = paramsToTree(doc.content.params) return Object.assign(doc, { isFPFn, args, relatedDocs: Object.assign( { default: urlId, fp: `fp/${urlId}` }, withOptions(args) ? { fpWithOptions: `fp/${urlId}WithOptions` } : {} ), usage: generateUsage(title, isFPFn), usageTabs: generateUsageTabs(isFPFn), syntax: generateSyntaxString(title, args, isFPFn), }) } function generateFPFnDoc(dirtyDoc) { const doc = cloneDeep(dirtyDoc) const isFPFn = true const { urlId, title } = doc const exceptions = doc.content.exceptions.filter( (exception) => !exception.description.includes('options.') ) const params = doc.content.params .filter((param) => !param.name.startsWith('options')) .reverse() const args = paramsToTree(params) return Object.assign(doc, { isFPFn, args, generatedFrom: title, urlId: `fp/${urlId}`, relatedDocs: Object.assign( { default: urlId, fp: `fp/${urlId}` }, withOptions(args) ? { fpWithOptions: `fp/${urlId}WithOptions` } : {} ), usage: generateUsage(title, isFPFn), usageTabs: generateUsageTabs(isFPFn), syntax: generateSyntaxString(title, args, isFPFn), content: Object.assign(doc.content, { exceptions, params, examples: 'See [FP Guide](https://date-fns.org/docs/FP-Guide) for more information', }), }) } function generateFPFnWithOptionsDoc(dirtyDoc) { const doc = cloneDeep(dirtyDoc) const isFPFn = true const { urlId, title } = doc const params = doc.content.params .map((param) => { if (!param.name.includes('.')) { param.optional = false } return param }) .reverse() const args = paramsToTree(params) if (!withOptions(args)) return return Object.assign(doc, { isFPFn, args, generatedFrom: title, title: `${title}WithOptions`, urlId: `fp/${urlId}WithOptions`, relatedDocs: { default: urlId, fp: `fp/${urlId}`, fpWithOptions: `fp/${urlId}WithOptions`, }, usage: generateUsage(title, isFPFn), usageTabs: generateUsageTabs(isFPFn), syntax: generateSyntaxString(title, args, isFPFn), content: Object.assign(doc.content, { params, id: `${doc.content.id}WithOptions`, longname: `${doc.content.longname}WithOptions`, name: `${doc.content.name}WithOptions`, examples: 'See [FP Guide](https://date-fns.org/docs/FP-Guide) for more information', }), }) } function withOptions(args) { return args && args[0].name === 'options' } function generateUsageTabs(isFPFn) { return isFPFn ? ['commonjs', 'es2015', 'esm'] : ['commonjs', 'umd', 'es2015', 'esm'] } function generateUsage(name, isFPFn) { const submodule = isFPFn ? '/fp' : '' let usage = { commonjs: { title: 'CommonJS', code: `var ${name} = require('date-fns${submodule}/${name}')`, }, es2015: { title: 'ES 2015', code: `import ${name} from 'date-fns${submodule}/${name}'`, }, esm: { title: 'ESM', code: `import { ${name} } from 'date-fns${ submodule && `/esm/${submodule}` }'`, text: 'See [ECMAScript Modules guide](https://date-fns.org/docs/ECMAScript-Modules) for more information', }, } return usage } function paramsToTree(dirtyParams) { if (!dirtyParams) { return null } const params = cloneDeep(dirtyParams) const paramIndices = params.reduce((result, { name }, index) => { result[name] = index return result }, {}) return params .map((param) => { const { name, isProperty } = param const indexOfDot = name.indexOf('.') if (indexOfDot >= 0 && !isProperty) { const parentIndex = paramIndices[name.substring(0, indexOfDot)] const parent = params[parentIndex] param.name = name.substring(indexOfDot + 1) param.isProperty = true if (!parent.props) { parent.props = [param] } else { parent.props.push(param) } } return param }) .filter((param) => !param.isProperty) } function generateSyntaxString(name, args, isFPFn) { if (!args) { return undefined } else if (isFPFn) { return args.reduce((acc, arg) => acc.concat(`(${arg.name})`), name) } else { const argsString = args .map((arg) => (arg.optional ? `[${arg.name}]` : arg.name)) .join(', ') return `${name}(${argsString})` } }