tsdoc switch

This commit is contained in:
Max Hoffmann 2023-09-27 17:54:03 -07:00
parent a8bfa03cf7
commit 276174bd17
No known key found for this signature in database
GPG Key ID: 3015B3271A63BCAE
11 changed files with 652 additions and 1106 deletions

View File

@ -13,7 +13,9 @@ module.exports = {
},
rules: {
'jest/valid-title': 'off',
'tsdoc/syntax': 'warn',
},
plugins: ['eslint-plugin-tsdoc'],
settings: {
'import/resolver': {
node: {
@ -24,4 +26,24 @@ module.exports = {
},
},
},
overrides: [
{
files: ['src/**/*.js', 'src/**/*.ts'],
excludedFiles: ['src/**/*.test.js', 'src/**/*.test.ts'],
rules: {
'require-jsdoc': [
'error',
{
require: {
FunctionDeclaration: false,
MethodDefinition: true,
ClassDeclaration: true,
ArrowFunctionExpression: true,
FunctionExpression: true,
},
},
],
},
},
],
};

View File

@ -1,8 +0,0 @@
{
"source": "./src",
"destination": "./docs",
"plugins": [
{"name": "esdoc-standard-plugin"},
{"name": "esdoc-ecmascript-proposal-plugin", "option": {"all": true}}
]
}

View File

@ -34,7 +34,7 @@
"release": "yarn run build:production && changeset publish",
"lint": "eslint ./src ./test --max-warnings 0",
"type-check": "tsc -b",
"esdoc": "esdoc -c esdoc.json",
"docs": "typedoc",
"test": "jest",
"build:development": "yarn rollup --config rollup.development.config.ts --configPlugin @rollup/plugin-typescript",
"build:production": "tsc && tsc-alias && yarn rollup --config rollup.config.ts --configPlugin @rollup/plugin-typescript && yarn uglifyjs --compress --mangle -- build/umd/index.js -o build/umd/index.min.js",
@ -53,6 +53,8 @@
"@babel/preset-typescript": "^7.23.0",
"@changesets/changelog-github": "^0.4.8",
"@changesets/cli": "^2.26.2",
"@microsoft/tsdoc": "^0.14.2",
"@microsoft/tsdoc-config": "^0.16.2",
"@rollup/plugin-babel": "^6.0.3",
"@rollup/plugin-commonjs": "^25.0.4",
"@rollup/plugin-node-resolve": "^15.2.1",
@ -63,10 +65,8 @@
"@types/jest": "^29.5.5",
"@types/node": "^20.6.3",
"concurrently": "^3.5.1",
"esdoc": "^1.1.0",
"esdoc-ecmascript-proposal-plugin": "^1.0.0",
"esdoc-standard-plugin": "^1.0.0",
"eslint": "^8.49.0",
"eslint-plugin-tsdoc": "^0.2.17",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"prettier": "^3.0.3",
@ -76,6 +76,8 @@
"rollup-plugin-node-externals": "^6.1.1",
"timers": "^0.1.1",
"tsc-alias": "^1.8.8",
"typedoc": "^0.25.1",
"typedoc-plugin-markdown": "^3.16.0",
"typescript": "^5.2.2",
"uglify-js": "^3.17.4"
},

31
scripts/generate-doc.ts Normal file
View File

@ -0,0 +1,31 @@
import * as path from 'path';
import fs from 'fs';
import {TSDocParser, TSDocConfiguration, ParserContext} from '@microsoft/tsdoc';
import {TSDocConfigFile} from '@microsoft/tsdoc-config';
const mySourceFile = './src/shared/AbstractEvent/AbstractEvent.ts';
const tsdocConfigFile: TSDocConfigFile = TSDocConfigFile.loadForFolder(
path.dirname(mySourceFile),
);
if (tsdocConfigFile.hasErrors) {
// Report any errors
console.log(tsdocConfigFile.getErrorSummary());
}
const tsdocConfiguration: TSDocConfiguration = new TSDocConfiguration();
tsdocConfigFile.configureParser(tsdocConfiguration);
const tsdocParser: TSDocParser = new TSDocParser(tsdocConfiguration);
// console.log(tsdocParser.configuration);
const parserContext: ParserContext = tsdocParser.parseString(
fs.readFileSync(mySourceFile, {encoding: 'utf8'}),
);
if (parserContext.log.messages.length > 0) {
throw new Error(`Syntax error: ${parserContext.log.messages[0].text}`);
}
console.log(parserContext.docComment.params.blocks);

