From 1922feab1e85292cc2c57730565a9d8d1410e06d Mon Sep 17 00:00:00 2001 From: KernelDeimos Date: Tue, 3 Dec 2024 17:01:11 -0500 Subject: [PATCH] dev: add module documentation generator --- tools/module-docgen/main.js | 166 +++++++++++++++++++++++++++++++ tools/module-docgen/package.json | 17 ++++ 2 files changed, 183 insertions(+) create mode 100644 tools/module-docgen/main.js create mode 100644 tools/module-docgen/package.json diff --git a/tools/module-docgen/main.js b/tools/module-docgen/main.js new file mode 100644 index 00000000..34f4479f --- /dev/null +++ b/tools/module-docgen/main.js @@ -0,0 +1,166 @@ +const fs = require("fs"); +const path_ = require("path"); + +const rootdir = path_.resolve(process.argv[2] ?? '.'); + +const parser = require('@babel/parser'); +const traverse = require('@babel/traverse').default; +const doctrine = require('doctrine'); + +const def_module = { + services: [], +}; + +const to_module_add_service = (def_module, values) => { + def_module.services.push({ + ...values, + }); +} + +const to_service_add_listener = (def_service, values) => { + const parsed_comment = doctrine.parse(values.comment, { unwrap: true }); + + const params = []; + for ( const tag of parsed_comment.tags ) { + if ( tag.title !== 'evtparam' ) continue; + const name = tag.description.slice(0, tag.description.indexOf(' ')); + const desc = tag.description.slice(tag.description.indexOf(' ')); + params.push({ name, desc }) + } + + def_service.listeners.push({ + ...values, + comment: parsed_comment.description, + params, + }); +}; + +const to_service_add_comment = (def_service, comment) => { + console.log('comment', comment); + const parsed_comment = doctrine.parse(comment.value, { unwrap: true }); + def_service.comment = parsed_comment.description; +}; + +const to_module_add_comment = (def_module, comment) => { + console.log('comment', comment); + const parsed_comment = doctrine.parse(comment.value, { unwrap: true }); + def_module.comment = parsed_comment.description; +}; + +const create_service = () => ({ + listeners: [] +}); + +// List files in this directory +const files = fs.readdirSync(rootdir); +for ( const file of files ) { + const stat = fs.statSync(path_.join(rootdir, file)); + if ( stat.isDirectory() ) { + continue; + } + if ( ! file.endsWith('.js') ) continue; + + const type = + file.endsWith('Service.js') ? 'service' : + file.endsWith('Module.js') ? 'module' : + null; + + if ( type === null ) continue; + + const def_service = create_service(); + + console.log('file', file); + const code = fs.readFileSync(path_.join(rootdir, file), 'utf8'); + const ast = parser.parse(code); + traverse(ast, { + ClassDeclaration (path) { + const node = path.node; + const name = node.id.name; + def_service.name = name; + + // Skip utility classes (for now) + if ( name !== file.slice(0, -3) ) { + return; + } + + const comment = (node.leadingComments && ( + node.leadingComments.length < 1 ? '' : + node.leadingComments[node.leadingComments.length - 1] + )) ?? ''; + + if ( type === 'module' ) { + def_module.name = name; + if ( comment !== '' ) { + to_module_add_comment(def_module, comment); + } + return; + } + + if ( comment !== '' ) { + to_service_add_comment(def_service, comment); + } + + console.log('class', name); + path.node.body.body.forEach(member => { + const key = member.key.name ?? member.key.value; + + const comment = member.leadingComments?.[0]?.value ?? ''; + + if ( key.startsWith('__on_') ) { + // 2nd argument is always an object destructuring; + // we want the list of keys in the object: + const params = member.params?.[1]?.properties ?? []; + + to_service_add_listener(def_service, { + key: key.slice(5), + comment, + params, + }); + } + console.log(member.type, key, member.leadingComments); + }); + // module_info.services.push({ + // name, + // file, + // }); + } + }) + + to_module_add_service(def_module, def_service); + // console.log('parsed?', parsed); +} + +console.log('module', JSON.stringify(def_module, undefined, ' ')); + +const outfile = path_.join(rootdir, 'README.md'); + +let out = ''; + +out += `# ${def_module.name}\n\n`; + +if ( def_module.comment ) { + out += `${def_module.comment}\n\n`; +} + +out += '## Services\n\n'; + +for ( const service of def_module.services ) { + out += `### ${service.name}\n\n`; + out += `${service.comment}\n\n`; + + out += '#### Listeners\n\n'; + for ( const listener of service.listeners ) { + out += `##### \`${listener.key}\`\n\n`; + out += `${listener.comment}\n\n`; + + if ( listener.params.length > 0 ) { + out += '###### Parameters\n\n'; + for ( const param of listener.params ) { + out += `- \`${param.name}\`: ${param.desc}\n`; + } + out += '\n'; + } + } +} + +fs.writeFileSync(outfile, out); \ No newline at end of file diff --git a/tools/module-docgen/package.json b/tools/module-docgen/package.json new file mode 100644 index 00000000..cf258dc8 --- /dev/null +++ b/tools/module-docgen/package.json @@ -0,0 +1,17 @@ +{ + "name": "module-docgen", + "version": "1.0.0", + "main": "main.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "AGPL-3.0-only", + "description": "", + "dependencies": { + "@babel/parser": "^7.26.2", + "@babel/traverse": "^7.25.9", + "doctrine": "^3.0.0" + } +}