-
Notifications
You must be signed in to change notification settings - Fork 2.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[ESLint plugin] Add a rule to enforce folder architecture on frontend #7481
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
PR Summary
This PR introduces an ESLint plugin to enforce a consistent folder structure in the Twenty frontend project.
- Added
eslint-plugin-project-structure
dependency inpackage.json
- Implemented folder structure rules in new file
packages/twenty-front/project-structure.cjs
- Configured ESLint to use the new folder structure rules in
packages/twenty-front/.eslintrc.cjs
- Set rule to 'warn' mode, allowing for gradual adoption without breaking existing builds
3 file(s) reviewed, no comment(s)
Edit PR Review Bot Settings
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hello @ijreilly 👋 , we were almost finishing this PR but a problem arose, after adding the configurations to the modules
folder, eslint is taking a long time to perform the check, as this involves checking many folders, we are investigating this performance issue. That's why we sent the PR as a draft, to see how it would behave in the CI. But we also have some highlighted points about the structure that would be nice if you could clarify, I marked these points in the comments below.
I will let @charlesBochet answer to you ! |
children: [ | ||
{ | ||
name: 'twenty-front', | ||
children: [ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i'm not sure we should talk about the files out of the src/modules folder for now, is there a way not taking them into account at all?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@gitstart-twenty Thanks for the PR, let's only focus on the modules pages, make the rule a warning and treat / discuss edge cases in other PRs
@gitstart-twenty I'm going to close this one to keep the history clean, feel free to re-open it whenever you are ready |
@gitstart-twenty is this ready for review? |
bf55d6e
to
790e897
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hello @ijreilly Hello @charlesBochet
since we managed to improve the performance we are adding the entire frontend folder in the configuration, I've left a explanation on the description about it.
I'm sending the comments to discuss again from here
// TODO: edge cases for subfolders of __stories__ that are also stories folders | ||
/* | ||
- /src/pages/settings/developers/__stories__/api-keys | ||
- /src/pages/settings/developers/__stories__/webhooks | ||
- /src/pages/settings/data-model/__stories__/SettingsObjectNewField | ||
|
||
*/ | ||
// should we enforce StrictPascalCase? | ||
{ name: 'kebab-case', ruleId: 'StorybookFolder' }, | ||
{ name: 'StrictPascalCase', ruleId: 'StorybookFolder' }, | ||
{ name: 'perf', ruleId: 'StorybookFolder' }, | ||
{ ruleId: 'StorybookFolder' }, | ||
], | ||
}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
{ | ||
name: { | ||
namePattern: ['StrictPascalCase', 'camelCase', 'kebab-case'], | ||
// TODO: SVG extensions is a edge case for list-view-grip.svg |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
UtilsFolder: { | ||
name: 'utils', | ||
children: [ | ||
// TODO: what is the correct rule for utils? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- we are accepting everything now, but we can define a pattern
{ | ||
name: { | ||
namePattern: ['StrictPascalCase', 'camelCase', 'kebab-case'], | ||
// TODO: test.tsx extensions is a edge case, because the files that end with test.tsx, infact do not use tsx |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
{ | ||
name: { | ||
namePattern: ['StrictPascalCase', 'camelCase', 'kebab-case'], | ||
// TODO: getFiledButtonIcon.tsx, assertWorkflowWithCurrentVersionIsDefined.tsx extensions is a edge case, because the files that end .tsx, infact do not use tsx |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// src/loading/__stories__ | ||
{ | ||
name: '__stories__', | ||
// TODO: should we also include the Loader Suffix here? we have PrefetchLoading.stories in the folder |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
{ ruleId: 'StorybookFolder' }, | ||
// TODO: this is an edge case for ComponentFolderWithStories | ||
// This is the only folder that breaks the recursive rule for page/settings | ||
{ | ||
name: 'data-model', | ||
children: [ | ||
{ ruleId: 'UtilsFolder' }, | ||
{ ruleId: 'TypesFolder' }, | ||
{ ruleId: 'HooksFolder' }, | ||
{ ruleId: 'ConstantsFolder' }, | ||
{ ruleId: 'StorybookFolder' }, | ||
{ | ||
name: 'SettingsObjectNewField', | ||
ruleId: 'ComponentsGroup', | ||
}, | ||
{ ruleId: 'ComponentsGroup' }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
{ ruleId: 'ComponentsGroup' }, | ||
], | ||
}, | ||
// TODO: Edge case because SettingsCRMMigration is a folder and does not pass on StrictPascalCase |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// TODO: is it fine the way we are handling this? | ||
{ | ||
name: { | ||
namePattern: 'kebab-case', | ||
extension: 'd.ts', | ||
}, | ||
}, | ||
{ | ||
name: { | ||
namePattern: 'index', | ||
extension: 'css', | ||
}, | ||
}, | ||
{ | ||
name: { | ||
namePattern: 'index', | ||
extension: 'tsx', | ||
}, | ||
}, | ||
{ | ||
name: { | ||
namePattern: 'App', | ||
extension: 'tsx', | ||
}, | ||
}, | ||
{ | ||
name: { | ||
namePattern: 'SettingsRoutes', | ||
extension: 'tsx', | ||
}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- we could accept any file, but since there are few files, we are listing here
namePattern: [ | ||
'kebab-case', | ||
'StrictPascalCase', | ||
// TODO: Edge case for windows11 folder |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
PR Summary
(updates since last review)
This PR introduces a new ESLint rule to enforce folder architecture on the frontend, along with significant changes to the codebase structure and component implementations.
- Implemented custom ESLint rule in
folder-structure-rule.ts
to validate folder and file names against predefined patterns - Removed several field-specific components (e.g., EmailFieldDisplay, LinkFieldInput) in favor of more generic implementations
- Refactored action menu and context menu functionality throughout the application, replacing context menus with action menus in many components
- Updated object metadata handling, including new types and utilities for composite and non-composite field types
- Improved responsiveness and layout of various components, particularly in settings and record table areas
299 file(s) reviewed, 274 comment(s)
Edit PR Review Bot Settings | Greptile
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@gitstart-twenty I think the overall approach is good but this is way too detailed and likely unmaintainable :(
Can we focus on the modules and pages folder only.
Modules:
Good example: packages/twenty-front/src/modules/object-record/record-table
Pages:
Good example: packages/twenty-front/src/pages/auth
should only have .tsx filed in it and no subfolders (except stories)
Regarding the case, it is not tied to the folder architecture and it is actually way simpler:
.tsx ==> PascalCase
.ts => camelCase
I believe this should be checked by another rule TBH
The suffix thing is interesting to check and is folder related but let's not be overfit there too: checking that stories only contains .stories.tsx makes sense, checking for .svg does not at this point
Thank you for the update @charlesBochet Ignoring the other folders, we have some pending confirmations about the changes we should make in the next PRs:
|
Thanks! Generally as the rule should be a warning it's OK if the current state of modules and pages folder are not perfect yet. Let's not make the rule fit the folders, we will make the folders fit the rule piece by piece We added the Ignoring the other folders, we have some pending confirmations about the changes we should make in the next PRs:
|
ok, thanks for the clarifications @charlesBochet 🤝
you mean that " stories_" should not have sub folders, rigth? |
Yes :) |
@@ -0,0 +1,396 @@ | |||
export const CASES: Record<string, RegExp | undefined> = { | |||
'kebab-case': /^[a-z]+(-[a-z]+)*$/, | |||
StrictCamelCase: /^[a-z]+((\d)|([A-Z0-9][a-z0-9]+))*([A-Z])?$/, |
Check failure
Code scanning / CodeQL
Inefficient regular expression High
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix AI 2 months ago
To fix the problem, we need to remove the ambiguity in the regular expression that causes exponential backtracking. The best way to do this is to refactor the regular expression to avoid nested quantifiers and ensure that each part of the pattern can only match in one way.
For the regular expression ^[a-z]+((\d)|([A-Z0-9][a-z0-9]+))*([A-Z])?$
, the problematic part is ((\d)|([A-Z0-9][a-z0-9]+))*
. We can refactor this to avoid ambiguity by ensuring that each part of the pattern is distinct and does not overlap in what it can match.
-
Copy modified lines R3-R4
@@ -2,4 +2,4 @@ | ||
'kebab-case': /^[a-z]+(-[a-z]+)*$/, | ||
StrictCamelCase: /^[a-z]+((\d)|([A-Z0-9][a-z0-9]+))*([A-Z])?$/, | ||
StrictPascalCase: /^[A-Z](([a-z0-9]+[A-Z]?)*)$/, | ||
StrictCamelCase: /^[a-z]+(\d|[A-Z0-9][a-z0-9]*)*([A-Z])?$/, | ||
StrictPascalCase: /^[A-Z]([a-z0-9]*[A-Z]?)*$/, | ||
}; |
@@ -0,0 +1,396 @@ | |||
export const CASES: Record<string, RegExp | undefined> = { | |||
'kebab-case': /^[a-z]+(-[a-z]+)*$/, | |||
StrictCamelCase: /^[a-z]+((\d)|([A-Z0-9][a-z0-9]+))*([A-Z])?$/, |
Check failure
Code scanning / CodeQL
Inefficient regular expression High
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix AI 2 months ago
To fix the problem, we need to modify the regular expression to remove the ambiguity that causes exponential backtracking. Specifically, we can change the sub-expression [a-z0-9]+
to a more precise pattern that avoids ambiguity. One way to achieve this is by using a non-capturing group with a more specific pattern.
- General Fix: Modify the regular expression to eliminate ambiguous patterns that can cause backtracking.
- Detailed Fix: Change the regular expression on line 3 to use a non-capturing group and a more specific pattern to avoid ambiguity.
- Specific Changes: Update the regular expression in the
CASES
object on line 3. - Requirements: No additional methods, imports, or definitions are needed.
-
Copy modified line R3
@@ -2,3 +2,3 @@ | ||
'kebab-case': /^[a-z]+(-[a-z]+)*$/, | ||
StrictCamelCase: /^[a-z]+((\d)|([A-Z0-9][a-z0-9]+))*([A-Z])?$/, | ||
StrictCamelCase: /^[a-z]+((\d)|([A-Z0-9](?:[a-z0-9]+)))*([A-Z])?$/, | ||
StrictPascalCase: /^[A-Z](([a-z0-9]+[A-Z]?)*)$/, |
export const CASES: Record<string, RegExp | undefined> = { | ||
'kebab-case': /^[a-z]+(-[a-z]+)*$/, | ||
StrictCamelCase: /^[a-z]+((\d)|([A-Z0-9][a-z0-9]+))*([A-Z])?$/, | ||
StrictPascalCase: /^[A-Z](([a-z0-9]+[A-Z]?)*)$/, |
Check failure
Code scanning / CodeQL
Inefficient regular expression High
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix AI 2 months ago
To fix the problem, we need to modify the regular expression to remove the ambiguity that causes exponential backtracking. Specifically, we can change the nested quantifiers to ensure that the pattern is more deterministic. One way to achieve this is by using a non-capturing group and making the inner repetition non-greedy.
- Modify the regular expression on line 4 to avoid nested quantifiers that can cause exponential backtracking.
- Change
[a-z0-9]+
to[a-z0-9]*
and adjust the overall pattern to ensure it still matches the intended strings without causing performance issues.
-
Copy modified line R4
@@ -3,3 +3,3 @@ | ||
StrictCamelCase: /^[a-z]+((\d)|([A-Z0-9][a-z0-9]+))*([A-Z])?$/, | ||
StrictPascalCase: /^[A-Z](([a-z0-9]+[A-Z]?)*)$/, | ||
StrictPascalCase: /^[A-Z](?:[a-z0-9]*[A-Z]?)*$/, | ||
}; |
Hello @charles, we updated the PR. One thing that we did not update for the hooks folder is the we also included better regexes to check the file names with strict Pascal and strict Camel case more precisely (we check the AI suggestion after pushing, they make the validation not strict), along with the check of the patterns you suggested here: .tsx ==> PascalCase this causes a lot of warnings to be shown, we are not sure if you want to keep it or if you want to define some exception in total we have almost 400 warnings |
@gitstart-twenty I have taken a deep look at your PR before merging it and wanted to double check that we could not use I've managed to implement it in a simpler way than your proposal in: #7863 Is there anything I'm missing? |
i'll close this PR, I'm sorry but this does not meet our quality standard |
Hey @charlesBochet |
I think there was an infinite loop in their camelCase regex, that's why i have overriden it (see on main) |
Description
### **How String Validation Works**
The
validateString
function is responsible for validating filenames based on configuration:1. Configuration definition
2. RuleId
3. Children
4. **Prefix, Suffix, and Name Pattern**:
5. **Wildcard Handling**:
6. The
checkFolderStructure
function checks the folder name, and if there is a ruleId calls itself passing the object that represents the rule id, if there is the children prop we split the rules for folders and files and check all of the folders and files against every corresponding rule until one of them passes7. The folder is considered valid if It’s name and all of the sub-files and sub-folders names are valid.
8. We store the validations in an object and then we pass the result to the eslint reporter
---
### **Why
hasRun
Check is Needed**The
hasRun
flag is used to prevent the validation process from running multiple times. Running this rule multiple times, especially on large folder structures, would have a significant performance impact due to repeated validation.```tsx
let hasRun = false;
if (hasRun) return {};
hasRun = true;
```
The
hasRun
check ensures that we avoid redundant validation and maintain efficiency.The library that we were using was running for each eslint event triggered (based on each file), so that’s why the commands were not finishing because the eslint was triggering the check multiple times and we have too many files and folders to check each time.
this causes the error to be computed for the first eslint file that triggers the event even if this file passes in the test, but in the reporter, we are passing the path that is invalid
Fixes: #7329