chore(doc-gen): add TypeScript parsing

This commit is contained in:
Peter Bacon Darwin 2015-05-15 13:57:38 +01:00
parent 25a952755e
commit 9a72f19b97
16 changed files with 546 additions and 19 deletions

View File

@ -3,8 +3,8 @@
{% block body %}
p.location-badge.
exported from <a href="/{$ doc.moduleDoc.path $}">{$ doc.moduleDoc.id $}</a>
defined in <a href="https://github.com/angular/angular/tree/master/modules/{$ doc.location.start.source.name $}.js#L{$ doc.location.start.line $}">{$ doc.location.start.source.name $}.js (line {$ doc.location.start.line $})</a>
exported from {@link {$ doc.moduleDoc.id $} {$doc.moduleDoc.id $} }
defined in <a href="https://github.com/angular/angular/tree/master/modules/{$ doc.fileInfo.relativePath $}#L{$ doc.location.start.line+1 $}-L{$ doc.location.end.line+1 $}">{$ doc.fileInfo.relativePath $} (line {$ doc.location.start.line+1 $})</a>
:markdown
{$ doc.description | indent(2, true) $}
@ -17,10 +17,10 @@ p.location-badge.
.l-sub-section
h3 {$ doc.constructorDoc.name $}
{% if doc.constructorDoc.params %}
{% if doc.constructorDoc.parameters %}
pre.prettyprint
code.
{$ doc.constructorDoc.name $}{$ paramList(doc.constructorDoc.params) | indent(4, true) | trim $}
{$ doc.constructorDoc.name $}{$ paramList(doc.constructorDoc.parameters) | indent(4, true) | trim $}
{% endif %}
:markdown
{$ doc.constructorDoc.description | indent(6, true) | replace('## Example', '') | replace('# Example', '') $}
@ -32,10 +32,10 @@ p.location-badge.
.l-sub-section
h3 {$ member.name $}
{% if member.params %}
{% if member.parameters %}
pre.prettyprint
code.
{$ member.name $}{$ paramList(member.params) | indent(4, true) | trim $}
{$ member.name $}{$ paramList(member.parameters) | indent(4, true) | trim $}{$ returnType(doc.returnType) $}
{% endif %}
:markdown

View File

@ -5,10 +5,15 @@
.l-main-section
h2(class="function export") {$ doc.name $}
p <code>{$ paramList(doc.parameters) $}</code>
{% if doc.parameters %}
pre.prettyprint
code.
{$ doc.name $}{$ paramList(doc.parameters) | indent(4, true) | trim $}{$ returnType(doc.returnType) $}
{% endif %}
p.location-badge.
exported from <a href="/{$ doc.moduleDoc.path $}">{$ doc.moduleDoc.id $}</a>
exported from {@link {$ doc.moduleDoc.id $} {$doc.moduleDoc.id $} }
defined in <a href="https://github.com/angular/angular/tree/master/modules/{$ doc.fileInfo.relativePath $}#L{$ doc.location.start.line+1 $}-L{$ doc.location.end.line+1 $}">{$ doc.fileInfo.relativePath $} (line {$ doc.location.start.line+1 $})</a>
:markdown
{$ doc.description | indent(4, true) $}

View File

@ -5,3 +5,8 @@
{%- endfor %})
{%- endif %}
{%- endmacro -%}
{% macro returnType(returnType) -%}
{%- if returnType %} : {$ returnType | escape $}{% endif -%}
{%- endmacro -%}

View File

@ -1,5 +1,8 @@
{% extends 'layout/base.template.html' -%}
{% block body -%}
p.location-badge.
defined in <a href="https://github.com/angular/angular/tree/master/modules/{$ doc.fileInfo.relativePath $}#L{$ doc.location.start.line+1 $}-L{$ doc.location.end.line+1 $}">{$ doc.fileInfo.relativePath $} (line {$ doc.location.start.line+1 $})</a>
ul
for page, slug in public.docs[current.path[1]][current.path[2]][current.path[3]][current.path[4]]._data
if slug != 'index'

View File

