Skip to content

Commit eb4b27a

Browse files
author
nickpape-msft
authored
Merge pull request microsoft#39 from Microsoft/nickpape/api-extractor
Move @microsoft/api-extractor to web-build-tools
2 parents c7476b0 + aecc490 commit eb4b27a

70 files changed

Lines changed: 6781 additions & 180 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,3 +76,11 @@ See individual projects for details:
7676
[![Dependencies](https://david-dm.org/Microsoft/web-library-build.svg)](https://david-dm.org/Microsoft/web-library-build)
7777

7878

79+
# Utilities
80+
81+
### [@microsoft/api-extractor](./api-extractor/README.md)
82+
83+
`api-extractor` is a utility which can analyze TypeScript source code and extract the public API into a single file (in several formats, such as markdown or .d.ts). This is especially useful when doing API reviews.
84+
85+
[![npm version](https://badge.fury.io/js/%40microsoft%2Fapi-extractor.svg)](https://badge.fury.io/js/%40microsoft%2Fapi-extractor)
86+
[![Dependencies](https://david-dm.org/Microsoft/api-extractor.svg)](https://david-dm.org/Microsoft/api-extractor)

api-extractor/Debug.cmd

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
@ECHO OFF
2+
@SETLOCAL
3+
node-debug "%~dp0lib\DebugRun.js" %*

api-extractor/LICENSE

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
@microsoft/api-extractor
2+
3+
Copyright (c) Microsoft Corporation. All rights reserved.
4+
5+
MIT License
6+
7+
Permission is hereby granted, free of charge, to any person obtaining
8+
a copy of this software and associated documentation files (the
9+
"Software"), to deal in the Software without restriction, including
10+
without limitation the rights to use, copy, modify, merge, publish,
11+
distribute, sublicense, and/or sell copies of the Software, and to
12+
permit persons to whom the Software is furnished to do so, subject to
13+
the following conditions:
14+
15+
The above copyright notice and this permission notice shall be
16+
included in all copies or substantial portions of the Software.
17+
18+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
19+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
20+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
21+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
22+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
23+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
24+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

api-extractor/README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# api-extractor
2+
3+
A utility that analyzes a project, detects common JSDoc problems , and generates
4+
a report of the exported Public API.
5+

api-extractor/Run.cmd

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
@ECHO OFF
2+
@SETLOCAL
3+
node "%~dp0\lib\DebugRun.js" %*

api-extractor/gulpfile.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
'use strict';
2+
3+
const build = require('@microsoft/node-library-build');
4+
5+
build.initialize(require('gulp'));

api-extractor/package.json

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{
2+
"name": "@microsoft/api-extractor",
3+
"version": "0.0.1",
4+
"description": "Validatation, documentation, and auditing for the exported API of a TypeScript package",
5+
"main": "lib/index.js",
6+
"typings": "lib/index.d.ts",
7+
"license": "MIT",
8+
"scripts": {
9+
"build": "gulp",
10+
"clean": "gulp clean",
11+
"test": "gulp test"
12+
},
13+
"devDependencies": {
14+
"@types/chai": ">=3.4.34 <3.6.0",
15+
"@types/mocha": ">=2.2.33 <2.6.0",
16+
"chai": "~3.5.0",
17+
"gulp": "~3.9.1",
18+
"mocha": "~2.5.3",
19+
"@microsoft/node-library-build": "~2.0.0"
20+
},
21+
"dependencies": {
22+
"@types/es6-collections": "^0.5.29",
23+
"@types/fs-extra": "~0.0.34",
24+
"@types/node": ">=6.0.51 <6.9.1",
25+
"@types/z-schema": "3.16.20-alpha",
26+
"fs-extra": "~0.26.0",
27+
"jju": "~1.3.0",
28+
"typescript": "~2.0.3",
29+
"z-schema": "~3.17.0"
30+
}
31+
}

api-extractor/src/Analyzer.ts

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import * as ts from 'typescript';
2+
import ApiPackage from './definitions/ApiPackage';
3+
import TypeScriptHelpers from './TypeScriptHelpers';
4+
import DocItemLoader from './DocItemLoader';
5+
6+
export type ApiErrorHandler = (message: string, fileName: string, lineNumber: number) => void;
7+
8+
/**
9+
* Options for Analyzer.analyze()
10+
*/
11+
export interface IApiAnalyzerOptions {
12+
/**
13+
* Configuration for the TypeScript compiler. The most important options to set are:
14+
*
15+
* - target: ts.ScriptTarget.ES5
16+
* - module: ts.ModuleKind.CommonJS
17+
* - moduleResolution: ts.ModuleResolutionKind.NodeJs
18+
* - rootDir: inputFolder
19+
*/
20+
compilerOptions: ts.CompilerOptions;
21+
22+
/**
23+
* The entry point for the project. This should correspond to the "main" field
24+
* from NPM's package.json file. If it is a relative path, it will be relative to
25+
* the project folder described by IApiAnalyzerOptions.compilerOptions.
26+
*/
27+
entryPointFile: string;
28+
29+
/**
30+
* This can be used to specify other files that should be processed by the TypeScript compiler
31+
* for some reason, e.g. a "typings/tsd.d.ts" file. It is NOT necessary to specify files that
32+
* are explicitly imported/required by the entryPointFile, since the compiler will trace
33+
* (the transitive closure of) ordinary dependencies.
34+
*/
35+
otherFiles?: string[];
36+
}
37+
38+
/**
39+
* The main entry point for the "api-extractor" utility. The Analyzer object invokes the
40+
* TypeScript Compiler API to analyze a project, and constructs the ApiItem
41+
* abstract syntax tree.
42+
*/
43+
export default class Analyzer {
44+
public errorHandler: ApiErrorHandler;
45+
public typeChecker: ts.TypeChecker;
46+
public package: ApiPackage;
47+
/**
48+
* One DocItemLoader is needed per analyzer to look up external API members
49+
* as needed.
50+
*/
51+
public docItemLoader: DocItemLoader;
52+
53+
/**
54+
* The default implementation of ApiErrorHandler, which merely writes to console.log().
55+
*/
56+
public static defaultErrorHandler(message: string, fileName: string, lineNumber: number): void {
57+
console.log(`ERROR: [${fileName}:${lineNumber}] ${message}`);
58+
}
59+
60+
constructor(errorHandler?: ApiErrorHandler) {
61+
this.errorHandler = errorHandler || Analyzer.defaultErrorHandler;
62+
}
63+
64+
/**
65+
* Analyzes the specified project.
66+
*/
67+
public analyze(options: IApiAnalyzerOptions): void {
68+
this.docItemLoader = new DocItemLoader(options.compilerOptions.rootDir);
69+
const rootFiles: string[] = [options.entryPointFile].concat(options.otherFiles || []);
70+
71+
const program: ts.Program = ts.createProgram(rootFiles, options.compilerOptions);
72+
73+
// This runs a full type analysis, and then augments the Abstract Syntax Tree (i.e. declarations)
74+
// with semantic information (i.e. symbols). The "diagnostics" are a subset of the everyday
75+
// compile errors that would result from a full compilation.
76+
for (const diagnostic of program.getSemanticDiagnostics()) {
77+
this.reportError('TypeScript: ' + diagnostic.messageText, diagnostic.file, diagnostic.start);
78+
}
79+
80+
this.typeChecker = program.getTypeChecker();
81+
82+
const rootFile: ts.SourceFile = program.getSourceFile(options.entryPointFile);
83+
if (!rootFile) {
84+
throw new Error('Unable to load file: ' + options.entryPointFile);
85+
}
86+
const rootFileSymbol: ts.Symbol = TypeScriptHelpers.getSymbolForDeclaration(rootFile);
87+
88+
this.package = new ApiPackage(this, rootFileSymbol);
89+
}
90+
91+
/**
92+
* Reports an error message to the registered ApiErrorHandler.
93+
*/
94+
public reportError(message: string, sourceFile: ts.SourceFile, start: number): void {
95+
const lineNumber: number = sourceFile.getLineAndCharacterOfPosition(start).line;
96+
this.errorHandler(message, sourceFile.fileName, lineNumber);
97+
}
98+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import ApiPackage from './definitions/ApiPackage';
2+
import ApiItem from './definitions/ApiItem';
3+
import ApiEnum from './definitions/ApiEnum';
4+
import ApiEnumValue from './definitions/ApiEnumValue';
5+
import ApiFunction from './definitions/ApiFunction';
6+
import ApiStructuredType from './definitions/ApiStructuredType';
7+
import ApiMember from './definitions/ApiMember';
8+
import ApiMethod from './definitions/ApiMethod';
9+
import ApiParameter from './definitions/ApiParameter';
10+
import ApiProperty from './definitions/ApiProperty';
11+
import { ApiTag } from './definitions/ApiDocumentation';
12+
13+
/**
14+
* This is a helper class that provides a standard way to walk the ApiItem
15+
* abstract syntax tree.
16+
*/
17+
abstract class ApiItemVisitor {
18+
protected apiTagsToSkip: ApiTag[];
19+
20+
protected visit(apiItem: ApiItem, refObject?: Object): void {
21+
if (this.apiTagsToSkip && this.apiTagsToSkip.indexOf(apiItem.documentation.apiTag) >= 0) {
22+
return;
23+
}
24+
25+
if (apiItem instanceof ApiStructuredType) {
26+
this.visitApiStructuredType(apiItem as ApiStructuredType, refObject);
27+
} else if (apiItem instanceof ApiEnum) {
28+
this.visitApiEnum(apiItem as ApiEnum, refObject);
29+
} else if (apiItem instanceof ApiEnumValue) {
30+
this.visitApiEnumValue(apiItem as ApiEnumValue, refObject);
31+
} else if (apiItem instanceof ApiFunction) {
32+
this.visitApiFunction(apiItem as ApiFunction, refObject);
33+
} else if (apiItem instanceof ApiPackage) {
34+
this.visitApiPackage(apiItem as ApiPackage, refObject);
35+
} else if (apiItem instanceof ApiProperty) {
36+
this.visitApiProperty(apiItem as ApiProperty, refObject);
37+
} else if (apiItem instanceof ApiMethod) {
38+
this.visitApiMethod(apiItem as ApiMethod, refObject);
39+
} else if (apiItem instanceof ApiMember) {
40+
this.visitApiMember(apiItem as ApiMember, refObject);
41+
} else {
42+
throw new Error('Not implemented');
43+
}
44+
}
45+
46+
protected abstract visitApiStructuredType(apiStructuredType: ApiStructuredType, refObject?: Object): void;
47+
48+
protected abstract visitApiEnum(apiEnum: ApiEnum, refObject?: Object): void;
49+
50+
protected abstract visitApiEnumValue(apiEnumValue: ApiEnumValue, refObject?: Object): void;
51+
52+
protected abstract visitApiFunction(apiFunction: ApiFunction, refObject?: Object): void;
53+
54+
protected abstract visitApiPackage(apiPackage: ApiPackage, refObject?: Object): void;
55+
56+
protected abstract visitApiMember(apiMember: ApiMember, refObject?: Object): void;
57+
58+
protected visitApiMethod(apiMethod: ApiMethod, refObject?: Object): void {
59+
this.visitApiMember(apiMethod, refObject);
60+
};
61+
62+
protected visitApiProperty(apiProperty: ApiProperty, refObject?: Object): void {
63+
this.visitApiMember(apiProperty, refObject);
64+
};
65+
66+
protected abstract visitApiParam(apiParam: ApiParameter, refObject?: Object): void;
67+
}
68+
69+
export default ApiItemVisitor;

api-extractor/src/DebugRun.ts

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
// NOTE: THIS SOURCE FILE IS FOR DEBUGGING PURPOSES ONLY.
2+
// IT IS INVOKED BY THE "Run.cmd" AND "Debug.cmd" BATCH FILES.
3+
4+
import * as ts from 'typescript';
5+
import Analyzer from './Analyzer';
6+
import ApiFileGenerator from './generators/ApiFileGenerator';
7+
import TypeDocGenerator from './generators/TypeDocGenerator';
8+
import ApiJsonGenerator from './generators/ApiJsonGenerator';
9+
import { IDocItem } from './IDocItem';
10+
import { IApiDefinitionReference } from './IApiDefinitionReference';
11+
import DocItemLoader from './DocItemLoader';
12+
import TestFileComparer from './TestFileComparer';
13+
import JsonFile from './JsonFile';
14+
import ApiStructuredType from './definitions/ApiStructuredType';
15+
import ApiDocumentation from './definitions/ApiDocumentation';
16+
17+
const analyzer: Analyzer = new Analyzer();
18+
19+
/**
20+
* Dummy class wrapping ApiDocumentation to test its protected methods
21+
*/
22+
let myDocumentedClass: ApiStructuredType;
23+
class TestApiDocumentation extends ApiDocumentation {
24+
constructor() {
25+
super(myDocumentedClass, analyzer.docItemLoader, (msg: string) => { return; });
26+
}
27+
28+
public tokenizeDocs(docs: string): string[] {
29+
return this._tokenizeDocs(docs);
30+
}
31+
32+
public parseDocsBlock(tokens: string[], startingIndex: number, tagName?: string): string {
33+
return this._parseDocsBlock(tokens, startingIndex, tagName);
34+
}
35+
36+
public parseDocsInline(token: string): string {
37+
return this._parseDocsInline(token);
38+
}
39+
40+
public parseApiReferenceExpression(apiDefinitionRef: string): IApiDefinitionReference {
41+
return this._parseApiReferenceExpression(apiDefinitionRef);
42+
}
43+
}
44+
45+
analyzer.analyze({
46+
compilerOptions: {
47+
target: ts.ScriptTarget.ES5,
48+
module: ts.ModuleKind.CommonJS,
49+
moduleResolution: ts.ModuleResolutionKind.NodeJs,
50+
experimentalDecorators: true,
51+
jsx: ts.JsxEmit.React,
52+
rootDir: '../../spfx-core/sp-loader'
53+
},
54+
entryPointFile: '../../spfx-core/sp-loader/src/index.ts', // local/bundles/platform-exports.ts',
55+
otherFiles: ['../../spfx-core/sp-loader/typings/tsd.d.ts']
56+
});
57+
58+
const apiFileGenerator: ApiFileGenerator = new ApiFileGenerator();
59+
apiFileGenerator.writeApiFile('./lib/DebugRun.api.ts', analyzer);
60+
61+
const typeDocGenerator: TypeDocGenerator = new TypeDocGenerator();
62+
typeDocGenerator.writeApiFile('./lib/DebugRun.typedoc.ts', analyzer);
63+
64+
const apiJsonGenerator: ApiJsonGenerator = new ApiJsonGenerator();
65+
apiJsonGenerator.writeJsonFile('./lib/DebugRun.json', analyzer);
66+
67+
/**
68+
* Debugging inheritdoc expression parser.
69+
* Analyzer on example2 is needed for testing the parser.
70+
*/
71+
analyzer.analyze({
72+
compilerOptions: {
73+
target: ts.ScriptTarget.ES5,
74+
module: ts.ModuleKind.CommonJS,
75+
moduleResolution: ts.ModuleResolutionKind.NodeJs,
76+
experimentalDecorators: true,
77+
jsx: ts.JsxEmit.React,
78+
rootDir: './testInputs/example2'
79+
},
80+
entryPointFile: './testInputs/example2/index.ts', // local/bundles/platform-exports.ts',
81+
otherFiles: []
82+
});
83+
84+
myDocumentedClass = analyzer.package.getSortedMemberItems()
85+
.filter(apiItem => apiItem.name === 'MyDocumentedClass')[0] as ApiStructuredType;
86+
const apiDoc: TestApiDocumentation = new TestApiDocumentation();
87+
88+
/**
89+
* Put test cases here
90+
*/
91+
let apiReferenceExpr: string = '@microsoft/sp-core-library:Guid.equals';
92+
let actual: IApiDefinitionReference;
93+
actual = apiDoc.parseApiReferenceExpression(apiReferenceExpr);
94+
95+
apiReferenceExpr = '@microsoft/sp-core-library:Guid';
96+
actual = apiDoc.parseApiReferenceExpression(apiReferenceExpr);
97+
98+
apiReferenceExpr = 'sp-core-library:Guid';
99+
actual = apiDoc.parseApiReferenceExpression(apiReferenceExpr);
100+
101+
apiReferenceExpr = 'Guid.equals';
102+
actual = apiDoc.parseApiReferenceExpression(apiReferenceExpr);
103+
104+
apiReferenceExpr = 'Guid';
105+
actual = apiDoc.parseApiReferenceExpression(apiReferenceExpr);
106+
107+
// Should report error
108+
apiReferenceExpr = 'sp-core-library:Guid:equals';
109+
try {
110+
actual = apiDoc.parseApiReferenceExpression(apiReferenceExpr);
111+
} catch (error) {
112+
console.log(error);
113+
}
114+
115+
/**
116+
* Debugging DocItemLoader
117+
*/
118+
const apiDefinitionRef: IApiDefinitionReference = {
119+
scopeName: '@microsoft',
120+
packageName: 'sp-core-library',
121+
exportName: 'DisplayMode',
122+
memberName: ''
123+
};
124+
125+
const docItemLoader: DocItemLoader = new DocItemLoader('./testInputs/example2');
126+
/* tslint:disable:no-unused-variable */
127+
const apiDocItemNotInCache: IDocItem = docItemLoader.getItem(apiDefinitionRef);
128+
JsonFile.saveJsonFile('./lib/inheritedDoc-output.json', JSON.stringify(apiDocItemNotInCache));
129+
TestFileComparer.assertFileMatchesExpected('./lib/inheritedDoc-output.json', './testInputs/inheritedDoc-output.json');
130+
/* tslint:disable:no-unused-variable */
131+
const apiDocItemInCache: IDocItem = docItemLoader.getItem(apiDefinitionRef);

0 commit comments

Comments
 (0)