Skip to content
Merged
Show file tree
Hide file tree
Changes from 37 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
c15b02a
[code-infra] Update docgen to v8 version
JCQuintas Jan 26, 2026
f558d76
refactor
JCQuintas Jan 26, 2026
d358ba0
fix handling object accessor eg`value.name`
JCQuintas Jan 26, 2026
d37263f
run script
JCQuintas Jan 26, 2026
90e92a2
prettier
JCQuintas Jan 26, 2026
74efc23
fix possible issues
JCQuintas Jan 26, 2026
371dc84
fix unknown
JCQuintas Jan 26, 2026
dc2367c
remove unknown
JCQuintas Jan 26, 2026
82bc7e1
get last version
JCQuintas Jan 27, 2026
0bb3470
merge all declarations
JCQuintas Jan 27, 2026
3caac7d
fix typedoc duplications
JCQuintas Jan 27, 2026
66f2347
fix prettier
JCQuintas Jan 27, 2026
120bcae
docs typescript
JCQuintas Jan 27, 2026
1bb640d
fix type
JCQuintas Jan 28, 2026
dfc3689
prettier
JCQuintas Jan 28, 2026
d83fc71
few tweaks
Janpot Jan 28, 2026
b0dd5fa
Revert "docs typescript"
JCQuintas Jan 28, 2026
b0cd87a
Revert "fix typedoc duplications"
JCQuintas Jan 28, 2026
831c7f5
rework
JCQuintas Jan 28, 2026
22076a6
autocomplete readOnly inherited from useautocomplete
JCQuintas Jan 28, 2026
004d90f
AvatarGroup was picking props that were immediately overridden
JCQuintas Jan 28, 2026
d67601c
Menu is using slots/slotProps from the PopperProps type
JCQuintas Jan 28, 2026
9d15781
RadioGroup now picks the correct value it declares itself
JCQuintas Jan 28, 2026
1bfc041
Select not picks the docs it declares itself
JCQuintas Jan 28, 2026
56371e3
ExtendButtonBaseTypeMap is an override type, so we should probably ju…
JCQuintas Jan 28, 2026
0b2d1f4
AccordionSummary children is already declared in base
JCQuintas Jan 28, 2026
28b3ecc
Select is always merged with BaseSelectProps with has its own label d…
JCQuintas Jan 28, 2026
6d4a1d4
Same as buttonbase
JCQuintas Jan 28, 2026
1825e0a
run docs api
JCQuintas Jan 28, 2026
e3b9369
fixes
JCQuintas Jan 28, 2026
34845d2
add issue comment
JCQuintas Jan 29, 2026
0145c29
use default instead of builtin
JCQuintas Jan 29, 2026
e3122f4
Merge commit 'e22f0452dbc1c53184d9982c20255a865bcda198' into docgen-v8
JCQuintas Feb 18, 2026
43c9fc8
revert irrelevant option.title change
JCQuintas Feb 18, 2026
73cf8a6
Preserve typescript-to-proptypes-ignore
JCQuintas Feb 18, 2026
5bf034f
fix prettier
JCQuintas Feb 18, 2026
acd6909
Merge commit '1682b6cfea85951e503c9c2a812b7bbad8055af1' into docgen-v8
JCQuintas Feb 18, 2026
e3c6346
Merge commit '7a59f8a8c6caa672b2705e3f3146f2892fde7364' into docgen-v8
JCQuintas Feb 24, 2026
e0d68da
Merge commit 'ab6e743e59454f3f9ab0488aa771cd68c41ed903' into docgen-v8
JCQuintas Feb 24, 2026
525e94f
Merge commit '18a829b00914239709e09e0679ea4de2c0af8dd1' into docgen-v8
JCQuintas Feb 24, 2026
2bb4a9b
Merge commit 'fe0e4b4248b0cd8f2d0b2df684b04966f7c7f14f' into docgen-v8
JCQuintas Feb 24, 2026
30071a9
Merge commit '6e9f2ab1004a1c3fc5f5760e132db9da67310af1' into docgen-v8
JCQuintas Feb 24, 2026
9da3f78
Merge commit '0081c48dff63278c95939d223461e0f754ecf39a' into docgen-v8
JCQuintas Feb 24, 2026
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
2 changes: 1 addition & 1 deletion docs/pages/joy-ui/api/select.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
"indicator": { "type": { "name": "node" } },
"listboxId": { "type": { "name": "string" } },
"listboxOpen": { "type": { "name": "bool" }, "default": "undefined" },
"multiple": { "type": { "name": "bool" } },
"multiple": { "type": { "name": "bool" }, "default": "false" },
"name": { "type": { "name": "string" } },
"onChange": { "type": { "name": "func" } },
"onClose": { "type": { "name": "func" } },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ function FormButton(props) {
FormButton.propTypes = {
/**
* If `true`, the component is disabled.
* @default false
*/
disabled: PropTypes.bool,
mounted: PropTypes.bool,
Expand Down
6 changes: 2 additions & 4 deletions docs/translations/api-docs-joy/avatar-group/avatar-group.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,12 @@
"description": "Used to render icon or text elements inside the AvatarGroup if <code>src</code> is not set. This can be an element, or just a string."
},
"color": {
"description": "The color of the component. It supports those theme colors that make sense for this component."
"description": "The color context for the avatar children. It has no effect on the AvatarGroup."
},
"component": {
"description": "The component used for the root node. Either a string to use a HTML element or a component."
},
"size": {
"description": "The size of the component. It accepts theme values between &#39;sm&#39; and &#39;lg&#39;."
},
"size": { "description": "The size of the component and the avatar children." },
"slotProps": { "description": "The props used for each slot inside." },
"slots": { "description": "The components used for each slot inside." },
"sx": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"description": "The color of the component. It supports those theme colors that make sense for this component."
},
"component": {
"description": "The component used for the root node. Either a string to use a HTML element or a component."
"description": "The component used for the Root slot. Either a string to use a HTML element or a component."
},
"defaultValue": {
"description": "The default value. Use when the component is not controlled."
Expand Down
1 change: 1 addition & 0 deletions docs/translations/api-docs-joy/select/select.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
"description": "If <code>true</code>, the Select cannot be empty when submitting form."
},
"size": { "description": "The size of the component." },
"slotProps": { "description": "The props used for each slot inside." },
"slots": { "description": "The components used for each slot inside." },
"startDecorator": { "description": "Leading adornment for the select." },
"sx": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ export interface GeneratePropTypesOptions {
* Previous source code of the validator for each prop type
*/
previousPropTypesSource?: Map<string, string>;
/**
* Previous JSDoc comment source for each prop type
*/
previousPropTypesJsDoc?: Map<string, string>;
/**
* Given the `prop`, the `previous` source of the validator and the `generated` source:
* What source should be injected? `previous` is `undefined` if the validator
Expand Down Expand Up @@ -108,6 +112,7 @@ export function generatePropTypes(
includeJSDoc = true,
sortProptypes = true,
previousPropTypesSource = new Map<string, string>(),
previousPropTypesJsDoc = new Map<string, string>(),
reconcilePropTypes = (_prop: PropTypeDefinition, _previous: string, generated: string) =>
generated,
shouldInclude,
Expand Down Expand Up @@ -306,7 +311,13 @@ export function generatePropTypes(
})}${isRequired === true ? '.isRequired' : ''}`,
);

return `${jsDoc(propTypeDefinition)}"${propTypeDefinition.name}": ${validatorSource},`;
const previousJsDoc = previousPropTypesJsDoc.get(propTypeDefinition.name);
const jsDocSource =
previousJsDoc !== undefined && validatorSource.includes('@typescript-to-proptypes-ignore')
? `${previousJsDoc}\n`
: jsDoc(propTypeDefinition);

return `${jsDocSource}"${propTypeDefinition.name}": ${validatorSource},`;
}

const propTypes = component.types.slice();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,20 +34,52 @@ function getSymbolDocumentation({
}: {
symbol: ts.Symbol | undefined;
project: TypeScriptProject;
parentType?: ts.Type;
}): string | undefined {
if (symbol === undefined) {
return undefined;
}

const decl = symbol.getDeclarations();
if (decl) {
// @ts-ignore
const comments = ts.getJSDocCommentsAndTags(decl[0]) as readonly any[];
if (comments && comments.length === 1) {
const commentNode = comments[0];
if (ts.isJSDoc(commentNode)) {
return doctrine.unwrapComment(commentNode.getText()).trim();
if (decl && decl.length > 0) {
// This behavior tries to replicate how TypeScript itself merges JSDoc comments
// It is a complex logic that changes based on the kind of declarations
// There is an open issue for it in: https://github.com/microsoft/TypeScript/issues/30901
//
// For intersection types (A & B), the symbol may have multiple declarations.
// We need to handle three cases:
// 1. Intersection (type C = A & B): merge JSDoc from all declarations (deduplicated)
// 2. Interface extends (interface Z extends X, Y): use the (only) declaration's JSDoc
// 3. Interface override (interface W extends X { prop }): use the override's JSDoc (which is the only declaration)
//
// Note: TypeScript gives us:
// - Multiple declarations for intersection types (one from each constituent type)
// - Single declaration for interface extends (from the original interface)
// - Single declaration for interface override (from the overriding interface)

// Get JSDoc comments paired with their declarations
const declarationsWithComments = decl
.map((d) => {
const jsDocNodes = ts.getJSDocCommentsAndTags(d).filter((node) => ts.isJSDoc(node));
const comment =
jsDocNodes.length > 0
? doctrine.unwrapComment(jsDocNodes[0].getText()).trim()
: undefined;
return { declaration: d, comment };
})
.filter((item) => item.comment !== undefined);

if (declarationsWithComments.length > 0) {
// If there's only one declaration with a comment, use it
// This handles both interface extends and interface override cases
if (declarationsWithComments.length === 1) {
return declarationsWithComments[0].comment;
}

// Multiple declarations with comments - this is the intersection case (type C = A & B)
Copy link
Copy Markdown
Member

@Janpot Janpot Jan 29, 2026

Choose a reason for hiding this comment

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

So that means if you change an interface to a type or vice-versa, the extracted comments suddenly can look different?

interface A {
  /** foo */
  prop: Prop
}

