This is the template project that's checked out and configured when you run the bando-up command from ljsthw-bandolier. This is where the code really lives.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
bandolier-template/commands/codedoc.js

249 lines
6.2 KiB

// 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...>", "source input globs"];
// put required options in the required variable
export const required = [
["--output <string>", "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)
};
}
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 = fname.replaceAll("\\", "/").slice(2);
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);
}