|
|
@@ -3,33 +3,65 @@ import klawSync from 'klaw-sync';
|
|
|
import path from 'path';
|
|
|
import ts from 'typescript';
|
|
|
|
|
|
+import { notNullOrUndefined } from '../shared/shared-utils';
|
|
|
+
|
|
|
// tslint:disable:no-console
|
|
|
+interface MethodParameterInfo {
|
|
|
+ name: string;
|
|
|
+ type: string;
|
|
|
+}
|
|
|
+
|
|
|
interface MemberInfo {
|
|
|
name: string;
|
|
|
description: string;
|
|
|
type: string;
|
|
|
+}
|
|
|
+
|
|
|
+interface PropertyInfo extends MemberInfo {
|
|
|
+ kind: 'property';
|
|
|
defaultValue: string;
|
|
|
}
|
|
|
|
|
|
+interface MethodInfo extends MemberInfo {
|
|
|
+ kind: 'method';
|
|
|
+ parameters: MethodParameterInfo[];
|
|
|
+}
|
|
|
+
|
|
|
interface InterfaceInfo {
|
|
|
title: string;
|
|
|
weight: number;
|
|
|
category: string;
|
|
|
description: string;
|
|
|
fileName: string;
|
|
|
- members: MemberInfo[];
|
|
|
+ members: Array<PropertyInfo | MethodInfo>;
|
|
|
}
|
|
|
|
|
|
+type TypeMap = Map<string, string>;
|
|
|
+
|
|
|
const docsPath = '/docs/api/';
|
|
|
const outputPath = path.join(__dirname, '../docs/content/docs/api');
|
|
|
const vendureConfig = path.join(__dirname, '../server/src/config/vendure-config.ts');
|
|
|
+const globalTypeMap: TypeMap = new Map();
|
|
|
+const tsFiles = klawSync(path.join(__dirname, '../server/src/'), {
|
|
|
+ nodir: true,
|
|
|
+ filter: item => {
|
|
|
+ return path.extname(item.path) === '.ts';
|
|
|
+ },
|
|
|
+ traverseAll: true,
|
|
|
+}).map(item => item.path);
|
|
|
|
|
|
deleteGeneratedDocs();
|
|
|
-generateDocs();
|
|
|
-console.log(`Watching for changes to ${vendureConfig}`);
|
|
|
-fs.watchFile(vendureConfig, { interval: 1000 }, () => {
|
|
|
- generateDocs();
|
|
|
-});
|
|
|
+generateDocs(tsFiles, globalTypeMap);
|
|
|
+
|
|
|
+const watchMode = !!process.argv.find(arg => arg === '--watch' || arg === '-w');
|
|
|
+if (watchMode) {
|
|
|
+ console.log(`Watching for changes to source files...`);
|
|
|
+ tsFiles.forEach(file => {
|
|
|
+ fs.watchFile(file, {interval: 1000}, () => {
|
|
|
+ generateDocs([file], globalTypeMap);
|
|
|
+ });
|
|
|
+ });
|
|
|
+}
|
|
|
|
|
|
/**
|
|
|
* Delete all generated docs found in the outputPath.
|
|
|
@@ -47,43 +79,67 @@ function deleteGeneratedDocs() {
|
|
|
console.log(`Deleted ${deleteCount} generated docs`);
|
|
|
}
|
|
|
|
|
|
+/**
|
|
|
+ * Returns true if the content matches that of a generated document.
|
|
|
+ */
|
|
|
function isGenerated(content: string) {
|
|
|
return /generated\: true\n---\n/.test(content);
|
|
|
}
|
|
|
|
|
|
-function generateDocs() {
|
|
|
+function generateDocs(filePaths: string[], typeMap: TypeMap) {
|
|
|
const timeStart = +new Date();
|
|
|
- const sourceFile = ts.createSourceFile(
|
|
|
- vendureConfig,
|
|
|
- fs.readFileSync(vendureConfig).toString(),
|
|
|
- ts.ScriptTarget.ES2015,
|
|
|
- true,
|
|
|
- );
|
|
|
- const knownTypeMap = new Map<string, string>();
|
|
|
-
|
|
|
- const interfaces = [...sourceFile.statements]
|
|
|
+ const sourceFiles = filePaths.map(filePath => {
|
|
|
+ return ts.createSourceFile(
|
|
|
+ filePath,
|
|
|
+ fs.readFileSync(filePath).toString(),
|
|
|
+ ts.ScriptTarget.ES2015,
|
|
|
+ true,
|
|
|
+ );
|
|
|
+ });
|
|
|
+
|
|
|
+ const statements = sourceFiles.reduce((st, sf) => [...st, ...sf.statements], [] as ts.Statement[]);
|
|
|
+ const interfaces = statements
|
|
|
.filter(ts.isInterfaceDeclaration)
|
|
|
.map(statement => {
|
|
|
const info = parseInterface(statement);
|
|
|
- knownTypeMap.set(info.title, info.category + '/' + info.fileName);
|
|
|
+ if (info) {
|
|
|
+ typeMap.set(info.title, info.category + '/' + info.fileName);
|
|
|
+ }
|
|
|
return info;
|
|
|
- });
|
|
|
+ })
|
|
|
+ .filter(notNullOrUndefined);
|
|
|
|
|
|
for (const info of interfaces) {
|
|
|
- const markdown = renderInterface(info, knownTypeMap);
|
|
|
- fs.writeFileSync(path.join(outputPath, info.category, info.fileName + '.md'), markdown);
|
|
|
+ const markdown = renderInterface(info, typeMap);
|
|
|
+ const categoryDir = path.join(outputPath, info.category);
|
|
|
+ const indexFile = path.join(categoryDir, '_index.md');
|
|
|
+ if (!fs.existsSync(categoryDir)) {
|
|
|
+ fs.mkdirSync(categoryDir);
|
|
|
+ }
|
|
|
+ if (!fs.existsSync(indexFile)) {
|
|
|
+ const indexFileContent = generateFrontMatter(info.category, 10) + `\n\n# ${info.category}`;
|
|
|
+ fs.writeFileSync(indexFile, indexFileContent);
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ fs.writeFileSync(path.join(categoryDir, info.fileName + '.md'), markdown);
|
|
|
}
|
|
|
|
|
|
- console.log(`Generated ${interfaces.length} docs in ${+new Date() - timeStart}ms`);
|
|
|
+ if (interfaces.length) {
|
|
|
+ console.log(`Generated ${interfaces.length} docs in ${+new Date() - timeStart}ms`);
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Parses an InterfaceDeclaration into a simple object which can be rendered into markdown.
|
|
|
*/
|
|
|
-function parseInterface(statement: ts.InterfaceDeclaration): InterfaceInfo {
|
|
|
+function parseInterface(statement: ts.InterfaceDeclaration): InterfaceInfo | undefined {
|
|
|
+ const category = getDocsCategory(statement);
|
|
|
+ if (category === undefined) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
const title = statement.name.text;
|
|
|
const weight = getInterfaceWeight(statement);
|
|
|
- const category = getDocsCategory(statement);
|
|
|
const description = getInterfaceDescription(statement);
|
|
|
const fileName = title.split(/(?=[A-Z])/).join('-').toLowerCase();
|
|
|
const members = parseMembers(statement.members);
|
|
|
@@ -100,15 +156,16 @@ function parseInterface(statement: ts.InterfaceDeclaration): InterfaceInfo {
|
|
|
/**
|
|
|
* Parses an array of inteface members into a simple object which can be rendered into markdown.
|
|
|
*/
|
|
|
-function parseMembers(members: ts.NodeArray<ts.TypeElement>): MemberInfo[] {
|
|
|
- const result: MemberInfo[] = [];
|
|
|
+function parseMembers(members: ts.NodeArray<ts.TypeElement>): Array<PropertyInfo | MethodInfo> {
|
|
|
+ const result: Array<PropertyInfo | MethodInfo> = [];
|
|
|
|
|
|
for (const member of members) {
|
|
|
- if (ts.isPropertySignature(member)) {
|
|
|
+ if (ts.isPropertySignature(member) || ts.isMethodSignature(member)) {
|
|
|
const name = member.name.getText();
|
|
|
let description = '';
|
|
|
let type = '';
|
|
|
let defaultValue = '';
|
|
|
+ let parameters: MethodParameterInfo[] = [];
|
|
|
parseTags(member, {
|
|
|
description: tag => description += tag.comment || '',
|
|
|
example: tag => description += formatExampleCode(tag.comment),
|
|
|
@@ -117,13 +174,27 @@ function parseMembers(members: ts.NodeArray<ts.TypeElement>): MemberInfo[] {
|
|
|
if (member.type) {
|
|
|
type = member.type.getFullText();
|
|
|
}
|
|
|
-
|
|
|
- result.push({
|
|
|
- name,
|
|
|
- description,
|
|
|
- type,
|
|
|
- defaultValue,
|
|
|
- });
|
|
|
+ if (ts.isMethodSignature(member)) {
|
|
|
+ parameters = member.parameters.map(p => ({
|
|
|
+ name: p.name.getText(),
|
|
|
+ type: p.type ? p.type.getFullText() : '',
|
|
|
+ }));
|
|
|
+ result.push({
|
|
|
+ kind: 'method',
|
|
|
+ name,
|
|
|
+ description,
|
|
|
+ type,
|
|
|
+ parameters,
|
|
|
+ });
|
|
|
+ } else {
|
|
|
+ result.push({
|
|
|
+ kind: 'property',
|
|
|
+ name,
|
|
|
+ description,
|
|
|
+ type,
|
|
|
+ defaultValue,
|
|
|
+ });
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -138,9 +209,19 @@ function renderInterface(interfaceInfo: InterfaceInfo, knownTypeMap: Map<string,
|
|
|
output += `${description}\n\n`;
|
|
|
|
|
|
for (const member of members) {
|
|
|
- const type = renderType(member.type, knownTypeMap);
|
|
|
+ let defaultParam = '';
|
|
|
+ let type = '';
|
|
|
+ if (member.kind === 'property') {
|
|
|
+ type = renderType(member.type, knownTypeMap);
|
|
|
+ defaultParam = member.defaultValue ? `default="${member.defaultValue}" ` : '';
|
|
|
+ } else {
|
|
|
+ const args = member.parameters.map(p => {
|
|
|
+ return `${p.name}: ${renderType(p.type, knownTypeMap)}`;
|
|
|
+ }).join(', ');
|
|
|
+ type = `(${args}) => ${renderType(member.type, knownTypeMap)}`;
|
|
|
+ }
|
|
|
output += `### ${member.name}\n\n`;
|
|
|
- output += `{{< member-info type="${type}" ${member.defaultValue ? `default="${member.defaultValue}" ` : ''}>}}\n\n`;
|
|
|
+ output += `{{< member-info type="${type}" ${defaultParam}>}}\n\n`;
|
|
|
output += `${member.description}\n\n`;
|
|
|
}
|
|
|
|
|
|
@@ -150,8 +231,8 @@ function renderInterface(interfaceInfo: InterfaceInfo, knownTypeMap: Map<string,
|
|
|
/**
|
|
|
* Extracts the "@docsCategory" value from the JSDoc comments if present.
|
|
|
*/
|
|
|
-function getDocsCategory(statement: ts.InterfaceDeclaration): string {
|
|
|
- let category = '';
|
|
|
+function getDocsCategory(statement: ts.InterfaceDeclaration): string | undefined {
|
|
|
+ let category: string | undefined;
|
|
|
parseTags(statement, {
|
|
|
docsCategory: tag => category = tag.comment || '',
|
|
|
});
|
|
|
@@ -173,10 +254,10 @@ function parseTags<T extends ts.Node>(node: T, tagMatcher: { [tagName: string]:
|
|
|
|
|
|
function renderType(type: string, knownTypeMap: Map<string, string>): string {
|
|
|
let typeText = type.trim().replace(/[\u00A0-\u9999<>\&]/gim, i => {
|
|
|
- return '&#' + i.charCodeAt(0) + ';';
|
|
|
- });
|
|
|
+ return '&#' + i.charCodeAt(0) + ';';
|
|
|
+ }).replace(/\n/, ' ');
|
|
|
for (const [key, val] of knownTypeMap) {
|
|
|
- typeText = typeText.replace(key, `<a href='${docsPath}${val}/'>${key}</a>`);
|
|
|
+ typeText = typeText.replace(key, `<a href='${docsPath}/${val}/'>${key}</a>`);
|
|
|
}
|
|
|
return typeText;
|
|
|
}
|