// 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, glob } from "../lib/builderator.js"; 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 input globs"]; // 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, code) { this.comments = comments; this.exported = []; this.code = code; } 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!"); new_class.range = [cls_node.start, cls_node.body.start]; this.add_export(cls_node.id, 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, range: [meth_node.start, meth_node.value.body.start], params: this.handle_params(meth_node.value.params), comment: this.find_comment(meth_node.loc.start.line), } new_method.code = this.slice_code(new_method.range); 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); exp.code = this.slice_code(exp.range); this.exported.push(exp); } slice_code(range) { return this.code.slice(range[0], range[1]); } handle_arrow_func(id, arrow) { this.add_export(id, { isa: "function", async: arrow.async, generator: arrow.generator, expression: arrow.expression, range: [id.start, arrow.body.start], params: this.handle_params(arrow.params), }); } handle_variable(root) { const declare = root.declaration.declarations[0]; const id = declare.id; const _node = declare.init; const init_is = declare.init.type; if(init_is === "ArrowFunctionExpression") { this.handle_arrow_func(id, declare.init); } else { this.add_export(id, { isa: _node.type.toLowerCase(), value: _node.value, range: declare.range, raw: _node.raw }); } } /** * 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); } } } const parse_source = (source) => { const code = fs.readFileSync(source); let comments = []; const acorn_opts = { sourceType: "module", ecmaVersion: "2023", locations: true, sourceFile: source, 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, code.toString()); // 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), }); return { // normalize to / even on windows source: source.replaceAll("\\", "/"), exports: walker.exported, orphan_comments: walker.comments.filter(c => !c.found) }; } const normalize_name = (fname) => { const no_slash = fname.replaceAll("\\", "/"); if(fname.startsWith("./")) { return no_slash.slice(2); } else if(fname.startsWith("/")) { return no_slash.slice(1); } else { return no_slash; } } export const main = async (source_globs, opts) => { const index = {}; mkdir_to(opts.output); for(let source of source_globs) { const source_list = glob(source); for(let fname of source_list) { const result = parse_source(fname); const target = `${path.join(opts.output, fname)}.json`; mkdir_to(target); fs.writeFileSync(target, dump(result)); const name = normalize_name(fname); index[name] = result.exports.map(e => { return {isa: e.isa, name: e.name}; }); } } // now write the grand index const index_name = path.join(opts.output, "index.json"); fs.writeFileSync(index_name, dump(index)); process.exit(0); }