Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

docs(eslDoc): automatic documentation generation #1250

Draft
wants to merge 6 commits into
base: main-beta
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pages/.eleventy.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ module.exports = (config) => {
// Init all 11ty config modules
const cfgFiles = fs.readdirSync('./11ty');
for (const file of cfgFiles) {
if (file.startsWith('_')) continue;
if (file.startsWith('_') || !file.endsWith('.js')) continue;
try {
console.info(color.blue(`Initializing module: ${file}`));
require('./11ty/' + file)(config);
Expand Down
39 changes: 39 additions & 0 deletions pages/11ty/eslDoc/eslDoc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
const fs = require('fs');
const ts = require('typescript');
const path = require('path');
const {addFunctionOverloads, getFunctionStatement} = require('./helpers/functions');
const {getClass} = require('./helpers/class');
const {isNodeExported} = require('./helpers/common');
const {getAlias} = require('./helpers/type');

class ESLDoc {
static render (filePath) {
const inputBuffer = fs.readFileSync(filePath).toString();
const node = ts.createSourceFile(
'class.ts',
inputBuffer,
ts.ScriptTarget.Latest,
true
);

const renderOutput = [];
node.statements.forEach((declaration) => visitChild(declaration, renderOutput, filePath));
return renderOutput;
}
}

function visitChild (declaration, output, filePath) {
if (ts.isExportDeclaration(declaration)) {
output.push(...ESLDoc.render(`${path.join(path.dirname(filePath), declaration.moduleSpecifier.text)}.ts`));
return;
}

if (!isNodeExported(declaration)) return;
if (ts.isClassDeclaration(declaration)) return output.push(getClass(declaration));
if (ts.isFunctionDeclaration(declaration)) return addFunctionOverloads(declaration, output);
if (ts.isTypeAliasDeclaration(declaration)) return output.push(getAlias(declaration));
if (ts.isVariableStatement(declaration) && ts.isFunctionLike(declaration.declarationList.declarations[0].initializer))
return output.push(getFunctionStatement(declaration));
}

module.exports.ESLDoc = ESLDoc;
56 changes: 56 additions & 0 deletions pages/11ty/eslDoc/helpers/class.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
const ts = require('typescript');
const {getFunctionDeclaration, addFunctionOverloads} = require('./functions');
const {
getDeclarationName,
getJSDocFullText,
getModifiers,
getArgumentsSignature,
getArgumentsList,
getTypeSignature
} = require('./common');

function getClass(declaration) {
const declarationObj = {
name: getDeclarationName(declaration),
type: 'Class',
comment: getJSDocFullText(declaration),
modifiers: getModifiers(declaration),
ctors: [],
methods: [],
properties: [],
getAccessors: [],
setAccessors: []
};

declaration.members.forEach((member) => {
if (ts.isConstructorDeclaration(member)) return declarationObj.ctors.push(getConstRuctorDeclaration(member, declarationObj.name));

if (!(member.modifiers && ts.SyntaxKind[member.modifiers[0]?.kind] === 'PublicKeyword')) return;
if (ts.isPropertyDeclaration(member)) return declarationObj.properties.push(getPropertyDeclaration(member));
if (ts.isFunctionDeclaration(member) || ts.isMethodDeclaration(member)) return addFunctionOverloads(member, declarationObj.methods);
if (ts.isGetAccessorDeclaration(member)) return declarationObj.getAccessors.push(getFunctionDeclaration(member));
if (ts.isSetAccessorDeclaration(member)) return declarationObj.setAccessors.push(getFunctionDeclaration(member));
});
if (!declarationObj.ctors.length) declarationObj.ctors.push(getConstRuctorDeclaration(declaration));
return declarationObj;
}

function getConstRuctorDeclaration(declaration, name = getDeclarationName(declaration)) {
const type = 'constructor';
const modifiers = getModifiers(declaration);
const parameters = getArgumentsList(declaration);
const signature = `new ${name}(${getArgumentsSignature(parameters)}): ${name}`;
return {name, type, signature, modifiers, parameters};
}

function getPropertyDeclaration(declaration) {
return {
name: getDeclarationName(declaration),
type: getTypeSignature(declaration),
modifiers: getModifiers(declaration),
defaultValue: declaration.initializer?.text,
comment: getJSDocFullText(declaration)
};
}

module.exports = {getClass};
16 changes: 16 additions & 0 deletions pages/11ty/eslDoc/helpers/common.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
const {getJSDocFullText} = require('./common/jsDoc');
const {getArgumentsSignature, getArgumentsList, getMemberList} = require('./common/arguments');
const {getDeclarationName, getModifiers, isNodeExported} = require('./common/common');
const {getTypeSignature, getGenericTypes} = require('./common/type');

module.exports = {
getArgumentsSignature,
getArgumentsList,
getDeclarationName,
getGenericTypes,
getJSDocFullText,
getMemberList,
getModifiers,
getTypeSignature,
isNodeExported
};
37 changes: 37 additions & 0 deletions pages/11ty/eslDoc/helpers/common/arguments.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
const ts = require('typescript');
const {getJSDocComment, getJSDocFullText} = require('./jsDoc');
const {getDeclarationName} = require('./common');
const {getTypeSignature} = require('./type');

function getArgumentsList(declaration) {
return declaration.parameters?.map((parameter) => getArgument(parameter)) || [];
}

function getMemberList(declaration) {
return declaration.members?.map((member) => getArgument(member)) || [];
}

function getArgument(parameter) {
const tags = ts.getJSDocTags(parameter)[0];
return {
name: getDeclarationName(parameter),
type: getTypeSignature(parameter),
defaultValue: parameter.initializer?.getFullText(),
isOptional: (!!parameter.questionToken || !!this.defaultValue),
isRest: !!parameter.dotDotDotToken,
comment: tags ? getJSDocComment(tags) : getJSDocFullText(parameter)
};
}

function getArgumentsSignature (parameters) {
return parameters?.map((parameter) => {
const {name, type, isRest, isOptional} = parameter;
return `${isRest ? '...' : ''}${name}${isOptional ? '?' : ''}${type ? `: ${type}` : ''}`;
}).join(', ') || '';
}

module.exports = {
getArgumentsSignature,
getArgumentsList,
getMemberList
};
26 changes: 26 additions & 0 deletions pages/11ty/eslDoc/helpers/common/common.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
const ts = require('typescript');

function getDeclarationName(declaration) {
return (declaration.name ? declaration.name.escapedText :
`[${declaration.parameters.map((parameter) => parameter.getText()).join(', ')}]`) || '';
}

function getModifiers(declaration) {
let isPublic = false;
let isAsync = false;
declaration.modifiers?.forEach((modifier) => {
if (ts.SyntaxKind[modifier.kind] === 'PublicKeyword') isPublic = true;
if (ts.SyntaxKind[modifier.kind] === 'AsyncKeyword') isAsync = true;
})
return {isPublic, isAsync};
}

function isNodeExported(declaration) {
return (ts.getCombinedModifierFlags(declaration) & ts.ModifierFlags.Export) !== 0;
}

module.exports = {
getDeclarationName,
getModifiers,
isNodeExported
};
36 changes: 36 additions & 0 deletions pages/11ty/eslDoc/helpers/common/jsDoc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
function getJSDocFullText(declaration) {
const jsDoc = declaration.jsDoc && declaration.jsDoc[0];
if (!jsDoc) return;
return {
text: getJSDocComment(jsDoc),
tags: getJSDocTags(jsDoc)
};
}

function getJSDocComment(jsDoc) {
const comments = jsDoc.comment;
if (!comments) return;
return typeof comments === 'string' ?
comments :
comments.map((comment) => `${comment.name?.escapedText ?? ''}${comment.text ?? ''}`).join('');
}

function getJSDocTags(jsDoc) {
return jsDoc.tags?.map((tag) => getJSDocTag(tag));
}

function getJSDocTag(tag) {
const name = tag.tagName.escapedText;
if (name === 'param') return;
const comment = tag.comment;
return {
name,
link: tag.name?.name.escapedText,
text: typeof comment === 'string' ? comment : comment?.map((comment) => comment.text).join('\n')
};
}

module.exports = {
getJSDocFullText,
getJSDocComment,
};
27 changes: 27 additions & 0 deletions pages/11ty/eslDoc/helpers/common/type.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
const ts = require('typescript');
const {getDeclarationName} = require('./common');

function getTypeSignature(declaration) {
const type = declaration.type;
if (type) return type.getText();
if (declaration.flags) return '';

const statement = declaration.body?.statements?.filter((statement) => !!ts.isReturnStatement(statement));
if (!(statement && statement.length)) return '';
const expression = statement[0].expression;
return `(${expression?.parameters?.map((parameter) => parameter.getText())}) => ${expression?.type?.getText()}`;
}

function getGenericTypes(declaration) {
if (!declaration.typeParameters) return;
const parameters = declaration.typeParameters.map((type) => {
const name = getDeclarationName(type);
return {name, signature: type.getText().split(name)[1]};
});
return {parameters, signature: parameters.map((parameter) => parameter.name).join('| ')};
}

module.exports = {
getTypeSignature,
getGenericTypes
};
57 changes: 57 additions & 0 deletions pages/11ty/eslDoc/helpers/functions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
const {
getDeclarationName,
getJSDocFullText,
getGenericTypes,
getModifiers,
getArgumentsList,
getArgumentsSignature,
getTypeSignature
} = require('./common');

function addFunctionOverloads(declaration, output) {
const lastDeclaration = output[output.length - 1];
if (lastDeclaration && lastDeclaration.name === getDeclarationName(declaration)) {
return declaration.body ?
lastDeclaration.commonComment = getJSDocFullText(declaration) :
lastDeclaration.declarations.push(getFunctionDeclaration(declaration));
}

const functionDeclaration = getFunctionDeclaration(declaration);
output.push({
name: functionDeclaration.name,
type: functionDeclaration.type,
declarations: [functionDeclaration]
});
}

function getFunctionDeclaration(declaration) {
const type = 'Function';
const name = getDeclarationName(declaration);
const comment = getJSDocFullText(declaration);
const typeParameters = getGenericTypes(declaration);
const modifiers = getModifiers(declaration);
const parameters = getArgumentsList(declaration);
const returnType = getTypeSignature(declaration);
const argsSignature = getArgumentsSignature(parameters);
const signature = `${modifiers.isAsync ? 'async' : ''}${name}${typeParameters ? `<${typeParameters.signature}>` : ''}(${argsSignature})${returnType ? `: ${returnType}` : ''}`;
return {type, name, comment, typeParameters, modifiers, parameters, returnType, signature};
}

function getFunctionStatement(declaration) {
const statement = declaration.declarationList.declarations[0];
const type = 'Function statement';
const name = getDeclarationName(statement);
const comment = getJSDocFullText(declaration);
const typeParameters = getGenericTypes(statement.initializer);
const parameters = getArgumentsList(statement.initializer);
const returnType = getTypeSignature(statement.initializer);
const argsSignature = getArgumentsSignature(parameters);
const signature = `const ${name} = ${typeParameters ? `<${typeParameters.signature}>` : ''}(${argsSignature}) => ${returnType || 'void'}`;
return {type, name, comment, typeParameters, parameters, returnType, signature};
}

module.exports = {
addFunctionOverloads,
getFunctionDeclaration,
getFunctionStatement
};
48 changes: 48 additions & 0 deletions pages/11ty/eslDoc/helpers/type.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
const ts = require('typescript');
const {
getArgumentsList,
getArgumentsSignature,
getDeclarationName,
getJSDocFullText,
getGenericTypes,
getMemberList,
getTypeSignature
} = require('./common');

function getAlias(declaration) {
if (ts.isTypeLiteralNode(declaration.type)) return getTypeLiteral(declaration);
if (ts.isFunctionTypeNode(declaration.type)) return getFunctionType(declaration);
return getDefaultType(declaration);
}

function getDefaultType(declaration) {
const type = 'Type alias';
const name = getDeclarationName(declaration);
const comment = getJSDocFullText(declaration);
const signature = `${name} = ${declaration.type.getText()}`;
return {type, name, comment, signature};
}

function getTypeLiteral(declaration) {
const type = 'Type alias';
const name = getDeclarationName(declaration);
const comment = getJSDocFullText(declaration);
const typeParameters = getGenericTypes(declaration);
const parameters = getMemberList(declaration.type);
const signature = `${name} ${typeParameters ? `<${typeParameters.signature}>` : ''}`;
return {type, name, comment, typeParameters, parameters, signature};
}

function getFunctionType(declaration) {
const type = 'Function type';
const name = getDeclarationName(declaration);
const comment = getJSDocFullText(declaration);
const typeParameters = getGenericTypes(declaration);
const parameters = getArgumentsList(declaration.type);
const returnType = getTypeSignature(declaration.type);
const argsSignature = getArgumentsSignature(parameters);
const signature = `type ${name}${typeParameters ? `<${typeParameters.signature}>` : ''}(${argsSignature})${returnType ? `: ${returnType}` : ''}`;
return {type, name, comment, typeParameters, parameters, returnType, signature};
}

module.exports = {getAlias};
12 changes: 12 additions & 0 deletions pages/11ty/esldoc.shortcut.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
const {ESLDoc} = require('./eslDoc/eslDoc');
const nunjucks = require('nunjucks');
const path = require('path');

const generateDoc = (entryPoint) => {
const syntaxTree = ESLDoc.render(entryPoint);
return nunjucks.render(path.resolve(__dirname, '../views/_includes/eslDoc/render.njk'), {declarations: syntaxTree});
}

module.exports = (config) => {
config.addNunjucksShortcode('eslDoc', generateDoc);
};
2 changes: 1 addition & 1 deletion pages/11ty/site.config.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const fs = require('fs');
const path =require('path')
const path = require('path');
const yaml = require('js-yaml');

const content = fs.readFileSync(path.resolve(__dirname, '../site.yml'), 'utf8');
Expand Down
Loading