interface B {
  /** bar */
  prop: Prop
}

type X = A & B & {
  /** baz */
  prop: Prop
}
interface C extends B, A {
  /** baz */
  prop: Prop
}

type X = C

So these wouldn't produce the same output in the proptypes, right? There seems to be an open issue about this microsoft/TypeScript#30901. Your stance is to try emulate intellisense as closely as possible? I hate this behavior, but it also kind of makes sense to make sure the docs and the intellisense in vscode look the same. Maybe link to that open issue so we can periodically check if it ever gets fixed.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yes, changing them could look different, which is the behaviour of the intellisense as you mentioned: https://www.typescriptlang.org/play/?#code/JYOwLgpgTgZghgYwgAgILIN4ChnIPQBUByMA9qcgXjsgA5Sm0BcyIArgLYBG0WAvliyhIsRCgBCmGoWJc4UStVz1GLdt14CsYAJ60UADWQBeNMgBkySZey4ZyOQC9FNFc1aceUfoOHR4SMgAwsgQAB6QIAAmAM5WADRmtvhEDnDOVK4M7upePtp6KACaJsGCCKQgMWDIYSxGpsnK2SwAjD4VVTU6LCWNNM2qyABM-EA

I find the internal behaviour of intellisense weird too, but I think it is sensible that we emulate what the users would actually see in the interface in order to better control it.