9
scripts/generate.ts Normal file
View File

@ -0,0 +1,9 @@
import doc from '../src/shared/AbstractEvent/AbstractEvent.doc.json';
console.log(doc);
interface API {
title: string;
}
doc.children.forEach((child) => {});

329
scripts/test.ts Normal file
View File

@ -0,0 +1,329 @@
import * as os from 'os';
import * as path from 'path';
import colors from 'colors';
import * as ts from 'typescript';
import * as tsdoc from '@microsoft/tsdoc';
/* eslint-disable no-console */
/**
* Returns true if the specified SyntaxKind is part of a declaration form.
*
* Based on ts.isDeclarationKind() from the compiler.
* https://github.com/microsoft/TypeScript/blob/v3.0.3/src/compiler/utilities.ts#L6382
*/
function isDeclarationKind(kind: ts.SyntaxKind): boolean {
return (
kind === ts.SyntaxKind.ArrowFunction ||
kind === ts.SyntaxKind.BindingElement ||
kind === ts.SyntaxKind.ClassDeclaration ||
kind === ts.SyntaxKind.ClassExpression ||
kind === ts.SyntaxKind.Constructor ||
kind === ts.SyntaxKind.EnumDeclaration ||
kind === ts.SyntaxKind.EnumMember ||
kind === ts.SyntaxKind.ExportSpecifier ||
kind === ts.SyntaxKind.FunctionDeclaration ||
kind === ts.SyntaxKind.FunctionExpression ||
kind === ts.SyntaxKind.GetAccessor ||
kind === ts.SyntaxKind.ImportClause ||
kind === ts.SyntaxKind.ImportEqualsDeclaration ||
kind === ts.SyntaxKind.ImportSpecifier ||
kind === ts.SyntaxKind.InterfaceDeclaration ||
kind === ts.SyntaxKind.JsxAttribute ||
kind === ts.SyntaxKind.MethodDeclaration ||
kind === ts.SyntaxKind.MethodSignature ||
kind === ts.SyntaxKind.ModuleDeclaration ||
kind === ts.SyntaxKind.NamespaceExportDeclaration ||
kind === ts.SyntaxKind.NamespaceImport ||
kind === ts.SyntaxKind.Parameter ||
kind === ts.SyntaxKind.PropertyAssignment ||
kind === ts.SyntaxKind.PropertyDeclaration ||
kind === ts.SyntaxKind.PropertySignature ||
kind === ts.SyntaxKind.SetAccessor ||
kind === ts.SyntaxKind.ShorthandPropertyAssignment ||
kind === ts.SyntaxKind.TypeAliasDeclaration ||
kind === ts.SyntaxKind.TypeParameter ||
kind === ts.SyntaxKind.VariableDeclaration ||
kind === ts.SyntaxKind.JSDocTypedefTag ||
kind === ts.SyntaxKind.JSDocCallbackTag ||
kind === ts.SyntaxKind.JSDocPropertyTag
);
}
/**
* Retrieves the JSDoc-style comments associated with a specific AST node.
*
* Based on ts.getJSDocCommentRanges() from the compiler.
* https://github.com/microsoft/TypeScript/blob/v3.0.3/src/compiler/utilities.ts#L924
*/
function getJSDocCommentRanges(node: ts.Node, text: string): ts.CommentRange[] {
const commentRanges: ts.CommentRange[] = [];
switch (node.kind) {
case ts.SyntaxKind.Parameter:
case ts.SyntaxKind.TypeParameter:
case ts.SyntaxKind.FunctionExpression:
case ts.SyntaxKind.ArrowFunction:
case ts.SyntaxKind.ParenthesizedExpression:
commentRanges.push(
...(ts.getTrailingCommentRanges(text, node.pos) || []),
);
break;
}
commentRanges.push(...(ts.getLeadingCommentRanges(text, node.pos) || []));
// True if the comment starts with '/**' but not if it is '/**/'
return commentRanges.filter(
(comment) =>
text.charCodeAt(comment.pos + 1) ===
0x2a /* ts.CharacterCodes.asterisk */ &&
text.charCodeAt(comment.pos + 2) ===
0x2a /* ts.CharacterCodes.asterisk */ &&
text.charCodeAt(comment.pos + 3) !== 0x2f /* ts.CharacterCodes.slash */,
);
}
interface FoundComment {
compilerNode: ts.Node;
textRange: tsdoc.TextRange;
}
function walkCompilerAstAndFindComments(
node: ts.Node,
indent: string,
foundComments: FoundComment[],
): void {
// The TypeScript AST doesn't store code comments directly. If you want to find *every* comment,
// you would need to rescan the SourceFile tokens similar to how tsutils.forEachComment() works:
// https://github.com/ajafff/tsutils/blob/v3.0.0/util/util.ts#L453
//
// However, for this demo we are modeling a tool that discovers declarations and then analyzes their doc comments,
// so we only care about TSDoc that would conventionally be associated with an interesting AST node.
let foundCommentsSuffix = '';
const buffer: string = node.getSourceFile().getFullText();
// Only consider nodes that are part of a declaration form. Without this, we could discover
// the same comment twice (e.g. for a MethodDeclaration and its PublicKeyword).
if (isDeclarationKind(node.kind)) {
// Find "/** */" style comments associated with this node.
// Note that this reinvokes the compiler's scanner -- the result is not cached.
const comments: ts.CommentRange[] = getJSDocCommentRanges(node, buffer);
if (comments.length > 0) {
if (comments.length === 1) {
foundCommentsSuffix = colors.cyan(` (FOUND 1 COMMENT)`);
} else {
foundCommentsSuffix = colors.cyan(
` (FOUND ${comments.length} COMMENTS)`,
);
}
for (const comment of comments) {
foundComments.push({
compilerNode: node,
textRange: tsdoc.TextRange.fromStringRange(
buffer,
comment.pos,
comment.end,
),
});
}
}
}
console.log(`${indent}- ${ts.SyntaxKind[node.kind]}${foundCommentsSuffix}`);
return node.forEachChild((child) =>
walkCompilerAstAndFindComments(child, `${indent} `, foundComments),
);
}
function dumpTSDocTree(docNode: tsdoc.DocNode, indent: string): void {
let dumpText = '';
if (docNode instanceof tsdoc.DocExcerpt) {
const content: string = docNode.content.toString();
dumpText +=
colors.gray(`${indent}* ${docNode.excerptKind}=`) +
colors.cyan(JSON.stringify(content));
} else {
dumpText += `${indent}- ${docNode.kind}`;
}
console.log(dumpText);
for (const child of docNode.getChildNodes()) {
dumpTSDocTree(child, `${indent} `);
}
}
function parseTSDoc(foundComment: FoundComment): void {
console.log(os.EOL + colors.green('Comment to be parsed:') + os.EOL);
console.log(colors.gray('<<<<<<'));
console.log(foundComment.textRange.toString());
console.log(colors.gray('>>>>>>'));
const customConfiguration: tsdoc.TSDocConfiguration =
new tsdoc.TSDocConfiguration();
// const customInlineDefinition: tsdoc.TSDocTagDefinition =
// new tsdoc.TSDocTagDefinition({
// tagName: '@class',
// syntaxKind: tsdoc.TSDocTagSyntaxKind.InlineTag,
// allowMultiple: true,
// });
// NOTE: Defining this causes a new DocBlock to be created under docComment.customBlocks.
// Otherwise, a simple DocBlockTag would appear inline in the @remarks section.
const customBlockDefinition: tsdoc.TSDocTagDefinition =
new tsdoc.TSDocTagDefinition({
tagName: '@customBlock',
syntaxKind: tsdoc.TSDocTagSyntaxKind.BlockTag,
});
// NOTE: Defining this causes @customModifier to be removed from its section,
// and added to the docComment.modifierTagSet
const classModifierDefinition: tsdoc.TSDocTagDefinition =
new tsdoc.TSDocTagDefinition({
tagName: '@class',
syntaxKind: tsdoc.TSDocTagSyntaxKind.ModifierTag,
});
customConfiguration.addTagDefinitions([
// customInlineDefinition,
customBlockDefinition,
classModifierDefinition,
]);
console.log(
`${os.EOL}Invoking TSDocParser with custom configuration...${os.EOL}`,
);
const tsdocParser: tsdoc.TSDocParser = new tsdoc.TSDocParser(
customConfiguration,
);
const parserContext: tsdoc.ParserContext = tsdocParser.parseRange(
foundComment.textRange,
);
const docComment: tsdoc.DocComment = parserContext.docComment;
console.log(os.EOL + colors.green('Parser Log Messages:') + os.EOL);
if (parserContext.log.messages.length === 0) {
console.log('No errors or warnings.');
} else {
const sourceFile: ts.SourceFile = foundComment.compilerNode.getSourceFile();
for (const message of parserContext.log.messages) {
// Since we have the compiler's analysis, use it to calculate the line/column information,
// since this is currently faster than TSDoc's TextRange.getLocation() lookup.
const location: ts.LineAndCharacter =
sourceFile.getLineAndCharacterOfPosition(message.textRange.pos);
const formattedMessage = `${sourceFile.fileName}(${location.line + 1},${
location.character + 1
}): [TSDoc] ${message}`;
console.log(formattedMessage);
}
}
if (parserContext.docComment.modifierTagSet.hasTag(classModifierDefinition)) {
console.log(
os.EOL +
colors.cyan(
`The ${classModifierDefinition.tagName} modifier was FOUND.`,
),
);
} else {
console.log(
os.EOL +
colors.cyan(
`The ${classModifierDefinition.tagName} modifier was NOT FOUND.`,
),
);
}
console.log(os.EOL + colors.green("Visiting TSDoc's DocNode tree") + os.EOL);
dumpTSDocTree(docComment, '');
}
/**
* The advanced demo invokes the TypeScript compiler and extracts the comment from the AST.
* It also illustrates how to define custom TSDoc tags using TSDocConfiguration.
*/
export function advancedDemo(): void {
console.log(
colors.yellow('*** TSDoc API demo: Advanced Scenario ***') + os.EOL,
);
const inputFilename: string = path.resolve(
path.join(
__dirname,
'..',
'src',
'shared',
'AbstractEvent',
'AbstractEvent.ts',
),
);
const compilerOptions: ts.CompilerOptions = {
target: ts.ScriptTarget.ES5,
};
// Compile the input
console.log('Invoking the TypeScript compiler to analyze typescript files');
const program: ts.Program = ts.createProgram(
[inputFilename],
compilerOptions,
);
// Report any compiler errors
const compilerDiagnostics: ReadonlyArray<ts.Diagnostic> =
program.getSemanticDiagnostics();
if (compilerDiagnostics.length > 0) {
for (const diagnostic of compilerDiagnostics) {
const message: string = ts.flattenDiagnosticMessageText(
diagnostic.messageText,
os.EOL,
);
if (diagnostic.file) {
const location: ts.LineAndCharacter =
diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start!);
const formattedMessage = `${diagnostic.file.fileName}(${
location.line + 1
},${location.character + 1}): [TypeScript] ${message}`;
console.log(colors.red(formattedMessage));
} else {
console.log(colors.red(message));
}
}
} else {
console.log('No compiler errors or warnings.');
}
const sourceFile: ts.SourceFile | undefined =
program.getSourceFile(inputFilename);
if (!sourceFile) {
throw new Error('Error retrieving source file');
}
console.log(
os.EOL +
colors.green('Scanning compiler AST for first code comment...') +
os.EOL,
);
const foundComments: FoundComment[] = [];
walkCompilerAstAndFindComments(sourceFile, '', foundComments);
if (foundComments.length === 0) {
console.log(
colors.red('Error: No code comments were found in the input file'),
);
} else {
foundComments.forEach(parseTSDoc);
}
}
advancedDemo();
/* eslint-enable no-console */

12
scripts/tsconfig.json Normal file
View File

@ -0,0 +1,12 @@
{
"extends": "@shopify/typescript-configs/base.json",
"compilerOptions": {
"baseUrl": ".",
"rootDir": "../",
"module": "commonjs",
"target": "es2022",
"esModuleInterop": true,
"useUnknownInCatchVariables": false
},
"include": ["./**/*"]
}

View File

@ -1,83 +1,128 @@
export class EventNotCancelable extends Error {
constructor(className: string) {
super(`${className} cannot be canceled`);
}
}
export function testMe() {}
export interface Hmm {}
export enum Enum {}
export type AnotherTest = any;
export const test = {};
/**
* All events fired by draggable inherit this class. You can call `cancel()` to
* cancel a specific event or you can check if an event has been canceled by
* calling `canceled()`.
* @abstract
* @class AbstractEvent
* @module AbstractEvent
*
* @typeParam T - Describes the event data used in `this.data`
*
* @example **Creating a new custom event for Draggable**
* ```ts
* import {AbstractEvent} from '@shopify/draggable';
*
* interface CustomEventData {}
*
* class MyCustomEvent extends AbstractEvent<CustomEventData> {
* static type = 'custom:event';
* }
* ```
*
* @example **Triggering event via Draggable**
* ```ts
* import {Draggable, AbstractEvent} from '@shopify/draggable';
*
* interface CustomEventData {}
*
* class MyCustomEvent extends AbstractEvent<CustomEventData> {
* static type = 'custom:event';
* }
*
* const draggable = new Draggable({draggable: 'li'});
*
* draggable.on('custom:event', (event) => {
* console.log(event);
* })
*
* draggable.trigger(new MyCustomEvent({}));
* ```
*/
export class AbstractEvent<T> {
/**
* Event type
* @static
* @abstract
* @property type
* @type {String}
* Event type name which is used for triggering and listening to events
* Override this so you can
* @virtual
*/
static type = 'event';
/**
* Event cancelable
* @static
* @abstract
* @property cancelable
* @type {Boolean}
* @virtual
*/
static cancelable = false;
/**
* Private instance variable to track canceled state
* @private
* @type {Boolean}
* @internal
*/
private _canceled = false;
#canceled = false;
/**
* AbstractEvent constructor.
* @constructs AbstractEvent
* @param {T} data - Event data
* Creates an `AbstractEvent` instance.
* @public
* @sealed
* @param data - Event data for this even instance
* @typeParam T - Describes the event data used in `this.data`
*/
constructor(public data: T) {}
/**
* Read-only type
* @abstract
* @return {String}
* Read-only property to find out event type
* @readonly
* @sealed
*/
get type() {
return (this.constructor as typeof AbstractEvent).type;
}
/**
* Read-only cancelable
* @abstract
* @return {Boolean}
* Read-only property to check if event is cancelable
* @readonly
* @sealed
*/
get cancelable() {
return (this.constructor as typeof AbstractEvent).cancelable;
}
/**
* Cancels the event instance
* @abstract
* Marks the event instance as canceled
* @throws {@link EventNotCancelable}
* This exception is thrown if the event is not cancelable
*/
cancel() {
this._canceled = true;
if (!this.cancelable) {
throw new EventNotCancelable(this.constructor.name);
}
this.#canceled = true;
}
/**
* Check if event has been canceled
* @abstract
* @return {Boolean}
* Checks if event has been canceled
* @returns True if `cancel()` has been called
*/
canceled() {
return this._canceled;
return this.#canceled;
}
/**
* Returns new event instance with existing event data.
* This method allows for overriding of event data.
* @param {T} data
* @return {AbstractEvent}
* @param T -
* @returns A new event instance
*/
clone(data: T) {
return new (this.constructor as typeof AbstractEvent)({
@ -86,3 +131,9 @@ export class AbstractEvent<T> {
});
}
}
const event = new AbstractEvent<{[key: string]: any}>({});
event.clone({
tst: 'a',
});

10
tsdoc.json Normal file
View File

@ -0,0 +1,10 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/tsdoc/v0/tsdoc.schema.json",
"extends": ["typedoc/tsdoc.json"],
"tagDefinitions": [
{
"tagName": "@cancelable",
"syntaxKind": "modifier"
}
]
}

10
typedoc.json Normal file
View File

@ -0,0 +1,10 @@
{
"$schema": "https://typedoc.org/schema.json",
"entryPoints": ["./src/**/*.ts"],
"out": "./docs",
"exclude": ["./src/**/index.ts", "./src/**/*.test.ts"],
"includeVersion": true,
"githubPages": false,
"gitRevision": "main",
"plugin": ["typedoc-plugin-markdown"]
}

1200
yarn.lock

File diff suppressed because it is too large Load Diff