// you may not need all of these but they come up a lot import fs from "fs"; import assert from "assert"; import logging from '../lib/logging.js'; import { mkdir, mkdir_to } from "../lib/builderator.js"; import glob from "fast-glob"; import path from "path"; import template from "lodash/template.js"; import * as acorn from "acorn"; import * as acorn_walk from "acorn-walk"; const log = logging.create(import.meta.url); export const description = "Describe your command here." // your command uses the npm package commander's options format export const options = [ ] // example of a positional argument, it's the 1st argument to main export const argument = ["source", "source directory"]; // put required options in the required variable export const required = [ ["--output ", "Save to file rather than stdout."], ] // handy function for checking things are good and aborting const check = (test, fail_message) => { if(!test) { log.error(fail_message); process.exit(1); } } const dump = (obj) => { return JSON.stringify(obj, null, 4); } class ParseWalker { constructor(comments) { this.comments = comments; this.exported = []; } handle_class(root) { const new_class = { isa: "class", name: root.declaration.id.name, line_start: root.loc.start.line, methods: [], } new_class.comment = this.find_comment(new_class.line_start); acorn_walk.simple(root, { ClassDeclaration: (cls_node) => { assert(cls_node.id.name === new_class.name, "Name of class changed!"); this.exported.push(new_class); }, MethodDefinition: (meth_node) => { const new_method = { isa: "method", static: meth_node.static, async: meth_node.value.async, generator: meth_node.value.generator, name: meth_node.key.name, line_start: meth_node.loc.start.line, params: this.handle_params(meth_node.value.params), comment: this.find_comment(meth_node.loc.start.line) } new_class.methods.push(new_method); } }); } handle_params(param_list) { const result = []; for(let param of param_list) { acorn_walk.simple(param, { Identifier: (_node) => { result.push({isa: "identifier", name: _node.name}); }, AssignmentPattern: (_node) => { result.push({ isa: "assignment", name: _node.left.name, right: { type: _node.right.type.toLowerCase(), raw: _node.right.raw, value: _node.right.value, name: _node.right.name, }, }); } }); } return result; } add_export(id, exp) { exp.name = id.name; exp.line_start = id.loc.start.line; exp.comment = this.find_comment(exp.line_start); this.exported.push(exp); } handle_arrow_func(id, arrow) { this.add_export(id, { isa: "function", async: arrow.async, generator: arrow.generator, expression: arrow.expression, params: this.handle_params(arrow.params), }); } handle_variable(root) { const declare = root.declaration.declarations[0]; // console.log("VARIABLE", declare); const id = declare.id; if(declare.init.type === "ArrowFunctionExpression") { this.handle_arrow_func(id, declare.init); } else { acorn_walk.simple(root, { Literal: (_node) => { this.add_export(id, { isa: _node.type.toLowerCase(), value: _node.value, raw: _node.raw }); }, CallExpression: (_node) => { this.add_export(id, { isa: _node.type.toLowerCase(), identifier: _node.callee.name, arguments: this.handle_params(_node.arguments) }); }, MemberExpression: (_node) => { this.add_export(id, { isa: _node.type.toLowerCase(), object: _node.object.name, property: _node.property.name, computed: _node.computed, options: _node.optional, }); } }); } } /** * Find the nearest comment to this line, giving * about 2 lines of slack. */ find_comment(line) { for(let c of this.comments) { const distance = c.end - line; if(!c.found && distance < 0 && distance > -3) { c.found = true; return c; } } return undefined; } handle_export(_node) { switch(_node.declaration.type) { case "ClassDeclaration": this.handle_class(_node); break; case "VariableDeclaration": { this.handle_variable(_node); break; } default: console.log(">>>", _node.declaration.type); } } } export const main = async (arg, opts) => { const code = fs.readFileSync(arg); mkdir(opts.output); let comments = []; const acorn_opts = { sourceType: "module", ecmaVersion: "2023", locations: true, sourceFile: arg, ranges: true, onComment: comments } const parsed = acorn.parse(code, acorn_opts); comments = comments.filter(c => c.type === "Block").map(c => { return { start: c.loc.start.line, end: c.loc.end.line, value: c.value, type: "comment", found: false, } }); const walker = new ParseWalker(comments); // acorn is stupid and they grab a reference to the functions so that _removes_ // this from the object, instead of just...calling walker.function() like a normal person acorn_walk.simple(parsed, { ExportNamedDeclaration: (_node) => walker.handle_export(_node), }); console.log(dump({ exports: walker.exported, orphan_comments: walker.comments.filter(c => !c.found) })); process.exit(0); }