Files
twenty/packages/twenty-sdk/scripts/remote-dom/twenty-ui-extractor/utils/classify-component-props-for-remote-dom-generation.ts
T
Raphaël Bosi cb7d0b83a8 [FRONT COMPONENTS] Twenty UI elements generation (#17866)
## PR description

This PR:
- Creates a TypeScript-based extractor that discovers all exported
twenty-ui components by scanning barrel files, extracting
props/slots/events via ts-morph type analysis, and generating the remote
DOM bindings automatically.

- Adds a new ESLint rule which enforces all *Props types in twenty-ui
components to be exported, which is required for the extractor to
discover component prop types. Existing twenty-ui components are updated
to comply with this rule.

- Extends the remote DOM generation to support slots, per-component
events, forwardRef wrappers, and richer property types (array, object,
function)

## Edge cases to fix in another PR

- Icons cannot be rendered inside buttons
- IconButtons throw an error when mounted
- MenuItems are not displayed correctly
- MenuItemNavigate throws on click

## Video Demo


https://github.com/user-attachments/assets/c2ed67cf-6a15-4896-9fec-e83fac0e862b
2026-02-12 12:48:16 +01:00

66 lines
1.9 KiB
TypeScript

import { type Type } from 'ts-morph';
import { type PropertySchema } from '@/front-component/types/PropertySchema';
import { isDefined, isNonEmptyArray } from 'twenty-shared/utils';
import { REACT_PROP_TO_DOM_EVENT } from '../constants/ReactPropToDomEvent';
import { mapTypeToPropertySchema } from './map-type-to-property-schema';
import { isDomEventHandler } from './is-dom-event-handler';
import { isReactElementType } from './is-react-element-type';
export type ClassifiedComponentProps = {
properties: Record<string, PropertySchema>;
events: string[];
slots: string[];
};
export const classifyComponentPropsForRemoteDomGeneration = (
propsType: Type,
): ClassifiedComponentProps => {
const properties: Record<string, PropertySchema> = {};
const events: string[] = [];
const slots: string[] = [];
const propsTypeProperties = propsType.getProperties();
for (const propertySymbol of propsTypeProperties) {
const propertyName = propertySymbol.getName();
const propertyDeclarations = propertySymbol.getDeclarations();
if (!isNonEmptyArray(propertyDeclarations)) {
continue;
}
const firstDeclaration = propertyDeclarations[0];
const propertyType = firstDeclaration.getType();
const correspondingDomEvent = REACT_PROP_TO_DOM_EVENT[propertyName];
if (isDefined(correspondingDomEvent) && isDomEventHandler(propertyType)) {
events.push(correspondingDomEvent);
continue;
}
const isOptional =
propertySymbol.isOptional() ||
propertyType.isNullable() ||
propertyType.isUndefined();
if (isReactElementType(propertyType)) {
slots.push(propertyName);
continue;
}
const classifiedType = mapTypeToPropertySchema(propertyType);
if (isDefined(classifiedType)) {
properties[propertyName] = {
type: classifiedType,
optional: isOptional,
};
}
}
return { properties, events, slots };
};