cb7d0b83a8
## 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
100 lines
2.6 KiB
TypeScript
100 lines
2.6 KiB
TypeScript
import { ESLintUtils, TSESTree } from '@typescript-eslint/utils';
|
|
import { isIdentifier } from '@typescript-eslint/utils/ast-utils';
|
|
|
|
export const RULE_NAME = 'export-component-props';
|
|
|
|
// NOTE: The rule will be available in ESLint configs as "@nx/workspace-export-component-props"
|
|
export const rule = ESLintUtils.RuleCreator(() => __filename)({
|
|
name: RULE_NAME,
|
|
meta: {
|
|
type: 'problem',
|
|
docs: {
|
|
description:
|
|
'Ensure types/interfaces ending with "Props" are exported',
|
|
},
|
|
fixable: 'code',
|
|
schema: [],
|
|
messages: {
|
|
mustExportProps:
|
|
"Props type '{{ typeName }}' must be exported.",
|
|
},
|
|
},
|
|
defaultOptions: [],
|
|
create: (context) => {
|
|
const reExportedNames = new Set<string>();
|
|
const unexportedPropsNodes = new Map<
|
|
string,
|
|
| TSESTree.TSTypeAliasDeclaration
|
|
| TSESTree.TSInterfaceDeclaration
|
|
>();
|
|
|
|
const collectUnexportedProps = (
|
|
node:
|
|
| TSESTree.TSTypeAliasDeclaration
|
|
| TSESTree.TSInterfaceDeclaration,
|
|
) => {
|
|
const typeName = node.id.name;
|
|
|
|
if (!typeName.endsWith('Props')) {
|
|
return;
|
|
}
|
|
|
|
const isExported =
|
|
node.parent?.type ===
|
|
TSESTree.AST_NODE_TYPES.ExportNamedDeclaration;
|
|
|
|
if (!isExported) {
|
|
unexportedPropsNodes.set(typeName, node);
|
|
}
|
|
};
|
|
|
|
return {
|
|
TSTypeAliasDeclaration: collectUnexportedProps,
|
|
TSInterfaceDeclaration: collectUnexportedProps,
|
|
|
|
ExportNamedDeclaration: (
|
|
node: TSESTree.ExportNamedDeclaration,
|
|
) => {
|
|
for (const specifier of node.specifiers) {
|
|
if (
|
|
specifier.type ===
|
|
TSESTree.AST_NODE_TYPES.ExportSpecifier &&
|
|
isIdentifier(specifier.local)
|
|
) {
|
|
const name = specifier.local.name;
|
|
|
|
if (name.endsWith('Props')) {
|
|
reExportedNames.add(name);
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
'Program:exit': () => {
|
|
for (const [typeName, node] of unexportedPropsNodes) {
|
|
if (reExportedNames.has(typeName)) {
|
|
continue;
|
|
}
|
|
|
|
context.report({
|
|
node: node.id,
|
|
messageId: 'mustExportProps',
|
|
data: { typeName },
|
|
fix: (fixer) => {
|
|
const sourceCode = context.sourceCode;
|
|
const firstToken = sourceCode.getFirstToken(node);
|
|
const target = firstToken ?? node;
|
|
|
|
if (firstToken?.value === 'export') {
|
|
return null;
|
|
}
|
|
|
|
return fixer.insertTextBefore(target, 'export ');
|
|
},
|
|
});
|
|
}
|
|
},
|
|
};
|
|
},
|
|
});
|