Skip to content

Commit 2285ec3

Browse files
committed
feat: Support awsuiSystem jsdoc tags
1 parent 14530c1 commit 2285ec3

File tree

11 files changed

+256
-55
lines changed

11 files changed

+256
-55
lines changed
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
import * as React from 'react';
4+
import { ButtonProps } from './interfaces';
5+
6+
export { ButtonProps };
7+
8+
export default function Button(props: ButtonProps) {
9+
return <div />;
10+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
export interface ButtonProps {
4+
variant?: ButtonProps.Variant;
5+
6+
/**
7+
* @awsuiSystem core
8+
*/
9+
size: 'small' | 'medium' | 'large';
10+
11+
color?:
12+
| 'normal'
13+
/** @awsuiSystem core */
14+
| 'danger';
15+
}
16+
17+
export namespace ButtonProps {
18+
export type Variant =
19+
| 'primary'
20+
| 'secondary'
21+
/** @awsuiSystem core */
22+
| 'fire'
23+
/**
24+
* @awsuiSystem core
25+
* @awsuiSystem experimental
26+
*/
27+
| 'ultra';
28+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
import * as React from 'react';
4+
5+
// eslint-disable-next-line @typescript-eslint/no-empty-interface
6+
export interface TreeProps {}
7+
8+
/**
9+
* @awsuiSystem core
10+
*/
11+
export default function Tree() {
12+
return <div />;
13+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"extends": "../tsconfig.json",
3+
"compilerOptions": {
4+
"rootDir": "."
5+
},
6+
"include": ["./**/*.tsx"]
7+
}

src/components/component-definition.ts

Lines changed: 38 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,26 @@ import type {
1010
ComponentRegion,
1111
EventHandler,
1212
} from './interfaces';
13-
import type { ExpandedProp } from './extractor';
13+
import type { ExpandedProp, ExtractedDescription } from './extractor';
1414
import { getObjectDefinition } from './object-definition';
1515

16-
function getCommentTag(property: ExpandedProp, name: string) {
17-
const tag = property.description.tags.find(tag => tag.name === name);
16+
function getCommentTag(description: ExtractedDescription, name: string) {
17+
const tag = description.tags.find(tag => tag.name === name);
1818
return tag ? tag.text ?? '' : undefined;
1919
}
2020

21+
function getMultipleCommentTag(description: ExtractedDescription, name: string) {
22+
const tags = description.tags
23+
.filter(tag => tag.name === name)
24+
.map(tag => {
25+
if (!tag.text) {
26+
throw new Error(`Tag ${name} is missing text`);
27+
}
28+
return tag.text;
29+
});
30+
return tags.length > 0 ? tags : undefined;
31+
}
32+
2133
function castI18nTag(tag: string | undefined) {
2234
return tag === undefined ? undefined : true;
2335
}
@@ -27,6 +39,7 @@ export function buildComponentDefinition(
2739
props: Array<ExpandedProp>,
2840
functions: Array<ExpandedProp>,
2941
defaultValues: Record<string, string>,
42+
componentDescription: ExtractedDescription,
3043
checker: ts.TypeChecker
3144
): ComponentDefinition {
3245
const regions = props.filter(prop => prop.type === 'React.ReactNode');
@@ -36,15 +49,18 @@ export function buildComponentDefinition(
3649
return {
3750
name,
3851
releaseStatus: 'stable',
52+
description: componentDescription.text,
53+
systemTag: getMultipleCommentTag(componentDescription, 'awsuiSystem'),
3954
regions: regions.map(
4055
(region): ComponentRegion => ({
4156
name: region.name,
42-
displayName: getCommentTag(region, 'displayname'),
57+
displayName: getCommentTag(region.description, 'displayname'),
4358
description: region.description.text,
4459
isDefault: region.name === 'children',
45-
visualRefreshTag: getCommentTag(region, 'visualrefresh'),
46-
deprecatedTag: getCommentTag(region, 'deprecated'),
47-
i18nTag: castI18nTag(getCommentTag(region, 'i18n')),
60+
systemTag: getMultipleCommentTag(region.description, 'awsuiSystem'),
61+
visualRefreshTag: getCommentTag(region.description, 'visualrefresh'),
62+
deprecatedTag: getCommentTag(region.description, 'deprecated'),
63+
i18nTag: castI18nTag(getCommentTag(region.description, 'i18n')),
4864
})
4965
),
5066
functions: functions.map(
@@ -66,35 +82,41 @@ export function buildComponentDefinition(
6682
})
6783
),
6884
properties: onlyProps.map((property): ComponentProperty => {
69-
const { type, inlineType } = getObjectDefinition(property.type, property.rawType, checker);
85+
const { type, inlineType } = getObjectDefinition(property.type, property.rawType, property.rawTypeNode, checker);
7086
return {
7187
name: property.name,
7288
type: type,
7389
inlineType: inlineType,
7490
optional: property.isOptional,
7591
description: property.description.text,
7692
defaultValue: defaultValues[property.name],
77-
visualRefreshTag: getCommentTag(property, 'visualrefresh'),
78-
deprecatedTag: getCommentTag(property, 'deprecated'),
79-
analyticsTag: getCommentTag(property, 'analytics'),
80-
i18nTag: castI18nTag(getCommentTag(property, 'i18n')),
93+
systemTag: getMultipleCommentTag(property.description, 'awsuiSystem'),
94+
visualRefreshTag: getCommentTag(property.description, 'visualrefresh'),
95+
deprecatedTag: getCommentTag(property.description, 'deprecated'),
96+
analyticsTag: getCommentTag(property.description, 'analytics'),
97+
i18nTag: castI18nTag(getCommentTag(property.description, 'i18n')),
8198
};
8299
}),
83100
events: events.map((event): EventHandler => {
84-
const { detailType, detailInlineType, cancelable } = extractEventDetails(event.rawType, checker);
101+
const { detailType, detailInlineType, cancelable } = extractEventDetails(
102+
event.rawType,
103+
event.rawTypeNode,
104+
checker
105+
);
85106
return {
86107
name: event.name,
87108
description: event.description.text,
88109
cancelable,
89110
detailType,
90111
detailInlineType,
91-
deprecatedTag: getCommentTag(event, 'deprecated'),
112+
systemTag: getMultipleCommentTag(event.description, 'awsuiSystem'),
113+
deprecatedTag: getCommentTag(event.description, 'deprecated'),
92114
};
93115
}),
94116
};
95117
}
96118

97-
function extractEventDetails(type: ts.Type, checker: ts.TypeChecker) {
119+
function extractEventDetails(type: ts.Type, typeNode: ts.TypeNode | undefined, checker: ts.TypeChecker) {
98120
const realType = type.getNonNullableType();
99121
const handlerName = realType.aliasSymbol?.getName();
100122
if (handlerName !== 'CancelableEventHandler' && handlerName !== 'NonCancelableEventHandler') {
@@ -103,7 +125,7 @@ function extractEventDetails(type: ts.Type, checker: ts.TypeChecker) {
103125
const cancelable = handlerName === 'CancelableEventHandler';
104126
const detailType = realType.aliasTypeArguments?.[0];
105127
if (detailType && detailType.getProperties().length > 0) {
106-
const { type, inlineType } = getObjectDefinition(stringifyType(detailType, checker), detailType, checker);
128+
const { type, inlineType } = getObjectDefinition(stringifyType(detailType, checker), detailType, typeNode, checker);
107129
return {
108130
detailType: type,
109131
detailInlineType: inlineType,

src/components/extractor.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,18 @@ import {
1010
unwrapNamespaceDeclaration,
1111
} from './type-utils';
1212

13+
export interface ExtractedDescription {
14+
text: string | undefined;
15+
tags: Array<{ name: string; text: string | undefined }>;
16+
}
17+
1318
export interface ExpandedProp {
1419
name: string;
1520
type: string;
1621
isOptional: boolean;
1722
rawType: ts.Type;
18-
description: {
19-
text: string | undefined;
20-
tags: Array<{ name: string; text: string | undefined }>;
21-
};
23+
rawTypeNode: ts.TypeNode | undefined;
24+
description: ExtractedDescription;
2225
}
2326

2427
export function extractDefaultValues(exportSymbol: ts.Symbol, checker: ts.TypeChecker) {
@@ -87,6 +90,7 @@ export function extractProps(propsSymbol: ts.Symbol, checker: ts.TypeChecker) {
8790
name: value.name,
8891
type: stringifyType(type, checker),
8992
rawType: type,
93+
rawTypeNode: (declaration as ts.PropertyDeclaration).type,
9094
isOptional: isOptional(type),
9195
description: getDescription(value.getDocumentationComment(checker), declaration),
9296
};
@@ -124,6 +128,7 @@ export function extractFunctions(propsSymbol: ts.Symbol, checker: ts.TypeChecker
124128
name: value.name,
125129
type: stringifyType(realType, checker),
126130
rawType: realType,
131+
rawTypeNode: (declaration as ts.PropertyDeclaration).type,
127132
isOptional: isOptional(type),
128133
description: getDescription(value.getDocumentationComment(checker), declaration),
129134
};

src/components/index.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { buildComponentDefinition } from './component-definition';
88
import { extractDefaultValues, extractExports, extractFunctions, extractProps } from './extractor';
99
import type { ComponentDefinition } from './interfaces';
1010
import { bootstrapTypescriptProject } from '../bootstrap/typescript';
11+
import { extractDeclaration, getDescription } from './type-utils';
1112

1213
function componentNameFromPath(componentPath: string) {
1314
const directoryName = pathe.dirname(componentPath);
@@ -51,7 +52,11 @@ export function documentComponents(
5152
const props = extractProps(propsSymbol, checker);
5253
const functions = extractFunctions(propsSymbol, checker);
5354
const defaultValues = extractDefaultValues(componentSymbol, checker);
55+
const componentDescription = getDescription(
56+
componentSymbol.getDocumentationComment(checker),
57+
extractDeclaration(componentSymbol)
58+
);
5459

55-
return buildComponentDefinition(name, props, functions, defaultValues, checker);
60+
return buildComponentDefinition(name, props, functions, defaultValues, componentDescription, checker);
5661
});
5762
}

src/components/interfaces.ts

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,33 +8,35 @@ export interface ComponentDefinition {
88
version?: string;
99
/** @deprecated */
1010
description?: string;
11+
systemTag?: Array<string>;
1112
properties: ComponentProperty[];
1213
regions: ComponentRegion[];
1314
functions: ComponentFunction[];
1415
events: EventHandler[];
1516
}
1617

17-
export interface ComponentProperty {
18+
interface Taggable {
19+
analyticsTag?: string;
20+
deprecatedTag?: string;
21+
visualRefreshTag?: string;
22+
i18nTag?: true | undefined;
23+
systemTag?: Array<string>;
24+
}
25+
26+
export interface ComponentProperty extends Taggable {
1827
name: string;
1928
description?: string;
2029
optional: boolean;
2130
type: string;
2231
inlineType?: TypeDefinition;
2332
defaultValue?: string;
24-
analyticsTag?: string;
25-
deprecatedTag?: string;
26-
visualRefreshTag?: string;
27-
i18nTag?: true | undefined;
2833
}
2934

30-
export interface ComponentRegion {
35+
export interface ComponentRegion extends Taggable {
3136
name: string;
3237
description?: string;
3338
displayName?: string;
3439
isDefault: boolean;
35-
deprecatedTag?: string;
36-
visualRefreshTag?: string;
37-
i18nTag?: true | undefined;
3840
}
3941

4042
export interface ComponentFunction {
@@ -73,14 +75,14 @@ export interface FunctionParameter {
7375
export interface UnionTypeDefinition {
7476
name: string;
7577
type: 'union';
78+
systemTag?: Record<string, Array<string>>;
7679
values: string[];
7780
}
7881

79-
export interface EventHandler {
82+
export interface EventHandler extends Taggable {
8083
name: string;
8184
description?: string;
8285
detailType?: string;
8386
detailInlineType?: TypeDefinition;
8487
cancelable: boolean;
85-
deprecatedTag?: string;
8688
}

src/components/object-definition.ts

Lines changed: 35 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// SPDX-License-Identifier: Apache-2.0
33
import ts from 'typescript';
44
import type { TypeDefinition, UnionTypeDefinition } from './interfaces';
5-
import { extractDeclaration, isOptional, stringifyType } from './type-utils';
5+
import { extractDeclaration, extractMemberComments, isOptional, stringifyType } from './type-utils';
66

77
function isArrayType(type: ts.Type) {
88
const symbol = type.getSymbol();
@@ -15,6 +15,7 @@ function isArrayType(type: ts.Type) {
1515
export function getObjectDefinition(
1616
type: string,
1717
rawType: ts.Type,
18+
rawTypeNode: ts.TypeNode | undefined,
1819
checker: ts.TypeChecker
1920
): { type: string; inlineType?: TypeDefinition } {
2021
const realType = rawType.getNonNullableType();
@@ -31,7 +32,7 @@ export function getObjectDefinition(
3132
return { type };
3233
}
3334
if (realType.isUnionOrIntersection()) {
34-
return getUnionTypeDefinition(realTypeName, realType, checker);
35+
return getUnionTypeDefinition(realTypeName, realType, rawTypeNode, checker);
3536
}
3637
if (realType.getProperties().length > 0) {
3738
return {
@@ -78,36 +79,47 @@ export function getObjectDefinition(
7879
return { type };
7980
}
8081

82+
function getSimpleType(type: ts.UnionOrIntersectionType) {
83+
if (type.types.every(subtype => subtype.isStringLiteral())) {
84+
return 'string';
85+
}
86+
if (type.types.every(subtype => subtype.isNumberLiteral())) {
87+
return 'number';
88+
}
89+
return undefined;
90+
}
91+
8192
function getUnionTypeDefinition(
8293
realTypeName: string,
8394
realType: ts.UnionOrIntersectionType,
95+
typeNode: ts.TypeNode | undefined,
8496
checker: ts.TypeChecker
8597
): { type: string; inlineType: UnionTypeDefinition } {
86-
if (realType.types.every(subtype => subtype.isStringLiteral())) {
87-
return {
88-
type: 'string',
89-
inlineType: {
90-
name: realTypeName,
91-
type: 'union',
92-
values: realType.types.map(subtype => (subtype as ts.StringLiteralType).value),
93-
},
94-
};
95-
} else if (realType.types.every(subtype => subtype.isNumberLiteral())) {
96-
return {
97-
type: 'number',
98-
inlineType: {
99-
name: realTypeName,
100-
type: 'union',
101-
values: realType.types.map(subtype => (subtype as ts.NumberLiteralType).value.toString()),
102-
},
103-
};
104-
}
98+
const memberComments = extractMemberComments(realType, typeNode);
99+
const values = realType.types.map(subtype =>
100+
subtype.isLiteral() ? subtype.value.toString() : stringifyType(subtype, checker)
101+
);
102+
105103
return {
106-
type: realTypeName,
104+
type: getSimpleType(realType) ?? realTypeName,
107105
inlineType: {
108106
name: realTypeName,
109107
type: 'union',
110-
values: realType.types.map(subtype => stringifyType(subtype, checker)),
108+
systemTag: memberComments.length > 0 ? zipMemberComments(values, memberComments) : undefined,
109+
values: values,
111110
},
112111
};
113112
}
113+
114+
function zipMemberComments(values: Array<string>, memberComments: Array<string | undefined>) {
115+
const systemTag: Record<string, Array<string>> = {};
116+
for (const value of values) {
117+
const index = values.indexOf(value);
118+
const comment = memberComments[index];
119+
if (!comment) {
120+
continue;
121+
}
122+
systemTag[value] = Array.from(comment.matchAll(/@awsuiSystem\s+(\w+)/g), ([_, system]) => system);
123+
}
124+
return systemTag;
125+
}

0 commit comments

Comments
 (0)