|
|
@@ -3,7 +3,17 @@ import klawSync from 'klaw-sync';
|
|
|
import path from 'path';
|
|
|
import ts from 'typescript';
|
|
|
|
|
|
-import { notNullOrUndefined } from '../shared/shared-utils';
|
|
|
+import { assertNever, notNullOrUndefined } from '../shared/shared-utils';
|
|
|
+
|
|
|
+// The absolute URL to the generated docs section
|
|
|
+const docsUrl = '/docs/configuration/';
|
|
|
+// The directory in which the markdown files will be saved
|
|
|
+const outputPath = path.join(__dirname, '../docs/content/docs/configuration');
|
|
|
+// The directories to scan for TypeScript source files
|
|
|
+const tsSourceDirs = [
|
|
|
+ '/server/src/',
|
|
|
+ '/shared/',
|
|
|
+];
|
|
|
|
|
|
// tslint:disable:no-console
|
|
|
interface MethodParameterInfo {
|
|
|
@@ -44,28 +54,33 @@ interface InterfaceInfo extends DeclarationInfo {
|
|
|
members: Array<PropertyInfo | MethodInfo>;
|
|
|
}
|
|
|
|
|
|
+interface ClassInfo extends DeclarationInfo {
|
|
|
+ kind: 'class';
|
|
|
+ members: Array<PropertyInfo | MethodInfo>;
|
|
|
+}
|
|
|
+
|
|
|
interface TypeAliasInfo extends DeclarationInfo {
|
|
|
kind: 'typeAlias';
|
|
|
type: string;
|
|
|
}
|
|
|
|
|
|
-type ValidDeclaration = ts.InterfaceDeclaration | ts.TypeAliasDeclaration;
|
|
|
+type ValidDeclaration = ts.InterfaceDeclaration | ts.TypeAliasDeclaration | ts.ClassDeclaration;
|
|
|
type TypeMap = Map<string, string>;
|
|
|
|
|
|
-const docsPath = '/docs/api/';
|
|
|
-const outputPath = path.join(__dirname, '../docs/content/docs/api');
|
|
|
-
|
|
|
/**
|
|
|
* This map is used to cache types and their corresponding Hugo path. It is used to enable
|
|
|
* hyperlinking from a member's "type" to the definition of that type.
|
|
|
*/
|
|
|
const globalTypeMap: TypeMap = new Map();
|
|
|
|
|
|
-const tsFiles = klawSync(path.join(__dirname, '../server/src/'), {
|
|
|
- nodir: true,
|
|
|
- filter: item => path.extname(item.path) === '.ts',
|
|
|
- traverseAll: true,
|
|
|
-}).map(item => item.path);
|
|
|
+const tsFiles = tsSourceDirs
|
|
|
+ .map(scanPath => klawSync( path.join(__dirname, '../', scanPath), {
|
|
|
+ nodir: true,
|
|
|
+ filter: item => path.extname(item.path) === '.ts',
|
|
|
+ traverseAll: true,
|
|
|
+ }))
|
|
|
+ .reduce((allFiles, files) => [...allFiles, ...files], [])
|
|
|
+ .map(item => item.path);
|
|
|
|
|
|
deleteGeneratedDocs();
|
|
|
generateDocs(tsFiles, outputPath, globalTypeMap);
|
|
|
@@ -133,11 +148,16 @@ function generateDocs(filePaths: string[], hugoApiDocsPath: string, typeMap: Typ
|
|
|
let markdown = '';
|
|
|
switch (info.kind) {
|
|
|
case 'interface':
|
|
|
- markdown = renderInterface(info, typeMap);
|
|
|
+ markdown = renderInterfaceOrClass(info, typeMap);
|
|
|
break;
|
|
|
case 'typeAlias':
|
|
|
markdown = renderTypeAlias(info, typeMap);
|
|
|
break;
|
|
|
+ case 'class':
|
|
|
+ markdown = renderInterfaceOrClass(info as any, typeMap);
|
|
|
+ break;
|
|
|
+ default:
|
|
|
+ assertNever(info);
|
|
|
}
|
|
|
|
|
|
const categoryDir = path.join(hugoApiDocsPath, info.category);
|
|
|
@@ -162,25 +182,30 @@ function generateDocs(filePaths: string[], hugoApiDocsPath: string, typeMap: Typ
|
|
|
* Maps an array of parsed SourceFiles into statements, including a reference to the original file each statement
|
|
|
* came from.
|
|
|
*/
|
|
|
-function getStatementsWithSourceLocation(sourceFiles: ts.SourceFile[]): Array<{ statement: ts.Statement; sourceFile: string; sourceLine: number; }> {
|
|
|
+function getStatementsWithSourceLocation(
|
|
|
+ sourceFiles: ts.SourceFile[],
|
|
|
+): Array<{ statement: ts.Statement; sourceFile: string; sourceLine: number }> {
|
|
|
return sourceFiles.reduce(
|
|
|
(st, sf) => {
|
|
|
const statementsWithSources = sf.statements.map(statement => {
|
|
|
- const serverSourceRoot = '/server/src';
|
|
|
- const sourceFile = sf.fileName.substring(sf.fileName.indexOf(serverSourceRoot));
|
|
|
+ const sourceFile = path.relative(path.join(__dirname, '..'), sf.fileName).replace(/\\/g, '/');
|
|
|
const sourceLine = sf.getLineAndCharacterOfPosition(statement.getStart()).line + 1;
|
|
|
- return {statement, sourceFile, sourceLine };
|
|
|
+ return { statement, sourceFile, sourceLine };
|
|
|
});
|
|
|
return [...st, ...statementsWithSources];
|
|
|
},
|
|
|
- [] as Array<{ statement: ts.Statement; sourceFile: string, sourceLine: number; }>,
|
|
|
+ [] as Array<{ statement: ts.Statement; sourceFile: string; sourceLine: number }>,
|
|
|
);
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Parses an InterfaceDeclaration into a simple object which can be rendered into markdown.
|
|
|
*/
|
|
|
-function parseDeclaration(statement: ts.Statement, sourceFile: string, sourceLine: number): InterfaceInfo | TypeAliasInfo | undefined {
|
|
|
+function parseDeclaration(
|
|
|
+ statement: ts.Statement,
|
|
|
+ sourceFile: string,
|
|
|
+ sourceLine: number,
|
|
|
+): InterfaceInfo | TypeAliasInfo | ClassInfo | undefined {
|
|
|
if (!isValidDeclaration(statement)) {
|
|
|
return;
|
|
|
}
|
|
|
@@ -188,7 +213,7 @@ function parseDeclaration(statement: ts.Statement, sourceFile: string, sourceLin
|
|
|
if (category === undefined) {
|
|
|
return;
|
|
|
}
|
|
|
- const title = statement.name.getText();
|
|
|
+ const title = statement.name ? statement.name.getText() : 'anonymous';
|
|
|
const fullText = getDeclarationFullText(statement);
|
|
|
const weight = getDeclarationWeight(statement);
|
|
|
const description = getDeclarationDescription(statement);
|
|
|
@@ -217,9 +242,15 @@ function parseDeclaration(statement: ts.Statement, sourceFile: string, sourceLin
|
|
|
} else if (ts.isTypeAliasDeclaration(statement)) {
|
|
|
return {
|
|
|
...info,
|
|
|
- type: statement.type.getFullText().trim(),
|
|
|
+ type: statement.type.getText().trim(),
|
|
|
kind: 'typeAlias',
|
|
|
};
|
|
|
+ } else if (ts.isClassDeclaration(statement)) {
|
|
|
+ return {
|
|
|
+ ...info,
|
|
|
+ kind: 'class',
|
|
|
+ members: parseMembers(statement.members),
|
|
|
+ };
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -227,7 +258,7 @@ function parseDeclaration(statement: ts.Statement, sourceFile: string, sourceLin
|
|
|
* Returns the declaration name plus any type parameters.
|
|
|
*/
|
|
|
function getDeclarationFullText(declaration: ValidDeclaration): string {
|
|
|
- const name = declaration.name.getText();
|
|
|
+ const name = declaration.name ? declaration.name.getText() : 'anonymous';
|
|
|
let typeParams = '';
|
|
|
if (declaration.typeParameters) {
|
|
|
typeParams = '<' + declaration.typeParameters.map(tp => tp.getText()).join(', ') + '>';
|
|
|
@@ -238,24 +269,43 @@ function getDeclarationFullText(declaration: ValidDeclaration): string {
|
|
|
/**
|
|
|
* Parses an array of inteface members into a simple object which can be rendered into markdown.
|
|
|
*/
|
|
|
-function parseMembers(members: ts.NodeArray<ts.TypeElement>): Array<PropertyInfo | MethodInfo> {
|
|
|
+function parseMembers(
|
|
|
+ members: ts.NodeArray<ts.TypeElement | ts.ClassElement>,
|
|
|
+): Array<PropertyInfo | MethodInfo> {
|
|
|
const result: Array<PropertyInfo | MethodInfo> = [];
|
|
|
|
|
|
for (const member of members) {
|
|
|
- if (ts.isPropertySignature(member) || ts.isMethodSignature(member)) {
|
|
|
- const name = member.name.getText();
|
|
|
+ const modifiers = member.modifiers ? member.modifiers.map(m => m.getText()) : [];
|
|
|
+ const isPrivate = modifiers.includes('private');
|
|
|
+ if (
|
|
|
+ !isPrivate && (
|
|
|
+ ts.isPropertySignature(member) ||
|
|
|
+ ts.isMethodSignature(member) ||
|
|
|
+ ts.isPropertyDeclaration(member) ||
|
|
|
+ ts.isMethodDeclaration(member) ||
|
|
|
+ ts.isConstructorDeclaration(member)
|
|
|
+ )
|
|
|
+ ) {
|
|
|
+ const name = member.name ? member.name.getText() : 'constructor';
|
|
|
let description = '';
|
|
|
let type = '';
|
|
|
let defaultValue = '';
|
|
|
let parameters: MethodParameterInfo[] = [];
|
|
|
- const fullText = member.getText();
|
|
|
+ let fullText = '';
|
|
|
+ if (ts.isConstructorDeclaration(member)) {
|
|
|
+ fullText = 'constructor';
|
|
|
+ } else if (ts.isMethodDeclaration(member)) {
|
|
|
+ fullText = member.name.getText();
|
|
|
+ } else {
|
|
|
+ fullText = member.getText();
|
|
|
+ }
|
|
|
parseTags(member, {
|
|
|
description: tag => (description += tag.comment || ''),
|
|
|
example: tag => (description += formatExampleCode(tag.comment)),
|
|
|
default: tag => (defaultValue = tag.comment || ''),
|
|
|
});
|
|
|
if (member.type) {
|
|
|
- type = member.type.getFullText().trim();
|
|
|
+ type = member.type.getText();
|
|
|
}
|
|
|
const memberInfo: MemberInfo = {
|
|
|
fullText,
|
|
|
@@ -263,10 +313,14 @@ function parseMembers(members: ts.NodeArray<ts.TypeElement>): Array<PropertyInfo
|
|
|
description,
|
|
|
type,
|
|
|
};
|
|
|
- if (ts.isMethodSignature(member)) {
|
|
|
+ if (
|
|
|
+ ts.isMethodSignature(member) ||
|
|
|
+ ts.isMethodDeclaration(member) ||
|
|
|
+ ts.isConstructorDeclaration(member)
|
|
|
+ ) {
|
|
|
parameters = member.parameters.map(p => ({
|
|
|
name: p.name.getText(),
|
|
|
- type: p.type ? p.type.getFullText() : '',
|
|
|
+ type: p.type ? p.type.getText() : '',
|
|
|
}));
|
|
|
result.push({
|
|
|
...memberInfo,
|
|
|
@@ -289,15 +343,15 @@ function parseMembers(members: ts.NodeArray<ts.TypeElement>): Array<PropertyInfo
|
|
|
/**
|
|
|
* Render the interface to a markdown string.
|
|
|
*/
|
|
|
-function renderInterface(interfaceInfo: InterfaceInfo, knownTypeMap: Map<string, string>): string {
|
|
|
- const { title, weight, category, description, members } = interfaceInfo;
|
|
|
+function renderInterfaceOrClass(info: InterfaceInfo | ClassInfo, knownTypeMap: Map<string, string>): string {
|
|
|
+ const { title, weight, category, description, members } = info;
|
|
|
let output = '';
|
|
|
output += generateFrontMatter(title, weight);
|
|
|
output += `\n\n# ${title}\n\n`;
|
|
|
- output += renderGenerationInfoShortcode(interfaceInfo);
|
|
|
- output += `${description}\n\n`;
|
|
|
+ output += renderGenerationInfoShortcode(info);
|
|
|
+ output += `${renderDescription(description, knownTypeMap)}\n\n`;
|
|
|
output += `## Signature\n\n`;
|
|
|
- output += renderInterfaceSignature(interfaceInfo);
|
|
|
+ output += info.kind === 'interface' ? renderInterfaceSignature(info) : renderClassSignature(info);
|
|
|
output += `## Members\n\n`;
|
|
|
|
|
|
for (const member of members) {
|
|
|
@@ -312,11 +366,16 @@ function renderInterface(interfaceInfo: InterfaceInfo, knownTypeMap: Map<string,
|
|
|
return `${p.name}: ${renderType(p.type, knownTypeMap)}`;
|
|
|
})
|
|
|
.join(', ');
|
|
|
- type = `(${args}) => ${renderType(member.type, knownTypeMap)}`;
|
|
|
+ if (member.fullText === 'constructor') {
|
|
|
+ type = `(${args}) => ${title}`;
|
|
|
+ } else {
|
|
|
+ type = `(${args}) => ${renderType(member.type, knownTypeMap)}`;
|
|
|
+ }
|
|
|
+
|
|
|
}
|
|
|
output += `### ${member.name}\n\n`;
|
|
|
output += `{{< member-info kind="${member.kind}" type="${type}" ${defaultParam}>}}\n\n`;
|
|
|
- output += `${member.description}\n\n`;
|
|
|
+ output += `${renderDescription(member.description, knownTypeMap)}\n\n`;
|
|
|
}
|
|
|
|
|
|
return output;
|
|
|
@@ -328,9 +387,36 @@ function renderInterface(interfaceInfo: InterfaceInfo, knownTypeMap: Map<string,
|
|
|
function renderInterfaceSignature(interfaceInfo: InterfaceInfo): string {
|
|
|
const { fullText, members } = interfaceInfo;
|
|
|
let output = '';
|
|
|
- output += `\`\`\`ts\n`;
|
|
|
+ output += `\`\`\`TypeScript\n`;
|
|
|
output += `interface ${fullText} {\n`;
|
|
|
- output += members.map(member => ` ${member.fullText}`).join(`\n`) ;
|
|
|
+ output += members.map(member => ` ${member.fullText}`).join(`\n`);
|
|
|
+ output += `\n}\n`;
|
|
|
+ output += `\`\`\`\n`;
|
|
|
+
|
|
|
+ return output;
|
|
|
+}
|
|
|
+
|
|
|
+function renderClassSignature(classInfo: ClassInfo): string {
|
|
|
+ const { fullText, members } = classInfo;
|
|
|
+ let output = '';
|
|
|
+ output += `\`\`\`TypeScript\n`;
|
|
|
+ output += `class ${fullText} {\n`;
|
|
|
+ output += members.map(member => {
|
|
|
+ if (member.kind === 'method') {
|
|
|
+ const args = member.parameters
|
|
|
+ .map(p => {
|
|
|
+ return `${p.name}: ${p.type}`;
|
|
|
+ })
|
|
|
+ .join(', ');
|
|
|
+ if (member.fullText === 'constructor') {
|
|
|
+ return ` constructor(${args})`;
|
|
|
+ } else {
|
|
|
+ return ` ${member.fullText}(${args}) => ${member.type};`;
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ return ` ${member.fullText}`;
|
|
|
+ }
|
|
|
+ }).join(`\n`);
|
|
|
output += `\n}\n`;
|
|
|
output += `\`\`\`\n`;
|
|
|
|
|
|
@@ -346,9 +432,9 @@ function renderTypeAlias(typeAliasInfo: TypeAliasInfo, knownTypeMap: Map<string,
|
|
|
output += generateFrontMatter(title, weight);
|
|
|
output += `\n\n# ${title}\n\n`;
|
|
|
output += renderGenerationInfoShortcode(typeAliasInfo);
|
|
|
- output += `${description}\n\n`;
|
|
|
+ output += `${renderDescription(description, knownTypeMap)}\n\n`;
|
|
|
output += `## Signature\n\n`;
|
|
|
- output += `\`\`\`ts\ntype ${fullText} = ${type};\n\`\`\``;
|
|
|
+ output += `\`\`\`TypeScript\ntype ${fullText} = ${type};\n\`\`\``;
|
|
|
|
|
|
return output;
|
|
|
}
|
|
|
@@ -388,7 +474,7 @@ function parseTags<T extends ts.Node>(
|
|
|
* This function takes a string representing a type (e.g. "Array<ShippingMethod>") and turns
|
|
|
* and known types (e.g. "ShippingMethod") into hyperlinks.
|
|
|
*/
|
|
|
-function renderType(type: string, knownTypeMap: Map<string, string>): string {
|
|
|
+function renderType(type: string, knownTypeMap: TypeMap): string {
|
|
|
let typeText = type
|
|
|
.trim()
|
|
|
// encode HTML entities
|
|
|
@@ -397,17 +483,29 @@ function renderType(type: string, knownTypeMap: Map<string, string>): string {
|
|
|
.replace(/\n/g, ' ');
|
|
|
|
|
|
for (const [key, val] of knownTypeMap) {
|
|
|
- typeText = typeText.replace(key, `<a href='${docsPath}/${val}/'>${key}</a>`);
|
|
|
+ const re = new RegExp(`\\b${key}\\b`, 'g');
|
|
|
+ typeText = typeText.replace(re, `<a href='${docsUrl}/${val}/'>${key}</a>`);
|
|
|
}
|
|
|
return typeText;
|
|
|
}
|
|
|
|
|
|
+/**
|
|
|
+ * Replaces any `{@link Foo}` references in the description with hyperlinks.
|
|
|
+ */
|
|
|
+function renderDescription(description: string, knownTypeMap: TypeMap): string {
|
|
|
+ for (const [key, val] of knownTypeMap) {
|
|
|
+ const re = new RegExp(`{@link\\s*${key}}`, 'g');
|
|
|
+ description = description.replace(re, `<a href='${docsUrl}/${val}/'>${key}</a>`);
|
|
|
+ }
|
|
|
+ return description;
|
|
|
+}
|
|
|
+
|
|
|
/**
|
|
|
* Generates the Hugo front matter with the title of the document
|
|
|
*/
|
|
|
function generateFrontMatter(title: string, weight: number, showToc: boolean = true): string {
|
|
|
return `---
|
|
|
-title: "${title}"
|
|
|
+title: "${title.replace('-', ' ')}"
|
|
|
weight: ${weight}
|
|
|
date: ${new Date().toISOString()}
|
|
|
showtoc: ${showToc}
|
|
|
@@ -435,6 +533,7 @@ function getDeclarationDescription(statement: ValidDeclaration): string {
|
|
|
let description = '';
|
|
|
parseTags(statement, {
|
|
|
description: tag => (description += tag.comment),
|
|
|
+ example: tag => (description += formatExampleCode(tag.comment)),
|
|
|
});
|
|
|
return description;
|
|
|
}
|
|
|
@@ -444,12 +543,16 @@ function getDeclarationDescription(statement: ValidDeclaration): string {
|
|
|
* wherein the asterisks are not stripped as they should be, see https://github.com/Microsoft/TypeScript/issues/23517)
|
|
|
*/
|
|
|
function formatExampleCode(example: string = ''): string {
|
|
|
- return '\n\n' + example.replace(/\n\s+\*\s/g, '');
|
|
|
+ return '\n\n*Example*\n\n' + example.replace(/\n\s+\*\s/g, '\n');
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Type guard for the types of statement which can ge processed by the doc generator.
|
|
|
*/
|
|
|
function isValidDeclaration(statement: ts.Statement): statement is ValidDeclaration {
|
|
|
- return ts.isInterfaceDeclaration(statement) || ts.isTypeAliasDeclaration(statement);
|
|
|
+ return (
|
|
|
+ ts.isInterfaceDeclaration(statement) ||
|
|
|
+ ts.isTypeAliasDeclaration(statement) ||
|
|
|
+ ts.isClassDeclaration(statement)
|
|
|
+ );
|
|
|
}
|