-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add a command to generate a Typescript class documentation in our format kuzdoc generate:js path/to/class
- Loading branch information
Showing
9 changed files
with
428 additions
and
1,018 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
import path from 'path' | ||
import { Command, flags } from '@oclif/command' | ||
import 'ts-node/register' | ||
|
||
import { ClassExtractor } from '../../lib/generator/javascript/ClassExtractor' | ||
import { MarkdownFormatter } from '../../lib/generator/javascript/MarkdownFormatter' | ||
|
||
const TEMPLATES_DIR = path.join(__dirname, '..', '..', 'lib', 'generator', 'javascript', 'templates') | ||
export default class GenerateJs extends Command { | ||
static description = `Generate the documentation of a class written in Typescript.` | ||
|
||
static flags = { | ||
path: flags.string({ | ||
description: 'Directory to write the doc', | ||
default: 'generated' | ||
}), | ||
help: flags.help({ char: 'h' }), | ||
} | ||
|
||
static args = [ | ||
{ name: 'filePath', description: 'File containing the class to generate the doc', required: true }, | ||
] | ||
|
||
async run() { | ||
const { args, flags } = this.parse(GenerateJs) | ||
|
||
this.log(`Generating documentation for the class defined in "${args.filePath}"`) | ||
|
||
const formatter = new MarkdownFormatter(flags.path, TEMPLATES_DIR) | ||
|
||
const extractor = new ClassExtractor(args.filePath) | ||
|
||
let className | ||
extractor.on('class', classInfo => { | ||
className = classInfo.name | ||
formatter.onClass(classInfo) | ||
}) | ||
|
||
extractor.on('method', methodInfo => { | ||
this.log(`Creates doc for method "${methodInfo.name}"`) | ||
}) | ||
|
||
extractor.extract() | ||
|
||
this.log(`Documentation for class "${className}" created`) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,150 @@ | ||
import { EventEmitter } from 'stream'; | ||
|
||
import { ClassDeclaration, JSDoc, MethodDeclaration, Project, SyntaxKind } from 'ts-morph'; | ||
|
||
export type InfoMethod = { | ||
name: string; | ||
signature: string; | ||
description: string; | ||
args: InfoMethodArgs[]; | ||
internal: boolean; | ||
returnType: string; | ||
returnTypeRaw: string; | ||
scope: 'public' | 'private' | 'protected'; | ||
} | ||
|
||
export type InfoMethodArgs = { | ||
name: string; | ||
description: string; | ||
type: string; | ||
typeRaw: string; | ||
optional: boolean; | ||
} | ||
|
||
export type InfoClass = { | ||
name: string; | ||
description: string; | ||
internal: boolean; | ||
methods: InfoMethod[]; | ||
} | ||
|
||
export class ClassExtractor extends EventEmitter { | ||
private filePath: string; | ||
|
||
public info: InfoClass | null = null; | ||
|
||
public methods: InfoMethod[] = []; | ||
|
||
constructor(filePath: string) { | ||
super(); | ||
|
||
this.filePath = filePath; | ||
} | ||
|
||
extract() { | ||
const project = new Project(); | ||
|
||
for (const classDeclaration of project.addSourceFileAtPath(this.filePath).getClasses()) { | ||
this.extractClass(classDeclaration); | ||
} | ||
} | ||
|
||
private extractClass(classDeclaration: ClassDeclaration) { | ||
try { | ||
const name = classDeclaration.getName() as string; | ||
|
||
const { description, internal } = this.extractClassProperties(classDeclaration); | ||
|
||
const methods = this.extractMethods(classDeclaration); | ||
|
||
this.info = { name, description, internal, methods }; | ||
|
||
this.emit('class', this.info); | ||
} | ||
catch (error) { | ||
throw new Error(`Cannot extract class "${classDeclaration.getName()}": ${error.message}${error.stack}`); | ||
} | ||
} | ||
|
||
private extractClassProperties(classDeclaration: ClassDeclaration) { | ||
const jsDoc = classDeclaration.getChildrenOfKind(SyntaxKind.JSDocComment)[0]; | ||
|
||
if (!jsDoc) { | ||
console.log(`[warn] Class "${classDeclaration.getName()}" does not have js doc comment`); | ||
return { internal: false, description: '' } | ||
} | ||
|
||
const internal = Boolean(jsDoc.getTags().find(tag => tag.getTagName() === 'internal')); | ||
|
||
const description = this.formatText(jsDoc.getComment()); | ||
|
||
return { internal, description } | ||
} | ||
|
||
private extractMethods(classDeclaration: ClassDeclaration): InfoMethod[] { | ||
const methods: InfoMethod[] = []; | ||
|
||
for (const method of classDeclaration.getMethods()) { | ||
const { description, args, internal } = this.extractMethodProperties(classDeclaration, method) | ||
|
||
const name = method.getName(); | ||
|
||
const scope = method.getScope(); | ||
|
||
const returnTypeRaw = method.getReturnType().getText(); | ||
const returnType = returnTypeRaw.replace(/import\(.*\)\./, ''); | ||
const signature = `${scope === 'public' ? '' : `${scope} `}${name} (${args.map(arg => `${arg.name}${arg.optional ? '?' : ''}: ${arg.type}`).join(', ')}): ${returnType}`; | ||
|
||
const methodInfo = { name, signature, description, args, internal, scope, returnType, returnTypeRaw }; | ||
|
||
methods.push(methodInfo); | ||
|
||
this.emit('method', methodInfo); | ||
} | ||
|
||
return methods; | ||
} | ||
|
||
private extractMethodProperties(classDeclaration: ClassDeclaration, method: MethodDeclaration) { | ||
const jsDoc = method.getChildrenOfKind(SyntaxKind.JSDocComment)[0]; | ||
|
||
if (!jsDoc) { | ||
console.log(`[warn] Method "${classDeclaration.getName()}.${method.getName()}" does not have js doc comment`); | ||
return { description: '', args: [], internal: false } | ||
} | ||
|
||
const description = this.formatText(jsDoc.getComment()); | ||
|
||
const args = this.getMethodArgs(jsDoc); | ||
|
||
const internal = Boolean(jsDoc.getTags().find(tag => tag.getTagName() === 'internal')); | ||
|
||
return { description, args, internal } | ||
} | ||
|
||
private getMethodArgs(jsDoc: JSDoc): InfoMethodArgs[] { | ||
const args: InfoMethodArgs[] = jsDoc.getTags() | ||
.filter(tag => tag.getTagName() === 'param') | ||
.map((tag: any) => { | ||
const typeRaw = tag.getSymbol().getValueDeclaration().getType().getText(); | ||
const type = typeRaw.replace(/import\(.*\)\./, ''); | ||
|
||
return { | ||
name: tag.getSymbol().getEscapedName(), | ||
description: this.formatText(tag.getComment()), | ||
type, | ||
typeRaw, | ||
// If someone find better than this ugly hack I'm in! | ||
optional: Boolean( | ||
tag.getSymbol().getValueDeclaration()['_compilerNode']['questionToken'] | ||
|| tag.getSymbol().getValueDeclaration()['_compilerNode']['initalizer']), | ||
}; | ||
}); | ||
|
||
return args; | ||
} | ||
|
||
private formatText(text: any) { | ||
return text.replace('\n\n', '\n') as string; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
import { readFileSync, writeFileSync, mkdirSync } from 'fs'; | ||
import path from 'path'; | ||
|
||
import _ from 'lodash'; | ||
|
||
import { InfoClass, InfoMethod } from './ClassExtractor'; | ||
|
||
function upFirst(string: string): string { | ||
return string.charAt(0).toUpperCase() + string.slice(1); | ||
} | ||
|
||
function kebabCase(string: string): string { | ||
return string | ||
// get all lowercase letters that are near to uppercase ones | ||
.replace(/([a-z])([A-Z])/g, '$1-$2') | ||
// replace all spaces and low dash | ||
.replace(/[\s_]+/g, '-') | ||
.toLowerCase(); | ||
} | ||
|
||
function contextualizeKeys(context: string, values: Record<string, any>) { | ||
const obj: Record<string, any> = {}; | ||
|
||
for (const [key, value] of Object.entries(values)) { | ||
obj[`${context}${upFirst(key)}`] = value; | ||
} | ||
|
||
return obj; | ||
} | ||
|
||
/** | ||
* "You who enter here, abandon all hope" | ||
* | ||
* If you really want to be completely disgusted about Typescript then I can advise you to start a project with Oclif. | ||
* The typescript compiler is so strict, it's keeping bothering you with complete useless nonsense. | ||
* | ||
* Additionally, it's simply bugged and will throw error when there is none, like here. | ||
* If I try to declare those as class attribute Oclif is complaining :) | ||
*/ | ||
let outputDir: any; | ||
let templateDir: any; | ||
let baseDir: any; | ||
|
||
export class MarkdownFormatter { | ||
constructor(outputDirArg: string, templateDirArg: string) { | ||
outputDir = outputDirArg; | ||
templateDir = templateDirArg; | ||
} | ||
|
||
onClass(classInfo: InfoClass) { | ||
if (classInfo.internal) { | ||
return; | ||
} | ||
|
||
baseDir = path.join(outputDir, kebabCase(classInfo.name)); | ||
|
||
const rootIndex = this.renderTemplate( | ||
['class', 'index.tpl.md'], | ||
contextualizeKeys('class', classInfo)); | ||
|
||
this.writeFile(['index.md'], rootIndex); | ||
|
||
const introduction = this.renderTemplate( | ||
['class', 'introduction', 'index.tpl.md'], | ||
contextualizeKeys('class', classInfo)); | ||
|
||
this.writeFile(['introduction', 'index.md'], introduction); | ||
|
||
for (const method of classInfo.methods) { | ||
this.onMethod(classInfo, method); | ||
} | ||
} | ||
|
||
onMethod(classInfo: InfoClass, infoMethod: InfoMethod) { | ||
if (infoMethod.internal) { | ||
return; | ||
} | ||
|
||
const method = this.renderTemplate( | ||
['class', 'method', 'index.tpl.md'], | ||
{ | ||
...contextualizeKeys('method', infoMethod), | ||
...contextualizeKeys('class', classInfo), | ||
}); | ||
|
||
this.writeFile([infoMethod.name, 'index.md'], method); | ||
} | ||
|
||
private writeFile(paths: string[], content: string) { | ||
const fullPath = path.join(baseDir as string, ...paths.map(p => kebabCase(p))); | ||
|
||
mkdirSync(path.dirname(fullPath), { recursive: true }); | ||
|
||
writeFileSync(fullPath, content); | ||
} | ||
|
||
private renderTemplate(paths: string[], values: Record<string, any> = {}): string { | ||
const fullPath = path.join(templateDir, ...paths); | ||
|
||
const compiled = _.template(readFileSync(fullPath, { encoding: 'utf-8' })); | ||
|
||
return compiled(values); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
--- | ||
code: true | ||
type: branch | ||
title: <%= className %> | ||
description: <%= className %> class documentation | ||
--- |
13 changes: 13 additions & 0 deletions
13
src/lib/generator/javascript/templates/class/introduction/index.tpl.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
--- | ||
code: false | ||
type: page | ||
title: Introduction | ||
description: <%= className %> class | ||
order: 0 | ||
--- | ||
|
||
# <%= className %> | ||
|
||
<SinceBadge version="auto-version" /> | ||
|
||
<%= classDescription %> |
23 changes: 23 additions & 0 deletions
23
src/lib/generator/javascript/templates/class/method/index.tpl.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
--- | ||
code: true | ||
type: page | ||
title: <%= methodName %> | ||
description: <%= className %> <%= methodName %> method | ||
--- | ||
|
||
# <%= methodName %> | ||
|
||
<SinceBadge version="auto-version" /> | ||
|
||
<%= methodDescription %> | ||
|
||
## Arguments | ||
|
||
```js | ||
<%= methodSignature %> | ||
``` | ||
|
||
| Argument | Type | Description | | ||
|----------|------|-------------| | ||
<% _.forEach(methodArgs, function(arg) { %>| `<%= arg.name %>` | <pre><%= arg.type %></pre> | <%= arg.description %> | | ||
<% }); %> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters