diff --git a/apps/api-documenter/package.json b/apps/api-documenter/package.json index 4acc0a4c3ed..060749e3f80 100644 --- a/apps/api-documenter/package.json +++ b/apps/api-documenter/package.json @@ -1,6 +1,6 @@ { "name": "@microsoft/api-documenter", - "version": "1.5.59", + "version": "6.0.0", "description": "Read JSON files from api-extractor, generate documentation pages", "repository": { "type": "git", @@ -18,6 +18,7 @@ "@microsoft/api-extractor": "6.3.0", "@microsoft/node-core-library": "3.7.0", "@microsoft/ts-command-line": "4.2.2", + "@microsoft/tsdoc": "0.12.4", "colors": "~1.2.1", "js-yaml": "~3.9.1" }, @@ -27,6 +28,7 @@ "@types/js-yaml": "3.9.1", "@types/node": "8.5.8", "gulp": "~3.9.1", - "@types/jest": "21.1.10" + "@types/jest": "21.1.10", + "jest": "~22.4.3" } } diff --git a/apps/api-documenter/src/cli/BaseAction.ts b/apps/api-documenter/src/cli/BaseAction.ts index 47492b0cf08..654530d1d42 100644 --- a/apps/api-documenter/src/cli/BaseAction.ts +++ b/apps/api-documenter/src/cli/BaseAction.ts @@ -7,10 +7,8 @@ import { CommandLineAction, CommandLineStringParameter } from '@microsoft/ts-command-line'; - import { FileSystem } from '@microsoft/node-core-library'; - -import { DocItemSet } from '../utils/DocItemSet'; +import { ApiModel } from '@microsoft/api-extractor'; export abstract class BaseAction extends CommandLineAction { protected inputFolder: string; @@ -38,8 +36,8 @@ export abstract class BaseAction extends CommandLineAction { }); } - protected buildDocItemSet(): DocItemSet { - const docItemSet: DocItemSet = new DocItemSet(); + protected buildApiModel(): ApiModel { + const apiModel: ApiModel = new ApiModel(); this.inputFolder = this._inputFolderParameter.value || './input'; if (!FileSystem.exists(this.inputFolder)) { @@ -53,10 +51,10 @@ export abstract class BaseAction extends CommandLineAction { if (filename.match(/\.api\.json$/i)) { console.log(`Reading ${filename}`); const filenamePath: string = path.join(this.inputFolder, filename); - docItemSet.loadApiJsonFile(filenamePath); + apiModel.loadPackage(filenamePath); } } - return docItemSet; + return apiModel; } } diff --git a/apps/api-documenter/src/cli/MarkdownAction.ts b/apps/api-documenter/src/cli/MarkdownAction.ts index d812b9d5cdd..36f7a08dc1e 100644 --- a/apps/api-documenter/src/cli/MarkdownAction.ts +++ b/apps/api-documenter/src/cli/MarkdownAction.ts @@ -3,8 +3,8 @@ import { ApiDocumenterCommandLine } from './ApiDocumenterCommandLine'; import { BaseAction } from './BaseAction'; -import { DocItemSet } from '../utils/DocItemSet'; -import { MarkdownDocumenter } from '../markdown/MarkdownDocumenter'; +import { MarkdownDocumenter } from '../documenters/MarkdownDocumenter'; +import { ApiModel } from '@microsoft/api-extractor'; export class MarkdownAction extends BaseAction { constructor(parser: ApiDocumenterCommandLine) { @@ -17,8 +17,8 @@ export class MarkdownAction extends BaseAction { } protected onExecute(): Promise { // override - const docItemSet: DocItemSet = this.buildDocItemSet(); - const markdownDocumenter: MarkdownDocumenter = new MarkdownDocumenter(docItemSet); + const apiModel: ApiModel = this.buildApiModel(); + const markdownDocumenter: MarkdownDocumenter = new MarkdownDocumenter(apiModel); markdownDocumenter.generateFiles(this.outputFolder); return Promise.resolve(); } diff --git a/apps/api-documenter/src/cli/YamlAction.ts b/apps/api-documenter/src/cli/YamlAction.ts index edf7a31d0d4..4b8059b65d7 100644 --- a/apps/api-documenter/src/cli/YamlAction.ts +++ b/apps/api-documenter/src/cli/YamlAction.ts @@ -7,10 +7,10 @@ import { import { ApiDocumenterCommandLine } from './ApiDocumenterCommandLine'; import { BaseAction } from './BaseAction'; -import { DocItemSet } from '../utils/DocItemSet'; -import { YamlDocumenter } from '../yaml/YamlDocumenter'; -import { OfficeYamlDocumenter } from '../yaml/OfficeYamlDocumenter'; +import { YamlDocumenter } from '../documenters/YamlDocumenter'; +import { OfficeYamlDocumenter } from '../documenters/OfficeYamlDocumenter'; +import { ApiModel } from '@microsoft/api-extractor'; export class YamlAction extends BaseAction { private _officeParameter: CommandLineFlagParameter; @@ -35,11 +35,11 @@ export class YamlAction extends BaseAction { } protected onExecute(): Promise { // override - const docItemSet: DocItemSet = this.buildDocItemSet(); + const apiModel: ApiModel = this.buildApiModel(); const yamlDocumenter: YamlDocumenter = this._officeParameter.value - ? new OfficeYamlDocumenter(docItemSet, this.inputFolder) - : new YamlDocumenter(docItemSet); + ? new OfficeYamlDocumenter(apiModel, this.inputFolder) + : new YamlDocumenter(apiModel); yamlDocumenter.generateFiles(this.outputFolder); return Promise.resolve(); diff --git a/apps/api-documenter/src/documenters/MarkdownDocumenter.ts b/apps/api-documenter/src/documenters/MarkdownDocumenter.ts new file mode 100644 index 00000000000..cd99242fad5 --- /dev/null +++ b/apps/api-documenter/src/documenters/MarkdownDocumenter.ts @@ -0,0 +1,748 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import * as path from 'path'; + +import { + PackageName, + FileSystem, + NewlineKind +} from '@microsoft/node-core-library'; +import { + DocSection, + DocPlainText, + DocLinkTag, + TSDocConfiguration, + StringBuilder, + DocNodeKind, + DocParagraph, + DocCodeSpan, + DocFencedCode, + StandardTags, + DocBlock, + DocComment +} from '@microsoft/tsdoc'; +import { + ApiModel, + ApiItem, + ApiEnum, + ApiPackage, + ApiItemKind, + ApiEntryPoint, + ApiReleaseTagMixin, + ApiDocumentedItem, + ApiClass, + ReleaseTag, + ApiDeclarationMixin, + ApiStaticMixin, + ApiPropertyItem, + ApiFunctionLikeMixin, + ApiInterface, + Excerpt +} from '@microsoft/api-extractor'; + +import { CustomDocNodes } from '../nodes/CustomDocNodeKind'; +import { DocHeading } from '../nodes/DocHeading'; +import { DocTable } from '../nodes/DocTable'; +import { DocEmphasisSpan } from '../nodes/DocEmphasisSpan'; +import { DocTableRow } from '../nodes/DocTableRow'; +import { DocTableCell } from '../nodes/DocTableCell'; +import { DocNoteBox } from '../nodes/DocNoteBox'; +import { Utilities } from '../utils/Utilities'; +import { CustomMarkdownEmitter } from '../markdown/CustomMarkdownEmitter'; + +/** + * Renders API documentation in the Markdown file format. + * For more info: https://en.wikipedia.org/wiki/Markdown + */ +export class MarkdownDocumenter { + private readonly _apiModel: ApiModel; + private readonly _tsdocConfiguration: TSDocConfiguration; + private readonly _markdownEmitter: CustomMarkdownEmitter; + private _outputFolder: string; + + public constructor(apiModel: ApiModel) { + this._apiModel = apiModel; + this._tsdocConfiguration = CustomDocNodes.configuration; + this._markdownEmitter = new CustomMarkdownEmitter(this._apiModel); + } + + public generateFiles(outputFolder: string): void { + this._outputFolder = outputFolder; + + console.log(); + this._deleteOldOutputFiles(); + + for (const apiPackage of this._apiModel.packages) { + console.log(`Writing ${apiPackage.name} package`); + this._writeApiItemPage(apiPackage); + } + } + + private _writeApiItemPage(apiItem: ApiItem): void { + const configuration: TSDocConfiguration = this._tsdocConfiguration; + const output: DocSection = new DocSection({ configuration: this._tsdocConfiguration }); + + this._writeBreadcrumb(output, apiItem); + + const scopedName: string = apiItem.getScopedNameWithinPackage(); + + switch (apiItem.kind) { + case ApiItemKind.Class: + output.appendNode(new DocHeading({ configuration, title: `${scopedName} class` })); + break; + case ApiItemKind.Enum: + output.appendNode(new DocHeading({ configuration, title: `${scopedName} enum` })); + break; + case ApiItemKind.Interface: + output.appendNode(new DocHeading({ configuration, title: `${scopedName} interface` })); + break; + case ApiItemKind.Method: + case ApiItemKind.MethodSignature: + output.appendNode(new DocHeading({ configuration, title: `${scopedName} method` })); + break; + case ApiItemKind.Namespace: + output.appendNode(new DocHeading({ configuration, title: `${scopedName} namespace` })); + break; + case ApiItemKind.Package: + const unscopedPackageName: string = PackageName.getUnscopedName(apiItem.name); + output.appendNode(new DocHeading({ configuration, title: `${unscopedPackageName} package` })); + break; + case ApiItemKind.Property: + case ApiItemKind.PropertySignature: + output.appendNode(new DocHeading({ configuration, title: `${scopedName} property` })); + break; + default: + throw new Error('Unsupported API item kind: ' + apiItem.kind); + } + + if (ApiReleaseTagMixin.isBaseClassOf(apiItem)) { + if (apiItem.releaseTag === ReleaseTag.Beta) { + this._writeBetaWarning(output); + } + } + + if (apiItem instanceof ApiDocumentedItem) { + const tsdocComment: DocComment | undefined = apiItem.tsdocComment; + + if (tsdocComment) { + + if (tsdocComment.deprecatedBlock) { + output.appendNode( + new DocNoteBox({ configuration: this._tsdocConfiguration }, + [ + new DocParagraph({ configuration: this._tsdocConfiguration }, [ + new DocPlainText({ + configuration: this._tsdocConfiguration, + text: 'Warning: This API is now obsolete. ' + }) + ]), + ...tsdocComment.deprecatedBlock.content.nodes + ] + ) + ); + } + + this._appendSection(output, tsdocComment.summarySection); + } + } + + if (ApiDeclarationMixin.isBaseClassOf(apiItem)) { + if (apiItem.excerpt.text.length > 0) { + output.appendNode( + new DocParagraph({ configuration }, [ + new DocEmphasisSpan({ configuration, bold: true}, [ + new DocPlainText({ configuration, text: 'Signature:' }) + ]) + ]) + ); + output.appendNode( + new DocFencedCode({ configuration, code: apiItem.getExcerptWithModifiers(), language: 'typescript' }) + ); + } + } + + switch (apiItem.kind) { + case ApiItemKind.Class: + this._writeClassTables(output, apiItem as ApiClass); + break; + case ApiItemKind.Enum: + this._writeEnumTables(output, apiItem as ApiEnum); + break; + case ApiItemKind.Interface: + this._writeInterfaceTables(output, apiItem as ApiInterface); + break; + case ApiItemKind.Method: + case ApiItemKind.MethodSignature: + this._writeFunctionLikeTables(output, apiItem as ApiFunctionLikeMixin); + break; + case ApiItemKind.Namespace: + break; + case ApiItemKind.Package: + this._writePackageTables(output, apiItem as ApiPackage); + break; + case ApiItemKind.Property: + case ApiItemKind.PropertySignature: + break; + default: + throw new Error('Unsupported API item kind: ' + apiItem.kind); + } + + if (apiItem instanceof ApiDocumentedItem) { + const tsdocComment: DocComment | undefined = apiItem.tsdocComment; + + if (tsdocComment) { + // Write the @remarks block + if (tsdocComment.remarksBlock) { + output.appendNode(new DocHeading({ configuration: this._tsdocConfiguration, title: 'Remarks' })); + this._appendSection(output, tsdocComment.remarksBlock.content); + } + + // Write the @example blocks + const exampleBlocks: DocBlock[] = tsdocComment.customBlocks.filter(x => x.blockTag.tagNameWithUpperCase + === StandardTags.example.tagNameWithUpperCase); + + let exampleNumber: number = 1; + for (const exampleBlock of exampleBlocks) { + const heading: string = exampleBlocks.length > 1 ? `Example ${exampleNumber}` : 'Example'; + + output.appendNode(new DocHeading({ configuration: this._tsdocConfiguration, title: heading })); + + this._appendSection(output, exampleBlock.content); + + ++exampleNumber; + } + } + } + + const filename: string = path.join(this._outputFolder, this._getFilenameForApiItem(apiItem)); + const stringBuilder: StringBuilder = new StringBuilder(); + + this._markdownEmitter.emit(stringBuilder, output, { + contextApiItem: apiItem, + onGetFilenameForApiItem: (apiItemForFilename: ApiItem) => { + return this._getLinkFilenameForApiItem(apiItemForFilename); + } + }); + + FileSystem.writeFile(filename, stringBuilder.toString(), { + convertLineEndings: NewlineKind.CrLf + }); + } + + /** + * GENERATE PAGE: PACKAGE + */ + private _writePackageTables(output: DocSection, apiPackage: ApiPackage): void { + const configuration: TSDocConfiguration = this._tsdocConfiguration; + + const classesTable: DocTable = new DocTable({ configuration, + headerTitles: [ 'Class', 'Description' ] + }); + + const enumerationsTable: DocTable = new DocTable({ configuration, + headerTitles: [ 'Enumeration', 'Description' ] + }); + + const functionsTable: DocTable = new DocTable({ configuration, + headerTitles: [ 'Function', 'Description' ] + }); + + const interfacesTable: DocTable = new DocTable({ configuration, + headerTitles: [ 'Interface', 'Description' ] + }); + + const namespacesTable: DocTable = new DocTable({ configuration, + headerTitles: [ 'Namespace', 'Description' ] + }); + + const apiEntryPoint: ApiEntryPoint = apiPackage.entryPoints[0]; + + for (const apiMember of apiEntryPoint.members) { + + const row: DocTableRow = new DocTableRow({ configuration }, [ + this._createTitleCell(apiMember), + this._createDescriptionCell(apiMember) + ]); + + switch (apiMember.kind) { + case ApiItemKind.Class: + classesTable.addRow(row); + this._writeApiItemPage(apiMember); + break; + + case ApiItemKind.Enum: + enumerationsTable.addRow(row); + this._writeApiItemPage(apiMember); + break; + + case ApiItemKind.Interface: + interfacesTable.addRow(row); + this._writeApiItemPage(apiMember); + break; + +/* + case 'function': + this._writeFunctionPage(docChild); + break; + case 'enum': + this._writeEnumPage(docChild); + break; + case ApiItemKind.Namespace: + this._writeNamespacePage(docChild); + break; +*/ + + } + } + + if (classesTable.rows.length > 0) { + output.appendNode(new DocHeading({ configuration: this._tsdocConfiguration, title: 'Classes' })); + output.appendNode(classesTable); + } + + if (enumerationsTable.rows.length > 0) { + output.appendNode(new DocHeading({ configuration: this._tsdocConfiguration, title: 'Enumerations' })); + output.appendNode(enumerationsTable); + } + if (functionsTable.rows.length > 0) { + output.appendNode(new DocHeading({ configuration: this._tsdocConfiguration, title: 'Functions' })); + output.appendNode(functionsTable); + } + + if (interfacesTable.rows.length > 0) { + output.appendNode(new DocHeading({ configuration: this._tsdocConfiguration, title: 'Interfaces' })); + output.appendNode(interfacesTable); + } + + if (namespacesTable.rows.length > 0) { + output.appendNode(new DocHeading({ configuration: this._tsdocConfiguration, title: 'Namespaces' })); + output.appendNode(namespacesTable); + } + } + + /** + * GENERATE PAGE: CLASS + */ + private _writeClassTables(output: DocSection, apiClass: ApiClass): void { + const configuration: TSDocConfiguration = this._tsdocConfiguration; + + const eventsTable: DocTable = new DocTable({ configuration, + headerTitles: [ 'Property', 'Modifiers', 'Type', 'Description' ] + }); + + const propertiesTable: DocTable = new DocTable({ configuration, + headerTitles: [ 'Property', 'Modifiers', 'Type', 'Description' ] + }); + + const methodsTable: DocTable = new DocTable({ configuration, + headerTitles: [ 'Method', 'Modifiers', 'Description' ] + }); + + for (const apiMember of apiClass.members) { + + switch (apiMember.kind) { + case ApiItemKind.Method: { + methodsTable.addRow( + new DocTableRow({ configuration }, [ + this._createTitleCell(apiMember), + this._createModifiersCell(apiMember), + this._createDescriptionCell(apiMember) + ]) + ); + + this._writeApiItemPage(apiMember); + break; + } + case ApiItemKind.Property: { + + if ((apiMember as ApiPropertyItem).isEventProperty) { + eventsTable.addRow( + new DocTableRow({ configuration }, [ + this._createTitleCell(apiMember), + this._createModifiersCell(apiMember), + this._createPropertyTypeCell(apiMember), + this._createDescriptionCell(apiMember) + ]) + ); + } else { + propertiesTable.addRow( + new DocTableRow({ configuration }, [ + this._createTitleCell(apiMember), + this._createModifiersCell(apiMember), + this._createPropertyTypeCell(apiMember), + this._createDescriptionCell(apiMember) + ]) + ); + } + + this._writeApiItemPage(apiMember); + break; + } + + } + } + + if (eventsTable.rows.length > 0) { + output.appendNode(new DocHeading({ configuration: this._tsdocConfiguration, title: 'Events' })); + output.appendNode(eventsTable); + } + + if (propertiesTable.rows.length > 0) { + output.appendNode(new DocHeading({ configuration: this._tsdocConfiguration, title: 'Properties' })); + output.appendNode(propertiesTable); + } + + if (methodsTable.rows.length > 0) { + output.appendNode(new DocHeading({ configuration: this._tsdocConfiguration, title: 'Methods' })); + output.appendNode(methodsTable); + } + } + + /** + * GENERATE PAGE: ENUM + */ + private _writeEnumTables(output: DocSection, apiEnum: ApiEnum): void { + const configuration: TSDocConfiguration = this._tsdocConfiguration; + + const enumMembersTable: DocTable = new DocTable({ configuration, + headerTitles: [ 'Member', 'Value', 'Description' ] + }); + + for (const apiEnumMember of apiEnum.members) { + enumMembersTable.addRow( + new DocTableRow({ configuration }, [ + + new DocTableCell({ configuration }, [ + new DocParagraph({ configuration }, [ + new DocPlainText({ configuration, text: Utilities.getConciseSignature(apiEnumMember) }) + ]) + ]), + + new DocTableCell({ configuration }, [ + new DocParagraph({ configuration }, [ + new DocCodeSpan({ configuration, code: apiEnumMember.initializerExcerpt.text }) + ]) + ]), + + this._createDescriptionCell(apiEnumMember) + ]) + ); + } + + if (enumMembersTable.rows.length > 0) { + output.appendNode(new DocHeading({ configuration: this._tsdocConfiguration, title: 'Enumeration Members' })); + output.appendNode(enumMembersTable); + } + } + + /** + * GENERATE PAGE: INTERFACE + */ + private _writeInterfaceTables(output: DocSection, apiClass: ApiClass): void { + const configuration: TSDocConfiguration = this._tsdocConfiguration; + + const eventsTable: DocTable = new DocTable({ configuration, + headerTitles: [ 'Property', 'Type', 'Description' ] + }); + + const propertiesTable: DocTable = new DocTable({ configuration, + headerTitles: [ 'Property', 'Type', 'Description' ] + }); + + const methodsTable: DocTable = new DocTable({ configuration, + headerTitles: [ 'Method', 'Description' ] + }); + + for (const apiMember of apiClass.members) { + + switch (apiMember.kind) { + case ApiItemKind.MethodSignature: { + methodsTable.addRow( + new DocTableRow({ configuration }, [ + this._createTitleCell(apiMember), + this._createDescriptionCell(apiMember) + ]) + ); + + this._writeApiItemPage(apiMember); + break; + } + case ApiItemKind.PropertySignature: { + + if ((apiMember as ApiPropertyItem).isEventProperty) { + eventsTable.addRow( + new DocTableRow({ configuration }, [ + this._createTitleCell(apiMember), + this._createPropertyTypeCell(apiMember), + this._createDescriptionCell(apiMember) + ]) + ); + } else { + propertiesTable.addRow( + new DocTableRow({ configuration }, [ + this._createTitleCell(apiMember), + this._createPropertyTypeCell(apiMember), + this._createDescriptionCell(apiMember) + ]) + ); + } + + this._writeApiItemPage(apiMember); + break; + } + + } + } + + if (eventsTable.rows.length > 0) { + output.appendNode(new DocHeading({ configuration: this._tsdocConfiguration, title: 'Events' })); + output.appendNode(eventsTable); + } + + if (propertiesTable.rows.length > 0) { + output.appendNode(new DocHeading({ configuration: this._tsdocConfiguration, title: 'Properties' })); + output.appendNode(propertiesTable); + } + + if (methodsTable.rows.length > 0) { + output.appendNode(new DocHeading({ configuration: this._tsdocConfiguration, title: 'Methods' })); + output.appendNode(methodsTable); + } + } + + /** + * GENERATE PAGE: FUNCTION-LIKE + */ + private _writeFunctionLikeTables(output: DocSection, apiFunctionLike: ApiFunctionLikeMixin): void { + const configuration: TSDocConfiguration = this._tsdocConfiguration; + + const parametersTable: DocTable = new DocTable({ configuration, + headerTitles: [ 'Parameter', 'Type', 'Description' ] + }); + + for (const apiParameter of apiFunctionLike.parameters) { + const parameterDescription: DocSection = new DocSection({ configuration } ); + if (apiParameter.tsdocParamBlock) { + this._appendSection(parameterDescription, apiParameter.tsdocParamBlock.content); + } + + parametersTable.addRow( + new DocTableRow({ configuration }, [ + new DocTableCell({configuration}, [ + new DocParagraph({ configuration }, [ + new DocPlainText({ configuration, text: apiParameter.name }) + ]) + ]), + new DocTableCell({configuration}, [ + new DocParagraph({ configuration }, [ + new DocCodeSpan({ configuration, code: apiParameter.parameterTypeExcerpt.text }) + ]) + ]), + new DocTableCell({configuration}, parameterDescription.nodes) + ]) + ); + } + + if (parametersTable.rows.length > 0) { + output.appendNode(new DocHeading({ configuration: this._tsdocConfiguration, title: 'Parameters' })); + output.appendNode(parametersTable); + } + + if (ApiDeclarationMixin.isBaseClassOf(apiFunctionLike)) { + + const returnTypeExcerpt: Excerpt | undefined = apiFunctionLike.embeddedExcerptsByName.get('returnType'); + if (returnTypeExcerpt !== undefined) { + + output.appendNode( + new DocParagraph({ configuration }, [ + new DocEmphasisSpan({ configuration, bold: true}, [ + new DocPlainText({ configuration, text: 'Returns:' }) + ]) + ]) + ); + + output.appendNode( + new DocParagraph({ configuration }, [ + new DocCodeSpan({ configuration, code: returnTypeExcerpt.text }) + ]) + ); + + if (apiFunctionLike instanceof ApiDocumentedItem) { + if (apiFunctionLike.tsdocComment) { + if (apiFunctionLike.tsdocComment.returnsBlock) { + this._appendSection(output, apiFunctionLike.tsdocComment.returnsBlock.content); + } + } + } + + } + + } + } + + private _createTitleCell(apiItem: ApiItem): DocTableCell { + const configuration: TSDocConfiguration = this._tsdocConfiguration; + + return new DocTableCell({ configuration }, [ + new DocParagraph({ configuration }, [ + new DocLinkTag({ + configuration, + tagName: '@link', + linkText: Utilities.getConciseSignature(apiItem), + urlDestination: this._getLinkFilenameForApiItem(apiItem) + }) + ]) + ]); + } + + private _createDescriptionCell(apiItem: ApiItem): DocTableCell { + const configuration: TSDocConfiguration = this._tsdocConfiguration; + + const section: DocSection = new DocSection({ configuration }); + + if (ApiReleaseTagMixin.isBaseClassOf(apiItem)) { + if (apiItem.releaseTag === ReleaseTag.Beta) { + section.appendNodesInParagraph([ + new DocEmphasisSpan({ configuration, bold: true, italic: true }, [ + new DocPlainText({ configuration, text: '(BETA)' }) + ]), + new DocPlainText({ configuration, text: ' ' }) + ]); + } + } + + if (apiItem instanceof ApiDocumentedItem) { + if (apiItem.tsdocComment !== undefined) { + this._appendAndMergeSection(section, apiItem.tsdocComment.summarySection); + } + } + + return new DocTableCell({ configuration }, section.nodes); + } + + private _createModifiersCell(apiItem: ApiItem): DocTableCell { + const configuration: TSDocConfiguration = this._tsdocConfiguration; + + const section: DocSection = new DocSection({ configuration }); + + if (ApiStaticMixin.isBaseClassOf(apiItem)) { + if (apiItem.isStatic) { + section.appendNodeInParagraph(new DocCodeSpan({ configuration, code: 'static' })); + } + } + + return new DocTableCell({ configuration }, section.nodes); + } + + private _createPropertyTypeCell(apiItem: ApiItem): DocTableCell { + const configuration: TSDocConfiguration = this._tsdocConfiguration; + + const section: DocSection = new DocSection({ configuration }); + + if (apiItem instanceof ApiPropertyItem) { + section.appendNodeInParagraph(new DocCodeSpan({ configuration, code: apiItem.propertyTypeExcerpt.text })); + } + + return new DocTableCell({ configuration }, section.nodes); + } + + private _writeBreadcrumb(output: DocSection, apiItem: ApiItem): void { + output.appendNodeInParagraph(new DocLinkTag({ + configuration: this._tsdocConfiguration, + tagName: '@link', + linkText: 'Home', + urlDestination: './index' + })); + + for (const hierarchyItem of apiItem.getHierarchy()) { + switch (hierarchyItem.kind) { + case ApiItemKind.Model: + case ApiItemKind.EntryPoint: + break; + default: + output.appendNodesInParagraph([ + new DocPlainText({ + configuration: this._tsdocConfiguration, + text: ' > ' + }), + new DocLinkTag({ + configuration: this._tsdocConfiguration, + tagName: '@link', + linkText: hierarchyItem.name, + urlDestination: this._getLinkFilenameForApiItem(hierarchyItem) + }) + ]); + } + } + } + + private _writeBetaWarning(output: DocSection): void { + const configuration: TSDocConfiguration = this._tsdocConfiguration; + const betaWarning: string = 'This API is provided as a preview for developers and may change' + + ' based on feedback that we receive. Do not use this API in a production environment.'; + output.appendNode( + new DocNoteBox({ configuration }, [ + new DocParagraph({ configuration }, [ + new DocPlainText({ configuration, text: betaWarning }) + ]) + ]) + ); + } + + private _appendSection(output: DocSection, docSection: DocSection): void { + for (const node of docSection.nodes) { + output.appendNode(node); + } + } + + private _appendAndMergeSection(output: DocSection, docSection: DocSection): void { + let firstNode: boolean = true; + for (const node of docSection.nodes) { + if (firstNode) { + if (node.kind === DocNodeKind.Paragraph) { + output.appendNodesInParagraph(node.getChildNodes()); + firstNode = false; + continue; + } + } + firstNode = false; + + output.appendNode(node); + } + } + + private _getFilenameForApiItem(apiItem: ApiItem): string { + let baseName: string = ''; + for (const hierarchyItem of apiItem.getHierarchy()) { + // For overloaded methods, add a suffix such as "MyClass.myMethod_2". + let qualifiedName: string = hierarchyItem.name; + if (ApiFunctionLikeMixin.isBaseClassOf(hierarchyItem)) { + if (hierarchyItem.overloadIndex > 0) { + qualifiedName += `_${hierarchyItem.overloadIndex}`; + } + } + + switch (hierarchyItem.kind) { + case ApiItemKind.Model: + case ApiItemKind.EntryPoint: + break; + case ApiItemKind.Package: + baseName = PackageName.getUnscopedName(hierarchyItem.name); + break; + default: + baseName += '.' + qualifiedName; + } + } + return baseName.toLowerCase() + '.md'; + } + + private _getLinkFilenameForApiItem(apiItem: ApiItem): string { + return './' + this._getFilenameForApiItem(apiItem); + } + + private _deleteOldOutputFiles(): void { + console.log('Deleting old output from ' + this._outputFolder); + FileSystem.ensureEmptyFolder(this._outputFolder); + } +} diff --git a/apps/api-documenter/src/yaml/OfficeYamlDocumenter.ts b/apps/api-documenter/src/documenters/OfficeYamlDocumenter.ts similarity index 87% rename from apps/api-documenter/src/yaml/OfficeYamlDocumenter.ts rename to apps/api-documenter/src/documenters/OfficeYamlDocumenter.ts index 492b8936c3b..d91f2e8d329 100644 --- a/apps/api-documenter/src/yaml/OfficeYamlDocumenter.ts +++ b/apps/api-documenter/src/documenters/OfficeYamlDocumenter.ts @@ -5,12 +5,13 @@ import * as colors from 'colors'; import * as path from 'path'; import yaml = require('js-yaml'); -import { DocItemSet } from '../utils/DocItemSet'; -import { IYamlTocItem } from './IYamlTocFile'; -import { IYamlItem } from './IYamlApiFile'; -import { YamlDocumenter } from './YamlDocumenter'; +import { ApiModel } from '@microsoft/api-extractor'; import { Text, FileSystem } from '@microsoft/node-core-library'; +import { IYamlTocItem } from '../yaml/IYamlTocFile'; +import { IYamlItem } from '../yaml/IYamlApiFile'; +import { YamlDocumenter } from './YamlDocumenter'; + interface ISnippetsFile { /** * The keys are API names like "Excel.Range.clear". @@ -37,8 +38,8 @@ export class OfficeYamlDocumenter extends YamlDocumenter { 'Word': '/office/dev/add-ins/reference/requirement-sets/word-api-requirement-sets' }; - public constructor(docItemSet: DocItemSet, inputFolder: string) { - super(docItemSet); + public constructor(apiModel: ApiModel, inputFolder: string) { + super(apiModel); const snippetsFilePath: string = path.join(inputFolder, 'snippets.yaml'); @@ -48,7 +49,8 @@ export class OfficeYamlDocumenter extends YamlDocumenter { this._snippets = yaml.load(snippetsContent, { filename: snippetsFilePath }); } - public generateFiles(outputFolder: string): void { // override + /** @override */ + public generateFiles(outputFolder: string): void { super.generateFiles(outputFolder); // After we generate everything, check for any unused snippets @@ -58,6 +60,7 @@ export class OfficeYamlDocumenter extends YamlDocumenter { } } + /** @override */ protected onGetTocRoot(): IYamlTocItem { // override return { name: 'API reference', @@ -66,22 +69,20 @@ export class OfficeYamlDocumenter extends YamlDocumenter { }; } - protected onCustomizeYamlItem(yamlItem: IYamlItem): void { // override + /** @override */ + protected onCustomizeYamlItem(yamlItem: IYamlItem): void { const nameWithoutPackage: string = yamlItem.uid.replace(/^[^.]+\./, ''); if (yamlItem.summary) { yamlItem.summary = this._fixupApiSet(yamlItem.summary, yamlItem.uid); yamlItem.summary = this._fixBoldAndItalics(yamlItem.summary); - yamlItem.summary = this._fixCodeTicks(yamlItem.summary); } if (yamlItem.remarks) { yamlItem.remarks = this._fixupApiSet(yamlItem.remarks, yamlItem.uid); yamlItem.remarks = this._fixBoldAndItalics(yamlItem.remarks); - yamlItem.remarks = this._fixCodeTicks(yamlItem.remarks); } if (yamlItem.syntax && yamlItem.syntax.parameters) { yamlItem.syntax.parameters.forEach(part => { if (part.description) { - part.description = this._fixCodeTicks(part.description); part.description = this._fixBoldAndItalics(part.description); } }); @@ -129,10 +130,6 @@ export class OfficeYamlDocumenter extends YamlDocumenter { return Text.replaceAll(text, '\\*', '*'); } - private _fixCodeTicks(text: string): string { - return Text.replaceAll(text, '\\`', '`'); - } - private _generateExampleSnippetText(snippets: string[]): string { const text: string[] = ['\n#### Examples\n']; for (const snippet of snippets) { diff --git a/apps/api-documenter/src/documenters/YamlDocumenter.ts b/apps/api-documenter/src/documenters/YamlDocumenter.ts new file mode 100644 index 00000000000..84afb3589ec --- /dev/null +++ b/apps/api-documenter/src/documenters/YamlDocumenter.ts @@ -0,0 +1,644 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import * as path from 'path'; + +import yaml = require('js-yaml'); +import { + JsonFile, + JsonSchema, + PackageName, + FileSystem, + NewlineKind +} from '@microsoft/node-core-library'; +import { StringBuilder, DocSection, DocComment } from '@microsoft/tsdoc'; +import { + ApiModel, + ApiItem, + ApiItemKind, + ApiDocumentedItem, + ApiReleaseTagMixin, + ReleaseTag, + ApiProperty, + ApiPropertyItem, + ApiMethod, + ApiMethodSignature, + ApiPropertySignature, + ApiItemContainerMixin, + ApiPackage, + ApiFunctionLikeMixin, + ApiEnumMember +} from '@microsoft/api-extractor'; + +import { + IYamlApiFile, + IYamlItem, + IYamlSyntax, + IYamlParameter +} from '../yaml/IYamlApiFile'; +import { + IYamlTocFile, + IYamlTocItem +} from '../yaml/IYamlTocFile'; +import { Utilities } from '../utils/Utilities'; +import { CustomMarkdownEmitter} from '../markdown/CustomMarkdownEmitter'; + +const yamlApiSchema: JsonSchema = JsonSchema.fromFile(path.join(__dirname, '..', 'yaml', 'typescript.schema.json')); + +/** + * Writes documentation in the Universal Reference YAML file format, as defined by typescript.schema.json. + */ +export class YamlDocumenter { + private readonly _apiModel: ApiModel; + private readonly _markdownEmitter: CustomMarkdownEmitter; + + // This is used by the _linkToUidIfPossible() workaround. + // It stores a mapping from type name (e.g. "MyClass") to the corresponding ApiItem. + // If the mapping would be ambiguous (e.g. "MyClass" is defined by multiple packages) + // then it is excluded from the mapping. Also excluded are ApiItem objects (such as package + // and function) which are not typically used as a data type. + private _apiItemsByTypeName: Map; + + private _outputFolder: string; + + public constructor(apiModel: ApiModel) { + this._apiModel = apiModel; + this._markdownEmitter = new CustomMarkdownEmitter(this._apiModel); + this._apiItemsByTypeName = new Map(); + + this._initApiItemsByTypeName(); + } + + /** @virtual */ + public generateFiles(outputFolder: string): void { + this._outputFolder = outputFolder; + + console.log(); + this._deleteOldOutputFiles(); + + for (const apiPackage of this._apiModel.packages) { + console.log(`Writing ${apiPackage.name} package`); + this._visitApiItems(apiPackage, undefined); + } + + this._writeTocFile(this._apiModel.packages); + } + + /** @virtual */ + protected onGetTocRoot(): IYamlTocItem { + return { + name: 'SharePoint Framework reference', + href: '~/overview/sharepoint.md', + items: [ ] + }; + } + + /** @virtual */ + protected onCustomizeYamlItem(yamlItem: IYamlItem): void { // virtual + // (overridden by child class) + } + + private _visitApiItems(apiItem: ApiDocumentedItem, parentYamlFile: IYamlApiFile | undefined): boolean { + const yamlItem: IYamlItem | undefined = this._generateYamlItem(apiItem); + if (!yamlItem) { + return false; + } + + this.onCustomizeYamlItem(yamlItem); + + if (this._shouldEmbed(apiItem.kind)) { + if (!parentYamlFile) { + throw new Error('Missing file context'); // program bug + } + parentYamlFile.items.push(yamlItem); + } else { + const newYamlFile: IYamlApiFile = { + items: [] + }; + newYamlFile.items.push(yamlItem); + + let children: ReadonlyArray; + if (apiItem.kind === ApiItemKind.Package) { + // Skip over the entry point, since it's not part of the documentation hierarchy + children = apiItem.members[0].members; + } else { + children = apiItem.members; + } + + const flattenedChildren: ApiItem[] = this._flattenNamespaces(children); + + for (const child of flattenedChildren) { + if (child instanceof ApiDocumentedItem) { + if (this._visitApiItems(child, newYamlFile)) { + if (!yamlItem.children) { + yamlItem.children = []; + } + yamlItem.children.push(this._getUid(child)); + } + } + } + + const yamlFilePath: string = this._getYamlFilePath(apiItem); + + if (apiItem.kind === ApiItemKind.Package) { + console.log('Writing ' + yamlFilePath); + } + + this._writeYamlFile(newYamlFile, yamlFilePath, 'UniversalReference', yamlApiSchema); + + if (parentYamlFile) { + if (!parentYamlFile.references) { + parentYamlFile.references = []; + } + + parentYamlFile.references.push({ + uid: this._getUid(apiItem), + name: this._getYamlItemName(apiItem) + }); + + } + } + + return true; + } + + // Since the YAML schema does not yet support nested namespaces, we simply omit them from + // the tree. However, _getYamlItemName() will show the namespace. + private _flattenNamespaces(items: ReadonlyArray): ApiItem[] { + const flattened: ApiItem[] = []; + for (const item of items) { + if (item.kind === ApiItemKind.Namespace) { + flattened.push(... this._flattenNamespaces(item.members)); + } else { + flattened.push(item); + } + } + return flattened; + } + + /** + * Write the table of contents + */ + private _writeTocFile(apiItems: ReadonlyArray): void { + const tocFile: IYamlTocFile = { + items: [ ] + }; + + const rootItem: IYamlTocItem = this.onGetTocRoot(); + tocFile.items.push(rootItem); + + rootItem.items!.push(...this._buildTocItems(apiItems)); + + const tocFilePath: string = path.join(this._outputFolder, 'toc.yml'); + console.log('Writing ' + tocFilePath); + this._writeYamlFile(tocFile, tocFilePath, '', undefined); + } + + private _buildTocItems(apiItems: ReadonlyArray): IYamlTocItem[] { + const tocItems: IYamlTocItem[] = []; + for (const apiItem of apiItems) { + let tocItem: IYamlTocItem; + + if (apiItem.kind === ApiItemKind.Namespace) { + // Namespaces don't have nodes yet + tocItem = { + name: apiItem.name + }; + } else { + if (this._shouldEmbed(apiItem.kind)) { + // Don't generate table of contents items for embedded definitions + continue; + } + + if (apiItem.kind === ApiItemKind.Package) { + tocItem = { + name: PackageName.getUnscopedName(apiItem.name), + uid: this._getUid(apiItem) + }; + } else { + tocItem = { + name: apiItem.name, + uid: this._getUid(apiItem) + }; + } + } + + tocItems.push(tocItem); + + let children: ReadonlyArray; + if (apiItem.kind === ApiItemKind.Package) { + // Skip over the entry point, since it's not part of the documentation hierarchy + children = apiItem.members[0].members; + } else { + children = apiItem.members; + } + + const childItems: IYamlTocItem[] = this._buildTocItems(children); + if (childItems.length > 0) { + tocItem.items = childItems; + } + } + return tocItems; + } + + private _shouldEmbed(apiItemKind: ApiItemKind): boolean { + switch (apiItemKind) { + case ApiItemKind.Class: + case ApiItemKind.Package: + case ApiItemKind.Interface: + case ApiItemKind.Enum: + return false; + } + return true; + } + + private _generateYamlItem(apiItem: ApiDocumentedItem): IYamlItem | undefined { + const yamlItem: Partial = { }; + yamlItem.uid = this._getUid(apiItem); + + if (apiItem.tsdocComment) { + const tsdocComment: DocComment = apiItem.tsdocComment; + if (tsdocComment.summarySection) { + const summary: string = this._renderMarkdown(tsdocComment.summarySection, apiItem); + if (summary) { + yamlItem.summary = summary; + } + } + + if (tsdocComment.remarksBlock) { + const remarks: string = this._renderMarkdown(tsdocComment.remarksBlock.content, apiItem); + if (remarks) { + yamlItem.remarks = remarks; + } + } + + if (tsdocComment.deprecatedBlock) { + const deprecatedMessage: string = this._renderMarkdown(tsdocComment.deprecatedBlock.content, apiItem); + if (deprecatedMessage.length > 0) { + yamlItem.deprecated = { content: deprecatedMessage }; + } + } + } + + if (ApiReleaseTagMixin.isBaseClassOf(apiItem)) { + if (apiItem.releaseTag === ReleaseTag.Beta) { + yamlItem.isPreview = true; + } + } + + yamlItem.name = this._getYamlItemName(apiItem); + + yamlItem.fullName = yamlItem.name; + yamlItem.langs = [ 'typeScript' ]; + + switch (apiItem.kind) { + case ApiItemKind.Enum: + yamlItem.type = 'enum'; + break; + case ApiItemKind.EnumMember: + yamlItem.type = 'field'; + const enumMember: ApiEnumMember = apiItem as ApiEnumMember; + + if (enumMember.initializerExcerpt.text.length > 0) { + yamlItem.numericValue = enumMember.initializerExcerpt.text; + } + + break; + case ApiItemKind.Class: + yamlItem.type = 'class'; + this._populateYamlClassOrInterface(yamlItem, apiItem); + break; + case ApiItemKind.Interface: + yamlItem.type = 'interface'; + this._populateYamlClassOrInterface(yamlItem, apiItem); + break; + case ApiItemKind.Method: + case ApiItemKind.MethodSignature: + yamlItem.type = 'method'; + this._populateYamlFunctionLike(yamlItem, apiItem as ApiMethod); + break; + /* + case ApiItemKind.Constructor: + yamlItem.type = 'constructor'; + this._populateYamlFunctionLike(yamlItem, apiItem); + break; + */ + case ApiItemKind.Package: + yamlItem.type = 'package'; + break; + case ApiItemKind.Property: + case ApiItemKind.PropertySignature: + const apiProperty: ApiPropertyItem = apiItem as ApiPropertyItem; + if (apiProperty.isEventProperty) { + yamlItem.type = 'event'; + } else { + yamlItem.type = 'property'; + } + this._populateYamlProperty(yamlItem, apiItem as ApiProperty); + break; + /* + case ApiItemKind.Function: + yamlItem.type = 'function'; + this._populateYamlFunctionLike(yamlItem, apiItem); + break; + default: + throw new Error('Unimplemented item kind: ' + apiItem.kind); + */ + } + + if (apiItem.kind !== ApiItemKind.Package && !this._shouldEmbed(apiItem.kind)) { + const associatedPackage: ApiPackage | undefined = apiItem.getAssociatedPackage(); + if (!associatedPackage) { + throw new Error('Unable to determine associated package for ' + apiItem.name); + } + yamlItem.package = this._getUid(associatedPackage); + } + + return yamlItem as IYamlItem; + } + + private _populateYamlClassOrInterface(yamlItem: Partial, apiItem: ApiDocumentedItem): void { + /* + if (apiStructure.extends) { + yamlItem.extends = [ this._linkToUidIfPossible(apiStructure.extends) ]; + } + + if (apiStructure.implements) { + yamlItem.implements = [ this._linkToUidIfPossible(apiStructure.implements) ]; + } + */ + + if (apiItem.tsdocComment) { + if (apiItem.tsdocComment.modifierTagSet.isSealed()) { + let sealedMessage: string; + if (apiItem.kind === ApiItemKind.Class) { + sealedMessage = 'This class is marked as `@sealed`. Subclasses should not extend it.'; + } else { + sealedMessage = 'This interface is marked as `@sealed`. Other interfaces should not extend it.'; + } + if (!yamlItem.remarks) { + yamlItem.remarks = sealedMessage; + } else { + yamlItem.remarks = sealedMessage + '\n\n' + yamlItem.remarks; + } + } + } + } + + private _populateYamlFunctionLike(yamlItem: Partial, apiItem: ApiMethod | ApiMethodSignature): void { + const syntax: IYamlSyntax = { + content: apiItem.getExcerptWithModifiers() + }; + yamlItem.syntax = syntax; + + const returnType: string = this._linkToUidIfPossible(apiItem.returnTypeExcerpt.text); + + let returnDescription: string = ''; + + if (apiItem.tsdocComment && apiItem.tsdocComment.returnsBlock) { + returnDescription = this._renderMarkdown(apiItem.tsdocComment.returnsBlock.content, apiItem); + // temporary workaround for people who mistakenly add a hyphen, e.g. "@returns - blah" + returnDescription = returnDescription.replace(/^\s*-\s+/, ''); + } + + if (returnType || returnDescription) { + syntax.return = { + type: [ returnType ], + description: returnDescription + }; + } + + const parameters: IYamlParameter[] = []; + for (const apiParameter of apiItem.parameters) { + let parameterDescription: string = ''; + if (apiParameter.tsdocParamBlock) { + parameterDescription = this._renderMarkdown(apiParameter.tsdocParamBlock.content, apiItem); + } + + parameters.push( + { + id: apiParameter.name, + description: parameterDescription, + type: [ this._linkToUidIfPossible(apiParameter.parameterTypeExcerpt.text) ] + } as IYamlParameter + ); + } + + if (parameters.length) { + syntax.parameters = parameters; + } + } + + private _populateYamlProperty(yamlItem: Partial, apiItem: ApiProperty | ApiPropertySignature): void { + const syntax: IYamlSyntax = { + content: apiItem.getExcerptWithModifiers() + }; + yamlItem.syntax = syntax; + + if (apiItem.propertyTypeExcerpt.text) { + syntax.return = { + type: [ this._linkToUidIfPossible(apiItem.propertyTypeExcerpt.text) ] + }; + } + } + + private _renderMarkdown(docSection: DocSection, contextApiItem: ApiItem): string { + const stringBuilder: StringBuilder = new StringBuilder(); + + this._markdownEmitter.emit(stringBuilder, docSection, { + contextApiItem, + onGetFilenameForApiItem: (apiItem: ApiItem) => { + // NOTE: GitHub's markdown renderer does not resolve relative hyperlinks correctly + // unless they start with "./" or "../". + return `xref:${this._getUid(apiItem)}`; + } + }); + + return stringBuilder.toString().trim(); + } + + private _writeYamlFile(dataObject: {}, filePath: string, yamlMimeType: string, + schema: JsonSchema|undefined): void { + + JsonFile.validateNoUndefinedMembers(dataObject); + + let stringified: string = yaml.safeDump(dataObject, { + lineWidth: 120 + }); + + if (yamlMimeType) { + stringified = `### YamlMime:${yamlMimeType}\n` + stringified; + } + + FileSystem.writeFile(filePath, stringified, { + convertLineEndings: NewlineKind.CrLf, + ensureFolderExists: true + }); + + if (schema) { + schema.validateObject(dataObject, filePath); + } + } + + /** + * Calculate the DocFX "uid" for the ApiItem + * Example: node-core-library.JsonFile.load + */ + private _getUid(apiItem: ApiItem): string { + let result: string = ''; + for (const hierarchyItem of apiItem.getHierarchy()) { + + // For overloaded methods, add a suffix such as "MyClass.myMethod_2". + let qualifiedName: string = hierarchyItem.name; + if (ApiFunctionLikeMixin.isBaseClassOf(hierarchyItem)) { + if (hierarchyItem.overloadIndex > 0) { + qualifiedName += `_${hierarchyItem.overloadIndex}`; + } + } + + switch (hierarchyItem.kind) { + case ApiItemKind.Model: + case ApiItemKind.EntryPoint: + break; + case ApiItemKind.Package: + result += PackageName.getUnscopedName(hierarchyItem.name); + break; + default: + result += '.'; + result += qualifiedName; + break; + } + } + return result; + } + + /** + * Initialize the _apiItemsByTypeName data structure. + */ + private _initApiItemsByTypeName(): void { + // Collect the _apiItemsByTypeName table + const ambiguousNames: Set = new Set(); + + this._initApiItemsByTypeNameRecursive(this._apiModel, ambiguousNames); + + // Remove the ambiguous matches + for (const ambiguousName of ambiguousNames) { + this._apiItemsByTypeName.delete(ambiguousName); + } + } + + /** + * Helper for _initApiItemsByTypeName() + */ + private _initApiItemsByTypeNameRecursive(apiItem: ApiItem, ambiguousNames: Set): void { + switch (apiItem.kind) { + case ApiItemKind.Class: + case ApiItemKind.Enum: + case ApiItemKind.Interface: + // Attempt to register both the fully qualified name and the short name + const namesForType: string[] = [apiItem.name]; + + // Note that nameWithDot cannot conflict with apiItem.name (because apiItem.name + // cannot contain a dot) + const nameWithDot: string | undefined = this._getTypeNameWithDot(apiItem); + if (nameWithDot) { + namesForType.push(nameWithDot); + } + + // Register all names + for (const typeName of namesForType) { + if (ambiguousNames.has(typeName)) { + break; + } + + if (this._apiItemsByTypeName.has(typeName)) { + // We saw this name before, so it's an ambiguous match + ambiguousNames.add(typeName); + break; + } + + this._apiItemsByTypeName.set(typeName, apiItem); + } + + break; + } + + // Recurse container members + if (ApiItemContainerMixin.isBaseClassOf(apiItem)) { + for (const apiMember of apiItem.members) { + this._initApiItemsByTypeNameRecursive(apiMember, ambiguousNames); + } + } + } + + /** + * This is a temporary workaround to enable limited autolinking of API item types + * until the YAML file format is enhanced to support general hyperlinks. + * @remarks + * In the current version, fields such as IApiProperty.type allow either: + * (1) a UID identifier such as "node-core-library.JsonFile" which will be rendered + * as a hyperlink to that type name, or (2) a block of freeform text that must not + * contain any Markdown links. The _substituteUidForSimpleType() function assumes + * it is given #2 but substitutes #1 if the name can be matched to a ApiItem. + */ + private _linkToUidIfPossible(typeName: string): string { + // Note that typeName might be a _getTypeNameWithDot() name or it might be a simple class name + const apiItem: ApiItem | undefined = this._apiItemsByTypeName.get(typeName.trim()); + if (apiItem) { + // Substitute the UID + return this._getUid(apiItem); + } + return typeName; + } + + /** + * If the apiItem represents a scoped name such as "my-library#MyNamespace.MyClass", + * this returns a string such as "MyNamespace.MyClass". If the result would not + * have at least one dot in it, then undefined is returned. + */ + private _getTypeNameWithDot(apiItem: ApiItem): string | undefined { + const result: string = apiItem.getScopedNameWithinPackage(); + if (result.indexOf('.') >= 0) { + return result; + } + return undefined; + } + + private _getYamlItemName(apiItem: ApiItem): string { + if (apiItem.parent && apiItem.parent.kind === ApiItemKind.Namespace) { + // For members a namespace, show the full name excluding the package part: + // Example: excel.Excel.Binding --> Excel.Binding + return this._getUid(apiItem).replace(/^[^.]+\./, ''); + } + return Utilities.getConciseSignature(apiItem); + } + + private _getYamlFilePath(apiItem: ApiItem): string { + let result: string = ''; + + for (const current of apiItem.getHierarchy()) { + switch (current.kind) { + case ApiItemKind.Model: + case ApiItemKind.EntryPoint: + break; + case ApiItemKind.Package: + result += PackageName.getUnscopedName(current.name); + break; + default: + if (current.parent && current.parent.kind === ApiItemKind.EntryPoint) { + result += '/'; + } else { + result += '.'; + } + result += current.name; + break; + } + } + return path.join(this._outputFolder, result.toLowerCase() + '.yml'); + } + + private _deleteOldOutputFiles(): void { + console.log('Deleting old output from ' + this._outputFolder); + FileSystem.ensureEmptyFolder(this._outputFolder); + } +} diff --git a/apps/api-documenter/src/markdown/CustomMarkdownEmitter.ts b/apps/api-documenter/src/markdown/CustomMarkdownEmitter.ts new file mode 100644 index 00000000000..dd378681fae --- /dev/null +++ b/apps/api-documenter/src/markdown/CustomMarkdownEmitter.ts @@ -0,0 +1,187 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import * as colors from 'colors'; + +import { + DocNode, DocLinkTag, StringBuilder +} from '@microsoft/tsdoc'; +import { CustomDocNodeKind } from '../nodes/CustomDocNodeKind'; +import { DocHeading } from '../nodes/DocHeading'; +import { DocNoteBox } from '../nodes/DocNoteBox'; +import { DocTable } from '../nodes/DocTable'; +import { DocTableCell } from '../nodes/DocTableCell'; +import { DocEmphasisSpan } from '../nodes/DocEmphasisSpan'; +import { + MarkdownEmitter, + IMarkdownEmitterContext, + IMarkdownEmitterOptions +} from './MarkdownEmitter'; +import { + ApiModel, + IResolveDeclarationReferenceResult, + ApiItem, + IndentedWriter +} from '@microsoft/api-extractor'; + +export interface ICustomMarkdownEmitterOptions extends IMarkdownEmitterOptions { + contextApiItem: ApiItem | undefined; + + onGetFilenameForApiItem: (apiItem: ApiItem) => string | undefined; +} + +export class CustomMarkdownEmitter extends MarkdownEmitter { + private _apiModel: ApiModel; + + public constructor (apiModel: ApiModel) { + super(); + + this._apiModel = apiModel; + } + + public emit(stringBuilder: StringBuilder, docNode: DocNode, options: ICustomMarkdownEmitterOptions): string { + return super.emit(stringBuilder, docNode, options); + } + + /** @override */ + protected writeNode(docNode: DocNode, context: IMarkdownEmitterContext): void { + const writer: IndentedWriter = context.writer; + + switch (docNode.kind) { + case CustomDocNodeKind.Heading: { + const docHeading: DocHeading = docNode as DocHeading; + writer.ensureSkippedLine(); + + let prefix: string; + switch (docHeading.level) { + case 1: prefix = '##'; break; + case 2: prefix = '###'; break; + case 3: prefix = '###'; break; + default: + prefix = '####'; + } + + writer.writeLine(prefix + ' ' + this.getEscapedText(docHeading.title)); + writer.writeLine(); + break; + } + case CustomDocNodeKind.NoteBox: { + const docNoteBox: DocNoteBox = docNode as DocNoteBox; + writer.ensureNewLine(); + + writer.increaseIndent('> '); + + this.writeNode(docNoteBox.content, context); + writer.ensureNewLine(); + + writer.decreaseIndent(); + + writer.writeLine(); + break; + } + case CustomDocNodeKind.Table: { + const docTable: DocTable = docNode as DocTable; + // GitHub's markdown renderer chokes on tables that don't have a blank line above them, + // whereas VS Code's renderer is totally fine with it. + writer.ensureSkippedLine(); + + context.insideTable = true; + + // Markdown table rows can have inconsistent cell counts. Size the table based on the longest row. + let columnCount: number = 0; + if (docTable.header) { + columnCount = docTable.header.cells.length; + } + for (const row of docTable.rows) { + if (row.cells.length > columnCount) { + columnCount = row.cells.length; + } + } + + // write the table header (which is required by Markdown) + writer.write('| '); + for (let i: number = 0; i < columnCount; ++i) { + writer.write(' '); + if (docTable.header) { + const cell: DocTableCell | undefined = docTable.header.cells[i]; + if (cell) { + this.writeNode(cell.content, context); + } + } + writer.write(' |'); + } + writer.writeLine(); + + // write the divider + writer.write('| '); + for (let i: number = 0; i < columnCount; ++i) { + writer.write(' --- |'); + } + writer.writeLine(); + + for (const row of docTable.rows) { + writer.write('| '); + for (const cell of row.cells) { + writer.write(' '); + this.writeNode(cell.content, context); + writer.write(' |'); + } + writer.writeLine(); + } + writer.writeLine(); + + context.insideTable = false; + + break; + } + case CustomDocNodeKind.EmphasisSpan: { + const docEmphasisSpan: DocEmphasisSpan = docNode as DocEmphasisSpan; + const oldBold: boolean = context.boldRequested; + const oldItalic: boolean = context.italicRequested; + context.boldRequested = docEmphasisSpan.bold; + context.italicRequested = docEmphasisSpan.italic; + this.writeNodes(docEmphasisSpan.nodes, context); + context.boldRequested = oldBold; + context.italicRequested = oldItalic; + break; + } + default: + super.writeNode(docNode, context); + } + } + + /** @override */ + protected writeLinkTagWithCodeDestination(docLinkTag: DocLinkTag, + context: IMarkdownEmitterContext): void { + + const options: ICustomMarkdownEmitterOptions = context.options; + + const result: IResolveDeclarationReferenceResult + = this._apiModel.resolveDeclarationReference(docLinkTag.codeDestination!, options.contextApiItem); + + if (result.resolvedApiItem) { + const filename: string | undefined = options.onGetFilenameForApiItem(result.resolvedApiItem); + + if (filename) { + let linkText: string = docLinkTag.linkText || ''; + if (linkText.length === 0) { + + // Generate a name such as Namespace1.Namespace2.MyClass.myMethod() + linkText = result.resolvedApiItem.getScopedNameWithinPackage(); + } + if (linkText.length > 0) { + const encodedLinkText: string = this.getEscapedText(linkText.replace(/\s+/g, ' ')); + + context.writer.write('['); + context.writer.write(encodedLinkText); + context.writer.write(`](${filename!})`); + } else { + console.log(colors.red('WARNING: Unable to determine link text')); + } + } + } else if (result.errorMessage) { + console.log(colors.red('WARNING: Unable to resolve reference: ' + result.errorMessage)); + } + } + +} diff --git a/apps/api-documenter/src/markdown/MarkdownDocumenter.ts b/apps/api-documenter/src/markdown/MarkdownDocumenter.ts deleted file mode 100644 index 9eb175c32c8..00000000000 --- a/apps/api-documenter/src/markdown/MarkdownDocumenter.ts +++ /dev/null @@ -1,663 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -// See LICENSE in the project root for license information. - -import * as colors from 'colors'; -import * as path from 'path'; - -import { - PackageName, - FileSystem, - NewlineKind -} from '@microsoft/node-core-library'; -import { - IApiClass, - IApiEnum, - IApiEnumMember, - IApiFunction, - IApiInterface, - IApiPackage, - ApiMember, - IApiProperty, - ApiItem, - IApiParameter, - IApiMethod, - IMarkupPage, - IMarkupTable, - Markup, - MarkupBasicElement, - MarkupStructuredElement, - IMarkupTableRow -} from '@microsoft/api-extractor'; - -import { - DocItemSet, - DocItem, - DocItemKind, - IDocItemSetResolveResult -} from '../utils/DocItemSet'; -import { Utilities } from '../utils/Utilities'; -import { MarkdownRenderer, IMarkdownRenderApiLinkArgs } from '../utils/MarkdownRenderer'; - -/** - * Renders API documentation in the Markdown file format. - * For more info: https://en.wikipedia.org/wiki/Markdown - */ -export class MarkdownDocumenter { - private _docItemSet: DocItemSet; - private _outputFolder: string; - - public constructor(docItemSet: DocItemSet) { - this._docItemSet = docItemSet; - } - - public generateFiles(outputFolder: string): void { - this._outputFolder = outputFolder; - - console.log(); - this._deleteOldOutputFiles(); - - for (const docPackage of this._docItemSet.docPackages) { - this._writePackagePage(docPackage); - } - - } - - /** - * GENERATE PAGE: PACKAGE - */ - private _writePackagePage(docPackage: DocItem): void { - console.log(`Writing ${docPackage.name} package`); - - const unscopedPackageName: string = PackageName.getUnscopedName(docPackage.name); - - const markupPage: IMarkupPage = Markup.createPage(`${unscopedPackageName} package`); - this._writeBreadcrumb(markupPage, docPackage); - - const apiPackage: IApiPackage = docPackage.apiItem as IApiPackage; - - markupPage.elements.push(...apiPackage.summary); - - const namespacesTable: IMarkupTable = Markup.createTable([ - Markup.createTextElements('Namespaces'), - Markup.createTextElements('Description') - ]); - - const classesTable: IMarkupTable = Markup.createTable([ - Markup.createTextElements('Class'), - Markup.createTextElements('Description') - ]); - - const interfacesTable: IMarkupTable = Markup.createTable([ - Markup.createTextElements('Interface'), - Markup.createTextElements('Description') - ]); - - const functionsTable: IMarkupTable = Markup.createTable([ - Markup.createTextElements('Function'), - Markup.createTextElements('Returns'), - Markup.createTextElements('Description') - ]); - - const enumerationsTable: IMarkupTable = Markup.createTable([ - Markup.createTextElements('Enumeration'), - Markup.createTextElements('Description') - ]); - - for (const docChild of docPackage.children) { - const apiChild: ApiItem = docChild.apiItem; - - const docItemTitle: MarkupBasicElement[] = [ - Markup.createApiLink( - [ Markup.createCode(docChild.name, 'javascript') ], - docChild.getApiReference()) - ]; - - const docChildDescription: MarkupBasicElement[] = []; - - if (apiChild.isBeta) { - docChildDescription.push(...Markup.createTextElements('(BETA)', { italics: true, bold: true })); - docChildDescription.push(...Markup.createTextElements(' ')); - } - docChildDescription.push(...apiChild.summary); - - switch (apiChild.kind) { - case 'class': - classesTable.rows.push( - Markup.createTableRow([ - docItemTitle, - docChildDescription - ]) - ); - this._writeClassPage(docChild); - break; - case 'interface': - interfacesTable.rows.push( - Markup.createTableRow([ - docItemTitle, - docChildDescription - ]) - ); - this._writeInterfacePage(docChild); - break; - case 'function': - functionsTable.rows.push( - Markup.createTableRow([ - docItemTitle, - apiChild.returnValue ? [Markup.createCode(apiChild.returnValue.type, 'javascript')] : [], - docChildDescription - ]) - ); - this._writeFunctionPage(docChild); - break; - case 'enum': - enumerationsTable.rows.push( - Markup.createTableRow([ - docItemTitle, - docChildDescription - ]) - ); - this._writeEnumPage(docChild); - break; - case 'namespace': - namespacesTable.rows.push( - Markup.createTableRow([ - docItemTitle, - docChildDescription - ]) - ); - this._writePackagePage(docChild); - break; - } - } - - if (apiPackage.remarks && apiPackage.remarks.length) { - markupPage.elements.push(Markup.createHeading1('Remarks')); - markupPage.elements.push(...apiPackage.remarks); - } - - if (namespacesTable.rows.length > 0) { - markupPage.elements.push(Markup.createHeading1('Namespaces')); - markupPage.elements.push(namespacesTable); - } - - if (classesTable.rows.length > 0) { - markupPage.elements.push(Markup.createHeading1('Classes')); - markupPage.elements.push(classesTable); - } - - if (interfacesTable.rows.length > 0) { - markupPage.elements.push(Markup.createHeading1('Interfaces')); - markupPage.elements.push(interfacesTable); - } - - if (functionsTable.rows.length > 0) { - markupPage.elements.push(Markup.createHeading1('Functions')); - markupPage.elements.push(functionsTable); - } - - if (enumerationsTable.rows.length > 0) { - markupPage.elements.push(Markup.createHeading1('Enumerations')); - markupPage.elements.push(enumerationsTable); - } - - this._writePage(markupPage, docPackage); - } - - /** - * GENERATE PAGE: CLASS - */ - private _writeClassPage(docClass: DocItem): void { - const apiClass: IApiClass = docClass.apiItem as IApiClass; - - // TODO: Show concise generic parameters with class name - const markupPage: IMarkupPage = Markup.createPage(`${docClass.name} class`); - this._writeBreadcrumb(markupPage, docClass); - - if (apiClass.isBeta) { - this._writeBetaWarning(markupPage.elements); - } - - markupPage.elements.push(...apiClass.summary); - - const propertiesTable: IMarkupTable = Markup.createTable([ - Markup.createTextElements('Property'), - Markup.createTextElements('Access Modifier'), - Markup.createTextElements('Type'), - Markup.createTextElements('Description') - ]); - - const eventsTable: IMarkupTable = Markup.createTable([ - Markup.createTextElements('Property'), - Markup.createTextElements('Access Modifier'), - Markup.createTextElements('Type'), - Markup.createTextElements('Description') - ]); - - const methodsTable: IMarkupTable = Markup.createTable([ - Markup.createTextElements('Method'), - Markup.createTextElements('Access Modifier'), - Markup.createTextElements('Returns'), - Markup.createTextElements('Description') - ]); - - for (const docMember of docClass.children) { - const apiMember: ApiMember = docMember.apiItem as ApiMember; - - switch (apiMember.kind) { - case 'property': - const propertyTitle: MarkupBasicElement[] = [ - Markup.createApiLink( - [Markup.createCode(docMember.name, 'javascript')], - docMember.getApiReference()) - ]; - - const row: IMarkupTableRow = Markup.createTableRow([ - propertyTitle, - [], - [Markup.createCode(apiMember.type, 'javascript')], - apiMember.summary - ]); - - if (apiMember.isEventProperty) { - eventsTable.rows.push(row); - } else { - propertiesTable.rows.push(row); - } - this._writePropertyPage(docMember); - break; - - case 'constructor': - // TODO: Extract constructor into its own section - const constructorTitle: MarkupBasicElement[] = [ - Markup.createApiLink( - [Markup.createCode(Utilities.getConciseSignature(docMember.name, apiMember), 'javascript')], - docMember.getApiReference()) - ]; - - methodsTable.rows.push( - Markup.createTableRow([ - constructorTitle, - [], - [], - apiMember.summary - ]) - ); - this._writeMethodPage(docMember); - break; - - case 'method': - const methodTitle: MarkupBasicElement[] = [ - Markup.createApiLink( - [Markup.createCode(Utilities.getConciseSignature(docMember.name, apiMember), 'javascript')], - docMember.getApiReference()) - ]; - - methodsTable.rows.push( - Markup.createTableRow([ - methodTitle, - apiMember.accessModifier ? [Markup.createCode(apiMember.accessModifier, 'javascript')] : [], - apiMember.returnValue ? [Markup.createCode(apiMember.returnValue.type, 'javascript')] : [], - apiMember.summary - ]) - ); - this._writeMethodPage(docMember); - break; - } - } - - if (eventsTable.rows.length > 0) { - markupPage.elements.push(Markup.createHeading1('Events')); - markupPage.elements.push(eventsTable); - } - - if (propertiesTable.rows.length > 0) { - markupPage.elements.push(Markup.createHeading1('Properties')); - markupPage.elements.push(propertiesTable); - } - - if (methodsTable.rows.length > 0) { - markupPage.elements.push(Markup.createHeading1('Methods')); - markupPage.elements.push(methodsTable); - } - - if (apiClass.remarks && apiClass.remarks.length) { - markupPage.elements.push(Markup.createHeading1('Remarks')); - markupPage.elements.push(...apiClass.remarks); - } - - this._writePage(markupPage, docClass); - } - - /** - * GENERATE PAGE: INTERFACE - */ - private _writeInterfacePage(docInterface: DocItem): void { - const apiInterface: IApiInterface = docInterface.apiItem as IApiInterface; - - // TODO: Show concise generic parameters with class name - const markupPage: IMarkupPage = Markup.createPage(`${docInterface.name} interface`); - this._writeBreadcrumb(markupPage, docInterface); - - if (apiInterface.isBeta) { - this._writeBetaWarning(markupPage.elements); - } - - markupPage.elements.push(...apiInterface.summary); - - const propertiesTable: IMarkupTable = Markup.createTable([ - Markup.createTextElements('Property'), - Markup.createTextElements('Type'), - Markup.createTextElements('Description') - ]); - - const methodsTable: IMarkupTable = Markup.createTable([ - Markup.createTextElements('Method'), - Markup.createTextElements('Returns'), - Markup.createTextElements('Description') - ]); - - for (const docMember of docInterface.children) { - const apiMember: ApiMember = docMember.apiItem as ApiMember; - - switch (apiMember.kind) { - case 'property': - const propertyTitle: MarkupBasicElement[] = [ - Markup.createApiLink( - [Markup.createCode(docMember.name, 'javascript')], - docMember.getApiReference()) - ]; - - propertiesTable.rows.push( - Markup.createTableRow([ - propertyTitle, - [Markup.createCode(apiMember.type)], - apiMember.summary - ]) - ); - this._writePropertyPage(docMember); - break; - - case 'method': - const methodTitle: MarkupBasicElement[] = [ - Markup.createApiLink( - [Markup.createCode(Utilities.getConciseSignature(docMember.name, apiMember), 'javascript')], - docMember.getApiReference()) - ]; - - methodsTable.rows.push( - Markup.createTableRow([ - methodTitle, - apiMember.returnValue ? [Markup.createCode(apiMember.returnValue.type, 'javascript')] : [], - apiMember.summary - ]) - ); - this._writeMethodPage(docMember); - break; - } - } - - if (propertiesTable.rows.length > 0) { - markupPage.elements.push(Markup.createHeading1('Properties')); - markupPage.elements.push(propertiesTable); - } - - if (methodsTable.rows.length > 0) { - markupPage.elements.push(Markup.createHeading1('Methods')); - markupPage.elements.push(methodsTable); - } - - if (apiInterface.remarks && apiInterface.remarks.length) { - markupPage.elements.push(Markup.createHeading1('Remarks')); - markupPage.elements.push(...apiInterface.remarks); - } - - this._writePage(markupPage, docInterface); - } - - /** - * GENERATE PAGE: ENUM - */ - private _writeEnumPage(docEnum: DocItem): void { - const apiEnum: IApiEnum = docEnum.apiItem as IApiEnum; - - // TODO: Show concise generic parameters with class name - const markupPage: IMarkupPage = Markup.createPage(`${docEnum.name} enumeration`); - this._writeBreadcrumb(markupPage, docEnum); - - if (apiEnum.isBeta) { - this._writeBetaWarning(markupPage.elements); - } - - markupPage.elements.push(...apiEnum.summary); - - const membersTable: IMarkupTable = Markup.createTable([ - Markup.createTextElements('Member'), - Markup.createTextElements('Value'), - Markup.createTextElements('Description') - ]); - - for (const docEnumMember of docEnum.children) { - const apiEnumMember: IApiEnumMember = docEnumMember.apiItem as IApiEnumMember; - - const enumValue: MarkupBasicElement[] = []; - - if (apiEnumMember.value) { - enumValue.push(Markup.createCode('= ' + apiEnumMember.value)); - } - - membersTable.rows.push( - Markup.createTableRow([ - Markup.createTextElements(docEnumMember.name), - enumValue, - apiEnumMember.summary - ]) - ); - } - - if (membersTable.rows.length > 0) { - markupPage.elements.push(membersTable); - } - - this._writePage(markupPage, docEnum); - } - - /** - * GENERATE PAGE: PROPERTY - */ - private _writePropertyPage(docProperty: DocItem): void { - const apiProperty: IApiProperty = docProperty.apiItem as IApiProperty; - const fullProperyName: string = docProperty.parent!.name + '.' + docProperty.name; - - const markupPage: IMarkupPage = Markup.createPage(`${fullProperyName} property`); - this._writeBreadcrumb(markupPage, docProperty); - - if (apiProperty.isBeta) { - this._writeBetaWarning(markupPage.elements); - } - - markupPage.elements.push(...apiProperty.summary); - - markupPage.elements.push(Markup.PARAGRAPH); - markupPage.elements.push(...Markup.createTextElements('Signature:', { bold: true })); - markupPage.elements.push(Markup.createCodeBox(docProperty.name + ': ' + apiProperty.type, 'javascript')); - - if (apiProperty.remarks && apiProperty.remarks.length) { - markupPage.elements.push(Markup.createHeading1('Remarks')); - markupPage.elements.push(...apiProperty.remarks); - } - - this._writePage(markupPage, docProperty); - } - - /** - * GENERATE PAGE: METHOD - */ - private _writeMethodPage(docMethod: DocItem): void { - const apiMethod: IApiMethod = docMethod.apiItem as IApiMethod; - - const fullMethodName: string = docMethod.parent!.name + '.' + docMethod.name; - - const markupPage: IMarkupPage = Markup.createPage(`${fullMethodName} method`); - this._writeBreadcrumb(markupPage, docMethod); - - if (apiMethod.isBeta) { - this._writeBetaWarning(markupPage.elements); - } - - markupPage.elements.push(...apiMethod.summary); - - markupPage.elements.push(Markup.PARAGRAPH); - markupPage.elements.push(...Markup.createTextElements('Signature:', { bold: true })); - markupPage.elements.push(Markup.createCodeBox(apiMethod.signature, 'javascript')); - - if (apiMethod.returnValue) { - markupPage.elements.push(...Markup.createTextElements('Returns:', { bold: true })); - markupPage.elements.push(...Markup.createTextElements(' ')); - markupPage.elements.push(Markup.createCode(apiMethod.returnValue.type, 'javascript')); - markupPage.elements.push(Markup.PARAGRAPH); - markupPage.elements.push(...apiMethod.returnValue.description); - } - - if (apiMethod.remarks && apiMethod.remarks.length) { - markupPage.elements.push(Markup.createHeading1('Remarks')); - markupPage.elements.push(...apiMethod.remarks); - } - - if (Object.keys(apiMethod.parameters).length > 0) { - const parametersTable: IMarkupTable = Markup.createTable([ - Markup.createTextElements('Parameter'), - Markup.createTextElements('Type'), - Markup.createTextElements('Description') - ]); - - markupPage.elements.push(Markup.createHeading1('Parameters')); - markupPage.elements.push(parametersTable); - for (const parameterName of Object.keys(apiMethod.parameters)) { - const apiParameter: IApiParameter = apiMethod.parameters[parameterName]; - parametersTable.rows.push(Markup.createTableRow([ - [Markup.createCode(parameterName, 'javascript')], - apiParameter.type ? [Markup.createCode(apiParameter.type, 'javascript')] : [], - apiParameter.description - ]) - ); - } - } - - this._writePage(markupPage, docMethod); - } - - /** - * GENERATE PAGE: FUNCTION - */ - private _writeFunctionPage(docFunction: DocItem): void { - const apiFunction: IApiFunction = docFunction.apiItem as IApiFunction; - - const markupPage: IMarkupPage = Markup.createPage(`${docFunction.name} function`); - this._writeBreadcrumb(markupPage, docFunction); - - if (apiFunction.isBeta) { - this._writeBetaWarning(markupPage.elements); - } - - markupPage.elements.push(...apiFunction.summary); - - markupPage.elements.push(Markup.PARAGRAPH); - markupPage.elements.push(...Markup.createTextElements('Signature:', { bold: true })); - markupPage.elements.push(Markup.createCodeBox(docFunction.name, 'javascript')); - - if (apiFunction.returnValue) { - markupPage.elements.push(...Markup.createTextElements('Returns:', { bold: true })); - markupPage.elements.push(...Markup.createTextElements(' ')); - markupPage.elements.push(Markup.createCode(apiFunction.returnValue.type, 'javascript')); - markupPage.elements.push(Markup.PARAGRAPH); - markupPage.elements.push(...apiFunction.returnValue.description); - } - - if (apiFunction.remarks && apiFunction.remarks.length) { - markupPage.elements.push(Markup.createHeading1('Remarks')); - markupPage.elements.push(...apiFunction.remarks); - } - - if (Object.keys(apiFunction.parameters).length > 0) { - const parametersTable: IMarkupTable = Markup.createTable([ - Markup.createTextElements('Parameter'), - Markup.createTextElements('Type'), - Markup.createTextElements('Description') - ]); - - markupPage.elements.push(Markup.createHeading1('Parameters')); - markupPage.elements.push(parametersTable); - for (const parameterName of Object.keys(apiFunction.parameters)) { - const apiParameter: IApiParameter = apiFunction.parameters[parameterName]; - parametersTable.rows.push(Markup.createTableRow([ - [Markup.createCode(parameterName, 'javascript')], - apiParameter.type ? [Markup.createCode(apiParameter.type, 'javascript')] : [], - apiParameter.description - ]) - ); - } - } - - this._writePage(markupPage, docFunction); - } - - private _writeBreadcrumb(markupPage: IMarkupPage, docItem: DocItem): void { - markupPage.breadcrumb.push(Markup.createWebLinkFromText('Home', './index')); - - for (const hierarchyItem of docItem.getHierarchy()) { - markupPage.breadcrumb.push(...Markup.createTextElements(' > ')); - markupPage.breadcrumb.push(Markup.createApiLinkFromText( - hierarchyItem.name, hierarchyItem.getApiReference())); - } - } - - private _writeBetaWarning(elements: MarkupStructuredElement[]): void { - const betaWarning: string = 'This API is provided as a preview for developers and may change' - + ' based on feedback that we receive. Do not use this API in a production environment.'; - elements.push( - Markup.createNoteBoxFromText(betaWarning) - ); - } - - private _writePage(markupPage: IMarkupPage, docItem: DocItem): void { // override - const filename: string = path.join(this._outputFolder, this._getFilenameForDocItem(docItem)); - - const content: string = MarkdownRenderer.renderElements([markupPage], { - onRenderApiLink: (args: IMarkdownRenderApiLinkArgs) => { - const resolveResult: IDocItemSetResolveResult = this._docItemSet.resolveApiItemReference(args.reference); - if (!resolveResult.docItem) { - // Eventually we should introduce a warnings file - console.error(colors.yellow('Warning: Unresolved hyperlink to ' - + Markup.formatApiItemReference(args.reference))); - } else { - // NOTE: GitHub's markdown renderer does not resolve relative hyperlinks correctly - // unless they start with "./" or "../". - const docFilename: string = './' + this._getFilenameForDocItem(resolveResult.docItem); - args.prefix = '['; - args.suffix = '](' + docFilename + ')'; - } - } - }); - - FileSystem.writeFile(filename, content, { - convertLineEndings: NewlineKind.CrLf - }); - } - - private _getFilenameForDocItem(docItem: DocItem): string { - let baseName: string = ''; - for (const part of docItem.getHierarchy()) { - if (part.kind === DocItemKind.Package) { - baseName = PackageName.getUnscopedName(part.name); - } else { - baseName += '.' + part.name; - } - } - return baseName.toLowerCase() + '.md'; - } - - private _deleteOldOutputFiles(): void { - console.log('Deleting old output from ' + this._outputFolder); - FileSystem.ensureEmptyFolder(this._outputFolder); - } -} diff --git a/apps/api-documenter/src/markdown/MarkdownEmitter.ts b/apps/api-documenter/src/markdown/MarkdownEmitter.ts new file mode 100644 index 00000000000..94ec6dca685 --- /dev/null +++ b/apps/api-documenter/src/markdown/MarkdownEmitter.ts @@ -0,0 +1,242 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { + DocNode, + DocNodeKind, + StringBuilder, + DocPlainText, + DocHtmlStartTag, + DocHtmlEndTag, + DocCodeSpan, + DocLinkTag, + DocParagraph, + DocFencedCode, + DocSection, + DocNodeTransforms, + DocEscapedText, + DocErrorText +} from '@microsoft/tsdoc'; +import { IndentedWriter } from '@microsoft/api-extractor'; + +export interface IMarkdownEmitterOptions { +} + +export interface IMarkdownEmitterContext { + writer: IndentedWriter; + insideTable: boolean; + + boldRequested: boolean; + italicRequested: boolean; + + writingBold: boolean; + writingItalic: boolean; + + options: TOptions; +} + +/** + * Renders MarkupElement content in the Markdown file format. + * For more info: https://en.wikipedia.org/wiki/Markdown + */ +export class MarkdownEmitter { + + public emit(stringBuilder: StringBuilder, docNode: DocNode, options: IMarkdownEmitterOptions): string { + const writer: IndentedWriter = new IndentedWriter(stringBuilder); + + const context: IMarkdownEmitterContext = { + writer, + insideTable: false, + + boldRequested: false, + italicRequested: false, + + writingBold: false, + writingItalic: false, + + options + }; + + this.writeNode(docNode, context); + + writer.ensureNewLine(); // finish the last line + + return writer.toString(); + } + + protected getEscapedText(text: string): string { + const textWithBackslashes: string = text + .replace(/\\/g, '\\\\') // first replace the escape character + .replace(/[*#[\]_|`~]/g, (x) => '\\' + x) // then escape any special characters + .replace(/---/g, '\\-\\-\\-') // hyphens only if it's 3 or more + .replace(/&/g, '&') + .replace(//g, '>'); + return textWithBackslashes; + } + + /** + * @virtual + */ + protected writeNode(docNode: DocNode, context: IMarkdownEmitterContext): void { + const writer: IndentedWriter = context.writer; + + switch (docNode.kind) { + case DocNodeKind.PlainText: { + const docPlainText: DocPlainText = docNode as DocPlainText; + this.writePlainText(docPlainText.text, context); + break; + } + case DocNodeKind.HtmlStartTag: + case DocNodeKind.HtmlEndTag: { + const docHtmlTag: DocHtmlStartTag | DocHtmlEndTag = docNode as DocHtmlStartTag | DocHtmlEndTag; + // write the HTML element verbatim into the output + writer.write(docHtmlTag.emitAsHtml()); + break; + } + case DocNodeKind.CodeSpan: { + const docCodeSpan: DocCodeSpan = docNode as DocCodeSpan; + writer.write('`'); + if (context.insideTable) { + const parts: string[] = docCodeSpan.code.split(/\r?\n/g); + writer.write(parts.join('`

`')); + } else { + writer.write(docCodeSpan.code); + } + writer.write('`'); + break; + } + case DocNodeKind.LinkTag: { + const docLinkTag: DocLinkTag = docNode as DocLinkTag; + if (docLinkTag.codeDestination) { + this.writeLinkTagWithCodeDestination(docLinkTag, context); + } else if (docLinkTag.urlDestination) { + this.writeLinkTagWithUrlDestination(docLinkTag, context); + } else if (docLinkTag.linkText) { + this.writePlainText(docLinkTag.linkText, context); + } + break; + } + case DocNodeKind.Paragraph: { + const docParagraph: DocParagraph = docNode as DocParagraph; + const trimmedParagraph: DocParagraph = DocNodeTransforms.trimSpacesInParagraph(docParagraph); + if (context.insideTable) { + writer.write('

'); + this.writeNodes(trimmedParagraph.nodes, context); + writer.write('

'); + } else { + this.writeNodes(trimmedParagraph.nodes, context); + writer.ensureNewLine(); + writer.writeLine(); + } + break; + } + case DocNodeKind.FencedCode: { + const docFencedCode: DocFencedCode = docNode as DocFencedCode; + writer.ensureNewLine(); + writer.write('```'); + writer.write(docFencedCode.language); + writer.writeLine(); + writer.write(docFencedCode.code); + writer.writeLine(); + writer.writeLine('```'); + break; + } + case DocNodeKind.Section: { + const docSection: DocSection = docNode as DocSection; + this.writeNodes(docSection.nodes, context); + break; + } + case DocNodeKind.SoftBreak: { + if (!/^\s?$/.test(writer.peekLastCharacter())) { + writer.write(' '); + } + break; + } + case DocNodeKind.EscapedText: { + const docEscapedText: DocEscapedText = docNode as DocEscapedText; + this.writePlainText(docEscapedText.decodedText, context); + break; + } + case DocNodeKind.ErrorText: { + const docErrorText: DocErrorText = docNode as DocErrorText; + this.writePlainText(docErrorText.text, context); + break; + } + default: + throw new Error('Unsupported element kind: ' + docNode.kind); + } + } + + /** @virtual */ + protected writeLinkTagWithCodeDestination(docLinkTag: DocLinkTag, context: IMarkdownEmitterContext): void { + + // The subclass needs to implement this to support code destinations + throw new Error('writeLinkTagWithCodeDestination()'); + } + + /** @virtual */ + protected writeLinkTagWithUrlDestination(docLinkTag: DocLinkTag, context: IMarkdownEmitterContext): void { + const linkText: string = docLinkTag.linkText !== undefined ? docLinkTag.linkText + : docLinkTag.urlDestination!; + + const encodedLinkText: string = this.getEscapedText(linkText.replace(/\s+/g, ' ')); + + context.writer.write('['); + context.writer.write(encodedLinkText); + context.writer.write(`](${docLinkTag.urlDestination!})`); + } + + protected writePlainText(text: string, context: IMarkdownEmitterContext): void { + const writer: IndentedWriter = context.writer; + + // split out the [ leading whitespace, content, trailing whitespace ] + const parts: string[] = text.match(/^(\s*)(.*?)(\s*)$/) || []; + + writer.write(parts[1]); // write leading whitespace + + const middle: string = parts[2]; + + if (middle !== '') { + switch (writer.peekLastCharacter()) { + case '': + case '\n': + case ' ': + case '[': + case '>': + // okay to put a symbol + break; + default: + // This is no problem: "**one** *two* **three**" + // But this is trouble: "**one***two***three**" + // The most general solution: "**one***two***three**" + writer.write(''); + break; + } + + if (context.boldRequested) { + writer.write(''); + } + if (context.italicRequested) { + writer.write(''); + } + + writer.write(this.getEscapedText(middle)); + + if (context.italicRequested) { + writer.write(''); + } + if (context.boldRequested) { + writer.write(''); + } + } + + writer.write(parts[3]); // write trailing whitespace + } + + protected writeNodes(docNodes: ReadonlyArray, context: IMarkdownEmitterContext): void { + for (const docNode of docNodes) { + this.writeNode(docNode, context); + } + } +} diff --git a/apps/api-documenter/src/markdown/test/CustomMarkdownEmitter.test.ts b/apps/api-documenter/src/markdown/test/CustomMarkdownEmitter.test.ts new file mode 100644 index 00000000000..d529f0970c7 --- /dev/null +++ b/apps/api-documenter/src/markdown/test/CustomMarkdownEmitter.test.ts @@ -0,0 +1,214 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import * as path from 'path'; +import { FileDiffTest, FileSystem } from '@microsoft/node-core-library'; +import { + DocSection, + TSDocConfiguration, + DocPlainText, + StringBuilder, + DocParagraph, + DocSoftBreak, + DocLinkTag, + DocHtmlStartTag, + DocHtmlEndTag +} from '@microsoft/tsdoc'; + +import { CustomDocNodes } from '../../nodes/CustomDocNodeKind'; +import { DocHeading } from '../../nodes/DocHeading'; +import { DocEmphasisSpan } from '../../nodes/DocEmphasisSpan'; +import { DocTable } from '../../nodes/DocTable'; +import { DocTableRow } from '../../nodes/DocTableRow'; +import { DocTableCell } from '../../nodes/DocTableCell'; +import { CustomMarkdownEmitter } from '../CustomMarkdownEmitter'; +import { ApiModel, ApiItem } from '@microsoft/api-extractor'; + +test('render Markdown from TSDoc', done => { + const outputFolder: string = FileDiffTest.prepareFolder(__dirname, 'MarkdownPageRenderer'); + const configuration: TSDocConfiguration = CustomDocNodes.configuration; + + const output: DocSection = new DocSection({ configuration }); + + output.appendNodes([ + new DocHeading({ configuration, title: 'Simple bold test' }), + new DocParagraph({ configuration }, + [ + new DocPlainText({ configuration, text: 'This is a ' }), + new DocEmphasisSpan({ configuration, bold: true }, + [ + new DocPlainText({ configuration, text: 'bold' }) + ] + ), + new DocPlainText({ configuration, text: ' word.' }) + ] + ) + ]); + + output.appendNodes([ + new DocHeading({ configuration, title: 'All whitespace bold' }), + new DocParagraph({ configuration }, + [ + new DocEmphasisSpan({ configuration, bold: true }, + [ + new DocPlainText({ configuration, text: ' ' }) + ] + ) + ] + ) + ]); + + output.appendNodes([ + new DocHeading({ configuration, title: 'Newline bold' }), + new DocParagraph({ configuration }, + [ + new DocEmphasisSpan({ configuration, bold: true }, + [ + new DocPlainText({ configuration, text: 'line 1' }), + new DocSoftBreak({ configuration }), + new DocPlainText({ configuration, text: 'line 2' }) + ] + ) + ] + ) + ]); + + output.appendNodes([ + new DocHeading({ configuration, title: 'Newline bold with spaces' }), + new DocParagraph({ configuration }, + [ + new DocEmphasisSpan({ configuration, bold: true }, + [ + new DocPlainText({ configuration, text: ' line 1 ' }), + new DocSoftBreak({ configuration }), + new DocPlainText({ configuration, text: ' line 2 ' }), + new DocSoftBreak({ configuration }), + new DocPlainText({ configuration, text: ' line 3 ' }) + ] + ) + ] + ) + ]); + + output.appendNodes([ + new DocHeading({ configuration, title: 'Adjacent bold regions' }), + new DocParagraph({ configuration }, + [ + new DocEmphasisSpan({ configuration, bold: true }, + [ new DocPlainText({ configuration, text: 'one' }) ] + ), + new DocEmphasisSpan({ configuration, bold: true }, + [ new DocPlainText({ configuration, text: 'two' }) ] + ), + new DocEmphasisSpan({ configuration, bold: true }, + [ new DocPlainText({ configuration, text: ' three ' }) ] + ), + new DocPlainText({ configuration, text: '' }), + new DocEmphasisSpan({ configuration, bold: true }, + [ new DocPlainText({ configuration, text: 'four' }) ] + ), + new DocPlainText({ configuration, text: 'non-bold' }), + new DocEmphasisSpan({ configuration, bold: true }, + [ new DocPlainText({ configuration, text: 'five' }) ] + ) + ] + ) + ]); + + output.appendNodes([ + new DocHeading({ configuration, title: 'Adjacent to other characters' }), + new DocParagraph({ configuration }, + [ + new DocLinkTag({ + configuration, + tagName: '@link', + linkText: 'a link', + urlDestination: './index.md' + }), + new DocEmphasisSpan({ configuration, bold: true }, + [ new DocPlainText({ configuration, text: 'bold' }) ] + ), + new DocPlainText({ configuration, text: 'non-bold' }), + new DocPlainText({ configuration, text: 'more-non-bold' }) + ] + ) + ]); + + output.appendNodes([ + new DocHeading({ configuration, title: 'Bad characters' }), + new DocParagraph({ configuration }, + [ + new DocEmphasisSpan({ configuration, bold: true }, + [ new DocPlainText({ configuration, text: '*one*two*' }) ] + ), + new DocEmphasisSpan({ configuration, bold: true }, + [ new DocPlainText({ configuration, text: 'three*four' }) ] + ) + ] + ) + ]); + + output.appendNodes([ + new DocHeading({ configuration, title: 'Characters that should be escaped' }), + new DocParagraph({ configuration }, [ + new DocPlainText({ configuration, text: 'Double-encoded JSON: "{ \\"A\\": 123}"' }) + ]), + new DocParagraph({ configuration }, + [ new DocPlainText({ configuration, text: 'HTML chars: ' }) ] + ), + new DocParagraph({ configuration }, + [ new DocPlainText({ configuration, text: 'HTML escape: "' }) ] + ), + new DocParagraph({ configuration }, + [ new DocPlainText({ configuration, text: '3 or more hyphens: - -- --- ---- ----- ------' }) ] + ) + ]); + + output.appendNodes([ + new DocHeading({ configuration, title: 'HTML tag' }), + new DocParagraph({ configuration }, + [ + new DocHtmlStartTag({ configuration, name: 'b' }), + new DocPlainText({ configuration, text: 'bold' }), + new DocHtmlEndTag({ configuration, name: 'b' }) + ] + ) + ]); + + output.appendNodes([ + new DocHeading({ configuration, title: 'Table' }), + new DocTable({ + configuration, + headerTitles: [ 'Header 1', 'Header 2' ] + }, [ + new DocTableRow({ configuration }, [ + new DocTableCell({ configuration }, [ + new DocParagraph({ configuration }, + [ new DocPlainText({ configuration, text: 'Cell 1' }) ] + ) + ]), + new DocTableCell({ configuration }, [ + new DocParagraph({ configuration }, + [ new DocPlainText({ configuration, text: 'Cell 2' }) ] + ) + ]) + ]) + ]) + ]); + + const outputFilename: string = path.join(outputFolder, 'ActualOutput.md'); + const stringBuilder: StringBuilder = new StringBuilder(); + const apiModel: ApiModel = new ApiModel(); + const markdownEmitter: CustomMarkdownEmitter = new CustomMarkdownEmitter(apiModel); + markdownEmitter.emit(stringBuilder, output, { + contextApiItem: undefined, + onGetFilenameForApiItem: (apiItem: ApiItem) => { + return '#'; + } + }); + FileSystem.writeFile(outputFilename, stringBuilder.toString()); + + FileDiffTest.assertEqual(outputFilename, path.join(__dirname, 'ExpectedOutput.md')); + + done(); +}); diff --git a/apps/api-documenter/src/utils/test/ExpectedOutput.md b/apps/api-documenter/src/markdown/test/ExpectedOutput.md similarity index 51% rename from apps/api-documenter/src/utils/test/ExpectedOutput.md rename to apps/api-documenter/src/markdown/test/ExpectedOutput.md index bcd4b980b4c..28fb9d900b5 100644 --- a/apps/api-documenter/src/utils/test/ExpectedOutput.md +++ b/apps/api-documenter/src/markdown/test/ExpectedOutput.md @@ -1,8 +1,8 @@ -# Test page + ## Simple bold test -This is a **bold** word. +This is a bold word. ## All whitespace bold @@ -10,23 +10,23 @@ This is a **bold** word. ## Newline bold -**line 1 line 2** +line 1 line 2 ## Newline bold with spaces - **line 1 line 2 line 3** + line 1 line 2 line 3 ## Adjacent bold regions -**onetwo threefour**non-bold**five** +onetwo three fournon-boldfive ## Adjacent to other characters -[a link](./index.md)**bold**non-boldmore-non-bold +[a link](./index.md)boldnon-boldmore-non-bold ## Bad characters -**\*one\*two\*three\*four** +\*one\*two\*three\*four ## Characters that should be escaped @@ -38,4 +38,13 @@ HTML escape: &quot; 3 or more hyphens: - -- \-\-\- \-\-\-- \-\-\--- \-\-\-\-\-\- +## HTML tag + bold + +## Table + +|

Header 1

|

Header 2

| +| --- | --- | +|

Cell 1

|

Cell 2

| + diff --git a/apps/api-documenter/src/nodes/CustomDocNodeKind.ts b/apps/api-documenter/src/nodes/CustomDocNodeKind.ts new file mode 100644 index 00000000000..94bb644ac6a --- /dev/null +++ b/apps/api-documenter/src/nodes/CustomDocNodeKind.ts @@ -0,0 +1,59 @@ +import { TSDocConfiguration, DocNodeKind } from '@microsoft/tsdoc'; +import { DocEmphasisSpan } from './DocEmphasisSpan'; +import { DocHeading } from './DocHeading'; +import { DocNoteBox } from './DocNoteBox'; +import { DocTable } from './DocTable'; +import { DocTableCell } from './DocTableCell'; +import { DocTableRow } from './DocTableRow'; + +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +/** + * Identifies custom subclasses of {@link DocNode}. + */ +export const enum CustomDocNodeKind { + EmphasisSpan = 'EmphasisSpan', + Heading = 'Heading', + NoteBox = 'NoteBox', + Table = 'Table', + TableCell = 'TableCell', + TableRow = 'TableRow' +} + +export class CustomDocNodes { + private static _configuration: TSDocConfiguration | undefined; + + public static get configuration(): TSDocConfiguration { + if (CustomDocNodes._configuration === undefined) { + const configuration: TSDocConfiguration = new TSDocConfiguration(); + + configuration.docNodeManager.registerDocNodes('@micrososft/api-documenter', [ + { docNodeKind: CustomDocNodeKind.EmphasisSpan, constructor: DocEmphasisSpan }, + { docNodeKind: CustomDocNodeKind.Heading, constructor: DocHeading }, + { docNodeKind: CustomDocNodeKind.NoteBox, constructor: DocNoteBox }, + { docNodeKind: CustomDocNodeKind.Table, constructor: DocTable }, + { docNodeKind: CustomDocNodeKind.TableCell, constructor: DocTableCell }, + { docNodeKind: CustomDocNodeKind.TableRow, constructor: DocTableRow } + ]); + + configuration.docNodeManager.registerAllowableChildren(CustomDocNodeKind.EmphasisSpan, [ + DocNodeKind.PlainText, + DocNodeKind.SoftBreak + ]); + + configuration.docNodeManager.registerAllowableChildren(DocNodeKind.Section, [ + CustomDocNodeKind.Heading, + CustomDocNodeKind.NoteBox, + CustomDocNodeKind.Table + ]); + + configuration.docNodeManager.registerAllowableChildren(DocNodeKind.Paragraph, [ + CustomDocNodeKind.EmphasisSpan + ]); + + CustomDocNodes._configuration = configuration; + } + return CustomDocNodes._configuration; + } +} diff --git a/apps/api-documenter/src/nodes/DocEmphasisSpan.ts b/apps/api-documenter/src/nodes/DocEmphasisSpan.ts new file mode 100644 index 00000000000..986d5029a41 --- /dev/null +++ b/apps/api-documenter/src/nodes/DocEmphasisSpan.ts @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { + DocNode, + DocNodeContainer, + IDocNodeContainerParameters +} from '@microsoft/tsdoc'; +import { CustomDocNodeKind } from './CustomDocNodeKind'; + +/** + * Constructor parameters for {@link DocEmphasisSpan}. + */ +export interface IDocEmphasisSpanParameters extends IDocNodeContainerParameters { + bold?: boolean; + italic?: boolean; +} + +/** + * Represents a span of text that is styled with CommonMark emphasis (italics), strong emphasis (boldface), + * or both. + */ +export class DocEmphasisSpan extends DocNodeContainer { + public readonly bold: boolean; + public readonly italic: boolean; + + public constructor(parameters: IDocEmphasisSpanParameters, children?: DocNode[]) { + super(parameters, children); + this.bold = !!parameters.bold; + this.italic = !!parameters.italic; + } + + /** @override */ + public get kind(): string { + return CustomDocNodeKind.EmphasisSpan; + } +} diff --git a/apps/api-documenter/src/nodes/DocHeading.ts b/apps/api-documenter/src/nodes/DocHeading.ts new file mode 100644 index 00000000000..3f925f1a3f6 --- /dev/null +++ b/apps/api-documenter/src/nodes/DocHeading.ts @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { + IDocNodeParameters, + DocNode +} from '@microsoft/tsdoc'; +import { CustomDocNodeKind } from './CustomDocNodeKind'; + +/** + * Constructor parameters for {@link DocHeading}. + */ +export interface IDocHeadingParameters extends IDocNodeParameters { + title: string; + level?: number; +} + +/** + * Represents table, similar to an HTML `

` or `

` element. + */ +export class DocHeading extends DocNode { + public readonly title: string; + public readonly level: number; + + /** + * Don't call this directly. Instead use {@link TSDocParser} + * @internal + */ + public constructor(parameters: IDocHeadingParameters) { + super(parameters); + this.title = parameters.title; + this.level = parameters.level !== undefined ? parameters.level : 1; + + if (this.level < 1 || this.level > 5) { + throw new Error('IDocHeadingParameters.level must be a number between 1 and 5'); + } + } + + /** @override */ + public get kind(): string { + return CustomDocNodeKind.Heading; + } +} diff --git a/apps/api-documenter/src/nodes/DocNoteBox.ts b/apps/api-documenter/src/nodes/DocNoteBox.ts new file mode 100644 index 00000000000..302112d45f3 --- /dev/null +++ b/apps/api-documenter/src/nodes/DocNoteBox.ts @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { + IDocNodeParameters, + DocNode, + DocSection +} from '@microsoft/tsdoc'; +import { CustomDocNodeKind } from './CustomDocNodeKind'; + +/** + * Constructor parameters for {@link DocNoteBox}. + */ +export interface IDocNoteBoxParameters extends IDocNodeParameters { +} + +/** + * Represents a note box, which is typically displayed as a bordered box containing informational text. + */ +export class DocNoteBox extends DocNode { + public readonly content: DocSection; + + public constructor(parameters: IDocNoteBoxParameters, sectionChildNodes?: ReadonlyArray) { + super(parameters); + this.content = new DocSection({ configuration: this.configuration }, sectionChildNodes); + } + + /** @override */ + public get kind(): string { + return CustomDocNodeKind.NoteBox; + } + + /** @override */ + protected onGetChildNodes(): ReadonlyArray { + return [this.content]; + } +} diff --git a/apps/api-documenter/src/nodes/DocTable.ts b/apps/api-documenter/src/nodes/DocTable.ts new file mode 100644 index 00000000000..0d74003c59d --- /dev/null +++ b/apps/api-documenter/src/nodes/DocTable.ts @@ -0,0 +1,80 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { + IDocNodeParameters, + DocNode +} from '@microsoft/tsdoc'; +import { CustomDocNodeKind } from './CustomDocNodeKind'; +import { DocTableRow } from './DocTableRow'; +import { DocTableCell } from './DocTableCell'; + +/** + * Constructor parameters for {@link DocTable}. + */ +export interface IDocTableParameters extends IDocNodeParameters { + headerCells?: ReadonlyArray; + headerTitles?: string[]; +} + +/** + * Represents table, similar to an HTML `` element. + */ +export class DocTable extends DocNode { + public readonly header: DocTableRow; + + private _rows: DocTableRow[]; + + public constructor(parameters: IDocTableParameters, rows?: ReadonlyArray) { + super(parameters); + + this.header = new DocTableRow({ configuration: this.configuration }); + this._rows = []; + + if (parameters) { + if (parameters.headerTitles) { + if (parameters.headerCells) { + throw new Error('IDocTableParameters.headerCells and IDocTableParameters.headerTitles' + + ' cannot both be specified'); + } + for (const cellText of parameters.headerTitles) { + this.header.addPlainTextCell(cellText); + } + } else if (parameters.headerCells) { + for (const cell of parameters.headerCells) { + this.header.addCell(cell); + } + } + } + + if (rows) { + for (const row of rows) { + this.addRow(row); + } + } + } + + /** @override */ + public get kind(): string { + return CustomDocNodeKind.Table; + } + + public get rows(): ReadonlyArray { + return this._rows; + } + + public addRow(row: DocTableRow): void { + this._rows.push(row); + } + + public createAndAddRow(): DocTableRow { + const row: DocTableRow = new DocTableRow({ configuration: this.configuration }); + this.addRow(row); + return row; + } + + /** @override */ + protected onGetChildNodes(): ReadonlyArray { + return [this.header, ...this._rows]; + } +} diff --git a/apps/api-documenter/src/nodes/DocTableCell.ts b/apps/api-documenter/src/nodes/DocTableCell.ts new file mode 100644 index 00000000000..23b679d8c5f --- /dev/null +++ b/apps/api-documenter/src/nodes/DocTableCell.ts @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { + IDocNodeParameters, + DocNode, + DocSection +} from '@microsoft/tsdoc'; +import { CustomDocNodeKind } from './CustomDocNodeKind'; + +/** + * Constructor parameters for {@link DocTableCell}. + */ +export interface IDocTableCellParameters extends IDocNodeParameters { +} + +/** + * Represents table cell, similar to an HTML `` element. + */ +export class DocTableRow extends DocNode { + private readonly _cells: DocTableCell[]; + + public constructor(parameters: IDocTableRowParameters, cells?: ReadonlyArray) { + super(parameters); + + this._cells = []; + if (cells) { + for (const cell of cells) { + this.addCell(cell); + } + } + } + + /** @override */ + public get kind(): string { + return CustomDocNodeKind.TableRow; + } + + public get cells(): ReadonlyArray { + return this._cells; + } + + public addCell(cell: DocTableCell): void { + this._cells.push(cell); + } + + public createAndAddCell(): DocTableCell { + const newCell: DocTableCell = new DocTableCell({ configuration: this.configuration }); + this.addCell(newCell); + return newCell; + } + + public addPlainTextCell(cellContent: string): DocTableCell { + const cell: DocTableCell = this.createAndAddCell(); + cell.content.appendNodeInParagraph(new DocPlainText({ + configuration: this.configuration, + text: cellContent + })); + return cell; + } + + /** @override */ + protected onGetChildNodes(): ReadonlyArray { + return this._cells; + } +} diff --git a/apps/api-documenter/src/utils/DocItemSet.ts b/apps/api-documenter/src/utils/DocItemSet.ts deleted file mode 100644 index a1b7bcd1ab4..00000000000 --- a/apps/api-documenter/src/utils/DocItemSet.ts +++ /dev/null @@ -1,266 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -// See LICENSE in the project root for license information. - -import { PackageName } from '@microsoft/node-core-library'; -import { - IApiPackage, - ApiItem, - ApiJsonFile, - IApiItemReference -} from '@microsoft/api-extractor'; - -export enum DocItemKind { - Package, - Namespace, - Class, - Interface, - Method, - Constructor, - Function, - Property, - Enum, - EnumMember -} - -/** - * Tracks additional metadata for an ApiItem while generating documentation. - * - * @remarks - * - * The api-documenter tool reads a tree of ApiItem objects from *.api.json files, and then - * generates documentation output. To facilitate this process, a DocItem object is created - * for each hyperlinkable ApiItem (i.e. major types such as package, class, member, etc). - * The DocItems track the parent/child hierarchy, which is used for scenarios such as: - * - * - Preventing broken links, by checking that a referenced object is part of the documentation set - * before generating a link - * - Detecting the base classes and derived classes for a class - * - Walking the parent chain to build a unique documentation ID for each item - * - * The set of DocItem objects is managed by DocItemSet. - */ -export class DocItem { - public readonly kind: DocItemKind; - - public readonly apiItem: ApiItem; - public readonly name: string; - - public readonly docItemSet: DocItemSet; - public readonly parent: DocItem | undefined; - public readonly children: DocItem[] = []; - - public constructor(apiItem: ApiItem, name: string, docItemSet: DocItemSet, - parent: DocItem | undefined) { - - this.apiItem = apiItem; - this.name = name; - this.docItemSet = docItemSet; - - switch (this.apiItem.kind) { - case 'package': - case 'namespace': - this.kind = this.apiItem.kind === 'package' ? DocItemKind.Package : DocItemKind.Namespace; - for (const exportName of Object.keys(this.apiItem.exports)) { - const child: ApiItem = this.apiItem.exports[exportName]; - this.children.push(new DocItem(child, exportName, this.docItemSet, this)); - } - break; - - case 'class': - case 'interface': - this.kind = this.apiItem.kind === 'class' ? DocItemKind.Class : DocItemKind.Interface; - if (this.apiItem.members) { - for (const memberName of Object.keys(this.apiItem.members)) { - const child: ApiItem = this.apiItem.members[memberName]; - this.children.push(new DocItem(child, memberName, this.docItemSet, this)); - } - } - break; - - case 'method': - this.kind = DocItemKind.Method; - break; - case 'constructor': - this.kind = DocItemKind.Constructor; - this.name = 'constructor'; - break; - case 'function': - this.kind = DocItemKind.Function; - break; - case 'property': - this.kind = DocItemKind.Property; - break; - case 'enum': - this.kind = DocItemKind.Enum; - if (this.apiItem.values) { - for (const memberName of Object.keys(this.apiItem.values)) { - const child: ApiItem = this.apiItem.values[memberName]; - this.children.push(new DocItem(child, memberName, this.docItemSet, this)); - } - } - break; - case 'enum value': - this.kind = DocItemKind.EnumMember; - break; - default: - throw new Error('Unsupported item kind: ' + (this.apiItem as ApiItem).kind); - } - - this.parent = parent; - } - - /** - * Returns the parent chain in reverse order, i.e. starting with the root of the tree - * (which is the package). - */ - public getHierarchy(): DocItem[] { - const result: DocItem[] = []; - for (let current: DocItem | undefined = this; current; current = current.parent) { - result.unshift(current); - } - return result; - } - - public getApiReference(): IApiItemReference { - const reference: IApiItemReference & { moreHierarchies: string[]; } = { - scopeName: '', - packageName: '', - exportName: '', - memberName: '', - // TODO: quick fix for api ref inside namespace, need to adjust IApiItemReference later - moreHierarchies: [] - }; - let i: number = 0; - for (const docItem of this.getHierarchy()) { - switch (i) { - case 0: - reference.scopeName = PackageName.getScope(docItem.name); - reference.packageName = PackageName.getUnscopedName(docItem.name); - break; - case 1: - reference.exportName = docItem.name; - break; - case 2: - reference.memberName = docItem.name; - break; - default: - reference.moreHierarchies.push(docItem.name); - break; - // throw new Error('Unable to create API reference for ' + this.name); - } - ++i; - } - return reference; - } - - public tryGetChild(name: string): DocItem | undefined { - for (const child of this.children) { - if (child.name === name) { - return child; - } - } - return undefined; - } - - /** - * Visits this DocItem and every child DocItem in a preorder traversal. - */ - public forEach(callback: (docItem: DocItem) => void): void { - callback(this); - for (const child of this.children) { - child.forEach(callback); - } - } -} - -/** - * Return value for DocItemSet.resolveApiItemReference() - */ -export interface IDocItemSetResolveResult { - /** - * The matching DocItem object, if found. - */ - docItem: DocItem | undefined; - - /** - * The closest matching parent DocItem, if any. - */ - closestMatch: DocItem | undefined; -} - -/** - * The collection of DocItem objects that api-documenter is processing. - * - * @remarks - * - * The DocItemSet is built by repeatedly calling loadApiJsonFile() for each file that we want - * to process. After all files are loaded, calculateReferences() is used to calculate - * cross-references and build up the indexes. - */ -export class DocItemSet { - public readonly docPackagesByName: Map = new Map(); - public readonly docPackages: DocItem[] = []; - - public loadApiJsonFile(apiJsonFilename: string): void { - const apiPackage: IApiPackage = ApiJsonFile.loadFromFile(apiJsonFilename); - - const docItem: DocItem = new DocItem(apiPackage, apiPackage.name, this, undefined); - this.docPackagesByName.set(apiPackage.name, docItem); - this.docPackages.push(docItem); - } - - /** - * Attempts to find the DocItem described by an IApiItemReference. If no matching item is - * found, then undefined is returned. - */ - public resolveApiItemReference( - reference: IApiItemReference & { moreHierarchies?: string[]; } - ): IDocItemSetResolveResult { - const result: IDocItemSetResolveResult = { - docItem: undefined, - closestMatch: undefined - }; - - const packageName: string = PackageName.combineParts(reference.scopeName, reference.packageName); - - let current: DocItem | undefined = undefined; - - for (const nameToMatch of [ - packageName, reference.exportName, reference.memberName, - // TODO: quick fix for api ref inside namespace, need to adjust IApiItemReference later - ...(reference.moreHierarchies || []) - ]) { - if (!nameToMatch) { - // Success, since we ran out of stuff to match - break; - } - - // Is this the first time through the loop? - if (!current) { - // Yes, start with the package - current = this.docPackagesByName.get(nameToMatch); - } else { - // No, walk the tree - current = current.tryGetChild(nameToMatch); - } - - if (!current) { - return result; // no match; result.closestMatch has the closest match - } - - result.closestMatch = current; - } - - result.docItem = result.closestMatch; - return result; - } - - /** - * Visits every DocItem in the tree. - */ - public forEach(callback: (docItem: DocItem) => void): void { - for (const docPackage of this.docPackages) { - docPackage.forEach(callback); - } - } -} diff --git a/apps/api-documenter/src/utils/MarkdownRenderer.ts b/apps/api-documenter/src/utils/MarkdownRenderer.ts deleted file mode 100644 index f8a48e0dc3b..00000000000 --- a/apps/api-documenter/src/utils/MarkdownRenderer.ts +++ /dev/null @@ -1,392 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -// See LICENSE in the project root for license information. - -import { - IMarkupText, - MarkupElement, - IApiItemReference -} from '@microsoft/api-extractor'; - -/** - * Helper class used by MarkdownPageRenderer - */ -class SimpleWriter { - private _buffer: string = ''; - - public write(s: string): void { - this._buffer += s; - } - - public writeLine(s: string = ''): void { - this._buffer += s + '\n'; - } - - /** - * Adds a newline if the file pointer is not already at the start of the line - */ - public ensureNewLine(): void { - if (this.peekLastCharacter() !== '\n') { - this.write('\n'); - } - } - - /** - * Adds up to two newlines to ensure that there is a blank line above the current line. - */ - public ensureSkippedLine(): void { - this.ensureNewLine(); - if (this.peekSecondLastCharacter() !== '\n') { - this.write('\n'); - } - } - - public peekLastCharacter(): string { - return this._buffer.substr(-1, 1); - } - - public peekSecondLastCharacter(): string { - return this._buffer.substr(-2, 1); - } - - public toString(): string { - return this._buffer; - } -} - -interface IRenderContext { - writer: SimpleWriter; - insideTable: boolean; - options: IMarkdownRendererOptions; - depth: number; -} - -export interface IMarkdownRenderApiLinkArgs { - /** - * The IApiItemReference being rendered. - */ - readonly reference: IApiItemReference; - /** - * The callback can assign text here that will be inserted before the link text. - * Example: "[" - */ - prefix: string; - - /** - * The callback can assign text here that will be inserted after the link text. - * Example: "](./TargetPage.md)" - */ - suffix: string; -} - -export interface IMarkdownRendererOptions { - /** - * This callback receives an IMarkupApiLink, and returns the rendered markdown content. - * If the callback is not provided, an error occurs if an IMarkupApiLink is encountered. - */ - onRenderApiLink?: (args: IMarkdownRenderApiLinkArgs) => void; -} - -/** - * Renders MarkupElement content in the Markdown file format. - * For more info: https://en.wikipedia.org/wiki/Markdown - */ -export class MarkdownRenderer { - - public static renderElements(elements: MarkupElement[], options: IMarkdownRendererOptions): string { - const writer: SimpleWriter = new SimpleWriter(); - - const context: IRenderContext = { - writer: writer, - insideTable: false, - options: options, - depth: 0 - }; - - MarkdownRenderer._writeElements(elements, context); - - if (context.depth !== 0) { - throw new Error('Unbalanced depth'); // this would indicate a program bug - } - - writer.ensureNewLine(); // finish the last line - - return writer.toString(); - } - - private static _getEscapedText(text: string): string { - const textWithBackslashes: string = text - .replace(/\\/g, '\\\\') // first replace the escape character - .replace(/[*#[\]_|`~]/g, (x) => '\\' + x) // then escape any special characters - .replace(/---/g, '\\-\\-\\-') // hyphens only if it's 3 or more - .replace(/&/g, '&') - .replace(//g, '>'); - return textWithBackslashes; - } - - /** - * Merges any IMarkupText elements with compatible styles; this simplifies the emitted Markdown - */ - private static _mergeTextElements(elements: MarkupElement[]): MarkupElement[] { - const mergedElements: MarkupElement[] = []; - let previousElement: MarkupElement|undefined; - - for (const element of elements) { - if (previousElement) { - if (element.kind === 'text' && previousElement.kind === 'text') { - if (element.bold === previousElement.bold && element.italics === previousElement.italics) { - // merge them - mergedElements.pop(); // pop the previous element - - const combinedElement: IMarkupText = { // push a combined element - kind: 'text', - text: previousElement.text + element.text, - bold: previousElement.bold, - italics: previousElement.italics - }; - - mergedElements.push(combinedElement); - previousElement = combinedElement; - continue; - } - } - } - - mergedElements.push(element); - previousElement = element; - } - - return mergedElements; - } - - private static _writeElements(elements: MarkupElement[], context: IRenderContext): void { - ++context.depth; - const writer: SimpleWriter = context.writer; - - const mergedElements: MarkupElement[] = MarkdownRenderer._mergeTextElements(elements); - - for (const element of mergedElements) { - switch (element.kind) { - case 'text': - let normalizedContent: string = element.text; - if (context.insideTable) { - normalizedContent = normalizedContent.replace('\n', ' '); - } - - const lines: string[] = normalizedContent.split('\n'); - - let firstLine: boolean = true; - - for (const line of lines) { - if (firstLine) { - firstLine = false; - } else { - writer.writeLine(); - } - - // split out the [ leading whitespace, content, trailing whitespace ] - const parts: string[] = line.match(/^(\s*)(.*?)(\s*)$/) || []; - - writer.write(parts[1]); // write leading whitespace - - const middle: string = parts[2]; - - if (middle !== '') { - switch (writer.peekLastCharacter()) { - case '': - case '\n': - case ' ': - case '[': - case '>': - // okay to put a symbol - break; - default: - // This is no problem: "**one** *two* **three**" - // But this is trouble: "**one***two***three**" - // The most general solution: "**one***two***three**" - writer.write(''); - break; - } - - if (element.bold) { - writer.write('**'); - } - if (element.italics) { - writer.write('_'); - } - - writer.write(this._getEscapedText(middle)); - - if (element.italics) { - writer.write('_'); - } - if (element.bold) { - writer.write('**'); - } - } - - writer.write(parts[3]); // write trailing whitespace - } - break; - case 'html-tag': - // write the HTML element verbatim into the output - writer.write(element.token); - break; - case 'code': - writer.write('`'); - if (context.insideTable) { - const parts: string[] = element.text.split(/[\r\n]+/g); - writer.write(parts.join('`

`')); - } else { - writer.write(element.text); - } - writer.write('`'); - break; - case 'api-link': - if (!context.options.onRenderApiLink) { - throw new Error('IMarkupApiLink cannot be rendered because a renderApiLink handler was not provided'); - } - - const args: IMarkdownRenderApiLinkArgs = { - reference: element.target, - prefix: '', - suffix: '' - }; - - // NOTE: The onRenderApiLink() callback will assign values to the args.prefix - // and args.suffix properties, which are used below. (It is modeled this way because - // MarkdownRenderer._writeElements() may need to emit different escaping e.g. depending - // on what characters were written by writer.write(args.prefix).) - context.options.onRenderApiLink(args); - - if (args.prefix) { - writer.write(args.prefix); - } - MarkdownRenderer._writeElements(element.elements, context); - if (args.suffix) { - writer.write(args.suffix); - } - - break; - case 'web-link': - writer.write('['); - MarkdownRenderer._writeElements(element.elements, context); - writer.write(`](${element.targetUrl})`); - break; - case 'paragraph': - if (context.insideTable) { - writer.write('

'); - } else { - writer.ensureNewLine(); - writer.writeLine(); - } - break; - case 'break': - writer.writeLine('
'); - break; - case 'heading1': - writer.ensureSkippedLine(); - writer.writeLine('## ' + MarkdownRenderer._getEscapedText(element.text)); - writer.writeLine(); - break; - case 'heading2': - writer.ensureSkippedLine(); - writer.writeLine('### ' + MarkdownRenderer._getEscapedText(element.text)); - writer.writeLine(); - break; - case 'code-box': - writer.ensureNewLine(); - writer.write('```'); - switch (element.highlighter) { - case 'javascript': - writer.write('javascript'); - break; - case 'plain': - break; - default: - throw new Error('Unimplemented highlighter'); - } - writer.writeLine(); - writer.write(element.text); - writer.writeLine(); - writer.writeLine('```'); - break; - case 'note-box': - writer.ensureNewLine(); - writer.write('> '); - MarkdownRenderer._writeElements(element.elements, context); - writer.ensureNewLine(); - writer.writeLine(); - break; - case 'table': - // GitHub's markdown renderer chokes on tables that don't have a blank line above them, - // whereas VS Code's renderer is totally fine with it. - writer.ensureSkippedLine(); - - context.insideTable = true; - - let columnCount: number = 0; - for (const row of element.rows.concat(element.header || [])) { - if (row.cells.length > columnCount) { - columnCount = row.cells.length; - } - } - - // write the header - writer.write('| '); - for (let i: number = 0; i < columnCount; ++i) { - writer.write(' '); - if (element.header) { // markdown requires the header - MarkdownRenderer._writeElements(element.header.cells[i].elements, context); - } - writer.write(' |'); - } - writer.writeLine(); - - // write the divider - writer.write('| '); - for (let i: number = 0; i < columnCount; ++i) { - writer.write(' --- |'); - } - writer.writeLine(); - - for (const row of element.rows) { - writer.write('| '); - for (const cell of row.cells) { - writer.write(' '); - MarkdownRenderer._writeElements(cell.elements, context); - writer.write(' |'); - } - writer.writeLine(); - } - writer.writeLine(); - - context.insideTable = false; - - break; - case 'page': - if (context.depth !== 1 || elements.length !== 1) { - throw new Error('The page element must be the top-level element of the document'); - } - - if (element.breadcrumb.length) { - // Write the breadcrumb before the title - MarkdownRenderer._writeElements(element.breadcrumb, context); - writer.ensureNewLine(); - writer.writeLine(); - } - - writer.writeLine('# ' + this._getEscapedText(element.title)); - writer.writeLine(); - - MarkdownRenderer._writeElements(element.elements, context); - writer.ensureNewLine(); // finish the last line - break; - - default: - throw new Error('Unsupported element kind: ' + element.kind); - } - } - --context.depth; - } -} diff --git a/apps/api-documenter/src/utils/Utilities.ts b/apps/api-documenter/src/utils/Utilities.ts index 7bf1b1a6638..5c8bddc714b 100644 --- a/apps/api-documenter/src/utils/Utilities.ts +++ b/apps/api-documenter/src/utils/Utilities.ts @@ -2,40 +2,17 @@ // See LICENSE in the project root for license information. import { - IApiMethod, - IApiFunction, - IApiConstructor + ApiFunctionLikeMixin, ApiItem } from '@microsoft/api-extractor'; export class Utilities { - - /** - * Used to validate a data structure before converting to JSON or YAML format. Reports - * an error if there are any undefined members, since "undefined" is not supported in JSON. - */ - // tslint:disable-next-line:no-any - public static validateNoUndefinedMembers(json: any, jsonPath: string = ''): void { - if (!json) { - return; - } - if (typeof json === 'object') { - for (const key of Object.keys(json)) { - const keyWithPath: string = jsonPath + '/' + key; - // tslint:disable-next-line:no-any - const value: any = json[key]; - if (value === undefined) { - throw new Error(`The key "${keyWithPath}" is undefined`); - } - Utilities.validateNoUndefinedMembers(value, keyWithPath); - } - } - - } - /** * Generates a concise signature for a function. Example: "getArea(width, height)" */ - public static getConciseSignature(methodName: string, method: IApiMethod | IApiConstructor | IApiFunction): string { - return methodName + '(' + Object.keys(method.parameters).join(', ') + ')'; + public static getConciseSignature(apiItem: ApiItem): string { + if (ApiFunctionLikeMixin.isBaseClassOf(apiItem)) { + return apiItem.name + '(' + apiItem.parameters.map(x => x.name).join(', ') + ')'; + } + return apiItem.name; } } diff --git a/apps/api-documenter/src/utils/test/MarkdownRenderer.test.ts b/apps/api-documenter/src/utils/test/MarkdownRenderer.test.ts deleted file mode 100644 index 24c8b8e3553..00000000000 --- a/apps/api-documenter/src/utils/test/MarkdownRenderer.test.ts +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -// See LICENSE in the project root for license information. - -import * as path from 'path'; -import { FileDiffTest, FileSystem } from '@microsoft/node-core-library'; -import { IMarkupPage, Markup } from '@microsoft/api-extractor'; - -import { MarkdownRenderer } from '../MarkdownRenderer'; - -describe('MarkdownPageRenderer', () => { - it('renders markdown', done => { - - const outputFolder: string = FileDiffTest.prepareFolder(__dirname, 'MarkdownPageRenderer'); - - const markupPage: IMarkupPage = Markup.createPage('Test page'); - - markupPage.elements.push(Markup.createHeading1('Simple bold test')); - markupPage.elements.push(...Markup.createTextElements('This is a ')); - markupPage.elements.push(...Markup.createTextElements('bold', { bold: true })); - markupPage.elements.push(...Markup.createTextElements(' word.')); - - markupPage.elements.push(Markup.createHeading1('All whitespace bold')); - markupPage.elements.push(...Markup.createTextElements(' ', { bold: true })); - - markupPage.elements.push(Markup.createHeading1('Newline bold')); - markupPage.elements.push(...Markup.createTextElements('line 1\nline 2', { bold: true })); - - markupPage.elements.push(Markup.createHeading1('Newline bold with spaces')); - markupPage.elements.push(...Markup.createTextElements(' line 1 \n line 2 \n line 3 ', { bold: true })); - - markupPage.elements.push(Markup.createHeading1('Adjacent bold regions')); - markupPage.elements.push(...Markup.createTextElements('one', { bold: true })); - markupPage.elements.push(...Markup.createTextElements('two', { bold: true })); - markupPage.elements.push(...Markup.createTextElements(' three', { bold: true })); - markupPage.elements.push(...Markup.createTextElements('', { bold: false })); - markupPage.elements.push(...Markup.createTextElements('four', { bold: true })); - markupPage.elements.push(...Markup.createTextElements('non-bold', { bold: false })); - markupPage.elements.push(...Markup.createTextElements('five', { bold: true })); - - markupPage.elements.push(Markup.createHeading1('Adjacent to other characters')); - // Creates a "[" before the bold text - markupPage.elements.push(Markup.createWebLinkFromText('a link', './index.md')); - markupPage.elements.push(...Markup.createTextElements('bold', { bold: true })); - markupPage.elements.push(...Markup.createTextElements('non-bold', { bold: false })); - markupPage.elements.push(...Markup.createTextElements('more-non-bold', { bold: false })); - - markupPage.elements.push(Markup.createHeading1('Bad characters')); - markupPage.elements.push(...Markup.createTextElements('*one*two*', { bold: true })); - markupPage.elements.push(...Markup.createTextElements('three*four', { bold: true })); - - markupPage.elements.push(Markup.createHeading1('Characters that should be escaped')); - markupPage.elements.push(...Markup.createTextParagraphs( - 'Double-encoded JSON: "{ \\"A\\": 123}"\n\n')); - markupPage.elements.push(...Markup.createTextParagraphs( - 'HTML chars: \n\n')); - markupPage.elements.push(...Markup.createTextParagraphs( - 'HTML escape: "\n\n')); - markupPage.elements.push(...Markup.createTextParagraphs( - '3 or more hyphens: - -- --- ---- ----- ------\n\n')); - - markupPage.elements.push(...[ - Markup.createHtmlTag(''), - ...Markup.createTextElements('bold'), - Markup.createHtmlTag('') - ]); - - const outputFilename: string = path.join(outputFolder, 'ActualOutput.md'); - FileSystem.writeFile(outputFilename, MarkdownRenderer.renderElements([markupPage], { })); - - FileDiffTest.assertEqual(outputFilename, path.join(__dirname, 'ExpectedOutput.md')); - - done(); - }); -}); diff --git a/apps/api-documenter/src/yaml/IYamlApiFile.ts b/apps/api-documenter/src/yaml/IYamlApiFile.ts index cbea1b37aa9..abcd8e4c5f4 100644 --- a/apps/api-documenter/src/yaml/IYamlApiFile.ts +++ b/apps/api-documenter/src/yaml/IYamlApiFile.ts @@ -45,7 +45,12 @@ export interface IYamlItem { isPreview?: boolean; langs?: string[]; name?: string; - numericValue?: number; + /** + * NOTE: In TypeScript, enum members can be strings or integers. + * If it is an integer, then enumMember.value will be a string representation of the integer. + * If it is a string, then enumMember.value will include the quotation marks. + */ + numericValue?: string; package?: string; source?: IYamlSource; summary?: string; diff --git a/apps/api-documenter/src/yaml/YamlDocumenter.ts b/apps/api-documenter/src/yaml/YamlDocumenter.ts deleted file mode 100644 index 5691755a03d..00000000000 --- a/apps/api-documenter/src/yaml/YamlDocumenter.ts +++ /dev/null @@ -1,590 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -// See LICENSE in the project root for license information. - -import * as path from 'path'; -import * as colors from 'colors'; - -import yaml = require('js-yaml'); -import { - JsonFile, - JsonSchema, - PackageName, - FileSystem, - NewlineKind -} from '@microsoft/node-core-library'; -import { - MarkupElement, - IApiMethod, - IApiConstructor, - IApiParameter, - IApiProperty, - IApiEnumMember, - IApiClass, - IApiInterface, - Markup -} from '@microsoft/api-extractor'; - -import { DocItemSet, DocItem, DocItemKind, IDocItemSetResolveResult } from '../utils/DocItemSet'; -import { - IYamlApiFile, - IYamlItem, - IYamlSyntax, - IYamlParameter -} from './IYamlApiFile'; -import { - IYamlTocFile, - IYamlTocItem -} from './IYamlTocFile'; -import { Utilities } from '../utils/Utilities'; -import { MarkdownRenderer, IMarkdownRenderApiLinkArgs } from '../utils/MarkdownRenderer'; - -const yamlApiSchema: JsonSchema = JsonSchema.fromFile(path.join(__dirname, 'typescript.schema.json')); - -/** - * Writes documentation in the Universal Reference YAML file format, as defined by typescript.schema.json. - */ -export class YamlDocumenter { - private _docItemSet: DocItemSet; - - // This is used by the _linkToUidIfPossible() workaround. - // It stores a mapping from type name (e.g. "MyClass") to the corresponding DocItem. - // If the mapping would be ambiguous (e.g. "MyClass" is defined by multiple packages) - // then it is excluded from the mapping. Also excluded are DocItems (such as package - // and function) which are not typically used as a data type. - private _docItemsByTypeName: Map; - - private _outputFolder: string; - - public constructor(docItemSet: DocItemSet) { - this._docItemSet = docItemSet; - this._docItemsByTypeName = new Map(); - - this._initDocItemsByTypeName(); - } - - public generateFiles(outputFolder: string): void { // virtual - this._outputFolder = outputFolder; - - console.log(); - this._deleteOldOutputFiles(); - - for (const docPackage of this._docItemSet.docPackages) { - this._visitDocItems(docPackage, undefined); - } - - this._writeTocFile(this._docItemSet.docPackages); - } - - protected onGetTocRoot(): IYamlTocItem { // virtual - return { - name: 'SharePoint Framework reference', - href: '~/overview/sharepoint.md', - items: [ ] - }; - } - - protected onCustomizeYamlItem(yamlItem: IYamlItem): void { // virtual - // (overridden by child class) - } - - private _visitDocItems(docItem: DocItem, parentYamlFile: IYamlApiFile | undefined): boolean { - const yamlItem: IYamlItem | undefined = this._generateYamlItem(docItem); - if (!yamlItem) { - return false; - } - - this.onCustomizeYamlItem(yamlItem); - - if (this._shouldEmbed(docItem.kind)) { - if (!parentYamlFile) { - throw new Error('Missing file context'); // program bug - } - parentYamlFile.items.push(yamlItem); - } else { - const newYamlFile: IYamlApiFile = { - items: [] - }; - newYamlFile.items.push(yamlItem); - - const flattenedChildren: DocItem[] = this._flattenNamespaces(docItem.children); - - for (const child of flattenedChildren) { - if (this._visitDocItems(child, newYamlFile)) { - if (!yamlItem.children) { - yamlItem.children = []; - } - yamlItem.children.push(this._getUid(child)); - } - } - - const yamlFilePath: string = this._getYamlFilePath(docItem); - - if (docItem.kind === DocItemKind.Package) { - console.log('Writing ' + this._getYamlFilePath(docItem)); - } - - this._writeYamlFile(newYamlFile, yamlFilePath, 'UniversalReference', yamlApiSchema); - - if (parentYamlFile) { - if (!parentYamlFile.references) { - parentYamlFile.references = []; - } - - parentYamlFile.references.push({ - uid: this._getUid(docItem), - name: this._getYamlItemName(docItem) - }); - - } - } - - return true; - } - - // Since the YAML schema does not yet support nested namespaces, we simply omit them from - // the tree. However, _getYamlItemName() will show the namespace. - private _flattenNamespaces(items: DocItem[]): DocItem[] { - const flattened: DocItem[] = []; - for (const item of items) { - if (item.kind === DocItemKind.Namespace) { - flattened.push(... this._flattenNamespaces(item.children)); - } else { - flattened.push(item); - } - } - return flattened; - } - - /** - * Write the table of contents - */ - private _writeTocFile(docItems: DocItem[]): void { - const tocFile: IYamlTocFile = { - items: [ ] - }; - - const rootItem: IYamlTocItem = this.onGetTocRoot(); - tocFile.items.push(rootItem); - - rootItem.items!.push(...this._buildTocItems(docItems)); - - const tocFilePath: string = path.join(this._outputFolder, 'toc.yml'); - console.log('Writing ' + tocFilePath); - this._writeYamlFile(tocFile, tocFilePath, '', undefined); - } - - private _buildTocItems(docItems: DocItem[]): IYamlTocItem[] { - const tocItems: IYamlTocItem[] = []; - for (const docItem of docItems) { - let tocItem: IYamlTocItem; - - if (docItem.kind === DocItemKind.Namespace) { - // Namespaces don't have nodes yet - tocItem = { - name: docItem.name - }; - } else { - if (this._shouldEmbed(docItem.kind)) { - // Don't generate table of contents items for embedded definitions - continue; - } - - if (docItem.kind === DocItemKind.Package) { - tocItem = { - name: PackageName.getUnscopedName(docItem.name), - uid: this._getUid(docItem) - }; - } else { - tocItem = { - name: docItem.name, - uid: this._getUid(docItem) - }; - } - } - - tocItems.push(tocItem); - - const childItems: IYamlTocItem[] = this._buildTocItems(docItem.children); - if (childItems.length > 0) { - tocItem.items = childItems; - } - } - return tocItems; - } - - private _shouldEmbed(docItemKind: DocItemKind): boolean { - switch (docItemKind) { - case DocItemKind.Class: - case DocItemKind.Package: - case DocItemKind.Interface: - case DocItemKind.Enum: - return false; - } - return true; - } - - private _generateYamlItem(docItem: DocItem): IYamlItem | undefined { - const yamlItem: Partial = { }; - yamlItem.uid = this._getUid(docItem); - - const summary: string = this._renderMarkdown(docItem.apiItem.summary, docItem); - if (summary) { - yamlItem.summary = summary; - } - - const remarks: string = this._renderMarkdown(docItem.apiItem.remarks, docItem); - if (remarks) { - yamlItem.remarks = remarks; - } - - if (docItem.apiItem.deprecatedMessage) { - if (docItem.apiItem.deprecatedMessage.length > 0) { - const deprecatedMessage: string = this._renderMarkdown(docItem.apiItem.deprecatedMessage, docItem); - yamlItem.deprecated = { content: deprecatedMessage }; - } - } - - if (docItem.apiItem.isBeta) { - yamlItem.isPreview = true; - } - - yamlItem.name = this._getYamlItemName(docItem); - - yamlItem.fullName = yamlItem.name; - yamlItem.langs = [ 'typeScript' ]; - - switch (docItem.kind) { - case DocItemKind.Package: - yamlItem.type = 'package'; - break; - case DocItemKind.Enum: - yamlItem.type = 'enum'; - break; - case DocItemKind.EnumMember: - yamlItem.type = 'field'; - const enumMember: IApiEnumMember = docItem.apiItem as IApiEnumMember; - if (enumMember.value) { - // NOTE: In TypeScript, enum members can be strings or integers. - // If it is an integer, then enumMember.value will be a string representation of the integer. - // If it is a string, then enumMember.value will include the quotation marks. - // Enum values can also be calculated numbers, however this is not implemented yet. - yamlItem.numericValue = enumMember.value as any; // tslint:disable-line:no-any - } - break; - case DocItemKind.Class: - yamlItem.type = 'class'; - this._populateYamlClassOrInterface(yamlItem, docItem); - break; - case DocItemKind.Interface: - yamlItem.type = 'interface'; - this._populateYamlClassOrInterface(yamlItem, docItem); - break; - case DocItemKind.Method: - yamlItem.type = 'method'; - this._populateYamlMethod(yamlItem, docItem); - break; - case DocItemKind.Constructor: - yamlItem.type = 'constructor'; - this._populateYamlMethod(yamlItem, docItem); - break; - case DocItemKind.Property: - if ((docItem.apiItem as IApiProperty).isEventProperty) { - yamlItem.type = 'event'; - } else { - yamlItem.type = 'property'; - } - this._populateYamlProperty(yamlItem, docItem); - break; - case DocItemKind.Function: - yamlItem.type = 'function'; - this._populateYamlMethod(yamlItem, docItem); - break; - default: - throw new Error('Unimplemented item kind: ' + DocItemKind[docItem.kind as DocItemKind]); - } - - if (docItem.kind !== DocItemKind.Package && !this._shouldEmbed(docItem.kind)) { - yamlItem.package = this._getUid(docItem.getHierarchy()[0]); - } - - return yamlItem as IYamlItem; - } - - private _populateYamlClassOrInterface(yamlItem: Partial, docItem: DocItem): void { - const apiStructure: IApiClass | IApiInterface = docItem.apiItem as IApiClass | IApiInterface; - - if (apiStructure.extends) { - yamlItem.extends = [ this._linkToUidIfPossible(apiStructure.extends) ]; - } - - if (apiStructure.implements) { - yamlItem.implements = [ this._linkToUidIfPossible(apiStructure.implements) ]; - } - - if (apiStructure.isSealed) { - let sealedMessage: string; - if (docItem.kind === DocItemKind.Class) { - sealedMessage = 'This class is marked as `@sealed`. Subclasses should not extend it.'; - } else { - sealedMessage = 'This interface is marked as `@sealed`. Other interfaces should not extend it.'; - } - if (!yamlItem.remarks) { - yamlItem.remarks = sealedMessage; - } else { - yamlItem.remarks = sealedMessage + '\n\n' + yamlItem.remarks; - } - } - } - - private _populateYamlMethod(yamlItem: Partial, docItem: DocItem): void { - const apiMethod: IApiMethod | IApiConstructor = docItem.apiItem as IApiMethod; - yamlItem.name = Utilities.getConciseSignature(docItem.name, apiMethod); - - const syntax: IYamlSyntax = { - content: this._formatCommentedAnnotations(apiMethod.signature, apiMethod) - }; - yamlItem.syntax = syntax; - - if (apiMethod.returnValue) { - const returnDescription: string = this._renderMarkdown(apiMethod.returnValue.description, docItem) - .replace(/^\s*-\s+/, ''); // temporary workaround for people who mistakenly add a hyphen, e.g. "@returns - blah" - - syntax.return = { - type: [ this._linkToUidIfPossible(apiMethod.returnValue.type) ], - description: returnDescription - }; - } - - const parameters: IYamlParameter[] = []; - for (const parameterName of Object.keys(apiMethod.parameters)) { - const apiParameter: IApiParameter = apiMethod.parameters[parameterName]; - parameters.push( - { - id: parameterName, - description: this._renderMarkdown(apiParameter.description, docItem), - type: [ this._linkToUidIfPossible(apiParameter.type || '') ] - } as IYamlParameter - ); - } - - if (parameters.length) { - syntax.parameters = parameters; - } - - } - - private _populateYamlProperty(yamlItem: Partial, docItem: DocItem): void { - const apiProperty: IApiProperty = docItem.apiItem as IApiProperty; - - const syntax: IYamlSyntax = { - content: this._formatCommentedAnnotations(apiProperty.signature, apiProperty) - }; - - yamlItem.syntax = syntax; - - if (apiProperty.type) { - syntax.return = { - type: [ this._linkToUidIfPossible(apiProperty.type) ] - }; - } - } - - private _renderMarkdown(markupElements: MarkupElement[], containingDocItem: DocItem): string { - if (!markupElements.length) { - return ''; - } - - return MarkdownRenderer.renderElements(markupElements, { - onRenderApiLink: (args: IMarkdownRenderApiLinkArgs) => { - const result: IDocItemSetResolveResult = this._docItemSet.resolveApiItemReference(args.reference); - if (!result.docItem) { - // Eventually we should introduce a warnings file - console.error(colors.yellow('Warning: Unresolved hyperlink to ' - + Markup.formatApiItemReference(args.reference))); - } else { - args.prefix = '['; - args.suffix = `](xref:${this._getUid(result.docItem)})`; - } - } - }).trim(); - } - - private _writeYamlFile(dataObject: {}, filePath: string, yamlMimeType: string, - schema: JsonSchema|undefined): void { - - JsonFile.validateNoUndefinedMembers(dataObject); - - let stringified: string = yaml.safeDump(dataObject, { - lineWidth: 120 - }); - - if (yamlMimeType) { - stringified = `### YamlMime:${yamlMimeType}\n` + stringified; - } - - FileSystem.writeFile(filePath, stringified, { - convertLineEndings: NewlineKind.CrLf, - ensureFolderExists: true - }); - - if (schema) { - schema.validateObject(dataObject, filePath); - } - } - - // Prepends a string such as "/** @sealed @override */" to an item signature where appropriate. - private _formatCommentedAnnotations(signature: string, apiItem: IApiMethod | IApiProperty): string { - const annotations: string[] = []; - if (apiItem.isSealed) { - annotations.push('@sealed'); - } - if (apiItem.isVirtual) { - annotations.push('@virtual'); - } - if (apiItem.isOverride) { - annotations.push('@override'); - } - if (annotations.length === 0) { - return signature; - } - return '/** ' + annotations.join(' ') + ' */\n' + signature; - } - - /** - * Calculate the docfx "uid" for the DocItem - * Example: node-core-library.JsonFile.load - */ - private _getUid(docItem: DocItem): string { - let result: string = ''; - for (const current of docItem.getHierarchy()) { - switch (current.kind) { - case DocItemKind.Package: - result += PackageName.getUnscopedName(current.name); - break; - default: - result += '.'; - result += current.name; - break; - } - } - return result; - } - - /** - * Initialize the _docItemsByTypeName() data structure. - */ - private _initDocItemsByTypeName(): void { - // Collect the _docItemsByTypeName table - const ambiguousNames: Set = new Set(); - - this._docItemSet.forEach((docItem: DocItem) => { - switch (docItem.kind) { - case DocItemKind.Class: - case DocItemKind.Enum: - case DocItemKind.Interface: - // Attempt to register both the fully qualified name and the short name - const namesForType: string[] = [docItem.name]; - - // Note that nameWithDot cannot conflict with docItem.name (because docItem.name - // cannot contain a dot) - const nameWithDot: string | undefined = this._getTypeNameWithDot(docItem); - if (nameWithDot) { - namesForType.push(nameWithDot); - } - - // Register all names - for (const typeName of namesForType) { - if (ambiguousNames.has(typeName)) { - break; - } - - if (this._docItemsByTypeName.has(typeName)) { - // We saw this name before, so it's an ambiguous match - ambiguousNames.add(typeName); - break; - } - - this._docItemsByTypeName.set(typeName, docItem); - } - - break; - } - }); - - // Remove the ambiguous matches - for (const ambiguousName of ambiguousNames) { - this._docItemsByTypeName.delete(ambiguousName); - } - } - - /** - * This is a temporary workaround to enable limited autolinking of API item types - * until the YAML file format is enhanced to support general hyperlinks. - * @remarks - * In the current version, fields such as IApiProperty.type allow either: - * (1) a UID identifier such as "node-core-library.JsonFile" which will be rendered - * as a hyperlink to that type name, or (2) a block of freeform text that must not - * contain any Markdown links. The _substituteUidForSimpleType() function assumes - * it is given #2 but substitutes #1 if the name can be matched to a DocItem. - */ - private _linkToUidIfPossible(typeName: string): string { - // Note that typeName might be a _getTypeNameWithDot() name or it might be a simple class name - const docItem: DocItem | undefined = this._docItemsByTypeName.get(typeName.trim()); - if (docItem) { - // Substitute the UID - return this._getUid(docItem); - } - return typeName; - } - - /** - * If the docItem represents a scoped name such as "my-library:MyNamespace.MyClass", - * this returns a string such as "MyNamespace.MyClass". If the result would not - * have at least one dot in it, then undefined is returned. - */ - private _getTypeNameWithDot(docItem: DocItem): string | undefined { - const hierarchy: DocItem[] = docItem.getHierarchy(); - if (hierarchy.length > 0 && hierarchy[0].kind === DocItemKind.Package) { - hierarchy.shift(); // ignore the package qualifier - } - if (hierarchy.length < 2) { - return undefined; - } - return hierarchy.map(x => x.name).join('.'); - } - - private _getYamlItemName(docItem: DocItem): string { - if (docItem.parent && docItem.parent.kind === DocItemKind.Namespace) { - // For members a namespace, show the full name excluding the package part: - // Example: excel.Excel.Binding --> Excel.Binding - return this._getUid(docItem).replace(/^[^.]+\./, ''); - } - return docItem.name; - } - - private _getYamlFilePath(docItem: DocItem): string { - let result: string = ''; - - for (const current of docItem.getHierarchy()) { - switch (current.kind) { - case DocItemKind.Package: - result += PackageName.getUnscopedName(current.name); - break; - default: - if (current.parent && current.parent.kind === DocItemKind.Package) { - result += '/'; - } else { - result += '.'; - } - result += current.name; - break; - } - } - return path.join(this._outputFolder, result.toLowerCase() + '.yml'); - } - - private _deleteOldOutputFiles(): void { - console.log('Deleting old output from ' + this._outputFolder); - FileSystem.ensureEmptyFolder(this._outputFolder); - } -} diff --git a/apps/api-extractor/Debug.cmd b/apps/api-extractor/Debug.cmd deleted file mode 100644 index f23fdb7cc41..00000000000 --- a/apps/api-extractor/Debug.cmd +++ /dev/null @@ -1,3 +0,0 @@ -@ECHO OFF -@SETLOCAL -node-debug "%~dp0lib\DebugRun.js" %* diff --git a/apps/api-extractor/Run.cmd b/apps/api-extractor/Run.cmd deleted file mode 100644 index 5d4c625e36c..00000000000 --- a/apps/api-extractor/Run.cmd +++ /dev/null @@ -1,3 +0,0 @@ -@ECHO OFF -@SETLOCAL -node "%~dp0\lib\DebugRun.js" %* diff --git a/apps/api-extractor/package.json b/apps/api-extractor/package.json index b4075e875ca..fbc1b1359b8 100644 --- a/apps/api-extractor/package.json +++ b/apps/api-extractor/package.json @@ -33,7 +33,7 @@ "dependencies": { "@microsoft/node-core-library": "3.7.0", "@microsoft/ts-command-line": "4.2.2", - "@microsoft/tsdoc": "0.12.2", + "@microsoft/tsdoc": "0.12.4", "@types/node": "8.5.8", "@types/z-schema": "3.16.31", "colors": "~1.2.1", diff --git a/apps/api-extractor/src/ApiDefinitionReference.ts b/apps/api-extractor/src/ApiDefinitionReference.ts deleted file mode 100644 index c803fcd2b16..00000000000 --- a/apps/api-extractor/src/ApiDefinitionReference.ts +++ /dev/null @@ -1,122 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -// See LICENSE in the project root for license information. - -import { PackageName } from '@microsoft/node-core-library'; -import { IApiItemReference } from './api/ApiItem'; - -/** - * An API definition reference that is used to locate the documentation of exported - * API items that may or may not belong to an external package. - * - * The format of the API definition reference is: - * scopeName/packageName:exportName.memberName - * - * The following are valid API definition references: - * \@microsoft/sp-core-library:DisplayMode - * \@microsoft/sp-core-library:Guid - * \@microsoft/sp-core-library:Guid.equals - * es6-collections:Map - */ -export interface IApiDefinitionReferenceParts { - /** - * This is an optional property to denote that a package name is scoped under this name. - * For example, a common case is when having the '@microsoft' scope name in the - * API definition reference: '\@microsoft/sp-core-library'. - */ - scopeName: string; - /** - * The name of the package that the exportName belongs to. - */ - packageName: string; - /** - * The name of the export API item. - */ - exportName: string; - /** - * The name of the member API item. - */ - memberName: string; -} - -/** - * {@inheritdoc IApiDefinitionReferenceParts} - */ -export class ApiDefinitionReference { - /** - * {@inheritdoc IApiDefinitionReferenceParts.scopeName} - */ - public scopeName: string; - /** - * {@inheritdoc IApiDefinitionReferenceParts.packageName} - */ - public packageName: string; - /** - * {@inheritdoc IApiDefinitionReferenceParts.exportName} - */ - public exportName: string; - /** - * {@inheritdoc IApiDefinitionReferenceParts.memberName} - */ - public memberName: string; - - /** - * Creates an ApiDefinitionReference instance given strings that symbolize the public - * properties of ApiDefinitionReference. - */ - public static createFromParts(parts: IApiDefinitionReferenceParts): ApiDefinitionReference { - return new ApiDefinitionReference(parts); - } - - /** - * Stringifies the ApiDefinitionReferenceOptions up and including the - * scope and package name. - * - * Example output: '@microsoft/Utilities' - */ - public toScopePackageString(): string { - if (!this.packageName) { - return ''; - } - return PackageName.combineParts(this.scopeName, this.packageName); - } - - /** - * Stringifies the ApiDefinitionReferenceOptions up and including the - * scope, package and export name. - * - * Example output: '@microsoft/Utilities.Parse' - */ - public toExportString(): string { - let result: string = this.toScopePackageString(); - if (result) { - result += '#'; - } - return result + `${this.exportName}`; - } - - /** - * Stringifies the ApiDefinitionReferenceOptions up and including the - * scope, package, export and member name. - * - * Example output: '@microsoft/Utilities#Parse.parseJsonToString' - */ - public toMemberString(): string { - return this.toExportString() + `.${this.memberName}`; - } - - public toApiItemReference(): IApiItemReference { - return { - scopeName: this.scopeName, - packageName: this.packageName, - exportName: this.exportName, - memberName: this.memberName - }; - } - - private constructor(parts: IApiDefinitionReferenceParts) { - this.scopeName = parts.scopeName; - this.packageName = parts.packageName; - this.exportName = parts.exportName; - this.memberName = parts.memberName; - } -} \ No newline at end of file diff --git a/apps/api-extractor/src/DebugRun.ts b/apps/api-extractor/src/DebugRun.ts deleted file mode 100644 index 9e997751d49..00000000000 --- a/apps/api-extractor/src/DebugRun.ts +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -// See LICENSE in the project root for license information. - -// NOTE: THIS SOURCE FILE IS FOR DEBUGGING PURPOSES ONLY. -// IT IS INVOKED BY THE 'Run.cmd' AND 'Debug.cmd' BATCH FILES. - -import { Extractor } from './extractor/Extractor'; -import * as path from 'path'; - -const extractor: Extractor = new Extractor( - { - compiler: { - configType: 'tsconfig', - overrideTsconfig: { - 'compilerOptions': { - 'target': 'es6', - 'forceConsistentCasingInFileNames': true, - 'module': 'commonjs', - 'declaration': true, - 'sourceMap': true, - 'experimentalDecorators': true, - 'types': [ - 'node' - ], - 'lib': [ - 'es5', - 'scripthost', - 'es2015.collection', - 'es2015.promise', - 'es2015.iterable', - 'dom' - ], - 'strictNullChecks': true - }, - 'include': [ 'lib/**/*.d.ts' ] - }, - rootFolder: '../../libraries/node-core-library' - }, - project: { - entryPointSourceFile: 'lib/index.d.ts', - externalJsonFileFolders: [ ] - }, - apiReviewFile: { - enabled: false, - apiReviewFolder: path.join(__dirname, 'debug') - }, - apiJsonFile: { - enabled: true, - outputFolder: path.join(__dirname, 'debug') - }, - dtsRollup: { - enabled: true, - publishFolderForInternal: path.join(__dirname, 'debug/internal'), - publishFolderForBeta: path.join(__dirname, 'debug/beta'), - publishFolderForPublic: path.join(__dirname, 'debug/public') - } - } -); - -console.log('CONFIG:' + JSON.stringify(extractor.actualConfig, undefined, ' ')); - -if (!extractor.processProject()) { - console.log('processProject() failed the build'); -} else { - console.log('processProject() succeeded'); -} - -console.log('DebugRun completed.'); diff --git a/apps/api-extractor/src/DocItemLoader.ts b/apps/api-extractor/src/DocItemLoader.ts deleted file mode 100644 index 944418cba47..00000000000 --- a/apps/api-extractor/src/DocItemLoader.ts +++ /dev/null @@ -1,225 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -// See LICENSE in the project root for license information. - -import * as os from 'os'; -import * as path from 'path'; -import { - JsonFile, - FileSystem, - FileConstants -} from '@microsoft/node-core-library'; - -import { - ApiItem, - IApiPackage, - ApiMember -} from './api/ApiItem'; - -import { ApiDefinitionReference } from './ApiDefinitionReference'; -import { AstItem } from './ast/AstItem'; -import { AstItemContainer } from './ast/AstItemContainer'; -import { AstPackage } from './ast/AstPackage'; -import { ResolvedApiItem } from './ResolvedApiItem'; -import { ApiJsonFile } from './api/ApiJsonFile'; -import { IReferenceResolver } from './aedoc/ApiDocumentation'; - -/** - * A loader for locating the ApiItem associated with a given project and API item, or - * for locating an AstItem locally. - * No processing on the ApiItem orAstItem should be done in this class, this class is only - * concerned with communicating state. - * The ApiItem can then be used to enforce correct API usage, like enforcing internal. - * To use DocItemLoader: provide a projectFolder to construct a instance of the DocItemLoader, - * then use DocItemLoader.getItem to retrieve the ApiItem of a particular API item. - */ -export class DocItemLoader implements IReferenceResolver { - private _cache: Map; - private _projectFolder: string; // Root directory to check for node modules - - /** - * The projectFolder is the top-level folder containing package.json for a project - * that we are compiling. - */ - constructor(projectFolder: string) { - if (!FileSystem.exists(path.join(projectFolder, FileConstants.PackageJson))) { - throw new Error(`An NPM project was not found in the specified folder: ${projectFolder}`); - } - - this._projectFolder = projectFolder; - this._cache = new Map(); - } - - /** - * {@inheritdoc IReferenceResolver.resolve} - */ - public resolve(apiDefinitionRef: ApiDefinitionReference, - astPackage: AstPackage, - warnings: string[]): ResolvedApiItem | undefined { - - // We determine if an 'apiDefinitionRef' is local if it has no package name or if the scoped - // package name is equal to the current package's scoped package name. - if (!apiDefinitionRef.packageName || apiDefinitionRef.toScopePackageString() === astPackage.name) { - // Resolution for local references - return this.resolveLocalReferences(apiDefinitionRef, astPackage, warnings); - - } else { - - // If there was no resolved astItem then try loading from JSON - return this.resolveJsonReferences(apiDefinitionRef, warnings); - } - } - - /** - * Resolution of API definition references in the scenario that the reference given indicates - * that we should search within the current AstPackage to resolve. - * No processing on the AstItem should be done here, this class is only concerned - * with communicating state. - */ - public resolveLocalReferences(apiDefinitionRef: ApiDefinitionReference, - astPackage: AstPackage, - warnings: string[]): ResolvedApiItem | undefined { - - let astItem: AstItem | undefined = astPackage.getMemberItem(apiDefinitionRef.exportName); - // Check if export name was not found - if (!astItem) { - warnings.push(`Unable to find referenced export \"${apiDefinitionRef.toExportString()}\"`); - return undefined; - } - - // If memberName exists then check for the existence of the name - if (apiDefinitionRef.memberName) { - if (astItem instanceof AstItemContainer) { - const astItemContainer: AstItemContainer = (astItem as AstItemContainer); - // get() returns undefined if there is no match - astItem = astItemContainer.getMemberItem(apiDefinitionRef.memberName); - } else { - // There are no other instances of astItem that has members, - // thus there must be a mistake with the apiDefinitionRef. - astItem = undefined; - } - } - - if (!astItem) { - // If we are here, we can be sure there was a problem with the memberName. - // memberName was not found, apiDefinitionRef is invalid - warnings.push(`Unable to find referenced member \"${apiDefinitionRef.toMemberString()}\"`); - return undefined; - } - - return ResolvedApiItem.createFromAstItem(astItem); - } - - /** - * Resolution of API definition references in the scenario that the reference given indicates - * that we should search outside of this AstPackage and instead search within the JSON API file - * that is associated with the apiDefinitionRef. - */ - public resolveJsonReferences(apiDefinitionRef: ApiDefinitionReference, - warnings: string[]): ResolvedApiItem | undefined { - - // Check if package can be not found - const docPackage: IApiPackage | undefined = this.getPackage(apiDefinitionRef); - if (!docPackage) { - // package not found in node_modules - warnings.push(`Unable to find a documentation file (\"${apiDefinitionRef.packageName}.api.json\")` + - ' for the referenced package'); - return undefined; - } - - // found JSON package, now ensure export name is there - // hasOwnProperty() not needed for JJU objects - if (!(apiDefinitionRef.exportName in docPackage.exports)) { - warnings.push(`Unable to find referenced export \"${apiDefinitionRef.toExportString()}\""`); - return undefined; - } - - let docItem: ApiItem = docPackage.exports[apiDefinitionRef.exportName]; - - // If memberName exists then check for the existence of the name - if (apiDefinitionRef.memberName) { - let member: ApiMember | undefined = undefined; - switch (docItem.kind) { - case 'class': - - // hasOwnProperty() not needed for JJU objects - member = apiDefinitionRef.memberName in docItem.members ? - docItem.members[apiDefinitionRef.memberName] : undefined; - break; - case 'interface': - // hasOwnProperty() not needed for JJU objects - member = apiDefinitionRef.memberName in docItem.members ? - docItem.members[apiDefinitionRef.memberName] : undefined; - break; - case 'enum': - // hasOwnProperty() not needed for JJU objects - member = apiDefinitionRef.memberName in docItem.values ? - docItem.values[apiDefinitionRef.memberName] : undefined; - break; - default: - // Any other docItem.kind does not have a 'members' property - break; - } - - if (member) { - docItem = member; - } else { - // member name was not found, apiDefinitionRef is invalid - warnings.push(`Unable to find referenced member \"${apiDefinitionRef.toMemberString()}\"`); - return undefined; - } - } - - return ResolvedApiItem.createFromJson(docItem); - } - - /** - * Attempts to locate and load the IApiPackage object from the project folder's - * node modules. If the package already exists in the cache, nothing is done. - * - * @param apiDefinitionRef - interface with properties pertaining to the API definition reference - */ - public getPackage(apiDefinitionRef: ApiDefinitionReference): IApiPackage | undefined { - let cachePackageName: string = ''; - - // We concatenate the scopeName and packageName in case there are packageName conflicts - if (apiDefinitionRef.scopeName) { - cachePackageName = `${apiDefinitionRef.scopeName}/${apiDefinitionRef.packageName}`; - } else { - cachePackageName = apiDefinitionRef.packageName; - } - // Check if package exists in cache - if (this._cache.has(cachePackageName)) { - return this._cache.get(cachePackageName); - } - - // Doesn't exist in cache, attempt to load the json file - const apiJsonFilePath: string = path.join( - this._projectFolder, - 'node_modules', - apiDefinitionRef.scopeName, - apiDefinitionRef.packageName, - `dist/${apiDefinitionRef.packageName}.api.json` - ); - - if (!FileSystem.exists(path.join(apiJsonFilePath))) { - // Error should be handled by the caller - return undefined; - } - - return this.loadPackageIntoCache(apiJsonFilePath, cachePackageName); - } - - /** - * Loads the API documentation json file and validates that it conforms to our schema. If it does, - * then the json file is saved in the cache and returned. - */ - public loadPackageIntoCache(apiJsonFilePath: string, cachePackageName: string): IApiPackage { - const astPackage: IApiPackage = JsonFile.loadAndValidate(apiJsonFilePath, ApiJsonFile.jsonSchema, { - customErrorHeader: 'The API JSON file does not conform to the expected schema, and may' + os.EOL - + 'have been created by an incompatible release of API Extractor:' - }); - - this._cache.set(cachePackageName, astPackage); - return astPackage; - } -} diff --git a/apps/api-extractor/src/ExternalApiHelper.ts b/apps/api-extractor/src/ExternalApiHelper.ts deleted file mode 100644 index 1c4d2c604b2..00000000000 --- a/apps/api-extractor/src/ExternalApiHelper.ts +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -// See LICENSE in the project root for license information. - -import * as path from 'path'; -import { FileSystem } from '@microsoft/node-core-library'; - -import { Extractor } from './extractor/Extractor'; - -/** - * ExternalApiHelper has the specific use case of generating an API json file from third-party definition files. - * This class is invoked by the gulp-core-build-typescript gulpfile, where the external package names are - * hard wired. - * The job of this method is almost the same as the API Extractor task that is executed on first party packages, - * with the exception that all packages analyzed here are external packages with definition files. - * - * @beta - */ -export class ExternalApiHelper { - - /** - * @param rootDir - the absolute path containing a 'package.json' file and is also a parent of the - * external package file. Ex: build.absolute_build_path. - * @param libFolder - the path to the lib folder relative to the rootDir, this is where - * 'external-api-json/external_package.api.json' file will be written. Ex: 'lib'. - * @param externalPackageFilePath - the path to the '*.d.ts' file of the external package relative to the rootDir. - * Ex: 'resources/external-api-json/es6-collection/index.t.ds' - */ - public static generateApiJson(rootDir: string, libFolder: string, externalPackageFilePath: string): void { - const entryPointFile: string = path.resolve(rootDir, externalPackageFilePath); - const entryPointFolder: string = path.dirname(entryPointFile); - - const overrideTsconfig: { } = { - target: 'es5', - module: 'commonjs', - moduleResolution: 'node', - experimentalDecorators: true, - jsx: 'react', - rootDir: entryPointFolder - }; - - const outputPath: string = path.resolve(rootDir, libFolder, 'external-api-json'); - FileSystem.ensureFolder(outputPath); - - const extractor: Extractor = new Extractor({ - compiler: { - configType: 'tsconfig', - rootFolder: entryPointFolder, - overrideTsconfig: overrideTsconfig - }, - project: { - entryPointSourceFile: entryPointFile - }, - apiReviewFile: { - enabled: false - }, - apiJsonFile: { - enabled: true, - outputFolder: outputPath - } - }, { - customLogger: { - logVerbose: (message: string) => { /* don't log */ } - } - }); - - if (!extractor.processProject()) { - throw new Error('API Extractor failed to process the input: ' + externalPackageFilePath); - } - } -} diff --git a/apps/api-extractor/src/ExtractorContext.ts b/apps/api-extractor/src/ExtractorContext.ts deleted file mode 100644 index e6f6ae07a17..00000000000 --- a/apps/api-extractor/src/ExtractorContext.ts +++ /dev/null @@ -1,182 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -// See LICENSE in the project root for license information. - -import * as ts from 'typescript'; -import * as path from 'path'; -import { - PackageJsonLookup, - IPackageJson, - PackageName, - IParsedPackageName, - FileSystem -} from '@microsoft/node-core-library'; - -import { AstPackage } from './ast/AstPackage'; -import { DocItemLoader } from './DocItemLoader'; -import { ILogger } from './extractor/ILogger'; -import { IExtractorPoliciesConfig, IExtractorValidationRulesConfig } from './extractor/IExtractorConfig'; -import { TypeScriptMessageFormatter } from './utils/TypeScriptMessageFormatter'; - -/** - * Options for ExtractorContext constructor. - */ -export interface IExtractorContextOptions { - /** - * Configuration for the TypeScript compiler. The most important options to set are: - * - * - target: ts.ScriptTarget.ES5 - * - module: ts.ModuleKind.CommonJS - * - moduleResolution: ts.ModuleResolutionKind.NodeJs - * - rootDir: inputFolder - */ - program: ts.Program; - - /** - * The entry point for the project. This should correspond to the "main" field - * from NPM's package.json file. If it is a relative path, it will be relative to - * the project folder described by IExtractorAnalyzeOptions.compilerOptions. - */ - entryPointFile: string; - - logger: ILogger; - - policies: IExtractorPoliciesConfig; - - validationRules: IExtractorValidationRulesConfig; -} - -/** - * The main entry point for the "api-extractor" utility. The Analyzer object invokes the - * TypeScript Compiler API to analyze a project, and constructs the AstItem - * abstract syntax tree. - */ -export class ExtractorContext { - public readonly program: ts.Program; - public readonly typeChecker: ts.TypeChecker; - public package: AstPackage; - - /** - * The parsed package.json file for this package. - */ - public readonly packageJson: IPackageJson; - - public readonly parsedPackageName: IParsedPackageName; - - /** - * One DocItemLoader is needed per analyzer to look up external API members - * as needed. - */ - public readonly docItemLoader: DocItemLoader; - - public readonly packageJsonLookup: PackageJsonLookup; - - public readonly policies: IExtractorPoliciesConfig; - - public readonly validationRules: IExtractorValidationRulesConfig; - - public readonly logger: ILogger; - - // If the entry point is "C:\Folder\project\src\index.ts" and the nearest package.json - // is "C:\Folder\project\package.json", then the packageFolder is "C:\Folder\project" - private _packageFolder: string; - - constructor(options: IExtractorContextOptions) { - this.packageJsonLookup = new PackageJsonLookup(); - - this.policies = options.policies; - this.validationRules = options.validationRules; - - const folder: string | undefined = this.packageJsonLookup.tryGetPackageFolderFor(options.entryPointFile); - if (!folder) { - throw new Error('Unable to find a package.json for entry point: ' + options.entryPointFile); - } - this._packageFolder = folder; - - this.packageJson = this.packageJsonLookup.tryLoadPackageJsonFor(this._packageFolder)!; - - this.parsedPackageName = PackageName.parse(this.packageJson.name); - - this.docItemLoader = new DocItemLoader(this._packageFolder); - - this.logger = options.logger; - - // This runs a full type analysis, and then augments the Abstract Syntax Tree (i.e. declarations) - // with semantic information (i.e. symbols). The "diagnostics" are a subset of the everyday - // compile errors that would result from a full compilation. - for (const diagnostic of options.program.getSemanticDiagnostics()) { - const errorText: string = TypeScriptMessageFormatter.format(diagnostic.messageText); - this.reportError(`TypeScript: ${errorText}`, diagnostic.file, diagnostic.start); - } - - this.program = options.program; - this.typeChecker = options.program.getTypeChecker(); - - const rootFile: ts.SourceFile | undefined = options.program.getSourceFile(options.entryPointFile); - if (!rootFile) { - throw new Error('Unable to load file: ' + options.entryPointFile); - } - - this.package = new AstPackage(this, rootFile); // construct members - this.package.completeInitialization(); // creates ApiDocumentation - this.package.visitTypeReferencesForAstItem(); - } - - /** - * Returns the full name of the package being analyzed. - */ - public get packageName(): string { - return this.packageJson.name; - } - - /** - * Returns the folder for the package being analyzed. - */ - public get packageFolder(): string { - return this._packageFolder; - } - - /** - * Reports an error message to the registered ApiErrorHandler. - */ - public reportError(message: string, sourceFile: ts.SourceFile | undefined, start: number | undefined): void { - if (sourceFile && start) { - const lineAndCharacter: ts.LineAndCharacter = sourceFile.getLineAndCharacterOfPosition(start); - - // If the file is under the packageFolder, then show a relative path - const relativePath: string = path.relative(this.packageFolder, sourceFile.fileName); - const shownPath: string = relativePath.substr(0, 2) === '..' ? sourceFile.fileName : relativePath; - - // Format the error so that VS Code can follow it. For example: - // "src\MyClass.ts(15,1): The JSDoc tag "@blah" is not supported by AEDoc" - this.logger.logError(`${shownPath}(${lineAndCharacter.line + 1},${lineAndCharacter.character + 1}): ` - + message); - } else { - this.logger.logError(message); - } - } - - /** - * Scans for external package api files and loads them into the docItemLoader member before - * any API analysis begins. - * - * @param externalJsonCollectionPath - an absolute path to to the folder that contains all the external - * api json files. - * Ex: if externalJsonPath is './resources', then in that folder - * are 'es6-collections.api.json', etc. - */ - public loadExternalPackages(externalJsonCollectionPath: string): void { - if (!externalJsonCollectionPath) { - return; - } - - FileSystem.readFolder(externalJsonCollectionPath, { - absolutePaths: true - }).forEach(file => { - if (path.extname(file) === '.json') { - // Example: "C:\Example\my-package.json" --> "my-package" - const packageName: string = path.parse(file).name; - this.docItemLoader.loadPackageIntoCache(file, packageName); - } - }); - } -} diff --git a/apps/api-extractor/src/ResolvedApiItem.ts b/apps/api-extractor/src/ResolvedApiItem.ts deleted file mode 100644 index 01a8f1dd8ce..00000000000 --- a/apps/api-extractor/src/ResolvedApiItem.ts +++ /dev/null @@ -1,100 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -// See LICENSE in the project root for license information. - -import { AstItem, AstItemKind } from './ast/AstItem'; -import { ReleaseTag } from './aedoc/ReleaseTag'; -import { MarkupElement, MarkupBasicElement } from './markup/MarkupElement'; -import { ApiItem } from './api/ApiItem'; -import { ApiJsonConverter } from './api/ApiJsonConverter'; -import { IAedocParameter } from './aedoc/ApiDocumentation'; - -/** - * A class to abstract away the difference between an item from our public API that could be - * represented by either an AstItem or an ApiItem that is retrieved from a JSON file. - */ -export class ResolvedApiItem { - public kind: AstItemKind; - public summary: MarkupElement[]; - public remarks: MarkupElement[]; - public deprecatedMessage: MarkupBasicElement[] | undefined; - public releaseTag: ReleaseTag; - public isBeta: boolean; - public params: { [name: string]: IAedocParameter } | undefined; - public returnsMessage: MarkupBasicElement[] | undefined; - /** - * This property will either be an AstItem or undefined. - */ - public astItem: AstItem | undefined; - - /** - * A function to abstract the construction of a ResolvedApiItem instance - * from an AstItem. - */ - public static createFromAstItem(astItem: AstItem): ResolvedApiItem { - return new ResolvedApiItem( - astItem.kind, - astItem.documentation.summary, - astItem.documentation.remarks, - astItem.documentation.deprecatedMessage, - astItem.documentation.releaseTag === ReleaseTag.Beta, - astItem.documentation.parameters, - astItem.documentation.returnsMessage, - astItem.documentation.releaseTag, - astItem - ); - } - - /** - * A function to abstract the construction of a ResolvedApiItem instance - * from a JSON object that symbolizes an ApiItem. - */ - public static createFromJson(docItem: ApiItem): ResolvedApiItem { - let parameters: { [name: string]: IAedocParameter } | undefined = undefined; - let returnsMessage: MarkupBasicElement[] | undefined = undefined; - switch (docItem.kind) { - case 'function': - parameters = docItem.parameters; - returnsMessage = docItem.returnValue.description; - break; - case 'method': - parameters = docItem.parameters; - returnsMessage = docItem.returnValue.description; - break; - default: - break; - } - - return new ResolvedApiItem( - ApiJsonConverter.convertJsonToKind(docItem.kind), - docItem.summary, - docItem.remarks, - docItem.deprecatedMessage, - docItem.isBeta, - parameters, - returnsMessage, - ReleaseTag.Public, - undefined - ); - } - - private constructor( - kind: AstItemKind, - summary: MarkupElement[], - remarks: MarkupElement[], - deprecatedMessage: MarkupBasicElement[] | undefined, - isBeta: boolean, - params: { [name: string]: IAedocParameter } | undefined, - returnsMessage: MarkupBasicElement[] | undefined, - releaseTag: ReleaseTag, - astItem: AstItem | undefined) { - this.kind = kind; - this.summary = summary; - this.remarks = remarks; - this.deprecatedMessage = deprecatedMessage; - this.isBeta = isBeta; - this.params = params; - this.returnsMessage = returnsMessage; - this.releaseTag = releaseTag; - this.astItem = astItem; - } -} diff --git a/apps/api-extractor/src/aedoc/AedocDefinitions.ts b/apps/api-extractor/src/aedoc/AedocDefinitions.ts index 4a337aea63f..a098d8a907b 100644 --- a/apps/api-extractor/src/aedoc/AedocDefinitions.ts +++ b/apps/api-extractor/src/aedoc/AedocDefinitions.ts @@ -24,8 +24,8 @@ export class AedocDefinitions { syntaxKind: TSDocTagSyntaxKind.ModifierTag }); - public static get parserConfiguration(): TSDocConfiguration { - if (!AedocDefinitions._parserConfiguration) { + public static get tsdocConfiguration(): TSDocConfiguration { + if (!AedocDefinitions._tsdocConfiguration) { const configuration: TSDocConfiguration = new TSDocConfiguration(); configuration.addTagDefinitions([ AedocDefinitions.betaDocumentation, @@ -58,10 +58,10 @@ export class AedocDefinitions { true ); - AedocDefinitions._parserConfiguration = configuration; + AedocDefinitions._tsdocConfiguration = configuration; } - return AedocDefinitions._parserConfiguration; + return AedocDefinitions._tsdocConfiguration; } - private static _parserConfiguration: TSDocConfiguration | undefined; + private static _tsdocConfiguration: TSDocConfiguration | undefined; } diff --git a/apps/api-extractor/src/aedoc/ApiDocumentation.ts b/apps/api-extractor/src/aedoc/ApiDocumentation.ts deleted file mode 100644 index 9ec8f7f7f50..00000000000 --- a/apps/api-extractor/src/aedoc/ApiDocumentation.ts +++ /dev/null @@ -1,679 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -// See LICENSE in the project root for license information. - -/* tslint:disable:no-bitwise */ -/* tslint:disable:member-ordering */ - -import { - TSDocParser, - TSDocTagDefinition, - TextRange, - ParserContext, - ModifierTagSet, - DocBlockTag, - StandardTags, - StandardModifierTagSet, - DocBlock, - DocComment, - DocSection, - DocNodeKind, - DocPlainText, - DocCodeSpan, - DocErrorText, - DocEscapedText, - DocNode, - DocParagraph, - DocFencedCode, - DocHtmlStartTag, - DocHtmlEndTag, - DocInlineTag, - DocLinkTag, - DocDeclarationReference, - DocMemberReference, - DocNodeTransforms -} from '@microsoft/tsdoc'; -import { AstPackage } from '../ast/AstPackage'; -import { ApiDefinitionReference, IApiDefinitionReferenceParts } from '../ApiDefinitionReference'; -import { ExtractorContext } from '../ExtractorContext'; -import { ResolvedApiItem } from '../ResolvedApiItem'; -import { ReleaseTag } from './ReleaseTag'; -import { - MarkupElement, - MarkupBasicElement, - IMarkupApiLink, - MarkupHighlighter -} from '../markup/MarkupElement'; -import { Markup } from '../markup/Markup'; -import { TypeScriptHelpers } from '../utils/TypeScriptHelpers'; -import { AedocDefinitions } from './AedocDefinitions'; -import { IApiItemReference } from '../api/ApiItem'; -import { PackageName, IParsedPackageNameOrError } from '@microsoft/node-core-library'; -import { AstItemKind } from '../ast/AstItem'; - -/** - * A dependency for ApiDocumentation constructor that abstracts away the function - * of resolving an API definition reference. - * - * @internalremarks reportError() will be called if the apiDefinitionRef is to a non local - * item and the package of that non local item can not be found. - * If there is no package given and an item can not be found we will return undefined. - * Once we support local references, we can be sure that reportError will only be - * called once if the item can not be found (and undefined will be returned by the reference - * function). - */ -export interface IReferenceResolver { - resolve( - apiDefinitionRef: ApiDefinitionReference, - astPackage: AstPackage, - warnings: string[]): ResolvedApiItem | undefined; -} - -/** - * Used by ApiDocumentation to represent the AEDoc description for a function parameter. - */ -export interface IAedocParameter { - name: string; - description: MarkupBasicElement[]; -} - -export class ApiDocumentation { - /** - * docCommentTokens that are parsed into Doc Elements. - */ - public summary: MarkupElement[]; - public deprecatedMessage: MarkupBasicElement[]; - public remarks: MarkupElement[]; - public returnsMessage: MarkupBasicElement[]; - public parameters: { [name: string]: IAedocParameter; }; - - /** - * A list of \@link elements to be post-processed after all basic documentation has been created - * for all items in the project. We save the processing for later because we need ReleaseTag - * information before we can determine whether a link element is valid. - * Example: If API item A has a \@link in its documentation to API item B, then B must not - * have ReleaseTag.Internal. - */ - public incompleteLinks: IMarkupApiLink[]; - - /** - * A "release tag" is an AEDoc tag which indicates whether this definition - * is considered Public API for third party developers, as well as its release - * stage (alpha, beta, etc). - */ - public releaseTag: ReleaseTag; - - /** - * True if the "\@preapproved" tag was specified. - * Indicates that this internal API is exempt from further reviews. - */ - public preapproved: boolean | undefined; - - /** - * True if the "\@packagedocumentation" tag was specified. - */ - public isPackageDocumentation: boolean | undefined; - - /** - * True if the documentation content has not been reviewed yet. - */ - public isDocBeta: boolean | undefined; - - /** - * True if the \@eventproperty tag was specified. This means class/interface property - * represents and event. It should be a read-only property that returns a user-defined class - * with operations such as addEventHandler() or removeEventHandler(). - */ - public isEventProperty: boolean | undefined; - - /** - * True if the \@inheritdoc tag was specified. - */ - public isDocInherited: boolean | undefined; - - /** - * True if the \@inheritdoc tag was specified and is inheriting from a target object - * that was marked as \@deprecated. - */ - public isDocInheritedDeprecated: boolean | undefined; - - /** - * True if the \@readonly tag was specified. - */ - public hasReadOnlyTag: boolean | undefined; - - public warnings: string[]; - - /** - * Whether the "\@sealed" AEDoc tag was specified. - */ - public isSealed: boolean; - - /** - * Whether the "\@virtual" AEDoc tag was specified. - */ - public isVirtual: boolean; - - /** - * Whether the "\@override" AEDoc tag was specified. - */ - public isOverride: boolean; - - /** - * A function type interface that abstracts away resolving - * an API definition reference to an item that has friendly - * accessible AstItem properties. - * - * Ex: this is useful in the case of parsing inheritdoc expressions, - * in the sense that we do not know if we the inherited documentation - * is coming from an AstItem or a ApiItem. - */ - public referenceResolver: IReferenceResolver; - - /** - * We need the extractor to access the package that this AstItem - * belongs to in order to resolve references. - */ - public context: ExtractorContext; - - /** - * True if any errors were encountered while parsing the AEDoc tokens. - * This is used to suppress other "collateral damage" errors, e.g. if "@public" was - * misspelled then we shouldn't also complain that the "@public" tag is missing. - */ - public failedToParse: boolean; - - public readonly reportError: (message: string, startIndex?: number) => void; - - private _parserContext: ParserContext | undefined; - private _docComment: DocComment | undefined; - - constructor(inputTextRange: TextRange, - referenceResolver: IReferenceResolver, - context: ExtractorContext, - errorLogger: (message: string, startIndex?: number) => void, - warnings: string[]) { - - this.reportError = (message: string, startIndex?: number) => { - errorLogger(message, startIndex); - this.failedToParse = true; - }; - - this.referenceResolver = referenceResolver; - this.context = context; - this.reportError = errorLogger; - this.parameters = {}; - this.warnings = warnings; - - this.isSealed = false; - this.isVirtual = false; - this.isOverride = false; - - this.summary = []; - this.returnsMessage = []; - this.deprecatedMessage = []; - this.remarks = []; - this.incompleteLinks = []; - this.releaseTag = ReleaseTag.None; - - this._parserContext = undefined; - this._docComment = undefined; - - if (!inputTextRange.isEmpty()) { - this._parseDocs(inputTextRange); - } - } - - /** - * Returns true if an AEDoc comment was parsed for the API item. - */ - public get aedocCommentFound(): boolean { - if (this._parserContext) { - return this._parserContext.tokens.length > 0; - } - return false; - } - - /** - * Returns the original AEDoc comment - */ - public emitNormalizedComment(): string { - if (this._parserContext) { - const content: string = this._parserContext.lines.map(x => x.toString()).join('\n'); - return TypeScriptHelpers.formatJSDocContent(content); - } - return ''; - } - - /** - * Executes the implementation details involved in completing the documentation initialization. - * Currently completes link and inheritdocs. - */ - public completeInitialization(warnings: string[]): void { - // Ensure links are valid - this._completeLinks(); - // Ensure inheritdocs are valid - this._completeInheritdocs(warnings); - } - - private _parseDocs(inputTextRange: TextRange): void { - const tsdocParser: TSDocParser = new TSDocParser(AedocDefinitions.parserConfiguration); - this._parserContext = tsdocParser.parseRange(inputTextRange); - this._docComment = this._parserContext.docComment; - - for (const message of this._parserContext.log.messages) { - this.reportError(message.unformattedText, message.textRange.pos); - } - - this._parseModifierTags(); - this._parseSections(); - } - - private _parseModifierTags(): void { - if (!this._docComment) { - return; - } - const modifierTagSet: StandardModifierTagSet = this._docComment.modifierTagSet; - - // The first function call that encounters a duplicate will return false. - // When there are duplicates, the broadest release tag wins. - // tslint:disable-next-line:no-unused-expression - this._parseReleaseTag(modifierTagSet, StandardTags.public, ReleaseTag.Public) - && this._parseReleaseTag(modifierTagSet, StandardTags.beta, ReleaseTag.Beta) - && this._parseReleaseTag(modifierTagSet, StandardTags.alpha, ReleaseTag.Alpha) - && this._parseReleaseTag(modifierTagSet, StandardTags.internal, ReleaseTag.Internal); - - this.preapproved = modifierTagSet.hasTag(AedocDefinitions.preapprovedTag); - - this.isPackageDocumentation = modifierTagSet.isPackageDocumentation(); - this.hasReadOnlyTag = modifierTagSet.isReadonly(); - this.isDocBeta = modifierTagSet.hasTag(AedocDefinitions.betaDocumentation); - this.isEventProperty = modifierTagSet.isEventProperty(); - this.isSealed = modifierTagSet.isSealed(); - this.isVirtual = modifierTagSet.isVirtual(); - this.isOverride = modifierTagSet.isOverride(); - - if (this.preapproved && this.releaseTag !== ReleaseTag.Internal) { - this.reportError('The @preapproved tag may only be applied to @internal definitions'); - this.preapproved = false; - } - - if (this.isSealed && this.isVirtual) { - this.reportError('The @sealed and @virtual tags may not be used together'); - } - - if (this.isVirtual && this.isOverride) { - this.reportError('The @virtual and @override tags may not be used together'); - } - } - - private _parseReleaseTag(modifierTagSet: ModifierTagSet, tagDefinition: TSDocTagDefinition, - releaseTag: ReleaseTag): boolean { - - const node: DocBlockTag | undefined = modifierTagSet.tryGetTag(tagDefinition); - if (node) { - if (this.releaseTag !== ReleaseTag.None) { - this.reportError('More than one release tag was specified (@alpha, @beta, @public, @internal)'); - return false; - } - this.releaseTag = releaseTag; - } - - return true; - } - - private _parseSections(): void { - if (!this._docComment) { - return; - } - - this._renderAsMarkupElementsInto(this.summary, this._docComment.summarySection, - 'the summary section', false); - - if (this._docComment.remarksBlock) { - this._renderAsMarkupElementsInto(this.remarks, this._docComment.remarksBlock, - 'the remarks section', true); - } - - if (this._docComment.deprecatedBlock) { - this._renderAsMarkupElementsInto(this.deprecatedMessage, this._docComment.deprecatedBlock, - 'a deprecation notice', false); - } - - if (this._docComment.returnsBlock) { - this._renderAsMarkupElementsInto(this.returnsMessage, this._docComment.returnsBlock, - 'a return value description', false); - } - - for (const paramBlock of this._docComment.params.blocks) { - const aedocParameter: IAedocParameter = { - name: paramBlock.parameterName, - description: [] - }; - this._renderAsMarkupElementsInto(aedocParameter.description, paramBlock, - 'a parameter description', false); - - this.parameters[paramBlock.parameterName] = aedocParameter; - } - } - - private _renderAsMarkupElementsInto(result: MarkupElement[], node: DocNode, sectionName: string, - allowStructuredContent: boolean): void { - switch (node.kind) { - case DocNodeKind.Block: - case DocNodeKind.ParamBlock: - const docBlock: DocBlock = node as DocBlock; - this._renderAsMarkupElementsInto(result, docBlock.content, sectionName, allowStructuredContent); - break; - case DocNodeKind.Section: - const docSection: DocSection = node as DocSection; - for (const childNode of docSection.nodes) { - this._renderAsMarkupElementsInto(result, childNode, sectionName, allowStructuredContent); - } - break; - case DocNodeKind.BlockTag: - // If an unrecognized TSDoc block tag appears in the content, don't render it - break; - case DocNodeKind.CodeSpan: - const docCodeSpan: DocCodeSpan = node as DocCodeSpan; - result.push(Markup.createCode(docCodeSpan.code)); - break; - case DocNodeKind.ErrorText: - const docErrorText: DocErrorText = node as DocErrorText; - Markup.appendTextElements(result, docErrorText.text); - break; - case DocNodeKind.EscapedText: - const docEscapedText: DocEscapedText = node as DocEscapedText; - Markup.appendTextElements(result, docEscapedText.decodedText); - break; - case DocNodeKind.FencedCode: - if (allowStructuredContent) { - const docCodeFence: DocFencedCode = node as DocFencedCode; - let markupHighlighter: MarkupHighlighter = 'plain'; - switch (docCodeFence.language.toUpperCase()) { - case 'TS': - case 'TYPESCRIPT': - case 'JS': - case 'JAVASCRIPT': - markupHighlighter = 'javascript'; - break; - } - result.push(Markup.createCodeBox(docCodeFence.code, markupHighlighter)); - } else { - this._reportIncorrectStructuredContent('a fenced code block', sectionName); - return; - } - break; - case DocNodeKind.HtmlStartTag: - const docHtmlStartTag: DocHtmlStartTag = node as DocHtmlStartTag; - let htmlStartTag: string = '<'; - htmlStartTag += docHtmlStartTag.name; - for (const attribute of docHtmlStartTag.htmlAttributes) { - htmlStartTag += ` ${attribute.name}=${attribute.value}`; - } - if (docHtmlStartTag.selfClosingTag) { - htmlStartTag += '/'; - } - htmlStartTag += '>'; - result.push(Markup.createHtmlTag(htmlStartTag)); - break; - case DocNodeKind.HtmlEndTag: - const docHtmlEndTag: DocHtmlEndTag = node as DocHtmlEndTag; - result.push(Markup.createHtmlTag(``)); - break; - case DocNodeKind.InlineTag: - const docInlineTag: DocInlineTag = node as DocInlineTag; - Markup.appendTextElements(result, '{' + docInlineTag.tagName + '}'); - break; - case DocNodeKind.LinkTag: - const docLinkTag: DocLinkTag = node as DocLinkTag; - if (docLinkTag.urlDestination) { - result.push(Markup.createWebLinkFromText(docLinkTag.linkText || docLinkTag.urlDestination, - docLinkTag.urlDestination)); - } else if (docLinkTag.codeDestination) { - const apiItemReference: IApiItemReference | undefined - = this._tryCreateApiItemReference(docLinkTag.codeDestination); - if (apiItemReference) { - let linkText: string | undefined = docLinkTag.linkText; - if (!linkText) { - linkText = apiItemReference.exportName; - if (apiItemReference.memberName) { - linkText += '.' + apiItemReference.memberName; - } - } - const linkElement: IMarkupApiLink = Markup.createApiLinkFromText(linkText, apiItemReference); - result.push(linkElement); - - // The link will get resolved later in _completeLinks() - this.incompleteLinks.push(linkElement); - } - } - break; - case DocNodeKind.Paragraph: - if (result.length > 0) { - switch (result[result.length - 1].kind) { - case 'code-box': - case 'heading1': - case 'heading2': - case 'note-box': - case 'page': - case 'paragraph': - case 'table': - // Don't put a Markup.PARAGRAPH after a structural element, - // since it is implicit. - break; - default: - result.push(Markup.PARAGRAPH); - break; - } - } - const docParagraph: DocParagraph = node as DocParagraph; - for (const childNode of DocNodeTransforms.trimSpacesInParagraph(docParagraph).nodes) { - this._renderAsMarkupElementsInto(result, childNode, sectionName, allowStructuredContent); - } - break; - case DocNodeKind.PlainText: - const docPlainText: DocPlainText = node as DocPlainText; - Markup.appendTextElements(result, docPlainText.text); - break; - case DocNodeKind.SoftBreak: - Markup.appendTextElements(result, ' '); - break; - default: - this.reportError('Unsupported TSDoc element: ' + node.kind); - } - } - - private _reportIncorrectStructuredContent(constructName: string, sectionName: string): void { - this.reportError(`Structured content such as ${constructName} cannot be used in ${sectionName}`); - } - - // This is a temporary adapter until we fully generalize IApiItemReference to support TSDoc declaration references - private _tryCreateApiItemReference(declarationReference: DocDeclarationReference): IApiItemReference | undefined { - if (declarationReference.importPath) { - this.reportError(`API Extractor does not yet support TSDoc declaration references containing an import path:` - + ` "(declarationReference.importPath)"`); - return undefined; - } - - const memberReferences: ReadonlyArray = declarationReference.memberReferences; - if (memberReferences.length > 2) { - // This will get be fixed soon - this.reportError('API Extractor does not yet support TSDoc declaration references containing' - + ' more than 2 levels of nesting'); - return undefined; - } - if (memberReferences.length === 0) { - this.reportError('API Extractor does not yet support TSDoc declaration references without a member reference'); - return undefined; - } - - const apiItemReference: IApiItemReference = { - scopeName: '', - packageName: '', - exportName: '', - memberName: '' - }; - - if (declarationReference.packageName) { - const parsedPackageName: IParsedPackageNameOrError = PackageName.tryParse(declarationReference.packageName); - if (parsedPackageName.error) { - this.reportError(`Invalid package name ${declarationReference.packageName}: ${parsedPackageName.error}`); - return undefined; - } - - apiItemReference.scopeName = parsedPackageName.scope; - apiItemReference.packageName = parsedPackageName.unscopedName; - } else { - - // If the package name is unspecified, assume it is the current package - apiItemReference.scopeName = this.context.parsedPackageName.scope; - apiItemReference.packageName = this.context.parsedPackageName.unscopedName; - } - - let identifier: string | undefined = this._tryGetMemberReferenceIdentifier(memberReferences[0]); - if (!identifier) { - return undefined; - } - apiItemReference.exportName = identifier; - - if (memberReferences.length > 1) { - identifier = this._tryGetMemberReferenceIdentifier(memberReferences[1]); - if (!identifier) { - return undefined; - } - apiItemReference.memberName = identifier; - } - - return apiItemReference; - } - - private _tryGetMemberReferenceIdentifier(memberReference: DocMemberReference): string | undefined { - if (!memberReference.memberIdentifier) { - this.reportError('API Extractor currently only supports TSDoc member references using identifiers'); - return undefined; - } - - if (memberReference.memberIdentifier.hasQuotes) { - // Allow quotes if the identifier is being quoted because it is a system name. - // (What's not supported is special characters in the identifier.) - if (!/[_a-z][_a-z0-0]*/i.test(memberReference.memberIdentifier.identifier)) { - this.reportError('API Extractor does not yet support TSDoc member references using quotes'); - return undefined; - } - } - - return memberReference.memberIdentifier.identifier; - } - - /** - * A processing of linkDocElements that refer to an ApiDefinitionReference. This method - * ensures that the reference is to an API item that is not 'Internal'. - */ - private _completeLinks(): void { - for ( ; ; ) { - const codeLink: IMarkupApiLink | undefined = this.incompleteLinks.pop(); - if (!codeLink) { - break; - } - - const parts: IApiDefinitionReferenceParts = { - scopeName: codeLink.target.scopeName, - packageName: codeLink.target.packageName, - exportName: codeLink.target.exportName, - memberName: codeLink.target.memberName - }; - - const apiDefinitionRef: ApiDefinitionReference = ApiDefinitionReference.createFromParts(parts); - const resolvedAstItem: ResolvedApiItem | undefined = this.referenceResolver.resolve( - apiDefinitionRef, - this.context.package, - this.warnings - ); - - // If the apiDefinitionRef can not be found the resolvedAstItem will be - // undefined and an error will have been reported via this.reportError - if (resolvedAstItem) { - if (resolvedAstItem.releaseTag === ReleaseTag.Internal - || resolvedAstItem.releaseTag === ReleaseTag.Alpha) { - - this.reportError('The {@link} tag references an @internal or @alpha API item, ' - + 'which will not appear in the generated documentation'); - } - } - } - } - - /** - * A processing of inheritdoc 'Tokens'. This processing occurs after we have created documentation - * for all API items. - */ - private _completeInheritdocs(warnings: string[]): void { - if (!this._docComment || !this._docComment.inheritDocTag) { - return; - } - if (!this._docComment.inheritDocTag.declarationReference) { - return; - } - const apiItemReference: IApiItemReference | undefined = this._tryCreateApiItemReference( - this._docComment.inheritDocTag.declarationReference); - if (!apiItemReference) { - return; - } - - const apiDefinitionRef: ApiDefinitionReference = ApiDefinitionReference.createFromParts(apiItemReference); - - // Atempt to locate the apiDefinitionRef - const resolvedAstItem: ResolvedApiItem | undefined = this.referenceResolver.resolve( - apiDefinitionRef, - this.context.package, - warnings - ); - - // If no resolvedAstItem found then nothing to inherit - // But for the time being set the summary to a text object - if (!resolvedAstItem) { - let unresolvedAstItemName: string = apiDefinitionRef.exportName; - if (apiDefinitionRef.memberName) { - unresolvedAstItemName += '.' + apiDefinitionRef.memberName; - } - this.summary.push(...Markup.createTextElements( - `See documentation for ${unresolvedAstItemName}`)); - return; - } - - // We are going to copy the resolvedAstItem's documentation - // We must make sure it's documentation can be completed, - // if we cannot, an error will be reported viathe documentation error handler. - // This will only be the case our resolvedAstItem was created from a local - // AstItem. Resolutions from JSON will have an undefined 'astItem' property. - // Example: a circular reference will report an error. - if (resolvedAstItem.astItem) { - resolvedAstItem.astItem.completeInitialization(); - } - - // inheritdoc found, copy over IApiBaseDefinition properties - this.summary = resolvedAstItem.summary; - this.remarks = resolvedAstItem.remarks; - - // Copy over detailed properties if neccessary - // Add additional cases if needed - switch (resolvedAstItem.kind) { - case AstItemKind.Function: - this.parameters = resolvedAstItem.params || { }; - this.returnsMessage = resolvedAstItem.returnsMessage || []; - break; - case AstItemKind.Method: - case AstItemKind.Constructor: - this.parameters = resolvedAstItem.params || { }; - this.returnsMessage = resolvedAstItem.returnsMessage || []; - break; - } - - // Check if inheritdoc is depreacted - // We need to check if this documentation has a deprecated message - // but it may not appear until after this token. - if (resolvedAstItem.deprecatedMessage && resolvedAstItem.deprecatedMessage.length > 0) { - this.isDocInheritedDeprecated = true; - } - } -} diff --git a/apps/api-extractor/src/aedoc/PackageDocComment.ts b/apps/api-extractor/src/aedoc/PackageDocComment.ts new file mode 100644 index 00000000000..3c20e6931a7 --- /dev/null +++ b/apps/api-extractor/src/aedoc/PackageDocComment.ts @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import * as ts from 'typescript'; +import { Collector } from '../collector/Collector'; + +export class PackageDocComment { + /** + * For the given source file, see if it starts with a TSDoc comment containing the `@packageDocumentation` tag. + */ + public static tryFindInSourceFile(sourceFile: ts.SourceFile, + collector: Collector): ts.TextRange | undefined { + + // The @packageDocumentation comment is special because it is not attached to an AST + // definition. Instead, it is part of the "trivia" tokens that the compiler treats + // as irrelevant white space. + // + // WARNING: If the comment doesn't precede an export statement, the compiler will omit + // it from the *.d.ts file, and API Extractor won't find it. If this happens, you need + // to rearrange your statements to ensure it is passed through. + // + // This implementation assumes that the "@packageDocumentation" will be in the first TSDoc comment + // that appears in the entry point *.d.ts file. We could possibly look in other places, + // but the above warning suggests enforcing a standardized layout. This design choice is open + // to feedback. + let packageCommentRange: ts.TextRange | undefined = undefined; // empty string + + for (const commentRange of ts.getLeadingCommentRanges(sourceFile.text, sourceFile.getFullStart()) || []) { + if (commentRange.kind === ts.SyntaxKind.MultiLineCommentTrivia) { + const commentBody: string = sourceFile.text.substring(commentRange.pos, commentRange.end); + + // Choose the first JSDoc-style comment + if (/^\s*\/\*\*/.test(commentBody)) { + // But only if it looks like it's trying to be @packageDocumentation + // (The TSDoc parser will validate this more rigorously) + if (/\@packageDocumentation/i.test(commentBody)) { + packageCommentRange = commentRange; + } + break; + } + } + } + + if (!packageCommentRange) { + // If we didn't find the @packageDocumentation tag in the expected place, is it in some + // wrong place? This sanity check helps people to figure out why there comment isn't working. + for (const statement of sourceFile.statements) { + const ranges: ts.CommentRange[] = []; + ranges.push(...ts.getLeadingCommentRanges(sourceFile.text, statement.getFullStart()) || []); + ranges.push(...ts.getTrailingCommentRanges(sourceFile.text, statement.getEnd()) || []); + + for (const commentRange of ranges) { + const commentBody: string = sourceFile.text.substring(commentRange.pos, commentRange.end); + + if (/\@packageDocumentation/i.test(commentBody)) { + collector.reportError( + 'The @packageDocumentation comment must appear at the top of entry point *.d.ts file', + sourceFile, commentRange.pos + ); + break; + } + } + } + } + + return packageCommentRange; + } + +} diff --git a/apps/api-extractor/src/aedoc/ReleaseTag.ts b/apps/api-extractor/src/aedoc/ReleaseTag.ts index 4d0e198b953..bd77fd8b190 100644 --- a/apps/api-extractor/src/aedoc/ReleaseTag.ts +++ b/apps/api-extractor/src/aedoc/ReleaseTag.ts @@ -2,11 +2,18 @@ // See LICENSE in the project root for license information. /** - * A "release tag" is an AEDoc tag which indicates whether an AstItem definition - * is considered Public API for third party developers, as well as its release - * stage (alpha, beta, etc). - * @see https://onedrive.visualstudio.com/DefaultCollection/SPPPlat/_git/sp-client - * ?path=/common/docs/ApiPrinciplesAndProcess.md + * A "release tag" is a custom TSDoc tag that is applied to an API to communicate the level of support + * provided for third-party developers. + * + * @remarks + * + * The four release tags are: `@internal`, `@alpha`, `@beta`, and `@public`. They are applied to API items such + * as classes, member functions, enums, etc. The release tag applies recursively to members of a container + * (e.g. class or interface). For example, if a class is marked as `@beta`, then all of its members automatically + * have this status; you DON'T need add the `@beta` tag to each member function. However, you could add + * `@internal` to a member function to give it a different release status. + * + * @public */ export enum ReleaseTag { /** diff --git a/apps/api-extractor/src/generators/dtsRollup/AstDeclaration.ts b/apps/api-extractor/src/analyzer/AstDeclaration.ts similarity index 90% rename from apps/api-extractor/src/generators/dtsRollup/AstDeclaration.ts rename to apps/api-extractor/src/analyzer/AstDeclaration.ts index 15481c29b44..df50778e3b4 100644 --- a/apps/api-extractor/src/generators/dtsRollup/AstDeclaration.ts +++ b/apps/api-extractor/src/analyzer/AstDeclaration.ts @@ -3,12 +3,12 @@ import * as ts from 'typescript'; import { AstSymbol } from './AstSymbol'; -import { Span } from '../../utils/Span'; +import { Span } from './Span'; /** - * Constructor parameters for AstDeclaration + * Constructor options for AstDeclaration */ -export interface IAstDeclarationParameters { +export interface IAstDeclarationOptions { readonly declaration: ts.Declaration; readonly astSymbol: AstSymbol; readonly parent: AstDeclaration | undefined; @@ -41,20 +41,32 @@ export class AstDeclaration { */ public readonly parent: AstDeclaration | undefined; + /** + * A bit set of TypeScript modifiers such as "private", "protected", etc. + */ + public readonly modifierFlags: ts.ModifierFlags; + + /** + * Additional information applied later by the Collector. + */ + public metadata: unknown; + private readonly _analyzedChildren: AstDeclaration[] = []; private readonly _analyzedReferencedAstSymbolsSet: Set = new Set(); - public constructor(parameters: IAstDeclarationParameters) { - this.declaration = parameters.declaration; - this.astSymbol = parameters.astSymbol; - this.parent = parameters.parent; + public constructor(options: IAstDeclarationOptions) { + this.declaration = options.declaration; + this.astSymbol = options.astSymbol; + this.parent = options.parent; this.astSymbol._notifyDeclarationAttach(this); if (this.parent) { this.parent._notifyChildAttach(this); } + + this.modifierFlags = ts.getCombinedModifierFlags(this.declaration); } /** diff --git a/apps/api-extractor/src/generators/dtsRollup/AstEntryPoint.ts b/apps/api-extractor/src/analyzer/AstEntryPoint.ts similarity index 76% rename from apps/api-extractor/src/generators/dtsRollup/AstEntryPoint.ts rename to apps/api-extractor/src/analyzer/AstEntryPoint.ts index 8f3f7f43dbe..aa19c397e27 100644 --- a/apps/api-extractor/src/generators/dtsRollup/AstEntryPoint.ts +++ b/apps/api-extractor/src/analyzer/AstEntryPoint.ts @@ -4,14 +4,14 @@ import { AstSymbol } from './AstSymbol'; /** - * Constructor parameters for AstEntryPoint + * Constructor options for AstEntryPoint */ export interface IExportedMember { readonly name: string; readonly astSymbol: AstSymbol; } -export interface IAstEntryPointParameters { +export interface IAstEntryPointOptions { readonly exportedMembers: ReadonlyArray; } @@ -23,7 +23,7 @@ export interface IAstEntryPointParameters { export class AstEntryPoint { public readonly exportedMembers: ReadonlyArray; - public constructor(parameters: IAstEntryPointParameters) { - this.exportedMembers = parameters.exportedMembers; + public constructor(options: IAstEntryPointOptions) { + this.exportedMembers = options.exportedMembers; } } diff --git a/apps/api-extractor/src/generators/dtsRollup/AstImport.ts b/apps/api-extractor/src/analyzer/AstImport.ts similarity index 85% rename from apps/api-extractor/src/generators/dtsRollup/AstImport.ts rename to apps/api-extractor/src/analyzer/AstImport.ts index 9b089676aea..99511387957 100644 --- a/apps/api-extractor/src/generators/dtsRollup/AstImport.ts +++ b/apps/api-extractor/src/analyzer/AstImport.ts @@ -2,9 +2,9 @@ // See LICENSE in the project root for license information. /** - * Constructor parameters for AstImport + * Constructor options for AstImport */ -export interface IAstImportParameters { +export interface IAstImportOptions { readonly modulePath: string; readonly exportName: string; } @@ -39,9 +39,9 @@ export class AstImport { */ public readonly key: string; - public constructor(parameters: IAstImportParameters) { - this.modulePath = parameters.modulePath; - this.exportName = parameters.exportName; + public constructor(options: IAstImportOptions) { + this.modulePath = options.modulePath; + this.exportName = options.exportName; this.key = `${this.modulePath}:${this.exportName}`; } diff --git a/apps/api-extractor/src/generators/dtsRollup/AstSymbol.ts b/apps/api-extractor/src/analyzer/AstSymbol.ts similarity index 90% rename from apps/api-extractor/src/generators/dtsRollup/AstSymbol.ts rename to apps/api-extractor/src/analyzer/AstSymbol.ts index 7dbf1a7415e..834a68fe3f4 100644 --- a/apps/api-extractor/src/generators/dtsRollup/AstSymbol.ts +++ b/apps/api-extractor/src/analyzer/AstSymbol.ts @@ -6,9 +6,9 @@ import { AstImport } from './AstImport'; import { AstDeclaration } from './AstDeclaration'; /** - * Constructor parameters for AstSymbol + * Constructor options for AstSymbol */ -export interface IAstSymbolParameters { +export interface IAstSymbolOptions { readonly followedSymbol: ts.Symbol; readonly localName: string; readonly astImport: AstImport | undefined; @@ -43,7 +43,7 @@ export class AstSymbol { /** * If this symbol was imported from another package, that information is tracked here. - * Otherwies, the value is undefined. + * Otherwise, the value is undefined. */ public readonly astImport: AstImport | undefined; @@ -74,19 +74,24 @@ export class AstSymbol { */ public readonly rootAstSymbol: AstSymbol; + /** + * Additional information applied later by the Collector. + */ + public metadata: unknown; + private readonly _astDeclarations: AstDeclaration[]; // This flag is unused if this is not the root symbol. // Being "analyzed" is a property of the root symbol. private _analyzed: boolean = false; - public constructor(parameters: IAstSymbolParameters) { - this.followedSymbol = parameters.followedSymbol; - this.localName = parameters.localName; - this.astImport = parameters.astImport; - this.nominal = parameters.nominal; - this.parentAstSymbol = parameters.parentAstSymbol; - this.rootAstSymbol = parameters.rootAstSymbol || this; + public constructor(options: IAstSymbolOptions) { + this.followedSymbol = options.followedSymbol; + this.localName = options.localName; + this.astImport = options.astImport; + this.nominal = options.nominal; + this.parentAstSymbol = options.parentAstSymbol; + this.rootAstSymbol = options.rootAstSymbol || this; this._astDeclarations = []; } diff --git a/apps/api-extractor/src/generators/dtsRollup/AstSymbolTable.ts b/apps/api-extractor/src/analyzer/AstSymbolTable.ts similarity index 99% rename from apps/api-extractor/src/generators/dtsRollup/AstSymbolTable.ts rename to apps/api-extractor/src/analyzer/AstSymbolTable.ts index cd4fbac5bdf..46ba0aba50b 100644 --- a/apps/api-extractor/src/generators/dtsRollup/AstSymbolTable.ts +++ b/apps/api-extractor/src/analyzer/AstSymbolTable.ts @@ -8,12 +8,12 @@ import { PackageJsonLookup } from '@microsoft/node-core-library'; import { AstDeclaration } from './AstDeclaration'; import { SymbolAnalyzer, IFollowAliasesResult } from './SymbolAnalyzer'; -import { TypeScriptHelpers } from '../../utils/TypeScriptHelpers'; +import { TypeScriptHelpers } from './TypeScriptHelpers'; import { AstSymbol } from './AstSymbol'; import { AstImport } from './AstImport'; import { AstEntryPoint, IExportedMember } from './AstEntryPoint'; import { PackageMetadataManager } from './PackageMetadataManager'; -import { ILogger } from '../../extractor/ILogger'; +import { ILogger } from '../api/ILogger'; /** * AstSymbolTable is the workhorse that builds AstSymbol and AstDeclaration objects. diff --git a/apps/api-extractor/src/generators/dtsRollup/PackageMetadataManager.ts b/apps/api-extractor/src/analyzer/PackageMetadataManager.ts similarity index 98% rename from apps/api-extractor/src/generators/dtsRollup/PackageMetadataManager.ts rename to apps/api-extractor/src/analyzer/PackageMetadataManager.ts index a2712304659..14ea88d0cb3 100644 --- a/apps/api-extractor/src/generators/dtsRollup/PackageMetadataManager.ts +++ b/apps/api-extractor/src/analyzer/PackageMetadataManager.ts @@ -10,8 +10,8 @@ import { JsonFile, NewlineKind } from '@microsoft/node-core-library'; -import { Extractor } from '../../extractor/Extractor'; -import { ILogger } from '../../extractor/ILogger'; +import { Extractor } from '../api/Extractor'; +import { ILogger } from '../api/ILogger'; /** * Represents analyzed information for a package.json file. diff --git a/apps/api-extractor/src/utils/Span.ts b/apps/api-extractor/src/analyzer/Span.ts similarity index 67% rename from apps/api-extractor/src/utils/Span.ts rename to apps/api-extractor/src/analyzer/Span.ts index 6690e7c86e8..64dea546a20 100644 --- a/apps/api-extractor/src/utils/Span.ts +++ b/apps/api-extractor/src/analyzer/Span.ts @@ -2,6 +2,8 @@ // See LICENSE in the project root for license information. import * as ts from 'typescript'; +import { StringBuilder } from '@microsoft/tsdoc'; +import { Sort } from '@microsoft/node-core-library'; /** * Specifies various transformations that will be performed by Span.getModifiedText(). @@ -19,6 +21,18 @@ export class SpanModification { */ public omitSeparatorAfter: boolean; + /** + * If true, then Span.getModifiedText() will sort the immediate children according to their Span.sortKey + * property. The separators will also be fixed up to ensure correct indentation. If the Span.sortKey is undefined + * for some items, those items will not be moved, i.e. their array indexes will be unchanged. + */ + public sortChildren: boolean; + + /** + * Used if the parent span has Span.sortChildren=true. + */ + public sortKey: string | undefined; + private readonly span: Span; private _prefix: string | undefined; private _suffix: string | undefined; @@ -56,6 +70,8 @@ export class SpanModification { public reset(): void { this.omitChildren = false; this.omitSeparatorAfter = false; + this.sortChildren = false; + this.sortKey = undefined; this._prefix = undefined; this._suffix = undefined; } @@ -253,6 +269,25 @@ export class Span { return ''; } + /** + * Starting from the first character of this span, walk backwards until we find the start of the line, + * and return that whitespace. + */ + public getIndent(): string { + const buffer: string = this.node.getSourceFile().text; + let lineStartIndex: number = this.startIndex; + + while (lineStartIndex > 0) { + const c: number = buffer.charCodeAt(lineStartIndex - 1); + if (c !== 32 /* space */ && c !== 9 /* tab */) { + break; + } + --lineStartIndex; + } + + return buffer.substring(lineStartIndex, this.startIndex); + } + /** * Recursively invokes the callback on this Span and all its children. The callback * can make changes to Span.modification for each node. @@ -285,21 +320,21 @@ export class Span { * Returns the text represented by this Span, after applying all requested modifications. */ public getModifiedText(): string { - let result: string = ''; - result += this.modification.prefix; + const output: StringBuilder = new StringBuilder(); - if (!this.modification.omitChildren) { - for (const child of this.children) { - result += child.getModifiedText(); - } - } + this._writeModifiedText({ + output, + separatorOverride: undefined + }); - result += this.modification.suffix; - if (!this.modification.omitSeparatorAfter) { - result += this.separator; - } + return output.toString(); + } - return result; + public writeModifiedText(output: StringBuilder): void { + this._writeModifiedText({ + output, + separatorOverride: undefined + }); } /** @@ -327,6 +362,97 @@ export class Span { return result; } + private _writeModifiedText(options: IWriteModifiedTextOptions): void { + options.output.append(this.modification.prefix); + + const childCount: number = this.children.length; + + if (!this.modification.omitChildren) { + + if (this.modification.sortChildren && childCount > 1) { + // We will only sort the items with a sortKey + const sortedSubset: Span[] = this.children.filter(x => x.modification.sortKey !== undefined); + const sortedSubsetCount: number = sortedSubset.length; + + // Is there at least one of them? + if (sortedSubsetCount > 1) { + + // Remember the separator for the first and last ones + const firstSeparator: string = sortedSubset[0].getLastInnerSeparator(); + const lastSeparator: string = sortedSubset[sortedSubsetCount - 1].getLastInnerSeparator(); + + Sort.sortBy(sortedSubset, x => x.modification.sortKey); + + const childOptions: IWriteModifiedTextOptions = { ...options }; + + let sortedSubsetIndex: number = 0; + for (let index: number = 0; index < childCount; ++index) { + let current: Span; + + // Is this an item that we sorted? + if (this.children[index].modification.sortKey === undefined) { + // No, take the next item from the original array + current = this.children[index]; + childOptions.separatorOverride = undefined; + } else { + // Yes, take the next item from the sortedSubset + current = sortedSubset[sortedSubsetIndex++]; + + if (sortedSubsetIndex < sortedSubsetCount) { + childOptions.separatorOverride = firstSeparator; + } else { + childOptions.separatorOverride = lastSeparator; + } + } + + current._writeModifiedText(childOptions); + } + + return; + } + // (fall through to the other implementations) + } + + if (options.separatorOverride !== undefined) { + // Special case where the separatorOverride is passed down to the "last inner separator" span + for (let i: number = 0; i < childCount; ++i) { + const child: Span = this.children[i]; + + if ( + // Only the last child inherits the separatorOverride, because only it can contain + // the "last inner separator" span + i < childCount - 1 + // If this.separator is specified, then we will write separatorOverride below, so don't pass it along + || this.separator + ) { + const childOptions: IWriteModifiedTextOptions = { ...options }; + childOptions.separatorOverride = undefined; + child._writeModifiedText(childOptions); + } else { + child._writeModifiedText(options); + } + } + } else { + // The normal simple case + for (const child of this.children) { + child._writeModifiedText(options); + } + } + } + + options.output.append(this.modification.suffix); + + if (options.separatorOverride !== undefined) { + if (this.separator || childCount === 0) { + options.output.append(options.separatorOverride); + } + } else { + if (!this.modification.omitSeparatorAfter) { + options.output.append(this.separator); + } + } + } + private _getTrimmed(text: string): string { const trimmed: string = text.replace(/[\r\n]/g, '\\n'); @@ -343,3 +469,8 @@ export class Span { return this.node.getSourceFile().text.substring(startIndex, endIndex); } } + +interface IWriteModifiedTextOptions { + output: StringBuilder; + separatorOverride: string | undefined; +} \ No newline at end of file diff --git a/apps/api-extractor/src/generators/dtsRollup/SymbolAnalyzer.ts b/apps/api-extractor/src/analyzer/SymbolAnalyzer.ts similarity index 99% rename from apps/api-extractor/src/generators/dtsRollup/SymbolAnalyzer.ts rename to apps/api-extractor/src/analyzer/SymbolAnalyzer.ts index ec2e5261909..9fb645a47a5 100644 --- a/apps/api-extractor/src/generators/dtsRollup/SymbolAnalyzer.ts +++ b/apps/api-extractor/src/analyzer/SymbolAnalyzer.ts @@ -5,7 +5,7 @@ import * as ts from 'typescript'; -import { TypeScriptHelpers } from '../../utils/TypeScriptHelpers'; +import { TypeScriptHelpers } from './TypeScriptHelpers'; import { AstImport } from './AstImport'; /** diff --git a/apps/api-extractor/src/utils/TypeScriptHelpers.ts b/apps/api-extractor/src/analyzer/TypeScriptHelpers.ts similarity index 56% rename from apps/api-extractor/src/utils/TypeScriptHelpers.ts rename to apps/api-extractor/src/analyzer/TypeScriptHelpers.ts index afa94565336..a45fbaea2c4 100644 --- a/apps/api-extractor/src/utils/TypeScriptHelpers.ts +++ b/apps/api-extractor/src/analyzer/TypeScriptHelpers.ts @@ -4,39 +4,9 @@ /* tslint:disable:no-bitwise */ import * as ts from 'typescript'; -import { PrettyPrinter } from './PrettyPrinter'; +import { TypeScriptMessageFormatter } from './TypeScriptMessageFormatter'; export class TypeScriptHelpers { - /** - * Splits on CRLF and other newline sequences - */ - private static _newLineRegEx: RegExp = /\r\n|\n\r|\r|\n/g; - - /** - * Start sequence is '/**'. - */ - private static _jsdocStartRegEx: RegExp = /^\s*\/\*\*+\s*/; - - /** - * End sequence is '*\/'. - */ - private static _jsdocEndRegEx: RegExp = /\s*\*+\/\s*$/; - - /** - * Intermediate lines of JSDoc comment character. - */ - private static _jsdocIntermediateRegEx: RegExp = /^\s*\*\s?/; - - /** - * Trailing white space - */ - private static _jsdocTrimRightRegEx: RegExp = /\s*$/; - - /** - * Invalid comment sequence - */ - private static _jsdocCommentTerminator: RegExp = /[*][/]/g; - /** * This traverses any type aliases to find the original place where an item was defined. * For example, suppose a class is defined as "export default class MyClass { }" @@ -90,7 +60,7 @@ export class TypeScriptHelpers { public static getSymbolForDeclaration(declaration: ts.Declaration): ts.Symbol { const symbol: ts.Symbol | undefined = TypeScriptHelpers.tryGetSymbolForDeclaration(declaration); if (!symbol) { - throw new Error(PrettyPrinter.formatFileAndLineNumber(declaration) + ': ' + throw new Error(TypeScriptMessageFormatter.formatFileAndLineNumber(declaration) + ': ' + 'Unable to determine semantic information for this declaration'); } return symbol; @@ -107,140 +77,6 @@ export class TypeScriptHelpers { return (ts as any).getJSDocCommentRanges.apply(this, arguments); } - /** - * Similar to calling string.split() with a RegExp, except that the delimiters - * are included in the result. - * - * Example: _splitStringWithRegEx("ABCDaFG", /A/gi) -> [ "A", "BCD", "a", "FG" ] - * Example: _splitStringWithRegEx("", /A/gi) -> [ ] - * Example: _splitStringWithRegEx("", /A?/gi) -> [ "" ] - */ - public static splitStringWithRegEx(text: string, regExp: RegExp): string[] { - if (!regExp.global) { - throw new Error('RegExp must have the /g flag'); - } - if (text === undefined) { - return []; - } - - const result: string[] = []; - let index: number = 0; - let match: RegExpExecArray | null; - - do { - match = regExp.exec(text); - if (match) { - if (match.index > index) { - result.push(text.substring(index, match.index)); - } - const matchText: string = match[0]; - if (!matchText) { - // It might be interesting to support matching e.g. '\b', but regExp.exec() - // doesn't seem to iterate properly in this situation. - throw new Error('The regular expression must match a nonzero number of characters'); - } - result.push(matchText); - index = regExp.lastIndex; - } - } while (match && regExp.global); - - if (index < text.length) { - result.push(text.substr(index)); - } - return result; - } - - /** - * Extracts the body of a JSDoc comment and returns it. - */ - // Examples: - // "/**\n * this is\n * a test\n */\n" --> "this is\na test\n" - // "/** single line comment */" --> "single line comment" - public static extractJSDocContent(text: string, errorLogger: (message: string) => void): string { - // Remove any leading/trailing whitespace around the comment characters, then split on newlines - const lines: string[] = text.trim().split(TypeScriptHelpers._newLineRegEx); - if (lines.length === 0) { - return ''; - } - - let matched: boolean; - - // Remove "/**" from the first line - matched = false; - lines[0] = lines[0].replace(TypeScriptHelpers._jsdocStartRegEx, () => { - matched = true; - return ''; - }); - if (!matched) { - errorLogger('The comment does not begin with a \"/**\" delimiter.'); - return ''; - } - - // Remove "*/" from the last line - matched = false; - lines[lines.length - 1] = lines[lines.length - 1].replace(TypeScriptHelpers._jsdocEndRegEx, () => { - matched = true; - return ''; - }); - if (!matched) { - errorLogger('The comment does not end with a \"*/\" delimiter.'); - return ''; - } - - // Remove a leading "*" from all lines except the first one - for (let i: number = 1; i < lines.length; ++i) { - lines[i] = lines[i].replace(TypeScriptHelpers._jsdocIntermediateRegEx, ''); - } - - // Remove trailing spaces from all lines - for (let i: number = 0; i < lines.length; ++i) { - lines[i] = lines[i].replace(TypeScriptHelpers._jsdocTrimRightRegEx, ''); - } - - // If the first line is blank, then remove it - if (lines[0] === '') { - lines.shift(); - } - - return lines.join('\n'); - } - - /** - * Returns a JSDoc comment containing the provided content. - * - * @remarks - * This is the inverse of the extractJSDocContent() operation. - */ - // Examples: - // "this is\na test\n" --> "/**\n * this is\n * a test\n */\n" - // "single line comment" --> "/** single line comment */" - public static formatJSDocContent(content: string): string { - if (!content) { - return ''; - } - - // If the string contains "*/", then replace it with "*\/" - const escapedContent: string = content.replace(TypeScriptHelpers._jsdocCommentTerminator, '*\\/'); - - const lines: string[] = escapedContent.split(TypeScriptHelpers._newLineRegEx); - if (lines.length === 0) { - return ''; - } - - if (lines.length < 2) { - return `/** ${escapedContent} */`; - } else { - // If there was a trailing newline, remove it - if (lines[lines.length - 1] === '') { - lines.pop(); - } - - return '/**\n * ' - + lines.join('\n * ') - + '\n */'; - } - } - /** * Returns an ancestor of "node", such that the ancestor, any intermediary nodes, * and the starting node match a list of expected kinds. Undefined is returned diff --git a/apps/api-extractor/src/utils/TypeScriptMessageFormatter.ts b/apps/api-extractor/src/analyzer/TypeScriptMessageFormatter.ts similarity index 67% rename from apps/api-extractor/src/utils/TypeScriptMessageFormatter.ts rename to apps/api-extractor/src/analyzer/TypeScriptMessageFormatter.ts index 561f1f375a0..9505de9efe3 100644 --- a/apps/api-extractor/src/utils/TypeScriptMessageFormatter.ts +++ b/apps/api-extractor/src/analyzer/TypeScriptMessageFormatter.ts @@ -23,4 +23,14 @@ export class TypeScriptMessageFormatter { return formattedErrors.join('; '); } + + /** + * Returns a string such as this, based on the context information in the provided node: + * "[C:\Folder\File.ts#123]" + */ + public static formatFileAndLineNumber(node: ts.Node): string { + const sourceFile: ts.SourceFile = node.getSourceFile(); + const lineAndCharacter: ts.LineAndCharacter = sourceFile.getLineAndCharacterOfPosition(node.getStart()); + return `[${sourceFile.fileName}#${lineAndCharacter.line}]`; + } } diff --git a/apps/api-extractor/src/api/ApiItem.ts b/apps/api-extractor/src/api/ApiItem.ts deleted file mode 100644 index 0dc14f0aeef..00000000000 --- a/apps/api-extractor/src/api/ApiItem.ts +++ /dev/null @@ -1,464 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -// See LICENSE in the project root for license information. - -import { - MarkupBasicElement, - MarkupStructuredElement -} from '../markup/MarkupElement'; - -/** - * Represents a reference to an ApiItem. - * @alpha - */ -export interface IApiItemReference { - /** - * The name of the NPM scope, or an empty string if there is no scope. - * @remarks - * Example: `@microsoft` - */ - scopeName: string; - - /** - * The name of the NPM package that the API item belongs to, without the NPM scope. - * @remarks - * Example: `sample-package` - */ - packageName: string; - - /** - * The name of an exported API item, or an empty string. - * @remarks - * The name does not include any generic parameters or other punctuation. - * Example: `SampleClass` - */ - exportName: string; - - /** - * The name of a member of the exported item, or an empty string. - * @remarks - * The name does not include any parameters or punctuation. - * Example: `toString` - */ - memberName: string; -} - -/** - * Whether the function is public, private, or protected. - * @alpha - */ -export type ApiAccessModifier = 'public' | 'private' | 'protected' | ''; - -/** - * Parameter Doc item. - * @alpha - */ -export interface IApiParameter { - /** - * the parameter name - */ - name: string; - - /** - * describes the parameter - */ - description: MarkupBasicElement[]; - - /** - * Whether the parameter is optional - */ - isOptional: boolean; - - /** - * Whether the parameter has the '...' spread suffix - */ - isSpread: boolean; - - /** - * The data type of the parameter - */ - type: string; -} - -/** - * An ordered map of items, indexed by the symbol name. - * @alpha - */ -export interface IApiNameMap { - /** - * For a given name, returns the object with that name. - */ - [name: string]: T; -} - -/** - * Return value of a method or function. - * @alpha - */ -export interface IApiReturnValue { - /** - * The data type returned by the function - */ - type: string; - - /** - * Describes the return value - */ - description: MarkupBasicElement[]; -} - -/** - * DocItems are the typescript adaption of the json schemas - * defined in API-json-schema.json. IDocElement is a component - * for IDocItems because they represent formated rich text. - * - * This is the base class for other DocItem types. - * @alpha - */ -export interface IApiBaseDefinition { - /** - * kind of item: 'class', 'enum', 'function', etc. - */ - kind: string; - isBeta: boolean; - summary: MarkupBasicElement[]; - remarks: MarkupStructuredElement[]; - deprecatedMessage?: MarkupBasicElement[]; -} - -/** - * A property of a TypeScript class or interface - * @alpha - */ -export interface IApiProperty extends IApiBaseDefinition { - - /** - * {@inheritdoc IApiBaseDefinition.kind} - */ - kind: 'property'; - - /** - * a text summary of the method definition - */ - signature: string; - - /** - * For an interface member, whether it is optional - */ - isOptional: boolean; - - /** - * Whether the property is read-only - */ - isReadOnly: boolean; - - /** - * For a class member, whether it is static - */ - isStatic: boolean; - - /** - * Whether the item was marked as "\@eventproperty", which indicates an event object that event - * handlers can be attached to. - */ - isEventProperty: boolean; - - /** - * Indicates that the item was marked as "\@sealed" and must not be extended. - */ - isSealed: boolean; - - /** - * Indicates that the item was marked as "\@virtual" and may be extended. - */ - isVirtual: boolean; - - /** - * Indicates that the item was marked as "\@override" and is overriding a base definition. - */ - isOverride: boolean; - - /** - * The data type of this property - */ - type: string; -} - -/** - * A member function of a typescript class or interface. - * @alpha - */ -export interface IApiMethod extends IApiBaseDefinition { - /** - * {@inheritdoc IApiBaseDefinition.kind} - */ - kind: 'method'; - - /** - * a text summary of the method definition - */ - signature: string; - - /** - * the access modifier of the method - */ - accessModifier: ApiAccessModifier; - - /** - * for an interface member, whether it is optional - */ - isOptional: boolean; - - /** - * for a class member, whether it is static - */ - isStatic: boolean; - - /** - * Indicates that the item was marked as "\@sealed" and must not be extended. - */ - isSealed: boolean; - - /** - * Indicates that the item was marked as "\@virtual" and may be extended. - */ - isVirtual: boolean; - - /** - * Indicates that the item was marked as "\@override" and is overriding a base definition. - */ - isOverride: boolean; - - /** - * a mapping of parameter name to IApiParameter - */ - - parameters: IApiNameMap; - - /** - * describes the return value of the method - */ - returnValue: IApiReturnValue; -} - -/** - * A Typescript function. - * @alpha - */ -export interface IApiFunction extends IApiBaseDefinition { - /** - * {@inheritdoc IApiBaseDefinition.kind} - */ - kind: 'function'; - - /** - * a text summary of the method definition - */ - signature: string; - - /** - * parameters of the function - */ - parameters: IApiNameMap; - - /** - * a description of the return value - */ - returnValue: IApiReturnValue; -} - -/** - * A Typescript function. - * @alpha - */ -export interface IApiConstructor extends IApiBaseDefinition { - /** - * {@inheritdoc IApiBaseDefinition.kind} - */ - kind: 'constructor'; - - /** - * a text summary of the method definition - */ - signature: string; - - /** - * Indicates that the item was marked as "\@sealed" and must not be extended. - */ - isSealed: boolean; - - /** - * Indicates that the item was marked as "\@virtual" and may be extended. - */ - isVirtual: boolean; - - /** - * Indicates that the item was marked as "\@override" and is overriding a base definition. - */ - isOverride: boolean; - - /** - * parameters of the function - */ - parameters: IApiNameMap; -} - -/** - * IApiClass represetns an exported class. - * @alpha - */ -export interface IApiClass extends IApiBaseDefinition { - /** - * {@inheritdoc IApiBaseDefinition.kind} - */ - kind: 'class'; - /** - * Can be a combination of methods and/or properties - */ - members: IApiNameMap; - - /** - * Interfaces implemented by this class - */ - implements?: string; - - /** - * The base class for this class - */ - extends?: string; - - /** - * Generic type parameters for this class - */ - typeParameters?: string[]; - - /** - * Indicates that the item was marked as "\@sealed" and must not be extended. - */ - isSealed: boolean; -} - -/** - * IApiEnum represents an exported enum. - * @alpha - */ -export interface IApiEnum extends IApiBaseDefinition { - /** - * {@inheritdoc IApiBaseDefinition.kind} - */ - kind: 'enum'; - - values: IApiEnumMember[]; -} - -/** - * A member of an IApiEnum. - * - * @alpha - */ -export interface IApiEnumMember extends IApiBaseDefinition { - /** - * {@inheritdoc IApiBaseDefinition.kind} - */ - kind: 'enum value'; - - value: string; -} - -/** - * IApiInterface represents an exported interface. - * @alpha - */ -export interface IApiInterface extends IApiBaseDefinition { - /** - * {@inheritdoc IApiBaseDefinition.kind} - */ - kind: 'interface'; - /** - * A mapping from the name of a member API to its ApiMember - */ - members: IApiNameMap; - - /** - * Interfaces implemented by this interface - */ - implements?: string; - - /** - * The base interface for this interface - */ - extends?: string; - - /** - * Generic type parameters for this interface - */ - typeParameters?: string[]; - - /** - * Indicates that the item was marked as "\@sealed" and must not be extended. - */ - isSealed: boolean; -} - -/** - * IApiInterface represents an exported interface. - * @alpha - */ -export interface IApiNamespace extends IApiBaseDefinition { - /** - * {@inheritdoc IApiBaseDefinition.kind} - */ - kind: 'namespace'; - - /** - * A mapping from the name of a member API to its ApiMember - */ - exports: IApiNameMap; -} - -/** - * IApiPackage is an object contaning the exported - * definions of this API package. The exports can include: - * classes, interfaces, enums, functions. - * @alpha - */ -export interface IApiPackage { - /** - * {@inheritdoc IApiBaseDefinition.kind} - */ - kind: 'package'; - - /** - * The name of the NPM package, including the optional scope. - * @remarks - * Example: `@microsoft/example-package` - */ - name: string; - - /** - * IDocItems of exported API items - */ - exports: IApiNameMap; - - /** - * The following are needed so that this interface and can share - * common properties with others that extend IApiBaseDefinition. The IApiPackage - * does not extend the IApiBaseDefinition because a summary is not required for - * a package. - */ - isBeta: boolean; - summary: MarkupBasicElement[]; - remarks: MarkupStructuredElement[]; - deprecatedMessage?: MarkupBasicElement[]; -} - -/** - * A member of a class. - * @alpha - */ -export type ApiMember = IApiProperty | IApiMethod | IApiConstructor; - -/** - * @alpha - */ -export type ApiItem = IApiProperty | ApiMember | IApiFunction | IApiConstructor | - IApiClass | IApiEnum | IApiEnumMember | IApiInterface | IApiNamespace | IApiPackage; diff --git a/apps/api-extractor/src/api/ApiJsonConverter.ts b/apps/api-extractor/src/api/ApiJsonConverter.ts deleted file mode 100644 index 3f94d3f2ced..00000000000 --- a/apps/api-extractor/src/api/ApiJsonConverter.ts +++ /dev/null @@ -1,90 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -// See LICENSE in the project root for license information. - -import { AstItemKind } from '../ast/AstItem'; - -/** - * Supports the conversion between AstItems that are loaded from AstItem to JSON notation - * and vice versa. - */ -export class ApiJsonConverter { - private static _KIND_CONSTRUCTOR: string = 'constructor'; - private static _KIND_CLASS: string = 'class'; - private static _KIND_ENUM: string = 'enum'; - private static _KIND_ENUM_VALUE: string = 'enum value'; - private static _KIND_INTERFACE: string = 'interface'; - private static _KIND_FUNCTION: string = 'function'; - private static _KIND_PACKAGE: string = 'package'; - private static _KIND_PROPERTY: string = 'property'; - private static _KIND_METHOD: string = 'method'; - private static _KIND_NAMESPACE: string = 'namespace'; - private static _KIND_MODULEVARIABLE: string = 'module variable'; - - /** - * Uses the lowercase string that represents 'kind' in an API JSON file, and - * converts it to an AstItemKind enum value. - * There are two cases we do not include here, (Parameter and StructuredType), - * this is intential as we do not expect to be loading these kind of JSON object - * from file. - */ - public static convertJsonToKind(jsonItemKind: string): AstItemKind { - switch (jsonItemKind) { - case (this._KIND_CONSTRUCTOR): - return AstItemKind.Constructor; - case (this._KIND_CLASS): - return AstItemKind.Class; - case (this._KIND_ENUM): - return AstItemKind.Enum; - case (this._KIND_ENUM_VALUE): - return AstItemKind.EnumValue; - case (this._KIND_INTERFACE): - return AstItemKind.Interface; - case (this._KIND_FUNCTION): - return AstItemKind.Function; - case (this._KIND_PACKAGE): - return AstItemKind.Package; - case (this._KIND_PROPERTY): - return AstItemKind.Property; - case (this._KIND_METHOD): - return AstItemKind.Method; - case (this._KIND_NAMESPACE): - return AstItemKind.Namespace; - case (this._KIND_MODULEVARIABLE): - return AstItemKind.ModuleVariable; - default: - throw new Error('Unsupported kind when converting JSON item kind to API item kind.'); - } - } - - /** - * Converts the an AstItemKind into a lower-case string that is written to API JSON files. - */ - public static convertKindToJson(astItemKind: AstItemKind): string { - switch (astItemKind) { - case (AstItemKind.Constructor): - return this._KIND_CONSTRUCTOR; - case (AstItemKind.Class): - return this._KIND_CLASS; - case (AstItemKind.Enum): - return this._KIND_ENUM; - case (AstItemKind.EnumValue): - return this._KIND_ENUM_VALUE; - case (AstItemKind.Interface): - return this._KIND_INTERFACE; - case (AstItemKind.Function): - return this._KIND_FUNCTION; - case (AstItemKind.Package): - return this._KIND_PACKAGE; - case (AstItemKind.Property): - return this._KIND_PROPERTY; - case (AstItemKind.Method): - return this._KIND_METHOD; - case (AstItemKind.Namespace): - return this._KIND_NAMESPACE; - case (AstItemKind.ModuleVariable): - return this._KIND_MODULEVARIABLE; - default: - throw new Error('Unsupported API item kind when converting to string used in API JSON file.'); - } - } -} \ No newline at end of file diff --git a/apps/api-extractor/src/api/ApiJsonFile.ts b/apps/api-extractor/src/api/ApiJsonFile.ts deleted file mode 100644 index 6d805fa3b6f..00000000000 --- a/apps/api-extractor/src/api/ApiJsonFile.ts +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -// See LICENSE in the project root for license information. - -import * as path from 'path'; - -import { IApiPackage } from './ApiItem'; -import { JsonSchema, JsonFile, IJsonSchemaErrorInfo } from '@microsoft/node-core-library'; - -/** - * Support for loading the *.api.json file. - * - * @public - */ -export class ApiJsonFile { - /** - * The JSON Schema for API Extractor's *.api.json files (api-json.schema.json). - */ - public static jsonSchema: JsonSchema = JsonSchema.fromFile( - path.join(__dirname, './api-json.schema.json')); - - /** - * Loads an *.api.json data file, and validates that it conforms to the api-json.schema.json - * schema. - */ - public static loadFromFile(apiJsonFilePath: string): IApiPackage { - return JsonFile.loadAndValidateWithCallback(apiJsonFilePath, ApiJsonFile.jsonSchema, - (errorInfo: IJsonSchemaErrorInfo) => { - - const errorMessage: string - = path.basename(apiJsonFilePath) + ' does not conform to the expected schema.\n' - + '(Was it created by an incompatible release of API Extractor?)\n' - + errorInfo.details; - - throw new Error(errorMessage); - } - ); - } -} diff --git a/apps/api-extractor/src/extractor/Extractor.ts b/apps/api-extractor/src/api/Extractor.ts similarity index 88% rename from apps/api-extractor/src/extractor/Extractor.ts rename to apps/api-extractor/src/api/Extractor.ts index e5bab56235a..2ced10321f1 100644 --- a/apps/api-extractor/src/extractor/Extractor.ts +++ b/apps/api-extractor/src/api/Extractor.ts @@ -12,22 +12,25 @@ import { JsonSchema, Path, FileSystem, + IPackageJson, + NewlineKind, PackageJsonLookup } from '@microsoft/node-core-library'; import { IExtractorConfig, IExtractorProjectConfig, - IExtractorApiJsonFileConfig, - IExtractorDtsRollupConfig + IExtractorDtsRollupConfig, + IExtractorApiJsonFileConfig } from './IExtractorConfig'; -import { ExtractorContext } from '../ExtractorContext'; import { ILogger } from './ILogger'; -import { ApiJsonGenerator } from '../generators/ApiJsonGenerator'; -import { ApiFileGenerator } from '../generators/ApiFileGenerator'; -import { DtsRollupGenerator, DtsRollupKind } from '../generators/dtsRollup/DtsRollupGenerator'; +import { Collector } from '../collector/Collector'; +import { DtsRollupGenerator, DtsRollupKind } from '../generators/DtsRollupGenerator'; import { MonitoredLogger } from './MonitoredLogger'; -import { TypeScriptMessageFormatter } from '../utils/TypeScriptMessageFormatter'; -import { PackageMetadataManager } from '../generators/dtsRollup/PackageMetadataManager'; +import { TypeScriptMessageFormatter } from '../analyzer/TypeScriptMessageFormatter'; +import { ApiModelGenerator } from '../generators/ApiModelGenerator'; +import { ApiPackage } from './model/ApiPackage'; +import { ReviewFileGenerator } from '../generators/ReviewFileGenerator'; +import { PackageMetadataManager } from '../analyzer/PackageMetadataManager'; /** * Options for {@link Extractor.processProject}. @@ -102,10 +105,28 @@ export class Extractor { * The JSON Schema for API Extractor config file (api-extractor-config.schema.json). */ public static jsonSchema: JsonSchema = JsonSchema.fromFile( - path.join(__dirname, './api-extractor.schema.json')); + path.join(__dirname, '../schemas/api-extractor.schema.json')); + + /** + * Returns the version number of the API Extractor NPM package. + */ + public static get version(): string { + return Extractor._getPackageJson().version; + } + + /** + * Returns the package name of the API Extractor NPM package. + */ + public static get packageName(): string { + return Extractor._getPackageJson().name; + } + + private static _getPackageJson(): IPackageJson { + return PackageJsonLookup.loadOwnPackageJson(__dirname); + } private static _defaultConfig: Partial = JsonFile.load(path.join(__dirname, - './api-extractor-defaults.json')); + '../schemas/api-extractor-defaults.json')); private static _declarationFileExtensionRegExp: RegExp = /\.d\.ts$/i; @@ -122,13 +143,6 @@ export class Extractor { private readonly _monitoredLogger: MonitoredLogger; private readonly _absoluteRootFolder: string; - /** - * The NPM package version for the currently executing instance of the "\@microsoft/api-extractor" library. - */ - public static get version(): string { - return PackageJsonLookup.loadOwnPackageJson(__dirname).version; - } - /** * Given a list of absolute file paths, return a list containing only the declaration * files. Duplicates are also eliminated. @@ -379,7 +393,7 @@ export class Extractor { throw new Error('The entry point is not a declaration file: ' + projectConfig.entryPointSourceFile); } - const context: ExtractorContext = new ExtractorContext({ + const collector: Collector = new Collector({ program: this._program, entryPointFile: path.resolve(this._absoluteRootFolder, projectConfig.entryPointSourceFile), logger: this._monitoredLogger, @@ -387,27 +401,28 @@ export class Extractor { validationRules: this.actualConfig.validationRules }); - for (const externalJsonFileFolder of projectConfig.externalJsonFileFolders || []) { - context.loadExternalPackages(path.resolve(this._absoluteRootFolder, externalJsonFileFolder)); - } + collector.analyze(); - const packageBaseName: string = path.basename(context.packageName); + const modelBuilder: ApiModelGenerator = new ApiModelGenerator(collector); + const apiPackage: ApiPackage = modelBuilder.buildApiPackage(); + + const packageBaseName: string = path.basename(collector.package.name); const apiJsonFileConfig: IExtractorApiJsonFileConfig = this.actualConfig.apiJsonFile; if (apiJsonFileConfig.enabled) { - const outputFolder: string = path.resolve(this._absoluteRootFolder, - apiJsonFileConfig.outputFolder); + const outputFolder: string = path.resolve(this._absoluteRootFolder, apiJsonFileConfig.outputFolder); - const jsonGenerator: ApiJsonGenerator = new ApiJsonGenerator(); const apiJsonFilename: string = path.join(outputFolder, packageBaseName + '.api.json'); this._monitoredLogger.logVerbose('Writing: ' + apiJsonFilename); - jsonGenerator.writeJsonFile(apiJsonFilename, context); + apiPackage.saveToJsonFile(apiJsonFilename, { + newlineConversion: NewlineKind.CrLf, + ensureFolderExists: true + }); } if (this.actualConfig.apiReviewFile.enabled) { - const generator: ApiFileGenerator = new ApiFileGenerator(); const apiReviewFilename: string = packageBaseName + '.api.ts'; const actualApiReviewPath: string = path.resolve(this._absoluteRootFolder, @@ -418,7 +433,7 @@ export class Extractor { this.actualConfig.apiReviewFile.apiReviewFolder, apiReviewFilename); const expectedApiReviewShortPath: string = this._getShortFilePath(expectedApiReviewPath); - const actualApiReviewContent: string = generator.generateApiFileContent(context); + const actualApiReviewContent: string = ReviewFileGenerator.generateReviewFileContent(collector); // Write the actual file FileSystem.writeFile(actualApiReviewPath, actualApiReviewContent, { @@ -429,7 +444,7 @@ export class Extractor { if (FileSystem.exists(expectedApiReviewPath)) { const expectedApiReviewContent: string = FileSystem.readFile(expectedApiReviewPath); - if (!ApiFileGenerator.areEquivalentApiFileContents(actualApiReviewContent, expectedApiReviewContent)) { + if (!ReviewFileGenerator.areEquivalentApiFileContents(actualApiReviewContent, expectedApiReviewContent)) { if (!this._localBuild) { // For production, issue a warning that will break the CI build. this._monitoredLogger.logWarning('You have changed the public API signature for this project.' @@ -458,10 +473,10 @@ export class Extractor { } } - this._generateRollupDtsFiles(context); + this._generateRollupDtsFiles(collector); // Write the tsdoc-metadata.json file for this project - PackageMetadataManager.writeTsdocMetadataFile(context.packageFolder); + PackageMetadataManager.writeTsdocMetadataFile(collector.package.packageFolder); if (this._localBuild) { // For a local build, fail if there were errors (but ignore warnings) @@ -472,21 +487,23 @@ export class Extractor { } } - private _generateRollupDtsFiles(context: ExtractorContext): void { + private _generateRollupDtsFiles(collector: Collector): void { + const packageFolder: string = collector.package.packageFolder; + const dtsRollup: IExtractorDtsRollupConfig = this.actualConfig.dtsRollup!; if (dtsRollup.enabled) { let mainDtsRollupPath: string = dtsRollup.mainDtsRollupPath!; if (!mainDtsRollupPath) { // If the mainDtsRollupPath is not specified, then infer it from the package.json file - if (!context.packageJson.typings) { + if (!collector.package.packageJson.typings) { this._monitoredLogger.logError('Either the "mainDtsRollupPath" setting must be specified,' + ' or else the package.json file must contain a "typings" field.'); return; } // Resolve the "typings" field relative to package.json itself - const resolvedTypings: string = path.resolve(context.packageFolder, context.packageJson.typings); + const resolvedTypings: string = path.resolve(packageFolder, collector.package.packageJson.typings); if (dtsRollup.trimming) { if (!Path.isUnder(resolvedTypings, dtsRollup.publishFolderForInternal!)) { @@ -521,37 +538,35 @@ export class Extractor { } } - const dtsRollupGenerator: DtsRollupGenerator = new DtsRollupGenerator(context); - dtsRollupGenerator.analyze(); - if (dtsRollup.trimming) { - this._generateRollupDtsFile(dtsRollupGenerator, - path.resolve(context.packageFolder, dtsRollup.publishFolderForPublic!, mainDtsRollupPath), + this._generateRollupDtsFile(collector, + path.resolve(packageFolder, dtsRollup.publishFolderForPublic!, mainDtsRollupPath), DtsRollupKind.PublicRelease); - this._generateRollupDtsFile(dtsRollupGenerator, - path.resolve(context.packageFolder, dtsRollup.publishFolderForBeta!, mainDtsRollupPath), + this._generateRollupDtsFile(collector, + path.resolve(packageFolder, dtsRollup.publishFolderForBeta!, mainDtsRollupPath), DtsRollupKind.BetaRelease); - this._generateRollupDtsFile(dtsRollupGenerator, - path.resolve(context.packageFolder, dtsRollup.publishFolderForInternal!, mainDtsRollupPath), + this._generateRollupDtsFile(collector, + path.resolve(packageFolder, dtsRollup.publishFolderForInternal!, mainDtsRollupPath), DtsRollupKind.InternalRelease); } else { - this._generateRollupDtsFile(dtsRollupGenerator, - path.resolve(context.packageFolder, dtsRollup.publishFolder!, mainDtsRollupPath), + this._generateRollupDtsFile(collector, + path.resolve(packageFolder, dtsRollup.publishFolder!, mainDtsRollupPath), DtsRollupKind.InternalRelease); // (no trimming) } } } - private _generateRollupDtsFile(dtsRollupGenerator: DtsRollupGenerator, mainDtsRollupFullPath: string, + private _generateRollupDtsFile(collector: Collector, mainDtsRollupFullPath: string, dtsKind: DtsRollupKind): void { this._monitoredLogger.logVerbose(`Writing package typings: ${mainDtsRollupFullPath}`); - dtsRollupGenerator.writeTypingsFile(mainDtsRollupFullPath, dtsKind); + DtsRollupGenerator.writeTypingsFile(collector, mainDtsRollupFullPath, dtsKind); } + // Returns a simplified file path for use in error messages private _getShortFilePath(absolutePath: string): string { if (!path.isAbsolute(absolutePath)) { throw new Error('Expected absolute path: ' + absolutePath); diff --git a/apps/api-extractor/src/extractor/IExtractorConfig.ts b/apps/api-extractor/src/api/IExtractorConfig.ts similarity index 97% rename from apps/api-extractor/src/extractor/IExtractorConfig.ts rename to apps/api-extractor/src/api/IExtractorConfig.ts index deb87f1ea31..d443aabe392 100644 --- a/apps/api-extractor/src/extractor/IExtractorConfig.ts +++ b/apps/api-extractor/src/api/IExtractorConfig.ts @@ -63,13 +63,6 @@ export interface IExtractorProjectConfig { * need to parse implementation code. */ entryPointSourceFile: string; - - /** - * Indicates folders containing additional APJ JSON files (*.api.json) that will be - * consulted during the analysis. This is useful for providing annotations for - * external packages that were not built using API Extractor. - */ - externalJsonFileFolders?: string[]; } /** diff --git a/apps/api-extractor/src/extractor/ILogger.ts b/apps/api-extractor/src/api/ILogger.ts similarity index 100% rename from apps/api-extractor/src/extractor/ILogger.ts rename to apps/api-extractor/src/api/ILogger.ts diff --git a/apps/api-extractor/src/api/IndentedWriter.ts b/apps/api-extractor/src/api/IndentedWriter.ts new file mode 100644 index 00000000000..424caa99240 --- /dev/null +++ b/apps/api-extractor/src/api/IndentedWriter.ts @@ -0,0 +1,226 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { StringBuilder, IStringBuilder } from '@microsoft/node-core-library'; + +/** + * A utility for writing indented text. + * + * @remarks + * + * Note that the indentation is inserted at the last possible opportunity. + * For example, this code... + * + * ```ts + * writer.write('begin\n'); + * writer.increaseIndent(); + * writer.write('one\ntwo\n'); + * writer.decreaseIndent(); + * writer.increaseIndent(); + * writer.decreaseIndent(); + * writer.write('end'); + * ``` + * + * ...would produce this output: + * + * ``` + * begin + * one + * two + * end + * ``` + * + * @beta + */ +export class IndentedWriter { + /** + * The text characters used to create one level of indentation. + * Two spaces by default. + */ + public defaultIndentPrefix: string = ' '; + + private readonly _builder: IStringBuilder; + + private _latestChunk: string | undefined; + private _previousChunk: string | undefined; + private _atStartOfLine: boolean; + + private readonly _indentStack: string[]; + private _indentText: string; + + public constructor(builder?: IStringBuilder) { + this._builder = builder === undefined ? new StringBuilder() : builder; + + this._latestChunk = undefined; + this._previousChunk = undefined; + this._atStartOfLine = true; + + this._indentStack = []; + this._indentText = ''; + } + + /** + * Retrieves the output that was built so far. + */ + public getText(): string { + return this._builder.toString(); + } + + public toString(): string { + return this.getText(); + } + + /** + * Increases the indentation. Normally the indentation is two spaces, + * however an arbitrary prefix can optional be specified. (For example, + * the prefix could be "// " to indent and comment simultaneously.) + * Each call to IndentedWriter.increaseIndent() must be followed by a + * corresponding call to IndentedWriter.decreaseIndent(). + */ + public increaseIndent(indentPrefix?: string): void { + this._indentStack.push(indentPrefix !== undefined ? indentPrefix : this.defaultIndentPrefix); + this._updateIndentText(); + } + + /** + * Decreases the indentation, reverting the effect of the corresponding call + * to IndentedWriter.increaseIndent(). + */ + public decreaseIndent(): void { + this._indentStack.pop(); + this._updateIndentText(); + } + + /** + * A shorthand for ensuring that increaseIndent()/decreaseIndent() occur + * in pairs. + */ + public indentScope(scope: () => void, indentPrefix?: string): void { + this.increaseIndent(indentPrefix); + scope(); + this.decreaseIndent(); + } + + /** + * Adds a newline if the file pointer is not already at the start of the line (or start of the stream). + */ + public ensureNewLine(): void { + const lastCharacter: string = this.peekLastCharacter(); + if (lastCharacter !== '\n' && lastCharacter !== '') { + this._writeNewLine(); + } + } + + /** + * Adds up to two newlines to ensure that there is a blank line above the current line. + */ + public ensureSkippedLine(): void { + if (this.peekLastCharacter() !== '\n') { + this._writeNewLine(); + } + + const secondLastCharacter: string = this.peekSecondLastCharacter(); + if (secondLastCharacter !== '\n' && secondLastCharacter !== '') { + this._writeNewLine(); + } + } + + /** + * Returns the last character that was written, or an empty string if no characters have been written yet. + */ + public peekLastCharacter(): string { + if (this._latestChunk !== undefined) { + return this._latestChunk.substr(-1, 1); + } + return ''; + } + + /** + * Returns the second to last character that was written, or an empty string if less than one characters + * have been written yet. + */ + public peekSecondLastCharacter(): string { + if (this._latestChunk !== undefined) { + if (this._latestChunk.length > 1) { + return this._latestChunk.substr(-2, 1); + } + if (this._previousChunk !== undefined) { + return this._previousChunk.substr(-1, 1); + } + } + return ''; + } + + /** + * Writes some text to the internal string buffer, applying indentation according + * to the current indentation level. If the string contains multiple newlines, + * each line will be indented separately. + */ + public write(message: string): void { + if (message.length === 0) { + return; + } + + // If there are no newline characters, then append the string verbatim + if (!/[\r\n]/.test(message)) { + this._writeLinePart(message); + return; + } + + // Otherwise split the lines and write each one individually + let first: boolean = true; + for (const linePart of message.split('\n')) { + if (!first) { + this._writeNewLine(); + } else { + first = false; + } + if (linePart) { + this._writeLinePart(linePart.replace(/[\r]/g, '')); + } + } + } + + /** + * A shorthand for writing an optional message, followed by a newline. + * Indentation is applied following the semantics of IndentedWriter.write(). + */ + public writeLine(message: string = ''): void { + if (message.length > 0) { + this.write(message); + } + this._writeNewLine(); + } + + /** + * Writes a string that does not contain any newline characters. + */ + private _writeLinePart(message: string): void { + if (message.length > 0) { + if (this._atStartOfLine && this._indentText.length > 0) { + this._write(this._indentText); + } + this._write(message); + this._atStartOfLine = false; + } + } + + private _writeNewLine(): void { + if (this._atStartOfLine && this._indentText.length > 0) { + this._write(this._indentText); + } + + this._write('\n'); + this._atStartOfLine = true; + } + + private _write(s: string): void { + this._previousChunk = this._latestChunk; + this._latestChunk = s; + this._builder.append(s); + } + + private _updateIndentText(): void { + this._indentText = this._indentStack.join(''); + } +} diff --git a/apps/api-extractor/src/extractor/MonitoredLogger.ts b/apps/api-extractor/src/api/MonitoredLogger.ts similarity index 100% rename from apps/api-extractor/src/extractor/MonitoredLogger.ts rename to apps/api-extractor/src/api/MonitoredLogger.ts diff --git a/apps/api-extractor/src/api/api-json.schema.json b/apps/api-extractor/src/api/api-json.schema.json deleted file mode 100644 index 4181807edb6..00000000000 --- a/apps/api-extractor/src/api/api-json.schema.json +++ /dev/null @@ -1,1126 +0,0 @@ -{ - "title": "api-extractor documentation JSON file", - "description": "Describes the public API types and documentation for a TypeScript project", - - "definitions": { - //============================================================================================= - // Markup Elements - //============================================================================================= - - //--------------------------------------------------------------------------------------------- - "markupText": { - "description": "A block of plain text, possibly with simple formatting such as bold or italics", - "type": "object", - - "properties": { - "kind": { - "description": "The kind of markup element", - "type": "string", - "enum": [ "text" ] - }, - "text": { - "description": "The text content to display", - "type": "string" - }, - "bold": { - "description": "Whether the text should be formatted using boldface", - "type": "boolean" - }, - "italics": { - "description": "Whether the text should be formatted using italics", - "type": "boolean" - } - }, - "additionalProperties": false, - "required": [ "kind", "text" ] - }, - - //--------------------------------------------------------------------------------------------- - "markupHighlightedText": { - "description": "Source code shown in a fixed-width font, with syntax highlighting", - "type": "object", - - "properties": { - "kind": { - "description": "The kind of markup element", - "type": "string", - "enum": [ "code" ] - }, - "text": { - "description": "The text content to display", - "type": "string" - }, - "highlighter": { - "description": "The syntax highlighting to be applied to the text", - "type": "string", - "enum": [ "javascript", "plain" ] - } - }, - "additionalProperties": false, - "required": [ "kind", "text", "highlighter" ] - }, - - //--------------------------------------------------------------------------------------------- - "markupHtmlTag": { - "description": "An HTML tag such as \"

\" or \"\".", - "type": "object", - - "properties": { - "kind": { - "description": "The kind of markup element", - "type": "string", - "enum": [ "html-tag" ] - }, - "token": { - "description": "A string containing the HTML tag", - "type": "string" - } - }, - "additionalProperties": false, - "required": [ "kind", "token" ] - }, - - //--------------------------------------------------------------------------------------------- - "markupLinkTextElement": { - "description": "Represents markup that can be used as the link text for a hyperlink", - "type": "object", - "oneOf": [ - { "$ref": "#/definitions/markupText" }, - { "$ref": "#/definitions/markupHighlightedText" }, - { "$ref": "#/definitions/markupHtmlTag" } - ] - }, - - //--------------------------------------------------------------------------------------------- - "markupApiLink": { - "description": "A hyperlink to an API item", - "type": "object", - - "properties": { - "kind": { - "description": "The kind of markup element", - "type": "string", - "enum": [ "api-link" ] - }, - "elements": { - "description": "The link text", - "type": "array", - "items": { "$ref": "#/definitions/markupLinkTextElement" } - }, - "target": { - "description": "The API item that will serve as the hyperlink target", - "type": "object", - "properties": { - "scopeName": { - "description": "An optional NPM scope for the package name", - "type": "string" - }, - "packageName": { - "description": "The name of the NPM package being referenced (must be a nonempty string)", - "type": "string", - "minLength": 1 - }, - "exportName": { - "description": "The name of the package export", - "type": "string" - }, - "memberName": { - "description": "An optional name of a member of the package export", - "type": "string" - } - }, - "additionalProperties": false, - "required": [ "scopeName", "packageName", "exportName", "memberName" ] - } - }, - "additionalProperties": false, - "required": [ "kind", "elements", "target" ] - }, - - //--------------------------------------------------------------------------------------------- - "markupWebLink": { - "description": "A hyperlink to an internet URL", - "type": "object", - - "properties": { - "kind": { - "description": "The kind of markup element", - "type": "string", - "enum": [ "web-link" ] - }, - "elements": { - "description": "The link text", - "type": "array", - "items": { "$ref": "#/definitions/markupLinkTextElement" } - }, - "targetUrl": { - "description": "The internet URL that will serve as the hyperlink target", - "type": "string" - } - }, - "additionalProperties": false, - "required": [ "kind", "elements", "targetUrl" ] - }, - - //--------------------------------------------------------------------------------------------- - "markupParagraph": { - "description": "A paragraph separator, similar to the \"

\" tag in HTML", - "type": "object", - - "properties": { - "kind": { - "description": "The kind of markup element", - "type": "string", - "enum": [ "paragraph" ] - } - }, - "additionalProperties": false, - "required": [ "kind" ] - }, - - //--------------------------------------------------------------------------------------------- - "markupLineBreak": { - "description": "A line break, similar to the \"
\" tag in HTML.", - "type": "object", - - "properties": { - "kind": { - "description": "The kind of markup element", - "type": "string", - "enum": [ "break" ] - } - }, - "additionalProperties": false, - "required": [ "kind" ] - }, - - //--------------------------------------------------------------------------------------------- - "markupBasicElement": { - "description": "Represents basic text consisting of paragraphs and links (without structures such as headers or tables)", - "type": "object", - "oneOf": [ - { "$ref": "#/definitions/markupLinkTextElement" }, - { "$ref": "#/definitions/markupApiLink" }, - { "$ref": "#/definitions/markupWebLink" }, - { "$ref": "#/definitions/markupParagraph" }, - { "$ref": "#/definitions/markupLineBreak" } - ] - }, - - //--------------------------------------------------------------------------------------------- - "markupHeading1": { - "description": "A top-level heading", - "type": "object", - - "properties": { - "kind": { - "description": "The kind of markup element", - "type": "string", - "enum": [ "heading1" ] - }, - "text": { - "description": "The heading title", - "type": "string" - } - }, - "additionalProperties": false, - "required": [ "kind", "text" ] - }, - - //--------------------------------------------------------------------------------------------- - "markupHeading2": { - "description": "A sub heading", - "type": "object", - - "properties": { - "kind": { - "description": "The kind of markup element", - "type": "string", - "enum": [ "heading2" ] - }, - "text": { - "description": "The heading title", - "type": "string" - } - }, - "additionalProperties": false, - "required": [ "kind", "text" ] - }, - - //--------------------------------------------------------------------------------------------- - "markupCodeBox": { - "description": "A box containing source code with syntax highlighting", - "type": "object", - - "properties": { - "kind": { - "description": "The kind of markup element", - "type": "string", - "enum": [ "code-box" ] - }, - "text": { - "description": "The text content to display", - "type": "string" - }, - "highlighter": { - "description": "The syntax highlighting to be applied to the text", - "type": "string", - "enum": [ "javascript", "plain" ] - } - }, - "additionalProperties": false, - "required": [ "kind", "text", "highlighter" ] - }, - - //--------------------------------------------------------------------------------------------- - "markupNoteBox": { - "description": "A call-out box containing an informational note", - "type": "object", - - "properties": { - "kind": { - "description": "The kind of markup element", - "type": "string", - "enum": [ "note-box" ] - }, - "text": { - "description": "The text content to display", - "type": "string" - } - }, - "additionalProperties": false, - "required": [ "kind", "text" ] - }, - - //--------------------------------------------------------------------------------------------- - "markupTable": { - "description": "A table, with an optional header row", - "type": "object", - - "properties": { - "kind": { - "description": "The kind of markup element", - "type": "string", - "enum": [ "table" ] - }, - - "header": { "$ref": "#/definitions/markupTableRow" }, - - "rows": { - "description": "The table rows", - "type": "array", - "items": { "$ref": "#/definitions/markupTableRow" } - } - }, - "additionalProperties": false, - "required": [ "kind", "rows" ] - }, - - //--------------------------------------------------------------------------------------------- - "markupStructuredElement": { - "description": "Represents structured text that contains headings, tables, and boxes", - "type": "object", - "oneOf": [ - { "$ref": "#/definitions/markupBasicElement" }, - { "$ref": "#/definitions/markupHeading1" }, - { "$ref": "#/definitions/markupHeading2" }, - { "$ref": "#/definitions/markupCodeBox" }, - { "$ref": "#/definitions/markupNoteBox" }, - { "$ref": "#/definitions/markupTable" } - ] - }, - - //--------------------------------------------------------------------------------------------- - "markupTableCell": { - "description": "A cell inside an table row element", - "type": "object", - - "properties": { - "kind": { - "description": "The kind of markup element", - "type": "string", - "enum": [ "table-cell" ] - }, - - "elements": { - "description": "The cell text", - "type": "array", - "items": { "$ref": "#/definitions/markupBasicElement" } - } - }, - "additionalProperties": false, - "required": [ "kind", "elements" ] - }, - - //--------------------------------------------------------------------------------------------- - "markupTableRow": { - "description": "A row inside an table element", - "type": "object", - - "properties": { - "kind": { - "description": "The kind of markup element", - "type": "string", - "enum": [ "table-row" ] - }, - - "cells": { - "description": "The cells comprising this row, ordered from left to right", - "type": "array", - "items": { "$ref": "#/definitions/markupTableCell" } - } - }, - "additionalProperties": false, - "required": [ "kind", "elements" ] - }, - - //============================================================================================= - // API Items - //============================================================================================= - - //--------------------------------------------------------------------------------------------- - "constructorApiItem": { - "description": "A constructor of a TypeScript class", - "type": "object", - - "properties": { - "kind": { - "description": "The kind of documentation element", - "type": "string", - "enum": [ "constructor" ] - }, - "summary": { - "description": "A short description of the item", - "type": "array", - "items": { "$ref": "#/definitions/markupBasicElement" } - }, - "signature": { - "description": "A text summary of the method definition", - "type": "string" - }, - "parameters": { - "description": "The list of function parameters", - "type": "object", - "patternProperties": { - "^[a-zA-Z_]+[a-zA-Z_0-9]*$": { - "type": "object", - "properties": { - "name": { - "description": "The parameter name", - "type": "string" - }, - "description": { - "description": "A description of the parameter", - "type": "array", - "items": { "$ref": "#/definitions/markupBasicElement" } - }, - "isOptional": { - "description": "Whether the parameter is optional", - "type": "boolean" - }, - "isSpread": { - "description": "Whether the parameter has the '...' spread suffix", - "type": "boolean" - }, - "type": { - "description": "The data type of the parameter", - "type": "string" - } - } - } - }, - "additionalProperties": false - }, - - // Optional properties: - "remarks": { - "description": "Detailed additional notes regarding the item", - "type": "array", - "items": { "$ref": "#/definitions/markupStructuredElement" } - }, - "deprecatedMessage": { - "description": "If present, indicates that the item is deprecated and describes the recommended alternative", - "type": "array", - "items": { "$ref": "#/definitions/markupBasicElement" } - }, - "isSealed": { - "description": "If present, indicates that the item was marked as \"@sealed\" and must not be extended", - "type": "boolean" - }, - "isVirtual": { - "description": "If present, indicates that the item was marked as \"@virtual\" and may be extended", - "type": "boolean" - }, - "isOverride": { - "description": "If present, indicates that the item was marked as \"@override\" and is overriding a base definition", - "type": "boolean" - } - }, - "additionalProperties": false, - "required": ["summary"] - }, - - //--------------------------------------------------------------------------------------------- - - //--------------------------------------------------------------------------------------------- - "propertyApiItem": { - "description": "A property of a TypeScript class or interface", - "type": "object", - - "properties": { - "kind": { - "description": "The kind of API definition", - "type": "string", - "enum": [ "property" ] - }, - "signature": { - "description": "A text summary of the property definition", - "type": "string" - }, - "isOptional": { - "description": "For an interface member, whether it is optional", - "type": "boolean" - }, - "isReadOnly": { - "description": "Whether the property is read-only", - "type": "boolean" - }, - "isStatic": { - "description": "For a class member, whether it is static", - "type": "boolean" - }, - "isEventProperty": { - "description": "Whether the item was marked as \"@eventproperty\", which indicates an event object that event handlers can be attached to", - "type": "boolean" - }, - "type": { - "description": "The data type of this property", - "type": "string" - }, - "isBeta": { - "description": "Whether the API property is beta", - "type": "boolean" - }, - "summary": { - "description": "A short description of the item", - "type": "array", - "items": { "$ref": "#/definitions/markupBasicElement" } - }, - - // Optional properties: - "remarks": { - "description": "Detailed additional notes regarding the item", - "type": "array", - "items": { "$ref": "#/definitions/markupStructuredElement" } - }, - "deprecatedMessage": { - "description": "If present, indicates that the item is deprecated and describes the recommended alternative", - "type": "array", - "items": { "$ref": "#/definitions/markupBasicElement" } - }, - "isSealed": { - "description": "If present, indicates that the item was marked as \"@sealed\" and must not be extended", - "type": "boolean" - }, - "isVirtual": { - "description": "If present, indicates that the item was marked as \"@virtual\" and may be extended", - "type": "boolean" - }, - "isOverride": { - "description": "If present, indicates that the item was marked as \"@override\" and is overriding a base definition", - "type": "boolean" - } - }, - "additionalProperties": false, - "required": [ "kind", "isOptional", "isReadOnly", "isStatic", "isEventProperty", "type", "summary", "isBeta" ] - }, - - //--------------------------------------------------------------------------------------------- - "moduleVariableApiItem": { - "description": "A TypeScript BlockScopedVariable", - "type": "object", - - "properties": { - "kind": { - "description": "The kind of API definition", - "type": "string", - "enum": [ "module variable" ] - }, - "signature": { - "description": "A text summary of the variable definition", - "type": "string" - }, - "type": { - "description": "The data type of this module variable", - "type": "string" - }, - "value": { - "description": "The value of this module variable", - "type": "string" - }, - "summary": { - "description": "A short description of the item", - "type": "array", - "items": { "$ref": "#/definitions/markupBasicElement" } - }, - "isBeta": { - "description": "Whether the API method is beta", - "type": "boolean" - }, - - // Optional properties: - "remarks": { - "description": "Detailed additional notes regarding the item", - "type": "array", - "items": { "$ref": "#/definitions/markupStructuredElement" } - }, - "deprecatedMessage": { - "description": "If present, indicates that the item is deprecated and describes the recommended alternative", - "type": "array", - "items": { "$ref": "#/definitions/markupBasicElement" } - } - }, - "additionalProperties": false, - "required": [ "kind", "type", "value", "summary", "isBeta"] - }, - - //--------------------------------------------------------------------------------------------- - "methodApiItem": { - "description": "A member function of a TypeScript class or interface", - "type": "object", - - "properties": { - "kind": { - "description": "The kind of API definition", - "type": "string", - "enum": [ "method" ] - }, - "signature": { - "description": "A text summary of the method definition", - "type": "string" - }, - "accessModifier": { - "description": "Whether the function is public, private, or protected", - "type": "string", - "enum": [ "public", "private", "protected", ""] - }, - "isOptional": { - "description": "For an interface member, whether it is optional", - "type": "boolean" - }, - "isStatic": { - "description": "For a class member, whether it is static", - "type": "boolean" - }, - "isBeta": { - "description": "Whether the API method is beta", - "type": "boolean" - }, - "parameters": { - "description": "The list of function parameters", - "type": "object", - "patternProperties": { - "^[a-zA-Z_]+[a-zA-Z_0-9]*$": { - "type": "object", - "properties": { - "name": { - "description": "The parameter name", - "type": "string" - }, - "description": { - "description": "A description of the parameter", - "type": "array", - "items": { "$ref": "#/definitions/markupBasicElement" } - }, - "isOptional": { - "description": "Whether the parameter is optional", - "type": "boolean" - }, - "isSpread": { - "description": "Whether the parameter has the '...' spread suffix", - "type": "boolean" - }, - "type": { - "description": "The data type of the parameter", - "type": "string" - } - } - } - }, - "additionalProperties": false - }, - "returnValue": { - "description": "Information about the return value of this function", - "type": "object", - "properties": { - "type": { - "description": "The data type returned by the function", - "type": "string" - }, - "description": { - "description": "A description of the return value", - "type": "array", - "items": { "$ref": "#/definitions/markupBasicElement" } - }, - }, - "additionalProperties": false, - "required": [ "type", "description" ] - }, - "summary": { - "description": "A short description of the item", - "type": "array", - "items": { "$ref": "#/definitions/markupBasicElement" } - }, - - // Optional properties: - "remarks": { - "description": "Detailed additional notes regarding the item", - "type": "array", - "items": { "$ref": "#/definitions/markupStructuredElement" } - }, - "deprecatedMessage": { - "description": "If present, indicates that the item is deprecated and describes the recommended alternative", - "type": "array", - "items": { "$ref": "#/definitions/markupBasicElement" } - }, - "isSealed": { - "description": "If present, indicates that the item was marked as \"@sealed\" and must not be extended", - "type": "boolean" - }, - "isVirtual": { - "description": "If present, indicates that the item was marked as \"@virtual\" and may be extended", - "type": "boolean" - }, - "isOverride": { - "description": "If present, indicates that the item was marked as \"@override\" and is overriding a base definition", - "type": "boolean" - } - }, - "additionalProperties": false, - "required": [ "kind", "isOptional", "isStatic", "parameters", "returnValue", "summary", "isBeta" ] - }, - - //--------------------------------------------------------------------------------------------- - "functionApiItem": { - "description": "A Typescript function", - "type": "object", - - "properties": { - "kind": { - "description": "The kind of API definition", - "type": "string", - "enum": [ "function" ] - }, - "signature": { - "description": "A text summary of the function definition", - "type": "string" - }, - "parameters": { - "description": "The list of function parameters", - "type": "object", - "patternProperties": { - "^[a-zA-Z_]+[a-zA-Z_0-9]*$": { - "type": "object", - "properties": { - "name": { - "description": "The parameter name", - "type": "string" - }, - "description": { - "description": "A description of the parameter", - "type": "array", - "items": { "$ref": "#/definitions/markupBasicElement" } - }, - "isOptional": { - "description": "Whether the parameter is optional", - "type": "boolean" - }, - "isSpread": { - "description": "Whether the parameter has the '...' spread suffix", - "type": "boolean" - }, - "type": { - "description": "The data type of the parameter", - "type": "string" - } - } - } - }, - "additionalProperties": false - }, - "returnValue": { - "description": "Information about the return value of this function", - "type": "object", - "properties": { - "type": { - "description": "The data type returned by the function", - "type": "string" - }, - "description": { - "description": "A description of the return value", - "type": "array", - "items": { "$ref": "#/definitions/markupBasicElement" } - } - }, - "additionalProperties": false, - "required": [ "type", "description" ] - }, - "summary": { - "description": "A short description of the item", - "type": "array", - "items": { "$ref": "#/definitions/markupBasicElement" } - }, - "isBeta": { - "description": "Whether the API method is beta", - "type": "boolean" - }, - - // Optional properties: - "remarks": { - "description": "Detailed additional notes regarding the item", - "type": "array", - "items": { "$ref": "#/definitions/markupStructuredElement" } - }, - "deprecatedMessage": { - "description": "If present, indicates that the item is deprecated and describes the recommended alternative", - "type": "array", - "items": { "$ref": "#/definitions/markupBasicElement" } - } - }, - "additionalProperties": false, - "required": [ "kind", "parameters", "returnValue", "summary", "isBeta" ] - }, - - - //--------------------------------------------------------------------------------------------- - "classApiItem": { - "description": "A TypeScript class definition", - "type": "object", - - "properties": { - "kind": { - "description": "The kind of API definition", - "type": "string", - "enum": [ "class" ] - }, - "summary": { - "description": "A short description of the item", - "type": "array", - "items": { "$ref": "#/definitions/markupBasicElement" } - }, - "members": { - "type": "object", - "patternProperties": { - "^[a-zA-Z_]+[a-zA-Z_0-9]*$": { - "oneOf": [ - { "$ref": "#/definitions/propertyApiItem" }, - { "$ref": "#/definitions/constructorApiItem" }, - { "$ref": "#/definitions/methodApiItem" } - ] - } - }, - "additionalProperties": false - }, - - "implements": { - "description": "Interfaces implemented by this class", - "type": "string" - }, - "extends": { - "description": "The base class for this class", - "type": "string" - }, - "typeParameters": { - "description": "Generic type parameters for this class", - "type": "array", - "items": { - "type": "string" - } - }, - "isBeta": { - "description": "Whether the API class is beta", - "type": "boolean" - }, - - // Optional properties: - "remarks": { - "description": "Detailed additional notes regarding the item", - "type": "array", - "items": { "$ref": "#/definitions/markupStructuredElement" } - }, - "deprecatedMessage": { - "description": "If present, indicates that the item is deprecated and describes the recommended alternative", - "type": "array", - "items": { "$ref": "#/definitions/markupBasicElement" } - }, - "isSealed": { - "description": "If present, indicates that the item was marked as \"@sealed\" and must not be extended", - "type": "boolean" - } - }, - "additionalProperties": false, - "required": [ "kind", "summary", "isBeta" ] - }, - - //--------------------------------------------------------------------------------------------- - "enumApiItem": { - "description": "A TypeScript enum definition", - "type": "object", - - "properties": { - "kind": { - "description": "The kind of API definition", - "type": "string", - "enum": [ "enum" ] - }, - "summary": { - "description": "A short description of the item", - "type": "array", - "items": { "$ref": "#/definitions/markupBasicElement" } - }, - "isBeta": { - "description": "Whether the API enum is beta", - "type": "boolean" - }, - "values": { - "type": "object", - "patternProperties": { - "^[a-zA-Z_]+[a-zA-Z_0-9]*$": { - "$ref": "#/definitions/enumMemberApiItem" - } - }, - "additionalProperties": false - }, - - // Optional properties: - "remarks": { - "description": "Detailed additional notes regarding the item", - "type": "array", - "items": { "$ref": "#/definitions/markupStructuredElement" } - }, - "deprecatedMessage": { - "description": "If present, indicates that the item is deprecated and describes the recommended alternative", - "type": "array", - "items": { "$ref": "#/definitions/markupBasicElement" } - } - }, - "additionalProperties": false, - "required": [ "kind", "summary", "values", "isBeta" ] - }, - - //--------------------------------------------------------------------------------------------- - "enumMemberApiItem": { - "description": "A TypeScript enum member definition", - "type": "object", - - "properties": { - "kind": { - "description": "The kind of API definition", - "type": "string", - "enum": [ "enum value" ] - }, - "summary": { - "description": "A short description of the item", - "type": "array", - "items": { "$ref": "#/definitions/markupBasicElement" } - }, - "isBeta": { - "description": "Whether the API enum member is beta", - "type": "boolean" - }, - "value": { - "description": "The value of the enum member. This string may be a number, a string literal (including quotation marks), or an empty string (e.g. if the number is an automatically assigned increment)", - "type": "string" - }, - - // Optional properties: - "remarks": { - "description": "Detailed additional notes regarding the item", - "type": "array", - "items": { "$ref": "#/definitions/markupStructuredElement" } - }, - "deprecatedMessage": { - "description": "If present, indicates that the item is deprecated and describes the recommended alternative", - "type": "array", - "items": { "$ref": "#/definitions/markupBasicElement" } - } - }, - "additionalProperties": false, - "required": [ "kind", "summary", "isBeta" ] - }, - - //--------------------------------------------------------------------------------------------- - "interfaceApiItem": { - "description": "A TypeScript interface definition", - "type": "object", - - "properties": { - "kind": { - "description": "The kind of API definition", - "type": "string", - "enum": [ "interface" ] - }, - "summary": { - "description": "A short description of the item", - "type": "array", - "items": { "$ref": "#/definitions/markupBasicElement" } - }, - "members": { - "type": "object", - "patternProperties": { - "^[a-zA-Z_]+[a-zA-Z_0-9]*$": { - "oneOf": [ - { "$ref": "#/definitions/propertyApiItem" }, - { "$ref": "#/definitions/methodApiItem" } - ] - } - }, - "additionalProperties": false - }, - - "implements": { - "description": "Interfaces implemented by this interface", - "type": "string" - }, - "extends": { - "description": "The base interface for this interface", - "type": "string" - }, - "typeParameters": { - "description": "Generic type parameters for this interface", - "type": "array", - "items": { - "type": "string" - } - }, - "isBeta": { - "description": "Whether the API interface is beta", - "type": "boolean" - }, - - // Optional properties: - "remarks": { - "description": "Detailed additional notes regarding the item", - "type": "array", - "items": { "$ref": "#/definitions/markupStructuredElement" } - }, - "deprecatedMessage": { - "description": "If present, indicates that the item is deprecated and describes the recommended alternative", - "type": "array", - "items": { "$ref": "#/definitions/markupBasicElement" } - }, - "isSealed": { - "description": "If present, indicates that the item was marked as \"@sealed\" and must not be extended", - "type": "boolean" - } - }, - "additionalProperties": false, - "required": [ "kind", "summary", "isBeta" ] - }, - - //--------------------------------------------------------------------------------------------- - "namespaceApiItem": { - "description": "A TypeScript namespace definition", - "type": "object", - - "properties": { - "kind": { - "description": "The kind of API definition", - "type": "string", - "enum": [ "namespace" ] - }, - "summary": { - "description": "A short description of the item", - "type": "array", - "items": { "$ref": "#/definitions/markupBasicElement" } - }, - "exports": { - "type": "object", - "patternProperties": { - "^[a-zA-Z_]+[a-zA-Z_0-9]*$": { - "oneOf": [ - // Conservative mode - { "$ref": "#/definitions/moduleVariableApiItem" }, - - // Permissive mode - { "$ref": "#/definitions/classApiItem" }, - { "$ref": "#/definitions/interfaceApiItem" }, - { "$ref": "#/definitions/namespaceApiItem" }, - { "$ref": "#/definitions/enumApiItem" }, - { "$ref": "#/definitions/functionApiItem" } - ] - } - }, - "additionalProperties": false - }, - "isBeta": { - "description": "Whether the API interface is beta", - "type": "boolean" - }, - - // Optional properties: - "remarks": { - "description": "Detailed additional notes regarding the item", - "type": "array", - "items": { "$ref": "#/definitions/markupStructuredElement" } - }, - "deprecatedMessage": { - "description": "If present, indicates that the item is deprecated and describes the recommended alternative", - "type": "array", - "items": { "$ref": "#/definitions/markupBasicElement" } - } - }, - "additionalProperties": false, - "required": [ "kind", "exports", "summary", "isBeta" ] - } - }, - - //=============================================================================================== - // Package - //=============================================================================================== - "type": "object", - "properties": { - "$schema": { - "description": "Part of the JSON Schema standard, this optional keyword declares the URL of the schema that the file conforms to. Editors may download the schema and use it to perform syntax highlighting.", - "type": "string" - }, - "kind": { - "description": "The kind of API definition", - "type": "string", - "enum": [ "package" ] - }, - "name": { - "description": "The name of the NPM package, including the optional scope. Example: \"@microsoft/example-package\"", - "type": "string" - }, - "summary": { - "description": "A short description of the item", - "type": "array", - "items": { "$ref": "#/definitions/markupBasicElement" } - }, - "remarks": { - "description": "Detailed additional notes regarding the item", - "type": "array", - "items": { "$ref": "#/definitions/markupStructuredElement" } - }, - "exports": { - "description": "The exported definitions for this API package", - "type": "object", - - "patternProperties": { - "^[a-zA-Z_]+[a-zA-Z_0-9]*$": { - "oneOf": [ - { "$ref": "#/definitions/classApiItem" }, - { "$ref": "#/definitions/interfaceApiItem" }, - { "$ref": "#/definitions/namespaceApiItem" }, - { "$ref": "#/definitions/enumApiItem" }, - { "$ref": "#/definitions/functionApiItem" } - ] - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false, - "required": [ "kind", "name", "exports" ] -} diff --git a/apps/api-extractor/src/api/mixins/ApiDeclarationMixin.ts b/apps/api-extractor/src/api/mixins/ApiDeclarationMixin.ts new file mode 100644 index 00000000000..4bce221b8b3 --- /dev/null +++ b/apps/api-extractor/src/api/mixins/ApiDeclarationMixin.ts @@ -0,0 +1,156 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information.s + +import { ApiItem, IApiItemJson, IApiItemConstructor, IApiItemOptions } from '../model/ApiItem'; +import { ApiDocumentedItem } from '../model/ApiDocumentedItem'; +import { Excerpt, ExcerptToken, IExcerptTokenRange, IDeclarationExcerpt, ExcerptName } from './Excerpt'; + +/** @public */ +export interface IApiDeclarationMixinOptions extends IApiItemOptions { + declarationExcerpt: IDeclarationExcerpt; +} + +export interface IApiDeclarationMixinJson extends IApiItemJson, IDeclarationExcerpt { +} + +const _excerpt: unique symbol = Symbol('ApiDeclarationMixin._excerpt'); +const _excerptTokens: unique symbol = Symbol('ApiDeclarationMixin._excerptTokens'); +const _embeddedExcerptsByName: unique symbol = Symbol('ApiDeclarationMixin._embeddedExcerptsByName'); + +/** @public */ +// tslint:disable-next-line:interface-name +export interface ApiDeclarationMixin extends ApiItem { + readonly excerpt: Excerpt; + + readonly excerptTokens: ReadonlyArray; + + readonly embeddedExcerptsByName: ReadonlyMap; + + /** @override */ + serializeInto(jsonObject: Partial): void; + + getEmbeddedExcerpt(name: ExcerptName): Excerpt; + + /** + * If the API item has certain important modifier tags such as `@sealed`, `@virtual`, or `@override`, + * this prepends them as a doc comment above the excerpt. + */ + getExcerptWithModifiers(): string; +} + +/** @public */ +export function ApiDeclarationMixin(baseClass: TBaseClass): + TBaseClass & (new (...args: any[]) => ApiDeclarationMixin) { // tslint:disable-line:no-any + + abstract class MixedClass extends baseClass implements ApiDeclarationMixin { + public [_excerptTokens]: ExcerptToken[]; + public [_embeddedExcerptsByName]: Map; + public [_excerpt]: Excerpt; + + /** @override */ + public static onDeserializeInto(options: Partial, + jsonObject: IApiDeclarationMixinJson): void { + + baseClass.onDeserializeInto(options, jsonObject); + + const declarationExcerpt: IDeclarationExcerpt = { + excerptTokens: jsonObject.excerptTokens.map(x => new ExcerptToken(x.kind, x.text)), + embeddedExcerpts: { } + }; + + for (const key of Object.getOwnPropertyNames(jsonObject.embeddedExcerpts)) { + const range: IExcerptTokenRange = jsonObject.embeddedExcerpts[key]; + declarationExcerpt.embeddedExcerpts[key] = range; + } + + options.declarationExcerpt = declarationExcerpt; + } + + // tslint:disable-next-line:no-any + constructor(...args: any[]) { + super(...args); + + const options: IApiDeclarationMixinOptions = args[0]; + this[_excerptTokens] = [...options.declarationExcerpt.excerptTokens]; + + this[_embeddedExcerptsByName] = new Map(); + + for (const key of Object.getOwnPropertyNames(options.declarationExcerpt.embeddedExcerpts)) { + const excerptRange: IExcerptTokenRange = options.declarationExcerpt.embeddedExcerpts[key]; + this[_embeddedExcerptsByName].set(key as ExcerptName, new Excerpt(this[_excerptTokens], excerptRange)); + } + + // this.excerpt is a Excerpt that spans the entire list of tokens + this[_excerpt] = new Excerpt(this[_excerptTokens], + { startIndex: 0, endIndex: this[_excerptTokens].length }); + } + + public get excerpt(): Excerpt { + return this[_excerpt]; + } + + public get excerptTokens(): ReadonlyArray { + return this[_excerptTokens]; + } + + public get embeddedExcerptsByName(): ReadonlyMap { + return this[_embeddedExcerptsByName]; + } + + public getEmbeddedExcerpt(name: ExcerptName): Excerpt { + const excerpt: Excerpt | undefined = this.embeddedExcerptsByName.get(name); + if (excerpt === undefined) { + throw new Error(`The embedded excerpt "${name}" must be defined for ${this.kind} objects`); + } + return excerpt; + } + + public getExcerptWithModifiers(): string { + const excerpt: string = this.excerpt.text; + const modifierTags: string[] = []; + + if (excerpt.length > 0) { + if (this instanceof ApiDocumentedItem) { + if (this.tsdocComment) { + if (this.tsdocComment.modifierTagSet.isSealed()) { + modifierTags.push('@sealed'); + } + if (this.tsdocComment.modifierTagSet.isVirtual()) { + modifierTags.push('@virtual'); + } + if (this.tsdocComment.modifierTagSet.isOverride()) { + modifierTags.push('@override'); + } + } + if (modifierTags.length > 0) { + return '/** ' + modifierTags.join(' ') + ' */\n' + + excerpt; + } + } + } + + return excerpt; + } + + /** @override */ + public serializeInto(jsonObject: Partial): void { + super.serializeInto(jsonObject); + + jsonObject.excerptTokens = this.excerptTokens.map(x => ({ kind: x.kind, text: x.text })); + + jsonObject.embeddedExcerpts = { }; + for (const [key, value] of this.embeddedExcerptsByName) { + jsonObject.embeddedExcerpts[key] = value.tokenRange; + } + } + } + + return MixedClass; +} + +/** @public */ +export namespace ApiDeclarationMixin { + export function isBaseClassOf(apiItem: ApiItem): apiItem is ApiDeclarationMixin { + return apiItem.hasOwnProperty(_excerpt); + } +} diff --git a/apps/api-extractor/src/api/mixins/ApiFunctionLikeMixin.ts b/apps/api-extractor/src/api/mixins/ApiFunctionLikeMixin.ts new file mode 100644 index 00000000000..aa51d67ffc4 --- /dev/null +++ b/apps/api-extractor/src/api/mixins/ApiFunctionLikeMixin.ts @@ -0,0 +1,112 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information.s + +import { ApiItem, ApiItem_parent, IApiItemJson, IApiItemConstructor, IApiItemOptions } from '../model/ApiItem'; +import { ApiParameter } from '../model/ApiParameter'; + +/** @public */ +export interface IApiFunctionLikeMixinOptions extends IApiItemOptions { + overloadIndex: number; + parameters?: ApiParameter[]; +} + +export interface IApiFunctionLikeJson extends IApiItemJson { + overloadIndex: number; + parameters: IApiItemJson[]; +} + +const _overloadIndex: unique symbol = Symbol('ApiFunctionLikeMixin._overloadIndex'); +const _parameters: unique symbol = Symbol('ApiFunctionLikeMixin._parameters'); + +/** @public */ +// tslint:disable-next-line:interface-name +export interface ApiFunctionLikeMixin extends ApiItem { + readonly overloadIndex: number; + readonly parameters: ReadonlyArray; + addParameter(parameter: ApiParameter): void; + serializeInto(jsonObject: Partial): void; +} + +/** @public */ +export function ApiFunctionLikeMixin(baseClass: TBaseClass): + TBaseClass & (new (...args: any[]) => ApiFunctionLikeMixin) { // tslint:disable-line:no-any + + abstract class MixedClass extends baseClass implements ApiFunctionLikeMixin { + public readonly [_overloadIndex]: number; + public readonly [_parameters]: ApiParameter[]; + + /** @override */ + public static onDeserializeInto(options: Partial, + jsonObject: IApiFunctionLikeJson): void { + + baseClass.onDeserializeInto(options, jsonObject); + + options.overloadIndex = jsonObject.overloadIndex; + options.parameters = []; + + for (const parameterObject of jsonObject.parameters) { + options.parameters.push(ApiItem.deserialize(parameterObject) as ApiParameter); + } + } + + // tslint:disable-next-line:no-any + constructor(...args: any[]) { + super(...args); + + const options: IApiFunctionLikeMixinOptions = args[0]; + this[_overloadIndex] = options.overloadIndex; + + this[_parameters] = []; + + if (options.parameters) { + for (const parameter of options.parameters) { + this.addParameter(parameter); + } + } + } + + public get overloadIndex(): number { + return this[_overloadIndex]; + } + + public get parameters(): ReadonlyArray { + return this[_parameters]; + } + + public addParameter(parameter: ApiParameter): void { + const existingParent: ApiItem | undefined = parameter[ApiItem_parent]; + if (existingParent !== undefined) { + throw new Error(`This ApiParameter has already been added to another function: "${existingParent.name}"`); + } + + this[_parameters].push(parameter); + + parameter[ApiItem_parent] = this; + } + + /** @override */ + public serializeInto(jsonObject: Partial): void { + super.serializeInto(jsonObject); + + jsonObject.overloadIndex = this.overloadIndex; + + const parameterObjects: IApiItemJson[] = []; + for (const parameter of this.parameters) { + const parameterJsonObject: Partial = {}; + parameter.serializeInto(parameterJsonObject); + parameterObjects.push(parameterJsonObject as IApiItemJson); + } + + jsonObject.parameters = parameterObjects; + } + } + + return MixedClass; +} + +/** @public */ +export namespace ApiFunctionLikeMixin { + export function isBaseClassOf(apiItem: ApiItem): apiItem is ApiFunctionLikeMixin { + return apiItem.hasOwnProperty(_parameters); + } +} diff --git a/apps/api-extractor/src/api/mixins/ApiItemContainerMixin.ts b/apps/api-extractor/src/api/mixins/ApiItemContainerMixin.ts new file mode 100644 index 00000000000..eb314ebd35c --- /dev/null +++ b/apps/api-extractor/src/api/mixins/ApiItemContainerMixin.ts @@ -0,0 +1,159 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information.s + +import { ApiItem, ApiItem_parent, IApiItemJson, IApiItemOptions, IApiItemConstructor } from '../model/ApiItem'; + +/** @public */ +export interface IApiItemContainerMixinOptions extends IApiItemOptions { + members?: ApiItem[]; +} + +export interface IApiItemContainerJson extends IApiItemJson { + members: IApiItemJson[]; +} + +const _members: unique symbol = Symbol('ApiItemContainerMixin._members'); +const _membersSorted: unique symbol = Symbol('ApiItemContainerMixin._membersSorted'); +const _membersByCanonicalReference: unique symbol = Symbol('ApiItemContainerMixin._membersByCanonicalReference'); +const _membersByName: unique symbol = Symbol('ApiItemContainerMixin._membersByName'); + +/** @public */ +// tslint:disable-next-line:interface-name +export interface ApiItemContainerMixin extends ApiItem { + /** + * Returns the members of this container, sorted alphabetically. + */ + readonly members: ReadonlyArray; + + /** + * Adds a new member to the container. + * + * @remarks + * An ApiItem cannot be added to more than one container. + */ + addMember(member: ApiItem): void; + + tryGetMember(canonicalReference: string): ApiItem | undefined; + + /** + * Returns a list of members with the specified name. + */ + findMembersByName(name: string): ReadonlyArray; + + /** @override */ + serializeInto(jsonObject: Partial): void; +} + +/** @public */ +export function ApiItemContainerMixin(baseClass: TBaseClass): + TBaseClass & (new (...args: any[]) => ApiItemContainerMixin) { // tslint:disable-line:no-any + + abstract class MixedClass extends baseClass implements ApiItemContainerMixin { + public readonly [_members]: ApiItem[]; + public [_membersSorted]: boolean; + public [_membersByCanonicalReference]: Map; + public [_membersByName]: Map | undefined; + + /** @override */ + public static onDeserializeInto(options: Partial, + jsonObject: IApiItemContainerJson): void { + + baseClass.onDeserializeInto(options, jsonObject); + + options.members = []; + for (const memberObject of jsonObject.members) { + options.members.push(ApiItem.deserialize(memberObject)); + } + } + + // tslint:disable-next-line:no-any + constructor(...args: any[]) { + super(...args); + const options: IApiItemContainerMixinOptions = args[0] as IApiItemContainerMixinOptions; + + this[_members] = []; + this[_membersByCanonicalReference] = new Map(); + + if (options.members) { + for (const member of options.members) { + this.addMember(member); + } + } + } + + public get members(): ReadonlyArray { + if (!this[_membersSorted]) { + this[_members].sort((x, y) => x.getSortKey().localeCompare(y.getSortKey())); + this[_membersSorted] = true; + } + + return this[_members]; + } + + public addMember(member: ApiItem): void { + if (this[_membersByCanonicalReference].has(member.canonicalReference)) { + throw new Error('Another member has already been added with the same name and canonicalReference'); + } + + const existingParent: ApiItem | undefined = member[ApiItem_parent]; + if (existingParent !== undefined) { + throw new Error(`This item has already been added to another container: "${existingParent.name}"`); + } + + this[_members].push(member); + this[_membersByName] = undefined; // invalidate the lookup + this[_membersSorted] = false; + this[_membersByCanonicalReference].set(member.canonicalReference, member); + + member[ApiItem_parent] = this; + } + + public tryGetMember(canonicalReference: string): ApiItem | undefined { + return this[_membersByCanonicalReference].get(canonicalReference); + } + + public findMembersByName(name: string): ReadonlyArray { + // Build the lookup on demand + if (this[_membersByName] === undefined) { + const map: Map = new Map(); + + for (const member of this[_members]) { + let list: ApiItem[] | undefined = map.get(member.name); + if (list === undefined) { + list = []; + map.set(member.name, list); + } + list.push(member); + } + + this[_membersByName] = map; + } + + return this[_membersByName]!.get(name) || []; + } + + /** @override */ + public serializeInto(jsonObject: Partial): void { + super.serializeInto(jsonObject); + + const memberObjects: IApiItemJson[] = []; + + for (const member of this.members) { + const memberJsonObject: Partial = {}; + member.serializeInto(memberJsonObject); + memberObjects.push(memberJsonObject as IApiItemJson); + } + + jsonObject.members = memberObjects; + } + } + + return MixedClass; +} + +/** @public */ +export namespace ApiItemContainerMixin { + export function isBaseClassOf(apiItem: ApiItem): apiItem is ApiItemContainerMixin { + return apiItem.hasOwnProperty(_members); + } +} diff --git a/apps/api-extractor/src/api/mixins/ApiReleaseTagMixin.ts b/apps/api-extractor/src/api/mixins/ApiReleaseTagMixin.ts new file mode 100644 index 00000000000..0c582322b2c --- /dev/null +++ b/apps/api-extractor/src/api/mixins/ApiReleaseTagMixin.ts @@ -0,0 +1,76 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information.s + +import { ApiItem, IApiItemJson, IApiItemConstructor, IApiItemOptions } from '../model/ApiItem'; +import { ReleaseTag } from '../../aedoc/ReleaseTag'; + +/** @public */ +export interface IApiReleaseTagMixinOptions extends IApiItemOptions { + releaseTag: ReleaseTag; +} + +export interface IApiReleaseTagMixinJson extends IApiItemJson { + releaseTag: string; +} + +const _releaseTag: unique symbol = Symbol('ApiReleaseTagMixin._releaseTag'); + +/** @public */ +// tslint:disable-next-line:interface-name +export interface ApiReleaseTagMixin extends ApiItem { + readonly releaseTag: ReleaseTag; + + /** @override */ + serializeInto(jsonObject: Partial): void; +} + +/** @public */ +export function ApiReleaseTagMixin(baseClass: TBaseClass): + TBaseClass & (new (...args: any[]) => ApiReleaseTagMixin) { // tslint:disable-line:no-any + + abstract class MixedClass extends baseClass implements ApiReleaseTagMixin { + public [_releaseTag]: ReleaseTag; + + /** @override */ + public static onDeserializeInto(options: Partial, + jsonObject: IApiReleaseTagMixinJson): void { + + baseClass.onDeserializeInto(options, jsonObject); + + const deserializedReleaseTag: ReleaseTag | undefined = ReleaseTag[jsonObject.releaseTag]; + if (deserializedReleaseTag === undefined) { + throw new Error(`Failed to deserialize release tag for ${JSON.stringify(jsonObject.name)}`); + } + + options.releaseTag = deserializedReleaseTag; + } + + // tslint:disable-next-line:no-any + constructor(...args: any[]) { + super(...args); + + const options: IApiReleaseTagMixinOptions = args[0]; + this[_releaseTag] = options.releaseTag; + } + + public get releaseTag(): ReleaseTag { + return this[_releaseTag]; + } + + /** @override */ + public serializeInto(jsonObject: Partial): void { + super.serializeInto(jsonObject); + + jsonObject.releaseTag = ReleaseTag[this.releaseTag]; + } + } + + return MixedClass; +} + +/** @public */ +export namespace ApiReleaseTagMixin { + export function isBaseClassOf(apiItem: ApiItem): apiItem is ApiReleaseTagMixin { + return apiItem.hasOwnProperty(_releaseTag); + } +} diff --git a/apps/api-extractor/src/api/mixins/ApiStaticMixin.ts b/apps/api-extractor/src/api/mixins/ApiStaticMixin.ts new file mode 100644 index 00000000000..a77352373e3 --- /dev/null +++ b/apps/api-extractor/src/api/mixins/ApiStaticMixin.ts @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information.s + +import { ApiItem, IApiItemJson, IApiItemConstructor, IApiItemOptions } from '../model/ApiItem'; + +/** @public */ +export interface IApiStaticMixinOptions extends IApiItemOptions { + isStatic: boolean; +} + +export interface IApiStaticMixinJson extends IApiItemJson { + isStatic: boolean; +} + +const _isStatic: unique symbol = Symbol('ApiStaticMixin._isStatic'); + +/** @public */ +// tslint:disable-next-line:interface-name +export interface ApiStaticMixin extends ApiItem { + readonly isStatic: boolean; + + /** @override */ + serializeInto(jsonObject: Partial): void; +} + +/** @public */ +export function ApiStaticMixin(baseClass: TBaseClass): + TBaseClass & (new (...args: any[]) => ApiStaticMixin) { // tslint:disable-line:no-any + + abstract class MixedClass extends baseClass implements ApiStaticMixin { + public [_isStatic]: boolean; + + /** @override */ + public static onDeserializeInto(options: Partial, jsonObject: IApiStaticMixinJson): void { + baseClass.onDeserializeInto(options, jsonObject); + + options.isStatic = jsonObject.isStatic; + } + + // tslint:disable-next-line:no-any + constructor(...args: any[]) { + super(...args); + + const options: IApiStaticMixinOptions = args[0]; + this[_isStatic] = options.isStatic; + } + + public get isStatic(): boolean { + return this[_isStatic]; + } + + /** @override */ + public serializeInto(jsonObject: Partial): void { + super.serializeInto(jsonObject); + + jsonObject.isStatic = this.isStatic; + } + } + + return MixedClass; +} + +/** @public */ +export namespace ApiStaticMixin { + export function isBaseClassOf(apiItem: ApiItem): apiItem is ApiStaticMixin { + return apiItem.hasOwnProperty(_isStatic); + } +} diff --git a/apps/api-extractor/src/api/mixins/Excerpt.ts b/apps/api-extractor/src/api/mixins/Excerpt.ts new file mode 100644 index 00000000000..a354261f44b --- /dev/null +++ b/apps/api-extractor/src/api/mixins/Excerpt.ts @@ -0,0 +1,76 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +/** @public */ +export const enum ExcerptTokenKind { + Content = 'Content', + + // Soon we will support hyperlinks to other API declarations + Reference = 'Reference' +} + +/** + * Names used in {@link ApiDeclarationMixin.embeddedExcerptsByName}. + * + * @remarks + * These strings use camelCase because they are property names for IDeclarationExcerpt.embeddedExcerpts. + * + * @public + */ +export type ExcerptName = 'returnType' | 'parameterType' | 'propertyType' | 'initializer'; + +/** @public */ +export interface IExcerptTokenRange { + readonly startIndex: number; + readonly endIndex: number; +} + +/** @public */ +export interface IExcerptToken { + readonly kind: ExcerptTokenKind; + text: string; +} + +/** + * @remarks + * This object must be completely JSON serializable, since it is included in IApiDeclarationMixinJson + * @public + */ +export interface IDeclarationExcerpt { + excerptTokens: IExcerptToken[]; + + embeddedExcerpts: { [name in ExcerptName]?: IExcerptTokenRange }; +} + +/** @public */ +export class ExcerptToken { + public readonly kind: ExcerptTokenKind; + public readonly text: string; + + public constructor(kind: ExcerptTokenKind, text: string) { + this.kind = kind; + this.text = text; + } +} + +/** @public */ +export class Excerpt { + public readonly tokenRange: IExcerptTokenRange; + + public readonly tokens: ReadonlyArray; + + private _text: string | undefined; + + public constructor(tokens: ReadonlyArray, tokenRange: IExcerptTokenRange) { + this.tokens = tokens; + this.tokenRange = tokenRange; + } + + public get text(): string { + if (this._text === undefined) { + this._text = this.tokens.slice(this.tokenRange.startIndex, this.tokenRange.endIndex) + .map(x => x.text).join(''); + } + return this._text; + } +} diff --git a/apps/api-extractor/src/api/mixins/Mixin.ts b/apps/api-extractor/src/api/mixins/Mixin.ts new file mode 100644 index 00000000000..c8d9fd383d9 --- /dev/null +++ b/apps/api-extractor/src/api/mixins/Mixin.ts @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +// tslint:disable-next-line:no-any +export type Constructor = new (...args: any[]) => T; + +export type PropertiesOf = { [K in keyof T]: T[K] }; + +export type Mixin = TBase & Constructor; diff --git a/apps/api-extractor/src/api/model/ApiClass.ts b/apps/api-extractor/src/api/model/ApiClass.ts new file mode 100644 index 00000000000..c6f8fd88afa --- /dev/null +++ b/apps/api-extractor/src/api/model/ApiClass.ts @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { ApiItemKind } from './ApiItem'; +import { ApiDeclarationMixin, IApiDeclarationMixinOptions } from '../mixins/ApiDeclarationMixin'; +import { ApiItemContainerMixin, IApiItemContainerMixinOptions } from '../mixins/ApiItemContainerMixin'; +import { ApiDocumentedItem, IApiDocumentedItemOptions } from './ApiDocumentedItem'; +import { ApiReleaseTagMixin, IApiReleaseTagMixinOptions } from '../mixins/ApiReleaseTagMixin'; + +/** @public */ +export interface IApiClassOptions extends + IApiDeclarationMixinOptions, + IApiItemContainerMixinOptions, + IApiReleaseTagMixinOptions, + IApiDocumentedItemOptions { +} + +/** @public */ +export class ApiClass extends ApiDeclarationMixin(ApiItemContainerMixin(ApiReleaseTagMixin(ApiDocumentedItem))) { + public static getCanonicalReference(name: string): string { + return `(${name}:class)`; + } + + public constructor(options: IApiClassOptions) { + super(options); + } + + /** @override */ + public get kind(): ApiItemKind { + return ApiItemKind.Class; + } + + /** @override */ + public get canonicalReference(): string { + return ApiClass.getCanonicalReference(this.name); + } +} diff --git a/apps/api-extractor/src/api/model/ApiDocumentedItem.ts b/apps/api-extractor/src/api/model/ApiDocumentedItem.ts new file mode 100644 index 00000000000..04a7d18ce7d --- /dev/null +++ b/apps/api-extractor/src/api/model/ApiDocumentedItem.ts @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import * as tsdoc from '@microsoft/tsdoc'; +import { ApiItem, IApiItemOptions, IApiItemJson } from './ApiItem'; +import { AedocDefinitions } from '../../aedoc/AedocDefinitions'; + +/** @public */ +export interface IApiDocumentedItemOptions extends IApiItemOptions { + docComment: tsdoc.DocComment | undefined; +} + +export interface IApiDocumentedItemJson extends IApiItemJson { + docComment: string; +} + +/** @public */ +export class ApiDocumentedItem extends ApiItem { + private _tsdocComment: tsdoc.DocComment | undefined; + + /** @override */ + public static onDeserializeInto(options: Partial, + jsonObject: IApiItemJson): void { + + ApiItem.onDeserializeInto(options, jsonObject); + + const documentedJson: IApiDocumentedItemJson = jsonObject as IApiDocumentedItemJson; + + if (documentedJson.docComment) { + const tsdocParser: tsdoc.TSDocParser = new tsdoc.TSDocParser(AedocDefinitions.tsdocConfiguration); + const parserContext: tsdoc.ParserContext = tsdocParser.parseString(documentedJson.docComment); + + // TODO: Warn about parser errors + + options.docComment = parserContext.docComment; + } + } + + public constructor(options: IApiDocumentedItemOptions) { + super(options); + this._tsdocComment = options.docComment; + } + + public get tsdocComment(): tsdoc.DocComment | undefined { + return this._tsdocComment; + } + + /** @override */ + public serializeInto(jsonObject: Partial): void { + super.serializeInto(jsonObject); + if (this.tsdocComment !== undefined) { + jsonObject.docComment = this.tsdocComment.emitAsTsdoc(); + } else { + jsonObject.docComment = ''; + } + } +} diff --git a/apps/api-extractor/src/api/model/ApiEntryPoint.ts b/apps/api-extractor/src/api/model/ApiEntryPoint.ts new file mode 100644 index 00000000000..dd97efd251b --- /dev/null +++ b/apps/api-extractor/src/api/model/ApiEntryPoint.ts @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { ApiItem, ApiItemKind } from './ApiItem'; +import { ApiItemContainerMixin, IApiItemContainerMixinOptions } from '../mixins/ApiItemContainerMixin'; + +/** @public */ +export interface IApiEntryPointOptions extends IApiItemContainerMixinOptions { +} + +/** @public */ +export class ApiEntryPoint extends ApiItemContainerMixin(ApiItem) { + public constructor(options: IApiEntryPointOptions) { + super(options); + } + + /** @override */ + public get kind(): ApiItemKind { + return ApiItemKind.EntryPoint; + } + + /** @override */ + public get canonicalReference(): string { + return this.name; + } +} diff --git a/apps/api-extractor/src/api/model/ApiEnum.ts b/apps/api-extractor/src/api/model/ApiEnum.ts new file mode 100644 index 00000000000..510e19de4fe --- /dev/null +++ b/apps/api-extractor/src/api/model/ApiEnum.ts @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { ApiItemKind } from './ApiItem'; +import { ApiDeclarationMixin, IApiDeclarationMixinOptions } from '../mixins/ApiDeclarationMixin'; +import { ApiDocumentedItem, IApiDocumentedItemOptions } from './ApiDocumentedItem'; +import { ApiReleaseTagMixin, IApiReleaseTagMixinOptions } from '../mixins/ApiReleaseTagMixin'; +import { ApiItemContainerMixin, IApiItemContainerMixinOptions } from '../mixins/ApiItemContainerMixin'; +import { ApiEnumMember } from './ApiEnumMember'; + +/** @public */ +export interface IApiEnumOptions extends + IApiDeclarationMixinOptions, + IApiItemContainerMixinOptions, + IApiReleaseTagMixinOptions, + IApiDocumentedItemOptions { +} + +/** @public */ +export class ApiEnum extends ApiDeclarationMixin(ApiItemContainerMixin(ApiReleaseTagMixin(ApiDocumentedItem))) { + + public static getCanonicalReference(name: string): string { + return `(${name}:enum)`; + } + + public constructor(options: IApiEnumOptions) { + super(options); + } + + /** @override */ + public get kind(): ApiItemKind { + return ApiItemKind.Enum; + } + + /** @override */ + public get members(): ReadonlyArray { + return super.members as ReadonlyArray; + } + + /** @override */ + public get canonicalReference(): string { + return ApiEnum.getCanonicalReference(this.name); + } + + /** @override */ + public addMember(member: ApiEnumMember): void { + if (member.kind !== ApiItemKind.EnumMember) { + throw new Error('Only ApiEnumMember objects can be added to an ApiEnum'); + } + super.addMember(member); + } +} diff --git a/apps/api-extractor/src/api/model/ApiEnumMember.ts b/apps/api-extractor/src/api/model/ApiEnumMember.ts new file mode 100644 index 00000000000..9a4ae7cc5be --- /dev/null +++ b/apps/api-extractor/src/api/model/ApiEnumMember.ts @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { ApiItemKind } from './ApiItem'; +import { ApiDeclarationMixin, IApiDeclarationMixinOptions } from '../mixins/ApiDeclarationMixin'; +import { ApiDocumentedItem, IApiDocumentedItemOptions } from './ApiDocumentedItem'; +import { ApiReleaseTagMixin, IApiReleaseTagMixinOptions } from '../mixins/ApiReleaseTagMixin'; +import { Excerpt } from '../mixins/Excerpt'; + +/** @public */ +export interface IApiEnumMemberOptions extends + IApiDeclarationMixinOptions, + IApiReleaseTagMixinOptions, + IApiDocumentedItemOptions { +} + +/** @public */ +export class ApiEnumMember extends ApiDeclarationMixin(ApiReleaseTagMixin(ApiDocumentedItem)) { + public readonly initializerExcerpt: Excerpt; + + public static getCanonicalReference(name: string): string { + return name; + } + + public constructor(options: IApiEnumMemberOptions) { + super(options); + + this.initializerExcerpt = this.getEmbeddedExcerpt('initializer'); + } + + /** @override */ + public get kind(): ApiItemKind { + return ApiItemKind.EnumMember; + } + + /** @override */ + public get canonicalReference(): string { + return ApiEnumMember.getCanonicalReference(this.name); + } +} diff --git a/apps/api-extractor/src/api/model/ApiInterface.ts b/apps/api-extractor/src/api/model/ApiInterface.ts new file mode 100644 index 00000000000..28a15145ea8 --- /dev/null +++ b/apps/api-extractor/src/api/model/ApiInterface.ts @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { ApiItemKind } from './ApiItem'; +import { ApiItemContainerMixin, IApiItemContainerMixinOptions } from '../mixins/ApiItemContainerMixin'; +import { ApiDeclarationMixin, IApiDeclarationMixinOptions } from '../mixins/ApiDeclarationMixin'; +import { ApiDocumentedItem, IApiDocumentedItemOptions } from './ApiDocumentedItem'; +import { IApiReleaseTagMixinOptions, ApiReleaseTagMixin } from '../mixins/ApiReleaseTagMixin'; + +/** @public */ +export interface IApiInterfaceOptions extends + IApiDeclarationMixinOptions, + IApiItemContainerMixinOptions, + IApiReleaseTagMixinOptions, + IApiDocumentedItemOptions { +} + +/** @public */ +export class ApiInterface extends ApiDeclarationMixin(ApiItemContainerMixin(ApiReleaseTagMixin(ApiDocumentedItem))) { + public static getCanonicalReference(name: string): string { + return `(${name}:interface)`; + } + + public constructor(options: IApiInterfaceOptions) { + super(options); + } + + /** @override */ + public get kind(): ApiItemKind { + return ApiItemKind.Interface; + } + + /** @override */ + public get canonicalReference(): string { + return ApiInterface.getCanonicalReference(this.name); + } +} diff --git a/apps/api-extractor/src/api/model/ApiItem.ts b/apps/api-extractor/src/api/model/ApiItem.ts new file mode 100644 index 00000000000..05612ec56c7 --- /dev/null +++ b/apps/api-extractor/src/api/model/ApiItem.ts @@ -0,0 +1,167 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { Constructor, PropertiesOf } from '../mixins/Mixin'; + +/** @public */ +export const enum ApiItemKind { + Class = 'Class', + EntryPoint = 'EntryPoint', + Enum = 'Enum', + EnumMember = 'EnumMember', + Interface = 'Interface', + Method = 'Method', + MethodSignature = 'MethodSignature', + Model = 'Model', + Namespace = 'Namespace', + Package = 'Package', + Parameter = 'Parameter', + Property = 'Property', + PropertySignature = 'PropertySignature', + None = 'None' +} + +/** @public */ +export interface IApiItemOptions { + name: string; +} + +export interface IApiItemJson { + name: string; + kind: ApiItemKind; + canonicalReference: string; +} + +/** + * PRIVATE + * Allows ApiItemContainerMixin to assign the parent. + */ +// tslint:disable-next-line:variable-name +export const ApiItem_parent: unique symbol = Symbol('ApiItem._parent'); + +/** @public */ +export class ApiItem { + public [ApiItem_parent]: ApiItem | undefined; + + private readonly _name: string; + + public static deserialize(jsonObject: IApiItemJson): ApiItem { + // tslint:disable-next-line:no-use-before-declare + return Deserializer.deserialize(jsonObject); + } + + /** @virtual */ + public static onDeserializeInto(options: Partial, jsonObject: IApiItemJson): void { + options.name = jsonObject.name; + } + + public constructor(options: IApiItemOptions) { + this._name = options.name; + } + + /** @virtual */ + public serializeInto(jsonObject: Partial): void { + jsonObject.kind = this.kind; + jsonObject.name = this.name; + jsonObject.canonicalReference = this.canonicalReference; + } + + /** @virtual */ + public get kind(): ApiItemKind { + throw new Error('ApiItem.kind was not implemented by the child class'); + } + + public get name(): string { + return this._name; + } + + /** @virtual */ + public get canonicalReference(): string { + throw new Error('ApiItem.canonicalReference was not implemented by the child class'); + } + + /** + * If this item was added to a ApiItemContainerMixin item, then this returns the container item. + * If this is an ApiParameter that was added to a method or function, then this returns the function item. + * Otherwise, it returns undefined. + * @virtual + */ + public get parent(): ApiItem | undefined { + return this[ApiItem_parent]; + } + + /** + * This property supports a visitor pattern for walking the tree. + * For items with ApiItemContainerMixin, it returns the contained items. + * Otherwise it returns an empty array. + * @virtual + */ + public get members(): ReadonlyArray { + return []; + } + + /** + * Returns the chain of ancestors, starting from the root of the tree, and ending with the this item. + */ + public getHierarchy(): ReadonlyArray { + const hierarchy: ApiItem[] = []; + for (let current: ApiItem | undefined = this; current !== undefined; current = current.parent) { + hierarchy.push(current); + } + hierarchy.reverse(); + return hierarchy; + } + + /** + * This returns a scoped name such as `"Namespace1.Namespace2.MyClass.myMember()"`. It does not include the + * package name or entry point. + * + * @remarks + * If called on an ApiEntrypoint, ApiPackage, or ApiModel item, the result is an empty string. + */ + public getScopedNameWithinPackage(): string { + const reversedParts: string[] = []; + + for (let current: ApiItem | undefined = this; current !== undefined; current = current.parent) { + if (current.kind === ApiItemKind.Model + || current.kind === ApiItemKind.Package + || current.kind === ApiItemKind.EntryPoint) { + break; + } + if (reversedParts.length !== 0) { + reversedParts.push('.'); + } else if (ApiFunctionLikeMixin.isBaseClassOf(current)) { // tslint:disable-line:no-use-before-declare + reversedParts.push('()'); + } + reversedParts.push(current.name); + } + + return reversedParts.reverse().join(''); + } + + /** + * If this item is an ApiPackage or has an ApiPackage as one of its parents, then that object is returned. + * Otherwise undefined is returned. + */ + public getAssociatedPackage(): ApiPackage | undefined { + for (let current: ApiItem | undefined = this; current !== undefined; current = current.parent) { + if (current.kind === ApiItemKind.Package) { + return current as ApiPackage; + } + } + return undefined; + } + + /** @virtual */ + public getSortKey(): string { + return this.canonicalReference; + } +} + +// For mixins +export interface IApiItemConstructor extends Constructor, PropertiesOf { } + +// Circular import +import { Deserializer } from './Deserializer'; +import { ApiPackage } from './ApiPackage'; +import { ApiFunctionLikeMixin } from '../mixins/ApiFunctionLikeMixin'; diff --git a/apps/api-extractor/src/api/model/ApiMethod.ts b/apps/api-extractor/src/api/model/ApiMethod.ts new file mode 100644 index 00000000000..5c1cc04a045 --- /dev/null +++ b/apps/api-extractor/src/api/model/ApiMethod.ts @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { ApiItemKind } from './ApiItem'; +import { ApiDeclarationMixin, IApiDeclarationMixinOptions } from '../mixins/ApiDeclarationMixin'; +import { ApiStaticMixin, IApiStaticMixinOptions } from '../mixins/ApiStaticMixin'; +import { ApiFunctionLikeMixin, IApiFunctionLikeMixinOptions } from '../mixins/ApiFunctionLikeMixin'; +import { ApiDocumentedItem, IApiDocumentedItemOptions } from './ApiDocumentedItem'; +import { ApiReleaseTagMixin, IApiReleaseTagMixinOptions } from '../mixins/ApiReleaseTagMixin'; +import { Excerpt } from '../mixins/Excerpt'; + +/** @public */ +export interface IApiMethodOptions extends + IApiDeclarationMixinOptions, + IApiFunctionLikeMixinOptions, + IApiReleaseTagMixinOptions, + IApiStaticMixinOptions, + IApiDocumentedItemOptions { +} + +/** @public */ +export class ApiMethod extends ApiDeclarationMixin(ApiFunctionLikeMixin(ApiReleaseTagMixin( + ApiStaticMixin(ApiDocumentedItem)))) { + + public readonly returnTypeExcerpt: Excerpt; + + public static getCanonicalReference(name: string, isStatic: boolean, overloadIndex: number): string { + if (isStatic) { + return `(${name}:static,${overloadIndex})`; + } else { + return `(${name}:instance,${overloadIndex})`; + } + } + + public constructor(options: IApiMethodOptions) { + super(options); + + this.returnTypeExcerpt = this.getEmbeddedExcerpt('returnType'); + } + + /** @override */ + public get kind(): ApiItemKind { + return ApiItemKind.Method; + } + + /** @override */ + public get canonicalReference(): string { + return ApiMethod.getCanonicalReference(this.name, this.isStatic, this.overloadIndex); + } +} diff --git a/apps/api-extractor/src/api/model/ApiMethodSignature.ts b/apps/api-extractor/src/api/model/ApiMethodSignature.ts new file mode 100644 index 00000000000..1bf57b7c22b --- /dev/null +++ b/apps/api-extractor/src/api/model/ApiMethodSignature.ts @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { ApiItemKind } from './ApiItem'; +import { ApiDeclarationMixin, IApiDeclarationMixinOptions } from '../mixins/ApiDeclarationMixin'; +import { ApiFunctionLikeMixin, IApiFunctionLikeMixinOptions } from '../mixins/ApiFunctionLikeMixin'; +import { ApiDocumentedItem, IApiDocumentedItemOptions } from './ApiDocumentedItem'; +import { ApiReleaseTagMixin, IApiReleaseTagMixinOptions } from '../mixins/ApiReleaseTagMixin'; +import { Excerpt } from '../mixins/Excerpt'; + +/** @public */ +export interface IApiMethodSignatureOptions extends + IApiDeclarationMixinOptions, + IApiFunctionLikeMixinOptions, + IApiReleaseTagMixinOptions, + IApiDocumentedItemOptions { +} + +/** @public */ +export class ApiMethodSignature extends ApiDeclarationMixin(ApiFunctionLikeMixin(ApiReleaseTagMixin( + ApiDocumentedItem))) { + + public readonly returnTypeExcerpt: Excerpt; + + public static getCanonicalReference(name: string, overloadIndex: number): string { + return `(${name}:${overloadIndex})`; + } + + public constructor(options: IApiMethodSignatureOptions) { + super(options); + + this.returnTypeExcerpt = this.getEmbeddedExcerpt('returnType'); + } + + /** @override */ + public get kind(): ApiItemKind { + return ApiItemKind.MethodSignature; + } + + /** @override */ + public get canonicalReference(): string { + return ApiMethodSignature.getCanonicalReference(this.name, this.overloadIndex); + } +} diff --git a/apps/api-extractor/src/api/model/ApiModel.ts b/apps/api-extractor/src/api/model/ApiModel.ts new file mode 100644 index 00000000000..cc62d7b6871 --- /dev/null +++ b/apps/api-extractor/src/api/model/ApiModel.ts @@ -0,0 +1,102 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { ApiItem, ApiItemKind } from './ApiItem'; +import { ApiItemContainerMixin } from '../mixins/ApiItemContainerMixin'; +import { ApiPackage } from './ApiPackage'; +import { PackageName } from '@microsoft/node-core-library'; +import { DeclarationReferenceResolver, IResolveDeclarationReferenceResult } from './DeclarationReferenceResolver'; +import { DocDeclarationReference } from '@microsoft/tsdoc'; + +/** @public */ +export class ApiModel extends ApiItemContainerMixin(ApiItem) { + private readonly _resolver: DeclarationReferenceResolver; + + private _packagesByName: Map | undefined = undefined; + + public constructor() { + super({ name: 'MODEL' }); + + this._resolver = new DeclarationReferenceResolver(this); + } + + public loadPackage(apiJsonFilename: string): ApiPackage { + const apiPackage: ApiPackage = ApiPackage.loadFromJsonFile(apiJsonFilename); + this.addMember(apiPackage); + return apiPackage; + } + + /** @override */ + public get kind(): ApiItemKind { + return ApiItemKind.Model; + } + + /** @override */ + public get canonicalReference(): string { + return this.name; + } + + public get packages(): ReadonlyArray { + return this.members as ReadonlyArray; + } + + /** @override */ + public addMember(member: ApiPackage): void { + if (member.kind !== ApiItemKind.Package) { + throw new Error('Only items of type ApiPackage may be added to an ApiModel'); + } + super.addMember(member); + + this._packagesByName = undefined; // invalidate the cache + } + + /** + * Efficiently finds a package by the NPM package name. + * + * @remarks + * + * If the NPM scope is omitted in the package name, it will still be found provided that it is an unambiguous match. + */ + public tryGetPackageByName(packageName: string): ApiPackage | undefined { + // Build the lookup on demand + if (this._packagesByName === undefined) { + this._packagesByName = new Map(); + + const unscopedMap: Map = new Map(); + + for (const apiPackage of this.packages) { + if (this._packagesByName.get(apiPackage.name)) { + // This should not happen + throw new Error(`The model contains multiple packages with the name ${apiPackage.name}`); + } + + this._packagesByName.set(apiPackage.name, apiPackage); + + const unscopedName: string = PackageName.parse(apiPackage.name).unscopedName; + + if (unscopedMap.has(unscopedName)) { + // If another package has the same unscoped name, then we won't register it + unscopedMap.set(unscopedName, undefined); + } else { + unscopedMap.set(unscopedName, apiPackage); + } + } + + for (const [unscopedName, apiPackage] of unscopedMap) { + if (apiPackage) { + if (!this._packagesByName.has(unscopedName)) { + // If the unscoped name is unambiguous, then we can also use it as a lookup + this._packagesByName.set(unscopedName, apiPackage); + } + } + } + } + + return this._packagesByName.get(packageName); + } + + public resolveDeclarationReference(declarationReference: DocDeclarationReference, + contextApiItem: ApiItem | undefined): IResolveDeclarationReferenceResult { + return this._resolver.resolve(declarationReference, contextApiItem); + } +} diff --git a/apps/api-extractor/src/api/model/ApiNamespace.ts b/apps/api-extractor/src/api/model/ApiNamespace.ts new file mode 100644 index 00000000000..1f60657f9e8 --- /dev/null +++ b/apps/api-extractor/src/api/model/ApiNamespace.ts @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { ApiItemKind } from './ApiItem'; +import { ApiItemContainerMixin, IApiItemContainerMixinOptions } from '../mixins/ApiItemContainerMixin'; +import { IApiDeclarationMixinOptions, ApiDeclarationMixin } from '../mixins/ApiDeclarationMixin'; +import { IApiDocumentedItemOptions, ApiDocumentedItem } from './ApiDocumentedItem'; +import { ApiReleaseTagMixin, IApiReleaseTagMixinOptions } from '../mixins/ApiReleaseTagMixin'; + +/** @public */ +export interface IApiNamespaceOptions extends + IApiDeclarationMixinOptions, + IApiItemContainerMixinOptions, + IApiReleaseTagMixinOptions, + IApiDocumentedItemOptions { +} + +/** @public */ +export class ApiNamespace extends ApiDeclarationMixin(ApiItemContainerMixin(ApiReleaseTagMixin(ApiDocumentedItem))) { + public static getCanonicalReference(name: string): string { + return `(${name}:namespace)`; + } + + public constructor(options: IApiNamespaceOptions) { + super(options); + } + + /** @override */ + public get kind(): ApiItemKind { + return ApiItemKind.Namespace; + } + + /** @override */ + public get canonicalReference(): string { + return ApiNamespace.getCanonicalReference(this.name); + } +} diff --git a/apps/api-extractor/src/api/model/ApiPackage.ts b/apps/api-extractor/src/api/model/ApiPackage.ts new file mode 100644 index 00000000000..bad6a1f899b --- /dev/null +++ b/apps/api-extractor/src/api/model/ApiPackage.ts @@ -0,0 +1,98 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { ApiItem, ApiItemKind, IApiItemJson } from './ApiItem'; +import { ApiItemContainerMixin, IApiItemContainerMixinOptions } from '../mixins/ApiItemContainerMixin'; +import { JsonFile, IJsonFileSaveOptions } from '@microsoft/node-core-library'; +import { ApiDocumentedItem, IApiDocumentedItemOptions } from './ApiDocumentedItem'; +import { Extractor } from '../Extractor'; +import { ApiEntryPoint } from './ApiEntryPoint'; + +/** @public */ +export interface IApiPackageOptions extends + IApiItemContainerMixinOptions, + IApiDocumentedItemOptions { +} + +export enum ApiJsonSchemaVersion { + /** + * The initial release. + */ + V_1000 = 1000 +} + +export interface IApiPackageMetadataJson { + /** + * The NPM package name for the tool that wrote the *.api.json file. + * For informational purposes only. + */ + toolPackage: string; + /** + * The NPM package version for the tool that wrote the *.api.json file. + * For informational purposes only. + */ + toolVersion: string; + + /** + * The *.api.json schema version. Used for determining whether the file format is + * supported, and for backwards compatibility. + */ + schemaVersion: ApiJsonSchemaVersion; +} + +export interface IApiPackageJson extends IApiItemJson { + /** + * A file header that stores metadata about the tool that wrote the *.api.json file. + */ + metadata: IApiPackageMetadataJson; +} + +/** @public */ +export class ApiPackage extends ApiItemContainerMixin(ApiDocumentedItem) { + public static loadFromJsonFile(apiJsonFilename: string): ApiPackage { + const jsonObject: { } = JsonFile.load(apiJsonFilename); + return ApiItem.deserialize(jsonObject as IApiItemJson) as ApiPackage; + } + + public constructor(options: IApiPackageOptions) { + super(options); + } + + /** @override */ + public get kind(): ApiItemKind { + return ApiItemKind.Package; + } + + /** @override */ + public get canonicalReference(): string { + return this.name; + } + + public get entryPoints(): ReadonlyArray { + return this.members as ReadonlyArray; + } + + /** @override */ + public addMember(member: ApiEntryPoint): void { + if (member.kind !== ApiItemKind.EntryPoint) { + throw new Error('Only items of type ApiEntryPoint may be added to an ApiPackage'); + } + super.addMember(member); + } + + public findEntryPointsByPath(importPath: string): ReadonlyArray { + return this.findMembersByName(importPath) as ReadonlyArray; + } + + public saveToJsonFile(apiJsonFilename: string, options?: IJsonFileSaveOptions): void { + const jsonObject: IApiPackageJson = { + metadata: { + toolPackage: Extractor.packageName, + toolVersion: Extractor.version, + schemaVersion: ApiJsonSchemaVersion.V_1000 + } + } as IApiPackageJson; + this.serializeInto(jsonObject); + JsonFile.save(jsonObject, apiJsonFilename, options); + } +} diff --git a/apps/api-extractor/src/api/model/ApiParameter.ts b/apps/api-extractor/src/api/model/ApiParameter.ts new file mode 100644 index 00000000000..115b052eaef --- /dev/null +++ b/apps/api-extractor/src/api/model/ApiParameter.ts @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import * as tsdoc from '@microsoft/tsdoc'; + +import { ApiItemKind, ApiItem, IApiItemOptions } from './ApiItem'; +import { IApiDeclarationMixinOptions, ApiDeclarationMixin } from '../mixins/ApiDeclarationMixin'; +import { ApiDocumentedItem } from './ApiDocumentedItem'; +import { Excerpt } from '../mixins/Excerpt'; + +/** @public */ +export interface IApiParameterOptions extends + IApiDeclarationMixinOptions, + IApiItemOptions { +} + +/** @public */ +export class ApiParameter extends ApiDeclarationMixin(ApiItem) { + public readonly parameterTypeExcerpt: Excerpt; + + public constructor(options: IApiParameterOptions) { + super(options); + + this.parameterTypeExcerpt = this.getEmbeddedExcerpt('parameterType'); + } + + /** @override */ + public get kind(): ApiItemKind { + return ApiItemKind.Parameter; + } + + /** @override */ + public get canonicalReference(): string { + return this.name; + } + + /** + * Returns the `@param` documentation for this parameter, if present. + */ + public get tsdocParamBlock(): tsdoc.DocParamBlock | undefined { + const parent: ApiItem | undefined = this.parent; + if (parent) { + if (parent instanceof ApiDocumentedItem) { + if (parent.tsdocComment) { + return parent.tsdocComment.params.tryGetBlockByName(this.name); + } + } + } + } +} diff --git a/apps/api-extractor/src/api/model/ApiProperty.ts b/apps/api-extractor/src/api/model/ApiProperty.ts new file mode 100644 index 00000000000..c054251a71b --- /dev/null +++ b/apps/api-extractor/src/api/model/ApiProperty.ts @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { ApiItemKind } from './ApiItem'; +import { ApiStaticMixin, IApiStaticMixinOptions } from '../mixins/ApiStaticMixin'; +import { ApiReleaseTagMixin, IApiReleaseTagMixinOptions } from '../mixins/ApiReleaseTagMixin'; +import { ApiPropertyItem, IApiPropertyItemOptions } from './ApiPropertyItem'; + +/** @public */ +export interface IApiPropertyOptions extends + IApiReleaseTagMixinOptions, + IApiStaticMixinOptions, + IApiPropertyItemOptions { +} + +/** @public */ +export class ApiProperty extends ApiReleaseTagMixin(ApiStaticMixin(ApiPropertyItem)) { + + public static getCanonicalReference(name: string, isStatic: boolean): string { + if (isStatic) { + return `(${name}:static)`; + } else { + return `(${name}:instance)`; + } + } + + public constructor(options: IApiPropertyOptions) { + super(options); + } + + /** @override */ + public get kind(): ApiItemKind { + return ApiItemKind.Property; + } + + /** @override */ + public get canonicalReference(): string { + return ApiProperty.getCanonicalReference(this.name, this.isStatic); + } +} diff --git a/apps/api-extractor/src/api/model/ApiPropertyItem.ts b/apps/api-extractor/src/api/model/ApiPropertyItem.ts new file mode 100644 index 00000000000..e69a632b127 --- /dev/null +++ b/apps/api-extractor/src/api/model/ApiPropertyItem.ts @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { ApiDocumentedItem, IApiDocumentedItemOptions } from './ApiDocumentedItem'; +import { Excerpt } from '../mixins/Excerpt'; +import { IApiDeclarationMixinOptions, ApiDeclarationMixin } from '../mixins/ApiDeclarationMixin'; + +/** @public */ +export interface IApiPropertyItemOptions extends + IApiDocumentedItemOptions, + IApiDeclarationMixinOptions { +} + +/** + * Common base class for ApiProperty and ApiPropertySignature. + * + * @public + */ +export class ApiPropertyItem extends ApiDeclarationMixin(ApiDocumentedItem) { + public readonly propertyTypeExcerpt: Excerpt; + + public constructor(options: IApiPropertyItemOptions) { + super(options); + + this.propertyTypeExcerpt = this.getEmbeddedExcerpt('propertyType'); + } + + /** + * Returns true if this property should be documented as an event. + * + * @remarks + * The `@eventProperty` TSDoc modifier can be added to readonly properties to indicate that they return an + * event object that event handlers can be attached to. The event-handling API is implementation-defined, but + * typically the return type would be a class with members such as `addHandler()` and `removeHandler()`. + * The documentation should display such properties under an "Events" heading instead of the + * usual "Properties" heading. + */ + public get isEventProperty(): boolean { + if (this.tsdocComment) { + return this.tsdocComment.modifierTagSet.isEventProperty(); + } + return false; + } +} diff --git a/apps/api-extractor/src/api/model/ApiPropertySignature.ts b/apps/api-extractor/src/api/model/ApiPropertySignature.ts new file mode 100644 index 00000000000..b6d19c04e81 --- /dev/null +++ b/apps/api-extractor/src/api/model/ApiPropertySignature.ts @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { ApiItemKind } from './ApiItem'; +import { ApiReleaseTagMixin, IApiReleaseTagMixinOptions } from '../mixins/ApiReleaseTagMixin'; +import { ApiPropertyItem, IApiPropertyItemOptions } from './ApiPropertyItem'; + +/** @public */ +export interface IApiPropertySignatureOptions extends + IApiReleaseTagMixinOptions, + IApiPropertyItemOptions { +} + +/** @public */ +export class ApiPropertySignature extends ApiReleaseTagMixin(ApiPropertyItem) { + + public static getCanonicalReference(name: string): string { + return name; + } + + public constructor(options: IApiPropertySignatureOptions) { + super(options); + } + + /** @override */ + public get kind(): ApiItemKind { + return ApiItemKind.PropertySignature; + } + + /** @override */ + public get canonicalReference(): string { + return ApiPropertySignature.getCanonicalReference(this.name); + } +} diff --git a/apps/api-extractor/src/api/model/DeclarationReferenceResolver.ts b/apps/api-extractor/src/api/model/DeclarationReferenceResolver.ts new file mode 100644 index 00000000000..8b0ea4d8f53 --- /dev/null +++ b/apps/api-extractor/src/api/model/DeclarationReferenceResolver.ts @@ -0,0 +1,114 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { DocDeclarationReference } from '@microsoft/tsdoc'; +import { ApiItem } from './ApiItem'; +import { ApiModel } from './ApiModel'; +import { ApiPackage } from './ApiPackage'; +import { ApiEntryPoint } from './ApiEntryPoint'; +import { ApiItemContainerMixin } from '../mixins/ApiItemContainerMixin'; + +/** + * Result object for {@link ApiModel.resolveDeclarationReference}. + * + * @public + */ +export interface IResolveDeclarationReferenceResult { + /** + * The referenced ApiItem, if the declaration reference could be resolved. + */ + resolvedApiItem: ApiItem | undefined; + + /** + * If resolvedApiItem is undefined, then this will always contain an error message explaining why the + * resolution failed. + */ + errorMessage: string | undefined; +} + +export class DeclarationReferenceResolver { + private readonly _apiModel: ApiModel; + + public constructor(apiModel: ApiModel) { + this._apiModel = apiModel; + } + + public resolve(declarationReference: DocDeclarationReference, + contextApiItem: ApiItem | undefined): IResolveDeclarationReferenceResult { + + const result: IResolveDeclarationReferenceResult = { + resolvedApiItem: undefined, + errorMessage: undefined + }; + + let apiPackage: ApiPackage | undefined = undefined; + + // Is this an absolute reference? + if (declarationReference.packageName !== undefined) { + apiPackage = this._apiModel.tryGetPackageByName(declarationReference.packageName); + if (apiPackage === undefined) { + result.errorMessage = `The package "${declarationReference.packageName}" could not be located`; + return result; + } + } else { + // If the package name is omitted, try to infer it from the context + if (contextApiItem !== undefined) { + apiPackage = contextApiItem.getAssociatedPackage(); + } + + if (apiPackage === undefined) { + result.errorMessage = `The reference does not include a package name, and the package could not be inferred` + + ` from the context`; + return result; + } + } + + const importPath: string = declarationReference.importPath || ''; + + const foundEntryPoints: ReadonlyArray = apiPackage.findEntryPointsByPath(importPath); + if (foundEntryPoints.length !== 1) { + result.errorMessage = `The import path "${importPath}" could not be resolved`; + return result; + } + + let currentItem: ApiItem = foundEntryPoints[0]; + + // Now search for the member reference + for (const memberReference of declarationReference.memberReferences) { + if (memberReference.memberSymbol !== undefined) { + result.errorMessage = `Symbols are not yet supported in declaration references` ; + return result; + } + + if (memberReference.memberIdentifier === undefined) { + result.errorMessage = `Missing member identifier`; + return result; + } + + const identifier: string = memberReference.memberIdentifier.identifier; + + if (!ApiItemContainerMixin.isBaseClassOf(currentItem)) { + // For example, {@link MyClass.myMethod.X} is invalid because methods cannot contain members + result.errorMessage = `Unable to resolve ${JSON.stringify(identifier)} because ${JSON.stringify(currentItem)}` + + ` cannot act as a container`; + return result; + } + + const foundMembers: ReadonlyArray = currentItem.findMembersByName(identifier); + if (foundMembers.length === 0) { + result.errorMessage = `The member reference ${JSON.stringify(identifier)} was not found` ; + return result; + } + if (foundMembers.length > 1) { + // TODO: Support TSDoc selectors + result.errorMessage = `The member reference ${JSON.stringify(identifier)} was ambiguous` ; + return result; + } + + currentItem = foundMembers[0]; + } + result.resolvedApiItem = currentItem; + return result; + } + +} diff --git a/apps/api-extractor/src/api/model/Deserializer.ts b/apps/api-extractor/src/api/model/Deserializer.ts new file mode 100644 index 00000000000..d33eb652f87 --- /dev/null +++ b/apps/api-extractor/src/api/model/Deserializer.ts @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { IApiItemJson, IApiItemOptions, ApiItem, ApiItemKind } from './ApiItem'; +import { ApiClass } from './ApiClass'; +import { ApiEntryPoint } from './ApiEntryPoint'; +import { ApiMethod } from './ApiMethod'; +import { ApiModel } from './ApiModel'; +import { ApiNamespace } from './ApiNamespace'; +import { ApiPackage } from './ApiPackage'; +import { ApiInterface } from './ApiInterface'; +import { ApiPropertySignature } from './ApiPropertySignature'; +import { ApiParameter } from './ApiParameter'; +import { ApiMethodSignature } from './ApiMethodSignature'; +import { ApiProperty } from './ApiProperty'; +import { ApiEnumMember } from './ApiEnumMember'; +import { ApiEnum } from './ApiEnum'; + +export class Deserializer { + public static deserialize(jsonObject: IApiItemJson): ApiItem { + const options: Partial = { }; + + switch (jsonObject.kind) { + case ApiItemKind.Class: + ApiClass.onDeserializeInto(options, jsonObject); + return new ApiClass(options as any); // tslint:disable-line:no-any + case ApiItemKind.EntryPoint: + ApiEntryPoint.onDeserializeInto(options, jsonObject); + return new ApiEntryPoint(options as any); // tslint:disable-line:no-any + case ApiItemKind.Enum: + ApiEnum.onDeserializeInto(options, jsonObject); + return new ApiEnum(options as any); // tslint:disable-line:no-any + case ApiItemKind.EnumMember: + ApiEnumMember.onDeserializeInto(options, jsonObject); + return new ApiEnumMember(options as any); // tslint:disable-line:no-any + case ApiItemKind.Interface: + ApiInterface.onDeserializeInto(options, jsonObject); + return new ApiInterface(options as any); // tslint:disable-line:no-any + case ApiItemKind.Method: + ApiMethod.onDeserializeInto(options, jsonObject); + return new ApiMethod(options as any); // tslint:disable-line:no-any + case ApiItemKind.MethodSignature: + ApiMethodSignature.onDeserializeInto(options, jsonObject); + return new ApiMethodSignature(options as any); // tslint:disable-line:no-any + case ApiItemKind.Model: + return new ApiModel(); + case ApiItemKind.Namespace: + ApiNamespace.onDeserializeInto(options, jsonObject); + return new ApiNamespace(options as any); // tslint:disable-line:no-any + case ApiItemKind.Package: + ApiPackage.onDeserializeInto(options, jsonObject); + return new ApiPackage(options as any); // tslint:disable-line:no-any + case ApiItemKind.Parameter: + ApiParameter.onDeserializeInto(options, jsonObject); + return new ApiParameter(options as any); // tslint:disable-line:no-any + case ApiItemKind.Property: + ApiProperty.onDeserializeInto(options, jsonObject); + return new ApiProperty(options as any); // tslint:disable-line:no-any + case ApiItemKind.PropertySignature: + ApiPropertySignature.onDeserializeInto(options, jsonObject); + return new ApiPropertySignature(options as any); // tslint:disable-line:no-any + default: + throw new Error(`Failed to deserialize unsupported API item type ${JSON.stringify(jsonObject.kind)}`); + } + } +} diff --git a/apps/api-extractor/src/api/test/IndentedWriter.test.ts b/apps/api-extractor/src/api/test/IndentedWriter.test.ts new file mode 100644 index 00000000000..8ca09139a4b --- /dev/null +++ b/apps/api-extractor/src/api/test/IndentedWriter.test.ts @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { IndentedWriter } from '../IndentedWriter'; + +test('01 Demo from docs', () => { + const indentedWriter: IndentedWriter = new IndentedWriter(); + indentedWriter.write('begin\n'); + indentedWriter.increaseIndent(); + indentedWriter.write('one\ntwo\n'); + indentedWriter.decreaseIndent(); + indentedWriter.increaseIndent(); + indentedWriter.decreaseIndent(); + indentedWriter.write('end'); + + expect(indentedWriter.toString()).toMatchSnapshot(); +}); + +test('02 Indent something', () => { + const indentedWriter: IndentedWriter = new IndentedWriter(); + indentedWriter.write('a'); + indentedWriter.write('b'); + indentedWriter.increaseIndent(); + indentedWriter.writeLine('c'); + indentedWriter.writeLine('d'); + indentedWriter.decreaseIndent(); + indentedWriter.writeLine('e'); + + indentedWriter.increaseIndent('>>> '); + indentedWriter.writeLine(); + indentedWriter.writeLine(); + indentedWriter.writeLine('g'); + indentedWriter.decreaseIndent(); + + expect(indentedWriter.toString()).toMatchSnapshot(); +}); + +test('03 Two kinds of indents', () => { + const indentedWriter: IndentedWriter = new IndentedWriter(); + + indentedWriter.writeLine('---'); + indentedWriter.indentScope(() => { + indentedWriter.write('a\nb'); + indentedWriter.indentScope(() => { + indentedWriter.write('c\nd\n'); + }); + indentedWriter.write('e\n'); + }, '> '); + indentedWriter.writeLine('---'); + + expect(indentedWriter.toString()).toMatchSnapshot(); +}); + +test('04 Edge cases for ensureNewLine()', () => { + let indentedWriter: IndentedWriter = new IndentedWriter(); + indentedWriter.ensureNewLine(); + indentedWriter.write('line'); + expect(indentedWriter.toString()).toMatchSnapshot(); + + indentedWriter = new IndentedWriter(); + indentedWriter.write('previous'); + indentedWriter.ensureNewLine(); + indentedWriter.write('line'); + expect(indentedWriter.toString()).toMatchSnapshot(); +}); + +test('04 Edge cases for ensureSkippedLine()', () => { + let indentedWriter: IndentedWriter = new IndentedWriter(); + indentedWriter.ensureSkippedLine(); + indentedWriter.write('line'); + expect(indentedWriter.toString()).toMatchSnapshot(); + + indentedWriter = new IndentedWriter(); + indentedWriter.write('previous'); + indentedWriter.ensureSkippedLine(); + indentedWriter.write('line'); + expect(indentedWriter.toString()).toMatchSnapshot(); +}); diff --git a/apps/api-extractor/src/api/test/__snapshots__/IndentedWriter.test.ts.snap b/apps/api-extractor/src/api/test/__snapshots__/IndentedWriter.test.ts.snap new file mode 100644 index 00000000000..62778d301c8 --- /dev/null +++ b/apps/api-extractor/src/api/test/__snapshots__/IndentedWriter.test.ts.snap @@ -0,0 +1,46 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`01 Demo from docs 1`] = ` +"begin + one + two +end" +`; + +exports[`02 Indent something 1`] = ` +"abc + d +e +>>> +>>> +>>> g +" +`; + +exports[`03 Two kinds of indents 1`] = ` +"--- +> a +> bc +> d +> e +--- +" +`; + +exports[`04 Edge cases for ensureNewLine() 1`] = `"line"`; + +exports[`04 Edge cases for ensureNewLine() 2`] = ` +"previous +line" +`; + +exports[`04 Edge cases for ensureSkippedLine() 1`] = ` +" +line" +`; + +exports[`04 Edge cases for ensureSkippedLine() 2`] = ` +"previous + +line" +`; diff --git a/apps/api-extractor/src/ast/AstEnum.ts b/apps/api-extractor/src/ast/AstEnum.ts deleted file mode 100644 index 8dc7b6e865c..00000000000 --- a/apps/api-extractor/src/ast/AstEnum.ts +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -// See LICENSE in the project root for license information. - -import * as ts from 'typescript'; -import { AstItemKind } from './AstItem'; -import { AstItemContainer } from './AstItemContainer'; -import { IAstItemOptions } from './AstItem'; -import { AstEnumValue } from './AstEnumValue'; -import { TypeScriptHelpers } from '../utils/TypeScriptHelpers'; - -/** - * This class is part of the AstItem abstract syntax tree. It represents a TypeScript enum definition. - * The individual enum values are represented using AstEnumValue. - */ -export class AstEnum extends AstItemContainer { - constructor(options: IAstItemOptions) { - super(options); - this.kind = AstItemKind.Enum; - - for (const memberDeclaration of (options.declaration as ts.EnumDeclaration).members) { - const memberSymbol: ts.Symbol = TypeScriptHelpers.getSymbolForDeclaration(memberDeclaration); - - const memberOptions: IAstItemOptions = { - context: this.context, - declaration: memberDeclaration, - declarationSymbol: memberSymbol - }; - - this.addMemberItem(new AstEnumValue(memberOptions)); - } - - } -} diff --git a/apps/api-extractor/src/ast/AstEnumValue.ts b/apps/api-extractor/src/ast/AstEnumValue.ts deleted file mode 100644 index 2ca61d7590f..00000000000 --- a/apps/api-extractor/src/ast/AstEnumValue.ts +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -// See LICENSE in the project root for license information. - -import { AstItem, AstItemKind, IAstItemOptions } from './AstItem'; -import { PrettyPrinter } from '../utils/PrettyPrinter'; - -/** - * This class is part of the AstItem abstract syntax tree. It represents a TypeScript enum value. - * The parent container will always be an AstEnum instance. - */ -export class AstEnumValue extends AstItem { - constructor(options: IAstItemOptions) { - super(options); - this.kind = AstItemKind.EnumValue; - } - - /** - * Returns a text string such as "MyValue = 123," - */ - public getDeclarationLine(): string { - return PrettyPrinter.getDeclarationSummary(this.declaration); - } -} diff --git a/apps/api-extractor/src/ast/AstFunction.ts b/apps/api-extractor/src/ast/AstFunction.ts deleted file mode 100644 index 5253113ac67..00000000000 --- a/apps/api-extractor/src/ast/AstFunction.ts +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -// See LICENSE in the project root for license information. - -import * as ts from 'typescript'; -import { Text } from '@microsoft/node-core-library'; - -import { AstItem, AstItemKind, IAstItemOptions } from './AstItem'; -import { AstParameter } from './AstParameter'; -import { TypeScriptHelpers } from '../utils/TypeScriptHelpers'; -import { PrettyPrinter } from '../utils/PrettyPrinter'; - -/** - * This class is part of the AstItem abstract syntax tree. It represents functions that are directly - * defined inside a package and are not member of classes, interfaces, or nested type literal expressions - * - * @see AstMethod for functions that are members of classes, interfaces, or nested type literal expressions - */ -export class AstFunction extends AstItem { - public returnType: string; - public params: AstParameter[]; - - constructor(options: IAstItemOptions) { - super(options); - this.kind = AstItemKind.Function; - - const methodDeclaration: ts.FunctionDeclaration = options.declaration as ts.FunctionDeclaration; - - // Parameters - if (methodDeclaration.parameters) { - this.params = []; - for (const param of methodDeclaration.parameters) { - const declarationSymbol: ts.Symbol = TypeScriptHelpers.getSymbolForDeclaration(param); - const astParameter: AstParameter = new AstParameter({ - context: this.context, - declaration: param, - declarationSymbol: declarationSymbol - }); - this.innerItems.push(astParameter); - this.params.push(astParameter); - } - } - - // Return type - if (methodDeclaration.type) { - this.returnType = Text.convertToLf(methodDeclaration.type.getText()); - } else { - this.hasIncompleteTypes = true; - this.returnType = 'any'; - } - } - - /** - * Returns a text string such as "someName?: SomeTypeName;", or in the case of a type - * literal expression, returns a text string such as "someName?:". - */ - public getDeclarationLine(): string { - return PrettyPrinter.getDeclarationSummary(this.declaration); - } -} diff --git a/apps/api-extractor/src/ast/AstItem.ts b/apps/api-extractor/src/ast/AstItem.ts deleted file mode 100644 index 23267f0c134..00000000000 --- a/apps/api-extractor/src/ast/AstItem.ts +++ /dev/null @@ -1,700 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -// See LICENSE in the project root for license information. - -/* tslint:disable:no-bitwise */ -/* tslint:disable:no-constant-condition */ - -import * as ts from 'typescript'; -import * as tsdoc from '@microsoft/tsdoc'; -import { IPackageJson, IParsedPackageName, PackageName } from '@microsoft/node-core-library'; -import { ExtractorContext } from '../ExtractorContext'; -import { ApiDocumentation } from '../aedoc/ApiDocumentation'; -import { MarkupElement } from '../markup/MarkupElement'; -import { ReleaseTag } from '../aedoc/ReleaseTag'; -import { TypeScriptHelpers } from '../utils/TypeScriptHelpers'; -import { Markup } from '../markup/Markup'; -import { ResolvedApiItem } from '../ResolvedApiItem'; -import { - ApiDefinitionReference, - IApiDefinitionReferenceParts -} from '../ApiDefinitionReference'; -import { AstItemContainer } from './AstItemContainer'; - -/** - * Indicates the type of definition represented by a AstItem object. - */ -export enum AstItemKind { - /** - * A TypeScript class. - */ - Class = 0, - /** - * A TypeScript enum. - */ - Enum = 1, - /** - * A TypeScript value on an enum. - */ - EnumValue = 2, - /** - * A TypeScript function. - */ - Function = 3, - /** - * A TypeScript interface. - */ - Interface = 4, - /** - * A TypeScript method. - */ - Method = 5, - /** - * A TypeScript package. - */ - Package = 6, - /** - * A TypeScript parameter. - */ - Parameter = 7, - /** - * A TypeScript property. - */ - Property = 8, - /** - * A TypeScript type literal expression, i.e. which defines an anonymous interface. - */ - TypeLiteral = 9, - /** - * A Typescript class constructor function. - */ - Constructor = 10, - /** - * A Typescript namespace. - */ - Namespace = 11, - /** - * A Typescript BlockScopedVariable. - */ - ModuleVariable = 12 -} - -/** - * The state of completing the AstItem's doc comment references inside a recursive call to AstItem.resolveReferences(). - */ -enum InitializationState { - /** - * The references of this AstItem have not begun to be completed. - */ - Incomplete = 0, - /** - * The references of this AstItem are in the process of being completed. - * If we encounter this state again during completing, a circular dependency - * has occurred. - */ - Completing = 1, - /** - * The references of this AstItem have all been completed and the documentation can - * now safely be created. - */ - Completed = 2 -} - -/** - * This interface is used to pass options between constructors for AstItem child classes. - */ -export interface IAstItemOptions { - /** - * The associated ExtractorContext object for this AstItem - */ - context: ExtractorContext; - /** - * The declaration node for the main syntax item that this AstItem is associated with. - */ - declaration: ts.Declaration; - /** - * The semantic information for the declaration. - */ - declarationSymbol: ts.Symbol; - - /** - * The JSDoc-style comment range (including the "/**" characters), which is assumed - * to be in the same source file as the IAstItemOptions.declaration node. - * If this is undefined, then the comment will be obtained from the - * IAstItemOptions.declaration node. - */ - aedocCommentRange?: ts.TextRange; - - /** - * The symbol used to export this AstItem from the AstPackage. - */ - exportSymbol?: ts.Symbol; -} - -// Names of NPM scopes that contain packages that provide typings for the real package. -// The TypeScript compiler's typings design doesn't seem to handle scoped NPM packages, -// so the transformation will always be simple, like this: -// "@types/example" --> "example" -// NOT like this: -// "@types/@contoso/example" --> "@contoso/example" -// "@contosotypes/example" --> "@contoso/example" -// Eventually this constant should be provided by the gulp task that invokes the compiler. -const typingsScopeNames: string[] = [ '@types' ]; - -/** - * AstItem is an abstract base that represents TypeScript API definitions such as classes, - * interfaces, enums, properties, functions, and variables. Rather than directly using the - * abstract syntax tree from the TypeScript Compiler API, we use AstItem to extract a - * simplified tree which correponds to the major topics for our API documentation. - */ -export abstract class AstItem { - - /** - * Names of API items should only contain letters, numbers and underscores. - */ - private static _allowedNameRegex: RegExp = /^[a-zA-Z_]+[a-zA-Z_0-9]*$/; - - /** - * The name of the definition, as seen by external consumers of the Public API. - * For example, suppose a class is defined as "export default class MyClass { }" - * but exported from the package's index.ts like this: - * - * export { default as _MyClass } from './MyClass'; - * - * In this example, the AstItem.name would be "_MyClass", i.e. the alias as exported - * from the top-level AstPackage, not "MyClass" from the original definition. - */ - public name: string; - - /** - * The name of an API item should be readable and not contain any special characters. - */ - public supportedName: boolean; - - /** - * Indicates the type of definition represented by this AstItem instance. - */ - public kind: AstItemKind; - - /** - * A superset of memberItems. Includes memberItems and also other AstItems that - * comprise this AstItem. - * - * Ex: if this AstItem is an AstFunction, then in it's innerItems would - * consist of AstParameters. - * Ex: if this AstItem is an AstMember that is a type literal, then it's - * innerItems would contain ApiProperties. - */ - public innerItems: AstItem[] = []; - - /** - * True if this AstItem either itself has missing type information or one - * of it's innerItems is missing type information. - * - * Ex: if this AstItem is an AstMethod and has no type on the return value, then - * we consider the AstItem as 'itself' missing type informations and this property - * is set to true. - * Ex: If this AstItem is an AstMethod and one of its innerItems is an AstParameter - * that has no type specified, then we say an innerItem of this AstMethod is missing - * type information and this property is set to true. - */ - public hasIncompleteTypes: boolean = false; - - /** - * A list of extractor warnings that were reported using AstItem.reportWarning(). - * Whereas an "error" will break the build, a "warning" will merely be tracked in - * the API file produced by ApiFileGenerator. - */ - public warnings: string[]; - - /** - * The parsed AEDoc comment for this item. - */ - public documentation: ApiDocumentation; - - /** - * Indicates that this AstItem does not have adequate AEDoc comments. If shouldHaveDocumentation()=true, - * and there is less than 10 characters of summary text in the AEDoc, then this will be set to true and - * noted in the API file produced by ApiFileGenerator. - * (The AEDoc text itself is not included in that report, because documentation - * changes do not require an API review, and thus should not cause a diff for that report.) - */ - public needsDocumentation: boolean; - - /** - * The release tag for this item, which may be inherited from a parent. - * By contrast, ApiDocumentation.releaseTag merely tracks the release tag that was - * explicitly applied to this item, and does not consider inheritance. - * @remarks - * This is calculated during completeInitialization() and should not be used beforehand. - */ - public inheritedReleaseTag: ReleaseTag = ReleaseTag.None; - - /** - * The deprecated message for this item, which may be inherited from a parent. - * By contrast, ApiDocumentation.deprecatedMessage merely tracks the message that was - * explicitly applied to this item, and does not consider inheritance. - * @remarks - * This is calculated during completeInitialization() and should not be used beforehand. - */ - public inheritedDeprecatedMessage: MarkupElement[] = []; - - /** - * The ExtractorContext object provides common contextual information for all of - * items in the AstItem tree. - */ - protected context: ExtractorContext; - - /** - * Syntax information from the TypeScript Compiler API, corresponding to the place - * where this object is originally defined. - */ - protected declaration: ts.Declaration; - - /** - * Semantic information from the TypeScript Compiler API, corresponding to the place - * where this object is originally defined. - */ - protected declarationSymbol: ts.Symbol; - - /** - * Semantic information from the TypeScript Compiler API, corresponding to the symbol - * that is seen by external consumers of the Public API. For an aliased symbol, this - * would be the alias that is exported from the top-level package (i.e. AstPackage). - */ - protected exportSymbol: ts.Symbol; - - protected typeChecker: ts.TypeChecker; - - /** - * Syntax information from the TypeScript Compiler API, used to locate the file name - * and line number when reporting an error for this AstItem. - */ - private _errorNode: ts.Node; - - /** - * The state of this AstItems references. These references could include \@inheritdoc references - * or type references. - */ - private _state: InitializationState; - - private _parentContainer: AstItemContainer | undefined; - - constructor(options: IAstItemOptions) { - this.reportError = this.reportError.bind(this); - - this.declaration = options.declaration; - this._errorNode = options.declaration; - this._state = InitializationState.Incomplete; - this.warnings = []; - - this.context = options.context; - this.typeChecker = this.context.typeChecker; - this.declarationSymbol = options.declarationSymbol; - this.exportSymbol = options.exportSymbol || this.declarationSymbol; - - this.name = this.exportSymbol.name || '???'; - - const sourceFileText: string = this.declaration.getSourceFile().text; - - // This will contain the AEDoc content, including the "/**" characters - let inputTextRange: tsdoc.TextRange = tsdoc.TextRange.empty; - - if (options.aedocCommentRange) { // but might be "" - // This is e.g. for the special @packagedocumentation comment, which is pulled - // from elsewhere in the AST. - inputTextRange = tsdoc.TextRange.fromStringRange(sourceFileText, - options.aedocCommentRange.pos, options.aedocCommentRange.end); - } else { - // This is the typical case - const ranges: ts.CommentRange[] = TypeScriptHelpers.getJSDocCommentRanges( - this.declaration, sourceFileText) || []; - if (ranges.length > 0) { - // We use the JSDoc comment block that is closest to the definition, i.e. - // the last one preceding it - const lastRange: ts.TextRange = ranges[ranges.length - 1]; - inputTextRange = tsdoc.TextRange.fromStringRange(sourceFileText, - lastRange.pos, lastRange.end); - } - } - - this.documentation = new ApiDocumentation( - inputTextRange, - this.context.docItemLoader, - this.context, - this.reportError, - this.warnings - ); - } - - /** - * Called by AstItemContainer.addMemberItem(). Other code should NOT call this method. - */ - public notifyAddedToContainer(parentContainer: AstItemContainer): void { - if (this._parentContainer) { - // This would indicate a program bug - throw new Error('The API item has already been added to another container: ' + this._parentContainer.name); - } - this._parentContainer = parentContainer; - } - - /** - * Called after the constructor to finish the analysis. - */ - public visitTypeReferencesForAstItem(): void { - // (virtual) - } - - /** - * Return the compiler's underlying Declaration object - * @todo Generally AstItem classes don't expose ts API objects; we should add - * an appropriate member to avoid the need for this. - */ - public getDeclaration(): ts.Declaration { - return this.declaration; - } - - /** - * Return the compiler's underlying Symbol object that contains semantic information about the item - * @todo Generally AstItem classes don't expose ts API objects; we should add - * an appropriate member to avoid the need for this. - */ - public getDeclarationSymbol(): ts.Symbol { - return this.declarationSymbol; - } - - /** - * Whether this APiItem should have documentation or not. If false, then - * AstItem.missingDocumentation will never be set. - */ - public shouldHaveDocumentation(): boolean { - return true; - } - - /** - * The AstItemContainer that this member belongs to, or undefined if there is none. - */ - public get parentContainer(): AstItemContainer|undefined { - return this._parentContainer; - } - - /** - * This function is a second stage that happens after ExtractorContext.analyze() calls AstItem constructor to build up - * the abstract syntax tree. In this second stage, we are creating the documentation for each AstItem. - * - * This function makes sure we create the documentation for each AstItem in the correct order. - * In the event that a circular dependency occurs, an error is reported. For example, if AstItemOne has - * an \@inheritdoc referencing AstItemTwo, and AstItemTwo has an \@inheritdoc referencing AstItemOne then - * we have a circular dependency and an error will be reported. - */ - public completeInitialization(): void { - switch (this._state) { - case InitializationState.Completed: - return; - case InitializationState.Incomplete: - this._state = InitializationState.Completing; - this.onCompleteInitialization(); - this._state = InitializationState.Completed; - - for (const innerItem of this.innerItems) { - innerItem.completeInitialization(); - } - return; - case InitializationState.Completing: - this.reportError('circular reference'); - return; - default: - throw new Error('AstItem state is invalid'); - } - } - - /** - * A procedure for determining if this AstItem is missing type - * information. We first check if the AstItem itself is missing - * any type information and if not then we check each of it's - * innerItems for missing types. - * - * Ex: On the AstItem itself, there may be missing type information - * on the return value or missing type declaration of itself - * (const name;). - * Ex: For each innerItem, there may be an AstParameter that is missing - * a type. Or for an AstMember that is a type literal, there may be an - * AstProperty that is missing type information. - */ - public hasAnyIncompleteTypes(): boolean { - if (this.hasIncompleteTypes) { - return true; - } - - for (const innerItem of this.innerItems) { - if (innerItem.hasIncompleteTypes) { - return true; - } - } - - return false; - } - - /** - * Reports an error through the ApiErrorHandler interface that was registered with the Extractor, - * adding the filename and line number information for the declaration of this AstItem. - */ - protected reportError(message: string, startIndex?: number): void { - if (!startIndex) { - startIndex = this._errorNode.getStart(); - } - this.context.reportError(message, this._errorNode.getSourceFile(), startIndex); - } - - /** - * Adds a warning to the AstItem.warnings list. These warnings will be emitted in the API file - * produced by ApiFileGenerator. - */ - protected reportWarning(message: string): void { - this.warnings.push(message); - } - - /** - * This function assumes all references from this AstItem have been resolved and we can now safely create - * the documentation. - */ - protected onCompleteInitialization(): void { - this.documentation.completeInitialization(this.warnings); - - // Calculate the inherited release tag - if (this.documentation.releaseTag !== ReleaseTag.None) { - this.inheritedReleaseTag = this.documentation.releaseTag; - } else if (this.parentContainer) { - this.inheritedReleaseTag = this.parentContainer.inheritedReleaseTag; - } - - // Calculate the inherited deprecation message - if (this.documentation.deprecatedMessage.length) { - this.inheritedDeprecatedMessage = this.documentation.deprecatedMessage; - } else if (this.parentContainer) { - this.inheritedDeprecatedMessage = this.parentContainer.inheritedDeprecatedMessage; - } - - // TODO: this.visitTypeReferencesForNode(this); - - const summaryTextCondensed: string = Markup.extractTextContent( - this.documentation.summary).replace(/\s\s/g, ' '); - this.needsDocumentation = this.shouldHaveDocumentation() && summaryTextCondensed.length <= 10; - - this.supportedName = (this.kind === AstItemKind.Package) || AstItem._allowedNameRegex.test(this.name); - if (!this.supportedName) { - this.warnings.push(`The name "${this.name}" contains unsupported characters; ` + - 'API names should use only letters, numbers, and underscores'); - } - - if (this.kind === AstItemKind.Package) { - if (this.documentation.aedocCommentFound) { - if (!this.documentation.isPackageDocumentation) { - this.reportError('A package comment was found, but it is missing the @packagedocumentation tag'); - } - } - - if (this.documentation.releaseTag !== ReleaseTag.None) { - const tag: string = '@' + ReleaseTag[this.documentation.releaseTag].toLowerCase(); - this.reportError(`The ${tag} tag is not allowed on the package, which is always considered to be @public`); - } - } else { - if (this.documentation.isPackageDocumentation) { - this.reportError(`The @packagedocumentation tag cannot be used for an item of type ${AstItemKind[this.kind]}`); - } - } - - if (this.documentation.preapproved) { - if (!(this.getDeclaration().kind & (ts.SyntaxKind.InterfaceDeclaration | ts.SyntaxKind.ClassDeclaration))) { - this.reportError('The @preapproved tag may only be applied to classes and interfaces'); - this.documentation.preapproved = false; - } - } - - if (this.documentation.isEventProperty) { - if (this.kind !== AstItemKind.Property) { - this.reportError('The @eventProperty tag may only be applied to a property'); - } - } - - if (this.documentation.isDocInheritedDeprecated && this.documentation.deprecatedMessage.length === 0) { - this.reportError('The @inheritdoc target has been marked as @deprecated. ' + - 'Add a @deprecated message here, or else remove the @inheritdoc relationship.'); - } - - if (this.name.substr(0, 1) === '_') { - if (this.documentation.releaseTag !== ReleaseTag.Internal - && this.documentation.releaseTag !== ReleaseTag.None) { - this.reportWarning('The underscore prefix ("_") should only be used with definitions' - + ' that are explicitly marked as @internal'); - } - } else { - if (this.documentation.releaseTag === ReleaseTag.Internal) { - this.reportWarning('Because this definition is explicitly marked as @internal, an underscore prefix ("_")' - + ' should be added to its name'); - } - } - - // Is it missing a release tag? - if (this.documentation.releaseTag === ReleaseTag.None) { - // Only warn about top-level exports - if (this.parentContainer && this.parentContainer.kind === AstItemKind.Package) { - // Don't warn about items that failed to parse. - if (!this.documentation.failedToParse) { - if (this.context.validationRules.missingReleaseTags === 'error') { - // If there is no release tag, and this is a top-level export of the package, then - // report an error - this.reportError(`A release tag (@alpha, @beta, @public, @internal) must be specified` - + ` for ${this.name}`); - } - } - - // If the release tag was not specified for a top-level export, then it defaults - // to @public (even if we reported an error above) - this.documentation.releaseTag = ReleaseTag.Public; - } - } - } - - /** - * This is called by AstItems to visit the types that appear in an expression. For example, - * if a Public API function returns a class that is defined in this package, but not exported, - * this is a problem. visitTypeReferencesForNode() finds all TypeReference child nodes under the - * specified node and analyzes each one. - */ - protected visitTypeReferencesForNode(node: ts.Node): void { - if (node.kind === ts.SyntaxKind.Block || - (node.kind >= ts.SyntaxKind.JSDocTypeExpression && node.kind <= ts.SyntaxKind.NeverKeyword)) { - // Don't traverse into code blocks or JSDoc items; we only care about the function signature - return; - } - - if (node.kind === ts.SyntaxKind.TypeReference) { - const typeReference: ts.TypeReferenceNode = node as ts.TypeReferenceNode; - this._analyzeTypeReference(typeReference); - } - - // Recurse the tree - for (const childNode of node.getChildren()) { - this.visitTypeReferencesForNode(childNode); - } - } - - /** - * This is a helper for visitTypeReferencesForNode(). It analyzes a single TypeReferenceNode. - */ - private _analyzeTypeReference(typeReferenceNode: ts.TypeReferenceNode): void { - const symbol: ts.Symbol | undefined = this.context.typeChecker.getSymbolAtLocation(typeReferenceNode.typeName); - if (!symbol) { - // Is this bad? - return; - } - - if (symbol.flags & ts.SymbolFlags.TypeParameter) { - // Don't analyze e.g. "T" in "Set" - return; - } - - // Follow the aliases all the way to the ending SourceFile - const currentSymbol: ts.Symbol = TypeScriptHelpers.followAliases(symbol, this.typeChecker); - - if (!currentSymbol.declarations || !currentSymbol.declarations.length) { - // This is a degenerate case that happens sometimes - return; - } - const sourceFile: ts.SourceFile = currentSymbol.declarations[0].getSourceFile(); - - // Walk upwards from that directory until you find a directory containing package.json, - // this is where the referenced type is located. - // Example: "c:\users\\sp-client\spfx-core\sp-core-library" - const typeReferencePackageJson: IPackageJson | undefined = this.context.packageJsonLookup - .tryLoadPackageJsonFor(sourceFile.fileName); - // Example: "@microsoft/sp-core-library" - let typeReferencePackageName: string = ''; - - // If we can not find a package path, we consider the type to be part of the current project's package. - // One case where this happens is when looking for a type that is a symlink - if (!typeReferencePackageJson) { - typeReferencePackageName = this.context.package.name; - } else { - typeReferencePackageName = typeReferencePackageJson.name; - - typingsScopeNames.every(typingScopeName => { - if (typeReferencePackageName.indexOf(typingScopeName) > -1) { - typeReferencePackageName = typeReferencePackageName.replace(typingScopeName + '/', ''); - // returning true breaks the every loop - return true; - } - return false; - }); - } - - // Read the name/version from package.json -- that tells you what package the symbol - // belongs to. If it is your own AstPackage.name/version, then you know it's a local symbol. - const currentPackageName: string = this.context.package.name; - - const typeName: string = typeReferenceNode.typeName.getText(); - if (!typeReferencePackageJson || typeReferencePackageName === currentPackageName) { - // The type is defined in this project. Did the person remember to export it? - const exportedLocalName: string | undefined = this.context.package.tryGetExportedSymbolName(currentSymbol); - if (exportedLocalName) { - // [CASE 1] Local/Exported - // Yes; the type is properly exported. - // TODO: In the future, here we can check for issues such as a @public type - // referencing an @internal type. - return; - } else { - // [CASE 2] Local/Unexported - // No; issue a warning - this.reportWarning(`The type "${typeName}" needs to be exported by the package` - + ` (e.g. added to index.ts)`); - return; - } - } - - // External - // Attempt to load from docItemLoader - const parsedPackageName: IParsedPackageName = PackageName.parse( - typeReferencePackageName - ); - const apiDefinitionRefParts: IApiDefinitionReferenceParts = { - scopeName: parsedPackageName.scope, - packageName: parsedPackageName.unscopedName, - exportName: '', - memberName: '' - }; - - // the currentSymbol.name is the name of an export, if it contains a '.' then the substring - // after the period is the member name - if (currentSymbol.name.indexOf('.') > -1) { - const exportMemberName: string[] = currentSymbol.name.split('.'); - apiDefinitionRefParts.exportName = exportMemberName.pop() || ''; - apiDefinitionRefParts.memberName = exportMemberName.pop() || ''; - } else { - apiDefinitionRefParts.exportName = currentSymbol.name; - } - - const apiDefinitionRef: ApiDefinitionReference = ApiDefinitionReference.createFromParts( - apiDefinitionRefParts - ); - - // Attempt to resolve the type by checking the node modules - const referenceResolutionWarnings: string[] = []; - const resolvedAstItem: ResolvedApiItem | undefined = this.context.docItemLoader.resolveJsonReferences( - apiDefinitionRef, - referenceResolutionWarnings - ); - - if (resolvedAstItem) { - // [CASE 3] External/Resolved - // This is a reference to a type from an external package, and it was resolved. - return; - } else { - // [CASE 4] External/Unresolved - // For cases when we can't find the external package, we are going to write a report - // at the bottom of the *api.ts file. We do this because we do not yet support references - // to items like react:Component. - // For now we are going to silently ignore these errors. - return; - } - } -} diff --git a/apps/api-extractor/src/ast/AstItemContainer.ts b/apps/api-extractor/src/ast/AstItemContainer.ts deleted file mode 100644 index e432c00f50a..00000000000 --- a/apps/api-extractor/src/ast/AstItemContainer.ts +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -// See LICENSE in the project root for license information. - -import { AstItem, IAstItemOptions } from './AstItem'; - -/** - * This is an abstract base class for AstModule, AstEnum, and AstStructuredType, - * which all act as containers for other AstItem definitions. - */ -export abstract class AstItemContainer extends AstItem { - private _memberItems: Map = new Map(); - - constructor(options: IAstItemOptions) { - super(options); - } - - /** - * Find a member in this namespace by name and return it if found. - * - * @param memberName - the name of the exported AstItem - */ - public getMemberItem(memberName: string): AstItem | undefined { - return this._memberItems.get(memberName); - } - - /** - * Return a list of the child items for this container, sorted alphabetically. - */ - public getSortedMemberItems(): AstItem[] { - const astItems: AstItem[] = []; - this._memberItems.forEach((astItem: AstItem) => { - astItems.push(astItem); - }); - - return astItems - .sort((a: AstItem, b: AstItem) => a.name.localeCompare(b.name)); - } - - /** - * @virtual - */ - public visitTypeReferencesForAstItem(): void { - super.visitTypeReferencesForAstItem(); - - this._memberItems.forEach((astItem) => { - astItem.visitTypeReferencesForAstItem(); - }); - } - - /** - * Add a child item to the container. - */ - protected addMemberItem(astItem: AstItem): void { - if (astItem.hasAnyIncompleteTypes()) { - this.reportWarning(`${astItem.name} has incomplete type information`); - } else { - this.innerItems.push(astItem); - this._memberItems.set(astItem.name, astItem); - astItem.notifyAddedToContainer(this); - } - } -} diff --git a/apps/api-extractor/src/ast/AstMember.ts b/apps/api-extractor/src/ast/AstMember.ts deleted file mode 100644 index ab83c16fe82..00000000000 --- a/apps/api-extractor/src/ast/AstMember.ts +++ /dev/null @@ -1,114 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -// See LICENSE in the project root for license information. - -import * as ts from 'typescript'; -import { Text } from '@microsoft/node-core-library'; -import { AstItem, IAstItemOptions } from './AstItem'; -import { AstStructuredType } from './AstStructuredType'; -import { PrettyPrinter } from '../utils/PrettyPrinter'; -import { TypeScriptHelpers } from '../utils/TypeScriptHelpers'; - -export enum ApiAccessModifier { - Private, - Protected, - Public -} - -/** - * This class is part of the AstItem abstract syntax tree. It represents syntax following - * these types of patterns: - * - * - "someName: SomeTypeName;" - * - "someName?: SomeTypeName;" - * - "someName: { someOtherName: SomeOtherTypeName }", i.e. involving a type literal expression - * - "someFunction(): void;" - * - * AstMember is used to represent members of classes, interfaces, and nested type literal expressions. - */ -export class AstMember extends AstItem { - public accessModifier: ApiAccessModifier; - /** - * True if the member is an optional field value, indicated by a question mark ("?") after the name - */ - public isOptional: boolean; - public isStatic: boolean; - - /** - * The type of the member item, if specified as a type literal expression. Otherwise, - * this field is undefined. - */ - public typeLiteral: AstStructuredType | undefined; - - constructor(options: IAstItemOptions) { - super(options); - - this.typeLiteral = undefined; - - const memberSignature: ts.PropertySignature = this.declaration as ts.PropertySignature; - - this.isOptional = !!memberSignature.questionToken; - - // Modifiers - if (memberSignature.modifiers) { - for (const modifier of memberSignature.modifiers) { - if (modifier.kind === ts.SyntaxKind.PublicKeyword) { - this.accessModifier = ApiAccessModifier.Public; - } else if (modifier.kind === ts.SyntaxKind.ProtectedKeyword) { - this.accessModifier = ApiAccessModifier.Protected; - } else if (modifier.kind === ts.SyntaxKind.PrivateKeyword) { - this.accessModifier = ApiAccessModifier.Private; - } else if (modifier.kind === ts.SyntaxKind.StaticKeyword) { - this.isStatic = true; - } - } - } - - if (memberSignature.type && memberSignature.type.kind === ts.SyntaxKind.TypeLiteral) { - const propertyTypeDeclaration: ts.Declaration = memberSignature.type as ts.Node as ts.Declaration; - const propertyTypeSymbol: ts.Symbol = TypeScriptHelpers.getSymbolForDeclaration(propertyTypeDeclaration); - - const typeLiteralOptions: IAstItemOptions = { - context: this.context, - declaration: propertyTypeDeclaration, - declarationSymbol: propertyTypeSymbol - }; - - this.typeLiteral = new AstStructuredType(typeLiteralOptions); - this.innerItems.push(this.typeLiteral); - } - } - - /** - * @virtual - */ - public visitTypeReferencesForAstItem(): void { - super.visitTypeReferencesForAstItem(); - - if (this.declaration.kind !== ts.SyntaxKind.PropertySignature) { - this.visitTypeReferencesForNode(this.declaration); - } - } - - /** - * Returns a text string such as "someName?: SomeTypeName;", or in the case of a type - * literal expression, returns a text string such as "someName?:". - */ - public getDeclarationLine(property?: {type: string; readonly: boolean}): string { - if (this.typeLiteral || !!property) { - const accessModifier: string | undefined = - this.accessModifier ? ApiAccessModifier[this.accessModifier].toLowerCase() : undefined; - - let result: string = accessModifier ? `${accessModifier} ` : ''; - result += this.isStatic ? 'static ' : ''; - result += property && property.readonly ? 'readonly ' : ''; - result += `${this.name}`; - result += this.isOptional ? '?' : ''; - result += ':'; - result += !this.typeLiteral && property && property.type ? ` ${property.type};` : ''; - return Text.convertToLf(result); - } else { - return PrettyPrinter.getDeclarationSummary(this.declaration); - } - } - -} diff --git a/apps/api-extractor/src/ast/AstMethod.ts b/apps/api-extractor/src/ast/AstMethod.ts deleted file mode 100644 index 1a4b24ced1b..00000000000 --- a/apps/api-extractor/src/ast/AstMethod.ts +++ /dev/null @@ -1,107 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -// See LICENSE in the project root for license information. - -import * as ts from 'typescript'; -import { Text } from '@microsoft/node-core-library'; - -import { IParsedPackageName } from '@microsoft/node-core-library'; -import { AstItem, AstItemKind, IAstItemOptions } from './AstItem'; -import { AstMember } from './AstMember'; -import { AstParameter } from './AstParameter'; -import { TypeScriptHelpers } from '../utils/TypeScriptHelpers'; -import { Markup } from '../markup/Markup'; - -/** - * This class is part of the AstItem abstract syntax tree. It represents functions that are members of - * classes, interfaces, or nested type literal expressions. Unlike AstFunctions, AstMethods can have - * access modifiers (public, private, etc.) or be optional, because they are members of a structured type - * - * @see AstFunction for functions that are defined inside of a package - */ -export class AstMethod extends AstMember { - public readonly returnType: string; - public readonly params: AstParameter[]; - - constructor(options: IAstItemOptions) { - super(options); - - // tslint:disable-next-line:no-bitwise - if ((options.declarationSymbol.flags & ts.SymbolFlags.Constructor) !== 0) { - this.kind = AstItemKind.Constructor; - } else { - this.kind = AstItemKind.Method; - } - - const methodDeclaration: ts.MethodDeclaration = options.declaration as ts.MethodDeclaration; - - // Parameters - if (methodDeclaration.parameters) { - this.params = []; - for (const param of methodDeclaration.parameters) { - const declarationSymbol: ts.Symbol = TypeScriptHelpers.getSymbolForDeclaration(param); - const astParameter: AstParameter = new AstParameter({ - context: this.context, - declaration: param, - declarationSymbol: declarationSymbol - }); - - this.innerItems.push(astParameter); - this.params.push(astParameter); - } - } - - // Return type - if (this.kind !== AstItemKind.Constructor) { - if (methodDeclaration.type) { - this.returnType = Text.convertToLf(methodDeclaration.type.getText()); - } else { - this.returnType = 'any'; - this.hasIncompleteTypes = true; - } - } - } - - protected onCompleteInitialization(): void { - super.onCompleteInitialization(); - - // If this is a class constructor, and if the documentation summary was omitted, then - // we fill in a default summary versus flagging it as "undocumented". - // Generally class constructors have uninteresting documentation. - if (this.kind === AstItemKind.Constructor && this.parentContainer) { - if (this.documentation.summary.length === 0) { - this.documentation.summary.push( - ...Markup.createTextElements('Constructs a new instance of the ')); - - const parsedPackageName: IParsedPackageName = this.context.parsedPackageName; - - const parentParentContainer: AstItem | undefined = this.parentContainer.parentContainer; - if (parentParentContainer && parentParentContainer.kind === AstItemKind.Namespace) { - // This is a temporary workaround to support policies.namespaceSupport === permissive - // until the new AstSymbolTable engine is wired up - this.documentation.summary.push( - Markup.createApiLinkFromText(this.parentContainer.name, { - scopeName: parsedPackageName.scope, - packageName: parsedPackageName.unscopedName, - exportName: parentParentContainer.name, - memberName: this.parentContainer.name - } - ) - ); - } else { - this.documentation.summary.push( - Markup.createApiLinkFromText(this.parentContainer.name, { - scopeName: parsedPackageName.scope, - packageName: parsedPackageName.unscopedName, - exportName: this.parentContainer.name, - memberName: '' - } - ) - ); - } - - this.documentation.summary.push(...Markup.createTextElements(' class')); - } - this.needsDocumentation = false; - } - } -} diff --git a/apps/api-extractor/src/ast/AstModule.ts b/apps/api-extractor/src/ast/AstModule.ts deleted file mode 100644 index 3e5ece40f92..00000000000 --- a/apps/api-extractor/src/ast/AstModule.ts +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -// See LICENSE in the project root for license information. - -/* tslint:disable:no-bitwise */ - -import * as ts from 'typescript'; -import { IAstItemOptions } from './AstItem'; -import { AstItemContainer } from './AstItemContainer'; -import { TypeScriptHelpers } from '../utils/TypeScriptHelpers'; -import { AstStructuredType } from './AstStructuredType'; -import { AstEnum } from './AstEnum'; -import { AstFunction } from './AstFunction'; - -/** - * This is an abstract base class for AstPackage and AstNamespace. - */ -export abstract class AstModule extends AstItemContainer { - - protected processModuleExport(exportSymbol: ts.Symbol): void { - const followedSymbol: ts.Symbol = TypeScriptHelpers.followAliases(exportSymbol, this.typeChecker); - - if (!followedSymbol.declarations) { - // This is an API Extractor bug, but it could happen e.g. if we upgrade to a new - // version of the TypeScript compiler that introduces new AST variations that we - // haven't tested before. - this.reportWarning(`Definition with no declarations: ${exportSymbol.name}`); - return; - } - - for (const declaration of followedSymbol.declarations) { - const options: IAstItemOptions = { - context: this.context, - declaration, - declarationSymbol: followedSymbol, - exportSymbol - }; - - if (followedSymbol.flags & (ts.SymbolFlags.Class | ts.SymbolFlags.Interface)) { - this.addMemberItem(new AstStructuredType(options)); - } else if (followedSymbol.flags & (ts.SymbolFlags.ValueModule | ts.SymbolFlags.NamespaceModule)) { - this.addMemberItem(new AstNamespace(options)); // tslint:disable-line:no-use-before-declare - } else if (followedSymbol.flags & ts.SymbolFlags.Function) { - this.addMemberItem(new AstFunction(options)); - } else if (followedSymbol.flags & ts.SymbolFlags.Enum) { - this.addMemberItem(new AstEnum(options)); - } else { - this.reportWarning(`Unsupported export: ${exportSymbol.name}`); - } - } - } -} - -// This is defer imported to break the circular dependency -import { AstNamespace } from './AstNamespace'; diff --git a/apps/api-extractor/src/ast/AstModuleVariable.ts b/apps/api-extractor/src/ast/AstModuleVariable.ts deleted file mode 100644 index 32fd1299a7c..00000000000 --- a/apps/api-extractor/src/ast/AstModuleVariable.ts +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -// See LICENSE in the project root for license information. - -import * as ts from 'typescript'; -import { AstItemKind, IAstItemOptions } from './AstItem'; -import { AstMember } from './AstMember'; - -/** - * This class is part of the AstItem abstract syntax tree. It represents variables - * that are exported by an AstNamespace (or conceivably an AstPackage in the future). - * The variables have a name, a type, and an initializer. The AstNamespace implementation - * currently requires them to use a primitive type and be declared as "const". - */ -export class AstModuleVariable extends AstMember { - public type: string; - public name: string; - public value: string; - - constructor(options: IAstItemOptions) { - super(options); - this.kind = AstItemKind.ModuleVariable; - - const propertySignature: ts.PropertySignature = options.declaration as ts.PropertySignature; - if (propertySignature.type) { - this.type = propertySignature.type.getText(); - } else { - this.type = ''; - } - - this.name = propertySignature.name.getText(); - - if (propertySignature.initializer) { - this.value = propertySignature.initializer.getText(); // value of the export - } else { - this.value = ''; - } - } -} diff --git a/apps/api-extractor/src/ast/AstNamespace.ts b/apps/api-extractor/src/ast/AstNamespace.ts deleted file mode 100644 index 46424f690ee..00000000000 --- a/apps/api-extractor/src/ast/AstNamespace.ts +++ /dev/null @@ -1,136 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -// See LICENSE in the project root for license information. - -/* tslint:disable:no-bitwise */ - -import * as ts from 'typescript'; -import { AstModuleVariable } from './AstModuleVariable'; -import { AstItemKind, IAstItemOptions } from './AstItem'; -import { IExportedSymbol } from './IExportedSymbol'; -import { AstModule } from './AstModule'; -import { TypeScriptHelpers } from '../utils/TypeScriptHelpers'; - -const allowedTypes: string[] = ['string', 'number', 'boolean']; - -/** - * This class is part of the AstItem abstract syntax tree. It represents exports of - * a namespace, the exports can be module variable constants of type "string", "boolean" or "number". - * An AstNamespace is defined using TypeScript's "namespace" keyword. - * - * @remarks A note about terminology: - * - EcmaScript "namespace modules" are not conventional namespaces; their semantics are - * more like static classes in C# or Java. - * - API Extractor's support for namespaces is currently limited to representing tables of - * constants, and has a benefit of enabling WebPack to avoid bundling unused values. - * - We currently still recommend to use static classes for utility libraries, since this - * provides getters/setters, public/private, and some other structure missing from namespaces. - */ -export class AstNamespace extends AstModule { - private _exportedNormalizedSymbols: IExportedSymbol[] = []; - - constructor(options: IAstItemOptions) { - super(options); - this.kind = AstItemKind.Namespace; - - // NOTE: For this.name, we keep the default this.exportSymbol.name because when we used - // options.declarationSymbol.name, this case was mishandled: - // - // import { sub } from './sub'; export { sub }; - // - // For details, see: https://github.com/Microsoft/web-build-tools/pull/773 - - const exportSymbols: ts.Symbol[] = this.typeChecker.getExportsOfModule(this.declarationSymbol); - if (exportSymbols) { - if (this.context.policies.namespaceSupport === 'conservative') { - this._processConservativeMembers(exportSymbols); - } else { - this._processPermissiveMembers(exportSymbols); - } - } - } - - // Used when policies.namespaceSupport=conservative - private _processConservativeMembers(exportSymbols: ts.Symbol[]): void { - for (const exportSymbol of exportSymbols) { - const followedSymbol: ts.Symbol = TypeScriptHelpers.followAliases(exportSymbol, this.typeChecker); - - if (!followedSymbol.declarations) { - // This is an API Extractor bug, but it could happen e.g. if we upgrade to a new - // version of the TypeScript compiler that introduces new AST variations that we - // haven't tested before. - this.reportWarning(`The definition "${exportSymbol.name}" has no declarations`); - continue; - } - - if (!(followedSymbol.flags === ts.SymbolFlags.BlockScopedVariable)) { - this.reportWarning(`Unsupported export "${exportSymbol.name}" ` + - 'Currently the "namespace" block only supports constant variables.'); - continue; - } - - // Since we are imposing that the items within a namespace be - // const properties we are only taking the first declaration. - // If we decide to add support for other types within a namespace - // we will have for evaluate each declaration. - const declarations: ts.Declaration[] | undefined = followedSymbol.getDeclarations(); - if (!declarations) { - throw new Error('Missing declaration'); - } - - const declaration: ts.Declaration = declarations[0]; - - if (declaration.parent && (declaration.parent.flags & ts.NodeFlags.Const) === 0) { - this.reportWarning(`Export "${exportSymbol.name}" is missing the "const" ` + - 'modifier. Currently the "namespace" block only supports constant variables.'); - continue; - } - - const propertySignature: ts.PropertySignature = declaration as ts.PropertySignature; - - if (!propertySignature.type) { - this.reportWarning(`Export "${exportSymbol.name}" must specify a type`); - continue; - } - - // Note that we also allow type references that refer to one of the supported primitive types - if (propertySignature.type.kind !== ts.SyntaxKind.TypeReference - && allowedTypes.indexOf(propertySignature.type.getText()) < 0) { - this.reportWarning(`Export "${exportSymbol.name}" must be of type` + - ' "string", "number" or "boolean" when API Extractor is configured for conservative namespaces'); - continue; - } - - // Typescript's VariableDeclaration AST nodes have an VariableDeclarationList parent, - // and the VariableDeclarationList exists within a VariableStatement, which is where - // the JSDoc comment Node can be found. - // If there is no parent or grandparent of this VariableDeclaration then - // we do not know how to obtain the JSDoc comment. - if (!declaration.parent || !declaration.parent.parent || - declaration.parent.parent.kind !== ts.SyntaxKind.VariableStatement) { - this.reportWarning(`Unable to locate the documentation node for "${exportSymbol.name}"; ` - + `this may be an API Extractor bug`); - } - - const exportMemberOptions: IAstItemOptions = { - context: this.context, - declaration, - declarationSymbol: followedSymbol, - exportSymbol - }; - - this.addMemberItem(new AstModuleVariable(exportMemberOptions)); - - this._exportedNormalizedSymbols.push({ - exportedName: exportSymbol.name, - followedSymbol: followedSymbol - }); - } - } - - // Used when policies.namespaceSupport=permissive - private _processPermissiveMembers(exportSymbols: ts.Symbol[]): void { - for (const exportSymbol of exportSymbols) { - this.processModuleExport(exportSymbol); - } - } -} diff --git a/apps/api-extractor/src/ast/AstPackage.ts b/apps/api-extractor/src/ast/AstPackage.ts deleted file mode 100644 index bfb711d457b..00000000000 --- a/apps/api-extractor/src/ast/AstPackage.ts +++ /dev/null @@ -1,135 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -// See LICENSE in the project root for license information. - -/* tslint:disable:no-bitwise */ - -import * as ts from 'typescript'; -import { ExtractorContext } from '../ExtractorContext'; -import { AstItemKind, IAstItemOptions } from './AstItem'; -import { AstModule } from './AstModule'; -import { TypeScriptHelpers } from '../utils/TypeScriptHelpers'; -import { IExportedSymbol } from './IExportedSymbol'; - -/** - * This class is part of the AstItem abstract syntax tree. It represents the top-level - * exports for an Rush package. This object acts as the root of the Extractor's tree. - */ -export class AstPackage extends AstModule { - private _exportedNormalizedSymbols: IExportedSymbol[] = []; - - private static _getOptions(context: ExtractorContext, rootFile: ts.SourceFile): IAstItemOptions { - const rootFileSymbol: ts.Symbol | undefined = TypeScriptHelpers.tryGetSymbolForDeclaration(rootFile); - - if (!rootFileSymbol) { - throw new Error('The entry point file does not appear to have any exports:\n' + rootFile.fileName - + '\nNote that API Extractor does not yet support libraries consisting entirely of ambient types.'); - } - - if (!rootFileSymbol.declarations) { - throw new Error('Unable to find a root declaration for this package'); - } - - // The @packagedocumentation comment is special because it is not attached to an AST - // definition. Instead, it is part of the "trivia" tokens that the compiler treats - // as irrelevant white space. - // - // WARNING: If the comment doesn't precede an export statement, the compiler will omit - // it from the *.d.ts file, and API Extractor won't find it. If this happens, you need - // to rearrange your statements to ensure it is passed through. - // - // This implementation assumes that the "@packagedocumentation" will be in the first JSDoc-comment - // that appears in the entry point *.d.ts file. We could possibly look in other places, - // but the above warning suggests enforcing a standardized layout. This design choice is open - // to feedback. - let packageCommentRange: ts.TextRange | undefined = undefined; // empty string - - for (const commentRange of ts.getLeadingCommentRanges(rootFile.text, rootFile.getFullStart()) || []) { - if (commentRange.kind === ts.SyntaxKind.MultiLineCommentTrivia) { - const commentBody: string = rootFile.text.substring(commentRange.pos, commentRange.end); - - // Choose the first JSDoc-style comment - if (/^\s*\/\*\*/.test(commentBody)) { - // But onliy if it looks like it's trying to be @packagedocumentation - // (The ApiDocumentation parser will validate this more rigorously) - if (commentBody.indexOf('@packagedocumentation') >= 0) { - packageCommentRange = commentRange; - } - break; - } - } - } - - if (!packageCommentRange) { - // If we didn't find the @packagedocumentation tag in the expected place, is it in some - // wrong place? This sanity check helps people to figure out why there comment isn't working. - for (const statement of rootFile.statements) { - const ranges: ts.CommentRange[] = []; - ranges.push(...ts.getLeadingCommentRanges(rootFile.text, statement.getFullStart()) || []); - ranges.push(...ts.getTrailingCommentRanges(rootFile.text, statement.getEnd()) || []); - - for (const commentRange of ranges) { - const commentBody: string = rootFile.text.substring(commentRange.pos, commentRange.end); - - if (commentBody.indexOf('@packagedocumentation') >= 0) { - context.reportError('The @packagedocumentation comment must appear at the top of entry point *.d.ts file', - rootFile, commentRange.pos); - } - } - } - } - - return { - context, - declaration: rootFileSymbol.declarations[0], - declarationSymbol: rootFileSymbol, - // NOTE: If there is no range, then provide an empty range to prevent ApiItem from - // looking in the default place - aedocCommentRange: packageCommentRange || { pos: 0, end: 0 } - }; - } - - constructor(context: ExtractorContext, rootFile: ts.SourceFile) { - super(AstPackage._getOptions(context, rootFile)); - this.kind = AstItemKind.Package; - // The scoped package name. (E.g. "@microsoft/api-extractor") - this.name = context.packageName; - - const exportSymbols: ts.Symbol[] = this.typeChecker.getExportsOfModule(this.declarationSymbol) || []; - - for (const exportSymbol of exportSymbols) { - this.processModuleExport(exportSymbol); - - const followedSymbol: ts.Symbol = TypeScriptHelpers.followAliases(exportSymbol, this.typeChecker); - this._exportedNormalizedSymbols.push({ - exportedName: exportSymbol.name, - followedSymbol: followedSymbol - }); - } - } - - /** - * Finds and returns the original symbol name. - * - * For example, suppose a class is defined as "export default class MyClass { }" - * but exported from the package's index.ts like this: - * - * export { default as _MyClass } from './MyClass'; - * - * In this example, given the symbol for _MyClass, getExportedSymbolName() will return - * the string "MyClass". - */ - public tryGetExportedSymbolName(symbol: ts.Symbol): string | undefined { - const followedSymbol: ts.Symbol = TypeScriptHelpers.followAliases(symbol, this.typeChecker); - for (const exportedSymbol of this._exportedNormalizedSymbols) { - if (exportedSymbol.followedSymbol === followedSymbol) { - return exportedSymbol.exportedName; - } - } - return undefined; - } - - public shouldHaveDocumentation(): boolean { - // We don't write JSDoc for the AstPackage object - return false; - } -} diff --git a/apps/api-extractor/src/ast/AstParameter.ts b/apps/api-extractor/src/ast/AstParameter.ts deleted file mode 100644 index b9051a2b8ab..00000000000 --- a/apps/api-extractor/src/ast/AstParameter.ts +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -// See LICENSE in the project root for license information. - -import * as ts from 'typescript'; -import { Text } from '@microsoft/node-core-library'; - -import { AstItem, AstItemKind, IAstItemOptions } from './AstItem'; - -/** - * This class is part of the AstItem abstract syntax tree. It represents parameters of a function declaration - */ -export class AstParameter extends AstItem { - public isOptional: boolean; - public type: string; - - /** - * If there is a spread operator before the parameter declaration - * Example: foo(...params: string[]) - */ - public isSpread: boolean; - - constructor(options: IAstItemOptions, docComment?: string) { - super(options); - this.kind = AstItemKind.Parameter; - - const parameterDeclaration: ts.ParameterDeclaration = options.declaration as ts.ParameterDeclaration; - this.isOptional = !!parameterDeclaration.questionToken || !!parameterDeclaration.initializer; - if (parameterDeclaration.type) { - this.type = Text.convertToLf(parameterDeclaration.type.getText()); - } else { - this.hasIncompleteTypes = true; - this.type = 'any'; - } - - this.isSpread = !!parameterDeclaration.dotDotDotToken; - } -} diff --git a/apps/api-extractor/src/ast/AstProperty.ts b/apps/api-extractor/src/ast/AstProperty.ts deleted file mode 100644 index e1fa8add6e1..00000000000 --- a/apps/api-extractor/src/ast/AstProperty.ts +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -// See LICENSE in the project root for license information. - -import * as ts from 'typescript'; -import { Text } from '@microsoft/node-core-library'; - -import { AstItemKind, IAstItemOptions } from './AstItem'; -import { AstMember } from './AstMember'; - -/** - * This class is part of the AstItem abstract syntax tree. It represents properties of classes or interfaces - * (It does not represent member methods) - */ -export class AstProperty extends AstMember { - public type: string; - public isStatic: boolean; - public isReadOnly: boolean; - public isEventProperty: boolean; - - constructor(options: IAstItemOptions) { - super(options); - this.kind = AstItemKind.Property; - - const declaration: ts.PropertyDeclaration = options.declaration as ts.PropertyDeclaration; - if (declaration.type) { - this.type = Text.convertToLf(declaration.type.getText()); - } else { - this.hasIncompleteTypes = true; - this.type = 'any'; - } - - if (this.documentation.hasReadOnlyTag) { - this.isReadOnly = true; - } else { - // Check for a readonly modifier - for (const modifier of declaration.modifiers || []) { - if (modifier.kind === ts.SyntaxKind.ReadonlyKeyword) { - this.isReadOnly = true; - } - } - } - - this.isEventProperty = this.documentation.isEventProperty || false; - if (this.isEventProperty && !this.isReadOnly) { - this.reportWarning('The @eventProperty tag requires the property to be readonly'); - } - } - - public getDeclarationLine(): string { - return super.getDeclarationLine({ - type: this.type, - readonly: this.isReadOnly - }); - } -} diff --git a/apps/api-extractor/src/ast/AstStructuredType.ts b/apps/api-extractor/src/ast/AstStructuredType.ts deleted file mode 100644 index 1d6103dd40a..00000000000 --- a/apps/api-extractor/src/ast/AstStructuredType.ts +++ /dev/null @@ -1,228 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -// See LICENSE in the project root for license information. - -/* tslint:disable:no-bitwise */ - -import * as ts from 'typescript'; -import { Text } from '@microsoft/node-core-library'; - -import { ReleaseTag } from '../aedoc/ReleaseTag'; -import { Markup } from '../markup/Markup'; -import { AstMethod } from './AstMethod'; -import { AstProperty } from './AstProperty'; -import { AstItemKind, IAstItemOptions } from './AstItem'; -import { AstItemContainer } from './AstItemContainer'; -import { TypeScriptHelpers } from '../utils/TypeScriptHelpers'; -import { PrettyPrinter } from '../utils/PrettyPrinter'; - -/** - * This class is part of the AstItem abstract syntax tree. It represents a class, - * interface, or type literal expression. - */ -export class AstStructuredType extends AstItemContainer { - public implements?: string; - public extends?: string; - - /** - * An array of type parameters for generic classes - * Example: Foo => ['T', 'S'] - */ - public typeParameters: string[]; - - /** - * The data type of the AstItem.declarationSymbol. This is not the exported alias, - * but rather the original that has complete member and inheritance information. - */ - protected type: ts.Type; - - private _classLikeDeclaration: ts.ClassLikeDeclaration; - private _processedMemberNames: Set = new Set(); - private _setterNames: Set = new Set(); - - constructor(options: IAstItemOptions) { - super(options); - - this._classLikeDeclaration = options.declaration as ts.ClassLikeDeclaration; - this.type = this.typeChecker.getDeclaredTypeOfSymbol(this.declarationSymbol); - - if (this.declarationSymbol.flags & ts.SymbolFlags.Interface) { - this.kind = AstItemKind.Interface; - } else if (this.declarationSymbol.flags & ts.SymbolFlags.TypeLiteral) { - this.kind = AstItemKind.TypeLiteral; - } else { - this.kind = AstItemKind.Class; - } - - for (const memberDeclaration of this._classLikeDeclaration.members || []) { - const memberSymbol: ts.Symbol = TypeScriptHelpers.getSymbolForDeclaration(memberDeclaration); - if (memberSymbol) { - this._processMember(memberSymbol, memberDeclaration); - } else { - // If someone put an extra semicolon after their function, we don't care about that - if (memberDeclaration.kind !== ts.SyntaxKind.SemicolonClassElement) { - // If there is some other non-semantic junk, add a warning so we can investigate it - this.reportWarning(PrettyPrinter.formatFileAndLineNumber(memberDeclaration) - + `: No semantic information for "${memberDeclaration.getText()}"`); - } - } - } - - // If there is a getter and no setter, mark it as readonly. - for (const member of this.getSortedMemberItems()) { - const memberSymbol: ts.Symbol = TypeScriptHelpers.getSymbolForDeclaration(member.getDeclaration()); - if (memberSymbol && (memberSymbol.flags === ts.SymbolFlags.GetAccessor)) { - if (!this._setterNames.has(member.name)) { - (member as AstProperty).isReadOnly = true; - } - } - } - - // Check for heritage clauses (implements and extends) - if (this._classLikeDeclaration.heritageClauses) { - for (const heritage of this._classLikeDeclaration.heritageClauses) { - - const typeText: string | undefined = heritage.types && heritage.types.length - && heritage.types[0].expression - ? heritage.types[0].expression.getText() : undefined; - - if (heritage.token === ts.SyntaxKind.ExtendsKeyword) { - this.extends = typeText; - } else if (heritage.token === ts.SyntaxKind.ImplementsKeyword) { - this.implements = typeText; - } - } - } - - // Check for type parameters - if (this._classLikeDeclaration.typeParameters && this._classLikeDeclaration.typeParameters.length) { - if (!this.typeParameters) { - this.typeParameters = []; - } - for (const param of this._classLikeDeclaration.typeParameters) { - this.typeParameters.push(param.getText()); - } - } - - // Throw errors for setters that don't have a corresponding getter - this._setterNames.forEach((setterName: string) => { - if (!this.getMemberItem(setterName)) { - // Normally we treat API design changes as warnings rather than errors. However, - // a missing getter is bizarre enough that it's reasonable to assume it's a mistake, - // not a conscious design choice. - this.reportError(`The "${setterName}" property has a setter, but no a getter`); - } - }); - } - - /** - * @virtual - */ - public visitTypeReferencesForAstItem(): void { - super.visitTypeReferencesForAstItem(); - - // Collect type references from the base classes - if (this._classLikeDeclaration && this._classLikeDeclaration.heritageClauses) { - for (const clause of this._classLikeDeclaration.heritageClauses) { - this.visitTypeReferencesForNode(clause); - } - } - } - - /** - * Returns a line of text such as "class MyClass extends MyBaseClass", excluding the - * curly braces and body. The name "MyClass" will be the public name seen by external - * callers, not the declared name of the class; @see AstItem.name documentation for details. - */ - public getDeclarationLine(): string { - let result: string = ''; - - if (this.kind !== AstItemKind.TypeLiteral) { - result += (this.declarationSymbol.flags & ts.SymbolFlags.Interface) - ? 'interface ' : 'class '; - - result += this.name; - - if (this._classLikeDeclaration.typeParameters) { - result += '<'; - - result += this._classLikeDeclaration.typeParameters - .map((param: ts.TypeParameterDeclaration) => param.getText()) - .join(', '); - - result += '>'; - } - - if (this._classLikeDeclaration.heritageClauses) { - result += ' '; - result += this._classLikeDeclaration.heritageClauses - .map((clause: ts.HeritageClause) => clause.getText()) - .join(', '); - } - } - return Text.convertToLf(result); - } - - protected onCompleteInitialization(): void { - super.onCompleteInitialization(); - - // Is the constructor internal? - for (const member of this.getSortedMemberItems()) { - if (member.kind === AstItemKind.Constructor) { - if (member.documentation.releaseTag === ReleaseTag.Internal) { - // Add a boilerplate notice for classes with internal constructors - this.documentation.remarks.unshift( - ...Markup.createTextElements(`The constructor for this class is marked as internal. Third-party code` - + ` should not call the constructor directly or create subclasses that extend the ${this.name} class.`), - Markup.PARAGRAPH - ); - } - } - } - } - - private _processMember(memberSymbol: ts.Symbol, memberDeclaration: ts.Declaration): void { - if (memberDeclaration.modifiers) { - for (let i: number = 0; i < memberDeclaration.modifiers.length; i++ ) { - const modifier: ts.Modifier = memberDeclaration.modifiers[i]; - if (modifier.kind === ts.SyntaxKind.PrivateKeyword) { - return; - } - } - } - - if (this._processedMemberNames.has(memberSymbol.name)) { - if (memberSymbol.flags === ts.SymbolFlags.SetAccessor) { - // In case of setters, just add them to a list to check later if they have a getter - this._setterNames.add(memberSymbol.name); - } - // Throw an error for duplicate names, because we use names as identifiers - // @todo #261549 Define an AEDoc tag to allow defining an identifier for overloaded methods eg. @overload method2 - return; - } - - // Proceed to add the member - this._processedMemberNames.add(memberSymbol.name); - - const memberOptions: IAstItemOptions = { - context: this.context, - declaration: memberDeclaration, - declarationSymbol: memberSymbol - }; - - if (memberSymbol.flags & ( - ts.SymbolFlags.Method | - ts.SymbolFlags.Constructor | - ts.SymbolFlags.Signature | - ts.SymbolFlags.Function - )) { - this.addMemberItem(new AstMethod(memberOptions)); - } else if (memberSymbol.flags & ( - ts.SymbolFlags.Property | - ts.SymbolFlags.GetAccessor - )) { - this.addMemberItem(new AstProperty(memberOptions)); - } else { - this.reportWarning(`Unsupported member: ${memberSymbol.name}`); - } - } -} diff --git a/apps/api-extractor/src/ast/IExportedSymbol.ts b/apps/api-extractor/src/ast/IExportedSymbol.ts deleted file mode 100644 index 2ad7db370c6..00000000000 --- a/apps/api-extractor/src/ast/IExportedSymbol.ts +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -// See LICENSE in the project root for license information. - -import * as ts from 'typescript'; - -/** - * An export name and the symbol from which the export was originally defined. - * - * For example, suppose a class is defined as "export default class MyClass { }" - * but exported from the package's index.ts like this: - * - * export { default as _MyClass } from './MyClass'; - * - * In this example, the exportedName is _MyClass and the followed symbol will be the - * original definition of MyClass. - */ -export interface IExportedSymbol { - exportedName: string; - followedSymbol: ts.Symbol; -} \ No newline at end of file diff --git a/apps/api-extractor/src/cli/RunAction.ts b/apps/api-extractor/src/cli/RunAction.ts index 90f061e675e..3b7f41b2a5d 100644 --- a/apps/api-extractor/src/cli/RunAction.ts +++ b/apps/api-extractor/src/cli/RunAction.ts @@ -16,8 +16,8 @@ import { CommandLineFlagParameter } from '@microsoft/ts-command-line'; -import { Extractor } from '../extractor/Extractor'; -import { IExtractorConfig } from '../extractor/IExtractorConfig'; +import { Extractor } from '../api/Extractor'; +import { IExtractorConfig } from '../api/IExtractorConfig'; import { ApiExtractorCommandLine } from './ApiExtractorCommandLine'; diff --git a/apps/api-extractor/src/collector/Collector.ts b/apps/api-extractor/src/collector/Collector.ts new file mode 100644 index 00000000000..2d405b63b59 --- /dev/null +++ b/apps/api-extractor/src/collector/Collector.ts @@ -0,0 +1,553 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import * as ts from 'typescript'; +import * as path from 'path'; +import * as tsdoc from '@microsoft/tsdoc'; +import { + PackageJsonLookup, + IPackageJson, + Sort +} from '@microsoft/node-core-library'; + +import { ILogger } from '../api/ILogger'; +import { + IExtractorPoliciesConfig, + IExtractorValidationRulesConfig, + ExtractorValidationRulePolicy +} from '../api/IExtractorConfig'; +import { TypeScriptMessageFormatter } from '../analyzer/TypeScriptMessageFormatter'; +import { CollectorEntity } from './CollectorEntity'; +import { AstSymbolTable } from '../analyzer/AstSymbolTable'; +import { AstEntryPoint } from '../analyzer/AstEntryPoint'; +import { AstSymbol } from '../analyzer/AstSymbol'; +import { ReleaseTag } from '../aedoc/ReleaseTag'; +import { AstDeclaration } from '../analyzer/AstDeclaration'; +import { TypeScriptHelpers } from '../analyzer/TypeScriptHelpers'; +import { CollectorPackage } from './CollectorPackage'; +import { PackageDocComment } from '../aedoc/PackageDocComment'; +import { DeclarationMetadata } from './DeclarationMetadata'; +import { SymbolMetadata } from './SymbolMetadata'; + +/** + * Options for Collector constructor. + */ +export interface ICollectorOptions { + /** + * Configuration for the TypeScript compiler. The most important options to set are: + * + * - target: ts.ScriptTarget.ES5 + * - module: ts.ModuleKind.CommonJS + * - moduleResolution: ts.ModuleResolutionKind.NodeJs + * - rootDir: inputFolder + */ + program: ts.Program; + + /** + * The entry point for the project. This should correspond to the "main" field + * from NPM's package.json file. If it is a relative path, it will be relative to + * the project folder described by IExtractorAnalyzeOptions.compilerOptions. + */ + entryPointFile: string; + + logger: ILogger; + + policies: IExtractorPoliciesConfig; + + validationRules: IExtractorValidationRulesConfig; +} + +/** + * The main entry point for the "api-extractor" utility. The Analyzer object invokes the + * TypeScript Compiler API to analyze a project, and constructs the AstItem + * abstract syntax tree. + */ +export class Collector { + public readonly program: ts.Program; + public readonly typeChecker: ts.TypeChecker; + public readonly astSymbolTable: AstSymbolTable; + + public readonly packageJsonLookup: PackageJsonLookup; + + public readonly policies: IExtractorPoliciesConfig; + public readonly validationRules: IExtractorValidationRulesConfig; + + public readonly logger: ILogger; + + public readonly package: CollectorPackage; + + private readonly _program: ts.Program; + + private readonly _tsdocParser: tsdoc.TSDocParser; + + private _astEntryPoint: AstEntryPoint | undefined; + + private readonly _entities: CollectorEntity[] = []; + private readonly _entitiesByAstSymbol: Map = new Map(); + private readonly _entitiesBySymbol: Map = new Map(); + + private readonly _dtsTypeReferenceDirectives: Set = new Set(); + private readonly _dtsLibReferenceDirectives: Set = new Set(); + + constructor(options: ICollectorOptions) { + this.packageJsonLookup = new PackageJsonLookup(); + + this.policies = options.policies; + this.validationRules = options.validationRules; + + this.logger = options.logger; + this._program = options.program; + + const packageFolder: string | undefined = this.packageJsonLookup.tryGetPackageFolderFor(options.entryPointFile); + if (!packageFolder) { + throw new Error('Unable to find a package.json for entry point: ' + options.entryPointFile); + } + + const packageJson: IPackageJson = this.packageJsonLookup.tryLoadPackageJsonFor(packageFolder)!; + + const entryPointSourceFile: ts.SourceFile | undefined = options.program.getSourceFile(options.entryPointFile); + if (!entryPointSourceFile) { + throw new Error('Unable to load file: ' + options.entryPointFile); + } + + this.package = new CollectorPackage({ + packageFolder, + packageJson, + entryPointSourceFile + }); + + this.program = options.program; + this.typeChecker = options.program.getTypeChecker(); + + this._tsdocParser = new tsdoc.TSDocParser(); + this.astSymbolTable = new AstSymbolTable(this.program, this.typeChecker, this.packageJsonLookup, this.logger); + } + + /** + * Returns a list of names (e.g. "example-library") that should appear in a reference like this: + * + * ``` + * /// + * ``` + */ + public get dtsTypeReferenceDirectives(): ReadonlySet { + return this._dtsTypeReferenceDirectives; + } + + /** + * A list of names (e.g. "runtime-library") that should appear in a reference like this: + * + * ``` + * /// + * ``` + */ + public get dtsLibReferenceDirectives(): ReadonlySet { + return this._dtsLibReferenceDirectives; + } + + public get entities(): ReadonlyArray { + return this._entities; + } + + /** + * Perform the analysis. + */ + public analyze(): void { + if (this._astEntryPoint) { + throw new Error('DtsRollupGenerator.analyze() was already called'); + } + + // This runs a full type analysis, and then augments the Abstract Syntax Tree (i.e. declarations) + // with semantic information (i.e. symbols). The "diagnostics" are a subset of the everyday + // compile errors that would result from a full compilation. + for (const diagnostic of this._program.getSemanticDiagnostics()) { + const errorText: string = TypeScriptMessageFormatter.format(diagnostic.messageText); + this.reportError(`TypeScript: ${errorText}`, diagnostic.file, diagnostic.start); + } + + // Build the entry point + const astEntryPoint: AstEntryPoint = this.astSymbolTable.fetchEntryPoint(this.package.entryPointSourceFile); + + const packageDocCommentTextRange: ts.TextRange | undefined = PackageDocComment.tryFindInSourceFile( + this.package.entryPointSourceFile, this); + + if (packageDocCommentTextRange) { + const range: tsdoc.TextRange = tsdoc.TextRange.fromStringRange(this.package.entryPointSourceFile.text, + packageDocCommentTextRange.pos, packageDocCommentTextRange.end); + + this.package.tsdocParserContext = this._tsdocParser.parseRange(range); + this.package.tsdocComment = this.package.tsdocParserContext!.docComment; + } + + const exportedAstSymbols: AstSymbol[] = []; + + // Create a CollectorEntity for each top-level export + for (const exportedMember of astEntryPoint.exportedMembers) { + const astSymbol: AstSymbol = exportedMember.astSymbol; + + this._createEntityForSymbol(exportedMember.astSymbol, exportedMember.name); + + exportedAstSymbols.push(astSymbol); + } + + // Create a CollectorEntity for each indirectly referenced export. + // Note that we do this *after* the above loop, so that references to exported AstSymbols + // are encountered first as exports. + const alreadySeenAstSymbols: Set = new Set(); + for (const exportedAstSymbol of exportedAstSymbols) { + this._createEntityForIndirectReferences(exportedAstSymbol, alreadySeenAstSymbols); + + this.fetchMetadata(exportedAstSymbol); + } + + this._makeUniqueNames(); + + Sort.sortBy(this._entities, x => x.getSortKey()); + Sort.sortSet(this._dtsTypeReferenceDirectives); + Sort.sortSet(this._dtsLibReferenceDirectives); + } + + public tryGetEntityBySymbol(symbol: ts.Symbol): CollectorEntity | undefined { + return this._entitiesBySymbol.get(symbol); + } + + public fetchMetadata(astSymbol: AstSymbol): SymbolMetadata; + public fetchMetadata(astDeclaration: AstDeclaration): DeclarationMetadata; + public fetchMetadata(symbolOrDeclaration: AstSymbol | AstDeclaration): SymbolMetadata | DeclarationMetadata { + if (symbolOrDeclaration.metadata === undefined) { + const astSymbol: AstSymbol = symbolOrDeclaration instanceof AstSymbol + ? symbolOrDeclaration : symbolOrDeclaration.astSymbol; + this._fetchSymbolMetadata(astSymbol); + } + return symbolOrDeclaration.metadata as SymbolMetadata | DeclarationMetadata; + } + + /** + * Removes the leading underscore, for example: "_Example" --> "example*Example*_" + * + * @remarks + * This causes internal definitions to sort alphabetically case-insensitive, then case-sensitive, and + * initially ignoring the underscore prefix, while still deterministically comparing it. + * The star is used as a delimiter because it is not a legal identifier character. + */ + public static getSortKeyIgnoringUnderscore(identifier: string): string { + let parts: string[]; + + if (identifier[0] === '_') { + const withoutUnderscore: string = identifier.substr(1); + parts = [withoutUnderscore.toLowerCase(), '*', withoutUnderscore, '*', '_']; + } else { + parts = [identifier.toLowerCase(), '*', identifier]; + } + + return parts.join(''); + } + + private _createEntityForSymbol(astSymbol: AstSymbol, exportedName: string | undefined): void { + let entity: CollectorEntity | undefined = this._entitiesByAstSymbol.get(astSymbol); + + if (!entity) { + entity = new CollectorEntity({ + astSymbol: astSymbol, + originalName: exportedName || astSymbol.localName, + exported: !!exportedName + }); + + this._entitiesByAstSymbol.set(astSymbol, entity); + this._entitiesBySymbol.set(astSymbol.followedSymbol, entity); + this._entities.push(entity); + + this._collectReferenceDirectives(astSymbol); + } else { + if (exportedName) { + if (!entity.exported) { + throw new Error('Program Bug: CollectorEntity should have been marked as exported'); + } + if (entity.originalName !== exportedName) { + throw new Error(`The symbol ${exportedName} was also exported as ${entity.originalName};` + + ` this is not supported yet`); + } + } + } + } + + private _createEntityForIndirectReferences(astSymbol: AstSymbol, alreadySeenAstSymbols: Set): void { + if (alreadySeenAstSymbols.has(astSymbol)) { + return; + } + alreadySeenAstSymbols.add(astSymbol); + + astSymbol.forEachDeclarationRecursive((astDeclaration: AstDeclaration) => { + for (const referencedAstSymbol of astDeclaration.referencedAstSymbols) { + this._createEntityForSymbol(referencedAstSymbol, undefined); + this._createEntityForIndirectReferences(referencedAstSymbol, alreadySeenAstSymbols); + } + }); + } + + /** + * Ensures a unique name for each item in the package typings file. + */ + private _makeUniqueNames(): void { + const usedNames: Set = new Set(); + + // First collect the explicit package exports + for (const entity of this._entities) { + if (entity.exported) { + + if (usedNames.has(entity.originalName)) { + // This should be impossible + throw new Error(`Program bug: a package cannot have two exports with the name ${entity.originalName}`); + } + + entity.nameForEmit = entity.originalName; + + usedNames.add(entity.nameForEmit); + } + } + + // Next generate unique names for the non-exports that will be emitted + for (const entity of this._entities) { + if (!entity.exported) { + let suffix: number = 1; + entity.nameForEmit = entity.originalName; + + while (usedNames.has(entity.nameForEmit)) { + entity.nameForEmit = `${entity.originalName}_${++suffix}`; + } + + usedNames.add(entity.nameForEmit); + } + } + } + + /** + * Reports an error message to the registered ApiErrorHandler. + */ + public reportError(message: string, sourceFile: ts.SourceFile | undefined, start: number | undefined): void { + if (sourceFile && start) { + const lineAndCharacter: ts.LineAndCharacter = sourceFile.getLineAndCharacterOfPosition(start); + + // If the file is under the packageFolder, then show a relative path + const relativePath: string = path.relative(this.package.packageFolder, sourceFile.fileName); + const shownPath: string = relativePath.substr(0, 2) === '..' ? sourceFile.fileName : relativePath; + + // Format the error so that VS Code can follow it. For example: + // "src\MyClass.ts(15,1): The JSDoc tag "@blah" is not supported by AEDoc" + this.logger.logError(`${shownPath}(${lineAndCharacter.line + 1},${lineAndCharacter.character + 1}): ` + + message); + } else { + this.logger.logError(message); + } + } + + private _fetchSymbolMetadata(astSymbol: AstSymbol): void { + if (astSymbol.metadata) { + return; + } + + // When we solve an astSymbol, then we always also solve all of its parents and all of its declarations + if (astSymbol.parentAstSymbol && astSymbol.parentAstSymbol.metadata === undefined) { + this._fetchSymbolMetadata(astSymbol.parentAstSymbol); + } + + for (const astDeclaration of astSymbol.astDeclarations) { + this._calculateMetadataForDeclaration(astDeclaration); + } + + // We know we solved parentAstSymbol.metadata above + const parentSymbolMetadata: SymbolMetadata | undefined = astSymbol.parentAstSymbol + ? astSymbol.parentAstSymbol.metadata as SymbolMetadata : undefined; + + const symbolMetadata: SymbolMetadata = new SymbolMetadata(); + + // Do any of the declarations have a release tag? + let effectiveReleaseTag: ReleaseTag = ReleaseTag.None; + + for (const astDeclaration of astSymbol.astDeclarations) { + // We know we solved this above + const declarationMetadata: DeclarationMetadata = astDeclaration.metadata as DeclarationMetadata; + + const declaredReleaseTag: ReleaseTag = declarationMetadata.declaredReleaseTag; + + if (declaredReleaseTag !== ReleaseTag.None) { + if (effectiveReleaseTag !== ReleaseTag.None && effectiveReleaseTag !== declaredReleaseTag) { + if (!astSymbol.rootAstSymbol.imported) { // for now, don't report errors for external code + // TODO: Report error message + this.reportError('Inconsistent release tags between declarations', undefined, undefined); + } + } else { + effectiveReleaseTag = declaredReleaseTag; + } + } + } + + // If this declaration doesn't have a release tag, then inherit it from the parent + if (effectiveReleaseTag === ReleaseTag.None && astSymbol.parentAstSymbol) { + if (parentSymbolMetadata) { + effectiveReleaseTag = parentSymbolMetadata.releaseTag; + } + } + + if (effectiveReleaseTag === ReleaseTag.None) { + if (this.validationRules.missingReleaseTags !== ExtractorValidationRulePolicy.allow) { + if (!astSymbol.rootAstSymbol.imported) { // for now, don't report errors for external code + // For now, don't report errors for forgotten exports + const entity: CollectorEntity | undefined = this._entitiesByAstSymbol.get(astSymbol.rootAstSymbol); + if (entity && entity.exported) { + // We also don't report errors for the default export of an entry point, since its doc comment + // isn't easy to obtain from the .d.ts file + if (astSymbol.rootAstSymbol.localName !== '_default') { + // TODO: Report error message + const loc: string = astSymbol.rootAstSymbol.localName + ' in ' + + astSymbol.rootAstSymbol.astDeclarations[0].declaration.getSourceFile().fileName; + this.reportError('Missing release tag for ' + loc, undefined, undefined); + } + } + } + } + + effectiveReleaseTag = ReleaseTag.Public; + } + + symbolMetadata.releaseTag = effectiveReleaseTag; + symbolMetadata.releaseTagSameAsParent = false; + if (parentSymbolMetadata) { + symbolMetadata.releaseTagSameAsParent = symbolMetadata.releaseTag === parentSymbolMetadata.releaseTag; + } + + // Update this last when we're sure no exceptions were thrown + astSymbol.metadata = symbolMetadata; + } + + private _calculateMetadataForDeclaration(astDeclaration: AstDeclaration): void { + const declarationMetadata: DeclarationMetadata = new DeclarationMetadata(); + astDeclaration.metadata = declarationMetadata; + + const parserContext: tsdoc.ParserContext | undefined = this._parseTsdocForAstDeclaration(astDeclaration); + if (parserContext) { + const modifierTagSet: tsdoc.StandardModifierTagSet = parserContext.docComment.modifierTagSet; + + let declaredReleaseTag: ReleaseTag = ReleaseTag.None; + let inconsistentReleaseTags: boolean = false; + + if (modifierTagSet.isPublic()) { + declaredReleaseTag = ReleaseTag.Public; + } + if (modifierTagSet.isBeta()) { + if (declaredReleaseTag !== ReleaseTag.None) { + inconsistentReleaseTags = true; + } else { + declaredReleaseTag = ReleaseTag.Beta; + } + } + if (modifierTagSet.isAlpha()) { + if (declaredReleaseTag !== ReleaseTag.None) { + inconsistentReleaseTags = true; + } else { + declaredReleaseTag = ReleaseTag.Alpha; + } + } + if (modifierTagSet.isInternal()) { + if (declaredReleaseTag !== ReleaseTag.None) { + inconsistentReleaseTags = true; + } else { + declaredReleaseTag = ReleaseTag.Internal; + } + } + + if (inconsistentReleaseTags) { + if (!astDeclaration.astSymbol.rootAstSymbol.imported) { // for now, don't report errors for external code + // TODO: Report error message + this.reportError('Inconsistent release tags in doc comment', undefined, undefined); + } + } + + declarationMetadata.tsdocParserContext = parserContext; + declarationMetadata.tsdocComment = parserContext.docComment; + + declarationMetadata.declaredReleaseTag = declaredReleaseTag; + + declarationMetadata.isEventProperty = modifierTagSet.isEventProperty(); + declarationMetadata.isOverride = modifierTagSet.isOverride(); + declarationMetadata.isSealed = modifierTagSet.isSealed(); + declarationMetadata.isVirtual = modifierTagSet.isVirtual(); + + // Require the summary to contain at least 10 non-spacing characters + declarationMetadata.needsDocumentation = !tsdoc.PlainTextEmitter.hasAnyTextContent( + parserContext.docComment.summarySection, 10); + } + } + + private _parseTsdocForAstDeclaration(astDeclaration: AstDeclaration): tsdoc.ParserContext | undefined { + const declaration: ts.Declaration = astDeclaration.declaration; + let nodeForComment: ts.Node = declaration; + + if (ts.isVariableDeclaration(declaration)) { + // Variable declarations are special because they can be combined into a list. For example: + // + // /** A */ export /** B */ const /** C */ x = 1, /** D **/ [ /** E */ y, z] = [3, 4]; + // + // The compiler will only emit comments A and C in the .d.ts file, so in general there isn't a well-defined + // way to document these parts. API Extractor requires you to break them into separate exports like this: + // + // /** A */ export const x = 1; + // + // But _getReleaseTagForDeclaration() still receives a node corresponding to "x", so we need to walk upwards + // and find the containing statement in order for getJSDocCommentRanges() to read the comment that we expect. + const statement: ts.VariableStatement | undefined = TypeScriptHelpers.findFirstParent(declaration, + ts.SyntaxKind.VariableStatement) as ts.VariableStatement | undefined; + if (statement !== undefined) { + // For a compound declaration, fall back to looking for C instead of A + if (statement.declarationList.declarations.length === 1) { + nodeForComment = statement; + } + } + } + + const sourceFileText: string = declaration.getSourceFile().text; + const ranges: ts.CommentRange[] = TypeScriptHelpers.getJSDocCommentRanges(nodeForComment, sourceFileText) || []; + + if (ranges.length === 0) { + return undefined; + } + + // We use the JSDoc comment block that is closest to the definition, i.e. + // the last one preceding it + const range: ts.TextRange = ranges[ranges.length - 1]; + + const tsdocTextRange: tsdoc.TextRange = tsdoc.TextRange.fromStringRange(sourceFileText, + range.pos, range.end); + + return this._tsdocParser.parseRange(tsdocTextRange); + } + + private _collectReferenceDirectives(astSymbol: AstSymbol): void { + // Are we emitting declarations? + if (astSymbol.astImport) { + return; // no, it's an import + } + + const seenFilenames: Set = new Set(); + + for (const astDeclaration of astSymbol.astDeclarations) { + const sourceFile: ts.SourceFile = astDeclaration.declaration.getSourceFile(); + if (sourceFile && sourceFile.fileName) { + if (!seenFilenames.has(sourceFile.fileName)) { + seenFilenames.add(sourceFile.fileName); + + for (const typeReferenceDirective of sourceFile.typeReferenceDirectives) { + const name: string = sourceFile.text.substring(typeReferenceDirective.pos, typeReferenceDirective.end); + this._dtsTypeReferenceDirectives.add(name); + } + + for (const libReferenceDirective of sourceFile.libReferenceDirectives) { + const name: string = sourceFile.text.substring(libReferenceDirective.pos, libReferenceDirective.end); + this._dtsLibReferenceDirectives.add(name); + } + + } + } + } + } +} diff --git a/apps/api-extractor/src/generators/dtsRollup/DtsEntry.ts b/apps/api-extractor/src/collector/CollectorEntity.ts similarity index 67% rename from apps/api-extractor/src/generators/dtsRollup/DtsEntry.ts rename to apps/api-extractor/src/collector/CollectorEntity.ts index 9e8e383287a..d194831e6e3 100644 --- a/apps/api-extractor/src/generators/dtsRollup/DtsEntry.ts +++ b/apps/api-extractor/src/collector/CollectorEntity.ts @@ -1,12 +1,13 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import { AstSymbol } from './AstSymbol'; +import { AstSymbol } from '../analyzer/AstSymbol'; +import { Collector } from './Collector'; /** - * Constructor parameters for DtsEntry + * Constructor options for CollectorEntity */ -export interface IDtsEntryParameters { +export interface ICollectorEntityOptions { readonly astSymbol: AstSymbol; readonly originalName: string; readonly exported: boolean; @@ -18,10 +19,9 @@ export interface IDtsEntryParameters { * @remarks * The additional contextual state beyond AstSymbol is: * - Whether it's an export of this entry point or not - * - The calculated ReleaseTag, which we use for trimming * - The nameForEmit, which may get renamed by DtsRollupGenerator._makeUniqueNames() */ -export class DtsEntry { +export class CollectorEntity { /** * The AstSymbol that this entry represents. */ @@ -41,10 +41,10 @@ export class DtsEntry { private _sortKey: string | undefined = undefined; - public constructor(parameters: IDtsEntryParameters) { - this.astSymbol = parameters.astSymbol; - this.originalName = parameters.originalName; - this.exported = parameters.exported; + public constructor(options: ICollectorEntityOptions) { + this.astSymbol = options.astSymbol; + this.originalName = options.originalName; + this.exported = options.exported; } /** @@ -65,14 +65,7 @@ export class DtsEntry { public getSortKey(): string { if (!this._sortKey) { const name: string = this.nameForEmit || this.originalName; - if (name.substr(0, 1) === '_') { - // Removes the leading underscore, for example: "_example" --> "example*" - // This causes internal definitions to sort alphabetically with regular definitions. - // The star is appended to preserve uniqueness, since "*" is not a legal identifier character. - this._sortKey = name.substr(1) + '*'; - } else { - this._sortKey = name; - } + this._sortKey = Collector.getSortKeyIgnoringUnderscore(name); } return this._sortKey; } diff --git a/apps/api-extractor/src/collector/CollectorPackage.ts b/apps/api-extractor/src/collector/CollectorPackage.ts new file mode 100644 index 00000000000..aea153a6559 --- /dev/null +++ b/apps/api-extractor/src/collector/CollectorPackage.ts @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import * as ts from 'typescript'; +import * as tsdoc from '@microsoft/tsdoc'; + +import { + IPackageJson +} from '@microsoft/node-core-library'; + +/** + * Constructor options for CollectorPackage + */ +export interface ICollectorPackageOptions { + packageFolder: string; + packageJson: IPackageJson; + entryPointSourceFile: ts.SourceFile; +} + +export class CollectorPackage { + /** + * Returns the folder for the package being analyzed. + * + * @remarks + * If the entry point is `C:\Folder\project\src\index.ts` and the nearest package.json + * is `C:\Folder\project\package.json`, then the packageFolder is `C:\Folder\project` + */ + public readonly packageFolder: string; + + /** + * The parsed package.json file for this package. + */ + public readonly packageJson: IPackageJson; + + public readonly entryPointSourceFile: ts.SourceFile; + + public tsdocComment: tsdoc.DocComment | undefined; + public tsdocParserContext: tsdoc.ParserContext | undefined; + + public constructor(options: ICollectorPackageOptions) { + this.packageFolder = options.packageFolder; + this.packageJson = options.packageJson; + this.entryPointSourceFile = options.entryPointSourceFile; + } + + /** + * Returns the full name of the package being analyzed. + */ + public get name(): string { + return this.packageJson.name; + } +} diff --git a/apps/api-extractor/src/collector/DeclarationMetadata.ts b/apps/api-extractor/src/collector/DeclarationMetadata.ts new file mode 100644 index 00000000000..654fe75168e --- /dev/null +++ b/apps/api-extractor/src/collector/DeclarationMetadata.ts @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import * as tsdoc from '@microsoft/tsdoc'; +import { ReleaseTag } from '../aedoc/ReleaseTag'; + +export class DeclarationMetadata { + public tsdocComment: tsdoc.DocComment | undefined = undefined; + public tsdocParserContext: tsdoc.ParserContext | undefined = undefined; + + /** + * This is the release tag that was explicitly specified in the original doc comment, if any. + * Compare with SymbolMetadata.releaseTag, which is the effective release tag, possibly inherited from + * a parent. + */ + public declaredReleaseTag: ReleaseTag = ReleaseTag.None; + + // NOTE: In the future, the Collector may infer or error-correct some of these states. + // Generators should rely on these instead of tsdocComment.modifierTagSet. + public isEventProperty: boolean = false; + public isOverride: boolean = false; + public isSealed: boolean = false; + public isVirtual: boolean = false; + + public needsDocumentation: boolean = true; +} diff --git a/apps/api-extractor/src/collector/SymbolMetadata.ts b/apps/api-extractor/src/collector/SymbolMetadata.ts new file mode 100644 index 00000000000..931bc5c5912 --- /dev/null +++ b/apps/api-extractor/src/collector/SymbolMetadata.ts @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { ReleaseTag } from '../aedoc/ReleaseTag'; + +export class SymbolMetadata { + public releaseTag: ReleaseTag = ReleaseTag.None; + + // If true, then it would be redundant to show this release tag + public releaseTagSameAsParent: boolean = false; +} diff --git a/apps/api-extractor/src/generators/ApiFileGenerator.ts b/apps/api-extractor/src/generators/ApiFileGenerator.ts deleted file mode 100644 index 7a1fc496abd..00000000000 --- a/apps/api-extractor/src/generators/ApiFileGenerator.ts +++ /dev/null @@ -1,260 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -// See LICENSE in the project root for license information. - -import { Text, FileSystem } from '@microsoft/node-core-library'; -import { ExtractorContext } from '../ExtractorContext'; -import { AstStructuredType } from '../ast/AstStructuredType'; -import { AstEnum } from '../ast/AstEnum'; -import { AstEnumValue } from '../ast/AstEnumValue'; -import { AstFunction } from '../ast/AstFunction'; -import { AstItem, AstItemKind } from '../ast/AstItem'; -import { AstItemVisitor } from './AstItemVisitor'; -import { AstPackage } from '../ast/AstPackage'; -import { AstMember } from '../ast/AstMember'; -import { AstNamespace } from '../ast/AstNamespace'; -import { AstModuleVariable } from '../ast/AstModuleVariable'; -import { IndentedWriter } from '../utils/IndentedWriter'; -import { ReleaseTag } from '../aedoc/ReleaseTag'; - -/** - * For a library such as "example-package", ApiFileGenerator generates the "example-package.api.ts" - * report which is used to detect API changes. The output is pseudocode whose syntax is similar - * but not identical to a "*.d.ts" typings file. The output file is designed to be committed to - * Git with a branch policy that will trigger an API review workflow whenever the file contents - * have changed. For example, the API file indicates *whether* a class has been documented, - * but it does not include the documentation text (since minor text changes should not require - * an API review). - * - * @public - */ -export class ApiFileGenerator extends AstItemVisitor { - protected _indentedWriter: IndentedWriter = new IndentedWriter(); - - /** - * We don't want to require documentation for any properties that occur - * anywhere within a TypeLiteral. If this value is above 0, then we are - * visiting something within a TypeLiteral. - */ - private _insideTypeLiteral: number; - - /** - * Compares the contents of two API files that were created using ApiFileGenerator, - * and returns true if they are equivalent. Note that these files are not normally edited - * by a human; the "equivalence" comparison here is intended to ignore spurious changes that - * might be introduced by a tool, e.g. Git newline normalization or an editor that strips - * whitespace when saving. - */ - public static areEquivalentApiFileContents(actualFileContent: string, expectedFileContent: string): boolean { - // NOTE: "\s" also matches "\r" and "\n" - const normalizedActual: string = actualFileContent.replace(/[\s]+/g, ' '); - const normalizedExpected: string = expectedFileContent.replace(/[\s]+/g, ' '); - return normalizedActual === normalizedExpected; - } - - /** - * Generates the report and writes it to disk. - * - * @param reportFilename - The output filename - * @param analyzer - An Analyzer object representing the input project. - */ - public writeApiFile(reportFilename: string, context: ExtractorContext): void { - const fileContent: string = this.generateApiFileContent(context); - FileSystem.writeFile(reportFilename, fileContent); - } - - public generateApiFileContent(context: ExtractorContext): string { - this._insideTypeLiteral = 0; - // Normalize to CRLF - this.visit(context.package); - const fileContent: string = Text.convertToCrLf(this._indentedWriter.toString()); - return fileContent; - } - - protected visitAstStructuredType(astStructuredType: AstStructuredType): void { - const declarationLine: string = astStructuredType.getDeclarationLine(); - - if (astStructuredType.documentation.preapproved) { - this._indentedWriter.writeLine('// @internal (preapproved)'); - this._indentedWriter.writeLine(declarationLine + ' {'); - this._indentedWriter.writeLine('}'); - return; - } - - if (astStructuredType.kind !== AstItemKind.TypeLiteral) { - this._writeAedocSynopsis(astStructuredType); - } - - this._indentedWriter.writeLine(declarationLine + ' {'); - - this._indentedWriter.indentScope(() => { - if (astStructuredType.kind === AstItemKind.TypeLiteral) { - // Type literals don't have normal JSDoc. Write only the warnings, - // and put them after the '{' since the declaration is nested. - this._writeWarnings(astStructuredType); - } - - for (const member of astStructuredType.getSortedMemberItems()) { - this.visit(member); - this._indentedWriter.writeLine(); - } - }); - - this._indentedWriter.write('}'); - } - - protected visitAstEnum(astEnum: AstEnum): void { - this._writeAedocSynopsis(astEnum); - - this._indentedWriter.writeLine(`enum ${astEnum.name} {`); - - this._indentedWriter.indentScope(() => { - const members: AstItem[] = astEnum.getSortedMemberItems(); - for (let i: number = 0; i < members.length; ++i) { - this.visit(members[i]); - this._indentedWriter.writeLine(i < members.length - 1 ? ',' : ''); - } - }); - - this._indentedWriter.write('}'); - } - - protected visitAstEnumValue(astEnumValue: AstEnumValue): void { - this._writeAedocSynopsis(astEnumValue); - - this._indentedWriter.write(astEnumValue.getDeclarationLine()); - } - - protected visitAstPackage(astPackage: AstPackage): void { - for (const astItem of astPackage.getSortedMemberItems()) { - this.visit(astItem); - this._indentedWriter.writeLine(); - this._indentedWriter.writeLine(); - } - - this._writeAedocSynopsis(astPackage); - } - - protected visitAstNamespace(astNamespace: AstNamespace): void { - this._writeAedocSynopsis(astNamespace); - - // We have decided to call the astNamespace a 'module' in our - // public API documentation. - this._indentedWriter.writeLine(`module ${astNamespace.name} {`); - - this._indentedWriter.indentScope(() => { - for (const astItem of astNamespace.getSortedMemberItems()) { - this.visit(astItem); - this._indentedWriter.writeLine(); - this._indentedWriter.writeLine(); - } - }); - - this._indentedWriter.write('}'); - } - - protected visitAstModuleVariable(astModuleVariable: AstModuleVariable): void { - this._writeAedocSynopsis(astModuleVariable); - - if (astModuleVariable.value) { - this._indentedWriter.write(`${astModuleVariable.name}: ${astModuleVariable.type} = ${astModuleVariable.value};`); - } else { - this._indentedWriter.write(`${astModuleVariable.name}: ${astModuleVariable.type};`); - } - } - - protected visitAstMember(astMember: AstMember): void { - if (astMember.documentation) { - this._writeAedocSynopsis(astMember); - } - - this._indentedWriter.write(astMember.getDeclarationLine()); - - if (astMember.typeLiteral) { - this._insideTypeLiteral += 1; - this.visit(astMember.typeLiteral); - this._insideTypeLiteral -= 1; - } - } - - protected visitAstFunction(astFunction: AstFunction): void { - this._writeAedocSynopsis(astFunction); - this._indentedWriter.write(astFunction.getDeclarationLine()); - } - - /** - * Writes a synopsis of the AEDoc comments, which indicates the release tag, - * whether the item has been documented, and any warnings that were detected - * by the analysis. - */ - private _writeAedocSynopsis(astItem: AstItem): void { - this._writeWarnings(astItem); - const lines: string[] = []; - - if (astItem instanceof AstPackage && !astItem.documentation.summary.length) { - lines.push('(No @packagedocumentation comment for this package)'); - } else { - const footerParts: string[] = []; - switch (astItem.documentation.releaseTag) { - case ReleaseTag.Internal: - footerParts.push('@internal'); - break; - case ReleaseTag.Alpha: - footerParts.push('@alpha'); - break; - case ReleaseTag.Beta: - footerParts.push('@beta'); - break; - case ReleaseTag.Public: - footerParts.push('@public'); - break; - } - - if (astItem.documentation.isSealed) { - footerParts.push('@sealed'); - } - - if (astItem.documentation.isVirtual) { - footerParts.push('@virtual'); - } - - if (astItem.documentation.isOverride) { - footerParts.push('@override'); - } - - if (astItem.documentation.isEventProperty) { - footerParts.push('@eventproperty'); - } - - // deprecatedMessage is initialized by default, - // this ensures it has contents before adding '@deprecated' - if (astItem.documentation.deprecatedMessage.length > 0) { - footerParts.push('@deprecated'); - } - - // If we are anywhere inside a TypeLiteral, _insideTypeLiteral is greater than 0 - if (this._insideTypeLiteral === 0 && astItem.needsDocumentation) { - footerParts.push('(undocumented)'); - } - - if (footerParts.length > 0) { - lines.push(footerParts.join(' ')); - } - } - - this._writeLinesAsComments(lines); - } - - private _writeWarnings(astItem: AstItem): void { - const lines: string[] = astItem.warnings.map((x: string) => 'WARNING: ' + x); - this._writeLinesAsComments(lines); - } - - private _writeLinesAsComments(lines: string[]): void { - if (lines.length) { - // Write the lines prefixed by slashes. If there are multiple lines, add "//" to each line - this._indentedWriter.write('// '); - this._indentedWriter.write(lines.join('\n// ')); - this._indentedWriter.writeLine(); - } - } -} diff --git a/apps/api-extractor/src/generators/ApiJsonGenerator.ts b/apps/api-extractor/src/generators/ApiJsonGenerator.ts deleted file mode 100644 index 355ec2e453c..00000000000 --- a/apps/api-extractor/src/generators/ApiJsonGenerator.ts +++ /dev/null @@ -1,347 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -// See LICENSE in the project root for license information. - -import * as os from 'os'; -import * as path from 'path'; -import * as ts from 'typescript'; -import { JsonFile, IJsonSchemaErrorInfo, NewlineKind } from '@microsoft/node-core-library'; - -import { ExtractorContext } from '../ExtractorContext'; -import { AstStructuredType } from '../ast/AstStructuredType'; -import { AstEnum } from '../ast/AstEnum'; -import { AstEnumValue } from '../ast/AstEnumValue'; -import { AstFunction } from '../ast/AstFunction'; -import { AstItem, AstItemKind } from '../ast/AstItem'; -import { AstItemVisitor } from './AstItemVisitor'; -import { AstPackage } from '../ast/AstPackage'; -import { AstProperty } from '../ast/AstProperty'; -import { AstMember, ApiAccessModifier } from '../ast/AstMember'; -import { AstNamespace } from '../ast/AstNamespace'; -import { AstModuleVariable } from '../ast/AstModuleVariable'; -import { AstMethod } from '../ast/AstMethod'; -import { ReleaseTag } from '../aedoc/ReleaseTag'; -import { IAedocParameter } from '../aedoc/ApiDocumentation'; -import { IApiReturnValue, IApiParameter, IApiNameMap } from '../api/ApiItem'; -import { ApiJsonConverter } from '../api/ApiJsonConverter'; -import { ApiJsonFile } from '../api/ApiJsonFile'; - -/** - * For a library such as "example-package", ApiFileGenerator generates the "example-package.api.json" - * file which represents the API surface for that package. This file should be published as part - * of the library's NPM package. API Extractor will read this file later when it is analyzing - * another project that consumes the library. (Otherwise, API Extractor would have to re-analyze all - * the *.d.ts files, which would be bad because the compiler definitions might not be available for - * a published package, or the results of the analysis might be different somehow.) Documentation - * tools such as api-documenter can also use the *.api.json files. - * - * @public - */ -export class ApiJsonGenerator extends AstItemVisitor { - private static _MEMBERS_KEY: string = 'members'; - private static _EXPORTS_KEY: string = 'exports'; - - protected jsonOutput: Object = {}; - - public writeJsonFile(reportFilename: string, context: ExtractorContext): void { - this.visit(context.package, this.jsonOutput); - - // Write the output before validating the schema, so we can debug it - JsonFile.save(this.jsonOutput, reportFilename, - { - ensureFolderExists: true, - newlineConversion: NewlineKind.CrLf - } - ); - - // Validate that the output conforms to our JSON schema - ApiJsonFile.jsonSchema.validateObjectWithCallback(this.jsonOutput, (errorInfo: IJsonSchemaErrorInfo) => { - const errorMessage: string = path.basename(reportFilename) - + ` does not conform to the expected schema -- please report this API Extractor bug:` - + os.EOL + errorInfo.details; - - console.log(os.EOL + 'ERROR: ' + errorMessage + os.EOL + os.EOL); - throw new Error(errorMessage); - }); - } - - // @override - protected visit(astItem: AstItem, refObject?: Object): void { - switch (astItem.inheritedReleaseTag) { - case ReleaseTag.None: - case ReleaseTag.Beta: - case ReleaseTag.Public: - break; - default: - return; // skip @alpha and @internal definitions - } - - super.visit(astItem, refObject); - } - - protected visitAstStructuredType(astStructuredType: AstStructuredType, refObject?: Object): void { - if (!astStructuredType.supportedName) { - return; - } - - const kind: string = - astStructuredType.kind === AstItemKind.Class ? ApiJsonConverter.convertKindToJson(AstItemKind.Class) : - astStructuredType.kind === AstItemKind.Interface ? - ApiJsonConverter.convertKindToJson(AstItemKind.Interface) : ''; - - const structureNode: Object = { - kind: kind, - extends: astStructuredType.extends || '', - implements: astStructuredType.implements || '', - typeParameters: astStructuredType.typeParameters || [], - deprecatedMessage: astStructuredType.inheritedDeprecatedMessage || [], - summary: astStructuredType.documentation.summary || [], - remarks: astStructuredType.documentation.remarks || [], - isBeta: astStructuredType.inheritedReleaseTag === ReleaseTag.Beta - }; - - // Type literals don't support isSealed - if (astStructuredType.kind === AstItemKind.Class || astStructuredType.kind === AstItemKind.Interface) { - // tslint:disable-next-line:no-any - (structureNode as any).isSealed = !!astStructuredType.documentation.isSealed; - } - - refObject![astStructuredType.name] = structureNode; - - const members: AstItem[] = astStructuredType.getSortedMemberItems(); - - if (members && members.length) { - const membersNode: Object = {}; - structureNode[ApiJsonGenerator._MEMBERS_KEY] = membersNode; - - for (const astItem of members) { - this.visit(astItem, membersNode); - } - } - } - - protected visitAstEnum(astEnum: AstEnum, refObject?: Object): void { - if (!astEnum.supportedName) { - return; - } - - const valuesNode: Object = {}; - const enumNode: Object = { - kind: ApiJsonConverter.convertKindToJson(astEnum.kind), - values: valuesNode, - deprecatedMessage: astEnum.inheritedDeprecatedMessage || [], - summary: astEnum.documentation.summary || [], - remarks: astEnum.documentation.remarks || [], - isBeta: astEnum.inheritedReleaseTag === ReleaseTag.Beta - }; - refObject![astEnum.name] = enumNode; - - for (const astItem of astEnum.getSortedMemberItems()) { - this.visit(astItem, valuesNode); - } - } - - protected visitAstEnumValue(astEnumValue: AstEnumValue, refObject?: Object): void { - if (!astEnumValue.supportedName) { - return; - } - - const declaration: ts.Declaration = astEnumValue.getDeclaration(); - const firstToken: ts.Node | undefined = declaration ? declaration.getFirstToken() : undefined; - const lastToken: ts.Node | undefined = declaration ? declaration.getLastToken() : undefined; - - const value: string = lastToken && lastToken !== firstToken ? lastToken.getText() : ''; - - refObject![astEnumValue.name] = { - kind: ApiJsonConverter.convertKindToJson(astEnumValue.kind), - value: value, - deprecatedMessage: astEnumValue.inheritedDeprecatedMessage || [], - summary: astEnumValue.documentation.summary || [], - remarks: astEnumValue.documentation.remarks || [], - isBeta: astEnumValue.inheritedReleaseTag === ReleaseTag.Beta - }; - } - - protected visitAstFunction(astFunction: AstFunction, refObject?: Object): void { - if (!astFunction.supportedName) { - return; - } - - const returnValueNode: IApiReturnValue = { - type: astFunction.returnType, - description: astFunction.documentation.returnsMessage - }; - - const newNode: Object = { - kind: ApiJsonConverter.convertKindToJson(astFunction.kind), - signature: astFunction.getDeclarationLine(), - returnValue: returnValueNode, - parameters: this._createParameters(astFunction), - deprecatedMessage: astFunction.inheritedDeprecatedMessage || [], - summary: astFunction.documentation.summary || [], - remarks: astFunction.documentation.remarks || [], - isBeta: astFunction.inheritedReleaseTag === ReleaseTag.Beta - }; - - refObject![astFunction.name] = newNode; - } - - protected visitAstPackage(astPackage: AstPackage, refObject?: Object): void { - /* tslint:disable:no-string-literal */ - refObject!['kind'] = ApiJsonConverter.convertKindToJson(astPackage.kind); - refObject!['name'] = astPackage.name; - refObject!['summary'] = astPackage.documentation.summary; - refObject!['remarks'] = astPackage.documentation.remarks; - /* tslint:enable:no-string-literal */ - - const membersNode: Object = {}; - refObject![ApiJsonGenerator._EXPORTS_KEY] = membersNode; - - for (const astItem of astPackage.getSortedMemberItems()) { - this.visit(astItem, membersNode); - } - } - - protected visitAstNamespace(astNamespace: AstNamespace, refObject?: Object): void { - if (!astNamespace.supportedName) { - return; - } - - const membersNode: Object = {}; - for (const astItem of astNamespace.getSortedMemberItems()) { - this.visit(astItem, membersNode); - } - - const newNode: Object = { - kind: ApiJsonConverter.convertKindToJson(astNamespace.kind), - deprecatedMessage: astNamespace.inheritedDeprecatedMessage || [], - summary: astNamespace.documentation.summary || [], - remarks: astNamespace.documentation.remarks || [], - isBeta: astNamespace.inheritedReleaseTag === ReleaseTag.Beta, - exports: membersNode - }; - - refObject![astNamespace.name] = newNode; - } - - protected visitAstMember(astMember: AstMember, refObject?: Object): void { - if (!astMember.supportedName) { - return; - } - - refObject![astMember.name] = 'astMember-' + astMember.getDeclaration().kind; - } - - protected visitAstProperty(astProperty: AstProperty, refObject?: Object): void { - if (!astProperty.supportedName) { - return; - } - - if (astProperty.getDeclaration().kind === ts.SyntaxKind.SetAccessor) { - return; - } - - const newNode: Object = { - kind: ApiJsonConverter.convertKindToJson(astProperty.kind), - signature: astProperty.getDeclarationLine(), - isOptional: !!astProperty.isOptional, - isReadOnly: !!astProperty.isReadOnly, - isStatic: !!astProperty.isStatic, - type: astProperty.type, - deprecatedMessage: astProperty.inheritedDeprecatedMessage || [], - summary: astProperty.documentation.summary || [], - remarks: astProperty.documentation.remarks || [], - isBeta: astProperty.inheritedReleaseTag === ReleaseTag.Beta, - isSealed: !!astProperty.documentation.isSealed, - isVirtual: !!astProperty.documentation.isVirtual, - isOverride: !!astProperty.documentation.isOverride, - isEventProperty: astProperty.isEventProperty - }; - - refObject![astProperty.name] = newNode; - } - - protected visitAstModuleVariable(astModuleVariable: AstModuleVariable, refObject?: Object): void { - const newNode: Object = { - kind: ApiJsonConverter.convertKindToJson(astModuleVariable.kind), - signature: astModuleVariable.getDeclarationLine(), - type: astModuleVariable.type, - value: astModuleVariable.value, - deprecatedMessage: astModuleVariable.inheritedDeprecatedMessage || [], - summary: astModuleVariable.documentation.summary || [], - remarks: astModuleVariable.documentation.remarks || [], - isBeta: astModuleVariable.inheritedReleaseTag === ReleaseTag.Beta - }; - - refObject![astModuleVariable.name] = newNode; - } - - protected visitAstMethod(astMethod: AstMethod, refObject?: Object): void { - if (!astMethod.supportedName) { - return; - } - - let newNode: Object; - if (astMethod.name === '__constructor') { - newNode = { - kind: ApiJsonConverter.convertKindToJson(AstItemKind.Constructor), - signature: astMethod.getDeclarationLine(), - parameters: this._createParameters(astMethod), - deprecatedMessage: astMethod.inheritedDeprecatedMessage || [], - summary: astMethod.documentation.summary || [], - remarks: astMethod.documentation.remarks || [] - }; - } else { - const returnValueNode: IApiReturnValue = { - type: astMethod.returnType, - description: astMethod.documentation.returnsMessage - }; - - newNode = { - kind: ApiJsonConverter.convertKindToJson(astMethod.kind), - signature: astMethod.getDeclarationLine(), - accessModifier: astMethod.accessModifier ? ApiAccessModifier[astMethod.accessModifier].toLowerCase() : '', - isOptional: !!astMethod.isOptional, - isStatic: !!astMethod.isStatic, - returnValue: returnValueNode, - parameters: this._createParameters(astMethod), - deprecatedMessage: astMethod.inheritedDeprecatedMessage || [], - summary: astMethod.documentation.summary || [], - remarks: astMethod.documentation.remarks || [], - isBeta: astMethod.inheritedReleaseTag === ReleaseTag.Beta, - isSealed: !!astMethod.documentation.isSealed, - isVirtual: !!astMethod.documentation.isVirtual, - isOverride: !!astMethod.documentation.isOverride - }; - } - - refObject![astMethod.name] = newNode; - } - - private _createParameters(astFunction: AstMethod | AstFunction): IApiNameMap { - const result: IApiNameMap = { }; - - for (const astParameter of astFunction.params) { - if (!astParameter.supportedName) { - continue; // skip parameter names with unusual characters - } - - const apiParameter: IApiParameter = { - name: astParameter.name, - description: [], - isOptional: astParameter.isOptional, - isSpread: astParameter.isSpread, - type: astParameter.type || '' - }; - - const aedocParameter: IAedocParameter = astFunction.documentation.parameters[astParameter.name]; - if (aedocParameter) { - apiParameter.description = aedocParameter.description; - } - - result[astParameter.name] = apiParameter; - } - - return result; - } - -} diff --git a/apps/api-extractor/src/generators/ApiModelGenerator.ts b/apps/api-extractor/src/generators/ApiModelGenerator.ts new file mode 100644 index 00000000000..d50e4fe8011 --- /dev/null +++ b/apps/api-extractor/src/generators/ApiModelGenerator.ts @@ -0,0 +1,419 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +// tslint:disable:no-bitwise + +import * as ts from 'typescript'; +import * as tsdoc from '@microsoft/tsdoc'; + +import { Collector } from '../collector/Collector'; +import { ApiModel } from '../api/model/ApiModel'; +import { AstDeclaration } from '../analyzer/AstDeclaration'; +import { ApiClass } from '../api/model/ApiClass'; +import { ApiPackage } from '../api/model/ApiPackage'; +import { ApiEntryPoint } from '../api/model/ApiEntryPoint'; +import { ApiMethod } from '../api/model/ApiMethod'; +import { ApiNamespace } from '../api/model/ApiNamespace'; +import { ApiInterface } from '../api/model/ApiInterface'; +import { ApiPropertySignature } from '../api/model/ApiPropertySignature'; +import { ApiParameter } from '../api/model/ApiParameter'; +import { ApiItemContainerMixin } from '../api/mixins/ApiItemContainerMixin'; +import { ReleaseTag } from '../aedoc/ReleaseTag'; +import { ApiProperty } from '../api/model/ApiProperty'; +import { ApiMethodSignature } from '../api/model/ApiMethodSignature'; +import { ApiFunctionLikeMixin } from '../api/mixins/ApiFunctionLikeMixin'; +import { ApiEnum } from '../api/model/ApiEnum'; +import { ApiEnumMember } from '../api/model/ApiEnumMember'; +import { IDeclarationExcerpt } from '../api/mixins/Excerpt'; +import { ExcerptBuilder } from './ExcerptBuilder'; + +export class ApiModelGenerator { + private readonly _collector: Collector; + private readonly _cachedOverloadIndexesByDeclaration: Map; + private readonly _apiModel: ApiModel; + + public constructor(collector: Collector) { + this._collector = collector; + this._cachedOverloadIndexesByDeclaration = new Map(); + this._apiModel = new ApiModel(); + } + + public get apiModel(): ApiModel { + return this._apiModel; + } + + public buildApiPackage(): ApiPackage { + const packageDocComment: tsdoc.DocComment | undefined = this._collector.package.tsdocComment; + + const apiPackage: ApiPackage = new ApiPackage({ + name: this._collector.package.name, + docComment: packageDocComment + }); + this._apiModel.addMember(apiPackage); + + const apiEntryPoint: ApiEntryPoint = new ApiEntryPoint({ name: '' }); + apiPackage.addMember(apiEntryPoint); + + // Create a CollectorEntity for each top-level export + for (const entity of this._collector.entities) { + for (const astDeclaration of entity.astSymbol.astDeclarations) { + if (entity.exported) { + this._processDeclaration(astDeclaration, entity.nameForEmit, apiEntryPoint); + } + } + } + + return apiPackage; + } + + private _processDeclaration(astDeclaration: AstDeclaration, exportedName: string | undefined, + parentApiItem: ApiItemContainerMixin): void { + + if ((astDeclaration.modifierFlags & ts.ModifierFlags.Private) !== 0) { + return; // trim out private declarations + } + + const releaseTag: ReleaseTag = this._collector.fetchMetadata(astDeclaration.astSymbol).releaseTag; + if (releaseTag === ReleaseTag.Internal || releaseTag === ReleaseTag.Alpha) { + return; // trim out items marked as "@internal" or "@alpha" + } + + switch (astDeclaration.declaration.kind) { + case ts.SyntaxKind.ClassDeclaration: + this._processApiClass(astDeclaration, exportedName, parentApiItem); + break; + + case ts.SyntaxKind.EnumDeclaration: + this._processApiEnum(astDeclaration, exportedName, parentApiItem); + break; + + case ts.SyntaxKind.EnumMember: + this._processApiEnumMember(astDeclaration, exportedName, parentApiItem); + break; + + case ts.SyntaxKind.InterfaceDeclaration: + this._processApiInterface(astDeclaration, exportedName, parentApiItem); + break; + + case ts.SyntaxKind.MethodDeclaration: + this._processApiMethod(astDeclaration, exportedName, parentApiItem); + break; + + case ts.SyntaxKind.MethodSignature: + this._processApiMethodSignature(astDeclaration, exportedName, parentApiItem); + break; + + case ts.SyntaxKind.ModuleDeclaration: + this._processApiNamespace(astDeclaration, exportedName, parentApiItem); + break; + + case ts.SyntaxKind.PropertyDeclaration: + this._processApiProperty(astDeclaration, exportedName, parentApiItem); + break; + + case ts.SyntaxKind.PropertySignature: + this._processApiPropertySignature(astDeclaration, exportedName, parentApiItem); + break; + + case ts.SyntaxKind.Constructor: + case ts.SyntaxKind.ConstructSignature: + case ts.SyntaxKind.FunctionDeclaration: + case ts.SyntaxKind.IndexSignature: + case ts.SyntaxKind.TypeAliasDeclaration: + case ts.SyntaxKind.VariableDeclaration: + default: + } + } + + private _processChildDeclarations(astDeclaration: AstDeclaration, exportedName: string | undefined, + parentApiItem: ApiItemContainerMixin): void { + for (const childDeclaration of astDeclaration.children) { + this._processDeclaration(childDeclaration, undefined, parentApiItem); + } + } + + private _processApiClass(astDeclaration: AstDeclaration, exportedName: string | undefined, + parentApiItem: ApiItemContainerMixin): void { + + const name: string = !!exportedName ? exportedName : astDeclaration.astSymbol.localName; + const canonicalReference: string = ApiClass.getCanonicalReference(name); + + let apiClass: ApiClass | undefined = parentApiItem.tryGetMember(canonicalReference) as ApiClass; + + if (apiClass === undefined) { + const declarationExcerpt: IDeclarationExcerpt = ExcerptBuilder.build({ + startingNode: astDeclaration.declaration, + nodeToStopAt: ts.SyntaxKind.FirstPunctuation // FirstPunctuation = "{" + }); + const docComment: tsdoc.DocComment | undefined = this._collector.fetchMetadata(astDeclaration).tsdocComment; + const releaseTag: ReleaseTag = this._collector.fetchMetadata(astDeclaration.astSymbol).releaseTag; + + apiClass = new ApiClass({ name, declarationExcerpt, docComment, releaseTag }); + + parentApiItem.addMember(apiClass); + } + + this._processChildDeclarations(astDeclaration, exportedName, apiClass); + } + + private _processApiEnum(astDeclaration: AstDeclaration, exportedName: string | undefined, + parentApiItem: ApiItemContainerMixin): void { + + const name: string = !!exportedName ? exportedName : astDeclaration.astSymbol.localName; + const canonicalReference: string = ApiEnum.getCanonicalReference(name); + + let apiEnum: ApiEnum | undefined = parentApiItem.tryGetMember(canonicalReference) as ApiEnum; + + if (apiEnum === undefined) { + const declarationExcerpt: IDeclarationExcerpt = ExcerptBuilder.build({ + startingNode: astDeclaration.declaration, + nodeToStopAt: ts.SyntaxKind.FirstPunctuation // FirstPunctuation = "{" + }); + + const docComment: tsdoc.DocComment | undefined = this._collector.fetchMetadata(astDeclaration).tsdocComment; + const releaseTag: ReleaseTag = this._collector.fetchMetadata(astDeclaration.astSymbol).releaseTag; + + apiEnum = new ApiEnum({ name, declarationExcerpt, docComment, releaseTag }); + parentApiItem.addMember(apiEnum); + } + + this._processChildDeclarations(astDeclaration, exportedName, apiEnum); + } + + private _processApiEnumMember(astDeclaration: AstDeclaration, exportedName: string | undefined, + parentApiItem: ApiItemContainerMixin): void { + + const name: string = !!exportedName ? exportedName : astDeclaration.astSymbol.localName; + const canonicalReference: string = ApiEnumMember.getCanonicalReference(name); + + let apiEnumMember: ApiEnumMember | undefined = parentApiItem.tryGetMember(canonicalReference) as ApiEnumMember; + + if (apiEnumMember === undefined) { + const enumMember: ts.EnumMember = astDeclaration.declaration as ts.EnumMember; + + const declarationExcerpt: IDeclarationExcerpt = ExcerptBuilder.build({ + startingNode: astDeclaration.declaration, + embeddedExcerpts: [{ embeddedExcerptName: 'initializer', node: enumMember.initializer }] + }); + + const docComment: tsdoc.DocComment | undefined = this._collector.fetchMetadata(astDeclaration).tsdocComment; + const releaseTag: ReleaseTag = this._collector.fetchMetadata(astDeclaration.astSymbol).releaseTag; + + apiEnumMember = new ApiEnumMember({ name, declarationExcerpt, docComment, releaseTag }); + + parentApiItem.addMember(apiEnumMember); + } + } + + private _processApiInterface(astDeclaration: AstDeclaration, exportedName: string | undefined, + parentApiItem: ApiItemContainerMixin): void { + + const name: string = !!exportedName ? exportedName : astDeclaration.astSymbol.localName; + const canonicalReference: string = ApiInterface.getCanonicalReference(name); + + let apiInterface: ApiClass | undefined = parentApiItem.tryGetMember(canonicalReference) as ApiInterface; + + if (apiInterface === undefined) { + const declarationExcerpt: IDeclarationExcerpt = ExcerptBuilder.build({ + startingNode: astDeclaration.declaration, + nodeToStopAt: ts.SyntaxKind.FirstPunctuation // FirstPunctuation = "{" + }); + + const docComment: tsdoc.DocComment | undefined = this._collector.fetchMetadata(astDeclaration).tsdocComment; + const releaseTag: ReleaseTag = this._collector.fetchMetadata(astDeclaration.astSymbol).releaseTag; + + apiInterface = new ApiInterface({ name, declarationExcerpt, docComment, releaseTag }); + parentApiItem.addMember(apiInterface); + } + + this._processChildDeclarations(astDeclaration, exportedName, apiInterface); + } + + private _processApiMethod(astDeclaration: AstDeclaration, exportedName: string | undefined, + parentApiItem: ApiItemContainerMixin): void { + + const name: string = !!exportedName ? exportedName : astDeclaration.astSymbol.localName; + + const methodDeclaration: ts.MethodDeclaration = astDeclaration.declaration as ts.MethodDeclaration; + + const isStatic: boolean = (astDeclaration.modifierFlags & ts.ModifierFlags.Static) !== 0; + const overloadIndex: number = this._getOverloadIndex(astDeclaration); + const canonicalReference: string = ApiMethod.getCanonicalReference(name, isStatic, overloadIndex); + + let apiMethod: ApiMethod | undefined = parentApiItem.tryGetMember(canonicalReference) as ApiMethod; + + if (apiMethod === undefined) { + const declarationExcerpt: IDeclarationExcerpt = ExcerptBuilder.build({ + startingNode: astDeclaration.declaration, + embeddedExcerpts: [{ embeddedExcerptName: 'returnType', node: methodDeclaration.type }] + }); + + const docComment: tsdoc.DocComment | undefined = this._collector.fetchMetadata(astDeclaration).tsdocComment; + const releaseTag: ReleaseTag = this._collector.fetchMetadata(astDeclaration.astSymbol).releaseTag; + + apiMethod = new ApiMethod({ name, declarationExcerpt, docComment, releaseTag, isStatic, overloadIndex }); + + for (const parameter of methodDeclaration.parameters) { + this._processApiParameter(parameter, apiMethod); + } + + parentApiItem.addMember(apiMethod); + } + } + + private _processApiMethodSignature(astDeclaration: AstDeclaration, exportedName: string | undefined, + parentApiItem: ApiItemContainerMixin): void { + + const name: string = !!exportedName ? exportedName : astDeclaration.astSymbol.localName; + + const methodSignature: ts.MethodSignature = astDeclaration.declaration as ts.MethodSignature; + + const overloadIndex: number = this._getOverloadIndex(astDeclaration); + const canonicalReference: string = ApiMethodSignature.getCanonicalReference(name, overloadIndex); + + let apiMethodSignature: ApiMethodSignature | undefined = parentApiItem.tryGetMember(canonicalReference) as + ApiMethodSignature; + + if (apiMethodSignature === undefined) { + const declarationExcerpt: IDeclarationExcerpt = ExcerptBuilder.build({ + startingNode: astDeclaration.declaration, + embeddedExcerpts: [{ embeddedExcerptName: 'returnType', node: methodSignature.type }] + }); + const docComment: tsdoc.DocComment | undefined = this._collector.fetchMetadata(astDeclaration).tsdocComment; + const releaseTag: ReleaseTag = this._collector.fetchMetadata(astDeclaration.astSymbol).releaseTag; + + apiMethodSignature = new ApiMethodSignature({ name, declarationExcerpt, docComment, releaseTag, + overloadIndex }); + + for (const parameter of methodSignature.parameters) { + this._processApiParameter(parameter, apiMethodSignature); + } + + parentApiItem.addMember(apiMethodSignature); + } + } + + private _processApiParameter(parameterDeclaration: ts.ParameterDeclaration, + functionLikeItem: ApiFunctionLikeMixin): void { + + const declarationExcerpt: IDeclarationExcerpt = ExcerptBuilder.build({ + startingNode: parameterDeclaration, + embeddedExcerpts: [{ embeddedExcerptName: 'parameterType', node: parameterDeclaration.type }] + }); + + functionLikeItem.addParameter(new ApiParameter({ + name: parameterDeclaration.name.getText() || '', + declarationExcerpt + })); + } + + private _processApiNamespace(astDeclaration: AstDeclaration, exportedName: string | undefined, + parentApiItem: ApiItemContainerMixin): void { + + const name: string = !!exportedName ? exportedName : astDeclaration.astSymbol.localName; + const canonicalReference: string = ApiNamespace.getCanonicalReference(name); + + let apiNamespace: ApiNamespace | undefined = parentApiItem.tryGetMember(canonicalReference) as ApiNamespace; + + if (apiNamespace === undefined) { + const declarationExcerpt: IDeclarationExcerpt = ExcerptBuilder.build({ + startingNode: astDeclaration.declaration, + nodeToStopAt: ts.SyntaxKind.ModuleBlock // ModuleBlock = the "{ ... }" block + }); + + const docComment: tsdoc.DocComment | undefined = this._collector.fetchMetadata(astDeclaration).tsdocComment; + const releaseTag: ReleaseTag = this._collector.fetchMetadata(astDeclaration.astSymbol).releaseTag; + + apiNamespace = new ApiNamespace({ name, declarationExcerpt, docComment, releaseTag }); + parentApiItem.addMember(apiNamespace); + } + + this._processChildDeclarations(astDeclaration, exportedName, apiNamespace); + } + + private _processApiProperty(astDeclaration: AstDeclaration, exportedName: string | undefined, + parentApiItem: ApiItemContainerMixin): void { + + const name: string = !!exportedName ? exportedName : astDeclaration.astSymbol.localName; + + const propertyDeclaration: ts.PropertyDeclaration = astDeclaration.declaration as ts.PropertyDeclaration; + + const isStatic: boolean = (astDeclaration.modifierFlags & ts.ModifierFlags.Static) !== 0; + + const canonicalReference: string = ApiProperty.getCanonicalReference(name, isStatic); + + let apiProperty: ApiProperty | undefined + = parentApiItem.tryGetMember(canonicalReference) as ApiProperty; + + if (apiProperty === undefined) { + const declarationExcerpt: IDeclarationExcerpt = ExcerptBuilder.build({ + startingNode: astDeclaration.declaration, + embeddedExcerpts: [{ embeddedExcerptName: 'propertyType', node: propertyDeclaration.type }] + }); + const docComment: tsdoc.DocComment | undefined = this._collector.fetchMetadata(astDeclaration).tsdocComment; + const releaseTag: ReleaseTag = this._collector.fetchMetadata(astDeclaration.astSymbol).releaseTag; + + apiProperty = new ApiProperty({ name, declarationExcerpt, docComment, releaseTag, isStatic }); + parentApiItem.addMember(apiProperty); + } else { + // If the property was already declared before (via a merged interface declaration), + // we assume its signature is identical, because the language requires that. + } + } + + private _processApiPropertySignature(astDeclaration: AstDeclaration, exportedName: string | undefined, + parentApiItem: ApiItemContainerMixin): void { + + const name: string = !!exportedName ? exportedName : astDeclaration.astSymbol.localName; + const canonicalReference: string = ApiPropertySignature.getCanonicalReference(name); + + const propertySignature: ts.PropertySignature = astDeclaration.declaration as ts.PropertySignature; + + let apiPropertySignature: ApiPropertySignature | undefined + = parentApiItem.tryGetMember(canonicalReference) as ApiPropertySignature; + + if (apiPropertySignature === undefined) { + const declarationExcerpt: IDeclarationExcerpt = ExcerptBuilder.build({ + startingNode: astDeclaration.declaration, + embeddedExcerpts: [{ embeddedExcerptName: 'propertyType', node: propertySignature.type }] + }); + const docComment: tsdoc.DocComment | undefined = this._collector.fetchMetadata(astDeclaration).tsdocComment; + const releaseTag: ReleaseTag = this._collector.fetchMetadata(astDeclaration.astSymbol).releaseTag; + + apiPropertySignature = new ApiPropertySignature({ name, declarationExcerpt, docComment, releaseTag }); + parentApiItem.addMember(apiPropertySignature); + } else { + // If the property was already declared before (via a merged interface declaration), + // we assume its signature is identical, because the language requires that. + } + } + + private _getOverloadIndex(astDeclaration: AstDeclaration): number { + const allDeclarations: ReadonlyArray = astDeclaration.astSymbol.astDeclarations; + if (allDeclarations.length === 1) { + return 0; // trivial case + } + + let overloadIndex: number | undefined = this._cachedOverloadIndexesByDeclaration.get(astDeclaration); + + if (overloadIndex === undefined) { + let nextIndex: number = 0; + for (const other of allDeclarations) { + // Filter out other declarations that are not overloads. For example, an overloaded function can also + // be a namespace. + if (other.declaration.kind === astDeclaration.declaration.kind) { + this._cachedOverloadIndexesByDeclaration.set(other, nextIndex); + ++nextIndex; + } + } + overloadIndex = this._cachedOverloadIndexesByDeclaration.get(astDeclaration); + } + + if (overloadIndex === undefined) { + // This should never happen + throw new Error('Error calculating overload index for declaration'); + } + + return overloadIndex; + } +} diff --git a/apps/api-extractor/src/generators/AstItemVisitor.ts b/apps/api-extractor/src/generators/AstItemVisitor.ts deleted file mode 100644 index ae37e9174b2..00000000000 --- a/apps/api-extractor/src/generators/AstItemVisitor.ts +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -// See LICENSE in the project root for license information. - -import { AstPackage } from '../ast/AstPackage'; -import { AstItem } from '../ast/AstItem'; -import { AstEnum } from '../ast/AstEnum'; -import { AstEnumValue } from '../ast/AstEnumValue'; -import { AstFunction } from '../ast/AstFunction'; -import { AstStructuredType } from '../ast/AstStructuredType'; -import { AstMember } from '../ast/AstMember'; -import { AstMethod } from '../ast/AstMethod'; -import { AstNamespace } from '../ast/AstNamespace'; -import { AstProperty } from '../ast/AstProperty'; -import { AstModuleVariable } from '../ast/AstModuleVariable'; - -/** - * This is a helper class that provides a standard way to walk the AstItem - * abstract syntax tree. - */ -export abstract class AstItemVisitor { - protected visit(astItem: AstItem, refObject?: Object): void { - if (astItem instanceof AstStructuredType) { - this.visitAstStructuredType(astItem as AstStructuredType, refObject); - } else if (astItem instanceof AstEnum) { - this.visitAstEnum(astItem as AstEnum, refObject); - } else if (astItem instanceof AstEnumValue) { - this.visitAstEnumValue(astItem as AstEnumValue, refObject); - } else if (astItem instanceof AstFunction) { - this.visitAstFunction(astItem as AstFunction, refObject); - } else if (astItem instanceof AstPackage) { - this.visitAstPackage(astItem as AstPackage, refObject); - } else if (astItem instanceof AstProperty) { - this.visitAstProperty(astItem as AstProperty, refObject); - } else if (astItem instanceof AstMethod) { - this.visitAstMethod(astItem as AstMethod, refObject); - } else if (astItem instanceof AstNamespace) { - this.visitAstNamespace(astItem as AstNamespace, refObject); - } else if (astItem instanceof AstModuleVariable) { - this.visitAstModuleVariable(astItem as AstModuleVariable, refObject); - } else { - throw new Error('Not implemented'); - } - } - - protected abstract visitAstStructuredType(astStructuredType: AstStructuredType, refObject?: Object): void; - - protected abstract visitAstEnum(astEnum: AstEnum, refObject?: Object): void; - - protected abstract visitAstEnumValue(astEnumValue: AstEnumValue, refObject?: Object): void; - - protected abstract visitAstFunction(astFunction: AstFunction, refObject?: Object): void; - - protected abstract visitAstPackage(astPackage: AstPackage, refObject?: Object): void; - - protected abstract visitAstMember(astMember: AstMember, refObject?: Object): void; - - protected abstract visitAstNamespace(astNamespace: AstNamespace, refObject?: Object): void; - - protected abstract visitAstModuleVariable(astModuleVariable: AstModuleVariable, refObject?: Object): void; - - protected visitAstMethod(astMethod: AstMethod, refObject?: Object): void { - this.visitAstMember(astMethod, refObject); - } - - protected visitAstProperty(astProperty: AstProperty, refObject?: Object): void { - this.visitAstMember(astProperty, refObject); - } -} diff --git a/apps/api-extractor/src/generators/DtsRollupGenerator.ts b/apps/api-extractor/src/generators/DtsRollupGenerator.ts new file mode 100644 index 00000000000..710d061cbfb --- /dev/null +++ b/apps/api-extractor/src/generators/DtsRollupGenerator.ts @@ -0,0 +1,311 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +/* tslint:disable:no-bitwise */ + +import * as ts from 'typescript'; +import { FileSystem, NewlineKind } from '@microsoft/node-core-library'; + +import { Collector } from '../collector/Collector'; +import { IndentedWriter } from '../api/IndentedWriter'; +import { TypeScriptHelpers } from '../analyzer/TypeScriptHelpers'; +import { Span, SpanModification } from '../analyzer/Span'; +import { ReleaseTag } from '../aedoc/ReleaseTag'; +import { AstImport } from '../analyzer/AstImport'; +import { CollectorEntity } from '../collector/CollectorEntity'; +import { AstDeclaration } from '../analyzer/AstDeclaration'; +import { SymbolAnalyzer } from '../analyzer/SymbolAnalyzer'; +import { DeclarationMetadata } from '../collector/DeclarationMetadata'; + +/** + * Used with DtsRollupGenerator.writeTypingsFile() + */ +export enum DtsRollupKind { + /** + * Generate a *.d.ts file for an internal release, or for the trimming=false mode. + * This output file will contain all definitions that are reachable from the entry point. + */ + InternalRelease, + + /** + * Generate a *.d.ts file for a preview release. + * This output file will contain all definitions that are reachable from the entry point, + * except definitions marked as \@alpha or \@internal. + */ + BetaRelease, + + /** + * Generate a *.d.ts file for a public release. + * This output file will contain all definitions that are reachable from the entry point, + * except definitions marked as \@beta, \@alpha, or \@internal. + */ + PublicRelease +} + +export class DtsRollupGenerator { + /** + * Generates the typings file and writes it to disk. + * + * @param dtsFilename - The *.d.ts output filename + */ + public static writeTypingsFile(collector: Collector, dtsFilename: string, dtsKind: DtsRollupKind): void { + const indentedWriter: IndentedWriter = new IndentedWriter(); + + DtsRollupGenerator._generateTypingsFileContent(collector, indentedWriter, dtsKind); + + FileSystem.writeFile(dtsFilename, indentedWriter.toString(), { + convertLineEndings: NewlineKind.CrLf, + ensureFolderExists: true + }); + } + + private static _generateTypingsFileContent(collector: Collector, indentedWriter: IndentedWriter, + dtsKind: DtsRollupKind): void { + + if (collector.package.tsdocParserContext) { + indentedWriter.writeLine(collector.package.tsdocParserContext.sourceRange.toString()); + indentedWriter.writeLine(); + } + + // Emit the triple slash directives + for (const typeDirectiveReference of collector.dtsTypeReferenceDirectives) { + // tslint:disable-next-line:max-line-length + // https://github.com/Microsoft/TypeScript/blob/611ebc7aadd7a44a4c0447698bfda9222a78cb66/src/compiler/declarationEmitter.ts#L162 + indentedWriter.writeLine(`/// `); + } + + for (const libDirectiveReference of collector.dtsLibReferenceDirectives) { + indentedWriter.writeLine(`/// `); + } + + // Emit the imports + for (const entity of collector.entities) { + if (entity.astSymbol.astImport) { + + const releaseTag: ReleaseTag = collector.fetchMetadata(entity.astSymbol).releaseTag; + if (this._shouldIncludeReleaseTag(releaseTag, dtsKind)) { + const astImport: AstImport = entity.astSymbol.astImport; + + if (astImport.exportName === '*') { + indentedWriter.write(`import * as ${entity.nameForEmit}`); + } else if (entity.nameForEmit !== astImport.exportName) { + indentedWriter.write(`import { ${astImport.exportName} as ${entity.nameForEmit} }`); + } else { + indentedWriter.write(`import { ${astImport.exportName} }`); + } + indentedWriter.writeLine(` from '${astImport.modulePath}';`); + } + } + } + + // Emit the regular declarations + for (const entity of collector.entities) { + if (!entity.astSymbol.astImport) { + + const releaseTag: ReleaseTag = collector.fetchMetadata(entity.astSymbol).releaseTag; + if (this._shouldIncludeReleaseTag(releaseTag, dtsKind)) { + + // Emit all the declarations for this entry + for (const astDeclaration of entity.astSymbol.astDeclarations || []) { + + indentedWriter.writeLine(); + + const span: Span = new Span(astDeclaration.declaration); + DtsRollupGenerator._modifySpan(collector, span, entity, astDeclaration, dtsKind); + indentedWriter.writeLine(span.getModifiedText()); + } + } else { + indentedWriter.writeLine(); + indentedWriter.writeLine(`/* Excluded from this release type: ${entity.nameForEmit} */`); + } + } + } + } + + /** + * Before writing out a declaration, _modifySpan() applies various fixups to make it nice. + */ + private static _modifySpan(collector: Collector, span: Span, entity: CollectorEntity, + astDeclaration: AstDeclaration, dtsKind: DtsRollupKind): void { + + const previousSpan: Span | undefined = span.previousSibling; + + let recurseChildren: boolean = true; + switch (span.kind) { + case ts.SyntaxKind.JSDocComment: + // If the @packagedocumentation comment seems to be attached to one of the regular API items, + // omit it. It gets explictly emitted at the top of the file. + if (span.node.getText().match(/(?:\s|\*)@packagedocumentation(?:\s|\*)/g)) { + span.modification.skipAll(); + } + + // For now, we don't transform JSDoc comment nodes at all + recurseChildren = false; + break; + + case ts.SyntaxKind.ExportKeyword: + case ts.SyntaxKind.DefaultKeyword: + case ts.SyntaxKind.DeclareKeyword: + // Delete any explicit "export" or "declare" keywords -- we will re-add them below + span.modification.skipAll(); + break; + + case ts.SyntaxKind.InterfaceKeyword: + case ts.SyntaxKind.ClassKeyword: + case ts.SyntaxKind.EnumKeyword: + case ts.SyntaxKind.NamespaceKeyword: + case ts.SyntaxKind.ModuleKeyword: + case ts.SyntaxKind.TypeKeyword: + case ts.SyntaxKind.FunctionKeyword: + // Replace the stuff we possibly deleted above + let replacedModifiers: string = ''; + + // Add a declare statement for root declarations (but not for nested declarations) + if (!astDeclaration.parent) { + replacedModifiers += 'declare '; + } + + if (entity.exported) { + replacedModifiers = 'export ' + replacedModifiers; + } + + if (previousSpan && previousSpan.kind === ts.SyntaxKind.SyntaxList) { + // If there is a previous span of type SyntaxList, then apply it before any other modifiers + // (e.g. "abstract") that appear there. + previousSpan.modification.prefix = replacedModifiers + previousSpan.modification.prefix; + } else { + // Otherwise just stick it in front of this span + span.modification.prefix = replacedModifiers + span.modification.prefix; + } + break; + + case ts.SyntaxKind.VariableDeclaration: + if (!span.parent) { + // The VariableDeclaration node is part of a VariableDeclarationList, however + // the Entry.followedSymbol points to the VariableDeclaration part because + // multiple definitions might share the same VariableDeclarationList. + // + // Since we are emitting a separate declaration for each one, we need to look upwards + // in the ts.Node tree and write a copy of the enclosing VariableDeclarationList + // content (e.g. "var" from "var x=1, y=2"). + const list: ts.VariableDeclarationList | undefined = TypeScriptHelpers.matchAncestor(span.node, + [ts.SyntaxKind.VariableDeclarationList, ts.SyntaxKind.VariableDeclaration]); + if (!list) { + throw new Error('Unsupported variable declaration'); + } + const listPrefix: string = list.getSourceFile().text + .substring(list.getStart(), list.declarations[0].getStart()); + span.modification.prefix = 'declare ' + listPrefix + span.modification.prefix; + + if (entity.exported) { + span.modification.prefix = 'export ' + span.modification.prefix; + } + + const declarationMetadata: DeclarationMetadata = collector.fetchMetadata(astDeclaration); + if (declarationMetadata.tsdocParserContext) { + // Typically the comment for a variable declaration is attached to the outer variable statement + // (which may possibly contain multiple variable declarations), so it's not part of the Span. + // Instead we need to manually inject it. + let originalComment: string = declarationMetadata.tsdocParserContext.sourceRange.toString(); + if (!/[\r\n]\s*$/.test(originalComment)) { + originalComment += '\n'; + } + span.modification.prefix = originalComment + span.modification.prefix; + } + + span.modification.suffix = ';'; + } + break; + + case ts.SyntaxKind.Identifier: + let nameFixup: boolean = false; + const identifierSymbol: ts.Symbol | undefined = collector.typeChecker.getSymbolAtLocation(span.node); + if (identifierSymbol) { + const followedSymbol: ts.Symbol = TypeScriptHelpers.followAliases(identifierSymbol, collector.typeChecker); + + const referencedEntity: CollectorEntity | undefined = collector.tryGetEntityBySymbol(followedSymbol); + + if (referencedEntity) { + if (!referencedEntity.nameForEmit) { + // This should never happen + throw new Error('referencedEntry.uniqueName is undefined'); + } + + span.modification.prefix = referencedEntity.nameForEmit; + nameFixup = true; + // For debugging: + // span.modification.prefix += '/*R=FIX*/'; + } + + } + + if (!nameFixup) { + // For debugging: + // span.modification.prefix += '/*R=KEEP*/'; + } + + break; + } + + if (recurseChildren) { + for (const child of span.children) { + let childAstDeclaration: AstDeclaration = astDeclaration; + + // Should we trim this node? + let trimmed: boolean = false; + if (SymbolAnalyzer.isAstDeclaration(child.kind)) { + childAstDeclaration = collector.astSymbolTable.getChildAstDeclarationByNode(child.node, astDeclaration); + + const releaseTag: ReleaseTag = collector.fetchMetadata(childAstDeclaration.astSymbol).releaseTag; + if (!this._shouldIncludeReleaseTag(releaseTag, dtsKind)) { + const modification: SpanModification = child.modification; + + // Yes, trim it and stop here + const name: string = childAstDeclaration.astSymbol.localName; + modification.omitChildren = true; + + modification.prefix = `/* Excluded from this release type: ${name} */`; + modification.suffix = ''; + + if (child.children.length > 0) { + // If there are grandchildren, then keep the last grandchild's separator, + // since it often has useful whitespace + modification.suffix = child.children[child.children.length - 1].separator; + } + + if (child.nextSibling) { + // If the thing we are trimming is followed by a comma, then trim the comma also. + // An example would be an enum member. + if (child.nextSibling.kind === ts.SyntaxKind.CommaToken) { + // Keep its separator since it often has useful whitespace + modification.suffix += child.nextSibling.separator; + child.nextSibling.modification.skipAll(); + } + } + + trimmed = true; + } + } + + if (!trimmed) { + DtsRollupGenerator._modifySpan(collector, child, entity, childAstDeclaration, dtsKind); + } + } + } + } + + private static _shouldIncludeReleaseTag(releaseTag: ReleaseTag, dtsKind: DtsRollupKind): boolean { + + switch (dtsKind) { + case DtsRollupKind.InternalRelease: + return true; + case DtsRollupKind.BetaRelease: + // NOTE: If the release tag is "None", then we don't have enough information to trim it + return releaseTag === ReleaseTag.Beta || releaseTag === ReleaseTag.Public || releaseTag === ReleaseTag.None; + case DtsRollupKind.PublicRelease: + return releaseTag === ReleaseTag.Public || releaseTag === ReleaseTag.None; + } + + throw new Error(`${DtsRollupKind[dtsKind]} is not implemented`); + } +} diff --git a/apps/api-extractor/src/generators/ExcerptBuilder.ts b/apps/api-extractor/src/generators/ExcerptBuilder.ts new file mode 100644 index 00000000000..ad2c2a95214 --- /dev/null +++ b/apps/api-extractor/src/generators/ExcerptBuilder.ts @@ -0,0 +1,165 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import * as ts from 'typescript'; +import { + ExcerptToken, + ExcerptTokenKind, + IDeclarationExcerpt, + ExcerptName, + IExcerptToken +} from '../api/mixins/Excerpt'; +import { Span } from '../analyzer/Span'; + +export interface IExcerptBuilderEmbeddedExcerpt { + embeddedExcerptName: ExcerptName; + node: ts.Node | undefined; +} + +export interface ISignatureBuilderOptions { + startingNode: ts.Node; + nodeToStopAt?: ts.SyntaxKind; + embeddedExcerpts?: IExcerptBuilderEmbeddedExcerpt[]; +} + +interface IBuildSpanState { + nodeToStopAt?: ts.SyntaxKind; + nameByNode: Map; + remainingExcerptNames: Set; + + /** + * Normally adjacent tokens of the same kind get merged, to avoid creating lots of unnecessary extra tokens. + * However when an embedded excerpt needs to start/end at a specific character, we temporarily disable merging by + * setting this flag. After the new token is added, this flag is cleared. + */ + disableMergingForNextToken: boolean; +} + +export class ExcerptBuilder { + public static build(options: ISignatureBuilderOptions): IDeclarationExcerpt { + const span: Span = new Span(options.startingNode); + + const remainingExcerptNames: Set = new Set(); + + const nameByNode: Map = new Map(); + for (const excerpt of options.embeddedExcerpts || []) { + // Collect all names + remainingExcerptNames.add(excerpt.embeddedExcerptName); + + // If nodes were specify, add them to our map so we will look for them + if (excerpt.node) { + nameByNode.set(excerpt.node, excerpt.embeddedExcerptName); + } + } + + const declarationExcerpt: IDeclarationExcerpt = { + excerptTokens: [ ], + embeddedExcerpts: { } + }; + + ExcerptBuilder._buildSpan(declarationExcerpt, span, { + nodeToStopAt: options.nodeToStopAt, + nameByNode, + remainingExcerptNames, + disableMergingForNextToken: false + }); + + // For any excerpts that we didn't find, add empty entries + for (const embeddedExcerptName of remainingExcerptNames) { + declarationExcerpt.embeddedExcerpts[embeddedExcerptName] = { startIndex: 0, endIndex: 0 }; + } + + return declarationExcerpt; + } + + private static _buildSpan(declarationExcerpt: IDeclarationExcerpt, span: Span, + state: IBuildSpanState): boolean { + + if (state.nodeToStopAt && span.kind === state.nodeToStopAt) { + return false; + } + + if (span.kind === ts.SyntaxKind.JSDocComment) { + // Discard any comments + return true; + } + + // Can this node start a excerpt? + const embeddedExcerptName: ExcerptName | undefined = state.nameByNode.get(span.node); + let excerptStartIndex: number | undefined = undefined; + if (embeddedExcerptName) { + // Did we not already build this excerpt? + if (state.remainingExcerptNames.has(embeddedExcerptName)) { + state.remainingExcerptNames.delete(embeddedExcerptName); + + excerptStartIndex = declarationExcerpt.excerptTokens.length; + state.disableMergingForNextToken = true; + } + } + + if (span.prefix) { + if (span.kind === ts.SyntaxKind.Identifier) { + ExcerptBuilder._appendToken(declarationExcerpt.excerptTokens, ExcerptTokenKind.Reference, + span.prefix, state); + } else { + ExcerptBuilder._appendToken(declarationExcerpt.excerptTokens, ExcerptTokenKind.Content, + span.prefix, state); + } + } + + for (const child of span.children) { + if (!this._buildSpan(declarationExcerpt, child, state)) { + return false; + } + } + + if (span.suffix) { + ExcerptBuilder._appendToken(declarationExcerpt.excerptTokens, ExcerptTokenKind.Content, + span.suffix, state); + } + if (span.separator) { + ExcerptBuilder._appendToken(declarationExcerpt.excerptTokens, ExcerptTokenKind.Content, + span.separator, state); + } + + // Are we building a excerpt? If so, add it. + if (excerptStartIndex !== undefined) { + declarationExcerpt.embeddedExcerpts[embeddedExcerptName!] = { + startIndex: excerptStartIndex, + endIndex: declarationExcerpt.excerptTokens.length + }; + + state.disableMergingForNextToken = true; + } + + return true; + } + + private static _appendToken(excerptTokens: IExcerptToken[], excerptTokenKind: ExcerptTokenKind, + text: string, state: IBuildSpanState): void { + + if (text.length === 0) { + return; + } + + if (excerptTokenKind !== ExcerptTokenKind.Content) { + excerptTokens.push(new ExcerptToken(excerptTokenKind, text)); + state.disableMergingForNextToken = false; + + } else { + // If someone referenced this index, then we need to start a new token + if (excerptTokens.length > 0 && !state.disableMergingForNextToken) { + // Otherwise, can we merge with the previous token? + const previousToken: IExcerptToken = excerptTokens[excerptTokens.length - 1]; + if (previousToken.kind === excerptTokenKind) { + previousToken.text += text; + return; + } + } + + excerptTokens.push(new ExcerptToken(excerptTokenKind, text)); + state.disableMergingForNextToken = false; + } + } + +} diff --git a/apps/api-extractor/src/generators/ReviewFileGenerator.ts b/apps/api-extractor/src/generators/ReviewFileGenerator.ts new file mode 100644 index 00000000000..251591036d7 --- /dev/null +++ b/apps/api-extractor/src/generators/ReviewFileGenerator.ts @@ -0,0 +1,252 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import * as ts from 'typescript'; + +import { Collector } from '../collector/Collector'; +import { TypeScriptHelpers } from '../analyzer/TypeScriptHelpers'; +import { Span } from '../analyzer/Span'; +import { CollectorEntity } from '../collector/CollectorEntity'; +import { AstDeclaration } from '../analyzer/AstDeclaration'; +import { StringBuilder } from '@microsoft/tsdoc'; +import { SymbolAnalyzer } from '../analyzer/SymbolAnalyzer'; +import { DeclarationMetadata } from '../collector/DeclarationMetadata'; +import { SymbolMetadata } from '../collector/SymbolMetadata'; +import { ReleaseTag } from '../aedoc/ReleaseTag'; +import { Text } from '@microsoft/node-core-library'; + +export class ReviewFileGenerator { + /** + * Compares the contents of two API files that were created using ApiFileGenerator, + * and returns true if they are equivalent. Note that these files are not normally edited + * by a human; the "equivalence" comparison here is intended to ignore spurious changes that + * might be introduced by a tool, e.g. Git newline normalization or an editor that strips + * whitespace when saving. + */ + public static areEquivalentApiFileContents(actualFileContent: string, expectedFileContent: string): boolean { + // NOTE: "\s" also matches "\r" and "\n" + const normalizedActual: string = actualFileContent.replace(/[\s]+/g, ' '); + const normalizedExpected: string = expectedFileContent.replace(/[\s]+/g, ' '); + return normalizedActual === normalizedExpected; + } + + public static generateReviewFileContent(collector: Collector): string { + const output: StringBuilder = new StringBuilder(); + + for (const entity of collector.entities) { + if (entity.exported) { + // Emit all the declarations for this entry + for (const astDeclaration of entity.astSymbol.astDeclarations || []) { + + output.append(ReviewFileGenerator._getAedocSynopsis(collector, astDeclaration)); + + const span: Span = new Span(astDeclaration.declaration); + ReviewFileGenerator._modifySpan(collector, span, entity, astDeclaration); + span.writeModifiedText(output); + output.append('\n\n'); + } + } + } + + if (collector.package.tsdocComment === undefined) { + output.append('\n'); + ReviewFileGenerator._writeLineAsComment(output, '(No @packageDocumentation comment for this package)'); + } + + return output.toString(); + } + + /** + * Before writing out a declaration, _modifySpan() applies various fixups to make it nice. + */ + private static _modifySpan(collector: Collector, span: Span, entity: CollectorEntity, + astDeclaration: AstDeclaration): void { + + // Should we process this declaration at all? + if ((astDeclaration.modifierFlags & ts.ModifierFlags.Private) !== 0) { // tslint:disable-line:no-bitwise + span.modification.skipAll(); + return; + } + + let recurseChildren: boolean = true; + let sortChildren: boolean = false; + + switch (span.kind) { + case ts.SyntaxKind.JSDocComment: + span.modification.skipAll(); + // For now, we don't transform JSDoc comment nodes at all + recurseChildren = false; + break; + + case ts.SyntaxKind.ExportKeyword: + case ts.SyntaxKind.DefaultKeyword: + span.modification.skipAll(); + break; + + case ts.SyntaxKind.SyntaxList: + if (span.parent) { + if (SymbolAnalyzer.isAstDeclaration(span.parent.kind)) { + // If the immediate parent is an API declaration, and the immediate children are API declarations, + // then sort the children alphabetically + sortChildren = true; + } else if (span.parent.kind === ts.SyntaxKind.ModuleBlock) { + // Namespaces are special because their chain goes ModuleDeclaration -> ModuleBlock -> SyntaxList + sortChildren = true; + } + } + break; + + case ts.SyntaxKind.VariableDeclaration: + if (!span.parent) { + // The VariableDeclaration node is part of a VariableDeclarationList, however + // the Entry.followedSymbol points to the VariableDeclaration part because + // multiple definitions might share the same VariableDeclarationList. + // + // Since we are emitting a separate declaration for each one, we need to look upwards + // in the ts.Node tree and write a copy of the enclosing VariableDeclarationList + // content (e.g. "var" from "var x=1, y=2"). + const list: ts.VariableDeclarationList | undefined = TypeScriptHelpers.matchAncestor(span.node, + [ts.SyntaxKind.VariableDeclarationList, ts.SyntaxKind.VariableDeclaration]); + if (!list) { + throw new Error('Unsupported variable declaration'); + } + const listPrefix: string = list.getSourceFile().text + .substring(list.getStart(), list.declarations[0].getStart()); + span.modification.prefix = 'declare ' + listPrefix + span.modification.prefix; + + span.modification.suffix = ';'; + } + break; + + case ts.SyntaxKind.Identifier: + let nameFixup: boolean = false; + const identifierSymbol: ts.Symbol | undefined = collector.typeChecker.getSymbolAtLocation(span.node); + if (identifierSymbol) { + const followedSymbol: ts.Symbol = TypeScriptHelpers.followAliases(identifierSymbol, collector.typeChecker); + + const referencedEntity: CollectorEntity | undefined = collector.tryGetEntityBySymbol(followedSymbol); + + if (referencedEntity) { + if (!referencedEntity.nameForEmit) { + // This should never happen + throw new Error('referencedEntry.uniqueName is undefined'); + } + + span.modification.prefix = referencedEntity.nameForEmit; + nameFixup = true; + // For debugging: + // span.modification.prefix += '/*R=FIX*/'; + } + + } + + if (!nameFixup) { + // For debugging: + // span.modification.prefix += '/*R=KEEP*/'; + } + + break; + } + + if (recurseChildren) { + for (const child of span.children) { + let childAstDeclaration: AstDeclaration = astDeclaration; + + if (SymbolAnalyzer.isAstDeclaration(child.kind)) { + childAstDeclaration = collector.astSymbolTable.getChildAstDeclarationByNode(child.node, astDeclaration); + + if (sortChildren) { + span.modification.sortChildren = true; + child.modification.sortKey = Collector.getSortKeyIgnoringUnderscore( + childAstDeclaration.astSymbol.localName); + } + + const aedocSynopsis: string = ReviewFileGenerator._getAedocSynopsis(collector, childAstDeclaration); + const indentedAedocSynopsis: string = ReviewFileGenerator._addIndentAfterNewlines(aedocSynopsis, + child.getIndent()); + + child.modification.prefix = indentedAedocSynopsis + child.modification.prefix; + } + + ReviewFileGenerator._modifySpan(collector, child, entity, childAstDeclaration); + } + } + } + + /** + * Writes a synopsis of the AEDoc comments, which indicates the release tag, + * whether the item has been documented, and any warnings that were detected + * by the analysis. + */ + private static _getAedocSynopsis(collector: Collector, astDeclaration: AstDeclaration): string { + const output: StringBuilder = new StringBuilder(); + + const declarationMetadata: DeclarationMetadata = collector.fetchMetadata(astDeclaration); + const symbolMetadata: SymbolMetadata = collector.fetchMetadata(astDeclaration.astSymbol); + + const footerParts: string[] = []; + + if (!symbolMetadata.releaseTagSameAsParent) { + switch (symbolMetadata.releaseTag) { + case ReleaseTag.Internal: + footerParts.push('@internal'); + break; + case ReleaseTag.Alpha: + footerParts.push('@alpha'); + break; + case ReleaseTag.Beta: + footerParts.push('@beta'); + break; + case ReleaseTag.Public: + footerParts.push('@public'); + break; + } + } + + if (declarationMetadata.isSealed) { + footerParts.push('@sealed'); + } + + if (declarationMetadata.isVirtual) { + footerParts.push('@virtual'); + } + + if (declarationMetadata.isOverride) { + footerParts.push('@override'); + } + + if (declarationMetadata.isEventProperty) { + footerParts.push('@eventproperty'); + } + + if (declarationMetadata.tsdocComment) { + if (declarationMetadata.tsdocComment.deprecatedBlock) { + footerParts.push('@deprecated'); + } + } + + if (declarationMetadata.needsDocumentation) { + footerParts.push('(undocumented)'); + } + + if (footerParts.length > 0) { + ReviewFileGenerator._writeLineAsComment(output, footerParts.join(' ')); + } + + return output.toString(); + } + + private static _writeLineAsComment(output: StringBuilder, line: string): void { + output.append('// '); + output.append(line); + output.append('\n'); + } + + private static _addIndentAfterNewlines(text: string, indent: string): string { + if (text.length === 0 || indent.length === 0) { + return text; + } + return Text.replaceAll(text, '\n', '\n' + indent); + } + +} diff --git a/apps/api-extractor/src/generators/dtsRollup/DtsRollupGenerator.ts b/apps/api-extractor/src/generators/dtsRollup/DtsRollupGenerator.ts deleted file mode 100644 index 058536d725b..00000000000 --- a/apps/api-extractor/src/generators/dtsRollup/DtsRollupGenerator.ts +++ /dev/null @@ -1,574 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -// See LICENSE in the project root for license information. - -/* tslint:disable:no-bitwise */ - -import * as ts from 'typescript'; -import * as tsdoc from '@microsoft/tsdoc'; -import { FileSystem, NewlineKind, Sort } from '@microsoft/node-core-library'; - -import { ExtractorContext } from '../../ExtractorContext'; -import { IndentedWriter } from '../../utils/IndentedWriter'; -import { TypeScriptHelpers } from '../../utils/TypeScriptHelpers'; -import { Span, SpanModification } from '../../utils/Span'; -import { ReleaseTag } from '../../aedoc/ReleaseTag'; -import { AstSymbolTable } from './AstSymbolTable'; -import { AstEntryPoint } from './AstEntryPoint'; -import { AstSymbol } from './AstSymbol'; -import { AstImport } from './AstImport'; -import { DtsEntry } from './DtsEntry'; -import { AstDeclaration } from './AstDeclaration'; -import { SymbolAnalyzer } from './SymbolAnalyzer'; - -/** - * Used with DtsRollupGenerator.writeTypingsFile() - */ -export enum DtsRollupKind { - /** - * Generate a *.d.ts file for an internal release, or for the trimming=false mode. - * This output file will contain all definitions that are reachable from the entry point. - */ - InternalRelease, - - /** - * Generate a *.d.ts file for a preview release. - * This output file will contain all definitions that are reachable from the entry point, - * except definitions marked as \@alpha or \@internal. - */ - BetaRelease, - - /** - * Generate a *.d.ts file for a public release. - * This output file will contain all definitions that are reachable from the entry point, - * except definitions marked as \@beta, \@alpha, or \@internal. - */ - PublicRelease -} - -export class DtsRollupGenerator { - private _context: ExtractorContext; - private _typeChecker: ts.TypeChecker; - private _tsdocParser: tsdoc.TSDocParser; - private _astSymbolTable: AstSymbolTable; - private _astEntryPoint: AstEntryPoint | undefined; - - private _dtsEntries: DtsEntry[] = []; - private _dtsEntriesByAstSymbol: Map = new Map(); - private _dtsEntriesBySymbol: Map = new Map(); - private _releaseTagByAstSymbol: Map = new Map(); - - /** - * A list of names (e.g. "example-library") that should appear in a reference like this: - * - * /// - */ - private _dtsTypeReferenceDirectives: Set = new Set(); - - /** - * A list of names (e.g. "runtime-library") that should appear in a reference like this: - * - * /// - */ - private _dtsLibReferenceDirectives: Set = new Set(); - - public constructor(context: ExtractorContext) { - this._context = context; - this._typeChecker = context.typeChecker; - this._tsdocParser = new tsdoc.TSDocParser(); - this._astSymbolTable = new AstSymbolTable(this._context.program, this._context.typeChecker, - this._context.packageJsonLookup, context.logger); - } - - /** - * Perform the analysis. This must be called before writeTypingsFile(). - */ - public analyze(): void { - if (this._astEntryPoint) { - throw new Error('DtsRollupGenerator.analyze() was already called'); - } - - // Build the entry point - const sourceFile: ts.SourceFile = this._context.package.getDeclaration().getSourceFile(); - this._astEntryPoint = this._astSymbolTable.fetchEntryPoint(sourceFile); - - const exportedAstSymbols: AstSymbol[] = []; - - // Create a DtsEntry for each top-level export - for (const exportedMember of this._astEntryPoint.exportedMembers) { - const astSymbol: AstSymbol = exportedMember.astSymbol; - - this._createDtsEntryForSymbol(exportedMember.astSymbol, exportedMember.name); - - exportedAstSymbols.push(astSymbol); - } - - // Create a DtsEntry for each indirectly referenced export. - // Note that we do this *after* the above loop, so that references to exported AstSymbols - // are encountered first as exports. - const alreadySeenAstSymbols: Set = new Set(); - for (const exportedAstSymbol of exportedAstSymbols) { - this._createDtsEntryForIndirectReferences(exportedAstSymbol, alreadySeenAstSymbols); - } - - this._makeUniqueNames(); - - Sort.sortBy(this._dtsEntries, x => x.getSortKey()); - Sort.sortSet(this._dtsTypeReferenceDirectives); - Sort.sortSet(this._dtsLibReferenceDirectives); - } - - /** - * Generates the typings file and writes it to disk. - * - * @param dtsFilename - The *.d.ts output filename - */ - public writeTypingsFile(dtsFilename: string, dtsKind: DtsRollupKind): void { - const indentedWriter: IndentedWriter = new IndentedWriter(); - - this._generateTypingsFileContent(indentedWriter, dtsKind); - - FileSystem.writeFile(dtsFilename, indentedWriter.toString(), { - convertLineEndings: NewlineKind.CrLf, - ensureFolderExists: true - }); - } - - private _createDtsEntryForSymbol(astSymbol: AstSymbol, exportedName: string | undefined): void { - let dtsEntry: DtsEntry | undefined = this._dtsEntriesByAstSymbol.get(astSymbol); - - if (!dtsEntry) { - dtsEntry = new DtsEntry({ - astSymbol: astSymbol, - originalName: exportedName || astSymbol.localName, - exported: !!exportedName - }); - - this._dtsEntriesByAstSymbol.set(astSymbol, dtsEntry); - this._dtsEntriesBySymbol.set(astSymbol.followedSymbol, dtsEntry); - this._dtsEntries.push(dtsEntry); - - this._collectTypeDefinitionReferences(astSymbol); - } else { - if (exportedName) { - if (!dtsEntry.exported) { - throw new Error('Program Bug: DtsEntry should have been marked as exported'); - } - if (dtsEntry.originalName !== exportedName) { - throw new Error(`The symbol ${exportedName} was also exported as ${dtsEntry.originalName};` - + ` this is not supported yet`); - } - } - } - } - - private _createDtsEntryForIndirectReferences(astSymbol: AstSymbol, alreadySeenAstSymbols: Set): void { - if (alreadySeenAstSymbols.has(astSymbol)) { - return; - } - alreadySeenAstSymbols.add(astSymbol); - - astSymbol.forEachDeclarationRecursive((astDeclaration: AstDeclaration) => { - for (const referencedAstSymbol of astDeclaration.referencedAstSymbols) { - this._createDtsEntryForSymbol(referencedAstSymbol, undefined); - this._createDtsEntryForIndirectReferences(referencedAstSymbol, alreadySeenAstSymbols); - } - }); - } - - /** - * Ensures a unique name for each item in the package typings file. - */ - private _makeUniqueNames(): void { - const usedNames: Set = new Set(); - - // First collect the explicit package exports - for (const dtsEntry of this._dtsEntries) { - if (dtsEntry.exported) { - - if (usedNames.has(dtsEntry.originalName)) { - // This should be impossible - throw new Error(`Program bug: a package cannot have two exports with the name ${dtsEntry.originalName}`); - } - - dtsEntry.nameForEmit = dtsEntry.originalName; - - usedNames.add(dtsEntry.nameForEmit); - } - } - - // Next generate unique names for the non-exports that will be emitted - for (const dtsEntry of this._dtsEntries) { - if (!dtsEntry.exported) { - let suffix: number = 1; - dtsEntry.nameForEmit = dtsEntry.originalName; - - while (usedNames.has(dtsEntry.nameForEmit)) { - dtsEntry.nameForEmit = `${dtsEntry.originalName}_${++suffix}`; - } - - usedNames.add(dtsEntry.nameForEmit); - } - } - } - - private _generateTypingsFileContent(indentedWriter: IndentedWriter, dtsKind: DtsRollupKind): void { - - indentedWriter.spacing = ''; - indentedWriter.clear(); - - // If there is a @packagedocumentation header, put it first: - const packageDocumentation: string = this._context.package.documentation.emitNormalizedComment(); - if (packageDocumentation) { - indentedWriter.writeLine(packageDocumentation); - indentedWriter.writeLine(); - } - - // Emit the triple slash directives - for (const typeDirectiveReference of this._dtsTypeReferenceDirectives) { - // tslint:disable-next-line:max-line-length - // https://github.com/Microsoft/TypeScript/blob/611ebc7aadd7a44a4c0447698bfda9222a78cb66/src/compiler/declarationEmitter.ts#L162 - indentedWriter.writeLine(`/// `); - } - - for (const libDirectiveReference of this._dtsLibReferenceDirectives) { - indentedWriter.writeLine(`/// `); - } - - // Emit the imports - for (const dtsEntry of this._dtsEntries) { - if (dtsEntry.astSymbol.astImport) { - - const releaseTag: ReleaseTag = this._getReleaseTagForAstSymbol(dtsEntry.astSymbol); - if (this._shouldIncludeReleaseTag(releaseTag, dtsKind)) { - const astImport: AstImport = dtsEntry.astSymbol.astImport; - - if (astImport.exportName === '*') { - indentedWriter.write(`import * as ${dtsEntry.nameForEmit}`); - } else if (dtsEntry.nameForEmit !== astImport.exportName) { - indentedWriter.write(`import { ${astImport.exportName} as ${dtsEntry.nameForEmit} }`); - } else { - indentedWriter.write(`import { ${astImport.exportName} }`); - } - indentedWriter.writeLine(` from '${astImport.modulePath}';`); - } - } - } - - // Emit the regular declarations - for (const dtsEntry of this._dtsEntries) { - if (!dtsEntry.astSymbol.astImport) { - - const releaseTag: ReleaseTag = this._getReleaseTagForAstSymbol(dtsEntry.astSymbol); - if (this._shouldIncludeReleaseTag(releaseTag, dtsKind)) { - - // Emit all the declarations for this entry - for (const astDeclaration of dtsEntry.astSymbol.astDeclarations || []) { - - indentedWriter.writeLine(); - - const span: Span = new Span(astDeclaration.declaration); - this._modifySpan(span, dtsEntry, astDeclaration, dtsKind); - indentedWriter.writeLine(span.getModifiedText()); - } - } else { - indentedWriter.writeLine(); - indentedWriter.writeLine(`/* Excluded from this release type: ${dtsEntry.nameForEmit} */`); - } - } - } - } - - /** - * Before writing out a declaration, _modifySpan() applies various fixups to make it nice. - */ - private _modifySpan(span: Span, dtsEntry: DtsEntry, astDeclaration: AstDeclaration, - dtsKind: DtsRollupKind): void { - - const previousSpan: Span | undefined = span.previousSibling; - - let recurseChildren: boolean = true; - switch (span.kind) { - case ts.SyntaxKind.JSDocComment: - // If the @packagedocumentation comment seems to be attached to one of the regular API items, - // omit it. It gets explictly emitted at the top of the file. - if (span.node.getText().match(/(?:\s|\*)@packagedocumentation(?:\s|\*)/g)) { - span.modification.skipAll(); - } - - // For now, we don't transform JSDoc comment nodes at all - recurseChildren = false; - break; - - case ts.SyntaxKind.ExportKeyword: - case ts.SyntaxKind.DefaultKeyword: - case ts.SyntaxKind.DeclareKeyword: - // Delete any explicit "export" or "declare" keywords -- we will re-add them below - span.modification.skipAll(); - break; - - case ts.SyntaxKind.InterfaceKeyword: - case ts.SyntaxKind.ClassKeyword: - case ts.SyntaxKind.EnumKeyword: - case ts.SyntaxKind.NamespaceKeyword: - case ts.SyntaxKind.ModuleKeyword: - case ts.SyntaxKind.TypeKeyword: - case ts.SyntaxKind.FunctionKeyword: - // Replace the stuff we possibly deleted above - let replacedModifiers: string = ''; - - // Add a declare statement for root declarations (but not for nested declarations) - if (!astDeclaration.parent) { - replacedModifiers += 'declare '; - } - - if (dtsEntry.exported) { - replacedModifiers = 'export ' + replacedModifiers; - } - - if (previousSpan && previousSpan.kind === ts.SyntaxKind.SyntaxList) { - // If there is a previous span of type SyntaxList, then apply it before any other modifiers - // (e.g. "abstract") that appear there. - previousSpan.modification.prefix = replacedModifiers + previousSpan.modification.prefix; - } else { - // Otherwise just stick it in front of this span - span.modification.prefix = replacedModifiers + span.modification.prefix; - } - break; - - case ts.SyntaxKind.VariableDeclaration: - if (!span.parent) { - // The VariableDeclaration node is part of a VariableDeclarationList, however - // the Entry.followedSymbol points to the VariableDeclaration part because - // multiple definitions might share the same VariableDeclarationList. - // - // Since we are emitting a separate declaration for each one, we need to look upwards - // in the ts.Node tree and write a copy of the enclosing VariableDeclarationList - // content (e.g. "var" from "var x=1, y=2"). - const list: ts.VariableDeclarationList | undefined = TypeScriptHelpers.matchAncestor(span.node, - [ts.SyntaxKind.VariableDeclarationList, ts.SyntaxKind.VariableDeclaration]); - if (!list) { - throw new Error('Unsupported variable declaration'); - } - const listPrefix: string = list.getSourceFile().text - .substring(list.getStart(), list.declarations[0].getStart()); - span.modification.prefix = 'declare ' + listPrefix + span.modification.prefix; - - if (dtsEntry.exported) { - span.modification.prefix = 'export ' + span.modification.prefix; - } - - span.modification.suffix = ';'; - } - break; - - case ts.SyntaxKind.Identifier: - let nameFixup: boolean = false; - const identifierSymbol: ts.Symbol | undefined = this._typeChecker.getSymbolAtLocation(span.node); - if (identifierSymbol) { - const followedSymbol: ts.Symbol = TypeScriptHelpers.followAliases(identifierSymbol, this._typeChecker); - - const referencedDtsEntry: DtsEntry | undefined = this._dtsEntriesBySymbol.get(followedSymbol); - - if (referencedDtsEntry) { - if (!referencedDtsEntry.nameForEmit) { - // This should never happen - throw new Error('referencedEntry.uniqueName is undefined'); - } - - span.modification.prefix = referencedDtsEntry.nameForEmit; - nameFixup = true; - // For debugging: - // span.modification.prefix += '/*R=FIX*/'; - } - - } - - if (!nameFixup) { - // For debugging: - // span.modification.prefix += '/*R=KEEP*/'; - } - - break; - } - - if (recurseChildren) { - for (const child of span.children) { - let childAstDeclaration: AstDeclaration = astDeclaration; - - // Should we trim this node? - let trimmed: boolean = false; - if (SymbolAnalyzer.isAstDeclaration(child.kind)) { - childAstDeclaration = this._astSymbolTable.getChildAstDeclarationByNode(child.node, astDeclaration); - - const releaseTag: ReleaseTag = this._getReleaseTagForAstSymbol(childAstDeclaration.astSymbol); - if (!this._shouldIncludeReleaseTag(releaseTag, dtsKind)) { - const modification: SpanModification = child.modification; - - // Yes, trim it and stop here - const name: string = childAstDeclaration.astSymbol.localName; - modification.omitChildren = true; - - modification.prefix = `/* Excluded from this release type: ${name} */`; - modification.suffix = ''; - - if (child.children.length > 0) { - // If there are grandchildren, then keep the last grandchild's separator, - // since it often has useful whitespace - modification.suffix = child.children[child.children.length - 1].separator; - } - - if (child.nextSibling) { - // If the thing we are trimming is followed by a comma, then trim the comma also. - // An example would be an enum member. - if (child.nextSibling.kind === ts.SyntaxKind.CommaToken) { - // Keep its separator since it often has useful whitespace - modification.suffix += child.nextSibling.separator; - child.nextSibling.modification.skipAll(); - } - } - - trimmed = true; - } - } - - if (!trimmed) { - this._modifySpan(child, dtsEntry, childAstDeclaration, dtsKind); - } - } - } - } - - private _shouldIncludeReleaseTag(releaseTag: ReleaseTag, dtsKind: DtsRollupKind): boolean { - switch (dtsKind) { - case DtsRollupKind.InternalRelease: - return true; - case DtsRollupKind.BetaRelease: - // NOTE: If the release tag is "None", then we don't have enough information to trim it - return releaseTag === ReleaseTag.Beta || releaseTag === ReleaseTag.Public || releaseTag === ReleaseTag.None; - case DtsRollupKind.PublicRelease: - return releaseTag === ReleaseTag.Public || releaseTag === ReleaseTag.None; - } - - throw new Error(`DtsRollupKind[dtsKind] is not implemented`); - } - - private _getReleaseTagForAstSymbol(astSymbol: AstSymbol): ReleaseTag { - let releaseTag: ReleaseTag | undefined = this._releaseTagByAstSymbol.get(astSymbol); - if (releaseTag) { - return releaseTag; - } - - releaseTag = ReleaseTag.None; - - let current: AstSymbol | undefined = astSymbol; - while (current) { - for (const astDeclaration of current.astDeclarations) { - const declarationReleaseTag: ReleaseTag = this._getReleaseTagForDeclaration(astDeclaration.declaration); - if (releaseTag !== ReleaseTag.None && declarationReleaseTag !== releaseTag) { - // this._analyzeWarnings.push('WARNING: Conflicting release tags found for ' + symbol.name); - break; - } - - releaseTag = declarationReleaseTag; - } - - if (releaseTag !== ReleaseTag.None) { - break; - } - - current = current.parentAstSymbol; - } - - if (releaseTag === ReleaseTag.None) { - releaseTag = ReleaseTag.Public; // public by default - } - - this._releaseTagByAstSymbol.set(astSymbol, releaseTag); - - return releaseTag; - } - - // NOTE: THIS IS A TEMPORARY WORKAROUND. - // In the near future we will overhaul the AEDoc parser to separate syntactic/semantic analysis, - // at which point this will be wired up to the same ApiDocumentation layer used for the API Review files - private _getReleaseTagForDeclaration(declaration: ts.Node): ReleaseTag { - let nodeForComment: ts.Node = declaration; - - if (ts.isVariableDeclaration(declaration)) { - // Variable declarations are special because they can be combined into a list. For example: - // - // /** A */ export /** B */ const /** C */ x = 1, /** D **/ [ /** E */ y, z] = [3, 4]; - // - // The compiler will only emit comments A and C in the .d.ts file, so in general there isn't a well-defined - // way to document these parts. API Extractor requires you to break them into separate exports like this: - // - // /** A */ export const x = 1; - // - // But _getReleaseTagForDeclaration() still receives a node corresponding to "x", so we need to walk upwards - // and find the containing statement in order for getJSDocCommentRanges() to read the comment that we expect. - const statement: ts.VariableStatement | undefined = TypeScriptHelpers.findFirstParent(declaration, - ts.SyntaxKind.VariableStatement) as ts.VariableStatement | undefined; - if (statement !== undefined) { - // For a compound declaration, fall back to looking for C instead of A - if (statement.declarationList.declarations.length === 1) { - nodeForComment = statement; - } - } - } - - const sourceFileText: string = declaration.getSourceFile().text; - - for (const commentRange of TypeScriptHelpers.getJSDocCommentRanges(nodeForComment, sourceFileText) || []) { - // NOTE: This string includes "/**" - const commentTextRange: tsdoc.TextRange = tsdoc.TextRange.fromStringRange( - sourceFileText, commentRange.pos, commentRange.end); - - const parserContext: tsdoc.ParserContext = this._tsdocParser.parseRange(commentTextRange); - const modifierTagSet: tsdoc.StandardModifierTagSet = parserContext.docComment.modifierTagSet; - - if (modifierTagSet.isPublic()) { - return ReleaseTag.Public; - } - if (modifierTagSet.isBeta()) { - return ReleaseTag.Beta; - } - if (modifierTagSet.isAlpha()) { - return ReleaseTag.Alpha; - } - if (modifierTagSet.isInternal()) { - return ReleaseTag.Internal; - } - } - - return ReleaseTag.None; - } - - private _collectTypeDefinitionReferences(astSymbol: AstSymbol): void { - // Are we emitting declarations? - if (astSymbol.astImport) { - return; // no, it's an import - } - - const seenFilenames: Set = new Set(); - - for (const astDeclaration of astSymbol.astDeclarations) { - const sourceFile: ts.SourceFile = astDeclaration.declaration.getSourceFile(); - if (sourceFile && sourceFile.fileName) { - if (!seenFilenames.has(sourceFile.fileName)) { - seenFilenames.add(sourceFile.fileName); - - for (const typeReferenceDirective of sourceFile.typeReferenceDirectives) { - const name: string = sourceFile.text.substring(typeReferenceDirective.pos, typeReferenceDirective.end); - this._dtsTypeReferenceDirectives.add(name); - } - - for (const libReferenceDirective of sourceFile.libReferenceDirectives) { - const name: string = sourceFile.text.substring(libReferenceDirective.pos, libReferenceDirective.end); - this._dtsLibReferenceDirectives.add(name); - } - - } - } - } - } -} diff --git a/apps/api-extractor/src/index.ts b/apps/api-extractor/src/index.ts index 5e287a85889..fa2fc89ce3d 100644 --- a/apps/api-extractor/src/index.ts +++ b/apps/api-extractor/src/index.ts @@ -9,9 +9,9 @@ * @packagedocumentation */ -export { ExternalApiHelper } from './ExternalApiHelper'; +export { ReleaseTag } from './aedoc/ReleaseTag'; -export { Extractor, IAnalyzeProjectOptions, IExtractorOptions } from './extractor/Extractor'; +export { Extractor, IAnalyzeProjectOptions, IExtractorOptions } from './api/Extractor'; export { IExtractorTsconfigCompilerConfig, IExtractorRuntimeCompilerConfig, @@ -23,11 +23,110 @@ export { IExtractorApiJsonFileConfig, IExtractorDtsRollupConfig, IExtractorConfig -} from './extractor/IExtractorConfig'; +} from './api/IExtractorConfig'; -export { ILogger } from './extractor/ILogger'; +export { ILogger } from './api/ILogger'; -export * from './api/ApiItem'; -export { ApiJsonFile } from './api/ApiJsonFile'; -export * from './markup/MarkupElement'; -export { Markup, IMarkupCreateTextOptions } from './markup/Markup'; +export { IndentedWriter } from './api/IndentedWriter'; + +export { + IApiDeclarationMixinOptions, + ApiDeclarationMixin +} from './api/mixins/ApiDeclarationMixin'; +export { + IApiFunctionLikeMixinOptions, + ApiFunctionLikeMixin +} from './api/mixins/ApiFunctionLikeMixin'; +export { + IApiItemContainerMixinOptions, + ApiItemContainerMixin +} from './api/mixins/ApiItemContainerMixin'; +export { + IApiReleaseTagMixinOptions, + ApiReleaseTagMixin +} from './api/mixins/ApiReleaseTagMixin'; +export { + IApiStaticMixinOptions, + ApiStaticMixin +} from './api/mixins/ApiStaticMixin'; +export { + ExcerptTokenKind, + ExcerptName, + IExcerptTokenRange, + IExcerptToken, + IDeclarationExcerpt, + ExcerptToken, + Excerpt +} from './api/mixins/Excerpt'; +export { + Constructor, + PropertiesOf +} from './api/mixins/Mixin'; + +export { + IApiClassOptions, + ApiClass +} from './api/model/ApiClass'; +export { + IApiDocumentedItemOptions, + ApiDocumentedItem +} from './api/model/ApiDocumentedItem'; +export { + IApiEntryPointOptions, + ApiEntryPoint +} from './api/model/ApiEntryPoint'; +export { + IApiEnumOptions, + ApiEnum +} from './api/model/ApiEnum'; +export { + IApiEnumMemberOptions, + ApiEnumMember +} from './api/model/ApiEnumMember'; +export { + IApiInterfaceOptions, + ApiInterface +} from './api/model/ApiInterface'; +export { + ApiItemKind, + IApiItemOptions, + ApiItem +} from './api/model/ApiItem'; +export { + IApiMethodOptions, + ApiMethod +} from './api/model/ApiMethod'; +export { + IApiMethodSignatureOptions, + ApiMethodSignature +} from './api/model/ApiMethodSignature'; +export { + ApiModel +} from './api/model/ApiModel'; +export { + IApiNamespaceOptions, + ApiNamespace +} from './api/model/ApiNamespace'; +export { + IApiPackageOptions, + ApiPackage +} from './api/model/ApiPackage'; +export { + IApiParameterOptions, + ApiParameter +} from './api/model/ApiParameter'; +export { + IApiPropertyOptions, + ApiProperty +} from './api/model/ApiProperty'; +export { + IApiPropertyItemOptions, + ApiPropertyItem +} from './api/model/ApiPropertyItem'; +export { + IApiPropertySignatureOptions, + ApiPropertySignature +} from './api/model/ApiPropertySignature'; +export { + IResolveDeclarationReferenceResult +} from './api/model/DeclarationReferenceResolver'; diff --git a/apps/api-extractor/src/markup/Markup.ts b/apps/api-extractor/src/markup/Markup.ts deleted file mode 100644 index ab572fad797..00000000000 --- a/apps/api-extractor/src/markup/Markup.ts +++ /dev/null @@ -1,507 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -// See LICENSE in the project root for license information. - -import { PackageName } from '@microsoft/node-core-library'; -import { - MarkupElement, - MarkupBasicElement, - IMarkupWebLink, - IMarkupApiLink, - IMarkupText, - IMarkupParagraph, - IMarkupLineBreak, - IMarkupTable, - IMarkupTableRow, - IMarkupTableCell, - IMarkupHeading1, - IMarkupHeading2, - IMarkupPage, - IMarkupHighlightedText, - IMarkupHtmlTag, - MarkupLinkTextElement, - IMarkupNoteBox, - IMarkupCodeBox, - MarkupHighlighter -} from './MarkupElement'; - -import { IApiItemReference } from '../api/ApiItem'; - -/** - * Options for {@link Markup.createTextElements} - * - * @public - */ -export interface IMarkupCreateTextOptions { - /** - * Whether the text should be boldfaced. - */ - bold?: boolean; - - /** - * Whether the text should be italicized. - */ - italics?: boolean; -} - -/** - * Provides various operations for working with MarkupElement objects. - * - * @public - */ -export class Markup { - /** - * A predefined constant for the IMarkupLineBreak element. - */ - public static BREAK: IMarkupLineBreak = { - kind: 'break' - }; - - /** - * A predefined constant for the IMarkupParagraph element. - */ - public static PARAGRAPH: IMarkupParagraph = { - kind: 'paragraph' - }; - - /** - * Appends text content to the `output` array. If the last item in the array is a - * compatible IMarkupText element, the text will be merged into it. Otherwise, a new - * IMarkupText element will be created. - */ - public static appendTextElements(output: MarkupElement[], text: string, options?: IMarkupCreateTextOptions): void { - if (text.length > 0) { - if (output.length > 0) { - const lastElement: MarkupElement = output[output.length - 1]; - if (lastElement.kind === 'text') { - const lastTextElement: IMarkupText = lastElement as IMarkupText; - if (!options) { - options = { }; - } - - if ((!!lastTextElement.bold === !!options.bold) - && (!!lastTextElement.italics === !!options.italics)) { - lastTextElement.text += text; - return; - } - } - } - - // We can't append to the previous element, so start a new one - const result: IMarkupText = { - kind: 'text', - text: text - } as IMarkupText; - - if (options) { - if (options.bold) { - result.bold = true; - } - if (options.italics) { - result.italics = true; - } - } - output.push(result); - } - } - - /** - * Constructs an IMarkupText element representing the specified text string, with - * optional formatting. - * - * @remarks - * NOTE: All whitespace (including newlines) will be collapsed to single spaces. - * This behavior is similar to how HTML handles whitespace. To preserve - * newlines, use {@link Markup.createTextParagraphs} instead. - */ - public static createTextElements(text: string, options?: IMarkupCreateTextOptions): IMarkupText[] { - if (!text) { - return []; - } else { - const result: IMarkupText = { - kind: 'text', - text: Markup._trimRawText(text) - } as IMarkupText; - - if (options) { - if (options.bold) { - result.bold = true; - } - if (options.italics) { - result.italics = true; - } - } - - // The return value is represented as an array containing at most one element. - // Another possible design would be to return a single IMarkupText object that - // is possibly undefined; however, in practice appending arrays turns out to be - // more concise than checking for undefined. - return [ result ]; - } - } - - /** - * This function is similar to {@link Markup.createTextElements}, except that multiple newlines - * will be converted to a Markup.PARAGRAPH object. - */ - public static createTextParagraphs(text: string, options?: IMarkupCreateTextOptions): MarkupBasicElement[] { - const result: MarkupBasicElement[] = []; - - if (text) { - // Split up the paragraphs - for (const paragraph of text.split(/\n\s*\n/g)) { - if (result.length > 0) { - result.push(Markup.PARAGRAPH); - } - - result.push(...Markup.createTextElements(paragraph, options)); - } - } - - return result; - } - - /** - * Constructs an IMarkupApiLink element that represents a hyperlink to the specified - * API object. The hyperlink is applied to an existing stream of markup elements. - * @param textElements - the markup sequence that will serve as the link text - * @param target - the API object that the hyperlink will point to - */ - public static createApiLink(textElements: MarkupLinkTextElement[], target: IApiItemReference): IMarkupApiLink { - if (!textElements.length) { - throw new Error('Missing text for link'); - } - - if (!target.packageName || target.packageName.length < 1) { - throw new Error('The IApiItemReference.packageName cannot be empty'); - } - - // Validate that the scopeName and packageName are formatted correctly - PackageName.combineParts(target.scopeName, target.packageName); - - return { - kind: 'api-link', - elements: textElements, - target: target - } as IMarkupApiLink; - } - - /** - * Constructs an IMarkupApiLink element that represents a hyperlink to the specified - * API object. The hyperlink is applied to a plain text string. - * @param text - the text string that will serve as the link text - * @param target - the API object that the hyperlink will point to - */ - public static createApiLinkFromText(text: string, target: IApiItemReference): IMarkupApiLink { - return Markup.createApiLink(Markup.createTextElements(text), target); - } - - /** - * Constructs an IMarkupWebLink element that represents a hyperlink an internet URL. - * @param textElements - the markup sequence that will serve as the link text - * @param targetUrl - the URL that the hyperlink will point to - */ - public static createWebLink(textElements: MarkupLinkTextElement[], targetUrl: string): IMarkupWebLink { - if (!textElements.length) { - throw new Error('Missing text for link'); - } - if (!targetUrl || !targetUrl.trim()) { - throw new Error('Missing link target'); - } - - return { - kind: 'web-link', - elements: textElements, - targetUrl: targetUrl - }; - } - - /** - * Constructs an IMarkupWebLink element that represents a hyperlink an internet URL. - * @param text - the plain text string that will serve as the link text - * @param targetUrl - the URL that the hyperlink will point to - */ - public static createWebLinkFromText(text: string, targetUrl: string): IMarkupWebLink { - return Markup.createWebLink(Markup.createTextElements(text), targetUrl); - } - - /** - * Constructs an IMarkupHighlightedText element representing a program code text - * with optional syntax highlighting - */ - public static createCode(code: string, highlighter?: MarkupHighlighter): IMarkupHighlightedText { - if (!code) { - throw new Error('The code parameter is missing'); - } - return { - kind: 'code', - text: code, - highlighter: highlighter || 'plain' - } as IMarkupHighlightedText; - } - - /** - * Constructs an IMarkupHtmlTag element representing an opening or closing HTML tag. - */ - public static createHtmlTag(token: string): IMarkupHtmlTag { - if (token.length === 0) { - throw new Error('The code parameter is missing'); - } - return { - kind: 'html-tag', - token: token - } as IMarkupHtmlTag; - } - - /** - * Constructs an IMarkupHeading1 element with the specified title text - */ - public static createHeading1(text: string): IMarkupHeading1 { - return { - kind: 'heading1', - text: Markup._trimRawText(text) - }; - } - - /** - * Constructs an IMarkupHeading2 element with the specified title text - */ - public static createHeading2(text: string): IMarkupHeading2 { - return { - kind: 'heading2', - text: Markup._trimRawText(text) - }; - } - - /** - * Constructs an IMarkupCodeBox element representing a program code text - * with the specified syntax highlighting - */ - public static createCodeBox(code: string, highlighter: MarkupHighlighter): IMarkupCodeBox { - if (!code) { - throw new Error('The code parameter is missing'); - } - return { - kind: 'code-box', - text: code, - highlighter: highlighter - } as IMarkupCodeBox; - } - - /** - * Constructs an IMarkupNoteBox element that will display the specified markup content - */ - public static createNoteBox(textElements: MarkupBasicElement[]): IMarkupNoteBox { - return { - kind: 'note-box', - elements: textElements - } as IMarkupNoteBox; - } - - /** - * Constructs an IMarkupNoteBox element that will display the specified plain text string - */ - public static createNoteBoxFromText(text: string): IMarkupNoteBox { - return Markup.createNoteBox(Markup.createTextElements(text)); - } - - /** - * Constructs an IMarkupTableRow element containing the specified cells, which each contain a - * sequence of MarkupBasicElement content - */ - public static createTableRow(cellValues: MarkupBasicElement[][] | undefined = undefined): IMarkupTableRow { - const row: IMarkupTableRow = { - kind: 'table-row', - cells: [] - }; - - if (cellValues) { - for (const cellValue of cellValues) { - const cell: IMarkupTableCell = { - kind: 'table-cell', - elements: cellValue - }; - row.cells.push(cell); - } - } - - return row; - } - - /** - * Constructs an IMarkupTable element containing the specified header cells, which each contain a - * sequence of MarkupBasicElement content. - * @remarks - * The table initially has zero rows. - */ - public static createTable(headerCellValues: MarkupBasicElement[][] | undefined = undefined): IMarkupTable { - let header: IMarkupTableRow | undefined = undefined; - if (headerCellValues) { - header = Markup.createTableRow(headerCellValues); - } - return { - kind: 'table', - header: header, - rows: [] - } as IMarkupTable; - } - - /** - * Constructs an IMarkupTable element with the specified title. - */ - public static createPage(title: string): IMarkupPage { - return { - kind: 'page', - breadcrumb: [], - title: Markup._trimRawText(title), - elements: [] - } as IMarkupPage; - } - - /** - * Extracts plain text from the provided markup elements, discarding any formatting. - * - * @remarks - * The returned string is suitable for counting words or extracting search keywords. - * Its formatting is not guaranteed, and may change in future updates of this API. - * - * API Extractor determines whether an API is "undocumented" by using extractTextContent() - * to extract the text from its summary, and then counting the number of words. - */ - public static extractTextContent(elements: MarkupElement[]): string { - // Pass a buffer, since "+=" uses less memory than "+" - const buffer: { text: string } = { text: '' }; - Markup._extractTextContent(elements, buffer); - return buffer.text; - } - - /** - * Use this to clean up a MarkupElement sequence, assuming the sequence is now in - * its final form. - * - * @remarks - * The following operations are performed: - * - * 1. Remove leading/trailing white space around paragraphs - * - * 2. Remove redundant paragraph elements - */ - public static normalize(elements: T[]): void { - let i: number = 0; - - while (i < elements.length) { - const element: T = elements[i]; - const previousElement: T | undefined = i - 1 >= 0 ? elements[i - 1] : undefined; - const nextElement: T | undefined = i + 1 < elements.length ? elements[i + 1] : undefined; - - const paragraphBefore: boolean = !!(previousElement && previousElement.kind === 'paragraph'); - const paragraphAfter: boolean = !!(nextElement && nextElement.kind === 'paragraph'); - - if (element.kind === 'paragraph') { - if (i === 0 || i === elements.length - 1 || paragraphBefore) { - // Delete this element. We do not update i because the "previous" item - // is unchanged on the next loop. - elements.splice(i, 1); - continue; - } - } else if (element.kind === 'text') { - const textElement: IMarkupText = element as IMarkupText; - if (paragraphBefore || i === 0) { - textElement.text = textElement.text.replace(/^\s+/, ''); // trim left - } - - if (paragraphAfter || i === elements.length - 1) { - textElement.text = textElement.text.replace(/\s+$/, ''); // trim right - } - } - - ++i; - } - } - - /** - * This formats an IApiItemReference as its AEDoc notation. - * - * @remarks - * Depending on the provided components, example return values might look like - * "\@ms/my-library:SomeClass.someProperty", "my-library:SomeClass", "SomeClass", - * or "SomeClass.someProperty". - */ - public static formatApiItemReference(apiItemReference: IApiItemReference): string { - // Example: "SomeClass" - let result: string = apiItemReference.exportName; - if (apiItemReference.packageName) { - // Example: "my-library:SomeClass" - result = apiItemReference.packageName + '#' + result; - - if (apiItemReference.scopeName) { - // Example: "@ms/my-library:SomeClass" - result = apiItemReference.scopeName + '/' + result; - } - } - if (apiItemReference.memberName) { - // Example: "@ms/my-library:SomeClass.someProperty" - result += '.' + apiItemReference.memberName; - } - return result; - } - - private static _extractTextContent(elements: MarkupElement[], buffer: { text: string }): void { - for (const element of elements) { - switch (element.kind) { - case 'api-link': - buffer.text += Markup.extractTextContent(element.elements); - break; - case 'break': - buffer.text += '\n'; - break; - case 'code': - case 'code-box': - buffer.text += element.text; - break; - case 'heading1': - case 'heading2': - buffer.text += element.text; - break; - case 'html-tag': - break; - case 'note-box': - buffer.text += Markup.extractTextContent(element.elements); - break; - case 'page': - buffer.text += element.title + '\n'; - buffer.text += Markup.extractTextContent(element.elements); - break; - case 'paragraph': - buffer.text += '\n\n'; - break; - case 'table': - if (element.header) { - buffer.text += Markup.extractTextContent([element.header]); - } - buffer.text += Markup.extractTextContent(element.rows); - break; - case 'table-cell': - buffer.text += Markup.extractTextContent(element.elements); - buffer.text += '\n'; - break; - case 'table-row': - buffer.text += Markup.extractTextContent(element.cells); - buffer.text += '\n'; - break; - case 'text': - buffer.text += element.text; - break; - case 'web-link': - buffer.text += Markup.extractTextContent(element.elements); - break; - default: - throw new Error('Unsupported element kind'); - } - } - } - - private static _trimRawText(text: string): string { - // Replace multiple whitespaces with a single space - return text.replace(/\s+/g, ' '); - } -} diff --git a/apps/api-extractor/src/markup/MarkupElement.ts b/apps/api-extractor/src/markup/MarkupElement.ts deleted file mode 100644 index 3cfb1781625..00000000000 --- a/apps/api-extractor/src/markup/MarkupElement.ts +++ /dev/null @@ -1,277 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -// See LICENSE in the project root for license information. - -import { IApiItemReference } from '../api/ApiItem'; - -// ---------------------------------------------------------------------------- - -/** - * A block of plain text, possibly with simple formatting such as bold or italics. - * - * @public - */ -export interface IMarkupText { - /** The kind of markup element */ - kind: 'text'; - - /** - * The plain text content to display. - * @remarks - * If this text contains symbols such as HTML codes, they will be rendered literally, - * without any special formatting. - */ - text: string; - - /** - * Whether the text should be formatted using boldface - */ - bold?: boolean; - - /** - * Whether the text should be formatted using italics - */ - italics?: boolean; -} - -/** - * Indicates the the text should be colorized according to the specified language syntax. - * If "plain" is specified, then no highlighting should be performed. - * - * @public - */ -export type MarkupHighlighter = 'javascript' | 'plain'; - -/** - * Source code shown in a fixed-width font, with syntax highlighting. - * @remarks - * NOTE: IMarkupHighlightedText is just a span of text, whereas IMarkupCodeBox is a box showing a larger code sample. - * @public - */ -export interface IMarkupHighlightedText { - /** The kind of markup element */ - kind: 'code'; - - /** - * The text content to display. - * @remarks - * This content will be highlighted using the specified syntax highlighter. - * If this text contains symbols such as HTML codes, they will be rendered literally. - */ - text: string; - - /** Indicates the syntax highlighting that will be applied to this text */ - highlighter: MarkupHighlighter; -} - -/** - * Represents an HTML tag such as `

` or ``. - * - * @public - */ -export interface IMarkupHtmlTag { - /** The kind of markup element */ - kind: 'html-tag'; - - /** - * A string containing the HTML tag. - * - * @remarks - * To avoid parsing ambiguities with other AEDoc constructs, API Extractor will ensure that - * this string is a complete and properly formatted opening or closing HTML tag such - * as `` or ``. Beyond this, API Extractor does NOT - * attempt to parse the tag attributes, or verify that opening/closing pairs are balanced, - * or determine whether the nested tree is valid HTML. That responsibility is left to the consuming - * documentation engine. - */ - token: string; -} - -/** - * Represents markup that can be used as the link text for a hyperlink - * - * @public - */ -export type MarkupLinkTextElement = IMarkupText | IMarkupHighlightedText | IMarkupHtmlTag; - -// ---------------------------------------------------------------------------- - -/** - * A hyperlink to an API item - * @public - */ -export interface IMarkupApiLink { - /** The kind of markup element */ - kind: 'api-link'; - - /** The link text */ - elements: MarkupLinkTextElement[]; - - /** The API item that will serve as the hyperlink target */ - target: IApiItemReference; -} - -/** - * A hyperlink to an internet URL - * @public - */ -export interface IMarkupWebLink { - /** The kind of markup element */ - kind: 'web-link'; - - /** The link text */ - elements: MarkupLinkTextElement[]; - - /** The internet URL that will serve as the hyperlink target */ - targetUrl: string; -} - -/** - * A paragraph separator, similar to the "

" tag in HTML - * @public - */ -export interface IMarkupParagraph { - /** The kind of markup element */ - kind: 'paragraph'; -} - -/** - * A line break, similar to the "
" tag in HTML. - * @public - */ -export interface IMarkupLineBreak { - /** The kind of markup element */ - kind: 'break'; -} - -/** - * Represents basic text consisting of paragraphs and links (without structures such as headers or tables). - * - * @public - */ -export type MarkupBasicElement = MarkupLinkTextElement | IMarkupApiLink | IMarkupWebLink | IMarkupParagraph - | IMarkupLineBreak; - -// ---------------------------------------------------------------------------- - -/** - * A top-level heading - * @public - */ -export interface IMarkupHeading1 { - /** The kind of markup element */ - kind: 'heading1'; - - /** - * The heading title - * @remarks - * Formatting such as bold/italics are not supported in headings. - * If this text contains symbols such as HTML codes, they will be rendered literally. - */ - text: string; -} - -/** - * A sub heading - * @public - */ -export interface IMarkupHeading2 { - /** The kind of markup element */ - kind: 'heading2'; - - /** {@inheritdoc IMarkupHeading1.text} */ - text: string; -} - -/** - * A box containing source code with syntax highlighting - * @remarks - * NOTE: IMarkupHighlightedText is just a span of text, whereas IMarkupCodeBox is a box showing a larger code sample. - * @public - */ -export interface IMarkupCodeBox { - /** The kind of markup element */ - kind: 'code-box'; - - /** {@inheritdoc IMarkupHighlightedText.text} */ - text: string; - highlighter: MarkupHighlighter; -} - -/** - * A call-out box containing an informational note - * @public - */ -export interface IMarkupNoteBox { - /** The kind of markup element */ - kind: 'note-box'; - elements: MarkupBasicElement[]; -} - -/** - * A table, with an optional header row - * @public - */ -export interface IMarkupTable { - /** The kind of markup element */ - kind: 'table'; - header?: IMarkupTableRow; - rows: IMarkupTableRow[]; -} - -/** - * Represents structured text that contains headings, tables, and boxes. These are the top-level - * elements of a IMarkupPage. - * - * @public - */ -export type MarkupStructuredElement = MarkupBasicElement | IMarkupHeading1 | IMarkupHeading2 | IMarkupCodeBox - | IMarkupNoteBox | IMarkupTable; - -// ---------------------------------------------------------------------------- - -/** - * A cell inside an IMarkupTableRow element. - * - * @public - */ -export interface IMarkupTableCell { - /** The kind of markup element */ - kind: 'table-cell'; - - /** The text content for the table cell */ - elements: MarkupBasicElement[]; -} - -/** - * A row inside an IMarkupTable element. - * - * @public - */ -export interface IMarkupTableRow { - /** The kind of markup element */ - kind: 'table-row'; - cells: IMarkupTableCell[]; -} - -/** - * Represents an entire page. - * - * @public - */ -export interface IMarkupPage { - /** The kind of markup element */ - kind: 'page'; - - breadcrumb: MarkupBasicElement[]; - title: string; - - elements: MarkupStructuredElement[]; -} - -/** - * The super set of all markup interfaces, used e.g. for functions that recursively traverse - * the tree. - * - * @public - */ -export type MarkupElement = MarkupStructuredElement | IMarkupTableCell | IMarkupTableRow | IMarkupPage; diff --git a/apps/api-extractor/src/extractor/api-extractor-defaults.json b/apps/api-extractor/src/schemas/api-extractor-defaults.json similarity index 94% rename from apps/api-extractor/src/extractor/api-extractor-defaults.json rename to apps/api-extractor/src/schemas/api-extractor-defaults.json index c70ecca3623..3d685d11935 100644 --- a/apps/api-extractor/src/extractor/api-extractor-defaults.json +++ b/apps/api-extractor/src/schemas/api-extractor-defaults.json @@ -3,8 +3,6 @@ "project": { // ("entryPointSourceFile" is required) - - "externalJsonFileFolders": [ ] }, "policies": { diff --git a/apps/api-extractor/src/extractor/api-extractor.schema.json b/apps/api-extractor/src/schemas/api-extractor.schema.json similarity index 96% rename from apps/api-extractor/src/extractor/api-extractor.schema.json rename to apps/api-extractor/src/schemas/api-extractor.schema.json index a3c96c9981f..04a5d5e27b3 100644 --- a/apps/api-extractor/src/extractor/api-extractor.schema.json +++ b/apps/api-extractor/src/schemas/api-extractor.schema.json @@ -59,10 +59,6 @@ "entryPointSourceFile": { "description": "Specifies the TypeScript source file that will be treated as the entry point for compilation.", "type": "string" - }, - "externalJsonFileFolders": { - "description": "Indicates folders containing additional APJ JSON files (*.api.json) that will be consulted during the analysis. This is useful for providing annotations for external packages that were not built using API Extractor.", - "type": "string" } }, "required": [ "entryPointSourceFile" ], diff --git a/apps/api-extractor/src/start.ts b/apps/api-extractor/src/start.ts index fed6539794a..3af0916397b 100644 --- a/apps/api-extractor/src/start.ts +++ b/apps/api-extractor/src/start.ts @@ -5,11 +5,9 @@ import * as os from 'os'; import * as colors from 'colors'; import { ApiExtractorCommandLine } from './cli/ApiExtractorCommandLine'; -import { Extractor } from './extractor/Extractor'; +import { Extractor } from './api/Extractor'; -const myPackageVersion: string = Extractor.version; - -console.log(os.EOL + colors.bold(`api-extractor ${myPackageVersion} ` +console.log(os.EOL + colors.bold(`api-extractor ${Extractor.version} ` + colors.cyan(' - http://aka.ms/extractor') + os.EOL)); const parser: ApiExtractorCommandLine = new ApiExtractorCommandLine(); diff --git a/apps/api-extractor/src/test/TypeScriptHelpers.test.ts b/apps/api-extractor/src/test/TypeScriptHelpers.test.ts deleted file mode 100644 index 5bc71715b18..00000000000 --- a/apps/api-extractor/src/test/TypeScriptHelpers.test.ts +++ /dev/null @@ -1,174 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -// See LICENSE in the project root for license information. - -import { TypeScriptHelpers } from '../utils/TypeScriptHelpers'; - -interface ITestCase { - input: string; - output: string; -} - -describe('TypeScriptHelpers tests', () => { - - describe('splitStringWithRegEx()', () => { - it('simple case', () => { - expect(TypeScriptHelpers.splitStringWithRegEx('ABCDaFG', /A/gi)) - .toEqual(['A', 'BCD', 'a', 'FG']); - }); - - it('empty match', () => { - expect(TypeScriptHelpers.splitStringWithRegEx('', /A/gi)) - .toEqual([]); - }); - - }); - - describe('extractJSDocContent()', () => { - - const testCases: ITestCase[] = [ - { // 0 - input: '/*A*/', - output: '' // error - }, - { // 1 - input: '/****A****/', - output: 'A' - }, - { // 2 - input: '/**A */', - output: 'A' - }, - { // 3 - input: '/** A */', - output: 'A' - }, - { // 4 - input: '/** A */', - output: 'A' - }, - { // 5 - input: '/**\n' + - 'A */', - output: 'A' - }, - { // 6 - input: '/**\n' + - ' A */', - output: ' A' - }, - { // 7 - input: '/**\n' + - ' *A */', - output: 'A' - }, - { // 8 - input: '/**\n' + - ' * A */', - output: 'A' - }, - { // 9 - input: '/**\n' + - ' * A*/', - output: ' A' - }, - { // 10 - input: '/**\n' + - ' * A\n' + - ' */', - output: ' A\n' - }, - { // 11 - input: '/*****\n' + - '*A\n' + - '******/', - output: 'A\n' - }, - { // 12 - input: '/******\n' + - ' ***A**\n' + - ' ******/', - output: '**A**\n' - }, - { // 13 - input: '/** A\n' + - ' * B\n' + - 'C */', - output: 'A\nB\nC' - }, - { // 14 - input: '/** A\n' + - ' * B\n' + - ' * C */', - output: 'A\n B\n C' - }, - { // 15 - input: '/**\n' + - ' * A\n' + - ' * \t \n' + - ' * B\n' + - ' \n' + - ' * C\n' + - ' */', - output: 'A\n\nB\n\nC\n' - }, - { // 16 - input: '/** *\\/ */', // a properly escaped terminator - output: '*\\/' - } - ]; - - for (let i: number = 0; i < testCases.length; ++i) { - it(`JSDoc test case ${i}`, () => { - expect(TypeScriptHelpers.extractJSDocContent(testCases[i].input, console.log)) - .toBe(testCases[i].output); - }); - } - - }); - - describe('formatJSDocContent()', () => { - - const testCases: ITestCase[] = [ - { // 0 - input: '', - output: '' - }, - { // 1 - input: 'a', - output: '/** a */' - }, - { // 2 - input: '\na', - output: '/**\n * \n * a\n */' - }, - { // 3 - input: 'a\n', - output: '/**\n * a\n */' - }, - { // 4 - input: ' \na\n ', - output: '/**\n * \n * a\n * \n */' - }, - { // 5 - input: 'this is\na test\n', - output: '/**\n * this is\n * a test\n */' - }, - { // 6 - input: 'single line comment', - output: '/** single line comment */' - }, - { // 7 - input: 'a */ b', - output: '/** a *\\/ b */' - } - ]; - - for (let i: number = 0; i < testCases.length; ++i) { - it(`JSDoc test case ${i}`, () => { - expect(TypeScriptHelpers.formatJSDocContent(testCases[i].input)) - .toBe(testCases[i].output); - }); - } - - }); -}); diff --git a/apps/api-extractor/src/utils/IndentedWriter.ts b/apps/api-extractor/src/utils/IndentedWriter.ts deleted file mode 100644 index 99f694af817..00000000000 --- a/apps/api-extractor/src/utils/IndentedWriter.ts +++ /dev/null @@ -1,138 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -// See LICENSE in the project root for license information. - -/** - * A utility for writing indented text. In the current implementation, - * IndentedWriter builds up an internal string buffer, which can be obtained - * by calling IndentedWriter.getOutput(). - * - * Note that the indentation is inserted at the last possible opportunity. - * For example, this code... - * - * writer.write('begin\n'); - * writer.increaseIndent(); - * writer.Write('one\ntwo\n'); - * writer.decreaseIndent(); - * writer.increaseIndent(); - * writer.decreaseIndent(); - * writer.Write('end'); - * - * ...would produce this output: - * - * begin - * one - * two - * end - */ -export class IndentedWriter { - /** - * The text characters used to create one level of indentation. - * Two spaces by default. - */ - public spacing: string = ' '; - - private _output: string; - private _indentStack: string[]; - private _indentText: string; - private _needsIndent: boolean; - - constructor() { - this.clear(); - } - - /** - * Resets the stream, erasing any output and indentation levels. - * Does not reset the "spacing" configuration. - */ - public clear(): void { - this._output = ''; - this._indentStack = []; - this._indentText = ''; - this._needsIndent = true; - } - - /** - * Retrieves the indented output. - */ - public toString(): string { - return this._output; - } - - /** - * Increases the indentation. Normally the indentation is two spaces, - * however an arbitrary prefix can optional be specified. (For example, - * the prefix could be "// " to indent and comment simultaneously.) - * Each call to IndentedWriter.increaseIndent() must be followed by a - * corresponding call to IndentedWriter.decreaseIndent(). - */ - public increaseIndent(): void { - this._indentStack.push(this.spacing); - this._updateIndentText(); - } - - /** - * Decreases the indentation, reverting the effect of the corresponding call - * to IndentedWriter.increaseIndent(). - */ - public decreaseIndent(): void { - this._indentStack.pop(); - this._updateIndentText(); - } - - /** - * A shorthand for ensuring that increaseIndent()/decreaseIndent() occur - * in pairs. - */ - public indentScope(scope: () => void): void { - this.increaseIndent(); - scope(); - this.decreaseIndent(); - } - - /** - * Writes some text to the internal string buffer, applying indentation according - * to the current indentation level. If the string contains multiple newlines, - * each line will be indented separately. - */ - public write(message: string): void { - let first: boolean = true; - for (const linePart of message.split('\n')) { - if (!first) { - this._writeNewLine(); - } else { - first = false; - } - if (linePart) { - this._writeLinePart(linePart); - } - } - } - - /** - * A shorthand for writing an optional message, followed by a newline. - * Indentation is applied following the semantics of IndentedWriter.write(). - */ - public writeLine(message: string = ''): void { - this.write(message + '\n'); - } - - /** - * Writes a string that does not contain any newline characters. - */ - private _writeLinePart(message: string): void { - if (this._needsIndent) { - this._output += this._indentText; - this._needsIndent = false; - } - this._output += message.replace(/\r/g, ''); - } - - private _writeNewLine(): void { - this._output += '\n'; - this._needsIndent = true; - } - - private _updateIndentText(): void { - this._indentText = this._indentStack.join(''); - } -} diff --git a/apps/api-extractor/src/utils/PrettyPrinter.ts b/apps/api-extractor/src/utils/PrettyPrinter.ts deleted file mode 100644 index e4f930daff0..00000000000 --- a/apps/api-extractor/src/utils/PrettyPrinter.ts +++ /dev/null @@ -1,117 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -// See LICENSE in the project root for license information. - -/* tslint:disable:no-bitwise */ - -import * as ts from 'typescript'; -import { Text } from '@microsoft/node-core-library'; - -import { Span } from './Span'; - -/** - * Some helper functions for formatting certain TypeScript Compiler API expressions. - */ -export class PrettyPrinter { - /** - * Used for debugging only. This dumps the TypeScript Compiler's abstract syntax tree. - */ - public static dumpTree(node: ts.Node, indent: string = ''): void { - const kindName: string = ts.SyntaxKind[node.kind]; - let trimmedText: string; - - try { - trimmedText = node.getText() - .replace(/[\r\n]/g, ' ') - .replace(/\s+/g, ' ') - .trim(); - - if (trimmedText.length > 100) { - trimmedText = trimmedText.substr(0, 97) + '...'; - } - } catch (e) { - trimmedText = '(error getting text)'; - } - - console.log(`${indent}${kindName}: [${trimmedText}]`); - - try { - for (const childNode of node.getChildren()) { - PrettyPrinter.dumpTree(childNode, indent + ' '); - } - } catch (e) { - // sometimes getChildren() throws an exception - } - } - - /** - * Returns a text representation of the enum flags. - */ - public static getSymbolFlagsString(flags: ts.SymbolFlags): string { - return PrettyPrinter._getFlagsString(flags, PrettyPrinter._getSymbolFlagString); - } - - /** - * Returns a text representation of the enum flags. - */ - public static getTypeFlagsString(flags: ts.TypeFlags): string { - return PrettyPrinter._getFlagsString(flags, PrettyPrinter._getTypeFlagString); - } - - /** - * Returns the first line of a potentially nested declaration. - * For example, for a class definition this might return - * "class Blah extends BaseClass" without the curly braces. - * For example, for a function definition, this might return - * "test(): void;" without the curly braces. - */ - public static getDeclarationSummary(node: ts.Node): string { - const rootSpan: Span = new Span(node); - rootSpan.forEach((span: Span) => { - switch (span.kind) { - case ts.SyntaxKind.JSDocComment: // strip any code comments - case ts.SyntaxKind.DeclareKeyword: // strip the "declare" keyword - span.modification.skipAll(); - break; - } - }); - - return Text.convertToLf(rootSpan.getModifiedText()); - } - - /** - * Returns a string such as this, based on the context information in the provided node: - * "[C:\Folder\File.ts#123]" - */ - public static formatFileAndLineNumber(node: ts.Node): string { - const sourceFile: ts.SourceFile = node.getSourceFile(); - const lineAndCharacter: ts.LineAndCharacter = sourceFile.getLineAndCharacterOfPosition(node.getStart()); - return `[${sourceFile.fileName}#${lineAndCharacter.line}]`; - } - - private static _getSymbolFlagString(flag: ts.SymbolFlags): string { - return ts.SymbolFlags[flag]; - } - - private static _getTypeFlagString(flag: ts.TypeFlags): string { - return ts.TypeFlags[flag]; - } - - private static _getFlagsString(flags: T, func: (x: T) => string): string { - /* tslint:disable:no-any */ - let result: string = ''; - - let flag: number = 1; - for (let bit: number = 0; bit < 32; ++bit) { - if ((flags as any as number) & flag) { - if (result !== '') { - result += ', '; - } - result += func(flag as any as T); - } - flag <<= 1; - } - return result === '' ? '???' : result; - /* tslint:enable:no-any */ - } - -} diff --git a/apps/api-extractor/tslint.json b/apps/api-extractor/tslint.json index 30e72fdf8b4..22e5d8575d4 100644 --- a/apps/api-extractor/tslint.json +++ b/apps/api-extractor/tslint.json @@ -1,3 +1,6 @@ { - "extends": "@microsoft/rush-stack-compiler-3.0/includes/tslint.json" + "extends": "@microsoft/rush-stack-compiler-3.0/includes/tslint.json", + "rules": { + "member-ordering": false + } } diff --git a/apps/api-extractor/typings/jju/jju.d.ts b/apps/api-extractor/typings/jju/jju.d.ts deleted file mode 100644 index 95c20fced22..00000000000 --- a/apps/api-extractor/typings/jju/jju.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -// Type definitions for jju 1.3.0 -// Project: https://www.npmjs.com/package/jju -// Definitions by: pgonzal - -interface IJjuOptions { - reserved_keys? : 'ignore' | 'throw' | 'replace'; - mode?: 'json'; - reviver?: (key: any, value: any) => any; -} - -declare module 'jju' { - export function parse(text: string, options?: IJjuOptions): any; -} diff --git a/apps/api-extractor/typings/tsd.d.ts b/apps/api-extractor/typings/tsd.d.ts deleted file mode 100644 index 84aa7f6fb05..00000000000 --- a/apps/api-extractor/typings/tsd.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -// See LICENSE in the project root for license information. - -/// diff --git a/build-tests/api-extractor-test-01/dist/beta/api-extractor-test-01.d.ts b/build-tests/api-extractor-test-01/dist/beta/api-extractor-test-01.d.ts index 1b00f4edd85..dc640cdef53 100644 --- a/build-tests/api-extractor-test-01/dist/beta/api-extractor-test-01.d.ts +++ b/build-tests/api-extractor-test-01/dist/beta/api-extractor-test-01.d.ts @@ -1,11 +1,11 @@ /** * api-extractor-test-01 - * + * * @remarks * This library is consumed by api-extractor-test-02 and api-extractor-test-03. * It tests the basic types of definitions, and all the weird cases for following * chains of type aliases. - * + * * @packagedocumentation */ @@ -72,6 +72,35 @@ export declare class AmbientConsumer { export declare class ClassExportedAsDefault { } +/** + * This class gets aliased twice before being exported from the package. + * @public + */ +export declare class ClassWithAccessModifiers { + /** Doc comment */ + private _privateField; + /** Doc comment */ + private privateMethod; + /** Doc comment */ + private readonly privateGetter; + /** Doc comment */ + private privateSetter; + /** Doc comment */ + private constructor(); + /** Doc comment */ + private static privateStaticMethod; + /** Doc comment */ + protected protectedField: number; + /** Doc comment */ + protected readonly protectedGetter: string; + /** Doc comment */ + protected protectedSetter(x: string): void; + /** Doc comment */ + static publicStaticField: number; + /** Doc comment */ + defaultPublicMethod(): void; +} + /** * @public */ @@ -98,6 +127,15 @@ export declare class ClassWithTypeLiterals { } | undefined; } +/** + * @public + */ +export declare const enum ConstEnum { + Zero = 0, + One = 1, + Two = 2 +} + /** * Tests a decorator * @public @@ -139,6 +177,9 @@ export declare class ForgottenExportConsumer3 { test2(): IForgottenDirectDependency | undefined; } +/** @public */ +export declare const fullyExportedCustomSymbol: unique symbol; + /** * This class is directly consumed by ForgottenExportConsumer3. */ @@ -193,6 +234,8 @@ export declare interface IInterfaceAsDefaultExport { export declare interface ISimpleInterface { } +declare const locallyExportedCustomSymbol: unique symbol; + /** * This class gets aliased twice before being exported from the package. * @public @@ -206,6 +249,24 @@ export declare class ReexportedClass { export declare class ReferenceLibDirective extends Intl.PluralRules { } +/** + * @public + */ +export declare enum RegularEnum { + /** + * These are some docs for Zero + */ + Zero = 0, + /** + * These are some docs for One + */ + One = 1, + /** + * These are some docs for Two + */ + Two = 2 +} + /** * This class has links such as {@link TypeReferencesInAedoc}. * @public @@ -225,14 +286,10 @@ export declare class TypeReferencesInAedoc { getValue3(arg1: TypeReferencesInAedoc): TypeReferencesInAedoc; } -/* Excluded from this release type: VARIABLE */ - -export declare const fullyExportedCustomSymbol: unique symbol; - -declare const locallyExportedCustomSymbol: unique symbol; - declare const unexportedCustomSymbol: unique symbol; +/* Excluded from this release type: VARIABLE */ + /** * Example decorator * @public diff --git a/build-tests/api-extractor-test-01/dist/internal/api-extractor-test-01.d.ts b/build-tests/api-extractor-test-01/dist/internal/api-extractor-test-01.d.ts index 1f121af884b..f3c487f4223 100644 --- a/build-tests/api-extractor-test-01/dist/internal/api-extractor-test-01.d.ts +++ b/build-tests/api-extractor-test-01/dist/internal/api-extractor-test-01.d.ts @@ -1,11 +1,11 @@ /** * api-extractor-test-01 - * + * * @remarks * This library is consumed by api-extractor-test-02 and api-extractor-test-03. * It tests the basic types of definitions, and all the weird cases for following * chains of type aliases. - * + * * @packagedocumentation */ @@ -72,6 +72,35 @@ export declare class AmbientConsumer { export declare class ClassExportedAsDefault { } +/** + * This class gets aliased twice before being exported from the package. + * @public + */ +export declare class ClassWithAccessModifiers { + /** Doc comment */ + private _privateField; + /** Doc comment */ + private privateMethod; + /** Doc comment */ + private readonly privateGetter; + /** Doc comment */ + private privateSetter; + /** Doc comment */ + private constructor(); + /** Doc comment */ + private static privateStaticMethod; + /** Doc comment */ + protected protectedField: number; + /** Doc comment */ + protected readonly protectedGetter: string; + /** Doc comment */ + protected protectedSetter(x: string): void; + /** Doc comment */ + static publicStaticField: number; + /** Doc comment */ + defaultPublicMethod(): void; +} + /** * @public */ @@ -98,6 +127,15 @@ export declare class ClassWithTypeLiterals { } | undefined; } +/** + * @public + */ +export declare const enum ConstEnum { + Zero = 0, + One = 1, + Two = 2 +} + /** * Tests a decorator * @public @@ -139,6 +177,9 @@ export declare class ForgottenExportConsumer3 { test2(): IForgottenDirectDependency | undefined; } +/** @public */ +export declare const fullyExportedCustomSymbol: unique symbol; + /** * This class is directly consumed by ForgottenExportConsumer3. */ @@ -213,6 +254,8 @@ export declare interface IMergedInterfaceReferencee { export declare interface ISimpleInterface { } +declare const locallyExportedCustomSymbol: unique symbol; + /** * This class gets aliased twice before being exported from the package. * @public @@ -226,6 +269,24 @@ export declare class ReexportedClass { export declare class ReferenceLibDirective extends Intl.PluralRules { } +/** + * @public + */ +export declare enum RegularEnum { + /** + * These are some docs for Zero + */ + Zero = 0, + /** + * These are some docs for One + */ + One = 1, + /** + * These are some docs for Two + */ + Two = 2 +} + /** * This class has links such as {@link TypeReferencesInAedoc}. * @public @@ -245,14 +306,11 @@ export declare class TypeReferencesInAedoc { getValue3(arg1: TypeReferencesInAedoc): TypeReferencesInAedoc; } -export declare const VARIABLE: string; - -export declare const fullyExportedCustomSymbol: unique symbol; - -declare const locallyExportedCustomSymbol: unique symbol; - declare const unexportedCustomSymbol: unique symbol; +/** @alpha */ +export declare const VARIABLE: string; + /** * Example decorator * @public diff --git a/build-tests/api-extractor-test-01/dist/public/api-extractor-test-01.d.ts b/build-tests/api-extractor-test-01/dist/public/api-extractor-test-01.d.ts index 43fb89a5849..4ca2fb7c7bd 100644 --- a/build-tests/api-extractor-test-01/dist/public/api-extractor-test-01.d.ts +++ b/build-tests/api-extractor-test-01/dist/public/api-extractor-test-01.d.ts @@ -1,11 +1,11 @@ /** * api-extractor-test-01 - * + * * @remarks * This library is consumed by api-extractor-test-02 and api-extractor-test-03. * It tests the basic types of definitions, and all the weird cases for following * chains of type aliases. - * + * * @packagedocumentation */ @@ -72,6 +72,35 @@ export declare class AmbientConsumer { export declare class ClassExportedAsDefault { } +/** + * This class gets aliased twice before being exported from the package. + * @public + */ +export declare class ClassWithAccessModifiers { + /** Doc comment */ + private _privateField; + /** Doc comment */ + private privateMethod; + /** Doc comment */ + private readonly privateGetter; + /** Doc comment */ + private privateSetter; + /** Doc comment */ + private constructor(); + /** Doc comment */ + private static privateStaticMethod; + /** Doc comment */ + protected protectedField: number; + /** Doc comment */ + protected readonly protectedGetter: string; + /** Doc comment */ + protected protectedSetter(x: string): void; + /** Doc comment */ + static publicStaticField: number; + /** Doc comment */ + defaultPublicMethod(): void; +} + /** * @public */ @@ -98,6 +127,15 @@ export declare class ClassWithTypeLiterals { } | undefined; } +/** + * @public + */ +export declare const enum ConstEnum { + Zero = 0, + One = 1, + Two = 2 +} + /** * Tests a decorator * @public @@ -132,6 +170,9 @@ export declare class ForgottenExportConsumer2 { /* Excluded from this release type: ForgottenExportConsumer3 */ +/** @public */ +export declare const fullyExportedCustomSymbol: unique symbol; + /** * This class is directly consumed by ForgottenExportConsumer3. */ @@ -186,6 +227,8 @@ export declare interface IInterfaceAsDefaultExport { export declare interface ISimpleInterface { } +declare const locallyExportedCustomSymbol: unique symbol; + /** * This class gets aliased twice before being exported from the package. * @public @@ -199,6 +242,24 @@ export declare class ReexportedClass { export declare class ReferenceLibDirective extends Intl.PluralRules { } +/** + * @public + */ +export declare enum RegularEnum { + /** + * These are some docs for Zero + */ + Zero = 0, + /** + * These are some docs for One + */ + One = 1, + /** + * These are some docs for Two + */ + Two = 2 +} + /** * This class has links such as {@link TypeReferencesInAedoc}. * @public @@ -218,14 +279,10 @@ export declare class TypeReferencesInAedoc { getValue3(arg1: TypeReferencesInAedoc): TypeReferencesInAedoc; } -/* Excluded from this release type: VARIABLE */ - -export declare const fullyExportedCustomSymbol: unique symbol; - -declare const locallyExportedCustomSymbol: unique symbol; - declare const unexportedCustomSymbol: unique symbol; +/* Excluded from this release type: VARIABLE */ + /** * Example decorator * @public diff --git a/build-tests/api-extractor-test-01/etc/api-extractor-test-01.api.ts b/build-tests/api-extractor-test-01/etc/api-extractor-test-01.api.ts index d96628447d9..43c9009a3f6 100644 --- a/build-tests/api-extractor-test-01/etc/api-extractor-test-01.api.ts +++ b/build-tests/api-extractor-test-01/etc/api-extractor-test-01.api.ts @@ -1,94 +1,128 @@ // @public -class AbstractClass { - // (undocumented) - abstract test(): void; +abstract class AbstractClass { + // (undocumented) + abstract test(): void; } // @public -class AbstractClass2 { - // (undocumented) - abstract test2(): void; +declare abstract class AbstractClass2 { + // (undocumented) + abstract test2(): void; } // @public -class AbstractClass3 { - // (undocumented) - abstract test3(): void; +declare abstract class AbstractClass3 { + // (undocumented) + abstract test3(): void; } // @public -class AmbientConsumer { - builtinDefinition1(): Map; - builtinDefinition2(): Promise; - definitelyTyped(): jest.Context; - // WARNING: The type "IAmbientInterfaceExample" needs to be exported by the package (e.g. added to index.ts) - localTypings(): IAmbientInterfaceExample; +declare class AmbientConsumer { + builtinDefinition1(): Map; + builtinDefinition2(): Promise; + definitelyTyped(): jest.Context; + localTypings(): IAmbientInterfaceExample; } // @public class ClassExportedAsDefault { } +// @public +declare class ClassWithAccessModifiers { + defaultPublicMethod(): void; + protected protectedField: number; + protected readonly protectedGetter: string; + protected protectedSetter(x: string): void; + static publicStaticField: number; +} + // @public (undocumented) -class ClassWithSymbols { - // (undocumented) - readonly __computed: number; +declare class ClassWithSymbols { + // (undocumented) + readonly [unexportedCustomSymbol]: number; + // (undocumented) + readonly [locallyExportedCustomSymbol]: string; + // (undocumented) + [fullyExportedCustomSymbol](): void; } // @public -class ClassWithTypeLiterals { - method1(vector: { - x: number; - y: number; - }): void; - method2(): { - classValue: ClassWithTypeLiterals; - callback: () => number; - } | undefined; +declare class ClassWithTypeLiterals { + method1(vector: { + // (undocumented) + x: number; + // (undocumented) + y: number; + }): void; + method2(): { + // (undocumented) + classValue: ClassWithTypeLiterals; + // (undocumented) + callback: () => number; + } | undefined; +} + +// @public (undocumented) +declare const enum ConstEnum { + // (undocumented) + One = 1, + // (undocumented) + Two = 2, + // (undocumented) + Zero = 0 } // @public -class DecoratorTest { - test(): void; +declare class DecoratorTest { + test(): void; } // @public (undocumented) -class DefaultExportEdgeCase { - reference: ClassExportedAsDefault; +declare class DefaultExportEdgeCase { + reference: ClassExportedAsDefault; } // @public (undocumented) -class ForgottenExportConsumer1 { - // WARNING: The type "IForgottenExport" needs to be exported by the package (e.g. added to index.ts) - // (undocumented) - test1(): IForgottenExport | undefined; +declare class ForgottenExportConsumer1 { + // (undocumented) + test1(): IForgottenExport | undefined; } // @public (undocumented) -class ForgottenExportConsumer2 { - // WARNING: The type "IForgottenExport" needs to be exported by the package (e.g. added to index.ts) - // (undocumented) - test2(): IForgottenExport | undefined; +declare class ForgottenExportConsumer2 { + // (undocumented) + test2(): IForgottenExport_2 | undefined; } // @beta -class ForgottenExportConsumer3 { - // WARNING: The type "IForgottenDirectDependency" needs to be exported by the package (e.g. added to index.ts) - // (undocumented) - test2(): IForgottenDirectDependency | undefined; +declare class ForgottenExportConsumer3 { + // (undocumented) + test2(): IForgottenDirectDependency | undefined; } +// @public (undocumented) +declare const fullyExportedCustomSymbol: unique symbol; + // @public interface IInterfaceAsDefaultExport { - member: string; + member: string; } // @alpha interface IMergedInterface { - // (undocumented) - reference: IMergedInterfaceReferencee; - // (undocumented) - type: string; + // (undocumented) + reference: IMergedInterfaceReferencee; + // (undocumented) + type: string; +} + +// @alpha +interface IMergedInterface { + // (undocumented) + reference: IMergedInterfaceReferencee; + // (undocumented) + type: string; } // @alpha (undocumented) @@ -100,27 +134,36 @@ interface ISimpleInterface { } // @public -class ReexportedClass { - // (undocumented) - getSelfReference(): ReexportedClass1; - // (undocumented) - getValue(): string; +declare class ReexportedClass { + // (undocumented) + getSelfReference(): ReexportedClass; + // (undocumented) + getValue(): string; +} + +// @public (undocumented) +declare class ReferenceLibDirective extends Intl.PluralRules { } // @public (undocumented) -class ReferenceLibDirective extends Intl.PluralRules { +declare enum RegularEnum { + One = 1, + Two = 2, + Zero = 0 } // @public -class TypeReferencesInAedoc { - getValue(arg1: TypeReferencesInAedoc): TypeReferencesInAedoc; - getValue2(arg1: TypeReferencesInAedoc): TypeReferencesInAedoc; - // (undocumented) - getValue3(arg1: TypeReferencesInAedoc): TypeReferencesInAedoc; +declare class TypeReferencesInAedoc { + getValue(arg1: TypeReferencesInAedoc): TypeReferencesInAedoc; + // (undocumented) + getValue2(arg1: TypeReferencesInAedoc): TypeReferencesInAedoc; + // (undocumented) + getValue3(arg1: TypeReferencesInAedoc): TypeReferencesInAedoc; } +// @alpha (undocumented) +declare const VARIABLE: string; + // @public -export function virtual(target: Object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor): void; +declare function virtual(target: Object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor): void; -// WARNING: Unsupported export: fullyExportedCustomSymbol -// WARNING: Unsupported export: VARIABLE diff --git a/build-tests/api-extractor-test-01/src/AccessModifiers.ts b/build-tests/api-extractor-test-01/src/AccessModifiers.ts new file mode 100644 index 00000000000..faf391265a8 --- /dev/null +++ b/build-tests/api-extractor-test-01/src/AccessModifiers.ts @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +/** + * This class gets aliased twice before being exported from the package. + * @public + */ +export class ClassWithAccessModifiers { + /** Doc comment */ + private _privateField: number = 123; + + /** Doc comment */ + private privateMethod(): void { + } + + /** Doc comment */ + private get privateGetter(): string { + return ''; + } + + /** Doc comment */ + private privateSetter(x: string) { + } + + /** Doc comment */ + private constructor() { + } + + /** Doc comment */ + private static privateStaticMethod() { + } + + + /** Doc comment */ + protected protectedField: number; + + /** Doc comment */ + protected get protectedGetter(): string { + return ''; + } + + /** Doc comment */ + protected protectedSetter(x: string) { + } + + /** Doc comment */ + public static publicStaticField: number = 123; + + /** Doc comment */ + defaultPublicMethod(): void { + } +} diff --git a/build-tests/api-extractor-test-01/src/EcmaScriptSymbols.ts b/build-tests/api-extractor-test-01/src/EcmaScriptSymbols.ts index 007458322c5..161fce16d29 100644 --- a/build-tests/api-extractor-test-01/src/EcmaScriptSymbols.ts +++ b/build-tests/api-extractor-test-01/src/EcmaScriptSymbols.ts @@ -3,6 +3,8 @@ const unexportedCustomSymbol: unique symbol = Symbol('unexportedCustomSymbol'); export const locallyExportedCustomSymbol: unique symbol = Symbol('locallyExportedCustomSymbol'); + +/** @public */ export const fullyExportedCustomSymbol: unique symbol = Symbol('fullyExportedCustomSymbol'); /** diff --git a/build-tests/api-extractor-test-01/src/Enums.ts b/build-tests/api-extractor-test-01/src/Enums.ts new file mode 100644 index 00000000000..782dd5544d5 --- /dev/null +++ b/build-tests/api-extractor-test-01/src/Enums.ts @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +/** + * @public + */ +export enum RegularEnum { + /** + * These are some docs for Zero + */ + Zero, + + /** + * These are some docs for One + */ + One = 1, + + /** + * These are some docs for Two + */ + Two = RegularEnum.One + 1 +} + +/** + * @public + */ +export const enum ConstEnum { + Zero, + One = 1, + Two = RegularEnum.One + 1 +} + diff --git a/build-tests/api-extractor-test-01/src/index.ts b/build-tests/api-extractor-test-01/src/index.ts index 75194aa09c2..2eadf23e95e 100644 --- a/build-tests/api-extractor-test-01/src/index.ts +++ b/build-tests/api-extractor-test-01/src/index.ts @@ -83,10 +83,14 @@ export class DecoratorTest { export { default as AbstractClass } from './AbstractClass'; export { default as AbstractClass2, AbstractClass3 } from './AbstractClass2'; +export { ClassWithAccessModifiers } from './AccessModifiers'; + export { ClassWithTypeLiterals } from './ClassWithTypeLiterals'; export * from './DeclarationMerging'; +export * from './Enums'; + export { DefaultExportEdgeCase, default as ClassExportedAsDefault @@ -109,4 +113,4 @@ export { ReexportedClass3 as ReexportedClass } from './ReexportedClass3/Reexport export { TypeReferencesInAedoc } from './TypeReferencesInAedoc'; export { ReferenceLibDirective } from './ReferenceLibDirective'; -export { VARIABLE } from './variableDeclarations'; \ No newline at end of file +export { VARIABLE } from './variableDeclarations'; diff --git a/build-tests/api-extractor-test-02/dist/beta/api-extractor-test-02.d.ts b/build-tests/api-extractor-test-02/dist/beta/api-extractor-test-02.d.ts index 277ac545773..7603bfcb344 100644 --- a/build-tests/api-extractor-test-02/dist/beta/api-extractor-test-02.d.ts +++ b/build-tests/api-extractor-test-02/dist/beta/api-extractor-test-02.d.ts @@ -1,9 +1,9 @@ /** * api-extractor-test-02 - * + * * @remarks * This library consumes api-extractor-test-01 and is consumed by api-extractor-test-03. - * + * * @packagedocumentation */ @@ -19,6 +19,12 @@ export declare interface GenericInterface { member: T; } +/** @public */ +export declare function importDeduping1(arg1: ISimpleInterface, arg2: ISimpleInterface): void; + +/** @public */ +export declare function importDeduping2(arg1: ISimpleInterface, arg2: ISimpleInterface): void; + /** * A class that inherits from a type defined in the "semver" module imported from \@types/semver. * @public @@ -26,20 +32,6 @@ export declare interface GenericInterface { export declare class ImportedModuleAsBaseClass extends semver1.SemVer { } -/** - * Example of a class that inherits from an externally imported class. - * @public - */ -export declare class SubclassWithImport extends ReexportedClass implements ISimpleInterface { - test(): void; -} - -/** @public */ -export declare function importDeduping1(arg1: ISimpleInterface, arg2: ISimpleInterface): void; - -/** @public */ -export declare function importDeduping2(arg1: ISimpleInterface, arg2: ISimpleInterface): void; - /** * A generic parameter that references the "semver" module imported from \@types/semver. * @public @@ -51,3 +43,11 @@ export declare function importedModuleAsGenericParameter(): GenericInterface { member: T; } +/** @public */ +export declare function importDeduping1(arg1: ISimpleInterface, arg2: ISimpleInterface): void; + +/** @public */ +export declare function importDeduping2(arg1: ISimpleInterface, arg2: ISimpleInterface): void; + /** * A class that inherits from a type defined in the "semver" module imported from \@types/semver. * @public @@ -26,20 +32,6 @@ export declare interface GenericInterface { export declare class ImportedModuleAsBaseClass extends semver1.SemVer { } -/** - * Example of a class that inherits from an externally imported class. - * @public - */ -export declare class SubclassWithImport extends ReexportedClass implements ISimpleInterface { - test(): void; -} - -/** @public */ -export declare function importDeduping1(arg1: ISimpleInterface, arg2: ISimpleInterface): void; - -/** @public */ -export declare function importDeduping2(arg1: ISimpleInterface, arg2: ISimpleInterface): void; - /** * A generic parameter that references the "semver" module imported from \@types/semver. * @public @@ -51,3 +43,11 @@ export declare function importedModuleAsGenericParameter(): GenericInterface { member: T; } +/** @public */ +export declare function importDeduping1(arg1: ISimpleInterface, arg2: ISimpleInterface): void; + +/** @public */ +export declare function importDeduping2(arg1: ISimpleInterface, arg2: ISimpleInterface): void; + /** * A class that inherits from a type defined in the "semver" module imported from \@types/semver. * @public @@ -26,20 +32,6 @@ export declare interface GenericInterface { export declare class ImportedModuleAsBaseClass extends semver1.SemVer { } -/** - * Example of a class that inherits from an externally imported class. - * @public - */ -export declare class SubclassWithImport extends ReexportedClass implements ISimpleInterface { - test(): void; -} - -/** @public */ -export declare function importDeduping1(arg1: ISimpleInterface, arg2: ISimpleInterface): void; - -/** @public */ -export declare function importDeduping2(arg1: ISimpleInterface, arg2: ISimpleInterface): void; - /** * A generic parameter that references the "semver" module imported from \@types/semver. * @public @@ -51,3 +43,11 @@ export declare function importedModuleAsGenericParameter(): GenericInterface { - // (undocumented) - member: T; + // (undocumented) + member: T; } // @public (undocumented) -export function importDeduping1(arg1: ISimpleInterface, arg2: ISimpleInterface2): void; +declare function importDeduping1(arg1: ISimpleInterface, arg2: ISimpleInterface): void; // @public (undocumented) -export function importDeduping2(arg1: ISimpleInterface, arg2: ISimpleInterface2): void; +declare function importDeduping2(arg1: ISimpleInterface, arg2: ISimpleInterface): void; // @public -class ImportedModuleAsBaseClass extends semver3.SemVer { +declare class ImportedModuleAsBaseClass extends semver1.SemVer { } // @public -export function importedModuleAsGenericParameter(): GenericInterface | undefined; +declare function importedModuleAsGenericParameter(): GenericInterface | undefined; // @public -export function importedModuleAsReturnType(): semver1.SemVer | undefined; +declare function importedModuleAsReturnType(): semver1.SemVer | undefined; // @public -class SubclassWithImport extends RenamedReexportedClass, implements ISimpleInterface { - // (undocumented) - test(): void; +declare class SubclassWithImport extends ReexportedClass implements ISimpleInterface { + // (undocumented) + test(): void; } diff --git a/build-tests/api-extractor-test-04/dist/beta/api-extractor-test-04.d.ts b/build-tests/api-extractor-test-04/dist/beta/api-extractor-test-04.d.ts index d2beab15c42..074c3bcad02 100644 --- a/build-tests/api-extractor-test-04/dist/beta/api-extractor-test-04.d.ts +++ b/build-tests/api-extractor-test-04/dist/beta/api-extractor-test-04.d.ts @@ -1,8 +1,8 @@ /** * api-extractor-test-04 - * + * * Test scenarios for trimming alpha/beta/internal definitions from the generated *.d.ts files. - * + * * @packagedocumentation */ @@ -71,6 +71,8 @@ export declare namespace EntangledNamespace { /* Excluded from this release type: ExportedAlias */ +/* Excluded from this release type: InternalClass */ + /* Excluded from this release type: IPublicClassInternalParameters */ /** @@ -82,8 +84,6 @@ export declare interface IPublicComplexInterface { /* Excluded from this release type: __new */ } -/* Excluded from this release type: InternalClass */ - /** * This is a public class * @public @@ -121,4 +121,8 @@ export declare enum RegularEnum { /* Excluded from this release type: _InternalMember */ } +/** + * This is a module-scoped variable. + * @beta + */ export declare const variableDeclaration: string; diff --git a/build-tests/api-extractor-test-04/dist/internal/api-extractor-test-04.d.ts b/build-tests/api-extractor-test-04/dist/internal/api-extractor-test-04.d.ts index b4ca54e4e9a..f66149787e9 100644 --- a/build-tests/api-extractor-test-04/dist/internal/api-extractor-test-04.d.ts +++ b/build-tests/api-extractor-test-04/dist/internal/api-extractor-test-04.d.ts @@ -1,8 +1,8 @@ /** * api-extractor-test-04 - * + * * Test scenarios for trimming alpha/beta/internal definitions from the generated *.d.ts files. - * + * * @packagedocumentation */ @@ -135,6 +135,17 @@ export declare namespace EntangledNamespace { */ export declare type ExportedAlias = AlphaClass; +/** + * This is an internal class + * @internal + */ +export declare class InternalClass { + /** + * This is a comment + */ + undecoratedMember(): void; +} + /** * These are internal constructor parameters for PublicClass's internal constructor. * @internal @@ -159,17 +170,6 @@ export declare interface IPublicComplexInterface { new (): any; } -/** - * This is an internal class - * @internal - */ -export declare class InternalClass { - /** - * This is a comment - */ - undecoratedMember(): void; -} - /** * This is a public class * @public @@ -224,4 +224,8 @@ export declare enum RegularEnum { _InternalMember = 102 } +/** + * This is a module-scoped variable. + * @beta + */ export declare const variableDeclaration: string; diff --git a/build-tests/api-extractor-test-04/dist/public/api-extractor-test-04.d.ts b/build-tests/api-extractor-test-04/dist/public/api-extractor-test-04.d.ts index 0aecad051f2..1b1470e4895 100644 --- a/build-tests/api-extractor-test-04/dist/public/api-extractor-test-04.d.ts +++ b/build-tests/api-extractor-test-04/dist/public/api-extractor-test-04.d.ts @@ -1,8 +1,8 @@ /** * api-extractor-test-04 - * + * * Test scenarios for trimming alpha/beta/internal definitions from the generated *.d.ts files. - * + * * @packagedocumentation */ @@ -19,6 +19,8 @@ /* Excluded from this release type: ExportedAlias */ +/* Excluded from this release type: InternalClass */ + /* Excluded from this release type: IPublicClassInternalParameters */ /** @@ -30,8 +32,6 @@ export declare interface IPublicComplexInterface { /* Excluded from this release type: __new */ } -/* Excluded from this release type: InternalClass */ - /** * This is a public class * @public diff --git a/build-tests/api-extractor-test-04/etc/api-extractor-test-04.api.ts b/build-tests/api-extractor-test-04/etc/api-extractor-test-04.api.ts index 40d94cf0bd0..f75d4327d8d 100644 --- a/build-tests/api-extractor-test-04/etc/api-extractor-test-04.api.ts +++ b/build-tests/api-extractor-test-04/etc/api-extractor-test-04.api.ts @@ -1,85 +1,98 @@ // @alpha -class AlphaClass { - // @internal - _internalMember(): void; - undecoratedMember(): void; +declare class AlphaClass { + // @internal + _internalMember(): void; + undecoratedMember(): void; } // @beta -class BetaClass implements BetaInterface { - // @internal - _internalMember(): void; - // @alpha - alphaMember(): void; - undecoratedMember(): void; +declare class BetaClass implements BetaInterface { + // @alpha + alphaMember(): void; + // @internal + _internalMember(): void; + undecoratedMember(): void; } // @beta interface BetaInterface { - // @internal - _internalMember(): void; - // @alpha - alphaMember(): void; - undecoratedMember(): void; + // @alpha + alphaMember(): void; + // @internal + _internalMember(): void; + undecoratedMember(): void; } // @beta -enum ConstEnum { - // @internal - _InternalMember = "_InternalMember", - // @alpha - AlphaMember = "AlphaMember", - BetaMember2 = "BetaMember2" +declare const enum ConstEnum { + // @alpha + AlphaMember = "AlphaMember", + BetaMember2 = "BetaMember2", + // @internal + _InternalMember = "_InternalMember" } -// WARNING: Unsupported export "N2" Currently the "namespace" block only supports constant variables. -// WARNING: Unsupported export "N3" Currently the "namespace" block only supports constant variables. // @beta -module EntangledNamespace { +declare namespace EntangledNamespace { + namespace N2 { + // @alpha + class ClassX { + static a: string; + } + } + namespace N3 { + // @internal + class _ClassY { + b: EntangledNamespace.N2.ClassX; + c(): typeof N2.ClassX.a; + } + } } -// WARNING: Because this definition is explicitly marked as @internal, an underscore prefix ("_") should be added to its name +// @alpha +declare type ExportedAlias = AlphaClass; + // @internal -class InternalClass { - undecoratedMember(): void; +declare class InternalClass { + undecoratedMember(): void; } -// WARNING: Because this definition is explicitly marked as @internal, an underscore prefix ("_") should be added to its name // @internal interface IPublicClassInternalParameters { } // @public interface IPublicComplexInterface { - // @internal - [key: string]: IPublicClassInternalParameters; - // @internal - new (): any; + // @internal + [key: string]: IPublicClassInternalParameters; + // @internal + new (): any; } // @public -class PublicClass { - // @internal - constructor(parameters: IPublicClassInternalParameters); - // @internal - _internalMember(): void; - // @alpha - alphaMember(): void; - // @beta - betaField: string; - // @beta - betaMember(): void; - undecoratedMember(): void; +declare class PublicClass { + // @internal (undocumented) + constructor(parameters: IPublicClassInternalParameters); + // @alpha + alphaMember(): void; + // @beta + betaField: string; + // @beta + betaMember(): void; + // @internal + _internalMember(): void; + undecoratedMember(): void; } // @beta -enum RegularEnum { - // @internal - _InternalMember = 102, - // @alpha - AlphaMember = 101, - BetaMember = 100 +declare enum RegularEnum { + // @alpha + AlphaMember = 101, + BetaMember = 100, + // @internal + _InternalMember = 102 } -// WARNING: Unsupported export: variableDeclaration -// WARNING: Unsupported export: ExportedAlias +// @beta +declare const variableDeclaration: string; + diff --git a/build-tests/api-extractor-test-05/etc/api-extractor-test-05.api.json b/build-tests/api-extractor-test-05/etc/api-extractor-test-05.api.json index 84fb4aad2b4..f434c05745e 100644 --- a/build-tests/api-extractor-test-05/etc/api-extractor-test-05.api.json +++ b/build-tests/api-extractor-test-05/etc/api-extractor-test-05.api.json @@ -1,321 +1,717 @@ { - "kind": "package", + "metadata": { + "toolPackage": "@microsoft/api-extractor", + "toolVersion": "6.3.0", + "schemaVersion": 1000 + }, + "kind": "Package", "name": "api-extractor-test-05", - "summary": [ + "canonicalReference": "api-extractor-test-05", + "docComment": "/**\n * api-extractor-test-05\n *\n * This project tests various documentation generation scenarios and doc comment syntaxes.\n *\n * @packagedocumentation\n */\n", + "members": [ { - "kind": "text", - "text": "api-extractor-test-05" - }, - { - "kind": "paragraph" - }, - { - "kind": "text", - "text": "This project tests various documentation generation scenarios and doc comment syntaxes." - } - ], - "remarks": [], - "exports": { - "DocClass1": { - "kind": "class", - "extends": "", - "implements": "", - "typeParameters": [], - "deprecatedMessage": [], - "summary": [ + "kind": "EntryPoint", + "name": "", + "canonicalReference": "", + "members": [ { - "kind": "text", - "text": "This is an example class." - } - ], - "remarks": [ - { - "kind": "text", - "text": "These are some remarks." - } - ], - "isBeta": false, - "isSealed": false, - "members": { - "exampleFunction": { - "kind": "method", - "signature": "exampleFunction(a: string, b: string): string;", - "accessModifier": "", - "isOptional": false, - "isStatic": false, - "returnValue": { - "type": "string", - "description": [] - }, - "parameters": { - "a": { - "name": "a", - "description": [ - { - "kind": "text", - "text": "the first string" + "kind": "Class", + "name": "DocClass1", + "canonicalReference": "(DocClass1:class)", + "docComment": "/**\n * This is an example class.\n *\n * @remarks\n *\n * These are some remarks.\n *\n * @defaultvalue\n *\n * a default value for this function\n *\n * @public\n */\n", + "releaseTag": "Public", + "members": [ + { + "kind": "Method", + "name": "deprecatedExample", + "canonicalReference": "(deprecatedExample:instance,0)", + "docComment": "/**\n * @deprecated\n *\n * Use `otherThing()` instead.\n */\n", + "isStatic": false, + "releaseTag": "Public", + "overloadIndex": 0, + "parameters": [], + "excerptTokens": [ + { + "kind": "Reference", + "text": "deprecatedExample" + }, + { + "kind": "Content", + "text": "(): " + }, + { + "kind": "Content", + "text": "void" + }, + { + "kind": "Content", + "text": ";" } ], - "isOptional": false, - "isSpread": false, - "type": "string" + "embeddedExcerpts": { + "returnType": { + "startIndex": 2, + "endIndex": 3 + } + } }, - "b": { - "name": "b", - "description": [ + { + "kind": "Method", + "name": "exampleFunction", + "canonicalReference": "(exampleFunction:instance,0)", + "docComment": "/**\n * This is an overloaded function.\n *\n * @param a - the first string\n *\n * @param b - the second string\n */\n", + "isStatic": false, + "releaseTag": "Public", + "overloadIndex": 0, + "parameters": [ + { + "kind": "Parameter", + "name": "a", + "canonicalReference": "a", + "excerptTokens": [ + { + "kind": "Reference", + "text": "a" + }, + { + "kind": "Content", + "text": ": " + }, + { + "kind": "Content", + "text": "string" + } + ], + "embeddedExcerpts": { + "parameterType": { + "startIndex": 2, + "endIndex": 3 + } + } + }, { - "kind": "text", - "text": "the second string" + "kind": "Parameter", + "name": "b", + "canonicalReference": "b", + "excerptTokens": [ + { + "kind": "Reference", + "text": "b" + }, + { + "kind": "Content", + "text": ": " + }, + { + "kind": "Content", + "text": "string" + } + ], + "embeddedExcerpts": { + "parameterType": { + "startIndex": 2, + "endIndex": 3 + } + } } ], - "isOptional": false, - "isSpread": false, - "type": "string" - } - }, - "deprecatedMessage": [], - "summary": [ - { - "kind": "text", - "text": "This is an overloaded function." - } - ], - "remarks": [], - "isBeta": false, - "isSealed": false, - "isVirtual": false, - "isOverride": false - }, - "interestingEdgeCases": { - "kind": "method", - "signature": "interestingEdgeCases(): void;", - "accessModifier": "", - "isOptional": false, - "isStatic": false, - "returnValue": { - "type": "void", - "description": [] - }, - "parameters": {}, - "deprecatedMessage": [], - "summary": [ - { - "kind": "text", - "text": "Example: \"{ \\\"maxItemsToShow\\\": 123 }\"" + "excerptTokens": [ + { + "kind": "Reference", + "text": "exampleFunction" + }, + { + "kind": "Content", + "text": "(" + }, + { + "kind": "Reference", + "text": "a" + }, + { + "kind": "Content", + "text": ": string, " + }, + { + "kind": "Reference", + "text": "b" + }, + { + "kind": "Content", + "text": ": string): " + }, + { + "kind": "Content", + "text": "string" + }, + { + "kind": "Content", + "text": ";" + } + ], + "embeddedExcerpts": { + "returnType": { + "startIndex": 6, + "endIndex": 7 + } + } }, { - "kind": "paragraph" + "kind": "Method", + "name": "exampleFunction", + "canonicalReference": "(exampleFunction:instance,1)", + "docComment": "/**\n * This is also an overloaded function.\n *\n * @param x - the number\n */\n", + "isStatic": false, + "releaseTag": "Public", + "overloadIndex": 1, + "parameters": [ + { + "kind": "Parameter", + "name": "x", + "canonicalReference": "x", + "excerptTokens": [ + { + "kind": "Reference", + "text": "x" + }, + { + "kind": "Content", + "text": ": " + }, + { + "kind": "Content", + "text": "number" + } + ], + "embeddedExcerpts": { + "parameterType": { + "startIndex": 2, + "endIndex": 3 + } + } + } + ], + "excerptTokens": [ + { + "kind": "Reference", + "text": "exampleFunction" + }, + { + "kind": "Content", + "text": "(" + }, + { + "kind": "Reference", + "text": "x" + }, + { + "kind": "Content", + "text": ": number): " + }, + { + "kind": "Content", + "text": "number" + }, + { + "kind": "Content", + "text": ";" + } + ], + "embeddedExcerpts": { + "returnType": { + "startIndex": 4, + "endIndex": 5 + } + } }, { - "kind": "text", - "text": "The regular expression used to validate the constraints is /^[a-zA-Z0-9\\-_]+$/" - } - ], - "remarks": [], - "isBeta": false, - "isSealed": false, - "isVirtual": false, - "isOverride": false - }, - "malformedEvent": { - "kind": "property", - "signature": "malformedEvent: SystemEvent;", - "isOptional": false, - "isReadOnly": false, - "isStatic": false, - "type": "SystemEvent", - "deprecatedMessage": [], - "summary": [ - { - "kind": "text", - "text": "This event should have been marked as readonly." - } - ], - "remarks": [], - "isBeta": false, - "isSealed": false, - "isVirtual": false, - "isOverride": false, - "isEventProperty": true - }, - "modifiedEvent": { - "kind": "property", - "signature": "readonly modifiedEvent: SystemEvent;", - "isOptional": false, - "isReadOnly": true, - "isStatic": false, - "type": "SystemEvent", - "deprecatedMessage": [], - "summary": [ - { - "kind": "text", - "text": "This event is fired whenever the object is modified." - } - ], - "remarks": [], - "isBeta": false, - "isSealed": false, - "isVirtual": false, - "isOverride": false, - "isEventProperty": true - }, - "regularProperty": { - "kind": "property", - "signature": "regularProperty: SystemEvent;", - "isOptional": false, - "isReadOnly": false, - "isStatic": false, - "type": "SystemEvent", - "deprecatedMessage": [], - "summary": [ - { - "kind": "text", - "text": "This is a regular property that happens to use the SystemEvent type." - } - ], - "remarks": [], - "isBeta": false, - "isSealed": false, - "isVirtual": false, - "isOverride": false, - "isEventProperty": false - }, - "tableExample": { - "kind": "method", - "signature": "tableExample(): void;", - "accessModifier": "", - "isOptional": false, - "isStatic": false, - "returnValue": { - "type": "void", - "description": [] - }, - "parameters": {}, - "deprecatedMessage": [], - "summary": [ - { - "kind": "text", - "text": "An example with tables:" - } - ], - "remarks": [ - { - "kind": "html-tag", - "token": "

` element. + */ +export class DocTableCell extends DocNode { + public readonly content: DocSection; + + public constructor(parameters: IDocTableCellParameters, sectionChildNodes?: ReadonlyArray) { + super(parameters); + + this.content = new DocSection({ configuration: this.configuration }, sectionChildNodes); + } + + /** @override */ + public get kind(): string { + return CustomDocNodeKind.TableCell; + } +} diff --git a/apps/api-documenter/src/nodes/DocTableRow.ts b/apps/api-documenter/src/nodes/DocTableRow.ts new file mode 100644 index 00000000000..2391b01f14d --- /dev/null +++ b/apps/api-documenter/src/nodes/DocTableRow.ts @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { + IDocNodeParameters, + DocNode, + DocPlainText +} from '@microsoft/tsdoc'; +import { CustomDocNodeKind } from './CustomDocNodeKind'; +import { DocTableCell } from './DocTableCell'; + +/** + * Constructor parameters for {@link DocTableRow}. + */ +export interface IDocTableRowParameters extends IDocNodeParameters { +} + +/** + * Represents table row, similar to an HTML `
\" or \"` or `` or `
" + "kind": "Method", + "name": "interestingEdgeCases", + "canonicalReference": "(interestingEdgeCases:instance,0)", + "docComment": "/**\n * Example: \"\\{ \\\\\"maxItemsToShow\\\\\": 123 \\}\"\n *\n * The regular expression used to validate the constraints is /^[a-zA-Z0-9\\\\-_]+$/\n */\n", + "isStatic": false, + "releaseTag": "Public", + "overloadIndex": 0, + "parameters": [], + "excerptTokens": [ + { + "kind": "Reference", + "text": "interestingEdgeCases" + }, + { + "kind": "Content", + "text": "(): " + }, + { + "kind": "Content", + "text": "void" + }, + { + "kind": "Content", + "text": ";" + } + ], + "embeddedExcerpts": { + "returnType": { + "startIndex": 2, + "endIndex": 3 + } + } }, { - "kind": "text", - "text": " " + "kind": "Property", + "name": "malformedEvent", + "canonicalReference": "(malformedEvent:instance)", + "docComment": "/**\n * This event should have been marked as readonly.\n *\n * @eventproperty\n */\n", + "excerptTokens": [ + { + "kind": "Reference", + "text": "malformedEvent" + }, + { + "kind": "Content", + "text": ": " + }, + { + "kind": "Reference", + "text": "SystemEvent" + }, + { + "kind": "Content", + "text": ";" + } + ], + "embeddedExcerpts": { + "propertyType": { + "startIndex": 2, + "endIndex": 3 + } + }, + "isStatic": false, + "releaseTag": "Public" }, { - "kind": "html-tag", - "token": "" + "kind": "Property", + "name": "modifiedEvent", + "canonicalReference": "(modifiedEvent:instance)", + "docComment": "/**\n * This event is fired whenever the object is modified.\n *\n * @eventproperty\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "readonly " + }, + { + "kind": "Reference", + "text": "modifiedEvent" + }, + { + "kind": "Content", + "text": ": " + }, + { + "kind": "Reference", + "text": "SystemEvent" + }, + { + "kind": "Content", + "text": ";" + } + ], + "embeddedExcerpts": { + "propertyType": { + "startIndex": 3, + "endIndex": 4 + } + }, + "isStatic": false, + "releaseTag": "Public" }, { - "kind": "text", - "text": " " + "kind": "Property", + "name": "regularProperty", + "canonicalReference": "(regularProperty:instance)", + "docComment": "/**\n * This is a regular property that happens to use the SystemEvent type.\n */\n", + "excerptTokens": [ + { + "kind": "Reference", + "text": "regularProperty" + }, + { + "kind": "Content", + "text": ": " + }, + { + "kind": "Reference", + "text": "SystemEvent" + }, + { + "kind": "Content", + "text": ";" + } + ], + "embeddedExcerpts": { + "propertyType": { + "startIndex": 2, + "endIndex": 3 + } + }, + "isStatic": false, + "releaseTag": "Public" }, { - "kind": "html-tag", - "token": "" + "kind": "Reference", + "text": "DocClass1" }, { - "kind": "text", + "kind": "Content", "text": " " - }, + } + ], + "embeddedExcerpts": {} + }, + { + "kind": "Enum", + "name": "DocEnum", + "canonicalReference": "(DocEnum:enum)", + "docComment": "/**\n * Docs for DocEnum\n *\n * @public\n */\n", + "releaseTag": "Public", + "members": [ { - "kind": "html-tag", - "token": "" - }, + "kind": "EnumMember", + "name": "Zero", + "canonicalReference": "Zero", + "docComment": "/**\n * These are some docs for Zero\n */\n", + "releaseTag": "Public", + "excerptTokens": [ + { + "kind": "Reference", + "text": "Zero" + }, + { + "kind": "Content", + "text": " = " + }, + { + "kind": "Content", + "text": "0" + } + ], + "embeddedExcerpts": { + "initializer": { + "startIndex": 2, + "endIndex": 3 + } + } + } + ], + "excerptTokens": [ { - "kind": "text", - "text": " " + "kind": "Content", + "text": "export declare enum " }, { - "kind": "html-tag", - "token": "" + "kind": "Reference", + "text": "DocEnum" }, { - "kind": "text", + "kind": "Content", "text": " " - }, - { - "kind": "html-tag", - "token": "
" + "kind": "Method", + "name": "sumWithExample", + "canonicalReference": "(sumWithExample:static,0)", + "docComment": "/**\n * Returns the sum of two numbers.\n *\n * @remarks\n *\n * This illustrates usage of the `@example` block tag.\n *\n * @param x - the first number to add\n *\n * @param y - the second number to add\n *\n * @returns the sum of the two numbers\n *\n * @example\n *\n * Here's a simple example:\n * ```\n * // Prints \"2\":\n * console.log(DocClass1.sumWithExample(1,1));\n * ```\n *\n * @example\n *\n * Here's an example with negative numbers:\n * ```\n * // Prints \"0\":\n * console.log(DocClass1.sumWithExample(1,-1));\n * ```\n *\n */\n", + "isStatic": true, + "releaseTag": "Public", + "overloadIndex": 0, + "parameters": [ + { + "kind": "Parameter", + "name": "x", + "canonicalReference": "x", + "excerptTokens": [ + { + "kind": "Reference", + "text": "x" + }, + { + "kind": "Content", + "text": ": " + }, + { + "kind": "Content", + "text": "number" + } + ], + "embeddedExcerpts": { + "parameterType": { + "startIndex": 2, + "endIndex": 3 + } + } + }, + { + "kind": "Parameter", + "name": "y", + "canonicalReference": "y", + "excerptTokens": [ + { + "kind": "Reference", + "text": "y" + }, + { + "kind": "Content", + "text": ": " + }, + { + "kind": "Content", + "text": "number" + } + ], + "embeddedExcerpts": { + "parameterType": { + "startIndex": 2, + "endIndex": 3 + } + } + } + ], + "excerptTokens": [ + { + "kind": "Content", + "text": "static " + }, + { + "kind": "Reference", + "text": "sumWithExample" + }, + { + "kind": "Content", + "text": "(" + }, + { + "kind": "Reference", + "text": "x" + }, + { + "kind": "Content", + "text": ": number, " + }, + { + "kind": "Reference", + "text": "y" + }, + { + "kind": "Content", + "text": ": number): " + }, + { + "kind": "Content", + "text": "number" + }, + { + "kind": "Content", + "text": ";" + } + ], + "embeddedExcerpts": { + "returnType": { + "startIndex": 7, + "endIndex": 8 + } + } }, { - "kind": "text", - "text": "John" + "kind": "Method", + "name": "tableExample", + "canonicalReference": "(tableExample:instance,0)", + "docComment": "/**\n * An example with tables:\n *\n * @remarks\n *\n *
John Doe
\n */\n", + "isStatic": false, + "releaseTag": "Public", + "overloadIndex": 0, + "parameters": [], + "excerptTokens": [ + { + "kind": "Reference", + "text": "tableExample" + }, + { + "kind": "Content", + "text": "(): " + }, + { + "kind": "Content", + "text": "void" + }, + { + "kind": "Content", + "text": ";" + } + ], + "embeddedExcerpts": { + "returnType": { + "startIndex": 2, + "endIndex": 3 + } + } + } + ], + "excerptTokens": [ + { + "kind": "Content", + "text": "export declare class " }, { - "kind": "html-tag", - "token": "
" + "kind": "EnumMember", + "name": "One", + "canonicalReference": "One", + "docComment": "/**\n * These are some docs for One\n */\n", + "releaseTag": "Public", + "excerptTokens": [ + { + "kind": "Reference", + "text": "One" + }, + { + "kind": "Content", + "text": " = " + }, + { + "kind": "Content", + "text": "1" + } + ], + "embeddedExcerpts": { + "initializer": { + "startIndex": 2, + "endIndex": 3 + } + } }, { - "kind": "text", - "text": "Doe" + "kind": "EnumMember", + "name": "Two", + "canonicalReference": "Two", + "docComment": "/**\n * These are some docs for Two\n */\n", + "releaseTag": "Public", + "excerptTokens": [ + { + "kind": "Reference", + "text": "Two" + }, + { + "kind": "Content", + "text": " = " + }, + { + "kind": "Content", + "text": "2" + } + ], + "embeddedExcerpts": { + "initializer": { + "startIndex": 2, + "endIndex": 3 + } + } }, { - "kind": "html-tag", - "token": "
" } ], - "isBeta": false, - "isSealed": false, - "isVirtual": false, - "isOverride": false - } - } - }, - "SystemEvent": { - "kind": "class", - "extends": "", - "implements": "", - "typeParameters": [], - "deprecatedMessage": [], - "summary": [ + "embeddedExcerpts": {} + }, { - "kind": "text", - "text": "A class used to exposed events." - } - ], - "remarks": [], - "isBeta": false, - "isSealed": false, - "members": { - "addHandler": { - "kind": "method", - "signature": "addHandler(handler: () => void): void;", - "accessModifier": "", - "isOptional": false, - "isStatic": false, - "returnValue": { - "type": "void", - "description": [] - }, - "parameters": { - "handler": { - "name": "handler", - "description": [], - "isOptional": false, - "isSpread": false, - "type": "() => void" + "kind": "Class", + "name": "SystemEvent", + "canonicalReference": "(SystemEvent:class)", + "docComment": "/**\n * A class used to exposed events.\n *\n * @public\n */\n", + "releaseTag": "Public", + "members": [ + { + "kind": "Method", + "name": "addHandler", + "canonicalReference": "(addHandler:instance,0)", + "docComment": "/**\n * Adds an handler for the event.\n */\n", + "isStatic": false, + "releaseTag": "Public", + "overloadIndex": 0, + "parameters": [ + { + "kind": "Parameter", + "name": "handler", + "canonicalReference": "handler", + "excerptTokens": [ + { + "kind": "Reference", + "text": "handler" + }, + { + "kind": "Content", + "text": ": " + }, + { + "kind": "Content", + "text": "() => void" + } + ], + "embeddedExcerpts": { + "parameterType": { + "startIndex": 2, + "endIndex": 3 + } + } + } + ], + "excerptTokens": [ + { + "kind": "Reference", + "text": "addHandler" + }, + { + "kind": "Content", + "text": "(" + }, + { + "kind": "Reference", + "text": "handler" + }, + { + "kind": "Content", + "text": ": () => void): " + }, + { + "kind": "Content", + "text": "void" + }, + { + "kind": "Content", + "text": ";" + } + ], + "embeddedExcerpts": { + "returnType": { + "startIndex": 4, + "endIndex": 5 + } + } } - }, - "deprecatedMessage": [], - "summary": [ + ], + "excerptTokens": [ { - "kind": "text", - "text": "Adds an handler for the event." + "kind": "Content", + "text": "export declare class " + }, + { + "kind": "Reference", + "text": "SystemEvent" + }, + { + "kind": "Content", + "text": " " } ], - "remarks": [], - "isBeta": false, - "isSealed": false, - "isVirtual": false, - "isOverride": false + "embeddedExcerpts": {} } - } + ] } - } + ] } diff --git a/build-tests/api-extractor-test-05/etc/api-extractor-test-05.api.ts b/build-tests/api-extractor-test-05/etc/api-extractor-test-05.api.ts index 0456557f4da..fab01c02a1f 100644 --- a/build-tests/api-extractor-test-05/etc/api-extractor-test-05.api.ts +++ b/build-tests/api-extractor-test-05/etc/api-extractor-test-05.api.ts @@ -1,18 +1,28 @@ // @public -class DocClass1 { - exampleFunction(a: string, b: string): string; - interestingEdgeCases(): void; - // WARNING: The @eventProperty tag requires the property to be readonly - // @eventproperty - malformedEvent: SystemEvent; - // @eventproperty - readonly modifiedEvent: SystemEvent; - regularProperty: SystemEvent; - tableExample(): void; +declare class DocClass1 { + // @deprecated (undocumented) + deprecatedExample(): void; + exampleFunction(a: string, b: string): string; + exampleFunction(x: number): number; + interestingEdgeCases(): void; + // @eventproperty + malformedEvent: SystemEvent; + // @eventproperty + readonly modifiedEvent: SystemEvent; + regularProperty: SystemEvent; + static sumWithExample(x: number, y: number): number; + tableExample(): void; } // @public -class SystemEvent { - addHandler(handler: () => void): void; +declare enum DocEnum { + One = 1, + Two = 2, + Zero = 0 +} + +// @public +declare class SystemEvent { + addHandler(handler: () => void): void; } diff --git a/build-tests/api-extractor-test-05/etc/markdown/api-extractor-test-05.docclass1.deprecatedexample.md b/build-tests/api-extractor-test-05/etc/markdown/api-extractor-test-05.docclass1.deprecatedexample.md new file mode 100644 index 00000000000..6b73ae72638 --- /dev/null +++ b/build-tests/api-extractor-test-05/etc/markdown/api-extractor-test-05.docclass1.deprecatedexample.md @@ -0,0 +1,18 @@ +[Home](./index) > [api-extractor-test-05](./api-extractor-test-05.md) > [DocClass1](./api-extractor-test-05.docclass1.md) > [deprecatedExample](./api-extractor-test-05.docclass1.deprecatedexample.md) + +## DocClass1.deprecatedExample() method + +> Warning: This API is now obsolete. +> +> Use `otherThing()` instead. +> + +Signature: + +```typescript +deprecatedExample(): void; +``` +Returns: + +`void` + diff --git a/build-tests/api-extractor-test-05/etc/markdown/api-extractor-test-05.docclass1.examplefunction.md b/build-tests/api-extractor-test-05/etc/markdown/api-extractor-test-05.docclass1.examplefunction.md index ff13d1a0aa7..dccacbb375b 100644 --- a/build-tests/api-extractor-test-05/etc/markdown/api-extractor-test-05.docclass1.examplefunction.md +++ b/build-tests/api-extractor-test-05/etc/markdown/api-extractor-test-05.docclass1.examplefunction.md @@ -1,19 +1,23 @@ [Home](./index) > [api-extractor-test-05](./api-extractor-test-05.md) > [DocClass1](./api-extractor-test-05.docclass1.md) > [exampleFunction](./api-extractor-test-05.docclass1.examplefunction.md) -# DocClass1.exampleFunction method +## DocClass1.exampleFunction() method This is an overloaded function. -**Signature:** -```javascript +Signature: + +```typescript exampleFunction(a: string, b: string): string; ``` -**Returns:** `string` ## Parameters -| Parameter | Type | Description | +|

Parameter

|

Type

|

Description

| | --- | --- | --- | -| `a` | `string` | the first string | -| `b` | `string` | the second string | +|

a

|

`string`

|

the first string

| +|

b

|

`string`

|

the second string

| + +Returns: + +`string` diff --git a/build-tests/api-extractor-test-05/etc/markdown/api-extractor-test-05.docclass1.examplefunction_1.md b/build-tests/api-extractor-test-05/etc/markdown/api-extractor-test-05.docclass1.examplefunction_1.md new file mode 100644 index 00000000000..7f7cee861e5 --- /dev/null +++ b/build-tests/api-extractor-test-05/etc/markdown/api-extractor-test-05.docclass1.examplefunction_1.md @@ -0,0 +1,22 @@ +[Home](./index) > [api-extractor-test-05](./api-extractor-test-05.md) > [DocClass1](./api-extractor-test-05.docclass1.md) > [exampleFunction](./api-extractor-test-05.docclass1.examplefunction_1.md) + +## DocClass1.exampleFunction() method + +This is also an overloaded function. + +Signature: + +```typescript +exampleFunction(x: number): number; +``` + +## Parameters + +|

Parameter

|

Type

|

Description

| +| --- | --- | --- | +|

x

|

`number`

|

the number

| + +Returns: + +`number` + diff --git a/build-tests/api-extractor-test-05/etc/markdown/api-extractor-test-05.docclass1.interestingedgecases.md b/build-tests/api-extractor-test-05/etc/markdown/api-extractor-test-05.docclass1.interestingedgecases.md index 8f145adbd35..03925e5f560 100644 --- a/build-tests/api-extractor-test-05/etc/markdown/api-extractor-test-05.docclass1.interestingedgecases.md +++ b/build-tests/api-extractor-test-05/etc/markdown/api-extractor-test-05.docclass1.interestingedgecases.md @@ -1,14 +1,17 @@ [Home](./index) > [api-extractor-test-05](./api-extractor-test-05.md) > [DocClass1](./api-extractor-test-05.docclass1.md) > [interestingEdgeCases](./api-extractor-test-05.docclass1.interestingedgecases.md) -# DocClass1.interestingEdgeCases method +## DocClass1.interestingEdgeCases() method -Example: "{ \\"maxItemsToShow\\": 123 }" +Example: "{ \\"maxItemsToShow\\": 123 }" -The regular expression used to validate the constraints is /^\[a-zA-Z0-9\\-\_\]+$/ +The regular expression used to validate the constraints is /^\[a-zA-Z0-9\\-\_\]+$/ -**Signature:** -```javascript +Signature: + +```typescript interestingEdgeCases(): void; ``` -**Returns:** `void` +Returns: + +`void` diff --git a/build-tests/api-extractor-test-05/etc/markdown/api-extractor-test-05.docclass1.malformedevent.md b/build-tests/api-extractor-test-05/etc/markdown/api-extractor-test-05.docclass1.malformedevent.md index 03cc9edd45b..f9985f50cd3 100644 --- a/build-tests/api-extractor-test-05/etc/markdown/api-extractor-test-05.docclass1.malformedevent.md +++ b/build-tests/api-extractor-test-05/etc/markdown/api-extractor-test-05.docclass1.malformedevent.md @@ -1,10 +1,11 @@ [Home](./index) > [api-extractor-test-05](./api-extractor-test-05.md) > [DocClass1](./api-extractor-test-05.docclass1.md) > [malformedEvent](./api-extractor-test-05.docclass1.malformedevent.md) -# DocClass1.malformedEvent property +## DocClass1.malformedEvent property This event should have been marked as readonly. -**Signature:** -```javascript -malformedEvent: SystemEvent +Signature: + +```typescript +malformedEvent: SystemEvent; ``` diff --git a/build-tests/api-extractor-test-05/etc/markdown/api-extractor-test-05.docclass1.md b/build-tests/api-extractor-test-05/etc/markdown/api-extractor-test-05.docclass1.md index 234d3c34d1a..9cb777b681a 100644 --- a/build-tests/api-extractor-test-05/etc/markdown/api-extractor-test-05.docclass1.md +++ b/build-tests/api-extractor-test-05/etc/markdown/api-extractor-test-05.docclass1.md @@ -1,30 +1,40 @@ [Home](./index) > [api-extractor-test-05](./api-extractor-test-05.md) > [DocClass1](./api-extractor-test-05.docclass1.md) -# DocClass1 class +## DocClass1 class This is an example class. +Signature: + +```typescript +export declare class DocClass1 +``` + ## Events -| Property | Access Modifier | Type | Description | +|

Property

|

Modifiers

|

Type

|

Description

| | --- | --- | --- | --- | -| [`malformedEvent`](./api-extractor-test-05.docclass1.malformedevent.md) | | `SystemEvent` | This event should have been marked as readonly. | -| [`modifiedEvent`](./api-extractor-test-05.docclass1.modifiedevent.md) | | `SystemEvent` | This event is fired whenever the object is modified. | +|

[malformedEvent](./api-extractor-test-05.docclass1.malformedevent.md)

| |

`SystemEvent`

|

This event should have been marked as readonly.

| +|

[modifiedEvent](./api-extractor-test-05.docclass1.modifiedevent.md)

| |

`SystemEvent`

|

This event is fired whenever the object is modified.

| ## Properties -| Property | Access Modifier | Type | Description | +|

Property

|

Modifiers

|

Type

|

Description

| | --- | --- | --- | --- | -| [`regularProperty`](./api-extractor-test-05.docclass1.regularproperty.md) | | `SystemEvent` | This is a regular property that happens to use the SystemEvent type. | +|

[regularProperty](./api-extractor-test-05.docclass1.regularproperty.md)

| |

`SystemEvent`

|

This is a regular property that happens to use the SystemEvent type.

| ## Methods -| Method | Access Modifier | Returns | Description | -| --- | --- | --- | --- | -| [`exampleFunction(a, b)`](./api-extractor-test-05.docclass1.examplefunction.md) | | `string` | This is an overloaded function. | -| [`interestingEdgeCases()`](./api-extractor-test-05.docclass1.interestingedgecases.md) | | `void` | Example: "{ \\"maxItemsToShow\\": 123 }"

The regular expression used to validate the constraints is /^\[a-zA-Z0-9\\-\_\]+$/ | -| [`tableExample()`](./api-extractor-test-05.docclass1.tableexample.md) | | `void` | An example with tables: | +|

Method

|

Modifiers

|

Description

| +| --- | --- | --- | +|

[deprecatedExample()](./api-extractor-test-05.docclass1.deprecatedexample.md)

| | | +|

[exampleFunction(a, b)](./api-extractor-test-05.docclass1.examplefunction.md)

| |

This is an overloaded function.

| +|

[exampleFunction(x)](./api-extractor-test-05.docclass1.examplefunction_1.md)

| |

This is also an overloaded function.

| +|

[interestingEdgeCases()](./api-extractor-test-05.docclass1.interestingedgecases.md)

| |

Example: "{ \\"maxItemsToShow\\": 123 }"

The regular expression used to validate the constraints is /^\[a-zA-Z0-9\\-\_\]+$/

| +|

[sumWithExample(x, y)](./api-extractor-test-05.docclass1.sumwithexample.md)

|

`static`

|

Returns the sum of two numbers.

| +|

[tableExample()](./api-extractor-test-05.docclass1.tableexample.md)

| |

An example with tables:

| ## Remarks These are some remarks. + diff --git a/build-tests/api-extractor-test-05/etc/markdown/api-extractor-test-05.docclass1.modifiedevent.md b/build-tests/api-extractor-test-05/etc/markdown/api-extractor-test-05.docclass1.modifiedevent.md index 4cf3bb0e85e..938597a2b1a 100644 --- a/build-tests/api-extractor-test-05/etc/markdown/api-extractor-test-05.docclass1.modifiedevent.md +++ b/build-tests/api-extractor-test-05/etc/markdown/api-extractor-test-05.docclass1.modifiedevent.md @@ -1,10 +1,11 @@ [Home](./index) > [api-extractor-test-05](./api-extractor-test-05.md) > [DocClass1](./api-extractor-test-05.docclass1.md) > [modifiedEvent](./api-extractor-test-05.docclass1.modifiedevent.md) -# DocClass1.modifiedEvent property +## DocClass1.modifiedEvent property This event is fired whenever the object is modified. -**Signature:** -```javascript -modifiedEvent: SystemEvent +Signature: + +```typescript +readonly modifiedEvent: SystemEvent; ``` diff --git a/build-tests/api-extractor-test-05/etc/markdown/api-extractor-test-05.docclass1.regularproperty.md b/build-tests/api-extractor-test-05/etc/markdown/api-extractor-test-05.docclass1.regularproperty.md index d649acabf20..e0ffe2fc454 100644 --- a/build-tests/api-extractor-test-05/etc/markdown/api-extractor-test-05.docclass1.regularproperty.md +++ b/build-tests/api-extractor-test-05/etc/markdown/api-extractor-test-05.docclass1.regularproperty.md @@ -1,10 +1,11 @@ [Home](./index) > [api-extractor-test-05](./api-extractor-test-05.md) > [DocClass1](./api-extractor-test-05.docclass1.md) > [regularProperty](./api-extractor-test-05.docclass1.regularproperty.md) -# DocClass1.regularProperty property +## DocClass1.regularProperty property This is a regular property that happens to use the SystemEvent type. -**Signature:** -```javascript -regularProperty: SystemEvent +Signature: + +```typescript +regularProperty: SystemEvent; ``` diff --git a/build-tests/api-extractor-test-05/etc/markdown/api-extractor-test-05.docclass1.sumwithexample.md b/build-tests/api-extractor-test-05/etc/markdown/api-extractor-test-05.docclass1.sumwithexample.md new file mode 100644 index 00000000000..b1d1c6d364e --- /dev/null +++ b/build-tests/api-extractor-test-05/etc/markdown/api-extractor-test-05.docclass1.sumwithexample.md @@ -0,0 +1,49 @@ +[Home](./index) > [api-extractor-test-05](./api-extractor-test-05.md) > [DocClass1](./api-extractor-test-05.docclass1.md) > [sumWithExample](./api-extractor-test-05.docclass1.sumwithexample.md) + +## DocClass1.sumWithExample() method + +Returns the sum of two numbers. + +Signature: + +```typescript +static sumWithExample(x: number, y: number): number; +``` + +## Parameters + +|

Parameter

|

Type

|

Description

| +| --- | --- | --- | +|

x

|

`number`

|

the first number to add

| +|

y

|

`number`

|

the second number to add

| + +Returns: + +`number` + +the sum of the two numbers + +## Remarks + +This illustrates usage of the `@example` block tag. + +## Example 1 + +Here's a simple example: + +``` +// Prints "2": +console.log(DocClass1.sumWithExample(1,1)); + +``` + +## Example 2 + +Here's an example with negative numbers: + +``` +// Prints "0": +console.log(DocClass1.sumWithExample(1,-1)); + +``` + diff --git a/build-tests/api-extractor-test-05/etc/markdown/api-extractor-test-05.docclass1.tableexample.md b/build-tests/api-extractor-test-05/etc/markdown/api-extractor-test-05.docclass1.tableexample.md index b2f24e4ecf8..c9307181370 100644 --- a/build-tests/api-extractor-test-05/etc/markdown/api-extractor-test-05.docclass1.tableexample.md +++ b/build-tests/api-extractor-test-05/etc/markdown/api-extractor-test-05.docclass1.tableexample.md @@ -1,15 +1,19 @@ [Home](./index) > [api-extractor-test-05](./api-extractor-test-05.md) > [DocClass1](./api-extractor-test-05.docclass1.md) > [tableExample](./api-extractor-test-05.docclass1.tableexample.md) -# DocClass1.tableExample method +## DocClass1.tableExample() method An example with tables: -**Signature:** -```javascript +Signature: + +```typescript tableExample(): void; ``` -**Returns:** `void` +Returns: + +`void` ## Remarks
John Doe
+ diff --git a/build-tests/api-extractor-test-05/etc/markdown/api-extractor-test-05.docenum.md b/build-tests/api-extractor-test-05/etc/markdown/api-extractor-test-05.docenum.md new file mode 100644 index 00000000000..713fc153c5a --- /dev/null +++ b/build-tests/api-extractor-test-05/etc/markdown/api-extractor-test-05.docenum.md @@ -0,0 +1,20 @@ +[Home](./index) > [api-extractor-test-05](./api-extractor-test-05.md) > [DocEnum](./api-extractor-test-05.docenum.md) + +## DocEnum enum + +Docs for DocEnum + +Signature: + +```typescript +export declare enum DocEnum +``` + +## Enumeration Members + +|

Member

|

Value

|

Description

| +| --- | --- | --- | +|

One

|

`1`

|

These are some docs for One

| +|

Two

|

`2`

|

These are some docs for Two

| +|

Zero

|

`0`

|

These are some docs for Zero

| + diff --git a/build-tests/api-extractor-test-05/etc/markdown/api-extractor-test-05.md b/build-tests/api-extractor-test-05/etc/markdown/api-extractor-test-05.md index 3328fe74ac7..cbd13a0622f 100644 --- a/build-tests/api-extractor-test-05/etc/markdown/api-extractor-test-05.md +++ b/build-tests/api-extractor-test-05/etc/markdown/api-extractor-test-05.md @@ -1,6 +1,6 @@ [Home](./index) > [api-extractor-test-05](./api-extractor-test-05.md) -# api-extractor-test-05 package +## api-extractor-test-05 package api-extractor-test-05 @@ -8,8 +8,14 @@ This project tests various documentation generation scenarios and doc comment sy ## Classes -| Class | Description | +|

Class

|

Description

| | --- | --- | -| [`DocClass1`](./api-extractor-test-05.docclass1.md) | This is an example class. | -| [`SystemEvent`](./api-extractor-test-05.systemevent.md) | A class used to exposed events. | +|

[DocClass1](./api-extractor-test-05.docclass1.md)

|

This is an example class.

| +|

[SystemEvent](./api-extractor-test-05.systemevent.md)

|

A class used to exposed events.

| + +## Enumerations + +|

Enumeration

|

Description

| +| --- | --- | +|

[DocEnum](./api-extractor-test-05.docenum.md)

|

Docs for DocEnum

| diff --git a/build-tests/api-extractor-test-05/etc/markdown/api-extractor-test-05.systemevent.addhandler.md b/build-tests/api-extractor-test-05/etc/markdown/api-extractor-test-05.systemevent.addhandler.md index f1733c6a7ca..b1c09b8cfc3 100644 --- a/build-tests/api-extractor-test-05/etc/markdown/api-extractor-test-05.systemevent.addhandler.md +++ b/build-tests/api-extractor-test-05/etc/markdown/api-extractor-test-05.systemevent.addhandler.md @@ -1,18 +1,22 @@ [Home](./index) > [api-extractor-test-05](./api-extractor-test-05.md) > [SystemEvent](./api-extractor-test-05.systemevent.md) > [addHandler](./api-extractor-test-05.systemevent.addhandler.md) -# SystemEvent.addHandler method +## SystemEvent.addHandler() method Adds an handler for the event. -**Signature:** -```javascript +Signature: + +```typescript addHandler(handler: () => void): void; ``` -**Returns:** `void` ## Parameters -| Parameter | Type | Description | +|

Parameter

|

Type

|

Description

| | --- | --- | --- | -| `handler` | `() => void` | | +|

handler

|

`() => void`

| | + +Returns: + +`void` diff --git a/build-tests/api-extractor-test-05/etc/markdown/api-extractor-test-05.systemevent.md b/build-tests/api-extractor-test-05/etc/markdown/api-extractor-test-05.systemevent.md index 3a9869ed973..f85f9b78017 100644 --- a/build-tests/api-extractor-test-05/etc/markdown/api-extractor-test-05.systemevent.md +++ b/build-tests/api-extractor-test-05/etc/markdown/api-extractor-test-05.systemevent.md @@ -1,12 +1,18 @@ [Home](./index) > [api-extractor-test-05](./api-extractor-test-05.md) > [SystemEvent](./api-extractor-test-05.systemevent.md) -# SystemEvent class +## SystemEvent class A class used to exposed events. +Signature: + +```typescript +export declare class SystemEvent +``` + ## Methods -| Method | Access Modifier | Returns | Description | -| --- | --- | --- | --- | -| [`addHandler(handler)`](./api-extractor-test-05.systemevent.addhandler.md) | | `void` | Adds an handler for the event. | +|

Method

|

Modifiers

|

Description

| +| --- | --- | --- | +|

[addHandler(handler)](./api-extractor-test-05.systemevent.addhandler.md)

| |

Adds an handler for the event.

| diff --git a/build-tests/api-extractor-test-05/etc/yaml/api-extractor-test-05.yml b/build-tests/api-extractor-test-05/etc/yaml/api-extractor-test-05.yml index 65f2b5d9d16..a65f4e27d6f 100644 --- a/build-tests/api-extractor-test-05/etc/yaml/api-extractor-test-05.yml +++ b/build-tests/api-extractor-test-05/etc/yaml/api-extractor-test-05.yml @@ -12,9 +12,12 @@ items: type: package children: - api-extractor-test-05.DocClass1 + - api-extractor-test-05.DocEnum - api-extractor-test-05.SystemEvent references: - uid: api-extractor-test-05.DocClass1 name: DocClass1 + - uid: api-extractor-test-05.DocEnum + name: DocEnum - uid: api-extractor-test-05.SystemEvent name: SystemEvent diff --git a/build-tests/api-extractor-test-05/etc/yaml/api-extractor-test-05/docclass1.yml b/build-tests/api-extractor-test-05/etc/yaml/api-extractor-test-05/docclass1.yml index 065d980b557..d346c93ff52 100644 --- a/build-tests/api-extractor-test-05/etc/yaml/api-extractor-test-05/docclass1.yml +++ b/build-tests/api-extractor-test-05/etc/yaml/api-extractor-test-05/docclass1.yml @@ -10,16 +10,33 @@ items: type: class package: api-extractor-test-05 children: + - api-extractor-test-05.DocClass1.deprecatedExample - api-extractor-test-05.DocClass1.exampleFunction + - api-extractor-test-05.DocClass1.exampleFunction_1 - api-extractor-test-05.DocClass1.interestingEdgeCases - api-extractor-test-05.DocClass1.malformedEvent - api-extractor-test-05.DocClass1.modifiedEvent - api-extractor-test-05.DocClass1.regularProperty + - api-extractor-test-05.DocClass1.sumWithExample - api-extractor-test-05.DocClass1.tableExample + - uid: api-extractor-test-05.DocClass1.deprecatedExample + deprecated: + content: Use `otherThing()` instead. + name: deprecatedExample() + fullName: deprecatedExample() + langs: + - typeScript + type: method + syntax: + content: 'deprecatedExample(): void;' + return: + type: + - void + description: '' - uid: api-extractor-test-05.DocClass1.exampleFunction summary: This is an overloaded function. name: 'exampleFunction(a, b)' - fullName: exampleFunction + fullName: 'exampleFunction(a, b)' langs: - typeScript type: method @@ -38,13 +55,31 @@ items: description: the second string type: - string + - uid: api-extractor-test-05.DocClass1.exampleFunction_1 + summary: This is also an overloaded function. + name: exampleFunction(x) + fullName: exampleFunction(x) + langs: + - typeScript + type: method + syntax: + content: 'exampleFunction(x: number): number;' + return: + type: + - number + description: '' + parameters: + - id: x + description: the number + type: + - number - uid: api-extractor-test-05.DocClass1.interestingEdgeCases summary: |- - Example: "{ \\"maxItemsToShow\\": 123 }" + Example: "{ \\"maxItemsToShow\\": 123 }" - The regular expression used to validate the constraints is /^\[a-zA-Z0-9\\-\_\]+$/ + The regular expression used to validate the constraints is /^\[a-zA-Z0-9\\-\_\]+$/ name: interestingEdgeCases() - fullName: interestingEdgeCases + fullName: interestingEdgeCases() langs: - typeScript type: method @@ -90,11 +125,34 @@ items: return: type: - api-extractor-test-05.SystemEvent + - uid: api-extractor-test-05.DocClass1.sumWithExample + summary: Returns the sum of two numbers. + remarks: This illustrates usage of the `@example` block tag. + name: 'sumWithExample(x, y)' + fullName: 'sumWithExample(x, y)' + langs: + - typeScript + type: method + syntax: + content: 'static sumWithExample(x: number, y: number): number;' + return: + type: + - number + description: the sum of the two numbers + parameters: + - id: x + description: the first number to add + type: + - number + - id: 'y' + description: the second number to add + type: + - number - uid: api-extractor-test-05.DocClass1.tableExample summary: 'An example with tables:' remarks:
John Doe
name: tableExample() - fullName: tableExample + fullName: tableExample() langs: - typeScript type: method diff --git a/build-tests/api-extractor-test-05/etc/yaml/api-extractor-test-05/docenum.yml b/build-tests/api-extractor-test-05/etc/yaml/api-extractor-test-05/docenum.yml new file mode 100644 index 00000000000..e84fa19420f --- /dev/null +++ b/build-tests/api-extractor-test-05/etc/yaml/api-extractor-test-05/docenum.yml @@ -0,0 +1,38 @@ +### YamlMime:UniversalReference +items: + - uid: api-extractor-test-05.DocEnum + summary: Docs for DocEnum + name: DocEnum + fullName: DocEnum + langs: + - typeScript + type: enum + package: api-extractor-test-05 + children: + - api-extractor-test-05.DocEnum.One + - api-extractor-test-05.DocEnum.Two + - api-extractor-test-05.DocEnum.Zero + - uid: api-extractor-test-05.DocEnum.One + summary: These are some docs for One + name: One + fullName: One + langs: + - typeScript + type: field + numericValue: '1' + - uid: api-extractor-test-05.DocEnum.Two + summary: These are some docs for Two + name: Two + fullName: Two + langs: + - typeScript + type: field + numericValue: '2' + - uid: api-extractor-test-05.DocEnum.Zero + summary: These are some docs for Zero + name: Zero + fullName: Zero + langs: + - typeScript + type: field + numericValue: '0' diff --git a/build-tests/api-extractor-test-05/etc/yaml/api-extractor-test-05/systemevent.yml b/build-tests/api-extractor-test-05/etc/yaml/api-extractor-test-05/systemevent.yml index 03cd6c359bf..f9ee0cf93cb 100644 --- a/build-tests/api-extractor-test-05/etc/yaml/api-extractor-test-05/systemevent.yml +++ b/build-tests/api-extractor-test-05/etc/yaml/api-extractor-test-05/systemevent.yml @@ -13,7 +13,7 @@ items: - uid: api-extractor-test-05.SystemEvent.addHandler summary: Adds an handler for the event. name: addHandler(handler) - fullName: addHandler + fullName: addHandler(handler) langs: - typeScript type: method diff --git a/build-tests/api-extractor-test-05/etc/yaml/toc.yml b/build-tests/api-extractor-test-05/etc/yaml/toc.yml index 2ed19fa2861..0c9d7a7a2a2 100644 --- a/build-tests/api-extractor-test-05/etc/yaml/toc.yml +++ b/build-tests/api-extractor-test-05/etc/yaml/toc.yml @@ -7,5 +7,7 @@ items: items: - name: DocClass1 uid: api-extractor-test-05.DocClass1 + - name: DocEnum + uid: api-extractor-test-05.DocEnum - name: SystemEvent uid: api-extractor-test-05.SystemEvent diff --git a/build-tests/api-extractor-test-05/package.json b/build-tests/api-extractor-test-05/package.json index 39b63dd28c2..c6af13c5d99 100644 --- a/build-tests/api-extractor-test-05/package.json +++ b/build-tests/api-extractor-test-05/package.json @@ -13,7 +13,7 @@ }, "dependencies": { "@microsoft/api-extractor": "6.3.0", - "@microsoft/api-documenter": "1.5.59", + "@microsoft/api-documenter": "6.0.0", "@types/jest": "21.1.10", "@types/node": "8.5.8", "fs-extra": "~7.0.1", diff --git a/build-tests/api-extractor-test-05/src/DocClass1.ts b/build-tests/api-extractor-test-05/src/DocClass1.ts index eecf1128065..d07f6a5da85 100644 --- a/build-tests/api-extractor-test-05/src/DocClass1.ts +++ b/build-tests/api-extractor-test-05/src/DocClass1.ts @@ -23,12 +23,17 @@ export class DocClass1 { /** * This is an overloaded function. - * @param x - the number * @param a - the first string * @param b - the second string */ exampleFunction(a: string, b: string): string; + + /** + * This is also an overloaded function. + * @param x - the number + */ exampleFunction(x: number): number; + public exampleFunction(x: number | string, y?: string): string | number { return x; } @@ -70,4 +75,37 @@ export class DocClass1 { */ interestingEdgeCases(): void { } + + /** + * @deprecated Use `otherThing()` instead. + */ + public deprecatedExample(): void { + } + + /** + * Returns the sum of two numbers. + * + * @remarks + * This illustrates usage of the `@example` block tag. + * + * @param x - the first number to add + * @param y - the second number to add + * @returns the sum of the two numbers + * + * @example + * Here's a simple example: + * ``` + * // Prints "2": + * console.log(DocClass1.sumWithExample(1,1)); + * ``` + * @example + * Here's an example with negative numbers: + * ``` + * // Prints "0": + * console.log(DocClass1.sumWithExample(1,-1)); + * ``` + */ + public static sumWithExample(x: number, y: number): number { + return x + y; + } } diff --git a/build-tests/api-extractor-test-05/src/DocEnums.ts b/build-tests/api-extractor-test-05/src/DocEnums.ts new file mode 100644 index 00000000000..a60df6d230d --- /dev/null +++ b/build-tests/api-extractor-test-05/src/DocEnums.ts @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +/** + * Docs for DocEnum + * @public + */ +export enum DocEnum { + /** + * These are some docs for Zero + */ + Zero, + + /** + * These are some docs for One + */ + One = 1, + + /** + * These are some docs for Two + */ + Two = DocEnum.One + 1 +} diff --git a/build-tests/api-extractor-test-05/src/index.ts b/build-tests/api-extractor-test-05/src/index.ts index c9f7bb26205..6389315e9ac 100644 --- a/build-tests/api-extractor-test-05/src/index.ts +++ b/build-tests/api-extractor-test-05/src/index.ts @@ -10,4 +10,5 @@ * @packagedocumentation */ - export * from './DocClass1'; +export * from './DocClass1'; +export * from './DocEnums'; diff --git a/build-tests/web-library-build-test/package.json b/build-tests/web-library-build-test/package.json index 3c607436c86..799ca859d4c 100644 --- a/build-tests/web-library-build-test/package.json +++ b/build-tests/web-library-build-test/package.json @@ -4,6 +4,9 @@ "description": "", "main": "lib/test.js", "module": "lib-es6/test.js", + "tsdoc": { + "tsdocFlavor": "AEDoc" + }, "private": true, "scripts": { "build": "gulp --clean" diff --git a/common/changes/@microsoft/api-documenter/pgonzal-ae-overhaul_2018-11-29-06-18.json b/common/changes/@microsoft/api-documenter/pgonzal-ae-overhaul_2018-11-29-06-18.json new file mode 100644 index 00000000000..0b65e255df3 --- /dev/null +++ b/common/changes/@microsoft/api-documenter/pgonzal-ae-overhaul_2018-11-29-06-18.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "packageName": "@microsoft/api-documenter", + "comment": "THIS IS A BETA RELEASE - We are bumping the version to \"7.0.0\" to simplify dogfooding. This release is not yet ready for general usage.", + "type": "major" + } + ], + "packageName": "@microsoft/api-documenter", + "email": "pgonzal@users.noreply.github.com" +} \ No newline at end of file diff --git a/common/changes/@microsoft/api-extractor/pgonzal-ae-overhaul_2018-11-29-06-18.json b/common/changes/@microsoft/api-extractor/pgonzal-ae-overhaul_2018-11-29-06-18.json new file mode 100644 index 00000000000..8fc1b368679 --- /dev/null +++ b/common/changes/@microsoft/api-extractor/pgonzal-ae-overhaul_2018-11-29-06-18.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "packageName": "@microsoft/api-extractor", + "comment": "THIS IS A BETA RELEASE - We are bumping the version to \"7.0.0\" to simplify dogfooding. This release is not yet ready for general usage.", + "type": "major" + } + ], + "packageName": "@microsoft/api-extractor", + "email": "pgonzal@users.noreply.github.com" +} \ No newline at end of file diff --git a/common/changes/@microsoft/gulp-core-build-karma/pgonzal-ae-overhaul_2018-11-29-06-18.json b/common/changes/@microsoft/gulp-core-build-karma/pgonzal-ae-overhaul_2018-11-29-06-18.json new file mode 100644 index 00000000000..27769289f3e --- /dev/null +++ b/common/changes/@microsoft/gulp-core-build-karma/pgonzal-ae-overhaul_2018-11-29-06-18.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "comment": "", + "packageName": "@microsoft/gulp-core-build-karma", + "type": "none" + } + ], + "packageName": "@microsoft/gulp-core-build-karma", + "email": "pgonzal@users.noreply.github.com" +} \ No newline at end of file diff --git a/common/changes/@microsoft/gulp-core-build-mocha/pgonzal-ae-overhaul_2018-11-29-06-18.json b/common/changes/@microsoft/gulp-core-build-mocha/pgonzal-ae-overhaul_2018-11-29-06-18.json new file mode 100644 index 00000000000..bde4ca8c391 --- /dev/null +++ b/common/changes/@microsoft/gulp-core-build-mocha/pgonzal-ae-overhaul_2018-11-29-06-18.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "comment": "", + "packageName": "@microsoft/gulp-core-build-mocha", + "type": "none" + } + ], + "packageName": "@microsoft/gulp-core-build-mocha", + "email": "pgonzal@users.noreply.github.com" +} \ No newline at end of file diff --git a/common/changes/@microsoft/gulp-core-build-sass/pgonzal-ae-overhaul_2018-11-29-06-18.json b/common/changes/@microsoft/gulp-core-build-sass/pgonzal-ae-overhaul_2018-11-29-06-18.json new file mode 100644 index 00000000000..a6bdb5dd3cf --- /dev/null +++ b/common/changes/@microsoft/gulp-core-build-sass/pgonzal-ae-overhaul_2018-11-29-06-18.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "comment": "", + "packageName": "@microsoft/gulp-core-build-sass", + "type": "none" + } + ], + "packageName": "@microsoft/gulp-core-build-sass", + "email": "pgonzal@users.noreply.github.com" +} \ No newline at end of file diff --git a/common/changes/@microsoft/gulp-core-build-serve/pgonzal-ae-overhaul_2018-11-29-06-18.json b/common/changes/@microsoft/gulp-core-build-serve/pgonzal-ae-overhaul_2018-11-29-06-18.json new file mode 100644 index 00000000000..376e7b85a81 --- /dev/null +++ b/common/changes/@microsoft/gulp-core-build-serve/pgonzal-ae-overhaul_2018-11-29-06-18.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "comment": "", + "packageName": "@microsoft/gulp-core-build-serve", + "type": "none" + } + ], + "packageName": "@microsoft/gulp-core-build-serve", + "email": "pgonzal@users.noreply.github.com" +} \ No newline at end of file diff --git a/common/changes/@microsoft/gulp-core-build-typescript/pgonzal-ae-overhaul_2018-11-29-06-18.json b/common/changes/@microsoft/gulp-core-build-typescript/pgonzal-ae-overhaul_2018-11-29-06-18.json new file mode 100644 index 00000000000..d3cb9ff5661 --- /dev/null +++ b/common/changes/@microsoft/gulp-core-build-typescript/pgonzal-ae-overhaul_2018-11-29-06-18.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "comment": "", + "packageName": "@microsoft/gulp-core-build-typescript", + "type": "none" + } + ], + "packageName": "@microsoft/gulp-core-build-typescript", + "email": "pgonzal@users.noreply.github.com" +} \ No newline at end of file diff --git a/common/changes/@microsoft/gulp-core-build-webpack/pgonzal-ae-overhaul_2018-11-29-06-18.json b/common/changes/@microsoft/gulp-core-build-webpack/pgonzal-ae-overhaul_2018-11-29-06-18.json new file mode 100644 index 00000000000..53ddbe28ce7 --- /dev/null +++ b/common/changes/@microsoft/gulp-core-build-webpack/pgonzal-ae-overhaul_2018-11-29-06-18.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "comment": "", + "packageName": "@microsoft/gulp-core-build-webpack", + "type": "none" + } + ], + "packageName": "@microsoft/gulp-core-build-webpack", + "email": "pgonzal@users.noreply.github.com" +} \ No newline at end of file diff --git a/common/changes/@microsoft/gulp-core-build/pgonzal-ae-overhaul_2018-11-29-06-18.json b/common/changes/@microsoft/gulp-core-build/pgonzal-ae-overhaul_2018-11-29-06-18.json new file mode 100644 index 00000000000..62ca2d23568 --- /dev/null +++ b/common/changes/@microsoft/gulp-core-build/pgonzal-ae-overhaul_2018-11-29-06-18.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "comment": "", + "packageName": "@microsoft/gulp-core-build", + "type": "none" + } + ], + "packageName": "@microsoft/gulp-core-build", + "email": "pgonzal@users.noreply.github.com" +} \ No newline at end of file diff --git a/common/changes/@microsoft/loader-load-themed-styles/pgonzal-ae-overhaul_2018-11-29-06-18.json b/common/changes/@microsoft/loader-load-themed-styles/pgonzal-ae-overhaul_2018-11-29-06-18.json new file mode 100644 index 00000000000..a9104559b3d --- /dev/null +++ b/common/changes/@microsoft/loader-load-themed-styles/pgonzal-ae-overhaul_2018-11-29-06-18.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "comment": "", + "packageName": "@microsoft/loader-load-themed-styles", + "type": "none" + } + ], + "packageName": "@microsoft/loader-load-themed-styles", + "email": "pgonzal@users.noreply.github.com" +} \ No newline at end of file diff --git a/common/changes/@microsoft/node-core-library/pgonzal-ae-overhaul_2018-11-29-06-18.json b/common/changes/@microsoft/node-core-library/pgonzal-ae-overhaul_2018-11-29-06-18.json new file mode 100644 index 00000000000..30c131fc087 --- /dev/null +++ b/common/changes/@microsoft/node-core-library/pgonzal-ae-overhaul_2018-11-29-06-18.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "packageName": "@microsoft/node-core-library", + "comment": "Improve Sort.compareByValue() to consistently order \"null\" and \"undefined\" values", + "type": "patch" + } + ], + "packageName": "@microsoft/node-core-library", + "email": "pgonzal@users.noreply.github.com" +} \ No newline at end of file diff --git a/common/changes/@microsoft/node-library-build/pgonzal-ae-overhaul_2018-11-29-06-18.json b/common/changes/@microsoft/node-library-build/pgonzal-ae-overhaul_2018-11-29-06-18.json new file mode 100644 index 00000000000..000ea2a9a60 --- /dev/null +++ b/common/changes/@microsoft/node-library-build/pgonzal-ae-overhaul_2018-11-29-06-18.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "comment": "", + "packageName": "@microsoft/node-library-build", + "type": "none" + } + ], + "packageName": "@microsoft/node-library-build", + "email": "pgonzal@users.noreply.github.com" +} \ No newline at end of file diff --git a/common/changes/@microsoft/resolve-chunk-plugin/pgonzal-ae-overhaul_2018-11-29-06-18.json b/common/changes/@microsoft/resolve-chunk-plugin/pgonzal-ae-overhaul_2018-11-29-06-18.json new file mode 100644 index 00000000000..921f2cdd477 --- /dev/null +++ b/common/changes/@microsoft/resolve-chunk-plugin/pgonzal-ae-overhaul_2018-11-29-06-18.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "comment": "", + "packageName": "@microsoft/resolve-chunk-plugin", + "type": "none" + } + ], + "packageName": "@microsoft/resolve-chunk-plugin", + "email": "pgonzal@users.noreply.github.com" +} \ No newline at end of file diff --git a/common/changes/@microsoft/set-webpack-public-path-plugin/pgonzal-ae-overhaul_2018-11-29-06-18.json b/common/changes/@microsoft/set-webpack-public-path-plugin/pgonzal-ae-overhaul_2018-11-29-06-18.json new file mode 100644 index 00000000000..558cc38ce9a --- /dev/null +++ b/common/changes/@microsoft/set-webpack-public-path-plugin/pgonzal-ae-overhaul_2018-11-29-06-18.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "comment": "", + "packageName": "@microsoft/set-webpack-public-path-plugin", + "type": "none" + } + ], + "packageName": "@microsoft/set-webpack-public-path-plugin", + "email": "pgonzal@users.noreply.github.com" +} \ No newline at end of file diff --git a/common/changes/@microsoft/web-library-build/pgonzal-ae-overhaul_2018-11-29-06-18.json b/common/changes/@microsoft/web-library-build/pgonzal-ae-overhaul_2018-11-29-06-18.json new file mode 100644 index 00000000000..6eb24bb279f --- /dev/null +++ b/common/changes/@microsoft/web-library-build/pgonzal-ae-overhaul_2018-11-29-06-18.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "comment": "", + "packageName": "@microsoft/web-library-build", + "type": "none" + } + ], + "packageName": "@microsoft/web-library-build", + "email": "pgonzal@users.noreply.github.com" +} \ No newline at end of file diff --git a/common/config/rush/shrinkwrap.yaml b/common/config/rush/shrinkwrap.yaml index ca7ca092b7c..5554dfa8011 100644 --- a/common/config/rush/shrinkwrap.yaml +++ b/common/config/rush/shrinkwrap.yaml @@ -1,7 +1,8 @@ dependencies: + '@microsoft/api-extractor': 6.3.0 '@microsoft/node-library-build': 6.0.15 '@microsoft/rush-stack-compiler-3.0': 0.1.0 - '@microsoft/tsdoc': 0.12.2 + '@microsoft/tsdoc': 0.12.4 '@pnpm/link-bins': 1.0.3 '@pnpm/logger': 1.0.2 '@rush-temp/api-documenter': 'file:projects/api-documenter.tgz' @@ -188,38 +189,6 @@ packages: dev: false resolution: integrity: sha512-UFMC4ZeFC48Tpvj7C8UgLvtkaUuovQX+5xNWrsIoMG8o2z+XFKjKaN9iVmS84dPwVN00W4wPmqvYoZF3EGAsfw== - /@microsoft/api-extractor/6.1.1: - dependencies: - '@microsoft/node-core-library': 3.5.0 - '@microsoft/ts-command-line': 4.2.2 - '@microsoft/tsdoc': 0.9.3 - '@types/node': 8.5.8 - '@types/z-schema': 3.16.31 - colors: 1.2.5 - jju: 1.3.0 - lodash: 4.17.11 - typescript: 3.0.3 - z-schema: 3.18.4 - dev: false - hasBin: true - resolution: - integrity: sha512-bEGyeFZ34+fHEEnL7Eewcg7LZ6Ov00BL57bIn6ulU6vf9ulPCJvUBpeR85U/kA7Snd49Atjem5FubREG0fzwwA== - /@microsoft/api-extractor/6.1.6: - dependencies: - '@microsoft/node-core-library': 3.6.0 - '@microsoft/ts-command-line': 4.2.2 - '@microsoft/tsdoc': 0.12.2 - '@types/node': 8.5.8 - '@types/z-schema': 3.16.31 - colors: 1.2.5 - jju: 1.3.0 - lodash: 4.17.11 - typescript: 3.1.6 - z-schema: 3.18.4 - dev: false - hasBin: true - resolution: - integrity: sha512-Iik7A/PA3bhGBtlsxFWlEVIwBNuEshpzPZKAAfdQL2LrsUdPI3Ehjad0SJA1Sri9Hg241npVOx5L5g9aPIzBoQ== /@microsoft/api-extractor/6.3.0: dependencies: '@microsoft/node-core-library': 3.7.0 @@ -237,17 +206,6 @@ packages: hasBin: true resolution: integrity: sha512-rhC3Wc/Zaj44W8hJYJlaf+gFGxVwLvSEOnPslYUi3anFbtXBNZpK3ocF10SgtSaca2QfKA8UNN1rCIwXUCdz7g== - /@microsoft/gulp-core-build-mocha/3.5.33: - dependencies: - '@microsoft/gulp-core-build': 3.8.41 - '@types/node': 8.5.8 - glob: 7.0.6 - gulp: 3.9.1 - gulp-istanbul: 0.10.4 - gulp-mocha: 6.0.0 - dev: false - resolution: - integrity: sha512-pdThZpwexc7KcmUDgr7V4Cg2AE3XmAhFW6kaox/UPKWEb4yqGjM4s4hr3h6vR+6ojjdFra0sR9XU8nz2lgHtYw== /@microsoft/gulp-core-build-mocha/3.5.42: dependencies: '@microsoft/gulp-core-build': 3.8.50 @@ -259,18 +217,6 @@ packages: dev: false resolution: integrity: sha512-GPl1fnIElCtI3C/QUZelEZM5eCa84/IbSppa7VzR4bG16kWtei6safJS81b+tTox3+FEidOX+QopQHsj0Yh8LQ== - /@microsoft/gulp-core-build-typescript/7.1.2: - dependencies: - '@microsoft/gulp-core-build': 3.8.41 - '@microsoft/node-core-library': 3.5.0 - '@types/node': 8.5.8 - decomment: 0.9.2 - glob: 7.0.6 - glob-escape: 0.0.2 - resolve: 1.8.1 - dev: false - resolution: - integrity: sha512-RgD4WLJdkMqpM3iHH96+QCS2ukg/nYiNthOv3eg6w4DNT9vHhUjf/7h7soByFM377xR3OUBFNiB4+7i3ZGH7Kg== /@microsoft/gulp-core-build-typescript/7.3.0: dependencies: '@microsoft/gulp-core-build': 3.8.50 @@ -284,52 +230,6 @@ packages: dev: false resolution: integrity: sha512-N7k94Ht4PBqP7Pr5n2Bh2UXOJckk2WnWrfXWj6T21Vciajf5Cq4Czqn4CGqZIaRuGnkwa+I4YuOsKFRHI+L/uQ== - /@microsoft/gulp-core-build/3.8.41: - dependencies: - '@microsoft/node-core-library': 3.5.0 - '@types/assertion-error': 1.0.30 - '@types/chai': 3.4.34 - '@types/chalk': 0.4.31 - '@types/gulp': 3.8.32 - '@types/mocha': 5.2.5 - '@types/node': 8.5.8 - '@types/node-notifier': 0.0.28 - '@types/orchestrator': 0.0.30 - '@types/q': 0.0.32 - '@types/rimraf': 0.0.28 - '@types/semver': 5.3.33 - '@types/through2': 2.0.32 - '@types/vinyl': 1.2.30 - '@types/yargs': 0.0.34 - colors: 1.2.5 - del: 2.2.2 - end-of-stream: 1.1.0 - glob-escape: 0.0.2 - globby: 5.0.0 - gulp: 3.9.1 - gulp-flatten: 0.2.0 - gulp-if: 2.0.2 - jest: 22.4.4 - jest-cli: 22.4.4 - jest-environment-jsdom: 22.4.3 - jest-resolve: 22.4.3 - jju: 1.3.0 - jsdom: 11.11.0 - lodash.merge: 4.3.5 - merge2: 1.0.3 - node-notifier: 5.0.2 - object-assign: 4.1.1 - orchestrator: 0.3.8 - pretty-hrtime: 1.0.3 - rimraf: 2.5.4 - semver: 5.3.0 - through2: 2.0.5 - vinyl: 2.2.0 - yargs: 4.6.0 - z-schema: 3.18.4 - dev: false - resolution: - integrity: sha512-2PZ0aUc2mjxBIueHGYLgaVzi4bX87MXM8AKN07LQT3JNKZ2z1atnmEx3yM2sFk2QJ2Ia0RCkkzY8M25fFQiDfw== /@microsoft/gulp-core-build/3.8.50: dependencies: '@microsoft/node-core-library': 3.7.0 @@ -374,30 +274,6 @@ packages: dev: false resolution: integrity: sha512-FGJkKBraDvZWLgSLCvjYSMPQAE4F3aFFr1WqndkXxNJ/Z+H4o60gdvu+C46v+JFNQg3KROLxJ/cBUFvnJWt9mg== - /@microsoft/node-core-library/3.5.0: - dependencies: - '@types/fs-extra': 5.0.1 - '@types/node': 8.5.8 - '@types/z-schema': 3.16.31 - colors: 1.2.5 - fs-extra: 5.0.0 - jju: 1.3.0 - z-schema: 3.18.4 - dev: false - resolution: - integrity: sha512-DZ16Aa64BMwsfw8yvHUDVhAX2YWqIpPpWXC0BQnDawG1BgkMCox7aBxC2JofnMnMOSzaeXmuUSuy32s4D64ZeA== - /@microsoft/node-core-library/3.6.0: - dependencies: - '@types/fs-extra': 5.0.4 - '@types/node': 8.5.8 - '@types/z-schema': 3.16.31 - colors: 1.2.5 - fs-extra: 7.0.1 - jju: 1.3.0 - z-schema: 3.18.4 - dev: false - resolution: - integrity: sha512-dwxBTdr3i3JcntPiklv6//Z/UVZO9pO1TcvKYy+hNZ/bpK32hyT0LaCCS27jyWHPnxHIRA130Y9CptfqrDzrCg== /@microsoft/node-core-library/3.7.0: dependencies: '@types/fs-extra': 5.0.4 @@ -421,17 +297,6 @@ packages: dev: false resolution: integrity: sha512-7+k75oAMxMqLWV+9gkJsCTMCuFc09rDl3rNFYdB7tjhjPFyZGq6a4W2N4UZfDePg56h0tIPd/cxTQ/dV1cJTtQ== - /@microsoft/node-library-build/6.0.6: - dependencies: - '@microsoft/gulp-core-build': 3.8.41 - '@microsoft/gulp-core-build-mocha': 3.5.33 - '@microsoft/gulp-core-build-typescript': 7.1.2 - '@types/gulp': 3.8.32 - '@types/node': 8.5.8 - gulp: 3.9.1 - dev: false - resolution: - integrity: sha512-QrKj8Ra0HYiVnO7/KW5WrFtc3I/7ZP3WIbnk0wBClHqm9zZPD+Wz1QxLhmhBfzLay2VNlTonG9IbJ06M5uA4Gg== /@microsoft/rush-stack-compiler-2.7/0.1.0: dependencies: '@microsoft/api-extractor': 6.3.0 @@ -444,18 +309,6 @@ packages: hasBin: true resolution: integrity: sha512-hDqO7sh5a9VGXEs0HMeyWGKe5DsqC7SFWfJoPb0sanc4GoGpZmqYevna/Tbqta/QtnHLE8f79pB4xzabnm/ACA== - /@microsoft/rush-stack-compiler-2.9/0.1.0: - dependencies: - '@microsoft/api-extractor': 6.1.6 - '@microsoft/node-core-library': 3.6.0 - '@types/node': 8.5.8 - tslint: 5.11.0 - tslint-microsoft-contrib: /tslint-microsoft-contrib/5.2.1/tslint@5.11.0 - typescript: 2.9.2 - dev: false - hasBin: true - resolution: - integrity: sha512-SGxsBukkmESAWkPCo+tjZL8B1LC2AyyX6Bk3DisD6xcTTJ7anuXPDXcqAzDtazWxK41KDNejMH7YYUSmMBDF+w== /@microsoft/rush-stack-compiler-3.0/0.1.0: dependencies: '@microsoft/api-extractor': 6.3.0 @@ -468,42 +321,6 @@ packages: hasBin: true resolution: integrity: sha512-SD45muki+Ss3InSj+ifXd210QO76nfx3ausaJwRFsZXNguh2lcJ3R2A8v5qp7cKECmZCnCj2yk6fhh0yfzqigg== - /@microsoft/rush-stack-compiler/0.4.3: - dependencies: - '@microsoft/api-extractor': 6.1.1 - '@microsoft/node-core-library': 3.5.0 - '@types/node': 8.5.8 - tslint: 5.11.0 - tslint-microsoft-contrib: /tslint-microsoft-contrib/5.2.1/tslint@5.11.0 - typescript: 3.0.3 - dev: false - hasBin: true - resolution: - integrity: sha512-bV6bTELWdebu9Dr3RTioNxrygwgnaEkVGhKk3DhkJZ1IjY7TV8GYYKdtx0vub3iu7pRX5ZAGw7fPMgj3G+HfMA== - /@microsoft/rush-stack-compiler/0.5.4: - dependencies: - '@microsoft/api-extractor': 6.1.6 - '@microsoft/node-core-library': 3.6.0 - '@types/node': 8.5.8 - tslint: 5.11.0 - tslint-microsoft-contrib: /tslint-microsoft-contrib/5.2.1/tslint@5.11.0 - typescript: 3.0.3 - dev: false - hasBin: true - resolution: - integrity: sha512-T36ygaLl8hvmmwby5BO15iYFFcy7DRtDNrYWLXof2Fxa8AavX1ddXukOWBNMceVSK7Aqxrni/cRMvecXkvTmjQ== - /@microsoft/rush-stack-compiler/0.5.6: - dependencies: - '@microsoft/api-extractor': 6.3.0 - '@microsoft/node-core-library': 3.7.0 - '@types/node': 8.5.8 - tslint: 5.11.0 - tslint-microsoft-contrib: /tslint-microsoft-contrib/5.2.1/tslint@5.11.0 - typescript: 3.0.3 - dev: false - hasBin: true - resolution: - integrity: sha512-Gb82pRYnVHNUEu0XAFsq/pYq7fJOodkjotHifGf/YtEtCkPFrbH3QNZ/tx1ZhYTVFA9yb/vwpHtwwxVhKv7iEQ== /@microsoft/ts-command-line/4.2.2: dependencies: '@types/argparse': 1.0.33 @@ -517,10 +334,10 @@ packages: dev: false resolution: integrity: sha512-L/srhENhBtbZLUD9FfJ2ZQdnYv3A3MT3UI2EMbC06fHUzIxLjjbkomD6o42UrbsRMwlS9p1BtxExeaCdX73q2Q== - /@microsoft/tsdoc/0.9.3: + /@microsoft/tsdoc/0.12.4: dev: false resolution: - integrity: sha512-eRJ5mGGdv0YU4gXg+WMdN3XQ7/V/4eRoS/mpmqqHs2bFXFayaB+fqAP6dgzxlpm3aXsSdv4aHq/zLM5c1cx7Cg== + integrity: sha512-nQZVQg3fXygj+9JT/FSPZOz3vqAIVAAR3ZuAuUdU4DSM/ubJq5lbl1hpLtl+REFmEq1rkvDmmPoJAbSoqjcmZQ== /@pnpm/link-bins/1.0.3: dependencies: '@pnpm/package-bins': 1.0.0 @@ -645,12 +462,6 @@ packages: dev: false resolution: integrity: sha512-N1Wdp3v4KmdO3W/CM7KXrDwM4xcVZjlHF2dAOs7sNrTUX8PY3G4n9NkaHlfjGFEfgFeHmRRjywoBd4VkujDs9w== - /@types/fs-extra/5.0.1: - dependencies: - '@types/node': 8.5.8 - dev: false - resolution: - integrity: sha512-h3wnflb+jMTipvbbZnClgA2BexrT4w0GcfoCz5qyxd0IRsbqhLSyesM6mqZTAnhbVmhyTm5tuxfRu9R+8l+lGw== /@types/fs-extra/5.0.4: dependencies: '@types/node': 8.5.8 @@ -793,10 +604,6 @@ packages: dev: false resolution: integrity: sha512-auApPaJf3NPfe18hSoJkp8EbZzer2ISk7o8mCC3M9he/a04+gbMF97NkpD2S8riMGvm4BMRI59/SZQSaLTKpsQ== - /@types/rimraf/0.0.28: - dev: false - resolution: - integrity: sha1-VWJRm8eWPKyoq/fxKMrjtZTUHQY= /@types/rx-core-binding/4.0.4: dependencies: '@types/rx-core': 4.0.3 @@ -1858,7 +1665,7 @@ packages: dependencies: caniuse-lite: 1.0.30000912 electron-to-chromium: 1.3.85 - node-releases: 1.0.4 + node-releases: 1.0.5 dev: false hasBin: true resolution: @@ -2551,7 +2358,7 @@ packages: /deasync/0.1.14: dependencies: bindings: 1.2.1 - node-addon-api: 1.6.1 + node-addon-api: 1.6.2 dev: false engines: node: '>=0.11.0' @@ -3697,14 +3504,6 @@ packages: dev: false resolution: integrity: sha1-zTzl9+fLYUWIP8rjGR6Yd/hYeVA= - /fs-extra/5.0.0: - dependencies: - graceful-fs: 4.1.15 - jsonfile: 4.0.0 - universalify: 0.1.2 - dev: false - resolution: - integrity: sha512-66Pm4RYbjzdyeuqudYqhFiNBbCIuI9kgRqLPSHIlXHidW8NIQtVdkM1yeZ4lXwuhbTETv3EUGMNHAAw6hiundQ== /fs-extra/6.0.0: dependencies: graceful-fs: 4.1.15 @@ -6542,10 +6341,10 @@ packages: dev: false resolution: integrity: sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== - /node-addon-api/1.6.1: + /node-addon-api/1.6.2: dev: false resolution: - integrity: sha512-GcLOYrG5/enbqH4SMsqXt6GQUQGGnDnE3FLDZzXYkCgQHuZV5UDFR+EboeY8kpG0avroyOjpFQ2qLEBosFcRIA== + integrity: sha512-479Bjw9nTE5DdBSZZWprFryHGjUaQC31y1wHo19We/k0BZlrmhqQitWoUL0cD8+scljCbIUL+E58oRDEakdGGA== /node-fetch/2.1.2: dev: false engines: @@ -6626,12 +6425,12 @@ packages: dev: false resolution: integrity: sha512-AhENzCSGZnZJgBARsUjnQ7DnZbzyP+HxlVXuD0xqAnvL8q+OqtSX7lGg9e8nHzwXkMMXNdVeqq4E2M3EUAqX6Q== - /node-releases/1.0.4: + /node-releases/1.0.5: dependencies: semver: 5.3.0 dev: false resolution: - integrity: sha512-GqRV9GcHw8JCRDaP/JoeNMNzEGzHAknMvIHqMb2VeTOmg1Cf9+ej8bkV12tHfzWHQMCkQ5zUFgwFUkfraynNCw== + integrity: sha512-Ky7q0BO1BBkG/rQz6PkEZ59rwo+aSfhczHP1wwq8IowoVdN/FpiP7qp0XW0P2+BVCWe5fQUBozdbVd54q1RbCQ== /node-sass/4.9.3: dependencies: async-foreach: 0.1.3 @@ -7935,13 +7734,6 @@ packages: node: '>=0.10.0' resolution: integrity: sha1-YTObci/mo1FWiSENJOFMlhSGE+8= - /rimraf/2.5.4: - dependencies: - glob: 7.0.6 - dev: false - hasBin: true - resolution: - integrity: sha1-loAAk8vxoMhr2VtGJUZ1NcKd+gQ= /rimraf/2.6.2: dependencies: glob: 7.0.6 @@ -8985,7 +8777,7 @@ packages: lodash: 4.17.11 pkg-dir: 2.0.0 source-map-support: 0.5.9 - typescript: 2.4.2 + typescript: 3.0.3 yargs: 11.1.0 dev: false peerDependencies: @@ -9005,7 +8797,7 @@ packages: lodash: 4.17.11 pkg-dir: 2.0.0 source-map-support: 0.5.9 - typescript: 2.4.2 + typescript: 3.0.3 yargs: 11.1.0 dev: false id: registry.npmjs.org/ts-jest/22.4.6 @@ -9019,8 +8811,8 @@ packages: integrity: sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ== /tslint-microsoft-contrib/5.2.1: dependencies: - tsutils: /tsutils/2.28.0/typescript@2.4.2 - typescript: 2.4.2 + tsutils: /tsutils/2.28.0/typescript@3.0.3 + typescript: 3.0.3 dev: false peerDependencies: tslint: ^5.1.0 @@ -9029,8 +8821,8 @@ packages: /tslint-microsoft-contrib/5.2.1/tslint@5.11.0: dependencies: tslint: 5.11.0 - tsutils: /tsutils/2.28.0/typescript@2.4.2 - typescript: 2.4.2 + tsutils: /tsutils/2.28.0/typescript@3.0.3 + typescript: 3.0.3 dev: false id: registry.npmjs.org/tslint-microsoft-contrib/5.2.1 peerDependencies: @@ -9050,28 +8842,28 @@ packages: resolve: 1.8.1 semver: 5.3.0 tslib: 1.9.3 - tsutils: /tsutils/2.29.0/typescript@2.4.2 - typescript: 2.4.2 + tsutils: /tsutils/2.29.0/typescript@3.0.3 + typescript: 3.0.3 dev: false engines: node: '>=4.8.0' hasBin: true resolution: integrity: sha1-mPMMAurjzecAYgHkwzywi0hYHu0= - /tsutils/2.28.0/typescript@2.4.2: + /tsutils/2.28.0/typescript@3.0.3: dependencies: tslib: 1.9.3 - typescript: 2.4.2 + typescript: 3.0.3 dev: false id: registry.npmjs.org/tsutils/2.28.0 peerDependencies: typescript: '>=2.1.0 || >=2.1.0-dev || >=2.2.0-dev || >=2.3.0-dev || >=2.4.0-dev || >=2.5.0-dev || >=2.6.0-dev || >=2.7.0-dev || >=2.8.0-dev || >=2.9.0-dev || >= 3.0.0-dev || >= 3.1.0-dev' resolution: integrity: sha512-bh5nAtW0tuhvOJnx1GLRn5ScraRLICGyJV5wJhtRWOLsxW70Kk5tZtpK3O/hW6LDnqKS9mlUMPZj9fEMJ0gxqA== - /tsutils/2.29.0/typescript@2.4.2: + /tsutils/2.29.0/typescript@3.0.3: dependencies: tslib: 1.9.3 - typescript: 2.4.2 + typescript: 3.0.3 dev: false id: registry.npmjs.org/tsutils/2.29.0 peerDependencies: @@ -9865,16 +9657,18 @@ packages: integrity: sha512-DUOKC/IhbkdLKKiV89gw9DUauTV8U/8yJl1sjf6MtDmzevLKOF2duNJ495S3MFVjqZarr+qNGCPbkg4mu4PpLw== 'file:projects/api-documenter.tgz': dependencies: + '@microsoft/tsdoc': 0.12.4 '@types/jest': 21.1.10 '@types/js-yaml': 3.9.1 '@types/node': 8.5.8 colors: 1.2.5 gulp: 3.9.1 + jest: 22.4.4 js-yaml: 3.9.1 dev: false name: '@rush-temp/api-documenter' resolution: - integrity: sha512-4cMZrRtZimXLSuM4Yz1K1iYDjBPOb9wZKbp970OOIGvwYCgMcUGO8/WKl/tinyrqavL6H1nyf5mVZ1BQNHrEYA== + integrity: sha512-coYaEsIM99s8Cy1httd5XlUQcYEu314mguDeQypw8pXeu+BZ/n/CfkwEfc63geYYG6Mqc9rRuijltuz4lWd/Cg== tarball: 'file:projects/api-documenter.tgz' version: 0.0.0 'file:projects/api-extractor-test-01.tgz': @@ -9939,9 +9733,8 @@ packages: 'file:projects/api-extractor.tgz': dependencies: '@microsoft/node-library-build': 6.0.15 - '@microsoft/rush-stack-compiler': 0.5.6 '@microsoft/rush-stack-compiler-3.0': 0.1.0 - '@microsoft/tsdoc': 0.12.2 + '@microsoft/tsdoc': 0.12.4 '@types/jest': 21.1.10 '@types/lodash': 4.14.116 '@types/node': 8.5.8 @@ -9957,7 +9750,7 @@ packages: dev: false name: '@rush-temp/api-extractor' resolution: - integrity: sha512-Ufj69Wbdc51d5BlXm0oNjjEUzWiafjgh0gfX9w0jxZDNXe+Pn5RXq6QIoSrzwgFiXh7VlmtrS7OBPN80rShm3w== + integrity: sha512-cWbpZTgFf+Tk+q3MVmh/FSVI/JlxLAlGbUf1qA4hZxDYzDD+hO/6IJozkwiKOx0yhaX1vul5Ig99NlH64fsjvw== tarball: 'file:projects/api-extractor.tgz' version: 0.0.0 'file:projects/gulp-core-build-karma.tgz': @@ -9994,7 +9787,6 @@ packages: 'file:projects/gulp-core-build-mocha.tgz': dependencies: '@microsoft/node-library-build': 6.0.15 - '@microsoft/rush-stack-compiler': 0.5.4 '@types/glob': 5.0.30 '@types/gulp': 3.8.32 '@types/gulp-istanbul': 0.9.30 @@ -10062,9 +9854,8 @@ packages: version: 0.0.0 'file:projects/gulp-core-build-typescript.tgz': dependencies: - '@microsoft/api-extractor': 6.1.6 + '@microsoft/api-extractor': 6.3.0 '@microsoft/node-library-build': 6.0.15 - '@microsoft/rush-stack-compiler': 0.5.4 '@types/glob': 5.0.30 '@types/node': 8.5.8 '@types/resolve': 0.0.8 @@ -10078,7 +9869,7 @@ packages: dev: false name: '@rush-temp/gulp-core-build-typescript' resolution: - integrity: sha512-xOBuV8P7uQukGrsS4GCACJWD5HGrzx+x+zY+GlMrglhw1/DGDcKX9tYJi/6PYMRQ6aXkewzXgt80Y0d+aeNRNQ== + integrity: sha512-JJ9LTFIqxCBi+MMPG880MBiBqjv7y+mO2JZeQqazKkIaq47bADOSUClSZWD8KZgMOjQZTA/oJ2ivatNhMQW7HA== tarball: 'file:projects/gulp-core-build-typescript.tgz' version: 0.0.0 'file:projects/gulp-core-build-webpack.tgz': @@ -10102,7 +9893,6 @@ packages: 'file:projects/gulp-core-build.tgz': dependencies: '@microsoft/node-library-build': 6.0.15 - '@microsoft/rush-stack-compiler': 0.5.4 '@types/assertion-error': 1.0.30 '@types/chai': 3.4.34 '@types/chalk': 0.4.31 @@ -10210,7 +10000,6 @@ packages: 'file:projects/node-core-library.tgz': dependencies: '@microsoft/node-library-build': 6.0.15 - '@microsoft/rush-stack-compiler': 0.5.6 '@microsoft/rush-stack-compiler-3.0': 0.1.0 '@types/fs-extra': 5.0.4 '@types/jest': 21.1.10 @@ -10332,8 +10121,8 @@ packages: version: 0.0.0 'file:projects/rush-stack-compiler-2.4.tgz': dependencies: + '@microsoft/api-extractor': 6.3.0 '@microsoft/node-library-build': 6.0.15 - '@microsoft/rush-stack-compiler': 0.5.6 '@microsoft/rush-stack-compiler-3.0': 0.1.0 '@types/node': 8.5.8 gulp: 3.9.1 @@ -10343,7 +10132,7 @@ packages: dev: false name: '@rush-temp/rush-stack-compiler-2.4' resolution: - integrity: sha512-8VA9vfmOlNVA67PD/hwUIMM24i8WCXJsYsCeZl1vxT1QE0mfcn8mqCwZqJ49tustlt0Col/wFeodpXg1YGAzDw== + integrity: sha512-ttMM21obLr5rp+FxFvIWWgvjcqhc2NFWeN3yGh3U6Zwgpxpr2mLTes+oGFB6W9ylMXvE0LB6/r2OD5cADCcIFw== tarball: 'file:projects/rush-stack-compiler-2.4.tgz' version: 0.0.0 'file:projects/rush-stack-compiler-2.7-library-test.tgz': @@ -10358,8 +10147,8 @@ packages: version: 0.0.0 'file:projects/rush-stack-compiler-2.7.tgz': dependencies: + '@microsoft/api-extractor': 6.3.0 '@microsoft/node-library-build': 6.0.15 - '@microsoft/rush-stack-compiler': 0.5.6 '@microsoft/rush-stack-compiler-3.0': 0.1.0 '@types/node': 8.5.8 gulp: 3.9.1 @@ -10369,12 +10158,11 @@ packages: dev: false name: '@rush-temp/rush-stack-compiler-2.7' resolution: - integrity: sha512-yhovJIQw2TQ9UbD0ua5eIBq7SpAfDujUUZ9CPgMr02nDBifCj2KWNmG4oftzRm1DOKNfIl0MLwx/GCXjv7ixFQ== + integrity: sha512-TvACnlJJ1YBpMPsnXiGruXHbmSUxkz34AK0j4VMhgsoE/vzLBoFaEdB0Uax/MGlt1CMu18bIQX0zrKhlFw7dYQ== tarball: 'file:projects/rush-stack-compiler-2.7.tgz' version: 0.0.0 'file:projects/rush-stack-compiler-2.9-library-test.tgz': dependencies: - '@microsoft/rush-stack-compiler-2.9': 0.1.0 '@types/node': 8.5.8 gulp: 3.9.1 dev: false @@ -10385,8 +10173,8 @@ packages: version: 0.0.0 'file:projects/rush-stack-compiler-2.9.tgz': dependencies: + '@microsoft/api-extractor': 6.3.0 '@microsoft/node-library-build': 6.0.15 - '@microsoft/rush-stack-compiler': 0.5.6 '@microsoft/rush-stack-compiler-3.0': 0.1.0 '@types/node': 8.5.8 gulp: 3.9.1 @@ -10396,7 +10184,7 @@ packages: dev: false name: '@rush-temp/rush-stack-compiler-2.9' resolution: - integrity: sha512-59RkgS3NhUj+A/ew8CzsV8sIwFS3QwaMXqkAwTg2lrHOKtzhoNIW9g23m2/fJ+ZUMZKbZW+k/A370mlofpGgqw== + integrity: sha512-HJ07X0KkNaPmVCcKPtTL1MI4C8GSB9MfaF0DXN/BKZ97NSkM9RDimqOspzUv22E7N+RW9s1MUgMGa60lOfqPQg== tarball: 'file:projects/rush-stack-compiler-2.9.tgz' version: 0.0.0 'file:projects/rush-stack-compiler-3.0-library-test.tgz': @@ -10411,8 +10199,8 @@ packages: version: 0.0.0 'file:projects/rush-stack-compiler-3.0.tgz': dependencies: + '@microsoft/api-extractor': 6.3.0 '@microsoft/node-library-build': 6.0.15 - '@microsoft/rush-stack-compiler': 0.5.6 '@microsoft/rush-stack-compiler-3.0': 0.1.0 '@types/node': 8.5.8 gulp: 3.9.1 @@ -10422,7 +10210,7 @@ packages: dev: false name: '@rush-temp/rush-stack-compiler-3.0' resolution: - integrity: sha512-F8tg4Xet+sFkyWiKMB46yBr6O/AnAlFjXmgsxttGLG40Px/2R4pW4I8iTgA2SoNXFTs+ZhX3s+SnSrA7HS0S8Q== + integrity: sha512-tTXBMHc5ZnkWWl/+GKNqO4b+l4KVNJq5W/vPEXxEQn+a6ehz1JPWdwkjQI9vd8YEFpHgjhqlmc9J4u+sb6JRbQ== tarball: 'file:projects/rush-stack-compiler-3.0.tgz' version: 0.0.0 'file:projects/rush-stack-compiler-3.1-library-test.tgz': @@ -10437,8 +10225,8 @@ packages: version: 0.0.0 'file:projects/rush-stack-compiler-3.1.tgz': dependencies: + '@microsoft/api-extractor': 6.3.0 '@microsoft/node-library-build': 6.0.15 - '@microsoft/rush-stack-compiler': 0.5.6 '@microsoft/rush-stack-compiler-3.0': 0.1.0 '@types/node': 8.5.8 gulp: 3.9.1 @@ -10448,7 +10236,7 @@ packages: dev: false name: '@rush-temp/rush-stack-compiler-3.1' resolution: - integrity: sha512-7XmCF9mfOct1TxEpfH8kFgqVDbmXMrPP8YvGPBj7+dxkxYovEoVGZ55ltu7xgxkZ+XX8nZyWPwebWTftsvWYYg== + integrity: sha512-CklCZ+tcExdanG46gvsn8LD3sRaozzeTGnSY0aKfg6mBrhZJu1FQQnGs3tROF3fxlXA/LE6/rB3P4K4MFMb1kg== tarball: 'file:projects/rush-stack-compiler-3.1.tgz' version: 0.0.0 'file:projects/rush-stack-compiler-shared.tgz': @@ -10459,14 +10247,6 @@ packages: tarball: 'file:projects/rush-stack-compiler-shared.tgz' version: 0.0.0 'file:projects/rush-stack-compiler.tgz': - dependencies: - '@microsoft/node-library-build': 6.0.6 - '@microsoft/rush-stack-compiler': 0.4.3 - '@types/node': 8.5.8 - gulp: 3.9.1 - tslint: 5.11.0 - tslint-microsoft-contrib: /tslint-microsoft-contrib/5.2.1/tslint@5.11.0 - typescript: 3.0.3 dev: false name: '@rush-temp/rush-stack-compiler' resolution: @@ -10482,7 +10262,6 @@ packages: version: 0.0.0 'file:projects/rush-stack.tgz': dependencies: - '@microsoft/node-library-build': 6.0.6 '@types/node': 8.5.8 colors: 1.2.5 gulp: 3.9.1 @@ -10560,7 +10339,6 @@ packages: 'file:projects/ts-command-line.tgz': dependencies: '@microsoft/node-library-build': 6.0.15 - '@microsoft/rush-stack-compiler': 0.5.6 '@microsoft/rush-stack-compiler-3.0': 0.1.0 '@types/argparse': 1.0.33 '@types/jest': 21.1.10 @@ -10602,9 +10380,10 @@ registry: 'https://registry.npmjs.org/' shrinkwrapMinorVersion: 9 shrinkwrapVersion: 3 specifiers: + '@microsoft/api-extractor': 6.3.0 '@microsoft/node-library-build': 6.0.15 '@microsoft/rush-stack-compiler-3.0': 0.1.0 - '@microsoft/tsdoc': 0.12.2 + '@microsoft/tsdoc': 0.12.4 '@pnpm/link-bins': ~1.0.1 '@pnpm/logger': ~1.0.1 '@rush-temp/api-documenter': 'file:./projects/api-documenter.tgz' diff --git a/common/reviews/api/api-extractor.api.ts b/common/reviews/api/api-extractor.api.ts index 1370ccdcc68..dfaf63a16e0 100644 --- a/common/reviews/api/api-extractor.api.ts +++ b/common/reviews/api/api-extractor.api.ts @@ -1,13 +1,300 @@ +// @public (undocumented) +class ApiClass extends ApiClass_base { + constructor(options: IApiClassOptions); + // @override (undocumented) + readonly canonicalReference: string; + // (undocumented) + static getCanonicalReference(name: string): string; + // @override (undocumented) + readonly kind: ApiItemKind; +} + +// @public (undocumented) +interface ApiDeclarationMixin { +} + +// @public (undocumented) +class ApiDocumentedItem extends ApiItem { + constructor(options: IApiDocumentedItemOptions); + // WARNING: The type "IApiItemJson" needs to be exported by the package (e.g. added to index.ts) + // @override (undocumented) + static onDeserializeInto(options: Partial, jsonObject: IApiItemJson): void; + // WARNING: The type "IApiDocumentedItemJson" needs to be exported by the package (e.g. added to index.ts) + // @override (undocumented) + serializeInto(jsonObject: Partial): void; + // (undocumented) + readonly tsdocComment: tsdoc.DocComment | undefined; +} + +// @public (undocumented) +class ApiEntryPoint extends ApiEntryPoint_base { + constructor(options: IApiEntryPointOptions); + // @override (undocumented) + readonly canonicalReference: string; + // @override (undocumented) + readonly kind: ApiItemKind; +} + +// @public (undocumented) +class ApiEnum extends ApiEnum_base { + constructor(options: IApiEnumOptions); + // @override (undocumented) + addMember(member: ApiEnumMember): void; + // @override (undocumented) + readonly canonicalReference: string; + // (undocumented) + static getCanonicalReference(name: string): string; + // @override (undocumented) + readonly kind: ApiItemKind; + // @override (undocumented) + readonly members: ReadonlyArray; +} + +// @public (undocumented) +class ApiEnumMember extends ApiEnumMember_base { + constructor(options: IApiEnumMemberOptions); + // @override (undocumented) + readonly canonicalReference: string; + // (undocumented) + static getCanonicalReference(name: string): string; + // (undocumented) + readonly initializerExcerpt: Excerpt; + // @override (undocumented) + readonly kind: ApiItemKind; +} + +// @public (undocumented) +interface ApiFunctionLikeMixin { +} + +// @public (undocumented) +class ApiInterface extends ApiInterface_base { + constructor(options: IApiInterfaceOptions); + // @override (undocumented) + readonly canonicalReference: string; + // (undocumented) + static getCanonicalReference(name: string): string; + // @override (undocumented) + readonly kind: ApiItemKind; +} + +// @public (undocumented) +class ApiItem { + // (undocumented) + __computed: ApiItem | undefined; + constructor(options: IApiItemOptions); + // @virtual (undocumented) + readonly canonicalReference: string; + // WARNING: The type "IApiItemJson" needs to be exported by the package (e.g. added to index.ts) + // (undocumented) + static deserialize(jsonObject: IApiItemJson): ApiItem; + getAssociatedPackage(): ApiPackage | undefined; + getHierarchy(): ReadonlyArray; + getScopedNameWithinPackage(): string; + // @virtual (undocumented) + getSortKey(): string; + // @virtual (undocumented) + readonly kind: ApiItemKind; + // @virtual + readonly members: ReadonlyArray; + // (undocumented) + readonly name: string; + // WARNING: The type "IApiItemJson" needs to be exported by the package (e.g. added to index.ts) + // @virtual (undocumented) + static onDeserializeInto(options: Partial, jsonObject: IApiItemJson): void; + // @virtual + readonly parent: ApiItem | undefined; + // WARNING: The type "IApiItemJson" needs to be exported by the package (e.g. added to index.ts) + // @virtual (undocumented) + serializeInto(jsonObject: Partial): void; +} + +// @public (undocumented) +interface ApiItemContainerMixin { +} + +// @public (undocumented) +enum ApiItemKind { + // (undocumented) + Class = "Class", + // (undocumented) + EntryPoint = "EntryPoint", + // (undocumented) + Enum = "Enum", + // (undocumented) + EnumMember = "EnumMember", + // (undocumented) + Interface = "Interface", + // (undocumented) + Method = "Method", + // (undocumented) + MethodSignature = "MethodSignature", + // (undocumented) + Model = "Model", + // (undocumented) + Namespace = "Namespace", + // (undocumented) + None = "None", + // (undocumented) + Package = "Package", + // (undocumented) + Parameter = "Parameter", + // (undocumented) + Property = "Property", + // (undocumented) + PropertySignature = "PropertySignature" +} + +// @public (undocumented) +class ApiMethod extends ApiMethod_base { + constructor(options: IApiMethodOptions); + // @override (undocumented) + readonly canonicalReference: string; + // (undocumented) + static getCanonicalReference(name: string, isStatic: boolean, overloadIndex: number): string; + // @override (undocumented) + readonly kind: ApiItemKind; + // (undocumented) + readonly returnTypeExcerpt: Excerpt; +} + +// @public (undocumented) +class ApiMethodSignature extends ApiMethodSignature_base { + constructor(options: IApiMethodSignatureOptions); + // @override (undocumented) + readonly canonicalReference: string; + // (undocumented) + static getCanonicalReference(name: string, overloadIndex: number): string; + // @override (undocumented) + readonly kind: ApiItemKind; + // (undocumented) + readonly returnTypeExcerpt: Excerpt; +} + +// @public (undocumented) +class ApiModel extends ApiModel_base { + constructor(); + // @override (undocumented) + addMember(member: ApiPackage): void; + // @override (undocumented) + readonly canonicalReference: string; + // @override (undocumented) + readonly kind: ApiItemKind; + // (undocumented) + loadPackage(apiJsonFilename: string): ApiPackage; + // (undocumented) + readonly packages: ReadonlyArray; + // (undocumented) + resolveDeclarationReference(declarationReference: DocDeclarationReference, contextApiItem: ApiItem | undefined): IResolveDeclarationReferenceResult; + tryGetPackageByName(packageName: string): ApiPackage | undefined; +} + +// @public (undocumented) +class ApiNamespace extends ApiNamespace_base { + constructor(options: IApiNamespaceOptions); + // @override (undocumented) + readonly canonicalReference: string; + // (undocumented) + static getCanonicalReference(name: string): string; + // @override (undocumented) + readonly kind: ApiItemKind; +} + +// @public (undocumented) +class ApiPackage extends ApiPackage_base { + constructor(options: IApiPackageOptions); + // @override (undocumented) + addMember(member: ApiEntryPoint): void; + // @override (undocumented) + readonly canonicalReference: string; + // (undocumented) + readonly entryPoints: ReadonlyArray; + // (undocumented) + findEntryPointsByPath(importPath: string): ReadonlyArray; + // @override (undocumented) + readonly kind: ApiItemKind; + // (undocumented) + static loadFromJsonFile(apiJsonFilename: string): ApiPackage; + // (undocumented) + saveToJsonFile(apiJsonFilename: string, options?: IJsonFileSaveOptions): void; +} + +// @public (undocumented) +class ApiParameter extends ApiParameter_base { + constructor(options: IApiParameterOptions); + // @override (undocumented) + readonly canonicalReference: string; + // @override (undocumented) + readonly kind: ApiItemKind; + // (undocumented) + readonly parameterTypeExcerpt: Excerpt; + readonly tsdocParamBlock: tsdoc.DocParamBlock | undefined; +} + +// @public (undocumented) +class ApiProperty extends ApiProperty_base { + constructor(options: IApiPropertyOptions); + // @override (undocumented) + readonly canonicalReference: string; + // (undocumented) + static getCanonicalReference(name: string, isStatic: boolean): string; + // @override (undocumented) + readonly kind: ApiItemKind; +} + // @public -class ApiJsonFile { - static jsonSchema: JsonSchema; - static loadFromFile(apiJsonFilePath: string): IApiPackage; +class ApiPropertyItem extends ApiPropertyItem_base { + constructor(options: IApiPropertyItemOptions); + readonly isEventProperty: boolean; + // (undocumented) + readonly propertyTypeExcerpt: Excerpt; } -// @beta -class ExternalApiHelper { +// @public (undocumented) +class ApiPropertySignature extends ApiPropertySignature_base { + constructor(options: IApiPropertySignatureOptions); + // @override (undocumented) + readonly canonicalReference: string; + // (undocumented) + static getCanonicalReference(name: string): string; + // @override (undocumented) + readonly kind: ApiItemKind; +} + +// @public (undocumented) +interface ApiReleaseTagMixin { +} + +// @public (undocumented) +interface ApiStaticMixin { +} + +// @public (undocumented) +class Excerpt { + constructor(tokens: ReadonlyArray, tokenRange: IExcerptTokenRange); + // (undocumented) + readonly text: string; + // (undocumented) + readonly tokenRange: IExcerptTokenRange; // (undocumented) - static generateApiJson(rootDir: string, libFolder: string, externalPackageFilePath: string): void; + readonly tokens: ReadonlyArray; +} + +// @public (undocumented) +class ExcerptToken { + constructor(kind: ExcerptTokenKind, text: string); + // (undocumented) + readonly kind: ExcerptTokenKind; + // (undocumented) + readonly text: string; +} + +// @public (undocumented) +enum ExcerptTokenKind { + // (undocumented) + Content = "Content", + // (undocumented) + Reference = "Reference" } // @public @@ -19,6 +306,7 @@ class Extractor { static generateFilePathsForAnalysis(inputFilePaths: string[]): string[]; static jsonSchema: JsonSchema; static loadConfigObject(jsonConfigFile: string): IExtractorConfig; + static readonly packageName: string; processProject(options?: IAnalyzeProjectOptions): boolean; static processProjectFromConfigFile(jsonConfigFile: string, options?: IExtractorOptions): void; static readonly version: string; @@ -35,145 +323,126 @@ interface IAnalyzeProjectOptions { projectConfig?: IExtractorProjectConfig; } -// @alpha -interface IApiBaseDefinition { +// @public (undocumented) +interface IApiClassOptions extends IApiDeclarationMixinOptions, IApiItemContainerMixinOptions, IApiReleaseTagMixinOptions, IApiDocumentedItemOptions { +} + +// @public (undocumented) +interface IApiDeclarationMixinOptions extends IApiItemOptions { // (undocumented) - deprecatedMessage?: MarkupBasicElement[]; + declarationExcerpt: IDeclarationExcerpt; +} + +// @public (undocumented) +interface IApiDocumentedItemOptions extends IApiItemOptions { // (undocumented) - isBeta: boolean; - kind: string; + docComment: tsdoc.DocComment | undefined; +} + +// @public (undocumented) +interface IApiEntryPointOptions extends IApiItemContainerMixinOptions { +} + +// @public (undocumented) +interface IApiEnumMemberOptions extends IApiDeclarationMixinOptions, IApiReleaseTagMixinOptions, IApiDocumentedItemOptions { +} + +// @public (undocumented) +interface IApiEnumOptions extends IApiDeclarationMixinOptions, IApiItemContainerMixinOptions, IApiReleaseTagMixinOptions, IApiDocumentedItemOptions { +} + +// @public (undocumented) +interface IApiFunctionLikeMixinOptions extends IApiItemOptions { // (undocumented) - remarks: MarkupStructuredElement[]; + overloadIndex: number; // (undocumented) - summary: MarkupBasicElement[]; + parameters?: ApiParameter[]; } -// @alpha -interface IApiClass extends IApiBaseDefinition { - extends?: string; - implements?: string; - isSealed: boolean; - kind: 'class'; - members: IApiNameMap; - typeParameters?: string[]; +// @public (undocumented) +interface IApiInterfaceOptions extends IApiDeclarationMixinOptions, IApiItemContainerMixinOptions, IApiReleaseTagMixinOptions, IApiDocumentedItemOptions { } -// @alpha -interface IApiConstructor extends IApiBaseDefinition { - isOverride: boolean; - isSealed: boolean; - isVirtual: boolean; - kind: 'constructor'; - parameters: IApiNameMap; - signature: string; +// @public (undocumented) +interface IApiItemContainerMixinOptions extends IApiItemOptions { + // (undocumented) + members?: ApiItem[]; } -// @alpha -interface IApiEnum extends IApiBaseDefinition { - kind: 'enum'; +// @public (undocumented) +interface IApiItemOptions { // (undocumented) - values: IApiEnumMember[]; + name: string; } -// @alpha -interface IApiEnumMember extends IApiBaseDefinition { - kind: 'enum value'; - // (undocumented) - value: string; +// @public (undocumented) +interface IApiMethodOptions extends IApiDeclarationMixinOptions, IApiFunctionLikeMixinOptions, IApiReleaseTagMixinOptions, IApiStaticMixinOptions, IApiDocumentedItemOptions { } -// @alpha -interface IApiFunction extends IApiBaseDefinition { - kind: 'function'; - parameters: IApiNameMap; - returnValue: IApiReturnValue; - signature: string; +// @public (undocumented) +interface IApiMethodSignatureOptions extends IApiDeclarationMixinOptions, IApiFunctionLikeMixinOptions, IApiReleaseTagMixinOptions, IApiDocumentedItemOptions { } -// @alpha -interface IApiInterface extends IApiBaseDefinition { - extends?: string; - implements?: string; - isSealed: boolean; - kind: 'interface'; - members: IApiNameMap; - typeParameters?: string[]; -} - -// @alpha -interface IApiItemReference { - exportName: string; - memberName: string; - packageName: string; - scopeName: string; -} - -// @alpha -interface IApiMethod extends IApiBaseDefinition { - accessModifier: ApiAccessModifier; - isOptional: boolean; - isOverride: boolean; - isSealed: boolean; - isStatic: boolean; - isVirtual: boolean; - kind: 'method'; - parameters: IApiNameMap; - returnValue: IApiReturnValue; - signature: string; +// @public (undocumented) +interface IApiNamespaceOptions extends IApiDeclarationMixinOptions, IApiItemContainerMixinOptions, IApiReleaseTagMixinOptions, IApiDocumentedItemOptions { } -// @alpha -interface IApiNameMap { - [name: string]: T; +// @public (undocumented) +interface IApiPackageOptions extends IApiItemContainerMixinOptions, IApiDocumentedItemOptions { } -// @alpha -interface IApiNamespace extends IApiBaseDefinition { - exports: IApiNameMap; - kind: 'namespace'; +// @public (undocumented) +interface IApiParameterOptions extends IApiDeclarationMixinOptions, IApiItemOptions { } -// @alpha -interface IApiPackage { - // (undocumented) - deprecatedMessage?: MarkupBasicElement[]; - exports: IApiNameMap; - isBeta: boolean; - kind: 'package'; - name: string; +// @public (undocumented) +interface IApiPropertyItemOptions extends IApiDocumentedItemOptions, IApiDeclarationMixinOptions { +} + +// @public (undocumented) +interface IApiPropertyOptions extends IApiReleaseTagMixinOptions, IApiStaticMixinOptions, IApiPropertyItemOptions { +} + +// @public (undocumented) +interface IApiPropertySignatureOptions extends IApiReleaseTagMixinOptions, IApiPropertyItemOptions { +} + +// @public (undocumented) +interface IApiReleaseTagMixinOptions extends IApiItemOptions { // (undocumented) - remarks: MarkupStructuredElement[]; + releaseTag: ReleaseTag; +} + +// @public (undocumented) +interface IApiStaticMixinOptions extends IApiItemOptions { // (undocumented) - summary: MarkupBasicElement[]; + isStatic: boolean; } -// @alpha -interface IApiParameter { - description: MarkupBasicElement[]; - isOptional: boolean; - isSpread: boolean; - name: string; - type: string; +// @public (undocumented) +interface IDeclarationExcerpt { + // (undocumented) + embeddedExcerpts: { + [name in ExcerptName]?: IExcerptTokenRange; + }; + // (undocumented) + excerptTokens: IExcerptToken[]; } -// @alpha -interface IApiProperty extends IApiBaseDefinition { - isEventProperty: boolean; - isOptional: boolean; - isOverride: boolean; - isReadOnly: boolean; - isSealed: boolean; - isStatic: boolean; - isVirtual: boolean; - kind: 'property'; - signature: string; - type: string; +// @public (undocumented) +interface IExcerptToken { + // (undocumented) + readonly kind: ExcerptTokenKind; + // (undocumented) + text: string; } -// @alpha -interface IApiReturnValue { - description: MarkupBasicElement[]; - type: string; +// @public (undocumented) +interface IExcerptTokenRange { + // (undocumented) + readonly endIndex: number; + // (undocumented) + readonly startIndex: number; } // @public @@ -231,7 +500,6 @@ interface IExtractorPoliciesConfig { // @public interface IExtractorProjectConfig { entryPointSourceFile: string; - externalJsonFileFolders?: string[]; } // @public @@ -262,148 +530,39 @@ interface ILogger { logWarning(message: string): void; } -// @public -interface IMarkupApiLink { - elements: MarkupLinkTextElement[]; - kind: 'api-link'; - target: IApiItemReference; -} - -// @public -interface IMarkupCodeBox { - // (undocumented) - highlighter: MarkupHighlighter; - kind: 'code-box'; - text: string; -} - -// @public -interface IMarkupCreateTextOptions { - bold?: boolean; - italics?: boolean; -} - -// @public -interface IMarkupHeading1 { - kind: 'heading1'; - text: string; -} - -// @public -interface IMarkupHeading2 { - kind: 'heading2'; - text: string; -} - -// @public -interface IMarkupHighlightedText { - highlighter: MarkupHighlighter; - kind: 'code'; - text: string; -} - -// @public -interface IMarkupHtmlTag { - kind: 'html-tag'; - token: string; -} - -// @public -interface IMarkupLineBreak { - kind: 'break'; -} - -// @public -interface IMarkupNoteBox { - // (undocumented) - elements: MarkupBasicElement[]; - kind: 'note-box'; -} - -// @public -interface IMarkupPage { - // (undocumented) - breadcrumb: MarkupBasicElement[]; - // (undocumented) - elements: MarkupStructuredElement[]; - kind: 'page'; - // (undocumented) - title: string; -} - -// @public -interface IMarkupParagraph { - kind: 'paragraph'; -} - -// @public -interface IMarkupTable { - // (undocumented) - header?: IMarkupTableRow; - kind: 'table'; - // (undocumented) - rows: IMarkupTableRow[]; -} - -// @public -interface IMarkupTableCell { - elements: MarkupBasicElement[]; - kind: 'table-cell'; -} - -// @public -interface IMarkupTableRow { +// @beta +class IndentedWriter { + constructor(builder?: IStringBuilder); + decreaseIndent(): void; + defaultIndentPrefix: string; + ensureNewLine(): void; + ensureSkippedLine(): void; + getText(): string; + increaseIndent(indentPrefix?: string): void; + indentScope(scope: () => void, indentPrefix?: string): void; + peekLastCharacter(): string; + peekSecondLastCharacter(): string; // (undocumented) - cells: IMarkupTableCell[]; - kind: 'table-row'; + toString(): string; + write(message: string): void; + writeLine(message?: string): void; } // @public -interface IMarkupText { - bold?: boolean; - italics?: boolean; - kind: 'text'; - text: string; +interface IResolveDeclarationReferenceResult { + errorMessage: string | undefined; + resolvedApiItem: ApiItem | undefined; } // @public -interface IMarkupWebLink { - elements: MarkupLinkTextElement[]; - kind: 'web-link'; - targetUrl: string; +enum ReleaseTag { + Alpha = 2, + Beta = 3, + Internal = 1, + None = 0, + Public = 4 } -// @public -class Markup { - static appendTextElements(output: MarkupElement[], text: string, options?: IMarkupCreateTextOptions): void; - static BREAK: IMarkupLineBreak; - static createApiLink(textElements: MarkupLinkTextElement[], target: IApiItemReference): IMarkupApiLink; - static createApiLinkFromText(text: string, target: IApiItemReference): IMarkupApiLink; - static createCode(code: string, highlighter?: MarkupHighlighter): IMarkupHighlightedText; - static createCodeBox(code: string, highlighter: MarkupHighlighter): IMarkupCodeBox; - static createHeading1(text: string): IMarkupHeading1; - static createHeading2(text: string): IMarkupHeading2; - static createHtmlTag(token: string): IMarkupHtmlTag; - static createNoteBox(textElements: MarkupBasicElement[]): IMarkupNoteBox; - static createNoteBoxFromText(text: string): IMarkupNoteBox; - static createPage(title: string): IMarkupPage; - static createTable(headerCellValues?: MarkupBasicElement[][] | undefined): IMarkupTable; - static createTableRow(cellValues?: MarkupBasicElement[][] | undefined): IMarkupTableRow; - static createTextElements(text: string, options?: IMarkupCreateTextOptions): IMarkupText[]; - static createTextParagraphs(text: string, options?: IMarkupCreateTextOptions): MarkupBasicElement[]; - static createWebLink(textElements: MarkupLinkTextElement[], targetUrl: string): IMarkupWebLink; - static createWebLinkFromText(text: string, targetUrl: string): IMarkupWebLink; - static extractTextContent(elements: MarkupElement[]): string; - static formatApiItemReference(apiItemReference: IApiItemReference): string; - static normalize(elements: T[]): void; - static PARAGRAPH: IMarkupParagraph; -} - -// WARNING: Unsupported export: ApiAccessModifier -// WARNING: Unsupported export: ApiMember -// WARNING: Unsupported export: ApiItem -// WARNING: Unsupported export: MarkupHighlighter -// WARNING: Unsupported export: MarkupLinkTextElement -// WARNING: Unsupported export: MarkupBasicElement -// WARNING: Unsupported export: MarkupStructuredElement -// WARNING: Unsupported export: MarkupElement +// WARNING: Unsupported export: ExcerptName +// WARNING: Unsupported export: Constructor +// WARNING: Unsupported export: PropertiesOf diff --git a/common/reviews/api/node-core-library.api.ts b/common/reviews/api/node-core-library.api.ts index 834e1563caf..a9a0aa0cffc 100644 --- a/common/reviews/api/node-core-library.api.ts +++ b/common/reviews/api/node-core-library.api.ts @@ -284,6 +284,12 @@ interface IProtectableMapParameters { onSet?: (source: ProtectableMap, key: K, value: V) => V; } +// @public +interface IStringBuilder { + append(text: string): void; + toString(): string; +} + // @beta interface ITerminalProvider { eolCharacter: string; @@ -331,7 +337,7 @@ class LockFile { // @public class MapExtensions { - static mergeFromMap(targetMap: Map, sourceMap: Map): void; + static mergeFromMap(targetMap: Map, sourceMap: ReadonlyMap): void; } // @public @@ -409,8 +415,8 @@ class Sort { static sortSetBy(set: Set, keySelector: (element: T) => any, keyComparer?: (x: T, y: T) => number): void; } -// @beta -class StringBuilder { +// @public +class StringBuilder implements IStringBuilder { constructor(); append(text: string): void; toString(): string; diff --git a/core-build/gulp-core-build-karma/config/api-extractor.json b/core-build/gulp-core-build-karma/config/api-extractor.json index 5b65c425f79..d3ffdc526cd 100644 --- a/core-build/gulp-core-build-karma/config/api-extractor.json +++ b/core-build/gulp-core-build-karma/config/api-extractor.json @@ -1,5 +1,5 @@ { - "enabled": true, + "enabled": false, "apiReviewFolder": "../../common/reviews/api", "apiJsonFolder": "./temp", "entry": "lib/index.d.ts" diff --git a/core-build/gulp-core-build-karma/package.json b/core-build/gulp-core-build-karma/package.json index 2ecbba4a6a2..2d18d2cc537 100644 --- a/core-build/gulp-core-build-karma/package.json +++ b/core-build/gulp-core-build-karma/package.json @@ -4,6 +4,9 @@ "description": "", "main": "lib/index.js", "typings": "lib/index.d.ts", + "tsdoc": { + "tsdocFlavor": "AEDoc" + }, "license": "MIT", "repository": { "type": "git", diff --git a/core-build/gulp-core-build-mocha/package.json b/core-build/gulp-core-build-mocha/package.json index 4f18c37ca9c..41ad9076adc 100644 --- a/core-build/gulp-core-build-mocha/package.json +++ b/core-build/gulp-core-build-mocha/package.json @@ -4,6 +4,9 @@ "description": "", "main": "lib/index.js", "typings": "lib/index.d.ts", + "tsdoc": { + "tsdocFlavor": "AEDoc" + }, "license": "MIT", "repository": { "type": "git", diff --git a/core-build/gulp-core-build-mocha/src/index.ts b/core-build/gulp-core-build-mocha/src/index.ts index 38e890562e3..0ee11358b25 100644 --- a/core-build/gulp-core-build-mocha/src/index.ts +++ b/core-build/gulp-core-build-mocha/src/index.ts @@ -5,7 +5,9 @@ import { serial, IExecutable } from '@microsoft/gulp-core-build'; import { MochaTask } from './MochaTask'; import { InstrumentTask } from './InstrumentTask'; +/** @public */ export const instrument: InstrumentTask = new InstrumentTask(); +/** @public */ export const mocha: MochaTask = new MochaTask(); export default serial(instrument, mocha) as IExecutable; // tslint:disable-line:export-name no-any diff --git a/core-build/gulp-core-build-sass/config/api-extractor.json b/core-build/gulp-core-build-sass/config/api-extractor.json index 5b65c425f79..d3ffdc526cd 100644 --- a/core-build/gulp-core-build-sass/config/api-extractor.json +++ b/core-build/gulp-core-build-sass/config/api-extractor.json @@ -1,5 +1,5 @@ { - "enabled": true, + "enabled": false, "apiReviewFolder": "../../common/reviews/api", "apiJsonFolder": "./temp", "entry": "lib/index.d.ts" diff --git a/core-build/gulp-core-build-sass/package.json b/core-build/gulp-core-build-sass/package.json index b3ce1f1f950..b946d020765 100644 --- a/core-build/gulp-core-build-sass/package.json +++ b/core-build/gulp-core-build-sass/package.json @@ -4,6 +4,9 @@ "description": "", "main": "lib/index.js", "typings": "lib/index.d.ts", + "tsdoc": { + "tsdocFlavor": "AEDoc" + }, "license": "MIT", "repository": { "type": "git", diff --git a/core-build/gulp-core-build-serve/config/api-extractor.json b/core-build/gulp-core-build-serve/config/api-extractor.json index 5b65c425f79..d3ffdc526cd 100644 --- a/core-build/gulp-core-build-serve/config/api-extractor.json +++ b/core-build/gulp-core-build-serve/config/api-extractor.json @@ -1,5 +1,5 @@ { - "enabled": true, + "enabled": false, "apiReviewFolder": "../../common/reviews/api", "apiJsonFolder": "./temp", "entry": "lib/index.d.ts" diff --git a/core-build/gulp-core-build-serve/package.json b/core-build/gulp-core-build-serve/package.json index 0642ae6b35f..8ccdc08356e 100644 --- a/core-build/gulp-core-build-serve/package.json +++ b/core-build/gulp-core-build-serve/package.json @@ -4,6 +4,9 @@ "description": "", "main": "lib/index.js", "typings": "lib/index.d.ts", + "tsdoc": { + "tsdocFlavor": "AEDoc" + }, "license": "MIT", "repository": { "type": "git", diff --git a/core-build/gulp-core-build-typescript/package.json b/core-build/gulp-core-build-typescript/package.json index eddc34fb609..2585f4e06b8 100644 --- a/core-build/gulp-core-build-typescript/package.json +++ b/core-build/gulp-core-build-typescript/package.json @@ -4,6 +4,9 @@ "description": "", "main": "lib/index.js", "typings": "lib/index.d.ts", + "tsdoc": { + "tsdocFlavor": "AEDoc" + }, "license": "MIT", "repository": { "type": "git", diff --git a/core-build/gulp-core-build-typescript/src/index.ts b/core-build/gulp-core-build-typescript/src/index.ts index 536c7d73615..e1b01d8d3e3 100644 --- a/core-build/gulp-core-build-typescript/src/index.ts +++ b/core-build/gulp-core-build-typescript/src/index.ts @@ -18,6 +18,11 @@ export { ITslintCmdTaskConfig }; +/** @public */ export const tscCmd: TscCmdTask = new TscCmdTask(); + +/** @public */ export const tslintCmd: TslintCmdTask = new TslintCmdTask(); + +/** @public */ export const apiExtractor: ApiExtractorTask = new ApiExtractorTask(); diff --git a/core-build/gulp-core-build-webpack/config/api-extractor.json b/core-build/gulp-core-build-webpack/config/api-extractor.json index 5b65c425f79..d3ffdc526cd 100644 --- a/core-build/gulp-core-build-webpack/config/api-extractor.json +++ b/core-build/gulp-core-build-webpack/config/api-extractor.json @@ -1,5 +1,5 @@ { - "enabled": true, + "enabled": false, "apiReviewFolder": "../../common/reviews/api", "apiJsonFolder": "./temp", "entry": "lib/index.d.ts" diff --git a/core-build/gulp-core-build-webpack/package.json b/core-build/gulp-core-build-webpack/package.json index e4fce72a2d2..d88985356ec 100644 --- a/core-build/gulp-core-build-webpack/package.json +++ b/core-build/gulp-core-build-webpack/package.json @@ -4,6 +4,9 @@ "description": "", "main": "lib/index.js", "typings": "lib/index.d.ts", + "tsdoc": { + "tsdocFlavor": "AEDoc" + }, "license": "MIT", "repository": { "type": "git", diff --git a/core-build/gulp-core-build/package.json b/core-build/gulp-core-build/package.json index c03323204e8..2e88e6c754a 100644 --- a/core-build/gulp-core-build/package.json +++ b/core-build/gulp-core-build/package.json @@ -11,6 +11,9 @@ }, "main": "lib/index.js", "typings": "lib/index.d.ts", + "tsdoc": { + "tsdocFlavor": "AEDoc" + }, "license": "MIT", "dependencies": { "@microsoft/node-core-library": "3.7.0", diff --git a/core-build/gulp-core-build/src/index.ts b/core-build/gulp-core-build/src/index.ts index 6ddb58aef13..369bc1eeeba 100644 --- a/core-build/gulp-core-build/src/index.ts +++ b/core-build/gulp-core-build/src/index.ts @@ -463,8 +463,10 @@ function _handleTasksListArguments(): void { /** @public */ export const clean: IExecutable = new CleanTask(); +/** @public */ export const copyStaticAssets: CopyStaticAssetsTask = new CopyStaticAssetsTask(); +/** @public */ export const jest: JestTask = new JestTask(); // Register default clean task. diff --git a/core-build/node-library-build/config/api-extractor.json b/core-build/node-library-build/config/api-extractor.json index 5b65c425f79..d3ffdc526cd 100644 --- a/core-build/node-library-build/config/api-extractor.json +++ b/core-build/node-library-build/config/api-extractor.json @@ -1,5 +1,5 @@ { - "enabled": true, + "enabled": false, "apiReviewFolder": "../../common/reviews/api", "apiJsonFolder": "./temp", "entry": "lib/index.d.ts" diff --git a/core-build/node-library-build/package.json b/core-build/node-library-build/package.json index d09a569eb37..d47397adf93 100644 --- a/core-build/node-library-build/package.json +++ b/core-build/node-library-build/package.json @@ -4,6 +4,9 @@ "description": "", "main": "lib/index.js", "typings": "lib/index.d.ts", + "tsdoc": { + "tsdocFlavor": "AEDoc" + }, "license": "MIT", "repository": { "type": "git", diff --git a/core-build/web-library-build/config/api-extractor.json b/core-build/web-library-build/config/api-extractor.json index 5b65c425f79..d3ffdc526cd 100644 --- a/core-build/web-library-build/config/api-extractor.json +++ b/core-build/web-library-build/config/api-extractor.json @@ -1,5 +1,5 @@ { - "enabled": true, + "enabled": false, "apiReviewFolder": "../../common/reviews/api", "apiJsonFolder": "./temp", "entry": "lib/index.d.ts" diff --git a/core-build/web-library-build/package.json b/core-build/web-library-build/package.json index df8b5d17216..cc5038b03a5 100644 --- a/core-build/web-library-build/package.json +++ b/core-build/web-library-build/package.json @@ -14,6 +14,9 @@ }, "main": "lib/index.js", "typings": "lib/index.d.ts", + "tsdoc": { + "tsdocFlavor": "AEDoc" + }, "dependencies": { "@microsoft/gulp-core-build": "3.8.50", "@microsoft/gulp-core-build-karma": "4.6.48", diff --git a/libraries/node-core-library/src/MapExtensions.ts b/libraries/node-core-library/src/MapExtensions.ts index 7aad23ef590..b84ba96a960 100644 --- a/libraries/node-core-library/src/MapExtensions.ts +++ b/libraries/node-core-library/src/MapExtensions.ts @@ -14,7 +14,7 @@ export class MapExtensions { * @param targetMap - The map that entries will be added to * @param sourceMap - The map containing the entries to be added */ - public static mergeFromMap(targetMap: Map, sourceMap: Map): void { + public static mergeFromMap(targetMap: Map, sourceMap: ReadonlyMap): void { for (const pair of sourceMap.entries()) { targetMap.set(pair[0], pair[1]); } diff --git a/libraries/node-core-library/src/Sort.ts b/libraries/node-core-library/src/Sort.ts index 81b360dfbf7..ab6685958cf 100644 --- a/libraries/node-core-library/src/Sort.ts +++ b/libraries/node-core-library/src/Sort.ts @@ -10,6 +10,11 @@ export class Sort { /** * Compares `x` and `y` using the JavaScript `>` and `<` operators. This function is suitable for usage as * the callback for `array.sort()`. + * + * @remarks + * + * The JavaScript ordering is generalized so that `undefined` \< `null` \< all other values. + * * @returns -1 if `x` is smaller than `y`, 1 if `x` is greater than `y`, or 0 if the values are equal. * * @example @@ -24,18 +29,32 @@ export class Sort { if (x === y) { return 0; } + + // Undefined is smaller than anything else if (x === undefined) { return -1; } - if (x === null) { + if (y === undefined) { + return 1; + } + + // Null is smaller than anything except undefined + if (x === null) { // tslint:disable-line:no-null-keyword return -1; } + if (y === null) { // tslint:disable-line:no-null-keyword + return 1; + } + + // These comparisons always return false if either of the arguments is "undefined". + // These comparisons return nonsense for "null" (true for "null > -1", but false for "null < 0" and "null > 0") if (x < y) { return -1; } if (x > y) { return 1; } + return 0; } diff --git a/libraries/node-core-library/src/StringBuilder.ts b/libraries/node-core-library/src/StringBuilder.ts index 6b54ea0f203..0a03b37ba77 100644 --- a/libraries/node-core-library/src/StringBuilder.ts +++ b/libraries/node-core-library/src/StringBuilder.ts @@ -2,30 +2,68 @@ // See LICENSE in the project root for license information. /** - * Allows a string to be built by appending parts. + * An interface for a builder object that allows a large text string to be constructed incrementally by appending + * small chunks. * - * @beta + * @remarks + * + * {@link StringBuilder} is the default implementation of this contract. + * + * @public + */ +export interface IStringBuilder { + /** + * Append the specified text to the buffer. + */ + append(text: string): void; + + /** + * Returns a single string containing all the text that was appended to the buffer so far. + * + * @remarks + * + * This is a potentially expensive operation. + */ + toString(): string; +} + +/** + * This class allows a large text string to be constructed incrementally by appending small chunks. The final + * string can be obtained by calling StringBuilder.toString(). + * + * @remarks + * A naive approach might use the `+=` operator to append strings: This would have the downside of copying + * the entire string each time a chunk is appended, resulting in `O(n^2)` bytes of memory being allocated + * (and later freed by the garbage collector), and many of the allocations could be very large objects. + * StringBuilder avoids this overhead by accumulating the chunks in an array, and efficiently joining them + * when `getText()` is finally called. + * + * @public */ -export class StringBuilder { +export class StringBuilder implements IStringBuilder { private _chunks: string[]; - public constructor() { + constructor() { this._chunks = []; } - /** - * Appends a chunk to the string. - */ + /** {@inheritdoc IStringBuilder.append} */ public append(text: string): void { this._chunks.push(text); } - /** - * Collapses all of the appended chunks and returns the joined string. - */ + /** {@inheritdoc IStringBuilder.toString} */ public toString(): string { - const joined: string = this._chunks.join(''); - this._chunks = [joined]; - return joined; + if (this._chunks.length === 0) { + return ''; + } + + if (this._chunks.length > 1) { + const joined: string = this._chunks.join(''); + this._chunks.length = 1; + this._chunks[0] = joined; + } + + return this._chunks[0]; } -} \ No newline at end of file +} diff --git a/libraries/node-core-library/src/index.ts b/libraries/node-core-library/src/index.ts index 0f487cd1d13..1fb6dbb8400 100644 --- a/libraries/node-core-library/src/index.ts +++ b/libraries/node-core-library/src/index.ts @@ -74,7 +74,7 @@ export { LegacyAdapters, callback } from './LegacyAdapters'; -export { StringBuilder } from './StringBuilder'; +export { StringBuilder, IStringBuilder } from './StringBuilder'; export { Terminal } from './Terminal/Terminal'; export { Colors, diff --git a/libraries/node-core-library/src/test/Sort.test.ts b/libraries/node-core-library/src/test/Sort.test.ts new file mode 100644 index 00000000000..bf0b1f24433 --- /dev/null +++ b/libraries/node-core-library/src/test/Sort.test.ts @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { Sort } from '../Sort'; + +test('Sort.compareByValue', () => { + const array: number[] = [3, 6, 2]; + array.sort(Sort.compareByValue); // [2, 3, 6] +}); + +test('Sort.compareByValue cases', () => { + const values: unknown[] = [undefined, null, -1, 1]; // tslint:disable-line:no-null-keyword + const results: string[] = []; + for (let i: number = 0; i < values.length; ++i) { + for (let j: number = 0; j < values.length; ++j) { + const x: unknown = values[i]; + const y: unknown = values[j]; + let relation: string = '?'; + switch (Sort.compareByValue(x, y)) { + case -1: relation = '<'; break; + case 0: relation = '='; break; + case 1: relation = '>'; break; + } + results.push(`${x} ${relation} ${y}`); + } + } + expect(results).toMatchSnapshot(); +}); + +test('Sort.sortBy', () => { + const array: string[] = [ 'aaa', 'bb', 'c' ]; + Sort.sortBy(array, x => x.length); // [ 'c', 'bb', 'aaa' ] +}); + +test('Sort.isSortedBy', () => { + const array: string[] = [ 'a', 'bb', 'ccc' ]; + Sort.isSortedBy(array, x => x.length); // true +}); + +test('Sort.sortMapKeys', () => { + const map: Map = new Map(); + map.set('zebra', 1); + map.set('goose', 2); + map.set('aardvark', 3); + Sort.sortMapKeys(map); + expect(Array.from(map.keys())).toEqual(['aardvark', 'goose', 'zebra']); +}); + +test('Sort.sortSetBy', () => { + const set: Set = new Set(); + set.add('aaa'); + set.add('bb'); + set.add('c'); + Sort.sortSetBy(set, x => x.length); + expect(Array.from(set)).toEqual(['c', 'bb', 'aaa']); +}); + +test('Sort.sortSet', () => { + const set: Set = new Set(); + set.add('zebra'); + set.add('goose'); + set.add('aardvark'); + Sort.sortSet(set); + expect(Array.from(set)).toEqual(['aardvark', 'goose', 'zebra']); +}); diff --git a/libraries/node-core-library/src/test/__snapshots__/Sort.test.ts.snap b/libraries/node-core-library/src/test/__snapshots__/Sort.test.ts.snap new file mode 100644 index 00000000000..fcacb7be2f6 --- /dev/null +++ b/libraries/node-core-library/src/test/__snapshots__/Sort.test.ts.snap @@ -0,0 +1,22 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Sort.compareByValue cases 1`] = ` +Array [ + "undefined = undefined", + "undefined < null", + "undefined < -1", + "undefined < 1", + "null > undefined", + "null = null", + "null < -1", + "null < 1", + "-1 > undefined", + "-1 > null", + "-1 = -1", + "-1 < 1", + "1 > undefined", + "1 > null", + "1 > -1", + "1 = 1", +] +`; diff --git a/rush.json b/rush.json index 9de76c88aef..0c2caeb684c 100644 --- a/rush.json +++ b/rush.json @@ -423,6 +423,7 @@ "reviewCategory": "libraries", "shouldPublish": true, "cyclicDependencyProjects": [ + "@microsoft/api-extractor", "@microsoft/node-library-build", "@microsoft/rush-stack-compiler-3.0" ] @@ -511,6 +512,7 @@ "reviewCategory": "libraries", "shouldPublish": true, "cyclicDependencyProjects": [ + "@microsoft/api-extractor", "@microsoft/node-library-build", "@microsoft/rush-stack-compiler-3.0" ] @@ -521,6 +523,7 @@ "reviewCategory": "libraries", "shouldPublish": true, "cyclicDependencyProjects": [ + "@microsoft/api-extractor", "@microsoft/node-library-build", "@microsoft/rush-stack-compiler-3.0" ] @@ -531,6 +534,7 @@ "reviewCategory": "libraries", "shouldPublish": true, "cyclicDependencyProjects": [ + "@microsoft/api-extractor", "@microsoft/node-library-build", "@microsoft/rush-stack-compiler-3.0" ] @@ -541,6 +545,7 @@ "reviewCategory": "libraries", "shouldPublish": true, "cyclicDependencyProjects": [ + "@microsoft/api-extractor", "@microsoft/node-library-build", "@microsoft/rush-stack-compiler-3.0" ] @@ -551,6 +556,7 @@ "reviewCategory": "libraries", "shouldPublish": true, "cyclicDependencyProjects": [ + "@microsoft/api-extractor", "@microsoft/node-library-build", "@microsoft/rush-stack-compiler-3.0" ] diff --git a/webpack/loader-load-themed-styles/package.json b/webpack/loader-load-themed-styles/package.json index fb38a562b98..f14704e605d 100644 --- a/webpack/loader-load-themed-styles/package.json +++ b/webpack/loader-load-themed-styles/package.json @@ -4,6 +4,9 @@ "description": "This simple loader wraps the loading of CSS in script equivalent to `require('load-themed-styles').loadStyles( /* css text */ )`. It is designed to be a replacement for style-loader.", "main": "lib/index.js", "typings": "lib/index.d.ts", + "tsdoc": { + "tsdocFlavor": "AEDoc" + }, "license": "MIT", "repository": { "type": "git", diff --git a/webpack/resolve-chunk-plugin/package.json b/webpack/resolve-chunk-plugin/package.json index b8f1ae8af4c..5f15815b119 100644 --- a/webpack/resolve-chunk-plugin/package.json +++ b/webpack/resolve-chunk-plugin/package.json @@ -4,6 +4,9 @@ "description": "This is a webpack plugin that looks for calls to \"resolveChunk\" with a chunk name, and returns the chunk ID.", "main": "lib/index.js", "typings": "lib/index.d.ts", + "tsdoc": { + "tsdocFlavor": "AEDoc" + }, "license": "MIT", "repository": { "type": "git", diff --git a/webpack/set-webpack-public-path-plugin/package.json b/webpack/set-webpack-public-path-plugin/package.json index 564a1377daa..1c917a71007 100644 --- a/webpack/set-webpack-public-path-plugin/package.json +++ b/webpack/set-webpack-public-path-plugin/package.json @@ -4,6 +4,9 @@ "description": "This plugin sets the webpack public path at runtime.", "main": "lib/index.js", "typings": "lib/index.d.ts", + "tsdoc": { + "tsdocFlavor": "AEDoc" + }, "license": "MIT", "repository": { "type": "git", diff --git a/webpack/set-webpack-public-path-plugin/src/codeGenerator.ts b/webpack/set-webpack-public-path-plugin/src/codeGenerator.ts index 3a9f26a3567..06aa66fb437 100644 --- a/webpack/set-webpack-public-path-plugin/src/codeGenerator.ts +++ b/webpack/set-webpack-public-path-plugin/src/codeGenerator.ts @@ -8,6 +8,9 @@ import { ISetWebpackPublicPathOptions } from './SetPublicPathPlugin'; +/** + * @public + */ export const registryVariableName: string = 'window.__setWebpackPublicPathLoaderSrcRegistry__'; export interface IInternalOptions extends ISetWebpackPublicPathOptions {