@ -15,6 +15,11 @@ module.exports = new Package('angular', [jsdocPackage, nunjucksPackage, linksPac
// Register the services and file readers
.factory(require('./services/modules'))
.factory(require('./services/tsParser'))
.factory(require('./services/tsParser/createCompilerHost'))
.factory(require('./services/tsParser/getFileInfo'))
.factory(require('./services/tsParser/getExportDocType'))
.factory(require('./services/tsParser/getContent'))
.factory(require('./readers/ngdoc'))
.factory('EXPORT_DOC_TYPES', function() {
@ -28,6 +33,7 @@ module.exports = new Package('angular', [jsdocPackage, nunjucksPackage, linksPac
// Register the processors
.processor(require('./processors/readTypeScriptModules'))
.processor(require('./processors/generateNavigationDoc'))
.processor(require('./processors/extractTitleFromGuides'))
.processor(require('./processors/createOverviewDump'))
@ -40,13 +46,23 @@ module.exports = new Package('angular', [jsdocPackage, nunjucksPackage, linksPac
// Configure file reading
.config(function(readFilesProcessor, ngdocFileReader) {
.config(function(readFilesProcessor, ngdocFileReader, readTypeScriptModules) {
readFilesProcessor.fileReaders = [ngdocFileReader];
readFilesProcessor.basePath = path.resolve(__dirname, '../..');
readFilesProcessor.sourceFiles = [
{ include: 'modules/*/docs/**/*.md', basePath: 'modules' },
{ include: 'docs/content/**/*.md', basePath: 'docs/content' }
];
readTypeScriptModules.sourceFiles = [
'*/*.js',
'*/src/**/*.js',
'*/*.es6',
'*/src/**/*.es6',
'*/*.ts',
'*/src/**/*.ts'
];
readTypeScriptModules.basePath = 'modules';
})
@ -54,6 +70,14 @@ module.exports = new Package('angular', [jsdocPackage, nunjucksPackage, linksPac
parseTagsProcessor.tagDefinitions.push(require('./tag-defs/public'));
parseTagsProcessor.tagDefinitions.push(require('./tag-defs/private'));
parseTagsProcessor.tagDefinitions.push(require('./tag-defs/exportedAs'));
// We actually don't want to parse param docs in this package as we are getting the data out using TS
parseTagsProcessor.tagDefinitions.forEach(function(tagDef) {
if (tagDef.name === 'param') {
tagDef.ignore = true;
}
});
})
@ -88,12 +112,6 @@ module.exports = new Package('angular', [jsdocPackage, nunjucksPackage, linksPac
// Configure ids and paths
.config(function(computeIdsProcessor, computePathsProcessor, EXPORT_DOC_TYPES) {
computeIdsProcessor.idTemplates.push({
docTypes: EXPORT_DOC_TYPES,
idTemplate: '${moduleDoc.id}.${name}',
getAliases: function(doc) { return [doc.id, doc.name]; }
});
computeIdsProcessor.idTemplates.push({
docTypes: ['member'],
idTemplate: '${classDoc.id}.${name}',
@ -117,7 +135,7 @@ module.exports = new Package('angular', [jsdocPackage, nunjucksPackage, linksPac
computePathsProcessor.pathTemplates.push({
docTypes: ['module'],
pathTemplate: '${id}',
pathTemplate: '/${id}',
outputPathTemplate: MODULES_DOCS_PATH + '/${id}/index.html'
});
@ -136,7 +154,7 @@ module.exports = new Package('angular', [jsdocPackage, nunjucksPackage, linksPac
computePathsProcessor.pathTemplates.push({
docTypes: ['guide'],
pathTemplate: '${id}',
pathTemplate: '/${id}',
outputPathTemplate: GUIDES_PATH + '/${id}.html'
});
});

View File

@ -0,0 +1 @@
export var x = 100;

View File

@ -0,0 +1,38 @@
/**
* @module
* @description
* This is the module description
*/
export * from 'importedSrc';
/**
* This is some random other comment
*/
/**
* This is MyClass
*/
export class MyClass {
message: String;
/**
* Create a new MyClass
* @param {String} name The name to say hello to
*/
constructor(name) {
this.message = 'hello ' + name;
}
/**
* Return a greeting message
*/
greet() {
return this.message;
}
}
/**
* An exported function
*/
export var myFn = (val:number) => { return val*2; }

View File

@ -0,0 +1,201 @@
var glob = require('glob');
var path = require('canonical-path');
var _ = require('lodash');
var ts = require('typescript');
module.exports = function readTypeScriptModules(tsParser, readFilesProcessor, modules, getFileInfo, getExportDocType, getContent, log) {
return {
$runAfter: ['files-read'],
$runBefore: ['parsing-tags'],
$validate: {
sourceFiles: {presence: true},
basePath: {presence: true},
hidePrivateMembers: { inclusion: [true, false] },
hideSpecialExports: { inclusion: [true, false] }
},
sourceFiles: [],
basePath: '.',
hidePrivateMembers: false,
hideSpecialExports: true,
$process: function(docs) {
var hideSpecialExports = this.hideSpecialExports;
var hidePrivateMembers = this.hidePrivateMembers;
var basePath = path.resolve(readFilesProcessor.basePath, this.basePath);
var filesPaths = expandSourceFiles(this.sourceFiles, basePath);
var parseInfo = tsParser.parse(filesPaths, this.basePath);
var moduleSymbols = parseInfo.moduleSymbols;
// Iterate through each of the modules that were parsed and generate a module doc
// as well as docs for each module's exports.
moduleSymbols.forEach(function(moduleSymbol) {
var moduleDoc = createModuleDoc(moduleSymbol, basePath);
// Add this module doc to the module lookup collection and the docs collection
modules[moduleDoc.id] = moduleDoc;
docs.push(moduleDoc);
// Iterate through this module's exports and generate a doc for each
moduleSymbol.exportArray.forEach(function(exportSymbol) {
// Ignore exports starting with an underscore
if (hideSpecialExports && exportSymbol.name.charAt(0) === '_') return;
// If the symbol is an Alias then for most things we want the original resolved symbol
var resolvedExport = exportSymbol.resolvedSymbol || exportSymbol;
var exportDoc = createExportDoc(exportSymbol.name, resolvedExport, moduleDoc, basePath, parseInfo.typeChecker);
log.debug('>>>> EXPORT: ' + exportDoc.name + ' (' + exportDoc.docType + ') from ' + moduleDoc.id);
// Generate docs for each of the export's members
if (resolvedExport.flags & ts.SymbolFlags.HasMembers) {
exportDoc.members = [];
for(var memberName in resolvedExport.members) {
log.silly('>>>>>> member: ' + memberName + ' from ' + exportDoc.id + ' in ' + moduleDoc.id);
var memberSymbol = resolvedExport.members[memberName];
var memberDoc = createMemberDoc(memberSymbol, exportDoc, basePath, parseInfo.typeChecker);
// We special case the constructor and sort the other members alphabetically
if (memberSymbol.flags & ts.SymbolFlags.Constructor) {
exportDoc.constructorDoc = memberDoc;
docs.push(memberDoc);
} else if (!hidePrivateMembers || memberSymbol.name.charAt(0) !== '_') {
docs.push(memberDoc);
insertSorted(exportDoc.members, memberDoc, 'name');
}
}
}
// Add this export doc to its module doc
moduleDoc.exports.push(exportDoc);
docs.push(exportDoc);
});
});
}
};
function createModuleDoc(moduleSymbol, basePath) {
var id = moduleSymbol.name.replace(/^"|"$/g, '');
var moduleDoc = {
docType: 'module',
id: id,
aliases: [id],
moduleTree: moduleSymbol,
content: getContent(moduleSymbol),
exports: [],
fileInfo: getFileInfo(moduleSymbol, basePath),
location: getLocation(moduleSymbol)
};
return moduleDoc;
}
function createExportDoc(name, exportSymbol, moduleDoc, basePath, typeChecker) {
var exportDoc = {
docType: getExportDocType(exportSymbol),
name: name,
id: name,
aliases: [name],
moduleDoc: moduleDoc,
content: getContent(exportSymbol),
fileInfo: getFileInfo(exportSymbol, basePath),
location: getLocation(exportSymbol)
};
if(exportSymbol.flags & ts.SymbolFlags.Function) {
exportDoc.parameters = getParameters(typeChecker, exportSymbol);
exportDoc.returnType = getReturnType(typeChecker, exportSymbol);
}
return exportDoc;
}
function createMemberDoc(memberSymbol, classDoc, basePath, typeChecker) {
var memberDoc = {
docType: 'member',
classDoc: classDoc,
name: memberSymbol.name,
id: memberSymbol.name,
content: getContent(memberSymbol),
fileInfo: getFileInfo(memberSymbol, basePath),
location: getLocation(memberSymbol)
};
if (memberSymbol.flags & ts.SymbolFlags.Method) {
// NOTE: we use the property name `parameters` here so we don't conflict
// with the `params` property that will be updated by dgeni reading the
// `@param` tags from the docs
memberDoc.parameters = getParameters(typeChecker, memberSymbol);
memberDoc.returnType = getReturnType(typeChecker, memberSymbol);
}
if (memberSymbol.flags & ts.SymbolFlags.Constructor) {
memberDoc.parameters = getParameters(typeChecker, memberSymbol);
memberDoc.name = 'constructor';
}
return memberDoc;
}
function getParameters(typeChecker, symbol) {
var declaration = symbol.valueDeclaration || symbol.declarations[0];
var sourceFile = ts.getSourceFileOfNode(declaration);
if(!declaration.parameters) {
console.log(declaration);
throw 'missing declaration parameters';
}
var signature = typeChecker.getSignatureFromDeclaration(declaration);
return declaration.parameters.map(function(parameter) {
return getText(sourceFile, parameter).trim();
});
}
function getReturnType(typeChecker, symbol) {
var declaration = symbol.valueDeclaration || symbol.declarations[0];
var sourceFile = ts.getSourceFileOfNode(declaration);
if(declaration.type) {
var signature = typeChecker.getSignatureFromDeclaration(declaration);
return getText(sourceFile, declaration.type).trim();
}
}
function expandSourceFiles(sourceFiles, basePath) {
var filePaths = [];
sourceFiles.forEach(function(sourcePattern) {
filePaths = filePaths.concat(glob.sync(sourcePattern, { cwd: basePath }));
});
return filePaths;
}
function getText(sourceFile, node) {
return sourceFile.text.substring(node.pos, node.end);
}
function getLocation(symbol) {
var node = symbol.valueDeclaration || symbol.declarations[0];
var sourceFile = ts.getSourceFileOfNode(node);
var location = {
start: ts.getLineAndCharacterOfPosition(sourceFile, node.pos),
end: ts.getLineAndCharacterOfPosition(sourceFile, node.end)
};
return location;
}
};
function insertSorted(collection, item, property) {
var index = collection.length;
while(index>0) {
if(collection[index-1][property] < item[property]) break;
index -= 1;
}
collection.splice(index, 0, item);
}

View File

@ -0,0 +1,68 @@
var ts = require('typescript');
module.exports = function tsParser(createCompilerHost, log) {
return {
options: {
allowNonTsExtensions: true
},
parse: function(fileNames, baseDir) {
// This is the easiest way I could find to ensure that we loaded
// modules with paths relative to the baseDir
process.chdir(baseDir);
// "Compile" a program from the given module filenames, to get hold of a
// typeChecker that can be used to interrogate the modules, exports and so on.
var host = createCompilerHost(this.options);
var program = ts.createProgram(fileNames, this.options, host);
var typeChecker = program.getTypeChecker();
// Create an array of module symbols for each file we were given
var moduleSymbols = [];
fileNames.forEach(function(fileName) {
var sourceFile = program.getSourceFile(fileName);
if (!sourceFile) {
throw new Error('Invalid source file: ' + fileName);
} else if (!sourceFile.symbol) {
// Some files contain only a comment and no actual module code
log.warn('No module code found in ' + fileName);
} else {
moduleSymbols.push(sourceFile.symbol);
}
});
moduleSymbols.forEach(function(tsModule) {
// The type checker has a nice helper function that returns an array of Symbols
// representing the exports for a given module
tsModule.exportArray = typeChecker.getExportsOfModule(tsModule);
// Although 'star' imports (e.g. `export * from 'some/module';) get resolved automatically
// by the compiler/binder, it seems that explicit imports (e.g. `export {SomeClass} from 'some/module'`)
// do not so we have to do a little work.
tsModule.exportArray.forEach(function(moduleExport) {
if (moduleExport.flags & 8388608 /* Alias */) {
// To maintain the alias information (particularly the alias name)
// we just attach the original "resolved" symbol to the alias symbol
moduleExport.resolvedSymbol = typeChecker.getAliasedSymbol(moduleExport);
}
});
});
moduleSymbols.typeChecker = typeChecker;
return {
moduleSymbols: moduleSymbols,
typeChecker: typeChecker,
program: program,
host: host
};
}
};
};

View File

@ -0,0 +1,21 @@
var mockPackage = require('../mocks/mockPackage');
var Dgeni = require('dgeni');
var path = require('canonical-path');
describe('tsParser', function() {
var dgeni, injector, parser;
beforeEach(function() {
dgeni = new Dgeni([mockPackage()]);
injector = dgeni.configureInjector();
parser = injector.get('tsParser');
});
it("should parse a TS file", function() {
var parseInfo = parser.parse(['testSrc.ts'], path.resolve(__dirname, '../mocks/'));
var tsModules = parseInfo.moduleSymbols;
expect(tsModules.length).toEqual(1);
expect(tsModules[0].exportArray.length).toEqual(3);
expect(tsModules[0].exportArray.map(function(i) { return i.name; })).toEqual(['MyClass', 'myFn', 'x']);
});
});

View File

@ -0,0 +1,32 @@
var ts = require('typescript');
// These are the extension that we should consider when trying to load a module
var extensions = ['.ts', '.js', '.es6']
// We need to provide our own version of CompilerHost because, at the moment, there is
// a mix of `.ts`, `.es6` and `.js` (atScript) files in the project and the TypeScript
// compiler only looks for `.ts` files when trying to load imports.
module.exports = function createCompilerHost(log) {
return function createCompilerHost(options) {
var host = ts.createCompilerHost(options);
// Override the `getSourceFile` implementation to also look for js and es6 files
var getSourceFile = host.getSourceFile;
host.getSourceFile = function(filename, languageVersion, onError) {
// Iterate through each possible extension and return the first source file that is actually found
for(var i=0; i<extensions.length; i++) {
var extension = extensions[i];
var altFileName = filename.replace(/\.ts$/, extension);
log.silly('getSourceFile:', altFileName);
var sourceFile = getSourceFile.call(host, altFileName, languageVersion, onError);
if(sourceFile) {
log.debug('found source file:', altFileName);
return sourceFile;
}
}
};
return host;
};
};

View File

@ -0,0 +1,49 @@
var ts = require('typescript');
var LEADING_STAR = /^[^\S\r\n]*\*[^\S\n\r]?/gm;
module.exports = function getContent() {
return function(symbol) {
var content = "";
if (!symbol.declarations) return content;
symbol.declarations.forEach(function(declaration) {
// If this is left side of dotted module declaration, there is no doc comment associated with this declaration
if (declaration.kind === ts.SyntaxKind.ModuleDeclaration && declaration.body.kind === ts.SyntaxKind.ModuleDeclaration) {
return content;
}
// If this is dotted module name, get the doc comments from the parent
while (declaration.kind === ts.SyntaxKind.ModuleDeclaration && declaration.parent.kind === ts.SyntaxKind.ModuleDeclaration) {
declaration = declaration.parent;
}
// If this is a variable declaration then we get the doc comments from the grand parent
if (declaration.kind === ts.SyntaxKind.VariableDeclaration) {
declaration = declaration.parent.parent;
}
// Get the source file of this declaration
var sourceFile = ts.getSourceFileOfNode(declaration);
var commentRanges = ts.getJsDocComments(declaration, sourceFile);
if (commentRanges) {
commentRanges.forEach(function(commentRange) {
content += sourceFile.text
.substring(commentRange.pos+ '/**'.length, commentRange.end - '*/'.length)
.replace(LEADING_STAR, '')
.trim()
if (commentRange.hasTrailingNewLine) {
content += '\n';
}
});
}
content += '\n';
});
return content;
};
};

View File

@ -0,0 +1,46 @@
var ts = require('typescript');
module.exports = function getExportDocType() {
return function(symbol) {
if(symbol.flags & ts.SymbolFlags.FunctionScopedVariable) {
return 'var';
}
if(symbol.flags & ts.SymbolFlags.BlockScopedVariable) {
return getBlockScopedVariableDocType(symbol);
}
if(symbol.flags & ts.SymbolFlags.Function) {
return 'function';
}
if(symbol.flags & ts.SymbolFlags.Class) {
return 'class';
}
if(symbol.flags & ts.SymbolFlags.Interface) {
return 'interface';
}
if(symbol.flags & ts.SymbolFlags.ConstEnum) {
return 'enum';
}
if(symbol.flags & ts.SymbolFlags.RegularEnum) {
return 'enum';
}
if(symbol.flags & ts.SymbolFlags.Property) {
return 'module-property';
}
log.warn('Unknown symbol type', symbol.name, symbol.flags, symbol.target);
return 'unknown';
}
function getBlockScopedVariableDocType(symbol) {
var node = symbol.valueDeclaration;
while(node) {
if ( node.flags & 0x2000 /* const */) {
return 'const';
}
node = node.parent;
}
return 'let';
}
};

View File

@ -0,0 +1,20 @@
var path = require('canonical-path');
var ts = require('typescript');
module.exports = function getFileInfo(log) {
return function (symbol, basePath) {
var fileName = ts.getSourceFileOfNode(symbol.declarations[0]).fileName;
var file = path.resolve(basePath, fileName);
var fileInfo = {
filePath: file,
baseName: path.basename(file, path.extname(file)),
extension: path.extname(file).replace(/^\./, ''),
basePath: basePath,
relativePath: fileName,
projectRelativePath: fileName
};
return fileInfo;
};
};

View File

@ -4,7 +4,8 @@
{% block body %}
<h1 class="class export">{$ doc.name $} <span class="type">class</span></h1>
<p class="module">exported from <a href="/{$ doc.moduleDoc.path $}">{$ doc.moduleDoc.id $}</a><br/>
defined in <a href="https://github.com/angular/angular/tree/master/modules/{$ doc.location.start.source.name $}.js#L{$ doc.location.start.line $}">{$ doc.location.start.source.name $}.js (line {$ doc.location.start.line $})</a></p>
defined in <a href="https://github.com/angular/angular/tree/master/modules/{$ doc.fileInfo.relativePath $}#L{$ doc.location.start.line+1 $}-L{$ doc.location.end.line+1 $}">
{$ doc.fileInfo.relativePath $} (line {$ doc.location.start.line+1 $})</a></p>
<p>{$ doc.description | marked $}</p>
{%- if doc.constructorDoc or doc.members.length -%}

View File

@ -3,6 +3,25 @@ var basePackage = require('../dgeni-package');
module.exports = new Package('angular-public', [basePackage])
.config(function(readTypeScriptModules) {
readTypeScriptModules.sourceFiles = [
'angular2/annotations.js',
'angular2/change_detection.ts',
'angular2/core.js',
'angular2/di.ts',
'angular2/directives.js',
'angular2/forms.js',
'angular2/router.js',
'angular2/test.js',
'angular2/pipes.js'
];
readTypeScriptModules.hidePrivateMembers = true;
})
.config(function(getLinkInfo) {
getLinkInfo.useFirstAmbiguousLink = false;
})
// Configure file writing
.config(function(writeFilesProcessor) {
writeFilesProcessor.outputFolder = 'dist/public_docs';