diff --git a/fixtures/components/system-tag/button/index.tsx b/fixtures/components/system-tag/button/index.tsx
new file mode 100644
index 0000000..bdcb523
--- /dev/null
+++ b/fixtures/components/system-tag/button/index.tsx
@@ -0,0 +1,10 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+import * as React from 'react';
+import { ButtonProps } from './interfaces';
+
+export { ButtonProps };
+
+export default function Button(props: ButtonProps) {
+ return
;
+}
diff --git a/fixtures/components/system-tag/button/interfaces.ts b/fixtures/components/system-tag/button/interfaces.ts
new file mode 100644
index 0000000..eca6865
--- /dev/null
+++ b/fixtures/components/system-tag/button/interfaces.ts
@@ -0,0 +1,28 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+export interface ButtonProps {
+ variant?: ButtonProps.Variant;
+
+ /**
+ * @awsuiSystem core
+ */
+ size: 'small' | 'medium' | 'large';
+
+ color?:
+ | 'normal'
+ /** @awsuiSystem core */
+ | 'danger';
+}
+
+export namespace ButtonProps {
+ export type Variant =
+ | 'primary'
+ | 'secondary'
+ /** @awsuiSystem core */
+ | 'fire'
+ /**
+ * @awsuiSystem core
+ * @awsuiSystem experimental
+ */
+ | 'ultra';
+}
diff --git a/fixtures/components/system-tag/tree/index.tsx b/fixtures/components/system-tag/tree/index.tsx
new file mode 100644
index 0000000..9140f82
--- /dev/null
+++ b/fixtures/components/system-tag/tree/index.tsx
@@ -0,0 +1,13 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+import * as React from 'react';
+
+// eslint-disable-next-line @typescript-eslint/no-empty-interface
+export interface TreeProps {}
+
+/**
+ * @awsuiSystem core
+ */
+export default function Tree() {
+ return ;
+}
diff --git a/fixtures/components/system-tag/tsconfig.json b/fixtures/components/system-tag/tsconfig.json
new file mode 100644
index 0000000..82c6d86
--- /dev/null
+++ b/fixtures/components/system-tag/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "extends": "../tsconfig.json",
+ "compilerOptions": {
+ "rootDir": "."
+ },
+ "include": ["./**/*.tsx"]
+}
diff --git a/src/components/component-definition.ts b/src/components/component-definition.ts
index 9ea8e3a..8cd56ed 100644
--- a/src/components/component-definition.ts
+++ b/src/components/component-definition.ts
@@ -10,14 +10,26 @@ import type {
ComponentRegion,
EventHandler,
} from './interfaces';
-import type { ExpandedProp } from './extractor';
+import type { ExpandedProp, ExtractedDescription } from './extractor';
import { getObjectDefinition } from './object-definition';
-function getCommentTag(property: ExpandedProp, name: string) {
- const tag = property.description.tags.find(tag => tag.name === name);
+function getCommentTag(description: ExtractedDescription, name: string) {
+ const tag = description.tags.find(tag => tag.name === name);
return tag ? tag.text ?? '' : undefined;
}
+function getCommentTags(description: ExtractedDescription, name: string) {
+ const tags = description.tags
+ .filter(tag => tag.name === name)
+ .map(tag => {
+ if (!tag.text) {
+ throw new Error(`Tag ${name} is missing text`);
+ }
+ return tag.text;
+ });
+ return tags.length > 0 ? tags : undefined;
+}
+
function castI18nTag(tag: string | undefined) {
return tag === undefined ? undefined : true;
}
@@ -27,6 +39,7 @@ export function buildComponentDefinition(
props: Array,
functions: Array,
defaultValues: Record,
+ componentDescription: ExtractedDescription,
checker: ts.TypeChecker
): ComponentDefinition {
const regions = props.filter(prop => prop.type === 'React.ReactNode');
@@ -36,15 +49,18 @@ export function buildComponentDefinition(
return {
name,
releaseStatus: 'stable',
+ description: componentDescription.text,
+ systemTags: getCommentTags(componentDescription, 'awsuiSystem'),
regions: regions.map(
(region): ComponentRegion => ({
name: region.name,
- displayName: getCommentTag(region, 'displayname'),
+ displayName: getCommentTag(region.description, 'displayname'),
description: region.description.text,
isDefault: region.name === 'children',
- visualRefreshTag: getCommentTag(region, 'visualrefresh'),
- deprecatedTag: getCommentTag(region, 'deprecated'),
- i18nTag: castI18nTag(getCommentTag(region, 'i18n')),
+ systemTags: getCommentTags(region.description, 'awsuiSystem'),
+ visualRefreshTag: getCommentTag(region.description, 'visualrefresh'),
+ deprecatedTag: getCommentTag(region.description, 'deprecated'),
+ i18nTag: castI18nTag(getCommentTag(region.description, 'i18n')),
})
),
functions: functions.map(
@@ -66,7 +82,7 @@ export function buildComponentDefinition(
})
),
properties: onlyProps.map((property): ComponentProperty => {
- const { type, inlineType } = getObjectDefinition(property.type, property.rawType, checker);
+ const { type, inlineType } = getObjectDefinition(property.type, property.rawType, property.rawTypeNode, checker);
return {
name: property.name,
type: type,
@@ -74,27 +90,33 @@ export function buildComponentDefinition(
optional: property.isOptional,
description: property.description.text,
defaultValue: defaultValues[property.name],
- visualRefreshTag: getCommentTag(property, 'visualrefresh'),
- deprecatedTag: getCommentTag(property, 'deprecated'),
- analyticsTag: getCommentTag(property, 'analytics'),
- i18nTag: castI18nTag(getCommentTag(property, 'i18n')),
+ systemTags: getCommentTags(property.description, 'awsuiSystem'),
+ visualRefreshTag: getCommentTag(property.description, 'visualrefresh'),
+ deprecatedTag: getCommentTag(property.description, 'deprecated'),
+ analyticsTag: getCommentTag(property.description, 'analytics'),
+ i18nTag: castI18nTag(getCommentTag(property.description, 'i18n')),
};
}),
events: events.map((event): EventHandler => {
- const { detailType, detailInlineType, cancelable } = extractEventDetails(event.rawType, checker);
+ const { detailType, detailInlineType, cancelable } = extractEventDetails(
+ event.rawType,
+ event.rawTypeNode,
+ checker
+ );
return {
name: event.name,
description: event.description.text,
cancelable,
detailType,
detailInlineType,
- deprecatedTag: getCommentTag(event, 'deprecated'),
+ systemTags: getCommentTags(event.description, 'awsuiSystem'),
+ deprecatedTag: getCommentTag(event.description, 'deprecated'),
};
}),
};
}
-function extractEventDetails(type: ts.Type, checker: ts.TypeChecker) {
+function extractEventDetails(type: ts.Type, typeNode: ts.TypeNode | undefined, checker: ts.TypeChecker) {
const realType = type.getNonNullableType();
const handlerName = realType.aliasSymbol?.getName();
if (handlerName !== 'CancelableEventHandler' && handlerName !== 'NonCancelableEventHandler') {
@@ -103,7 +125,7 @@ function extractEventDetails(type: ts.Type, checker: ts.TypeChecker) {
const cancelable = handlerName === 'CancelableEventHandler';
const detailType = realType.aliasTypeArguments?.[0];
if (detailType && detailType.getProperties().length > 0) {
- const { type, inlineType } = getObjectDefinition(stringifyType(detailType, checker), detailType, checker);
+ const { type, inlineType } = getObjectDefinition(stringifyType(detailType, checker), detailType, typeNode, checker);
return {
detailType: type,
detailInlineType: inlineType,
diff --git a/src/components/extractor.ts b/src/components/extractor.ts
index 437faef..6714812 100644
--- a/src/components/extractor.ts
+++ b/src/components/extractor.ts
@@ -10,15 +10,18 @@ import {
unwrapNamespaceDeclaration,
} from './type-utils';
+export interface ExtractedDescription {
+ text: string | undefined;
+ tags: Array<{ name: string; text: string | undefined }>;
+}
+
export interface ExpandedProp {
name: string;
type: string;
isOptional: boolean;
rawType: ts.Type;
- description: {
- text: string | undefined;
- tags: Array<{ name: string; text: string | undefined }>;
- };
+ rawTypeNode: ts.TypeNode | undefined;
+ description: ExtractedDescription;
}
export function extractDefaultValues(exportSymbol: ts.Symbol, checker: ts.TypeChecker) {
@@ -87,6 +90,7 @@ export function extractProps(propsSymbol: ts.Symbol, checker: ts.TypeChecker) {
name: value.name,
type: stringifyType(type, checker),
rawType: type,
+ rawTypeNode: (declaration as ts.PropertyDeclaration).type,
isOptional: isOptional(type),
description: getDescription(value.getDocumentationComment(checker), declaration),
};
@@ -124,6 +128,7 @@ export function extractFunctions(propsSymbol: ts.Symbol, checker: ts.TypeChecker
name: value.name,
type: stringifyType(realType, checker),
rawType: realType,
+ rawTypeNode: (declaration as ts.PropertyDeclaration).type,
isOptional: isOptional(type),
description: getDescription(value.getDocumentationComment(checker), declaration),
};
diff --git a/src/components/index.ts b/src/components/index.ts
index 85528fa..643015c 100644
--- a/src/components/index.ts
+++ b/src/components/index.ts
@@ -8,6 +8,7 @@ import { buildComponentDefinition } from './component-definition';
import { extractDefaultValues, extractExports, extractFunctions, extractProps } from './extractor';
import type { ComponentDefinition } from './interfaces';
import { bootstrapTypescriptProject } from '../bootstrap/typescript';
+import { extractDeclaration, getDescription } from './type-utils';
function componentNameFromPath(componentPath: string) {
const directoryName = pathe.dirname(componentPath);
@@ -51,7 +52,11 @@ export function documentComponents(
const props = extractProps(propsSymbol, checker);
const functions = extractFunctions(propsSymbol, checker);
const defaultValues = extractDefaultValues(componentSymbol, checker);
+ const componentDescription = getDescription(
+ componentSymbol.getDocumentationComment(checker),
+ extractDeclaration(componentSymbol)
+ );
- return buildComponentDefinition(name, props, functions, defaultValues, checker);
+ return buildComponentDefinition(name, props, functions, defaultValues, componentDescription, checker);
});
}
diff --git a/src/components/interfaces.ts b/src/components/interfaces.ts
index f7ea1e5..c5e473e 100644
--- a/src/components/interfaces.ts
+++ b/src/components/interfaces.ts
@@ -8,13 +8,25 @@ export interface ComponentDefinition {
version?: string;
/** @deprecated */
description?: string;
+ systemTags?: Array;
properties: ComponentProperty[];
regions: ComponentRegion[];
functions: ComponentFunction[];
events: EventHandler[];
}
-export interface ComponentProperty {
+interface Taggable {
+ deprecatedTag?: string;
+ visualRefreshTag?: string;
+ i18nTag?: true | undefined;
+ systemTags?: Array;
+}
+
+export interface ValueDescription {
+ systemTags: Array;
+}
+
+export interface ComponentProperty extends Taggable {
name: string;
description?: string;
optional: boolean;
@@ -22,19 +34,13 @@ export interface ComponentProperty {
inlineType?: TypeDefinition;
defaultValue?: string;
analyticsTag?: string;
- deprecatedTag?: string;
- visualRefreshTag?: string;
- i18nTag?: true | undefined;
}
-export interface ComponentRegion {
+export interface ComponentRegion extends Taggable {
name: string;
description?: string;
displayName?: string;
isDefault: boolean;
- deprecatedTag?: string;
- visualRefreshTag?: string;
- i18nTag?: true | undefined;
}
export interface ComponentFunction {
@@ -73,14 +79,14 @@ export interface FunctionParameter {
export interface UnionTypeDefinition {
name: string;
type: 'union';
+ valueDescriptions?: Record;
values: string[];
}
-export interface EventHandler {
+export interface EventHandler extends Taggable {
name: string;
description?: string;
detailType?: string;
detailInlineType?: TypeDefinition;
cancelable: boolean;
- deprecatedTag?: string;
}
diff --git a/src/components/object-definition.ts b/src/components/object-definition.ts
index 84adfc4..24da72d 100644
--- a/src/components/object-definition.ts
+++ b/src/components/object-definition.ts
@@ -1,8 +1,8 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import ts from 'typescript';
-import type { TypeDefinition, UnionTypeDefinition } from './interfaces';
-import { extractDeclaration, isOptional, stringifyType } from './type-utils';
+import type { TypeDefinition, UnionTypeDefinition, ValueDescription } from './interfaces';
+import { extractDeclaration, extractValueDescriptions, isOptional, stringifyType } from './type-utils';
function isArrayType(type: ts.Type) {
const symbol = type.getSymbol();
@@ -15,6 +15,7 @@ function isArrayType(type: ts.Type) {
export function getObjectDefinition(
type: string,
rawType: ts.Type,
+ rawTypeNode: ts.TypeNode | undefined,
checker: ts.TypeChecker
): { type: string; inlineType?: TypeDefinition } {
const realType = rawType.getNonNullableType();
@@ -31,7 +32,7 @@ export function getObjectDefinition(
return { type };
}
if (realType.isUnionOrIntersection()) {
- return getUnionTypeDefinition(realTypeName, realType, checker);
+ return getUnionTypeDefinition(realTypeName, realType, rawTypeNode, checker);
}
if (realType.getProperties().length > 0) {
return {
@@ -78,36 +79,46 @@ export function getObjectDefinition(
return { type };
}
+function getPrimitiveType(type: ts.UnionOrIntersectionType) {
+ if (type.types.every(subtype => subtype.isStringLiteral())) {
+ return 'string';
+ }
+ if (type.types.every(subtype => subtype.isNumberLiteral())) {
+ return 'number';
+ }
+ return undefined;
+}
+
function getUnionTypeDefinition(
realTypeName: string,
realType: ts.UnionOrIntersectionType,
+ typeNode: ts.TypeNode | undefined,
checker: ts.TypeChecker
): { type: string; inlineType: UnionTypeDefinition } {
- if (realType.types.every(subtype => subtype.isStringLiteral())) {
- return {
- type: 'string',
- inlineType: {
- name: realTypeName,
- type: 'union',
- values: realType.types.map(subtype => (subtype as ts.StringLiteralType).value),
- },
- };
- } else if (realType.types.every(subtype => subtype.isNumberLiteral())) {
- return {
- type: 'number',
- inlineType: {
- name: realTypeName,
- type: 'union',
- values: realType.types.map(subtype => (subtype as ts.NumberLiteralType).value.toString()),
- },
- };
- }
+ const valueDescriptions = extractValueDescriptions(realType, typeNode);
+ const primitiveType = getPrimitiveType(realType);
+ const values = realType.types.map(subtype =>
+ primitiveType ? (subtype as ts.LiteralType).value.toString() : stringifyType(subtype, checker)
+ );
+
return {
- type: realTypeName,
+ type: primitiveType ?? realTypeName,
inlineType: {
name: realTypeName,
type: 'union',
- values: realType.types.map(subtype => stringifyType(subtype, checker)),
+ valueDescriptions: valueDescriptions.length > 0 ? zipValueDescriptions(values, valueDescriptions) : undefined,
+ values: values,
},
};
}
+
+function zipValueDescriptions(values: Array, descriptions: Array) {
+ const descriptionsMap: Record = {};
+ values.forEach((value, index) => {
+ const description = descriptions[index];
+ if (description) {
+ descriptionsMap[value] = description;
+ }
+ });
+ return descriptionsMap;
+}
diff --git a/src/components/type-utils.ts b/src/components/type-utils.ts
index ce634fe..1834751 100644
--- a/src/components/type-utils.ts
+++ b/src/components/type-utils.ts
@@ -1,6 +1,7 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import ts from 'typescript';
+import { ValueDescription } from './interfaces';
export function isOptional(type: ts.Type) {
if (!type.isUnionOrIntersection()) {
@@ -54,6 +55,46 @@ export function getDescription(docComment: Array, declarat
};
}
+export function extractValueDescriptions(type: ts.UnionOrIntersectionType, typeNode: ts.TypeNode | undefined) {
+ if (type.aliasSymbol) {
+ // Traverse from "variant: ButtonProps.Variant" to "type Variant = ..."
+ const aliasDeclaration = extractDeclaration(type.aliasSymbol);
+ if (ts.isTypeAliasDeclaration(aliasDeclaration)) {
+ typeNode = aliasDeclaration.type;
+ }
+ }
+
+ if (!typeNode) {
+ return [];
+ }
+
+ const maybeList = typeNode.getChildren()[0];
+ // based on similar code in typedoc
+ // https://github.com/TypeStrong/typedoc/blob/6090b3e31471cea3728db1b03888bca5703b437e/src/lib/converter/symbols.ts#L406-L438
+ if (maybeList.kind !== ts.SyntaxKind.SyntaxList) {
+ return [];
+ }
+ const rawComments: Array = [];
+ let memberIndex = 0;
+ for (const child of maybeList.getChildren()) {
+ const text = child.getFullText();
+ if (text.includes('/**')) {
+ rawComments[memberIndex] = (rawComments[memberIndex] ?? '') + child.getFullText();
+ }
+
+ if (child.kind !== ts.SyntaxKind.BarToken) {
+ memberIndex++;
+ }
+ }
+ return rawComments.map((comment): ValueDescription | undefined =>
+ comment
+ ? {
+ systemTags: Array.from(comment.matchAll(/@awsuiSystem\s+(\w+)/g), ([_, system]) => system),
+ }
+ : undefined
+ );
+}
+
export function extractDeclaration(symbol: ts.Symbol) {
const declarations = symbol.getDeclarations();
if (!declarations || declarations.length === 0) {
diff --git a/test/components/default-values-extractor.test.ts b/test/components/default-values-extractor.test.ts
index 10dbb6e..ac07465 100644
--- a/test/components/default-values-extractor.test.ts
+++ b/test/components/default-values-extractor.test.ts
@@ -1,20 +1,12 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
-import ts from 'typescript';
import { extractDefaultValues } from '../../src/components/extractor';
+import { getInMemoryProject } from './test-helpers';
function extractFromSource(source: string) {
- const host = ts.createCompilerHost({});
- const mockFs = new Map([['temp.ts', source]]);
- // mock file system access
- host.readFile = name => mockFs.get(name);
- host.writeFile = () => {};
- const program = ts.createProgram(['temp.ts'], {}, host);
- const checker = program.getTypeChecker();
+ const { exportSymbol, checker } = getInMemoryProject(source);
- const moduleSymbol = checker.getSymbolAtLocation(program.getSourceFile('temp.ts')!)!;
-
- return extractDefaultValues(checker.getExportsOfModule(moduleSymbol)[0], checker);
+ return extractDefaultValues(exportSymbol, checker);
}
test('should throw on unsupported syntax', () => {
diff --git a/test/components/simple.test.ts b/test/components/simple.test.ts
index f19f430..bb8b511 100644
--- a/test/components/simple.test.ts
+++ b/test/components/simple.test.ts
@@ -13,7 +13,7 @@ beforeAll(() => {
test('should have correct name, description and release status', () => {
expect(component.name).toBe('Simple');
- expect(component.description).toBeUndefined();
+ expect(component.description).toEqual('Component-level description');
expect(component.releaseStatus).toBe('stable');
});
diff --git a/test/components/system-tag.test.ts b/test/components/system-tag.test.ts
new file mode 100644
index 0000000..371f91a
--- /dev/null
+++ b/test/components/system-tag.test.ts
@@ -0,0 +1,61 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+import { ComponentDefinition } from '../../src/components/interfaces';
+import { buildProject } from './test-helpers';
+
+describe('System tag', () => {
+ let button: ComponentDefinition;
+ let tree: ComponentDefinition;
+ beforeAll(() => {
+ const result = buildProject('system-tag');
+ expect(result).toHaveLength(2);
+
+ [button, tree] = result;
+ });
+
+ test('should annotate whole components', () => {
+ expect(button.systemTags).toBeUndefined();
+ expect(tree.systemTags).toEqual(['core']);
+ });
+
+ test('should annotate individual properties', () => {
+ expect(button.properties).toEqual([
+ {
+ name: 'color',
+ type: 'string',
+ optional: true,
+ inlineType: {
+ name: '"normal" | "danger"',
+ type: 'union',
+ valueDescriptions: { danger: { systemTags: ['core'] } },
+ values: ['normal', 'danger'],
+ },
+ },
+ {
+ name: 'size',
+ type: 'string',
+ optional: false,
+ systemTags: ['core'],
+ inlineType: {
+ name: '"small" | "medium" | "large"',
+ type: 'union',
+ values: ['small', 'medium', 'large'],
+ },
+ },
+ {
+ name: 'variant',
+ type: 'string',
+ optional: true,
+ inlineType: {
+ name: 'ButtonProps.Variant',
+ type: 'union',
+ valueDescriptions: {
+ fire: { systemTags: ['core'] },
+ ultra: { systemTags: ['core', 'experimental'] },
+ },
+ values: ['primary', 'secondary', 'fire', 'ultra'],
+ },
+ },
+ ]);
+ });
+});
diff --git a/test/components/test-helpers.ts b/test/components/test-helpers.ts
index e83c918..e46ad3b 100644
--- a/test/components/test-helpers.ts
+++ b/test/components/test-helpers.ts
@@ -4,6 +4,7 @@ import { ProjectReflection } from 'typedoc';
import { ComponentDefinition, documentComponents, documentTestUtils } from '../../src';
import { bootstrapProject } from '../../src/bootstrap';
import { TestUtilsDoc } from '../../src/test-utils/interfaces';
+import ts from 'typescript';
export function buildProject(name: string): ComponentDefinition[] {
return documentComponents(
@@ -30,3 +31,17 @@ export function buildCustomProject(tsConfig: string, testGlob: string): ProjectR
);
return project;
}
+
+export function getInMemoryProject(source: string) {
+ const host = ts.createCompilerHost({});
+ const mockFs = new Map([['temp.ts', source]]);
+ // mock file system access
+ host.readFile = name => mockFs.get(name);
+ host.writeFile = () => {};
+ const program = ts.createProgram(['temp.ts'], {}, host);
+ const checker = program.getTypeChecker();
+ const moduleSymbol = checker.getSymbolAtLocation(program.getSourceFile('temp.ts')!)!;
+ const exportSymbol = checker.getExportsOfModule(moduleSymbol)[0];
+
+ return { exportSymbol, checker };
+}
diff --git a/test/components/value-descriptions-extractor.test.ts b/test/components/value-descriptions-extractor.test.ts
new file mode 100644
index 0000000..31d4ad5
--- /dev/null
+++ b/test/components/value-descriptions-extractor.test.ts
@@ -0,0 +1,104 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+import ts from 'typescript';
+import { extractDeclaration, extractValueDescriptions } from '../../src/components/type-utils';
+import { getInMemoryProject } from './test-helpers';
+
+function extractFromSource(source: string) {
+ const { exportSymbol, checker } = getInMemoryProject(source);
+
+ const exportType = extractDeclaration(exportSymbol) as ts.TypeAliasDeclaration;
+ return extractValueDescriptions(checker.getTypeAtLocation(exportType) as ts.UnionOrIntersectionType, exportType.type);
+}
+
+test('does not extract anything if union type has no comments', () => {
+ const source = `export type MyUnion =
+ | 'foo'
+ | 'bar';`;
+
+ expect(extractFromSource(source)).toEqual([]);
+});
+
+test('does not extract anything if this is not a type', () => {
+ const source = `export const test = 'true'`;
+
+ expect(extractFromSource(source)).toEqual([]);
+});
+
+test('extract description comments', () => {
+ const source = `export type MyUnion =
+ /** @awsuiSystem fooSystem */
+ | 'foo'
+ /** @awsuiSystem barSystem */
+ | 'bar';`;
+
+ expect(extractFromSource(source)).toEqual([{ systemTags: ['fooSystem'] }, { systemTags: ['barSystem'] }]);
+});
+
+test('extract description comments from a type alias', () => {
+ const source = `
+ export type MyUnion = InternalUnion;
+ type InternalUnion =
+ /** @awsuiSystem fooSystem */
+ | 'foo'
+ /** @awsuiSystem barSystem */
+ | 'bar';`;
+
+ expect(extractFromSource(source)).toEqual([{ systemTags: ['fooSystem'] }, { systemTags: ['barSystem'] }]);
+});
+
+test('allows some members without comments', () => {
+ const source = `export type MyUnion =
+ 'foo'
+ /** @awsuiSystem barSystem */
+ | 'bar'
+ | 'baz'
+ /** @awsuiSystem quxSystem */
+ | 'qux'
+ ;`;
+
+ expect(extractFromSource(source)).toEqual([
+ undefined,
+ { systemTags: ['barSystem'] },
+ undefined,
+ { systemTags: ['quxSystem'] },
+ ]);
+});
+
+test('ignores leading and trailing comments', () => {
+ const source = `
+ /** @awsuiSystem willNotBeParsed */
+ export type MyUnion =
+ /** @awsuiSystem fooSystem */
+ | 'foo'
+ /** @awsuiSystem barSystem */
+ | 'bar'
+ /** @awsuiSystem willNotBeParsedToo */
+ ;`;
+
+ expect(extractFromSource(source)).toEqual([{ systemTags: ['fooSystem'] }, { systemTags: ['barSystem'] }]);
+});
+
+test('merges comments before and after bar character', () => {
+ const source = `export type MyUnion =
+ /** @awsuiSystem fooBefore */
+ | /** @awsuiSystem fooAfter */ 'foo'
+ /** @awsuiSystem barSystem */
+ | 'bar'
+ ;`;
+
+ expect(extractFromSource(source)).toEqual([{ systemTags: ['fooBefore', 'fooAfter'] }, { systemTags: ['barSystem'] }]);
+});
+
+test('extracts multiple tags from a single comment', () => {
+ const source = `export type MyUnion =
+ | 'foo'
+ /**
+ * @awsuiSystem barFirst
+ * @awsuiSystem barSecond
+ */
+ | 'bar'
+ ;`;
+
+ expect(extractFromSource(source)).toEqual([undefined, { systemTags: ['barFirst', 'barSecond'] }]);
+});