Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@
// SPDX-License-Identifier: Apache-2.0

export interface ButtonGroupProps {
/**
* Main action for the group
*/
mainAction?: {
alwaysFalse: false;
alwaysOne: 1;
alwaysSomething: 'something';
};

/**
* This is variant
*/
Expand Down
10 changes: 9 additions & 1 deletion fixtures/components/slots/container/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,20 @@ export interface ContainerProps {
* @displayname content
*/
children?: React.ReactNode;

/**
* Media content
*/
media?: {
content: React.ReactNode;
};
}

export default function Container({ header, children }: ContainerProps) {
export default function Container({ header, children, media }: ContainerProps) {
return (
<div>
<header>{header}</header>
{media?.content}
{children}
</div>
);
Expand Down
1 change: 1 addition & 0 deletions src/components/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export interface ObjectDefinitionProperty {
name: string;
optional: boolean;
type: string;
inlineType?: TypeDefinition;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is recursive now

}

export interface FunctionDefinition {
Expand Down
30 changes: 16 additions & 14 deletions src/components/object-definition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,12 @@ export function getObjectDefinition(
const realTypeName = stringifyType(realType, checker);
if (
realType.flags & ts.TypeFlags.String ||
realType.flags & ts.TypeFlags.StringLiteral ||
realType.flags & ts.TypeFlags.Literal ||
Copy link
Member Author

@just-boris just-boris Jul 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make sure that boolean and number literals do not expand too.

Without this line it generates a type definition like this

{
  "inlineType": {
    "name": "() => boolean",
    "parameters": [],
    "returnType": "boolean",
    "type": "function"
  },
  "name": "valueOf",
  "optional": false,
  "type": "() => boolean"
}

realType.flags & ts.TypeFlags.Boolean ||
realType.flags & ts.TypeFlags.Number ||
isArrayType(realType) ||
realTypeName === 'HTMLElement'
realTypeName === 'HTMLElement' ||
type === 'React.ReactNode'
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prevent React types from expanding into boolean | React.ReactChild | React.ReactFragment | React.ReactPortal

) {
// do not expand built-in Javascript methods or primitive values
return { type };
Expand All @@ -38,24 +39,25 @@ export function getObjectDefinition(
const properties = realType
.getProperties()
.map(prop => {
const propType = checker.getTypeAtLocation(extractDeclaration(prop));
const propNode = extractDeclaration(prop) as ts.PropertyDeclaration;
const propType = checker.getTypeAtLocation(propNode);
const typeString = stringifyType(propType, checker);

return {
name: prop.getName(),
type: stringifyType(propType, checker),
optional: isOptional(propType),
...getObjectDefinition(typeString, propType, propNode.type, checker),
};
})
.sort((a, b) => a.name.localeCompare(b.name));
if (properties.every(prop => prop.type.length < 200)) {
return {
type: type,
inlineType: {
name: realTypeName,
type: 'object',
properties: properties,
},
};
}
return {
type: type,
inlineType: {
name: realTypeName.length < 100 ? realTypeName : 'object',
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If a type is anonymous there will be a string like

{ background?: { active?: string | undefined; default?: string | undefined; disabled?: string | undefined; hover?: string | undefined; } | undefined; color?: { active?: string | undefined; default?: string | undefined; disabled?: string | undefined; hover?: string | undefined; } | undefined; borderColor?: { ...; } |...

We do not need these.

I checked the longest type name in our system is CollectionPreferencesProps.ContentDisplayPreferenceI18nStrings, 62 chars, still below the limit

type: 'object',
properties: properties,
},
};
}
if (realType.getCallSignatures().length > 0) {
if (realType.getCallSignatures().length > 1) {
Expand Down
128 changes: 128 additions & 0 deletions test/components/__snapshots__/complex-types.test.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`should print long inline types 1`] = `
[
{
"analyticsTag": undefined,
"defaultValue": undefined,
"deprecatedTag": undefined,
"description": undefined,
"i18nTag": undefined,
"inlineType": {
"name": "ButtonProps.Style",
"properties": [
{
"inlineType": {
"name": "object",
"properties": [
{
"inlineType": {
"name": "object",
"properties": [
{
"name": "active",
"optional": true,
"type": "string",
},
{
"name": "default",
"optional": true,
"type": "string",
},
{
"name": "disabled",
"optional": true,
"type": "string",
},
{
"name": "hover",
"optional": true,
"type": "string",
},
],
"type": "object",
},
"name": "background",
"optional": true,
"type": "{ active?: string | undefined; default?: string | undefined; disabled?: string | undefined; hover?: string | undefined; }",
},
{
"inlineType": {
"name": "object",
"properties": [
{
"name": "active",
"optional": true,
"type": "string",
},
{
"name": "default",
"optional": true,
"type": "string",
},
{
"name": "disabled",
"optional": true,
"type": "string",
},
{
"name": "hover",
"optional": true,
"type": "string",
},
],
"type": "object",
},
"name": "borderColor",
"optional": true,
"type": "{ active?: string | undefined; default?: string | undefined; disabled?: string | undefined; hover?: string | undefined; }",
},
{
"inlineType": {
"name": "object",
"properties": [
{
"name": "active",
"optional": true,
"type": "string",
},
{
"name": "default",
"optional": true,
"type": "string",
},
{
"name": "disabled",
"optional": true,
"type": "string",
},
{
"name": "hover",
"optional": true,
"type": "string",
},
],
"type": "object",
},
"name": "color",
"optional": true,
"type": "{ active?: string | undefined; default?: string | undefined; disabled?: string | undefined; hover?: string | undefined; }",
},
],
"type": "object",
},
"name": "root",
"optional": true,
"type": "{ background?: { active?: string | undefined; default?: string | undefined; disabled?: string | undefined; hover?: string | undefined; } | undefined; color?: { active?: string | undefined; default?: string | undefined; disabled?: string | undefined; hover?: string | undefined; } | undefined; borderColor?: { ...; } |...",
},
],
"type": "object",
},
"name": "style",
"optional": false,
"systemTags": undefined,
"type": "ButtonProps.Style",
"visualRefreshTag": undefined,
},
]
`;
56 changes: 47 additions & 9 deletions test/components/complex-types.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,17 @@ test('should have correct property types', () => {
name: 'allItemsSelectionLabel',
optional: true,
type: '((data: TableProps.SelectionState<T>) => string)',
inlineType: {
name: '(data: TableProps.SelectionState<T>) => string',
parameters: [
{
name: 'data',
type: 'TableProps.SelectionState<T>',
},
],
returnType: 'string',
type: 'function',
},
},
],
},
Expand Down Expand Up @@ -149,7 +160,13 @@ test('should properly display string union types', () => {
{
name: 'type',
optional: true,
type: '"link" | "link-group" | "expandable-link-group"',
type: 'string',
inlineType: {
name: '"link" | "link-group" | "expandable-link-group"',
type: 'union',
valueDescriptions: undefined,
values: ['link', 'link-group', 'expandable-link-group'],
},
},
],
});
Expand Down Expand Up @@ -186,6 +203,33 @@ test('should parse string literal type as single-value union', () => {
type: 'ReadonlyArray<ButtonGroupProps.Item>',
optional: false,
},
{
name: 'mainAction',
description: 'Main action for the group',
type: '{ alwaysFalse: false; alwaysOne: 1; alwaysSomething: "something"; }',
optional: true,
inlineType: {
name: '{ alwaysFalse: false; alwaysOne: 1; alwaysSomething: "something"; }',
type: 'object',
properties: [
{
name: 'alwaysFalse',
optional: false,
type: 'false',
},
{
name: 'alwaysOne',
optional: false,
type: '1',
},
{
name: 'alwaysSomething',
optional: false,
type: '"something"',
},
],
},
},
{
name: 'variant',
description: 'This is variant',
Expand All @@ -195,12 +239,6 @@ test('should parse string literal type as single-value union', () => {
]);
});

test('should trim long inline types', () => {
expect(button.properties).toEqual([
{
name: 'style',
type: 'ButtonProps.Style',
optional: false,
},
]);
test('should print long inline types', () => {
expect(button.properties).toMatchSnapshot();
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This type is too long and updating it manually on every change is tedious, let's do a snapshot test

});
20 changes: 19 additions & 1 deletion test/components/slots.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,25 @@ beforeAll(() => {
});

test('should have correct region definitions', () => {
expect(component.properties).toEqual([]);
expect(component.properties).toEqual([
{
name: 'media',
description: 'Media content',
optional: true,
type: '{ content: React.ReactNode; }',
inlineType: {
name: '{ content: React.ReactNode; }',
properties: [
{
name: 'content',
optional: true,
Copy link
Member Author

@just-boris just-boris Jul 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A keen reviewer might ask why there is optional: true when the content above does not have a ?

This is happening because undefined is assignable to ReactNode, so {media: undefined} is supported, which makes it technically optional

type: 'React.ReactNode',
},
],
type: 'object',
},
},
]);
expect(component.regions).toEqual([
{
name: 'children',
Expand Down
Loading