Personally I don't look at the libraries docs most of the time. I'm reading the types documentation in either the UI or the declarations, so this is technically a "bugfix" for me 😆

I'll mention the issue in code

// Merge JSDoc comments, deduplicating identical ones
const uniqueComments = [...new Set(declarationsWithComments.map((d) => d.comment))];
return uniqueComments.join('\n');
}
}

Expand Down Expand Up @@ -116,7 +148,7 @@ function checkType({
return createObjectType({ jsDoc: undefined });
}

const typeNode = type as any;
const typeNode = type;
const symbol = typeNode.aliasSymbol ? typeNode.aliasSymbol : typeNode.symbol;
const jsDoc = getSymbolDocumentation({ symbol, project });

Expand Down Expand Up @@ -396,16 +428,18 @@ function checkSymbol({
symbol,
location,
typeStack,
parentType,
}: {
project: PropTypesProject;
symbol: ts.Symbol;
location: ts.Node;
typeStack: readonly number[];
parentType?: ts.Type;
}): PropTypeDefinition {
const declarations = symbol.getDeclarations();
const declaration = declarations && declarations[0];
const symbolFilenames = getSymbolFileNames(symbol);
const jsDoc = getSymbolDocumentation({ symbol, project });
const jsDoc = getSymbolDocumentation({ symbol, project, parentType });

// TypeChecker keeps the name for
// { a: React.ElementType, b: React.ReactElement | boolean }
Expand Down Expand Up @@ -498,9 +532,9 @@ function squashPropTypeDefinitions({
}): PropTypeDefinition {
const distinctDefinitions = new Map<number, PropTypeDefinition>();
propTypeDefinitions.forEach((definition) => {
if (!distinctDefinitions.has(definition.$$id)) {
distinctDefinitions.set(definition.$$id, definition);
}
// Always update so that the last definition's jsDoc wins
// This ensures that when types are intersected (A & B), the last type's documentation is used
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

So it went from first to last definition wins?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Pretty much

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Reasoning was more that usually we have

interface A extends B {} which is similar to A & B. It felt a bit arbitrary that the first wins, which would prevent some overriding.

Ideally the best way is to Omit<A, keyof B> & B though

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

interface A extends B {}

feels similar to

B & A

to me, as in A's props should win over Bs, but that isn't how this code interpretes it?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

True, A should win in the first, but B should in the second. 🫠

I checked it on the playground

Inheritance picks the first time the prop appears 🙃
If you override it, then it ignores any previous declaration.

Intersection merges all declarations together.

Copy link
Copy Markdown
Member

@Janpot Janpot Jan 28, 2026

Choose a reason for hiding this comment

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

I think ideally we take interface behavior, because that can't simply be altered to fix the comment. i.e.

interface X extends Y, Z {
  /** foo */
  prop: number
}

we can easily move Y and Z around, but we can't really reorder X. even though, if one comment should win, it's probably X here.

For types, we can freely change the order, so it doesn't really matter what we do in terms of "first wins" or "last wins", we can pick what interfaces do and adjust order where needed.

Instead of adding more Omit, maybe it's a matter of reordering the types in the intersection?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I've reworked it a bit to follow typescript, which required less changes in the end :)
Divided them into commits for each component, each with a vague-ish description of the reason 😆

In a few cases it still made sense to add Omit, else the types (when viewed by the user in a code editor) would still be merged/messy.

distinctDefinitions.set(definition.$$id, definition);
});

if (distinctDefinitions.size === 1 && !onlyUsedInSomeSignatures) {
Expand All @@ -513,16 +547,20 @@ function squashPropTypeDefinitions({
types.push(createUndefinedType({ jsDoc: undefined }));
}

// Use the last definition's jsDoc so that when types are intersected (A & B),
// the last type's documentation wins
const lastDefinition = definitions[definitions.length - 1];

return {
name: definitions[0].name,
jsDoc: definitions[0].jsDoc,
name: lastDefinition.name,
jsDoc: lastDefinition.jsDoc,
propType: createUnionType({
// TODO: jsDoc from squashing is dropped
jsDoc: undefined,
types,
}),
filenames: new Set(definitions.flatMap((definition) => Array.from(definition.filenames))),
$$id: definitions[0].$$id,
$$id: lastDefinition.$$id,
};
}

Expand All @@ -544,6 +582,7 @@ function generatePropTypesFromNode(
project: params.project,
location: parsedComponent.location,
typeStack: [(componentType as any).id],
parentType: componentType,
}),
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ function createBabelPlugin({
let alreadyImported = false;
const originalPropTypesPaths = new Map<string, babel.NodePath>();
const previousPropTypesSources = new Map<string, Map<string, string>>();
const previousPropTypesJsDocs = new Map<string, Map<string, string>>();

function injectPropTypes(injectOptions: {
path: babel.NodePath;
Expand All @@ -185,11 +186,14 @@ function createBabelPlugin({

const previousPropTypesSource =
previousPropTypesSources.get(nodeName) || new Map<string, string>();
const previousPropTypesJsDoc =
previousPropTypesJsDocs.get(nodeName) || new Map<string, string>();

const source = generatePropTypes(props, {
...otherOptions,
importedName: importName,
previousPropTypesSource,
previousPropTypesJsDoc,
reconcilePropTypes,
shouldInclude: (prop) => shouldInclude({ component: props, prop, usedProps }),
});
Expand Down Expand Up @@ -269,6 +273,9 @@ function createBabelPlugin({
const previousPropTypesSource = new Map<string, string>();
previousPropTypesSources.set(componentName, previousPropTypesSource);

const previousPropTypesJsDoc = new Map<string, string>();
previousPropTypesJsDocs.set(componentName, previousPropTypesJsDoc);

let maybeObjectExpression = node.expression.right;
// Component.propTypes = {} as any;
// ^^^^^^^^^ expression.right
Expand All @@ -286,15 +293,38 @@ function createBabelPlugin({
maybeObjectExpression.properties.forEach((property) => {
if (babelTypes.isObjectProperty(property)) {
const validatorSource = code.slice(property.value.start, property.value.end);

let propName: string | undefined;
if (babelTypes.isIdentifier(property.key)) {
previousPropTypesSource.set(property.key.name, validatorSource);
propName = property.key.name;
} else if (babelTypes.isStringLiteral(property.key)) {
previousPropTypesSource.set(property.key.value, validatorSource);
propName = property.key.value;
} else {
console.warn(
`${state.filename}: Possibly missed original proTypes source. Can only determine names for 'Identifiers' and 'StringLiteral' but received '${property.key.type}'.`,
);
}

if (propName !== undefined) {
previousPropTypesSource.set(propName, validatorSource);

const leadingComments = property.leadingComments;
if (leadingComments) {
const jsDocComment = leadingComments.find(
(c) => c.type === 'CommentBlock' && c.value.startsWith('*'),
);
if (
jsDocComment &&
jsDocComment.start != null &&
jsDocComment.end != null
) {
previousPropTypesJsDoc.set(
propName,
code.slice(jsDocComment.start, jsDocComment.end),
);
}
}
}
}
});
}
Expand Down
Loading
Loading