diff --git a/package.json b/package.json index 8e62be709810..189d4a080134 100644 --- a/package.json +++ b/package.json @@ -296,6 +296,7 @@ "eslint-plugin-jsx-a11y": "^6.8.0", "eslint-plugin-prefer-arrow": "^1.2.3", "eslint-plugin-prettier": "^5.1.2", + "eslint-plugin-project-structure": "^3.0.1", "eslint-plugin-react": "^7.33.2", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.4", diff --git a/packages/twenty-front/.eslintrc.cjs b/packages/twenty-front/.eslintrc.cjs index df4daf7633a4..4d638217b29a 100644 --- a/packages/twenty-front/.eslintrc.cjs +++ b/packages/twenty-front/.eslintrc.cjs @@ -1,3 +1,4 @@ +const folderStructureConfig = require('./project-structure.cjs'); module.exports = { extends: ['../../.eslintrc.cjs', '../../.eslintrc.react.cjs'], ignorePatterns: [ @@ -15,6 +16,10 @@ module.exports = { 'tsup.ui.index.tsx', '__mocks__', ], + plugins: ['project-structure'], + rules: { + 'project-structure/folder-structure': ['warn', folderStructureConfig], + }, overrides: [ { files: ['*.ts', '*.tsx'], diff --git a/packages/twenty-front/project-structure.cjs b/packages/twenty-front/project-structure.cjs new file mode 100644 index 000000000000..32e4d9a5d2fe --- /dev/null +++ b/packages/twenty-front/project-structure.cjs @@ -0,0 +1,472 @@ +/* eslint-disable project-structure/folder-structure */ +// @ts-check + +const { default: src } = require('afterframe'); +const { createFolderStructure } = require('eslint-plugin-project-structure'); + +module.exports = createFolderStructure({ + longPathsInfo: { + root: './packages/twenty-front', + mode: 'warn', + }, + structure: [ + // Allow any files in the root of your project, like package.json, eslint.config.mjs, etc. + // You can add rules for them separately. + // You can also add exceptions like this: "(?!folderStructure)*". + { name: '*' }, + { + name: 'packages', + children: [ + { + name: 'twenty-front', + children: [ + { + name: 'src', + children: [ + // src/__stories__/App.stories.tsx + { + name: '__stories__', + children: [{ name: 'App.stories.tsx' }], + }, + // src/config/index.ts + { + name: 'config', + children: [{ name: 'index.ts' }], + }, + // src/effect-components + { + name: 'effect-components', + children: [{ name: '{StrictPascalCase}Effect.tsx' }], + }, + // src/generated/graphql.tsx + { + name: 'generated', + children: [{ name: 'graphql.tsx' }], + }, + // src/generated-metadata + { + name: 'generated-metadata', + children: [ + { name: 'gql.ts' }, + { name: 'graphql.ts' }, + { name: 'index.ts' }, + ], + }, + // src/hooks + { + name: 'hooks', + ruleId: 'HooksFolder', + }, + // src/loading/ + { + name: 'loading', + children: [ + // src/loading/components + { + name: 'components', + children: [ + { name: '{StrictPascalCase}Loader.tsx' }, + { + name: '__stories__', + // TODO: should we also include the Loader Suffix here? we have PrefetchLoading.stories in the folder + children: [ + { name: '{StrictPascalCase}.stories.tsx' }, + ], + }, + ], + }, + // src/loading/__stories__ + { + name: 'hooks', + children: [{ name: 'use{StrictPascalCase}.ts' }], + }, + ], + }, + // src/modules + { + name: 'modules', + children: [ + { + name: '{kebab-case}', + ruleId: 'ModulesFolder', + }, + ], + }, + // src/pages + { + name: 'pages', + children: [ + { + name: '{kebab-case}', + ruleId: 'ComponentFolderWithStories', + }, + { + name: 'settings', + children: [ + { + name: '{kebab-case}', + ruleId: 'ComponentFolderWithStories', + }, + { 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', + children: [], + ruleId: 'ComponentsGroup', + }, + { ruleId: 'ComponentsGroup' }, + ], + }, + // TODO: Edge case because SettingsCRMMigration is a folder and does not pass on StrictPascalCase + // This folder should be able to be passed on ComponentFolderWithStories rule + { + name: 'crm-migration', + children: [{ name: 'SettingsCRMMigration.tsx' }], + }, + { ruleId: 'ComponentsGroup' }, + ], + }, + ], + }, + // src/testing + { + name: 'testing', + children: [ + { + name: 'constants', + ruleId: 'ConstantsFolder', + }, + { + name: 'decorators', + children: [ + { name: '{StrictPascalCase}Decorator.tsx' }, + { name: '{camelCase}Decorator.tsx' }, + ], + }, + { + name: 'hooks', + ruleId: 'HooksFolder', + }, + { + name: 'jest', + children: [ + { name: '{PascalCase}.tsx' }, + { name: '{camelCase}.tsx' }, + ], + }, + { + name: 'mock-data', + children: [ + { + name: 'generated', + children: [{ name: 'mock-{kebab-case}.ts' }], + }, + { name: '{kebab-case}.ts' }, + { name: '{camelCase}.ts' }, + ], + }, + { + name: 'profiling', + children: [ + { + name: 'components', + children: [{ name: '{StrictPascalCase}.tsx' }], + }, + { ruleId: 'StatesFolder' }, + { name: 'constants', ruleId: 'ConstantsFolder' }, + { name: 'types', ruleId: 'TypesFolder' }, + { name: 'utils', ruleId: 'UtilsFolder' }, + ], + }, + { + name: '{StrictPascalCase}.tsx', + }, + { + name: '{camelCase}.ts', + }, + ], + }, + // src/types + { + ruleId: 'TypesFolder', + }, + // src/utils + { ruleId: 'UtilsFolder' }, + // TODO: is it fine the way we are handling this? + { name: '{kebab-case}.d.ts' }, + { name: 'index.css' }, + { name: 'index.tsx' }, + { name: 'App.tsx' }, + { name: 'SettingsRoutes.tsx' }, + ], + }, + { + name: '__mocks__', + children: [{ name: 'hex-rgb.js' }, { name: '{camelCase}.js' }], + }, + { + name: '.storybook', + children: [{ name: '{kebab-case}.(ts|html|tsx|js)' }], + }, + { + name: 'public', + children: [ + { + name: '{kebab-case|PascalCase}', + ruleId: 'ImagesFolder', + }, + { name: 'env-config.js' }, + { name: 'manifest.json' }, + { name: 'mockServiceWorker.js' }, + ], + }, + { + name: 'script', + children: [{ name: '{kebab-case}.sh' }], + }, + { name: '*' }, + ], + }, + ], + }, + ], + rules: { + StorybookFolder: { + name: '__stories__', + children: [ + { name: '{StrictPascalCase}', ruleId: 'StorybookFolder' }, + { name: '{StrictPascalCase}.tsx' }, + { name: '{StrictPascalCase}.stories.tsx' }, + { name: '{StrictPascalCase}.perf.stories.tsx' }, + { name: '{camelCase}.ts' }, + { name: '{PascalCase}.tsx' }, + + // TODO: this is a edge case for + /* + - /src/pages/settings/developers/__stories__/api-keys + - /src/pages/settings/developers/__stories__/webhooks + */ + // should we enforce StrictPascalCase? + { name: '{kebab-case}', ruleId: 'StorybookFolder' }, + ], + }, + ComponentsGroup: { name: '{StrictPascalCase}.tsx' }, + ComponentFolderWithStories: { + children: [ + { ruleId: 'StorybookFolder' }, + { name: '{StrictPascalCase}.tsx' }, + { name: '{camelCase}.ts' }, + { name: '{kebab-case}.ts' }, + { name: '{kebab-case}', ruleId: 'ComponentFolderWithStories' }, + ], + }, + UtilsFolder: { + name: 'utils', + children: [ + // TODO: what is the correct rule for utils? + { + name: '__tests__', + children: [ + { name: '{kebab-case}(.utils)?.test.ts' }, + { name: '{PascalCase}(.utils)?.test.ts' }, + { name: '{camelCase}(.utils)?.test.ts' }, + ], + }, + { + name: '__test__', + children: [ + { name: '{kebab-case}(.utils)?.test.ts' }, + { name: '{PascalCase}(.utils)?.test.ts' }, + { name: '{camelCase}(.utils)?.test.ts' }, + ], + }, + { name: '{kebab-case}(.utils)?.ts' }, + { name: '{kebab-case}(.util)?.ts' }, + { name: '{PascalCase}(.utils)?.ts' }, + { name: '{camelCase}(.utils)?.ts' }, + { name: '{camelCase}.spec.ts' }, + { name: '{camelCase}.(ts|tsx)' }, + { name: '{kebab-case}', ruleId: 'UtilsFolder' }, + ], + }, + TypesFolder: { + name: 'types', + children: [ + { name: '{StrictPascalCase}.(ts|tsx)' }, + { name: '{camelCase}.(ts|tsx)' }, + { name: '{camelCase}.interface.ts' }, + { name: '{kebab-case}.(ts|tsx)' }, + { name: '{kebab-case}', ruleId: 'TypesFolder' }, + ], + }, + HooksFolder: { + name: 'hooks', + children: [ + { name: 'use{StrictPascalCase}.(ts|tsx)' }, + { name: 'use{PascalCase}.(ts|tsx)' }, + + { + name: '__tests__', + children: [ + { name: 'use{StrictPascalCase}.test.(ts|tsx)' }, + { name: 'use{PascalCase}.test.(ts|tsx)' }, + ], + }, + { + name: '__test__', + children: [{ name: 'use{StrictPascalCase}.test.(ts|tsx)' }], + }, + { + ruleId: 'MocksFolder', + }, + { + name: '{kebab-case}', + children: [{ name: '{camelCase}.(ts|tsx)' }], + }, + ], + }, + ConstantsFolder: { + name: 'constants', + children: [{ name: '{StrictPascalCase}.ts' }], + }, + ImagesFolder: { + name: '{kebab-case|PascalCase|snake_case}.(png|svg)', + }, + ServicesFolder: { + name: 'services', + children: [ + { + name: '__tests__', + children: [ + { name: '{camelCase}.factory.test.ts' }, + { name: '{PascalCase}.test.ts' }, + ], + }, + { name: '{camelCase}.factory.ts' }, + { name: '{StrictPascalCase}.ts' }, + ], + }, + StatesFolder: { + name: 'states', + children: [ + { name: '{camelCase}.ts' }, + { + name: '{kebab-case}', + children: [{ name: '{camelCase}.ts' }, { name: '{PascalCase}.ts' }], + }, + ], + }, + assetsFolder: { + name: 'assets', + children: [{ name: '{kebab-case|PascalCase|snake_case}.(png|svg)' }], + }, + ScopeFolder: { + name: 'scopes', + children: [ + { name: '{kebab-case}', children: [{ name: '{PascalCase}.(ts|tsx)' }] }, + { name: '{PascalCase}.(ts|tsx)' }, + ], + }, + MocksFolder: { + name: '__mocks__', + children: [ + { name: '{camelCase}.(ts|tsx)' }, + { name: '{PascalCase}.(ts|tsx)' }, + ], + }, + ThemesFolder: { + name: 'theme', + children: [{ name: '{PascalCase}.(ts|tsx)' }], + }, + TestFolder: { + name: 'tests', + children: [ + { + name: '{camelCase}.(ts|tsx)', + }, + ], + }, + TestsFolder: { + name: '__tests__', + children: [ + { name: '{kebab-case}(.utils)?.test.ts' }, + { name: '{PascalCase}(.utils)?.test.ts' }, + { name: '{camelCase}(.utils)?.test.ts' }, + ], + }, + EnumsFolder: { + name: 'enums', + children: [{ name: '{StrictPascalCase}.enum.ts' }], + }, + ModulesFolder: { + name: 'modules', + children: [ + { ruleId: 'ComponentFolderWithStories' }, + { ruleId: 'HooksFolder' }, + { ruleId: 'ConstantsFolder' }, + { ruleId: 'TypesFolder' }, + { ruleId: 'UtilsFolder' }, + { ruleId: 'StatesFolder' }, + { ruleId: 'assetsFolder' }, + { ruleId: 'ScopeFolder' }, + { ruleId: 'ServicesFolder' }, + { ruleId: 'MocksFolder' }, + { ruleId: 'ThemesFolder' }, + { ruleId: 'TestsFolder' }, + { ruleId: 'TestFolder' }, + { ruleId: 'StorybookFolder' }, + { ruleId: 'EnumsFolder' }, + { name: 'context', children: [{ name: 'StrictPascalCase' }] }, + { + name: 'graphql', + children: [ + { + name: '{kebab-case}', + children: [ + { name: '{camelCase}.ts' }, + { name: '{StrictPascalCase}.ts' }, + { + name: '{kebab-case}', + children: [ + { name: '{camelCase}.ts' }, + { name: '{StrictPascalCase}.ts' }, + ], + }, + ], + }, + { name: '{camelCase}.ts' }, + { ruleId: 'TypesFolder' }, + { ruleId: 'UtilsFolder' }, + ], + }, + { + name: 'queries', + children: [ + { name: '{camelCase}.ts' }, + { + name: '{kebab-case}', + children: [{ name: '{camelCase}.ts' }], + }, + { ruleId: 'TestsFolder' }, + ], + }, + { name: '{StrictPascalCase}.(ts|tsx)' }, + { name: '{PascalCase}.(ts|tsx)' }, + { name: '{camelCase}.(ts|tsx)' }, + { + name: '{kebab-case}', + ruleId: 'ModulesFolder', + }, + ], + }, + }, +}); diff --git a/yarn.lock b/yarn.lock index 94a11044ccf1..f13548b25db0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -17060,6 +17060,16 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/scope-manager@npm:8.8.0": + version: 8.8.0 + resolution: "@typescript-eslint/scope-manager@npm:8.8.0" + dependencies: + "@typescript-eslint/types": "npm:8.8.0" + "@typescript-eslint/visitor-keys": "npm:8.8.0" + checksum: 10c0/29ddf589ff0e465dbbf3eb87b79a29face4ec5a6cb617bbaafbac6ae8340d376b5b405bca762ee1c7a40cbdf7912a32734f9119f6864df048c7a0b2de21bdd3d + languageName: node + linkType: hard + "@typescript-eslint/type-utils@npm:6.21.0": version: 6.21.0 resolution: "@typescript-eslint/type-utils@npm:6.21.0" @@ -17115,6 +17125,13 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/types@npm:8.8.0": + version: 8.8.0 + resolution: "@typescript-eslint/types@npm:8.8.0" + checksum: 10c0/cd168fafcaf77641b023c4405ea3a8c30fbad1737abb5aec9fce67fe2ae20224b624b5a2e3e84900ba81dc7dd33343add3653763703a225326cc81356b182d09 + languageName: node + linkType: hard + "@typescript-eslint/typescript-estree@npm:5.62.0": version: 5.62.0 resolution: "@typescript-eslint/typescript-estree@npm:5.62.0" @@ -17171,6 +17188,25 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/typescript-estree@npm:8.8.0": + version: 8.8.0 + resolution: "@typescript-eslint/typescript-estree@npm:8.8.0" + dependencies: + "@typescript-eslint/types": "npm:8.8.0" + "@typescript-eslint/visitor-keys": "npm:8.8.0" + debug: "npm:^4.3.4" + fast-glob: "npm:^3.3.2" + is-glob: "npm:^4.0.3" + minimatch: "npm:^9.0.4" + semver: "npm:^7.6.0" + ts-api-utils: "npm:^1.3.0" + peerDependenciesMeta: + typescript: + optional: true + checksum: 10c0/9b9e849f6b2d4e250840ef8e05f55a97d6598adaf48c1e6df83084b94c30feca6a3e7916ee1c235178188d0db6364a877cbf8fe218c36d5f8d5acb50767f3273 + languageName: node + linkType: hard + "@typescript-eslint/utils@npm:5.62.0, @typescript-eslint/utils@npm:^5.45.0": version: 5.62.0 resolution: "@typescript-eslint/utils@npm:5.62.0" @@ -17220,6 +17256,20 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/utils@npm:^8.7.0": + version: 8.8.0 + resolution: "@typescript-eslint/utils@npm:8.8.0" + dependencies: + "@eslint-community/eslint-utils": "npm:^4.4.0" + "@typescript-eslint/scope-manager": "npm:8.8.0" + "@typescript-eslint/types": "npm:8.8.0" + "@typescript-eslint/typescript-estree": "npm:8.8.0" + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + checksum: 10c0/fcf2dfd4a2d9491aa096a29c2c1fdd891ca3c13933d20cfea44e51b3d10a397e7ed9a9cd71ac9a29e8c4706264ae00c25a29394e2a6bda3291be298062901f2c + languageName: node + linkType: hard + "@typescript-eslint/visitor-keys@npm:5.62.0": version: 5.62.0 resolution: "@typescript-eslint/visitor-keys@npm:5.62.0" @@ -17250,6 +17300,16 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/visitor-keys@npm:8.8.0": + version: 8.8.0 + resolution: "@typescript-eslint/visitor-keys@npm:8.8.0" + dependencies: + "@typescript-eslint/types": "npm:8.8.0" + eslint-visitor-keys: "npm:^3.4.3" + checksum: 10c0/580ce74c9b09b9e6a6f3f0ac2d2f0c6a6b983a78ce3b2544822ee08107c57142858d674897f61ff32a9a5e8fca00c916545c159bb75d134f4380884642542d38 + languageName: node + linkType: hard + "@ungap/structured-clone@npm:^1.2.0": version: 1.2.0 resolution: "@ungap/structured-clone@npm:1.2.0" @@ -22240,6 +22300,19 @@ __metadata: languageName: node linkType: hard +"comment-json@npm:^4.2.5": + version: 4.2.5 + resolution: "comment-json@npm:4.2.5" + dependencies: + array-timsort: "npm:^1.0.3" + core-util-is: "npm:^1.0.3" + esprima: "npm:^4.0.1" + has-own-prop: "npm:^2.0.0" + repeat-string: "npm:^1.6.1" + checksum: 10c0/e22f13f18fcc484ac33c8bc02a3d69c3f9467ae5063fdfb3df7735f83a8d9a2cab6a32b7d4a0c53123413a9577de8e17c8cc88369c433326799558febb34ef9c + languageName: node + linkType: hard + "common-ancestor-path@npm:^1.0.1": version: 1.0.1 resolution: "common-ancestor-path@npm:1.0.1" @@ -25488,6 +25561,19 @@ __metadata: languageName: node linkType: hard +"eslint-plugin-project-structure@npm:^3.0.1": + version: 3.0.1 + resolution: "eslint-plugin-project-structure@npm:3.0.1" + dependencies: + "@typescript-eslint/utils": "npm:^8.7.0" + comment-json: "npm:^4.2.5" + js-yaml: "npm:^4.1.0" + jsonschema: "npm:^1.4.1" + micromatch: "npm:^4.0.8" + checksum: 10c0/5672389f95e0fef2cfee3980610c1cf6763cf8e283ac4972ae6426c0e00dbf85aa5a2dcc84b77bb550b0c18e7c30e313536feb502690620222baff6c8ca1af28 + languageName: node + linkType: hard + "eslint-plugin-react-hooks@npm:^4.5.0 || 5.0.0-canary-7118f5dd7-20230705": version: 5.0.0-canary-7118f5dd7-20230705 resolution: "eslint-plugin-react-hooks@npm:5.0.0-canary-7118f5dd7-20230705" @@ -31920,6 +32006,13 @@ __metadata: languageName: node linkType: hard +"jsonschema@npm:^1.4.1": + version: 1.4.1 + resolution: "jsonschema@npm:1.4.1" + checksum: 10c0/c3422d3fc7d33ff7234a806ffa909bb6fb5d1cd664bea229c64a1785dc04cbccd5fc76cf547c6ab6dd7881dbcaf3540a6a9f925a5956c61a9cd3e23a3c1796ef + languageName: node + linkType: hard + "jsonwebtoken@npm:9.0.2, jsonwebtoken@npm:^9.0.0": version: 9.0.2 resolution: "jsonwebtoken@npm:9.0.2" @@ -34499,6 +34592,16 @@ __metadata: languageName: node linkType: hard +"micromatch@npm:^4.0.8": + version: 4.0.8 + resolution: "micromatch@npm:4.0.8" + dependencies: + braces: "npm:^3.0.3" + picomatch: "npm:^2.3.1" + checksum: 10c0/166fa6eb926b9553f32ef81f5f531d27b4ce7da60e5baf8c021d043b27a388fb95e46a8038d5045877881e673f8134122b59624d5cecbd16eb50a42e7a6b5ca8 + languageName: node + linkType: hard + "microseconds@npm:0.2.0": version: 0.2.0 resolution: "microseconds@npm:0.2.0" @@ -43994,6 +44097,7 @@ __metadata: eslint-plugin-jsx-a11y: "npm:^6.8.0" eslint-plugin-prefer-arrow: "npm:^1.2.3" eslint-plugin-prettier: "npm:^5.1.2" + eslint-plugin-project-structure: "npm:^3.0.1" eslint-plugin-react: "npm:^7.33.2" eslint-plugin-react-hooks: "npm:^4.6.0" eslint-plugin-react-refresh: "npm:^0.4.4"